Как происходит отслеживание свободного экстента? |
Переменные, присваивание и ссылки
Динамические псевдонимы
Давайте вернемся немного назад и посмотрим, что же делает ссылки коварными (этот раздел можно считать дополнительным и пропустить при первом чтении).
В дополнение к проблеме пустоты ссылок у них есть еще одна особенность — они позволяют одному объекту иметь несколько имен. Такие имена называются псевдонимами, как в случае, когда у некоторых людей, чаще писателей, есть псевдонимы (Марк Твен — это псевдоним писателя Самюэля Клеменса). Ссылочное присваивание приводит к появлению динамических псевдонимов, определяемых в период выполнения. Рассмотрим присваивание:
b : = a
Оно является причиной появления динамических псевдонимов, поскольку в результате присваивания а и b присоединены к одному и тому же объекту, являются его именами. Мы не можем полагать, что так будет всегда, поскольку это может встретиться при одном выполнении программы и не проявиться в других сеансах, — все зависит от структуры управления.
Почему динамические псевдонимы опасны? Проблема в том, что они создают дополнительные трудности в доказательстве свойств программы. Предположим, что можно выразить свойство объекта, используя его имя b. Пусть теперь выполняется операция над объектом, используя имя а. Операция может повлиять на свойство b, но как учесть это влияние, если операция вообще не упоминает имя b?
Рассмотрим следующую схему:
— Здесь вы знаете, что для b выполняется свойство: P (b) OP (a) — Операция, выполняемая над a и не упоминающая b — Можно ли быть уверенным, что P (b) по-прежнему будет выполняться?
Ответ на последний вопрос будет утвердительным, если а и b являются различными переменными базисных типов, таких как INTEGER: ничего из того, что вы делаете с а, не влияет на b.
Со ссылками и динамическими псевдонимами все становится по-другому! Предположим, что речь идет об объектах класса STUDENT и процедура raise увеличивает рейтинг. Рассмотрим схему:
— Здесь вы знаете, что b.gpa меньше 4 a.raise — Что можно сказать теперь о b.gpa?
Эта схема может встретиться в методе с явным контрактом:
dubious — Иллюстрация опасностей псевдонимов. require low_gpa: b.gpa < 4 do ... Возможно, другие инструкции ... a.raise ensure — Что можно утверждать о b.gpa? end
Ответ на последний вопрос зависит от того, является ли b псевдонимом а. Если нет, то выполнение метода сохранит b.gpa и не изменит все другие свойства b. Но, если b — псевдоним а, то свойство, установленное в предусловии, нарушится и не может служить частью постусловия. Если условие gpa < 4 является частью инварианта класса, то, будет ли dubious сохранять его, также зависит от наличия псевдонимов.
Проблема не в семантической неопределенности. Нет никаких сомнений, что все, что происходит, определяется тем, являются ли а и b псевдонимами. Трудности возникают в нашем методе вывода утверждений о поведении программы. При рассмотрении семантики оператора мы должны учитывать возможность существования псевдонимов. Это предохранит нас от ложных заключений, что если в операторе не встречается имя переменной, то оператор не может повлиять на любое свойство, связанное с этой переменной.
Действительной трудностью является то, что псевдонимы не знают границ, они проникают сквозь границы модулей вашей программы и потенциально могут распространяться на всю программу. Если бы речь шла только о присваивании в пределах одного класса, как в нашем примере, то с этим легче было бы справиться, зная область действия псевдонимов. Но передача аргументов методу r (a: T), в момент вызова r (b) приводит к появлению динамических псевдонимов, совершенно аналогично присваиванию переменных. Так что, как только вы передаете ссылку методу, который может передать ее далее другому методу, процесс распространения псевдонимов может повлиять на удаленные части всей программы и воздействовать на объект, который, как вы наивно предполагаете, принадлежит лично вам.
Возможность удаленной модификации часто желательна. Проблема в том, чтобы точно знать, как все происходит. Можно заметить, что сложности связаны главным образом со спецификациями. Предыдущая процедура может быть переписана так, чтобы постусловие более точно отражало наши намерения:
dubious — Иллюстрация опасностей псевдонимов. require low_gpa: b.gpa < 4 do ... Возможно, другие инструкции ... a.raise ensure a.gpa = old a.gpa + Increment end
Здесь Increment — это значение, добавляемое в методе raise. Как и ожидалось, выполнение влияет на a. Но эта спецификация ничего не говорит о том, что не должно происходить, — что другие объекты, такие как b, не должны изменяться. Конечно, можно было бы добавить кучу предложений постусловия в форме:
b = old b c = old c ... и так для каждого атрибута класса ...
Все это ужасно и мало привлекательно.
Вопрос в том, как написать практический контракт, который бы не только специфицировал, как изменяются некоторые свойства, но перечислял бы все свойства, которые не должны изменяться. Последние известные как рамочные свойства, а в целом проблема называется проблемой рамок.
Эта проблема ныне находится на этапе исследований. Пока еще не найден консенсус для выражения рамочных свойств, в отличие от принципиального соглашения по представлению (отличающегося деталями) обычных контрактов — предусловий, постусловий, инвариантов.
Обсуждение проблемы рамок далеко выходит за пределы нашего рассмотрения. Но следует понимать, что из-за динамических псевдонимов возникают серьезные проблемы с выводом утверждений о корректности работы программы. Практически это означает, что следует быть крайне внимательным при работе со ссылками, особенно при передаче объектов другим методам. По этой причине, как обсуждалось, нетривиальные операции над ссылками лучше возлагать на специализированные библиотеки и кластеры. Теперь вы знаете более точно, что понимается под "нетривиальностью": любые действия, которые могут стать причиной появления псевдонимов и приводить к неожиданным эффектам.
Еще одно общее замечание. Можно попытаться всю вину возложить на ссылки и предположить, что механизм неудачен и от него следует отказаться. Но эффект от этого, видимо, был бы еще хуже, поскольку дело не в ссылках. Старейший язык программирования Фортран не имел ссылок, но разрешал помещать данные в общий пул (массив), доступный для всех модулей. Элементы данных были доступны по индексу, что аналогично псевдонимам. Риски те же, но еще страшнее, поскольку работа ведется на более низком уровне абстракции.
Псевдонимы не являются программистской концепцией. Это свойство человеческой цивилизации — именовать вещи и давать одной и той же сущности разные имена. "Прекрасная дочь Леды, супруга несчастного Менелая, любовница Париса" — все это ссылки на одну и ту же героиню — прекрасную Елену Троянскую. Филологи вводят различные термины для такой синонимии: метафора, метонимия, синекдоха, аллегория.
Вот пример из обыденной жизни. Ваш друг, говоря об общей знакомой студентке, сообщил: "Мария озабочена, поскольку перед экзаменом ее рейтинг составлял 3,7". Вскоре вы услышали разговор студентов: "Моя подруга прекрасно сдала экзамен, но ее рейтинг может возрасти максимум на 0,4". Можете ли вы полагать, что в разговоре речь шла о Марии? Сомнительно, в псевдонимах нельзя быть уверенными. Возможно, у Марии все прекрасно, но гарантировать это нельзя.
Так что не стоит винить программистов в нечеткости мышления. Они, как и все, радуются и страдают от софизмов, свойственных образцам интеллектуального вывода.
9.7. Ключевые концепции, изучаемые в этой главе
- Программам требуется в ходе выполнения изменять значения своих переменных (исключение — программы, написанные на функциональных языках). Основным способом является присваивание.
- Присваивание применяется как к базисным значениям, которые копируются, так и к ссылкам, для которых копируется только ссылка, но не связанный с ней объект.
- Ссылочное присваивание вводит динамические псевдонимы, в результате которого объект становится доступен через различные имена. Это усложняет рассуждения о программе.
- Переменные включают атрибуты, представляющие поля объекта, и локальные переменные, используемые только внутри отдельных методов.
- Ссылки позволяют описать и выполнять действия над связанными структурами данных. Примером операции может служить обращение структуры данных.
Assignment | Присваивание | Attribute | Атрибут |
Local variable | Локальная переменная | Temporary | Временная |
variable entity | Переменная сущность | variable | переменная |
variable | Переменная |
9-У. Упражнения
9-У.1. Словарь
Дайте точные определения терминам словаря (словарь 1).
9-У.2. Словарь
Дайте точные определения терминам словаря (словарь 2).
9-У.3. Концептуальная карта
Добавьте новые термины в концептуальную карту, построенную в предыдущих главах
9-У.4. Терминология
- Является ли каждая функция сущностью?
- Является ли каждая сущность функцией?
- Может ли функция быть запросом?
- Является ли Result атрибутом?
- Является ли Result функцией?
- Является ли Result сущностью?
- Является ли Result переменной?
- Все ли переменные локальны?
- Является ли каждый атрибут сущностью?
- Является ли каждый метод запросом?
- Является ли каждый запрос сущностью?
- Является ли каждый атрибут переменной?
- Является ли каждая функция переменной?
- Является ли каждая сущность переменной?
- Может ли запрос быть переменной?
- Может ли функция быть переменной?
- Является ли каждая переменная сущностью?
9-У.5. Обмен значениями (свопинг)
Пусть var1 и var2 — переменные типа INTEGER с обычными арифметическими операциями. Можете ли вы написать операторы, выполняющие обмен значениями без использования любых локальных переменных и иных сущностей? (Ответом является старый программистский трюк) Какие ограничения при этом накладываются?
9-У.6. Процедура обращения
Пусть атрибут s в классе С обозначает ссылку на первый STOP-объект линии метро. Напишите процедуру reverse, обращающую порядок остановок на линии, так, чтобы s задавал теперь первую остановку в модифицированной линии (последнюю в оригинале). У процедуры reverse нет аргументов (если в процедуре задать аргумент s, это усложнило бы задачу, так как нельзя присваивать значение формальному аргументу, хотя и можно модифицировать присоединенный объект). Подсказка: используйте функцию reversed в качестве источника вдохновения.
9-У.7. Изменение остановок двумя способами
Внесите изменения в класс STOP, такие, чтобы каждая остановка была связана как с правым, так и с левым соседом, если он есть. Например, link метод должен теперь вызывать link_right и link_left.
- Добавьте put_left в дополнение к put_right и remove_left в дополнение к remove_right.
- Обновите reversed.
9-У.8. Список остановок как класс
Если вы выполнили предыдущее упражнение, то перепишите процедуры remove_right, put_right, reversed, а также reverse как методы класса LINE, а не STOP.
9-У.9. Правила языка
В присваивании var:= exp, левая часть var должна быть переменной, она не может быть выражением, включающим квалифицированный вызов, такой как some_object.one_of_its_fields. В чем причина появления такого правила? (Подсказка: обратитесь к принципу скрытия информации и инвариантам класса)