Property — Ключевое слово Delphi


Содержание

Сохраненное ключевое слово в Delphi

Delphi позволяет сохранять ключевое слово при определении свойств следующим образом:

Какова цель ключевого слова и что он делает?

Из моего файла справки Delphi 7:

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

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

Если свойство не имеет хранимой директивы, он обрабатывается так, как будто сохраненные True были указано.

Похоже, что он контролирует, сохранять или не сохранять свойство, относящееся к компоненту в файле .DFM для формы. (Только предположение, хотя)

Это ключевое слово определяет, следует ли сохранить значение свойства в файле формы; по умолчанию это true . Может быть полезно избежать, например, сохранения больших фрагментов двоичной информации в вашем файле .dfm (например, компонент изображения, который должен читать его содержимое только во время выполнения).

Хранимая директива принимает логическое значение: метод, который возвращает логический результат, ссылку на поле логического типа или константное выражение логического типа. RTT надлежащего записывает смещение поля, ссылку на метод или постоянное значение, и Delphis IDE использует эту информацию, чтобы решить, следует ли пропустить свойство из файла.dfm.

Среда IDE вызывает метод, проверяет значение полей или использует постоянное логическое значение, и если значение равно False, свойство не сохраняется в файл.dfm. Если сохраненное значение равно True, происходит поведение по умолчанию, а именно, что свойство сохраняется, если его значение отличается от значения по умолчанию.
Секреты и уловки

Посмотрите другие вопросы по метке delphi или Задайте вопрос

Property — Ключевое слово Delphi

It is an Object Oriented concept that internals of an object should be h >Property can be used to define how the data is read and written.

Versions 1, 2 and 3 These basic forms define read, write or read and write access to class fields. The data Type is returned from the field or method called Getter. The data is updated via the Setter field or method.

Note that you must use a different name for the Name and for Getter and Setter. For example: Property Age read fAge write fAge; You would use field names when there is no vetting or retrieval processing required. When using a method to read or write, the read or written value can be a lot simpler than the stored value. The stored value can even be entirely different.

Versions 4, 5 and 6 Using the Index keyword tells Delphi to pass the Constant value as the argument to the Getter and Setter methods. These must be functions that take this constant index value as an argument.

For example: Property Item2 : string Index 2 read ItemGetter; where ItemGetter is defined as : Function ItemGetter(Index : Integer) : string; Default provides run time information for the property. NoDefault does not. Stored is beyond the scope of Delphi Basics.

Versions 7, 8 and 9 This is a generalised version of versions 4,5 and 6. It requests the user to provide the index value for the Getter and Setter methods.

Default allows the Getter and Setter method calls to be replaced as in the following example : myValue := MyClass.Getter(23); can be replaced by : myValue := MyCLass[23];

Version 10 Allows the implementation of an Interface method to be delegated to a property. Access to the property invokes the interface implementation.

Version 11 By redclaring a parent class property, you can do so in a public or published clause, thereby raising the access rights of the property.

Property — Ключевое слово Delphi

обьясните пожалуйсто что значит ключевое слово «Stored»

ну если это касается к базам данных, то это скорее всего касается хранимых процедур:)

В этом случае Delphi будет понимать, что значение 15 является значением по умолчанию, т.е. если присвоить свойству значение 15, то оно не будет сохраняться в dfm.
Судя по всему это сделано для экономии места.

А сохранять значения самих свойств. Если хочешь узнать какие именно сохранены, то попробуй открыть файл dfm в обычном блокноте.

Если нужны более подробные сведения, то как обычно:
default -> F1
nodefault -> F1

Проще всего объяснить на примерах

property abc:integer read fabc write fabc STORED TRUE;

Означает, что published abc будет сохраняться в dfm. STORED TRUE — излишне.
Без этой директивы работает так же, как и с ней.
STORED FALSE — выключает сохранение в dfm. Что бы вы в инспекторе объектов не выставили, при следующей загрузке проекта значение сбросится.

property abc:integer read fabc write fabc DEFAULT 123;

тоже самое, что STORED FALSE, только инициализация не нулем, а любой константой.

property abc:integer read fabc write fabc NODEFAULT;

Может понадобиться только при наследовании, если в потомке хочется разрешить сохранять то,
что было запрещено в предке. Иными словами NODEFAULT отменяет DEFAULT.

И все вышесказанное имеет смысл в design time. Если вы инициализируете поля явно и в рантайм, то все эти директивы безразличны.

Блог GunSmoker-а

. when altering one’s mind becomes as easy as programming a computer, what does it mean to be human.

22 декабря 2008 г.

Новое ключевое слово static в Delphi

Недавно я переводил пост Почему методы класса должны быть помечены словом «static», чтобы их можно было использовать в качестве функции обратного вызова? Реймонда Чена. Там я оставил весь код «как есть» — на C++. Здесь я рассмотрю этот вопрос с точки зрения Delphi.

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

Функция (в дальнейшем здесь будет также подразумеваться и процедура) — это код. Процедурная переменная — это указатель на код. Например: Метод — это тоже код, но код, связанный с классом. Указатель на метод — это ссылка на код + ссылка на конкретный объект. Например: Когда путают одно с другим компилятор чаще всего показывает такое сообщение: «Incompatible types: regular procedure and method pointer». Чаще всего или забывают писать «of object» в объявлении своих процедурных типов или пытаются передать в функцию (чаще всего как callback — т.е. функцию обратного вызова) метод класса вместо обычной функции (а самым упорным это иногда удаётся).

Что делает эти две сущности такими принципиально несовместимыми? Функция — это просто код. Она не имеет связи с данными, отличными от тех, что передаются в её параметры. Методы класса помимо работы с параметрами (как и обычная функция) ещё могут оперировать с данными объекта (вот оно: «код» vs «код + данные»), например: С функциями такое невозможно — обратите внимание, как вы манипулируете с P3 (он же: Self.P3) в методе. Собственно сам объект (это встроенная переменная Self) неявно передаётся в метод первым параметром. Поэтому, если метод объявлен как function(const P1, P2: Integer): Integer of object — с двумя параметрами, то, на самом деле, он трактуется как функция с тремя параметрами: function(Self: TSomeObj; const P1, P2: Integer): Integer . Именно это различие (на бинарном уровне) делает несовместимыми обычные функции и методы.

Соответственно, указатель на обычную функцию — это просто указатель (pointer), только что типизированный (это я про TDoSomethingFunc) — т.е. 4 байта. А вот указатель на метод — это уже запись или, если будет угодно, два указателя — один на код, второй — на данные, т.е. всего 8 байт.

Понятно, что если у вас что-то предназначено для передачи указателя на функцию, то никакими силами указатель на метод вы туда не пропихнёте. Хотя бы потому, что размер у них разный.

Ещё в Delphi есть классовые методы. Это такие методы, которые можно вызывать не имея на руках объект. В этом случае вместо объекта в неявный параметр Self передаётся информация о классе. Т.е. в классовых методах вы не можете использовать информацию о конкретном объекте (например, читать/писать его поля), но можете использовать информацию о классе — например, вызывать конструктор класса. Также методы класса могут быть виртуальными. Заметим, что сигнатура функции, реализующей метод, всё ещё совпадает с сигнатурой обычного метода: неявный параметр (данные класса вместо Self) + все явные параметры метода.

Например: Теперь ещё один шажок и мы переходим к тому, о чём говорил Реймонд Чен. Классовый метод можно объявить статическим (только в новых версиях Delphi). В этом случае у него не будет неявного параметра. Разумеется, при этом он не может использовать информацию экземпляра и класса. Зато он и не отличается от обычной функции.

Рассматривая пример с потоком, вот что мы могли бы написать в старых Delphi без поддержки статических классовых методов: Теперь, с введением нового ключевого слова static, появилась возможность писать так: При этом Реймонд говорит о том, что если у Execute сделать модель вызова stdcall, то бинарные сигнатуры параметра CreateThread, методов ThreadProc и Execute совпадут — поэтому, мол, умный компилятор уменьшит код ThreadProc до простого jmp. Увы, но компилятор Delphi не настолько умён — в этом случае он генерирует полный вызов вместе с передачей параметра.

Properties (Delphi)

Contents

This topic describes the following material:

  • Property access
  • Array properties
  • Index specifiers
  • Storage specifiers
  • Property overr >About Properties

A property, like a field, defines an attribute of an object. But while a field is merely a storage location whose contents can be examined and changed, a property associates specific actions with reading or modifying its data. Properties provide control over access to an object’s attributes, and they allow attributes to be computed.

The declaration of a property specifies a name and a type, and includes at least one access specifier. The syntax of a property declaration is:

  • propertyName is any val >property Num: 0..9 . are invalid.
  • the indexintegerConstant clause is optional. For more information, see Index Specifiers, below.
  • specifiers is a sequence of read, write, stored, default (or nodefault), and implements specifiers. Every property declaration must have at least one read or write specifier.

Properties are defined by their access specifiers. Unlike fields, properties cannot be passed as var parameters, nor can the @ operator be applied to a property. The reason is that a property doesn’t necessarily exist in memory. It could, for instance, have a read method that retrieves a value from a database or generates a random value.

Property Access

Every property has a read specifier, a write specifier, or both. These are called access specifiers and they have the form:

