做短路操作|| 和&&存在可空布尔? RuntimeBinder有时会这样认为

我阅读了条件逻辑运算符 ||上的C#语言规范 和&&也被称为短路逻辑运算符。 对我来说,这似乎不清楚,如果这些存在可空布尔,即操作数typesNullable<bool> (也写bool? ),所以我尝试了与非dynamictypes:

 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); } } 

令人惊讶的结果是,这无一例外地运行。

那么, xy并不奇怪,它们的声明会导致两个属性被检索,并且结果值如预期的那样, xtrueynull

但对A || B xx的评价 A || B导致没有绑定时间exception,只有属性A被读取,而不是B 为什么会这样呢? 正如你所看到的,我们可以改变B getter来返回一个疯狂的对象,比如"Hello world" ,并且xx仍然会在没有绑定问题的情况下评估为true

评估A && B (for yy )也导致没有绑定时间的错误。 当然,这里的两个属性都被检索到。 为什么运行时绑定程序允许这样做? 如果从B返回的对象更改为“坏”对象(如string ),则会发生绑定exception。

这是正确的行为? (你怎么能从规范中推断出来?)

如果你尝试B作为第一个操作数,都是B || A B || AB && A给出运行时绑定程序exception( B | AB & A正常工作,因为对于非短路运算符|& ,一切都正常)。

(尝试使用Visual Studio 2013的C#编译器,以及运行时版本.NET 4.5.2。)

首先,谢谢指出,在非dynamicnullable-bool的情况下,规范并不清楚。 我将在未来的版本中解决这个问题。 编译器的行为是预期的行为; &&|| 不应该在可空的布尔上工作。

但是,dynamic联编程序似乎并没有实现这个限制。 相反,它会分别绑定组件操作: & / |?: 。 因此,如果第一个操作数恰好是truefalse (这是布尔值,因此允许作为?:的第一个操作数),但是如果给出null作为第一个操作数(例如,如果尝试B && A在上面的例子中),你会得到一个运行时绑定exception。

如果你仔细想想,你可以看到为什么我们实现了dynamic&&|| 这种方式而不是一个大的dynamic操作:dynamic操作在操作数被评估之后在运行时绑定,以便绑定可以基于这些评估结果的运行时types。 但是这样的热切评价击败了短线操作者的目的! 相反,生成的代码用于dynamic&&|| 将评估分解成若干部分,并按以下步骤进行:

  • 评估左操作数(让我们调用结果x
  • 尝试通过隐式转换将其转换为bool ,或者使用truefalse运算符(如果无法运行则失败)
  • 使用x作为?:操作中的条件
  • 在真正的分支中,使用x作为结果
  • 在假分支中, 现在评估第二个操作数(我们称之为y
  • 尝试绑定&| 运算符基于xy的运行时types(如果不能,则失败)
  • 应用选定的操作员

这是允许通过某些“非法”操作数组合的行为: ?:操作符成功地将第一个操作数视为非可空布尔值, &| 运算符成功地将其视为空的布尔值,并且两者从不协调以检查他们是否同意。

所以这不是那种dynamic&&和|| 在空值工作。 只是它们碰巧是以一种有点过于宽松的方式实现的,与静态的情况相比。 这应该可能被认为是一个错误,但我们将永远不会修复它,因为这将是一个突破性的变化。 此外,它也不会帮助任何人收紧行为。

希望这解释发生了什么,为什么! 这是一个有趣的领域,我经常发现自己对我们实施dynamic决策的后果感到困惑。 这个问题很好吃 – 感谢您提出!

MADS

这是正确的行为?

是的,我很确定。

你怎么能从规范中推断出来?

C#规范版本5.0的7.12节有关于条件运算符&&|| 以及如何dynamic绑定与他们有关。 相关部分:

如果条件逻辑运算符的操作数具有编译时typesdynamic,则该expression式是dynamic绑定的(第7.2.2节)。 在这种情况下,expression式的编译时types是dynamic的, 下面描述分辨率将在运行时使用编译时types为dynamic 的那些操作数的运行时types来进行

我认为这是解决您的问题的关键。 运行时发生的分辨率是多less? 用户定义的条件逻辑操作符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将被使用falsetrue操作符转换为bool。 然后调用适当的逻辑运算符。 考虑到这一点,我们有足够的信息来回答您的其他问题。

但对A ||的xx的评价 B导致没有绑定时间exception,只有属性A被读取,而不是B.为什么会发生这种情况?

对于|| 我们知道它是true(A) ? A : |(A, B) true(A) ? A : |(A, B) 。 我们短路,所以我们不会得到一个绑定时间exception。 即使Afalse ,由于指定的分辨率步骤,我们仍然不会得到运行时绑定exception。 如果Afalse ,那么我们做| 运算符,它可以成功处理空值,根据7.11.4节。

评估A && B(for yy)也导致没有绑定时间的错误。 当然,这里两个属性都被检索到。 为什么运行时绑定程序允许这样做? 如果从B返回的对象更改为“坏”对象(如string),则会发生绑定exception。

由于类似的原因,这一个也适用。 &&被评估为false(x) ? x : &(x, y) false(x) ? x : &(x, y)A可以成功地转换为一个bool ,所以这里没有问题。 因为B是空的,所以&运算符被取消(7.3.7节)从一个bool到一个bool的一个bool? 参数,因此没有运行时exception。

对于这两个条件运算符,如果B不是bool(或dynamic空值),则运行时绑定将失败,因为它无法find将bool和非bool作为参数的重载。 然而,只有当A不能满足操作符的第一个条件时,才会发生这种情况(对||true ,对&&false )。 发生这种情况的原因是因为dynamic绑定非常懒惰。 它不会尝试绑定逻辑运算符,除非A是假的,并且必须沿着该path去评估逻辑运算符。 一旦A不能满足运营商的第一个条件,它将会失败并产生绑定exception。

如果你尝试B作为第一个操作数,都是B || A和B && A给运行库绑定例外。

希望到现在为止,你已经知道为什么会发生这种情况(或者我做了一个糟糕的解释)。 解决此条件运算符的第一步是取第一个操作数B ,并在处理逻辑操作之前使用其中一个布尔转换运算符( false(B)true(B) )。 当然, Bnull不能转换为truefalse ,所以发生运行时绑定exception。

Nullabletypes不定义条件逻辑运算符|| 和&&。 我build议你下面的代码:

 bool a = true; bool? b = null; bool? xxxxOR = (b.HasValue == true) ? (b.Value || a) : a; bool? xxxxAND = (b.HasValue == true) ? (b.Value && a) : false;