更新:这个问题是the subject of my blog in November 2018。感谢您提出有趣的问题!
文档暗示out 参数在发送到方法之前不需要初始化(仅声明)。
没错。此外,当调用返回时,传递给out 参数的变量肯定会被分配,因为正如您所注意到的:
方法 T 需要在返回之前设置变量,所以这个错误对我来说似乎是废话
看起来是这样,不是吗?外表可能具有欺骗性!
请注意,即使&& 短路,也必须执行第二个表达式才能执行if 的“结果”块。
令人惊讶的是,这是错误的。 即使对T 的调用没有执行,也有办法执行结果。这样做需要我们严重滥用 C# 的规则,但我们可以 ,所以让我们开始吧!
代替
dynamic p = "";
string s;
if (p != null && T(out s))
System.Console.WriteLine(s);
我们会做的
P p = new P();
if (p != null && T())
System.Console.WriteLine("in the consequence");
并给出class P 的定义,它会导致该程序运行结果,但不运行对T 的调用。
我们要做的第一件事是把p != null变成一个方法调用而不是空检查,并且那个方法不能返回bool:
class P
{
public static C operator ==(P p1, P p2)
{
System.Console.WriteLine("P ==");
return new C();
}
public static C operator !=(P p1, P p2)
{
System.Console.WriteLine("P !=");
return new C();
}
}
我们需要在 C# 中同时重载 == 和 !=。覆盖 Equals 和 GetHashCode 是个好主意,但不是必需的,而且这个程序中没有什么是好主意,所以我们将跳过它。
好的,所以我们现在有了if (something_of_type_C && T()),并且由于C 不是bool,我们需要覆盖&& 运算符。但是 C# 不允许您直接覆盖 && 运算符。让我们跑题,谈谈&& 的语义。对于布尔返回函数A 和B,bool result = A() && B(); 的语义是:
bool a = A();
bool c;
if (a == false) // interesting operation
c = a;
else
{
bool b = B();
c = a & b; // interesting operation
}
bool r = c;
所以我们生成三个临时变量a、b和c,我们评估左侧A(),我们检查a是否为假。如果是,我们使用它的值。如果不是,我们计算B(),然后计算a & b。
该工作流程中特定于布尔类型的仅有的两个操作是检查虚假和非短路&,所以*那些是在用户定义的&& 中重载的操作。 C# 要求您重载三个操作:用户定义的&,用户定义的“我是真的吗?”和用户定义的“我是假的吗?”。 (如== 和!=,最后两个必须成对定义。)
现在,明智的人会写operator true 和operator false,这样它们总是会返回相反的内容。我们今天不是理智的人:
class C
{
public static bool operator true(C c)
{
System.Console.WriteLine("C operator true");
return true;
}
public static bool operator false(C c)
{
System.Console.WriteLine("C operator false");
return true; // Oops
}
public static C operator &(C a, C b)
{
System.Console.WriteLine("C operator &");
return a;
}
}
请注意,我们还要求用户定义的 & 采用两个 Cs 并返回一个 C,它确实如此。
好吧,所以,记得我们有
if (p != null && T())
而p != null 的类型为C。所以我们现在必须将其生成为:
C a = p != null; // Call to P.operator_!=
C c;
bool is_false = a is logically false; // call to C.operator_false
if (is_false)
c = a;
else
{
bool b = T();
c = a & b; // Call to C.operator_&
}
但是现在我们遇到了一个问题。 operator & 接受两个 Cs 并返回一个 C,但我们有一个从 T 返回的 bool。我们需要一个C。没问题,我们将添加一个从bool 到C 的隐式用户定义转换:
public static implicit operator C(bool b)
{
System.Console.WriteLine("C implicit conversion from bool");
return new C();
}
好的,所以我们现在的逻辑是:
C a = p != null; // Call to P.operator_!=
C c;
bool is_false = C.operator_false(a);
if (is_false)
c = a;
else
{
bool t = T();
C b = t; // call to C.operator_implicit_C(bool)
c = a & b; // Call to C.operator_&
}
记住我们的目标是:
if (c)
System.Console.WriteLine("in the consequence");
我们如何计算这个? C# 的原因是,如果您在C 上有operator true,那么您应该能够通过简单地调用operator true 在if 条件下使用它。所以完成它,最终我们有了语义:
C a = p != null; // Call to P.operator_!=
C c;
bool is_false = C.operator_false(a);
if (is_false)
c = a;
else
{
bool t = T();
C b = t; // call to C.operator_implicit_C(bool)
c = a & b; // Call to C.operator_&
}
bool is_true = C.operator_true(c);
if (is_true) …
但是正如我们在这个疯狂的例子中看到的那样,我们可以输入if 的结果而不调用T 没问题只要operator false 和operator true 都返回true。当我们运行程序时,我们得到:
P !=
C operator false
C operator true
in the consequence
一个明智的人永远不会编写代码,其中C 被认为是同时为真和假,但像我今天这样不明智的人可以,编译器知道因为我们将编译器设计为正确无论程序是否合理。
这就解释了为什么if (p != null && T(out s)) 说s 可以在结果中取消分配。如果p 是dynamic,那么编译器的原因是“p 可能是运行时这些疯狂的类型之一,在这种情况下,我们不再使用bool 操作数,因此可能不会分配s” .
故事的寓意是:dynamic 使编译器对可能发生的事情非常保守;它必须假设最坏的情况。在这种特殊情况下,它必须假设p != null 可能不是 是空引用检查并且可能不是 是bool,并且operator true 和@987654404 @ 可能都返回 true。
那么,这是一个合法的错误吗(我使用的是 C# 7.0)?
编译器的分析是正确的——相信我,这不是一个容易编写或测试的逻辑。
您的代码有错误;修复它。
我应该如何处理?
如果您想对dynamic 进行空引用检查,最好的选择是:如果这样做会造成伤害,请不要这样做。
丢弃dynamic 并返回object,然后然后进行引用相等检查:if (((object)p) == null && …
或者,另一个不错的解决方案是使其非常明确:
if (object.ReferenceEquals((object)p, null) && …
这些是我的首选解决方案。更糟糕的解决方案是将其分解:
if (p != null)
if (T(out string s))
consequence
现在即使在最坏的情况下也不会调用operator &。请注意,尽管在这种情况下,我们仍然可能处于p != null 为真而p 为空的情况,因为没有什么可以阻止任何人重载!= 以始终返回真,而不管其操作数如何。