Andriy Shyrokoryadov

.Net developer, data scientist

Тип string в языке C# - вопрос №27 на собеседование C# / .NET

Текст к видео "Тип string в языке C#" на канале YouTube

Во время собеседования тип string является плодородной почвой для большего числа вопросов и дискуссий, которые позволяют оценить технические навыки кандидата. Было бы странно если бы на моем канале не появилось видео на эту тему, ведь именно в этом видео мы разберем сразу несколько вопросов собеседования и все они, так или иначе, будут связаны с типом string.

Первый вопрос, который Вы можете услышать это каким типом, значимым или ссылочным, является тип string? В продолжении дискуссии Вас могут попросить обосновать вашу точку зрения. Правильным и коротким ответом на этот вопрос будет следующий ответ: тип string является ссылочным типом, однако имеет семантику значимого типа. Такой короткий ответ явно требует дополнительного объяснения. Для начала я рекомендую ознакомиться с объяснением что такое ссылочные и значимые типы и как они хранятся в памяти – на эту тему у меня снято видео, и ссылка будет в правом верхнем углу экрана. Итак, тип string является ссылочным и хранится на куче. Почему это сделано именно так? Почему было принято решение сделать тип string ссылочным, а не значимым. Дело в том, что в отличие от настоящих значимых типов, при создании типа string мы не знаем, какой будет размер в памяти этого типа. Например, для типа integer размер любого значения этого типа составляет 4 байта. А с типом string вряд ли получится изначально определить данное ограничение. К тому же что делать с большими строками? Величина стэка в 32-х битных системах составляет 1Мб, а в 64-х битных системах – 4Мб. Если у нас будет очень большая строка и тип string был бы значимым, нам бы пришлось её размещать на куче, то есть имели бы место операции boxing и unboxing. А из видео, посвященного обобщенному программированию, мы уже знаем, что данные операции влияют на производительность приложения. Ссылка на данное видео будет в правом верхнем углу. Возможно, эти аргументы повлияли на решение о том, что тип string является ссылочным типом.

Давайте подумаем над второй частью нашего ответа – тип с семантикой значимого типа. Что это значит? Давайте вспомним, когда 2 ссылочных типа считаются равными. Они считаются равными, когда их ссылки указывают на одно и тоже место в памяти. Хорошо, рассмотрим пример и предположим, что строки не имеют семантики значимых типов:

var string1 = String example;
var string2 = String example;
var isEqual = string1 == string2;

Какое значение будет иметь переменная isEqual, если тип string не имеет семантики значимого типа. Значение будет false. Переменная string1 указывает на одно место в памяти, переменная string2 указывает на другое место в памяти. Эти места разные, и следовательно переменные не равны, даже если значения этих переменных равны. Согласитесь это вообще бред – сказать, что строка «Привет» не равна строке «Привет», только лишь потому что эти 2 строки хранятся в разных местах в памяти. Программисты Microsoft, наверное, тоже так подумали и оставили для нас возможность сравнивать строки по значению, как значимые типы. Это и есть семантика значимых типов. Если еще раз подумать что произошло, то произошло обыкновенное переопределение операторов «==» и «!=» для строк. О переопределении операторов у меня уже есть видео на канале – ссылка, как всегда, в правом верхнем углу.

С вопросом каким типом является строка мы разобрались, теперь давайте рассмотрим вопрос, что значит утверждение «Строка это иммутабельный тип». Прилагательное «иммутабельный» пришло к нам из английского языка и означает «неизменяемый». То есть предыдущее выражение звучит так – «строка — это неизменяемый тип». Что конкретно это означает. Как только мы создали экземпляр строки в памяти компьютера мы не можем его изменить. Обратите внимание что большинство методов класса String возвращают значение типа string. Например, изменится ли переменная someString после выполнения данного когда?

string someString = Hello, World!;
someString.Remove(5);

