Робити оператори короткого замикання || і && існують для логічних булевих значень, що дозволяють онулювати? RuntimeBinder іноді вважає так


84

Я прочитав специфікацію мови C # про умовні логічні оператори || і &&, також відомий як коротке замикання логічних операторів. Мені здалося незрозумілим, чи існували вони для логічних значень, що допускають відхилення, тобто типу операнда 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.

Але оцінка приводу xxне 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?не має значення. (Звичайно, це не повна відповідь, лише зародок однієї.)
Jeroen Mostert

3
Це A || Bмає певний сенс у тому, що ви не хочете оцінювати, Bякщо Aне хибне, а це ні. Тож ви ніколи не знаєте типу виразу. A && BВерсія дивніше - я подивлюся , що я можу знайти в специфікації.
Джон Скіт,

2
@JeroenMostert: Ну, якщо компілятор не вирішив, що якщо тип Aє boolі значення Bє null, то bool && bool?може бути задіяний оператор.
Джон Скіт,

4
Цікаво, що, схоже, це виявило помилку компілятора або специфікації. Специфікація C # 5.0 для &&розмов про її вирішення так, ніби вона була &натомість, і конкретно включає випадок, коли є обидва операнди bool?- але тоді наступний розділ, на який він посилається, не обробляє випадки, що дозволяють обнулити. Я міг би додати своєрідну відповідь, яка б пішла детальніше про це, але це не могло б повністю пояснити це.
Джон Скіт,

14
Я надіслав електронною поштою Мадсу інформацію про проблему зі специфікацією, щоб побачити, чи це лише проблема в тому, як я її читаю ...
Джон Скіт,

Відповіді:


67

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

Здається, динамічний прив’язувальний файл не реалізує це обмеження. Натомість він пов'язує операції з компонентами окремо: &/ |та ?:. Таким чином, він може заплутатися, якщо першим операндом є trueабо false(які є булевими значеннями і, таким чином, дозволено як перший операнд ?:), але якщо ви дасте nullяк перший операнд (наприклад, якщо ви спробуєте B && Aу прикладі вище), ви зробите отримати виняток прив'язки під час виконання.

Якщо ви задумаєтесь, то зможете зрозуміти, чому ми реалізували динаміку &&і ||таким чином, а не як одну велику динамічну операцію: динамічні операції прив'язуються під час виконання після оцінки їх операндів , так що прив'язка може базуватися на типах виконання результатів цих оцінок. Але така завзята оцінка перемагає мету операторів короткого замикання! Отже, замість цього згенерований код для динамічного &&і ||розбиває оцінку на частини і буде діяти наступним чином:

  • Обчислити лівий операнд (назвемо результат x)
  • Спробуйте перетворити це на за boolдопомогою неявного перетворення trueабо falseоператорів або (не вдається, якщо не вдається)
  • Використовувати xяк умову в ?:операції
  • В істинному гілці використовуйте xяк результат
  • Тепер у гілці false обчислюємо другий операнд (назвемо результат y)
  • Спробуйте прив'язати оператор &or |на основі типу виконання xта y(не вдається, якщо не вдається)
  • Застосувати вибраний оператор

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

Тож це не така динаміка && та || робота над нульовими. Просто вони трапляються реалізовані у спосіб, який є занадто поблажливим у порівнянні зі статичним випадком. Це, мабуть, слід вважати помилкою, але ми ніколи не будемо її виправляти, оскільки це було б надзвичайною зміною. Крім того, це навряд чи допомогло б комусь посилити поведінку.

Сподіваємось, це пояснює, що відбувається і чому! Це інтригуюча сфера, і я часто збиваю з пантелику наслідки рішень, які ми приймали, коли реалізовували динаміку. Це питання було дуже смачним - дякую, що підняли!

Мадс


Я бачу, що ці оператори короткого замикання особливі, оскільки при динамічному прив'язуванні нам насправді не дозволяється знати тип другого операнда у тому випадку, коли ми замикаємо. Може, в специфікації слід про це згадати? Звичайно, оскільки все, що знаходиться всередині a dynamic, упаковано в коробки, ми не можемо відрізнити між bool?яким HasValue, і "простим" bool.
Jeppe Stig Nielsen

6

Це правильна поведінка?

Так, я майже впевнений, що це так.

Як ви можете зробити висновок про це за специфікацією?

Розділ 7.12 C # Specification Version 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) - виклик обраного оператора &
  • Операція x || y оцінюється як T.true (x)? x: T. | (x, y), де T.true (x) - виклик оператора true, оголошеного в T, а T. | (x, y) - виклик обраного оператора |.

В обох випадках перший операнд x буде перетворений на bool за допомогою операторів falseor 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можна успішно перетворити на a bool, тому там немає жодної проблеми. Оскільки Bзначення null, &оператор піднімається (розділ 7.3.7) з того, який приймає a, boolдо того, який приймає bool?параметри, і, отже, немає винятків часу виконання.

Для обох умовних операторів, якщо Bце щось інше, ніж bool (або нульова динаміка), прив'язка середовища виконання не вдається, оскільки він не може знайти перевантаження, яке приймає bool і non-bool як параметри. Однак це відбувається лише в тому випадку, якщо Aне вдається задовольнити перший умовний для оператора ( trueдля ||, falseдля &&). Причиною цього є те, що динамічне прив’язування досить ліниве. Він не намагатиметься прив’язати логічний оператор, якщо не Aмає значення false, і він повинен пройти цей шлях, щоб оцінити логічний оператор. Після того, як 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статично типізованих NULLABLE булеві, тобто bool? aі bool? b, з обов'язковим під час компіляції. Однак це заборонено.
Jeppe Stig Nielsen,

-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;
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.