Версия текста: 1.0
Введение
Общие правила
Перегрузка унарных операторов
Перегрузка бинарных операторов
Перегрузка операторов сравнения
Перегрузка операторов приведения типа
Расширенные возможности
Создание копирующего конструктора
Перегрузка оператора присваивания
Заключение
Введение
Прочитав название, Вы не ошиблись – в восьмой версии Delphi (Delphi.NET)
теперь действительно стала возможна перегрузка операторов. С выходом Delphi.NET
в языке Object Pascal появилось много языковых изменений, самых значительных
начиная с версии 1.0. Для того чтобы рассмотреть их все, понадобилась бы
отдельная книга, поэтому в данной статье мы рассмотрим только одно, на мой
взгляд, самое интересное расширение языка – перегрузку операторов.
Возможность автоматического замещения операторов в исходном тексте программы
пользовательскими функциями давно знакома программистам пишущим на языках C++ и
C#. Теперь эта замечательная возможность доступна и Delphi-программистам.
Общие правила
В Delphi.NET, в отличие от С-подобных языков, таких как C++ и C#, для того,
чтобы перегрузить оператор, нужно реализовать функцию с определенной сигнатурой
(а не с символом оператора!) – то есть реализовать функцию с определенным
именем, числом и типами параметров. В Таблице 1 приведен список
операторов, для которых допускается переопределение, и сигнатуры функций,
которые для этого нужно реализовать.
Символ оператора |
Сигнатура метода |
Категория |
Неявное преобразование |
Implicit(a : type) : resultType; |
Приведение |
Явное преобразование |
Explicit(a: type) : resultType; |
Приведение |
- |
Negative(a: type) : resultType; |
Унарный |
+ |
Positive(a: type): resultType; |
Унарный |
Inc |
Inc(a: type) : resultType; |
Унарный |
Dec |
Dec(a: type): resultType |
Унарный |
not |
LogicalNot(a: type): resultType; |
Унарный |
not |
BitwiseNot(a: type): resultType; |
Унарный |
Trunc |
Trunc(a: type): resultType; |
Унарный |
Round |
Round(a: type): resultType; |
Унарный |
= |
Equal(a: type; b: type) : Boolean; |
Сравнение |
<> |
NotEqual(a: type; b: type): Boolean; |
Сравнение |
> |
GreaterThan(a: type; b: type) Boolean; |
Сравнение |
>= |
GreaterThanOrEqual(a: type; b: type): resultType; |
Сравнение |
< |
LessThan(a: type; b: type): resultType; |
Сравнение |
<= |
LessThanOrEqual(a: type; b: type): resultType; |
Сравнение |
+ |
Add(a: type; b: type): resultType; |
Бинарный |
- |
Subtract(a: type; b: type) : resultType; |
Бинарный |
* |
Multiply(a: type; b: type) : resultType; |
Бинарный |
/ |
Divide(a: type; b: type) : resultType; |
Бинарный |
div |
IntDivide(a: type; b: type): resultType; |
Бинарный |
Mod |
Modulus(a: type; b: type): resultType; |
Бинарный |
shl |
ShiftLeft(a: type; b: type): resultType; |
Бинарный |
shr |
ShiftRight(a: type; b: type): resultType; |
Бинарный |
and |
LogicalAnd(a: type; b: type): resultType; |
Бинарный |
Or |
LogicalOr(a: type; b: type): resultType; |
Бинарный |
xor |
LogicalXor(a: type; b: type): resultType; |
Бинарный |
and |
BitwiseAnd(a: type; b: type): resultType; |
Бинарный |
Or |
BitwiseOr(a: type; b: type): resultType; |
Бинарный |
xor |
BitwiseXor(a: type; b: type): resultType; |
Бинарный |
Таблица
1.Список операторов, для которых в Delphi.NET возможна перегрузка
Интересно отметить, что такие часто используемые функции, как Round и
Trunc являются в Delphi.NET операторами, то есть по форме вызова ничем не
отличаются от функций, но могут быть перегружены.
Для того чтобы переопределить оператор для разработанного Вами класса,
необходимо объявить, и затем реализовать, метод класса (class method) с
определенной сигнатурой (колонка 2 Таблицы 1). При объявлении и
реализации данного метода необходимо указывать ключевое слово operator.
Пример
type
TMyClass = class
{Оператор арифметического сложения двух объектов типа TMyClass}
class operator Add(a, b: TMyClass): TMyClass;
end;
class operator TMyClass.Add(a, b: TMyClass): TMyClass;
begin
{Алгоритм сложения:}
end;
После этого в тексте программы возможно использование следующего кода:
var
v,v1,v2 : TMyClass;
begin
v1 := TMyClass.Create;
v2 := TMyClass.Create;
{ манипуляции с переменными v1 и v2}
v := v1 + v2; //вместо "+" компилятор подставляет вызов функции TMyClass.Add
end;
Далее мы более подробно рассмотрим перегрузку для различных групп операторов
Перегрузка унарных операторов
Унарные операторы получают на входе один аргумент, а в качестве результата
возвращают экземпляр определенного типа. В общем случае тип входного параметра и
тип результата оператора могут совпадать.
Рассмотрим пример подобного унарного оператора: предположим у нас есть тип:
расширенный список строк в виде класса TMyStringList наследованный от
класса TStringList библиотеки VCL:
type
TMyStringList = class(TStringList)
//Объявление дополнительных полей и методов ...
end;
Для данного класса мы хотим определить оператор арифметического отрицания
'-'. Пусть данный оператор будет производить переупорядочивание элементов списка
в обратном порядке: то есть первый элемент становится последним, второй -
предпоследним и т.д. В качестве результата данный оператор должен возвращать
новый результирующий список.
Для реализации такого оператора нам необходимо объявить следующий метод
класса:
class operator Negative(a: TMyStringList) : TMyStringList;
Для демонстрации этого примера создадим небольшое консольное приложение.
Полный исходный код будет выглядеть следующим образом:
program TestNegative;
{$APPTYPE CONSOLE}
uses
Classes;
type
TMyStringList = class(TStringList)
class operator Negative(a: TMyStringList) : TMyStringList;
end;
class operator TMyStringList.Negative(a: TMyStringList) : TMyStringList;
var
i : integer;
begin
Result := TMyStringList.Create;
for i := 0 to Pred(a.Count) do Result.Add(a[a.Count-1-i]);
end;
var
MyList,
MyListReverse : TMyStringList;
begin
MyList := TMyStringList.Create;
MyList.Add('Hello');
MyList.Add('World');
Writeln(MyList.Text);
Writeln('After negative operator: ');
MyListReverse := -MyList; //использование оператора
Writeln(MyListReverse.Text);
MyListReverse.Free;
MyList.Free;
end.
Перегрузка бинарных операторов
Бинарный оператор представляет собой функцию, которая получает два параметра,
а в качестве результата возвращает экземпляр определенного типа. Как и для
унарных операторов, типы входных параметра и тип результата работы оператора
также могут совпадать.
Рассмотрим пример подобного бинарного оператора: для нашего класса
TMyStringList определим оператор арифметического сложения со строкой "+”,
который будет добавлять эту строку в конец списка.
Для реализации такого оператора нам необходимо объявить следующей метод:
class operator Add(List : TMyStringLis; str : String) : TMyStringList;
Полный исходный код в виде консольного приложения выглядит следующим образом:
program TestAdd;
{$APPTYPE CONSOLE}
uses
Classes;
type
TMyStringList = class(TStringList)
class operator Add(List : TMyStringList; str : String) : TMyStringList;
end;
class operator TMyStringList.Add(List : TMyStringList; str : String) : TMyStringList;
begin
Result := List;
Result.Add(str);
end;
var
MyList : TMyStringList;
begin
MyList := TMyStringList.Create;
MyList := MyList + 'Hello';
MyList + 'World!';
writeln(MyList.Text);
MyList.Free;
end.
Теперь добавление новой строки в список сможет выглядеть следующим образом
MyList := MyList + 'World!'; //Оператор добавления новой строки с списку
Возможно, опытные программисты зададут вопрос: "А что будет если в операторе
сложения поменять слагаемые местами”?
MyList := 'World!' + MyList; // Будет ли вызываться оператор ?
В Delphi.NET подстановка определяется исходя из порядка следования параметров
в операторной функции, а поскольку следующий оператор
class operator Add(str : String; List : TMyStringLis) : TMyStringList;
в нашем классе неопределен, то мы получим ошибку компиляции:
Error: Incompatible types: 'string' and 'TMyStringList'
Поскольку наш оператор не создает новый объект, то можно обойтись без
оператора присваивания и сохранения результата в той же самой переменной:
MyList + 'World!'; //вызов оператора без сохранения результата !
Конечно, данная конструкция выглядит непривычно для языка Pascal, но является
синтаксически верной и компилируется транслятором без ошибок.
Перегрузка операторов сравнения
Операторы сравнения являются бинарными операторами, которые всегда возвращают
значение типа Boolean. Использование оператор сравнения возможно в любых
выражениях, которые вычисляют логическое значение: в операторе if, в
условиях циклов while и repeat и т.д.
В качестве примера, для нашего класса TMyStringList, определим
оператор сравнения на равенство "=”, который будет сравнивать два списка на
равенство его строк.
Для реализации такого оператора нам необходимо объявить следующий метод
класса:
class operator Equal(List : TMyStringLis; str : Sringt) : boolean;
Для того чтобы проверить использование данного оператора я разработал
небольшое консольное приложение:
program testEqual;
{$APPTYPE CONSOLE}
uses
Classes;
type
TMyStringList = class(TStringList)
class operator Equal(List1,List2 : TMyStringList) : boolean;
end;
class operator TMyStringList.Equal(List1,List2 : TMyStringList) : boolean;
var
i : integer;
begin
Result := false;
for i := 0 to Pred(List1.Count) Do
if List1[i] <> List2[i] then Exit;
Result := True;
end;
var
MyList1,
MyList2 : TMyStringList;
begin
MyList1 := TMyStringList.Create;
MyList1.Add('Hello');
MyList2 := TMyStringList.Create;
MyList2.Add('World!');
if MyList1 = MyList2 then //здесь вызывается наш оператор сравнения на равенство
Writeln('list is identical')
else
Writeln('different list');
MyList1.Free;
MyList2.Free;
end.
Перегрузка операторов приведения типа
Перегрузка операторов преобразования типа является, на мой взгляд, самой
востребованной возможностью для Delphi – программистов.
В самом синтаксисе приведения типов изменений в Delphi.NET не произошло: для
явного преобразования мы применяем функцию совпадающую c именем типа, для
неявного – ничего дополнительно указывать не нужно, как и в предыдущих версиях,
тип к которому нужно сделать преобразование определяется из типа выражения.
Например:
var
ListBase : TStringList;
ListChild : TMyStringList;
s : string;
begin
{..создание и работа с переменными ListBase ListChild}
ListBase := TStringList(ListChild); //явное преобразование ListChild в тип TStringList
ListBase := ListChild; //неявное преобразование ListChild в тип TStringList
s := ListChild; // ошибка компиляции: несовпадение типов String и TMyStringList
end;
Язык Pascal является языком со строгой типизацией, поэтому в версиях
Delphi1-Delphi7 нельзя было управлять этим процессом – преобразование типов
выполнялось компилятором либо на этапе трансляции, либо на этапе выполнения. Но
теперь у нас есть возможность полностью реализовывать и контролировать процесс
преобразования типов.
Итак, теперь Delphi позволяет нам полностью управлять преобразованиями типа,
причем можно раздельно сделать обработку явного и неявного преобразования. Для
этого предназначены два метода:
class operator Implicit(a : type) : resultType;
class operator Explicit(a: type) : resultType;
Метод Implicit предназначен для определения неявного преобразования
типа, а метод Explicit – для явного преобразования.
В качестве примера, для нашего класса расширенного списка строк
TMyStringList определим два оператора неявного приведения типа:
Приведение к типу Integer – будет возвращать число элементов в списке
Приведение к типу String – будет возвращать полный текст списка
(свойство Text).
Теперь определение класса TMyStringList будет выглядеть следующим
образом:
TMyStringList = class(TStringList)
class operator Implicit(List : TMyStringList) : String;
class operator Implicit(List : TMyStringList) : Integer;
end;
class operator TMyStringList.Implicit(List : TMyStringList) : Integer;
begin
Result := List.Count;
end;
class operator TMyStringList.Implicit(List : TMyStringList) : String;
begin
Result := List.Text;
end;
Теперь мы можем использовать экземпляры класса TMyStringList; в любом
месте программы, где необходимо значения типов integer и string
var
list : TMyStringList;
s : string;
i : integet;
b : boolean;
begin
list := TMyStringList.Create;
s := list; // ошибки нет: вызывается метод Implicit(List : TMyStringList) : String;
i := list; // ошибки нет: вызывается метод Implicit(List : TMyStringList) : Integer;
b := list; //ошибка компиляции: отсутствует оператор Implicit(List : //TMyStringList) : Boolean;
end.
В приведенном выше примере, для наглядности, преобразование осуществляется к
примитивным типам (String, integer, Boolean), в реальных приложениях
приведение возможно к произвольному классу. Например, приведение экземпляра
класса TAccount (банковский счет) к типу TConractor (контрагент)
может возвращать владельца счета, а приведение к типу Real будет
возвращать остаток на счету.
Перегрузка операторов приведения типа является очень мощным механизмом, и
использовать его надо с осторожностью, тщательно спланировав возможные
преобразования – особенно это касается неявного преобразования. В противном
случае это может привести к трудно обнаруживаемым ошибкам.
Расширенные возможности
В заключение своего рассказа я хотел бы рассказать Вам об интересных
возможностях, которые предоставляют перегрузки операторов.
Создание копирующего конструктора
Теперь в Delphi.NET при копировании объектов при помощи оператора
присваивания возможно создание полной копии объекта в виде нового экземпляра, а
не копирование ссылки.
Идея состоит в определении оператора явного преобразования экземпляра класса
к такому же классу.
TMyStringList = class(TStringList)
class operator Expplicit(List : TMyStringList) : TMyStringList;
end;
//преобразование экземпляра в тот же самый тип: создаем объект и копируем в него //параметр
class Operator TMyStringList.Expplicit(List : TMyStringList) : TMyStringList;
begin;
Result := TMyStringList.Create;
Result.AddStrings(List);
end;
Теперь вызов неявного преобразования создает полную копию объекта:
var
List1,
List2 : TMyStringList;
begin
List1 := TMyStringList.Create;
List1.Add('Hello World!');
List2 := TMyStringList(v_xTest); //Создается новый экземпляр, а не новая ссылка на объект!
//Теперь у нас есть два экземпляра – нужно оба их разрушить!
List1.Free;
List2.Free;
end.
Фактически в момент присваивания вызывается конструктор, то есть создается
новый объект. При этом необходимо помнить, что по окончании работы помимо
основного экземпляра также необходимо разрушить все его копии.
Что же произойдет, если мы переопределим оператор неявного преобразования
типа?
class operator Implicit(List : TMyStringList) : TMyStringList;
В этом случае компилятор позволит нам это сделать но при использовании такого
преобразования всегда будет выполняться стандартный оператор, который просто
копирует ссылку на экземпляр в переменную:
var
List,List1 : TMyStringList
begin
{……}
List := List1; // Implicit(List : TMyStringList) : TmyStringList не подставляется!
end;
Перегрузка оператора присваивания
Если Вы внимательно смотрели список операторов, для которых возможно
перегрузка, то обратили внимание, что там нет оператора присваивания.
Действительно, перегрузка оператора присваивания ":=” запрещена.
Однако в одном случае этого можно добиться: при использования классов которые
всегда имеют только один экземпляр. Примером такого экземпляра является
переменная Application типа TApplication библиотеки VCL. Может существовать
только один экземпляр класса TApplication, и он является глобальной переменной;
его создание и уничтожение реализовано в библиотеке VCL и происходит
автоматически.
Итак, для подобных объектов возможна неявная перегрузка оператора
присваивания.
Для этого экземпляр класса объявляется статическим полем того же самого
класса, и у него перекрывается оператор приведения к нужному типу. Создание
такого поля возможно в конструкторе класса. После этого становиться возможным
переопределение присваивания экземпляра произвольного класса данному текущему
экземпляру.
Проиллюстрируем вышесказанное примером: в нашем приложении реализован журнал
учета операций в виде текстового файла (log-файл). Данный log-файл
представлен в виде класса Logger, всегда существует только один экземпляр
данного класса, и все модули приложения обращаются к нему, чтобы записать
очередное сообщение.
type
TLogger = class
protected
class var
FLogger : TLogger; //поле класса (class-field)
public
FText : TStringList;
constructor Create;
destructor Destroy; override;
class operator Implicit(Line : String) : TLogger; //оператор преобразования строки к классу TLogger
strict private
class var
class constructor Create; //конструктор класса
end;
constructor TLogger.Create;
begin
inherited;
FText := TStringList.Create;
end;
destructor TLogger.Destroy;
begin
FText.Free;
inherited;
end;
class operator TLogger.Implicit(Line : String) : TLogger;
begin
FLogger.FText.Add(Line);
Result := FLogger;
end;
class constructor TLogger.Create;
begin
FLogger := TLogger.Create;
end;
Теперь становится возможным операция присвоения строки (string) переменной
Logger.
var
Logger : TLogger = TLogger.FLogger; //создаем глобальный лог-файл приложения
begin
Logger := 'Приложение стартовало';
{Код работы приложения …}
Logger := 'Приложение завершило свою работу';
Logger.Free;
end.
Заключение
Мы рассмотрели примеры перегрузки операций в Delphi.NET. Правильное
применение этой мощной возможности позволить сделать Ваш код более читабельным и
облегчит его понимание другими разработчиками и дальнейшее его сопровождение.
Весь код приведенных примеров вы можете загрузить по данной
ссылке.
|