Почему я должен поместить () в нулевое условное выражение, чтобы использовать корректную перегрузку метода?

У меня есть эти методы расширения и тип 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());

Если вы не поставите круглые скобки, он, вероятно, попытается оценить как нормальное выражение, таким образом, ошибку.

Другие вопросы по тегам