Строка выглядит как простой тип, но устроена сложнее числа. Одно её свойство объясняет и удобство, и главную ловушку производительности в C#.
Строка неизменяема
Ни один метод строки не меняет её. Все возвращают новую:
string text = "привет";
text.ToUpper(); // результат потерян: строка не изменилась
Console.WriteLine(text); // привет
string upper = text.ToUpper(); // вот теперь есть результат
Console.WriteLine(upper); // ПРИВЕТ
Забыть присвоить результат – ошибка настолько частая, что компилятор выдаёт на неё предупреждение.
Даже склейка создаёт новую строку, а не дописывает к существующей:
string a = "при";
a += "вет"; // создана новая строка, старая брошена
Неизменяемость – не недосмотр, а решение. Строку можно передать в чужой метод и не бояться, что её испортят. Одинаковые литералы рантайм хранит в единственном экземпляре, а хеш строки можно вычислить один раз – всё это работает только потому, что содержимое не меняется.
Так же ли в других языках
Поведение при попытке изменить строку:
string text = "привет";
text.ToUpper(); // строка не изменилась, результат выброшен
text = "привет"
text.upper() # то же самое: строки неизменяемы и здесь
text := "привет"
text[0] = 'П' // не соберётся: строка в Go тоже неизменяема
Все три языка сходятся: строку менять на месте нельзя. А вот в C++ std::string изменяемая – и
там text[0] = 'П' работает. Привычка из одного языка здесь подводит.
Что умеет строка
string text = " Привет, Мир! ";
Console.WriteLine(text.Length); // длина вместе с пробелами
Console.WriteLine(text.Trim()); // без пробелов по краям
Console.WriteLine(text.ToLower()); // нижний регистр
Console.WriteLine(text.Replace("Мир", "C#"));
Console.WriteLine(text.Contains("Привет")); // true
Console.WriteLine(text.IndexOf("Мир")); // позиция или -1
Обращение к отдельному символу – по индексу, как у массива, но только для чтения:
string name = "Аня";
Console.WriteLine(name[0]); // А – это char, а не string
Разбор и сборка:
string csv = "яблоко,груша,слива";
string[] parts = csv.Split(',');
string joined = string.Join(" и ", parts); // яблоко и груша и слива
Проверка на пустоту
Пустая строка и null – разные вещи, и проверять их по отдельности неудобно:
if (string.IsNullOrEmpty(text)) { } // null или ""
if (string.IsNullOrWhiteSpace(text)) { } // null, "" или одни пробелы
Второй вариант обычно и нужен: ввод из одних пробелов пользователь считает пустым, и программа должна считать так же.
Не пишите text == "" и тем более text.Length == 0: на null второй вариант упадёт. Готовые методы короче и безопаснее.
Цена склейки в цикле
Вот здесь неизменяемость выходит боком:
string result = "";
for (int i = 0; i < 10000; i++)
{
result += "строка"; // каждый раз новая строка целиком
}
На каждом шаге создаётся новая строка и в неё копируется всё, что накопилось. К десятитысячному повторению копируется уже почти вся длина – и общий объём работы растёт квадратично.
Для сборки строки по кускам есть StringBuilder: он держит изменяемый буфер и копирует
содержимое лишь изредка.
using System.Text;
var builder = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
builder.Append("строка");
}
string result = builder.ToString();
Не хватайтесь за StringBuilder всюду. Для двух-трёх склеек он только удлиняет код: интерполяция читается лучше и работает не медленнее. Он нужен там, где кусков много и их число заранее неизвестно, – прежде всего в циклах.
Сравнение строк
Оператор == сравнивает содержимое, а не адреса, – для строк это поведение определено особо:
string a = "привет";
string b = "при" + "вет";
Console.WriteLine(a == b); // True
Сравнение без учёта регистра делается явно:
bool same = string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
Не приводите строки к нижнему регистру ради сравнения: a.ToLower() == b.ToLower() создаёт две лишние строки и в некоторых языках даёт неверный результат. У турецкой буквы «i» другие правила, и приведение регистра ломает сравнение – это известная ловушка, у неё даже есть имя, «турецкая I».
Многострочные строки
Текст с переносами и кавычками удобно писать в тройных кавычках:
string json = """
{
"name": "Аня",
"age": 19
}
""";
Внутри не действуют служебные последовательности, и экранировать кавычки не нужно. Отступ считается по закрывающим кавычкам – всё, что левее, отбрасывается, поэтому текст можно выравнивать по коду.