Добавил:
natribu.org Все что нашел в интернете скидываю сюда Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Точно Не проект 2 / Не books / Источник_1

.pdf
Скачиваний:
10
Добавлен:
01.02.2024
Размер:
20.67 Mб
Скачать

270

Глава 5

 

 

(setf x (make-sportsman : name ’Victor : sport ’football))

#S(SPORTSMAN :NAME VICTOR :SPORT FOOTBALL)

2. Символ SPORTSMAN становится именем типа. Принадлежность некоторого объекта программы к типу SPORTSMAN можно проверить с помощью предиката TYPEP:

(typep x ’sportsman) T

Если объект не принадлежит типу SPORTSMAN, то вызов предиката TYPEP вернет значение NIL.

3. Генерируется предикат, имя которого образуется из имени структурного типа и строки “-P”. Например, SPORTSMAN-P. Предикат проверяет принадлежность объекта программы к введенному структурному типу:

(sportsman-p x) T

4. Генерируются функции-селекторы, обеспечивающие доступ к слотам структуры. Имя функции-селектора состоит из имени абстрактного типа и имени соответствующего слота. Например, чтение слотов структуры X можно выполнить с помощью вызовов:

(sportsman-name x) VICTOR

(sportsman-sport x) FOOTBALL

Функции-селекторы применяются также для изменения значений слотов структуры:

(setf (sportsman-name x) ’Aleksey) ALEKSEY

(setf (sportsman-sport x) ’Tennis) TENNIS

Структуры могут определяться иерархически, т. е. одни структуры могут наследовать слоты, определенные в других структурах:

(defstruct (sportsman_footballer (: include sportsman )) (team nil)

(position nil))

Здесь определяется структура SPORTSMAN_FOOTBALLER на основе структуры SPORTSMAN, в которую добавляются слоты TEAM и POSITION, предназначенные для хранения названия команды и типовой позиции футболиста на игровом поле, соответственно. При создании экземпляра структуры SPORTSMAN_FOOTBALLER доступны все слоты родительской структуры

SPORTSMAN:

Язык Лисп

271

 

 

(setf y (make-sportsman_footballer

:name ‘Dmitriy :sport ‘football

:team ‘Dinamo

:position ’centre_forward ))

Допускается также вложение структур. Определим структуру DATEOFBIRTH, предназначенную для хранения даты рождения:

(deftstruct dateofbirth

 

(day nil)

; день

(month nil)

; месяц

(year nil))

; год

Теперь можно создать экземпляр структуры SPORTSMAN, представив слот DATA в виде структуры:

(setf z (make-sportsman :name ’Olga :sport ’chess

:data (make-dateofbirth

: day 20 : month 10

: year 1983 )))

Доступ к вложенным слотам выполняется с помощью последовательных вызовов функций-селекторов:

(dateofbirth-day

(sportsman-data z)) 20

(dateofbirth-month (sportsman-data z)) 10

(dateofbirth-year

(sportsman-data z)) 1983

5.21. Объектно-ориентированное программирование

Определение класса и создание экземпляров класса. Объектно-

ориентированная система языка Коммон Лисп (Common Lisp Object System-CLOS) использует понятие родовой или общей функции. Этим она существенно отличается от языков Object Pascal или C++. Объектная модель языка Коммон Лисп включает в себя такие понятия как класс, экземпляр класса, множественное наследование и методы.

Интересно отметить, что понятие класс является неотъемлемой частью системы типов языка Коммон Лисп. Можно, например, выяснить от-

272

Глава 5

 

 

носится ли экземпляр объекта к заданному классу с помощью предиката TYPEP. Более того, многие типы данных – это классы. Например, тип ARRAY соответствует классу ARRAY.

Класс представляет собой объектно-ориентированный инструмент для создания новых абстрактных типов данных. Определение того или иного типа данных сопряжено с заданием множеств значений и определением допустимых операций над этими множествами. Поэтому можно сказать, что класс – это объект программы, который содержит (инкапсулирует) в себе данные и функции по их обработке.

Определение класса выполняется с помощью макроса DEFCLASS и в общих чертах напоминает определение структуры:

(defclass имя-класса ({имя-суперкласса}*) ({описание-слота}*))

Имя класса представляет собой символ. За именем класса следует список имен суперклассов, которые выступают в роли родительских классов для определяемого класса. Это даёт возможность дочерним классам наследовать свойства родительских классов. Список имен суперклассов может быть пустым. Описание слота представляет собой имя слота или список, состоящий из имени слота и его опций:

описание-слота::=имя-слота | ( имя-слота {опции-слота}+)

