更新:这个问题是我2018年11月博客的主题。感谢您提出有趣的问题!
该文档暗示out
参数在发送到方法之前不需要初始化(只需声明)。
这是正确的。此外,一个变量传递给out
当调用返回时,参数肯定被分配,因为正如您所注意到的:
方法 T 需要在返回之前设置变量,所以这个错误对我来说似乎是废话
看起来是这样,不是吗?外表会骗人!
请注意,即使短路&&
,第二个表达式必须执行才能执行“结果”块if
执行。
令人惊讶的是,这是错误的。即使调用了,也有一种方法可以执行结果T
不执行。这样做需要我们严重滥用 C# 的规则,但是我们can,所以让我们开始吧!
代替
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
这会导致该程序运行结果但是not运行调用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();
}
}
我们需要超载两者==
and !=
同时在C#中。覆盖Equals
and GetHashCode
是一个好主意,但不是必需的,并且该程序中没有任何内容是好主意,因此我们将跳过它。
好的,现在我们有了if (something_of_type_C && T())
,并且自从C
is not bool
,我们需要重写&&
操作员。但 C# 不允许你重写&&
直接操作员。让我们离题一下,谈谈语义&&
。对于返回布尔值的函数A
and B
,语义bool result = A() && B();
are:
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
, and c
,我们评估左侧A()
,我们检查是否a
是假的。如果是,我们就使用它的值。如果不是,我们计算B()
然后计算a & b
.
该工作流程中唯一的两个操作是特定于 bool 类型 are 检查是否虚假 and 非短路&
,所以*这些是在用户定义的操作中重载的操作&&
。 C# 要求您重载三个操作: 用户定义&
,用户定义“我是真的吗?”和用户定义的“我是假的吗?”。 (喜欢==
and !=
,最后两个必须成对定义。)
现在,一个明智的人会写operator true
and 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;
}
}
请注意,我们还要求用户定义&
拿两个C
s 并返回 aC
,确实如此。
好吧,所以,回想一下我们有
if (p != null && T())
and 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 &
需要两个C
s 并返回 aC
,但我们有一个bool
从返回T
。我们需要一个C
。没问题,我们将添加一个隐式的用户定义转换C
from bool
:
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# 的原因是如果你有operator true
on C
那么你应该能够在if
只需调用即可获得条件operator true
。完成后,最终我们得到了语义:
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
and operator true
两者都返回 true。当我们运行该程序时,我们得到:
P !=
C operator false
C operator true
in the consequence
一个明智的人永远不会编写这样的代码:C
被同时认为是对的和假的,但是像我今天这样不懂事的人可以,并且编译器知道,因为我们设计的编译器是正确的不管程序是否合理。
这就解释了为什么if (p != null && T(out s))
说s
可以在结果中取消分配。如果p
is dynamic
那么编译器的原因是“p
在运行时可能是这些疯狂类型之一,在这种情况下我们不再使用bool
操作数,因此s
可能不会被分配”。
这个故事的寓意是:dynamic
使编译器极其保守关于可能发生的事情;它必须假设最坏的情况。在这种特殊情况下,必须假设p != null
might not是一个空引用检查并且可能not be bool
, 然后operator true
and operator false
可能都会返回true
.
那么,这是一个合法的错误吗(我使用的是 C# 7.0)?
编译器的分析是正确的——相信我,这不是一个容易编写或测试的逻辑。
您的代码有错误;修理它。
我应该如何处理这个问题?
如果您想对某个对象进行空引用检查dynamic
,你最好的选择是:如果这样做会让你感到疼痛,那就不要这样做。
抛弃了dynamic
并回到object
, and then进行引用相等性检查:if (((object)p) == null && …
或者,另一个不错的解决方案是使其非常明确:if (object.ReferenceEquals((object)p, null) && …
这些是我首选的解决方案。更糟糕的解决方案是将其分解:
if (p != null)
if (T(out string s))
consequence
现在没有了operator &
即使在最坏的情况下也会打电话。请注意,在这种情况下,我们仍然可能处于这样的情况:p != null
是真的并且p
为空,因为没有什么可以阻止任何人超载!=
无论其操作数如何,始终返回 true。