where fieldOrMethod is the name of a field or method declared in the same class as the property or in an ancestor class.

  • If fieldOrMethod is declared in the same class, it must occur before the property declaration. If it is declared in an ancestor class, it must be visible from the descendant; that is, it cannot be a private field or method of an ancestor class declared in a different unit.
  • If fieldOrMethod is a field, it must be of the same type as the property.
  • If fieldOrMethod is a method, it cannot be dynamic and, if virtual, cannot be overloaded. Moreover, access methods for a published property must use the default register calling convention.
  • In a read specifier, if fieldOrMethod is a method, it must be a parameterless function whose result type is the same as the property’s type. (An exception is the access method for an indexed property or an array property.)
  • In a write specifier, if fieldOrMethod is a method, it must be a procedure that takes a single value or const parameter of the same type as the property (or more, if it is an array property or indexed property).

For example, given the declaration:

the GetColor method must be declared as:

and the SetColor method must be declared as one of these:

(The name of SetColor ‘s parameter, of course, doesn’t have to be Value .)

When a property is referenced in an expression, its value is read using the field or method listed in the read specifier. When a property is referenced in an assignment statement, its value is written using the field or method listed in the write specifier.

The example below declares a >TCompass with a published property called Heading . The value of Heading is read through the FHeading field and written through the SetHeading procedure:

Given this declaration, the statements:

In the TCompass >Heading property; the read operation consists of retrieving the value stored in the FHeading field. On the other hand, assigning a value to the Heading property translates into a call to the SetHeading method, which, presumably, stores the new value in the FHeading field as well as performing other actions. For example, SetHeading might be implemented like this:

A property whose declaration includes only a read specifier is a read-only property, and one whose declaration includes only a write specifier is a write-only property. It is an error to assign a value to a read-only property or use a write-only property in an expression.

Array Properties

Array properties are indexed properties. They can represent things like items in a list, child controls of a control, and pixels of a bitmap.

The declaration of an array property includes a parameter list that specifies the names and types of the indexes. For example:

The format of an index parameter list is the same as that of a procedure’s or function’s parameter list, except that the parameter declarations are enclosed in brackets instead of parentheses. Unlike arrays, which can use only ordinal-type indexes, array properties allow indexes of any type.

For array properties, access specifiers must list methods rather than fields. The method in a read specifier must be a function that takes the number and type of parameters listed in the property’s index parameter list, in the same order, and whose result type is identical to the property’s type. The method in a write specifier must be a procedure that takes the number and type of parameters listed in the property’s index parameter list, in the same order, plus an additional value or const parameter of the same type as the property.

For example, the access methods for the array properties above might be declared as:

An array property is accessed by indexing the property identifier. For example, the statements:

The definition of an array property can be followed by the default directive, in which case the array property becomes the default property of the class. For example:

If a >object[index] , which is equivalent to object.property[index] . For example, given the declaration above, StringArray.Strings[7] can be abbreviated to StringArray[7] . A class can have only one default property with a given signature (array parameter list), but it is possible to overload the default property. Changing or hiding the default property in descendent classes may lead to unexpected behavior, since the compiler always binds to properties statically.

Index Specifiers

Index specifiers allow several properties to share the same access method while representing different values. An index specifier consists of the directive index followed by an integer constant between -2147483647 and 2147483647. If a property has an index specifier, its read and write specifiers must list methods rather than fields. For example:

An access method for a property with an index specifier must take an extra value parameter of type Integer. For a read function, it must be the last parameter; for a write procedure, it must be the second-to-last parameter (preceding the parameter that specifies the property value). When a program accesses the property, the property’s integer constant is automatically passed to the access method.

Given the declaration above, if Rectangle is of type TRectangle , then:

Storage Specifiers

The optional stored, default, and nodefault directives are called storage specifiers. They have no effect on program behavior, but control whether or not to save the values of published properties in form files.

The stored directive must be followed by True, False, the name of a Boolean field, or the name of a parameterless method that returns a Boolean value. For example:

If a property has no stored directive, it is treated as if stored True were specified.

The default directive must be followed by a constant of the same type as the property. For example:

To overr >» (the empty string), respectively.

Note: You can’t use the ordinal value -2147483648 has a default value. This value is used internally to represent nodefault.

When saving a component’s state, the storage specifiers of the component’s published properties are checked. If a property’s current value is different from its default value (or if there is no default value) and the stored specifier is True, then the property’s value is saved. Otherwise, the property’s value is not saved.

Note: Property values are not automatically initialized to the default value. That is, the default directive controls only when property values are saved to the form file, but not the initial value of the property on a newly created instance.

Storage specifiers are not supported for array properties. The default directive has a different meaning when used in an array property declaration. See Array Properties, above.

Property Overrides and Redeclarations

A property declaration that does not specify a type is called a property override. Property overrides allow you to change a property’s inherited visibility or specifiers. The simplest override consists only of the reserved word property followed by an inherited property identifier; this form is used to change a property’s visibility. For example, if an ancestor class declares a property as protected, a derived class can redeclare it in a public or published section of the class. Property overrides can include read, write,stored, default, and nodefault directives; any such directive overrides the corresponding inherited directive. An override can replace an inherited access specifier, add a missing specifier, or increase a property’s visibility, but it cannot remove an access specifier or decrease a property’s visibility. An override can include an implements directive, which adds to the list of implemented interfaces without removing inherited ones.

The following declarations illustrate the use of property overrides:

The override of Size adds a write specifier to allow the property to be modified. The overrides of Text and Color change the visibility of the properties from protected to published. The property override of Color also specifies that the property should be filed if its value is not clBlue .

A redeclaration of a property that includes a type identifier hides the inherited property rather than overriding it. This means that a new property is created with the same name as the inherited one. Any property declaration that specifies a type must be a complete declaration, and must therefore include at least one access specifier.

Whether a property is hidden or overridden in a derived class, property look-up is always static. That is, the declared (compile-time) type of the variable used to identify an object determines the interpretation of its property identifiers. Hence, after the following code executes, reading or assigning a value to MyObject.Value invokes Method1 or Method2 , even though MyObject holds an instance of TDescendant . But you can cast MyObject to TDescendant to access the descendent class’s properties and their access specifiers:

Class Properties

Class properties can be accessed without an object reference. Class property accessors must themselves be declared as class static methods, or class fields. A class property is declared with the class property keywords. Class properties cannot be published, and cannot have stored or default value definitions.

You can introduce a block of class static fields within a class declaration by using the class var block declaration. All fields declared after class var have static storage attributes. A class var block is terminated by the following:

  1. Another class var declaration
  2. A procedure or function (i.e. method) declaration (including class procedures and class functions)
  3. A property declaration (including class properties)
  4. A constructor or destructor declaration
  5. A visibility scope specifier (public, private, protected, published, strict private, and strict protected)

You can access the above class properties with the code:

Работа с API онлайн-сервисов в Delphi. Авторизация и работа с методами API

Это вторая часть большой статьи про использование API онлайн-сервисов в Delphi. В первой части мы определились с тем, что необходимо знать прежде, чем начинать свои разработки.

В этой части мы рассмотрим весь процесс написания своего первого модуля Delphi для работы с API онлайн-сервиса.

Так как эта часть посвящено работе в Delphi, то здесь будет достаточно много кода.

Содержание части

Авторизация и получение доступа к API

Один из ключевых моментов реализации любого API — авторизация пользователя. Как уже было сказано ранее, в настоящее время наиболее активно используется авторизация пользователей по протоколу OAuth, а точнее — OAuth 2.0. Чтобы авторизовать пользователя по OAuth вы можете написать свой собственный класс или же, если Вы используете Delphi XE5-XE7, то можете воспользоваться компонентами REST Client Library. Чтобы не повторяться о том, как это сделать, я просто приведу здесь ссылки на статьи где рассматривалась авторизация по OAuth в разных онлайн-сервисах:

  1. Серия статей про OAuth в Google:
    1. Google API в Delphi. OAuth для Delphi-приложений,
    2. Google API в Delphi. Обновление модуля для OAuth,
    3. Решение проблем с Google OAuth 2.0. для Win-приложений,
    4. Тестирование запросов к API Google средствами Delphi. Компонент OAuthClient для Delphi XE — XE3.
  2. Использование REST Client Library для OAuth:
    1. Delphi XE5: REST Client Library,
    2. Delphi: авторизация по OAuth 2.0 в Dropbox своими силами,
    3. REST Client Library: использование API ВКонтакте

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

Пример с etxt.ru

Начинаем читать документацию. Что нам говорит сервис:

Порядок следования параметров в запросе значения не имеет, порядок параметров важен только при расчете подписи.

API-ключ token уникален для каждого пользователя и его можно узнать в разделе «Мой профиль/Настройка интерфейса».

Подпись sign расcчитывается по алгоритмe, приведенному ниже. Подписываются только параметры, переданные по GET.

Важные моменты в этой части документации выделены жирным:

  1. Все текстовые параметры запроса передаются в кодировке UTF-8
  2. Для каждого запроса нам необходимо рассчитывать по специальному алгоритму подпись.
  3. Порядок следования параметров в самом запросе не важен
  4. При расчёте подписи все параметры должны следовать в строго определенном порядке.

Что это всё значит? Ну, с кодировкой, допустим, всё понятно. В остальном же получается, что доступ к API предоставляется нам только, если мы передадим на сервер два верных параметра — это token (он не меняется и получается при регистрации пользователя) и sign — подпись, которая меняется при каждом запросе. Следовательно, нам необходимо в своей программе предусмотреть специальный метод, который будет рассчитывать нам эту подпись. Попробуем написать такой метод.

Снова смотрим документацию. Вот, что говорит нам сервис про алгоритм подписи запроса:

