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复合赋值
…
forms
x 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)后,它将停止迭代。