Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


C# von Eric Gunnerson
Die neue Sprache für Microsofts .NET-Plattform
C# - Zum Katalog
gp Kapitel 7 Basisklassen und Vererbung
  gp 7.1 Die Engineer-Klasse
  gp 7.2 Einfache Vererbung
  gp 7.3 Engineer-Arrays
  gp 7.4 Virtuelle Funktionen
  gp 7.5 Abstrakte Klassen
  gp 7.6 sealed-Schlüsselwort

Kapitel 7 Basisklassen und Vererbung

Wie bereits in Kapitel 1, Grundlagen der objektorientierten Programmierung, besprochen, ist es gelegentlich sinnvoll, eine Klasse aus einer anderen abzuleiten, und zwar dann, wenn die abgeleitete Klasse eine Sonderform der Basisklasse ist.


Galileo Computing

7.1 Die Engineer-Klasse  downtop

Nachfolgend werden die Klasse Engineer (Techniker/Ingenieur) sowie Methoden zur Rechnungsstellung für diesen Engineer implementiert.

using System;
class Engineer
{
        // Erstellungsroutine
    public Engineer(string name, float billingRate)
    {
        this.name = name;
        this.billingRate = billingRate;
    }
        // Rechnungsbetrag auf Grundlage des Stundensatzes des Technikers
            ermitteln
    public float CalculateCharge(float hours)
    {
        return(hours * billingRate);
    }
        // Name des Typs zurückgeben
    public string TypeName()
    {
        return("Engineer");
    }

    private string name;
    protected float billingRate;
}
class Test
{
    public static void Main()
    {
        Engineer engineer = new Engineer("Hank", 21.20F);
        Console.WriteLine("Name is: {0}", engineer.TypeName());
    }
}

Für dieses Szenario dient Engineer als Basisklasse. Die Klasse Engineer enthält das private Feld name sowie das geschützte Feld billingRate. Der Modifikator protected gewährt den gleichen Zugriff wie private, abgesehen davon, dass die von dieser Klasse abgeleiteten Klassen auch Zugriff auf dieses Feld erhalten. Protected wird demnach dazu eingesetzt, den von dieser Klasse abgeleiteten Klassen Zugriff auf ein Feld zu gewähren.

Der Zugriff über protected ermöglicht anderen Klassen, sich auf die interne Implementierung der Klasse zu verlassen, daher sollte dieser Zugriff nur gewährt werden, wenn dies erforderlich ist. Im Beispiel kann das Mitglied billingRate nicht umbenannt werden, da abgeleitete Klassen möglicherweise darauf zugreifen. Häufig ist es besser, eine geschützte Eigenschaft zu verwenden.

Die Engineer-Klasse besitzt außerdem eine Mitgliedsfunktion, mit der basierend auf den geleisteten Arbeitsstunden der in Rechnung zu stellende Betrag berechnet wird.


Galileo Computing

7.2 Einfache Vererbung  downtop

Ein CivilEngineer (Bauingenieur) gehörte auch zur Gruppe der Techniker und Ingenieure und kann daher von der Klasse Engineer abgeleitet werden:

using System;
class Engineer
{
    public Engineer(string name, float billingRate)
    {
        this.name = name;
        this.billingRate = billingRate;
    }

    public float CalculateCharge(float hours)
    {
        return(hours * billingRate);
    }

    public string TypeName()
    {
        return("Engineer");
    }

    private string name;
    protected float billingRate;
}
class CivilEngineer: Engineer
{
    public CivilEngineer(string name, float billingRate) :
        base(name, billingRate)
    {
    }
        // Neue Funktion, da anders als
        // Basisversion
    public new float CalculateCharge(float hours)
    {
        if (hours < 1.0F)
            hours = 1.0F;        // Mindestsatz
        return(hours * billingRate);
    }
        // Neue Funktion, da anders als
        // Basisversion
    public new string TypeName()
    {
        return("Civil Engineer");
    }
}
class Test
{
    public static void Main()
    {
        Engineer    e = new Engineer("George", 15.50F);
         CivilEngineer    c = new CivilEngineer("Sir John", 40F);

        Console.WriteLine("{0} charge = {1}",
                    e.TypeName(),
                    e.CalculateCharge(2F));
        Console.WriteLine("{0} charge = {1}",
                    c.TypeName(),
                    c.CalculateCharge(0.75F));
    }
}