Значение params — это конкатенация пар «имя=значение» отсортированных в алфавитом порядке по «имя», где «имя» — это название параметра, передаваемого в функцию API, «значение» — значение параметра. Разделитель в конкатенации не используется. Параметр sign при расчете подписи не учитывается, все остальные параметры запроса должны учитываться при расчете.

Теперь попробуем составить алгоритм расчёта такой подписи. Итак, что нам нужно:

  1. Найти в ключ api_pass (он как и token нам выдается сервисом один раз и на всю жизнь)
  2. Необходимо отсортировать все параметры запроса в алфавитном порядке
  3. Необходимо произвести конкатенацию, т.е. «склеивание» параметров
  4. Рассчитать MD5 для строки api_pass.’api-pass’
  5. Полученную в п.3 строку «склеить» с результатов п.4
  6. Рассчитать для полученной в п.5 строки MD5- это и будет наша подпись.

Реализуем этот алгоритм в Delphi.

Так как в дальнейшем предстоит использовать этот API, то я создал отдельный класс, который постепенно будет «обрастать» новыми методами и свойствами для работы с API. Класс этот вынесен в отдельный модуль и на данном этапе выглядит так:

Свойства Token и ApiPass — это ключ доступа и пароль к API, которые, как мы уже выяснили никогда не меняются. Теперь рассмотрим метод SignRequest, который будет вычислять подпись и добавлять её к параметрам запроса:

Так как параметр sign не участвует в расчёте подписи, то вначале мы проверили есть ли такой параметр в списке и, если sign присутствует, то удалили его. Далее мы проверили наличие параметра token и, при необходимости, добавили его в список. После этого мы отсортировали весь список параметров в алфавитном порядке и составили строку params. Для расчёта MD5 мы воспользовались возможностями класса TIdHashMessageDigest, который находится в модуле IdHashMessageDigest. Расчёт производился в два шага:

  1. Рассчитали хэш пароля API.
  2. Полученный хэш добавили к строке params и рассчитали новый хэш для полученной строки

После этого добавили подпись в параметр sign запроса. Теперь список AParams содержит все необходимые параметры для выполнения запроса к API. Как проверить, что рассчитанная подпись верная? Очень просто — попробовать выполнить какой-нибудь простой запрос к API.

Выполнение запросов к API

Выполнив авторизацию и получив доступ к API мы можем выполнять различные запросы к API. Различные API требуют могут предъявлять разные требования к выполнению запросов. И прежде, чем начинать писать код в Delphi, опять же, следует внимательно прочитать документацию к API и определиться с тем как могут выглядеть различные запросы к одному и тому же API.

В URL запроса всегда присутствует общая для всех запросов часть. Так, например, если используется API, использующий REST-принципы (любой API Яндекса, Google, ВКонтакте и т.д.), то запросы к такому серверу могут иметь следующий вид:

  • http://example.com/api/book/1
  • http://example.com/api/lists/
  • http://example.com/api/authors/123
  • и т.д.

Видите? В каждом из запросов есть http://example.com/api/. В различной документации к API этот URL может называться по-разному: точка доступа, Base URL или просто URL запроса. Base URL всегда следует выносить в раздел констант. Объясню почему это стоит делать. Причин две:


  1. Для того, чтобы не использовать в дальнейшем в своем коде одну и ту же строку по 100 раз и избегать случайных опечаток, которые потом довольно сложно обнаружить в большом массиве кода
  2. Редко, но тем не менее встречается ситуация, когда сервер изменяет Base URL полностью или частично. Если произойдет смена Base URL, например, в новой версии API, то вам будет достаточно изменить всего одну константу в коде.

Определившись с Base URL можно начать реализовывать выполнение запросов к API в Delphi. Рассмотрим это, опять же, на примере API etxt.ru.

Пример выполнения запросов к etxt.ru

Определяем Base URL. В случае с etxt.ru этот URL указан в документации и выглядит так:

Этот URL не изменяется — изменяются только параметры запроса. Так, например, запрос к списку категорий может выглядеть так:
https://www.etxt.ru/api/json/?token=12345&method=categories.listCategories&sign=1234fde4567ef
при запросе списка папок запрос будет таким:
https://www.etxt.ru/api/json/?token=12345&method=folders.listFolders&sign=1dnt34dde4567ee

То есть, наша константа в Delphi может выглядеть так:

Так как сервер требует доступа по https, то для дальнейшей работы нам потребуются два компонента Indy: TidHTTP и TIdSSLIOHandlerSocketOpenSSL, которые находятся, соответственно, в модулях idHTTP и idSSLOpenSSL. Так же нам потребуются две динамические библиотеки: libeay32.dll и ssleay32.dll, которые вы можете скачать со страницы с исходниками.

Добавим TidHTTP и TIdSSLIOHandlerSocketOpenSSL в наш класс для работы с API:

Динамические библиотеки необходимо положить в папку с exe-файлом приложения. Теперь напишем новый метод нашего класса, который будет выполнять GET-запрос на сервер и возвращать ответ. С учётом того, что у нас уже написана процедура подписи запроса, наш новый метод может выглядеть так:

Метод получает на входе список параметров, затем подписывает запрос, отправляет его на сервер и записывает полученный ответ в Result. Проверим работу нашего метода.
Для этого создадим новое приложение VCL, подключим в uses модуль с нашим классом, а на главную форму бросим всего два компонента — TButton и TMemo:

В обработчике OnClick кнопки напишем следующий код:

Если наша подпись была рассчитана верно, то в результате мы должны получить в Memo JSON-объект с данными по категориям. Запускаем приложение, кликаем по кнопке и видим следующий результат:

Результат получен, следовательно, можно приступать к следующему шагу работы над API — разбору результатов запроса.

Парсинг результатов запроса

На предыдущем шаге работы с API мы получили от сервера «сырые» для будущего приложения данные. То есть на данный момент ни наше приложение ни наш класс для работы с API «не знают» что делать с данными — это простая строка, которую необходимо правильно разобрать и представить пользователю приложения.

В своей работе с самыми различными API я придерживаюсь следующих двух положений:

    Один класс используется непосредственно для обмена данными с сервером: в этом классе реализованы методы выполнения GET-, POST-, DELETE- и других запросов к API по HTTP

Мне такая схема работы представляется наиболее удобной в плане отладки. Вы же в своих приложениях вольны делать как угодно.

Для разбора ответов сервера, как я упоминал в первой части статьи, Вы должны понимать, хотя бы, что такое XML и JSON и как их можно разобрать в Delphi. Данные, полученные от сервера, внутри своей программы вы можете хранить и представлять как вам угодно — хранить в виде простой строки, создавать свои собственные классы, записи (record) и т.д. Внутри вашего приложения — вы хозяин и только Вы решаете как хранить и использовать полученные данные.

Опять же (и я не устану это повторять), прежде чем писать код необходимо прочитать документацию по API. На этот раз надо изучить то:

  1. какие свойства содержат возвращаемые объекты и типы данных этих свойств
  2. какие общие свойства есть у всех объектов API (и есть ли такие общие свойства, в принципе).

Если упустить этот момент, то в итоге вы можете сильно «раздуть» свой код повторяющимися свойствами родственных объектов. Например, в API Box.com можно встретить объекты Folder (папка) и MiniFolder (краткая информация о той же папке). В этом случае лучше всего в Delphi сделать класс TFolder дочерним от TMinifolder — упростит, в дальнейшем, отладку приложения, сократит код и, плюс, поможет избежать ошибок при парсинге.

Чем просматривать ответы сервера? Если данные приходят в JSON, то могу вам порекомендовать использовать онлайн-сервис http://jsonviewer.stack.hu/. Вот как выглядит в этом сервисе объект, полученный в предыдущем примере:

Как видно на рисунке, все поля объектов представляют из себя обычные строки. Разобрать такой объект будет достаточно просто.

Для примера, рассмотрим как можно разбирать, хранить и представлять данные от сервера etxt.ru.

Разбор данных etxt.ru

Итак, класс для работы с сервером по HTTP у нас есть (впоследствии мы можем добавить в него, например, метод выполнения POST-запроса к серверу или любой другой по необходимости) — его содержимого нам пока хватит для реализации разных методов API.

В предыдущем примере мы получили большой JSON-объект с тематическими категориями. Посмотрим из чего состоит объект категории. Читаем документацию:

categories.listCategories

Возвращает список тематических категорий заказов/статей, отсортированный по названию категории.

Результат
Поле Описание
id_category Идентификатор категории
id_parent Идентификатор родительской категории
name Название категории
keyword Ключевое слово категории

На языке Delphi это может быть, например, такой класс:

Property — Ключевое слово Delphi

Начнем с самого простого — с создания объявления класса. Для этого в меню «Component» выбираем пункт «New Component…». В появившемся окне, нужно задать следующие значения: Ancestor Type (класс родитель создаваемого класса), Class Name (имя создаваемого класса), Palette Page (название закладки палитры компонентов, на которую будет установлен данный компонент; если такой закладки не существует, то она будет создана), Unit File Name (имя модуля, в котором будет размещено описание класса).

Итак, создадим потомок TСustomControl’а — TMyClass, в результате чего получим следующие объявление класса:

Ключевое слово class указывает на то, что мы создаем новый класс TMyClass, порождая его от родителя — TCustomControl.

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

Вот пример использования упреждающего объявления класса:

В соответствии с концепцией объектно-ориентированного программирования (инкапсуляцией) в Delphi существуют четыре директивы видимости, предназначенные для сокрытия полей и методов внутри объекта. Итак, первая из них — private. Это сама строгая из всех директив видимости. Поля и методы, объявленные в этой секции, не будут видны во всех классах-потомках. Вторая, менее строгая — protected. Она предназначена для объявления тех полей и методов, которые должны быть видны в классах-потомках. Следующая директива — public, предназначена для объявления элементов, которые будут видны программе или модулю, если они имеют доступ к модулю, в котором объявлен класс. И, наконец, последняя директива — published. Она предназначена для объявления тех свойств, которые должны быть видны в инспекторе объектов на этапе проектирования (design-time).

Вообще, поля — это обычные переменные, инкапсулированные внутри объекта. Объявляются они точно так же, как и в обычном Паскале (смотри пример ниже, FStr — поле типа String). Также полями могут быть и объекты.

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

Реализация методов производится в том же модуле, где и объявление класса, в секции implementation, следующим образом:

Этот пример также хорошо иллюстрирует сущность инкапсуляции. Класс TMyClass инкапсулирует поле FStr и реализацию методов записи — SetStr и чтения — GetStr этого поля.

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

Виртуальные и динамические методы

Методы могут быть виртуальными и динамическими. Они различаются способом решения классической проблемы «время — ресурс, ресурс — время» (в данном случае ресурс — это объем оперативной памяти).

Разница между ними заключается в структуре соответствующих таблиц методов VMT — Virtual Metod Table (таблица виртуальных методов) и DMT — Dynamic Metod Table (таблица динамических методов). Диспетчеризация виртуальных методов происходит быстрее, чем динамических, но с другой стороны, таблица виртуальных методов (VMT) занимает больше места в памяти, чем таблица динамических (DMT). Выбор остается за программистом.

Для определения типа метода (виртуальный или динамический) используются две директивы — virtual и dynamic соответственно. Для указания типа метода после его объявления через точку с запятой указывается одна из этих директив. Вот простой пример:

Также стоит сказать и об абстрактных методах — они определяются путем добавления после объявления метода директивы abstract. Такое объявление метода делает ненужным и даже неправомочным реализацию данного метода в текущем классе. Вызов такого метода также будет считаться неправомочным. Такие методы в основном предназначены для перекрытия их в потомках. Пример объявления абстрактного метода:

Методы обработки сообщений

Еще одна разновидность методов — это методы обработки сообщений (message-handling metods). Эти методы специально предназначены для обработки сообщений Windows. На них накладываются некоторые ограничения: они обязательно должны быть процедурами, должны иметь один параметр — переменную; при прикрытии таких методов директива override не применяется; также при перекрытии необходимо вызывать метод предка. Объявляются такие методы с помощью директивы message. Вот пример объявления и реализации метода обработки сообщений:

Константа после директивы message определяет сообщение, которое будет обрабатываться этим методом (в данном случае метод будет вызываться каждый раз, когда происходит перемещение курсора мыши над данным объектом). Тип единственного параметра процедуры задает, в каком формате методу будут переданы данные сообщения (например, в этом случае Msg.XPos и Msg.YPos — текущие координаты курсора мыши в области данного компонента). Описание констант и соответствующих типов помещено в модуле Messages.

Классовые методы (или методы класса)

Существует еще одно важное расширение методов — классовые методы. Это методы, которые можно вызывать, не создавая экземпляра объекта (например: TMyClass.MyClassMetod). В теле классового метода нельзя обращаться ни к одному методу или полю данного класса, но можно вызывать конструкторы и другие классовые методы. Обычно такие методы используются для предоставления информации о классе, версии реализации и т. д. Определяются такие методы путем добавления директивы class перед объявлением метода. Вот пример классового метода и его реализации:

Перерегружаемые (overload) методы

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

В данном примере, при вызове пользователем метода SetX, в зависимости от типа передаваемого параметра будет вызван соответствующий метод (для параметра типа String — первый, для Integer — второй).

Пример вызова первого варианта метода:

Пример вызова второго варианта метода:

Удобства, предоставляемые данной возможностью, очевидны, например, в данном случае пользователю не нужно самому конвертировать Integer в String.

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

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

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

Объявление свойства, чтение и запись.

Вот один из вариантов объявления свойства:

Свойства объявляются с использованием директивы property, за ней следует имя свойства и тип (как объявление переменной в обычном Паскале), затем директива read и указание «источника» чтения (это либо поле такого же типа, как свойство, либо функция без параметров, возвращающая значение того же типа), далее следует директива write с указанием «приемника» записи (поле с тем же типом, что и свойство, либо процедура с одним параметром, того же типа). В нашем случае чтение осуществляется напрямую из поля, а запись через процедуру. Здесь есть одна хитрость. Соответствующее поле не обязательно должно существовать или иметь тот же тип, что и свойство. Это позволяет снизить избыточность хранимой информации и уменьшить расход памяти. Распространенным примером такого подхода является предоставление информации о размере какого-либо массива, например так:

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

property StrCount: Integer read GetStrCount; — свойство только для чтения (read-only)
property StrCount: Integer write SetStrCount; — свойство только для записи (write-only).

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

Теперь разберемся, как организовать свойство, предоставляющее доступ непосредственно к элементам массива — строкам для предыдущего примера. Для этого обязательно чтение и запись производить через соответствующий метод, добавляя в его описание один параметр — индекс, и задать тип индекса для самого свойства. На практике это выглядит так:

Тип индекса может быть и другим, например строкой.

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

К таким свойствам следует обращаться, как к массивам, например для экземпляра Test класса TMyClass — Test.Str[5].

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

Как видно из примера, если для свойства определен индекс, совершенно необязательно для чтения или записи использовать метод, поддерживающий индексирование. Методом чтения и записи в качестве индекса (параметр Index, если у них таковой имеется) для каждого свойства будет передаваться то значение, которое указано после директивы index. Ну а в теле метода по полученному значению индекса выясняем, к какому из свойств было осуществлено обращение, и предпринимаем соответствующие действия.

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

Переобъявляются свойства очень просто, возьмем наш пример с массивом строк:

Эта директива применяется для задания значения по умолчанию для свойств. На практике выглядит так:

Тип свойства должен совпадать с типом значения, указываемого после default’а.

С этой директивой связано одно распространенное заблуждение — многие считают, что с ее помощью можно инициировать значения свойств. Это неверно. Дело в том, что все значения свойств, установленные программистом для определенного объекта, в инспекторе объектов сохраняются в файл формы (.dfm), который потом компилируется вместе с кодом. При запуске приложения автоматически создаются объекты, заданные на этапе проектирования, и для них загружаются и устанавливаются значения свойств. Это не выгодно, во-первых, потому, что увеличивает размер откомпилированного exe файла, а во-вторых, на все это тратится изрядная доля времени при запуске приложения.

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

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

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

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

На такое свойство можно ссылаться без указания его имени, например для экземпляра MyClass класса TMyClass — MyClass[10] равносильно MyClass.Str[10].

Эта директива предназначена для жесткого определения — сохранять значение свойства в файле формы или нет. Она добавляется в конец определения свойства, и после нее должно идти либо булево (boolean — FALSE или TRUE) значение, либо функция, возвращающая булево значение, либо поле булевого типа. Если булево значение ИСТИНА (TRUE), то сохранение значения свойства происходит, если ЛОЖЬ (FALSE) — нет. Этот механизм может быть полезен, если существует какая-либо избыточность полей и соответствующих им свойств. Возможно совместное использование директив default и stored. Пример (взят из модуля Controls):

События — это единственный для объекта способ спровоцировать какое-либо внешнее действие в ответ на изменение его состояния.

Событие как свойство

В Delphi все события реализуются как свойства определенного типа. Этот тип должен быть предварительно определен. Вот определение самого распространенного в Delphi свойства:

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

Само объявление же события будет выглядеть так:

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

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

Общепринятые правила именования

Напоследок стоит сказать об общепринятых правилах именования классов и их составляющих:

  • из названия должно быть понятно, для чего предназначен данный класс, метод, поле или свойство;
  • название классов принято называть с T, от type;
  • к названию иногда добавляют инициалы разработчика компонента или часть названия фирмы. Для Васи Иванова это может быть — TviComponent;
  • имена полей принято начинать с F, от Field (поле, область);
  • свойства принято называть точно так же, как и поля (если свойству в соответствие ставится поле), опуская F. Поле FStr — свойство Str;
  • имена методов чтения и записи принято образовывать путем добавления перед именем свойства Get и Set, соответственно. Свойство Str — метод чтения GetStr, метод записи SetStr;
  • именование полей и методов чтения и записи для событий подчиняются тем же правилам, что и для свойств;
  • имена событий принято начинать с On: OnClick, OnMouseMove.

Для того чтобы установить созданный класс в палитру компонентов, в меню «Component» выбираем пункт «Install Component…». В появившемся окне нужно задать следующие значения: Unit file name (имя модуля, в котором содержится описание устанавливаемого класса), Search path (пути через «;» по которым Delphi будет искать указанный модуль), Package file name (путь и имя файла пакета, в который будет добавлен устанавливаемый вами модуль с описанием класса).

Property — Ключевое слово Delphi

В первую очередь пошел искать в хелпе, не нашел (хотя узнал много интересного ^^), в яндексе тоже глухо. В книжках по делфи, которые мне попадались о сабже тоже нет упоминаний. Итак, что же делает ключевое слово «object»? Ссылки на документацию приветствуются, но и ответом «своими словами» не побрезгую.

ЗЫ В общем-то, какой-то конкретной цели не преследую, просто для повышения образованности.

Это устаревший способ работы с самодельными объектами. Теперь гораздо целесообразнее использовать class

> Это устаревший способ работы с самодельными объектами

