我知道||
和&&
被定义为C#中的短路运算符,这种行为是由语言规范保证的,但是|=
和&=
短路呢?
例如:
private bool IsEven(int n)
{
return n % 2 == 0;
}
private void Main()
{
var numbers = new int[] { 2, 4, 6, 8, 9, 10, 14, 16, 17, 18, 20 };
bool allEven = true;
bool anyOdd = false;
for (int i = 0; i < numbers.Length; i++)
{
allEven &= IsEven(numbers[i]);
anyOdd |= !IsEven(numbers[i]);
}
}
当命中9条目时,allEven
变为false,表示所有后续条目都不相关 - 对于将来对该表达式的所有调用,allEven
的值保证为false。 anyOdd
也是如此,当它看到9时设置为true,并且对于该表达式的所有后续调用都将保持为真。
那么,做&=
和|=
快捷方式,还是保证在每次迭代时调用IsEven
?在这种情况下,语言规范中是否有任何已定义的行为?是否存在这种短路问题的角落?
答案 0 :(得分:19)
C#规范保证双方从左到右精确评估一次,并且不会发生短路。
5.3.3.21具有嵌入式表达式的表达式的一般规则
以下规则适用于这些类型的表达式:带括号的表达式(第7.6.3节),元素访问表达式(第7.6.6节),带索引的基本访问表达式(第7.6.8节),递增和递减表达式(§ 7.6.9,§7.7.5),强制转换表达式(§7.7.6),一元+, - ,〜,*表达式,二进制+, - ,*,/,%,&lt;&lt;,&gt;&gt;, &lt;,&lt; =,&gt;,&gt; =,==,!=,is,as,&amp;,|,^表达式(§7.8,§7.9,§7.10,§7.11),复合赋值表达式(第7.17.2节),经过检查和未经检查的表达式(第7.6.12节),以及数组和委托创建表达式(第7.6.10节)。
这些表达式中的每一个都有一个或多个以固定顺序无条件评估的子表达式。
复合运营商的C#规范说:
7.17.2化合物分配
...
通过应用二元运算符重载决策(第7.3.4节)处理形式
x op= y
的操作,就好像操作是写x op y
一样。然后,
如果所选运算符的返回类型可隐式转换为
x
类型,则操作将计算为x = x op y
,但x
仅计算一次。否则,如果所选运算符是预定义运算符,则所选运算符的返回类型可明确转换为
x
类型,并且y
可隐式转换为类型x
或运算符是移位运算符,然后操作被评估为x = (T)(x op y)
,其中T是x
的类型,除了x
仅被评估一次...
在您的情况下,op
为&
或|
。短路行为反映了&
/ |
而非&&
/ ||
的行为。
请注意,这仅指单线程场景中可见的行为。因此,如果右侧没有在这种情况下可观察到的副作用,编译器或JITter仍然可以省略评估。
在您的示例中,编译器一旦知道结果就可以自由地终止循环,因为没有这样的副作用。但并不要求这样做。
特别是时间不算作这样的副作用,因此您不能依赖具有恒定运行时的代码。这在安全上下文中可能会有问题,因为它可能会引入时序侧通道。
答案 1 :(得分:10)
但
|=
和&=
也会发生短路?
没有。 &=
和|=
等同于操作&
和|
,而不是短路的逻辑运算符。
答案 2 :(得分:9)
不,&=
和|=
运营商没有进行短路评估。
它们是伪运算符,编译器将其转换为使用&
和|
运算符。
此代码:
allEven &= IsEven(numbers[i]);
完全等同于:
allEven = allEven & IsEven(numbers[i]);
如果您想要短路检查,那么您必须使用运营商的短路版本将其写出来:
allEven = allEven && IsEven(numbers[i]);
没有&&=
伪运算符,但上面的代码正是编译器在有一个代码时会做的。
答案 3 :(得分:5)
很容易找到:
bool b = false;
b &= Foo(1);
private static bool Foo(int id)
{
Console.WriteLine("test " + id);
return false;
}
答案是否,始终执行Foo()
。
但如果您正在寻找优化,那么真正的问题当然是循环:
allEven = numbers.All(n => IsEven(n));
可能会快得多。在看到第一个奇数(样本中为9)后,它将停止迭代。