Da die Klasse CivilEngineer von der Klasse Engineer abgeleitet wurde, erbt sie alle Datenmitglieder der Klasse (obwohl auf das als privat deklarierte Feld name nicht zugegriffen werden kann). Darüber hinaus erbt die CivilEngineer-Klasse auch die Mitgliedsfunktion CalculateCharge().

Erstellungsroutinen können nicht vererbt werden, daher muss für CivilEngineer eine neue Erstellungsroutine geschrieben werden. Die Erstellungsroutine muss keinen besonderen Code ausführen, daher wird unter Verwendung der Syntax base die Erstellungsroutine für Engineer aufgerufen. Wenn der Aufruf der Klassenerstellungsroutine base nicht erfolgt, ruft der Compiler die base-Klassenerstellungsroutine ohne Parameter auf.

CivilEngineer weist eine andere Berechnungsmethode auf. Der Mindestsatz wird mit einer Stunde veranschlagt, daher muss eine neue Version von CalculateCharge() definiert werden.

Bei Ausführung des Codebeispiels wird folgende Ausgabe erzeugt:

Engineer Charge = 31
Civil Engineer Charge = 40

Galileo Computing

7.3 Engineer-Arrays  downtop

Das vorgenannte Beispiel funktioniert prima bei einer kleinen Mitarbeiterzahl. Wenn das Unternehmen jedoch wächst, ist die Verwendung von Engineer-Arrays einfacher.

Da CivilEngineer sich aus der Klasse Engineer ableitet, kann ein Array vom Typ Engineer beide Klassentypen enthalten. Dieses Beispiel weist eine andere Main()-Funktion auf, mit der die Techniker und Ingenieure in einem Array zusammengefasst werden:

using System;
class Engineer
{
    public Engineer(string name, float billingRate)
    {
        this.name = name;
        this.billingRate = billingRate;
    }

    public float CalculateCharge(float hours)
    {
        return(hours * billingRate);
    }

    public string TypeName()
    {
        return("Engineer");
    }

    private string name;
    protected float billingRate;
}
class CivilEngineer: Engineer
{
    public CivilEngineer(string name, float billingRate) :
        base(name, billingRate)
    {
    }

    public new float CalculateCharge(float hours)
    {
        if (hours < 1.0F)
            hours = 1.0F;        // Mindestsatz
        return(hours * billingRate);
    }

    public new string TypeName()
    {
        return("Civil Engineer");
    }
}
class Test
{
    public static void Main()
    {
            // Engineer-Array erstellen
        Engineer[]    earray = new Engineer[2];
        earray[0] = new Engineer("George", 15.50F);
        earray[1] = new CivilEngineer("Sir John", 40F);

        Console.WriteLine("{0} charge = {1}",
                    earray[0].TypeName(),
                    earray[0].CalculateCharge(2F));
        Console.WriteLine("{0} charge = {1}",
                    earray[1].TypeName(),
                    earray[1].CalculateCharge(0.75F));
    }
}

Diese Codeversion erzeugt die folgende Ausgabe:

Engineer Charge = 31
Engineer Charge = 30

Leider ist das nicht richtig.

Bei der Platzierung der verschiedenen Ingenieure und Techniker im Array geht die Tatsache verloren, dass der zweite Engineer tatsächlich ein CivilEngineer ist. Da es sich um ein Engineer-Array handelt, wird beim Aufruf von CalculateCharge() die Version für Engineer aufgerufen.

Es ist also erforderlich, den Typ von Engineer richtig zu ermitteln. Diese Identifizierung kann über ein Feld in der Engineer-Klasse erfolgen, mit dem der Typ gekennzeichnet wird. Nach einer Umarbeitung der Klassen mit einem enum-Feld zur Kennzeichnung des Engineer-Typs erhalten Sie folgendes Beispiel:

using System;
enum EngineerTypeEnum
{
    Engineer,
    CivilEngineer
}
class Engineer
{
    public Engineer(string name, float billingRate)
    {
        this.name = name;
        this.billingRate = billingRate;
        type = EngineerTypeEnum.Engineer;
    }

    public float CalculateCharge(float hours)
    {
        if (type == EngineerTypeEnum.CivilEngineer)
        {
            CivilEngineer c = (CivilEngineer) this;
            return(c.CalculateCharge(hours));
        }
        else if (type == EngineerTypeEnum.Engineer)
            return(hours * billingRate);
        return(0F);
    }

