У короткозамкнутых операторов || и && существуют для логических значений, допускающих значение NULL? RuntimeBinder иногда так думает

84

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

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

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

Однако я хотел попробовать и 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.

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

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

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

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

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

Йеппе Стиг Нильсен
источник
4
Нет никаких экземпляров Nullable<Boolean>задействованных вообще, только логические значения в рамке, обрабатываемые как dynamic- ваш тест с не bool?имеет значения. (Конечно, это не полный ответ, а только его зародыш.)
Йерун Мостерт
3
Это A || Bимеет определенный смысл, поскольку вы не хотите оценивать, Bесли не Aявляется ложным, а это не так. На самом деле, вы никогда не узнаете тип выражения. A && BВерсия более удивительно - я посмотрю , что я могу найти в спецификации.
Джон Скит
2
@JeroenMostert: Ну, если только компилятор не решил, что если тип Ais boolи значение Bis null, тогда bool && bool?может быть задействован оператор.
Джон Скит
4
Что интересно, похоже, что здесь обнаружена ошибка компилятора или спецификации. В спецификации C # 5.0 &&говорится о его разрешении, как если бы оно было &вместо этого, и в частности, включает случай, когда оба операнда bool?- но тогда следующий раздел, на который он ссылается, не обрабатывает случай, допускающий значение NULL. Я мог бы добавить что-то вроде ответа, более подробно описав это, но он не объяснил бы это полностью.
Джон Скит
14
Я написал Мэдсу по электронной почте о проблеме со спецификацией, чтобы узнать, не проблема ли это только в том, как я ее читаю ...
Джон Скит

Ответы:

67

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

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

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

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

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

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

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

Мадс

Мадс Торгерсен - MSFT
источник
Я вижу, что эти операторы короткого замыкания являются особенными, поскольку при динамическом связывании нам действительно не разрешается знать тип второго операнда в случае, когда мы выполняем короткое замыкание. Может быть, в спецификации стоит упомянуть об этом? Конечно, поскольку все внутри a заключено dynamicв рамки, мы не можем отличить bool?which HasValueот "simple" bool.
Jeppe Stig Nielsen
6

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

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

Как вы можете вывести это из спецификации?

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

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

Я думаю, это ключевой момент, который отвечает на ваш вопрос. Какое разрешение происходит во время выполнения? В разделе 7.12.2, Пользовательские условные логические операторы объясняются:

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

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

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

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

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

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

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

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

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

Кристофер Карренс
источник
Неудивительно, что dynamicпривязка происходит во время выполнения с использованием фактических типов экземпляров, а не типов времени компиляции (ваша первая цитата). Ваша вторая цитата не имеет значения, поскольку ни один тип здесь не перегружает operator trueи operator false. explicit operatorВозвращение boolэто нечто иное , чем operator trueи false. Трудно читать спецификацию каким-либо образом, который позволяет A && B(в моем примере), не разрешая также, a && bгде aи bявляются статически типизированными логическими значениями, допускающими значение NULL, т.е. bool? aи bool? b, с привязкой во время компиляции. Но это запрещено.
Йеппе Стиг Нильсен
-1

Тип 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;
Томас Папамихос
источник