string в .NET это ссылочный тип с поведением значения: он неизменяемый, а == сравнивает содержимое. Отсюда большинство вопросов на собесе.
Неизменяемость
Любая «модификация» создаёт новую строку:
string s = "hello";
s.ToUpper(); // результат выброшен, s не изменилась
s = s.ToUpper(); // теперь s = "HELLO"
Плюсы: строку безопасно передавать между потоками, хранить как ключ словаря (хеш не меняется), разделять одинаковые литералы. Минус: склейка в цикле.
Конкатенация и StringBuilder
string result = "";
foreach (var part in parts) result += part; // O(n²) по копированию
Каждый += создаёт строку длины «всё накопленное плюс кусок». Для неизвестного числа частей берите StringBuilder или string.Join/string.Concat. А выражение a + b + c в одной строке компилятор сам превращает в один вызов string.Concat, там StringBuilder не нужен.
Интерполяция $"Привет, {name}" с C# 10 компилируется в DefaultInterpolatedStringHandler: значения добавляются без упаковки, буфер берётся из пула.
Сравнение
==иEqualsу строк сравнивают содержимое порядково.- Если статический тип
object,==сравнивает ссылки. - Регистр и культуру задают явно:
string.Equals(a, b, StringComparison.OrdinalIgnoreCase); // идентификаторы
string.Compare(a, b, StringComparison.CurrentCulture); // сортировка текста для людей
var dict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
a.ToLower() == b.ToLower() хуже: две лишние строки и зависимость от культуры (знаменитая турецкая I).
Интернирование
Строковые литералы в сборке хранятся в пуле: два одинаковых литерала это один объект. Строки, созданные во время выполнения, в пул не попадают, пока не вызвать string.Intern. Для собеса важно понимать это, а в коде сравнивать по содержимому.
Пустые строки и null
string.IsNullOrEmpty(s) ловит null и "", string.IsNullOrWhiteSpace(s) ещё и строку из пробелов.
Юникод
char это UTF-16 code unit. Эмодзи и редкие иероглифы занимают два char (суррогатная пара), поэтому "👍".Length == 2. Для «видимых символов» есть StringInfo и Rune.
Строки без аллокаций
ReadOnlySpan<char> даёт окно на часть строки без копирования:
ReadOnlySpan<char> line = "id=42;name=Ira";
int eq = line.IndexOf('=');
int id = int.Parse(line.Slice(eq + 1, 2)); // без Substring
Парсеры чисел, IndexOf, Trim, StartsWith принимают span. В горячем парсинге это убирает основную часть мусора.
Частые ошибки
- Вызывать
Replace,Trim,ToUpperи не использовать результат. - Склеивать строки в цикле через
+=. - Сравнивать без
StringComparisonи получать разные результаты на разных серверах. - Форматировать числа без
CultureInfo.InvariantCultureдля машинного вывода: на русской локали будет запятая.
Что спросят на собесе
- Почему строки неизменяемы и что из этого следует?
- Когда нужен
StringBuilder, а когда нет? - Чем
==отличается отEqualsиReferenceEqualsдля строк? - Что такое интернирование?
- Как правильно сравнить строки без учёта регистра?