Переменные, присваивание и ссылки
Локальные переменные
В предыдущей главе мы видели схему алгоритма для вычисления значения максимума множества значений. В отсутствие присваивания использовались элементы псевдокода:
Мы можем теперь выразить этот алгоритм, используя присваивание. Но давайте напишем функцию, вычисляющую "максимальное" (в лексикографическом порядке) имя среди имен всех станций линии:
highest_name (line: LINE): STRING — Последнее (в алфавитном порядке) имя станции на линии require line_exists: line /= Void local i: INTEGER new: STRING do from Result := line.south_end.name i := 1 invariant ... — Как ранее until i = line.count loop new := i_th (i).name if new > Result then Result := new end i := i + 1 end
Присваиваний в этом примере немало. В предложении from переменная Result инициализируется именем первой станции south_end, а переменная i получает значение 1. Затем в цикле, если имя текущей станции, обозначенное как new, больше, чем текущий максимум, то значение Result меняется на new. Корректность этого алгоритма зависит от двух свойств, выраженных инвариантами соответствующих классов:
- у LINE всегда есть хотя бы одна станция, доступная как south_end или, эквивалентно, i_th (1);
- каждая станция метро имеет имя name, отличное от void.
Заметьте, сравнение строк использует алфавитный порядок, его еще называют лексикографическим порядком: s2 > s1 имеет значение True, если и только если s2 следует за s1 в алфавитном порядке.
Время программирования!
Добавьте функцию highest_name в пример класса для этой главы - ASSIGNMENTS, и используйте ее для показа "максимального" в алфавитном порядке имени станции 8-й линии метро.
Принципиальной новинкой этого примера является использование локальных переменных. Рассмотрим объявление:
local i: INTEGER new: STRING
Здесь вводятся две сущности, i и new, которые метод может использовать для хранения промежуточных результатов, требуемых алгоритму. Локальные переменные и являются такими сущностями — локальные для метода и вводимые ключевым словом local. Можно было бы обойтись без локальных переменных, вводя их как атрибуты класса, о которых будем подробно говорить в этой главе. Но локальные переменные не заслуживают такого статуса. Атрибуты класса — это характеристики класса, его компоненты (feature), которыми обладают все экземпляры класса. Здесь же сущности i и new необходимы временно для каждого исполнения метода. Когда выполнение метода завершается, i и new выполнили свое дело и могут уйти.
Имена локальных переменных могут быть произвольными, лишь бы они не стали причиной конфликта имен.
Правило локальных переменных
В принципе, допустимо совпадение имен локальных переменных с компонентами класса при условии, что внутри метода конфликтующее имя означает локальную переменную, но такое соглашение может быть источником недоразумений и ошибок. Имена недорого стоят — когда вам нужна новая переменная, выбирайте для нее новое имя.
Заметьте, ничто не мешает использовать одни и те же имена локальных переменных для разных методов в одном и том же классе (некоторые имена — item, count, put... — встречаются многократно во многих различных классах). Такие случаи не приводят к двусмысленностям, так как омонимы появляются в разных контекстах — в разных областях видимости переменных.
Результат функции
Как показано в двух последних примерах, Result может появляться в функциях для обозначения результата, вычисляемого функцией. Напоминаю, процедуры и функции являются двумя видами методов класса. Функции вычисляют и возвращают значение, Result служит для обозначения этого значения в тексте функции. Процедуры, в отличие от функций, могут изменять объекты, но не возвращают результат. Очевидно, что Result не может использоваться в процедурах.
Рассмотрим вызов функции в методе класса ASSIGNMENTS:
Console.show (highest_name (Line8))
Здесь вызывается функция highest_name и отображается результат ее вычисления, который является последним значением Result, полученным в ходе вычисления функции непосредственно перед ее завершением. Вы могли наблюдать за этим в процессе выполнения последней сессии "Время программирования".
Формально Result является локальной переменной. Единственное отличие: он не объявляется в теле функции, а автоматически доступен для любой функции и неявно объявлен как переменная, тип которой совпадает с типом возвращаемого функцией значения, например, REAL для totaltime, STRING для highest_name .
Это также обозначает, что Result является резервируемым словом, которое нельзя использовать для собственных идентификаторов программы.
Определение: резервируемое слово
Резервируемые слова обобщают понятие ключевого слова, введенного ранее. Пример с Result иллюстрирует, почему ключевые слова являются только одним из двух видов резервируемых слов.
- Ключевые слова — class, do ... — играют только синтаксическую роль; они не обозначают значение в период выполнения.
- Другие резервируемые слова, такие как Result, имеют значение и семантику. Другими примерами резервируемых слов являются True и False, обозначающие булевские значения.
Обмен значениями (свопинг)
Рассмотрим типичный пример с присваиванием и локальной переменной. Предположим, что переменные var1 и var2 имеют один и тот же тип T. Пусть необходимо, чтобы переменные обменялись значениями. Следующие три оператора выполняют обмен:
swap := var1; var1 := var2; var2 := swap
Обмен требует третью переменную swap, типично объявляемую как локальную переменную типа T. Схема свопинга такова:
- первое присваивание сохраняет начальное значение var1в swap;
- второе присваивание изменяет значение var1 на начальное значение var2;
- третье присваивание изменяет значение var2 на значение swap, хранящее начальное значение var1.
Понятно, почему нам необходима переменная swap: нам нужна память для сохранения значения одной из переменных, прежде чем оно будет изменено. Заметьте, что важен порядок выполнения операторов при обмене, хотя он не единственно возможный (переменные var1 и var2 можно поменять местами ввиду симметричности обмена).
Переменные, такие как swap, используемые для промежуточных целей, известны как временные переменные, типично они объявляются как локальные переменные метода.
Мощь присваивания
Символ присваивания — ":=". Для присваивания i:= i + 1 обычно говорят: "i получает значение i + 1" или "i присваивается значение i + 1".
Эффект состоит в замене значения target на значение выражения source. Прежнее значение target будет потеряно навсегда: никакие записи не сохраняются. Присваивание является двойником (на более высоком уровне языка программирования) машинной операции компьютера — записи значения в ячейку памяти. Так что, если значение некоторой переменной вам может понадобиться в будущем, то, прежде чем изменить его, присвойте это значение другой переменной!
Рассмотрим часто встречающийся образец, когда источник присваивания (выражение) содержит в качестве одной из переменных цель присваивания. Этот образец встречался в примерах обоих рассмотренных нами методов:
Result := Result+ item.time_to_next i := i+ 1
Новое значение цели вычисляется на основе ранее вычисленного значения и новой информации. Эта схема близка к стандартной математической схеме определения последовательности значений рекуррентными соотношениями. Вот как выглядит слегка упрощенная версия примера, рассмотренного в начале лекции:
Здесь f некоторая функция (в примере с числами Фибоначчи функция f зависела от двух ранее вычисленных значений, а не от одного). Для вычисления для некоторого n >= 0 можно использовать цикл:
from Result := "Заданное начальное значение S_0" i := 0 invariant "Result = s_i until i = n variant n - i loop i:=i+1 Result:=f( Result) end
Эта схема применима только в том случае, если нет необходимости в хранении всех элементов последовательности и достаточно знать только последнее значение на каждом шаге. В обоих примерах так и происходит.
Убедитесь, что вы понимаете разницу между математическим свойством и оператором присваивания x:=f (x), в котором заключен механизм изменений, свойственный ПО. Этот механизм чужд математике и усложняет выводимость свойств программ. Заметьте, в частности, разницу между оператором
x := y
x = y
используемом, например, в условном операторе if x = у then .... Булевское выражение имеет те же свойства, что и равенство в математике, — оно дескриптивно (описательно), представляя возможное свойство true или false) двух значений x и у. Оператор присваивания императивен (повелителен) — он предписывает изменить значение переменной в результате вычислений. Вдобавок он деструктивен (разрушителен), уничтожая предыдущее значение этой переменной.
Замечательным примером этой разницы является часто встречающийся в циклах оператор
i := i + 1
Булевское выражение i = i + 1 вполне легально, но бессмысленно, поскольку всегда имеет значение false.
Почувствуй синтаксис
Первый широко применяемый язык программирования Fortran использовал для присваивания символ равенства "=". Это была оплошность. В последующих языках, таких как Algol и его последователи, для присваивания введена своя символика ":=", с сохранением символа равенства для операции эквивалентности.
По неизвестным причинам в языке С вернули знак равенства для присваивания, используя для эквивалентности "==". Это соглашение не только противоречит устоявшимся в математике свойствам (a = b в математике означает то же, что и b = a), но и является постоянным источником ошибок. Если вместо if (x == y) ... по ошибке написать if (x = y) то результат, допустимый в С, даст неожиданный эффект: x получит значение y, далее выработается булевское значение False, если значение y и новое значение x равно нулю, и True - в противном случае.
Такие современные языки, как C++, Java и C#, оставили C-соглашение для присваивания и равенства, хотя в двух последних случаях действуют более строгие правила, позволяющие избежать появления подобных ошибок при выполнении программы.