Die Klasse Complex

1. Aufgabe

Die Theorie der komplexen Zahlen ist auf den Mathematiker C. F. Gauß zurückzuführen, der diese erfand, um beispielsweise Gleichungen der Gestalt x2 = -1 lösen zu können. Für das Rechnen mit komplexen Zahlen hat man die Grundrechenarten +, -, * und / definiert, sie lauten für zwei komplexe Zahlen z1 = x1 + iy1 und z2 = x2 + iy2 wie folgt:

Addition:

z1 + z2 =

(x1 + iy1) + (x2 + iy2) = (x1 + x2) + i(y1 + y2)

Subtraktion:

z1 - z2 =

(x1 + iy1) - (x2 + iy2) = (x1 - x2) + i(y1 - y2)

Multiplikation:

z1 * z2 =

(x1 + iy1) * (x2 + iy2) = x1x2 + ix1y2 + ix2y1 - y1y2 =

 

 

x1x2 - y1y2 + i(x1y2 + x2y1)

Division:

z1 / z2 =

(x1 + iy1) / (x2 + iy2) =

 

 

(x1x2 + y1y2) / (x22 + y22) + i(x2y1 - x1y2) / (x22 + y22)

Tabelle 1. Die Grundrechenarten komplexer Zahlen.


Neben diesen elementaren Rechenoperationen gibt es noch den Betrag einer komplexen Zahl. Unter dem Betrag einer komplexen Zahl verstehen wir in der komplexen Zahlenebene ihren Abstand zum Ursprung, nach dem Satz von Pythagoras gilt für das Betragsquadrat einer komplexen Zahl z = x + iy

|z|2 = x2 + y2.

Mit der konjugiert komplexen Zahl schließen wir diesen Ausflug in die Theorie komplexer Zahlen ab, in Bezug auf eine komplexe Zahl z = x + iy besitzt sie den Wert x - iy.

Beim Entwurf der Klasse Complex (Gestaltung der Methodenschnittstellen samt Implementierung) können Sie prinzipiell zwei Wege einschlagen:

  • Entwurf einer Klasse mit veränderbarem Verhalten.

  • Entwurf einer Klasse mit unveränderbarem Verhalten.

Objekte (eines Klassentyps) mit veränderbarem Verhalten (engl. mutable behaviour) besitzen die Eigenschaft, dass ihr Zustand nach der Objekterzeugung veränderbar ist. Würden wir die Klasse Complex nach dieser Richtlinie entwerfen, bietet sich für eine Methode Add folgende Realisierung an:

public void Add (Complex c)
{
    this.x = this.x + c.x;
    this.y = this.y + c.y;
}

Die Add-Methode besitzt kein explizites Ergebnis (Rückgabewert void), das Resultat der Operation wird direkt am aufgerufenen Objekt abgelegt:

Complex a, b;
...
a.Add (b);  // a equals now a + b

Dieses Verhalten entspricht dem objektorientierten Paradigma der Gestalt, dass ein Objekt durch den Aufruf einer Methode in einen neuen Zustand übergeht (Abbildung 1).

Methodenaufrufe an einem Objekt mit veränderbarem Verhalten ändern den Objektzustand.

Abbildung 1. Methodenaufrufe an einem Objekt mit veränderbarem Verhalten ändern den Objektzustand.


Eine Nebenwirkung dieser Variante ist der Umstand, dass der alte Zustand des gerufenen Objekts nach dem Methodenaufruf nicht mehr existiert.

Objekte (eines Klassentyps) mit unveränderbarem Verhalten (engl. immutable behaviour) weisen die Eigenschaft auf, dass ihr Zustand nach der Objekterzeugung nicht mehr änderbar ist. In diesem Sinn können sie als konstante Objekte betrachtet werden. Die Instanzvariablen von unveränderbaren Klassen sollten zu diesem Zweck mit dem Modifizierer readonly spezifiziert werden. Auf diese Weise kann der Compiler bereits zur Übersetzungszeit sicher stellen, dass die Instanzvariablen nach ihrer Initialisierung nicht mehr modifiziert werden:

class Complex
{
    private readonly int x;
    private readonly int y;
    ...
}

Die Add-Methode könnten wir in dieser Variante in der Form

public Complex Add (Complex a)
{
    return new Complex (this.x + a.x, this.y + a.y);
}

realisieren, also insbesondere mit Rückgabewert Complex:

Complex c = a.Add (b);

Für das Ergebnis ist nun allerdings ein neues Objekt zu erzeugen, dessen Referenz beim Verlassen der Methode dem Aufrufer als Resultat ausgehändigt wird (Abbildung 2).

Objekte mit unveränderbarem Verhalten ändern bei Methodenaufrufen ihren Zustand nicht.

Abbildung 2. Objekte mit unveränderbarem Verhalten ändern bei Methodenaufrufen ihren Zustand nicht.


Zu beachten ist, dass der Zustand des gerufenen Objekts (hier: Instanz a) nicht verändert wird. Vorsicht: Ein Aufruf der Gestalt

a.Add (b);

ist syntaktisch korrekt (der Compiler generiert keine Fehlermeldung), das Resultat des Add-Methodenaufrufs geht allerdings aufgrund der fehlenden Wertzuweisung verloren.

Im Design von Klassen ist von Fall zu Fall zu entscheiden, für welche Strategie man sich in Bezug auf ihre Veränderbarkeit entscheidet. Da komplexe Zahlen ein vergleichsweise statisches Verhalten besitzen, liegt für mich die Variante des unveränderbaren Verhaltens nahe. Erstellen Sie eine Klasse Complex unter Berücksichtigung von Tabelle 2.

Tipp

Bitte beachten Sie, dass Sie Methoden mit unveränderbarem Verhalten sowohl als Instanz- wie auch als Klassenmethoden implementieren können. Ein Anwender kann dann für sich entscheiden, welche Notation ihm bei der Verwendung der Klasse besser gefällt: Entweder eine mehr objekt-orientierte Schreibweise wie

Complex sum = a.Add (b);

oder die Operatorenschreibweise eines mathematischen Umfelds:

Complex sum = Complex.Add (a, b);

Bei der objekt-orientierten Schreibweise ist eine Instanzmethode an einer Instanz aufzurufen (hier: Instanz a), für die Operatorenschreibweise ist eine Klassenmethode an der Klasse aufzurufen (hier: Klasse Complex).

Element

Beschreibung

Konstruktoren

public Complex();
public Complex(double real, double imag);

Erzeugung einer komplexen Zahl.

Eigenschaften

public double Real { get; set; }
public double Imag { get; set; }

Der Real- als auch der Imaginärteil können mit den beiden Eigenschaften Real und Imag gelesen und geschrieben werden.

Methode Add

public Complex Add (Complex a);
public static Complex Add (Complex a, Complex b);

Addition zweier komplexer Zahlen.

Methode Sub

public Complex Sub (Complex a);
public static Complex Sub (Complex a, Complex b);

Subtraktion zweier komplexer Zahlen.

Methode Mul

public Complex Mul (Complex a);
public static Complex Mul (Complex a, Complex b);

Multiplikation zweier komplexer Zahlen.

Methode Div

public Complex Div (Complex a);
public static Complex Div (Complex a, Complex b);

Division zweier komplexer Zahlen.

Methode Abs

public double Abs ();

Betrag einer komplexen Zahl.

Methode Conjugate

public Complex Conjugate ();

Liefert die konjugierte komplexe Zahl zurück.

Methode Print

public void Print ();

Gibt eine komplexe Zahl auf der Konsole im vektoriellen Format [x,y] aus.

Tabelle 2. Elemente der Klasse Complex.

2. Lösung

Die Lösung aus Listing 1 ist mit elementaren C#-Sprachmitteln verfasst. Die Anforderungen des .NET-Objektmodells – Kontrakt mit der universellen Basisklasse Object – werden unter der Rubrik Vererbung betrachtet:

01: class Complex
02: {
03:     private double real;
04:     private double imag;
05: 
06:     // c'tors
07:     public Complex() : this(0, 0) { }
08: 
09:     public Complex(double real, double imag)
10:     {
11:         this.real = real;
12:         this.imag = imag;
13:     }
14: 
15:     // properties
16:     public double Real
17:     {
18:         get { return this.real; }
19:         set { this.real = value; }
20:     }
21: 
22:     public double Imag
23:     {
24:         get { return this.imag; }
25:         set { this.imag = value; }
26:     }
27: 
28:     // class methods
29:     public static Complex Add(Complex a, Complex b)
30:     {
31:         double real = a.real + b.real;
32:         double imag = a.imag + b.imag;
33:         return new Complex(real, imag);
34:     }
35: 
36:     public static Complex Sub(Complex a, Complex b)
37:     {
38:         double real = a.real - b.real;
39:         double imag = a.imag - b.imag;
40:         return new Complex(real, imag);
41:     }
42: 
43:     public static Complex Mul(Complex a, Complex b)
44:     {
45:         double real = a.real * b.real - a.imag * b.imag;
46:         double imag = a.real * b.imag + a.imag * b.real;
47:         return new Complex(real, imag);
48:     }
49: 
50:     public static Complex Div(Complex a, Complex b)
51:     {
52:         double real =
53:             (b.real * a.real + b.imag * a.imag) /
54:             (b.imag * b.imag + a.imag * a.imag);
55: 
56:         double imag =
57:             (b.real * a.real - b.imag * a.imag) /
58:             (b.imag * b.imag + a.imag * a.imag);
59: 
60:         return new Complex(real, imag);
61:     }
62: 
63:     // instance methods - immutable behaviour
64:     public Complex Add(Complex c)
65:     {
66:         return Complex.Add(this, c);
67:     }
68: 
69:     public Complex Sub(Complex c)
70:     {
71:         return Complex.Sub(this, c);
72:     }
73: 
74:     public Complex Mul(Complex c)
75:     {
76:         return Complex.Mul(this, c);
77:     }
78: 
79:     public Complex Div(Complex c)
80:     {
81:         return Complex.Div(this, c);
82:     }
83: 
84:     public Complex Conjugate()
85:     {
86:         return new Complex(this.real, -this.imag);
87:     }
88: 
89:     public double Abs()
90:     {
91:         return Math.Sqrt(this.real * this.real + this.imag * this.imag);
92:     }
93: 
94:     public void Print()
95:     {
96:         String sign = (this.imag >= 0) ? "+" : "";
97:         Console.WriteLine("[{0}{1}{2}*i]", this.real, sign, this.imag);
98:     }
99: }

Beispiel 1. Eine Implementierung der Klasse Complex.


Beispiel:

private static void Main()
{
    // testing c'tors
    Complex c1 = new Complex(1.0, 2.0);
    Console.Write("c1:      ");
    c1.Print();

    Complex c2 = new Complex(3.0, 4.0);
    Console.Write("c2:      ");
    c2.Print();

    // testing properties
    c2.Real = 5;
    Console.Write("c2: ");
    c2.Print();
    double imag = c2.Imag;
    Console.WriteLine("Imaginary part of c2: : {0}", c2.Imag);

    // testing binary arithmetic operations
    Complex c3 = Complex.Add(c1, c2);
    Console.Write("c1 + c2: ");
    c3.Print();

    c3 = Complex.Sub(c1, c2);
    Console.Write("c1 - c2: ");
    c3.Print();

    c3 = Complex.Mul(c1, c2);
    Console.Write("c1 * c2: ");
    c3.Print();

    c3 = Complex.Div(c1, c2);
    Console.Write("c1 / c2: ");
    c3.Print();

    // testing Abs-method
    Console.Write("c1:      ");
    c1.Print();
    double d = c1.Abs();
    Console.WriteLine("Abs(c1): {0:F4}", d);

    // testing conjunction method
    Console.Write("c1:      ");
    c1.Print();
    c3 = c1.Conjugate();
    Console.Write("!c1:     ");
    c3.Print();
}

Ausgabe:

c1:      [1+2*i]
c2:      [3+4*i]
c2: [5+4*i]
Imaginary part of c2: : 4
c1 + c2: [6+6*i]
c1 - c2: [-4-2*i]
c1 * c2: [-3+14*i]
c1 / c2: [0,65-0,15*i]
c1:      [1+2*i]
Abs(c1): 2,2361
c1:      [1+2*i]
!c1:     [1-2*i]