null – это не false. Часть 2

В замечательной книге Реймонда Смаллиана про остров рыцарей и лжецов, на котором, как вы помните, рыцари говорят только правду, а лжецы – всегда лгут, рыцари и лжецы являются отличным литературным инструментом изучения проблем дедуктивной (*) логики. Я не могу припомнить, чтобы Смаллиан объяснял, что будет, если рыцари и лжецы будут говорить полуправду, использовать авторское право преследовать большую правду или другую форму правдоподобности. Nullable Boolean в языке C# дает нам, если не идею правдоподобности, то, по крайней мере, идею того, что true и false являются не единственными возможными значениями предиката: существует еще и «null», что бы он ни означал.

И что это значит? Значение null булевой переменной может означать: «существует истинное состояние, но я не знаю, чему оно равно». Например, при выполнении запроса к базе данных 1-го декабря вида: «был ли объем продаж за ноябрь выше, чем за октябрь?», то ответом должно быть true либо false, но база данных может еще не содержать ответа, поскольку еще не внесены все сведения об объемах продаж. Правильным ответом в данном случае будет «null», что означает: «ответ на этот вопрос существует, но я его не знаю».

Или null в качестве значения булевой переменной может означать «на данный вопрос нет ответа, ни true, ни false им не является». Является ли истинным высказывание: настоящий король Франции лысый. Количество действующих королей Франции равно нулю, что равно количеству действующих лысых королей Франции, но, вместо того, чтобы говорить, что данное высказывание «бессмысленно истинно», более разумным будет отрицать корректность самого вопроса. Безусловно, подобные ситуации возникают и в программировании, когда мы хотим выразить, что вопрос неверен и на него невозможно получить ответ, и null в этом случае кажется очень даже разумным выбором.

Поскольку null может означать «я не знаю», то результат практически всех «расширяющих операторов к nullable типам» (lifted to nullable operator) в языке C# равен null, если один из операндов равен null. Сумма 123 и null равна null, поскольку ответом на вопрос: «какова сумма 123 и неизвестного значения» является «неизвестное значение!» Примечательным исключениями из этого правила является эквивалентность (два значения, равных null, являются эквивалентными) и логические операторы «И» и «ИЛИ», обладающие очень интересным поведением. Выражение вида x & y для булевых nullable типов не определяется правилом вида: «если один из операндов равен null, то результат равен null». Данное выражение определяется так: «если один из операндов равен false, то результат равен false, иначе, если один из аргументов равен null, то результат null, иначе – результат true». Правило для x | y – аналогичное. «Если один из операндов равен true, то результат true, иначе, если один из операндов null, то результат null, иначе – результат false». Эти правила соответствуют нашему интуитивному поведению операторов «И» и «ИЛИ» при учете что «null» означает «я не знаю». Т.е. выражение «(истина) ИЛИ (что-то неизвестное)» истинно, не зависимо от того является ли истинным это «что-то неизвестное» или нет. Но если «null» означает «вопрос, на который вообще не существует ответа», тогда результатом выражения «(истина) ИЛИ (что-то бессмысленное)» скорее всего, должно быть «что-то бессмысленное».

Однако все становится более запутанным, если рассматривать операторы с «оптимизацией вычисления логических выражений» (short circuiting), && и ||. Как вы, вероятно, знаете, операторы && и || аналогичны оператором & и |, за исключением того, что оператор && не вычисляет правый аргумент, если левый аргумент равен false, и оператор || не вычисляет правый аргумент, если левый аргумент равен true. После вычисления левого аргумента этих операторов, у нас «может» быть достаточно информации для получения результата. Таким образом, мы можем (1) сэкономить стоимость вычисления правого аргумента и (2) вычислять правый аргумент в зависимости от значения предусловия, определенного левым операндом. Самым распространенным примером пункта (2), конечно является код вида: if (s == null || s.Length == 0), поскольку вычисление правого аргумента завершится с ошибкой, если левое выражение истинно.

Операторы && и || не расширяются к nullable типам (lifted to nullable), поскольку сделать это довольно сложно. Основной смысл логических операторов с оптимизацией вычисления заключается в том, чтобы избежать вычисления правого операнда, но мы не можем сделать этого без нарушения соответствующего поведения нерасширенной (nonlifted) версии! Предположим, мы вычисляем выражение x && y для двух выражений x и y, возвращающих nullable Boolean. Давайте рассмотрим все варианты:

  • xfalse: Мы не вычисляем y и результат равен false.
  • xtrue: Мы вычисляем y и результат равен значению y.
  • xnull: Что нам делать? У нас есть два варианта:
    • Вычисляем y, нарушая замечательное свойство, которое заключается в том, что мы вычисляем y, только если x равен true. Результат равен false, если y равен false, в противном случае результат равен null.
    • Не вычисляем y. Результатом может быть false или null.
      • Если общим результатом будет false, даже когда результат вычисления y будет null, тогда falseв качестве общего результата является неверным.
      • Если общим результатом будет null, даже когда результат вычисления y будет false, тогда null в качестве общего результата является неверным.

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

В прошлый раз я говорил о том, что мы обсудим роль операторов operatortrue и operatorfalse в языке C#, но я думаю, что мы перенесем это обсуждение на следующий раз.


(*) Потрясающая книга Смаллиана ToMockAMockingbird с множеством логических головоломок, которую я рекомендую всем, кто хочет познакомиться с данной темой.

Оригинал статьи