Почему оператор неявного преобразования из <T>в <U>принимает <T?>? - PullRequest
0 голосов
/ 17 мая 2018

Это странное поведение, которое я не могу понять. В моем примере у меня есть класс 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);
}

Проблема возникает при использовании типа значения Nullable для 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. Можно ли запретить компиляцию кода в этом сценарии?

Ответы [ 3 ]

0 голосов
/ 17 мая 2018

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

Пример кода из исходного кода 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.

0 голосов
/ 17 мая 2018

Мне кажется, это отмененный оператор преобразования в действии.Спецификация говорит, что:

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

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

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

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

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

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

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

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

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

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

0 голосов
/ 17 мая 2018

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

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: Так почему это возможно? Я не смог найти его в спецификации, потому что это преднамеренное нарушение спецификации, и это только для обратной совместимости. Вы можете прочитать об этом в код :

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

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

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

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

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

ЧАСТЬ 2 Как предотвратить компиляцию кода в этом сценарии? Ну, есть способ. Вы можете определить дополнительный неявный оператор специально для обнуляемого типа и украсить его атрибутом 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();
}

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

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

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

Это может привести к ошибке, если в любом месте кода есть ссылка:

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

...