Опции слота обеспечивают инициализацию значений слота, задание функций, обеспечивающих доступ к значениям слота, и др. Определим в качестве примера класс MOVING-OBJECT (подвижный объект), который будет характеризоваться своими координатами (слоты X-POS и Y-POS), скоростью (слоты X-SPEED и Y-SPEED), массой (слот MASS):

(defclass moving-object( )

(x-pos y-pos x-speed y-speed mass ))

#<STANDARD-CLASS MOVING-OBJECT>

Для создания экземпляра класса применяется конструктор MAKEINSTANCE. Аргументом конструктора является имя класса. В качестве результата конструктор возвращает экземпляр класса. Связать некоторый символ с экземпляром класса можно с помощью формы setq:

(setq mv1 (make-instance ’moving-object))

#< MOVING-OBJECT @ #x20ae2c22>

Синтаксис oпций слота в макроформе DEFCLASS определяется с помощью следующей формулы:

Язык Лисп

273

 

 

опции-слота::= :reader

имя_функции_чтения

:writer

имя_функции_записи

:accessor

имя_функции_доступа

:initarg

имя_функции_инициализатора

:initform

форма

:allocation

место_размещения

Опции слота объявляются с помощью ключевых параметров, которые являются необязательными. Перед ключевым параметром ставится двоеточие. Опции слота определяют имена функций, обеспечивающих обработку значений слота, а также определяют правила вычисления начальных значений слота. Опция READER объявляет функцию, возвращающую значение слота. Опция WRITER объявляет функцию, изменяющую значение слота. Опция ACCESSOR комбинирует возможности опций READER и WRITER. Она определяет функцию, которая может читать значения слота или использоваться, совместно с формой SETF, для изменения значений слота. Функцию, объявляемую с помощью ключа ACCESSOR, будем называть функцией доступа к слоту. Опция INITARG определяет имя, с помощью которого можно задавать начальное значение слота, а опция INITFORM определяет форму, с помощью которой при каждом вызове конструктора MAKE-INSTANCE вычисляются начальные значения слота. Опция ALLOCATION определяет способ распределения памяти для слота. Память, выделяемая под слот, может быть связана либо с классом, либо с экземпляром класса. Следовательно, место_размещения может иметь значение либо CLASS (класс), либо INSTANCE (экземпляр). Если в качестве места размещения указан класс, то доступ к слоту выполняется как к участку разделяемой памяти. Поэтому все экземпляры класса будут иметь одно и то же значение слота. Если в качестве места размещения слота указан экземпляр класса, то слоты экземпляров класса будут иметь уникальные значения. Если опция ALLOCATION не задана, то место_ размещения получает значение INSTANCE.

Следующий пример демонстрирует применение указанных опций:

(defclass moving-object()

 

 

((x-pos

:initarg

:x-pos

:initform 0. :accessor get-x-pos)

(y-pos

:initarg

:y-pos

:initform 0.

:accessor get-y-pos)

(x-speed

:initarg

:x-speed

:initform 0.

:accessor get-x-speed)

(y-speed

:initarg

:y-speed

:initform 0.

:accessor get-y-speed)

(mass

:initarg

:mass

:initform 0.

:accessor get-mass )))

#< STANDARD-CLASS MOVING-OBJECT>

274

Глава 5

 

 

В этом случае для задания начальных значений слотов в вызове MAKEINSTANCE могут использоваться ключевые аргументы x-pos, y-pos и т.д. Если эти аргументы не будут заданы, то начальное значение слота будет определяться с помощью выражения, стоящего после ключевого слова INITFORM. Доступ к значениям слотов выполняется с помощью функций GET-X-POS, GET-Y-POS и т.д. Кроме этого, для доступа к значениям слотов можно использовать функцию SLOT-VALUE. Аргументами функции являются имя экземпляра класса и имя слота. В качестве результата возвращается значение слота:

(setf (slot-value mv1 ’x-pos) 40)

Рассмотрим пример. Пусть требуется запрашивать начальное значение слота у пользователя, если оно не задано при вызове конструктора

MAKE-INSTANCE. Определим функцию READ-SLOT-VALUE, которая будет вводить значение слота:

(defun read-slot-value (zapros) (print zapros)(read))

Аргумент ZAPROS предназначен для вывода на экран строки запроса. Теперь переопределим класс MOVING-OBJECT

(defclass moving-object()

((x-pos

:initarg :x-pos

 

:initform (read-slot-value ”Введите координату Х”)

 

:accessor get-x-pos)

(y-pos

:initarg :y-pos

 

:initform (read-slot-value ”Введите координату У”)

 

:accessor get-y-pos)

(x-speed

:initarg :x-speed

 

:initform (read-slot-value ”Введите скорость Х”)

 

:accessor get-x-speed)

(y-speed

:initarg :x-speed

 

:initform (read-slot-value ”Введите скорость У”)

 

:accessor get-y-speed)

(mass

:initarg :mass

 

:initform (read-slot-value ”Введите массу”)

 

:accessor get-mass)))

