Чому в C # вказується тип рядка, який поводиться як тип значення?


371

Рядок - це тип посилання, хоча він має більшість характеристик типу значень, таких як незмінність і перевантаження == для порівняння тексту, а не переконання, що вони посилаються на один і той же об'єкт.

Чому тоді рядок не є лише типом значення?


Оскільки для непорушних типів відмінність - це здебільшого деталізація впровадження (залишаючи isтести осторонь), відповідь, ймовірно, "з історичних причин". Виконання копіювання не може бути причиною, оскільки немає необхідності фізично копіювати незмінні об’єкти. Тепер неможливо змінити без порушення коду, який фактично використовує isчеки (або подібні обмеження).
Елазар

BTW - це та сама відповідь для C ++ (хоча різниця між значеннями та типовими типами не є явною в мові), рішення зробити std::stringтак, як колекція - стара помилка, яку зараз неможливо виправити.
Елазар

Відповіді:


333

Рядки не є типовими значеннями, оскільки вони можуть бути величезними, і їх потрібно зберігати у купі. Типи значень (у всіх реалізаціях CLR досі) зберігаються у стеці. Виділення рядків у стеку може порушити всілякі речі: стек становить лише 1 Мб для 32-бітових і 4 Мб для 64-бітових, вам потрібно буде встановити в поле кожну рядок, понесла покарання за копію, ви не змогли інтернувати рядки та використання пам'яті. повітряна куля тощо.

