Skip to content

4. Klassen, Strukturen, Schnittstellen

4.1. Das Objekt am Beispiel

4.1.1. Allgemeines

Wir wenden uns nun anhand eines Beispiels der objektorientierten Programmierung zu. Ein Objekt ist eine Entität, die Daten enthält, die seinen Zustand definieren (sogenannte Felder, Attribute, ...), sowie Funktionen (sogenannte Methoden). Ein Objekt wird nach einem Modell erstellt, das als Klasse bezeichnet wird:

public class C1{
     Type1 p1        ; // field p1
     Type2 p2        ; // p2 field
    
     Type3 m3(        ) { // m3 method
        
    }
     Type4 m4(        ) { // m4 method
        
    }
    
}

Aus der Klasse C1 oben können Sie zahlreiche Objekte O1, O2, … erstellen. Alle verfügen über Felder p1, p2, … und Methoden m3, m4, …. Sie haben jedoch unterschiedliche Werte für ihre Felder p1, wobei jedes seinen eigenen Zustand hat. Wenn o1 ein Objekt vom Typ C1 ist, bezeichnet o1.p1 die Eigenschaft p1 von o1 und o1.m1 die Methode m1 von o1.

Betrachten wir ein erstes Objektmodell: die Person.

4.1.2. Erstellen eines C#-Projekts

In den vorherigen Beispielen hatten wir nur eine einzige Datei: Program.cs. Von nun an können wir mehrere Quelldateien in einem einzigen Projekt haben. Wir zeigen Ihnen, wie das geht.

Erstellen Sie unter [1] ein neues Projekt. Wählen Sie unter [2] „Application Console“ aus. Behalten Sie unter [3] den Standardwert bei. Bestätigen Sie unter [4]. Unter [5] sehen Sie das generierte Projekt. Der Inhalt von Program.cs lautet wie folgt:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace ConsoleApplication1 {
    class Program {
        static void Main(string[] args) {
        }
    }
}

Speichern wir das erstellte Projekt:

Wählen Sie in [1] die Option zum Speichern aus. Wählen Sie in [2] den Ordner aus, in dem das Projekt gespeichert werden soll. Geben Sie in [3] einen Namen für das Projekt ein. Geben Sie in [5] an, dass Sie eine Lösung erstellen möchten. Eine Lösung ist eine Sammlung von Projekten. Geben Sie in [4] einen Namen für die Lösung ein. Bestätigen Sie in [6] das Speichern.

In [1] das gespeicherte Projekt. In [2] fügen Sie dem Projekt ein neues Element hinzu.

Geben Sie in [1] an, dass Sie eine Klasse hinzufügen möchten. Geben Sie in [2] den Klassennamen ein. Überprüfen Sie in [3] die Angaben. In [4] enthält das Projekt [01] nun eine neue Quelldatei „Personne.cs“:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace ConsoleApplication1 {
    class Personne {
    }
}

Ändern Sie den Namespace jeder Quelldatei in „Chap2“ und vermeiden Sie so das Importieren unnötiger Namespaces:


using System;
 
namespace Chap2 {
    class Personne {
    }
}

using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
        }
    }
}

4.1.3. Definition der Klasse „Person“

Die Klassendefinition „Person“ in der Quelldatei [Personne.cs] lautet wie folgt:


using System;
 
namespace Chap2 {
    public class Personne {
         // attributes
        private string prenom;
        private string nom;
        private int age;
 
         // method
        public void Initialise(string P, string N, int age) {
            this.prenom = P;
            this.nom = N;
            this.age = age;
        }
 
         // method
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }
 
}

Hier haben wir die Definition einer Klasse, d. h. eines Datentyps. Wenn wir Variablen dieses Typs erstellen, nennen wir sie Objekte oder Instanzen der Klasse. Eine Klasse ist also eine Vorlage, nach der Objekte erstellt werden.

Die Elemente oder Felder einer Klasse können Daten (Attribute), Methoden (Funktionen) oder Eigenschaften sein. Eigenschaften sind spezielle Methoden, mit denen der Wert der Attribute eines Objekts abgerufen oder festgelegt wird. Diese Felder können mit einem der folgenden drei Schlüsselwörter versehen sein:

privé
Auf ein privates Feld kann nur über die internen Methoden der Klasse zugegriffen werden
public
Auf ein öffentliches Feld kann von jeder Methode zugegriffen werden, unabhängig davon, ob sie innerhalb der
geschützt
Ein geschütztes Feld (protected) ist nur für die internen Methoden der Klasse oder für ein abgeleitetes Objekt zugänglich (siehe das Konzept der Vererbung weiter unten).

Im Allgemeinen werden Klassendaten als privat deklariert, während ihre Methoden und Eigenschaften als öffentlich deklariert werden. Das bedeutet, dass der Benutzer eines Objekts (der Programmierer)

  • keinen direkten Zugriff auf die privaten Daten des Objekts hat
  • die öffentlichen Methoden des Objekts aufrufen kann, insbesondere jene, die Zugriff auf dessen private Daten gewähren.

Die Syntax für die Deklaration einer C-Klasse lautet wie folgt:


public class C{
    private  donnée ou méthode ou propriété privée;
    public  donnée ou méthode ou propriété publique;
    protected  donnée ou méthode ou propriété protégée;
}

Die Reihenfolge der Attributdeklarationen „private“, „protected“ und „public“ ist beliebig.

4.1.4. Die Initialize-Methode

Zurück zu unserer Klasse „Person“, die wie folgt deklariert wurde:


using System;
 
namespace Chap2 {
    public class Personne {
         // attributes
        private string prenom;
        private string nom;
        private int age;
 
         // method
        public void Initialise(string p, string n, int age) {
            this.prenom = p;
            this.nom = n;
            this.age = age;
        }
 
         // method
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }
 
}

Welche Rolle spielt die Funktion „Initializes“? Da Name, Vorname und Alter private Daten der Klasse „Person“ sind, gilt Folgendes:

Personne p1;
p1.prenom="Jean";
p1.nom="Dupont";
p1.age=30;

sind ungültig. Wir müssen ein Objekt vom Typ Person über eine öffentliche Methode initialisieren. Dies ist die Aufgabe der Initialisierer. Wir schreiben:

Personne p1;
p1.Initialise("Jean","Dupont",30);

Die Schreibweise p1.Initialise ist zulässig, da Initializes öffentlich zugänglich ist.

4.1.5. Der new-Operator

Die Anweisungssequenz

Personne p1;
p1.Initialise("Jean","Dupont",30);

ist falsch. Die Anweisung

    Personne p1;

bezeichnet p1 als Referenz auf ein Objekt vom Typ Person. Dieses Objekt existiert noch nicht, daher ist p1 nicht initialisiert. Das ist so, als würde man schreiben:

Personne p1=null;

wobei das Schlüsselwort null angibt, dass die Variable p1 noch auf kein Objekt verweist. Wenn man dann schreibt

p1.Initialise("Jean","Dupont",30);

verwenden wir die Initialisierung des Objekts, auf das p1 verweist. Wenn dieses Objekt noch nicht existiert, meldet der Compiler einen Fehler. Um sicherzustellen, dass p1 auf ein Objekt verweist, schreiben Sie:

Personne p1=new Personne();

Dadurch wird ein noch nicht initialisiertes Objekt vom Typ „Person“ erstellt: Die Attribute „name“ und „first_name“, die Verweise auf Objekte vom Typ „String“ sind, haben den Wert „null“, und „age“ hat den Wert „0“. Es findet also eine Standardinitialisierung statt. Da p1 nun auf ein Objekt verweist, lautet die Initialisierungsanweisung für dieses Objekt

p1.Initialise("Jean","Dupont",30);

gültig.

4.1.6. Das Schlüsselwort this

Schauen wir uns den Code für die Initialisierung an:


        public void Initialise(string p, string n, int age) {
            this.prenom = p;
            this.nom = n;
            this.age = age;
}

Die Anweisung this.prenom=p bedeutet, dass der Vorname des aktuellen Objekts (this) den Wert p erhält. Das Schlüsselwort this bezeichnet das aktuelle Objekt: dasjenige, in dem sich die ausgeführte Methode befindet. Woher wissen wir das? Werfen wir einen Blick auf die Initialisierung des Objekts, auf das p1 im aufrufenden Programm verweist:

p1.Initialise("Jean","Dupont",30);

Dies ist die Methode Initialise des Objekts p1. Wenn diese Methode auf „this“ verweist, ist damit tatsächlich das Objekt p1 gemeint. Die Methode Initialise hätte auch wie folgt geschrieben werden können:


        public void Initialise(string p, string n, int age) {
            prenom = p;
            nom = n;
            this.age = age;
}

Wenn die Methode eines Objekts auf ein Attribut A dieses Objekts verweist, ist die Schreibweise this.A implizit. Sie muss explizit verwendet werden, wenn Bezeichner miteinander in Konflikt stehen. Dies ist der Fall bei:


this.age=age;

wobei „age“ sowohl ein Attribut des aktuellen Objekts als auch den von der Methode empfangenen Parameter „age“ bezeichnet. Die Mehrdeutigkeit muss dann durch die Bezeichnung des Attributs „age“ als „this.age“ aufgelöst werden.

4.1.7. Ein Testprogramm

Hier ist ein kurzes Testprogramm. Es ist in der Quelldatei [Program.cs] geschrieben:


using System;
 
namespace Chap2 {
    class P01 {
        static void Main() {
            Personne p1 = new Personne();
            p1.Initialise("Jean", "Dupont", 30);
            p1.Identifie();
        }
    }
}

Bevor Sie das Projekt [01] ausführen, müssen Sie möglicherweise die auszuführende Quelldatei angeben:

In den Projekteigenschaften [01] wird die auszuführende Klasse unter [1] angegeben.

Die nach Abschluss erhaltenen Ergebnisse lauten wie folgt:

[Jean, Dupont, 30]

4.1.8. Eine weitere Methode Initialisieren

Betrachten wir die Klasse „Person“ und fügen wir die folgende Methode hinzu:


        public void Initialise(Personne p) {
            prenom = p.prenom;
            nom = p.nom;
            age = p.age;
}

Wir haben nun zwei Methoden mit dem Namen Initializes: Dies ist zulässig, solange sie unterschiedliche Parameter akzeptieren. Dies ist hier der Fall. Der Parameter ist nun eine Referenz p auf eine Person. Die Attribute der Person p werden dann dem aktuellen Objekt (this) zugewiesen. Beachten Sie, dass Initializes direkten Zugriff auf die Attribute des Objekts p hat, obwohl diese privat sind. Dies gilt immer: Ein Objekt o1 einer Klasse C hat immer Zugriff auf die Attribute von Objekten derselben Klasse C.

Hier ist ein Test der neuen Klasse „Personen“:


using System;
 
namespace Chap2 {
    class Program {
        static void Main() {
            Personne p1 = new Personne();
            p1.Initialise("Jean", "Dupont", 30);
            p1.Identifie();
            Personne p2 = new Personne();
            p2.Initialise(p1);
            p2.Identifie();
        }
    }
}

und die Ergebnisse:

[Jean, Dupont, 30]
[Jean, Dupont, 30]

4.1.9. Konstruktoren der Klasse „Person“

Ein Konstruktor ist eine nach der Klasse benannte Methode, die beim Erstellen des Objekts aufgerufen wird. Er dient im Allgemeinen dazu, das Objekt zu initialisieren. Er kann Argumente annehmen, gibt jedoch keine Ergebnisse zurück. Seinem Prototyp oder seiner Definition geht kein Typ voraus (auch nicht void).

Wenn eine C-Klasse einen Konstruktor hat, der n Argumente argi akzeptiert, können die Deklaration und Initialisierung eines Objekts dieser Klasse in folgender Form erfolgen:

        C objet =new C(arg1,arg2, ... argn);

oder

        C objet;
        objet=new C(arg1,arg2, ... argn);

Wenn eine Klasse C über einen oder mehrere Konstruktoren verfügt, muss einer dieser Konstruktoren verwendet werden, um ein Objekt dieser Klasse zu erstellen. Wenn eine Klasse C keinen Konstruktor hat, verfügt sie über einen Standardkonstruktor, nämlich den Konstruktor ohne Parameter: public C(). Die Attribute des Objekts werden dann mit Standardwerten initialisiert. Genau das ist in den vorherigen Programmen geschehen, wo:

    Personne p1;
    p1=new Personne();

Erstellen wir zwei Konstruktoren für unsere Klasse Person:


using System;
 
namespace Chap2 {
    public class Personne {
         // attributes
        private string prenom;
        private string nom;
        private int age;
 
         // manufacturers
        public Personne(String p, String n, int age) {
            Initialise(p, n, age);
        }
        public Personne(Personne P) {
            Initialise(P);
        }
 
         // method
        public void Initialise(string p, string n, int age) {
...
        }
 
        public void Initialise(Personne p) {
...
        }
 
         // method
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }
 
}

Unsere beiden Konstruktoren verwenden einfach die zuvor behandelten Initialisierungen. Denken Sie daran, dass, wenn ein Programmierer beispielsweise die Notation Initialise(p) verwendet, der Compiler dies in this.Initialise(p) übersetzt. Im Konstruktor wird die Initialisierung aufgerufen, um auf das Objekt zu wirken, auf das this verweist, d. h. das aktuelle Objekt, das gerade erstellt wird.

Hier ist ein kurzes Testprogramm:


using System;
 
namespace Chap2 {
    class Program {
        static void Main() {
            Personne p1 = new Personne("Jean", "Dupont", 30);
            p1.Identifie();
            Personne p2 = new Personne(p1);
            p2.Identifie();
        }
    }
}
 

und die erzielten Ergebnisse:

[Jean, Dupont, 30]
[Jean, Dupont, 30]

4.1.10. Objektreferenzen

Wir verwenden immer dieselbe Person. Das Testprogramm lautet nun:


using System;
 
namespace Chap2 {
    class Program2 {
        static void Main() {
             // p1
            Personne p1 = new Personne("Jean", "Dupont", 30);
            Console.Write("p1="); p1.Identifie();
             // p2 references the same object as p1
            Personne p2 = p1;
            Console.Write("p2="); p2.Identifie();
             // p3 references an object that will be a copy of the object referenced by p1
            Personne p3 = new Personne(p1);
            Console.Write("p3="); p3.Identifie();
             // change the state of the object referenced by p1
            p1.Initialise("Micheline", "Benoît", 67);
            Console.Write("p1="); p1.Identifie();
             // as p2=p1, the object referenced by p2 must have changed state
            Console.Write("p2="); p2.Identifie();
             // as p3 does not reference the same object as p1, the object referenced by p3 must not have changed
            Console.Write("p3="); p3.Identifie();
        }
    }
}

Die Ergebnisse lauten wie folgt:

1
2
3
4
5
6
p1=[Jean, Dupont, 30]
p2=[Jean, Dupont, 30]
p3=[Jean, Dupont, 30]
p1=[Micheline, Benoît, 67]
p2=[Micheline, Benoît, 67]
p3=[Jean, Dupont, 30]

Bei der Deklaration der Variablen p1 durch

Personne p1=new Personne("Jean","Dupont",30);

ist p1 eine Objektreferenz auf Personne("John", "Smith", 30), aber nicht das Objekt selbst. In C würde man sagen, es handelt sich um einen Zeiger, d. h. die Adresse des erstellten Objekts. Wenn man dann schreibt:

    p1=null;

wird nicht das Objekt Person("John", "Smith", 30) geändert, sondern die Referenz p1, deren Wert sich ändert. Das Objekt Person("John", "Smith", 30) geht „verloren“, wenn es von keiner anderen Variablen referenziert wird.

Wenn wir schreiben:

Personne p2=p1;

