可空布尔值是否存在短路运算符 | | 和 & & ? RuntimeBinder 有时这样认为

我读了关于 条件逻辑运算符||&&的 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

但是对 A || Bxx的计算没有导致绑定时异常,只读取了属性 A,而没有读取属性 B。为什么会这样?正如您可以看到的,我们可以更改 B getter 以返回一个疯狂的对象,比如 "Hello world",而且 xx仍然可以计算到 true而不会出现绑定问题..。

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

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

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

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

3670 次浏览

这是正确的行为吗?

是的,我很确定。

你怎么能从规格推断出来?

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)是对所选运算符 & 的调用
  • 操作 x | | y 被计算为 T.true (x) ?X: T | (x,y) ,其中 T.true (x)是 T.中声明的操作符 true 的调用,而 T | (x,y)是所选操作符 | 的调用。

在这两种情况下,第一个操作数 x 将使用 falsetrue运算符转换为一个 bool。然后调用适当的逻辑运算符。考虑到这一点,我们有足够的信息来回答你们剩下的问题。

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

对于 ||操作符,我们知道它遵循 true(A) ? A : |(A, B)。我们短路了,所以不会有绑定时间异常。即使 Afalse,由于指定的解析步骤,还是也不会得到运行时绑定异常。如果 Afalse,那么我们执行 |操作符,它可以成功地处理空值,按照7.11.4节。

评估 a & b (针对广州欢聚时代)也不会导致绑定时间错误。当然,这里两个属性都被检索到。为什么运行时活页夹允许这样做?如果从 B 返回的对象更改为“坏”对象(如字符串) ,则会发生绑定异常。

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

对于这两个条件运算符,如果 B不是 bool (或 null Dynamic) ,则运行时绑定将失败,因为它无法找到以 bool 和 non-bool 作为参数的重载。但是,只有当 A不能满足操作符的第一个条件(true代表 ||false代表 &&)时才会发生这种情况。之所以会发生这种情况,是因为动态绑定非常懒惰。它不会尝试绑定逻辑运算符,除非 A为 false,并且它必须沿着这条路径计算逻辑运算符。一旦 A不能满足操作符的第一个条件,它将因绑定异常而失败。

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

希望到目前为止,您已经知道为什么会发生这种情况(或者我没有很好地解释)。解决这个条件运算符的第一步是在处理逻辑操作之前使用第一个操作数 B,并使用一个 bool 转换操作符(false(B)true(B))。当然,作为 nullB无法转换为 truefalse,因此会发生运行时绑定异常。

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

但是,动态绑定器似乎没有实现这个限制。相反,它分别绑定组件操作: &/|?:。因此,如果第一个操作数恰好是 true或者 false(它们是布尔值,因此允许作为 ?:的第一个操作数) ,它能够应付过去,但是如果你给 null作为第一个操作数(例如,如果你在上面的例子中尝试 B && A) ,你会得到一个运行时绑定异常。

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

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

这种行为允许某些操作数的“非法”组合: ?:操作符成功地将第一个操作数作为 不可取消布尔值处理,&|操作符成功地将其作为 无效布尔值处理,并且这两个操作符从不协调以检查它们是否一致。

所以对于空值来说,并不是动态的。只是与静态情况相比,它们的实现方式有点过于宽松了。这可能应该被认为是一个 bug,但是我们永远不会修复它,因为那将是一个重大的变化。而且,这也很难帮助任何人收紧这种行为。

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

小麦

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;