Home » Tutorials » Object Pascal/RTL » Das Überladen von Operatoren

Das Überladen von Operatoren

Die Funktionsweise am Beispiel

Nun sehen wir uns an, wie man das Überladen von Operatoren an einem echten Beispiel anwendet. Dazu betrachten wir die rationalen Zahlen. Jede rationale Zahl lässt sich als Bruch darstellen, was genauere Rechenergebnisse erlaubt, als das Rechnen mit Dezimalzahlen, wo u.U. Rundungsfehler etc. dazu kommen können.
Machen wir uns dazu zunächst ein paar Gedanken. Wie soll der Datentyp implementiert werden? Nehmen wir ein Record, bestehend aus zwei Feldern, wobei eines der Felder für den Zähler (numerator), das andere für den Nenner (denominator) steht. Damit wir auch mit möglichst genauen Zahlen rechnen können, nutzen wir Int64 für Zähler und Nenner. Da ein Bruch durchaus positiv oder auch negativ sein kann, werden wir später dafür sorgen, dass das Vorzeichen immer durch den Zähler gegeben ist (Dies ist auch in der Mathematik so definiert).
Erschreckt euch nicht vor dem großen Codeblock, der gleich folgt, es handelt sich lediglich um die Deklaration sämtlicher Operatoren, die wir überladen werden. Als Namen wähle ich übrigens TFraction, das englische Wort für Bruch.

 TFraction = record
      numerator: Int64; // Zähler
      denominator: Int64; // Nenner
      class operator add(const a, b: TFraction): TFraction;
      class operator subtract(const a, b: TFraction): TFraction;
      class operator multiply(const a, b: TFraction): TFraction;
      class operator divide(const a, b: TFraction): TFraction;
      class operator equal(const a, b: TFraction): boolean;
      class operator notEqual(const a, b: TFraction): boolean;
      class operator greaterThan(const a, b: TFraction): boolean;
      class operator greaterThanOrEqual(const a, b: TFraction): boolean;
      class operator lessThan(const a, b: TFraction): boolean;
      class operator lessThanOrEqual(const a, b: TFraction): boolean;
      class operator negative(const z: TFraction): TFraction;
      class operator logicalnot(const z: TFraction): TFraction;
      class operator implicit(const z: TFraction): String;
      class operator implicit(const e: Int64): TFraction;
      class operator implicit(const z: TFraction): Extended;
    end;

function konstrfraction(const numerator, denominator: Int64): TFraction;

Die letzte Funktion scheint nicht sofort ersichtlich, doch später im Quellcode wird klar, warum wir den Umweg über eine Art Konstruktor wählen.Betrachten wir zunächst die Grundrechenarten.

{ TFraction }

class operator TFraction.add(const a, b: TFaction): TFraction;<br/ >begin
  Result := KonstrFraction(a.numerator * b.demoninator + b.numerator * a.denominator, a.denominator * b.denominator);
end;

class operator TFraction.multiply(const a, b: TFraction): TFraction;
begin
  Result := KonstrFraction(a.numerator * b.numerator, a.denominator * b.denominator);
end;

class operator TFraction.subtract(const a, b: TFraction): TFraction;
begin
  Result := KonstrFraction(a.numerator * b.denominator - b.numerator * a.denominator, a.denominator * b.denominator);
end;

Addition und Subtraktion sollte klar sein. Hier wurden lediglich Zähler und Nenner erweitert, so dass beide Brüche zusammen addiert werden können. Auch die Multiplikation ist relativ simpel (In jedem Fall wird hier der Umweg über KonstrFraction gemacht. Wie erwähnt, hat das seinen Sinn. Die Erklärung folgt später noch).
Die Division erfolgt absichtlich noch nicht, da wir später einen kleinen Trick zunutze machen werden.
Betrachten wir erst einmal die Negation (den Vorzeichenwechsel) und das logische Nicht. Letzteres erscheint zunächst bei Brüchen unpassend, macht jedoch als die Operation Sinn, welche den Kehrwert eines Bruchs bildet, also Zähler und Nenner vertauscht.

class operator TFraction.logicalnot(const z: TFraction): TFraction;
begin
  Result := KonstrFraction(z.denominator, z.numerator);
end;

class operator TFraction.negative(const z: TFraction): TFraction;
begin
  Result := KonstrFraction(-z.numerator, -z.denominator);
end;

Nun überlegen wir uns zur Divison: Was bedeutet es überhaupt, durch eine Zahl zu teilen? Es ist dasselbe, als wenn man eine Multiplikation mit dem Kehrwert ausführt. Die Multiplikation haben wir bereits implementiert, ebenso den Kehrwert.

class operator TFraction.divide(const a, b: TFraction): TFraction;
begin
  Result := a*(not b);
end;

