做短路操作器 ||&存在可空布尔值?运行时绑定器有时是这样认为的



我阅读了关于条件逻辑运算符||&&的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并不奇怪,它们的声明会导致检索两个属性,并且结果值符合预期,x truey null

但是对A || B xx的评估不会导致绑定时间异常,并且只读取了属性A,而不是B。为什么会这样?如您所见,我们可以更改B getter以返回一个疯狂的对象,例如"Hello world",并且xx仍然可以评估为没有绑定问题的true...

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

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

如果您尝试B作为第一个操作数,B || AB && A都会给出运行时绑定器异常(B | A并且B & A工作正常,因为非短路运算符|&一切正常(。

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

首先,感谢您指出规范在非动态可为空布尔的情况下不清楚。我将在未来的版本中修复它。编译器的行为是预期行为; &&||不应该在可为空的布尔值上工作。

但是,动态绑定器似乎没有实现此限制。相反,它分别绑定组件操作: &/|?: .因此,如果第一个操作数恰好是truefalse(它们是布尔值,因此允许作为?:的第一个操作数(,它就可以蒙混过关,但是如果你给出null作为第一个操作数(例如,如果你在上面的示例中尝试B && A(,你确实会得到一个运行时绑定异常。

如果您考虑一下,您就会明白为什么我们以这种方式实现动态&&||,而不是作为一个大的动态操作:动态操作在计算其操作数后在运行时绑定,以便绑定可以基于这些评估结果的运行时类型。但如此急切的评估违背了短路运营商的目的!因此,为动态&&||生成的代码会将评估分解为多个部分,并将按如下方式进行:

  • 计算左操作数(我们将结果称为x(
  • 尝试通过隐式转换或truefalse运算符将其转换为bool(如果无法,则失败(
  • ?:操作中使用x作为条件
  • 在 true 分支中,使用 x 作为结果
  • 在 false 分支中,现在计算第二个操作数(我们将结果称为 y (
  • 尝试根据xy的运行时类型绑定&|运算符(如果无法绑定,则失败(
  • 应用所选运算符

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

所以它不是动态的&&&和||在可空的工作。只是与静态情况相比,它们的实现方式恰好有点过于宽松。这可能应该被视为一个错误,但我们永远不会修复它,因为这将是一个重大更改。此外,它几乎无助于任何人收紧行为。

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

麦德斯

这是正确的行为吗?

是的,我很确定是的。

您如何从规范中推断出这一点?

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

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

我认为这是回答你问题的关键点。运行时发生的分辨率是什么?部分 7.12.2, 用户定义的条件逻辑运算符解释:

    操作 x && y 的计算结果为 T.false(x( ? x : T.&(x, y(,其中 T.false(x( 是对 T 中声明的运算符 false 的调用,
  • T.&(x, y( 是对所选运算符 false 的调用
  • 运算 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)。我们短路,所以我们不会得到绑定时间异常。即使Afalse,由于指定的解决步骤,我们仍然不会得到运行时绑定异常。如果Afalse,然后我们按照第 7.11.4 节执行 | 运算符,该运算符可以成功处理空值。

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

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

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

如果尝试 B 作为第一个操作数,则两个 B ||A 和 B && A 给出运行时绑定器异常。

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

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;

相关内容

最新更新