:: 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.
.: Пример к данной статье :.
|
При использовании материала - ссылка на сайт обязательна
|
|