    public string TypeName()
    {
        if (type == EngineerTypeEnum.CivilEngineer)
        {
            CivilEngineer c = (CivilEngineer) this;
            return(c.TypeName());
        }
        else if (type == EngineerTypeEnum.Engineer)
            return("Engineer");
        return("No Type Matched");
    }

    private string name;
    protected float billingRate;
    protected EngineerTypeEnum type;
}

class CivilEngineer: Engineer
{
    public CivilEngineer(string name, float billingRate) :
        base(name, billingRate)
    {
        type = EngineerTypeEnum.CivilEngineer;
    }

    public new float CalculateCharge(float hours)
    {
        if (hours < 1.0F)
            hours = 1.0F;        // Mindestsatz
        return(hours * billingRate);
    }

    public new string TypeName()
    {
        return("Civil Engineer");
    }
}
class Test
{
    public static void Main()
    {
        Engineer[]    earray = new Engineer[2];
        earray[0] = new Engineer("George", 15.50F);
        earray[1] = new CivilEngineer("Sir John", 40F);

        Console.WriteLine("{0} charge = {1}",
                    earray[0].TypeName(),
                    earray[0].CalculateCharge(2F));
        Console.WriteLine("{0} charge = {1}",
                    earray[1].TypeName(),
                    earray[1].CalculateCharge(0.75F));
    }
}

Anhand des type-Feldes können die Funktionen in Engineer den tatsächlichen Objekttyp ermitteln und die geeignete Funktion aufrufen.

Die Codeausgabe lautet wie erwartet:

Engineer Charge = 31
Civil Engineer Charge = 40

Unglücklicherweise ist die Basisklasse nun recht kompliziert geworden, denn für jede Funktion zu einem Klassentyp ist Code vorhanden, mit dem die möglichen Typen geprüft werden und anschließend die richtige Funktion aufgerufen wird. Dies bedeutet eine Menge Extracode; bei 50 unterschiedlichen Engineer-Typen wäre diese Methode nicht praktikabel.

Schlimmer noch ist die Tatsache, dass die Basisklasse die Namen aller abgeleiteten Klassen kennen muss, um funktionieren zu können. Wenn der Codebesitzer Unterstützungscode für einen neuen Typ Engineer hinzufügen möchte, muss die Basisklasse geändert werden. Ein Benutzer ohne Zugriff auf die Basisklasse ist überhaupt nicht in der Lage, einen neuen Engineer-Typ hinzuzufügen.


Galileo Computing

7.4 Virtuelle Funktionen  downtop

Zur Optimierung vorgenannter Codeabschnitte bieten objektorientierte Sprachen die Möglichkeit, Funktionen als virtuell (Schlüsselwort virtual) zu deklarieren. Virtuell bedeutet hierbei, dass beim Aufruf einer Mitgliedsfunktion der Compiler den tatsächlichen Objekttyp ermitteln (nicht lediglich den Verweistyp) und basierend auf diesem Typ die geeignete Funktion aufrufen sollte.

Mit diesem Hintergrundwissen kann das Beispiel folgendermaßen abgeändert werden:

using System;
class Engineer
{
    public Engineer(string name, float billingRate)
    {
        this.name = name;
        this.billingRate = billingRate;
    }
        // Funktion jetzt virtuell
    virtual public float CalculateCharge(float hours)
    {
        return(hours * billingRate);
    }
        // Funktion jetzt virtuell
    virtual public string TypeName()
    {
        return("Engineer");
    }

    private string name;
    protected float billingRate;
}

class CivilEngineer: Engineer
{
    public CivilEngineer(string name, float billingRate) :
        base(name, billingRate)
    {
    }
        // Hat Vorrang vor Funktion in Engineer
    override public float CalculateCharge(float hours)
    {
        if (hours < 1.0F)
            hours = 1.0F;        // Mindestsatz
        return(hours * billingRate);
    }
        // Hat Vorrang vor Funktion in Engineer
    override public string TypeName()
    {
        return("Civil Engineer");
    }
}
class Test
{
    public static void Main()
    {
        Engineer[]    earray = new Engineer[2];
        earray[0] = new Engineer("George", 15.50F);
        earray[1] = new CivilEngineer("Sir John", 40F);

        Console.WriteLine("{0} charge = {1}",
                    earray[0].TypeName(),
                    earray[0].CalculateCharge(2F));
        Console.WriteLine("{0} charge = {1}",
                    earray[1].TypeName(),
                    earray[1].CalculateCharge(0.75F));
    }
}

