Почему я должен поместить () в нулевое условное выражение, чтобы использовать корректную перегрузку метода?
У меня есть эти методы расширения и тип enum:
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?
, Я тогда проверил IL, и не было никаких бросков. Декомпиляция обратно в C# приводит к чему-то похожему:
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 не было бы той же проблемы.
4 ответа
Прежде всего, это поведение выглядит умышленно для меня. Редко кто-то добавляет методы расширения к обнуляемым типам, и очень часто люди смешивают нулевой условный и регулярный доступ к элементам в одном выражении, поэтому язык предпочитает последнее.
Рассмотрим следующие примеры:
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
равно нулю, мы ожидаем получить NPE ни в одном из случаев. Зачем? Потому что сразу после a
, Итак .c
часть должна быть оценена только если a
не был нулевым, делая доступ к члену зависимым от предыдущего результата нулевого условия. Добавляя явные скобки, (a?.b).c
мы заставляем компилятор пытаться получить доступ .c
из (a?.b)
невзирая на a
будучи нулевым, не позволяя ему "замкнуть" все выражение на нулевое. (используя слова @JamesBuck -s)
В твоем случае, x.Y?.Color.IsOneOf(Color.Red, Color.Green)
как a?.b.c
, Будет вызывать функцию с подписью bool IsOneOf(Color red)
(поэтому перегрузка, когда параметр не обнуляется, и я удалил общую часть), только когда x.Y
не был нулевым, таким образом, оборачивая тип выражения в Nullable для обработки случая, когда x.Y
нулевой. И потому что время оценивает bool?
вместо bool
, он не может быть использован в качестве теста в операторе if.
Значение 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.
Хорошо, я понял это, но я иду с ответом Тамаса. Тем не менее, я думаю, что здесь у меня есть ценность как средство мышления при работе с нулевыми условиями.
Ответ был отчасти уставился на меня в скриншоте в моем вопросе
Я смотрел в туннель на Color
/Color?
преобразование, но актуальная проблема заключается в том, что bool?
не может быть косвенно приведен к bool
,
В длинном "точечном" выражении, содержащем ?.
оператор, вы должны думать с точки зрения вещи, и ее тип, после крайней правой точки. В данном случае это метод расширения, который возвращает bool
, но использование ?.
эффективно "меняет" его на bool?
, Другими словами, думайте о самой правильной вещи как о ссылочном типе, который может быть null
или Nullable<T>
,
Вау, это заставило меня уйти.
Если вы поместите следующую строку, вы увидите, что.net создает динамический тип System.Nullable<UserQuery+Color>
для этого нулевого условного выражения в скобках.
Console.WriteLine((x.Y?.Color).GetType().ToString());
Если вы не поставите круглые скобки, он, вероятно, попытается оценить как нормальное выражение, таким образом, ошибку.