Каким образом возможно поведение упаковки / распаковки Nullable ‹T›?

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

Любая переменная типа Nullable<T> может быть присвоена null. Например:

int? i = null;

Сначала я не мог понять, как это возможно без какого-либо определения неявного преобразования из object в Nullable<T>:

public static implicit operator Nullable<T>(object box);

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

int? i = new object();

Затем я понял, что, возможно, тип Nullable<T> может определять неявное преобразование в некоторый произвольный ссылочный тип, который никогда не может быть создан, например:

public abstract class DummyBox
{
    private DummyBox()
    { }
}

public struct Nullable<T> where T : struct
{
    public static implicit operator Nullable<T>(DummyBox box)
    {
        if (box == null)
        {
            return new Nullable<T>();
        }

        // This should never be possible, as a DummyBox cannot be instantiated.
        throw new InvalidCastException();
    }
}

Однако это не объясняет, что со мной произошло дальше: если свойство HasValue равно false для любого значения Nullable<T>, тогда это значение будет помещено в рамку как null:

int? i = new int?();
object x = i; // Now x is null.

Кроме того, если HasValue равно true, тогда значение будет упаковано как T, а не как T?:

int? i = 5;
object x = i; // Now x is a boxed int, NOT a boxed Nullable<int>.

Но это, похоже, подразумевает, что существует настраиваемое неявное преобразование из Nullable<T> в object:

public static implicit operator object(Nullable<T> value);

Это явно не так, поскольку object является базовым классом для всех типов, а определяемые пользователем неявные преобразования в / из базовых типов недопустимы (как и должны быть).

Кажется, что object x = i; должен помещаться i, как и любой другой тип значения, чтобы x.GetType() давал тот же результат, что и typeof(int?) (вместо того, чтобы выдавать NullReferenceException).

Итак, я немного покопался и, конечно же, оказалось, что это поведение специфично для типа Nullable<T>, специально определенного в спецификациях C # и VB.NET, и не воспроизводится ни в каких пользовательских struct (C #) или Structure ( VB.NET).

Вот почему я до сих пор в замешательстве.

Это конкретное поведение упаковки и распаковки невозможно реализовать вручную. Это работает только потому, что и C #, и VB.NET уделяют особое внимание типу Nullable<T>.

  1. Разве теоретически не возможно, чтобы существовал другой язык на основе интерфейса командной строки, где Nullable<T> не пользовались этой специальной обработкой? И не будет ли поэтому тип Nullable<T> проявлять разное поведение на разных языках?

  2. Как C # и VB.NET достигают такого поведения? Поддерживается ли это CLR? (То есть позволяет ли CLR типу каким-либо образом «переопределять» способ упаковки, даже если сами C # и VB.NET это запрещают?)

  3. Возможно ли вообще (в C # или VB.NET) упаковать Nullable<T> как object?


person Dan Tao    schedule 23.09.2010    source источник
comment
Это JIT-компилятор, который реализует поведение. Подробнее здесь: stackoverflow.com/questions/1583050/   -  person Hans Passant    schedule 23.09.2010
comment
Я понимаю, что опаздываю на вечеринку на 7 лет. Но я хотел бы предложить всем любопытным, вроде меня, также прочитать справочный источник для более глубокого понимания. referenceource.microsoft.com/#mscorlib/system/   -  person Licht    schedule 27.03.2017


Ответы (3)


Происходят две вещи:

1) Компилятор рассматривает "null" не как пустую ссылку, а как нулевое значение ... нулевое значение для любого типа, в который ему нужно преобразовать. В случае Nullable<T> это просто значение, которое имеет значение False для поля / свойства HasValue. Итак, если у вас есть переменная типа int?, вполне возможно, что значение этой переменной будет null - вам просто нужно немного изменить свое понимание того, что означает null.

2) Упаковка типов, допускающих значение NULL, получает особую обработку со стороны самой среды CLR. Это актуально для вашего второго примера:

    int? i = new int?();
    object x = i;

компилятор будет помещать любое значение типа, допускающее значение NULL, иначе, чем значения типа, не допускающего значение NULL. Если значение не равно NULL, результат будет таким же, как при упаковке того же значения, что и значение типа, не допускающего значения NULL - поэтому int? со значением 5 будет помещено в ящик так же, как int со значением 5 - "допустимость NULL" потерян. Однако нулевое значение типа, допускающего значение NULL, помещается только в пустую ссылку, а не создает объект вообще.

Это было введено в конце цикла CLR v2 по просьбе сообщества.

