Особенности строк в .NET
Строковый тип данных является одним из самых важных в любом языке программировании. Вряд ли можно написать полезную программу не задействовав этот тип данных. При этом многие разработчики не знают некоторых нюансов связанных с этим типом. Поэтому давайте рассмотрим кое-какие особенности этого типа в .NET.
Итак, начнем с представления строк в памяти
В.NET строки располагаются согласно правилу BSTR (Basic string or binary string). Данный способ представления строковых данных используется в COM (слово basic от языка программирования VisualBasic, в котором он первоначально использовался). Как известно в C/C++ для представления строк используется PWSZ, что расшифровывается как Pointer to Wide-character String, Zero-terminated. При таком расположении в памяти в конце строки находится null-терминированный символ, по которому мы можем определить конец строки. Длина строки в PWSZ ограничена лишь объемом свободной памяти.
С BSTR дело обстоит немного иначе.
Основные особенности BSTR представления строки в памяти:
- Длина строки ограничена неким числом в отличие от PWSZ, где длина строки ограничена наличием свободной памяти.
- BSTR строка всегда указывает на первый символ в буфере. PWSZ может указывать на любой символ в буфере.
- У BSTR всегда в конце находится null символ, так же как и у PWSZ, но в отличие от последнего он является валидным символом и может встречаться в строке где угодно.
- За счет наличия null-символа в конце BSTR совместим с PWSZ, но не наоборот.
Использование такой реализации имеет ряд преимуществ: длину строки не нужно пересчитывать она хранится в заголовке, строка может содержать null-символы, где угодно, и самое главное адрес строки(pinned) можно без проблем передавать в неуправляемой код там, где ожидается WCHAR*.
Сколько памяти занимает объект строкового типа?
Мне встречались статьи где было написано, что размер строкового объекта равен size = 20 + (length/2)*4, однако эта формула не совсем правильная.
Начнем с того, что строка является ссылочным типом, поэтому первые 4 байта содержат SyncBlockIndex, а вторые 4 байта содержат указатель на тип.
Размер строки = 4 + 4 + .
Как было выше сказано, в буфере хранится длина строки — это поле типа int, значит еще 4 байта.
Размер строки = 4 + 4 + 4 + .
Для того, чтобы быстро передать строку в неуправляемый код (без копирования) в конце каждой строки стоит null-терминированный символ, который занимает 2 байта, значит
Размер строки = 4 + 4 + 4 + 2 + .
Осталось вспомнить, что каждый символ в строке находится в UTF -16 кодировке значит, занимает так же 2 байта, следовательно
Размер строки = 4 + 4 + 4 + 2 + 2 * length = 14 + 2 * length
Учтем еще один нюанс, и мы у цели. А именно менеджер памяти в CLR выделяет память кратной 4 байтам (4, 8, 12, 16, 20, 24, . ), то есть если длина строки суммарно будет занимать 34 байта, то выделено будет 36 байта. Нам необходимо округлить наше значение к ближайшему большему кратному четырем числу, для этого необходимо:
Размер строки = 4 * ((14 + 2 * length + 3) / 4) (деление естественно целочисленное)
Вопрос версий: В .NET до 4 версии в классе String хранится дополнительное поле m_arrayLength типа int, которое занимает 4 байта. Данное поле есть реальная длина буфера выделенного под строку включая null — терминированный символ, то есть это length + 1. В .NET 4.0 данное поля удалено из класса, в результате чего объект строкового типа занимает на 4 байта меньше.
Размер пустой строки без поля m_arrayLength(то есть в .NET 4.0 и выше) равен = 4 + 4 + 4 + 2 = 14 байт, а с этим полем (то есть ниже .NET 4.0) равен = 4 + 4 + 4 + 4 + 2 = 18 байт. Если округлять по 4 байта то 16 и 20 байт соответственно.
Особенности строк
Итак, мы рассмотрели, как представляются строки, и сколько на самом деле они занимают места в памяти. Теперь давайте погорим об их особенностях.
- Они являются ссылочными типами.
- Они неизменяемы. Однажды, создав строку, мы больше не можем ее изменить (честным способом). Каждый вызов метода этого класса возвращает новую строку, а предыдущая строка становится добычей для сборщика мусора.
- Они переопределяют метод Object.Equals, в результате чего он сравнивает не значения ссылок, а значения символов в строках.
Строки — ссылочные типы
Строки являются настоящими ссылочными типами, то есть они всегда располагаются в куче. Многие путают их со значимыми типами, потому что они ведут себя также, например, они неизменяемы и их сравнение происходит по значению, а не по ссылкам, но нужно помнить, что это ссылочный тип.
Строки — неизменяемы
- Строковый тип является потокобезопасным, так как ни один поток не может изменить содержимое строки.
- Использование неизменных строк ведет к снижению нагрузки на память, так как нет необходимости хранить 2 экземпляра одной строки. В таком случае и памяти меньше расходуется, и сравнение происходит быстрее, так как требует сравнение лишь ссылок. Механизм, который это реализует в .NET называется интернированием строк (пул строк), о нем поговорим чуть позже.
- При передаче неизменяемого параметра в метод мы можем не беспокоиться, что он будет изменен (если, конечно, он не был передан как ref или out).
Учитывая, что строки неизменны, они могли бы быть и персистентными, однако таковыми не являются. В .NET строки являются эфемерными. Подробнее о том, почему это именно так можно прочитать у Эрика Липперта по ссылке
Для сравнения возьмем строки Java. Они являются неизменяемыми, как и в .NET, но вдобавок и персистентными. Реализация класса String в Java выглядит так:
public final class String
- Ссылка на массив символов char;
- Индекс первого символа строки в массиве char (смещение он начала);
- Количество символов в строке;
- Посчитанный хэш-код, после первого вызова метода hashCode();
Реализация метода String.substring() в Java:
public String substring(int beginIndex, int endIndex) < if (beginIndex < 0) throw new StringIndexOutOfBoundsException(beginIndex); if (endIndex >count) throw new StringIndexOutOfBoundsException(endIndex); if (beginIndex > endIndex) throw new StringIndexOutOfBoundsException(endIndex - beginIndex); return ((beginIndex == 0) && (endIndex == count)) ? this : new String(offset + beginIndex, endIndex - beginIndex, value); > public String(int offset, int count, char value[])
Однако, согласно принципу ЛДНБ (ланчей даром не бывает), о котором так часто говорит Эрик Липперт не все так хорошо. Если исходная строка будет достаточно большой, а вырезаемая подстрока в пару символов, то весь массив символов первоначальной строки будет висеть в памяти пока есть ссылка на подстроку или, если вы сериализуете полученную подстроку стандартными средствами и передаете её по сети, то будет сериализован весь оригинальный массив и количество передаваемых байтов по сети будет большим. Поэтому в таком случае вместо кода
s = ss.substring(3)
можно использовать код
s = new String(ss.substring(3)),
который не будет хранить ссылку на массив символов исходной строки, а скопирует только реально используемую часть массива. Кстати, если этот конструктор вызывать на строке длиной равной длине массива символов, то копирования в этом случае происходить не будет, а будет использоваться ссылка на оригинальный массив.
Как оказалось в последней версии Java реализация строкового типа изменилась. xonix подсказал об этом. Теперь в классе нет полей offset и length, и появился новый hash32 (с другим алгоритмом хеширования). Это означает, что строки перестали быть персистентными. Теперь метод String.substring каждый раз будет создаваться новую строку.
Строки переопределяют Object.Equals
Класс String переопределяет метод Object.Equals, в результате чего сравнение происходит не по ссылке, а по значению. Я думаю, разработчики благодарны создателям класса String за то, что они переопределили оператор ==, так как код, использующий == для сравнения строк, выглядит более изящно, нежели вызов метода.
if (s1 == s2)
if (s1.Equals(s2))
Кстати, в Java оператор == сравнивает по ссылке, а для того чтобы сравнить строки посимвольно необходимо использовать метод string.equals().
Интернирование строк
Ну, и на последок поговорим об интернировании строк.
Рассмотрим простой пример, код который переворачивает строку.
var s = "Strings are immutuble"; int length = s.Length; for (int i = 0; i < length / 2; i++)
Очевидно, данный код не с компилируется. Компилятор будет ругаться на эти строки, потому что мы пытаемся изменить содержимое строки. Действительно, любой метод класса String возвращает новый экземпляр строки, вместо того чтобы изменять свое содержимое.
На самом деле строку можно изменить, но для этого придется прибегнуть к unsafe коду. Рассмотрим пример:
var s = "Strings are immutable"; int length = s.Length; unsafe < fixed (char* c = s) < for (int i = 0; i < length / 2; i++) < var temp = c[i]; c[i] = c[length - i - 1]; c[length - i - 1] = temp; >> >
После выполнения этого кода, как и ожидалось, в строке будет записано elbatummi era sgnirtS.
Тот факт, что строки являются все-таки изменяемыми, приводит к одному очень интересному казусу. Связан он с интернированием строк.
Интернирование строк — это механизм, при котором одинаковые литералы представляют собой один объект в памяти.
Если не вникать глубоко в подробности, то смысл интернирования строк заключается в следующем: в рамках процесса (именно процесса, а не домена приложения) существует одна внутренняя хеш-таблица, ключами которой являются строки, а значениями – ссылки на них. Во время JIT-компиляции литеральные строки последовательно заносятся в таблицу (каждая строка в таблице встречается только один раз). На этапе выполнения ссылки на литеральные строки присваиваются из этой таблицы. Можно поместить строку во внутреннюю таблицу во время выполнения с помощью метода String.Intern. Также можно проверить, содержится ли строка во внутренней таблице с помощью метода String.IsInterned.
var s1 = "habrahabr"; var s2 = "habrahabr"; var s3 = "habra" + "habr"; Console.WriteLine(object.ReferenceEquals(s1, s2));//true Console.WriteLine(object.ReferenceEquals(s1, s3));//true
Важно отметить, что интернируются по умолчанию только строковые литералы. Поскольку для реализации интернирования используется внутренняя хеш-таблица, то во время JIT компиляции происходит поиск по ней, что занимает время, поэтому если бы интернировались все строки, то это свело бы на нет всю оптимизацию. Во время компиляции в IL код, компилятор конкатенирует все литеральные строки, так как нет в необходимости содержать их по частям, поэтому 2 — ое равенство возвращает true. Так вот, в чем заключается казус. Рассмотрим следующий код:
var s = "Strings are immutable"; int length = s.Length; unsafe < fixed (char* c = s) < for (int i = 0; i < length / 2; i++) < var temp = c[i]; c[i] = c[length - i - 1]; c[length - i - 1] = temp; >> > Console.WriteLine("Strings are immutable");
Кажется, что здесь все очевидно и, что такой код должен распечатать Strings are immutable. Однако, нет! Код напечатает elbatummi era sgnirtS. Дело именно в интернировании, изменяя строку s, мы меняем ее содержимое, а так как она является литералом, то интернируется и представляется одним экземпляром строки.
От интернирования строк можно отказаться, если применить специальный атрибут CompilationRelaxationsAttribute к сборке. Атрибут CompilationRelaxationsAttribute контролирует точность кода, создаваемого JIT-компилятором среды CLR. Конструктор данного атрибута принимает перечисление CompilationRelaxations в состав, которого на текущий момент входит только CompilationRelaxations.NoStringInterning — что помечает сборку как не требующую интернирования.
Кстати, этот атрибут не обрабатывается в .NET Framework версии 1.0., поэтому отключить интернирование по умолчанию не было возможным. Сборка mscorlib, начиная со второй версии, помечена этим атрибутом.
Получается, что строки в .NET все-таки можно изменить, если очень захотеть, применяя unsafe код.
А что если без unsafe?
Оказывается, изменить содержимое строки было возможно и, не прибегая к unsafe коду, воспользовавшись механизмом рефлексии. Этот трюк мог прокатить в .NET до 2.0 версии включительно, потом разработчики класса String лишили нас такой возможности.
В версии .NET 2.0 у класса String есть два internal метода: SetChar, проверяющий выход за границы, и InternalSetCharNoBoundsCheck, не проверяющий выход за границы, которые устанавливают указанный символ по определенному индексу. Вот их имплементация:
internal unsafe void SetChar(int index, char value) < if ((uint)index >= (uint)this.Length) throw new ArgumentOutOfRangeException("index", Environment.GetResourceString("ArgumentOutOfRange_Index")); fixed (char* chPtr = &this.m_firstChar) chPtr[index] = value; > internal unsafe void InternalSetCharNoBoundsCheck (int index, char value)
Таким образом, используя следующий код, можно изменить содержимое строки, даже не прибегая к использованию unsafe коду.
var s = "Strings are immutable"; int length = s.Length; var method = typeof(string).GetMethod("InternalSetCharNoBoundsCheck", BindingFlags.Instance | BindingFlags.NonPublic); for (int i = 0; i < length / 2; i++) < var temp = s[i]; method.Invoke(s, new object[] < i, s[length - i - 1] >); method.Invoke(s, new object[] < length - i - 1, temp >); > Console.WriteLine("Strings are immutable");
Этот код как уже ожидалось, напечатает elbatummi era sgnirtS.
Вопрос версий: В разных версиях .NET Framework string.Empty может интернироваться, а может, и нет.
Рассмотрим код:
string str1 = String.Empty; StringBuilder sb = new StringBuilder().Append(String.Empty); string str2 = String.Intern(sb.ToString()); if (object.ReferenceEquals(str1, str2)) Console.WriteLine("Equal"); else Console.WriteLine("Not Equal");
В .NET Framework 1.0, .NET Framework 1.1 и .NET Framework 3.5 с пакетом обновления 1 (SP1), str1 и str2 равны. В .NET Framework 2.0 с пакетом обновления 1 (SP1) и .NET Framework 3.0, str1 и str2 не равны. В настоящее время string.Empty интернируется.
Особенности производительности
У интернирования есть отрицательный побочный эффект. Дело в том, что ссылка на интернированный объект String, которую хранит CLR, может сохраняться и после завершения работы приложения и даже домена приложения. Поэтому большие литеральные строки использовать не стоит или же, если это необходимо стоит отключить интернирование, применив атрибут CompilationRelaxations к сборке.
Надеюсь, статья оказалось полезной.
- строки
- структура данных
- особенности
String.Intern делает строки ещё интереснее
Проходя/проводя собеседования, приходится сталкиваться с вопросами, которые раскрывают общее понимание работы .NET. По моему мнению, наибольшей любовью среди таких вопросов пользуются вопросы о работе “сборщика мусора”, но однажды мне был задан вопрос о интернировании строк. И он, честно говоря, поставил меня в тупик. Поиск в рунете выдал несколько статей, но они не давали ответы на те вопросы, которые я искал. Надеюсь мой перевод статьи Эндрю Стеллмана (автора книги “Head First C#”) заполнит этот пробел. Думаю, этот материал будет полезен для начинающих .NET разработчиков и тем кому стало интересно, что же такое интернирование строк в .NET.
String.Intern делает строки ещё интереснее
Одна из первых вещей, с которой сталкивается каждый начинающий C# разработчик — это работа со строками. Я показываю основу работы со строками в начале «Head First C#», как поступают практически в любой другой книге по C#. Так что не следует удивляться, что C# разработчики уровня джуниор и мидл уровня чувствуют, что они получили довольно хорошую базу по строкам. Но строки интереснее, чем кажутся. Одним из самых интересных аспектов строк в C# и .NET является метод String.Intern. Понимание работы этого метода может улучшить ваши навыки в C# разработке. В этом посте, я сделаю краткий туториал для метода String.Intern, чтобы показать вам как он работает.
Примечание: В конце этого поста я собираюсь показать кое-что «под капотом», используя ILDasm. Если вы никогда не работали с ILDasm раньше, это будет хорошей возможностью что-бы познакомиться с очень полезным инструментом .NET.
Некоторые основы работы со строками
Давайте начнем с краткого обзора того, что ожидают от класса System.String. (Я не буду вдаваться в подробности — если кто-то хочет пост о основах строк в .NET, добавьте комментарий или свяжитесь со мной на Building Better Software, и я буду рад обсудить возможную статью вместе!)
Создайте новое консольное приложение в Visual Studio. (Все точно так же работает из командной строки, если вы хотите использовать csc.exe для компиляции кода, но ради легкости восприятия материала давайте придерживаться разработки в Visual Studio.) Вот код метода Main() — точки входа консольного приложения:
using System; class Program < static void Main(string[] args) < string a = "hello world"; string b = a; a = "hello"; Console.WriteLine(", ", a, b); Console.WriteLine(a == b); Console.WriteLine(object.ReferenceEquals(a, b)); > >
В этом коде не должно быть никаких сюрпризов. Программа выводит три строки на консоль (помните, если вы работаете в Visual Studio, используйте Ctrl-F5, чтобы запустить программу вне отладчика; также в программу будет добавлено «Press any key . », что-бы предотвратить закрытие окна консоли):
hello, hello world
False
False
Первый WriteLine() выводит две строки. Второй сравнивает их с помощью оператора равенства ==, который возвращает False, потому что строки не совпадают. И последний сравнивает их, чтобы увидеть не ссылаются ли обе переменные на один и тот же объект String. Поскольку это не так, метод отображает значение False.
Затем добавьте эти две строки в конец метода Main():
Console.WriteLine((a + " world") == b); Console.WriteLine(object.ReferenceEquals((a + " world"), b));
И опять вы получите довольно очевидный ответ. Оператор равенства возвращает True, так как обе строки равны. Но когда вы использовали конкатенацию строк «Hello» и «world», оператор + объединяет их и возвращает новый экземпляр System.String. Именно поэтому object.ReferenceEquals() вполне резонно возвращает False. Метод ReferenceEquals() возвращает True только в том случае, если оба аргумента ссылаются на один и тот же объект.
Такой способ позволяет нормально работать с объектами. Два разных объекта могут иметь одинаковые значения. Такое поведение является вполне практичным и предсказуемым. Если вы создаете два объекта “дом” и установите всем их свойствам одинаковые значения, вы будете иметь два одинаковых объекта типа “дом”, но это будут различные объекты.
Это все еще кажется немного запутанным? Если так, то я определенно рекомендую обратить внимание на несколько первых глав “Head First C#”, которые дадут вам представление о написании программ, отладке, и использование объектов и классов. Вы можете скачать их как бесплатные вырезки из этой книги.
Итак, пока мы работаем со строками — все прекрасно. Но как только мы начинаем играться ссылками на строки, все становится немного странным.
Что-то с этой ссылкой не так .
Создайте новое консольное приложение. Код ниже для него. Но, перед компиляцией и выполнением, внимательно посмотрите на код. Попробуйте угадать, что он отобразит в консоли?
using System; class Program < static void Main(string[] args) < string hello = "hello"; string helloWorld = "hello world"; string helloWorld2 = hello + " world"; Console.WriteLine(", : , ", helloWorld, helloWorld2, helloWorld == helloWorld2, object.ReferenceEquals(helloWorld, helloWorld2)); > >
Теперь запустите программу. Вот то, что она отобразит в консоли:
hello world, hello world: True, False
И так, это именно то, что мы ожидали. В объектах helloWorld и helloWorld2 строки содержат “Hello world", так что они равны, но ссылки разные.
Теперь добавьте в нижней части вашей программы этот код:
helloWorld2 = "hello world"; Console.WriteLine(", : , ", helloWorld, helloWorld2, helloWorld == helloWorld2, object.ReferenceEquals(helloWorld, helloWorld2));
Запустите его. На этот раз код отобразит в консоли следующую строку:
hello world, hello world: True, True
Подождите, получается что сейчас HelloWorld и HelloWorld2 ссылаться на одну и ту же строку? Наверное, некоторым может показаться такое поведение странным или, по крайней мере, немного неожиданным. Мы не меняли значение helloWorld2 вообще. Многие в конечном итоге думают что-то вроде этого: “переменная была уже равна «hello world». Установка в «hello world» ещё один раз не должна ничего изменить.” Так в чем же дело? Давайте разберёмся.
Что такое String.Intern? (погружаясь в пул интернирования . )
При использовании строк в C#, CLR делает что-то хитрое и это что-то называется интернирование строк. Это способ хранения одной копии любой строки. Если вы храните в ста или, что еще хуже, в миллионе строковых переменных одинаковое значение получится, что память для хранения значений строк будет выделяться снова и снова. Интернирование строки это способ обойти эту проблему. Среда CLR поддерживает таблицу называемую пул интернирования. Эта таблица содержит одну уникальную ссылку на каждую строку, которая либо объявлена, либо создана программно во время выполнения вашей программы. А .NET Framework предоставляет два полезных метода для взаимодействия с пулом интернирования: String.Intern() и String.IsInterned().
Метод String.Intern() работает очень простым способом. Вы передадите ему в качестве аргумента строку. Если эта строка уже находится в пуле интернирования, метод возвращает ссылку на эту строку. Если её еще не нет, он добавляет строку в пул и возвращает на неё ссылку. Вот пример:
Console.WriteLine(object.ReferenceEquals( String.Intern(helloWorld), String.Intern(helloWorld2)));
Этот код будет отображать True, даже если HelloWorld и HelloWorld2 ссылки на два разных строковых объекта, потому что они оба содержат строку «Hello World».
Остановитесь на минутку. Стоит ещё немного поразбираться с String.Intern() потому, что иногда метод дает немного нелогичные на первый взгляд результаты. Вот пример такого поведения:
string a = new string(new char[] ); object o = String.Copy(a); Console.WriteLine(object.ReferenceEquals(o, a)); String.Intern(o.ToString()); Console.WriteLine(object.ReferenceEquals(o, String.Intern(a)));
Выполнение кода выведет две строки на консоль. Первый метод WriteLine() покажет значение False, и это понятно, так как метод String.Copy() создает новую копию строки и возвращает ссылку на новый объект. Но почему выполнив вначале String.Intern(о.ToString()) затем String.Intern(a) вернёт ссылку на о? Остановитесь на минутку, чтобы подумать об этом. Это становится еще более нелогичным, если вы добавите еще три строки:
object o2 = String.Copy(a); String.Intern(o2.ToString()); Console.WriteLine(object.ReferenceEquals(o2, String.Intern(a)));
Похоже, эти строчки кода сделали то же самое, только с новой переменной объекта o2. Но в последнем WriteLine() выведет значение False. Так что же происходит?
Этот небольшой беспорядок поможет нам разобраться, что происходит под капотом String.Intern() и пула интернирования. Первое, что необходимо уяснить для себя это то, что метод строкового объекта в ToString() всегда возвращает ссылку на самого себя. Переменная o указывает на объект строки, содержащий значение «abc», поэтому вызов собственного метода ToString() возвращает ссылку на эту строку. Итак, вот что происходит.
В начале а указывает на объект строки №1, в котором содержится «abc». Переменная о указывает на другой объект строки №2 который также содержит «abc». Вызов String.Intern(o.ToString()) добавляет ссылку на строку №2 в пул интернирования. Теперь, когда объект строки №2 находится в пуле интернирования, в любое время String.Intern() вызывая с параметром «abc» будет возвращать ссылку на объект строки №2.
Поэтому, когда вы передаёте пременную о и String.Intern(а) в метод ReferenceEquals(), он возвращает True, потому что String.Intern(а) вернула ссылку на объект строки №2. Теперь мы создали новую переменную o2 и использовали метод String.Copy(), что бы создать еще один объект типа String. Это будет объект строки №3, который также содержит строку «abc». Вызов String.Intern(o2.ToString()) ничего не добавляет к пулу интернирования на этот раз, потому что «abc» уже есть, но вернёт указатель на строку №2.
Так что этот вызов Intern() фактически возвращает ссылку на строку №2, но мы отбрасываем его вместо того, чтобы присвоить переменной. Мы могли бы сделать что-то вроде этого: string q = String.Intern(o2.ToString()), что сделало бы переменную q ссылкой на объект строки №2. Именно поэтому, последний WriteLine() выводит False так как это сравнение ссылки строки №3 со ссылкой на строку №2.
Используйте String.IsInterned() для проверки, является ли строка в пуле интернирования
Есть другой, несколько парадоксально названный метод, который полезен при работе с интернированными строками: String.IsInterned(). Он принимает ссылку на объект строки. Если эта строка находится в пуле интернирования, он возвращает ссылку на интернированную строку строки, если она еще не находится в пуле интернирования, то метод возвращает null.
Причина, по которой его название звучит немного нелогичным в том, что этот метод начинается с «Is» но при этом возвращает не булев тип, как ожидают многие программисты.
При работе с методом IsInterned() для отображения того что строка отсутствует в пуле интернирования удобно использовать null-коалесцирующий оператор — ??. К примеру написав:
string o = String.IsInterned(str) ?? "not interned";
Теперь в переменную о вернется результат IsInterned() если он не нулевой, или строка «not interned», если строки нет в пуле интернирования.
Если этого не сделать, то метод Console.WriteLine() будет выводить пустые строки (что делает этот метод, когда сталкивается null).
Вот простой пример того, как String.IsInterned() работает:
string s = new string(new char[] ); Console.WriteLine(String.IsInterned(s) ?? "not interned"); String.Intern(s); Console.WriteLine(String.IsInterned(s) ?? "not interned"); Console.WriteLine(object.ReferenceEquals( String.IsInterned(new string(new char[] < 'x', 'y', 'z' >)), s));
Первый WriteLine() оператор отобразит в консоли «not interned», потому что «xyz» еще нет в пуле интернирования. Второй WriteLine() оператор печатает «xyz» потому, что пул интернирования уже содержит «xyz». И третий WriteLine () выведет True, так как объект s указывает на объект, добавленный в пул интернирования.
Литералы интернируются автоматически
Добавив всего одну строку в конец метода и запустив программу снова:
Сonsole.WriteLine(object.ReferenceEquals("xyz", с));
произойдет что-то совсем неожиданное!
Программа никогда не отобразит «not interned», а последние два метода WriteLine() покажут False! Если мы закомментируем последнюю строку, то программа действует именно так, как вы ожидали. Почему?! Как добавив код в конце программы, поменялось поведение программы кода над ним? Это очень, очень странно!
Это кажется действительно странным в первый раз, когда вы сталкиваетесь с этим, но в этом действительно есть смысл. Причина изменения поведения всей программы в том, что код содержит литерал «xyz». А когда вы добавляете литерал в вашу программу, CLR автоматически добавляет его в пул интернирования ещё до начала выполнения программы. Комментируете эту строку, вы убираете литерал из программы и пул интернирования уже не будет содержать строку «xyz».
Понимая, что «xyz» уже находится в пуле интернирования при запуске программы, так как эта строка в виде литерала появилась в коде, то сразу стаёт понятным такое изменение в поведении программы. String.IsInterned(s) больше не возвращает null. Вместо этого, он возвращает ссылку на литерал «xyz», что также объясняет, почему ReferenceEquals() возвращает False. Это происходит из за того, что строка s никогда не будет добавлена в пул интернирования («xyz» уже в пуле, указывая на другой объект).
Компилятор умнее, чем вы думаете!
Измените последнюю строку кода на эту:
Console.WriteLine( object.ReferenceEquals("x" + "y" + "z", s));
Запустите программу. Она работает точно так же, как если бы вы использовали литерал «xyz»! Неужели + не оператор? Разве это не метод, который запускается на выполнение по CLR во время выполнения? Если это так, то должен быть код, который предотвратит интернирование литерала «xyz».
В самом деле так и произойдёт если вы замените «х» + «у» + «z» на String.Format("", 'x', 'y', 'z'). Обе строчки кода возвращают «xyz». Почему же при помощи оператора + для конкатенации получаем поведение, как если бы вы использовали литерал «xyz», хотя в тоже самое время как String.Format() выполняется во время выполнения?
Самый простой способ ответить на этот вопрос — это увидеть то, что на самом деле получаем при компиляции кода «x» + «у» + «z» .
using System; class Program < public static void Main() < Console.WriteLine("x" + "y" + "z"); >>
Следующим шагом нужно выяснить, что компилятор собрал приложение исполняемого типа. Для этого мы будем использовать ILDasm.exe, дизассемблер MSIL. Этот инструмент устанавливается с каждой версией Visual Studio (в том числе и изданий Express). И даже если вы не знаете, как читать IL, вы сможете понять, что происходит.
Запустите Ildasm.exe. Если вы используете 64-разрядную версию Windows, выполните следующую команду: "%ProgramFiles (x86)%\Microsoft SDKs\Windows\v7.0A\Bin\Ildasm.exe" (включая кавычки), либо из Пуск >> окно Run, либо из командной строки. Если вы используете 32-разрядную версию Windows, вам стоит выполнить следующую команду: "%ProgramFiles%\Microsoft SDKs\Windows\v7.0A\Bin\ildasm.exe".
Если у вас .NET Framework 3.5 или более ранней версии
Если у вас .NET Framework 3.5 или более ранней версии, возможно, потребуется поискать ildasm.exe в соседних папках. Запустите окно проводника и перейдите в папку Program Files. Как правило нужная пограмма находится в папке «Microsoft SDKs\Windows\vX.X\bin». Кроме того вы можете запустить командную строку из «Visual Studio Command Prompt» которая находится в меню Пуск, после чего набрать «ILDASM» для его запуска.
Вот так, выглядит ILDasm при первом запуске:
Затем скомпилируйте свой код в исполняемый файл. Кликните на проект в Solution Explorer — в окне Properties должно располагаться поле Project Folder. Дважды кликните по нему и скопируйте. Перейдя в окно ILDasm, выберите Файл >> Открыть в меню, и вставьте путь к папке. Затем перейдите в папку «bin». Ваш исполняемый файл должен находиться либо в папке bin\Debug или bin\Release. Откройте исполнимый файл. ILDasm должен показать вам содержимое сборки.
(Если вам нужно освежить память о том, как создаются сборки, см. этот пост для понимания C# и .NET сборок и пространств имен ).
Разверните класс Program и дважды щелкните на методе Main(). После этих действий должен появиться дизасамблированный код метода:
Вам не нужно знать IL чтобы увидеть наличие литерала «xyz» в коде. Если закрыть ILDasm, а затем изменить код, чтобы использовать «xyz» вместо «х» + «у» + «z», разобрали IL код выглядит точно так же! Это потому, что компилятор достаточно умен, чтобы заменить «х» + «у» + «z» на «xyz» во время компиляции, так что не придётся тратить лишние операции на вызовы методов, которые всегда будет возвращать «xyz». А когда литерал компилируется в программе, то CLR добавляет его в пул интернирования при запуске программы.
Материал данной статьи должен дать вам хорошее представление о интернировании строк в C# и .NET. В принципе этого даже больше чем нужно для понимания работы интернирования строк. Если вы заинтересованы в получении дополнительной информации, хорошим плацдармом является раздел «Performance Considerations» на страницах MSDN о String.Intern.
P.S.: Спасибо команде за усердную вычитку и объективную критику перевода.
- Интернирование строк
- .NET framework
- String.Intern
- ildasm
Что такое интернирование строк
Поясним результат. Строки str1 и str2 добавлены в пул строк на этапе компиляции. Во время выполнения программы происходит конкатенация строки «interned» со значением строки str2, с последующим интернированием получившейся строки в пул строк (благодаря методу intern()). Но, так как пул строк уже содержит строку «interned TopJava», объекту String str3 будет присвоена ссылка на строку в пуле строк и, соответственно, выражение равенства ссылок «==» будет истинным.
Принимая во внимание всё вышесказанное, вы можете спросить: «Почему бы все строки сразу после их создания не добавлять в пул строк? Ведь это приведет к экономии памяти…». Да, среди достаточно большого количества программистов такое заблуждение присутствует. Именно заблуждение, поскольку не все учитывают дополнительные затраты виртуальной машины на процесс интернирования, а также падение производительности, в целом. Тесты и наглядное подтверждение этого приводятся в видео докладе Алексея Шипилёва — «Катехизис java.lang.String». Финализируя доклад Алексея можно сказать, что интернирование (в виде применения метода intern ()) рекомендуется вообще не использовать. Вместо интернирования необходимо использовать дедупликацию (рассматривается далее).
Оптимизация кода Python: интернирование строк
Обычно, когда мы создаем строковый объект, интерпретатор Python определяет, следует ли кэшировать эту строку. Это характерное поведение интерпретатора в определенных условиях, например, при обработке идентификаторов.
Объявление строки с именем, которое начинается либо с буквы, либо с подчеркиванием и включает только или комбинации букв / подчеркиваний / чисел, заставит Python интернировать строку и создать для нее хеш.
Так как у Python есть много внутреннего кода, использующего словари, это приводит к выполнению процесса поиска идентификаторов. Поэтому интернирование строк идентификатора ускоряет весь процесс. Проще говоря, Python хранит все идентификаторы в таблице и генерирует уникальные ключи (хеш) для каждого объекта для будущих поисков. Эта оптимизация происходит во время компиляции. И это также объединяет интернирование строковых литералов, которое напоминает идентификаторы. Таким образом, это довольно полезная функция в Python, она помогает ускорить обработку большого анализа текста или аналитики.
Строки, которые читаются из файла или получены через сетевое общение, не являются частью автоматического интернирования в Python. Но всегда можно использовать функцию intern () для обработки таких строк.