initialisieren wir den Zeiger p2: Er „zeigt“ auf dasselbe Objekt (bezeichnet dasselbe Objekt) wie der Zeiger p1. Wenn wir also das Objekt ändern, auf das p1 „zeigt“ (oder auf das verwiesen wird), ändern wir auch das Objekt, auf das p2 verweist.

Wenn wir schreiben:

Personne p3=new Personne(p1);

wird ein neues Objekt Person erstellt. Auf dieses neue Objekt wird durch p3 verwiesen. Wenn Sie das Objekt ändern, auf das p1 „zeigt“ (oder auf das verwiesen wird), ändert sich auch das Objekt, auf das p3 verweist. Das zeigen die Ergebnisse.

4.1.11. Übergabe von Objektreferenzparametern

Im vorigen Kapitel haben wir uns angesehen, wie Funktionsparameter übergeben werden, wenn sie einen einfachen C#-Typ darstellen, der durch eine .NET-Struktur repräsentiert wird. Schauen wir uns nun an, was passiert, wenn der Parameter ein :


using System;
using System.Text;
 
namespace Chap1 {
    class P12 {
        public static void Main() {
             // example 4
            StringBuilder sb0 = new StringBuilder("essai0"), sb1 = new StringBuilder("essai1"), sb2 = new StringBuilder("essai2"), sb3;
            Console.WriteLine("Dans fonction appelante avant appel : sb0={0}, sb1={1}, sb2={2}", sb0,sb1, sb2);
            ChangeStringBuilder(sb0, sb1, ref sb2, out sb3);
            Console.WriteLine("Dans fonction appelante après appel : sb0={0}, sb1={1}, sb2={2}, sb3={3}", sb0, sb1, sb2, sb3);
 
        }
 
        private static void ChangeStringBuilder(StringBuilder sbf0, StringBuilder sbf1, ref StringBuilder sbf2, out StringBuilder sbf3) {
            Console.WriteLine("Début fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}", sbf0,sbf1, sbf2);
            sbf0.Append("*****");
            sbf1 = new StringBuilder("essai1*****");
            sbf2 = new StringBuilder("essai2*****");
            sbf3 = new StringBuilder("essai3*****");
            Console.WriteLine("Fin fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}, sbf3={3}", sbf0, sbf1, sbf2, sbf3);
        }
    }
}
  • Zeile 8: definiert 3 StringBuilder. Ein StringBuilder-Objekt ähnelt einem String-Objekt. Bei der Verarbeitung eines String-Objekts erhält man ein neues String-Objekt als Rückgabewert. In der Code-Sequenz lautet dies also:
string s="une chaîne";
s=s.ToUpperCase();

In Zeile 1 wird eine Zeichenkette erstellt, und s ist deren Adresse. In Zeile 2 erstellt s.ToUpperCase() ein weiteres Zeichenkettenobjekt im Speicher. Somit hat sich der Wert von s zwischen Zeile 1 und 2 geändert (s verweist nun auf das neue Objekt). Die Klasse StringBuilder ermöglicht es, eine Zeichenkette zu transformieren, ohne ein zweites Objekt zu erstellen. Dies ist das oben angeführte Beispiel:

  • Zeile 8: 4 Referenzen [sb0, sb1, sb2, sb3] auf Objekte vom Typ StringBuilder
  • Zeile 10: Diese werden mit unterschiedlichen Modi an ChangeStringBuilder übergeben: sb0, sb1 im Standardmodus, sb2 mit dem Schlüsselwort ref, sb3 mit dem Schlüsselwort out.
  • Zeilen 15–22: eine Methode mit formalen Parametern [sbf0, sbf1, sbf2, sbf3]. Die Beziehungen zwischen den formalen Parametern sbfi und der Arbeitsgruppe sbi sind wie folgt:
  • sbf0 und sb0 sind zu Beginn der Methode zwei getrennte Referenzen, die auf dasselbe Objekt verweisen (Adresswertübergabe)
  • dasselbe gilt für sbf1 und sb1
  • sbf2 und sb2 sind zu Beginn der Methode dieselbe Referenz auf dasselbe Objekt (Schlüsselwort ref)
  • sbf3 und sb3 sind nach Ausführung der Methode dieselbe Referenz auf dasselbe Objekt (Schlüsselwort out)

Die Ergebnisse lauten wie folgt:

1
2
3
4
Dans fonction appelante avant appel : sb0=essai0, sb1=essai1, sb2=essai2
Début fonction appelée : sbf0=essai0, sbf1=essai1, sbf2=essai2
Fin fonction appelée : sbf0=essai0*****, sbf1=essai1*****, sbf2=essai2*****, sbf3=essai3*****
Dans fonction appelante après appel : sb0=essai0*****, sb1=essai1, sb2=essai2*****, sb3=essai3*****

Erläuterungen:

  • sb0 und sbf0 sind zwei separate Verweise auf dasselbe Objekt. Dieses wurde über sbf0 geändert – Zeile 3. Diese Änderung kann über sb0 eingesehen werden – Zeile 4.
  • sb1 und sbf1 sind zwei unterschiedliche Verweise auf dasselbe Objekt. sbf1 wurde in der Methode geändert und verweist nun auf ein neues Objekt – Zeile 3. Dies ändert nichts am Wert von sb1, das weiterhin auf dasselbe Objekt verweist – Zeile 4.
  • sb2 und sbf2 sind dieselbe Referenz auf dasselbe Objekt. sbf2 wird in der Methode geändert und verweist nun auf ein neues Objekt – Zeile 3. Da sbf2 und sb2 eine Einheit bilden, wurde auch der Wert von sb2 geändert und sb2 verweist auf dasselbe Objekt wie sbf2 – Zeilen 3 und 4.
  • Vor dem Aufruf der Methode war sb3 wertlos. Nach der Methode erhält sb3 den Wert von sbf3. Wir haben daher zwei Referenzen auf dasselbe Objekt – Zeilen 3 und 4

4.1.12. Temporäre Objekte

In einem Ausdruck können wir den Konstruktor eines Objekts explizit aufrufen: Das Objekt wird erstellt, aber wir haben keinen Zugriff darauf (um es beispielsweise zu ändern). Dieses temporäre Objekt wird zum Zwecke der Auswertung des Ausdrucks erstellt und anschließend verworfen. Der von ihm belegte Speicherplatz wird später automatisch von einem Programm namens „Garbage Collector“ freigegeben, dessen Aufgabe es ist, Speicherplatz zurückzugewinnen, der von Objekten belegt wird, auf die von den Programmdaten nicht mehr verwiesen wird.

Betrachten Sie das folgende neue Testprogramm:


using System;
 
namespace Chap2 {
    class Program {
        static void Main() {
            new Personne(new Personne("Jean", "Dupont", 30)).Identifie();
        }
    }
}

und ändern Sie die Konstruktoren der Klasse „Person“, um eine Meldung anzuzeigen:


         // manufacturers
        public Personne(String p, String n, int age) {
            Console.WriteLine("Constructeur Personne(string, string, int)");
            Initialise(p, n, age);
        }
        public Personne(Personne P) {
            Console.Out.WriteLine("Constructeur Personne(Personne)");
            Initialise(P);
}

Wir erhalten folgende Ergebnisse:

1
2
3
Constructeur Personne(string, string, int)
Constructeur Personne(Personne)
[Jean, Dupont, 30]

zeigt die aufeinanderfolgende Konstruktion der beiden temporären Objekte.

4.1.13. Methoden zum Lesen und Schreiben privater Attribute

Wir fügen der Klasse „Person“ Methoden zum Lesen oder Ändern des Zustands von Objektattributen hinzu:


using System;
 
namespace Chap2 {
    public class Personne {
         // attributes
        private string prenom;
        private string nom;
        private int age;
 
         // manufacturers
        public Personne(String p, String n, int age) {
            Console.WriteLine("Constructeur Personne(string, string, int)");
            Initialise(p, n, age);
        }
        public Personne(Personne p) {
            Console.Out.WriteLine("Constructeur Personne(Personne)");
            Initialise(p);
        }
 
         // method
        public void Initialise(string p, string n, int age) {
            this.prenom = p;
            this.nom = n;
            this.age = age;
        }
 
        public void Initialise(Personne p) {
            prenom = p.prenom;
            nom = p.nom;
            age = p.age;
        }
 
         // accessors
        public String GetPrenom() {
            return prenom;
        }
        public String GetNom() {
            return nom;
        }
        public int GetAge() {
            return age;
        }
 
         //modifiers
        public void SetPrenom(String P) {
            this.prenom = P;
        }
        public void SetNom(String N) {
            this.nom = N;
        }
        public void SetAge(int age) {
            this.age = age;
        }
 
         // method
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }
 
}

Wir testen die neue Klasse mit dem folgenden Programm:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Personne p = new Personne("Jean", "Michelin", 34);
            Console.Out.WriteLine("p=(" + p.GetPrenom() + "," + p.GetNom() + "," + p.GetAge() + ")");
            p.SetAge(56);
            Console.Out.WriteLine("p=(" + p.GetPrenom() + "," + p.GetNom() + "," + p.GetAge() + ")");
        }
    }
}

und wir erhalten folgende Ergebnisse:

1
2
3
Constructeur Personne(string, string, int)
p=(Jean,Michelin,34)
p=(Jean,Michelin,56)

4.1.14. Die Eigenschaften

Eine weitere Möglichkeit, auf die Attribute einer Klasse zuzugreifen, ist die Erstellung von Eigenschaften. Diese ermöglichen es uns, private Attribute so zu manipulieren, als wären sie öffentlich.

Betrachten wir die Klasse Personen, in der die bisherigen Accessoren und Modifikatoren durch Lese- und Schreib-Eigenschaften ersetzt wurden:


using System;
 
namespace Chap2 {
    public class Personne {
         // attributes
        private string prenom;
        private string nom;
        private int age;
 
         // manufacturers
        public Personne(String p, String n, int age) {
            Initialise(p, n, age);
        }
        public Personne(Personne p) {
            Initialise(p);
        }
 
         // method
        public void Initialise(string p, string n, int age) {
            this.prenom = p;
            this.nom = n;
            this.age = age;
        }
 
        public void Initialise(Personne p) {
            prenom = p.prenom;
            nom = p.nom;
            age = p.age;
        }
 
         // properties
        public string Prenom {
            get { return prenom; }
            set {
                 // valid first name?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("prénom (" + value + ") invalide");
                } else {
                    prenom = value;
                }
             }//if
         }//first name
 
        public string Nom {
            get { return nom; }
            set {
                 // valid name?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("nom (" + value + ") invalide");
                } else { nom = value; }
             }//if
         }//name
 
 
        public int Age {
            get { return age; }
            set {
                 // valid age?
                if (value >= 0) {
                    age = value;
                } else
                    throw new Exception("âge (" + value + ") invalide");
             }//if
         }//age

         // method
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }
 
}

Mit einer Eigenschaft können Sie den Wert eines Attributs lesen (get) oder festlegen (set). Eine Eigenschaft wird wie folgt deklariert:

public Type Propriété{
    get {...}
    set {...}
}

wobei „Type“ der Typ des Attributs sein muss, das von der Eigenschaft verwaltet wird. Es kann zwei Methoden namens „get“ und „set“ haben. Die Methode „get“ ist in der Regel dafür zuständig, den Wert des von ihr verwalteten Attributs auszugeben (sie könnte auch etwas anderes ausgeben, dem steht nichts im Wege). Die Methode „set“ erhält einen Parameter namens „value“, den sie normalerweise dem von ihr verwalteten Attribut zuweist. Sie kann dies nutzen, um die Gültigkeit des empfangenen Werts zu überprüfen und gegebenenfalls eine Ausnahme auszulösen, falls sich der Wert als ungültig erweist. Genau das tut „ici“.

Wie werden diese Methoden „get“ und „set“ aufgerufen? Betrachten Sie das folgende Testprogramm:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Personne p = new Personne("Jean", "Michelin", 34);
            Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
            p.Age = 56;
            Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
            try {
                p.Age = -4;
            } catch (Exception ex) {
                Console.Error.WriteLine(ex.Message);
             }//try-catch
        }
    }
}

In der


    Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");

Wir suchen nach den Werten für „Prenown“, „Nom“ und „Age“ der Person p. Dies ist die Abfrage dieser Eigenschaften, die dann aufgerufen wird und den Wert des von ihr verwalteten Attributs zurückgibt.

In der

        p.Age=56;

wollen wir den Wert der Eigenschaft Age festlegen. Dies ist die Set-Methode, die anschließend aufgerufen wird. Sie erhält den Wert 56 als Parameter.

Eine Eigenschaft P einer Klasse C, die nur den Get-Zugriff definiert, wird als schreibgeschützt bezeichnet. Ist c ein Objekt der Klasse C, wird die Operation c.P=valeur vom Compiler abgelehnt.

Die Ausführung des vorangegangenen Testprogramms liefert folgende Ergebnisse:

1
2
3
p=(Jean,Michelin,34)
p=(Jean,Michelin,56)
âge (-4) invalide

Eigenschaften ermöglichen es uns, private Attribute so zu behandeln, als wären sie öffentlich. Eine weitere Eigenschaft von Eigenschaften ist, dass sie in Verbindung mit einem Konstruktor unter Verwendung der folgenden Syntax verwendet werden können:

Classe objet=new Classe (...) {Propriété1=val1, Propriété2=val2, ...}

Diese Syntax entspricht dem folgenden Code:

1
2
3
4
Classe objet=new Classe(...);
objet.Propriété1=val1;
objet.Propriété2=val2;
...

Die Reihenfolge der Eigenschaften spielt keine Rolle. Hier ist ein Beispiel.

Die Klasse Person fügt einen neuen Konstruktor ohne Parameter hinzu:


        public Personne() {
}

Der Konstruktor initialisiert die Mitglieder des Objekts nicht. Dies wird als Standardkonstruktor bezeichnet. Er wird verwendet, wenn die Klasse keinen Konstruktor definiert.

Der folgende Code erstellt und initialisiert (Zeile 6) eine neue Person mit der oben dargestellten Syntax:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Personne p2 = new Personne { Age = 7, Prenom = "Arthur", Nom = "Martin" };
            Console.WriteLine("p2=({0},{1},{2})", p2.Prenom, p2.Nom, p2.Age);
        }
    }
}

In Zeile 6 oben wird der Konstruktor ohne Parameter Person() verwendet. In diesem speziellen Fall hätten wir auch schreiben können


            Personne p2 = new Personne() { Age = 7, Prenom = "Arthur", Nom = "Martin" };

Die Klammern des Konstruktors Person() ohne Parameter sind in dieser Syntax jedoch nicht zwingend erforderlich.

Die Ergebnisse lauten wie folgt:

p2=(Arthur,Martin,7)

In vielen Fällen lesen und schreiben die Get- und Set-Eigenschaften lediglich ein privates Feld, ohne weitere Verarbeitung. In diesem Szenario können wir eine automatische Deklaration wie folgt verwenden:

public Type Propriété{ get ; set ; }

Das mit der Eigenschaft verbundene private Feld wird nicht deklariert. Es wird automatisch vom Compiler generiert. Es ist nur über die Eigenschaft zugänglich. Anstatt also zu schreiben:


    private string prenom;
...
    // propriété associée
        public string Prenom {
            get { return prenom; }
            set {
                // prénom valide ?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("prénom (" + value + ") invalide");
                } else {
                    prenom = value;
                }
            }//if
        }//prenom

können wir schreiben:

public string Prenom {get; set;}

ohne das private Feld „first name“ zu deklarieren. Der Unterschied zwischen den beiden vorherigen Eigenschaften besteht darin, dass die erste die Gültigkeit des Vornamens beim Setzen überprüft, während die zweite dies nicht tut.

Verwenden Sie die automatische Eigenschaft „Vorname“, um ein Feld „Vorname“ als öffentlich zu deklarieren:

