Безопасный синтаксис

Часто сам синтаксис относят к скучным техническим деталям. Аргументируется это так — важно что именно вы скажете. При этом не так важно как вы это скажете. Это конечно же неверно. Ясность и однозначность очень важны при любом общении в цивилизованном мире.

Проводя аналогию, программа — это средство коммуникации между автором и читателем, будь то компилятор, другой член команды разработчиков, ответственный за контроль качества кода, либо любой другой человек. В самом деле, ведь чтение — это тоже способ общения. Ясный и однозначный синтаксис будет значительным подспорьем в подобном общении и, как мы убедимся далее, позволит избежать многих распространенных ошибок.

Важным свойством хорошо спроектированного синтаксиса является возможность находить ошибки в программе, вызванные типичными опечатками. Это позволит выявить их в момент компиляции вместо того, чтобы получить программу с непреднамеренным смыслом. Хотя тяжело предотвратить такие опечатки, как X вместо Y и + вместо *, многие опечатки, влияющие на структуру программы могут быть предотвращены. Заметим, однако, что полезно избегать коротких идентификаторов как раз по этой причине. К примеру, если финансовая программа оперирует курсом и временем, использовать в ней идентификаторы R и T чревато ошибками, поскольку легко ошибиться при наборе текста, ведь эти буквы находятся рядом на клавиатуре. В случае же использования идентификаторов Rate и Time возможные опечатки Tate, Rime будут легко выявлены компилятором.

Присваивание и проверка на равенство

Очевидно, что присваивание и проверка на равенство это разные действия. Если мы выполняем присваивание, мы изменяем состояние некоторой переменной. В то время как сравнение на равенство просто проверяет некоторое условие. Изменение состояния и проверка условия совершенно различные операции и осознавать это важно.

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

Так в Fortran‐е со дня его появления пишется:

X = X + 1

Но это весьма причудливая запись. В математике X никогда не равен X + 1. Данная инструкция в Fortran‐е означает конечно же «заменить текущее значение переменной X старым значением, увеличенным на единицу». Но зачем использовать знак равенства таким способом, если в течении столетий он использовался для обозначения сравнения? (Знак равенства был предложен в 1550 году английским математиком Робертом Рекордом.) Создатели языка Algol 60 обратили внимание на эту проблему и использовали комбинацию двоеточия и знака равенства для записи операции присваивания:

X := X + 1;

Таким образом знак равенства можно использовать в операциях сравнения не теряя однозначности:

if X = 0 then ...

Язык C использует = для операции присваивания и, как следствие, вводит для операции сравнения двойной знак равенства (==). Это может запутать еще больше.

Вот фрагмент программы на C для управления воротами железной дороги:

if (the_signal == clean)
{
  open_gates(...);
  start_train(...);
}

Та же программа на языке Ада:

if The_Signal = Clean then
   Open_Gates(...);
   Start_Train(...);
end if;

Посмотрим, что будет, если C‐программист допустит опечатку, введя единичный знак равенства вместо двойного:

if (the_signal = clean)
{
  open_gates(...);
  start_train(...);
}

Компиляция пройдет без ошибок, но вместо проверки the_signal произойдет присваивание значения clean переменной the_signal. Более того, в C нет различия между выражениями (вычисляющими значения) и присваиваниями (изменяющими состояние). Поэтому присваивание работает как выражение, а присвоенное значение затем используется в проверке условия. Если так случится, что clean не равно нулю, то условие будет считаться истинным, ворота откроются, the_signal примет значение clean и поезд уйдет в свой опасный путь. В противном случае, если clean закодирован как ноль, проверка не пройдет, ворота останутся заперты, а поезд — заблокированным. В любом случае все пойдет совсем не так, как нужно.

Ошибки связанные с использованием = для присваивания и == для проверки на равенство, то есть присваивания вместо выражений хорошо знакомы в C‐сообществе. Для их выявления появились дополнительные правила оформления кода MISRA C[3] и средства анализа кода, такие как lint. Однако, следовало бы избежать подобных ловушек с самого момента разработки языка, что и было достигнуто в Аде.

Если Ада‐программист по ошибке использует присваивание вместо проверки:

if The_Signal := Clean then  --  Ошибка

то программа просто не скомпилируется и все будет хорошо.

Группы инструкций

Часто необходимо сгруппировать последовательность из нескольких инструкций, например после проверки в условном операторе. Есть два типичных пути для этого:

  • взять всю группу в скобки (как в C),
  • закрыть последовательность чем‐то, отвечающим if (как в Аде).

