Как происходит отслеживание свободного экстента? |
Структуры управления
Время программирования!
Обновите цикл в методе traverse класса ROUTES в соответствии с последней версией с вариантом и (неформальным) инвариантом. Выполните систему.
Теперь удалите оператор Line8.forth, введя ошибку. Снова запустите систему и посмотрите, что получится (затем восстановите удаленную строку для дальнейших упражнений).
Давайте рассмотрим составляющие цикла детальнее: инициализация использует start, чтобы установит курсор в первую позицию. В облике контракта класса LINE (и в любом другом классе, построенном на понятии списка) можно видеть, что спецификация start говорит:
start —Установить курсор на первом элементе —(Не имеет эффекта, если список пуст) ensure at_first: (not is_empty) implies (index = 1) empty_convention: is_empty implies is_after
Булевский запрос is_empty определяет, пуст ли список. В данный момент рассмотрим только случай не пустого списка (подобного Line8). Первое предложение at_first постусловия для start устанавливает, что после инициализации курсор указывает на первый элемент, (index = 1), как нам и требуется.
Условие выхода из цикла — Line8.is_after. Для непустого списка оно не будет выполняться после инициализации: это нетрудно установить, анализируя инвариант класса, который говорит:
is_after = (index = count + 1)
Эквивалентность двух булевских значений означает, что is_after истинно, если и только если index = count + 1; для не пустого списка count будет, по крайней мере, равно 1, так что после инициализации, когда index = 1, невозможно выполнение is_after. В этом случае тело цикла будет выполнено, по крайней мере, один раз.
На каждом шаге тела цикла выполняется
show_spot (Line8.item.location)
В результате отображается красная точка в географическом местоположении элемента, на который указывает курсор.
Понимание и верификация цикла
Давайте добьемся более глубокого понимания цикла в нашем примере путем верификации его корректности. Проверим, что он выполняется во всех случаях в соответствии с ожиданиями. Хорошая идея — использовать отладчик, анализируя различные состояния объектов во время выполнения программы.
Время программирования!
Теперь, когда вы ознакомились с пояснениями примера, в частности, с аргументами корректности, полезно взглянуть на конкретную картину того, что происходит во время выполнения. Эту возможность обеспечивает отладчик EiffelStudio. Используйте его как для выполнения программы в целом, так и пошагово, оператор за оператором, при выполнении метода traverse класса ROUTES. В этом случае можно остановиться в любой момент и проанализировать содержимое всех интересующих вас объектов.
Например, можно видеть экземпляр LINE и проверить результаты запросов, таких как is_before и is_after, согласуются ли они с ожидаемыми значениями, выведенными из анализа программы.
Такой инструментарий времени выполнения не является заменой обоснования свойств программы. Выводы дают вам свойства, верные при всех выполнениях программы, в то время как проверка периода выполнения говорит нам о том, что свойство выполняется в данном конкретном запуске программы. Но все же отладка крайне полезна как способ получения преимуществ от практического понимания того, что делается: она позволяет вам буквально видеть, как программа работает.
В полном соответствии со своим именем отладчик помогает, когда программа выполняется не так, как ожидалось: найти ошибку - выловить "жучка". Но область его применения шире - жучок или не жучок, но это дает вам окно наблюдения за процессом выполнения программы. Не ждите, пока что-то случится, пользуйтесь преимуществами, предоставляемыми отладчиком. Раздел приложения EiffelStudio подскажет вам, как запустить отладчик.
Вернемся к нашему циклу:
from Line8 .start invariant — "Точка отображена для всех станций перед позицией курсора" — "Другие утверждения, задающие инвариант (смотри ниже)" until Line8 .is_after loop show_spot ( Line8 .item .location) Line8 .forth variant Line8 .count - Line8 .index + 1 end
Начнем анализ со случая пустого списка. Как отмечалось, постусловие команды start говорит:
ensure at_first: (not is_empty) implies (index = 1) empty_convention; is_empty implies is_after
В соответствии с соглашением на пустом списке после вызова start выполняется запрос is_after. Конечно, это "соглашение" не случайно — оно в точности соответствует нашим намерениям при создании типовой схемы итерации списка: начинать со start, выходить по is_after и каждый раз, выполнив операцию над элементом item, перейти к следующему элементу forth. Когда эта схема применяется к пустому списку, она не должна производить никакого видимого эффекта и должна останавливаться незамедлительно.
Почувствуй методологию:
Экстремальные случаи, такие как пустой список, являются частой причиной ошибок. Достаточно просто при проектировании программы думать о настоящих полных списках (и при отладке рассматривать только эти случаи). Когда же программа при одном из выполнений встречается с пустой структурой - она ломается. Это случается не только с пустыми структурами, но, в целом, с другими экстремальными случаями. Например, когда мы имеем дело со структурами ограниченного размера, экстремальным является случай, когда достигается предельный размер.
При проектировании программы и обосновании ее корректности убедитесь, что вы рассматриваете и граничные случаи. Это же верно и при тестировании - всегда включайте граничные случаи в систему тестов.
Вы можете запустить пример на пустой линии (класс TOURISM определяет метод Empty_line для этих целей) и использовать отладчик, чтобы посмотреть на результат.
Итак, цикл правильно себя ведет на пустом списке. В оставшейся части обсуждения корректности будем полагать, что список не пуст.
Рассмотрим спецификацию для item. Из предусловия ясно, что запрос применим, если курсор указывает на элемент списка:
item: STATION — Текущий элемент require not_off: not (is_after or is_before)
Этот факт отражает рисунок:
Так как в теле цикла вызывается item — в вызове show_spot — следует убедиться, что перед выполнением вызова предусловие всегда будет выполняться.
Заметьте, условием выхода является not is_after, так что is_after не выполняется, когда вызывается show_spot (если бы оно выполнялось, то тело цикла не выполнялось бы). Кроме того, is_before также не будет выполняться. Мы можем добавить следующее свойство в инвариант цикла:
not_before_unless_empty: (not is_empty) implies (not is_before)Листинг 7.1.
Давайте проверим, что это на самом деле инвариантное свойство. Как отмечалось, мы рассматриваем только непустые списки (если список пуст, то выполняется тривиально), так что нам нужно проверить, что not is_before удовлетворяет свойству инвариантности цикла. В инварианте класса мы установили, что:
is_before = (index = 0) index >= 0 index <= count + 1
Другими словами, is_before истинно, если и только если индекс позиции курсора равен нулю. После инициализации постусловие — предложение at_first, приведенное выше, — говорит, что индекс равен 1, так что is_before равно False и выполняется not is_before. Спецификация forth устанавливает:
forth — Передвинуть курсор к следующей позиции require not_after: not is_after ensure moved_forth: index = old index + 1
Так как index никогда не бывает отрицательным и увеличивается на каждом шаге на 1, то он не может быть нулем, следовательно, is_before не может иметь места. Так что пример 7.1 действительно инвариантное свойство.
Полезно выполнить трассировку выполнения цикла; используйте отладчик для выполнения цикла итерацией за итерацией, анализируя структуру объектов на каждом шаге.
Курсор - где он может находиться
Чтобы дополнить наше понимание цикла и примера, полезно вновь обратиться к инварианту класса LINE. Вы увидите два предложения, которые имеются у всех библиотечных классов, связанных со списковыми структурами:
non_negative_index: index. >= 0 index_small_enough: index <= count + 1
Это означает, что мы разрешаем курсору быть установленным:
Для общей схемы итерирования списка полезно то, что курсор может находиться на одной позиции слева или справа от элементов списка. Это иллюстрирует наш пример:
from some_list .start invariant — "Все элементы левее курсора обработаны" until some_list .is_after loop — "Обработка item в позиции курсора" some_list .forth variant some_list .count - some_list .index + 1 end
После обработки последнего элемента вызов forth передвинет курсор и is_after станет истинным, что гарантирует прекращение итераций. Что произойдет, когда в этой позиции (count + 1) вызвать forth, хотя здесь нет элементов списка? Предусловие метода forth запрещает такой вызов:
require not_after: not is_after
Этим наблюдением мы завершаем обзор основных свойств циклов и способов, позволяющих убедиться в их корректности.
7.6. Условные операторы
Следующая структура управления — условный оператор — не вызывает столько вопросов как цикл, но также относится к фундаментальным строительным блокам программы.
Условный оператор включает условие (в базисной форме) и два оператора. Он будет выполнять один из операторов в зависимости от выполнения условия.
Как способ решения задач, условный оператор соответствует разделению вариантов: разделить пространство задачи на две (или более) части, такие что проще решить задачу независимо для каждой части. Например, пусть мы пытаемся добраться от Эйфелевой башни до Лувра.
- Если погода хороша и мы не слишком устали, то дойти пешком до ближайшей станции метро, а далее добираться на метро.
- Иначе попытаться поймать такси.
Или классический пример из школьной математики: пусть нам нужно найти вещественные корни квадратного уравнения .
- Если дискриминант d, определенный как
, положителен, то можно получить два решения
.
- Иначе если dравно нулю, то решение одно b / 2a.
- Иначе нет вещественных корней (только комплексные).
Рисунок представляет стратегию решения задачи путем разделения ее на области, определяемые условием:
Базисной формой конструкции, реализующей эту стратегию решения задач, является:
if условие then — "Получить решение в Области 1" else —"Получить решение в Области 2" end
Вскоре мы обобщим конструкцию на случай разделения на большее число вариантов.