и создадим экземпляр класса с помощью MAKE-INSTANCE и SETF:

(setq mv2 (make-instance ’moving-object

:x-pos 35 :y-pos 40))

”Введите скорость Х” 10 ”Введите скорость У” 20 ”Введите массу” 250000

Язык Лисп

275

 

 

#< MOVING-OBJECT @ #x20bfddaa>

При запросе значений слотов X-SPEED и Y-SPEED получим:

(get-x-speed mv2) 10

(get-y-speed mv2) 20

Определение методов. В объектно-ориентированном программировании с понятием класс связывают не только информацию о некотором объекте, но и правила ее обработки, оформленные в виде программ. Такие программы называют методами класса.

В языке Коммон Лисп методы представляются функциями и определяются вне класса с помощью макроформы DEFMETHOD, которая вызывается с помощью следующей упрощенной записи:

(defmethod имя_функции специальный_лямбда_список {форма}*)

Вызов формы DEFMETHOD напоминает вызов формы DEFUN. Отличие состоит в том, что в вызове формы DEFMETHOD присутствует специальный_лямбда_список. Специальный лямбда список подобен обычному списку, за исключением одного – его формальные параметры могут представ-

лять пары (символ спецификатор_символа), где символ – это имя формаль-

ного параметра, а спецификатор_символа определяет класс принадлежности формального параметра. Наиболее общий класс в языке Коммон Лисп представляется атомом Т. Если спецификатор символа не задан, то параметр относится по умолчанию к классу Т.

В качестве примера определим методы для класса MOVING-OBJECT, позволяющие определять скорость движения объекта (VELOCITY), направление движения (DIRECTION) и кинетическую энергию (EKIN):

(defmethod velocity ((mv moving-object))

(sqrt (+ (*(get-x-speed mv)(get-x-speed mv)) (*(get-y-speed mv)(get-y-speed mv)) )))

(defmethod direction ((mv moving-object))

(atan (get-y-speed mv)(get-x-speed mv)) )

(defmethod ekin ((mv moving-object))

(* 0.5 (get-mass mv)(velocity mv)(velocity mv)) )

Как видно из приведенных примеров, связь метода и класса устанавливается посредством специального лямбда-списка.

276

Глава 5

 

 

Иногда различные классы характеризуются сходным поведением. Например, рассматривая перемещение на экране точки и круга, легко заметить сходство этих операций. В этом случае целесообразно определить функцию, которую можно было бы применять к разным классам. В языке Коммон Лисп для решения этой задачи применяется общая или родовая функция (generic function). Действия, выполняемые общей функцией, зависят от типов аргументов, заданных при её вызове. Можно говорить о том, что аргументы переопределяют поведение общей функции, и поэтому ее иногда называют переопределяемой функцией. Общая функция – это функция, на основе которой определяются один или несколько методов, каждый из которых посредством специального лямбда-списка связывается со своим классом.

Общая функция может быть определена либо с использованием формы DEFMETHOD, либо с помощью формы DEFGENERIC.

Форма DEFGENERIC позволяет одновременно определять несколько методов, включаемых в общую функцию. Форма DEFMETHOD предоставляет возможность отдельного определения каждого метода. Каждый новый вызов DEFMETHOD добавляет очередной метод к определению общей функции.

Синтаксис формы DEFGENERIC задается следующей упрощенной формулой:

(defgeneric имя_функции лямбда_список {описание_метода}*)

описание метода::=(:method специальный_лямбда_список {форма}*)

Аргументы формы DEFGENERIC заданы именем функции, лямбда списком и описаниями методов. Описание метода включает в себя специальный_лямбда_список. Каждый специальный лямбда-список по количеству параметров должен совпадать с основным лямбда-списком формы

DEFGENERIC.

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

(defclass rectangle() ;определение класса - прямоугольник

((height :initarg :height :accessor get-height) (width :initarg :width :accessor get-width)) )

(defclass circle-1() ; определение класса - круг

((radius :initarg :radius :accessor get-radius)) ) (defgeneric area(figure) ;общая функция AREA

Язык Лисп

277

 

 

(:method ((figure rectangle)) ;метод для прямоугольника

(* (get-height figure) (get-width figure)) )

(:method ((figure circle-1)) ;метод для круга

(* (get-radius figure) (get-radius figure) pi)) )

(setf rectangle1 (make-instance ’rectangle :height 5 :width 5)) (setf circle1 (make-instance ’circle-1 :radius 10))

