为什么我必须在 null 条件表达式周围放置 () 才能使用正确的方法重载?

Why do I have to place () around null-conditional expression to use the correct method overload?

我有这些扩展方法和枚举类型:

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? 的双重隐式转换,但是当我删除第一个扩展方法时,它在那里抱怨没有从 boolbool? 的隐式转换。然后我检查了 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 行不会有同样的问题。

如果放置以下行,您会看到 .net 正在为括号内的此空条件表达式创建动态类型 System.Nullable<UserQuery+Color>

Console.WriteLine((x.Y?.Color).GetType().ToString());

如果您不放置括号,它可能会尝试将其作为普通表达式求值,因此会出现错误。

x.Y?.Color.IsOneOf(Color.Red, Color.Green) 的含义完全独立于您的方法重载。 ?. 的伪操作数是 x.YColor.IsOneOf(Color.Red, Color.Green),即使后者本身不是有效表达式。首先,评估 x.Y 。调用结果 tmp。如果 tmp == null 那么整个表达式就是 null。如果 tmp != null,则整个表达式的计算结果为 tmp.Color.IsOneOf(Color.Red, Color.Green)

IsOneOf 是一个实例方法时,这几乎总是您想要的行为,这就是为什么这种行为最终进入规范和编译器的原因。

IsOneOf 是一个扩展方法时,就像在您的示例中一样,那么它可能不是您想要的行为,但行为是相同的,为了保持一致性并且因为有现成的您已经找到的解决方法。

The associativity and why the current result was picked are explained in a thread on the Roslyn CodePlex site.

好的,我明白了,但我要和 Tamas 一起去 。不过,我认为我的方法在这里很有价值,可以作为使用 null 条件时的思考辅助工具。

在我的问题的屏幕截图中,答案有点盯着我看

我对 Color/Color? 转换的看法很狭隘,但实际问题是 bool? 不能隐式转换为 bool

在包含 ?. 运算符的长 "dotted" 表达式中,您必须考虑事物及其类型,在最右边的点之后.那个东西,在这种情况下,是returns一个bool的扩展方法,但是使用?.有效地"changes"它到一个bool?。换句话说,将最右边的东西视为可以是 nullNullable<T>.

的引用类型

哇,这个让我兴奋不已。

首先,这种行为在我看来是故意的。很少有人向可空类型添加扩展方法,人们在一个表达式中混合空条件访问和常规成员访问是很常见的,因此语言倾向于后者。

考虑以下示例:

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.ca?.b?.c。如果 a 为空,我们期望在 none 个案例中获得 NPE。为什么?因为在 a 之后直接有一个 null 条件。因此 .c 部分必须仅在 a 不为空时才进行评估,从而使成员访问依赖于先前的空条件结果。通过添加显式括号,(a?.b).c 我们强制编译器尝试访问 (a?.b).c 而不管 a 是否为 null,从而阻止它 "short circuit" 整个表达式为空。 (使用@JamesBuck -s 的话)

在你的情况下,x.Y?.Color.IsOneOf(Color.Red, Color.Green) 就像 a?.b.c。它将仅在 x.Y 不为 null 时调用具有签名 bool IsOneOf(Color red) 的函数(因此参数不可为 null 的重载,并且我剥离了通用部分),从而将表达式的类型包装在 Nullable 中处理 x.Y 为空的情况。并且因为 while 的计算结果为 bool? 而不是 bool,它不能用作 if 语句中的测试。