Jep*_*sen 84 c# nullable short-circuiting logical-operators dynamictype
我读上的C#语言规范条件逻辑运算符 ||
和&&
,也被称为短路逻辑运算符.对我来说,似乎不清楚这些是否存在可空的布尔值,即操作数类型Nullable<bool>
(也是书面的bool?
),所以我尝试使用非动态类型:
bool a = true;
bool? b = null;
bool? xxxx = b || a; // compile-time error, || can't be applied to these types
Run Code Online (Sandbox Code Playgroud)
这似乎解决了这个问题(我无法清楚地理解规范,但假设Visual C#编译器的实现是正确的,现在我知道).
但是,我也想尝试dynamic
绑定.所以我尝试了这个:
static class Program
{
static dynamic A
{
get
{
Console.WriteLine("'A' evaluated");
return true;
}
}
static dynamic B
{
get
{
Console.WriteLine("'B' evaluated");
return null;
}
}
static void Main()
{
dynamic x = A | B;
Console.WriteLine((object)x);
dynamic y = A & B;
Console.WriteLine((object)y);
dynamic xx = A || B;
Console.WriteLine((object)xx);
dynamic yy = A && B;
Console.WriteLine((object)yy);
}
}
Run Code Online (Sandbox Code Playgroud)
令人惊讶的结果是,这种情况毫无例外地运行.
好了,x
而y
并不奇怪,他们的声明导致被检索到的这两个属性,并将得到的值如预期,x
是true
和y
是null
.
但评价xx
的A || B
导致了没有绑定时异常,而只有财产A
被读取,没有B
.为什么会这样?你可以告诉我们,我们可以改变B
getter来返回一个疯狂的对象"Hello world"
,并且xx
仍然会在true
没有绑定问题的情况下进行评估...
评估A && B
(for yy
)也会导致没有绑定时错误.当然,这里检索两个属性.为什么运行时绑定器允许这样做?如果返回的对象从B
更改为"坏"对象(如a string
),则会发生绑定异常.
这是正确的行为吗?(你怎么能从规范中推断出来?)
如果试图B
为第一个操作数,无论是B || A
和B && A
给运行时绑定异常(B | A
和B & A
为一切正常与非短路运营商做工精细|
和&
).
(尝试使用Visual Studio 2013的C#编译器和运行时版本.NET 4.5.2.)
小智 66
首先,感谢您指出该规范在非动态可空bool案例中并不明确.我将在未来的版本中解决这个问题.编译器的行为是预期的行为; &&
并且||
不应该在可空的bool上工作.
但是,动态绑定器似乎没有实现此限制.相反,它分别绑定组件操作:&
/ |
和?:
.因此,如果第一个操作数恰好是true
或者false
(它是布尔值并因此允许作为第一个操作数?:
),它能够混淆,但如果你给出null
第一个操作数(例如,如果你B && A
在上面的例子中尝试),你可以获取运行时绑定异常.
如果你考虑一下,你可以看到为什么我们实现动态&&
和||
这种方式而不是一个大的动态操作:动态操作在它们的操作数被评估之后在运行时被绑定,因此绑定可以基于结果的运行时类型那些评估.但是这种急切的评价却挫败了运营商短路的目的!所以取而代之,动态生成的代码&&
,并||
打破了评价成片,将进行如下操作:
x
)bool
via隐式转换,true
或者false
运算符(如果不能则失败)x
如在条件?:
操作x
结果y
)&
或|
基于运行时类型的运营商x
和y
(如果不能失败)这是允许操作数的某些"非法"组合的行为:?:
运算符成功地将第一个操作数视为不可空的布尔值,&
或者|
运算符成功地将其视为可以为空的布尔值,并且两者从不协调以检查它们是否同意.
所以这不是那种动态的&&和|| 研究nullables.与静态情况相比,它们恰好以一种有点过于宽松的方式实现.这应该被视为一个错误,但我们永远不会修复它,因为这将是一个突破性的变化.也很难帮助任何人收紧行为.
希望这能解释发生了什么以及为什么!这是一个有趣的领域,我经常发现自己对我们实施动态时做出的决策的后果感到困惑.这个问题很美味 - 感谢你提出来!
MADS
这是正确的行为吗?
是的,我很确定.
你怎么能从规范中推断出来?
C#Specification Version 5.0的7.12节有关于条件运算符&&
以及||
动态绑定如何与它们相关的信息.相关部分:
如果条件逻辑运算符的操作数具有动态编译时类型,则表达式是动态绑定的(第7.2.2节).在这种情况下,表达式的编译时类型是动态的,下面描述的解析将在运行时使用具有编译时类型dynamic 的那些操作数的运行时类型进行.
我认为这是回答你问题的关键点.在运行时发生的分辨率是多少?第7.12.2节,用户定义的条件逻辑运算符解释:
- 操作x && y被评估为T.false(x)?x:T.&(x,y),其中T.false(x)是在T中声明的运算符false的调用,并且T.&(x,y)是所选运算符的调用&
- 操作x || y被评估为T.true(x)?x:T.|(x,y),其中T.true(x)是在T中声明的运算符true的调用,而T. |(x,y)是所选运算符|的调用.
在这两种情况下,第一个操作数x将使用false
或true
运算符转换为bool .然后调用适当的逻辑运算符.考虑到这一点,我们有足够的信息来回答您的其余问题.
但是对A ||的xx的评估 B导致没有绑定时间异常,只读取属性A,而不是B.为什么会发生这种情况?
对于||
运营商,我们知道如下true(A) ? A : |(A, B)
.我们短路,所以我们不会得到绑定时间异常.即使A
是false
,我们仍然不会得到运行时绑定异常,因为指定的解析步骤.如果A
是false
,那么我们|
按照第7.11.4节的规定执行运算符,该运算符可以成功处理空值.
评估A && B(对于yy)也会导致没有绑定时错误.当然,这里检索两个属性.为什么运行时绑定器允许这样做?如果从B返回的对象更改为"坏"对象(如字符串),则会发生绑定异常.
出于类似的原因,这个也有效.&&
被评估为false(x) ? x : &(x, y)
.A
可以成功转换为a bool
,所以那里没有问题.因为B
为空,则&
操作者从一个接受一个抬起(第7.3.7节)bool
,以一个取bool?
参数,并且因此不存在运行时异常.
对于两个条件运算符,if B
不是bool(或null动态),运行时绑定失败,因为它无法找到以bool和非bool作为参数的重载.但是,只有在A
未满足运算符的第一个条件(true
for ||
,false
for &&
)时才会发生这种情况.发生这种情况的原因是因为动态绑定非常懒惰.它不会尝试绑定逻辑运算符,除非A
是false并且必须沿着该路径去评估逻辑运算符.一旦A
无法满足运算符的第一个条件,它将失败并出现绑定异常.
如果你尝试B作为第一个操作数,那么B || A和B && A给出了运行时绑定程序异常.
希望到现在为止,你已经知道为什么会这样(或者我做了一个糟糕的工作).解析此条件运算符的第一步是获取第一个操作数B
,并在处理逻辑操作之前使用其中一个bool转换运算符(false(B)
或true(B)
).当然,B
,存在null
不能被转换到要么true
或false
,因此运行时绑定发生异常.