我在条件逻辑运算符 ||
和&&
上阅读了C#语言规范,也称为短路逻辑运算符。对我来说,似乎不清楚这些是否存在可空的布尔值,即操作数类型Nullable<bool>
(也写为bool?
),所以我尝试使用非动态类型:
bool a = true;
bool? b = null;
bool? xxxx = b || a; // compile-time error, || can't be applied to these types
这似乎解决了这个问题(我无法清楚地理解规范,但假设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);
}
}
令人惊讶的结果是,这种情况毫无例外。
好吧,x
和y
并不奇怪,他们的声明导致两个属性都被检索,结果值是预期的,x
是true
和{ {1}}是y
。
但null
xx
的评估不会导致绑定时异常,只会读取属性A || B
,而不是A
。为什么会这样?正如您所知,我们可以更改B
getter以返回一个疯狂的对象,例如B
,而"Hello world"
仍会评估为xx
而没有绑定问题...
评估true
(对于A && B
)也会导致无绑定时错误。当然,这里检索两个属性。为什么运行时绑定器允许这样做?如果从yy
返回的对象更改为&#34; bad&#34;对象(如B
),确实发生了绑定异常。
这是正确的行为吗? (你怎么能从规范中推断出来?)
如果您尝试string
作为第一个操作数,B
和B || A
都会给运行时绑定器异常(B && A
和B | A
正常工作,因为一切正常非短路运营商B & A
和|
)。
(尝试使用Visual Studio 2013的C#编译器和运行时版本.NET 4.5.2。)
答案 0 :(得分:65)
首先,感谢您指出该规范在非动态可空的bool情况下并不明确。我将在未来的版本中解决这个问题。编译器的行为是预期的行为; &&
和||
不应该使用可空的bool。
但是,动态绑定器似乎没有实现此限制。相反,它会单独绑定组件操作:&
/ |
和?:
。因此,如果第一个操作数恰好是true
或false
(它们是布尔值,因此被允许作为?:
的第一个操作数),它能够混淆,但是如果你给null
作为第一个操作数(例如,如果你在上面的例子中尝试B && A
),你会得到一个运行时绑定异常。
如果您考虑一下,您可以看到为什么我们以这种方式实现动态&&
和||
而不是作为一个大的动态操作:动态操作在运行时绑定评估,以便绑定可以基于这些评估结果的运行时类型。但是这种急切的评价却挫败了运营商短路的目的!因此,生成的动态&&
和||
代码会将评估分成几部分,并按以下步骤操作:
x
)bool
,或true
或false
运算符(如果无法运行则失败)x
操作?:
作为条件
x
作为结果y
)&
和|
的运行时类型绑定x
或y
运算符(如果无法运行则失败)这是允许某些&#34;非法&#34;操作数组合:?:
运算符成功将第一个操作数视为非可空布尔值,&
或|
运算符成功将其视为 nullable 布尔值,两者从不协调检查他们是否同意。
所以它不那么动态&amp;&amp;和||研究nullables。与静态情况相比,它们恰好以一种有点过于宽松的方式实现。这应该被视为一个错误,但我们永远不会修复它,因为这将是一个突破性的变化。此外,任何人都无法收紧行为。
希望这可以解释发生了什么以及为什么!这是一个有趣的领域,我经常发现自己对实施动态时做出的决策的后果感到困惑。这个问题很美味 - 感谢你提出来!
的Mads
答案 1 :(得分:6)
这是正确的行为吗?
是的,我很确定。
如何从规范中推断出来?
C#规范版本5.0的7.12节有关于条件运算符&&
和||
的信息以及动态绑定与它们的关系。相关部分:
如果条件逻辑运算符的操作数具有编译时类型dynamic,则表达式是动态绑定的(第7.2.2节)。在这种情况下,表达式的编译时类型是动态的,下面描述的分辨率将在运行时使用具有编译时类型动态的那些操作数的运行时类型进行
我认为这是回答你问题的关键点。在运行时发生的分辨率是多少?第7.12.2节,用户定义的条件逻辑运算符解释:
- 操作x&amp;&amp; y被评估为T.false(x)? x:T。&amp;(x,y),其中T.false(x)是在T中声明的运算符false的调用,而T.&amp;(x,y)是所选运算符的调用&amp; < / LI>
- 操作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&amp;&amp; B(对于yy)也导致没有绑定时间错误。当然,这里检索两个属性。为什么运行时绑定器允许这样做?如果从B返回的对象更改为“坏”对象(如字符串),则会发生绑定异常。
出于类似的原因,这个也有效。 &&
评估为false(x) ? x : &(x, y)
。 A
可以成功转换为bool
,因此没有问题。因为B
为空,所以&
运算符从bool
转换为采用bool?
参数的运算符(第7.3.7节),因此没有运行时异常。
对于两个条件运算符,如果B
不是bool(或null动态),则运行时绑定失败,因为它无法找到以bool和非bool作为参数的重载。但是,只有A
无法满足运算符的第一个条件(true
为||
,false
为&&
时才会出现这种情况。发生这种情况的原因是因为动态绑定非常懒惰。它不会尝试绑定逻辑运算符,除非A
为false并且必须沿着该路径来评估逻辑运算符。一旦A
无法满足运算符的第一个条件,它就会因绑定异常而失败。
如果你尝试B作为第一个操作数,那么B || A和B&amp;&amp;给运行时绑定程序例外。
希望到现在为止,你已经知道为什么会这样(或者我做了一个糟糕的工作解释)。解析此条件运算符的第一步是在处理逻辑运算之前采用第一个操作数B
,并使用一个bool转换运算符(false(B)
或true(B)
)。当然,B
,null
无法转换为true
或false
,因此会发生运行时绑定异常。
答案 2 :(得分:-1)
Nullable类型没有定义条件逻辑运算符||和&amp;&amp; 我建议你使用以下代码:
bool a = true;
bool? b = null;
bool? xxxxOR = (b.HasValue == true) ? (b.Value || a) : a;
bool? xxxxAND = (b.HasValue == true) ? (b.Value && a) : false;