В C мы получим:

if (the_signal == clean)
{
  open_gates(...);
  start_train(...);
}

и, допустим, случайно добавим точку с запятой в конец первой строки. Получится следующее:

if (the_signal == clean) ;
{
  open_gates(...);
  start_train(...);
}

Теперь условие применяется только к пустому оператору, который неявно присутствует между условием и вновь введенным символом точки с запятой. Он практически невидим. И теперь не важно состояние the_signal, ворота все равно откроются и поезд поедет.

Вот для Ады соответствующая ошибка:

if The_Signal = Clean; then
   Open_Gates(...);
   Start_Train(...);
end if;

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

Именованное сопоставление

Еще одним свойством Ады синтаксической природы, помогающим избежать разнообразных ошибок, является именованная ассоциация, используемая в различных конструкциях. Хорошим примером может выступать запись даты, потому, что порядок следования ее составляющих различается в разных странах. К примеру 12 января 2008 года в Европе записывают, как 12/01/08, а в США обычно пишут 01/12/08 (не считая последних таможенных деклараций). В тоже время стандарт ISO требует писать вначале год — 08/01/12.

В C структура для хранения даты выглядит так:

struct date {
  int day, month, year;
};

что соответствует следующему типу в Аде:

type Date is record
   Day, Month, Year : Integer;
end record;

В C мы можем написать:

struct date today = {1, 12, 8};

Но не имея перед глазами описания типа, мы не можем сказать, какая дата имеется в виду: 1 декабря 2008, 12 января 2008 или 8 декабря 2001.

В Аде есть возможность записать так:

Today : Date := (Day => 1, Month => 12, Year => 08);

Здесь используется именное сопоставление. Теперь не будет разночтений, даже, если мы запишем компоненты в другом порядке. (Заметьте, что в Аде допустимы лидирующие нули).

Следующая запись:

Today : Date := (Month => 12, Day => 1, Year => 08);

по‐прежнему корректна и демонстрирует преимущество — нам нет нужды помнить порядок следования компонента записи.

Именованное сопоставление используется и в других конструкциях Ады. Подобные ошибки могут появляться при вызове подпрограмм, имеющих несколько параметров одного типа. Допустим, у нас есть функция вычисления индекса ожирения человека. Она имеет два параметра — высота и вес, заданные в фунтах и дюймах (либо в килограммах и метрах, для метрической системы измерений). В C мы имеем:

float index(float height, float weight) {
  ...
  return ...;
}

А в Аде:

function Index (Height, Weight : Float) return Float is
   ...
   return ...;
end;

Вызов функции index в C для автора примет вид:

my_index = index(68.0, 168.0);

Но по ошибке легко перепутать:

my_index = index(168.0, 68.0);

в результате вы становитесь очень тощим, но высоким гигантом! (По забавному совпадению оба числа оканчиваются на 68.0).

Такой нездоровой ситуации можно избежать в Аде, используя именованное сопоставление параметров в вызове:

My_Index := Index (Height => 68.0, Weight => 168.0);

Опять‐таки, мы можем перечислять параметры в любом порядке, в каком пожелаем, ничего не случится, если мы забудем, в каком порядке они следуют в определении функции.

Именованное сопоставление очень ценная конструкция языка Ада. Хотя его использование не является обязательным, им стоит пользоваться как можно чаще, т. к. кроме защиты от ошибок, оно значительно улучшает читаемость программы.

Целочисленные литералы

Обычно целочисленные литералы не часто встречаются в программе, за исключением может быть 0 и 1. Целочисленные литералы должны в основном использоваться для инициализации констант. Но если они используются, их смысл должен быть очевиден для читателя. Использование подходящего основания исчисления (10, 8, 16 и пр.) и разделители групп цифр помогут избежать ошибок.

В Аде это сделать легко. Ясный синтаксис позволяет указать любое основание исчисления от 2 до 16 (по умолчанию конечно 10), например 162B это целое число 43 по основанию 16. Символ подчеркивания используется для группировки цифр в значениях с большим количеством разрядов. Так, например, 16FFFF_FFFF_FFFF_FFFF вполне читаемая запись значения 264-1.

Для сравнения тот же литерал в C (так же как в C++, Java и пр. родственных языках) выглядит как 0xFFFFFFFFFFFFFFFF, о который не трудно сломать глаза, пытаясь сосчитать сколько F оно содержит. Более того, C трактует лидирующие нули особым образом, в результате число 031 означает совсем не то, что поймет любой школьник, а 25.