Это означает, что не существует такой вещи, как «упакованное значение типа значения, допускающего значение NULL».

person Jon Skeet    schedule 23.09.2010
comment
Как обычно выглядящий IL-код, объект x = i преобразуется в блочную инструкцию, указывающую на поддержку на уровне CLR. - person VinayC; 23.09.2010
comment
Я понимаю, что вы имеете в виду, говоря об особой обработке бокса со стороны CLR: я просто написал быстрый тест, посмотрел на IL в Reflector и заметил, что компилятор C #, похоже, не делает ничего особенного для удаления бокса самого Nullable<T>. Но тогда не странно, что эффект упаковки Nullable<T> определяется независимо в спецификациях C # и VB.NET? Вы в курсе, что это тоже входит в спецификацию интерфейса командной строки? - person Dan Tao; 23.09.2010
comment
@Dan: Да, это в спецификации C #. Я не знаю, есть ли это в спецификации VB.NET. В спецификации CLI (ECMA-335) он находится в разделе 8.2.4 раздела 1. - person Jon Skeet; 23.09.2010
comment
Мне также удалось найти его в спецификации VB (в разделе 8.6.1, по крайней мере, для версии 9.0 спецификации). Я думаю, я просто не понимаю, почему это должно быть указано в трех местах - в спецификации CLI, а также в спецификациях C # и VB - вместо того, чтобы просто в спецификации CLI. - person Dan Tao; 23.09.2010

Вы правильно поняли: Nullable<T> получает особую обработку от компилятора как в VB, так и в C #. Следовательно:

  1. да. Компилятору языка требуется особый случай Nullable<T>.
  2. Компилятор перестраивает использование Nullable<T>. Операторы - это просто синтаксический сахар.
  3. Не то, что я знаю из.
person Konrad Rudolph    schedule 23.09.2010
comment
Итак, в ответ на ваш второй ответ: компилятор C # (например) должен рефакторинг object x = i; до чего-то вроде object x = i.HasValue ? (object)i.Value : null;, я прав? Это означает, что язык фактически полностью запрещает стандартное поведение бокса (я только что понял, что object x = (int?)5; делает x упакованным int, не Nullable<int>). Интересный... - person Dan Tao; 23.09.2010
comment
Что касается IL, в CLR есть специальная поддержка Nullable. Complier выдает команду box, и среда CLR проверяет значение, чтобы решить, установить ли ref в значение null или значение типа фактического значения box. - person VinayC; 23.09.2010
comment
Глядя на IL, сгенерированный с помощью GetRandomNullable<T> метода, который я только что использовал в Reflector, кажется, что VinayC прав: я не вижу, чтобы компилятор C # на самом деле реорганизовал упаковку, что подразумевает (для меня), что особая обработка на самом деле действительно происходит на уровне CLR. Но если это правда, то (мне) кажется странным, что поведение определяется в самих спецификациях языка. Есть предположения? - person Dan Tao; 23.09.2010
comment
@ Дэн Тао: для бокса это правда. Мой ответ касается только «перегрузки» оператора (== null тестирование, подъем оператора и т. Д.). - person Konrad Rudolph; 23.09.2010

Я задавал себе тот же вопрос, и я также ожидал, что у меня будет неявный оператор для Nullable<T> в .net Исходный код, допускающий значение NULL, поэтому я посмотрел, какой код IL соответствует int? a = null;, чтобы понять, что происходит за кулисами:

код c #:

int? a = null;
int? a2 = new int?();
object a3 = null;
int? b = 5;
int? b2 = new int?(5);

Код IL (сгенерированный с помощью LINQPad 5):

IL_0000:  nop         
IL_0001:  ldloca.s    00 // a
IL_0003:  initobj     System.Nullable<System.Int32>
IL_0009:  ldloca.s    01 // a2
IL_000B:  initobj     System.Nullable<System.Int32>
IL_0011:  ldnull      
IL_0012:  stloc.2     // a3
IL_0013:  ldloca.s    03 // b
IL_0015:  ldc.i4.5    
IL_0016:  call        System.Nullable<System.Int32>..ctor
IL_001B:  ldloca.s    04 // b2
IL_001D:  ldc.i4.5    
IL_001E:  call        System.Nullable<System.Int32>..ctor
IL_0023:  ret   

Мы видим, что компилятор меняет int? a = null на что-то вроде int? a = new int?(), что сильно отличается от object a3 = null. Очевидно, что для Nullables есть особая обработка компилятора.

person asidis    schedule 09.05.2018