СТАТЬЯ 17.08.01

Предыдущая часть

Профессиональная разработка приложений с помощью Delphi5

Часть 1. Основы объектно-ориентированного программирования

Сергей Трепалин,
УКЦ Interface Ltd.
КомпьютерПресс #1 2001

Статья была опубликована в КомпьютерПресс

Область видимости переменных и методов

В любом классе определяется три секции, в которых можно определить переменные и методы: private, protected и public. В секции published, которая важна только для компонентов, можно определять только методы и свойства – переменные определять нельзя (правда, это и не требуется). Разбиение на секции необходимо для того, чтобы скрыть переменные и методы, – то есть чтобы запретить изменения значений переменных или вызов методов в классах-потомках или в экземплярах классов. Согласно правилам любой класс должен быть максимально скрытым и экспонировать только те методы, которые необходимы для изменения его состояния, но не вспомогательные. Что касается переменных, то их экспонирование вообще запрещено – вместо них используют свойства.

Рассмотрим следующий модельный класс:

TNameSave=class(TComponent)
private
 FData1:string;
 procedure CheckData1;
protected
 FData2:string;
 procedure CheckData2;
public
 FData3:string;
 procedure CheckData3;
end;

Описанное ниже поведение различных секций кода класса TNameSave относится к тому случаю, когда данный класс определен в отдельном модуле. Предположим, был создан экземпляр класса TNameSave. Используя ссылку на рабочую копию класса, можно изменить или прочитать значение переменной FData3 и вызвать метод CheckData3. При попытке же обратиться к переменным FData1, FData2 или вызвать методы CheckData1, CheckData2 компиляция завершается с диагностическим сообщением об отсутствии данной переменной или метода:

procedure TForm1.Button1Click(Sender: TObject);
var
 FC:TNameSave;
begin
 FC:=TNameSave.Create(Self);
 FC.FData3:='Test'; {Legal}
 FC.CheckData3; {Legal}
 FC.FData2:='12'; {Compile error}
 FC.CheckData1; {Compile error}
 FC.Free;
end;

Таким образом, переменные и методы, определенные в секции Private и Protected, скрыты для использования из экземпляра класса. Чтобы понять различие между ними, создадим новый класс – потомок класса TNameSave:

TSecondName=class(TNameSave)
private
 procedure DoSomething;
end;

В этом классе определим новый метод DoSomething, а в реализации этого метода попытаемся обратиться к переменным и методам класса TNameSave:

procedure TSecondName.DoSomething;
begin
 FData3:='AA'; {Legal}
 FData2:='BB'; {Legal}
 FData1:='CC'; {Compile error}
 CheckData3; {Legal}
 CheckData2; {Legal}
 CheckData1; {Compile error}
end;

В этом случае уже можно обратиться к методам и переменным, определенным в секции protected (так же, как и в public), но по-прежнему невозможно обратиться к методам, определенным в секции private, которые являются полностью закрытой. Поэтому в этой секции не имеет смысла определять виртуальные или динамические методы. В этом случае для выполнения приложения потребуется больше ресурсов (или код будет выполняться дольше), а переписать методы директивой override невозможно, поскольку они не видны в классах-потомках.

В C++ имеется такое понятие, как дружественный класс (friend class). Можно один класс объявить дружественным другому и после этого использовать переменные в секции private или вызывать методы из этой секции. В Delphi также существует понятие дружественных классов, однако синтаксис объявления классов дружественными отсутствует. Классы считаются дружественными, если они объявлены в одном и том же модуле (unit). Таким образом, если для примера выше класс TSecondName объявить в том же самом модуле, что и класс TNameSave, то можно вызывать все его методы и обращаться к переменным, даже определенным в секции private. Более того, если в этом же модуле будет создан экземпляр класса TNameSave, то, используя ссылку на него, можно также обращаться ко всем переменным и методам:

procedure TForm1.Button1Click(Sender: TObject);
var
 FC:TNameSave;
