Операторы короткого замыкания || и && существуют для обнуляемых булевых элементов? RuntimeBinder иногда так думает

Я прочитал спецификацию языка С# для условных логических операторов || и &&, также известных как короткие замыкающие логические операторы. Мне показалось неясным, существуют ли они для нулевых булевых элементов, то есть типа операнда Nullable<bool> (также написано bool?), поэтому я попробовал его с нединамической типизацией:

bool a = true;
bool? b = null;
bool? xxxx = b || a;  // compile-time error, || can't be applied to these types

Казалось, что он решил вопрос (я не мог четко понять спецификацию, но предполагая, что реализация компилятора Visual С# была правильной, теперь я знал).

Однако, я хотел попробовать с привязкой 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);
  }
}

Удивительный результат заключается в том, что это выполняется без исключения.

Ну, x и y не удивительны, их объявления приводят к восстановлению обоих свойств, а результирующие значения ожидаются, x - true и y - null.

Но оценка для xx of A || B приводит к отсутствию исключения времени привязки, и было прочитано только свойство A, а не B. Почему это происходит? Как вы можете сказать, мы могли бы изменить геттер B, чтобы вернуть сумасшедший объект, например "Hello world", и xx все равно оценили бы до true без проблем привязки...

Оценка A && B (для yy) также приводит к ошибке привязки-времени. И здесь оба свойства извлекаются, конечно. Почему это разрешено связующим ведром? Если возвращаемый объект из B изменен на "плохой" объект (например, string), произойдет исключение связывания.

Это правильное поведение? (Как вы можете сделать вывод, что из спецификации?)

Если вы попробуете B в качестве первого операнда, то как B || A, так и B && A выдают исключение связывания во время выполнения (B | A и B & A работают нормально, поскольку все нормально с операторами без короткого замыкания | и &).

