В C # почему String является ссылочным типом, который ведет себя как тип значения?

371

String - это ссылочный тип, даже несмотря на то, что он имеет большинство характеристик типа значения, таких как неизменяемость и перегруженность == для сравнения текста вместо того, чтобы быть уверенным, что они ссылаются на один и тот же объект.

Почему тогда строка не просто тип значения?

Davy8
источник
Поскольку для неизменяемых типов это различие в большей степени зависит от реализации (оставляя isтесты в стороне), ответ, вероятно, «по историческим причинам». Производительность копирования не может быть причиной, поскольку нет необходимости физически копировать неизменяемые объекты. Теперь это невозможно изменить, не нарушая код, который фактически использует isпроверки (или подобные ограничения).
Эльазар
Кстати, это тот же самый ответ для C ++ (хотя различие между типами value и reference не выражено в языке явно), решение сделать std::stringповедение, похожее на коллекцию, является старой ошибкой, которую сейчас нельзя исправить.
Эльазар

Ответы:

333

Строки не являются типами значений, поскольку они могут быть огромными и должны храниться в куче. Типы значений (во всех реализациях CLR на данный момент) хранятся в стеке. Строки, выделяющие стек, могут сломать все виды вещей: размер стека составляет всего 1 МБ для 32-разрядных и 4 МБ для 64-разрядных, вам нужно будет упаковать каждую строку, что приведет к штрафу за копирование, невозможность интернировать строки и использование памяти будет шар, и т.д ...

(Редактировать: Добавлено пояснение о том, что хранилище типов значений является подробностью реализации, что приводит к такой ситуации, когда у нас есть тип со значением sematics, не наследуемый от System.ValueType. Спасибо, Бен.)

codekaizen
источник
75
Я придираюсь здесь, но только потому, что это дает мне возможность сделать ссылку на пост в блоге, относящийся к вопросу: типы значений не обязательно хранятся в стеке. Чаще всего это верно для ms.net, но совсем не указано в спецификации CLI. Основное различие между значениями и ссылочными типами заключается в том, что ссылочные типы следуют семантике копирования по значению. См blogs.msdn.com/ericlippert/archive/2009/04/27/... и blogs.msdn.com/ericlippert/archive/2009/05/04/...
Бен Schwehn
8
@Qwertie: Stringэто не переменный размер. Когда вы добавляете к нему, вы фактически создаете другой Stringобъект, выделяя для него новую память.
Codekaizen
5
Тем не менее, строка, теоретически, могла бы быть типом значения (структурой), но «значение» было бы не чем иным, как ссылкой на строку. Разработчики .NET, естественно, решили отказаться от посредника (обработка структуры была неэффективна в .NET 1.0, и было естественно следовать Java, в котором строки уже были определены как ссылочный, а не примитивный тип. Плюс, если строка была тип значения, тогда преобразовывающий это в объект, потребовал бы, чтобы это было упаковано, ненужная неэффективность).
Qwertie
7
@codekaizen Qwertie прав, но я думаю, что формулировка была запутанной. Одна строка может иметь размер, отличный от другой строки, и, таким образом, в отличие от типа истинного значения, компилятор не мог заранее знать, сколько места нужно выделить для хранения значения строки. Например, Int32всегда 4 байта, таким образом компилятор выделяет 4 байта каждый раз, когда вы определяете строковую переменную. Сколько памяти должен выделять компилятор, когда он встречает intпеременную (если это был тип значения)? Поймите, что значение еще не было назначено в то время.
Кевин Брок
2
Извините, опечатка в моем комментарии, которую я не могу исправить сейчас; это должно было быть .... Например, Int32всегда 4 байта, таким образом, компилятор выделяет 4 байта каждый раз, когда вы определяете intпеременную. Сколько памяти должен выделять компилятор, когда он встречает stringпеременную (если это был тип значения)? Поймите, что значение еще не было назначено в то время.
Кевин Брок
57

Это не тип значения, потому что производительность (пространство и время!) Была бы ужасной, если бы это был тип значения, и его значение приходилось копировать каждый раз, когда оно передавалось и возвращалось из методов и т. Д.

У него есть ценностная семантика, чтобы держать мир в здравом уме. Можете ли вы представить, как трудно было бы кодировать, если

string s = "hello";
string t = "hello";
bool b = (s == t);

установить bбыть false? Представьте себе, насколько сложным будет кодирование практически любого приложения.

