Рубріки: Теория

Класс StringBuilder в C# — примеры и задачи

Денис Бородовский

В этом небольшом руководстве для всех освоивших основы C# и .NET, разберем часто используемый класс StringBuilder — чрезвычайно полезный инструмент для оптимизации операций со строками.

Содержание:
1. Что такое StringBuilder
2. Краткий обзор деталей реализации строк
3. Для чего нужна оптимизация
4. StringBuilder
5. Методы и свойства StringBuilder
6. Объявление и инициализация StringBuilder
7. Свойства класса
8. Методы StringBuilder
Заключение

1. Что такое StringBuilder

StringBuilderэто изменяемый (редактируемый) строковый класс, определенный в пространстве имен System.Text. С его помощью вы можете напрямую изменять содержимое строкового объекта, что обеспечивает более высокую производительность чем традиционный способ класса System.String, где любое редактирование строки не меняет ее, а возвращает новый строковый объект в измененном формате.

Рассмотрим концепцию класса на примере консольного приложения:

var stringA = Console.ReadLine();
var stringB = Console.ReadLine();
stringA = stringA + stringB;

Здесь мы определяем две строки, а затем объединяем их с помощью оператора «+» и присваиваем результат переменной A. Поскольку эти строки являются неизменяемыми, необходимо создать новую строку для хранения этой объединенной информации.

Оператор «+» вызывает статический метод Concat и выделяет для всего этого новую строку. Поэтому переопределяя stringA мы просто обновляем ссылку на которую указывает эта локальная переменная и получаем доступ к только что созданной, новой строке.

2. Краткий обзор деталей реализации строк

Строковый тип размещается в куче. Куча — это хранилище памяти, расположенное в ОЗУ, являющееся своего рода временным складом для переменных. Для хранения символов строки используются переменные типа char. Каждый Char в .NET представляет символ в кодировке UTF-16, являющийся форматом переменной длины.

Но давайте пропустим все эти Unicode-сложности. Здесь для нас важно знать, что для стандартных символов английского алфавита требуется два байта на букву, а для х64 — 8 байт.

Предположим, что пользователь вводит слово «Hello» в качестве первого ввода и «World» в качестве второго. Обе строки потребуют в куче — 32 байта каждая. После конкатенации у нас есть третья строка размером 42 байта.

Обратите внимание, что мы не просто суммируем размер каждого слова, чтобы вычислить величину окончательной объединенной строки. Каждая из исходных строк имеет свои собственные накладные расходы объекта, но для нас эта точная механика вычислений не имеет большого значения.

3. Для чего нужна оптимизация

Предыдущий код объединяет всего две строки и эффективен настолько, насколько это возможно для данного сценария. Однако если строк гораздо больше, ситуация может поменяться.

Взглянем на следующий кусок кода:

const string testString = "test string";
var output = string.Empty;
var iterations = int.Parse(Console.ReadLine() ?? "0");
for (var i = 0; i < iterations; i++)
{
    output += testString;
}

Вы вряд ли увидите именно такой код в реальном приложении, но в тоже время он не является чем-то необычным. В данном случае программа принимает пользовательский ввод, определяющий, сколько раз надо объединять строку. Сложение начинается с пустой строки, а затем, на каждой итерации, значение переменной testString объединяется с концом предыдущей строки, каждый раз увеличивая выходное значение.

Количество итераций в такой ситуации не ограничено, мы не можем предсказать во время разработки сколько итераций выберет пользователь — две или две тысячи. Чаще всего эта ситуация (потенциально это переполнение стека и последующее аварийное завершение программы) может возникать в различных формах с пользовательским вводом.

Предположим, этих итераций у нас — 100, а для окончательной строки требуется 2222 байта памяти в куче. Поскольку нам нужна эта окончательная строка, это выделение неизбежно и не является проблемой. Однако во время выполнения цикла выделяется 99 других строк, каждая из которых увеличивается в размере по мере того, как testString конкатенируется с концом предыдущей строки. Профилировщик памяти показывает, что для этих временных и ненужных нам (после следующей итерации)  строк выделяется 111 034 байта.

