Menu

StringBuilder в C#: Append, Insert, Replace и когда его использовать

Почему многократная конкатенация строк в цикле медленная в C# и как это исправляет StringBuilder: Append, AppendLine, AppendFormat, Insert, Remove, Replace, ёмкость и когда лучше обычная конкатенация или string.Join.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Строки в 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() устанавливает длину в 0 и сохраняет выделенный буфер, поэтому один построитель можно переиспользовать в итерациях внешнего цикла.

Индексатор быстр для недавно добавленных данных, но может быть медленным при произвольном доступе к очень большому построителю, потому что содержимое хранится в связанных фрагментах. Если нужно читать символы, вызовите ToString() один раз и индексируйте строку.

Проблема лишнего разделителя

Классическое применение это сборка списка через запятую. Если добавлять разделитель после каждого элемента, в конце остаётся лишний:

Вывод:

csharp, dotnet, linq
csharp, dotnet, linq

Когда элементы уже лежат в коллекции, string.Join короче и как минимум так же быстр. Оставьте StringBuilder для текста, форма которого зависит от логики: условные разделы, вложенные циклы, смешанное форматирование.

Ёмкость

StringBuilder начинает с места на 16 символов. Когда место заканчивается, он присоединяет новый блок памяти размером со всё, что уже хранит, но не больше 8000 символов на блок, поэтому пока он маленький, его ёмкость удваивается, а затем растёт шагами по 8000 символов. При росте уже сохранённый текст никогда не копируется. Если вы примерно знаете длину результата, передайте ёмкость в конструктор, и построителю вообще не придётся расти:

Вывод:

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").

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