מחרוזות ב-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 הוספות, גרסת ה-+= מעתיקה בסך הכול כמעט 2 מיליארד תווים. 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() מאפסת את האורך ושומרת את החוצץ שהוקצה, כך שאפשר להשתמש שוב באותו בונה לאורך האיטרציות של לולאה חיצונית.
האינדקסר מהיר עבור הוספות אחרונות, אבל יכול להיות איטי בגישה אקראית לבונה גדול מאוד, כי התוכן נשמר בחתיכות מקושרות. אם צריך לקרוא תווים, קראו ל-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 תוך כדי תנועה במקום לבנות את כל התוכן בזיכרון. ראו files.
שאלות נפוצות
מה זה 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 בטוח לשימוש בכמה תהליכונים (thread-safe)?
לא. קריאה ל-Append על אותו StringBuilder מכמה תהליכונים בו זמנית עלולה להשחית את התוכן שלו או לזרוק חריגה. תנו לכל תהליכון בונה משלו ואחדו את התוצאות, או הגנו על הבונה המשותף עם lock.
מה AppendLine מוסיפה ב-C#?
את הטקסט ואחריו Environment.NewLine, שהוא \r\n ב-Windows ו-\n ב-Linux וב-macOS. אם הפלט חייב להשתמש בסוף שורה מסוים, כמו \n לפורמט קובץ או \r\n לכותרת HTTP, הוסיפו אותו במפורש עם Append("\n").