public string Prenom;

Wir fragen uns, ob es einen Unterschied zwischen den beiden Deklarationen gibt. Es wird nicht empfohlen, ein Klassenfeld als public zu deklarieren. Dies verstößt gegen das Konzept der Kapselung des Zustands eines Objekts, der privat sein und über öffentliche Methoden zugänglich gemacht werden muss.

Wenn die automatische Eigenschaft als virtual deklariert wird, kann sie in einer Unterklasse neu definiert werden:


    class Class1 {
        public virtual string Prop { get; set; }
}

    class Class2 : Class1 {
        public override string Prop { get { return base.Prop; } set {... } }
}

In Zeile 2 oben kann die abgeleitete Klasse Class2 im Set-Code die Gültigkeit des Werts überprüfen, der der automatischen Eigenschaft base.Prop der übergeordneten Klasse Class1 zugewiesen wird.

4.1.15. Klassenmethoden und -attribute

Nehmen wir an, wir möchten die Anzahl der in einer Anwendung erstellten Objekte vom Typ Person zählen. Sie können einen Zähler selbst verwalten, laufen dabei jedoch Gefahr, die hier und da erstellten temporären Objekte zu vergessen. Es erscheint sicherer, in den Konstruktoren der Klasse Person eine Anweisung einzufügen, die einen Zähler inkrementiert. Das Problem besteht darin, eine Referenz auf diesen Zähler zu übergeben, damit der Konstruktor ihn inkrementieren kann: Es muss ein neuer Parameter übergeben werden. Alternativ können Sie den Zähler in die Klassendefinition aufnehmen. Da es sich um ein Attribut der Klasse selbst und nicht um eine bestimmte Instanz dieser Klasse handelt, deklarieren wir es anders mit dem Schlüsselwort static:


        private static long nbPersonnes;

Um darauf zu verweisen, schreiben wir Personne.nbPersonnes, um zu zeigen, dass es sich um ein Attribut der Klasse Person selbst handelt. Hier haben wir ein privates Attribut erstellt, auf das außerhalb der Klasse nicht direkt zugegriffen werden kann. Wir erstellen daher eine öffentliche Eigenschaft, um Zugriff auf das Klassenattribut nbPersonnes zu gewähren. Um den Wert von nbPersonnes zurückzugeben, benötigt die Methode get dieser Eigenschaft kein bestimmtes Objekt Person: Tatsächlich ist nbPersonnes das Attribut einer gesamten Klasse. Daher benötigen wir eine Eigenschaft, die ebenfalls als static deklariert ist:


        public static long NbPersonnes {
            get { return nbPersonnes; }
}

was von außen mit der Syntax Personne.NbPersonnes aufgerufen wird. Hier ist ein Beispiel.

Die Klasse Personnen sieht dann wie folgt aus:


using System;
 
namespace Chap2 {
    public class Personne {
 
         // class attributes
        private static long nbPersonnes;
        public static long NbPersonnes {
            get { return nbPersonnes; }
        }
 
         // instance attributes
        private string prenom;
        private string nom;
        private int age;
 
         // manufacturers
        public Personne(String p, String n, int age) {
            Initialise(p, n, age);
            nbPersonnes++;
        }
        public Personne(Personne p) {
            Initialise(p);
            nbPersonnes++;
        }
 
...
}

In den Zeilen 20 und 24 erhöhen die Builder das statische Feld in Zeile 7.

Mit dem folgenden Programm:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Personne p1 = new Personne("Jean", "Dupont", 30);
            Personne p2 = new Personne(p1);
            new Personne(p1);
            Console.WriteLine("Nombre de personnes créées : " + Personne.NbPersonnes);
        }
    }
}

Wir erhalten folgende Ergebnisse:

    Nombre de personnes créées : 3

4.1.16. Eine Tabelle mit Personen

Ein Objekt ist wie jede andere Datenart auch, und als solches können mehrere Objekte in einer Tabelle zusammengefasst werden:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
             // a table of people
            Personne[] amis = new Personne[3];
            amis[0] = new Personne("Jean", "Dupont", 30);
            amis[1] = new Personne("Sylvie", "Vartan", 52);
            amis[2] = new Personne("Neil", "Armstrong", 66);
             // display
            foreach (Personne ami in amis) {
                ami.Identifie();
            }
        }
    }
}
  • Zeile 7: Erstellt ein Array mit 3 Elementen vom Typ Person. Diese 3 Elemente werden hier mit dem Wert null initialisiert, d. h., sie verweisen auf keine Objekte. Auch dies ist eine falsche Verwendung des Begriffs „Array von Objekten“, da es sich in Wirklichkeit nur um ein Array von Objektreferenzen handelt. Die Erstellung des Arrays von Objekten, das selbst ein Objekt ist (Vorhandensein von new), erzeugt keine Objekte desselben Typs wie seine Elemente.
  • Zeilen 8–10: Erstellung von 3 Objekten vom Typ Person
  • Zeilen 12–14: Anzeige des Tabelleninhalts „friends“

Wir erhalten folgende Ergebnisse:

1
2
3
[Jean, Dupont, 30]
[Sylvie, Vartan, 52]
[Neil, Armstrong, 66]

4.2. Vermächtnis durch Vorbild

4.2.1. Allgemeines

Wir führen hier den Begriff der Vererbung ein. Das Ziel der Vererbung ist es, eine bestehende Klasse an unsere Bedürfnisse anzupassen. Angenommen, wir möchten eine Klasse „Enstructor“ erstellen: Ein Lehrer ist eine besondere Person. Er hat Eigenschaften, die keine andere Person hat: zum Beispiel das Fach, das er unterrichtet. Aber er hat auch die Eigenschaften jeder anderen Person: Vorn name, Nachname und Alter. Ein Lehrer ist daher vollständig Teil der Klasse Person, verfügt jedoch über zusätzliche Attribute. Anstatt eine Klasse Enstructor von Grund auf neu zu schreiben, möchten wir lieber das, was wir in der Klasse Person gelernt haben, an den besonderen Charakter der Lehrer anpassen. Das Konzept der Vererbung macht dies möglich.

Um auszudrücken, dass die Klasse „Teacher“ Eigenschaften von der Klasse „Person“ erbt, schreiben wir:

    public class Enseignant : Personne

„Person“ wird als übergeordnete Klasse bezeichnet, „Enseignant“ als abgeleitete (oder untergeordnete) Klasse. Ein Objekt vom Typ „Enseignant“ verfügt über alle Eigenschaften eines Objekts vom Typ „Person“: Es hat dieselben Attribute und Methoden. Diese Attribute und Methoden der übergeordneten Klasse werden in der Definition der untergeordneten Klasse nicht wiederholt: Wir geben lediglich die Attribute und Methoden an, die von der untergeordneten Klasse hinzugefügt wurden:

Wir nehmen an, dass Person wie folgt definiert ist:


using System;
 
namespace Chap2 {
    public class Personne {
 
         // class attributes
        private static long nbPersonnes;
        public static long NbPersonnes {
            get { return nbPersonnes; }
        }
 
         // instance attributes
        private string prenom;
        private string nom;
        private int age;
 
         // manufacturers
        public Personne(String prenom, String nom, int age) {
            Nom = nom;
            Prenom = prenom;
            Age = age;
            nbPersonnes++;
            Console.WriteLine("Constructeur Personne(string, string, int)");
        }
        public Personne(Personne p) {
            Nom = p.Nom;
            Prenom = p.Prenom;
            Age = p.Age;
            nbPersonnes++;
            Console.WriteLine("Constructeur Personne(Personne)");
        }
 
         // properties
        public string Prenom {
            get { return prenom; }
            set {
                 // valid first name?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("prénom (" + value + ") invalide");
                } else {
                    prenom = value;
                }
             }//if
         }//first name
 
        public string Nom {
            get { return nom; }
            set {
                 // valid name?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("nom (" + value + ") invalide");
                } else { nom = value; }
             }//if
         }//name
 
        public int Age {
            get { return age; }
            set {
                 // valid age?
                if (value >= 0) {
                    age = value;
                } else
                    throw new Exception("âge (" + value + ") invalide");
             }//if
         }//age
 
         // property
        public string Identite {
            get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age);}
        }
    }
 
}

Die Methode „Identifies“ wurde durch die Identität ersetzt, die die Person identifiziert. Wir erstellen eine Klasse „Enstructor“, die von „Person“ erbt:


using System;
 
namespace Chap2 {
    class Enseignant : Personne {
         // attributes
        private int section;
 
         // manufacturer
        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {
             // the section is saved using the Section property
            Section = section;
             // follow-up
            Console.WriteLine("Construction Enseignant(string, string, int, int)");
         }//manufacturer
 
         // property Section
        public int Section {
            get { return section; }
            set { section = value; }
         }// Section
 
    }
}

Die Klasse „Teacher“ erweitert die Methoden und Attribute der Klasse „Person“ um:

  • Zeile 4: Die Klasse „Lehrer“ leitet von der Klasse „Person“ ab
  • Zeile 6: ein Attribut „Section“, das die Sektionsnummer angibt, zu der der Lehrer im Lehrerkollegium gehört (grob gesagt eine Sektion pro Fach). Auf dieses private Attribut kann über die öffentliche Eigenschaft „Section“ in den Zeilen 18–21 zugegriffen werden
  • Zeile 9: ein neuer Konstruktor zur Initialisierung aller Lehrer-Attribute

4.2.2. Erstellen eines Teacher-Objekts

Eine Mädchenklasse erbt keine Konstruktoren ihrer übergeordneten Klasse. Sie muss daher ihre eigenen Konstruktoren definieren. Der Konstruktor der Klasse „Enstruktur“ lautet wie folgt:


         // manufacturer
        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {
             // section is memorized
            Section = section;
             // follow-up
            Console.WriteLine("Construction enseignant(string, string, int, int)");
}//manufacturer

Die Deklaration


        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {

gibt an, dass der Konstruktor vier Parameter (Vorname, Nachname, Alter, Abteilung) sowie drei weitere (Vorname, Nachname, Alter) an seine Basisklasse, in diesem Fall die Klasse „Person“, übergibt. Wir wissen, dass diese Klasse über einen Konstruktor „Person(string, string, int)“ verfügt, der anhand der übergebenen Parameter (Vorname, Nachname, Alter) eine Person erstellt. Sobald die Erstellung der Basisklasse abgeschlossen ist, wird die Erstellung der Klasse „Teacher“ mit der Ausführung des Konstruktor-Körpers fortgesetzt:


            // on mémorise la section
            Section = section;

Beachten Sie, dass links vom Gleichheitszeichen nicht die Eigenschaft „section“ des Objekts verwendet wird, sondern die damit verbundene „Section“. Dadurch kann der Konstruktor alle Gültigkeitsprüfungen nutzen, die von dieser Methode durchgeführt werden. So muss man sie nicht an zwei verschiedenen Stellen platzieren: im Konstruktor und in der Eigenschaft.

Kurz gesagt, der Konstruktor einer abgeleiteten Klasse:

  • übermittelt an seine Basisklasse die Parameter, die er benötigt, um sich selbst zu erstellen
  • verwendet die anderen Parameter, um seine eigenen Attribute zu initialisieren

Wir hätten es vielleicht vorgezogen, zu schreiben:


// constructeur
  public Enseignant(string prenom, string nom, int age, int section){
    this.prenom=prenom;
        this.nom=nom;
        this.age=age;
      this.section=section;
  }

Das ist nicht möglich. Die Klasse Person hat ihre drei Felder Vorname, Name und Alter als privat (private) deklariert. Nur Objekte derselben Klasse haben direkten Zugriff auf diese Felder. Alle anderen Objekte, einschließlich untergeordneter Objekte wie ici, müssen öffentliche Methoden verwenden, um darauf zuzugreifen. Es wäre anders gewesen, wenn die Klasse Person die drei Felder als geschützt (protected) deklariert hätte: Dies hätte abgeleiteten Klassen direkten Zugriff auf die drei Felder ermöglicht. In unserem Beispiel war die Verwendung des Konstruktors der übergeordneten Klasse daher die richtige Lösung, und dies ist die übliche Vorgehensweise: Beim Erstellen eines untergeordneten Objekts rufen wir zunächst den Konstruktor des übergeordneten Objekts auf und führen dann die für das untergeordnete Objekt spezifischen Initialisierungen durch (in unserem Beispiel section).

Probieren wir ein erstes Testprogramm aus [Program.cs]:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite);
        }
    }
}

Dieses Programm erstellt einfach einen Enstructor (new) und identifiziert ihn. Die Klasse Enstructor hat keine Methode Identite, aber ihre übergeordnete Klasse hat eine, die ebenfalls öffentlich ist: Durch Vererbung wird sie zu einer öffentlichen Methode der Klasse Enstructor.

Das gesamte Projekt sieht wie folgt aus:

Die Ergebnisse lauten wie folgt:

1
2
3
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
[Jean, Dupont, 30]

Wir sehen, dass:

  • ein Objekt Person (Zeile 1) vor dem Objekt Lehrer (Zeile 2) erstellt wurde
  • die ermittelte Identität ist die des Objekts Person

4.2.3. Neudefinition einer Methode oder Eigenschaft

Im vorherigen Beispiel hatten wir die Identität des Teils „Person“, aber es fehlen einige klassenspezifische Informationen zu „Enstructor“ (dem Abschnitt). Dies veranlasst uns, eine Eigenschaft zu definieren, die den Lehrer identifiziert:


using System;
 
namespace Chap2 {
    class Enseignant : Personne {
         // attributes
        private int section;
 
         // manufacturer
        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {
             // the section is saved using the Section property
            Section = section;
             // follow-up
            Console.WriteLine("Construction Enseignant(string, string, int, int)");
         }//manufacturer
 
         // property Section
        public int Section {
            get { return section; }
            set { section = value; }
         }// section
 
         // property Identity
        public new string Identite {
            get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
        }
    }
}

Zeilen 24–26: Die Eigenschaft „Identite“ der Klasse „Enseignant“ basiert auf der „Identite“ ihrer übergeordneten Klasse (base.Identite) (Zeile 25), um die „Person“ anzuzeigen, und wird dann durch den für „Enseignant“ spezifischen Abschnitt ergänzt. Beachten Sie die Deklaration der Eigenschaft „Identite“:


    public new string Identite{

Sei ein Objekt teacher E. Dieses Objekt enthält eine Person :

Die Eigenschaft Identity ist sowohl in der Klasse Teacher als auch in ihrer übergeordneten Klasse Person definiert. In der Klasse Teacher muss der Eigenschaft Identity das Schlüsselwort new vorangestellt werden, um anzuzeigen, dass eine neue Eigenschaft Identity für die Klasse Teacher neu definiert wird.


    public new string Identite{

Die Klasse „Teacher“ verfügt nun über zwei Eigenschaften „Identity“:

  • die von der übergeordneten Klasse „Person“ geerbte
  • ihre eigene

Wenn E ein Enstructor ist, bezeichnet E.Identite die Eigenschaft Identite der Klasse Enstructor. Wir sagen, dass die Eigenschaft Identite der Klasse Enstructor die Eigenschaft Identite der übergeordneten Klasse neu definiert oder verdeckt. Allgemein gilt: Wenn O ein Objekt und M eine Methode ist, sucht das System zur Ausführung von O.M in der folgenden Reihenfolge nach einer Methode M:

  • in O
  • in seiner übergeordneten Klasse, falls vorhanden
  • in der übergeordneten Klasse seiner übergeordneten Klasse, falls diese existiert
  • usw.

Die Vererbung ermöglicht es Ihnen, gleichnamige Methoden/Eigenschaften der übergeordneten Klasse in der untergeordneten Klasse neu zu definieren. So können Sie die untergeordnete Klasse an Ihre eigenen Bedürfnisse anpassen. In Kombination mit dem Polymorphismus, den wir gleich betrachten werden, ist das Neudefinieren von Methoden/Eigenschaften der Hauptvorteil der Vererbung.

Betrachten wir dasselbe Testprogramm wie oben:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite);
        }
    }
}

