Практика
Перезагрузка операторов

:: Меню ::
:: На главную ::
:: FAQ ::
:: Заметки ::
:: Практика ::
:: Win API ::
:: Проекты ::
:: Скачать ::
:: Секреты ::
:: Ссылки ::

:: Сервис ::
:: Написать ::

:: MVP ::

:: RSS ::

Яндекс.Метрика
В этой статье мы поговорим о перезагрузке операторов в Delphi for Win32. Начиная с BDS 2006 появилась возможность перезагрузки для записей - record (в отличии от Delphi for .NET, где есть такая же возможность для классов).

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

В приведенной ниже таблице приведен список операторов, которые можно реализовать в своей записи (реализовывать все нет необходимости, все зависит от назначения разрабатываемой вами записи).

Оператор Категория Сигнатура Оператор (символ)
Implicit Conversion Implicit( a: type ): resultType; implicit typecast (неявное приведение типа)
Explicit Conversion Explicit( a: type ): resultType; explicit typecast (явное приведение типа)
Negative Unary Negative( a: type ): resultType; -
Positive Unary Positive( a: type ): resultType; +
Inc Unary Inc( a: type ): resultType; Inc
Dec Unary Dec( a: type ): resultType Dec
LogicalNot Unary LogicalNot( a: type ): resultType; not
BitwiseNot Unary BitwiseNot( a: type ): resultType; not
Trunc Unary Trunc( a: type ): resultType; Trunc
Round Unary Round(a: type): resultType; Round
In Set In( a: type; b: type ): Boolean; in
Equal Comparison Equal( a: type; b: type ): Boolean; =
NotEqual Comparison NotEqual(a: type; b: type): Boolean; <>
GreaterThan Comparison GreaterThan( a: type; b: type ): Boolean; >
GreaterThanOrEqual Comparison GreaterThanOrEqual( a: type; b: type ): Boolean; >=
LessThan Comparison LessThan( a: type; b: type ): Boolean; <
LessThanOrEqual Comparison LessThanOrEqual( a: type; b: type ): Boolean; <=
Add Binary Add( a: type; b: type ): resultType; +
Subtract Binary Subtract( a: type; b: type ): resultType; -
Multiply Binary Multiply( a: type; b: type ): resultType; *
Divide Binary Divide( a: type; b: type ): resultType; /
IntDivide Binary IntDivide( a: type; b: type ): resultType; div
Modulus Binary Modulus( a: type; b: type ): resultType; mod
LeftShift Binary LeftShift( a: type; b: type ): resultType; shl
RightShift Binary RightShift( a: type; b: type ): resultType; shr
LogicalAnd Binary LogicalAnd( a: type; b: type ): resultType; and
LogicalOr Binary LogicalOr(a: type; b: type): resultType; or
LogicalXor Binary LogicalXor( a: type; b: type ): resultType; xor
BitwiseAnd Binary BitwiseAnd( a: type; b: type ): resultType; and
BitwiseOr Binary BitwiseOr( a: type; b: type ): resultType; or
BitwiseXor Binary BitwiseXor( a: type; b: type ): resultType; xor

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

Смысл всех операций интуитивно понятен из приведенной таблицы, но все же два из них (а именно Implicit и Explicit) хочется показать на простом примере.

type
  TTestRec = record
    {...}
  end;

var
  t: TTestRec;

t := значение; // В этом случае выполняется оператор неявного приведение типа Implicit
t := TTestRec( значение ); // В этом случае выполняется оператор явного приведение типа Explicit

По идее в реализации этих операторов разницы быть не должно, ведь явное и неявное приведение типа должны давать одинаковый результат, ну а на практике все зависит от разработчика.

Вот как может выглядеть объявление записи:

interface

type
  TMyRecord = packed record
  public
    type
      TInnerColorType = Integer;
      TMySubRecord = record
        str: string;
        i: integer;
      end;
    var
      Red: Integer;
  private
    class var
      Blue: Integer;
  public
    flag: boolean;
    constructor Create(val: Integer);
    procedure printRed;
    class procedure printBlue; static;
    property RedProp: TInnerColorType read Red write Red;
    class property BlueProp: TInnerColorType read Blue write Blue;
  end;