Wir wenden also einfach unsere bereits überladenen Operatoren an, um die Division zu definieren. Die Frage, warum wir ausgerechnet hier KonstrFraction nicht verwenden, lässt sich damit beantworten, dass es bereits durch die Multiplikation angewendet wird.
Bevor wir zu den Vergleichsoperatoren kommen, möchte ich jedoch noch das Rätsel um KonstrFraction auflösen. Wozu den Umweg? Ganz einfach: Wie bereits erwähnt, soll dafür gesorgt werden, dass das Vorzeichen sich stets im Zähle befindet. Dafür sorgt diese Funktion. Außerdem gibt es noch einen wesentlichen Grund. Nehmen wir an, wir möchten die Zahl 5/3 mit der Zahl 8/5 multiplizieren. Dann berechnet unserer Operator: 40/15. Nun sieht man aber sofort: Da lässt sich noch etwas kürzen. 40/15 ist dasselbe wie 8/3. Unsere bisherigen Operatoren rechnen ohne Rücksicht auf eben dieses, wobei die Zahl u.U. immer größer und größer wird. Um das zu vermeiden, sorgt KonstrFraction überdies dafür, dass die Zahl wenn möglich gekürzt wird.

function konstrfraction(const numerator, denominator: Int64): TFraction;
var
  Teiler: Int64;
begin
  // Das Vorzeichen richtig bestimmen
  if denominator >= 0 then
  begin
    Result.numerator := numerator;
    Result.denominator := denominator;
  end
  else
  begin
    Result.numerator := -numerator;
    Result.denominator := -denominator;
  end;  Teiler := ggT(AbsoluteValue(numerator),  AbsoluteValue(denominator));
  result.numerator := numerator div Teiler;
  result.denominator := denominator div Teiler;
end;

Wir verwenden hier die Prozedur ggT, die den größten gemeinsamen Teiler zweier Zahlen bestimmt. Wie das funktioniert, ist hier nicht weiter wichtig, aber ich werde den Quelltext am Ende noch auflisten. Übrigens: Dies wäre auch eine gute Stelle, die Division durch Null abzufangen.

Wir beschränken uns hier jedoch auf die drei impliziten Typumwandlungen. Die erste, von einem Bruch zu einem String, ist relativ simpel, da wir lediglich Zähler und Nenner in einen String umwandeln und einen Slash dazwischen bringen. Die zweite, von einem Bruch zu einem Extended-Datentyp ist auch recht einfach, da wir lediglich Zähler und Nenner durcheinander teilen zu brauchen. Die dritte, von einem Int64 zu einem Bruch, ist auch nicht schwer, wenn man sich überlegt, dass z.B. die Zahl 7 dasselbe ist wie 7/1.

class operator TFraction.implicit(const z: TFraction): String;
begin
  Result := IntToStr(z.numerator) + '/' + IntTostr(z.denominator);
end;

class operator TFraction.implicit(const e: Int64): TFraction;
begin
  Result := KonstrFraction(e, 1);
end;

class operator TFraction.implicit(const z: TFraction): Extended;
begin
  Result := z.numerator / z.denominator;end;

Wir haben uns bereits überlegt, dass die Umwandlung vom Bruch zum Extended recht simpel ist. Warum also nutzen wir das nicht aus und überlassen Delphi selbst die Vergleichsoperationen?

class operator TFraction.equal(const a, b: TFraction): boolean;
begin
  result := Extended(a) = Extended(b);
end;

class operator TFraction.greaterthan(const a, b: TFraction): boolean;
begin
  result := Extended(a) > Extended(b);
end;

class operator TFraction.greaterthanorequal(const a, b: TFraction): boolean;
begin
  result := Extended(a) >= Extended(b);
end;

class operator TFraction.lessThan(const a, b: TFraction): boolean;
begin
  result := Extended(a) < Extended(b);
end;

class operator TFraction.lessThanOrEqual(const a, b: TFraction): boolean;
begin
  result := Extended(a) <= Extended(b);
end;

class operator TFraction.notEqual(const a, b: TFraction): boolean;
begin
  result := Extended(a) <> Extended(b);
end;

Dies also zu unserem Beispiel. Es ist nur eine Möglichkeit, wie das Überladen von Operatoren zu implementieren ist. Wie erwähnt, müssen sich die Operatoren nicht auf arithmetische Operationen beschränken. Es gibt durchaus noch wesentlich mehr Einsatzgebiete und es ist mit Sicherheit möglich, den eigenen Datentyp so wesentlich handle-barer (wie der Engländer sagen würde) zu machen.
Wie bereits gesagt, bleibt uns nicht nur der Einsatz an arithmetischen Typen, wie zum Beispiel Brüche, komplexe Zahlen, selbsterweiternde Integers oder ähnliches, sondern auch zum Beispiel die Anwendung an eigenen Listen-Datentypen. Vielleicht wäre es ganz nützlich, seine eigene Listenimplementierung zu haben, der man mit einem + einen weiteren Eintrag hinzufügen kann. Ein einfaches = könnte dann zwei komplette Listen auf angenehme Weise vergleichen. Wir sehen: Es gibt noch viel mehr Möglichkeiten.