Die diesmal erzielten Ergebnisse lauten wie folgt:

1
2
3
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Enseignant[[Jean, Dupont, 30],27]

4.2.4. Polymorphismus

Betrachten wir eine Klassenhierarchie: C0 C1 C2 … ← Cn

wobei Ci Cj angibt, dass Cj von Ci abgeleitet ist. Das bedeutet, dass Cj alle Eigenschaften von Ci sowie weitere Eigenschaften besitzt. Seien Oi Objekte vom Typ Ci. Es ist zulässig zu schreiben:

    Oi=Oj avec j>i

Tatsächlich besitzt Cj durch Vererbung alle Eigenschaften der Klasse Ci sowie weitere. Ein Oj vom Typ Cj enthält also ein Objekt vom Typ Ci. Die Operation

    Oi=Oj

bedeutet, dass Oi eine Referenz auf das Objekt vom Typ Ci ist, das im Objekt Oj enthalten ist.

Die Tatsache, dass eine Variable vom Typ Ci nicht nur auf ein Objekt der Klasse Ci verweisen kann, sondern auf jedes beliebige von Ci abgeleitete Objekt, wird als Polymorphismus bezeichnet: die Fähigkeit einer Variablen, auf Objekte unterschiedlicher Typen zu verweisen.

Betrachten wir ein Beispiel und nehmen wir die folgende klassenunabhängige (statische) Funktion:

    public static void Affiche(Personne p){
        ….
    }

Wir könnten genauso gut schreiben

    Personne p;
    ...
    Affiche(p);

dass

    Enseignant e;
    ...
    Affiche(e);

Im letzteren Fall erhält die statische Methode Affiche des formalen Parameters p vom Typ Person einen Wert vom Typ Enstructor. Da der Typ Teacher von Person abgeleitet ist, ist dies zulässig.

4.2.5. Neudefinition und Polymorphismus

Vervollständigen wir unsere Methode Affiche:


        public static void Affiche(Personne p) {
             // displays identity of p
            Console.WriteLine(p.Identite);
}//poster

Die Eigenschaft p.Identite gibt eine Zeichenfolge zurück, die das Objekt Person p identifiziert. Was passiert im vorherigen Beispiel, wenn der an das Poster übergebene Parameter ein Objekt vom Typ Teacher ist:


            Enseignant e = new Enseignant(...);
            Affiche(e);

Sehen wir uns das folgende Beispiel an:


using System;
 
namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
             // a teacher
            Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
            Affiche(e);
             // a person
            Personne p = new Personne("Jean", "Dupont", 30);
            Affiche(p);
        }
 
         // poster
        public static void Affiche(Personne p) {
             // displays identity of p
            Console.WriteLine(p.Identite);
         }//poster
    }
}

Die Ergebnisse lauten wie folgt:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
[Lucile, Dumas, 56]
Constructeur Personne(string, string, int)
[Jean, Dupont, 30]

Die Ausführung zeigt, dass p.Identite (Zeile 17) die Identität einer Person ermittelt hat, zunächst (Zeile 7) die in dem Lehrer e enthaltene Person, dann (Zeile 10) die Person p selbst. Es wurde nicht an das Objekt angepasst, das tatsächlich als Parameter an Poster übergeben wurde. Wir hätten es vorgezogen, die vollständige Identität von e zu erhalten. Dies hätte die Notation p.Identite als Verweis auf die Eigenschaft Identity des Objekts erfordert, auf das p tatsächlich zeigt, anstatt die Eigenschaft Identity des Teils „Person“ des Objekts, auf das p tatsächlich zeigt.

Dieses Ergebnis lässt sich erzielen, indem man Identity in der Basisklasse Person als virtuelle Eigenschaft (virtual) deklariert:


public virtual string Identite {
            get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age); }
        }

Das Schlüsselwort virtual macht Identity zu einer virtuellen Eigenschaft. Dieses Schlüsselwort kann auch auf Methoden angewendet werden. Unterklassen, die eine virtuelle Eigenschaft oder Methode neu definieren, müssen dann das Schlüsselwort override anstelle von new verwenden, um ihre neu definierte Eigenschaft/Methode zu kennzeichnen. Daher wird in der Klasse Teacher die Eigenschaft Identity wie folgt neu definiert:


        public override string Identite {
            get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
}

Das vorstehende Programm liefert dann folgende Ergebnisse:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Enseignant[[Lucile, Dumas, 56],61]
Constructeur Personne(string, string, int)
[Jean, Dupont, 30]

Diesmal haben wir in Zeile 3 die vollständige Identität des Lehrers. Definieren wir nun eine Methode statt einer Eigenschaft neu. Die Klasse object (C#-Alias für System.Object) ist die „Mutterklasse“ aller C#-Klassen. Wenn Sie also schreiben:

    public class Personne

schreiben wir implizit:

    public class Personne : System.Object

Die Klasse System.Object definiert eine virtuelle Methode ToString:

Die Methode ToString gibt den Namen der Klasse zurück, zu der das Objekt gehört, wie im folgenden Beispiel gezeigt:


using System;
 
namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
             // a teacher
            Console.WriteLine(new Enseignant("Lucile", "Dumas", 56, 61).ToString());
             // a person
            Console.WriteLine(new Personne("Jean", "Dupont", 30).ToString());
        }
    }
}

Die Ergebnisse lauten wie folgt:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Chap2.Enseignant
Constructeur Personne(string, string, int)
Chap2.Personne

Beachten Sie, dass wir zwar die ToString-Methode in den Klassen Person und Lehrer nicht neu definiert haben, das ToString-Objekt der Klasse jedoch den tatsächlichen Klassennamen des Objekts anzeigen konnte.

Lassen Sie uns die ToString-Methode in den Klassen Person und Teacher neu definieren:


        // méthode ToString
        public override string ToString() {
            return Identite;
}

Die Definition ist in beiden Klassen identisch. Betrachten Sie das folgende Testprogramm:


using System;
namespace Chap2 {
    class Program3 {
        public static void Main() {
             // a teacher
            Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
            Affiche(e);
             // a person
            Personne p = new Personne("Jean", "Dupont", 30);
            Affiche(p);
        }
         // poster
        public static void Affiche(Personne p) {
             // displays identity of p
            Console.WriteLine(p);
         }//Poster
    }
}

Betrachten wir die Methode Poster, deren Parameter eine Person p ist. In Zeile 15 hat die Klasse Console keine Variante, die einen Parameter vom Typ Person zulässt. Unter den verschiedenen WriteLine-Methoden gibt es eine, die ein Object akzeptiert. Der Compiler wird diese Methode, WriteLine(Object o), verwenden, da diese Signatur bedeutet, dass o vom Typ Object oder einem davon abgeleiteten Typ sein kann. Da Object die Oberklasse aller Klassen ist, kann jedes Objekt als Parameter an WriteLine übergeben werden, also auch ein Objekt vom Typ Person oder Teacher. Die Methode WriteLine(Object o) schreibt o.ToString() in den Ausgabestrom Out. Da die Methode ToString virtuell ist, wird, falls das Objekt o (vom Typ Object oder einer abgeleiteten Klasse) die Methode ToString neu definiert hat, diese verwendet. Dies ist hier bei Person und Teacher der Fall.

Dies zeigen die Leistungsergebnisse:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Enseignant[[Lucile, Dumas, 56],61]
Constructeur Personne(string, string, int)
[Jean, Dupont, 30]

4.3. Die Bedeutung eines Operators für eine Klasse neu definieren

4.3.1. Einführung

Betrachten wir die Anweisung

op1 + op2

, wobei op1 und op2 zwei Operanden sind. Es ist möglich, die Bedeutung des Operators + neu zu definieren. Wenn der Operand op1 ein Objekt der Klasse C1 ist, muss in C1 eine statische Methode mit der folgenden Signatur definiert werden:

public static [type] operator +(C1 opérande1, C2 opérande2);

Wenn der Compiler auf den

op1 + op2

Er übersetzt dies dann als C1.operator+(op1,op2). Der vom Operator zurückgegebene Typ ist wichtig. Betrachten wir die Operation op1+op2+op3. Sie wird vom Compiler als (op1+op2)+op3 übersetzt. Sei res12 das Ergebnis von op1+op2. Die nächste Operation ist res12+op3. Wenn der Typ von res12 C1 ist, wird sie ebenfalls als C1.operator+(res12,op3) übersetzt. Dies ermöglicht die Verkettung von Operationen.

Unäre Operatoren mit einem einzigen Operanden können ebenfalls neu definiert werden. Wenn op1 beispielsweise ein Objekt vom Typ C1 ist, kann die Operation op1++ durch eine statische Methode von C1 neu definiert werden:

public static [type] operator ++(C1 opérande1);

Was hier gesagt wurde, gilt für die meisten Operatoren mit wenigen Ausnahmen:

  • Die Operatoren == und != müssen gleichzeitig neu definiert werden
  • Die Operatoren &&, ||, [], (), +=, -=, ... können nicht neu definiert werden

4.3.2. Ein Beispiel

Wir erstellen eine von ArrayList abgeleitete Klasse ListeDePersonnes. Diese Klasse implementiert eine dynamische Liste und wird im folgenden Kapitel vorgestellt. Wir verwenden nur die folgenden Elemente dieser Klasse:

  • die Methode L.Add(Object o), um der Liste L ein Objekt o hinzuzufügen. Hier ist das Objekt o ein Objekt vom Typ Person.
  • die Eigenschaft L.Count, die die Anzahl der Elemente in der Liste L angibt
  • die Notation L[i], die das Element i der Liste L angibt

Die Klasse „ListeDePersonnes“ erbt alle Attribute, Methoden und Eigenschaften der ArrayList. Ihre Definition lautet wie folgt:


using System;
using System.Collections;
using System.Text;
 
namespace Chap2 {
    class ListeDePersonnes : ArrayList{
         // redefine + operator, to add a person to the list
        public static ListeDePersonnes operator +(ListeDePersonnes l, Personne p) {
             // person p is added to the ListeDePersonnes l
            l.Add(p);
             // we return the ListeDePersonnes l
            return l;
         }// operator +
 
         // ToString
        public override string ToString() {
             // render (él1, él2, ..., éln)
             // opening parenthesis
            StringBuilder listeToString = new StringBuilder("(");
             // browse the list of people (this)
            for (int i = 0; i < Count - 1; i++) {
                listeToString.Append(this[i]).Append(",");
            }//for
             // last element
            if (Count != 0) {
                listeToString.Append(this[Count-1]);
            }
             // closing parenthesis
            listeToString.Append(")");
             // you must return a string
            return listeToString.ToString();
         }//ToString
    }
}
  • Zeile 6: Die Klasse „ListeDePersonnes“ ist von der Klasse „ArrayList“ abgeleitet
  • Zeilen 8–13: Definition des Operators + für die Operation l + p, wobei l vom Typ ListeDePersonnes und p vom Typ Person oder einer davon abgeleiteten Klasse ist.
  • Zeile 10: Die Person p wird zur Liste l hinzugefügt. Hier wird die Add-Methode der übergeordneten Klasse ArrayList verwendet.
  • Zeile 12: Die Referenz auf die Liste l wird so dargestellt, dass +-Operatoren verkettet werden können, wie in l + p1 + p2. Die Operation l+p1+p2 wird (gemäß der Operatorpriorität) als (l+p1)+p2 interpretiert. Die Operation l+p1 erzeugt die Referenz l. Die Operation (l+p1)+p2 wird dann zu l+p2, wodurch die Person p2 zur Liste der Personen l hinzugefügt wird.
  • Zeile 16: Wir definieren ToString neu, um eine Liste von Personen als (person1, person2, ..) anzuzeigen, wobei personi selbst das Ergebnis der ToString-Methode der Klasse Person ist.
  • Zeile 19: Wir verwenden ein Objekt vom Typ StringBuilder. Diese Klasse eignet sich besser als die String-Klasse, sobald zahlreiche String-Operationen erforderlich sind, in diesem Fall Hinzufügungen. Tatsächlich erzeugt jede Operation an einem String ein neues String-Objekt, während dieselben Operationen an einem StringBuilder das Objekt ändern, ohne ein neues zu erstellen. Wir verwenden die Methode Append, um Strings zu verketten.
  • Zeile 21: Durchlaufen der Elemente der Liste der Personen. Diese Liste wird hier durch this bezeichnet. Dies ist das aktuelle Objekt, auf das sich ToString bezieht. Die Eigenschaft Count ist eine Eigenschaft der übergeordneten Klasse ArrayList.
  • Zeile 22: Das Element Nr. i in der aktuellen Liste this ist über die Notation this[i] zugänglich. Auch hier handelt es sich um eine Eigenschaft der ArrayList. Da es um das Hinzufügen von Strings geht, wird this[i].ToString() verwendet. Da es sich um eine virtuelle Methode handelt, wird die ToString-Methode des this-Objekts vom Typ Person oder einer davon abgeleiteten Klasse verwendet.
  • Zeile 31: Wir müssen ein Objekt vom Typ String zurückgeben (Zeile 16). Die Klasse StringBuilder verfügt über eine Methode ToString, mit der man von einem StringBuilder zu einem String-Typ wechseln kann.

Beachten Sie, dass die ListeDePersonnes keinen Konstruktor hat. In diesem Fall wissen wir, dass die

public ListeDePersonnes(){
}

verwendet wird. Dieser Konstruktor führt nichts anderes aus, als den parameterlosen Konstruktor seiner übergeordneten Klasse aufzurufen:

public ArrayList(){
...
}

Eine Testklasse könnte wie folgt aussehen:


using System;
 
namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
             // a list of people
            ListeDePersonnes l = new ListeDePersonnes();
             // add people
            l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
             // display
            Console.WriteLine("l=" + l);
            l = l + new Enseignant("camille", "germain",27,60);
            Console.WriteLine("l=" + l);
        }
    }
}
  • Zeile 7: Erstellung einer Liste von Personen l
  • Zeile 9: Hinzufügen von 2 Personen mit dem Operator +
  • Zeile 12: Lehrer hinzugefügt
  • Zeilen 11 und 13: Verwendung der neu definierten Methode ListeDePersonnes.ToString().

Die Ergebnisse:

l=([jean, martin, 10],[pauline, leduc, 12])
l=([jean, martin, 10],[pauline, leduc, 12],Enseignant[[camille, germain, 27],60])

4.4. Definition eines Indexers für eine Klasse

Wir verwenden hier weiterhin die Klasse ListeDePersonnes. Wenn l ein Objekt der Klasse ListeDePersonnes ist, möchten wir l[i] verwenden können, um die Person Nr. i in der Liste l sowohl beim Lesen (Person p=l[i]) als auch beim Schreiben (l[i]=new Person(...)) zu bezeichnen.

Um l[i] schreiben zu können, wobei l[i] ein Objekt der Klasse Person bezeichnet, müssen wir für ListeDePersonnes die folgende Methode definieren:


        public Personne this[int i] {
            get { ... }
            set { ... }
}

Die Methode heißt this[int i] und wird als Indexer bezeichnet, da sie dem Ausdruck obj[i] eine Bedeutung verleiht, der an die Array-Notation erinnert, obwohl obj kein Array, sondern ein Objekt ist. Die get-Methode dieses Objekts obj wird aufgerufen, wenn variable = obj[i] ist, und die set-Methode, wenn obj[i] = value geschrieben wird.

Die Klasse ListeDePersonnes ist von der Klasse ArrayList abgeleitet, die selbst über einen Indexer verfügt:

    public object this[int i] { ... }