implementation

{ TMyRecord }

constructor TMyRecord.Create(val: Integer);
begin
   Red := val;
end;

procedure TMyRecord.printRed;
begin
   Writeln( 'Red: ', Red );
end;

class procedure TMyRecord.printBlue;
begin
   Writeln( 'Blue: ', Blue );
end;

Как видно, описание записи ничем не отличается от описания класса, и имеет поля, методы и свойства, каждое из которых может иметь свою область видимости. Так же в классе могут присутствовать объявления типов, переменные и константы. Не очень привычно (по крайней мере для тех, кто давно пишет на Delphi) видеть у записи конструктор. Тем не менее он есть, а его использование ничем ни отличается от использования конструктора класса.

var
  t: TMyRecord;

procedure TForm1.Button1Click(Sender: TObject);
begin
   t := TMyRecord.Create( значение );
end;

Объявить переменную типа TMySubRecord, объявленного в записи, можно следующим образом:

var
  MySubRecord: TMyRecord.TMySubRecord;

Переменные (или в более привычной терминологии – поля) могут быть объявлены как в самой записи (поле flag) так и в разделах var (поле Red) и class var (поле Blue). Отличия между первыми двумя вариантами нет (судя по всему, если не указан раздел var, он подразумевается по умолчанию). Переменные в этих разделах являются уникальными (локальными) для каждого экземпляра записи.

var
  t1, t2: TMyRecord;

procedure TForm1.Button1Click(Sender: TObject);
begin
   t1.Red := 1;
   ShowMessage( IntToStr( t1.Red ) ); // 1
   t2.Red := 2;
   ShowMessage( IntToStr( t2.Red ) ); // 2
   ShowMessage( IntToStr( t1.Red ) ); // 1
end;

Переменные в разделе class var являются общими (глобальными) для всех экземпляров записи.

var
  t1, t2: TMyRecord;

implementation

procedure TForm1.Button1Click(Sender: TObject);
begin
   t1.BlueProp := 1;
   ShowMessage( IntToStr( t1.BlueProp ) ); // 1
   t2.BlueProp := 2;
   ShowMessage( IntToStr( t2.BlueProp ) ); // 2
   ShowMessage( IntToStr( t1.BlueProp ) ); // 2
end;

Обращаться к полям, (объявленным без префикса class), методам, свойствам возможно как через саму запись, так и через экземпляр записи. Обращаться к статическим полям, методам, свойствам возможно только через запись.

Для следующего теста поле Blue необходимо перенести в раздел public (или просто закомментировать раздел private).

procedure TForm1.Button1Click(Sender: TObject);
begin
   t1.printBlue;
   t2.printRed;

   t1.Red := 1;
   t2.Blue := 2;

   t1.RedProp := 1;
   t2.BlueProp := 2;

   TMyRecord.printBlue;
   TMyRecord.printRed; // Ошибка!

   TMyRecord.Blue := 1;
   TMyRecord.Red := 2; // Ошибка!

   TMyRecord.BlueProp := 1;
   TMyRecord.RedProp := 2; // Ошибка!
end;

А теперь собственно о самой перезагрузке операторов. Синтаксис в данном случае будет следующий:

type
  TMyRecord = record
    class operator Add( a, b: TMyRecord ): TMyRecord;
    class operator Multiply( a: TMyRecord; b: Integer ): TMyRecord;
    class operator Multiply( a: Integer; b: TMyRecord ): TMyRecord;
    class operator Subtract( a, b: TMyRecord ): TMyRecord;
    class operator Implicit( a: Integer ): TMyRecord;
    class operator Implicit( a: TMyRecord ): Integer;
    class operator Explicit( a: Double ): TMyRecord;
  end;

Из приведенного описания видно, что переопределений одного оператора может быть и несколько. В этом случае для них применяется такое же правило, как и для перезагружаемых методов класса – переопределения должны отличаться друг от друга количеством и/или типом операндов.

Если, к примеру, предполагается возможность умножения двух операндов разных типов, то необходимо реализовать две версии перезагружаемой операции с разной последовательностью операторов. Допустим, предполагается возможность умножения TMyRecord на Integer.

