Почему Nullable ‹T› - это структура?

Мне было интересно, почему Nullable<T> является типом значения, если он предназначен для имитации поведения ссылочных типов? Я понимаю такие вещи, как давление сборщика мусора, но я не уверен - если мы хотим, чтобы int действовал как ссылка, мы, вероятно, согласны со всеми последствиями наличия реального ссылочного типа. Я не вижу причин, по которым Nullable<T> - это не просто коробочная версия T struct.

Как тип значения:

  1. он по-прежнему должен быть упакован и распакован, и, более того, упаковка должна немного отличаться от "нормальных" структур (чтобы обрабатывать нулевые значения, допускающие значение NULL, как настоящие null)
  2. к нему нужно относиться по-другому при проверке на нуль (выполняется просто в Equals, никаких реальных проблем)
  3. он изменяемый, нарушая правило, согласно которому структуры должны быть неизменными (хорошо, это логически неизменяемое)
  4. у него должно быть специальное ограничение, чтобы запретить рекурсию, например Nullable<Nullable<T>>

Разве создание Nullable<T> ссылочного типа не решает эти проблемы?

перефразировано и обновлено:

Я немного изменил свой список причин, но мой общий вопрос все еще открыт:

Чем ссылочный тип Nullable<T> будет хуже, чем реализация текущего типа значения? Это только давление GC и "маленькое, неизменное" правило? Мне все еще кажется странным ...


person NOtherDev    schedule 24.11.2010    source источник
comment
Настоящий вопрос заключается в следующем: почему бы не использовать универсальный вариант Maybe ‹T› для всех типов T.   -  person    schedule 25.11.2010
comment
@pst F # имеет тип Option ‹T›, но семантика не будет работать в C # без значительного перехода от «Все ссылки могут быть нулевыми» на «Нулевые» могут быть только указанные вами ссылки. В противном случае, какой смысл иметь тип Maybe?   -  person CodexArcanum    schedule 25.11.2010
comment
Это действительно сложный вопрос, вам следует прочитать соответствующие разделы стандарта CIL. Nullable ‹T› занимает особое положение в системе типов, и к нему применяется много специальной обработки в CLR. Боюсь, ответ не так прост, как вам хотелось бы.   -  person Johannes Rudolph    schedule 27.11.2010
comment
Nullable<T> - совершенно безумный тип, и пытаться рассуждать об этом логически - глупо. Закрой глаза, закрой уши и кричи ла ла ла ла ла Я не слышу тебя ла ла ла ла, пока он не уйдет, вот мой совет.   -  person Brian    schedule 28.11.2010
comment
Я не уверен только в том, что он предназначен для имитации поведения ссылочных типов. С тех пор как? Откуда у вас эта идея? Весь вопрос ошибочен, поэтому вы не получаете желаемого ответа.   -  person Aaronaught    schedule 28.11.2010
comment
@Aaronaught, я согласен, и затронул это в своем ответе.   -  person Jon Hanna    schedule 28.11.2010


Ответы (4)


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

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

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

Тип значения имеет концепцию равенства на основе содержащегося в нем значения, которое при желании может быть переопределено (существуют логические ограничения на то, как это переопределение может происходить *). Ссылочный тип имеет концепцию идентичности, которая не имеет смысла с типами значений (поскольку они не могут иметь прямого псевдонима, поэтому два таких значения не могут быть идентичными), которые не могут быть переопределены, что также дает значение по умолчанию для его концепции равенства. По умолчанию == имеет дело с этим равенством на основе значений, когда дело касается типов значений †, но с идентичностью, когда дело касается ссылочных типов. Кроме того, даже когда ссылочному типу дается концепция равенства, основанная на значениях, и он используется для ==, он никогда не теряет возможность сравниваться с другой ссылкой для идентификации.

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

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

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

Некоторые дополнительные различия между типами значений и ссылочными типами, возможно, связаны с деталями реализации. То, что тип значения в локальной переменной имеет значение, хранящееся в стеке, было аргументировано как деталь реализации; вероятно, довольно очевидный, если в вашей реализации есть стек, и, безусловно, важный в некоторых случаях, но не основной для определения. Его также часто преувеличивают (для начала, ссылочный тип в локальной переменной также имеет ссылку в стеке, во-вторых, много раз значение типа значения сохраняется в куче).

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


Теперь Nullable<T> - это тип, который ведет себя как тип значения всеми способами, описанными выше, за исключением того, что он может принимать нулевое значение. Возможно, вопрос о хранении локальных значений в стеке не так уж и важен (это скорее деталь реализации, чем что-либо еще), но остальное зависит от того, как оно определяется.

Nullable<T> определяется как

struct Nullable<T>
{
    private bool hasValue;
    internal T value;
    /* methods and properties I won't go into here */
}

Большая часть реализации с этого момента очевидна. Требуется некоторая особая обработка, позволяющая присвоить ему значение null - обрабатываемое так, как если бы было присвоено default(Nullable<T>) - и некоторая особая обработка при упаковке, а затем следует остальное (включая то, что его можно сравнить на равенство с null).

Если бы Nullable<T> был ссылочным типом, тогда нам потребовалась бы особая обработка, чтобы все остальное могло произойти, а также особая обработка функций, позволяющих .NET помочь разработчику (например, нам потребовалась бы особая обработка, чтобы сделать это спускаются с ValueType). Я даже не уверен, возможно ли это.

* Существуют некоторые ограничения на то, как нам разрешено переопределять равенство. Комбинируя эти правила с теми, которые используются в значениях по умолчанию, обычно мы можем позволить, чтобы два значения считались равными, которые по умолчанию считались бы неравными, но редко имеет смысл рассматривать два неравных значения, которые по умолчанию считались бы равными. Исключением является случай, когда структура содержит только типы-значения, но указанные типы-значения переопределяют равенство. Это результат оптимизации и обычно считается ошибкой, а не намеренно.

† Исключение составляют типы с плавающей запятой. Из-за определения типов-значений в стандарте CLI double.NaN.Equals(double.NaN) и float.NaN.Equals(float.NaN) возвращают true. Но из-за определения NaN в ISO 60559, float.NaN == float.NaN и double.NaN == double.NaN оба возвращают false.

person Jon Hanna    schedule 28.11.2010
comment
На самом деле я бы пошел еще дальше и сказал, что Nullable<T> не может даже действительно быть null. Каждая переменная или аргумент, который является Nullable<T>, на самом деле имеет значение, даже если оно равно нулю. Если бы не перегрузки == и Equals, вы даже не смогли бы напрямую сравнить его с null. Любое сходство Nullable<T> со ссылочным типом - просто синтаксический сахар. - person Aaronaught; 28.11.2010
comment
@Aaronaught, я бы поспорил иначе и сказал бы, что значение null другое, но перекрывается. null из int? сравнимо с null целочисленного поля, допускающего значение NULL, в базе данных - теоретическая и математическая концепция. null из string x = null означает оба одной и той же концепции, а также относится к основанной на идентичности природе ссылок - в этом случае не имеющей определенного значения. Таким образом, обе они являются разумными моделями ничтожности, и есть разумные сравнения, но они по-разному моделируют аннулирование. - person Jon Hanna; 28.11.2010
comment
Это все хорошо, Джон, я хотел сказать, что переменная или аргумент типа Nullable<T> никогда не может содержать буквальное значение null, и что единственное, что делает его даже своего рода похожим на ссылочный тип это его перегруженный оператор ==. - person Aaronaught; 28.11.2010
comment
Ааронаут, что значит держать ценность? Это означает наличие свойства, равного этому значению. Используется ли где-либо один и тот же битовый шаблон - это деталь реализации. Если он ходит как нуль и крякает как нуль, значит, это утка. Я имею ввиду нуль. - person Jon Hanna; 28.11.2010
comment
@Jon, я не могу согласиться с вами в некоторых моментах. Хотя вы правы в отношении концепции равенства для примитивов, вы ошибаетесь в том, что это применимо к типам значений в целом. ValueType определяет свои собственные методы Equals() и GethashCode(). Эта реализация Equals не сравнивает двоичные данные, а сравнивает их на предмет равенства посредством отражения: msdn.microsoft.com/en-us/library/2dts52z7 (v = VS.85) .aspx - это имеет важное значение и является частью причины, по которой следует всегда переопределять эти два метода в любой структуре. (продолжение следует) - person Lucero; 28.11.2010
comment
Другое дело, что вы смешиваете магию компилятора и поведение среды CLR. То, действительно ли язык предоставляет удобный интерфейс для использования Nullable<>, не имеет ничего общего с CLR - в CLR технически невозможно назначить null распакованному экземпляру Nullable<> (что правильно заметил @Aaronaught); C # добавляет эту абстракцию для идентичной реализации концепции нулевых значений для типов значений и ссылочных типов. Также различие между кучей и стеком не имеет значения; часто бывает, что класс с Nullable<> полями помещает их в кучу, сохраняя при этом семантику типа значения. - person Lucero; 28.11.2010
comment
@Lucero, я не утверждал, что этот тип значения сравнивает двоичные данные (в некоторых случаях это действительно так, это оптимизация, которую я отмечал как иногда вызывающую ошибку, но эта оптимизация предназначена для создания той же семантики, что и метод отражения, но провал в глючном корпусе). Это тот случай, который означает, что вам, возможно, придется переопределить равные значения, даже если отражение обеспечивает правильную семантику - в противном случае переопределение является вопросом оптимизации. - person Jon Hanna; 29.11.2010
comment
Можно перевести Nullable<T> в состояние, в котором он сравнивается с null как равным. Это семантически эквивалентно присвоению ему значения null независимо от того, позволяет ли язык нам использовать один и тот же синтаксис для присвоения null ссылочному типу или нет. На мой взгляд, это присвоение ему семантического нуля. Опять же, если он крякает как нуль ... - person Jon Hanna; 29.11.2010
comment
Что касается нерелевантности кучи и стека, это почти не отличается от мнения, которое я высказал, говоря, что это аргументировано как деталь реализации, часто преувеличенная и указывающая на то, что для начала ссылочный тип в локальной переменной также имеет саму ссылку в стек, с другой стороны, много раз значение типа значения сохраняется в куче. Это может иметь эффекты, которые имеют значение для некоторых оптимизаций низкого уровня, но я надеялся, что четко заявлял, что не считаю это особенно актуальным или важным, и упоминал это для полноты. - person Jon Hanna; 29.11.2010
comment
Я согласен с вышеизложенным Aaronaught. Использование null с Nullable является синтаксическим сахаром, вводящим в заблуждение. Вместо этого я по возможности использую HasValue. - person Concrete Gannet; 16.04.2013
comment
Конкретный Gannet: тип называется Nullable ‹T›, вы можете присвоить ему значения NULL, сравнить его с значениями NULL, и он даже выдает исключение нулевой ссылки, если вы обращаетесь к нему до того, как код приложения присвоил ему значение (не NULL), даже если есть фактическое значение по умолчанию во внутреннем поле значения, я не уверен, есть ли какие-либо причины для повышения производительности, чтобы использовать только HasValue вместо сравнения null, но компилятор нас покрыл (stackoverflow.com/questions/676078/). Если он ходит как утка и крякает как утка ... - person Kaveh Hadjari; 05.01.2016

