$ sudo teach IT
Модуль 10 · Полезные модули

Урок 10.3 — StringBuilder

Эффективная работа со строками: StringBuilder, Append, AppendLine и производительность.

Строки в C# — неизменяемые (immutable). Каждый раз, когда ты изменяешь строку (через +, Replace, Substring и т.д.), создаётся новая строка, а старая остаётся в памяти до сборки мусора. В большинстве случаев это нормально. Но когда ты конкатенируешь тысячи строк в цикле — это превращается в катастрофу. StringBuilder решает эту проблему.

🐌

Проблема конкатенации строк в цикле

Давай посмотрим, что происходит, когда ты склеиваешь строки через + в цикле:

C# · Проблема конкатенации
// Что здесь плохого?
string result = "";
for (int i = 0; i < 10000; i++)
{
    result += i.ToString() + " ";  // создаётся 10 000 строк!
}
Console.WriteLine(result);

Каждая итерация этого цикла:

  1. Создаёт строку i.ToString()
  2. Создаёт строку i.ToString() + " "
  3. Создаёт строку result + (i.ToString() + " ")
  4. Присваивает новую строку в result

При 10 000 итерациях создаётся ~20 000 строк, которые сразу отправляются в утиль. Это колоссальная нагрузка на память и сборщик мусора.

Давай сравним производительность:

C# · Сравнение скорости
using System;
using System.Diagnostics;
using System.Text;

class Program
{
    static void Main()
    {
        int n = 100_000;

        // 1. Конкатенация через +
        Stopwatch sw = Stopwatch.StartNew();
        string result1 = "";
        for (int i = 0; i < n; i++)
            result1 += "x";
        sw.Stop();
        Console.WriteLine($"+ конкатенация: {sw.ElapsedMilliseconds} мс");

        // 2. StringBuilder
        sw.Restart();
        StringBuilder sb = new StringBuilder(n);
        for (int i = 0; i < n; i++)
            sb.Append("x");
        string result2 = sb.ToString();
        sw.Stop();
        Console.WriteLine($"StringBuilder:  {sw.ElapsedMilliseconds} мс");
    }
}

// Примерный вывод:
// + конкатенация: 2314 мс
// StringBuilder:  3 мс

Разница может быть в сотни и тысячи раз. Для 100 000 конкатенаций — 2.3 секунды против 3 миллисекунд.

💡 Почему так? string — immutable (неизменяемый). Каждое «изменение» создаёт новую строку в памяти, копируя старую. Это O(n²) операций копирования. StringBuilder использует mutable-буфер, который растёт по мере необходимости — O(n) операций.

🔧

StringBuilder — изменяемая строка

StringBuilder находится в пространстве имён System.Text. Он представляет изменяемую последовательность символов. Внутри он использует массив char[], который автоматически расширяется при необходимости.

Создание StringBuilder

C# · Создание StringBuilder
using System.Text;

// Пустой StringBuilder (ёмкость по умолчанию — 16 символов)
StringBuilder sb1 = new StringBuilder();

// StringBuilder с начальным текстом
StringBuilder sb2 = new StringBuilder("Привет, мир!");

// StringBuilder с указанием ёмкости (оптимально!)
StringBuilder sb3 = new StringBuilder(1000);
// Если знаешь, что будет ~1000 символов — укажи ёмкость заранее

// StringBuilder из строки с ёмкостью
StringBuilder sb4 = new StringBuilder("Привет", 100);
💡 Если ты знаешь примерный размер конечной строки — сразу укажи ёмкость (capacity). Это избавит от многократного перераспределения внутреннего буфера.
📝

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

Append() и AppendLine()

C# · Append и AppendLine
StringBuilder sb = new StringBuilder();

// Append — добавляет текст в конец
sb.Append("Привет");
sb.Append(", ");
sb.Append("мир");
Console.WriteLine(sb.ToString());  // Привет, мир

// Можно добавлять любые типы — автоматически вызывается ToString()
sb.Append(42);         // int
sb.Append(3.14);       // double
sb.Append(true);       // bool
sb.AppendLine();       // перевод строки
sb.AppendLine("Новая строка");  // текст + перевод строки