Правильный ответ – переменная someString не изменится. Однако метод Remove возвращает результат типа string и данное значение мы можем присвоить новой переменной:

string someString = Hello, World!;
string anotherString = someString.Remove(5); // anotherString == “Hello”

Отсюда следует вывод – каждая манипуляция на переменной типа string не изменяет существующую переменную, а создает новую переменную. В этом заключается свойство неизменяемости строк. Так плавно мы подошли к вопросу создания кода, который эффективно манипулирует строками, не влияя негативно на производительность приложения. Помня о том, что строки иммутабельны, давайте подумает сколько строк будет создано в памяти после выполнения следующего кода:

var string1 = XYZ;
var string2 = ABC;
var string3 = string1 +   + string2;

В памяти компьютера будут созданы следующие строки: “XYZ”, “ABC”, “ ”, “XYZ ABC ”. Итого 4 переменные. А сколько строк в памяти будет в данном примере:

var string1 = XYZ;
var string2 = ABC;
var string3 = ABC;
var string4 = string1 +   + string2 +   + string3;

В этот раз также будут созданы 4 переменные: “XYZ”, “ABC”, “ ”, “XYZ ABC ABC”. Обратите внимание, что для строковых переменных с одинаковым значением не создается новая переменная в памяти (переменные со значениями “ABC” и “ ” ), а используется уже существующая. Все-таки строка — это ссылочный тип и, например, переменные string2 и string3 ссылаются на одно и тоже место в памяти, где хранится значение “ABC”. Об этом также следует помнить.

Из всего вышесказанного следует, что чем больше мы будем манипулировать нашими переменными типа string, тем больше нам будет необходимо памяти для нашего приложения. Каждая манипуляция со строкой создаёт новую строку и если в памяти нет такой же строки, то новое значение будет размещено в памяти. Получается, что приложение, которое манипулирует строками, будет медленным? Не совсем. В языки C# уже есть решение этой проблемы. Этим решением является использование класса StringBuilder. Из названия класса мы можем сделать вывод, что данный класс служит для создания строк. В классе StringBuilder определено несколько методов для соединения строк. Ниже пример использования данного класса:

var sb = new StringBuilder();
sb.Append("Hello, ");
sb.AppendLine("World!");
var text = sb.ToString();  // Hello, World!

Подобную операция мы могли бы сделать, просто запустив следующий код:

var text2 = "Hello," + " " + "World!"; // Hello, World!

Сравнивая эти 2 примера и помня о том, что каждая модификации строки создаёт новую строку, мы можем сказать, что в первом случае в памяти была создана одна строка “Hello, World!”, а во втором случае 4 строки: “Hello,” “ ”, “World!”, “Hello, World!”. Естественно, при создании только лишь одной переменной строкового типа, нам сложно оценить, насколько эффективно мы используем ресурсы компьютера. Обе операции, выполненные один раз, выполняются достаточно быстро и разница во времени почти незаметна. Давайте рассмотрим практический пример, где мы создаем десятки тысяч строк сначала используя простое сложение строк, а затем, используя класс StringBuilder.

[ПРАКТИЧЕСКИЙ ПРИМЕР]

Как вы уже убедились простое сложение строк, так называемая «конкатенация», не является самым эффективным решением. Поэтому если вы видите, что в вашем приложение будет иметь место частое сложение строк, то лучшим решением будет использование класса StringBuilder. Что же происходит внутри класса StringBuilder, что делает этот класс таким эффективным? Все строки, которые передаются в класс StringBuilder через соответствующие методы конвертируются в массив переменных типа char и сохраняются в буфере. То есть по факту все модификации строк происходят в виде модификаций массива с данными типа char, что более эффективно, чем копирование и создание новых строк в памяти. Например, внутренний буфер объекта класса будет выглядеть следующим образом:

var sb = new StringBuilder();
sb.Append("Hello, ");
var buffer = new [] {H, e, l, l, o, ,,  }; // пример буфера