Строки: иммутабельность, StringBuilder, интерполяция, сравнение
↑ 1.3 C♯: основы языка · ← Предыдущая · → Следующая
Зачем это на собесе
строки везде: логи, SQL, JSON, отчёты. Вопросы: почему string иммутабельна, когда StringBuilder, что такое интернирование, как правильно сравнивать строки.
Объяснение
Иммутабельность
string — ссылочный тип, но неизменяемый: любая «модификация» создаёт новую строку.
var s = "hello";
var upper = s.ToUpper(); // новая строка, s не изменилась
s += " world"; // новая строка, старая станет мусоромПлюсы: потокобезопасность, безопасные ключи словарей, возможность интернирования. Минус: конкатенация в цикле создаёт много мусора.
StringBuilder
// плохо: O(n²) копирований, тысячи временных строк
string csv = "";
foreach (var r in rows) csv += $"{r.Id};{r.Name}\n";
// хорошо: изменяемый буфер
var sb = new StringBuilder(capacity: rows.Count * 32);
foreach (var r in rows) sb.Append(r.Id).Append(';').AppendLine(r.Name);
var result = sb.ToString();Правило: для нескольких конкатенаций хватает + или интерполяции (компилятор оптимизирует), в циклах — StringBuilder или string.Join.
Интерполяция
var msg = $"Пациент {patient.Name}, возраст {age}, сумма {total:N2}";
var raw = $"""
SELECT * FROM "Patients" WHERE "Id" = '{id}' -- raw string literal, кавычки без экранирования
""";
var path = @"C:\logs\app.log"; // verbatim: \ без экранированияС .NET 6 интерполяция использует DefaultInterpolatedStringHandler — без лишних аллокаций. Для логирования не используйте интерполяцию, используйте шаблоны (_log.LogInformation("Patient {PatientId} loaded", id)) — структурированные логи и нет затрат, если уровень выключен.
Warning
Никогда не подставляйте значения в SQL интерполяцией строк — SQL-инъекция. Только параметры. (Исключение —
FromSqlInterpolatedв EF Core, который сам превращает интерполяцию в параметры.)
Сравнение строк
string.Equals(a, b, StringComparison.Ordinal) // побайтово, быстро — для кодов, ключей, путей
string.Equals(a, b, StringComparison.OrdinalIgnoreCase) // без учёта регистра, для технических значений
string.Equals(a, b, StringComparison.CurrentCulture) // лингвистически — для отображения и сортировки
a == b // Ordinal, с учётом регистра
"ё".Equals("е", StringComparison.CurrentCulture) // зависит от культуры
var dict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);ToLower() для сравнения — лишняя аллокация и ловушки культур (турецкая i). Используйте StringComparison.
Интернирование
Литералы одинакового содержания в сборке указывают на один объект (пул строк). string.Intern(s) — поместить в пул вручную (редко нужно). Поэтому (object)"abc" == (object)"abc" обычно true, а для строк, собранных в рантайме, — false.
Полезные API
string.IsNullOrWhiteSpace(s)
s.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
string.Join(", ", names)
s.Contains("abc", StringComparison.OrdinalIgnoreCase)
s.AsSpan().Slice(0, 5) // подстрока без аллокации
string.Create(len, state, (span, st) => { /* заполнить */ })Нюансы и подводные камни
string.Emptyи""— одно и то же (интернированный литерал). Разница только стилистическая.- Строки длиннее ~42 500 символов (85 000 байт) попадают в Large Object Heap.
Substringсоздаёт новую строку. Для парсинга без аллокаций —ReadOnlySpan<char>.Splitна больших текстах аллоцирует массив и строки. Для CSV — специализированные парсеры (CsvHelper) или Span.- Сравнение пароля или токена через
==уязвимо к timing-атакам →CryptographicOperations.FixedTimeEquals.
Тестирование
[Theory]
[InlineData("Иванов", "иванов", true)]
[InlineData("ABC", "abd", false)]
public void Compares_ignoring_case(string a, string b, bool expected) =>
string.Equals(a, b, StringComparison.OrdinalIgnoreCase).Should().Be(expected);Вопросы с ответами
Почему string иммутабельна и что это даёт?
Любое изменение создаёт новую строку. Это даёт потокобезопасность, безопасное использование в качестве ключей и в хэшах, интернирование и защиту от случайной модификации.
Когда использовать StringBuilder?
При многократной конкатенации, особенно в циклах. Для нескольких склеек хватает + или интерполяции.
Как правильно сравнивать строки?
Явно указывать StringComparison: Ordinal или OrdinalIgnoreCase для технических значений, CurrentCulture — для текста, который видит пользователь. Не использовать ToLower для сравнения.
Что такое интернирование строк?
Хранение одного экземпляра для одинаковых строковых литералов в пуле. Литералы с одинаковым текстом ссылаются на один объект.
Почему в логах не стоит использовать интерполяцию?
Шаблоны сохраняют параметры как отдельные поля (структурированный лог, поиск по PatientId) и не форматируют строку, если уровень лога выключен.
Связанные темы
- Предыдущая: Переменные, var, const и readonly, преобразования типов · Следующая: Nullable: значимые типы и nullable reference types
- Span и Memory: Span и Memory: stackalloc, ref struct
- Параметризованные запросы: Параметризованные запросы и SQL-инъекции
- Структурные логи: Структурные логи: Serilog, корреляция, уровни, что не логировать
- Кодировки: Кодировки, часовые пояса, время на сервере
- Строки в JS: Строки и регулярные выражения