Опубликован: 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-соглашение для присваивания и равенства, хотя в двух последних случаях действуют более строгие правила, позволяющие избежать появления подобных ошибок при выполнении программы.

Кирилл Юлаев
Кирилл Юлаев
Федор Антонов
Федор Антонов

Здравствуйте!

Записался на ваш курс, но не понимаю как произвести оплату.

Надо ли писать заявление и, если да, то куда отправлять?

как я получу диплом о профессиональной переподготовке?