Ясон
источник
44
Ява не известна как содержательная.
Джейсон
3
@Matt: точно. Когда я переключился на C #, это было немного странно, так как я всегда использовал (иногда делаю) .equals (..) для сравнения строк, в то время как мои товарищи по команде просто использовали "==". Я никогда не понимал, почему они не оставили «==» для сравнения ссылок, хотя, если вы думаете, 90% времени вы, вероятно, захотите сравнить контент, а не ссылки на строки.
Юрий
7
@Juri: На самом деле я думаю, что никогда не желательно проверять ссылки, так как иногда new String("foo");и другие new String("foo")могут оценивать по одной и той же ссылке, а это не то, что вы ожидаете от newоператора. (Или вы можете рассказать мне случай, когда я хотел бы сравнить ссылки?)
Майкл
1
@Michael Ну, вы должны включить эталонное сравнение во все сравнения, чтобы поймать сравнение с нулем. Другое хорошее место для сравнения ссылок со строками - это сравнение, а не сравнение на равенство. Две эквивалентные строки при сравнении должны возвращать 0. Проверка этого случая, тем не менее, занимает столько же времени, сколько и полное сравнение, поэтому не является полезным сокращением. Проверка ReferenceEquals(x, y)является быстрым тестом, и вы можете немедленно вернуть 0, а при смешивании с нулевым тестом даже не добавляете больше работы.
Джон Ханна
1
... наличие строк как типа значения этого стиля, а не типа класса, будет означать, что значение по умолчанию для a stringможет вести себя как пустая строка (как это было в системах до .NET), а не как нулевая ссылка. На самом деле, я бы предпочел иметь тип значения, Stringкоторый содержал бы ссылочный тип NullableString, причем первый имел значение по умолчанию, эквивалентное второму, String.Emptyа второй имел значение по умолчанию null, и со специальными правилами упаковки / распаковки (такими, чтобы бокс по умолчанию оценен NullableStringдаст ссылку на String.Empty).
суперкат
26

Различие между ссылочными типами и типами значений в основном является компромиссом производительности при разработке языка. У ссылочных типов есть некоторые накладные расходы на создание, уничтожение и сборку мусора, поскольку они создаются в куче. С другой стороны, типы значений накладываются на вызовы методов (если размер данных больше, чем указатель), потому что весь объект копируется, а не только указатель. Поскольку строки могут быть (и обычно имеют размер) намного больше, чем размер указателя, они разработаны как ссылочные типы. Кроме того, как указал Servy, размер типа значения должен быть известен во время компиляции, что не всегда имеет место для строк.

Вопрос изменчивости является отдельной проблемой. И ссылочные типы, и типы значений могут быть изменяемыми или неизменяемыми. Типы значений обычно являются неизменяемыми, поскольку семантика для изменяемых типов значений может сбивать с толку.

Ссылочные типы, как правило, являются изменяемыми, но могут быть разработаны как неизменяемые, если это имеет смысл. Строки определены как неизменяемые, потому что это делает возможной определенную оптимизацию. Например, если один и тот же строковый литерал встречается несколько раз в одной и той же программе (что довольно часто встречается), компилятор может повторно использовать один и тот же объект.

Так почему же "==" перегружено для сравнения строк по тексту? Потому что это самая полезная семантика. Если две строки равны по тексту, они могут или не могут быть одной и той же ссылкой на объект из-за оптимизации. Таким образом, сравнение ссылок довольно бесполезно, а сравнение текста - почти всегда то, что вы хотите.

Говоря более широко, Strings имеет то, что называется семантикой значения . Это более общая концепция, чем типы значений, что является специфической реализацией C #. Типы значений имеют семантику значений, но ссылочные типы также могут иметь семантику значений. Когда тип имеет семантику значений, вы не можете точно сказать, является ли базовая реализация ссылочным типом или типом значения, поэтому вы можете считать это реализацией.

JacquesB
источник
Различие между типами значений и ссылочными типами вообще не касается производительности. Речь идет о том, содержит ли переменная фактический объект или ссылку на объект. Строка никогда не может быть типом значения, потому что размер строки является переменным; он должен быть постоянным, чтобы быть типом значения; производительность почти не имеет к этому никакого отношения. Ссылочные типы также не дороги в создании вообще.
Servy
2
@Sevy: Размер строки является постоянной.
JacquesB
Потому что он просто содержит ссылку на массив символов, который имеет переменный размер. Наличие типа значения, единственным реальным «значением» которого является ссылочный тип, будет просто еще более запутанным, поскольку у него все еще будет семантика ссылок для всех интенсивных целей.
Servy
1
@Sevy: размер массива постоянен.
JacquesB
1
После того, как вы создали массив, его размер остается постоянным, но все массивы во всем мире не имеют абсолютно одинаковый размер. Это моя точка зрения. Чтобы строка была типом значения, все существующие строки должны иметь одинаковый размер, потому что именно так типы значений создаются в .NET. Необходимо иметь возможность зарезервировать место для хранения для таких типов значений, прежде чем они будут иметь фактическое значение , поэтому размер должен быть известен во время компиляции . Такой stringтип должен был бы иметь буфер символов некоторого фиксированного размера, который был бы как ограничительным, так и крайне неэффективным.
Обслуживание
16

