Урок 10.3 — StringBuilder
Эффективная работа со строками: StringBuilder, Append, AppendLine и производительность.
Строки в C# — неизменяемые (immutable). Каждый раз, когда ты изменяешь строку (через +, Replace, Substring и т.д.), создаётся новая строка, а старая остаётся в памяти до сборки мусора. В большинстве случаев это нормально. Но когда ты конкатенируешь тысячи строк в цикле — это превращается в катастрофу. StringBuilder решает эту проблему.
Проблема конкатенации строк в цикле
Давай посмотрим, что происходит, когда ты склеиваешь строки через + в цикле:
// Что здесь плохого?
string result = "";
for (int i = 0; i < 10000; i++)
{
result += i.ToString() + " "; // создаётся 10 000 строк!
}
Console.WriteLine(result);
Каждая итерация этого цикла:
- Создаёт строку
i.ToString() - Создаёт строку
i.ToString() + " " - Создаёт строку
result + (i.ToString() + " ") - Присваивает новую строку в
result
При 10 000 итерациях создаётся ~20 000 строк, которые сразу отправляются в утиль. Это колоссальная нагрузка на память и сборщик мусора.
Давай сравним производительность:
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
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);
Основные методы StringBuilder
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()
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() — гибкое форматирование
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]— доступ по индексу (только чтение/запись)
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 это был бы ад:
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
Задача
Задание: Генератор CSV-отчёта
Напиши программу, которая генерирует CSV-файл с данными о продажах. Требования:
- Создай массив из 10-15 товаров (название, категория, цена, количество проданных)
- Используй StringBuilder для построения CSV
- Первая строка — заголовки: Название, Категория, Цена, Продано
- Добавь строку с итогами: общая выручка = сумма (цена × количество)
- Выведи результат в консоль
Формат CSV (результат):
Название,Категория,Цена,Продано
Ноутбук,Электроника,75000,12
Мышь,Электроника,2500,45
...
Итого,,,1035000
Дополнительно: используй sb.AppendJoin() для строк с разделителем. Выравнивай цены до двух знаков после запятой.
Что важно запомнить
Тест: 10.3: StringBuilder
3 вопроса