begin
 FC:=TNameSave.Create(Self);
 FC.FData3:='Test'; {Legal}
 FC.CheckData3; {Legal}
 FC.FData2:='12'; {Now Legal!}
 FC.CheckData1; {Now Legal!}
 FC.Free;
end;

Наличие дружественных классов, а также возможность доступа в классах-потомках к методам и переменным секции protected можно использовать в целях получения доступа к секции protected из экземпляра класса в произвольном модуле. В качестве примера рассмотрим часто встречающуюся задачу: получены (например, посредством автоматизации) какие-то данные, на которые в адресном пространстве приложения имеется указатель и размер которых известен. Необходимо считать эти данные в приложение через поток. Тривиальное решение таково: создается экземпляр класса TMemoryStream, вызывается его метод Write, данные помещаются в поток. Далее указатель текущей позиции устанавливается на начало потока, и происходит считывание данных из него. У такого решения имеются два недостатка: тратятся системные ресурсы на создание второй копии данных и время на их копирование. С другой стороны, TMemoryStream имеет метод SetPointer, который позволяет просто объявить данный указатель началом потока без создания его копии в памяти. Но этот метод вызвать нельзя – он содержится в секции protected! Поэтому мы поступаем следующим образом:

type
 TMyStream=class(TMemoryStream)
 end;
 procedure ReadPointer(PData:pointer; Size:integer);
var
 Stream:TMyStream;
begin
 Stream:=TMyStream.Create;
 Stream.SetPointer(PData,Size);
 {... reading from stream ...}
 Stream.SetPointer(nil,0);
 Stream.Free;
end;

Иными словами, мы просто объявляем новый класс – и все! А поскольку класс для данного модуля считается дружественным, в экземпляре класса можно вызвать protected-метод SetPointer. Единственный нюанс: перед вызовом деструктора необходимо установить указатель равным nil, иначе деструктор TMemoryStream попытается освободить память и, если память была выделена не в данном модуле, произойдет исключение.

Свойства

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

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

TPropClass=class(TObject)
private
 FData:integer;
public
 property Data:integer read FData write FData;
end;

В данном классе объявлено свойство Data типа integer. При чтении свойства возвращается содержимое переменной FData, а при записи этой переменной присваивается новое значение. Переменная FData должна быть того же типа, что и свойство. Чтение и запись свойств в Delphi осуществляется посредством оператора присваивания (:=) с использованием ссылки на экземпляр класса:

procedure TForm1.Button1Click(Sender: TObject);
var
 PC:TPropClass;
 N:integer;
begin
 PC:=TPropClass.Create;
 PC.Data:=5; {Property writing}
 N:=PC.Data; {Property reading}
 PC.Free;
end;

Свойство присваивается как обычная переменная. Однако в отличие от обычной переменной свойство не может быть послано в качестве фактического параметра метода, если его формальный параметр имеет модификатор var.

Имя свойства может быть любым, но по соглашению оно совпадает с именем переменной без буквы F в начале слова. Типы свойств могут быть любые – как стандартные (integer, string и др.), в том числе и классовые типы, так и определенные ранее программистом (TMyListBox…). В классе TPropClass свойство Data имеет доступ как для чтения (служебное слово read после указания типа), так и для записи (служебное слово write). Также свойство может быть только для чтения (property Data:integer read FData) или только для записи (property Data:integer write FData). При попытке присвоить какое-либо значение свойству только для чтения или при попытке считать значение свойства только для записи возникает ошибка при компиляции проекта. Не бывает свойства, которое не будет доступно ни для чтения, ни для записи: конструкцию типа property Dummy, string не пропустит компилятор. Однако немного похожая конструкция property Dummy (без указания типа) вполне легальна (ее использование будет обсуждено ниже).

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

TPropClass=class(TObject)
private
 FData:integer;
 function GetData:integer;
 procedure SetData(Value:integer);
public
 property Data:integer read GetData write SetData;
end;

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

