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


84

我阅读了有关条件逻辑运算符 ||和的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);
  }
}

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

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

但评价xxA || B导致了没有绑定时异常,而只有财产A被读取,没有B。为什么会这样?如您所知,我们可以将Bgetter更改为返回疯狂的对象(例如)"Hello world",并且xx仍然可以得出true没有绑定问题的结果...

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

这是正确的行为吗?(如何从规范中推断出来?)

如果您尝试B作为第一个操作数,请同时给它们B || AB && A运行时绑定程序例外(B | A并且B & A可以正常工作,因为使用非短路运算符|和可以正常进行一切操作&)。

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


4
根本没有任何Nullable<Boolean>涉及的实例,只有装箱的布尔值被视为dynamic-与您的测试bool?无关。(当然,这不是一个完整的答案,只是一个问题的答案。)
Jeroen Mostert 2014年

3
这样A || B做有一定的道理,B除非除非A是假的,否则您不希望评估,不是。因此,您真的不知道表达式的类型。这个A && B版本更加令人惊讶-我将在规范中找到我所能找到的。
乔恩·斯基特

2
@JeroenMostert:嗯,除非编译器都决定,如果类型Abool和值Bnull,那么bool && bool?运营商可能参与。
乔恩·斯基特

4
有趣的是,这看起来像暴露了编译器或规范错误。C#5.0规范&&讨论如何解决它&,特别是包括两个操作数都为bool?-的情况,但随后它引用的下一部分将不处理可为空的情况。我可以添加一个答案,以对此进行更详细的说明,但是并不能完全解释它。
乔恩·斯基特

14
我已经向Mads发送了有关规范问题的电子邮件,以查看它是否只是我阅读过程中的一个问题...
Jon Skeet

Answers:


67

首先,感谢您指出规范在非动态nullable-bool情况下尚不清楚。我将在以后的版本中修复该问题。编译器的行为是预期的行为;&&并且||不应该在可为空的布尔值上工作。

但是,动态绑定器似乎并未实现此限制。而是将组件操作分别绑定:&/|?:。因此,它可以弄清楚第一个操作数是否恰好是truefalse(这是布尔值,因此被允许作为的第一个操作数?:),但是如果您给出null第一个操作数(例如,如果尝试B && A在上面的示例中过),则可以获取运行时绑定异常。

如果您考虑一下,您会明白为什么我们以这种方式实现动态&&||不是实现大型动态操作:动态操作在对操作数求值后在运行时进行绑定,因此绑定可以基于结果的运行时类型这些评估中。但是这种急切的评估无法达到使操作员短路的目的!所以取而代之,动态生成的代码&&||休息评估成小块,并会按下列程序进行:

  • 评估左操作数(让我们调用结果x
  • 尝试bool通过隐式转换或trueorfalse运算符将其转换为via (如果无法执行则失败)
  • 使用x如在条件?:操作
  • 在true分支中,x结果使用
  • 在false分支中,现在求值第二个操作数(让我们调用result y
  • 尝试根据and的运行时类型绑定&or|运算符(如果无法运行,则失败)xy
  • 应用选定的运算符

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

所以它不是动态的&&和||。在nullable上工作。与静态情况相比,只是它们的实现方式有点过分宽松。可能应该将其视为错误,但是我们永远都不会修复它,因为那将是一个重大的变化。而且,这几乎不会帮助任何人加强行为。

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

狂人


我看到这些短路运算符是特殊的,因为使用动态绑定时,在发生短路的情况下,实际上我们不允许知道第二个操作数的类型。也许规格中应该提到?当然,由于a内的所有内容dynamic都是装箱的,因此我们无法分辨出bool?whichHasValue和“ simple”之间的区别bool
Jeppe Stig Nielsen 2014年

6

这是正确的行为吗?

是的,我很确定。

您如何从规格中推断出来?

C#规范版本5.0的7.12节包含有关条件运算符&&以及||动态绑定与它们之间的关系的信息。相关部分:

如果条件逻辑运算符的操作数的编译时类型为dynamic,则该表达式将被动态绑定(第7.2.2节)。在这种情况下,表达式的编译时类型是动态的,并且下面描述解析将在运行时使用具有编译时类型动态的那些操作数的运行时类型进行

我认为,这是回答您问题的关键。运行时发生的分辨率是多少?第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运算符转换为布尔值。然后调用适当的逻辑运算符。考虑到这一点,我们有足够的信息来回答您的其余问题。

但是对A ||的xx的求值 B不会导致绑定时异常,并且只读取属性A,而不读取B。为什么会发生这种情况?

对于||操作员,我们知道true(A) ? A : |(A, B)。我们会短路,因此不会出现绑定时间异常。即使是Awas false,由于指定的解决步骤,我们仍然不会获得运行时绑定异常。如果A为is false,则|根据7.11.4节,执行可以成功处理空值的运算符。

评估A && B(对于yy)也不会导致绑定时间错误。当然,这里检索了两个属性。运行时绑定程序为什么允许这样做?如果从B返回的对象更改为“坏”对象(如字符串),则将发生绑定异常。

出于类似的原因,这也是可行的。&&被评估为false(x) ? x : &(x, y)A可以成功转换为bool,因此没有问题。因为B为null,所以将&运算符从采用a的操作符提升bool为采用bool?参数的操作符(第7.3.7节),因此没有运行时异常。

对于这两个条件运算符,如果B不是布尔值(或null动态值),则运行时绑定将失败,因为它找不到以布尔值和非布尔值作为参数的重载。但是,只有A在不满足运算符的第一个条件时(truefor ||falsefor &&),才会发生这种情况。发生这种情况的原因是因为动态绑定非常懒惰。除非A为false,否则它将不会尝试绑定逻辑运算符,并且必须沿着该路径进行操作以评估逻辑运算符。一旦A无法满足操作员的第一个条件,它将因绑定异常而失败。

如果将B用作第一个操作数,则两个B || A和B && A给出运行时绑定程序异常。

希望到现在为止,您已经知道为什么会发生这种情况(或者我在解释方面做得不好)。解决此条件运算符的第一步是采用第一个操作数B,并在处理逻辑运算之前使用一个布尔转换运算符(false(B)true(B))。当然B,存在null不能被转换到要么truefalse,因此运行时绑定发生异常。


dynamic绑定发生在运行时使用实例的实际类型而不是编译时类型(您的第一个引号)就不足为奇了。您的第二个引号无关紧要,因为此处没有类型会重载operator trueand operator false。一explicit operatorbool是别的东西比operator truefalse。很难以任何允许的方式A && B(在我的示例中)阅读规范,而又不允许a && b在静态时将aand和b静态类型为可空的布尔值(即bool? aand bool? b)绑定到编译时绑定。但这是不允许的。
Jeppe Stig Nielsen 2014年

-1

Nullable类型未定义条件逻辑运算符|| 和&&。我建议您遵循以下代码:

bool a = true;
bool? b = null;

bool? xxxxOR = (b.HasValue == true) ? (b.Value || a) : a;
bool? xxxxAND = (b.HasValue == true) ? (b.Value && a) : false;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.