Опубликован: 03.10.2011 | Уровень: для всех | Доступ: свободно
Лекция 10:

Переменные, присваивание и ссылки

Локальные переменные

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


\text{— Определить max равным }N_i\\
\text{— Определить i равным 1}\\
\text{— Переопределить max как большее из текущего максимума и }N_{i+1}\\
\text{— Увеличить i на единицу }\\

Мы можем теперь выразить этот алгоритм, используя присваивание. Но давайте напишем функцию, вычисляющую "максимальное" (в лексикографическом порядке) имя среди имен всех станций линии:

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

Новое значение цели вычисляется на основе ранее вычисленного значения и новой информации. Эта схема близка к стандартной математической схеме определения последовательности значений рекуррентными соотношениями. Вот как выглядит слегка упрощенная версия примера, рассмотренного в начале лекции:

\text {Пусть дано $s_0$, тогда $s_{i+1}= f (s_i)$ для каждого i $\ge$ 0}

Здесь f некоторая функция (в примере с числами Фибоначчи функция f зависела от двух ранее вычисленных значений, а не от одного). Для вычисления s_n для некоторого 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

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

Убедитесь, что вы понимаете разницу между математическим свойством S_{i+1}=f(s) и оператором присваивания 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-соглашение для присваивания и равенства, хотя в двух последних случаях действуют более строгие правила, позволяющие избежать появления подобных ошибок при выполнении программы.

Кирилл Юлаев
Кирилл Юлаев
Как происходит отслеживание свободного экстента?
Федор Антонов
Федор Антонов
Оплата и обучение
Михаил Васильев
Михаил Васильев
Россия, г. Санкт-Петербург