Главная страница
Навигация по странице:

  • 3.1. Краеугольные камни ООП 3.1.1. Формула объекта

  • 3.1.3. Объекты и компоненты

  • 3.3. Объекты Чтобы от описания класса перейти к объекту, следует выполнить соответствующее объявление в секции var

  • 3.4. Конструкторы и деструкторы

  • 3.6. Свойства 3.6.1. Понятие свойства

  • 3.6.2. Методы получения и установки значений свойств

  • 3.6.4. Свойство-массив как основное свойство объекта Свойство-массив можно сделать основным свойством объектов данного класса. Для этого в описание свойства добавляется слово default

  • 3.6.5. Методы, обслуживающие несколько свойств

  • 3.7. Наследование 3.7.1. Понятие наследования

  • 3.7.2. Прародитель всех классов

  • Учебное пособие для студентов Авторы А. Н. Вальвачев, К. А. Сурков, Д. А. Сурков, Ю. М. Четырько Содержание Содержание 1


    Скачать 2.61 Mb.
    НазваниеУчебное пособие для студентов Авторы А. Н. Вальвачев, К. А. Сурков, Д. А. Сурков, Ю. М. Четырько Содержание Содержание 1
    Дата15.01.2023
    Размер2.61 Mb.
    Формат файлаdoc
    Имя файлаlab_OOTP.doc
    ТипУчебное пособие
    #886736
    страница12 из 33
    1   ...   8   9   10   11   12   13   14   15   ...   33
    Глава 3. Объектно-ориентированное программирование (ООП)

    Объекты — это крупнейшее достижение в современной технологии программирования. Они позволили строить программу не из чудовищных по сложности процедур и функций, а из кирпичиков-объектов, заранее наделенных нужными свойствами. Самое приятное в объктах то, что их внутренняя сложность скрыта от программиста, который просто пользуется готовым строительным материалом.

    Сейчас преимущества использования объектов очевидны для всех. Однако так было не всегда. Сначала старая гвардия не поняла и не приняла объекты, поэтому они почти 20 лет потихоньку развивались в различных языках, первым из которых была Simula 67. Постепенно объектно-ориентированный подход нашел себе место и в более мощных языках, таких как C++, Delphi и множестве других языков. Блестящим примером реализации объектов была библиотека Turbo Vision, предназначенная для построения пользовательского интерфейса программ в операционной системе MS-DOS.

    Полную победу объекты одержали с приходом эпохи многофункциональных графических пользовательских интерфейсов. Теперь без объектов в программировании просто не обойтись. Чтобы вы не рылись в других книгах, собирая информацию по крохам, мы не поленились и объединили в этой главе все, что нужно знать об объектах. Для новичка важнейшее здесь: инкапсуляция, наследование, полиморфизм, остальное можно просто просмотреть и возвращаться к материалу по мере накопления опыта. Профессионалу полезно прочитать внимательно все от начала до конца. Поэтому давайте засучим рукава и приступим к делу.

    3.1. Краеугольные камни ООП

    3.1.1. Формула объекта

    Авторы надеются, что читатель помнит кое-что из главы 2 и такие понятия как тип данных, процедура, функция, запись для него не в новинку. Это прекрасно. Так вот, в конце 60-х годов кому-то пришло в голову объединить эти понятия, и то, что получилось, назвать объектом. Рассмотрение данных в неразрывной связи с методами их обработки позволило вывести формулу объекта:

    Объект = Данные + Операции

    На основании этой формулы была разработана методология объектно-ориентированного программирования (ООП).

    3.1.2. Природа объекта

    Об объектах можно думать как о полезных существах, которые "живут" в вашей программе и коллективно решают некоторую прикладную задачу. Вы, как Демиург, лепите этих существ, распределяете между ними обязанности и устанавливаете правила их взаимодействия.

    В общем случае каждый объект "помнит" необходимую информацию, "умеет" выполнять некоторый набор действий и характеризуется набором свойств. То, что объект "помнит", хранится в его полях. То, что объект "умеет делать", реализуется в виде его внутренних процедур и функций, называемых методами. Свойства объектов аналогичны свойствам, которые мы наблюдаем у обычных предметов. Значения свойств можно устанавливать и читать. Программно свойства реализуются через поля и методы.

    Например, объект "кнопка" имеет свойство "цвет". Значение цвета кнопка запоминает в одном из своих полей. При изменении значения свойства "цвет" вызывается метод, который перерисовывает кнопку.

    Кстати, этот пример позволяет сделать важный вывод: свойства имеют первостепенное значение для программиста, использующего объект. Чтобы понять суть и назначение объекта вы обязательно должны знать его свойства, иногда — методы, очень редко — поля (объект и сам знает, что с ними делать).

    3.1.3. Объекты и компоненты

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

    Компоненты в среде Delphi — это особые объекты, которые являются строительными кирпичиками визуальной среды разработки и приспособлены к визуальной установке свойств. Чтобы превратить объект в компонент, первый разрабатывается по определенным правилам, а затем помещается в палитру компонентов. Конструируя приложение, вы берете компоненты из Палитры Компонентов, располагаете на форме и устанавливаете их свойства в окне Инспектора Объектов. Внешне все выглядит просто, но чтобы достичь такой простоты, потребовалось создать механизмы, обеспечивающие функционирование объектов-компонентов уже на этапе проектирования приложения! Все это было придумано и блестяще реализовано в среде Delphi. Таким образом, компонентный подход значительно упростил создание приложений с графическим пользовательским интерфейсом и дал толчок развитию новой индустрии компонентов.

    В данной главе мы рассмотрим лишь вопросы создания и использования объектов. Чуть позже мы научим вас превращать объекты в компоненты (см. главу 13).

    3.1.4. Классы объектов

    Каждый объект всегда принадлежит некоторому классу объектов. Класс объектов — это обобщенное (абстрактное) описание множества однотипных объектов. Объекты являются конкретными представителями своего класса, их принято называть экземплярами класса. Например, класс СОБАКИ — понятие абстрактное, а экземпляр этого класса МОЙ ПЕС БОБИК — понятие конкретное.

    3.1.5. Три кита ООП

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

    Объединение данных и операций в одну сущность — объект — тесно связано с понятием инкапсуляции, которое означает сокрытие внутреннего устройства. Инкапсуляция делает объекты похожими на маленькие программные модули, в которых скрыты внутренние данные и у которых имеется интерфейс использования в виде подпрограмм. Переход от понятий «структура данных» и «алгоритм» к понятию «объект» значительно повысил ясность и надежность программ.

    Второй кит ООП — наследование. Этот простой принцип означает, что если вы хотите создать новый класс объектов, который расширяет возможности уже существующего класса, то нет необходимости в переписывании заново всех полей, методов и свойств. Вы объявляете, что новый класс является потомком (или дочерним классом) имеющегося класса объектов, называемого предком (или родительским классом), и добавляете к нему новые поля, методы и свойства. Процесс порождения новых классов на основе других классов называется наследованием. Новые классы объектов имеют как унаследованные признаки, так и, возможно, новые. Например, класс СОБАКИ унаследовал многие свойства своих предков — ВОЛКОВ.

    Третий кит — это полиморфизм. Он означает, что в производных классах вы можете изменять работу уже существующих в базовом классе методов. При этом весь программный код, управляющий объектами родительского класса, пригоден для управления объектами дочернего класса без всякой модификации. Например, вы можете породить новый класс кнопок с рельефной надписью, переопределив метод рисования кнопки. Новую кнопку можно "подсунуть" вместо стандартной в какую-нибудь подпрограмму, вызывающую рисование кнопки. При этом подпрограмма "думает", что работает со стандартной кнопкой, но на самом деле кнопка принадлежит производному классу кнопок и отображается в новом стиле. Пока достаточно самого поверхностного понимания всех приведенных выше понятий, ниже мы рассмотрим их подробнее и покажем, как они реализованы в среде Delphi.

    3.2. Классы

    Для поддержки ООП в язык Delphi введены объектные типы данных, с помощью которых одновременно описываются данные и операции над ними. Объектные типы данных называют классами, а их экземпляры — объектами.

    Классы объектов определяются в секции type глобального блока. Описание класса начинается с ключевого слова class и заканчивается ключевым словом end. По форме объявления классы похожи на обычные записи, но помимо полей данных могут содержать объявления пользовательских процедур и функций. Такие процедуры и функции обобщенно называют методами, они предназначены для выполнения над объектами различных операций. Приведем пример объявления класса, который предназначен для чтения текстового файла в формате "delimited text" (файл в таком формате представляет собой последовательность строк; каждая строка состоит из значений, которые отделены друг от друга символом-разделителем):

    type

    TDelimitedReader = class

    // Поля

    FileVar: TextFile;

    Items: array of string;

    Delimiter: Char;

    // Методы

    procedure PutItem(Index: Integer; const Item: string);

    procedure SetActive(const AActive: Boolean);

    function ParseLine(const Line: string): Integer;

    function NextLine: Boolean;

    function GetEndOfFile: Boolean;

    end;

    Класс содержит поля (FileVar, Items, Delimiter) и методы (PutItem, SetActive, ParseLine, NextLine, GetEndOfFile). Заголовки методов, (всегда) следующие за списком полей, играют роль упреждающих (forward) описаний. Программный код методов пишется отдельно от определения класса и будет приведен позже.

    Класс обычно описывает сущность, моделируемую в программе. Например, класс TDelimitedReader представляет собой "читатель" текстового файла с разбором считываемых строк на элементы (подстроки), которые отделены друг от друга некоторым символом, называемым разделителем.

    Класс содержит несколько полей:

    • FileVar — файловая переменная, необходимая для доступа к файлу;

    • Delimiter — символ, который служит разделителем элементов;

    • Items — массив элементов, полученных разбором последней считанной строки;

    Класс также содержит ряд методов (процедур и функций):

    • PutItem — помещает элемент в массив Items по индексу Index; если индекс превышает верхнюю границу массива, то размер массива автоматически увеличивается;

    • SetActive — открывает или закрывает файл, из которого производится чтение строк;

    • ParseLine — осуществляет разбор строки: выделяет элементы из строки и помещает их в массив Items; возвращает количество выделенных элементов;

    • NextLine — считывает очередную строку из файла и с помощью метода ParseLine осуществляет ее разбор; в случае успешного чтения очередной строки функция возвращает значение True, а иначе — значение False (достигнут конец файла);

    • GetEndOfFile — возвращает булевское значение, показывающее, достигнут ли конец файла.

    Обратите внимание, что приведенное выше описание является ничем иным, как декларацией интерфейса для работы с объектами класса TDelimitedReader. Реализация методов PutItem, SetActive, ParseLine, NextLine и GetEndOfFile на данный момент отсутствует, однако для создания и использования экземпляров класса она пока и не нужна.

    В некотором смысле объекты похожи на программные модули, для использования которых необходимо изучить лишь интерфейсную часть, раздел реализации для этого изучать не требуется. Поэтому дальше от описания класса мы перейдем не к реализации методов, а к созданию на их основе объектов.

    3.3. Объекты

    Чтобы от описания класса перейти к объекту, следует выполнить соответствующее объявление в секции var:

    var

    Reader: TDelimitedReader;

    При работе с обычными типами данных этого объявления было бы достаточно для получения экземпляра типа. Однако объекты в среде Delphi являются динамическими данными, т.е. распределяются в динамической памяти. Поэтому переменная Reader — это просто ссылка на экземпляр (объект в памяти), которого физически еще не существует. Чтобы сконструировать объект (выделить память для экземпляра) класса TDelimitedReader и связать с ним переменную Reader, нужно в тексте программы поместить следующий оператор:

    Reader := TDelimitedReader.Create;

    Create — это так называемый конструктор объекта; он всегда присутствует в классе и служит для создания и инициализации экземпляров. При создании объекта в памяти выделяется место только для его полей. Методы, как и обычные процедуры и функции, помещаются в область кода программы; они умеют работать с любыми экземплярами своего класса и не дублируются в памяти.

    После создания объект можно использовать в программе: получать и устанавливать значения его полей, вызывать его методы. Доступ к полям и методам объекта происходит с помощью уточненных имен, например:

    Reader.NextLine;

    Кроме того, как и при работе с записями, допустимо использование оператора with, например:

    with Reader do

    NextLine;

    Если объект становится ненужным, он должен быть удален вызовом специального метода Destroy, например:

    Reader.Destroy; // Освобождение памяти, занимаемой объектом

    Destroy — это так называемый деструктор объекта; он присутствует в классе наряду с конструктором и служит для удаления объекта из динамической памяти. После вызова деструктора переменная Reader становится несвязанной и не должна использоваться для доступа к полям и методам уже несуществующего объекта. Чтобы отличать в программе связанные объектные переменные от несвязанных, последние следует инициализировать значением nil. Например, в следующем фрагменте обращение к деструктору Destroy выполняется только в том случае, если объект реально существует:

    Reader := nil;

    ...

    if Reader <> nil then Reader.Destroy;

    Вызов деструктора для несуществующих объектов недопустим и при выполнении программы приведет к ошибке. Чтобы избавить программистов от лишних ошибок, в объекты ввели предопределенный метод Free, который следует вызывать вместо деструктора. Метод Free сам вызывает деструктор Destroy, но только в том случае, если значение объектной переменной не равно nil. Поэтому последнюю строчку в приведенном выше примере можно переписать следующим образом.

    Reader.Free;

    После уничтожения объекта переменная Reader сохраняет свое значение, продолжая ссылаться на место в памяти, где объекта уже нет. Если эту переменную предполагается еще использовать, то желательно присвоить ей значение nil, чтобы программа могла проверить, существует объект или нет. Таким образом, наиболее правильная последовательность действий при уничтожении объекта должна быть следующая:

    Reader.Free;

    Reader := nil;

    С помощью стандартной процедуры FreeAndNil это можно сделать проще и элегантнее:

    FreeAndNil(Reader); // Эквивалентно: Reader.Free; Reader := nil;

    Значение одной объектной переменной можно присвоить другой. При этом объект не копируется в памяти, а вторая переменная просто связывается с тем же объектом, что и первая:

    var

    R1, R2: TDelimitedReader; // Переменные R1 и R2 не связаны с объектом

    begin

    R1 := TDelimitedReader.Create; // Связывание переменной R1 с новым объектом

    // Переменная R2 пока еще не связана ни с каким объектом

    R2 := R1; // Связывание переменной R2 с тем же объектом, что и R1

    // Теперь обе переменные связаны с одним объектом

    R2.Free; // Уничтожение объекта

    // Теперь R1 и R2 не связаны ни с каким объектом

    end;

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

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

    type

    TReadersList = class; // упреждающее объявление класса TReadersList
    TDelimitedReader = class

    Owner: TReadersList;

    ...

    end;
    TReadersList = class

    Readers: array of TDelimitedReader;

    ...

    end;

    Первое объявление класса TReadersList называется упреждающим (от англ. forward). Оно необходимо для того, чтобы компилятор нормально воспринял объявление поля Owner в классе TDelimitedReader.

    Итак, вы уже имеете некоторое представление об объектах, перейдем теперь к вопросу реализации их методов.

    3.4. Конструкторы и деструкторы

    Особой разновидностью методов являются конструкторы и деструкторы. Напомним, что конструкторы создают, а деструкторы разрушают объекты. Создание объекта включает выделение памяти под экземпляр и инициализацию его полей, а разрушение — очистку полей и освобождение памяти. Действия по инициализации и очистке полей специфичны для каждого конкретного класса объектов. По этой причине язык Delphi позволяет переопределить стандартный конструктор Create и стандартный деструктор Destroy для выполнения любых полезных действий. Можно даже определить несколько конструкторов и деструкторов (имена им назначает сам программист), чтобы обеспечить различные процедуры создания и разрушения объектов.

    Объявление конструкторов и деструкторов похоже на объявление обычных методов с той лишь разницей, что вместо зарезервированных слов function и procedure используются слова constructor и destructor. Для нашего класса TDelimitedReader потребуется конструктор, которому в качестве параметра будет передаваться имя обрабатываемого файла и разделитель элементов:

    type

    TDelimitedReader = class

    ...

    // Конструкторы и деструкторы

    constructor Create(const FileName: string; const ADelimiter: Char = ';');

    destructor Destroy; override;

    ...

    end;

    Приведем их возможную реализацию:

    constructor TDelimitedReader.Create(const FileName: string;

    const ADelimiter: Char = ';');

    begin

    AssignFile(FileVar, FileName);

    Delimiter := ADelimiter;

    end;
    destructor TDelimitedReader.Destroy;

    begin

    // Пока ничего не делаем

    end;

    Если объект содержит встроенные объекты или другие динамические данные, то конструктор — это как раз то место, где их нужно создавать.

    Конструктор применяется к классу или к объекту. Если он применяется к классу,

    Reader := TDelimitedReader.Create('MyData.del', ';');

    то выполняется следующая последовательность действий:

    • в динамической памяти выделяется место для нового объекта;

    • выделенная память заполняется нулями. В результате все числовые поля и поля порядкового типа приобретают нулевые значения, строковые поля становятся пустыми, а поля, содержащие указатели и объекты получают значение nil;

    • затем выполняются заданные программистом действия конструктора;

    • ссылка на созданный объект возвращается в качестве значения конструктора. Тип возвращаемого значения совпадает с типом класса, использованного при вызове (в нашем примере это тип TDelimitedReader).

    Если конструктор применяется к объекту,

    Reader.Create('MyData.del', ';');

    то конструктор выполняется как обычный метод. Другими словами, новый объект не создается, а происходит повторная инициализация полей существующего объекта. В этом случае конструктор не возвращает никакого значения. Далеко не все объекты корректно себя ведут при повторной инициализации, поскольку программисты редко закладывают такую возможность в свои классы. Поэтому на практике повторная инициализация применяется крайне редко.

    Деструктор уничтожает объект, к которому применяется:

    Reader.Destroy;

    В результате:

    • выполняется заданный программистом код завершения;

    • освобождается занимаемая объектом динамическая память.

    В теле деструктора обычно должны уничтожаться встроенные объекты и динамические данные, как правило, созданные конструктором.

    Как и обычные методы, деструктор может иметь параметры, но эта возможность используется редко.

    3.5. Методы

    Процедуры и функции, предназначенные для выполнения над объектами действий, называются методами. Предварительное объявление методов выполняется при описании класса в секции interface модуля, а их программный код записывается в секции implementation. Однако в отличие от обычных процедур и функций заголовки методов должны иметь уточненные имена, т.е. содержать наименование класса. Приведем возможную реализацию одного из методов в классе TDelimitedReader:

    procedure TDelimitedReader.SetActive(const AActive: Boolean);

    begin

    if AActive then

    Reset(FileVar) // Открытие файла

    else

    CloseFile(FileVar); // Закрытие файла

    end;

    Обратите внимание, что внутри методов обращения к полям и другим методам выполняются как к обычным переменным и подпрограммам без уточнения экземпляра объекта. Такое упрощение достигается путем использования в пределах метода псевдопеременной Self (стандартный идентификатор). Физически Self представляет собой дополнительный неявный параметр, передаваемый в метод при вызове. Этот параметр и указывает экземпляр объекта, к которому данный метод применяется. Чтобы пояснить сказанное, перепишем метод SetActive, представив его в виде обычной процедуры:

    procedure TDelimitedReader_SetActive(Self: TDelimitedReader;

    const AActive: Boolean);

    begin

    if AActive then

    Reset(Self.FileVar) // Открытие файла

    else

    CloseFile(Self.FileVar); // Закрытие файла

    end;

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

    Практика показывает, что псевдопеременная Self редко используется в явном виде. Ее необходимо применять только тогда, когда при написании метода может возникнуть какая-либо двусмысленность для компилятора, например при использовании одинаковых имен и для локальных переменных, и для полей объекта.

    Если выполнить метод SetActive,

    Reader.SetActive(True);

    то обрабатываемый файл будет открыт. При этом неявный параметр Self будет содержать значение переменной Reader. Такой вызов реализуется обычными средствами процедурного программирования приблизительно так:

    TDelimitedReader_SetActive(Reader, True);

    3.6. Свойства

    3.6.1. Понятие свойства

    Помимо полей и методов в объектах существуют свойства. При работе с объектом свойства выглядят как поля: они принимают значения и участвуют в выражениях. Но в отличие от полей свойства не занимают места в памяти, а операции их чтения и записи ассоциируются с обычными полями или методами. Это позволяет создавать необходимые сопутствующие эффекты при обращении к свойствам. Например, в объекте Reader присваивание свойству Active значения True вызовет открытие файла, а присваивание значения False — закрытие файла. Создание сопутствующего эффекта (открытие или закрытие файла) достигается тем, что за присваиванием свойству значения стоит вызов метода.

    Объявление свойства выполняется с помощью зарезервированного слова property, например:

    type

    TDelimitedReader = class

    ...

    FActive: Boolean;

    ...

    // Метод записи (установки значения) свойства

    procedure SetActive(const AActive: Boolean);

    property Active: Boolean read FActive write SetActive; // Свойство

    end;

    Ключевые слова read и write называются спецификаторами доступа. После слова read указывается поле или метод, к которому происходит обращение при чтении (получении) значения свойства, а после слова write — поле или метод, к которому происходит обращение при записи (установке) значения свойства. Например, чтение свойства Active означает чтение поля FActive, а установка свойства — вызов метода SetActive. Чтобы имена свойств не совпадали с именами полей, последние принято писать с буквы F (от англ. field). Мы в дальнейшем также будем пользоваться этим соглашением. Начнем с того, что переименуем поля класса TDelimitedReader: поле FileVar переименуем в FFile, Items — в FItems, а поле Delimiter — в FDelimiter.

    type

    TDelimitedReader = class

    // Поля

    FFile: TextFile; // FileVar -> FFile

    FItems: array of string; // Items -> FItems

    FActive: Boolean;

    FDelimiter: Char; // Delimiter -> FDelimiter

    ...

    end;

    Обращение к свойствам выглядит в программе как обращение к полям:

    var

    Reader: TDelimitedReader;

    IsOpen: Boolean;

    ...

    Reader.Active := True; // Эквивалентно Reader.SetActive(True);

    IsOpen := Reader.Active; // Эквивалентно IsOpen := Reader.FActive

    Если один из спецификаторов доступа опущен, то значение свойства можно либо только читать (задан спецификатор read), либо только записывать (задан спецификатор write). В следующем примере объявлено свойство, значение которого можно только читать.

    type

    TDelimitedReader = class

    ...

    FItems: array of string;

    ...

    function GetItemCount: Integer;

    ...

    property ItemCount: Integer read GetItemCount; // Только для чтения!

    end;
    function TDelimitedReader.GetItemCount: Integer;

    begin

    Result := Length(FItems);

    end;

    Здесь свойство ItemCount показывает количество элементов в массиве FItems. Поскольку оно определяется в результате чтения и разбора очередной строки файла, пользователю объекта разрешено лишь узнавать количество элементов.

    В отличие от полей свойства не имеют адреса в памяти, поэтому к ним запрещено применять операцию @. Как следствие, их нельзя передавать в var- и out-параметрах процедур и функций.

    Технология объектно-ориентированного программирования в среде Delphi предписывает избегать прямого обращения к полям, создавая вместо этого соответствующие свойства. Это упорядочивает работу с объектами, изолируя их данные от непосредственной модификации. В будущем внутренняя структура класса, которая иногда является достаточно сложной, может быть изменена с целью повышения эффективности работы программы. При этом потребуется переработать только методы чтения и записи значений свойств; внешний интерфейс класса не изменится.

    3.6.2. Методы получения и установки значений свойств

    Методы получения (чтения) и установки (записи) значений свойств подчиняются определенным правилам. Метод чтения свойства — это всегда функция, возвращающая значение того же типа, что и тип свойства. Метод записи свойства — это обязательно процедура, принимающая параметр того же типа, что и тип свойства. В остальных отношениях это обычные методы объекта. Примерами методов чтения и записи свойств являются методы GetItemCount и SetActive в классе TDelimitedReader:

    type

    TDelimitedReader = class

    FActive: Boolean;

    ...

    procedure SetActive(const AActive: Boolean);

    function GetItemCount: Integer;

    ...

    property Active: Boolean read FActive write SetActive;

    property ItemCount: Integer read GetItemCount;

    end;

    Использование методов для получения и установки свойств позволяет проверить корректность значения свойства, сделать дополнительные вычисления, установить значения зависимых полей и т.д. Например, в методе SetActive вполне целесообразно осуществить проверку состояния файла (открыт или закрыт), чтобы избежать его повторного открытия или закрытия:

    procedure TDelimitedReader.SetActive(const AActive: Boolean);

    begin

    if Active <> AActive then // Если состояние изменяется

    begin

    if AActive then

    Reset(FFile) // Открытие файла

    else

    CloseFile(FFile); // Закрытие файла

    FActive := AActive; // Сохранение состояния в поле

    end;

    end;

    Наличие свойства Active позволяет нам отказаться от использования методов Open и Close, традиционных при работе с файлами. Согласитесь, что открывать и закрывать файл с помощью свойства Active гораздо удобнее и естественнее. Одновременно с этим свойство Active можно использовать и для проверки состояния файла (открыт или нет). Таким образом, для осуществления трех действий требуется всего лишь одно свойство! Это делает использование Ваших классов другими программистами более простым, поскольку им легче запомнить одно понятие Active, чем, например, три метода: Open, Close и IsOpen.

    Значение свойства может не храниться, а вычисляться при каждом обращении к свойству. Примером является свойство ItemCount, значение которого вычисляется как Length(FItems).

    3.6.3. Свойства-массивы

    Кроме обычных свойств в объектах существуют свойства-массивы (array properties). Свойство-массив — это индексированное множество значений. Например, в классе TDelimitedReader множество элементов, выделенных из считанной строки, удобно представить в виде свойства-массива:

    type

    TDelimitedReader = class

    ...

    FItems: array of string;

    ...

    function GetItem(Index: Integer): string;

    ...

    property Items[Index: Integer]: string read GetItem;

    end;
    function TDelimitedReader.GetItem(Index: Integer): string;

    begin

    Result := FItems[Index];

    end;

    Элементы массива Items можно только читать, поскольку класс TDelimitedReader предназначен только для чтения данных из файла.

    В описании свойства-массива разрешено использовать только методы, но не поля. В этом состоит отличие свойства-массива от обычного свойства.

    Основная выгода от применения свойства-массива — возможность выполнения итераций с помощью цикла for, например:

    var

    Reader: TDelimitedReader;

    I: Integer;

    ...

    for I := 0 to Reader.ItemCount - 1 do

    Writeln(Reader.Items[I]);

    ...

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

    Свойства-массивы имеют два важных отличия от обычных массивов:

    • их индексы не ограничиваются диапазоном и могут иметь любой тип данных, а не только Integer. Например, можно создать свойство-массив, в котором индексами будут строки. Обращение к такому свойству могло бы выглядеть примерно так:

    Reader.Items['FirstName'] := 'Alexander';

    • операции целиком со всем свойством-массивом запрещены; разрешены операции только с его элементами.

    3.6.4. Свойство-массив как основное свойство объекта

    Свойство-массив можно сделать основным свойством объектов данного класса. Для этого в описание свойства добавляется слово default:

    type

    TDelimitedReader = class

    ...

    property Items[Index: Integer]: string read GetItem; default;

    ...

    end;

    Такое объявление свойства Items позволяет рассматривать сам объект класса TDelimitedReader как массив и опускать имя свойства-массива при обращении к нему из программы, например:

    var

    R: TDelimitedReader;

    I: Integer;

    ...

    for I := 0 to R.ItemCount - 1 do

    Writeln(R[I]);

    ...

    Следует помнить, что только свойства-массивы могут быть основными свойствами объектов; для обычных свойств это недопустимо.

    3.6.5. Методы, обслуживающие несколько свойств

    Один и тот же метод может использоваться для получения (установки) значений нескольких свойств одного типа. В этом случае каждому свойству назначается целочисленный индекс, который передается в метод чтения (записи) первым параметром.

    В следующем примере уже известный Вам метод GetItem обслуживает три свойства: FirstName, LastName и Phone:

    type

    TDelimitedReader = class

    ...

    property FirstName: string index 0 read GetItem;

    property LastName: string index 1 read GetItem;

    property Phone: string index 2 read GetItem;

    end;

    Обращения к свойствам FirstName, LastName и Phone заменяются компилятором на вызовы одного и того же метода GetItem, но с разными значениями параметра Index:

    var

    Reader: TDelimitedReader;

    ...

    Writeln(Reader.FirstName); // Эквивалентно: Writeln(Reader.GetItem(0));

    Writeln(Reader.LastName); // Эквивалентно: Writeln(Reader.GetItem(1));

    Writeln(Reader.Phone); // Эквивалентно: Writeln(Reader.GetItem(2));

    ...

    Обратите внимание, что метод GetItem обслуживает как свойство-массив Items, так и свойства FirstName, LastName и Phone. Удобно, не правда ли!

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

    type

    TDelimitedReader = class

    // Поля

    FFile: TextFile;

    FItems: array of string;

    FActive: Boolean;

    FDelimiter: Char;

    // Методы чтения и записи свойств

    procedure SetActive(const AActive: Boolean);

    function GetItemCount: Integer;

    function GetEndOfFile: Boolean;

    function GetItem(Index: Integer): string;

    // Методы

    procedure PutItem(Index: Integer; const Item: string);

    function ParseLine(const Line: string): Integer;

    function NextLine: Boolean;

    // Конструкторы и деструкторы

    constructor Create(const FileName: string; const ADelimiter: Char = ';');

    destructor Destroy; override;

    // Свойства

    property Active: Boolean read FActive write SetActive;

    property Items[Index: Integer]: string read GetItem; default;

    property ItemCount: Integer read GetItemCount;

    property EndOfFile: Boolean read GetEndOfFile;

    property Delimiter: Char read FDelimiter;

    end;
    { TDelimitedReader }
    constructor TDelimitedReader.Create(const FileName: string;

    const ADelimiter: Char = ';');

    begin

    AssignFile(FFile, FileName);

    FActive := False;

    FDelimiter := ADelimiter;

    end;
    destructor TDelimitedReader.Destroy;

    begin

    Active := False;

    end;
    function TDelimitedReader.GetEndOfFile: Boolean;

    begin

    Result := Eof(FFile);

    end;
    function TDelimitedReader.GetItem(Index: Integer): string;

    begin

    Result := FItems[Index];

    end;
    function TDelimitedReader.GetItemCount: Integer;

    begin

    Result := Length(FItems);

    end;
    function TDelimitedReader.NextLine: Boolean;

    var

    S: string;

    N: Integer;

    begin

    Result := not EndOfFile;

    if Result then // Если не достигнут конец файла

    begin

    Readln(FFile, S); // Чтение очередной строки из файла

    N := ParseLine(S); // Разбор считанной строки

    if N <> ItemCount then

    SetLength(FItems, N); // Отсечение массива (если необходимо)

    end;

    end;
    function TDelimitedReader.ParseLine(const Line: string): Integer;

    var

    S: string;

    P: Integer;

    begin

    S := Line;

    Result := 0;

    repeat

    P := Pos(Delimiter, S); // Поиск разделителя

    if P = 0 then // Если разделитель не найден, то считается, что

    P := Length(S) + 1; // разделитель находится за последним символом

    PutItem(Result, Copy(S, 1, P - 1)); // Установка элемента

    Delete(S, 1, P); // Удаление элемента из строки

    Result := Result + 1; // Переход к следующему элементу

    until S = ''; // Пока в строке есть символы

    end;
    procedure TDelimitedReader.PutItem(Index: Integer; const Item: string);

    begin

    if Index > High(FItems) then // Если индекс выходит за границы массива,

    SetLength(FItems, Index + 1); // то увеличение размера массива

    FItems[Index] := Item; // Установка соответствующего элемента

    end;
    procedure TDelimitedReader.SetActive(const AActive: Boolean);

    begin

    if Active <> AActive then // Если состояние изменяется

    begin

    if AActive then

    Reset(FFile) // Открытие файла

    else

    CloseFile(FFile); // Закрытие файла

    FActive := AActive; // Сохранение состояния в поле

    end;

    end;

    3.7. Наследование

    3.7.1. Понятие наследования

    Классы инкапсулируют (т.е. включают в себя) поля, методы и свойства; это их первая черта. Следующая не менее важная черта классов — способность наследовать поля, методы и свойства других классов. Чтобы пояснить сущность наследования обратимся к примеру с читателем текстовых файлов в формате "delimited text".

    Класс TDelimitedReader описывает объекты для чтения из текстового файла элементов, разделенных некоторым символом. Он не пригоден для чтения элементов, хранящихся в другом формате, например в формате с фиксированным количеством символов для каждого элемента. Для этого необходим другой класс:

    type

    TFixedReader = class

    private

    // Поля

    FFile: TextFile;

    FItems: array of string;

    FActive: Boolean;

    FItemWidths: array of Integer;

    // Методы чтения и записи свойств

    procedure SetActive(const AActive: Boolean);

    function GetItemCount: Integer;

    function GetEndOfFile: Boolean;

    function GetItem(Index: Integer): string;

    // Методы

    procedure PutItem(Index: Integer; const Item: string);

    function ParseLine(const Line: string): Integer;

    function NextLine: Boolean;

    // Конструкторы и деструкторы

    constructor Create(const FileName: string;

    const AItemWidths: array of Integer);

    destructor Destroy; override;

    // Свойства

    property Active: Boolean read FActive write SetActive;

    property Items[Index: Integer]: string read GetItem; default;

    property ItemCount: Integer read GetItemCount;

    property EndOfFile: Boolean read GetEndOfFile;

    end;
    { TFixedReader }
    constructor TFixedReader.Create(const FileName: string;

    const AItemWidths: array of Integer);

    var

    I: Integer;

    begin

    AssignFile(FFile, FileName);

    FActive := False;

    // Копирование AItemWidths в FItemWidths

    SetLength(FItemWidths, Length(AItemWidths));

    for I := 0 to High(AItemWidths) do

    FItemWidths[I] := AItemWidths[I];

    end;
    destructor TFixedReader.Destroy;

    begin

    Active := False;

    end;
    function TFixedReader.GetEndOfFile: Boolean;

    begin

    Result := Eof(FFile);

    end;
    function TFixedReader.GetItem(Index: Integer): string;

    begin

    Result := FItems[Index];

    end;
    function TFixedReader.GetItemCount: Integer;

    begin

    Result := Length(FItems);

    end;
    function TFixedReader.NextLine: Boolean;

    var

    S: string;

    N: Integer;

    begin

    Result := not EndOfFile;

    if Result then // Если не достигнут конец файла

    begin

    Readln(FFile, S); // Чтение очередной строки из файла

    N := ParseLine(S); // Разбор считанной строки

    if N <> ItemCount then

    SetLength(FItems, N); // Отсечение массива (если необходимо)

    end;

    end;
    function TFixedReader.ParseLine(const Line: string): Integer;

    var

    I, P: Integer;

    begin

    P := 1;

    for I := 0 to High(FItemWidths) do

    begin

    PutItem(I, Copy(Line, P, FItemWidths[I])); // Установка элемента

    P := P + FItemWidths[I]; // Переход к следующему элементу

    end;

    Result := Length(FItemWidths); // Количество элементов постоянно

    end;
    procedure TFixedReader.PutItem(Index: Integer; const Item: string);

    begin

    if Index > High(FItems) then // Если индекс выходит за границы массива,

    SetLength(FItems, Index + 1); // то увеличение размера массива

    FItems[Index] := Item; // Установка соответствующего элемента

    end;
    procedure TFixedReader.SetActive(const AActive: Boolean);

    begin

    if Active <> AActive then // Если состояние изменяется

    begin

    if AActive then

    Reset(FFile) // Открытие файла

    else

    CloseFile(FFile); // Закрытие файла

    FActive := AActive; // Сохранение состояния в поле

    end;

    end;

    Поля, свойства и методы класса TFixedReader практически полностью аналогичны тем, что определены в классе TDelimitedReader. Отличие состоит в отсутствии свойства Delimiter, наличии поля FItemWidths (для хранения размеров элементов), другой реализации метода ParseLine и немного отличающемся конструкторе. Если в будущем появится класс для чтения элементов из файла еще одного формата (например, зашифрованного текста), то придется снова определять общие для всех классов поля, методы и свойства. Чтобы избавиться от дублирования общих атрибутов (полей, свойств и методов) при определении новых классов, воспользуемся механизмом наследования. Прежде всего, выделим в отдельный класс TTextReader общие атрибуты всех классов, предназначенных для чтения элементов из текстовых файлов. Реализация методов TTextReader, кроме метода ParseLine, полностью идентична реализации TDelimitedReader, приведенной в предыдущем разделе.

    type

    TTextReader = class

    private

    // Поля

    FFile: TextFile;

    FItems: array of string;

    FActive: Boolean;

    // Методы получения и установки значений свойств

    procedure SetActive(const AActive: Boolean);

    function GetItemCount: Integer;

    function GetItem(Index: Integer): string;

    function GetEndOfFile: Boolean;

    // Методы

    procedure PutItem(Index: Integer; const Item: string);

    function ParseLine(const Line: string): Integer;

    function NextLine: Boolean;

    // Конструкторы и деструкторы

    constructor Create(const FileName: string);

    destructor Destroy; override;

    // Свойства

    property Active: Boolean read FActive write SetActive;

    property Items[Index: Integer]: string read GetItem; default;

    property ItemCount: Integer read GetItemCount;

    property EndOfFile: Boolean read GetEndOfFile;

    end;

    ...

    constructor TTextReader.Create(const FileName: string);

    begin

    AssignFile(FFile, FileName);

    FActive := False;

    end;

    function TTextReader.ParseLine(const Line: string): Integer;

    begin

    // Функция просто возвращает 0, поскольку не известно,

    // в каком именно формате хранятся элементы

    Result := 0;

    end;

    ...

    При реализации класса TTextReader ничего не известно о том, как хранятся элементы в считываемых строках, поэтому метод ParseLine ничего не делает. Очевидно, что создавать объекты класса TTextReader не имеет смысла. Для чего тогда нужен класс TTextReader? Ответ: чтобы на его основе определить (породить) два других класса — TDelimitedReader и TFixedReader, предназначенных для чтения данных в конкретных форматах:

    type

    TDelimitedReader = class(TTextReader)

    FDelimiter: Char;

    function ParseLine(const Line: string): Integer; override;

    constructor Create(const FileName: string; const ADelimiter: Char = ';');

    property Delimiter: Char read FDelimiter;

    end;
    TFixedReader = class(TTextReader)

    FItemWidths: array of Integer;

    function ParseLine(const Line: string): Integer; override;

    constructor Create(const FileName: string;

    const AItemWidths: array of Integer);

    end;

    ...

    Классы TDelimitedReader и TFixedReader определены как наследники TTextReader (об этом говорит имя в скобках после слова class). Они автоматически включают в себя все описания, сделанные в классе TTextReader и добавляют к ним некоторые новые. В результате формируется дерево классов, показанное на рисунке 3.1 (оно всегда рисуется перевернутым).



    Рисунок 3.1. Дерево классов

    Класс, который наследует атрибуты другого класса, называется порожденным классом или потомком. Соответственно класс, от которого происходит наследование, выступает в роли базового, или предка. В нашем примере класс TDelimitedReader является прямым потомком класса TTextReader. Если от TDelimitedReader породить новый класс, то он тоже будет потомком класса TTextReader, но уже не прямым.

    Очень важно, что в отношениях наследования любой класс может иметь только одного непосредственного предка и сколь угодно много потомков. Поэтому все связанные отношением наследования классы образуют иерархию. Примером иерархии классов является библиотека VCL; с ее помощью в среде Delphi обеспечивается разработка GUI-приложений.

    3.7.2. Прародитель всех классов

    В языке Delphi существует предопределенный класс TObject, который служит неявным предком тех классов, для которых предок не указан. Это означает, что объявление

    type

    TTextReader = class

    ...

    end;

    эквивалентно следующему:

    type

    TTextReader = class(TObject)

    ...

    end;

    Класс TObject выступает корнем любой иерархии классов. Он содержит ряд методов, которые по наследству передаются всем остальным классам. Среди них конструктор Create, деструктор Destroy, метод Free и некоторые другие методы.

    Таким образом, полное дерево классов для чтения элементов из текстового файла в различных форматах выглядит так, как показано на рисунке 3.2.



    Рисунок 3.2. Полное дерево классов

    Поскольку класс TObject является предком для всех других классов (в том числе и для ваших собственных), то не лишним будет кратко ознакомиться с его методами:

    type

    TObject = class

    constructor Create;

    procedure Free;

    class function InitInstance(Instance: Pointer): TObject;

    procedure CleanupInstance;

    function ClassType: TClass;

    class function ClassName: ShortString;

    class function ClassNameIs(const Name: string): Boolean;

    class function ClassParent: TClass;

    class function ClassInfo: Pointer;

    class function InstanceSize: Longint;

    class function InheritsFrom(AClass: TClass): Boolean;

    class function MethodAddress(const Name: ShortString): Pointer;

    class function MethodName(Address: Pointer): ShortString;

    function FieldAddress(const Name: ShortString): Pointer;

    function GetInterface(const IID: TGUID; out Obj): Boolean;

    class function GetInterfaceEntry(const IID: TGUID): PInterfaceEntry;

    class function GetInterfaceTable: PInterfaceTable;

    function SafeCallException(ExceptObject: TObject;

    ExceptAddr: Pointer): HResult; virtual;

    procedure AfterConstruction; virtual;

    procedure BeforeDestruction; virtual;

    procedure Dispatch(var Message); virtual;

    procedure DefaultHandler(var Message); virtual;

    class function NewInstance: TObject; virtual;

    procedure FreeInstance; virtual;

    destructor Destroy; virtual;

    end;

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

    Краткое описание методов в классе TObject:
    1   ...   8   9   10   11   12   13   14   15   ...   33


    написать администратору сайта