Чому значення за замовчуванням типу рядка null замість порожнього рядка?


218

Це дуже дратує , щоб перевірити всі мої рядки для nullперш , ніж я можу сміливо застосовувати такі методи , як ToUpper(), і StartWith()т.д ...

Якщо значення за замовчуванням stringбули порожній рядок, я не мав би тест, і я відчуваю , що це буде більш сумісним з іншими типами значень , як intі double, наприклад. Додатково Nullable<String>було б сенс.

То чому ж дизайнери C # вирішили використовувати nullяк значення за замовчуванням рядки?

Примітка. Це стосується цього питання , але більше зосереджено на тому, чому замість того, що з ним робити.


53
Чи вважаєте ви це проблемою для інших еталонних типів?
Джон Скіт

17
@JonSkeet Ні, але тільки тому, що я спочатку, помилково, вважав, що рядки є типовими значеннями.
Марсель

21
@Marcel: Це досить вагомий привід для роздумів про це.
TJ Crowder

7
@JonSkeet Так. О, так. (Але ви не чужий для дискусії, що не стосується нульового типу…)
Конрад Рудольф

7
Я вважаю, що ви мали б набагато кращий час, якби ви використовували твердження на своїх рядках у місцях, де ви очікуєте, що їх НЕ буде null(а також я рекомендую концептуально ставитися до nullпорожніх рядків як до різних речей). Нульове значення може бути десь результатом помилки, тоді як порожній рядок повинен мати інше значення.
diegoreymendez

Відповіді:


312

Чому значення за замовчуванням типу рядка null замість порожнього рядка?

Тому що stringце тип посилання, а значенням за замовчуванням для всіх типів посилань є null.

Досить прикро перевіряти всі мої рядки на нуль, перш ніж я можу сміливо застосовувати такі методи, як ToUpper (), StartWith () тощо ...

Це відповідає поведінці референтних типів. Перш ніж викликати членів своїх екземплярів, слід поставити чек на нульову посилання.

Якщо за замовчуванням значення рядка було порожнім рядком, мені не доведеться тестувати, і я вважаю, що воно буде більш узгоджуватись з іншими типами значень, наприклад, наприклад, int або double.

Присвоєння значення за замовчуванням для певного типу посилання, крім nullзробив би непослідовним .

Додатково Nullable<String>було б сенс.

Nullable<T>працює з типами значень. Слід зазначити той факт, що Nullableвін не був представлений на оригінальній платформі .NET, тому було б багато порушеного коду, якби вони змінили це правило. ( Courtesy @jcolebrand )


10
@HenkHolterman Можна реалізувати цілу кількість речей, але навіщо вводити таку яскраву невідповідність?

4
@delnan - "чому" було тут питанням.
Хенк Холтерман

8
@HenkHolterman І "Узгодженість" є спростуванням до вашої точки "рядок може бути оброблений на відміну від інших типів посилань".

6
@delnan: Працюючи над мовою, яка розглядає рядок як типи цінності та працює 2+ років на dotnet, я погоджуюся з Henk. Я розглядаю це як основну ПОЛУ в dotnet .
Fabricio Araujo

1
@delnan: Можна створити тип значення, який поводився по суті як String, за винятком (1) поведінки типу-типу, що має корисне значення за замовчуванням, і (2) нещасного додаткового шару непрямості боксу в будь-який час, коли він був переданий Object. Зважаючи на те, що представлення купи stringунікальне, спеціальне лікування, щоб уникнути зайвого боксу, не було б великим розтягненням (насправді, можливість встановити поведінку боксу за замовчуванням було б хорошою справою і для інших типів).
supercat

40

Хабіб має рацію - тому що stringце еталонний тип.

Але що ще важливіше, вам не потрібно перевіряти nullкожен раз, коли ви його використовуєте. Ви, мабуть, повинні кинути, ArgumentNullExceptionякщо хтось передає вашій функції nullпосилання.

Ось у чому річ - рамка все одно кине NullReferenceExceptionдля вас, якби ви спробували зателефонувати .ToUpper()на рядок. Пам'ятайте, що цей випадок все-таки може статися, навіть якщо ви перевірите свої аргументи, nullоскільки будь-яке властивість або метод на об'єктах, переданих вашій функції, як параметри можуть оцінюватися null.

