Короткое замыкание на | = и & = операторы присваивания в С#

Я знаю, что || и && определены как операторы короткого замыкания на С#, и такое поведение гарантируется спецификацией языка, но также |= и &= короткое замыкание?

Например:

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 становится ложным, что означает, что все последующие записи не имеют значения - значение allEven гарантируется как ложное для всех будущих вызовов этого выражения. То же самое относится к anyOdd, который установлен в true, когда видит 9, и останется верным для всех последующих вызовов этого выражения.

Итак, выполняются ли &= и |= ярлыки, или IsEven гарантированно вызывается на каждой итерации? Есть ли какое-либо определенное поведение в спецификации языка для этого случая? Существуют ли какие-либо угловые случаи, когда такое короткое замыкание будет проблематичным?

Ответ 1

Спецификация С# гарантирует, что обе стороны оцениваются ровно один раз слева направо и что не происходит короткого замыкания.

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).

Каждое из этих выражений имеет одно или несколько подвыражений, которые безоговорочно оценены в фиксированном порядке.

Спецификация С# для составных операторов говорит:

7.17.2 Составное назначение

...

Операция формы 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 по-прежнему могут опустить оценку.

В вашем примере компилятор может завершить цикл, как только он знает результат, так как таких побочных эффектов нет. Но этого не требуется.

В частности, время не считается таким побочным эффектом, и поэтому вы не можете полагаться на свой код с постоянным временем выполнения. Это может быть проблематично в контексте безопасности, поскольку оно может вводить временный боковой канал.

Ответ 2

но сделать |= и &= слишком короткое замыкание?

Нет. &= и |= являются эквивалентами операций & и |, а не коротких замыкающих логических операторов.

Ответ 3

Нет, операторы &= и |= не проводят оценку короткого замыкания.

Они являются псевдооператорами, которые компилятор преобразует в использование операторов & и |.

Этот код:

allEven &= IsEven(numbers[i]);

в точности эквивалентно:

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

Если вам нужна короткая замыкающая проверка, вы должны записать ее с использованием коротких замыканий версий операторов:

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

Нет псевдо-оператора &&=, но указанный выше код является именно тем, что сделал бы компилятор, если бы он был.

Ответ 4

Легко узнать:

  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 в образце).