Es gibt einen Konflikt zwischen der Klasse „ListeDePersonnes“:


 public Personne this[int i] 

und die this-Klasse ArrayList


 public object this[int i] 

da sie denselben Namen und denselben Parametertyp (int) haben. Um anzugeben, dass die Klasse „ListeDePersonnes“ die gleichnamige Methode der Klasse ArrayList „zwischenspeichert“, müssen wir das Schlüsselwort new zur Deklaration von ListeDePersonnes hinzufügen. Wir schreiben daher:


    public new Personne this[int i]{
        get { ... }
        set { ... }
    }

Vervollständigen wir diese Methode. Die Methode this.get wird beispielsweise aufgerufen, wenn variable = l[i] gilt, wobei l vom Typ ListeDePersonnes ist. Wir müssen dann die Person Nr. i aus der Liste l zurückgeben. Dies geschieht mit der Notation base[i], die das Objekt Nr. i der Klasse ArrayList bildet, die der Klasse ListeDePersonnes zugrunde liegt. Das zurückgegebene Objekt ist vom Typ Object, daher ist eine Typumwandlung in die Klasse Person erforderlich.


    public new Personne this[int i]{
        get { return (Personne) base[i]; }
        set { ... }
    }

Die Methode set wird aufgerufen, wenn l[i]=p ist, wobei p eine Person ist. Das Ziel ist es, die Person p dem Element i in l zuzuweisen.


    public new Personne this[int i]{
        get { ... }
        set { base[i]=value; }
    }

Hier wird die durch das Schlüsselwort „value“ dargestellte Person p dem Element Nr. i der Basisklasse ArrayList zugewiesen.

Der Klassenindexer ListeDePersonnes sieht daher wie folgt aus:


    public new Personne this[int i]{
        get { return (Personne) base[i]; }
        set { base[i]=value; }
    }

Nun möchten wir in der Lage sein, Person p=l["name"] zu schreiben, d. h. die Liste l nicht anhand einer Elementnummer, sondern anhand des Namens einer Person zu indizieren. Dazu definieren wir einen neuen Indexer:


        // indexeur via un nom
        public int this[string nom] {
            get {
                // on recherche la personne
                for (int i = 0; i < Count; i++) {
                    if (((Personne)base[i]).Nom == nom)
                        return i;
                }//for
                return -1;
            }//get
}

Die erste Zeile


public int this[string nom]

bedeutet, dass die ListeDePersonnes über einen Zeichenfolgennamen aufgerufen wird und das Ergebnis von l[name] eine Ganzzahl ist. Diese Ganzzahl gibt die Position der Person mit dem Namen „name“ in der Liste an oder -1, falls die Person nicht in der Liste enthalten ist. Es handelt sich um einen reinen Lesezugriff, sodass die Zuweisung l["name"]=value nicht möglich ist, da hierfür die Definition von set erforderlich wäre. Das Schlüsselwort new ist in der Indexer-Deklaration nicht erforderlich, da die Basisklasse ArrayList keinen Indexer this[string] definiert.

Im Hauptteil des get-Ausdrucks wird die Liste der Personen nach dem als Parameter übergebenen Namen durchsucht. Wird dieser an Position i gefunden, wird i zurückgegeben, andernfalls wird -1 zurückgegeben.

Das vorherige Testprogramm wird wie folgt vervollständigt:


using System;
 
namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
             // a list of people
            ListeDePersonnes l = new ListeDePersonnes();
             // add people
            l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
             // display
            Console.WriteLine("l=" + l);
            l = l + new Enseignant("camille", "germain",27,60);
            Console.WriteLine("l=" + l);
             // change item 1
            l[1] = new Personne("franck", "gallon",5);
             // display element 1
            Console.WriteLine("l[1]=" + l[1]);
             // display list l
            Console.WriteLine("l=" + l);
             // people search
            string[] noms = { "martin", "germain", "xx" };
            for (int i = 0; i < noms.Length; i++) {
                int inom = l[noms[i]];
                if (inom != -1)
                    Console.WriteLine("Personne(" + noms[i] + ")=" + l[inom]);
                else
                    Console.WriteLine("Personne(" + noms[i] + ") n'existe pas");
            }//for
        }
    }
}

Die Ausführung liefert folgende Ergebnisse:

1
2
3
4
5
6
7
l=([jean, martin, 10],[pauline, leduc, 12])
l=([jean, martin, 10],[pauline, leduc, 12],Enseignant[[camille, germain, 27],60])
l[1]=[franck, gallon, 5]
l=([jean, martin, 10],[franck, gallon, 5],Enseignant[[camille, germain, 27],60])
Personne(martin)=[jean, martin, 10]
Personne(germain)=Enseignant[[camille, germain, 27],60]
Personne(xx) n'existe pas

4.5. Die Strukturen

Die C#-Struktur entspricht der Struktur der Programmiersprache C und kommt dem Begriff der Klasse sehr nahe. Eine Struktur wird wie folgt definiert:

struct NomStructure{
// attributs
    ...
// propriétés
...
// constructeurs
...
// méthodes
...
}

Trotz ähnlicher Deklarationen gibt es erhebliche Unterschiede zwischen Klassen und Strukturen. Beispielsweise gibt es bei Strukturen keinen Begriff der Vererbung. Wenn wir eine Klasse schreiben, die nicht abgeleitet werden muss, welche Unterschiede zwischen Strukturen und Klassen helfen uns dann bei der Wahl zwischen den beiden? Schauen wir uns das folgende Beispiel an, um das herauszufinden:


using System;
 
namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
             // a sp1 structure
            SPersonne sp1;
            sp1.Nom = "paul";
            sp1.Age = 10;
            Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
             // a sp2 structure
            SPersonne sp2 = sp1;
            Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
             // sp2 is modified
            sp2.Nom = "nicole";
            sp2.Age = 30;
             // checking sp1 and sp2
            Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
            Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
 
             // an op1 object
            CPersonne op1=new CPersonne();
            op1.Nom = "paul";
            op1.Age = 10;
            Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
             // an op2 object
            CPersonne op2=op1;
            Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
             // op2 is modified
            op2.Nom = "nicole";
            op2.Age = 30;
             // op1 and op2 verification
            Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
            Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
        }
    }
     // structure SPersonne
    struct SPersonne {
        public string Nom;
        public int Age;
    }
 
     // class CPersonne
    class CPersonne {
        public string Nom;
        public int Age;
    }
 
}
  • Zeilen 38–41: eine Struktur mit zwei öffentlichen Feldern: Nom, Age
  • Zeilen 44–47: eine Klasse mit zwei öffentlichen Feldern: Nom, Age

Wenn wir dieses Programm ausführen, erhalten wir folgende Ergebnisse:

1
2
3
4
5
6
7
8
sp1=SPersonne(paul,10)
sp2=SPersonne(paul,10)
sp1=SPersonne(paul,10)
sp2=SPersonne(nicole,30)
op1=CPersonne(paul,10)
op2=CPersonne(paul,10)
op1=CPersonne(nicole,30)
op2=CPersonne(nicole,30)

Wo wir zuvor eine Person verwendet haben, verwenden wir nun eine SPersonne:


    struct SPersonne {
        public string Nom;
        public int Age;
}

Die Struktur hat hier keinen Konstruktor. Sie könnte einen haben, wie wir später zeigen werden. Standardmäßig verfügt sie immer über den Konstruktor ohne Parameter, hier SPersonne().

  • Zeile 7 des Codes: die Deklaration

    SPersonne sp1;

entspricht der Anweisung:


    SPersonne sp1=new Spersonne();

Es wird eine Struktur (Name, Alter) erstellt, und der Wert von sp1 ist diese Struktur selbst. Im Falle der Klasse muss die Erstellung des Objekts (Name, Alter) explizit durch den Operator new (Zeile 22) erfolgen:


CPersonne op1=new CPersonne();

Die vorstehende Anweisung erstellt ein CPersonne-Objekt (das in etwa unserer Struktur entspricht), und der Wert von p1 ist dann die Adresse (die Referenz) dieses Objekts.

Zusammenfassend lässt sich sagen

  • Im Fall der Struktur ist der Wert von sp1 die Struktur selbst
  • Im Fall der Klasse ist der Wert von p1 die Adresse des erstellten Objekts

Wenn wir in dem Programm Zeile 12 schreiben:


            SPersonne sp2 = sp1;

wird eine neue Struktur sp2(Name,Age) erstellt und mit dem Wert von sp1, also der Struktur selbst, initialisiert.

Die Struktur von sp1 wird in sp2 dupliziert [1]. Dies ist eine Kopie eines Wertes. Betrachten wir nun die Anweisung in Zeile 27:


CPersonne op2=op1;

Bei Klassen wird der Wert von op1 in op2 kopiert, da dieser Wert jedoch tatsächlich die Objektadresse ist, wird er nicht dupliziert [2].

Im Fall einer Struktur [1] ändert sich der Wert von sp1, wenn wir den Wert von sp2 ändern, wie im Programm gezeigt. Im Fall eines Objekts [2] wird das von op1 angezeigte Objekt geändert, wenn wir das von op2 angezeigte Objekt ändern, da es sich um dasselbe Objekt handelt. Dies wird auch durch die Programmergebnisse verdeutlicht.

Diese Erklärungen zeigen, dass:

  • der Wert einer Strukturvariablen die Struktur selbst ist
  • der Wert einer Objektvariablen ist die Adresse des Objekts, auf das verwiesen wird

Sobald man diesen grundlegenden Unterschied verstanden hat, ist die Struktur der Klasse sehr ähnlich, wie das folgende neue Beispiel zeigt:


using System;
 
namespace Chap2 {
 
     // structure SPersonne
    struct SPersonne {
         // private attributes
        private string nom;
        private int age;
 
         // properties
        public string Nom {
            get { return nom; }
            set { nom = value; }
         }//name
 
        public int Age {
            get { return age; }
            set { age = value; }
         }//age
 
         // Manufacturer
        public SPersonne(string nom, int age) {
            this.nom = nom;
            this.age = age;
         }//manufacturer
 
         // ToString
        public override string ToString() {
            return "SPersonne(" + Nom + "," + Age + ")";
         }//ToString
     }//structure
}//namespace
  • Zeilen 8–9: zwei private Felder
  • Zeilen 12–20: zugehörige öffentliche Eigenschaften
  • Zeilen 23–26: Definieren Sie einen Konstruktor. Beachten Sie, dass der Konstruktor ohne Parameter `SPersonne()` immer vorhanden ist und nicht deklariert werden muss. Seine Deklaration wird vom Compiler abgelehnt. Im Konstruktor in den Zeilen 23–26 könnten Sie versucht sein, die privaten Felder `name` und `age` über ihre öffentlichen Eigenschaften `Name` und `Age` zu initialisieren. Dies wird vom Compiler abgelehnt. Strukturmethoden können während der Strukturkonstruktion nicht verwendet werden.
  • Zeilen 29–31: Neudefinition der Methode ToString.

Ein Testprogramm könnte wie folgt aussehen:


using System;
 
namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
             // one person p1
            SPersonne p1=new SPersonne();
            p1.Nom="paul";
            p1.Age= 10;
            Console.WriteLine("p1={0}",p1);
             // one person p2
            SPersonne p2 = p1;
            Console.WriteLine("p2=" + p2);
             // p2 is modified
            p2.Nom = "nicole";
            p2.Age = 30;
             // checking p1 and p2
            Console.WriteLine("p1=" + p1);
            Console.WriteLine("p2=" + p2);
             // one person p3
            SPersonne p3 = new SPersonne("amandin", 18);
            Console.WriteLine("p3=" + p3);
             // one person p4
            SPersonne p4 = new SPersonne { Nom = "x", Age = 10 };
            Console.WriteLine("p4=" + p4);
        }
    }
}
  • Zeile 7: Wir müssen den Konstruktor ohne Parameter explizit verwenden, da es in der Struktur einen weiteren Konstruktor gibt. Hätte die Struktur keinen Konstruktor, würde die Anweisung

            SPersonne p1;

ausgereicht, um eine leere Struktur zu erstellen.

  • Zeilen 8–9: Die Struktur wird über ihre öffentlichen Eigenschaften initialisiert
  • Zeile 10: Die Methode p1.ToString wird in WriteLine verwendet.
  • Zeile 21: Erstellung einer Struktur mit dem Konstruktor SPersonne(string, int)
  • Zeile 24: Erstellen einer Struktur mithilfe des Konstruktors ohne Parameter SPersonne() mit Initialisierung der privaten Felder über ihre öffentlichen Eigenschaften in geschweiften Klammern.

Es werden folgende Ergebnisse erzielt:

1
2
3
4
5
6
p1=SPersonne(paul,10)
p2=SPersonne(paul,10)
p1=SPersonne(paul,10)
p2=SPersonne(nicole,30)
p3=SPersonne(amandin,18)
p4=SPersonne(x,10)

Der einzige nennenswerte Unterschied zwischen Struktur und Klasse besteht darin, dass bei einer Klasse die Objekte p1 und p2 am Ende des Programms auf dasselbe Objekt verwiesen hätten.

4.6. Schnittstellen

Eine Schnittstelle ist eine Menge von Prototyp-Methoden oder -Eigenschaften, die einen Vertrag bilden. Eine Klasse, die sich entscheidet, eine Schnittstelle zu implementieren, verpflichtet sich, eine Implementierung aller in der Schnittstelle definierten Methoden bereitzustellen. Der Compiler überprüft diese Implementierung.

Hier ist zum Beispiel die Schnittstellendefinition System.Collections.IEnumerator :

public interface System.Collections.IEnumerator 


{    // Prop
e   rties Object Curren

t    { get; } 
     // Methods 
     bool MoveNe
xt(); void Reset(); }

Eigenschaften und Methoden einer Schnittstelle werden nur durch ihre Signaturen definiert. Sie sind nicht implementiert (haben keinen Code). Es sind die Klassen, die die Schnittstelle implementieren, die den Methoden und Eigenschaften der Schnittstelle Code zuweisen.

1
2
3
4
5
6
public class C : IEnumerator{
    ...
    Object Current{ get {...}}
    bool MoveNext{...}
    void Reset(){...}
}
  • Zeile 1: Die Klasse C implementiert die Schnittstelle IEnumerator. Beachten Sie, dass das Zeichen : zur Implementierung einer Schnittstelle dasselbe ist wie das zur Ableitung einer Klasse.
  • Zeilen 3–5: Implementierung der Schnittstellenmethoden und -eigenschaften von IEnumerator.

Betrachten Sie die folgende Schnittstelle:


namespace Chap2 {
    public interface IStats {
        double Moyenne { get; }
        double EcartType();
    }
}

Die Schnittstelle IStats bietet:

  • eine schreibgeschützte Eigenschaft Average: zur Berechnung des Durchschnitts einer Reihe von Werten
  • eine Methode „EcartType“: zur Berechnung der Standardabweichung

Beachten Sie, dass nirgendwo angegeben ist, um welche Wertereihen es sich handelt. Es könnte sich um den Notendurchschnitt einer Klasse, den durchschnittlichen Monatsumsatz eines bestimmten Produkts, die Durchschnittstemperatur an einem bestimmten Ort usw. handeln. Dies ist das Prinzip von Schnittstellen: Wir gehen von der Existenz von Methoden im Objekt aus, nicht jedoch von der Existenz spezifischer Daten.

Eine erste Klassenimplementierung von IStats könnte eine Klasse sein, die dazu dient, die Noten der Schüler einer Klasse in einem bestimmten Fach zu speichern. Ein Schüler würde durch die Struktur Student charakterisiert, wie folgt:


    public struct Elève {
        public string Nom { get; set; }
        public string Prénom { get; set; }
}//Student

Der Schüler würde durch Vor- und Nachnamen identifiziert werden. Die Zeilen 2–3 zeigen die automatischen Eigenschaften für diese beiden Attribute.

