Почему значение по умолчанию для строкового типа 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: Работая над языком, который рассматривает строку как типы значений, и работая над dotnet более двух лет, я согласен с Хенком. Я рассматриваю это как серьезную ошибку в DotNet.
Фабрицио Араужо
1
@delnan: Можно создать тип значения, который будет вести себя по существу так же String, за исключением того, что (1) поведение value-type-ish, имеющее пригодное для использования значение по умолчанию, и (2) неудачный дополнительный уровень косвенности бокса каждый раз, когда его приводят к Object, Принимая во внимание, что представление кучи stringуникально, наличие особой обработки, позволяющей избежать дополнительного бокса, не было бы большой натяжкой (на самом деле, возможность указать поведение бокса не по умолчанию было бы хорошо для других типов).
суперкат
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, как правило, являются одним из самых сложных исключений для диагностики». Я категорически не согласен, если вы регистрируете материал, его действительно легко найти и исправить (просто обработайте нулевой случай).
Луи Коттманн
6
Бросание ArgumentNullExceptionимеет дополнительное преимущество, заключающееся в возможности указать имя параметра. Во время отладки это экономит ... ошибки, секунды. Но важные секунды.
Кос
2
@DaveMarkle, возможно, вы захотите включить также IsNullOrWhitespace msdn.microsoft.com/en-us/library/…
Натан Куп
1
Я действительно думаю, что проверка на нулевое значение везде является источником огромного раздувания кода. это некрасиво, и выглядит неприлично, и трудно оставаться последовательным. Я думаю (по крайней мере, в C # -подобных языках) хорошее правило: «Запретите пустое ключевое слово в производственном коде, используйте его как сумасшедший в тестовом коде».
Сара
24

Вы можете написать метод расширения (для чего это стоит):

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

Теперь это работает безопасно:

string str = null;
string upper = str.EmptyNull().ToUpper();
Тим Шмельтер
источник
100
Но, пожалуйста, не надо. Последнее, что хочет увидеть другой программист, - это тысячи строк кода, везде с помощью .EmptyNull (), просто потому, что первый парень «напуган» исключениями.
Дэйв Маркл
15
@DaveMarkle: Но, очевидно, это именно то, что искал OP. «Довольно неприятно проверять все мои строки на ноль, прежде чем я смогу безопасно применять такие методы, как ToUpper (), StartWith () и т. Д.»
Тим Шмельтер
19
Комментарий был для ОП, а не для вас. Хотя ваш ответ однозначно верен, программисту, задающему такой основной вопрос, следует настоятельно рекомендовать не применять ваше решение в практике WIDE, как это часто бывает. Существует ряд компромиссов, которые вы не обсуждаете в своем ответе, такие как непрозрачность, повышенная сложность, сложность рефакторинга, потенциальное чрезмерное использование методов расширения и, да, производительность. Иногда (много раз) правильный ответ не является правильным путем, и именно поэтому я прокомментировал.
Дэйв Маркл
5
@Andy: решение проблемы отсутствия надлежащей проверки нуля состоит в том, чтобы правильно проверять наличие нулей, а не накладывать пластырь на проблему.
Дэйв Маркл
7
Если вам трудно писать .EmptyNull(), почему бы просто не использовать (str ?? "")его там, где это необходимо? Тем не менее, я согласен с мнением, выраженным в комментарии @ DaveMarkle: вы, вероятно, не должны. nullи String.Emptyконцептуально разные, и вы не можете относиться к одному так же, как к другому.
CVN
17

Вы также можете использовать следующее, начиная с C # 6.0

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

Строка результата будет нулевой.

russelrillema
источник
1
Если быть точным, начиная с c # 6.0, версия IDE не имеет к этому никакого отношения, поскольку это языковая функция.
Стейн Ван Антверпен
3
Другой вариант -public string Name { get; set; } = string.Empty;
Jaja Harris
Как это называется? ? туЗЬптд .ToUpper ();
Хантер Нельсон
1
Это называется нулевым условным оператором. Вы можете прочитать об этом здесь msdn.microsoft.com/en-us/magazine/dn802602.aspx
russelrillema
14

Пустые строки и нули принципиально разные. Ноль - это отсутствие значения, а пустая строка - это пустое значение.

Язык программирования, делающий предположения о «значении» переменной, в данном случае пустой строки, будет так же хорош, как инициализация строки любым другим значением, которое не вызовет проблему нулевой ссылки.

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

Другой случай, когда это может быть проблемой, - когда строка является возвращаемым значением какой-либо функции. Поскольку string является ссылочным типом и технически может иметь значение как нулевое, так и пустое оба, поэтому функция также может технически возвращать нулевое или пустое значение (ничто не мешает ей сделать это). Теперь, поскольку существует 2 понятия «отсутствия значения», то есть пустая строка и ноль, весь код, потребляющий эту функцию, должен будет выполнить 2 проверки. Один для пустого, а другой для нулевого.

Короче говоря, всегда хорошо иметь только одно представление для одного государства. Для более широкого обсуждения пустых и нулевых значений, смотрите ссылки ниже.

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

NULL vs Empty при работе с пользовательским вводом

Nerrve
источник
2
И как именно вы видите эту разницу, скажем, в текстовом поле? Пользователь забыл ввести значение в поле или намеренно оставил его пустым? Нуль в языке программирования имеет особое значение; Unassigned. Мы знаем, что у него нет значения, которое не совпадает с нулем базы данных.
Энди
1
Там нет большой разницы, когда вы используете его с текстовым полем. В любом случае, наличие одной нотации для представления отсутствия значения в строке имеет первостепенное значение. Если бы мне пришлось выбрать один, я бы выбрал ноль.
Nerrve
В Delphi строка является типом значения и поэтому не может быть нулевой. Это делает жизнь намного легче в этом отношении - я действительно нахожу очень раздражающим, что строка делает ссылочный тип.
Фабрицио Араужо
1
В COM (Common Object Model), которая предшествовала .net, строковый тип должен содержать указатель на данные строки или nullпредставлять пустую строку. Существует несколько способов, которыми .net мог бы реализовать аналогичную семантику, если бы они решили это сделать, особенно если учесть, что он Stringимеет ряд характеристик, которые в любом случае делают его уникальным типом [например, он и два типа массива являются единственными типами, чье распределение размер не постоянен.
суперкат
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. Попытка преобразовать Stringв NullableStringили Objectвернет, Valueесли не ноль, или String.Emptyесли ноль. При попытке привести к Stringненулевой ссылке на NullableStringэкземпляр будет сохранена ссылка Value(возможно, при сохранении нуля, если длина равна нулю); приведение любой другой ссылки вызовет исключение.

Несмотря на то, что строки должны храниться в куче, концептуально нет причин, по которым они не должны вести себя как типы значений, которые имеют ненулевое значение по умолчанию. Хранение их в виде «нормальной» структуры, содержащей ссылку, было бы эффективно для кода, который использовал их как тип «строка», но добавил бы дополнительный уровень косвенности и неэффективности при приведении к «объекту». Хотя я не предполагаю, что .net добавит любую из перечисленных выше функций в более позднюю дату, возможно, разработчики будущих фреймворков могут рассмотреть возможность их включения.

Supercat
источник
1
Говоря как кто-то, кто много работает в SQL и имел дело с головной болью Oracle, не делая различий между NULL и нулевой длиной, я очень рад, что .NET делает . «Пусто» - это значение, «ноль» - нет.
@JonofAllTrades: я не согласен. В коде приложения, кроме работы с кодом БД, нет смысла рассматривать строку как класс. Это тип значения и базовый. Суперкат: +1 тебе
Фабрицио Араужо
1
Код базы данных - это большое «кроме». До тех пор, пока существуют некоторые проблемные домены, в которых необходимо различать «присутствует / известен, пустая строка» и «не представлен / неизвестен / неприменим», например, базы данных, язык должен поддерживать это. Конечно, теперь, когда есть .NET Nullable<>, строки могут быть переопределены как типы значений; Я не могу говорить о затратах и ​​преимуществах такого выбора.
3
@JonofAllTrades: код, который работает с числами, должен иметь внеполосные средства, позволяющие отличить значение по умолчанию от нуля до «неопределенного». Как таковой, код обработки значений NULL, который работает со строками и числами, должен использовать один метод для строк, допускающих значение NULL, а другой - для значений NULL. Даже если обнуляемый тип класса stringболее эффективен, чем Nullable<string>был бы, необходимость использовать «более эффективный» метод более обременительна, чем возможность использовать тот же подход для всех значений базы данных обнуляемых данных.
суперкат
5

Поскольку строковая переменная является ссылкой , а не экземпляром .

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

Хенк Холтерман
источник
3
Нет особой причины, по stringкоторой должен быть ссылочный тип. Конечно, фактические символы, составляющие строку, безусловно, должны храниться в куче, но, учитывая количество выделенной поддержки, которую строки уже имеют в CLR, не будет растяжкой иметь System.Stringтип значения с одно закрытое поле Valueтипа HeapString. Это поле будет ссылочным типом и будет по умолчанию null, но Stringструктура, чье Valueполе имеет значение null, будет вести себя как пустая строка. Единственным недостатком такого подхода было бы ...
суперкат
1
... то, что приведение Stringк Object, при отсутствии кода особого случая во время выполнения приведет к созданию коробочного Stringэкземпляра в куче, а не просто к копированию ссылки на HeapString.
суперкат
1
@supercat - никто не говорит, что строка должна иметь тип значения.
Хенк Холтерман
1
Никто, кроме меня. Если строка является «особым» типом значения (с частным полем ссылочного типа), то большая часть обработки будет по существу такой же эффективной, как и сейчас, за исключением дополнительной проверки на пустые значения методов / свойств, подобных .Lengthт. Д., Чтобы экземпляры, которые содержат пустая ссылка не будет пытаться разыменовать ее, а будет вести себя как подходящая для пустой строки. Будет ли Framework лучше или хуже с stringреализованным таким образом, если кто-то хочет default(string)быть пустой строкой ...
суперкат
1
... наличие stringоболочки типа значения для поля ссылочного типа было бы подходом, который требовал бы наименьшего количества изменений в других частях .net [действительно, если бы кто-то согласился принять преобразование Stringдля Objectсоздания дополнительного элемента в штучной упаковке, можно просто иметь Stringобыкновенную структуру с полем типа, Char[]которое она никогда не выставляла]. Я думаю, что иметь HeapStringтип, вероятно, было бы лучше, но в некоторых отношениях строка типа значения, содержащая a, Char[]была бы проще.
суперкат
5

Почему разработчики C # решили использовать null в качестве значения по умолчанию для строк?

Поскольку строки являются ссылочными типами , ссылочные типы имеют значение по умолчанию null. Переменные ссылочных типов хранят ссылки на фактические данные.

Давайте использовать defaultключевое слово для этого случая;

string str = default(string); 

strэто string, так что это ссылочный тип , поэтому значение по умолчанию null.

int str = (default)(int);

strявляется int, так что это тип значения , поэтому значение по умолчанию zero.

Soner Gönül
источник
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класс, если это будет проблемой.

DJV
источник
Спасибо за упоминание о "первой инициализации".
Марсель
3
Как это будет проблемой при инициализации большого массива? Поскольку, как вы сказали, строки являются неизменяемыми, все элементы массива будут просто указателями на одно и то же String.Empty. Я ошибаюсь?
Дэн Бертон
2
Значение по умолчанию для любого типа будет иметь все биты равными нулю. Единственный способ для значения по умолчанию, stringчтобы быть пустой строкой, состоит в том, чтобы разрешить все биты ноль как представление пустой строки. Есть несколько способов, которыми это может быть достигнуто, но я не думаю, что это связано с инициализацией ссылок на String.Empty.
суперкат
Другие ответы также обсуждали этот вопрос. Я думаю, что люди пришли к выводу, что не имеет смысла рассматривать класс String как особый случай и предоставлять в качестве инициализации что-то отличное от нуля, равного нулю, даже если это было что-то вроде String.Emptyили "".
DJV
@DanV: изменение поведения инициализации мест stringхранения потребовало бы также изменения поведения инициализации всех структур или классов, имеющих поля типа string. Это было бы довольно большим изменением в дизайне .net, который в настоящее время ожидает инициализации нуля любого типа, даже не задумываясь о том, что это такое, за исключением только его общего размера.
суперкат
2

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

Акшай
источник
0

Возможно, stringключевое слово смутило вас, так как оно выглядит точно так же, как любое другое объявление типа значения , но на самом деле это псевдоним, System.Stringкак описано в этом вопросе .
Кроме того, темно-синий цвет в Visual Studio и первая строчная буква могут ввести в заблуждение, считая, что это struct.

Алессандро да Ругна
источник
3
Разве это не так же верно для objectключевого слова? Хотя по общему признанию, это используется гораздо меньше, чем string.
2
Как intпсевдоним для System.Int32. В чем ваша точка зрения? :)
Thorarin
@Thorari @delnan: Они оба псевдонимы, но System.Int32является , Structтаким образом , имея значение по умолчанию , в то время как System.Stringэто , Classимеющий указатель со значением по умолчанию null. Они визуально представлены в том же шрифте / цвете. Без знания можно думать, что они действуют одинаково (= имея значение по умолчанию). Мой ответ был написан на основе идеи когнитивной психологии en.wikipedia.org/wiki/Cognitive_psychology :-)
Алессандро Да Ругна
Я вполне уверен, что Андерс Хейлсберг сказал это в интервью 9-му каналу. Я знаю разницу между кучей и стеком, но идея C # заключается в том, что обычному программисту это не нужно.
Томас Коэль
0

Обнуляемые типы не появлялись до 2.0.

Если бы в начале языка были сделаны обнуляемые типы, то строка была бы не обнуляемой, а строка? был бы обнуляемым. Но они не могли сделать это из-за обратной совместимости.

Многие люди говорят о типе ref или не типе ref, но string - это необычный класс, и были бы найдены решения, позволяющие сделать это возможным.

Томас Коэль
источник