Почему оператор неявного преобразования из <t> <u>принять</u> <T?> <u>?</u>

Это странное поведение, о котором я не могу понять. В моем примере у меня есть класс Sample<T> и неявный оператор преобразования от T до Sample<T>.

private class Sample<T>
{
   public readonly T Value;

   public Sample(T value)
   {
      Value = value;
   }

   public static implicit operator Sample<T>(T value) => new Sample<T>(value);
}

Проблема возникает при использовании типа значения NULL для T такого как int? ,

{
   int? a = 3;
   Sample<int> sampleA = a;
}

Вот ключевая часть:
По-моему, это не должно компилироваться, потому что Sample<int> определяет преобразование из int в Sample<int> но не из int? Sample<int>. Но он компилируется и работает успешно! (Под этим я подразумеваю, что оператор преобразования вызывается, а 3 - для поля readonly.)

И это становится еще хуже. Здесь оператор преобразования не вызывается, а sampleB будет иметь значение null:

{
   int? b = null;
   Sample<int> sampleB = b;
}

Большой ответ, вероятно, будет разделен на две части:

  1. Почему код в первом фрагменте компиляции?
  2. Могу ли я предотвратить компиляцию кода в этом сценарии?

Ответ 1

Вы можете посмотреть, как компилятор снижает этот код:

int? a = 3;
Sample<int> sampleA = a;

в это:

int? nullable = 3;
int? nullable2 = nullable;
Sample<int> sample = nullable2.HasValue ? ((Sample<int>)nullable2.GetValueOrDefault()) : null;

Поскольку Sample<int> является классом, его экземпляру может быть присвоено нулевое значение, и с таким неявным оператором также может быть назначен базовый тип объекта с нулевым значением. Таким образом, такие присвоения действительны:

int? a = 3;
int? b = null;
Sample<int> sampleA = a; 
Sample<int> sampleB = b;

Если Sample<int> будет struct, это, конечно, даст ошибку.

EDIT: Так почему это возможно? Я не мог найти его в спецификации, потому что это преднамеренное нарушение спецификации, и это поддерживается только для обратной совместимости. Вы можете прочитать об этом в коде:

НАРУШЕНИЕ СПЕЦИФИКАЦИИ СПЕЦИФИКАЦИИ:
Собственный компилятор допускает "отмененное" преобразование, даже если тип возврата преобразования не является типом значения, не равным нулю. Например, если у нас есть преобразование из struct S в строку, то "отмененное" преобразование из S? к строке рассматривается родным компилятором, чтобы существовать, с семантикой "s.HasValue? (string) s.Value: (string) null". Компилятор Roslyn увековечивает эту ошибку ради обратной совместимости.

То, как эта "ошибка" реализована в Roslyn:

В противном случае, если возвращаемый тип преобразования является типом нулевых значений, ссылочным типом или типом указателя P, тогда мы понижаем его как:

temp = operand
temp.HasValue ? op_Whatever(temp.GetValueOrDefault()) : default(P)

Таким образом, согласно спецификации для заданного пользовательского оператора преобразования T → U существует поднятый оператор T? → U? T? → U? где T и U - типы значений с нулевым значением. Однако такая логика также реализована для оператора преобразования, где U является ссылочным типом из-за вышеуказанной причины.

ЧАСТЬ 2 Как предотвратить компиляцию кода в этом сценарии? Ну есть путь. Вы можете определить дополнительный неявный оператор специально для типа NULL и украсить его атрибутом Obsolete. Для этого требуется, чтобы параметр типа T был ограничен struct:

public class Sample<T> where T : struct
{
    ...

    [Obsolete("Some error message", error: true)]
    public static implicit operator Sample<T>(T? value) => throw new NotImplementedException();
}

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

Если вы не можете сделать такое ограничение, вы должны определить каждый оператор для каждого типа значения отдельно (если вы действительно уверены, что можете использовать отражение и генерировать код с использованием шаблонов):

[Obsolete("Some error message", error: true)]
public static implicit operator Sample<T>(int? value) => throw new NotImplementedException();

Это дало бы ошибку, если ссылаться в любом месте в коде:

Ошибка CS0619 "Sample.implicit operator Пример (int?)" Устарел: "Некоторое сообщение об ошибке"

Ответ 2

Я думаю, что он поднял оператор преобразования в действии. Спецификация говорит, что:

С учетом пользовательского оператора преобразования, который преобразует из значения типа N с нулевыми значениями в тип N с непустым значением T, существует оператор с отмененным преобразованием, который преобразует из S? к Т?. Этот оператор с отмененным преобразованием выполняет развертывание с S? на S, за которым следует пользовательское преобразование из S в T, за которым следует обертка из T в T?, за исключением того, что нулевое значение S? преобразуется непосредственно к нулевому значению T?.

Похоже, что здесь это не применимо, потому что в то время как тип S является типом значений здесь (int), тип T не является типом значения (класс Sample). Однако эта проблема в репозитории Roslyn заявляет, что это на самом деле ошибка в спецификации. И документация на код Roslyn подтверждает следующее:

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

Фактически, собственный компилятор определяет, следует ли проверять отмененную форму на основе:

  • Является ли тип, который мы в конечном счете преобразуем из типа значения NULL?
  • Является ли тип параметра преобразования нечетным значением типа?
  • Является ли тип, который мы в конечном счете переходим к типу нулевых значений, типу указателя или ссылочному типу?

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

Если компилятор выполнит спецификацию - это приведет к ошибке в этом случае, как вы ожидаете (и в некоторых старых версиях), но теперь это не так.

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

  • Это считается ошибкой в спецификации, а не в компиляторе.
  • Спецификация уже была нарушена старым компилятором pre-roslyn, и хорошо поддерживать обратную совместимость.

Как описано в первой цитате, описывающей, как работает оператор-оператор (с добавлением, что мы разрешаем T быть ссылочным типом), вы можете заметить, что он точно описывает, что происходит в вашем случае. null value S (int?) присваивается непосредственно T (Sample) без оператора преобразования, а non-null разворачивается в int и запускается через ваш оператор (обертка к T? очевидно, не нужна, если T является ссылочным типом).

Ответ 3

Почему код в первом фрагменте компиляции?

Образец кода из исходного кода Nullable<T> который можно найти здесь:

[System.Runtime.Versioning.NonVersionable]
public static explicit operator T(Nullable<T> value) {
    return value.Value;
}

[System.Runtime.Versioning.NonVersionable]
public T GetValueOrDefault(T defaultValue) {
    return hasValue ? value : defaultValue;
}

Строка Nullable<int> имеет переопределенный явный оператор, а также метод GetValueOrDefault один из этих двух используется компилятором для преобразования int? к T

После этого он запускает implicit operator Sample<T>(T value).

Грубая картина происходящего заключается в следующем:

Sample<int> sampleA = (Sample<int>)(int)a;

Если мы напечатаем typeof(T) внутри неявного оператора Sample<T> он отобразит: System.Int32.

В вашем втором сценарии компилятор не использует implicit operator Sample<T> и просто присваивает null sampleB.