Course program← C#: Базовый курс
1Знакомство с C# и .NET
2Данные и типы
3Ветвления и циклы
4Методы и ошибки
5Коллекции и строки
6Классы и объекты

Строки и StringBuilder

This lesson is not translated yet – the Russian original is below.

Строка выглядит как простой тип, но устроена сложнее числа. Одно её свойство объясняет и удобство, и главную ловушку производительности в 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
    }
    """;

Внутри не действуют служебные последовательности, и экранировать кавычки не нужно. Отступ считается по закрывающим кавычкам – всё, что левее, отбрасывается, поэтому текст можно выравнивать по коду.