يعمل Queue<T> مثل طابور أمام شبّاك: أول عنصر يُضاف هو أول عنصر يُؤخذ (FIFO، الداخل أولًا يخرج أولًا). تضيف في المؤخرة بـ Enqueue وتأخذ من المقدمة بـ Dequeue، كلاهما في زمن ثابت.
Enqueue وDequeue وPeek
المخرجات:
Waiting: 3
Next up: invoice.pdf
Printing invoice.pdf
Next up: photo.png
Printing photo.png
Printing report.docx
Waiting: 0
Peek لتقرير ما تفعله بالعنصر التالي قبل الالتزام به؛ وDequeue تلتزم. حلقة while (queue.Count > 0) التي تسحب حتى يفرغ الطابور هي الطريقة القياسية لمعالجة طابور، وتظل تعمل إن أضاف جسم الحلقة عناصر أخرى، وهو ما لا تسمح به foreach.
استثناء الطابور الفارغ وTryDequeue
ترمي Dequeue وPeek على طابور فارغ InvalidOperationException. لا يوجد سلوك "null عند الفراغ"، حتى للأنواع المرجعية.
المخرجات:
Caught InvalidOperationException
Serving ticket 41
Serving ticket 42
False
تعيد TryDequeue وTryPeek القيمة true وتضبطان متغيّر out حين يوجد عنصر، وتعيدان false حين يكون الطابور فارغًا. وصلتا في .NET Core 2.0؛ وفي شيفرة .NET Framework الأقدم افحص Count > 0 قبل استدعاء Dequeue.
النظر داخل الطابور
يمكن تعداد الطابور دون حذف شيء. ترى foreach وToArray وContains كلها العناصر من المقدمة إلى المؤخرة.
المخرجات:
Ana Ben Chloe
True
Ana is first, 3 in line
3
لا فهرس: لا تُترجم line[1]، ولا يمكنك حذف عنصر من المنتصف. إن احتجت إلى أي منهما فالبيانات ليست طابورًا حقًا؛ استخدم List<T> أو LinkedList<T>. وكما في المجموعات الأخرى، الإضافة أو السحب داخل foreach على الطابور نفسه يرمي InvalidOperationException.
البحث بالعرض أولًا بطابور
ترتيب الطابور هو بالضبط ما يحتاجه البحث بالعرض أولًا (BFS): زر كل ما يبعد خطوة واحدة، ثم كل ما يبعد خطوتين، وهكذا. هنا يجد عدد الوصلات التي تفصل Ana عن كل شخص آخر في شبكة صغيرة:
المخرجات:
Ana: 0 step(s) from Ana
Ben: 1 step(s) from Ana
Chloe: 1 step(s) from Ana
Dev: 2 step(s) from Ana
Eli: 2 step(s) from Ana
Fay: 3 step(s) from Ana
يُضاف كل شخص إلى الطابور مرة واحدة، أول مرة يُبلغ فيها، ولأن الطابور يعيدهم بترتيب العثور عليهم تكون المرة الأولى دائمًا على مسار أقصر. استبدل الطابور بـ Stack فتصبح الحلقة نفسها بحثًا بالعمق أولًا، الذي لا يجد أقصر المسارات بعد ذلك.
والشكل نفسه يعالج المرور على شجرة مستوى بمستوى، والتعبئة بالفيضان على شبكة، وزحف الروابط: ابدأ بعنصر واحد في الطابور، وما دام غير فارغ اسحب واحدًا وأضف جيرانه غير المزورين.
Queue مقابل List لعمل FIFO
يمكنك استخدام List<T> كطابور بـ Add وRemoveAt(0)، لكن RemoveAt(0) تزيح كل عنصر متبقٍّ خانة واحدة للخلف. تفريغ قائمة من 100,000 عنصر بهذه الطريقة يجري نحو خمسة مليارات نقل عنصر؛ بينما يجري Queue<T> مئة ألف خطوة بزمن ثابت. داخليًا الطابور مخزن دائري: يتتبّع فهرسي رأس وذيل في مصفوفة ولا يعيد الحجز إلا حين يمتلئ.
| العملية | Queue<T> | List<T> مستخدمة كطابور |
|---|---|---|
| الإضافة في المؤخرة | Enqueue، O(1) | Add، O(1) |
| الحذف من المقدمة | Dequeue، O(1) | RemoveAt(0)، O(n) |
| النظر إلى المقدمة | Peek | list[0] |
| الوصول بالفهرس | غير متاح | list[i] |
ConcurrentQueue لعدة خيوط
Queue<T> غير آمن مع الخيوط. حين تضيف عدة خيوط عناصر أو تأخذها استخدم ConcurrentQueue<T> من System.Collections.Concurrent. فيه Enqueue وTryDequeue وTryPeek لكن لا Dequeue، لأنه مع وجود خيوط أخرى قد يفشل "افحص Count ثم Dequeue" بين الاستدعاءين.
المخرجات:
4000
4000 processed
مع Queue<T> عادي مكان ConcurrentQueue<T> كان العدد سيخرج خاطئًا أو كان البرنامج سيرمي استثناءً، بحسب التوقيت. ولخيوط المنتج والمستهلك التي يجب أن تنتظر العمل بدل الدوران، تضيف BlockingCollection<T> (التي تغلّف ConcurrentQueue<T> افتراضيًا) أو System.Threading.Channels الحجب والإكمال. راجع lock لحماية مجموعة عادية يدويًا.
PriorityQueue
حين يجب أن تغادر العناصر بحسب الأولوية لا بترتيب الوصول (التذكرة الأكثر إلحاحًا أولًا، أو أقصر مسار حتى الآن في خوارزمية Dijkstra)، توفّر .NET 6 وما بعدها PriorityQueue<TElement, TPriority>. تخرج أدنى قيمة أولوية أولًا:
var triage = new PriorityQueue<string, int>();
triage.Enqueue("sprained ankle", 3);
triage.Enqueue("chest pain", 1);
triage.Enqueue("headache", 5);
while (triage.TryDequeue(out string patient, out int priority))
{
Console.WriteLine($"{priority}: {patient}");
}
// 1: chest pain
// 3: sprained ankle
// 5: headache
العناصر ذات الأولوية المتساوية قد تخرج بأي ترتيب؛ أضف رقمًا تسلسليًا إلى الأولوية إن كان يجب أن يحسم ترتيب الوصول التعادل.
أخطاء شائعة
- السحب دون فحص. الطابور الفارغ يرمي
InvalidOperationException؛ دُر علىCount > 0أو استخدمTryDequeue. - استدعاء
Peekوتوقّع أن يختفي العنصر. وحدهاDequeueتحذف. - الإضافة داخل
foreachعلى الطابور. يرمي استثناءً؛ عالج بحلقةwhileبدلًا من ذلك. - مشاركة
Queue<T>بين الخيوط. استخدمConcurrentQueue<T>أو قفلًا. - استخدام
List.RemoveAt(0)كطابور على بيانات كبيرة. كل استدعاء O(n).
الأسئلة الشائعة
ما هو Queue في C#؟
Queue<T> في System.Collections.Generic مجموعة الداخل أولًا يخرج أولًا (FIFO). تضيف Enqueue عنصرًا في المؤخرة، وتحذف Dequeue العنصر في المقدمة وتعيده، وتعيد Peek عنصر المقدمة دون حذفه. الثلاث تستغرق زمنًا ثابتًا.
ماذا يحدث حين تستدعي Dequeue على طابور فارغ في C#؟
ترمي Dequeue وPeek على طابور فارغ InvalidOperationException. افحص queue.Count > 0 أولًا، أو استخدم TryDequeue(out var item) وTryPeek(out var item)، اللتين تعيدان false بدل رمي استثناء (متاحتان منذ .NET Core 2.0).
ما الفرق بين Peek وDequeue؟
تعيد Peek العنصر في المقدمة وتتركه في الطابور، فيعيد استدعاؤها مرتين العنصر نفسه. أما Dequeue فتعيد عنصر المقدمة وتحذفه، فيعيد الاستدعاء التالي العنصر الذي خلفه.
هل Queue<T> آمن مع الخيوط في C#؟
لا. استدعاء خيطين Enqueue أو Dequeue على Queue<T> نفسه في الوقت نفسه قد يفسده. استخدم ConcurrentQueue<T> من System.Collections.Concurrent، التي يمكن استدعاء Enqueue وTryDequeue فيها بأمان من خيوط كثيرة، أو احمِ كل وصول إلى طابور عادي بـ lock.
لماذا أستخدم Queue بدل List؟
حذف العنصر الأول من List<T> بـ RemoveAt(0) يزيح كل عنصر آخر، فيبطؤ كلما كبرت القائمة. أما Queue<T> فتحذف من المقدمة في زمن ثابت، وواجهتها تعلن القصد: تُعالج العناصر بترتيب وصولها.