(Редагувати. Додано роз'яснення про те, що сховище значень типу є деталізацією реалізації, що призводить до такої ситуації, коли у нас є тип із значеннями сематики, що не успадковується від System.ValueType. Дякую Бен.)


75
Я тут забиваю, але тільки тому, що це дає мені можливість посилання на публікацію в блозі, що стосується питання: типи значень не обов'язково зберігаються у стеку. Найчастіше це стосується ms.net, але зовсім не визначено специфікацією CLI. Основна відмінність між значеннями та типовими типами полягає в тому, що типи посилань дотримуються семантики копіювання за значенням. Дивіться blogs.msdn.com/ericlippert/archive/2009/04/27/… та blogs.msdn.com/ericlippert/archive/2009/05/04/…
Бен Швен

8
@Qwertie: Stringне є змінним розміром. Коли ви додаєте до нього, ви фактично створюєте інший Stringоб’єкт, виділяючи для нього нову пам'ять.
codekaizen

5
Якщо сказати, рядок теоретично може бути типом значення (структура), але "значення" було б не що інше, як посилання на рядок. Дизайнери .NET, природно, вирішили вирізати посередника (обробка структури була неефективна в .NET 1.0, і було природно слідувати Java, в якій рядки вже були визначені як опорний, а не примітивний тип. Плюс, якщо рядок був тип значення, а потім перетворення його в об'єкт вимагає, щоб його було встановлено в полі, непотрібна неефективність).
Qwertie

7
@codekaizen Qwertie має рацію, але я думаю, що формулювання було заплутаним. Один рядок може мати інший розмір, ніж інший рядок, тому, на відміну від істинного типу значення, компілятор не міг заздалегідь знати, скільки місця виділити для зберігання значення рядка. Наприклад, an Int32- це завжди 4 байти, тому компілятор виділяє 4 байти в будь-який час, коли ви визначаєте змінну рядка. Скільки пам'яті повинен виділити компілятор, коли він зіштовхується зі intзмінною (якщо це був тип значення)? Зрозумійте, що значення ще не було призначено.
Кевін Брок

2
Вибачте, помилка друку в моєму коментарі, яку я зараз не можу виправити; це повинно було бути. Наприклад, a Int32- це завжди 4 байти, тому компілятор виділяє 4 байти в будь-який час, коли ви визначаєте intзмінну. Скільки пам'яті повинен виділити компілятор, коли він зіштовхується зі stringзмінною (якщо це був тип значення)? Зрозумійте, що значення ще не було призначено.
Кевін Брок

57

Це не тип значення, оскільки продуктивність (простір та час!) Була б жахливою, якби це тип значення, і його значення доводилося копіювати кожного разу, коли воно передавались та поверталися з методів тощо.

Він має ціннісну семантику, щоб підтримувати здоровий світ. Ви можете собі уявити, як важко було б кодувати, якби

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

налаштований bбути false? Уявіть, як складно буде кодування майже будь-якої програми.


44
Ява не відома тим, що була жалюгідною.
Jason

3
@Matt: точно. Коли я перейшов на C #, це було дещо заплутаним, оскільки я завжди використовував (досі іноді) .equals (..) для порівняння рядків, тоді як мої товариші по команді просто використовували "==". Я ніколи не розумів, чому вони не залишили "==" для порівняння посилань, хоча якщо ви думаєте, 90% часу ви, ймовірно, захочете порівнювати вміст, а не посилання на рядки.
Юрі

7
@Juri: Насправді я думаю, що ніколи не бажано перевіряти посилання, оскільки іноді new String("foo");та інше new String("foo")може оцінювати в тій самій довідці, який тип - це не те, що ви очікували б зробити newоператора. (Або ви можете сказати мені випадок, коли я хотів би порівняти посилання?)
Майкл

1
@Michael Ну, ви повинні включити порівняльне посилання у всіх порівняннях, щоб знайти порівняння з null. Ще одне хороше місце для порівняння посилань із рядками - це порівняння, а не порівняння рівності. Дві еквівалентні рядки в порівнянні мають повернути 0. Перевірка цього випадку хоч і займає стільки часу, скільки пройдеться через все порівняння, так що це не є корисним скороченням. Перевірка на предмет ReferenceEquals(x, y)- це швидкий тест, і ви можете повернути 0 негайно, а при змішуванні з вашим нульовим тестом навіть не додасть більше роботи.
Джон Ханна

1
... мати рядки як тип значення цього стилю, а не бути типом класу, означає, що значення за замовчуванням a stringможе поводитись як порожня рядок (як це було в системах pre-.net), а не як нульове посилання. Насправді моїм власним уподобанням було б мати тип значення, Stringякий містив тип посилання NullableString, причому перше має значення за замовчуванням, еквівалентне, String.Emptyа друге - за замовчуванням null, та спеціальні правила боксу / розпакування (такі, що бокс - оцінено NullableString, посилається на String.Empty).
supercat

26

Відмінність типів відліку та типів значень в основному є компромісом продуктивності мови. Довідкові типи мають певні накладні витрати на будівництво та знищення та вивезення сміття, оскільки вони створюються на купі. Типи значень, з іншого боку, мають накладні витрати на виклики методів (якщо розмір даних більший за покажчик), оскільки весь об'єкт скопійований, а не лише вказівник. Оскільки рядки можуть бути (і зазвичай є) набагато більшими за розмір вказівника, вони розроблені як еталонні типи. Крім того, як Серві вказував, розмір типу значення повинен бути відомий під час компіляції, що не завжди стосується рядків.

Питання про незмінність - окреме питання. Як типи посилань, так і типи значень можуть бути або змінними, або незмінними. Типи значень, як правило, незмінні, оскільки семантика типів змінних значень може бути заплутаною.

Довідкові типи, як правило, змінюються, але можуть бути розроблені як незмінні, якщо це має сенс. Рядки визначаються як незмінні, оскільки це робить можливими певні оптимізації. Наприклад, якщо один і той же рядковий літерал трапляється кілька разів в одній програмі (що досить часто), компілятор може повторно використовувати той самий об'єкт.

Так чому "==" перевантажений для порівняння рядків за текстом? Тому що це найкорисніша семантика. Якщо два рядки однакові за текстом, вони можуть бути або не бути однаковими посиланнями на об'єкт через оптимізацію. Тому порівнювати посилання досить марно, тоді як порівнювати текст - це майже завжди те, що ви хочете.

Якщо говорити загальніше, Strings має те, що називається семантикою значення . Це більш загальне поняття, ніж типи значень, що є специфікою C # для реалізації. Типи значень мають значення семантики значень, але типи посилань також можуть мати значення семантики. Коли тип має семантику значень, ви не можете реально визначити, чи є базовою реалізацією тип опорного типу або тип значення, тому ви можете врахувати цю деталь реалізації.


Відмінність між типами значень та типовими типами насправді взагалі не стосується продуктивності. Йдеться про те, чи містить змінна фактичний об'єкт чи посилання на об'єкт. Рядок ніколи не може бути типом значення, оскільки розмір рядка є змінним; вона повинна бути постійною, щоб бути типом значення; виступ майже нічого спільного з цим. Довідкові типи також взагалі недешеві.
Сервіс

2
@Sevy: розмір рядка є постійним.
ЖакБ

Тому що він просто містить посилання на символьний масив, який має змінний розмір. Наявність типу значення, у якого єдине реальне "значення" було еталонним типом, було б ще більше заплутаним, оскільки воно все ще матиме еталонну семантику для всіх інтенсивних цілей.
Сервіс

1
@Sevy: розмір масиву є постійним.
ЖакБ

1
Після того як ви створили масив, його розмір стає постійним, але всі масиви в усьому світі не всі мають однаковий розмір. Це моя суть. Щоб рядок був типом значення, всі існуючі рядки повинні мати абсолютно однаковий розмір, тому що так розроблені типи значень у .NET. Потрібно мати можливість резервувати місце для зберігання таких типів значень, перш ніж воно фактично має значення , тому розмір повинен знати про час компіляції . Такий stringтип повинен мати буфер символів певного фіксованого розміру, який був би як обмежувальним, так і вкрай неефективним.
Сервіс

16

Це пізня відповідь на старе запитання, але у всіх інших відповідях відсутня суть, а саме те, що .NET не мав загальної інформації до .NET 2.0 у 2005 році.

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

Збереження типу значення в колекції, що не є загальним, потребує спеціального перетворення у тип, objectякий називається бокс. Коли CLR вказує тип значення, він загортає значення всередині a System.Objectі зберігає його в керованій купі.

Читання значення з колекції вимагає зворотної операції, яку називають розпакуванням.

І бокс, і розгортання мають незначну вартість: бокс вимагає додаткового розподілу, для розпакування необхідна перевірка типу.

Деякі відповіді неправильно стверджують, що stringніколи не могли бути реалізовані як тип значення, оскільки його розмір є змінним. Насправді легко реалізувати рядок як структуру даних фіксованої довжини за допомогою стратегії оптимізації малих рядків: рядки зберігатимуться в пам'яті безпосередньо як послідовність символів Unicode, за винятком великих рядків, які зберігатимуться як вказівник на зовнішній буфер. Обидва зображення можуть бути розроблені так, щоб вони мали однакову фіксовану довжину, тобто розмір вказівника.

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


Моя, дякую за цю відповідь! Я дивився на всі інші відповіді, в яких говорив про розподіл купи та стека, тоді як стек - це деталь реалізації . Зрештою, він stringмістить лише його розмір і вказівник на charмасив у будь-якому випадку, тому він не буде "величезним типом значення". Але це проста, відповідна причина цього дизайнерського рішення. Дякую!
V0ldek

8

Не тільки рядки є незмінними еталонними типами. Делегати з кількома ролями теж. Ось чому це безпечно писати

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

Я вважаю, що рядки незмінні, оскільки це найбезпечніший метод роботи з ними та розподілення пам'яті. Чому вони не є типовими значеннями? Попередні автори мають рацію щодо розміру стека і т. Д. Я також додам, що створення рядків типу посилань дозволяє заощадити на розмірі збірки, коли ви використовуєте ту саму постійну рядок у програмі. Якщо ви визначаєте

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

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

Якщо ви хочете керувати рядками типу звичайного довідкового типу, покладіть рядок всередину нового StringBuilder (string s). Або використовувати MemoryStreams.

Якщо ви маєте створити бібліотеку, де ви очікуєте, що у ваших функціях буде передано величезний рядок, або визначте параметр як StringBuilder або як Stream.


1
Існує маса прикладів незмінних референтних типів. І повторимо рядковий приклад, що справді майже гарантується за нинішніх реалізацій - технічно це за модуль (а не за збірку) - але це майже завжди те саме ...
Марк Гравелл

5
Останній пункт: StringBuilder не допомагає, якщо ви намагаєтеся пропустити велику рядок (оскільки вона все-таки реалізована як рядок) - StringBuilder корисний для маніпулювання рядком кілька разів.
Марк Гравелл

Ви мали на увазі обробника делегата, а не hadler? (вибачте, що вибагливий, але це дуже близько до (не поширеного) прізвища, яке я знаю ....)
Pure.Krome

6

Також реалізуються способи рядків (різні для кожної платформи) і коли ви почнете їх зшивати. Як і використання StringBuilder. Він виділяє буфер для копіювання, як тільки ви досягнете мети, він виділяє ще більше пам’яті для вас, сподіваючись, що якщо ви зробите велику конкатенацію, ефективність не буде перешкоджати.

Може, Джон Скіт може допомогти тут?


5

В основному це питання ефективності.

Якщо рядки поводяться як тип значення LIKE, це допомагає при написанні коду, але, якщо це буде значення типу BE, це призвело б до величезного успіху в продуктивності.

Щоб поглибити погляд, погляньте на приємну статтю про рядки в .net-рамках.


3

Дуже простими словами будь-яке значення, яке має певний розмір, може трактуватися як тип значення.


Це має бути коментар
ρyaσѕρєя K

простіше зрозуміти для ppl new to c #
ДЛЯ

2

Як ви можете сказати string, що це еталонний тип? Я не впевнений, що має значення, як це реалізується. Рядки в C # незмінні саме тому, що вам не доведеться турбуватися з цього приводу.


Це тип посилання (я вважаю), тому що він не походить від System.ValueType із зауважень MSDN щодо System.ValueType: типи даних розділяються на типи значень та типи посилань. Типи значень або виділяються стеком, або розподіляються вбудованими в структуру. Довідкові типи виділяються купу.
Davy8

І тип посилань, і значення походять від кінцевого базового класу Object. У випадках, коли тип значення повинен вести себе як об'єкт, обгортка, яка робить тип значення схожим на опорний об'єкт, виділяється на купу, а значення типу значення копіюється в нього.
Davy8

Обгортка позначена таким чином, що система знає, що вона містить тип значення. Цей процес відомий як бокс, а зворотний процес відомий як розпакування. Бокс та розпакування дозволяють будь-якому типу трактуватись як об'єкт. (На задньому сайті, напевно, слід було щойно пов’язати зі статтею.)
Davy8,

2

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

Струни незмінні з дуже вагомих причин, це не має нічого спільного з тим, що він є еталонним типом, але має багато спільного з управлінням пам'яттю. Просто ефективніше створити новий об’єкт при зміні розміру рядка, ніж для переміщення речей на керованій купі. Я думаю, ви поєднуєте цінні / референтні типи та незмінні поняття об'єктів.

Щодо "==": Як ви сказали, "==" - це перевантаження оператора, і знову це було реалізовано з дуже вагомої причини, щоб зробити рамку більш корисною при роботі з рядками.


Я усвідомлюю, що типи значень за визначенням не є незмінними, але, здається, більшість найкращих практик свідчать про те, що вони повинні бути при створенні власних. Я сказав характеристики, а не властивості типів значень, що для мене означає, що часто цінні типи демонструють такі, але не обов'язково за визначенням
Davy8,

5
@WebMatrix, @ Davy8: Примітивні типи (int, double, bool, ...) незмінні.
Ясон

1
@Jason, я вважав, що незмінний термін в основному застосовується до об'єктів (типи посилань), які не можуть змінюватися після ініціалізації, як рядки, коли змінюється значення рядків, всередині створюється новий екземпляр рядка, а вихідний об'єкт залишається незмінним. Як це стосується типів значень?
WebMatrix

8
Так чи інакше, в "int n = 4; n = 9;" не те, що ваша змінна int є "незмінною", у значенні "константа"; це те, що значення 4 незмінне, воно не змінюється на 9. Ваша змінна int "n" спочатку має значення 4, а потім інше значення 9; але самі значення незмінні. Чесно кажучи, мені це дуже близько до wtf.
Даніель Даранас

1
+1. Мені не подобається чути це "рядки - це як типи значень", коли вони просто не є.
Джон Ханна

1

Це не так просто, як рядки складаються з масивів символів. Я дивлюся на рядки як масиви символів []. Тому вони знаходяться в купі, оскільки місце розташування опорної пам'яті зберігається в стеці і вказує на початок розташування пам'яті масиву в купі. Розмір рядка не відомий до його виділення ... ідеально підходить для купи.

Ось чому рядок дійсно незмінний, оскільки коли ви змінюєте його, навіть якщо він однакового розміру, компілятор цього не знає і повинен виділяти новий масив та призначати символи позиціям масиву. Це має сенс, якщо ви думаєте про струни як про спосіб, який мови захищає вас від необхідності виділяти пам'ять під час руху (читайте програму C як програмування)


1
"розмір рядка не відомий до його виділення" - це неправильно в CLR.
codekaizen

-1

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

Примітивні типи з плаваючою комою обробляються ФПУ, що має 80 біт.

Це все було вирішено задовго до того, як з'явилася мова OOP, щоб придушити визначення примітивного типу, і я припускаю, що тип значень - це термін, створений спеціально для мов OOP.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.