Почему я должен помещать() вокруг нулевого условного выражения, чтобы использовать правильную перегрузку метода?
У меня есть эти методы расширения и тип перечисления:
public static bool IsOneOf<T>(this T thing, params T[] things)
{
return things.Contains(thing);
}
public static bool IsOneOf<T>(this T? thing, params T[] things) where T : struct
{
return thing.HasValue && things.Contains(thing.Value);
}
public enum Color { Red, Green, Blue }
Первый if
ниже компилируется; вторая не делает:
if ((x.Y?.Color).IsOneOf(Color.Red, Color.Green))
;
if (x.Y?.Color.IsOneOf(Color.Red, Color.Green))
;
Они зависят только от дополнительного набора круглых скобок. Почему я должен это делать?
Сначала я подозревал, что он выполнял двойное неявное преобразование с bool?
до bool
, а затем обратно на bool?
, но когда я удаляю первый метод расширения, он жалуется, что нет никакого неявного перевода из bool
до bool?
. Затем я проверил ИЛ и не было отливок. Декомпиляция обратно на С# приводит к тому, что выглядит следующим образом:
if (!(y != null ? new Color?(y.Color) : new Color?()).IsOneOf<Color>(new Color[2]
{
Color.Red,
Color.Green
}));
который подходит для версии CLR, в которой я запущен, и чего я ожидаю. Я не ожидал, что x.Y?.Color.IsOneOf(Color.Red, Color.Green)
не компилируется.
Что происходит? Это просто способ реализации языка, для которого требуется ()
?
Обновление
Вот экранная надпись, показывающая ошибку в контексте. Это становится еще более запутанным для меня. Ошибка действительно имеет смысл; но то, что нет (на мой взгляд, сейчас), почему строка 18 не будет иметь одинаковую проблему.
![введите описание изображения здесь]()
Ответы
Ответ 1
Прежде всего, это поведение выглядит преднамеренным для меня. Редко кто-то добавляет методы расширения к типам с возможностью NULL, и очень часто люди смешивают нулевые условные и регулярные обращения к члену в одном выражении, поэтому язык поддерживает последнее.
Рассмотрим следующие примеры:
class B { bool c; }
class A { B b; }
...
A a;
var output = a?.b.c; // infers bool?, throws NPE if (a != null && a.b == null)
// roughly translates to
// var output = (a == null) ? null : a.b.c;
а
A a;
var output = (a?.b).c; // infers bool, throws NPE if (a == null || a.b == null)
// roughly translates to
// var output = ((a == null) ? null : a.b).c;
а затем
A a;
var output = a?.b?.c; // infers bool?, *cannot* throw NPE
// roughly translates to
// var output = (a == null) ? null : (a.b == null) ? null : a.b.c;
// and this is almost the same as
// var output = (a?.b)?.c; // infers bool?, cannot throw NPE
// Only that the second `?.` is forced to evaluate every time.
Цель дизайна здесь, по-видимому, помогает разграничить между a?.b.c
и a?.b?.c
. Если a
равно null, мы ожидаем получить NPE в none. Зачем? Потому что существует нулевое условие непосредственно после a
. Таким образом, часть .c
должна оцениваться только в том случае, если a
не является нулевым, что делает доступ к члену зависимым от предыдущего результата. Добавляя явные скобки, (a?.b).c
, мы применяем компилятор, чтобы попытаться получить доступ к .c
из (a?.b)
, независимо от того, что a
имеет значение null, что предотвращает "короткое замыкание" всего выражения на null. (используя слова @JamesBuck -s)
В вашем случае x.Y?.Color.IsOneOf(Color.Red, Color.Green)
похож на a?.b.c
. Он вызовет функцию с сигнатурой bool IsOneOf(Color red)
(поэтому перегрузка, где параметр не может быть нулевым, и я разделил общую часть), только когда x.Y
не был null, таким образом, обертывая тип выражения в Nullable, чтобы обрабатывать случай, когда x.Y
имеет значение null. И поскольку while оценивает bool?
вместо bool
, он не может использоваться как тест в выражении if.
Ответ 2
Значение x.Y?.Color.IsOneOf(Color.Red, Color.Green)
не зависит от перегрузок вашего метода. ?.
псевдооперанды x.Y
и Color.IsOneOf(Color.Red, Color.Green)
, хотя последнее само по себе не является допустимым выражением. Сначала оценивается x.Y
. Вызвать результат tmp
. Если tmp == null
, то все выражение null
. Если tmp != null
, то все выражение оценивается как tmp.Color.IsOneOf(Color.Red, Color.Green)
.
Когда IsOneOf
является методом экземпляра, это почти всегда поведение, которое вам нужно, и именно поэтому это поведение, которое попадает в спецификацию и в компилятор.
Когда IsOneOf
- это метод расширения, как и в вашем примере, то это может быть не поведение, которое вам нужно, но поведение одинаковое, для согласованности и, поскольку имеется доступное обходное решение, вы уже нашли.
Ассоциативность и то, почему текущий результат был выбран, объясняются в потоке на сайте Roslyn CodePlex.
Ответ 3
Хорошо, я понял это, но я согласен с ответом . Я считаю, что моя ценность здесь, однако, как мыслящая помощь при работе с нулевыми условностями.
Ответ был как бы смотрел на меня в захвате экрана в моем вопросе
![Ошибка преобразования типа]()
Я был туннельным видением в преобразовании Color
/Color?
, но актуальной проблемой является то, что bool?
не может быть неявно приведен к bool
.
В длинном "пунктирном" выражении, содержащем оператор ?.
, вы должны думать в терминах вещи и ее типа после самой правой точки. В этом случае это метод расширения, который возвращает bool
, но использование ?.
эффективно "изменяет" его на bool?
. Другими словами, думать о самом правом, как о ссылочном типе, который может быть null
или Nullable<T>
.
Ничего себе, этот меня заставил меня идти.
Ответ 4
Если вы поместите следующую строку, вы увидите, что .net создает динамический тип System.Nullable<UserQuery+Color>
для этого нулевого условного выражения внутри круглых скобок.
Console.WriteLine((x.Y?.Color).GetType().ToString());
Если вы не помещаете скобки, он, вероятно, пытается оценить как нормальное выражение, таким образом, ошибку.