Теперь можно использовать функцию AREA для вычисления площади различных фигур:

(area rectangle1) 25

(area circle1) 314.159265

Необычным для общей функции является то, что в нее можно добавлять новые методы. Это можно выполнять с помощью формы DEFMETHOD. Добавим в функцию AREA метод, позволяющий вычислить площадь прямоугольного треугольника:

(defclass triangle()

((side-a :initarg :side-a :accessor get-side-a) (side-b :initarg :side-b :accessor get-side-b) ))

(defmethod area ((figure triangle))

(* (get-side-a figure)(get-side-b figure) 0.5))

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

(setf triangle1 (make-instance ’triangle

:side-a 5

:side-b 3))

(area triangle1) 7.5

Наследование. Классы в языке Коммон Лисп упорядочены в иерархическую структуру. Это позволяет дочерним классам наследовать свойства родительских классов. Родительские классы в Коммон Лиспе называют суперклассами. Дочерние классы могут наследовать свойства сразу нескольких родителей, т.е. допускается множественное наследование.

Определим класс SHIP, представляющий судно. При этом будем исходить из того, что судно обладает свойствами подвижного объекта (класс MOVING-OBJECT), а также для него характерны свойства, приписываемые объектам, построенным человеком. Объекты построенные (сконструированные) человеком можно представить в виде класса CONSTRUCTEDOBJECT:

278

Глава 5

 

 

(defclass constructed-object()

((constructor :initarg :constructor :accessor constructor) (date-of-construction :initarg :date-of-construction

:accessor date-of-construction)

(next-date-of-revision :initarg :next-date-of-revision :accessor next-date-of-revision)))

С данным классом будет связан метод, определяющий время эксплуатации объекта в днях до следующей проверки, начиная с текущего момента:

(defmethod days-to-go ((co constructed-object))

(truncate (- (next-date-of-revision co) (get-universal-time)) 86400))

Здесь GET-UNIVERSAL-TIME – встроенная функция языка Коммон Лисп, возвращающая значение Всемирного времени. Число 86400 обозначает количество секунд в сутках.

Теперь определим класс SHIP (судно), дополнительно указав слоты названия судна, порта приписки и фамилии капитана:

(defclass ship (moving-object constructed-object)

((name :initarg :name :accessor name)

(native-harbour :initarg :native-harbour

:accessor native-harbour)

(captain :initarg :captain :accessor captain)))

При создании экземпляра объекта в вызове MAKE-INSTANCE необходимо определить начальные значения всех атрибутов:

(setf ship1 (make-instance ’ship :x-pos 1289 :y-pos 311 :x-speed 12

:y-speed 28

:mass 250000

:constructor ”Blohm”

:date-of-construction 312348876 :next-date-of-revision 4045060272

:name ”AMOCO” :native-harbour ”Hamburg”

:captain ”Ole Jensen”))

В данном примере символ SHIP1 представляет экземпляр класса SHIP, который унаследовал от своих родительских классов MOVING-OBJECT и CONSTRUCTED-OBJECT все их слоты (атрибуты) и методы. Поэтому вызов (ekin ship1) вернет значение 1.16*108 , хотя метод EKIN непосредственно не связан с классом SHIP.

Язык Лисп

279

 

 

В рассмотренном примере с классами MOVING-OBJECT и CONSTRUCTED-OBJECT не были связаны одноименные методы, поэтому вопросы о порядке наследования методов не возникали. Если с родительскими классами связаны одноименные методы, то необходимо определить порядок наследования методов. Этот вопрос в Коммон Лиспе решается с помощью списка предшествований, который определяет общую иерархию классов. Список предшествований упорядочен в направлении от ближайших, по времени определения, классов к классам, определенным ранее. Кроме этого, если дочерний класс наследует свойства нескольких родительских классов, то они просматриваются в порядке их упоминания в вы-

зове DEFCLASS.

На рисунке 5.12 изображена иерархия классов для рассмотренного выше примера. Стрелками показан порядок просмотра суперклассов.

Рисунок 5.12 – Иерархия классов

Указанный порядок просмотра соответствует списку предшествований:

(ship, moving-object, constructed-object, standard-object ,t)

Объектно-ориентированная система языка Коммон Лисп (CLOS) весьма развита. Здесь были рассмотрены только ее основные возможности. За рамками рассмотрения оказались дополнительные методы, комбинации методов, мета-классы и др.

5.21. Макросы

Аналогично функциям макросы позволяют упростить написание программ, в которых встречаются повторяющиеся участки кода. Поэтому синтаксис определения макроса напоминает определение функции:

(defmacro имя лямбда-список {форма}*)

Соседние файлы в папке Не books