Подумаешь, 111 КБ — в некоторых приложениях вполне себе приемлемая цифра, например, когда этот код запускается один раз только при включении приложения. Однако представьте, что такой код выполняется внутри какого-нибудь метода, действующего приложения ASP.NET Core. Теперь это может оказаться не так безобидно, поскольку каждый HTTP-запрос нашего приложения будет вызывать подобное ненужное выделение памяти.

В масштабе это может легко привести к большему количеству пауз в работе сборщика мусора, а это не есть хорошо. На макроуровне это настоящая уязвимость, которая создает возможность как минимум для нестабильной (или замедленной) работы приложения, как максимум — для довольно болезненной DDoS-атаки.

4. StringBuilder

StringBuilder поможет избежать выделения строк, для этого используя свой собственный внутренний буфер символов, что обеспечит эффективное управление их последовательностями. Он делает модификацию и конкатенацию гораздо более оптимальным образом, позволяя манипулировать символами с меньшим объемом памяти.

Что изменится в нашем примере:

const string testString = "test string";
var iterations = int.Parse(Console.ReadLine() ?? "0");
 
var str = new StringBuilder();
for (var i = 0; i < iterations; i++)
{
    str.Append(testString);
}
var output = str.ToString();

Этот код по-прежнему довольно легко читать и понимать. Это важно, так как некоторые оптимизации могут привести к ухудшению читабельности. Разница здесь в том, что мы добавляем testString вызывая метод Append в StringBuilder.

Такой подход не приводит к выделению новой строки на каждой итерации. Вместо этого внутренний буфер «расширяется» по мере добавления новых символов. И если сильно не вдаваться в подробности, можно отметить, что на этот раз расходы процесса конкатенации составляют всего 4,7 КБ. Помните, что при объединении вручную без использования StringBuilder — было 111 КБ.

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

5. Методы и свойства StringBuilder

StringBuilder предоставляет полезные методы и свойства, помогающие нам легко и эффективно манипулировать содержимым строки.

Наиболее важные перечислены ниже.

Свойства StringBuilder

  • Length (длина) — возвращает количество символов в классе.
  • Capacity (емкость) — извлекает или задает количество символов, которое StringBuilder может хранить в буфере.
  • MaxCapacity (максимальная емкость) — возвращает максимальную емкость.
  • [index] — показатель возвращающий символ в указанной позиции.

Методы StringBuilder

  • Append() — добавляет строку после последнего символа текущей строки.
  • Insert() — вставляет подстроку в заданной позиции в текущую строку.
  • Replace() — заменяет все вхождения заданной подстроки или символа новой подстрокой или символом в текущей строке.
  • Remove() — удаляет указанные символы из текущей строки.
  • Clear() — удаляет все символы из текущей строки.
  • ToString() — преобразует значение объекта в обычную строку.

Напомню, что для работы со StringBuilder необходимо добавить в начало кода пространство имен:

using System.Text

6. Объявление и инициализация StringBuilder

Используем ключевое слово newс конструктором по умолчанию, чтобы инициализировать пустой класс StringBuilder и присвоим его переменной strbuild.

Создадим объект из класса StringBuilder:

StringBuilder strbuild = new StringBuilder () ;

Конструктор StringBuilder дополнительно принимает исходную строку в качестве параметра для создания нового экземпляра класса StringBuilder, например:

string myString = "Нello world!" ;

StringBuilder strbuild = new StringBuilder ( myString ) ;

// или

