На этом шаге мы рассмотрим правила записи и использования простых свойств.
Будем изучать свойства по мере усложнения синтаксиса их описания. Одно из наиболее простых корректно работающих свойств представлено в следующем примере.
unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) Memo1: TMemo; procedure FormCreate(Sender: TObject); private { Private declarations } public { Public declarations } end; TVerySimpleProperty = class FField : Integer; property VerySimpleProperty : Integer read FField write FField; end; var Form1: TForm1; MyVar : TVerySimpleProperty; St : String; implementation {$R *.dfm} procedure TForm1.FormCreate(Sender: TObject); begin MyVar := TVerySimpleProperty.Create; MyVar.VerySimpleProperty := 10; Str (MyVar.VerySimpleProperty, St); Form1.Memo1.Lines.Add(St); MyVar.Free; end; end.
Результат:
Рис.1. Результат работы приложения
В этом примере выполняются только элементарные действия:
Как отмечалось выше, свойство служит для решения двух задач: обеспечения дополнительного уровня защищенности полей объектов и обеспечения доступности свойств на этапе проектирования через Object Inspector. Простейшие свойства, в таком виде, как они описаны в примере, ни первую, ни вторую задачу фактически не решают, поскольку получается только подмена одного идентификатора (FField) другим (VerySimpleProperty).
Поэтому логично будет те поля классов, обращение к которым сделано с помощью свойств, включать в приватную (private) или защищенную (protected) часть класса, а само свойство - в часть общедоступных (public) или опубликованных (published) описаний.
Причем свойство описывается общедоступным, если преследуется цель решения первой задачи, а опубликованным, - если обеих. Именно так делается во всех стандартных классах Object Pascal. Но, само собой разумеется, что "это - не догма, а руководство к действию". Таким образом, стандартный вид описания простых свойств будет следующим:
type TVerySimpleProperties = class private FField1 : Integer; { Поле первого свойства } FField2 : Integer; { Поле второго свойства } protected FField3 : Integer; { Поле третьего свойства } public { Первое свойство } property VerySimpleProperty1 : Integer read FField1 write FField1; published { Второе свойство } property VerySimpleProperty2 : Integer read FField2 write FField2; { Третье свойство } property VerySimpleProperty3 : Integer read FField3 write FField3; end;
Поскольку при этих описаниях получается, что для обеспечения инкапсуляции свойство используется вместо методов. Однако в таком виде свойство поддерживает "неинтеллектуальную" работу с полем - только прямое чтение и прямую запись. Для обеспечения гибкого доступа к полю по чтению и записи в синтаксисе свойства предусмотрена возможность указания имен методов после директив read и write, которые в рамках свойств называются спецификаторами доступа (access specifiers).
В результате, соединение возможностей таких языковых конструкций, как свойства, директивы области видимости (private, protected, public и published), и методы в качестве спецификаторов read и write, позволяет достичь гибкого и надежного механизма скрытия информации в рамках класса.
Таким образом, структура хорошо реализованного свойства приобретает вид, показанный в следующем примере:
unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) Memo1: TMemo; procedure FormCreate(Sender: TObject); private { Private declarations } public { Public declarations } end; TSimpleProperty2 = class private FField : Integer; { Поле свойства } protected { Заголовок метода для записи значения свойства } procedure SetSimpleProperty2 (Value : Integer); { Заголовок метода для чтения значения свойства } function GetSimpleProperty2 : Integer; public { Описание свойства } property SimpleProperty2 : Integer read GetSimpleProperty2 { Указание метода для чтения } write SetSimpleProperty2; { Указание метода для записи } end; var Form1: TForm1; MyVar : TSimpleProperty2; St : String; implementation {$R *.dfm} procedure TSimpleProperty2.SetSimpleProperty2; begin FField := Value end; function TSimpleProperty2.GetSimpleProperty2; begin Result := FField end; procedure TForm1.FormCreate(Sender: TObject); begin MyVar := TSimpleProperty2.Create; MyVar.SimpleProperty2 := 10; {!!! Обращение к свойству остается таким же, как и в } { случае простейшего свойства !!!} Str (MyVar.SimpleProperty2, St); Form1.Memo1.Lines.Add(St); MyVar.Free; end; end.
Рассматривая этот пример, сделаем несколько замечаний.
Хотя способ доступа к полю свойства сделан уже не прямо по идентификатору, а с помощью методов, работа со свойством остается такой же, как и раньше! Здесь проявляется одна важная особенность работы компилятора.
При обработке свойств компилятор автоматически подставляет команды вызова требуемых методов чтения и записи в те точки кода программы, где стоит идентификатор свойства.
Сказанное, однако, не означает, что вызывать методы чтения и записи свойства по идентификаторам методов запрещено. Например, оператор установки значения свойства в последнем примере может быть, с равным успехом, записан так:
MyVar.SetSimpleProperty2 (10);
Выбор способа обращения к свойству остается прерогативой программиста, хотя в выражениях использование идентификатора свойства, а не идентификаторов методов, выглядит лучше.
Заметим также, что поскольку "интеллектуальность" требуется в основном только для установки значения свойства, то широко применяется нижепоказанная форма описания свойства:
TSimpleProperty2 = class private FField : Integer; { Поле свойства } protected { Заголовок метода для записи значения свойства } procedure SetSimpleProperty2 (Value : Integer); public { Описание свойства } property SimpleProperty2 : Integer read FField {Прямое обращение к полю для чтения} write SetSimpleProperty2; {Указание метода для записи} end;
То есть метод описывается только для записи, а чтение выполняется прямым обращением к полю.
Еще одно замечание касается случая, если описание одного из спецификаторов доступа read или write опущено.
Если в свойстве спецификатор write опущен, то свойство становится доступным только по чтению, если опущено описание спецификатора read, то свойство будет доступным только по записи.
Теперь остановимся на одной особенности свойств, которая зачастую вводит в заблуждение начинающих изучать Object Pascal. Эта особенность состоит в "обманчивости" стандартной директивы default.
В рамках описания свойства слово default может нести два принципиально различных смысла, в зависимости от контекста и вида объявляемого свойства.
При описании простых свойств слово default является одним из спецификаторов сохранения, отделяется от предыдущих описаний пробелом, после него обязательно указывается значение соответствующего типа. Но к значению свойства по умолчанию, которое устанавливается при инициализации объекта, значение спецификатора default никакого отношения не имеет.
При описании свойств-массивов слово default является директивой умолчания, отделяется от предыдущих описаний точкой с запятой, после него значение никогда не указывается, а означает оно, что идентификатор данного свойства-массива при обращениях к свойству можно не записывать - он будет взят по умолчанию. Более подробно свойства-массивы рассматриваются в шаге 136.
Почему разработчики языка допустили в рамках одной синтаксической конструкции такую семантическую неоднозначность остается загадкой.
Приведем пример, подтверждающий тот факт, что слово default значения по умолчанию для простых свойств не устанавливает.
unit Unit1; { Тест, демонстрирующий отсутствие влияния спецификатора } { default на значение простого свойства, устанавливаемое } { по умолчанию при инициализации. } interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) Memo1: TMemo; procedure FormCreate(Sender: TObject); private { Private declarations } public { Public declarations } end; TDefaultIntProperty = class private FInt : Integer; procedure SetDefaultIntProperty (Value : Integer); public property DefaultIntProperty : Integer read FInt write SetDefaultIntProperty default 10; { default определяет значение 10, } { но поле Fint будет иметь значение 0 ! } end; TDefaultBoolProperty = class private FBool : Boolean; procedure SetDefaultBoolProperty (Value : Boolean); public property DefaultBoolProperty : Boolean read FBool write SetDefaultBoolProperty default True; { default определяет значение True, } { но поле FBool будет иметь значение False ! } end; var Form1: TForm1; MyIntVar : TDefaultIntProperty; MyBoolVar : TDefaultBoolProperty; St : String; implementation {$R *.dfm} procedure TDefaultIntProperty.SetDefaultIntProperty; begin FInt := Value end; procedure TDefaultBoolProperty.SetDefaultBoolProperty; begin FBool := Value end; procedure TForm1.FormCreate(Sender: TObject); begin MyIntVar := TDefaultIntProperty.Create; Str (MyIntVar.DefaultIntProperty, St); Form1.Memo1.Lines.Add(St); MyIntVar.Free; MyBoolVar := TDefaultBoolProperty.Create; If MyBoolVar.DefaultBoolProperty then Form1.Memo1.Lines.Add( 'True' ) else Form1.Memo1.Lines.Add( 'False' ); MyBoolVar.Free; end; end.
Рис.2. Результат работы приложения
Как видно из данного примера, несмотря на то, что спецификатор default устанавливает для свойства типа Integer значение 10, а для свойства типа Boolean - значение True, при выполнении проекта на экран выводятся 0 и False, то есть значения, полученные свойствами при создании и инициализации объектов MyIntVar и MyBoolVar. Дело в том, что default в рамках описания простого свойства является одним из, так называемых, спецификаторов сохранения (storage specifiers), к которым принадлежат также stored и nodefault. Эти спецификаторы могут быть описаны только для свойств, не являющихся составными структурами данных, и используются библиотекой VCL для управления автоматическим сохранением значений опубликованных свойств в файле формы.
Основным спецификатором сохранения является stored, после которого должно быть указано только булевское значение, представление константой, переменной, полем типа Boolean или именем не имеющего параметров метода-функции, возвращающего значение булевского типа.
Рассмотрим смысл спецификаторов сохранения. Когда происходит сохранение в файле формы состояния компонентов, то просматриваются все опубликованные свойства. Если такое свойство не имеет спецификатора stored или имеет его, а значение булевской величины, стоящей после него, равно False, то свойство не сохраняется. Если же результат равен True, то текущее значение свойства сравнивается со значением, установленным по умолчанию спецификатором default. Если эти значения равны, то свойство не сохраняется, а если не равны или default отсутствует, то сохраняется. Спецификатор nodefault имеет чисто декоративный смысл и обозначает отсутствие спецификатора умолчания default.
Пример корректного описания спецификаторов сохранения:
TDefaultIntProperty = class private FInt : Integer; procedure SetDefaultlntProperty (Value : Integer); function PropertyStored : Boolean; published property DefaultIntProperty : Integer read FInt write SetDefaultIntProperty stored PropertyStored default 10; end; implementation procedure TDefaultIntProperty.SetDefaultIntProperty; begin FInt := Value end; function TDefaultIntProperty.PropertyStored; begin { В общем случае здесь может располагаться любой код } Result := True end;
На следующем шаге мы рассмотрим переопределение свойств.