Як сказано, перевірка на наявність порожніх рядків чи нулів - це звичайна річ, тому вони передбачають String.IsNullOrEmpty()і саме String.IsNullOrWhiteSpace()для цього.


30
Ніколи не слід кидати NullReferenceExceptionсебе ( msdn.microsoft.com/en-us/library/ms173163.aspx ); ви кидаєте, ArgumentNullExceptionякщо ваш метод не може прийняти нульові реф. Крім того, NullRef, як правило, є одним із складніших винятків діагнозів, коли ви виправляєте проблеми, тому я не думаю, що рекомендація не перевіряти наявність нуля є дуже хорошою.
Енді

3
@Andy "NullRef's, як правило, є одним із найскладніших винятків для діагностики" Я абсолютно не погоджуюся, якщо ви реєструєте речі, це дуже легко знайти та виправити (просто обробіть нульовий випадок).
Луї Котманн

6
Кидання ArgumentNullExceptionмає додаткову перевагу в тому, що можна надати ім'я параметра. Під час налагодження це економить ... помилку, секунди. Але важливі секунди.
Кос

2
@DaveMarkle ви можете також включити IsNullOrWhitespace msdn.microsoft.com/en-us/library/…
Натан Кооп

1
Я дійсно думаю, що перевірка нуля скрізь є джерелом величезного роздуття коду. це некрасиво, і це виглядає хакі, і важко залишатися послідовним. Я думаю (принаймні на мовах, подібних до C #) хорошим правилом є "заборонити ключове слово" null "у виробничому коді, використовувати його як божевільне у тестовому коді".
сара

24

Ви можете написати метод розширення (для чого це варто):

public static string EmptyNull(this string str)
{
    return str ?? "";
}

Тепер це працює безпечно:

string str = null;
string upper = str.EmptyNull().ToUpper();

100
Але, будь ласка, не варто. Останнє, що хоче побачити ще один програміст, - це тисячі рядків коду, що пронизані .EmptyNull () скрізь лише тому, що перший хлопець "злякався" винятків.
Дейв Маркл

15
@DaveMarkle: Але очевидно, це саме те, що шукала ОП. "Досить прикро перевіряти всі мої рядки на нуль, перш ніж я можу сміливо застосовувати такі методи, як ToUpper (), StartWith () тощо",
Тім Шмелтер

19
Коментар був до ОП, а не до вас. Незважаючи на те, що ваша відповідь однозначно правильна, програміст, який задає основне запитання, як-от це, повинен бути наполегливо застережений від фактичного введення вашого рішення в широку практику, як це часто є їхнім звичаєм. Існує ряд вигод, про які ви не обговорюєте у своїй відповіді, такі як непрозорість, підвищена складність, складність рефакторингу, потенційне використання методів розширення та так, ефективність. Іноді (багато разів) правильна відповідь - це не вірний шлях, і саме тому я коментував.
Дейв Маркл

5
@Andy: Рішення не виконати належну перевірку нуля - це належним чином перевірити наявність нулів, а не поставити смугу допомоги на проблему.
Дейв Маркл

7
Якщо ви переживаєте проблеми з написанням .EmptyNull(), чому б просто не використовувати (str ?? "")замість того, де це потрібно? Це означає, що я погоджуюся з почуттями, висловленими в коментарі @ DaveMarkle: ви, мабуть, не повинні. nullі String.Emptyвони концептуально різні, і ви не можете обов'язково ставитися до одного так само, як до іншого.
CVn

17

Ви також можете використовувати наступне, як для C # 6.0

string myString = null;
string result = myString?.ToUpper();

Результат рядка буде нульовим.


1
Щоб бути правильним, оскільки c # 6.0, версія IDE не має нічого спільного з ним, оскільки це мовна особливість.
Штійн Ван Антверпен

3
Ще один варіант -public string Name { get; set; } = string.Empty;
Jaja Harris

Як це називається? myString? .ToUpper ();
Мисливець Нельсон

1
Це називається Нульовим умовним оператором. Про це можна прочитати тут msdn.microsoft.com/en-us/magazine/dn802602.aspx
russelrillema

14

Порожні рядки та нулі принципово відрізняються. Нуль - це відсутність значення, а порожня рядок - це значення, яке є порожнім.

Мова програмування, що робить припущення про "значення" змінної, в цьому випадку порожній рядок, буде настільки ж хорошим, як ініціалізація рядка будь-яким іншим значенням, яке не спричинить нульову проблему посилання.

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

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

Словом, завжди добре мати лише 1 представництво для однієї держави. Для більш широкої дискусії про порожні та нульові параметри див. Посилання нижче.

/software/32578/sql-empty-string-vs-null-value

NULL vs Empty під час роботи з введенням користувача


2
І як саме ви бачите цю різницю, скажімо, у текстовому полі? Користувач забув ввести значення в поле чи вони цілеспрямовано залишають його порожнім? Null в мові програмування має певне значення; непризначений Ми знаємо, що воно не має значення, яке не збігається з нульовою базою даних.
Енді

1
це не велика різниця, коли ви використовуєте його з текстовим полем. Будь-який спосіб - наявність однієї позначення для відображення відсутності значення в рядку є першорядним. Якби мені довелося вибрати один, я вибрав би null.
Неррв

У Delphi рядок є типом значення і тому не може бути нульовим. Це значно полегшує життя в цьому відношенні - я дійсно мені дуже дратує зробити рядок еталонним типом.
Fabricio Araujo

1
У COM (загальній моделі об'єкта), яка передувала .net, тип рядка повинен містити вказівник на дані рядка, або nullпредставляти порожню рядок. Існує декілька способів .net міг би реалізувати подібну семантику, якби вони вирішили це зробити, особливо враховуючи, що Stringвона має ряд характеристик, які все одно роблять її унікальним типом [наприклад, це два типи масивів - єдині типи, розподіл яких розмір не постійний].
supercat

7

Основна причина / проблема полягає в тому, що конструктори специфікації CLS (яка визначає, як мови взаємодіють з .net) не визначили спосіб, за допомогою якого члени класу могли б вказати, що їх потрібно викликати безпосередньо, а не через callvirt, без того, щоб абонент виконував нульова перевірка; а також не передбачав безлічі визначальних структур, які не підлягали б "нормальному" боксу.

Якби специфікація CLS визначала такий засіб, тоді .net міг би послідовно слідувати керівництву, встановленому загальною модель об'єкта (COM), згідно з якою нульова посилання рядка вважалася семантично еквівалентною порожній рядку та для інших визначені користувачем типи незмінних класів, які повинні мати значення семантики значень, щоб також визначати значення за замовчуванням. По суті, що трапилося б, було б для кожного члена String, наприклад, Lengthписати як щось подібне [InvokableOnNull()] int String Length { get { if (this==null) return 0; else return _Length;} }. Цей підхід запропонував би дуже приємну семантику для речей, які повинні вести себе як цінності, але через проблеми з реалізацією потрібно зберігати їх у купі. Найбільша складність цього підходу полягає в тому, що семантика перетворення між такими типами і Objectмогла б стати трохи мутною.

Альтернативним підходом було б дозволити визначення типів спеціальних структур, які не успадковували, Objectа натомість мали власні операції боксу та розпакування (які перетворювали б на / з іншого типу класу). При такому підході існували б тип класу, NullableStringякий веде себе так, як зараз це рядок, та власний структурований тип структури String, який би містив єдине приватне поле Valueтипу String. Спроба конвертувати a Stringв NullableStringабо Objectповернеться, Valueякщо не є null, або String.Emptyякщо null. Спроба привласнення до Stringненульової посилання на NullableStringекземпляр зберігатиме посилання в Value(можливо, зберігання нуля, якщо довжина дорівнює нулю); кастинг будь-якої іншої посилання кине виняток.

Незважаючи на те, що рядки повинні зберігатися в купі, концептуально немає причини, чому вони не повинні вести себе як типи значень, які мають ненулеве значення за замовчуванням. Зберігання їх як "нормальної" структури, на яку було посилання, було б ефективним для коду, який використовував їх як тип "рядок", але додав би додатковий шар непрямості та неефективності при передачі на "об'єкт". Хоча я не передбачаю .net додавати будь-яку з перерахованих вище функцій в цей пізній час, можливо, дизайнери майбутніх фреймворків можуть розглянути можливість їх включення.


1
Виступаючи як хтось, хто багато працює в SQL, і мав справу з головним болем Oracle, не розрізняючи NULL і нульову довжину, я дуже радий, що .NET це робить . "Порожня" - це значення, "нульова" - ні.

@JonofAllTrades: Я не згоден. У коді програми, крім роботи з кодом db, немає сенсу, що рядок розглядається як клас. Це тип значення та базовий. Supercat: +1 для вас
Fabricio Araujo

1
Код бази даних є великим "за винятком". Поки існують деякі проблемні домени, де потрібно розрізняти "присутній / відомий, порожній рядок" та "не присутній / невідомий / непридатний", наприклад, бази даних, тоді мова повинна підтримувати його. Звичайно тепер, що має .NET Nullable<>, рядки можуть бути повторно застосовані як типи значень; Я не можу говорити про витрати та переваги такого вибору.

3
@JonofAllTrades: Код, який має справу з числами, повинен мати позадіапазонний спосіб розрізнення значення нуля за замовчуванням від "undefined". Насправді це код для обробки нуль, який працює з рядками та номерами, повинен використовувати один метод для змінних рядків та інший для нульових чисел. Навіть якщо тип нульового класу stringє більш ефективним, ніж Nullable<string>було б, використання методу "більш ефективний" є більш обтяжливим, ніж можливість використовувати один і той же підхід для всіх значень бази даних, що зводяться до змін.
supercat

5

Оскільки рядкова змінна - це посилання , а не екземпляр .

Ініціалізація його до Empty за замовчуванням була б можливою, але це ввело б багато невідповідностей по всій раді.


3
Ніякої конкретної причини stringне повинно бути еталонним типом. Зрозуміло, що фактичні символи, що складають рядок, безумовно, повинні зберігатися в купі, але враховуючи кількість виділеної підтримки, яку струни вже мають у CLR, не буде розтяжкою мати System.Stringтип значень з a єдине приватне поле Valueтипу HeapString. Це поле було б посилальним типом і було б за замовчуванням null, але Stringструктура, Valueполе якої було недійсною, поводиться як порожній рядок. Єдиним недоліком такого підходу був би ...
supercat

1
... що при перекладі Stringна a Object, за відсутності спеціального коду під час виконання програми, спричинить створення кодового Stringекземпляра на купі, а не просто копіювання посилання на HeapString.
supercat

1
@supercat - ніхто не каже, що рядок повинен / може бути типом значення.
Хенк Холтерман

1
Ніхто, крім мене. Якщо рядок є "спеціальним" типом значення (з приватним полем типу посилань), це дозволить більшій обробці бути по суті такою ж ефективною, як зараз, за ​​винятком доданої нульової перевірки методів / властивостей, таких як .Lengthтощо, так що екземпляри, які містять нульова посилання не намагатиметься її знеструмити, а натомість поводитиметься як слід для порожнього рядка. Чи буде Рамка кращою чи гіршою string, якщо реалізовано таким чином, якщо хотіли default(string)б бути порожнім рядком ...
supercat

1
... мати stringобгортку типу значень у полі референсного типу - це був би підхід, який вимагав би найменших змін у інших частинах .net [дійсно, якщо хтось готовий прийняти перетворення з Stringметою Objectстворення додаткового елемента, можна просто мати Stringзвичайну структуру з полем типу, Char[]яке вона ніколи не піддавалася]. Я думаю, що мати HeapStringтип буде, ймовірно, краще, але в деякому роді рядок, що містить тип значень, був Char[]би простішим.
supercat

5

Чому дизайнери C # вирішили використовувати null як значення за замовчуванням рядків?

Оскільки рядки є еталонними типами , типовими є типові значення null. Змінні типи посилань зберігають посилання на фактичні дані.

Давайте використаємо defaultдля цього випадку ключове слово;

string str = default(string); 

strє a string, тому це посилальний тип , тому значенням за замовчуванням є null.

int str = (default)(int);

strє int, тому це тип значення , тому значення за замовчуванням є zero.


4

Якщо значенням за замовчуванням stringбула порожня рядок, я б не повинен був тестувати

Неправильно! Зміна значень за замовчуванням не змінює той факт, що це тип посилання, і хтось може явно встановити посилання null.

Додатково Nullable<String>було б сенс.

Справжня точка. Було б більше сенсу не допускати nullжодних посилальних типів, а вимагати Nullable<TheRefType>для цієї функції.

То чому ж дизайнери C # вирішили використовувати nullяк значення за замовчуванням рядки?

Узгодженість з іншими еталонними типами. Тепер, навіщо nullвзагалі допускати еталонні типи? Напевно, так, що він відчуває себе як C, хоча це сумнівне дизайнерське рішення мовою, яка також надає Nullable.


3
Це може бути тому, що Nullable був представлений лише в .NET 2.0 Framework, тож до цього він був недоступний?
jcolebrand

3
Дякую Бернону, що вказав, що хтось МОЖЕ згодом встановити ініціалізоване значення на нульові типи посилань. Роздумуючи це, мені підказує, що мій початковий намір у питанні не приносить користі.
Марсель

4

Можливо, якщо ви скористаєтеся ??оператором при призначенні змінної рядка, це може вам допомогти.

string str = SomeMethodThatReturnsaString() ?? "";
// if SomeMethodThatReturnsaString() returns a null value, "" is assigned to str.

2

String - це незмінний об'єкт, який означає, що коли йому дано значення, старе значення не стирається з пам'яті, а залишається в старому місці, а нове значення ставиться в нове місце. Отже, якщо значення за замовчуванням String aбуло String.Empty, воно би втрачало String.Emptyблок в пам'яті, коли йому було надано перше значення.

Хоча це здається мізерним, це може перетворитись на проблему при ініціалізації великого масиву рядків із значеннями за замовчуванням String.Empty. Звичайно, ви завжди можете використовувати мутаційний StringBuilderклас, якщо це буде проблемою.


Дякуємо, що згадали про "першу ініціалізацію".
Марсель

3
Яка б це була проблема при ініціалізації великого масиву? Оскільки, як ви сказали, рядки незмінні, всі елементи масиву просто були б вказівниками на одне і те ж String.Empty. Я помиляюся?
Ден Бертон

2
Значення за замовчуванням для будь-якого типу матиме всі біти, встановлені на нуль. Єдиний спосіб для значення за замовчуванням stringбути порожнім рядком - це дозволити всі біти-нулі як подання порожнього рядка. Існує декілька способів цього досягти, але я не думаю, що будь-який процес ініціалізації посилань на них String.Empty.
supercat

Інші відповіді також обговорювали цю тему. Я думаю, що люди прийшли до висновку, що не було б сенсу трактувати клас String як особливий випадок і надавати щось інакше, ніж all-bits-zero, як ініціалізацію, навіть якщо це було щось на зразок String.Emptyабо "".
djv

@DanV: Зміна поведінки ініціалізації місць stringзберігання вимагала б також змінити поведінку ініціалізації всіх структур або класів, які мають поля типу string. Це означало б досить велику зміну в дизайні .net, яка в даний час розраховує зробити нульову ініціалізацію будь-якого типу, навіть не замислюючись про те, що це таке, за винятком лише його загального розміру.
supercat

2

Оскільки рядок є еталонним типом, а значення за замовчуванням для типу посилання є нульовим.


0

Можливо, stringключове слово вас збентежило, оскільки воно виглядає точно так само, як і будь-яке інше оголошення типу значення , але насправді це псевдонім, System.Stringяк це пояснено в цьому запитанні .
Також темно-синій колір у візуальній студії та малі перші літери можуть ввести в оману думку про це struct.


3
Чи не те ж саме стосується objectключового слова? Хоча, правда, це набагато менше, ніж використовується string.

2
Як intце псевдонім для System.Int32. Який твій погляд? :)
Торарін

@Thorari @delnan: Вони обоє псевдоніми, але System.Int32, Structтаким чином, мають значення за замовчуванням, а System.Stringце Class- покажчик зі значенням за замовчуванням null. Вони візуально представлені в одному шрифті / кольорі. Без знання можна подумати, що вони діють однаково (= мають значення за замовчуванням). Моя відповідь була написана з когнітивною психологічною ідеєю когнітивної психології на en.wikipedia.org/wiki/ :-)
Алессандро Да Рунья

Я досить впевнений, що Андер Хейльсберг сказав про це в інтерв'ю каналу 9. Я знаю різницю між купою та стеком, але ідея C # полягає в тому, що випадковому програмісту не потрібно.
Томас Коелл

0

Зменшувані типи з'явилися лише до 2.0.

Якби нульові типи були зроблені на початку мови, то рядок був би ненульовим і рядковим? був би зведений нанівець. Але вони не могли цього зробити для зворотної сумісності.

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

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