C#中的| =和&=赋值运算符短路

我知道||&&被定义为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,并且对于该表达式的所有后续调用将保持为true。

那么,做&=|=快捷方式,还是保证在每次迭代时调用IsEven ? 在这种情况下,语言规范中是否有任何已定义的行为? 是否有任何角落情况会导致这种短路问题?

C#规范保证双方从左到右精确评估一次,并且不会发生短路。

5.3.3.21具有嵌入式表达式的表达式的一般规则

以下规则适用于这些类型的表达式:带括号的表达式(第7.6.3节),元素访问表达式(第7.6.6节),带索引的基本访问表达式(第7.6.8节),递增和递减表达式(第7.6.9节) ,§7.7.5),强制转换表达式(§7.7.6),一元+, – ,〜,*表达式,二进制+, – ,*,/,%,<<,>>,<,<=,>, > =,==,!=,is,as,&,|,^表达式(§7.8,§7.9,§7.10,§7.11), 复合赋值表达式(§7.17.2) ,检查和未检查表达式(§7.6) .12),加上数组和委托创建表达式(第7.6.10节)。

这些表达式中的每一个都具有一个或多个子表达式,这些子表达式以固定顺序无条件地评估

复合运营商的C#规范说:

7.17.2复合赋值

formsx op= y操作通过应用二元运算符重载决策(第7.3.4节)来处理,就好像操作是在x op y中编写的一样。 然后,

  • 如果所选运算符的返回类型可隐式转换为x的类型,则操作将计算为x = x op y ,但x仅计算一次。

  • 否则,如果所选运算符是预定义运算符,如果所选运算符的返回类型可显式转换为x类型,并且如果y可隐式转换为x类型或运算符是移位运算符,则操作被评估为x = (T)(x op y) ,其中T是x的类型,除了x仅被计算一次。

在你的情况下, op&| 。 短路行为反映了& / |行为 而不是&& / ||


请注意,这仅指单线程场景中可见的行为。 因此,如果右侧没有在这种情况下可观察到的副作用,编译器或JITter仍然可以省略评估。

在您的示例中,编译器一旦知道结果就可以自由地终止循环,因为没有这样的副作用。 但并不要求这样做。

特别是时间并不算作这样的副作用,因此您不能依赖具有恒定运行时间的代码。 这在安全上下文中可能是有问题的,因为它可以引入定时侧信道。

但是也要做|=&=短路?

编号&=|=是操作&|的等价物 而不是短路的逻辑运营商。

不, &=|=运算符不进行短路评估。

它们是伪运算符,编译器将其转换为使用&| 运营商。

这段代码:

 allEven &= IsEven(numbers[i]); 

完全等同于:

 allEven = allEven & IsEven(numbers[i]); 

如果您想要短路检查,那么您必须使用运算符的短路版本将其写出:

 allEven = allEven && IsEven(numbers[i]); 

没有&&=伪运算符,但上面的代码正是编译器在有一个代码时会做的事情。

容易找到:

  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)后,它将停止迭代。