Моё любопытсво все еще не удовлетворено :). Чем object отличается от class, что именно устарело. Попадался мне как то код с этим волшебным словом, так что хотелось бы понять что это из себя представляет.

Идеальное слово для описания всего.

Даже для искуственного интелекта.

> Desdechado © (03.03.07 17:06) [1]
> Это устаревший способ работы с самодельными объектами. Теперь гораздо целесообразнее использовать class

В Делфях классом считается все, что прождено от TObject. Но проблема в том, что нужно было объявить сам TObject в юните «system.pas», для чего и использовали ключевое слово языка Паскаль object. Ключевое слово class не является зарезервированным словом языка Паскаль, хотя поддерживается и зарезервировано сейчас почти всеми компиляторами (Delpi, FPC, Lazarus, Kylix, TNT).


> Чем object отличается от class, что именно устарело

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

Руководство щас погуглю.
А под статическим объектом понимается объект, все методы которого статические? А конструктор с деструктором присутствуют? (=

Servelat © (03.03.07 17:19) [6]
нет просто память под такой объект выделяется статически, то есть при загрузке программы в память, а не когда программист соизволит

> PEAKTOP © (03.03.07 17:14) [4]
TObject = class;
T >
купи «твикс». жуй. молчи. заодно почитай материалы по Object Pascal (тому, из которого упёрли концепцию обёектов в Turbo Pascal 5.5).

Собственно, вопрос закрыт; нашел у себя на винте UserGuide по BP70 (что прям удивительно, на русском), где тема полностью раскрыта, просвещаюсь. Всем спасибо за ответы.

Удобен тем, что содержит в себе идеи ООП (наследование) и не требует лишней работы и памяти. Посмотри, сколько всего делается и создается в конструкторе TObject! Если мне нужно будет использовать сотню-другую тысяч объектов, то выбор будет за object»ами, а не за классами, наверное.

> Если мне нужно будет использовать сотню-другую тысяч объектов,
> то выбор будет за object»ами, а не за классами, наверное.

Так и до record»a дойти не долго :))

> Ketmar © (03.03.07 17:34) [8]

> Loginov Dmitry © (03.03.07 18:58) [11]

Структурное программирование рулит. Я серьезно.


> Посмотри, сколько всего делается и создается в конструкторе
> TObject! Если мне нужно будет использовать сотню-другую
> тысяч объектов, то выбор будет за object»ами, а не за классами,
> наверное.

Гм. Наверное наооборот :-) class как раз под такие случаи и заточен :-)

> tesseract © (03.03.07 20:52) [14]

На пустых объектах/классах поэкспериментировал, 100000000 TObject»ов создано за 19 секунд и использовано 720 метров памяти, столько просто обджектов — за ноль секунд и использовано памяти метр с копейками. Если объекты буду содержать какие-то данные и выполнять какие-то действия, то и на сотне разницу почуствуем.

> использовано памяти метр с копейками

Сами-то в это верите?

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

Или эта разница просто-напросто исчезнет.

> Сами-то в это верите?

Четырехзначная цифирь, начинается на один .

to PEAKTOP © (03.03.07 17:14) [4]:
Что за трава? Или это были грибы? :)))

to TUser © (03.03.07 21:04) [15]:
>На пустых объектах/классах поэкспериментировал
Угу, а производительность будем на «Hello, World!» мерять. :)
Пустой object занимает 0 байт, в отличие от наследника TObject, который занимает 4 минимум байта.

>Если объекты буду содержать какие-то данные и выполнять какие-то
>действия, то и на сотне разницу почуствуем.
Фиг. Если в object будет поле, допустим integer, вес уравняется. Преимуществ не будет почти никаких. При одинаковом количестве полей будут те самые 400 байт на сотне.

И вообще, что-то цифры подозрительные. Что там в коде-то?

TUser © (03.03.07 21:04) [15]

Тут такой момент — пустые объекты, как и пустые классы никому не нужны. Давай ты полные насоздаешь ?

33 секунды для объектов vs минута думанья и Out of memory в случае классов :) Объекты и классы содержали по два поля типа integer + то, что от TObject. Говорю же — объекты (те, которые экземпляры классов) память любят кушать. Больше, чем объекты, которые просто, т.е. типа record с наследованием.

> Говорю же — объекты (те, которые экземпляры классов) память
> любят кушать. Больше, чем объекты, которые просто, т.е.
> типа record с наследованием.

Ай-ай! Какая беда! 100000000 объектов всю память скушали!
Только нюансик один: что вообще в принципе можно делать с таким количеством объектов? Их же надо где-то индексировать (в массиве там, или в списке), это еще 400 метров памяти надо. Нужен метод доступа, поиска и т.п., что при таком количестве объектов (будь то object или class) будет выполняться катастрофически долго.
То есть пример со 100000000 объектов выглядет очень уж надуманным.

to TUser © (04.03.07 04:27) [21]:
>Говорю же — объекты (те, которые экземпляры классов) память любят
>кушать. Больше, чем объекты, которые просто, т.е. типа record с
>наследованием.
А вот мои подсчеты говорят, что памяти в результате понадобится одинаковое количество. Догадываетесь, почему? А времени, тут да, создание экземпляра класса требует чуть больше, т.к. экземпляру класса требуется инициализация, а у object её по-умолчанию нет, но это опять же, до тех пор пока там данных в нормальном количестве нет.

Поэтому еще раз говорю, что-то там не то с методикой. Код в студию.

Чтобы вопросов не возникало:

uses
SysUtils,
Windows;

type
Cls = class
a,b: integer;
end;

PObj = ^Obj;
Obj = object
private
a,b: integer;
end;

procedure AllocCls(count: integer);
var
i: integer;
begin
for i := 0 to Count — 1 do
Cls.Create;
end;

procedure AllocObj(count: integer);
var
i: integer;
o: PObj;
begin
for i := 0 to Count — 1 do
new(o);
end;

var
T1, T2, T3: Cardinal;
S1, S2, S3: Cardinal;
begin
T1 := GetTickCount;
S1 := GetHeapStatus.TotalAllocated;
AllocCls(10000000);
S2 := GetHeapStatus.TotalAllocated;
T2 := GetTickCount;
AllocObj(10000000);
S3 := GetHeapStatus.TotalAllocated;
T3 := GetTickCount;
writeln(Format(«%d %d», [T2-T1, S2-S1]));
writeln(Format(«%d %d», [T3-T2, S3-S2]));
readln;
end.

Результат (CoreDuo 6600 2.4 ГГц, RAM 2ГБ):
500 120000000
109 120000000


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

Но проблема в том, что нужно было объявить сам TObject в юните «system.pas», для чего и использовали ключевое слово языка Паскаль object

О как. Оказывается, object использовали для создания class. Вот так и возникают религии. Хвала всевышнему, объект придумавшему, ибо класс и экземпляр класса — суть порождение его. Во имя object, class и instance его! Аминь.

Я, наверное, еретик. бо у меня возник вопрос, откуда же в таком случае взялся сам object :0)

и если от record»а, то откуда взялся record? и так далее. )

to Virgo_Style © (04.03.07 12:29) [26]:
>откуда взялся record?
record был всегда :)

record придумал Вирт, а Вирта придумали мама с папой. выводы очевидны :)

хех
неужто настолько делать нефига людям что ещё не влом думать о такой архаике как object:)

> PEAKTOP © (03.03.07 19:03) [12]
расслабься. приведённый мной код — цитата из system.pas. советую перед тем, как вещать, таки читать генофонд. ну и — да, язык, о котором вещаешь, тоже выучить бы не помешало.

>>неужто настолько делать нефига людям что ещё не влом думать о такой архаике как object:)
что за наезды на обжект? фундаментальная штука. это класс придумка борланда, насколько помню в стандарте паскаля нет такого словечка:)

object — это отнюдь не архаика.

Во-первых, без слова object затруднительно определить callback типа

type
TForProgressBar = procedure(Current, Total: Integer) of object;

Это, ясное дело, очень мощное и «современное» средство, не реализованное, к примеру, в С++. Там аналог убогий.

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

> просто так (04.03.07 14:07) [32]
я-таки скажу откровение: в том стандарте и object ни разу нет.


> я-таки скажу откровение: в том стандарте и object ни разу
> нет

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

_uw_ (04.03.07 14:08) [33]
ну я не про все применения ключевого слова object, а именно про то применение которое оставлено для совместимости

в .NET структуры наделённые свойствами ООП юзаются порой в качестве «легковесных» объектов ибо для них в стеке память выделяется(то есть быстро) и освобождается быстро(а не когда GC соизволит)
в каком-то таком же смысле использования object-объектов возможно и может быть оправдано

Поля, свойства и методы класса

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

Согласно принятому обозначению в Delphi имена полей должно начинаться с буквы F (Field – поле), а имена классов с буквы T.

Изменение значений полей обычно выполняется с помощью методов и свойств объекта.

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

Описание свойства начинается со слова property, при этом типы свойства и соответствующего поля должны совпадать.

Метод представляет собой подпрограмму (процедуру или функцию), предназначенную для обработки полей.

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

Метод, объявленный в классе, может вызываться различными способами, что зависит от вида этого метода. Вид метода определяется модификатором, который указывается в описании класса после заголовка метода и отделяется от заголовка точкой с запятой: virtual (виртуальный метод); dynamic (динамический метод) и др.

По умолчанию все методы, объявленные в классе, являются статическими и вызываются как обычные подпрограммы.

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

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

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

property x1:Integer read Fx1 write Fx2;

