В предыдущем параграфе обсуждались преимущества, которые дают интерфейсные методы класса. Однако с точки зрения пользователя класса применение методов для доступа к полям имеет небольшой недостаток – громоздкость вызова. Развитие концепций ООП привело к появлению понятия свойства (property). Свойство – это элемент класса, работа с которым происходит так же, как с полем объекта. Разница между полем и свойством заключается в следующем: обращение к свойству компилятор транслирует в обращение к полю или в вызов метода, следовательно, при работе со свойствами могут выполняться некоторые действия.
В Object Pascal для объявления свойства используется ключевое слово property. Далее следует имя свойства и указывается его тип. После директивы read указывается имя поля или метода для чтения свойства. Аналогично, после директивы write указывается имя поля или метода для записи свойства. Считается, что свойство записывается, когда ему присваивается некое значение, иначе свойство читается. Тип полей, употребляемых после read и write, должен совпадать с типом свойства. Метод, используемый для чтения простого свойства, должен быть функцией без параметров, тип возвращаемого значения которой совпадает с типом свойства. Метод для записи – процедура с одним параметром, имеющим тип свойства. Принято соглашение, согласно которому имена методов чтения свойств начинаются с префикса Get, а имена методов записи – с префикса Set. Объявление свойства может следовать только после объявления полей и методов, которые используются свойством.
Добавим свойства в класс TPerson:
type TPerson = class
private
fName: string;
fAge: Integer;
public
procedure SetAge(Age: Integer);
function GetAge: Integer;
procedure SetName(Name: string);
function SayName: string;
property Age: Integer read GetAge write SetAge;
property Name: string read SayName write SetName;
end;
Работу со свойствами демонстрирует следующий фрагмент кода:
var Man: TPerson;
. . .
Man.Name := 'Alex'; // транслируется в Man.SetName('Alex')
Man.Age := 101; // транслируется в Man.SetAge(101)
Подчеркнём, что свойства класса призваны облегчить работу с объектом для пользователя. Фактически, свойства «живут» только до компиляции программы, во время которой заменяются методами или полями. В отличие от полей свойства не занимают места в памяти. Это накладывает определённые ограничения на их использование. Свойства нельзя передавать в качестве var-параметров в подпрограммы, к ним нельзя применять операцию взятия адреса.
Употребление свойств позволяет «сэкономить» на методах класса. Например, в классе TPerson методы GetAge и SetName введены только для того, чтобы обеспечить полный доступ к полям, никаких особых действий они не выполняют. Заменим в определении свойств эти методы полями fAge и fName. В свою очередь, методы, обслуживающие свойства, поместим в секцию private, так как именно свойства теперь будут составлять интерфейс класса:
type TPerson = class
private
fName: string;
fAge: Integer;
procedure SetAge(Age: Integer);
function SayName: string;
public
property Age: Integer read fAge write SetAge;
property Name: string read SayName write fName;
end;
Разберём некоторые нюансы описания и использования свойств. Если опустить директиву read, можно получить свойство только для записи. Если опустить write, получим свойство, значение которого можно читать, но не записывать. Однако какая-то из директив должна в объявлении свойства присутствовать.
Рассмотрим пример класса TArray. Он будет представлять массив вещественных чисел, в котором кроме самих чисел хранится количество элементов, а также имеется свойство для получения максимального элемента. Начальный вариант такого класса может выглядеть так:
type TArray = class
private
fData: array[1..1000] of double; // массив «с запасом»
function TArray.ReadElement(Ind: Integer): double;
begin
if (Ind > 0) and (Ind <= fLength)
then Result := fData[Ind]
else Result := 0
end;
function TArray.FindMax: double;
var i: Integer;
begin
Result := fData[1];
for i := 2 to fLength do
if fData[i] > Result then Result := fData[i]
end;
. . .
var Mas: TArray;
. . .
Mas.WriteElement(1, 10);
Mas.WriteElement(2, 100);
Mas.WriteElement(10, 1);
k := Mas.Max; // k = 100
l := Mas.Length; // l = 10
Обратите внимание: свойства Length и Max являются свойствами только для чтения. Более того, свойство Max вообще можно считать «виртуальным», так как оно не связано ни с одним полем класса (но пользователь класса этого не заметит).
Для пользователя класса TArray естественным желанием является получить более удобный способ доступа к данным в fData. Object Pascal разрешает объявлять свойства-массивы. Подобные свойства используются в основном в классах, данные которых подразумевают доступ с использованием различных индексаторов. Добавим свойство-массив в класс TArray (и перенесём методы WriteElement и ReadElement в секцию private):
Для объявления свойства-массива после имени свойства в квадратных скобках указывается имя и тип индекса. Как и для индексированных свойств, для доступа к свойствам-массивам возможно только использование методов. Первый параметр этих методов должен совпадать по типу с индексом свойства. Ниже приведён пример работы со свойством Data, и указано, во что транслируются обращения к нему:
for i := 1 to 5 do
Mas.Data[i] := 1000; // транслируется в Mas.WriteElement(i, 1000)
Тип индекса свойства-массива не ограничен диапазоном (индекс может быть любого типа – строка, вещественное число, класс). Допускается работа только с элементами свойства-массива, а не со всем свойством целиком.
Свойства-массивы могут быть многомерными. В этом случае количество необходимых параметров у методов чтения и записи увеличивается на соответствующее число.
Если при описании свойства-массива добавить в конце директиву default, то такое свойство становится основным свойством класса. Для основного свойства можно указывать индекс непосредственно после имени объекта, не используя идентификатор свойства. Сделаем свойство Data основным: