Лекция №1. Введение
Вид материала | Лекция |
СодержаниеСоответствие между методами и сообщениями. Классы как типы. Классы как объекты. |
- С. В. Шадрина Лекция 5 сентября, 15: 00-16: 30, Введение в геометрию пространства модулей, 5.97kb.
- Первая лекция. Введение 6 Вторая лекция, 30.95kb.
- Текст лекций н. О. Воскресенская Оглавление Лекция 1: Введение в дисциплину. Предмет, 1185.25kb.
- А. И. Мицкевич Догматика Оглавление Введение Лекция, 2083.65kb.
- Лекция введение в экологию (В. И. Торшин), 1146.79kb.
- Конспект лекций н. О. Воскресенская Москва 2008 Оглавление: Лекция Введение в дисциплину, 567.5kb.
- План лекций педиатрический факультет 1 семестр 1 лекция. Введение в анатомию человека., 216.63kb.
- Сидоров Сергей Владимирович Планы лекций Введение в профессионально-педагогическую, 19.81kb.
- Русской Православной Церкви и их особенности. 22 сентября лекция, 30.24kb.
- План лекций: Лекция №1. Введение в тему, общие сведения. Введение, 99.54kb.
Компиляторы.
Типичной отличительной чертой компиляторов является то, что некоторая информация теряется при переводе исходного текста в машинный код. Это наиболее заметно при преобразовании символических имен в адреса ячеек памяти. Так, к локальным переменным внутри процедуры скомпилированный код адресуется не по их именам, а через фиксированный сдвиг относительно начала блока памяти, создаваемого при входе в процедуру. Этот блок динамически выделенной памяти, называемый также записью активации процедуры, является ч
астью run – time стека. Он создается для хранения параметров, локальных переменных и т. п. Пусть, например, некоторая процедура содержит переменную x и структуру (запись) d, которая, в свою очередь, имеет поле данных y. Допустим, что x запоминается в ячейке локального блока выделенной памяти с индексом 20, а d начинается с ячейки 28. Пусть поле данных y н
ачинается с восьмого байта записи d.
При этих предположениях оператор присваивания X=D.Y может быть оттранслирован в одну ассемблерную команду, которая перемещает содержимое слова, расположенного в 36-м байте от начала блока, в 20-ю ячейку от начала блока:
Move Local_Block+36, Local_Block+20
Здесь существенным является то, что операторы ассемблера используют не символические имена, а только их смещения от начала блока. Ранее было отмечено, что объект напоминает структуру или запись в традиционных языках программирования. Подобно записям или структурам, полям данных объекта приписываются фиксированные смещения относительно начала объекта. Подклассы могут только расширять эту область памяти, но не сокращать ее, так что объем памяти, выделенный для объекта – потомка, строго больше, чем у объекта – предка. Смещения данных для подкласса должны соответствовать расположению аналогичных полей в надклассах.
Класс «Точка» содержит поля X и Y – координаты точки, класс «Окружность» добавляет к ним поля для значения радиуса и цвета, одновременно сохраняя для полей класса – предшественника в точности те же смещения. То, что смещения для полей родительского класса сохраняются в дочернем классе, позволяет методам, определенным для родительского класса, обрабатывать данные объекта с использованием постоянных смещений. Отсюда следует, что эти функции будут работать правильно независимо от того, к какому классу относится аргумент. Например, метод «Переместить» класса «Точка» будет работать как полагается независимо от класса объекта – получателя, поскольку этот метод пользуется только полями X и Y объекта.
Соответствие между методами и сообщениями.
Наиболее новаторское свойство ООП с точки зрения реализации заключается в том, что интерпретация сообщения зависит от класса получателя. То есть, различные классы объектов могут выполнять различные процедуры в качестве реакции на одно и то же сообщение. По этой причине каждый объект обязан содержать какой – то способ определения, какая процедура должна вызываться для сообщения, воспринимаемого объектом. Одно из возможных решений проблемы соответствия между методами и сообщениями заключается в том, чтобы разместить поля для методов в точности тем же способом, как выделяется память для полей данных. Значениями полей методов являются указатели на соответствующие функции.
Д
ля того, чтобы вызвать нужный метод, достаточно взять значение по правильному смещению внутри объекта, разыменовать его, чтобы получить процедуру, а затем вызвать полученную процедуру. Такая ситуация характерна для раннего связывания. Однако такой подход является затратным с точки зрения использования памяти. Каждый объект должен отводить память (один указатель) для каждого метода. Кроме того, создание объекта включает в себя инициализацию всех полей методов, что представляет собой ненужные затраты. Более прогрессивным является подход, при котором все экземпляры одного класса должны совместно использовать одни и те же методы. При таком подходе для каждого класса создается единственная таблица, называемая таблицей виртуальных методов (VMT). Все экземпляры класса содержат указатель на эту таблицу. Инициализация нового экземпляра подразумевает установку этого указателя на таблицу виртуальных методов. Значения полей в таблице виртуальных методов – это указатели на процедуры. В предположении, что эти процедуры известны компилятору и не изменяются во время выполнения программы, таблица виртуальных методов может быть создана статически во время компиляции. Чтобы выполнить метод, необходимо и достаточно знать только его смещение в таблице виртуальных методов. Пример класс «Фигура» и его порожденные классы «окружность и «квадрат».
Как и область данных, таблица VMT родительского класса входит во все таблицы классов – потомков, а смещения методов в VMT родительского класса будут теми же самыми и в таблицах дочерних классов. Класс, который наследует методы надкласса, просто копирует общую часть из его VMT в свою. В рассматриваемом примере у классов – потомков общие указатели на методы, унаследованные от родительского класса, и что порядок методов совпадает с тем, который задан в родительском классе. В силу того, что компилятор знает, где найти указатель на метод, то метод может быть вызван как стандартная процедура. Получатель рассматривается как если бы он был первым в списке аргументов, и тем самым он доступен как значение переменной this в С++ или self в Delphi. Пусть, например, что vtab – это внутреннее имя поля, представляющее указатель на VMT в объекте X, и что смещение для метода SHOW в таблице равно 12. Тогда вызов метода X.SHOW(x,y) будет преобразован в следующее внутреннее представление:
(* (* (X.vtab) ) [12]) (x,y)
Видно, что имя метода не появляется в выходном коде, и надлежащий метод будет выбираться независимо от того, является ли X объектом класса «Окружность» или «Квадрат». В терминах выполнения программы заголовок посылаемого сообщения требует две операции с указателями и одну операцию нахождения элемента в массиве. Поскольку все методы известны во время компиляции и не могут быть изменены во время выполнения, то VMT – это просто статические области данных, устанавливаемые компилятором. Рассмотренная ситуация характерна для техники позднего связывания. Из рассмотренного примера видно, что механизм позднего связывания работает весьма эффективно, поэтому, при создании классов следует отдавать предпочтение виртуальным методам. С точки зрения развития ООП приоритет виртуальных методов над статическими имеет основополагающее значение.
Поскольку редакторы связей (linker) и загрузчики (loader) превращают ссылки в смещения (разрешают ссылки), исходя из символических имен, необходимо обеспечить некоторый механизм, позволяющий избежать противоречия в случае, если два метода имеют одно и то же имя. Типичная схема комбинирует имя класса и имя метода, формируя некоторое внутреннее имя в виде: POINT::SHOW и т. п.
В случае наличия перегружаемых методов внутренние имена получаются кусочно-составными, например, в случае наличия трех конструкторов с различными сигнатурами: Complex::Complex, Complex::Complex_float, Complex::Complex_float_float. Но внутреннее имя не используется для пересылки сообщений. Оно применяется только для конструирования VMT с целью сделать имена уникальными для linker.
Интерпретаторы.
Подход, используемый для интерпретаторов, заключается в преобразовании исходной программы в некоторый «ассемблерный язык» высокого уровня, называемый байт – кодом. Каждая инструкция закодирована в одном байте. Старшие четыре бита используются для кодирования операции, младшие четыре бита используются для указания номера операнда. Сердцем интерпретатора является цикл, который охватывает огромный оператор типа swith. Цикл считывает последовательные байт – коды, а оператор выбора передает управление той цепочке кода, которая выполняет требуемое действие. Каждый объект в таких системах должен быть экземпляром некоторого класса, который называется Class. Этот класс позволяет перемещаться по иерархии типов в программе. Создание объекта инициируется сообщением new, которое определено как метод класса Class. Среди полей, содержащихся в объекте – классе, есть набор всех методов, которые соответствуют сообщениям, распознаваемым экземплярами класса. Другое поле указывает на надкласс этого класса. Когда посылается сообщение, интерпретатор прежде всего обязан определить п
олучателя.
Через указатель на класс внутри получателя интерпретатор находит объект, соответствующий классу получателя. Затем интерпретатор производит поиск в таблице методов, стараясь найти тот, который соответствует пересылаемому сообщению. Если такого метода не обнаружено, интерпретатор поднимается по цепочке наследования, производя поиск среди методов надклассов, пока или не будет найден подходящий метод, или цепочка надклассов не будет исчерпана. В последнем случае интерпретатор сообщит об ошибке.
Лекция №13. Классы в языках программирования.
Понятие «класс» можно представлять себе по – разному. Можно представлять классы как некоторые шаблоны, с помощью которых штампуются готовые программы. Можно представлять классы как обобщение структуры или записи, т. е., класс – это структура с полями данных и полями функций. Представление о классе зависит от того, какой язык программирования рассматривается. Одни языки, такие, как С++ или Delphi, рассматривают класс как тип данных, подобный целым числам, записям или структурам. Другие языки, подобные Java, рассматривают класс как объект.
Классы как типы.
В классических языках программирования понятие «тип данных» является основополагающим. Типы данных разбивают значения на классы эквивалентности, обладающие общими атрибутами и операциями. Типы данных накладывают семантические ограничения на выражения: операторы и операнды составных выражений должны быть совместимы. Система типов есть набор правил, связывающий тип данных и любые имеющие смысл подвыражения в языках программирования. Такие языки программирования, как С++ или Delphi – Pascal, рассматривают классы как обобщение понятия «структура» или «запись». Класс также определяет поля, и каждый экземпляр класса, т. е., объект, содержит свои собственные значения полей. В отличие от структур или записей класс имеет поля нового типа, представляющие собой методы (процедуры или функции). В отличие от полей данных имеется только одна копия такого поля, совместно используемая всеми экземплярами (объектами) класса. Такая интерпретация понятия класса хорошо стыкуется с традиционным подходом. Все объекты определенного типа имеют единообразные поля, и они по крайней мере реагируют на один и тот же набор команд. Однако как только к концепции класса добавляется наследование, тип данных превращается в нечто более сложное. Наследование, вообще говоря, есть механизм модификации поведения, приводящий к эволюции системы. В некотором смысле можно думать о наследовании просто как о средстве, расширяющем существующий тип данных, но такая интерпретация не является полностью адекватной по целому ряду причин. Известно, что методы могут частично или полностью переопределяться. Переопределенный метод не просто изменяет поле в записи, но скорее изменяет поведение объекта, причем произвольным образом. Пусть, например, имеется разработанный набор классов, и для каждого метода имеются наборы контрольных данных и результаты их обработки. Выполнение программы с контрольными данными доказывает правильность ее работы. Далее некто создает подклассы исходных классов, переопределяя некоторые методы. И если типы данных определяют инварианты поведения экземпляров, как это имеет место при рассмотрении классов как типов, то для объектов подкласса это условие также будет выполнено. Тогда можно заменить в программе объекты класса на объекты подкласса предполагая, что получившаяся программа останется правильной. Таким образом здесь проявляется принцип подстановки. Однако переопределение методов, вообще говоря, не дает гарантии полной корректности работы модифицированной программы, так как не всегда некоторые единые входные и выходные условия будут справедливы для родительского и переопределенного методов. Типичным источником проблемы неоднозначности является замена одного метода другим, для которого не сохранены некоторые существенные аспекты поведения.
Пусть, например, имеется программа, отображающая колоду карт на карточном столе. В ней реализован метод, создающий изображение карт путем прохода по их списку. Пусть некоторым образом доказана и проверена корректность этой программы. Однако известно, что графические операции выполняются достаточно медленно, и метод отображения модифицирован для рисования карты в фоновом режиме. То есть, когда объект – карта отображает самого себя, то запускается фоновый процесс, осуществляющий рисование, а программа продолжает работу в фоновом режиме. Все, что требуется для решения такой задачи – это создать дочерний класс, который наследует все из родительского класса, и переопределяет только метод рисования. Далее, исходя из принципа полиморфизма, можно заменить ссылки на родительский класс ссылками на дочерний класс в инициализирующей части программы, чтобы использовать последний. Однако в результате такого изменения пропадает уверенность в том, что карты будут рисоваться в правильном порядке, т. е., будет наблюдаться некоторый случайный порядок рисования. Существенным моментом здесь является то, что интерфейс метода и результат его работы не изменились (как исходный, так и переопределенный методы по – прежнему рисуют карту). Однако было изменено поведение метода рисования. Поэтому доказательство правильности программы, основанное на предположении, что изменений не будет, больше не проходит. Кроме того, необходимо учитывать связь между типами данных и управлением памятью. В случае классов возникают проблемы при выполнении операции присваивания. Отсюда следует вывод, что понятие класса почти точно соответствует большинству интуитивных представлений о типах, но совпадение не является полным. Второй взгляд на классы как на объекты устраняет некоторые проблемы, обходя стороной собственно типы данных.
Классы как объекты.
Основная философия ООП заключается в делегировании полномочий индивидуальному объекту. Он ответственен за свое внутреннее состояние и изменяет его в соответствии с несколькими фиксированными правилами поведения. С другой стороны, каждое действие должно быть обязанностью некоторого объекта, или же оно не будет выполнено. Отсюда следует, что создание новых объектов есть некоторое действие, за которое, вообще говоря, кто – то должен отвечать. Пусть имеется некоторый централизованный объект «для создания объектов». Тогда запрос на создание нового объекта преобразовывается в обращение к этому объекту. В качестве аргументов передается размер создаваемого объекта и список его методов. Такой механизм работоспособен, однако внутреннюю информацию о классе (его размер и методы) должен хранить некоторый внешний объект. Более удачное решение инкапсулирует эту информацию в самом классе, помещая между пользователем, который хочет создать новый объект, и кодом, который осуществляет распределение памяти, управляющую прослойку. Она знает размеры объектов и их методы. Поэтому не требуется запоминания указанных данных ни пользователю, ни коду распределения памяти. Отсюда получается схема, в которой имеется один новый объект для каждого класса системы. Основная функция этого объекта заключается в создании новых экземпляров класса. Для этого он должен поддерживать информацию о размере класса и его методах. В практическом отношении этот объект и есть класс. В свою очередь, так как каждый объект должен быть экземпляром некоторого класса, то и описанный выше объект не является исключением. В языке Java он является экземпляром класса, называемого Class. Таким образом, для каждого класса в системе имеется представляющий его объект Class. Он может использоваться для получения основных сведений о классе и для создания новых объектов класса. Кроме того, он позволяет перемещаться по иерархии типов в программе, которая фактически становится частью программной среды. Появляется новая возможность работы с классами, заключающаяся в создании объектов, тип которых задается в в
Object
Class
aSet
иде строки. Создание объекта инициируется сообщением типа new, которое определено как метод класса Class. Каждый создаваемый объект является экземпляром некоторого класса и содержит указатель на объект, представляющий этот класс. Во время пересылки сообщения этот указатель используется для того, чтобы найти метод, соответствующий селектору обрабатываемого сообщения. Необходимо различать взаимосвязь подклассов и взаимосвязь экземпляров.
Class
aSet
Set
Object
На рисунке класс Class является подклассом класса Object. Тем самым объект Class указывает на объект Object как на свой надкласс. С другой стороны, объект Object является экземпляром класса Class – тем самым Object указывает обратно на Class. Класс Class является классом сам по себе и тем самым является экземпляром самого себя. Класс Set имеет объект aSet, являющийся экземпляром класса Class, но, кроме того, неявно и подклассом класса Object.
Метаклассы.
Инициализация объектов является важной частью процесса создания объекта. Поскольку объекты отвечают за поддержание собственного состояния, было бы полезно, если бы объект, занимающийся созданием новых экземпляров класса, мог бы обеспечить и их должную инициализацию. Если все объекты класса являются экземплярами одного и того же класса, то все они обязаны выполнять один и тот же набор методов, и, стало быть, вести себя одинаково. Однако создание и инициализация часто требуют больше информации, чем размер объекта и список его методов. Например, при создании массива нужно знать его размерность, и т. п. Поэтому, чтобы экземпляры класса могли иметь свое собственное поведение, необходимо сделать их экземплярами своих собственных классов. Метакласс является классом классов. Каждый метакласс имеет только один экземпляр, который является собственно классом. Метаклассы организованы в иерархию «класс – подкласс», которая отражает аналогичную иерархию для исходного класса. Эта иерархия содержит метаклассы и имеет корень в классе Class, а не в классе Object. Код, специфический для отдельного класса, связывается с метаклассом этого класса. Пусть имеется следующая иерархия классов:
Object – надкласс всех объектов;
Collection – абстрактный надкласс всех совокупностей;
Bag – класс множеств.
Соответствующая иерархия метаклассов имеет так:
Object – надкласс всех объектов;
Class – поведение, общее для всех классов;
Metaclass-object – инициализация всех объектов;
Metaclass-Collection – инициализация совокупностей;
Metaclass-Bag – инициализация множеств.
Пусть, например, объекты класса Bag содержат некоторый словарь, используемый для хранения фактических значений. Метакласс для класса Bag переопределяет метод new, чтобы производить инициализацию словаря при создании новых объектов. Это действие осуществляется методом new, определенном в классе Metaclass-Bag и переопределяющем метод класса Class. Поскольку класс Bag является экземпляром класса Metaclass-Bag, то указанный метод будет вызываться в ответ на сообщение new. Таким способом надкласс создает новый объект. Как только новый объект создан, ему посылается сообщение «инициализировать словарь». Концепция метаклассов позволяет придать конкретные свойства функции инициализации для индивидуальных классов.