Eine Notiz würde durch die Struktur „Note“ wie folgt charakterisiert:


    public struct Note {
        public Elève Elève { get; set; }
        public double Valeur { get; set; }
}//Note

Die Note würde durch den bewerteten Schüler und die Note selbst identifiziert werden. Die Zeilen 2–3 zeigen die automatischen Eigenschaften für diese beiden Attribute.

Die Noten aller Schüler in einem bestimmten Fach werden im folgenden in der Klasse TableauDeNotes zusammengefasst:


using System;
using System.Text;
 
namespace Chap2 {
 
    public class TableauDeNotes : IStats {
         // attributes
        public string Matière { get; set; }
        public Note[] Notes { get; set; }
        public double Moyenne { get; private set; }
        private double ecartType;
 
         // manufacturer
        public TableauDeNotes(string matière, Note[] notes) {
             // saving via public properties
            Matière = matière;
            Notes = notes;
             // calculating the average score
            double somme = 0;
            for (int i = 0; i < Notes.Length; i++) {
                somme += Notes[i].Valeur;
            }
            if (Notes.Length != 0) Moyenne = somme / Notes.Length;
            else Moyenne = -1;
             // standard deviation
            double carrés = 0;
            for (int i = 0; i < Notes.Length; i++) {
                carrés += Math.Pow((Notes[i].Valeur - Moyenne), 2);
            }//for
            if (Notes.Length != 0)
                ecartType = Math.Sqrt(carrés / Notes.Length);
            else ecartType = -1;
         }//manufacturer
 
        public double EcartType() {
            return ecartType;
        }
 
         // ToString
        public override string ToString() {
            StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
            int i;
             // concatenate all the notes
            for (i = 0; i < Notes.Length-1; i++) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
            };
             //final note
            if (Notes.Length != 0) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
            }
            valeur.Append(")");
             // end
            return valeur.ToString();
         }//ToString
 
     }//class
}
  • Zeile 6: Die Klasse TableauDeNotes implementiert die Schnittstelle IStats. Sie muss daher die Methoden Average und EcartType implementieren. Diese sind in Zeile 10 (Average) und 35–37 (EcartType) implementiert
  • Zeilen 8–10: drei automatische Eigenschaften
  • Zeile 8: das Fach, dessen Noten das Objekt speichert
  • Zeile 9: Tabelle mit den Noten der Schüler (Student, Grade)
  • Zeile 10: Durchschnittsnote – Eigenschaft, die die Schnittstelle Average von IStats implementiert.
  • Zeile 11: Feld, das die Standardabweichung der Noten speichert – die Methode get, die EcartType in den Zeilen 35–37 zugeordnet ist, implementiert die Schnittstelle EcartType von IStats.
  • Zeile 9: Noten werden in einer Tabelle gespeichert. Diese wird beim Erstellen der Klasse „TableauDeNotes“ an den Konstruktor der Zeilen 14–33 übergeben.
  • Zeilen 14–33: Der Konstruktor. Hier wird davon ausgegangen, dass sich die an den Konstruktor übermittelten Noten in Zukunft nicht ändern werden. Wir verwenden den Konstruktor daher, um sofort den Mittelwert und die Standardabweichung dieser Noten zu berechnen und sie in den Feldern in den Zeilen 10–11 zu speichern. Der Mittelwert wird in dem privaten Feld gespeichert, das der automatischen Eigenschaft Average in Zeile 10 zugrunde liegt, und die Standardabweichung in dem privaten Feld in Zeile 11.
  • Zeile 10: Die Methode get mit automatischer Eigentumszuweisung Average gibt das zugrunde liegende private Feld zurück.
  • Zeilen 35–37: Die Methode „EcartType“ gibt den Wert des privaten Feldes in Zeile 11 zurück.

Dieser Code weist einige Feinheiten auf:

  • Zeile 23: Die Methode „set property Average“ wird verwendet, um die Zuweisung vorzunehmen. Diese Methode wurde in Zeile 10 als privat deklariert, sodass die Zuweisung eines Werts an „Average“ nur innerhalb des Klassenraums möglich ist.
  • Zeilen 40–54: Verwenden Sie ein StringBuilder-Objekt, um die Zeichenkette zu erstellen, die TableauDeNotes darstellt, und so die Leistung zu verbessern. Es ist jedoch zu beachten, dass die Lesbarkeit des Codes dadurch erheblich leidet. Das ist die Kehrseite der Medaille.

In der vorherigen Klasse wurden Notizen in einer Tabelle gespeichert. Es war nicht möglich, eine neue Notiz hinzuzufügen, sobald das TableauDeNotes erstellt war. Wir schlagen nun eine zweite Implementierung von IStats vor, genannt ListeDeNotes, bei der die Notizen diesmal in einer Liste gespeichert werden, mit der Möglichkeit, Notizen nach der anfänglichen Erstellung des Objekts ListeDeNotes hinzuzufügen.

Der Klassencode für ListeDeNotes lautet wie folgt:


using System;
using System.Text;
using System.Collections.Generic;
 
namespace Chap2 {
 
    public class ListeDeNotes : IStats {
         // attributes
        public string Matière { get; set; }
        public List<Note> Notes { get; set; }
        public double moyenne = -1;
        public double ecartType = -1;
 
         // manufacturer
        public ListeDeNotes(string matière, List<Note> notes) {
             // saving via public properties
            Matière = matière;
            Notes = notes;
         }//manufacturer
 
         // add a note
        public void Ajouter(Note note) {
             // add note
            Notes.Add(note);
             // mean and standard deviation reset
            moyenne = -1;
            ecartType = -1;
        }
 
         // ToString
        public override string ToString() {
            StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
            int i;
             // concatenate all the notes
            for (i = 0; i < Notes.Count - 1; i++) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
            };
             //final note
            if (Notes.Count != 0) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
            }
            valeur.Append(")");
             // end
            return valeur.ToString();
         }//ToString
 
         // average score
        public double Moyenne {
            get {
                if (moyenne != -1) return moyenne;
                 // calculating the average score
                double somme = 0;
                for (int i = 0; i < Notes.Count; i++) {
                    somme += Notes[i].Valeur;
                }
                 // we return the average
                if (Notes.Count != 0) moyenne = somme / Notes.Count;
                return moyenne;
            }
        }
 
        public double EcartType() {
             // standard deviation
            if (ecartType != -1) return ecartType;
             // average
            double moyenne = Moyenne;
            double carrés = 0;
            for (int i = 0; i < Notes.Count; i++) {
                carrés += Math.Pow((Notes[i].Valeur - moyenne), 2);
            }//for
             // we return the standard deviation
            if (Notes.Count != 0)
                ecartType = Math.Sqrt(carrés / Notes.Count);
            return ecartType;
        }
     }//class
}
  • Zeile 7: Die Klasse ListeDeNotes implementiert die Schnittstelle IStats
  • Zeile 10: Notizen werden nun in einer Liste statt in einer Tabelle angezeigt
  • Zeile 11: Die automatische Eigenschaft „Average“ der Klasse „TableauDeNotes“ wurde hier zugunsten eines privaten Feldes „average“ (Zeile 11) aufgegeben, das mit der schreibgeschützten öffentlichen Eigenschaft „Average“ (Zeilen 48–60) verknüpft ist
  • Zeilen 22–28: Sie können nun eine Notiz zu den bereits gespeicherten hinzufügen, was zuvor nicht möglich war.
  • Zeilen 15–19: Infolgedessen werden der Mittelwert und die Standardabweichung nicht mehr im Konstruktor berechnet, sondern in den Schnittstellenmethoden selbst: Average (Zeilen 48–60) und EcartType (62–76). Die Neuberechnung wird jedoch nur dann neu gestartet, wenn der Mittelwert und die Standardabweichung von -1 abweichen (Zeilen 50 und 64).

Eine Testklasse könnte wie folgt aussehen:


using System;
using System.Collections.Generic;
 
namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
             // some students & english notes
            Elève[] élèves1 =  { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
            Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
             // which we save in a TableauDeNotes object
            TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
             // average and standard deviation display
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", anglais.Moyenne, anglais.EcartType(), anglais);
             // we put the students and the material in a ListeDeNotes object
            ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
             // average and standard deviation display
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
             // we add a note
            français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
             // average and standard deviation display
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
        }
    }
}
  • Zeile 8: Erstellen eines Arrays von Schülern mithilfe des Konstruktors ohne Parameter und Initialisierung über öffentliche Eigenschaften
  • Zeile 9: Erstellung einer Tabelle mit Notizen unter Verwendung derselben Technik
  • Zeile 11: Ein Objekt TableauDeNotes, dessen Mittelwert und Standardabweichung in Zeile 13 berechnet werden
  • Zeile 15: Ein Objekt „ListeDeNotes“, dessen Mittelwert und Standardabweichung in Zeile 17 berechnet werden. Die Klasse „List<Note>“ verfügt über einen Konstruktor, der ein Objekt akzeptiert, das die Schnittstelle „IEnumerable<Note>“ implementiert. Die Tabelle „notes1“ implementiert diese Schnittstelle und kann zum Erstellen der „List<Note>“ verwendet werden.
  • Zeile 19: neue Notiz hinzugefügt
  • Zeile 21: Neuberechnung von Mittelwert und Standardabweichung

Die Ergebnisse lauten wie folgt:

1
2
3
matière=anglais, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18]), Moyenne=16, Ecart-type=1,63299316185545
matière=français, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18]), Moyenne=16, Ecart-type=1,63299316185545
matière=français, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18],[Jérôme,Jaric,10]), Moyenne=14,5, Ecart-type=2,95803989154981

Im vorherigen Beispiel implementieren zwei Klassen die Schnittstelle IStats. Allerdings zeigt das Beispiel nicht, wie nützlich die Schnittstelle IStats ist. Schreiben wir das Testprogramm wie folgt um:


using System;
using System.Collections.Generic;
 
namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
             // some students & english notes
            Elève[] élèves1 =  { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
            Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
             // which we save in a TableauDeNotes object
            TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
             // average and standard deviation display
            AfficheStats(anglais);
             // we put the students and the material in a ListeDeNotes object
            ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
             // average and standard deviation display
            AfficheStats(français);
             // we add a note
            français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
             // average and standard deviation display
            AfficheStats(français);
        }
 
         // display mean and standard deviation of a type IStats
        static void AfficheStats(IStats valeurs) {
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", valeurs.Moyenne, valeurs.EcartType(), valeurs);
        }
    }
}
  • Zeilen 25–27: Die statische Methode AfficheStats erhält ein IStats, Typ Interface. Das bedeutet, dass der tatsächliche Parameter jedes Objekt sein kann, das IStats implementiert. Wenn Sie Daten mit einem Schnittstellentyp verwenden, bedeutet dies, dass Sie nur die Schnittstellenmethoden nutzen, die von den Daten implementiert werden. Der Rest wird ignoriert. Dies ist eine Eigenschaft, die dem bei Klassen beobachteten Polymorphismus ähnelt. Wenn eine Gruppe von Ci, die nicht durch Vererbung verbunden ist (sodass Sie den Polymorphismus der Vererbung nicht nutzen können), eine Reihe von Methoden mit derselben Signatur aufweist, kann es interessant sein, diese Methoden in einer Schnittstelle I zu gruppieren, die von allen betroffenen Klassen implementiert wird. Instanzen dieser Klassen Ci können dann als effektive Parameter von Funktionen verwendet werden, die einen formalen Parameter vom Typ I, d. h. c.a.d., zulassen, wobei diese Funktionen nur die in I definierten Objektmethoden Ci verwenden und nicht die Attribute und Methoden der einzelnen Klassen Ci.
  • Zeile 13: Die Methode AfficheStats wird mit einem TableauDeNotes aufgerufen, das IStats implementiert
  • Zeile 17: ebenso mit einem Typ ListeDeNotes

Die Ergebnisse dieses Durchlaufs sind identisch mit denen des vorherigen.

Eine Variable kann vom Typ „Interface“ sein. Daher können wir schreiben:

1
2
3
IStats stats1=new TableauDeNotes(...);
...
stats1=new ListeDeNotes(...);

Die Anweisung in Zeile 1 gibt an, dass stats1 die Instanz einer Klasse ist, die IStats implementiert. Diese Anweisung bedeutet, dass der Compiler nur den Zugriff auf die Schnittstellenmethoden von stats1 zulässt: Average und EcartType.

Abschließend sei angemerkt, dass Schnittstellen auf verschiedene Arten implementiert werden können, d. h. sie können wie folgt geschrieben werden:

public class ClasseDérivée:ClasseDeBase,I1,I2,..,In{
...
}

wobei die Ij Schnittstellen sind.

4.7. Abstrakte Klassen

Eine abstrakte Klasse ist eine Klasse, die nicht instanziiert werden kann. Sie müssen abgeleitete Klassen erstellen, die instanziiert werden können.

Abstrakte Klassen können verwendet werden, um den Code einer Reihe von Klassen zu faktorisieren. Betrachten Sie den folgenden Fall:


using System;
 
namespace Chap2 {
    abstract class Utilisateur {
         // fields
        private string login;
        private string motDePasse;
        private string role;
 
         // manufacturer
        public Utilisateur(string login, string motDePasse) {
             // information is recorded
            this.login = login;
            this.motDePasse = motDePasse;
            // on identifie l'utilisateur
            role=identifie();
             // identified?
            if (role == null) {
                throw new ExceptionUtilisateurInconnu(String.Format("[{0},{1}]", login, motDePasse));
            }
        }
 
         // toString
        public override string ToString() {
            return String.Format("Utilisateur[{0},{1},{2}]", login, motDePasse, role);
        }
 
         // identifies
        abstract public string identifie();
    }
}
  • Zeilen 11–21: der Klassen-Builder User. Diese Klasse speichert Informationen über den Benutzer einer Webanwendung. Diese Anwendung hat verschiedene Arten von Benutzern, die sich über Benutzername und Passwort authentifizieren (Zeilen 6–7). Diese beiden Informationen werden für einige Benutzer über einen LDAP-Dienst, für andere über ein Relationales Datenbankmanagementsystem (RDBMS) usw. überprüft.
  • Zeilen 13–14: Die Authentifizierungsinformationen werden im Speicher abgelegt
  • Zeile 16: Sie werden durch eine Methode `identifies` überprüft. Da die Identifizierungsmethode nicht bekannt ist, wird sie in Zeile 29 mit dem Schlüsselwort `abstract` als abstrakt deklariert. Die Methode `identifies` gibt eine Zeichenkette zurück, die die Rolle des Benutzers angibt (im Grunde genommen, was er tun darf). Ist diese Zeichenkette null, wird in Zeile 19 eine Ausnahme ausgelöst.
  • Zeile 4: Da sie eine abstrakte Methode enthält, wird die Klasse selbst mit dem Schlüsselwort „abstract“ als abstrakt deklariert.
  • Zeile 29: Die abstrakte Methode `identifies` hat keine Definition. Abgeleitete Klassen werden ihr eine geben.
  • Zeilen 24–26: Die Methode `ToString`, die eine Instanz der Klasse identifiziert.

Hier wird davon ausgegangen, dass der Entwickler die Konstruktion von Instanzen der Klasse User und abgeleiteter Klassen steuern möchte, vielleicht weil er sicherstellen will, dass eine Ausnahme eines bestimmten Typs ausgelöst wird, wenn der Benutzer nicht erkannt wird (Zeile 19). Abgeleitete Klassen können sich auf diesen Konstruktor stützen. Dazu müssen sie die Identifikatoren bereitstellen.

Die Klasse „ExceptionUtilisateurInconnu“ sieht wie folgt aus:


using System;
 
