C# StringBuilder // Как соединять, чтобы много и быстро

Довольно часто бывают случаи, когда нужно объединить несколько строк в одну. Если их немного, то это можно сделать с помощью обычного сложения, конкатенации или интерполяции. Но что делать, если строк очень много? Тут обычное сложение уже не подойдет, потому что сложение строк – это неэффективно с точки зрения использования памяти, поскольку каждое сложение создает новую строку в памяти. Вместо этого, в таких случаях лучше использовать класс StringBuilder.

Если хочется узнать в чём отличия сложения, конкатенации и интерполяции строк по времени и использованию памяти, то можете посмотреть в ответах к этому вопросу на StackOverflow. Если вкратце, то почти ни в чём.

Не могу читать, надо сейчас

Хорошо, он работает быстро, потому что это связный список, который не сохраняет промежуточные строки.

Основные методы

  • ToString – собирает и возвращает строку
  • Append – добавляет подстроку
  • Insert – вставляет подстроку, начиная с определенного индекса
  • Remove – удаляет определенное количество символов, начиная с определенного индекса
  • Replace – заменяет все вхождения определенной подстроки на другую подстроку
  • AppendFormat – добавляет подстроку с форматированием

Вообще их гораздо больше, но в основном там перегруженные версии этих же методов, чтобы, например, добавлять int в строку, который конвертируется в string и потом передастся в обычный Append(string? value).

Пример кода

using System.Text; var builder = new StringBuilder("Initial string"); // Initial string builder.Append("!"); // Initial string! builder.Insert(0, "My "); // My Initial string! builder.Remove(3, 8); // My string! builder.Replace("!", ""); // My string builder.AppendFormat(" {0}_{1}", "0", "0"); // My string 0_0 var text = builder.ToString(); // My string 0_0

Устройство

Теперь можно более подробно. StringBuilder поддерживает изменяемый массив, в который можно положить или убрать строку. В какой-то момент массив заполнится, и чтобы продолжить работу нужно как-то увеличить место, куда складывать строки, одним из способов ниже.

Увеличение массива

В предыдущих версиях StringBuilder’а создавался новый массив с удвоенным размером. Данные из старого массива переносились в новый и старый удалялся. Похоже на устройство List’а. Но у этого подхода есть один минус. Массив – это непрерывная область в памяти и если массив будет очень большим, то будет довольно сложно выделить для него достаточно памяти, что может привести к OutOfMemoryException. А в StringBuilder’е мы как раз и работаем с большими строками. Поэтому текущий StringBuilder реализует другой подход.

Связный список

Сейчас StringBuilder ведет себя почти также, но как связный список ¯\(•_•) /¯. Когда происходит заполнение массива, создается новый StringBuilder с новым массивом. Размер этого массива удваивается (размер первого массива у первого StringBuilder’а по умолчанию 16), но так, чтобы не превышать 8000 элементов, чтобы не попасть в кучу больших объектов. После чего новый в новый StringBuilder добавляется указатель на старый и новый StringBuilder возвращается нам.

Дополнительные оптимизации и особенности

Небезопасный код

Кроме того, StringBuilder использует небезопасный код, который перекладывает ответственность за управлением памятью на разработчика.

Итерирование по блокам

Существует компромисс между производительностью и выделением памяти. Поскольку блоки являются связанным списком, где каждый блок указывает на свой предыдущий, прохождение списка вперед неэффективно. Если блоков мало (< 8), то сканирование идет с начала каждый раз. Однако при большем размере выделяется массив для хранения ссылок на все блоки.

Потокобезопасность

StringBuilder не потокобезопасен. Если вы попытаетесь работать с StringBuilder из нескольких потоков одновременно, то это может привести к непредсказуемым результатам, потому что он не гарантирует, что содержимое останется неизменным во время перечисления блоков.

ReadOnlyMemory

StringBuilder возвращает блоки ReadOnlyMemory, которые не гарантируют, что они останутся неизменными, если StringBuilder изменяется. Это означает, что, если вы кэшируете эти блоки для последующего использования и затем StringBuilder изменяется, ваши кэшированные блоки могут стать недействительными. Поэтому не рекомендуется кэшировать блоки ReadOnlyMemory для последующего использования.

Источники

22
1 комментарий

Очень полезно для тех, кто стремится повысить эффективность работы с большим объемом строк

Ответить