Delphi.NET: перегрузка операторов

Описание возможностей по переопределению операторов в Delphi.NET

Автор: Игорь Мельников (imelnikov@topsbi.ru)
TOPS BI
Версия текста: 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. Правильное применение этой мощной возможности позволить сделать Ваш код более читабельным и облегчит его понимание другими разработчиками и дальнейшее его сопровождение.

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


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


Опубликовал admin
26 Июл, Понедельник 2004г.



Программирование для чайников.