namespace Chap2 {
    class ExceptionUtilisateurInconnu : Exception {
        public ExceptionUtilisateurInconnu(string message) : base(message){
        }
    }
}
  • Zeile 3: abgeleitet von der Klasse Exception
  • Zeilen 4–6: Sie verfügt über einen einzigen Konstruktor, der eine Fehlermeldung als Parameter akzeptiert. Diese wird an die übergeordnete Klasse (Zeile 5) übergeben, die denselben Konstruktor besitzt.

Wir leiten nun den User in der Klasse Director der Mädchen ab:


namespace Chap2 {
    class Administrateur : Utilisateur {
         // manufacturer
        public Administrateur(string login, string motDePasse)
            : base(login, motDePasse) {
        }
 
         // identifies
        public override string identifie() {
             // identification LDAP
            // ...
            return "admin";
        }
    }
}
  • Zeilen 4–6: Der Konstruktor übergibt die empfangenen Parameter einfach an seine übergeordnete Klasse
  • Zeilen 9–12: Die Methode identifiziert die Klasse Director. Es wird davon ausgegangen, dass ein Administrator durch ein LDAP-System identifiziert wird. Diese Methode definiert die Identifikatoren ihrer übergeordneten Klasse neu. Da sie ein abstraktes Element neu definiert, ist es sinnlos, das Schlüsselwort override zu verwenden.

Wir leiten nun den User von der Oberklasse Observer ab:


namespace Chap2 {
    class Observateur : Utilisateur{
         // manufacturer
        public Observateur(string login, string motDePasse)
            : base(login, motDePasse) {
        }
 
         //identifies
        public override string identifie() {
             // identification SGBD
            // ...
            return "observateur";
        }
 
    }
}
  • Zeilen 4–6: Der Konstruktor übergibt die empfangenen Parameter einfach an seine übergeordnete Klasse
  • Zeilen 9–13: Die Methode identifiziert die Klasse Observer. Es wird davon ausgegangen, dass ein Beobachter durch Überprüfung seiner Identifikationsdaten in einer Datenbank identifiziert wird.

Letztendlich werden die Objekte Director und Observer durch denselben Konstruktor instanziiert wie die übergeordnete Klasse User. Dieser Konstruktor nutzt die Identifikationen, die diese Klassen bereitstellen.

Eine dritte Klasse „Unknown“ leitet sich ebenfalls von „User“ ab:


namespace Chap2 {
    class Inconnu : Utilisateur{
 
         // manufacturer
        public Inconnu(string login, string motDePasse)
            : base(login, motDePasse) {
        }
 
         //identifies
        public override string identifie() {
             // unknown user
            // ...
            return null;
        }
 
    }
}
  • Zeile 13: Die Methode setzt den Zeiger auf null, um anzuzeigen, dass der Benutzer nicht erkannt wurde.

Ein Testprogramm könnte wie folgt aussehen:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Console.WriteLine(new Observateur("observer","mdp1"));
            Console.WriteLine(new Administrateur("admin", "mdp2"));
            try {
                Console.WriteLine(new Inconnu("xx", "yy"));
            } catch (ExceptionUtilisateurInconnu e) {
                Console.WriteLine("Utilisateur non connu : "+ e.Message);
            }
        }
    }
}

Beachten Sie, dass in den Zeilen 6, 7 und 9 die Methode [User].ToString() verwendet wird, die von WriteLine genutzt wird.

Die Ergebnisse lauten wie folgt:

1
2
3
Utilisateur[observer,mdp1,observateur]
Utilisateur[admin,mdp2,admin]
Utilisateur non connu : [xx,yy]

4.8. Klassen, Schnittstellen und generische Methoden

Nehmen wir an, wir möchten eine Methode schreiben, die zwei Ganzzahlen vertauscht. Diese Methode könnte wie folgt aussehen:


        public static void Echanger1(ref int value1, ref int value2){
            // on échange les références value1 et value2
            int temp = value2;
            value2 = value1;
            value1 = temp;
}

Wenn wir nun zwei Objektverweise vom Typ Person vertauschen wollten, würden wir schreiben:


        public static void Echanger2(ref Personne value1, ref Personne value2){
            // on échange les références value1 et value2
            Personne temp = value2;
            value2 = value1;
            value1 = temp;
}

Der Unterschied zwischen den beiden Methoden liegt im Typ T der Parameter: int in Exchange1, Person in Exchange2. Generische Klassen und Schnittstellen erfüllen den Bedarf an Methoden, die sich nur im Typ einiger ihrer Parameter unterscheiden.

Mit einer generischen Klasse könnte Exchange wie folgt umgeschrieben werden:


namespace Chap2 {
    class Generic1<T> {
        public static void Echanger(ref T value1, ref T value2){
             // exchange the value1 and value2 references
            T temp = value2;
            value2 = value1;
            value1 = temp;
        }
    }
}
  • Zeile 2: Die Klasse Generic1 wird durch einen Typ mit der Bezeichnung T parametrisiert. Du kannst ihr einen beliebigen Namen geben. Dieser Typ T wird dann in der Klasse in den Zeilen 3 und 5 wiederverwendet. Wir sagen, dass Generic1 eine generische Klasse ist.
  • Zeile 3: definiert die beiden Referenzen auf einen T-Typ, die vertauscht werden sollen
  • Zeile 5: Die temporäre Variable temp hat den Typ T.

Ein Testprogramm für die Klasse könnte wie folgt aussehen:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
             // int
            int i1 = 1, i2 = 2;
            Generic1<int>.Echanger(ref i1, ref i2);
            Console.WriteLine("i1={0},i2={1}", i1, i2);
            // string
            string s1 = "s1", s2 = "s2";
            Generic1<string>.Echanger(ref s1, ref s2);
            Console.WriteLine("s1={0},s2={1}", s1, s2);
             // Person
            Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
            Generic1<Personne>.Echanger(ref p1, ref p2);
            Console.WriteLine("p1={0},p2={1}", p1, p2);
 
        }
    }
}
  • Zeile 8: Bei der Verwendung einer generischen Klasse, die durch die Typen T1, T2, ... parametrisiert ist, müssen diese „instanziiert“ werden. Zeile 8: Verwenden Sie die statische Methode Exchange vom Typ Generic1<int>, um anzugeben, dass die an Exchange übergebenen Referenzen vom Typ int sind.
  • Zeile 12: Die statische Methode Exchange vom Typ Generic1<string> wird verwendet, um anzugeben, dass die an Exchange übergebenen Referenzen vom Typ string sind.
  • Zeile 16: Die statische Methode Exchange vom Typ Generic1<Person> wird verwendet, um anzugeben, dass die an Exchange übergebenen Referenzen vom Typ Person sind.

Die Ergebnisse lauten wie folgt:

1
2
3
i1=2,i2=1
s1=s2,s2=s1
p1=[pauline, dard, 55],p2=[jean, clu, 20]

Die Methode „Exchange“ hätte auch wie folgt geschrieben werden können:


namespace Chap2 {
    class Generic2 {
        public static void Echanger<T>(ref T value1, ref T value2){
             // exchange the value1 and value2 references
            T temp = value2;
            value2 = value1;
            value1 = temp;
        }
    }
}
  • Zeile 2: Die Klasse Generic2 ist nicht mehr generisch
  • Zeile 3: Die statische Methode Exchange ist generisch

Das Testprogramm sieht dann wie folgt aus:


using System;
 
namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
             // int
            int i1 = 1, i2 = 2;
            Generic2.Echanger<int>(ref i1, ref i2);
            Console.WriteLine("i1={0},i2={1}", i1, i2);
            // string
            string s1 = "s1", s2 = "s2";
            Generic2.Echanger<string>(ref s1, ref s2);
            Console.WriteLine("s1={0},s2={1}", s1, s2);
             // Person
            Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
            Generic2.Echanger<Personne>(ref p1, ref p2);
            Console.WriteLine("p1={0},p2={1}", p1, p2);
        }
    }
}
  • Zeilen 8, 12 und 16: Rufen Sie den Exchange auf, indem Sie den Parametertyp in <> angeben. Tatsächlich kann der Compiler die zu verwendende Variante des Exchange ableiten. Der folgende Eintrag ist daher zulässig:

            Generic2.Echanger(ref i1, ref i2);
...
            Generic2.Echanger(ref s1, ref s2);
...
            Generic2.Echanger(ref p1, ref p2);

Zeilen 1, 3 und 5: Die Methodenvariante Exchange wird nicht mehr angegeben. Der Compiler kann sie aus der Art der verwendeten tatsächlichen Parameter ableiten.

Generische Parameter können mit Einschränkungen versehen werden:

Image

Betrachten wir die neue generische Methode „Exchange“ im Folgenden:


namespace Chap2 {
    class Generic3 {
        public static void Echanger<T>(ref T value1, ref T value2) where T : class {
             // exchange the value1 and value2 references
            T temp = value2;
            value2 = value1;
            value1 = temp;
        }
    }
}
  • Zeile 3: Der Typ T muss eine Referenz sein (Klasse, Schnittstelle)

Betrachten Sie das folgende Testprogramm:


using System;
 
namespace Chap2 {
    class Program4 {
        static void Main(string[] args) {
             // int
            int i1 = 1, i2 = 2;
            Generic3.Echanger<int>(ref i1, ref i2);
            Console.WriteLine("i1={0},i2={1}", i1, i2);
            // string
            string s1 = "s1", s2 = "s2";
            Generic3.Echanger(ref s1, ref s2);
            Console.WriteLine("s1={0},s2={1}", s1, s2);
             // Person
            Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
            Generic3.Echanger(ref p1, ref p2);
            Console.WriteLine("p1={0},p2={1}", p1, p2);
 
        }
    }
}

Der Compiler meldet in Zeile 8 einen Fehler, da der Typ int keine Klasse oder Schnittstelle ist, sondern eine Struktur:

Image

Betrachten Sie die neue generische Methode Exchange im Folgenden:


namespace Chap2 {
    class Generic4 {
        public static void Echanger<T>(ref T element1, ref T element2) where T : Interface1 {
             // retrieve the value of the 2 elements
            int value1 = element1.Value();
            int value2 = element2.Value();
             // if 1st element > 2nd element, exchange elements
            if (value1 > value2) {
                T temp = element2;
                element2 = element1;
                element1 = temp;
            }
        }
    }
}
  • Zeile 3: Der Typ T muss die Schnittstelle Interface1 implementieren. Er verfügt über die Methode Value, die in den Zeilen 5 und 6 verwendet wird und den Wert des Objekts vom Typ T liefert.
  • Zeilen 8–12: Die beiden Referenzen element1 und element2 werden nur dann vertauscht, wenn der Wert von element1 größer ist als der Wert von element2.

Die Schnittstelle Interface1 sieht wie folgt aus:


namespace Chap2 {
    interface Interface1 {
        int Value();
    }
}

Es wird von der folgenden Klasse Class1 implementiert:


using System;
using System.Threading;
 
namespace Chap2 {
    class Class1 : Interface1 {
         // object value
        private int value;
 
         // manufacturer
        public Class1() {
             // wait 1 ms
            Thread.Sleep(1);
             // random value between 0 and 99
            value = new Random(DateTime.Now.Millisecond).Next(100);
        }
 
         // accessor private field value
        public int Value() {
            return value;
        }
 
         // instance status
        public override string ToString() {
            return value.ToString();
        }
    }
}
  • Zeile 5: Class1 implementiert Interface1
  • Zeile 7: Der Wert einer Instanz von Class1
  • Zeilen 10–14: Der Feldwert wird mit einem Zufallswert zwischen 0 und 99 initialisiert
  • Zeilen 18–20: Die Methode Value des Interfaces Interface1
  • Zeilen 23–25: die Methode ToString der Klasse

Die Schnittstelle Interface1 wird auch von der Klasse Class2 implementiert:


using System;
 
namespace Chap2 {
    class Class2 : Interface1 {
         // object values
        private int value;
        private String s;
 
         // manufacturer
        public Class2(String s) {
            this.s = s;
            value = s.Length;
        }
 
         // accessor private field value
        public int Value() {
            return value;
        }
 
         // instance status
        public override string ToString() {
            return s;
        }
    }
}
  • Zeile 4: Class2 implementiert Interface1
  • Zeile 6: Der Wert einer Instanz von Class2
  • Zeilen 10–13: Der Feldwert wird mit der Länge der an den Konstruktor übergebenen Zeichenkette initialisiert
  • Zeilen 16–18: Die Methode Value der Schnittstelle Interface1
  • Zeilen 21–22: die Methode ToString der Klasse

Ein Testprogramm könnte wie folgt aussehen:


using System;
 
namespace Chap2 {
    class Program5 {
        static void Main(string[] args) {
             // exchange instances of type Class1
            Class1 c1, c2;
            for (int i = 0; i < 5; i++) {
                c1 = new Class1();
                c2 = new Class1();
                Console.WriteLine("Avant échange --> c1={0},c2={1}", c1, c2);
                Generic4.Echanger(ref c1, ref c2);
                Console.WriteLine("Après échange --> c1={0},c2={1}", c1, c2);
            }
             // exchange Class2 instances
            Class2 c3, c4;
            c3 = new Class2("xxxxxxxxxxxxxx");
            c4 = new Class2("xx");
            Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
            Generic4.Echanger(ref c3, ref c4);
            Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
        }
    }
}
  • Zeilen 8–14: Instanzen der Klasse Class1 werden ausgetauscht
  • Zeilen 16–22: Instanzen vom Typ Class2 werden ausgetauscht

Die Ergebnisse lauten wie folgt:

Avant échange --> c1=43,c2=79
Après échange --> c1=43,c2=79
Avant échange --> c1=72,c2=56
Après échange --> c1=56,c2=72
Avant échange --> c1=92,c2=75
Après échange --> c1=75,c2=92
Avant échange --> c1=11,c2=47
Après échange --> c1=11,c2=47
Avant échange --> c1=31,c2=67
Après échange --> c1=31,c2=67
Avant échange --> c3=xxxxxxxxxxxxxx,c4=xx
Après échange --> c3=xx,c4=xxxxxxxxxxxxxx

Um das Konzept der generischen Schnittstelle “ zu veranschaulichen, werden wir ein Array von Personen zunächst nach ihren Namen und dann nach ihrem Alter sortieren. Die Methode, die wir zum Sortieren eines Arrays verwenden, ist die statische Methode der Klasse „Spell“:

Image

Denken Sie daran, dass eine statische Methode verwendet wird, indem man dem Methodennamen den Namen der Klasse voranstellt und nicht den Namen einer Instanz der Klasse. Die Methode „Spell“ hat verschiedene Signaturen (sie ist überladen). Wir verwenden die folgende Signatur:

public static void Sort<T>(T[] tableau, IComparer<T> comparateur)

Spell ist eine generische Methode, wobei T für einen beliebigen Typ steht. Die Methode erhält zwei Parameter:

  • T[] table: das Array der zu sortierenden T-Elemente
  • IComparer<T> comparator: eine Objektreferenz, die die Schnittstelle IComparer<T> implementiert.

IComparer<T> ist eine generische Schnittstelle, die wie folgt definiert ist:

1
2
3
public interface IComparer<T>{
    int Compare(T t1, T t2);
}

Die Schnittstelle IComparer<T> verfügt nur über eine Methode. Die Methode Compare:

  • empfängt zwei Elemente als Parameter t1 und t2 vom Typ T
  • gibt 1 zurück, wenn t1 > t2, 0, wenn t1 == t2, und -1, wenn t1 < t2. Es liegt am Entwickler, den Operatoren <, == und > eine Bedeutung zuzuweisen. Sind p1 und p2 beispielsweise zwei Objekte vom Typ Person, können wir sagen, dass p1 > p2 gilt, wenn der Name von p1 in alphabetischer Reihenfolge vor dem Namen von p2 steht. Wir sortieren dann in aufsteigender Reihenfolge nach dem Namen. Wenn Sie nach dem Alter sortieren möchten, sagen Sie p1 > p2, wenn das Alter von p1 größer ist als das von p2.
  • Um in absteigender Reihenfolge zu sortieren, kehren Sie einfach die Ergebnisse von +1 und -1 um.