StringBuilder strbuild = new StringBuilder ( "Нello world! ) ;

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

StringBuilder strbuild = new StringBuilder ( 7 ) ;

Вы можете явно задать исходную строку и емкость объекта StringBuilder, вызвав следующий конструктор.

StringBuilder strbuild = new StringBuilder ( "Hello" , 7 ) ;

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

string finString = strbuild.ToString();

Console.Write(finString);

7. Свойства класса

Длина

Свойство Length получает или задает длину текущего объекта StringBuilder. А еще стоит отметить, что она не влияет на емкость.

StringBuilder strbuild = new StringBuilder ( "Нello world! " ) ;

Console.Write(strbuild.Length); // Выведет 11

Давайте уменьшим длину с 11 до 2.

strbuild.Length = 2;

Если длина меньше общего количества символов StringBuilder, содержимое усекается для корректировки целевой длины.

Console.Write(strbuild.Length); // Теперь текущая длина равна 2

Console.Write(strbuild.ToString()); // выведет he

А теперь, давайте увеличим длину с 11 до 14.

strbuild.Length = 14;

Если целевая длина больше, чем текущее количество символов StringBuilder, пустые символы добавляются до тех пор, пока не будет достигнута нужная величина:

Console.Write(strbuild.Length); // текущая длина 14
Console.Write(strbuild.ToString()); // распечатает Нello world!

Емкость

Данное свойство (Capacity) получает или задает количество символов, которое объект может хранить в памяти. Емкость StringBuilder по умолчанию составляет 16 символов при инициализации (без указания ее значения), например:

StringBuilder strbuild = new StringBuilder();
Console.Write(strbuild.Capacity); // емкость по умолчанию 16
Console.Write(strbuild.MaxCapacity); // максимальная вместимость 2147483647

Если количество символов превышает 16, размер объекта StringBuilder увеличивается для размещения дополнительных символов. При каждом изменении размера емкость автоматически удваивается с 16 до 32, 64, 128 и т. д., например:

StringBuilder strbuild = new StringBuilder();
Console.Write(strbuild.Capacity); // емкость по умолчанию 16
strbuild.Append("Hello, wonderful world”);
Console.Write(strbuild.Capacity); // удвоилась до 32

Если вы устанавливаете емкость во время или после инициализации объекта StringBuilder, а количество символов превышает ее значение, возникает исключение ArgumentOutOfRangException с упоминанием: 'capacity was less than the current size.’

StringBuilder strbuild = new StringBuilder();

strbuild.Append("Hello");

Емкость текущего объекта 16, длина 5 символов. Давайте установим емкость меньше длины.

strbuild.Capacity = 2; // выбрасывает исключение

Вы также можете использовать конструктор, который принимает аргумент int в качестве начальной емкости для создания пустого StringBuilder. По мере добавления символов в объект, емкость автоматически увеличивается, чтобы вместить больше символов.

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

StringBuilder strbuild = new StringBuilder(6);
Console.Write(strbuild.Capacity); // текущая емкость 6
strbuild.Append("Нello world! ");
Console.Write(strbuild.Capacity);// удвоилась до 12

Мы видим, что емкость автоматически удваивается в определенный момент, чтобы вместить больше символов. Это увеличение может отличаться при инициализации в качестве аргумента конструктора.

Рассмотрим следующий пример:

StringBuilder strbuild = new StringBuilder("123456", 5);
Console.Write(strbuild.Length); // 6
Console.Write(strbuild.Capacity);  // емкость равна длине: 6
strbuild.Append("78901234567");
Console.WriteLine(strbuild.Length); // 17
Console.WriteLine(strbuild.Capacity);  // емкость равна длине: 17
Console.WriteLine(strbuild.ToString());

или

StringBuilder strbuild = new StringBuilder(5);
Console.Write(strbuild.Capacity); // начальная емкость: 5
strbuild.Append("123456");
Console.Write(strbuild.Length); // 6
Console.Write(strbuild.Capacity); // вместимость удвоена: 10
Console.Write(strbuild.ToString());

Максимальная емкость

MaxCapacity — это свойство только для чтения, указывающее ограничение по максимальному количеству символов в памяти. По умолчанию MaxCapacity задается — 2147483647.

StringBuilder strbuild = new StringBuilder();
Console.Write(strbuild.MaxCapacity); // Выведет 2147483647

Вы можете явно установить максимальную емкость объекта StringBuilder, вызвав конструктор.

StringBuilder strbuild = new StringBuilder ( 3 , 7 ) ;

Мы установили начальную емкость на 3, а максимальную сделали равной 7, ограничив ее увеличение. Теперь если вдруг понадобиться выделить дополнительную память, StringBuilder вызовет исключение ArgumentOutOfRangeException с сообщением:

'capacity was less than the current size.’

Вот пример этого кода:

strbuild.Append("highload"); 
strbuild.Append("today"); вызываем исключение
Console.Write(strbuild.ToString());

8. Методы StringBuilder

Давайте посмотрим как работают методы рассматриваемого класса:

  • Метод Append() добавляет указанную строку в конец текущего объекта StringBuilder. Вместимость объекта регулируется по мере необходимости. Он также помогает связать вызовы для добавления, чтобы вставить больше строк.
StringBuilder strbuild= new StringBuilder();
strbuild.Append("Hi, Mr");
strbuild.Append("Smith”);
Console.Write(strbuild.ToString());

Вывод:

Hi, Mr Smith
  • Метод Insert() вставляет указанную строку в указанную позицию. При этом существующие символы смещаются, чтобы освободить место для нового текста. Емкость регулируется по мере необходимости.
StringBuilder strbuild = new StringBuilder("Hi, Smith");
strbuild.Insert(4, "Mr ");
Console.Write(strbuild.ToString());

Вывод:

Hi, Mr Smith
  • Метод Replace() ищет указанную строку или символ и заменяет все ее/его вхождения другой строкой/символом.
StringBuilder strbuild = new StringBuilder("Hi, Smith");
strbuild.Replace("Hi", "Hello");
Console.Write(strbuild.ToString());

Вывод:

Hello Smith
  • Метод Remove() удаляет указанный диапазон символов из объекта StringBuilder, принимая два аргумента — индекс, с которого начинается удаление, и количество удаляемых символов. При этом строковое значение текущего объекта укорачивается по длине. Емкость не влияет.
StringBuilder strbuild = new StringBuilder("Hi, Smith");
strbuild.Remove(0, 4);
Console.WriteLine(strbuild.ToString());

Вывод:

Smith
  • Метод Clear() удаляет все символы и устанавливает значение свойства Length равным нулю. Причем установка длины текущего объекта на ноль не уменьшает его внутреннюю емкость.
StringBuilder strbuild = new StringBuilder("Hi, Smith");
Console.Write(strbuild.Length); // длина перед очисткой равна 11
strbuild.Clear();
Console.Write(strbuild.Length); // длина после очистки равна 0

Доступ к отдельному символу

Вы можете выводить символы StringBuilder по отдельности с помощью индекса, целочисленного типа, написанного внутри квадратных скобок [ ]. Индекс-порядковый номер символа, причем счет начинается с нуля.

StringBuilder strbuild = new StringBuilder("Hi, Smith”);
Console.Write(strbuild[6]); // выведет i

Также вы можете использовать такой подход для переопределения отдельных символов, например:

strbuild [ 9 ] = 'e' ;
Console.Write(strbuild[ 9 ]) ; // выведет e
Console.Write(strbuild) ; // выведет Hi, Smite

Если вы попытаетесь получить доступ к символу с несуществующим индексом, возникнет исключение типа:

IndexOutOfRangException с сообщением:Index was outside the bounds of the array‘.

Заключение

Класс StringBuilder рекомендуют использовать в двух случаях:

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

Во всех остальных случаях разработчики рекомендуют использовать класс String.

Как обычно, в конце несколько полезных видеороликов по теме:

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023