Отредактировано с учетом обновленного вопроса ...

Вы можете упаковывать и распаковывать объекты, если хотите использовать структуру в качестве ссылки.

Однако тип Nullable<> в основном позволяет улучшить любой тип значения с помощью дополнительного флага состояния, который сообщает, должно ли значение использоваться как null или является ли объект «действительным».

Итак, чтобы ответить на ваши вопросы:

  1. Это преимущество при использовании в коллекциях или из-за разной семантики (копирование вместо ссылки)

  2. Нет, это не так. CLR учитывает это при упаковке и распаковке, так что вы фактически никогда не упаковываете Nullable<> экземпляр. Упаковка Nullable<>, которая "не имеет" значения, вернет ссылку null, а при распаковке произойдет обратное.

  3. Неа.

  4. Опять же, это не так. Фактически общие ограничения для структуры не позволяют использовать структуры, допускающие значение NULL. Это имеет смысл из-за особого поведения упаковки / распаковки. Следовательно, если у вас есть where T: struct для ограничения универсального типа, типы, допускающие значение NULL, будут запрещены. Поскольку это ограничение также определено для типа Nullable<T>, вы не можете вкладывать их без специальной обработки, чтобы предотвратить это.

Почему бы не использовать ссылки? Я уже упоминал о важных смысловых различиях. Но помимо этого ссылочные типы используют гораздо больше памяти: каждая ссылка, особенно в 64-битных средах, использует не только память кучи для экземпляра, но также память для ссылки, информации о типе экземпляра, битов блокировки и т. Д. Таким образом, помимо семантики и различий в производительности (косвенное обращение через ссылку), вы в конечном итоге используете кратную часть памяти, используемой для самой сущности, для наиболее распространенных сущностей. И GC получает больше объектов для обработки, что еще больше ухудшает общую производительность по сравнению со структурами.

