可空布尔值是否存在短路运算符||和&&?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);
  }
}

出乎意料的是,这段代码运行时没有异常。
变量x和y不足为奇,它们的声明导致检索到两个属性,并且结果值符合预期,x为true,y为null。
但是对于xx的A || B表达式求值并没有绑定时异常,且只读取了属性A,而没有读取B。这是为什么呢?正如您所知道的,我们可以将getter方法中的B更改为返回一个非常规的对象,比如"Hello World",但xx仍然会在没有绑定问题的情况下计算为true...
对于yy的A && B 表达式求值也没有绑定时错误。当然,这里会检索到两个属性。为什么运行时绑定程序允许这种情况呢?如果将B返回的对象更改为不良对象(例如字符串),则会产生绑定异常。
这样做是否正确?从规范中你怎样推断?
如果试着将B作为第一个操作数, B || A 和 B && A 均会产生运行时绑定程序异常(使用非短路操作符|和&时一切正常)。
(尝试使用Visual Studio 2013的C#编译器和运行时版本.NET 4.5.2。)

4
完全没有涉及到Nullable<Boolean>的实例,只有被视为dynamic的装箱布尔值--你使用bool?进行的测试是不相关的。(当然,这并不是完整的答案,只是一个概括。) - Jeroen Mostert
你有尝试过 x.HasValue && x.Value || y.HasValue && y.Value 吗?你需要明确地表达出你想要如何处理 null 值。 - HABO
3
A || B 的意思是,如果 A 为真,就不需要计算 B 的值了,而 A 是为真的。所以你实际上不知道这个表达式的类型。A && B 的版本更加令人惊讶,我会查一下规范说明。 - Jon Skeet
2
@JeroenMostert:嗯,除非编译器决定如果A的类型是boolB的值为null,那么可能涉及到bool && bool?运算符。 - Jon Skeet
4
有趣的是,看起来这暴露了编译器或规范上的一个bug。C# 5.0版本中关于&&的规范提到应该将其解析为&,并着重讨论了两个操作数都是bool?类型的情况,但接下来所提到的部分没有处理可空类型的情况。我可以添加一些更详细的答案来解释这个问题,但那并不能完全解释它。 - Jon Skeet
15
我已经给马兹发送了一封电子邮件,询问规格问题是否只是我读取时的问题... - Jon Skeet
3个回答

67

首先,感谢指出规范在非动态可空布尔值情况下不清晰的问题。我将在未来的版本中修复这个问题。编译器的行为是预期的行为;&&||不能用于可空布尔值。

然而,动态绑定程序似乎没有实现这个限制。相反,它单独绑定组件操作: &/|?:。因此,如果第一个操作数恰好是truefalse(它们是布尔值,因此允许作为?:的第一个操作数),那么它就能够混淆。但是,如果您把null作为第一个操作数(例如,如果您尝试在上面的示例中执行B && A),则会导致运行时绑定异常。

如果您考虑一下,就可以看到为什么我们以这种方式实现动态&&||,而不是作为一个大型动态操作:动态操作在运行时绑定,在其操作数被评估之后,因此绑定可以基于这些评估结果的运行时类型。但是这样的急切评估破坏了短路运算符的目的!因此,动态&&||的生成代码将评估分成几个部分,并按以下方式进行:

  • 评估左操作数(我们称结果为x
  • 尝试通过隐式转换或truefalse运算符将其转换为bool(如果无法转换,则失败)
  • ?:操作中使用x作为条件
  • 在true分支中,将x用作结果
  • 在false分支中,现在评估第二个操作数(我们称结果为y
  • 尝试根据运行时类型的xy来绑定&|操作符(如果无法绑定则失败)
  • 应用所选的操作符
  • 这种行为允许通过某些“非法”操作数组合:?操作符将第一个操作数成功视为非空布尔值,&|操作符成功将其视为可空布尔值,而这两个操作数从不协调检查是否一致。

    因此,并不是动态&&||适用于可空类型。只是它们以稍微过于宽松的方式实现,相比之下,静态情况下则要严格得多。这可能应该被认为是一个错误,但我们永远不会修复它,因为那将是一个破坏性的变化。此外,收紧行为几乎不会对任何人有所帮助。

    希望这解释了发生了什么以及原因!这是一个有趣的领域,我经常发现自己被我们在实现动态时所做决策的后果弄糊涂。这个问题很有趣 - 谢谢你提出来!

    Mads


    我可以看出这些短路运算符很特殊,因为在动态绑定中,我们实际上不允许知道第二个操作数的类型,尤其是在我们进行短路操作时。也许规范应该提到这一点?当然,由于dynamic内部的所有内容都是装箱的,我们无法区分具有HasValuebool?和“简单”的`bool”。 - Jeppe Stig Nielsen

    6
    这是正确的行为吗?
    是的,我非常确定。
    你怎么能从规范中推断出来?
    C#规范版本5.0的第7.12节,包含关于条件运算符&&和||以及动态绑定相关的信息。相关部分如下:
    如果条件逻辑运算符的操作数具有编译时类型dynamic,则表达式是动态绑定的(§7.2.2)。在这种情况下,表达式的编译时类型是dynamic,并且使用编译时类型为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 || B的评估(xx),没有绑定时异常,并且只读取了属性A,而没有读取B。为什么会这样呢?
    对于||运算符,我们知道它遵循true(A) ? A : |(A, B)。我们进行短路计算,所以不会出现绑定时异常。即使A为false,由于指定的解析步骤,我们仍然不会出现运行时绑定异常。如果A为false,则执行|运算符,该运算符可以成功处理null值,参见第7.11.4节。
    对于A && B的评估(yy),也不会导致绑定时错误。这里当然会检索两个属性。为什么运行时绑定器允许这样做?如果从B返回的对象更改为“坏”对象(比如一个字符串),那么就会发生绑定异常。
    出于类似的原因,这个也有效。 && 被计算为 false(x) ? x : &(x, y)A 可以成功转换为 bool,所以没有问题。因为 B 是 null,所以 & 运算符被升级(第 7.3.7 节)从一个接受 bool 的运算符提升到了一个接受 bool? 参数的运算符,因此不会出现运行时异常。
    对于两个条件运算符,如果 B 不是 bool(或空的动态类型),则由于找不到将 bool 和非 bool 作为参数的重载,运行时绑定失败。但是,只有当 A 不能满足运算符的第一个条件时才会发生这种情况(|| 为 true,&& 为 false)。之所以会发生这种情况,是因为动态绑定非常懒惰。除非 A 为假并且必须通过该路径来评估逻辑运算符,否则它将不会尝试绑定逻辑运算符。一旦 A 不能满足运算符的第一个条件,它就会因绑定异常而失败。

    如果将 B 作为第一个操作数,则 B || A 和 B && A 都会出现运行时绑定器异常。

    希望你现在已经知道为什么会发生这种情况(或者我解释得不好)。解决条件运算符的第一步是取第一个操作数 B,并在处理逻辑操作之前使用一个 bool 转换运算符(false(B)true(B))中的一个。当然,由于 Bnull,所以不能转换为 truefalse,因此会出现运行时绑定异常。

    毫不奇怪,使用dynamic时绑定发生在运行时,使用实例的实际类型而非编译时类型(您的第一个引用)。您的第二个引用是无关紧要的,因为没有类型在此处重载了operator trueoperator false。返回boolexplicit operatoroperator truefalse不同。很难以任何方式阅读规范,允许A && B(在我的示例中),而不允许静态类型可空布尔值ab,即bool? abool? b,并在编译时进行绑定。然而,这是被禁止的。 - Jeppe Stig Nielsen

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

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接