Особенности строк в .NETИсточник: habrahabr timyrik20
Строковый тип данных является одним из самых важных в любом языке программировании. Вряд ли можно написать полезную программу не задействовав этот тип данных. При этом многие разработчики не знают некоторых нюансов связанных с этим типом. Поэтому давайте рассмотрим кое-какие особенности этого типа в .NET.
Итак, начнем с представления строк в памятиВ.NET строки располагаются согласно правилу BSTR (Basic string or binary string). Данный способ представления строковых данных используется в COM (слово basic от языка программирования VisualBasic, в котором он первоначально использовался). Как известно в C/C++ для представления строк используется PWSZ, что расшифровывается как Pointer to Wide-character String, Zero-terminated. При таком расположении в памяти в конце строки находится null-терминированный символ, по которому мы можем определить конец строки. Длина строки в PWSZ ограничена лишь объемом свободной памяти.
Так вот, строки в .NET представляются в памяти согласно правилу BSTR. В буфере находится четырехбайтовая длина строки, за которой следуют двухбайтовые символы строки в формате UTF-16, за которыми следует два нулевых байта (\u0000). Использование такой реализации имеет ряд преимуществ: длину строки не нужно пересчитывать она хранится в заголовке, строка может содержать null-символы, где угодно, и самое главное адрес строки(pinned) можно без проблем передавать в неуправляемой код там, где ожидается WCHAR*. Идем далее…
Сколько памяти занимает объект строкового типа?Мне встречались статьи где было написано, что размер строкового объекта равен size = 20 + (length/2)*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 байт соответственно.
Особенности строкИтак, мы рассмотрели, как представляются строки, и сколько на самом деле они занимают места в памяти. Теперь давайте погорим об их особенностях. Основные особенности строк в .NET:
Рассмотрим каждый пункт подробнее.
Строки - ссылочные типыСтроки являются настоящими ссылочными типами, то есть они всегда располагаются в куче. Многие путают их со значимыми типами, потому что они ведут себя также, например, они неизменяемы и их сравнение происходит по значению, а не по ссылкам, но нужно помнить, что это ссылочный тип.
Строки - неизменяемыСтроки являются неизменяемыми. Это сделано не просто так. В неизменности строк есть немало преимуществ:
Структуры данных можно разделить на два вида - эфемерные и персистентные. Эфемерными называют структуры данных, хранящие только последнюю свою версию. Персистентными называют структуры, которые сохраняют все свои предыдущие версии при изменении. Последние фактически неизменяемы, так как их операции не изменяют структуру на месте, вместо этого они возвращают новую основанную на предыдущей структуру. Учитывая, что строки неизменны, они могли бы быть и персистентными, однако таковыми не являются. В .NET строки являются эфемерными. Подробнее о том, почему это именно так можно прочитать у Эрика Липперта поссылке Для сравнения возьмем строки Java. Они являются неизменяемыми, как и в .NET, но вдобавок и персистентными. Реализация класса String в Java выглядит так:
Как видно, строки в Java занимают больше памяти, чем в .NET, так как содержат дополнительные поля, которые и позволяют им быть персистентными. Благодаря персистентости метод String.substring() в Java выполняется за O(1), так как не требует копирования строки как в .NET, где этот метод выполняется за O(n). Реализация метода String.substring() в Java:
s = ss.substring(3) можно использовать код s = new String(ss.substring(3)), который не будет хранить ссылку на массив символов исходной строки, а скопирует только реально используемую часть массива. Кстати, если этот конструктор вызывать на строке длиной равной длине массива символов, то копирования в этом случае происходить не будет, а будет использоваться ссылка на оригинальный массив. Как оказалось в последней версии Java реализация строкового типа изменилась. xonix подсказал об этом. Теперь в классе нет полей offset и length, и появился новый hash32 (с другим алгоритмом хеширования). Это означает, что строки перестали быть персистентными. Теперь метод String.substring каждый раз будет создаваться новую строку.
Строки переопределяют Object.EqualsКласс String переопределяет метод Object.Equals, в результате чего сравнение происходит не по ссылки, а по значению. Я думаю, разработчики благодарны создателям класса String за то, что они переопределили оператор ==, так как код, использующий == для сравнения строк, выглядит более изящно, нежели вызов метода.
Интернирование строкНу, и на последок поговорим об интернировании строк.
На самом деле строку можно изменить, но для этого придется прибегнуть к unsafe коду. Рассмотрим пример:
Интернирование строк - это механизм, при котором одинаковые литералы представляют собой один объект в памяти. Если не вникать глубоко в подробности, то смысл интернирования строк заключается в следующем: в рамках процесса (именно процесса, а не домена приложения) существует одна внутренняя хеш-таблица, ключами которой являются строки, а значениями - ссылки на них. Во время JIT-компиляции литеральные строки последовательно заносятся в таблицу (каждая строка в таблице встречается только один раз). На этапе выполнения ссылки на литеральные строки присваиваются из этой таблицы. Можно поместить строку во внутреннюю таблицу во время выполнения с помощью метода String.Intern. Также можно проверить, содержится ли строка во внутренней таблице с помощью метода String.IsInterned.
От интернирования строк можно отказаться, если применить специальный атрибутCompilationRelaxationsAttribute к сборке. Атрибут CompilationRelaxationsAttribute контролирует точность кода, создаваемого JIT-компилятором среды CLR. Конструктор данного атрибута принимает перечислениеCompilationRelaxations в состав, которого на текущий момент входит толькоCompilationRelaxations.NoStringInterning - что помечает сборку как не требующую интернирования. Кстати, этот атрибут не обрабатывается в .NET Framework версии 1.0., поэтому отключить интернирование по умолчанию не было возможным. Сборка mscorlib, начиная со второй версии, помечена этим атрибутом. Получается, что строки в .NET все-таки можно изменить, если очень захотеть, применяя unsafe код.
А что если без unsafe?Оказывается, изменить содержимое строки было возможно и, не прибегая к unsafe коду, воспользовавшись механизмом рефлексии. Этот трюк мог прокатить в .NET до 2.0 версии включительно, потом разработчики класса String лишили нас такой возможности.
Вопрос версий: В разных версиях .NET Framework string.Empty может интернироваться, а может, и нет.
Особенности производительностиУ интернирования есть отрицательный побочный эффект. Дело в том, что ссылка на интернированный объект String, которую хранит CLR, может сохраняться и после завершения работы приложения и даже домена приложения. Поэтому большие литеральные строки использовать не стоит или же, если это необходимо стоит отключить интернирование, применив атрибут CompilationRelaxations к сборке. |