person Lucero    schedule 24.11.2010
comment
Объявление 2. int? a = null; bool b = (a == null); Поскольку a не является настоящей нулевой ссылкой, просто Nullable содержит значение null, разве это не особый подход? Объявление 3. int? a = 1; a++; // a is now 2 - не мутирует ли состояние? - person NOtherDev; 25.11.2010
comment
Это зависит от обстоятельств, мне нужно будет проверить IL, сгенерированный различными версиями C #. Если == null всегда возвращается к сравнению ссылок, тогда нет, это не будет специальной обработкой в ​​компиляторе, поскольку упаковка, необходимая для сравнения с нулевым значением, фактически даст нулевую ссылку. Обратите внимание, что при использовании неограниченного универсального кода и сравнении с null он будет фактически выполнить операцию упаковки (и выдаст предупреждение, по крайней мере, R #). - person Lucero; 25.11.2010
comment
Я не согласен с тем, что вы говорите об использовании памяти. Простое различие заключается в размере указателя (32 или 64 бита), и если он имеет значение null или псевдоним, ссылочный тип будет занимать меньше общей памяти, чем тип значения, если размер класса / структуры больше этого. Поскольку в большинстве случаев вы не будете копировать само значение, существует тенденция к большому наложению псевдонимов (хотя и без рисков, поскольку они накладываются на разные кадры стека), поэтому ссылки, как правило, имеют более низкую общую нагрузку на память. Есть компромиссы, которые означают, что тип значения по-прежнему выигрывает примерно до 16 байт, но компромиссы могут идти в любом случае. - person Jon Hanna; 28.11.2010
comment
@Jon, извини, ты ошибаешься. Каждый экземпляр кучи имеет не только поля в памяти, но также использует дополнительную память для синхроблока, дескриптора типа и т. Д .: msdn.microsoft.com/en-us/magazine/cc163791.aspx В зависимости от типа у вас будет заполнение адресов, которое также может добавить дополнительную потраченную впустую память кучи на каждом пример. Даже в случае null и (ИМХО, не так часто) вы всегда тратите память указателя, которая равна или больше, чем bool?, byte?, sbyte?, bool?, short? или ushort?, а на 64-битных также int?, uint?, single? , большинство перечисляемых типов, допускающих значение NULL, и другие! - person Lucero; 28.11.2010
comment
Но все же есть случаи, когда размер значения перевешивает это, и когда это происходит, оно увеличивается с каждым кадром стека. Это уменьшает эффект заполнения и другой информации в памяти и приближает практическое сравнение к тому, сравнивается ли размер с размером указателя. Примерно после 16 байт баланс в пользу эталонного типа. Это не всегда так (и не во многих случаях, что было бы наиболее распространено), и это не особенно хорошо. Когда типы значений являются очевидным подходом, использование ссылочных типов должно быть довольно значительным преимуществом. - person Jon Hanna; 29.11.2010
comment
@Lucero: в 32-битном .net накладные расходы для объекта кучи, который никогда не используется блокировкой монитора или другой подобной конструкцией, составляют восемь байтов, плюс все, что потребуется, чтобы довести общий размер до двенадцати. Если существует одна ссылка на упакованную 4-байтовую структуру, общая стоимость будет 16 байтов. Если существуют две ссылки на одну и ту же упакованную структуру, общая стоимость будет 20 байтов. Если три, 24. Четыре, 28. Одно место хранения обнуляемого четырехбайтового типа структуры стоит 8 байтов; два, 16; три, 24; четыре, 32. Так что Nullable<T> не сэкономит много по сравнению с ImmutableHolder<T> классом. - person supercat; 17.10.2012

Это не изменчиво; Проверьте еще раз.

Бокс тоже другой; пустые «ящики» обнулить.

Но; он маленький (чуть больше T), неизменяемый и инкапсулирует только структуры - идеален как структура. Возможно, что еще более важно, если T действительно "значение", то и T тоже? логическое «значение».