Die Funktionen CalculateCharge() und TypeName() wurden nun in der Basisklasse mit dem Schlüsselwort virtual deklariert, damit verfügt die Basisklasse über ausreichende Informationen. Die Basisklasse benötigt keine Informationen zu den abgeleiteten Typen, sie muss lediglich Kenntnis darüber haben, dass jede abgeleitete Klasse ggf. CalculateCharge() und TypeName() implementieren kann. In der abgeleiteten Klasse werden die Funktionen mit dem Schlüsselwort override deklariert, das bedeutet, dass es sich um dieselbe Funktion handelt, die in der Basisklasse deklariert wurde. Fehlt das Schlüsselwort override, nimmt der Compiler an, dass die Funktion nicht in Beziehung zur Funktion der Basisklasse steht. Dies hat zur Folge, dass das virtuelle Dispatching fehlschlägt.

Die Ausführung dieses Beispiels führt zur erwarteten Ausgabe:

Engineer Charge = 31
Civil Engineer Charge = 40

Wenn der Compiler einen Aufruf von TypeName() oder CalculateCharge() ermittelt, wechselt er zur Funktionsdefinition und stellt fest, dass es sich um eine virtuelle Funktion handelt. Statt Code für den direkten Funktionsaufruf zu generieren, wird Dispatchercode erzeugt, mit dem zur Laufzeit der tatsächliche Objekttyp ermittelt wird. Anschließend wird die mit dem tatsächlichen Typ verknüpfte Funktion aufgerufen, nicht lediglich der Typ des Verweises. Dies ermöglicht auch dann den Aufruf der richtigen Funktion, wenn die Klasse zur Kompilierungszeit nicht implementiert war.

Beim virtuellen Dispatching entsteht ein leichter Overhead, daher sollte diese Methode nicht unnötig eingesetzt werden. Es kann jedoch passieren, dass ein JIT-Compiler (Just In Time) ermittelt, dass keine abgeleiteten Klassen für diejenige Klasse vorlagen, für die der Funktionsaufruf erfolgte, und deshalb das virtuelle Dispatching in einen direkten Aufruf konvertiert.


Galileo Computing

7.5 Abstrakte Klassen  downtop

Bei dem bisher verwendeten Ansatz besteht ein kleines Problem. Eine neue Klasse muss die Funktion TypeName() nicht implementieren, da sie die Implementierung von Engineer erben kann. Daher kann eine neue Engineer-Klasse leicht mit dem falschen Namen verknüpft werden.

Angenommen, die Klasse ChemicalEngineer wird hinzugefügt:

using System;
class Engineer
{
    public Engineer(string name, float billingRate)
    {
        this.name = name;
        this.billingRate = billingRate;
    }

    virtual public float CalculateCharge(float hours)
    {
        return(hours * billingRate);
    }

    virtual public string TypeName()
    {
        return("Engineer");
    }

    private string name;
    protected float billingRate;
}
class ChemicalEngineer: Engineer
{
    public ChemicalEngineer(string name, float billingRate) :
        base(name, billingRate)
    {
    }

    // Schlüsselwort override wird versehentlich vergessen
}
class Test
{
    public static void Main()
    {
        Engineer[]    earray = new Engineer[2];
        earray[0] = new Engineer("George", 15.50F);
        earray[1] = new ChemicalEngineer("Dr. Curie", 45.50F);

        Console.WriteLine("{0} charge = {1}",
                    earray[0].TypeName(),
                    earray[0].CalculateCharge(2F));
        Console.WriteLine("{0} charge = {1}",
                    earray[1].TypeName(),
                    earray[1].CalculateCharge(0.75F));
    }
}

Die Klasse ChemicalEngineer erbt in diesem Fall die Funktion CalculateCharge() von Engineer. Dies kann richtig sein, es wird jedoch auch TypeName() vererbt, was eindeutig falsch ist. Es ist also eine Methode erforderlich, mit der ChemicalEngineer zur Implementierung von TypeName() gezwungen ist.