Это поздний ответ на старый вопрос, но во всех остальных ответах не хватает смысла, заключающегося в том, что в .NET не было дженериков до .NET 2.0 в 2005 году.

Stringявляется ссылочным типом, а не типом значения, потому что для Microsoft было крайне важно обеспечить, чтобы строки могли храниться наиболее эффективным способом в неуниверсальных коллекциях , таких какSystem.Collections.ArrayList .

Хранение типа значения в неуниверсальной коллекции требует специального преобразования в тип, objectкоторый называется боксом. Когда CLR упаковывает тип значения, он оборачивает значение внутриSystem.Object и сохраняет его в управляемой куче.

Чтение значения из коллекции требует обратной операции, которая называется распаковкой.

И бокс, и распаковка имеют немаловажную стоимость: для бокса требуется дополнительное выделение, для распаковки требуется проверка типов.

Некоторые ответы неправильно утверждают, что string никогда не могли быть реализованы как тип значения, потому что его размер является переменным. На самом деле, легко реализовать строку как структуру данных фиксированной длины, используя стратегию оптимизации небольших строк: строки будут храниться в памяти непосредственно как последовательность символов Юникода, за исключением больших строк, которые будут храниться в виде указателя на внешний буфер. Оба представления могут иметь одинаковую фиксированную длину, то есть размер указателя.

Если бы дженерики существовали с первого дня, я думаю, что иметь строку в качестве типа значения было бы лучшим решением, с более простой семантикой, лучшим использованием памяти и лучшей локализацией кэша. A , List<string>содержащие лишь небольшие струны могли бы быть один непрерывный блок памяти.

ZunTzu
источник
Спасибо за ответ! Я рассмотрел все остальные ответы, в которых говорится о распределении кучи и стека, а стек - это деталь реализации . В конце концов, stringв charлюбом случае содержит только его размер и указатель на массив, так что это не будет «тип с огромным значением». Но это простая, уместная причина для этого дизайнерского решения. Спасибо!
V0ldek
8

Не только строки являются неизменяемыми ссылочными типами. Многоадресные делегаты тоже. Вот почему это безопасно писать

protected void OnMyEventHandler()
{
     delegate handler = this.MyEventHandler;
     if (null != handler)
     {
        handler(this, new EventArgs());
     }
}

Я предполагаю, что строки являются неизменяемыми, потому что это самый безопасный метод для работы с ними и выделения памяти. Почему они не являются типами Value? Предыдущие авторы правы в отношении размера стека и т. Д. Я также добавил бы, что создание строк ссылочных типов позволяет сэкономить на размере сборки, когда вы используете ту же самую константную строку в программе. Если вы определите

string s1 = "my string";
//some code here
string s2 = "my string";

Скорее всего, оба экземпляра константы "моя строка" будут размещены в вашей сборке только один раз.

Если вы хотите управлять строками, как обычными ссылочными типами, поместите строку в новый StringBuilder (string s). Или используйте MemoryStreams.

Если вы хотите создать библиотеку, в которой вы ожидаете, что в ваши функции будут переданы огромные строки, либо определите параметр как StringBuilder или как Stream.

Bogdan_Ch
источник
1
Существует множество примеров неизменяемых ссылочных типов. И пример строки, который действительно в значительной степени гарантирован в текущих реализациях - технически это для каждого модуля (не для сборки) - но это почти всегда одно и то же ...
Марк Гравелл
5
В заключение: StringBuilder не помогает, если вы пытаетесь передать большую строку (поскольку она в любом случае реализована как строка) - StringBuilder полезен для многократного манипулирования строкой.
Марк Гравелл
Ты имел в виду делегат обработчик, а не хадлер? (извините за придирчивость ... но это очень близко к (не распространенной) фамилии, которую я знаю ....)
Pure.Krome
6

Кроме того, как реализованы строки (разные для каждой платформы) и когда вы начинаете соединять их вместе. Как с помощьюStringBuilder . Он выделяет буфер для вас, чтобы скопировать в него, как только вы достигнете конца, он выделяет еще больше памяти для вас, в надежде, что если вы сделаете большую конкатенацию, производительность не будет ограничена.

Может быть, Джон Скит может помочь здесь?

Крис
источник
5

В основном это проблема производительности.