// AppendFormat — форматированное добавление
sb.AppendFormat("Сегодня {0:dd.MM.yyyy}", DateTime.Today);

// AppendJoin — добавление с разделителем (C# 5+)
var numbers = new[] { 1, 2, 3, 4, 5 };
sb.AppendJoin(", ", numbers);  // 1, 2, 3, 4, 5

Console.WriteLine(sb.ToString());

Insert(), Remove(), Replace()

C# · Insert, Remove, Replace
StringBuilder sb = new StringBuilder("Привет, мир!");

// Insert — вставляет текст по индексу
sb.Insert(0, "Старт: ");
Console.WriteLine(sb);  // Старт: Привет, мир!

// Remove — удаляет символы (начальная позиция, количество)
sb.Remove(7, 1);  // удаляем пробел после ":"
Console.WriteLine(sb);  // Старт:Привет, мир!

// Replace — заменяет все вхождения подстроки
sb.Replace("мир", "C#");
Console.WriteLine(sb);  // Старт:Привет, C#!

// Replace также работает с отдельными символами
sb.Replace(' ', '_');
Console.WriteLine(sb);  // Старт:Привет,_C#!

// ToString() — получить финальную строку
string result = sb.ToString();
Console.WriteLine(result);

AppendFormat() — гибкое форматирование

C# · AppendFormat
StringBuilder sb = new StringBuilder();

string name = "Иван";
int age = 25;
double salary = 125000.50;

sb.AppendFormat("Имя: {0}, Возраст: {1}", name, age);
sb.AppendLine();
sb.AppendFormat("Зарплата: {2:F2} руб.", name, age, salary);

Console.WriteLine(sb.ToString());
// Имя: Иван, Возраст: 25
// Зарплата: 125000.50 руб.

Методы и свойства

Метод Описание
Append(value) Добавляет текст/число/объект в конец
AppendLine(text) Добавляет текст + перевод строки
AppendFormat(fmt, args) Форматированное добавление (как string.Format)
Insert(index, value) Вставляет текст по указанному индексу
Remove(start, length) Удаляет length символов, начиная с start
Replace(old, new) Заменяет все вхождения old на new
Clear() Очищает содержимое (сбрасывает длину)
ToString() Возвращает финальную строку

Свойства

  • Length — текущая длина содержимого
  • Capacity — текущая ёмкость буфера
  • MaxCapacity — максимальная ёмкость
  • [index] — доступ по индексу (только чтение/запись)
C# · Пример со свойствами
StringBuilder sb = new StringBuilder("Привет", 100);

Console.WriteLine($"Длина: {sb.Length}");      // 6
Console.WriteLine($"Ёмкость: {sb.Capacity}");  // 100

// Доступ по индексу (как у массива)
Console.WriteLine(sb[0]);  // П
sb[0] = 'п';  // изменить символ
Console.WriteLine(sb);   // привет
⚡

Когда использовать StringBuilder vs string

Не нужно использовать StringBuilder везде. Для большинства операций с одной-двумя строками обычные string — отличный выбор. Вот правило:

Правило: используй StringBuilder, когда конкатенируешь строки в цикле с неизвестным или большим (> 10-20) количеством итераций.

✅ Используй string

  • 2-3 строки склеить: $"{a}, {b}, {c}"
  • Одна операция Replace/Substring
  • Интерполяция строк $"..."
  • Парсинг, поиск, проверка (IndexOf, Contains)
  • Передача строки в метод
  • Ключи словаря (Dictionary)

✅ Используй StringBuilder

  • Цикл с конкатенацией (> 10 итераций)
  • Построение большого текста (HTML, CSV, JSON)
  • Много операций Insert/Remove/Replace
  • Генерация отчётов
  • Логирование в буфер
  • Построение SQL-запросов (хотя лучше ORM)

💡 Современный C# компилятор достаточно умён, чтобы оптимизировать конкатенацию до 5-6 строк. Например, a + b + c + d компилируется в один вызов string.Concat(a, b, c, d), а не в цепочку вызовов. Но для циклов — StringBuilder всё равно необходим.

📄

Практический пример — генерация HTML-таблицы

Реальная задача: сгенерировать HTML-таблицу из данных. Без StringBuilder это был бы ад:

C# · Генерация HTML через StringBuilder
using System;
using System.Text;

class Program
{
    static void Main()
    {
        var employees = new[]
        {
            new { Name = "Иван",  Age = 30, Salary = 120000m },
            new { Name = "Анна",  Age = 28, Salary = 95000m  },
            new { Name = "Пётр",  Age = 35, Salary = 150000m },
        };

        StringBuilder html = new StringBuilder(1000);

        html.AppendLine("<table border='1'>");
        html.AppendLine("  <tr><th>Имя</th><th>Возраст</th><th>Зарплата</th></tr>");

        foreach (var emp in employees)
        {
            html.Append("  <tr>");
            html.AppendFormat("<td>{0}</td>", emp.Name);
            html.AppendFormat("<td>{0}</td>", emp.Age);
            html.AppendFormat("<td>{0:N2} руб.</td>", emp.Salary);
            html.AppendLine("</tr>");
        }

        html.AppendLine("</table>");

        Console.WriteLine(html.ToString());
    }
}

// Вывод:
// <table border='1'>
//   <tr><th>Имя</th><th>Возраст</th><th>Зарплата</th></tr>
//   <tr><td>Иван</td><td>30</td><td>120000.00 руб.</td></tr>
//   <tr><td>Анна</td><td>28</td><td>95000.00 руб.</td></tr>
//   <tr><td>Пётр</td><td>35</td><td>150000.00 руб.</td></tr>
// </table>

Обрати внимание на цепочку вызовов: sb.Append(...).AppendFormat(...).AppendLine(...) — StringBuilder возвращает сам себя (fluent interface).

🧪

Мини-тест

Проверь понимание StringBuilder.

Вопрос 1

Почему конкатенация строк через + в цикле работает медленно?

A) + медленный оператор   B) Строки immutable, каждая операция создаёт новую строку   C) Компилятор не оптимизирует +

Вопрос 2

В каком пространстве имён находится StringBuilder?

A) System   B) System.Text   C) System.Collections

Вопрос 3

Какой метод StringBuilder добавляет текст с переводом строки?

A) Append()   B) AppendLine()   C) AppendFormat()

Вопрос 4

Как получить финальную строку из StringBuilder?

A) sb.ToString()   B) (string)sb   C) sb.Result

Вопрос 5

Когда НЕ нужно использовать StringBuilder?

A) Склеивание 2-3 строк через интерполяцию   B) Генерация CSV-файла из 10 000 строк   C) Построение большого HTML

💡 Ответы: 1 — B, 2 — B, 3 — B, 4 — A, 5 — A
🎮

Задача

Задание: Генератор CSV-отчёта

Напиши программу, которая генерирует CSV-файл с данными о продажах. Требования:

  1. Создай массив из 10-15 товаров (название, категория, цена, количество проданных)
  2. Используй StringBuilder для построения CSV
  3. Первая строка — заголовки: Название, Категория, Цена, Продано
  4. Добавь строку с итогами: общая выручка = сумма (цена × количество)
  5. Выведи результат в консоль

Формат CSV (результат):

Название,Категория,Цена,Продано
Ноутбук,Электроника,75000,12
Мышь,Электроника,2500,45
...
Итого,,,1035000

Дополнительно: используй sb.AppendJoin() для строк с разделителем. Выравнивай цены до двух знаков после запятой.

💡 CSV — простой формат, где данные разделены запятыми. В реальных проектах используй библиотеки типа CsvHelper, но для обучения — отличная задача на StringBuilder.
📌

Что важно запомнить

1️⃣
Строки immutable — каждое «изменение» создаёт новую строку. В циклах это O(n²) операций.
2️⃣
StringBuilder — mutable-буфер для эффективной работы со строками. Находится в System.Text.
3️⃣
Основные методы: Append, AppendLine, AppendFormat, Insert, Remove, Replace. Получить результат — ToString().
4️⃣
Ёмкость (Capacity) — задавай заранее, если знаешь примерный размер. Это уменьшает количество перераспределений.
5️⃣
Когда использовать: StringBuilder — для циклов с конкатенацией (>10). Обычные строки — для всего остального.

Тест: 10.3: StringBuilder

3 вопроса

StringBuilder

Premium