var
  t1, t2: TMyRecord;

t1 := t2 * 2; // будет использован оператор Multiply (a: TMyRecord; b: Integer): TMyRecord
t1 := 2 * t2; // будет использован оператор Multiply (a: Integer; b: TMyRecord): TMyRecord

В качестве примера к статье прилагается модуль с записью, описывающей деньги (хотя модуль универсален и может применяться для любой валюты, в определении полей я использовал отечественную валюту – рубли и копейки :) – TMoney. Для записи определен конструктор, операции явного и неявного приведения типа, сложение и вычитание операндов одного типа (TMoney) и операции умножения и деления на число.

Присвоить значение переменной типа TMoney можно одним из следующих образов:

var
  Money: TMoney;

Money := ‘2,51’;
Money := TMoney( ‘2,51’ );
Money := TMoney.Create( ‘2,51’ );

Операторы сложения и вычитания определены для двух переменных типа TMoney.

var
  m1, m2, Money: TMoney;

m1:= ‘2,51’;
m2:= ‘3,14’;
Money := m1 + m2;

В случае сложения переменной типа TMoney с суммой в текстовом представлении будет выполнено неявное преобразование и следующий код отработает правильно, при этом порядок операторов, принадлежащих разным типам, неважен.

var
  m1, Money: TMoney;

m1:= ‘2,51’;
Money := m1 + ‘3,14’;
Money := ‘3,14’ + m1;

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

var
  Money: TMoney;

Money := ‘2,51’ + ‘3,14’; // Ошибка – неверный формат денежной суммы

Для того, чтобы этот код отработал верно, необходимо явное приведение типа одного из операндов или сразу обоих.

var
  Money: TMoney;

Money := TMoney( ‘2,51’ ) + TMoney( ‘3,14’ );

При внимательном изучении прилагаемого примера могут возникнуть вопросы у людей, не знакомых с числами с фиксированной точностью. Их обзор выходит за рамки данной статьи, тем же, кто все же хочет узнать о них больше, будет полезно прочитать статью Числа с фиксированной точностью (запятой).

Тем не менее хочется привести небольшой пример, который наглядно покажет необходимость применения чисел с фиксированной точностью в данном примере.

Допустим у вас система рассчитывает в у.е., а все внешние расчеты идут в рублях и при этом курс у.е. к рублю 1 у.е. = 30 рублей. Пусть у Вас есть товар в 3 у.е. (3 у.е. = 90 рублей) покупатель дает Вам 100 рублей.
100 / 30 = 3.33 у.е.
3.33 - 3 = 0.33 у.е. сдачи в у.е.
0.33 * 30 = 9.90 рублей сдача в рублях.
Система "наварила" 10 копеек за одну операцию! Здравствуй налоговая и "маски шоу"!

Это стандартная проблема всех подобных систем, и общего решения для нее нет. В серьезных системах, таких как SAP, для минимизации погрешности вычислений, есть два свойства – одно указывает на количество знаков после запятой во внутреннем представлении, а другое для внешнего. Практика показывает – для "нормальной" работы внутреннее представление значений денег должно быть не менее 6 знаков после запятой, а на экран выводить соответственно два (01 копейка). Отдельно пишутся программы, которые выявляют такие ситуации и прибавляют "хвостик" (в 1,2,… копейки) то там, то здесь.

Подводя итог всему выше сказанному, хочется резюмировать следующим. Представленное нововведение хорошо подходит к задачам с использованием сложно-структурных данных в математических и логических операциях (к примеру, вектор или матрица). Каждый, кто хоть раз пробовал создавать 3D сцены на Delphi, знает, на какие ухищрения приходилось идти для реализации аффинных преобразований через векторное произведение вектора на матрицу. Теперь можно создать классы TVector и TMatrix так, что их экземплярами можно оперировать привычной математической формой записи.

На сегодня все, успехов в программировании.

P.S.
Выражаю благодарность за помощь и дельные советы при написании статьи и разработке примера Thunderchild.

.: Пример к данной статье :.


При использовании материала - ссылка на сайт обязательна