person Marc Gravell    schedule 24.11.2010
comment
int? a = 1; a++; // a is now 2 - разве это не мутирует состояние? - person NOtherDev; 25.11.2010
comment
@A. Нет, вы поменяли местами состояние. Это похоже на вопрос, если (для int) i ++ означает, что int является изменяемым. - person Marc Gravell; 25.11.2010
comment
int a = 1; a++; //a is now 2 делает ли это изменяемым int? Нет. Изменчивый означает не это. - person Erv Walter; 25.11.2010
comment
Хорошо, похоже, я не совсем понимаю изменчивость. Так, например, если var x = DateTime.Now; x.AddMinutes(1); изменит значение x, DateTime будет изменчивым, верно? Так в чем же разница между моим примером? - person NOtherDev; 25.11.2010
comment
@A. нет, это не меняет значение x вообще; AddMinutes возвращает значение, которое вы игнорируете. Если вы сделали x = x.AddMinutes(1), это означает переназначение переменной x. Изменчивость - это когда мы можем изменить внутреннее состояние значения или объекта (которое отличается от изменения переменной). Итак: cust.Name = "Fred" изменяет внутреннее состояние объекта, на который ссылается cust; с типами-значениями это сложнее объяснить, потому что семантика копирования сбивает с толку; но с переменной x struct, если бы я мог сделать x.Foo = 123, чтобы изменить ее, тогда значение x изменяемо. - person Marc Gravell; 25.11.2010
comment
Однако вы можете всегда переназначить локальную переменную; это ортогонально изменчивости; аналогично вы можете переназначить поле (если оно не только для чтения), но это не имеет ничего общего с изменчивостью. - person Marc Gravell; 25.11.2010
comment
Я знаю, что DateTime изменчив, это был только гипотетический пример. Хорошо, поэтому с моим предыдущим примером - struct int и ее operator++ - он неизменен, потому что мы знаем о семантике его копирования, но теоретически он может быть реализован как изменяемый, не меняя ничего в том, как он выглядит снаружи , правильно? - person NOtherDev; 26.11.2010
comment
@A. - нет, DateTime im изменяемый. - person Marc Gravell; 26.11.2010
comment
Да, верно, было поздно, это то, что я имел в виду с самого начала. - person NOtherDev; 26.11.2010
comment
a++ не означает что-то вроде a.Increment(), а a = a + 1, поэтому вы действительно назначаете новое значение, заменяя старое, а не изменяете часть структуры. В частности, если a является свойством, оно сначала вызывает метод получения, затем вычисляет a+1, а затем вызывает метод установки с параметром a+1. И если у a есть ссылочный тип, a впоследствии будет указывать на новый экземпляр, а не на измененный старый. - person CodesInChaos; 27.11.2010
comment
Как правило, неизменяемость может относиться либо к переменной (например, readonly List<int> x; не может заменить x, но могут изменить члены x), либо к самому значению (например, string x = "abc"; не может изменить свои части, хотя x можно заменить), либо и то, и другое. Когда мы говорим о неизменяемом типе, мы имеем в виду второй. Для простого типа, такого как int, который не имеет составных частей, различие исчезает, и сравнение int с неизменяемой структурой бессмысленно, что позволяет нам рассматривать int как изменяемый или неизменный, насколько нам это подходит. Нас это устраивает при сравнении с другими типами ценностей, но не всегда. - person Jon Hanna; 28.11.2010

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

namespace ClassLibrary1

{используя NFluent;

using NUnit.Framework;

[TestFixture]
class MyNullableShould
{
    [Test]
    public void operator_equals_btw_nullable_and_value_works()
    {
        var myNullable = new MyNullable<int>(1);

        Check.That(myNullable == 1).IsEqualTo(true);
        Check.That(myNullable == 2).IsEqualTo(false);
    }

    [Test]
    public void Can_be_comparedi_with_operator_equal_equals()
    {
        var myNullable = new MyNullable<int>(1);
        var myNullable2 = new MyNullable<int>(1);

        Check.That(myNullable == myNullable2).IsTrue();
        Check.That(myNullable == myNullable2).IsTrue();

        var myNullable3 = new MyNullable<int>(2);
        Check.That(myNullable == myNullable3).IsFalse();
    }
}

} пространство имен ClassLibrary1 {using System;

public class MyNullable<T> where T : struct
{
    internal T value;

    public MyNullable(T value)
    {
        this.value = value;
        this.HasValue = true;
    }

    public bool HasValue { get; }

    public T Value
    {
        get
        {
            if (!this.HasValue) throw new Exception("Cannot grab value when has no value");
            return this.value;
        }
    }

    public static explicit operator T(MyNullable<T> value)
    {
        return value.Value;
    }

    public static implicit operator MyNullable<T>(T value)
    {
        return new MyNullable<T>(value);
    }

    public static bool operator ==(MyNullable<T> n1, MyNullable<T> n2)
    {
        if (!n1.HasValue) return !n2.HasValue;
        if (!n2.HasValue) return false;
        return Equals(n1.value, n2.value);
    }

    public static bool operator !=(MyNullable<T> n1, MyNullable<T> n2)
    {
        return !(n1 == n2);
    }

    public override bool Equals(object other)
    {
        if (!this.HasValue) return other == null;
        if (other == null) return false;
        return this.value.Equals(other);
    }

    public override int GetHashCode()
    {
        return this.HasValue ? this.value.GetHashCode() : 0;
    }

    public T GetValueOrDefault()
    {
        return this.value;
    }

    public T GetValueOrDefault(T defaultValue)
    {
        return this.HasValue ? this.value : defaultValue;
    }

    public override string ToString()
    {
        return this.HasValue ? this.value.ToString() : string.Empty;
    }
}

}

person Community    schedule 28.03.2018