procedure TPropClass.SetData(Value:integer);
begin
 if (Value>0) and (Value<100) then FData:=Value 
    else begin
 beep;
 ShowMessage(‘Illegal value’);
 end;
end;

Название функции для считывания значения свойства GetData:integer может быть произвольным, но по соглашению оно должно совпадать с именем свойства с приставкой Get перед ним. Возвращаемый результат обязан совпадать с типом свойства (в данном случае – integer). В процедуре SetData (Value:integer) название также не играет роли, но по соглашению должно совпадать с названием свойства с добавлением приставки Set перед ним. Название параметра (Value) может быть любым, но обычно используют Value. А вот тип параметра обязан совпадать с типом свойства. Методы для чтения/записи свойства могут быть статическими и виртуальными, но не динамическими. И наконец, свойство может быть смешанным. Это означает, что при чтении свойства вызывается метод, а при записи обращаются к переменной и наоборот:

property Data:integer read GetData write FData;
property Data:integer read FData write SetData;

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

TArrayClass=class(TObject)
 private
 procedure SetFirstName(Index:integer; 
    Value:string);
 function GetFirstName(Index:integer):string;
 public
 property FirstName[NIndex:integer]:string 
    read GetFirstName write SetFirstName;
 end;

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

Нетрудно объявить и двухмерный массив:

TArrayClass=class(TObject)
 private
 procedure SetFirstName(Index1,Index2:integer; 
    Value:string);
 function GetFirstName(Index1,Index2:integer):string;
 public
 property FirstName[I1,I2:integer]:string 
    read GetFirstName write SetFirstName;
 end;

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

К свойствам-массивам обращаются как к обычному массиву после создания экземпляра класса:

procedure TForm1.Button1Click(Sender: TObject);
var
 AC:TArrayClass;
 S:string;
begin
 AC:=TArrayClass.Create;
 AC.FirstName[1,2]:='Test';
 S:=AC.FirstName[5,1];
 AC.Free;
end;

Теперь о типе индексов массивов. В Object Pascal в качестве индексов используются переменные ординального (ordinal) типа (то есть переменные, содержащие данные, которые можно упорядочить и пронумеровать). Разрешается объявлять массивы:

var
 N:array [1..5] of integer;
 K:array ['A'..'Z'] of string;
 M:array [mrOK..mrNo] of boolean; {Modal result enumeration}

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

var
 N:array [1.4..3.14] of integer; {Floating-point index}
 K:array ['AZ'..'ZA'] of string; {String index}

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

TDistanceClass=class(TObject)
 private
 function GetDistance(Index:double):string;
 public
 property Distance[Index:double]:string 
    read GetDistance;
end;
function TDistanceClass.GetDistance(Index:double):string;
begin
 if Index<0 then Result:='Illegal' else
 if Index<1 then Result:='Short' else
 if Index<3.24 then Result:='Medium' else Result:='Long';
end;

и при создании экземпляра класса использовать индексы – действительные числа:

procedure TForm1.Button2Click(Sender: TObject);
var
 DC:TDistanceClass;
begin
 DC:=TDistanceClass.Create;
 Caption:=DC.Distance[-1.345]+' '+DC.Distance[Pi];
 DC.Free;
end;

Формально это выглядит как обращение к массиву, индексированному действительными числами!

Со свойствами-массивами тесно связано одно из двух значений служебного слова default. Если свойство-массив определить со служебным словом default (для примера выше):

property Distance[Index:double]:string read GetDistance; default;

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

Caption:=DC[-1.345]+' '+DC[Pi];

Обратите внимание, что идентификатор distance отсутствует после идентификатора экземпляра класса (DC). Если имеется несколько свойств-массивов, то служебное слово default можно использовать только с одним из них, в противном случае во время компиляции произойдет ошибка.

Второе значение служебного слова default – информирование компилятора Delphi о необходимости запоминания текущего (указанного в инспекторе объектов) свойства в поток ресурсов. Эта директива работает только с ординарными типами данных и используется следующим образом:

property Age:integer read FAge write FAge default 30;