(Пробовался с компилятором С# Visual Studio 2013 и версией .NET.NET 4.5.2.)

Ответ 1

Прежде всего, спасибо, что указали, что спецификация не ясна в случае нединамического случая с возможностью nullable-bool. Я исправлю это в будущей версии. Поведение компилятора - это предполагаемое поведение; && и || не должны работать с обнуляемыми bools.

Однако динамическое связующее не реализует это ограничение. Вместо этого он связывает операции компонента отдельно: &/| и ?:. Таким образом, он может запутываться, если первым операндом является true или false (которые являются логическими значениями и поэтому разрешены как первый операнд ?:), но если вы дадите null в качестве первого операнда ( например, если вы попробуете B && A в приведенном выше примере), вы получите исключение связи времени выполнения.

Если вы думаете об этом, вы можете понять, почему мы реализовали динамические && и || таким образом, а не как одну большую динамическую операцию: динамические операции связаны во время выполнения после того, как их операнды оцениваются, так что привязка может основываться на типах результатов выполнения этих оценок. Но такая нетерпеливая оценка поражает цель операторов короткого замыкания! Таким образом, сгенерированный код для динамических && и || разбивает оценку на части и будет действовать следующим образом:

  • Оцените левый операнд (позвольте вызвать результат x)
  • Попробуйте превратить его в bool через неявное преобразование или операторы true или false (сбой, если не удается)
  • Используйте x как условие в операции ?:
  • В истинной ветки используйте x как результат
  • В ложной ветки теперь оцениваем второй операнд (позвольте вызвать результат y)
  • Попробуйте связать оператор & или | на основе типа времени выполнения x и y (сбой, если не удается)
  • Применить выбранный оператор

Это поведение, которое позволяет с помощью определенных "незаконных" комбинаций операндов: оператор ?: успешно обрабатывает первый операнд как невозбудимый логический, оператор & или | успешно обрабатывает его как нулевую boolean, и оба никогда не согласуются, чтобы проверить, что они согласны.

Так что это не то, что динамический && и || работа над ошибками. Это просто, что они реализованы таким образом, что это немного снисходительно, по сравнению со статическим случаем. Это, вероятно, следует считать ошибкой, но мы никогда не будем ее исправлять, так как это будет изменением. Также вряд ли кто-нибудь сможет подтянуть поведение.

Надеюсь, это объясняет, что происходит и почему! Это интригующая область, и я часто нахожу себя озадаченным последствиями решений, которые мы приняли, когда мы внедрили динамику. Этот вопрос был очень вкусным - спасибо за то, что он поднял его!

Мадс

Ответ 2

Это правильное поведение?

Да, я уверен, что это так.

Как вы можете сделать вывод, что из спецификации?

Раздел 7.12 спецификации С# версии 5.0 содержит информацию об условных операторах && и || и о том, как с ними связано динамическое связывание. Соответствующий раздел:

Если операнд условного логического оператора имеет динамику типа времени компиляции, то выражение динамически связано (§7.2.2). В этом случае тип времени компиляции выражения является динамическим, а описанное ниже разрешение будет иметь место во время выполнения с использованием типа времени выполнения этих операндов, которые имеют динамический тип времени компиляции.

Это ключевой момент, который отвечает на ваш вопрос, я думаю. Какова резолюция, которая происходит во время выполнения? Раздел 7.12.2, Определенные пользователем условно-логические операторы объясняют:

  • Операция x && y оценивается как T.false(x)? x: T. & (x, y), где T.false(x) является вызовом оператора false, объявленного в T, и T. & (x, y) является вызовом выбранного оператора &/li >
  • Операция x || y оценивается как T.true(x)? x: T. | (x, y), где T.true(x) является вызовом оператора true, объявленного в T, а T. | (x, y) является вызовом выбранного оператора |.

В обоих случаях первый операнд x будет преобразован в bool с помощью операторов false или true. Затем вызывается соответствующий логический оператор. Имея это в виду, у нас достаточно информации, чтобы ответить на остальные ваши вопросы.

Но оценка для xx из A || B приводит к отсутствию исключения времени привязки, и было прочитано только свойство A, а не B. Почему это происходит?

Для оператора || мы знаем, что это следует true(A) ? A : |(A, B). Мы короткое замыкание, поэтому мы не получим исключение времени привязки. Даже если A был false, мы все равно не получили бы исключение связывания во время выполнения из-за указанных шагов разрешения. Если A - false, тогда мы выполняем оператор |, который может успешно обрабатывать нулевые значения в соответствии с разделом 7.11.4.

Оценка A && B (для yy) также приводит к отсутствию ошибки времени привязки. И здесь оба свойства извлекаются, конечно. Почему это разрешено связующим ведром? Если возвращаемый объект из B изменяется на "плохой" объект (например, на строку), возникает исключение связывания.

По аналогичным причинам это тоже работает. && оценивается как false(x) ? x : &(x, y). A можно с успехом преобразовать в bool, так что проблем там нет. Поскольку B является нулевым, оператор & снимается (раздел 7.3.7) с того, который принимает bool на тот, который принимает параметры bool?, и, следовательно, исключение для выполнения не существует.

Для обоих условных операторов, если B - это что-то иное, чем bool (или нулевая динамика), привязка времени выполнения не выполняется, потому что не может найти перегрузку, которая принимает параметры bool и non-bool. Однако это происходит только в том случае, если A не удовлетворяет первому условию для оператора (true для ||, false для &&). Причина этого в том, что динамическое связывание довольно ленивое. Он не будет пытаться связать логический оператор, если только A не является ложным, и он должен спуститься по этому пути для оценки логического оператора. Как только A не удовлетворяет первому условию для оператора, он завершит сбой исключение.

Если вы попробуете B как первый операнд, оба B || A и B && A дать исключение связующего времени выполнения.

Надеюсь, к настоящему времени вы уже знаете, почему это происходит (или я плохо объяснил работу). Первым шагом в разрешении этого условного оператора является принятие первого операнда B и использование одного из операторов преобразования bool (false(B) или true(B)) перед обработкой логической операции. Конечно, B, будучи null не может быть преобразовано в true или false, и поэтому происходит исключение связывания во время выполнения.

Ответ 3

Тип 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;