做短路运营商|| 和&&存在可空的布尔?RuntimeBinder有时会这么认为

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)

令人惊讶的结果是,这种情况毫无例外地运行.

好了,xy并不奇怪,他们的声明导致被检索到的这两个属性,并将得到的值如预期,xtrueynull.

但评价xxA || B导致了没有绑定时异常,而只有财产A被读取,没有B.为什么会这样?你可以告诉我们,我们可以改变Bgetter来返回一个疯狂的对象"Hello world",并且xx仍然会在true没有绑定问题的情况下进行评估...

评估A && B(for yy)也会导致没有绑定时错误.当然,这里检索两个属性.为什么运行时绑定器允许这样做?如果返回的对象从B更改为"坏"对象(如a string),则会发生绑定异常.

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

如果试图B为第一个操作数,无论是B || AB && A给运行时绑定异常(B | AB & A为一切正常与非短路运营商做工精细|&).

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

小智 66

首先,感谢您指出该规范在非动态可空bool案例中并不明确.我将在未来的版本中解决这个问题.编译器的行为是预期的行为; &&并且||不应该在可空的bool上工作.

但是,动态绑定器似乎没有实现此限制.相反,它分别绑定组件操作:&/ |?:.因此,如果第一个操作数恰好是true或者false(它是布尔值并因此允许作为第一个操作数?:),它能够混淆,但如果你给出null第一个操作数(例如,如果你B && A在上面的例子中尝试),你可以获取运行时绑定异常.

如果你考虑一下,你可以看到为什么我们实现动态&&||这种方式而不是一个大的动态操作:动态操作在它们的操作数被评估之后在运行时被绑定,因此绑定可以基于结果的运行时类型那些评估.但是这种急切的评价却挫败了运营商短路的目的!所以取而代之,动态生成的代码&&,并||打破了评价成片,将进行如下操作:

  • 评估左操作数(让我们调用结果x)
  • 尝试将其转换为boolvia隐式转换,true或者false运算符(如果不能则失败)
  • 使用x如在条件?:操作
  • 在真正的分支中,使用x结果
  • 在false分支中,现在计算第二个操作数(让我们调用结果y)
  • 尝试绑定&|基于运行时类型的运营商xy(如果不能失败)
  • 应用选定的运算符

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

所以这不是那种动态的&&和|| 研究nullables.与静态情况相比,它们恰好以一种有点过于宽松的方式实现.这应该被视为一个错误,但我们永远不会修复它,因为这将是一个突破性的变化.也很难帮助任何人收紧行为.

希望这能解释发生了什么以及为什么!这是一个有趣的领域,我经常发现自己对我们实施动态时做出的决策的后果感到困惑.这个问题很美味 - 感谢你提出来!

MADS


Chr*_*ens 6

这是正确的行为吗?

是的,我很确定.

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

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将使用falsetrue运算符转换为bool .然后调用适当的逻辑运算符.考虑到这一点,我们有足够的信息来回答您的其余问题.

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

对于||运营商,我们知道如下true(A) ? A : |(A, B).我们短路,所以我们不会得到绑定时间异常.即使Afalse,我们仍然不会得到运行时绑定异常,因为指定的解析步骤.如果Afalse,那么我们|按照第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未满足运算符的第一个条件(truefor ||,falsefor &&)时才会发生这种情况.发生这种情况的原因是因为动态绑定非常懒惰.它不会尝试绑定逻辑运算符,除非A是false并且必须沿着该路径去评估逻辑运算符.一旦A无法满足运算符的第一个条件,它将失败并出现绑定异常.

如果你尝试B作为第一个操作数,那么B || A和B && A给出了运行时绑定程序异常.

希望到现在为止,你已经知道为什么会这样(或者我做了一个糟糕的工作).解析此条件运算符的第一步是获取第一个操作数B,并在处理逻辑操作之前使用其中一个bool转换运算符(false(B)true(B)).当然,B,存在null不能被转换到要么truefalse,因此运行时绑定发生异常.