Если значение свойства, установленное в инспекторе объектов, отличается от 30 и отсутствует директива stored (см ниже) или ее значение равно True, то это значение будет сохранено в потоке ресурсов. Если для свойства не указывается директива default, то берется соответствующее значение для класса-предка. Если директива вообще отсутствует, то любое значение свойства будет запоминаться в потоке ресурсов. Начинающие программисты часто пытаются при помощи этой директивы установить значения свойств по умолчанию, то есть немедленно после выполнения конструктора класса. Из вышесказанного ясно, что желаемый результат в этом случае достигнут не будет. Для изменения свойств по умолчанию необходимо в явном виде присваивать им значения в конструкторе. Как видно, данное значение директивы default настолько отличается от ее использования с массивами, что было бы разумно сделать два различающихся служебных слова, чтобы не путать их действия.

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

И наконец, последняя директива, используемая со свойствами, – stored. Она используется следующим образом:

property Age:integer read FAge write FAge stored False;

После директивы stored указывается либо True, либо False, либо идентификатор логической переменной в данном классе. Если значение этой переменной равно True, то будет ли запоминаться свойство в ресурсах или нет регулируется директивой default. Если же значение этой переменной равно False, то значение свойства не будет запоминаться в ресурсах, каким бы мы его не назначили в инспекторе объектов.

Свойства можно определять в любой из секций (private, protected, public и published). Компилятор Delphi разрешает определять свойства в секции private, но это не имеет смысла. Свойство в секции private невозможно ни использовать, ни переопределить. Далее, если свойство объявлено в секции published, то для компонента оно показывается в инспекторе объектов (если оно имеет доступ read и write). Секция public – стандартное место объявления свойств для классов, не являющихся компонентами. Для компонентов в секции public объявляются так называемые run-time properties – свойства, к которым можно обращаться только во время выполнения приложения. И наконец, в секции protected свойства объявляются для переопределения в классах-потомках.

Переопределение (редекларация) свойств означает повторное объявление свойства в классе-потомке. Редекларация используется в целях:

  1. Изменения способа чтения или записи свойства. Если в классе-предке при чтении свойство ссылалось на переменную, то в классе-потомке можно определить метод.
  2. Изменения значение default (для параметра) или в случае необходимости отмены ранее определенной директивы default либо изменения директивы stored. Все эти изменения влияют на способ запоминания свойств в ресурсах.
  3. Экспонирования свойства компонента в инспекторе объектов. Если свойство ранее не демонстрировалось в инспекторе объектов, то его можно экспонировать в классе-потомке простым переопределением в секции published.

Сказанное выше можно проиллюстрировать следующим примером:

TInitialClass=class(TComponent)
 private
 FData1,FData2,FData3,FData4,FData5:integer;
 protected
 property Data1:integer read 
    FData1 write FData1;
 property Data2:integer read 
    FData2 write FData2 default 20;
 property Data3:integer read 
    FData3 write FData3 default 20;
 property Data4:integer read 
    FData4 write FData4 stored True;
 property Data5:integer read 
    FData5 write FData5;
 end;
 TRedeclaredClass=class(TInitialClass)
 private
 procedure SetData1(Value:integer);
 protected
 property Data1 write SetData1; 
    {Using method instead of variable}
 property Data2 default 30; 
    {Changing default value}
 property Data3 nodefault; {Reject 
    early defined Default}
 property Data4 stored False; 
    {Not store in stream now}
 published
 property Data5; {The property 
    will be exposured in the Object Inspector}
 end;

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

Продолжение статьи

Дополнительную информацию Вы можете получить в компании Interface Ltd.

Отправить ссылку на страницу по e-mail
Обсудить на форуме Inprise/Borland


Interface Ltd.
Тel/Fax: +7(095) 105-0049 (многоканальный)
Отправить E-Mail
http://www.interface.ru
Ваши замечания и предложения отправляйте автору
По техническим вопросам обращайтесь к вебмастеру
Документ опубликован: 17.08.01