Вести поведение строк LIKE-тип значения помогает при написании кода, но наличие БИ-типа значения может привести к значительному снижению производительности.

Для более детального изучения взгляните на хорошую статью о строках в .net framework.

Денис Троллер
источник
3

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

saurav.net
источник
Это должен быть комментарий
ρяσѕρєя K
легче понять для людей, впервые в C #
Длительный
2

Как вы можете определить stringтип ссылки? Я не уверен, что это важно, как это реализовано. Строки в C # являются неизменяемыми, поэтому вам не нужно беспокоиться об этой проблеме.


источник
Это ссылочный тип (я полагаю), потому что он не является производным от System.ValueType из MSDN Замечания по System.ValueType: Типы данных разделены на типы значений и ссылочные типы. Типы значений либо распределяются по стеку, либо распределяются внутри структуры. Типы ссылок выделяются в куче.
Davy8
Типы ссылок и значений являются производными от базового базового класса Object. В тех случаях, когда необходимо, чтобы тип значения вел себя как объект, в куче выделяется оболочка, которая делает тип значения похожим на ссылочный объект, и значение типа значения копируется в него.
Davy8
Оболочка помечена, чтобы система знала, что она содержит тип значения. Этот процесс известен как бокс, а обратный процесс известен как распаковка. Упаковка и распаковка позволяют рассматривать любой тип как объект. (На заднем сайте, наверное, следовало бы просто дать
ссылку
2

На самом деле строки очень мало похожи на типы значений. Начнем с того, что не все типы значений являются неизменяемыми, вы можете изменить значение типа Int32 так, как вам будет угодно, и это будет тот же адрес в стеке.

Строки являются неизменяемыми по очень хорошей причине, они не имеют ничего общего с ссылочным типом, но имеют много общего с управлением памятью. Просто более эффективно создавать новый объект при изменении размера строки, чем перемещать объекты в управляемой куче. Я думаю, что вы смешиваете значения / ссылочные типы и понятия неизменных объектов.

Что касается "==": Как вы сказали, "==" - это перегрузка оператора, и снова это было реализовано по очень веской причине, чтобы сделать среду более полезной при работе со строками.

WebMatrix
источник
Я понимаю, что типы значений по определению не являются неизменяемыми, но, как представляется, большинство лучших практик предлагают их при создании своих собственных. Я сказал характеристики, а не свойства типов значений, что для меня означает, что часто типы значений демонстрируют их, но не обязательно по определению
Davy8
5
@WebMatrix, @ Davy8: примитивные типы (int, double, bool, ...) являются неизменяемыми.
Джейсон
1
@ Джейсон, я думал, что неизменяемый термин в основном применяется к объектам (ссылочным типам), которые не могут изменяться после инициализации, например, строки, когда изменяется значение строки, внутренне создается новый экземпляр строки, а исходный объект остается неизменным. Как это относится к типам значений?
WebMatrix
8
Каким-то образом в «int n = 4; n = 9;» ваша переменная int не является «неизменяемой» в смысле «константы»; это то, что значение 4 является неизменным, оно не меняется на 9. Сначала ваша переменная int "n" имеет значение 4, а затем другое значение, 9; но сами ценности неизменны. Честно говоря, мне это очень близко к ВТФ.
Даниэль Даранас
1
+1. Мне надоело слышать, что «строки похожи на типы значений», когда их просто нет.
Джон Ханна
1

Не так просто, как строки состоят из массивов символов. Я смотрю на строки как на символьные массивы []. Поэтому они находятся в куче, поскольку эталонная ячейка памяти хранится в стеке и указывает на начало ячейки памяти массива в куче. Размер строки неизвестен до ее выделения ... идеально подходит для кучи.

Вот почему строка действительно неизменна, потому что при ее изменении, даже если она имеет одинаковый размер, компилятор не знает об этом и должен выделить новый массив и назначить символы позициям в массиве. Это имеет смысл, если вы думаете о строках как о способе, которым языки защищают вас от необходимости выделять память на лету (читайте C как программирование)

BionicCyborg
источник
1
«Размер строки неизвестен до ее выделения» - это неверно в CLR.
Codekaizen
-1

Риск получить еще одно загадочное отрицательное голосование ... тот факт, что многие упоминают стек и память относительно типов значений и примитивных типов, заключается в том, что они должны вписываться в регистр в микропроцессоре. Вы не можете вытолкнуть или вытолкнуть что-либо в / из стека, если для этого требуется больше битов, чем в регистре ... инструкции, например, «pop eax» - потому что eax имеет ширину 32 бита в 32-битной системе.

Типы примитивов с плавающей точкой обрабатываются FPU, ширина которого составляет 80 бит.

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

jinzai
источник