Wir wissen nun genug, um eine Tabelle mit Personen zu sortieren. Das Programm sieht wie folgt aus:


using System;
using System.Collections.Generic;
 
namespace Chap2 {
    class Program6 {
        static void Main(string[] args) {
             // a table of people
            Personne[] personnes1 = { new Personne("claude", "pollon", 25), new Personne("valentine", "germain", 35), new Personne("paul", "germain", 32) };
             // display
            Affiche("Tableau à trier", personnes1);
             // sort by name
            Array.Sort(personnes1, new CompareNoms());
             // display
            Affiche("Tableau après le tri selon les nom et prénom", personnes1);
             // sorted by age
            Array.Sort(personnes1, new CompareAges());
             // display
            Affiche("Tableau après le tri selon l'âge", personnes1);
        }
 
        static void Affiche(string texte, Personne[] personnes) {
            Console.WriteLine(texte.PadRight(50, '-'));
            foreach (Personne p in personnes) {
                Console.WriteLine(p);
            }
        }
    }
 
     // first and last name comparison class
    class CompareNoms : IComparer<Personne> {
        public int Compare(Personne p1, Personne p2) {
             // compare names
            int i = p1.Nom.CompareTo(p2.Nom);
            if (i != 0)
                return i;
             // equal names - first names are compared
            return p1.Prenom.CompareTo(p2.Prenom);
        }
    }
 
     // age comparison class
    class CompareAges : IComparer<Personne> {
        public int Compare(Personne p1, Personne p2) {
             // comparing ages
            if (p1.Age > p2.Age)
                return 1;
            else if (p1.Age == p2.Age)
                return 0;
            else
                return -1;
        }
    }
 
}
  • Zeile 8: die Tabelle der Personen
  • Zeile 12: Sortiere die Tabelle der Personen nach Vor- und Nachnamen. Der zweite Parameter der generischen Methode „Spell“ ist eine Instanz von „CompareNoms“, die die generische Schnittstelle „IComparer<Person>“ implementiert.
  • Zeilen 30–39: Die Klasse „CompareNoms“, die das generische IComparer<Person> implementiert.
  • Zeilen 31–38: Implementierung der generischen Methode int CompareTo(T, T) der Schnittstelle IComparer<T>. Die Methode verwendet die in Clipboard 3.3.5.4 vorgestellte Methode String.CompareTo, um zwei Zeichenfolgen zu vergleichen.
  • Zeile 16: Sortiere die Tabelle der Personen nach Alter. Der zweite Parameter der generischen Methode „Spell“ ist eine Instanz von „CompareAges“, die die generische Schnittstelle „IComparer<Person>“ implementiert und in den Zeilen 42–51 definiert ist.

Die Ergebnisse lauten wie folgt:

Tableau à trier-----------------------------------
[claude, pollon, 25]
[valentine, germain, 35]
[paul, germain, 32]
Tableau après le tri selon les nom et prénom------
[paul, germain, 32]
[valentine, germain, 35]
[claude, pollon, 25]
Tableau après le tri selon l'âge------------------
[claude, pollon, 25]
[paul, germain, 32]
[valentine, germain, 35]

4.9. Namensräume

Um eine Zeile auf den Bildschirm zu schreiben, verwenden wir die Anweisung

Console.WriteLine(...)

Wenn wir uns die Definition von Console ansehen


Namespace: System
Assembly: Mscorlib (in Mscorlib.dll)

stellen wir fest, dass sie Teil von System ist. Das bedeutet, dass die Konsole mit System.Console bezeichnet werden sollte und wir eigentlich schreiben sollten:

System.Console.WriteLine(...)

Dies wird durch die Verwendung von „using“ vermieden:

using System;
...
Console.WriteLine(...)

Wir sagen, dass wir den Namespace System mit der using-Klausel importieren. Wenn der Compiler auf den Namen einer Klasse stößt (hier Console), versucht er, diese in den verschiedenen Namespaces zu finden, die durch das using importiert wurden. Hier findet er die Klasse Console im Namespace System. Beachten wir nun die zweite Information, die mit der Klasse Console verbunden ist:

Assembly: Mscorlib (in Mscorlib.dll)

Diese Zeile gibt an, in welcher „Assembly“ sich die Klassendefinition von Console befindet. Wenn Sie außerhalb von Visual Studio kompilieren und Verweise auf die verschiedenen DLLs angeben müssen, die die zu verwendenden Klassen enthalten, kann diese Information nützlich sein. Um auf die zum Kompilieren einer Klasse erforderlichen DLLs zu verweisen, schreiben wir:

csc /r:fic1.dll /r:fic2.dll ... prog.cs

wobei CSC der C#-Compiler ist. Wenn wir eine Klasse erstellen, können wir diese innerhalb eines Namespace anlegen. Der Zweck dieser Namespaces besteht darin, Namenskonflikte zwischen Klassen zu vermeiden, beispielsweise wenn diese verkauft werden. Betrachten wir zwei Unternehmen, E1 und E2, die Klassen vertreiben, die jeweils in den DLLs e1.dll und e2.dll verpackt sind. Nehmen wir an, ein Kunde C kauft diese beiden Klassensätze, in denen beide Unternehmen eine Klasse Person definiert haben. Der Kunde C kompiliert ein Programm wie folgt:

csc /r:e1.dll /r:e2.dll prog.cs

Wenn die Quelldatei prog.cs die Klasse „Person“ verwendet, weiß der Compiler nicht, ob er die „Person“ aus e1.dll oder die aus e2.dll nehmen soll. Er meldet einen Fehler. Wenn Unternehmen E1 darauf achtet, seine Klassen in einem Namespace namens E1 und Unternehmen E2 in einem Namespace namens E2 zu erstellen, heißen die beiden Klassen „Person“ dann E1.Person und E2.Personne. Der Kunde muss entweder E1.Personne oder E2.Personne verwenden, aber nicht Person. Der Namespace beseitigt jegliche Mehrdeutigkeit.

Um eine Klasse in einem Namespace zu erstellen, schreiben Sie:

namespace EspaceDeNoms{
     // class definition
}

4.10. Beispielanwendung – V2

Wir wiederholen die bereits im vorigen Kapitel, Abschnitt 3.6, behandelte Steuerberechnung und setzen sie nun mithilfe von Klassen und Schnittstellen um. Erinnern wir uns an die Aufgabe:

Wir schlagen vor, ein Programm zur Berechnung der Einkommensteuer eines Steuerpflichtigen zu schreiben. Der vereinfachte Fall ist der eines Steuerpflichtigen, der nur sein Gehalt anzugeben hat (Zahlen von 2004 für das Einkommen von 2003):

  • Die Anzahl der Arbeitnehmeranteile wird berechnet als nbParts = nbEnfants/2 + 1, wenn unverheiratet, bzw. nbEnfants/2 + 2, wenn verheiratet, wobei nbEnfants die Anzahl der Kinder ist.
  • Wenn er mindestens drei Kinder hat, erhält er einen halben Anteil mehr
  • Berechnen Sie Ihr zu versteuerndes Einkommen R=0,72*S, wobei S sein Jahresgehalt ist
  • Berechnen Sie Ihren Familienkoeffizienten QF=R/nbParts
  • Berechnen Sie Ihre Steuer I. Betrachten Sie die folgende Tabelle:
4262
0
0
8382
0,0683
291,09
14753
0,1914
1322,92
23888
0,2826
2668,39
38868
0,3738
4846,98
47932
0,4262
6883,66
0
0,4809
9505,54

Jede Zeile enthält 3 Felder. Um die Steuer I zu berechnen, suchen Sie nach der ersten Zeile, in der QF <= champ1 ist. Wenn beispielsweise QF = 5000 ist, finden wir die Zeile

    8382        0.0683        291.09

Die Steuer I ist dann gleich 0,0683*R – 291,09*nbParts. Wenn QF so gewählt ist, dass die Bedingung QF <= champ1 nie geprüft wird, werden die Koeffizienten der letzten Zeile verwendet. Hier:

    0                0.4809    9505.54

was zu Steuer I = 0,4809*R - 9505,54*nbParts führt.

Zunächst definieren wir eine Struktur, die eine Zeile des vorangehenden Arrays kapseln kann:


namespace Chap2 {
     // a tax bracket
    struct TrancheImpot {
        public decimal Limite { get; set; }
        public decimal CoeffR { get; set; }
        public decimal CoeffN { get; set; }
    }
}
 

Dann definieren wir eine Schnittstelle IImpot, die Steuern berechnen kann:


namespace Chap2 {
    interface IImpot {
        int calculer(bool marié, int nbEnfants, int salaire);
    }
}
  • Zeile 3: Methode zur Steuerberechnung auf der Grundlage von drei Daten: ob der Steuerzahler verheiratet ist oder nicht, Anzahl der Kinder, Gehalt

Als Nächstes definieren wir eine abstrakte Klasse, die diese Schnittstelle implementiert:


namespace Chap2 {
    abstract class AbstractImpot : IImpot {
 
         // tax brackets required to calculate tax
         // come from an external source
 
        protected TrancheImpot[] tranchesImpot;
 
         // tAX CALCULATION
        public int calculer(bool marié, int nbEnfants, int salaire) {
             // calculating the number of shares
            decimal nbParts;
            if (marié) nbParts = (decimal)nbEnfants / 2 + 2;
            else nbParts = (decimal)nbEnfants / 2 + 1;
            if (nbEnfants >= 3) nbParts += 0.5M;
             // calculation of taxable income & family quota
            decimal revenu = 0.72M * salaire;
            decimal QF = revenu / nbParts;
             // tAX CALCULATION
            tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
            int i = 0;
            while (QF > tranchesImpot[i].Limite) i++;
             // return result
            return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
         }//calculate
     }//class
 
}
  • Zeile 2: Die Klasse AbstractImpot implementiert die Schnittstelle IImpot.
  • Zeile 7: Daten zur jährlichen Steuerberechnung in Form eines geschützten Feldes. Die Klasse AbstractImpot weiß nicht, wie dieses Feld initialisiert wird. Sie überlässt dies den abgeleiteten Klassen. Deshalb wird sie als abstrakt deklariert (Zeile 2), um eine Instanziierung zu verhindern.
  • Zeilen 10–25: Implementierung der Schnittstelle IImpot. Abgeleitete Klassen müssen diese Methode nicht neu schreiben. Die Klasse „AbstractImpot“ dient als Basisklasse für abgeleitete Klassen. Hier werden die gemeinsamen Elemente aller abgeleiteten Klassen untergebracht.

Eine Klasse, die die Schnittstelle IImpot implementiert, kann durch Ableitung von AbstractImpot erstellt werden. Genau das tun wir jetzt:


using System;
 
namespace Chap2 {
    class HardwiredImpot : AbstractImpot {
 
         // data tables for tax calculations
        decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
        decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
        decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };
 
        public HardwiredImpot() {
                 // creation of tax bracket table
            tranchesImpot = new TrancheImpot[limites.Length];
                 // filling
            for (int i = 0; i < tranchesImpot.Length; i++) {
                tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
                }
        }
     }// class
}// namespace

Die Klasse HardwiredImpot definiert in den Zeilen 7–9 die für die Steuerberechnung erforderlichen Festdaten. Ihr Konstruktor (Zeilen 11–18) verwendet diese Daten, um das geschützte Feld tranchesImpot der übergeordneten Klasse AbstractImpot zu initialisieren.

Ein Testprogramm könnte wie folgt aussehen:


using System;
 
namespace Chap2 {
    class Program {
        static void Main() {
             // interactive Tax calculation program
            // l'user types three data into keyboard: married nbEnfants salary
             // the program then displays Tax payable
 
            const string syntaxe = "syntaxe : Marié NbEnfants Salaire\n"
                            + "Marié : o pour marié, n pour non marié\n"
                            + "NbEnfants : nombre d'enfants\n"
                            + "Salaire : salaire annuel en F";
 
             // creation of a IImpot object
            IImpot impot = new HardwiredImpot();
 
             // infinite loop
            while (true) {
                 // tax calculation parameters are requested
                Console.Write("Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :");
                string paramètres = Console.ReadLine().Trim();
                 // anything to do?
                if (paramètres == null || paramètres == "") break;
                 // check number of arguments in the input line
                string[] args = paramètres.Split(null);
                int nbParamètres = args.Length;
                if (nbParamètres != 3) {
                    Console.WriteLine(syntaxe);
                    continue;
                 }//if
                 // checking the validity of parameters
                 // married
                string marié = args[0].ToLower();
                if (marié != "o" && marié != "n") {
                    Console.WriteLine(syntaxe + "\nArgument marié incorrect : tapez o ou n");
                    continue;
                 }//if
                 // nbEnfants
                int nbEnfants = 0;
                bool dataOk = false;
                try {
                    nbEnfants = int.Parse(args[1]);
                    dataOk = nbEnfants >= 0;
                } catch {
                 }//if
                 // correct data?
                if (!dataOk) {
                    Console.WriteLine(syntaxe + "\nArgument NbEnfants incorrect : tapez un entier positif ou nul");
                    continue;
                }
                 // salary
                int salaire = 0;
                dataOk = false;
                try {
                    salaire = int.Parse(args[2]);
                    dataOk = salaire >= 0;
                } catch {
                 }//try-catch
                 // correct data?
                if (!dataOk) {
                    Console.WriteLine(syntaxe + "\nArgument salaire incorrect : tapez un entier positif ou nul");
                    continue;
                }
                 // parameters are correct - Tax is calculated
                Console.WriteLine("Impot=" + impot.calculer(marié == "o", nbEnfants, salaire) + " euros");
                 // next taxpayer
             }//while
        }
    }
}

Das obige Programm ermöglicht es dem Benutzer, wiederholte Steuerberechnungssimulationen durchzuführen.

  • Zeile 16: Erstellung des Objekts „tax“, das die Schnittstelle IImpot implementiert. Dieses Objekt wird durch Instanziierung eines „HardwiredImpot“ erhalten, einem Typ, der die Schnittstelle IImpot implementiert. Beachten Sie, dass wir der Variablen „tax“ nicht den Typ „HardwiredImpot“, sondern die Schnittstelle IImpot zugewiesen haben. Dies zeigt, dass wir nur am Objekt „tax“ interessiert sind und nicht am Rest.
  • Zeilen 19–68: Die Schleife für die Steuerberechnungssimulation
  • Zeile 22: Die drei für die Methode „calculate“ erforderlichen Parameter werden in einer einzigen, über die Tastatur eingegebenen Zeile abgefragt.
  • Zeile 26: Die Methode [string].Split(null) zerlegt [string] in Wörter. Diese werden in einem Array args gespeichert.
  • Zeile 66: Ruft das Objekt „tax“ auf, das die Schnittstelle „IImpot“ implementiert.

Hier ist ein Beispiel für die Ausführung des Programms:

Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :q s d
syntaxe : Marié NbEnfants Salaire
Marié : o pour marié, n pour non marié
NbEnfants : nombre d'enfants
Salaire : salaire annuel en euros
Argument marié incorrect : tapez o ou n
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 d
syntaxe : Marié NbEnfants Salaire
Marié : o pour marié, n pour non marié
NbEnfants : nombre d'enfants
Salaire : salaire annuel en euros
Argument salaire incorrect : tapez un entier positif ou nul
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :q s d f
syntaxe : Marié NbEnfants Salaire
Marié : o pour marié, n pour non marié
NbEnfants : nombre d'enfants
Salaire : salaire annuel en euros
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 60000
Impot=4282 euros