Почему неявное преобразование оператора от принять?



это странное поведение, которое я не могу осмыслить. В моем примере у меня есть класс 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 назначается

210   3  

3 ответов:

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

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

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

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

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

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

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

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

этот оператор будет выбран в качестве первого оператора преобразования для типа nullable, потому что он более специфичен.

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

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

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

ошибка CS0619 'образец.неявный пример оператора(int?) 'устарело:' какое-то сообщение об ошибке'

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

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

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

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

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

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

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

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

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

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

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

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

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

    Ничего не найдено.

Добавить ответ:
Отменить.