Dies kann erreicht werden, indem Engineer von einer normalen Klasse in eine abstrakte Klasse geändert wird. In dieser abstrakten Klasse wird die Mitgliedsfunktion TypeName() als abstrakte Funktion gekennzeichnet, d. h. alle Klassen, die von Engineer abgeleitet werden, müssen die Funktion TypeName() implementieren.

Eine abstrakte Klasse definiert eine Festlegung, die von den abgeleiteten Klassen eingehalten werden muss. Da eine abstrakte Klasse keine erforderliche Funktionalität aufweisen muss, kann sie nicht instanziiert werden, was im Beispiel bedeutet, dass keine Instanzen der Engineer-Klasse erstellt werden können. Damit weiterhin zwei unterschiedliche Engineer-Typen vorliegen, wurde die Klasse ChemicalEngineer hinzugefügt.

Abstrakte Klassen verhalten sich wie normale Klassen, nur dass eine oder mehrere Mitgliedsfunktionen als abstract gekennzeichnet sind.

using System;
abstract class Engineer
{
    public Engineer(string name, float billingRate)
    {
        this.name = name;
        this.billingRate = billingRate;
    }

    virtual public float CalculateCharge(float hours)
    {
        return(hours * billingRate);
    }

    abstract public string TypeName();

    private string name;
    protected float billingRate;
}

class CivilEngineer: Engineer
{
    public CivilEngineer(string name, float billingRate) :
        base(name, billingRate)

    {
    }

    override public float CalculateCharge(float hours)
    {
        if (hours < 1.0F)
            hours = 1.0F;        // Mindestsatz
        return(hours * billingRate);
    }

    override public string TypeName()
    {
        return("Civil Engineer");
    }
}

class ChemicalEngineer: Engineer
{
    public ChemicalEngineer(string name, float billingRate) :
        base(name, billingRate)
    {
    }

    override public string TypeName()
    {
        return("Chemical Engineer");
    }
}
class Test
{
    public static void Main()
    {
        Engineer[]    earray = new Engineer[2];
        earray[0] = new CivilEngineer("Sir John", 40.0F);
        earray[1] = new ChemicalEngineer("Dr. Curie", 45.0F);

        Console.WriteLine("{0} charge = {1}",
                    earray[0].TypeName(),
                    earray[0].CalculateCharge(2F));
        Console.WriteLine("{0} charge = {1}",
                    earray[1].TypeName(),
                    earray[1].CalculateCharge(0.75F));
    }
}

Durch das Hinzufügen von abstract vor der Engineer-Klasse wird gekennzeichnet, dass die Klasse abstrakt ist (d. h. sie verfügt über mindestens eine abstrakte Funktion). Des Weiteren wurde ein abstract vor der virtuellen Funktion TypeName() eingefügt. Die Verwendung von abstract vor der virtuellen Funktion ist wichtig; das Einfügen von abstract vor dem Klassennamen kennzeichnet, dass es sich um eine abstrakte Klasse handelt, da die abstrakte Funktion leicht zwischen den anderen Funktionen untergehen könnte.

Die Implementierung von CivilEngineer ist identisch, abgesehen davon, dass der Compiler nun sicherstellen wird, dass TypeName() sowohl von CivilEngineer als auch von ChemicalEngineer implementiert wird.


Galileo Computing

7.6 sealed-Schlüsselwort  toptop

Die Verwendung des Schlüsselwortes sealed für Klassen verhindert den Einsatz der Klasse als Basisklasse. Dieses Schlüsselwort wird vor allem dazu verwendet, eine ungewollte Ableitung zu verhindern.

// Fehler
sealed class MyClass
{
    MyClass() {}
}
class MyNewClass : MyClass
{
}

Diese Operation schlägt fehl, da MyNewClass die MyClass-Klasse nicht als Basisklasse verwenden kann, weil MyClass mit dem Schlüsselwort sealed gekennzeichnet ist.






1    Eine Erläuterung zur Funktionsweise dieser Methode finden Sie in Kapitel 11, Versionssteuerung mit Hilfe von New und Override.

2    Ein ähnlicher Effekt kann durch die Verwendung von Schnittstellen erzielt werden. Einen Vergleich zwischen diesen beiden Methoden finden Sie in Kapitel 10, Schnittstellen.

   

Select * from SQL Server 2000




Copyright © Galileo Press GmbH 2001 - 2002
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, fon: 0228.42150.0, fax 0228.42150.77, info@galileo-press.de