Ключевые слова read и write называются спецификаторами доступа. После слова read указывается поле или метод, к которому происходит обращение при чтении (получении) значения свойства, а после слова write – поле или метод, к которому происходит обращение при записи (установке) значения свойства. Например, чтение свойства x1 означает чтение поля Fx1, а запись свойства x1 – чтение поля Fx2. Чтобы имена свойств не совпадали с именами полей, последние принято писать с буквы F (от англ. field).

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

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

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

Программирование на языке Delphi

Глава 3. Объектно-ориентированное программирование (ООП)


Авторы: А.Н. Вальвачев
К.А. Сурков
Д.А. Сурков
Ю.М. Четырько

Опубликовано: 19.11.2005
Исправлено: 10.12.2020
Версия текста: 1.0

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

Сейчас преимущества использования объектов очевидны для всех. Однако так было не всегда. Сначала старая гвардия не поняла и не приняла объекты, поэтому они почти 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» (файл в таком формате представляет собой последовательность строк; каждая строка состоит из значений, которые отделены друг от друга символом-разделителем):

Класс содержит поля (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 :

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • выполняется заданный программистом код завершения;
  • освобождается занимаемая объектом динамическая память.

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

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

3.5. Методы

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

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

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

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

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

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

3.6. Свойства


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Краткое описание методов в классе TObject:

  • Create — стандартный конструктор.
  • Free — уничтожает объект: вызывает стандартный деструктор Destroy, если значение псевдопеременной Self не равно nil .
  • InitInstance (Instance: Pointer): TObject — при создании объекта инициализирует нулями выделенную память. На практике нет необходимости вызывать этот метод явно.
  • CleanupInstance — освобождает память, занимаемую полями с типом string, Variant, динамический массив и интерфейс. На практике нет необходимости вызывать этот метод явно.
  • ClassType : TClass — возвращает описатель класса (метакласс).
  • ClassName : ShortString — возвращает имя класса.
  • ClassNameIs (const Name: string): Boolean — проверяет, является ли заданная строка именем класса.
  • ClassParent : TClass — возвращает описатель базового класса.
  • ClassInfo : Pointer — возвращает указатель на соответствующую классу таблицу RTTI (от англ. Runtime Type Information). Таблица RTTI используется для проверки типов данных на этапе выполнения программы.
  • InstanceSize : Longint — возвращает количество байт, необходимых для хранения в памяти одного объекта соответствующего класса. Заметим, что значение, возвращаемое этим методом и значение, возвращаемое функцией SizeOf при передаче ей в качестве аргумента объектной переменной — это разные значения. Функция SizeOf всегда возвращает значение 4 (SizeOf(Pointer)), поскольку объектная переменная — это ни что иное, как ссылка на данные объекта в памяти. Значение InstanceSize — это размер этих данных, а не размер объектной переменной.
  • InheritsFrom (AClass: TClass): Boolean — проверяет, является ли класс AClass базовым классом.
  • MethodAddress (const Name: ShortString): Pointer — возвращает адрес published-метода, имя которого задается параметром Name.
  • MethodName (Address: Pointer): ShortString — возвращает имя published-метода по заданному адресу.
  • FieldAddress (const Name: ShortString): Pointer — возвращает адрес published-поля, имя которого задается параметром Name.
  • GetInterface (const IID: TGUID; out Obj): Boolean — возвращает ссылку на интерфейс через параметр Obj; идентификатор интерфейса задается параметром IID. (Интерфейсы рассмотрены в главе 6)
  • GetInterfaceEntry (const IID: TGUID): PInterfaceEntry — возвращает информацию об интерфейсе, который реализуется классом. Идентификатор интерфейса задается параметром IID.
  • GetInterfaceTable : PInterfaceTable — возвращает указатель на таблицу с информацией обо всех интерфейсах, реализуемых классом.
  • AfterConstruction — автоматически вызывается после создания объекта. Метод не предназначен для явного вызова из программы. Используется для того, чтобы выполнить определенные действия уже после создания объекта (для этого его необходимо переопределить в производных классах).
  • BeforeDestruction — автоматически вызывается перед уничтожением объекта. Метод не предназначен для явного вызова из программы. Используется для того, чтобы выполнить определенные действия непосредственно перед уничтожением объекта (для этого его необходимо переопределить в производных классах).
  • Dispatch (var Message) — служит для вызова методов, объявленных с ключевым словом message .
  • DefaultHandler (var Message) — вызывается методом Dispatch в том случае, если метод, соответствующий сообщению Message, не был найден.
  • NewInstance : TObject — вызывается при создании объекта для выделения динамической памяти, чтобы разместить в ней данные объекта. Метод вызывается автоматически, поэтому нет необходимости вызывать его явно.
  • FreeInstance — вызывается при уничтожении объекта для освобождения занятой объектом динамической памяти. Метод вызывается автоматически, поэтому нет необходимости вызывать его явно.
  • Destroy — стандартный деструктор.

3.7.3. Перекрытие атрибутов в наследниках

В механизме наследования можно условно выделить три основных момента:

  • наследование полей;
  • наследование свойств;
  • наследование методов.

Любой порожденный класс наследует от родительского все поля данных, поэтому классы TDelimitedReader и TFixedReader автоматически содержат поля FFile, FActive и FItems, объявленные в классе TTextReader. Доступ к полям предка осуществляется по имени, как если бы они были определены в потомке. В потомках можно определять новые поля, но их имена должны отличаться от имен полей предка.

Наследование свойств и методов имеет свои особенности.

Свойство базового класса можно перекрыть (от англ. override) в производном классе, например чтобы добавить ему новый атрибут доступа или связать с другим полем или методом.

Метод базового класса тоже можно перекрыть в производном классе, например чтобы изменить логику его работы. Обратимся к классам TDelimitedReader и TFixedReader. В них методы PutItem, GetItem, SetActive и GetEndOfFile унаследованы от TTextReader, поскольку логика их работы не зависит от того, в каком формате хранятся данные в файле. А вот метод ParseLine перекрыт, так как способ разбора строк зависит от формата данных:

В классах TDelimitedReader и TFixedReader перекрыт еще и конструктор Create. Это необходимо для инициализации специфических полей этих классов (поля FDelimiter в классе TDelimitedReader и поля FItemWidths в классе TFixedReader):

Как видно из примера, в наследнике можно вызвать перекрытый метод предка, указав перед именем метода зарезервированное слово inherited . Когда метод предка полностью совпадает с методом потомка по формату заголовка, то можно использовать более короткую запись. Воспользуемся ей и перепишем деструктор в классе TTextReader правильно:

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

3.7.4. Совместимость объектов различных классов

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

Объектная переменная Reader формально имеет тип TTextReader, а фактически связана с экземпляром класса TDelimitedReader.

Правило совместимости классов чаще всего применяется при передаче объектов в параметрах процедур и функций. Например, если процедура работает с объектом класса TTextReader, то вместо него можно передать объект класса TDelimitedReader или TFixedReader.

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

3.7.5. Контроль и преобразование типов

Поскольку реальный экземпляр объекта может оказаться наследником класса, указанного при описании объектной переменной или параметра, бывает необходимо проверить, к какому классу принадлежит объект на самом деле. Чтобы программист мог выполнять такого рода проверки, каждый объект хранит информацию о своем классе. В языке Delphi существуют операторы is и as , с помощью которых выполняется соответственно проверка на тип (type checking) и преобразование к типу (type casting).

Например, чтобы выяснить, принадлежит ли некоторый объект Obj к классу TTextReader или его наследнику, следует использовать оператор is :

Для преобразования объекта к нужному типу используется оператор as , например

Стоит отметить, что для объектов применим и обычный способ приведения типа:

Вариант с оператором as лучше, поскольку безопасен. Он генерирует ошибку (точнее исключительную ситуацию; об исключительных ситуациях мы расскажем в главе 4) при выполнении программы (run-time error), если реальный экземпляр объекта Obj не совместим с классом TTextReader. Забегая вперед, скажем, что ошибку приведения типа можно обработать и таким образом избежать досрочного завершения программы.

3.8. Виртуальные методы


3.8.1. Понятие виртуального метода

Все методы, которые до сих пор рассматривались, имеют одну общую черту — все они статические . При обращении к статическому методу компилятор точно знает класс, которому данный метод принадлежит. Поэтому, например, обращение к статическому методу ParseLine в методе NextLine (принадлежащем классу TTextReader) компилируется в вызов TTextReader.ParseLine:

В результате метод NextLine работает неправильно в наследниках класса TTextReader, так как внутри него вызов перекрытого метода ParseLine не происходит. Конечно, в классах TDelimitedReader и TFixedReader можно продублировать все методы и свойства, которые прямо или косвенно вызывают ParseLine, но при этом теряются преимущества наследования, и мы возвращаемся к тому, что необходимо описать два класса, в которых большая часть кода идентична. ООП предлагает изящное решение этой проблемы — метод ParseLine всего-навсего объявляется виртуальным :

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

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

Работа виртуальных методов основана на механизме позднего связывания (late binding). В отличие от раннего связывания (early binding), характерного для статических методов, позднее связывание основано на вычислении адреса вызываемого метода при выполнении программы. Адрес метода вычисляется по хранящемуся в каждом объекте описателю класса.

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

3.8.2. Механизм вызова виртуальных методов

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

Все процедурные переменные с адресами виртуальных методов пронумерованы и хранятся в таблице, называемой таблицей виртуальных методов (VMT — от англ. Virtual Method Table). Такая таблица создается одна для каждого класса объектов, и все объекты этого класса хранят на нее ссылку.

Структуру объекта в оперативной памяти поясняет рисунок 3.3:

Рисунок 3.3. Структура объекта TTextReader в оперативной памяти

Вызов виртуального метода осуществляется следующим образом:

  1. Через объектную переменную выполняется обращение к занятому объектом блоку памяти;
  2. Далее из этого блока извлекается адрес таблицы виртуальных методов (он записан в четырех первых байтах);
  3. На основании порядкового номера виртуального метода извлекается адрес соответствующей подпрограммы;
  4. Вызывается код, находящийся по этому адресу.

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

Поддержка механизма вызова виртуальных методов на уровне языка Delphi избавляет программиста от всей этой сложности.

3.8.3. Абстрактные виртуальные методы

При построении иерархии классов часто возникает ситуация, когда работа виртуального метода в базовом классе не известна и наполняется содержанием только в наследниках. Так случилось, например, с методом ParseLine, тело которого в классе TTextReader объявлено пустым. Конечно, тело метода всегда можно сделать пустым или почти пустым (так мы и поступили), но лучше воспользоваться директивой abstract :

Директива abstract записывается после слова virtual и исключает необходимость написания кода виртуального метода для данного класса. Такой метод называется абстрактным , т.е. подразумевает логическое действие, а не конкретный способ его реализации. Абстрактные виртуальные методы часто используются при создании классов-полуфабрикатов. Свою реализацию такие методы получают в законченных наследниках.

3.8.4. Динамические методы

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

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

По смыслу динамические и виртуальные методы идентичны. Различие состоит только в механизме их вызова. Методы, объявленные с директивой virtual , вызываются максимально быстро, но платой за это является большой размер системных таблиц, с помощью которых определяются их адреса. Размер этих таблиц начинает сказываться с увеличением числа классов в иерархии. Методы, объявленные с директивой dynamic вызываются несколько дольше, но при этом таблицы с адресами методов имеют более компактный вид, что способствует экономии памяти. Таким образом, программисту предоставляются два способа оптимизации объектов: по скорости работы ( virtual ) или по объему памяти ( dynamic ).

3.8.5. Методы обработки сообщений

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

Метод обработки сообщений имеет формат процедуры и содержит единственный var -параметр. При перекрытии такого метода название метода и имя параметра могут быть любыми, важно лишь, чтобы неизменным остался номер сообщения, используемый для вызова метода. Вызов метода выполняется не по имени, как обычно, а с помощью обращения к специальному методу Dispatch, который имеется в каждом классе (метод Dispatch определен в классе TObject).

Методы обработки сообщений применяются внутри библиотеки VCL для обработки команд пользовательского интерфейса и редко нужны при написании прикладных программ.

3.9. Классы в программных модулях

Классы очень удобно собирать в модули. При этом их описание помещается в секцию interface , а код методов — в секцию implementation . Создавая модули классов, нужно придерживаться следующих правил:

  • все классы, предназначенные для использования за пределами модуля, следует определять в секции interface ;
  • описание классов, предназначенных для употребления внутри модуля, следует располагать в секции implementation ;
  • если модуль B использует модуль A, то в модуле B можно определять классы, порожденные от классов модуля A.

Соберем рассмотренные ранее классы TTextReader, TDelimitedReader и TFixedReader в отдельный модуль ReadersUnit:

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

3.10. Разграничение доступа к атрибутам объектов

Программист может разграничить доступ к атрибутам своих объектов для других программистов (и себя самого) с помощью специальных ключевых слов: private , protected , public , published (последнее не используется в модуле ReadersUnit).

  • Private . Все, что объявлено в секции private недоступно за пределами модуля. Секция private позволяет скрыть те поля и методы, которые относятся к так называемым особеностям реализации. Например, в этой секции класса TTextReader объявлены поля FFile, FActive и FItems, а также методы PutItem, SetActive, GetItemCount и GetEndOfFile.
  • Public . Поля, методы и свойства, объявленные в секции public не имеют никаких ограничений на использование, т.е. всегда видны за пределами модуля. Все, что помещается в секцию public , служит для манипуляций с объектами и составляет программный интерфейс класса. Например, в классе TTextReader в эту секцию помещены конструктор Create, метод NextLine, свойства Active, Items, ItemCount.
  • Protected . Поля, методы и свойства, объявленные в секции protected , видны за пределами модуля только потомкам данного класса; остальным частям программы они не видны. Так же как и private , директива protected позволяет скрыть особенности реализации класса, но в отличие от нее разрешает другим программистам порождать новые классы и обращаться к полям, методам и свойствам, которые составляют так называемый интерфейс разработчика. В эту секцию обычно помещаются виртуальные методы. Примером такого метода является ParseLine.
  • Published . Устанавливает правила видимости те же, что и директива public . Особенность состоит в том, что для элементов, помещенных в секцию published , компилятор генерирует информацию о типах этих элементов. Эта информация доступна во время выполнения программы, что позволяет превращать объекты в компоненты визуальной среды разработки. Секцию published разрешено использовать только тогда, когда для самого класса или его предка включена директива компилятора $TYPEINFO .

Перечисленные секции могут чередоваться в объявлении класса в произвольном порядке, однако в пределах секции сначала следует описание полей, а потом методов и свойств. Если в определении класса нет ключевых слов private , protected , public и published , то для обычных классов всем полям, методам и свойствам приписывается атрибут видимости public , а для тех классов, которые порождены от классов библиотеки VCL, — атрибут видимости published .

Внутри модуля никакие ограничения на доступ к атрибутам классов, реализованных в этом же модуле, не действуют. Кстати, это отличается от соглашений, принятых в некоторых других языках программирования, в частности в языке C++.

3.11. Указатели на методы объектов

В языке Delphi существуют процедурные типы данных для методов объектов. Внешне объявление процедурного типа для метода отличается от обычного словосочетанием of object , записанным после прототипа процедуры или функции:

Переменная такого типа называется указателем на метод (method pointer). Она занимает в памяти 8 байт и хранит одновременно ссылку на объект и адрес его метода.

Методы объектов, объявленные по приведенному выше шаблону, становятся совместимы по типу со свойством OnReadLine.

Если установить значение свойства OnReadLine:

и переписать метод NextLine,

то объект Form1 через метод HandleLine получит уведомление об очередной считанной строке. Обратите внимание, что вызов метода через указатель происходит лишь в том случае, если указатель не равен nil . Эта проверка выполняется с помощью стандартной функции Assigned, которая возвращает True, если ее аргумент является связанным указателем.

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

3.12. Метаклассы


3.12.1. Ссылки на классы

Язык Delphi позволяет рассматривать классы объектов как своего рода объекты, которыми можно манипулировать в программе. Такая возможность рождает новое понятие — класс класса ; его принято обозначать термином метакласс .

Для поддержки метаклассов введен специальный тип данных — ссылка на класс (class reference). Он описывается с помощью словосочетания class of , например:

Переменная типа TTextReaderClass объявляется в программе обычным образом:

Значениями переменной ClassRef могут быть класс TTextReader и все порожденные от него классы. Допустимы следующие операторы:

По аналогии с тем, как для всех классов существует общий предок TObject, у ссылок на классы существует базовый тип TClass, определенный, как:

Переменная типа TClass может ссылаться на любой класс.

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

Физический смысл и взаимосвязь таких понятий, как переменная-объект, экземпляр объекта в памяти, переменная-класс и экземпляр класса в памяти поясняет рисунок 3.4.

Рисунок 3.4. Переменная-объект, экземпляр объекта в памяти, переменная-класс и экземпляр класса в памяти

3.12.2. Методы классов

Метаклассы привели к возникновению нового типа методов — методов класса. Метод класса оперирует не экземпляром объекта, а непосредственно классом. Он объявляется как обычный метод, но перед словом procedure или function записывается зарезервированное слово class , например:

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

Метод ClassName объявлен в классе TObject и возвращает имя класса, к которому применяется. Очевидно, что надуманный метод GetClassName просто дублирует эту функциональность для класса TTextReader и всех его наследников.

Методы класса применимы и к классам, и к объектам. В обоих случаях в параметре Self передается ссылка на класс объекта. Пример:

Методы классов могут быть виртуальными. Например, в классе TObject определен виртуальный метод класса NewInstance. Он служит для распределения памяти под объект и автоматически вызывается конструктором. Его можно перекрыть в своем классе, чтобы обеспечить нестандартный способ выделения памяти для экземпляров. Метод NewInstance должен перекрываться вместе с другим методом FreeInstance, который автоматически вызывается из деструктора и служит для освобождения памяти. Добавим, что размер памяти, требуемый для экземпляра, можно узнать вызовом предопределенного метода класса InstanceSize.

3.12.3. Виртуальные конструкторы

Особая прелесть ссылок на классы проявляется в сочетании с виртуальными конструкторами. Виртуальный конструктор объявляется с ключевым словом virtual . Вызов виртуального конструктора происходит по фактическому значению ссылки на класс, а не по ее формальному типу. Это позволяет создавать объекты, классы которых неизвестны на этапе компиляции. Механизм виртуальных конструкторов применяется в среде Delphi при восстановлении компонентов формы из файла. Восстановление компонента происходит следующим образом. Из файла считывается имя класса. По этому имени отыскивается ссылка на класс (метакласс). У метакласса вызывается виртуальный конструктор, который создает объект нужного класса.

На этом закончим изучение теории объектно-ориентированного программирования и в качестве практики рассмотрим несколько широко используемых инструментальных классов среды Delphi. Разберитесь с их назначением и работой. Это поможет глубже понять ООП и пригодится на будущее.

3.13. Классы общего назначения

Как показывает практика, в большинстве задач приходится использовать однотипные структуры данных: списки, массивы, множества и т.д. От задачи к задаче изменяются только их элементы, а методы работы сохраняются. Например, для любого списка нужны процедуры вставки и удаления элементов. В связи с этим возникает естественное желание решить задачу «в общем виде», т.е. создать универсальные средства для управления основными структурами данных. Эта идея не нова. Она давно пришла в голову разработчикам инструментальных пакетов, которые быстро наплодили множество вспомогательных библиотек. Эти библиотеки содержали классы объектов для работы со списками, коллекциями (динамические массивы с переменным количеством элементов), словарями (коллекции, индексированные строками) и другими «абстрактными» структурами. Для среды Delphi тоже разработаны аналогичные классы объектов. Их большая часть сосредоточена в модуле Classes. Наиболее нужными для вас являются списки строк (TStrings, TStringList) и потоки (TSream, THandleSream, TFileStream, TMemoryStream и TBlobStream). Рассмотрим кратко их назначение и применение.

3.13.1. Классы для представления списка строк

Для работы со списками строк служат классы TStrings и TStringList. Они используются в библиотеке VCL повсеместно и имеют гораздо большую универсальность, чем та, что можно почерпнуть из их названия. Классы TStrings и TStringList служат для представления не просто списка строк, а списка элементов, каждый из которых представляет собой пару строка-объект. Если со строками не ассоциированы объекты, получается обычный список строк.

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

Свойства класса TStrings описаны ниже.

  • Count : Integer — число элементов в списке.
  • Strings [Index: Integer]: string — обеспечивает доступ к массиву строк по индексу. Первая строка имеет индекс, равный 0. Свойство Strings является основным свойством объекта.
  • Objects [Index: Integer]: TObject — обеспечивает доступ к массиву объектов. Свойства Strings и Objects позволяют использовать объект TStrings как хранилище строк и ассоциированных с ними объектов произвольных классов.
  • Text : string — позволяет интерпретировать список строк, как одну большую строку, в которой элементы разделены символами #13#10 (возврат каретки и перевод строки).

Наследники класса TStrings иногда используются для хранения строк вида Имя=Значение, в частности, строк INI-файлов (см. гл. 6). Для удобной работы с такими строками в классе TStrings дополнительно имеются следующие свойства.

  • Names [Index: Integer]: string — обеспечивает доступ к той части строки, в которой содержится имя.
  • Values [const Name: string]: string — обеспечивает доступ к той части строки, в которой содержится значение. Указывая вместо Name ту часть строки, которая находится слева от знака равенства, вы получаете ту часть, что находится справа.

Управление элементами списка осуществляется с помощью следующих методов:

  • Add (const S: string): Integer — добавляет новую строку S в список и возвращает ее позицию. Новая строка добавляется в конец списка.
  • AddObject (const S: string; AObject: TObject): Integer — добавляет в список строку S и ассоциированный с ней объект AObject. Возвращает индекс пары строка-объект.
  • AddStrings (Strings: TStrings) — добавляет группу строк в существующий список.
  • Append (const S: string) — делает то же, что и Add, но не возвращает значения.
  • Clear — удаляет из списка все элементы.
  • Delete (Index: Integer) — удаляет строку и ассоциированный с ней объект. Метод Delete, также как метод Clear не разрушают объектов, т.е. не вызывают у них деструктор. Об этом вы должны позаботиться сами.
  • Equals (Strings: TStrings): Boolean — Возвращает True, если список строк в точности равен тому, что передан в параметре Strings.
  • Exchange (Index1, Index2: Integer) — меняет два элемента местами.
  • GetText : PChar — возвращает все строки списка в виде одной большой нуль-терминированной строки.
  • IndexOf (const S: string): Integer — возвращает позицию строки S в списке. Если заданная строка в списке отсутствует, функция возвращает значение -1.
  • IndexOfName (const Name: string): Integer — возвращает позицию строки, которая имеет вид Имя=Значение и содержит в себе Имя, равное Name.
  • IndexOfObject (AObject: TObject): Integer — возвращает позицию объекта AObject в массиве Objects. Если заданный объект в списке отсутствует, функция возвращает значение -1.
  • Insert (Index: Integer; const S: string) — вставляет в список строку S в позицию Index.
  • InsertObject (Index: Integer; const S: string; AObject: TObject) — вставляет в список строку S и ассоциированный с ней объект AObject в позицию Index.
  • LoadFromFile (const FileName: string) — загружает строки списка из текстового файла.
  • LoadFromStream (Stream: TStream) — загружает строки списка из потока данных (см. ниже).
  • Move (CurIndex, NewIndex: Integer) — изменяет позицию элемента (пары строка-объект) в списке.
  • SaveToFile (const FileName: string) — сохраняет строки списка в текстовом файле.
  • SaveToStream (Stream: TStream) — сохраняет строки списка в потоке данных.
  • SetText (Text: PChar) — загружает строки списка из одной большой нуль-терминированной строки.

Класс TStringList добавляет к TStrings несколько дополнительных свойств и методов, а также два свойства-события для уведомления об изменениях в списке. Они описаны ниже.

Свойства:


  • Duplicates : TDuplicates — определяет, разрешено ли использовать дублированные строки в списке. Свойство может принимать следующие значения: dupIgnore (дубликаты игнорируются), dupAccept (дубликаты разрешены), dupError (дубликаты запрещены, попытка добавить в список дубликат вызывает ошибку).
  • Sorted : Boolean — если имеет значение True, то строки автоматически сортируются в алфавитном порядке.

Методы:


  • Find (const S: string; var Index: Integer): Boolean — выполняет поиск строки S в списке строк. Если строка найдена, Find помещает ее позицию в переменную, переданную в параметре Index, и возвращает True.
  • Sort — сортирует строки в алфавитном порядке.

События:


  • OnChange : TNotifyEvent — указывает на обработчик события, который выполнится при изменении содержимого списка. Событие OnChange генерируется после того, как были сделаны изменения.
  • OnChanging : TNotifyEvent — указывает на обработчик события, который выполнится при изменении содержимого списка. Событие OnChanging генерируется перед тем, как будут сделаны изменения.

Ниже приводится фрагмент программы, демонстрирующий создание списка строк и манипулирование его элементами:

3.13.2. Классы для представления потока данных

В среде Delphi существует иерархия классов для хранения и последовательного ввода-вывода данных. Классы этой иерархии называются потоками . Потоки лучше всего представлять как файлы. Классы потоков обеспечивают различное физическое представление данных: файл на диске, раздел оперативной памяти, поле в таблице базы данных (таблица 3.1).

Класс Описание
TStream Абстрактный поток, от которого наследуются все остальные. Свойства и методы класса TStream образуют базовый интерфейс потоковых объектов.
THandleStream Поток, который хранит свои данные в файле. Для чтения-записи файла используется дескриптор (handle), поэтому поток называется дескрипторным. Дескриптор — это номер открытого файла в операционной системе. Его возвращают низкоуровневые функции создания и открытия файла.
TFileStream Поток, который хранит свои данные в файле. Отличается от ThandleStream тем, что сам открывает (создает) файл по имени, переданному в конструктор.
TMemoryStream Поток, который хранит свои данные в оперативной памяти. Моделирует работу с файлом. Используется для хранения промежуточных результатов, когда файловый поток не подходит из-за низкой скорости передачи данных.
TResourceStream Поток, обеспечивающий доступ к ресурсам в Windows-приложении.
TBlobStream Обеспечивает последовательный доступ к большим полям таблиц в базах данных.
Таблица 3.1. Классы потоков

Потоки широко применяются в библиотеке VCL и наверняка вам понадобятся. Поэтому ниже кратко перечислены их основные общие свойства и методы.

Общие свойства:


  • Position : Longint — текущая позиция чтения-записи.
  • Size : Longint — текущий размер потока в байтах.

Общие методы:


  • CopyFrom (Source: TStream; Count: Longint): Longint — копирует Count байт из потока Source в свой поток.
  • Read (var Buffer; Count: Longint): Longint — читает Count байт из потока в буфер Buffer, продвигает текущую позицию на Count байт вперед и возвращает число прочитанных байт. Если значение функции меньше значения Count, то в результате чтения был достигнут конец потока.
  • ReadBuffer (var Buffer; Count: Longint) — читает из потока Count байт в буфер Buffer и продвигает текущую позицию на Count байт вперед. Если выполняется попытка чтения за концом потока, то генерируется ошибка.
  • Seek (Offset: Longint; Origin: Word): Longint — продвигает текущую позицию в потоке на Offset байт относительно позиции, заданной параметром Origin. Параметр Origin может иметь одно из следующих значений: 0 — смещение задается относительно начала потока; 1 — смещение задается относительно текущей позиции в потоке; 2 — смещение задается относительно конца потока.
  • Write (const Buffer; Count: Longint): Longint — записывает в поток Count байт из буфера Buffer, продвигает текущую позицию на Count байт вперед и возвращает реально записанное количество байт. Если значение функции отличается от значения Count, то при записи была ошибка.
  • WriteBuffer (const Buffer; Count: Longint) — записывает в поток Count байт из буфера Buffer и продвигает текущую позицию на Count байт вперед. Если по какой-либо причине невозможно записать все байты буфера, то генерируется ошибка.

Ниже приводится фрагмент программы, демонстрирующий создание файлового потока и запись в него строки:

3.14. Итоги

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

Илон Маск рекомендует:  Что такое код mcrypt_get_block_size
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL