مقدمة: لماذا دمج السلاسل النصية ليس مجرد "جمع"
\n\nتخيل أنك تكتب خدمة ويب بلغة C# تستقبل قائمة من العلامات (tags) من المستخدم وتحتاج إلى إرسالها إلى قاعدة البيانات كسلسلة نصية واحدة مفصولة بفواصل. أو ربما تقوم بإنشاء تقرير CSV للتصدير. أو تسجل مجموعة من المعرفات. موقف مألوف، أليس كذلك؟ سؤال "إنشاء قائمة مفصولة بفواصل من IList
قد يبدو الأمر بسيطًا: خذ قائمة السلاسل، مرر عليها بحلقة، وأضف الفواصل. لكن في الواقع، تخفي هذه المهمة عدة مطبات: من فواصل زائدة في النهاية إلى انهيار كارثي في الأداء مع البيانات الكبيرة. في هذا المقال، لن نعرض الحلول الجاهزة فحسب، بل سننظر داخل كل منها. ستتعلم كيف تعمل string.Join وStringBuilder وحتى LINQ Aggregate، وستتمكن من اختيار الأداة المناسبة لمهمتك.
هيا بنا!
\n\nالمشكلة: طلب بسيط، تعقيدات غير واضحة
\n\nلنفترض أن لدينا مجموعة من السلاسل النصية:
\n\nIEnumerable<string> fruits = new List<string> { "Apple", "Banana", "Cherry", "Date" };\n\nنريد الحصول على: "Apple,Banana,Cherry,Date". أول ما يتبادر إلى ذهن المبتدئ هو كتابة حلقة:
string result = "";\nbool first = true;\nforeach (var fruit in fruits)\n{\n if (!first)\n result += ",";\n result += fruit;\n first = false;\n}\n\nهذا الكود يعمل، لكنه يفعل ذلك بطريقة سيئة. لماذا؟ لأن السلاسل النصية في C# غير قابلة للتغيير. كل عملية += تنشئ سلسلة جديدة في الذاكرة، وتنسخ محتويات السابقة إليها. مع 4 عناصر، هذا غير ملحوظ، لكن مع 10,000 سلسلة، ستحصل على تعقيد تربيعي O(n²) واستهلاك هائل للذاكرة. داخليًا، هذا يعني آلاف التخصيصات وعمليات جمع القمامة.
من الواضح أننا بحاجة إلى نهج أكثر ذكاءً. وهنا تظهر الشخصيات الرئيسية على المسرح.
\n\nالطريقة 1: string.Join - ملك الأداء
\n\nالطريقة الأصح والأكثر كفاءة هي استخدام الأسلوب الثابت string.Join:
string csv = string.Join(",", fruits);\nConsole.WriteLine(csv); // Apple,Banana,Cherry,Date\n\nسطر واحد من الكود وينتهي الأمر. دعنا ننظر داخل string.Join (في .NET Core / .NET 5+).
ماذا يحدث داخليًا؟
\n- \n
- حساب الطول: يقوم الأسلوب أولاً بالمرور على المجموعة بأكملها (إذا كانت
IEnumerable<T>) لمعرفة العدد الإجمالي للأحرف. إذا كانت المجموعة مصفوفة أوIList، فإنه يحصل على الطول فورًا عبرCount. \n - تخصيص المخزن المؤقت: يتم تخصيص مصفوفة أحرف كبيرة واحدة (
char[]) بالحجم المطلوب: مجموع أطوال جميع السلاسل + (عدد السلاسل - 1) * طول الفاصل. \n - النسخ: يتم نسخ جميع السلاسل والفواصل إلى هذا المخزن المؤقت دون أي تخصيص إضافي. \n
- إنشاء السلسلة: من المخزن المؤقت، يتم إنشاء سلسلة نصية نهائية واحدة. \n
النتيجة: تخصيص ذاكرة واحد (للسلسلة النهائية) + تخصيص المخزن المؤقت المؤقت. لا توجد سلاسل وسيطة. التعقيد هو O(n) تمامًا.
\n\nبالنسبة لـ IList<string>، يعمل الأسلوب بشكل أسرع لأنه يمكنه الحصول فورًا على Count وطول كل عنصر عبر المفهرس، دون استدعاء GetEnumerator.
الطريقة 2: StringBuilder - الصديق القديم
\n\nقبل ظهور string.Join المحسّن (أو إذا كنت عالقًا في إصدار قديم من .NET Framework) كان الحل القياسي هو StringBuilder:
var sb = new StringBuilder();\nbool first = true;\nforeach (var fruit in fruits)\n{\n if (!first)\n sb.Append(',');\n sb