النصوص في C# غير قابلة للتغيير: لا تمدّ s += "x" النص s، بل تنشئ نصًا جديدًا يحتوي على المحارف القديمة والجديدة، ويصبح النص القديم نفايات. مرة واحدة لا تعني شيئًا. أما في حلقة تعمل آلاف المرات فكل تكرار ينسخ كل ما بُني حتى الآن، فينمو العمل الكلي مع مربّع الطول.
StringBuilder، في System.Text، مخزن قابل للتغيير. تكتب Append في المساحة الفارغة في النهاية، فيكبر المخزن من حين لآخر، وتصنع ToString() نصًا واحدًا في النهاية.
المخرجات:
Order #1042, total 59.97
Status: shipped
**********
51
لماذا الضمّ في حلقة بطيء
هذا النص نفسه مبنيًا بالطريقتين. تختلف الأزمنة من جهاز لآخر، فالأرقام أدناه مجرد مثال؛ المهم هو الفجوة، وهي تتسع كلما كبرت الحلقة:
مخرجات على سبيل المثال:
string += 482 ms, length 188890
StringBuilder 1 ms, length 188890
مع 20,000 إضافة ينسخ إصدار += قرابة ملياري محرف إجمالًا. أما StringBuilder فينسخ كل محرف نحو مرتين، مرة حين يُضاف ومرة أخرى في ToString()، ولهذا يبقى قرب ميلي ثانية واحدة.
Append وAppendLine وAppendFormat
لـ Append أشكال لكل نوع مدمج، وتعيد المُنشئ نفسه، فيمكن تسلسل الاستدعاءات. تضيف AppendLine سطرًا جديدًا بعد وسيطها. وتأخذ AppendFormat نص تنسيق مركّبًا مثل string.Format:
المخرجات:
RECEIPT
-------
Espresso 2.40
Croissant 3.10
Orange juice 4.50
TOTAL 10.00
إضافة نص مستوفى (sb.Append($"...")) مقبولة وسهلة القراءة. وفي .NET 6 وما بعده يكتب المترجم القطع مباشرة في المُنشئ دون إنشاء النص الوسيط.
تستخدم AppendLine القيمة Environment.NewLine: \n في Linux وmacOS، و\r\n في Windows. حين تتطلب صيغة ملف نهاية سطر معينة فأضفها بنفسك.
Insert وRemove وReplace والمفهرس
بخلاف النص يمكن تعديل StringBuilder في مكانه:
المخرجات:
Hello, World
Hello, Maya
Maya
maya
maya!
[] length 0
ضبط Length على قيمة أصغر يقتطع؛ وهي أرخص طريقة لإزالة فاصل زائد في النهاية. تضبط Clear() الطول على 0 وتحتفظ بالمخزن المحجوز، فيمكن إعادة استخدام مُنشئ واحد عبر تكرارات حلقة خارجية.
المفهرس سريع للإضافات الحديثة لكنه قد يكون بطيئًا للوصول العشوائي في مُنشئ كبير جدًا، لأن المحتوى يُخزَّن في قطع مترابطة. إن احتجت إلى قراءة المحارف فاستدعِ ToString() مرة واحدة وفهرس النص.
مشكلة الفاصل الزائد في النهاية
استخدام تقليدي هو بناء قائمة مفصولة بفواصل. إضافة فاصل بعد كل عنصر تترك فاصلًا زائدًا في النهاية:
المخرجات:
csharp, dotnet, linq
csharp, dotnet, linq
حين تكون العناصر في مجموعة أصلًا تكون string.Join أقصر وبالسرعة نفسها على الأقل. احتفظ بـ StringBuilder للنص الذي يعتمد شكله على المنطق: أقسام شرطية، وحلقات متداخلة، وتنسيق مختلط.
السعة
يبدأ StringBuilder بمساحة لـ 16 محرفًا. حين تنفد يربط كتلة تخزين جديدة بحجم كل ما يحمله أصلًا، حتى 8,000 محرف لكل كتلة، فتتضاعف سعته ما دام صغيرًا ثم تنمو بخطوات من 8,000 محرف. النمو لا ينسخ النص المخزّن أبدًا. إن كنت تعرف تقريبًا طول النتيجة فمرّر السعة إلى المُنشئ ولن يحتاج المُنشئ إلى النمو أبدًا:
المخرجات:
12000
10000
True
True
التحجيم المسبق تحسين لا شرط. النمو يحجز كتلة جديدة لكنه لا ينقل النص الموجود أبدًا، فالافتراضي مناسب ما لم تكن تبني نصوصًا كبيرة كثيرة في مسار ساخن.
متى لا تستخدم StringBuilder
StringBuilder ليس بديلًا أسرع لكل +. يحوّل المترجم أصلًا a + b + c + d في تعبير واحد إلى استدعاء string.Concat واحد، يحسب الطول النهائي وينسخ كل قطعة مرة واحدة، وهو العمل نفسه الذي سيؤديه المُنشئ بشيفرة أكثر. استخدم النصوص العادية حين:
- يكون عدد القطع صغيرًا وثابتًا:
$"{first} {last} ({age})". - تكون القطع في مجموعة وتحتاج إلى فاصل:
string.Join(", ", items). - لا تحتاج القطع إلى فاصل:
string.Concat(parts). - تضمّ بضع مرات خارج أي حلقة.
استخدم StringBuilder حين تضيف داخل حلقة، أو تضيف عددًا من المرات لا تعرفه مسبقًا، أو تعدّل النص (إدراج أو استبدال أو حذف) أثناء بنائه. التحويل ذهابًا وإيابًا (sb.ToString() داخل الحلقة) يضيّع الفائدة؛ استدعها مرة واحدة في النهاية.
التدفقات هي البديل الآخر: لإنتاج ملف كبير اكتب الأسطر إلى StreamWriter أولًا بأول بدل بناء المحتوى كله في الذاكرة. راجع الملفات.
الأسئلة الشائعة
ما هو StringBuilder في C#؟
System.Text.StringBuilder مخزن محارف قابل للتغيير لبناء نص على خطوات كثيرة. تضيف Append إلى نهاية المخزن بدل إنشاء نص جديد في كل مرة، وتنتج ToString() النص النهائي مرة واحدة. وهو الأداة القياسية لبناء النص في حلقة.
متى أستخدم StringBuilder بدل ضمّ النصوص؟
حين تضيف في حلقة أو عددًا غير معروف من المرات، كبناء تقرير أو ملف CSV أو HTML من مجموعة. أما لبضع قطع ثابتة (a + b + c، أو نص مستوفى واحد) فالضمّ العادي بالسرعة نفسها وأسهل قراءة، لأن المترجم يجمعه أصلًا في استدعاء string.Concat واحد.
كيف أحذف آخر محرف من StringBuilder؟
أنقص Length: if (sb.Length > 0) sb.Length--; تحذف المحرف الأخير دون نسخ. وتفعل sb.Remove(sb.Length - 1, 1) الشيء نفسه. هذه الطريقة المعتادة لإزالة فاصلة زائدة في النهاية، مع أن string.Join تتجنّب الفاصل الزائد من الأساس.
هل StringBuilder آمن مع الخيوط؟
لا. استدعاء Append على StringBuilder نفسه من عدة خيوط في الوقت نفسه قد يفسد محتواه أو يرمي استثناءً. أعطِ كل خيط مُنشئ نصوص خاصًا به واجمع النتائج، أو احمِ المشترك بـ lock.
ماذا تضيف AppendLine في C#؟
النص متبوعًا بـ Environment.NewLine، وهو \r\n في Windows و\n في Linux وmacOS. إن كان على المخرجات أن تستخدم نهاية سطر محددة، مثل \n لصيغة ملف أو \r\n لترويسة HTTP، فأضفها صراحة بـ Append("\n") بدلًا من ذلك.