Pull to refresh

C#: как не «выстрелить себе в ногу»

Reading time4 min
Views4.9K
Сегодня мы рассмотрим более детально, как стало возможным «выстрелить себе в ногу» на C#, а также в целом на .NET, при работе с логическими значениями, в каких практических кейсах это может произойти, и как этого не допустить.

Какие же строки выводятся на экран этим консольным приложением?
Запустив приложение, предварительно собрав его в среде Visual Studio Community 2013, получим следующий результат:

Unsafe Mode
01: b1        : True
02: b2        : True
03:  b1 ==  b2: False
04: !b1 == !b2: True
05: b1 && b2  : True
06: b1 &  b2  : True
07: b1 ^  b2  : True
08: b1 && b3  : True
09: b1 &  b3  : False
10: b1 ^  b3  : True

Safe Mode
11: b1        : True
12: b2        : True
13:  b1 ==  b2: True
14: !b1 == !b2: True
15: b1 && b2  : True
16: b1 &  b2  : True
17: b1 ^  b2  : False
18: b1 && b3  : True
19: b1 &  b3  : True
20: b1 ^  b3  : False

Исходя из допущения, что в каждой из логических переменных b1, b2, b3 находится либо «истинное» значение, либо значение, отличное от «ложного» (а, значит, тоже «истинное»? — ведь это булевы переменные?), возникают несколько вопросов:
  1. Почему в блоках Unsafe и Safe Mode разные результат в позициях 03 и 13, 07 и 17, 09 и 19, 10 и 20 соответственно?
    (а почему тогда значения в других соответствующих друг другу позициях в блоках Unsafe и Safe совпадают?)
  2. Почему внутри блока Unsafe результаты в позициях 05 и 06 одинаковы, а в 08 и 09 — разные?
    И почему результаты в 08 и 09 разные?

Попробуем разобраться:

Наверное, все знают, что изначально в языках программирования отсутствовали специальные логические (булевы) типы данных.
В качестве булевых типов использовались целочисленные типы данных.

Ноль трактовался как ложное значение (False), значение отличное от нуля — как истинное (True).
Таким, образом к целочисленному операнду мог быть применен оператор ветвления if.

Широко известна ошибка, которую легко совершить на языках C/C++, перепутав операторы присвоения (=) и равенства (==).
Следующий код всегда выведет на экран строку «i == 1»:
int i = 0;
if (i = 1)
  printf("i == 1");
else
  printf("i == 0");

Связано это с тем, что в операторе ветвления if в операнде «i = 1» ошибочно использован оператор присвоения (=) вместо оператора равенства (==).
В результате в переменную «i» записывается значение «1», соответственно, оператор "=" возвращает значение «1», и в качестве операнда оператора if используется целочисленное значение «1», трактуемое как логическое (булево) значение, и всегда будет выполняться код из-первой ветки (printf(«i == 1»)).

Поэтому в языках C/C++ принято оператор сравнения использовать следующим образом:
int i = 0;
if (1 == i)
  printf("i == 1");
else
  printf("i == 0");
вместо «интуитивно понятного»:
int i = 0;
if (i == 1)
  printf("i == 1");
else
  printf("i == 0");
Причина в том, что в операторе «1 == i» мы не сможем сделать ошибку и записать его как «1 = i» — компилятор не позволит присвоить константе (1) новое значение (i).

Видимо, в какой-то момент разработчики языков программирования решили добавить в языки поддержку «полноценных» логических типов:
Так, в Turbo/Borland Pascal и Delphi появился тип Boolean. Переменные данного типа могли принимать значения False и True. Причем было документировано, что размер типа — 1 байт, а порядковые (целочисленные) значения, возвращаемые функцией Ord — 0 и 1 для False и True соответственно.

А что же с другими возможными внутренними значениями, отличными от нуля? Поведение в этом случае могло быть неопределенным, и в документации/книгах уточнялось, что булевы значения следует тестировать таким образом:
var
  b: Boolean;
begin
  b := True;
  if b then
    WriteLn('b = True')
  else
    WriteLn('b = False');
end
но не таким:
var
  b: Boolean;
begin
  b := True;
  if b = True then
    WriteLn('b = True')
  else
    WriteLn('b = False');
end
В переменной «b» могло оказаться ненулевое значение, отличное от единицы, и тогда результат сравнения «b = True» был бы неопределен — результат мог оказаться ложным (если сравнение выполнялось как сравнение двух целых чисел, минуя этап «нормализации» значений — очевидно, из соображений производительности).

С другой стороны, тем самым косвенно признавалось, что возможен кейс, когда логическая переменная может содержать внутренний код, отличный от нуля и единицы, и что ненулевое значение считается «истинным», хотя не всегда может быть корректно обработано:
  • логическая переменная реализована как целое число, и при этом возможно приведение целого числа к Boolean (не говоря о возможностях адресной арифметики);
  • также это подтверждается этим: «Casting the variable to a Boolean type is unreliable» — т.е., привести целое число к Boolean мы можем, но результат «ненадежен» (unreliable) — практически это означает, что результат тестирования такого значения неопределен.

Позже в Delphi были добавлены булевы типы ByteBool, WordBool, LongBool размерами 1, 2 и 4 байта для совместимости с булевыми типами при работе с кодом, написанным на C/C++, COM-объектами, и другим сторонним кодом.
Для них определено, что, в отличие от типа Boolean, «истинным» считается любое ненулевое значение.

В C++ точно так же был добавлен «нативный» тип bool (переменные данного могут принимать значение false и true), причем его размер недетерминирован (вероятно, зависит от разрядности платформы — из соображений производительности или каких-то других; размерности типа данных для конкретных версий компиляторов Microsoft приведены здесь и здесь).
А также нет явного определения внутренних кодов false и true, хотя из примеров кода, сопутствующих определениям false и true, косвенно следует, что false имеет внутренний числовой код 0, а true — внутренний числовой код 1.

Мы провели исторический экскурс генезиса булевых типов, чтобы увидеть подводные камни при работе с таким, казалось бы, простым типом данных — логическим (булевым) типом, и с пониманием вопроса подойти к рассмотрению внутреннего устройства логического типа данных в C#, обсуждению, почему в тестовой программе результаты получились такие, как получились, и как корректно работать в C# с булевыми значениями при взаимодействии с неуправляемым кодом.
Отступление получилось достаточно объемным, поэтому эти вопросы мы рассмотрим в следующий раз.
Tags:
Hubs:
-6
Comments10

Articles

Change theme settings