Frage

Ich habe einige Vererbungsprobleme, da ich eine Gruppe miteinander verbundener abstrakter Klassen habe, die alle gemeinsam überschrieben werden müssen, um eine Client-Implementierung zu erstellen.Idealerweise würde ich gerne so etwas machen:

abstract class Animal
{
  public Leg GetLeg() {...}
}

abstract class Leg { }

class Dog : Animal
{
  public override DogLeg Leg() {...}
}

class DogLeg : Leg { }

Dies würde es ermöglichen, dass jeder, der die Dog-Klasse verwendet, automatisch DogLegs erhält, und jeder, der die Animal-Klasse verwendet, Legs erhält.Das Problem besteht darin, dass die überschriebene Funktion denselben Typ wie die Basisklasse haben muss, damit sie nicht kompiliert werden kann.Ich verstehe allerdings nicht, warum das nicht so sein sollte, da DogLeg implizit in Leg umgewandelt werden kann.Ich weiß, dass es viele Möglichkeiten gibt, dies zu umgehen, aber ich bin neugieriger, warum dies in C# nicht möglich/implementiert ist.

BEARBEITEN:Ich habe dies etwas geändert, da ich in meinem Code tatsächlich Eigenschaften anstelle von Funktionen verwende.

BEARBEITEN:Ich habe es wieder in Funktionen geändert, da die Antwort nur für diese Situation gilt (Kovarianz des Wertparameters der Set-Funktion einer Eigenschaft). sollte nicht arbeiten).Sorry für die Schwankungen!Mir ist klar, dass dadurch viele Antworten irrelevant erscheinen.

War es hilfreich?

Lösung

Die kurze Antwort lautet: GetLeg ist in seinem Rückgabetyp unveränderlich.Die lange Antwort finden Sie hier: Kovarianz und Kontravarianz

Ich möchte hinzufügen, dass Vererbung zwar normalerweise das erste Abstraktionswerkzeug ist, das die meisten Entwickler aus ihrer Toolbox holen, es aber fast immer möglich ist, stattdessen Komposition zu verwenden.Die Komposition bedeutet für den API-Entwickler etwas mehr Arbeit, macht die API jedoch für ihre Verbraucher nützlicher.

Andere Tipps

Natürlich benötigen Sie eine Besetzung, wenn Sie auf einem gebrochenen Dogleg arbeiten.

Als Rückgabetyp sollte „Dog“ ein „Leg“ und kein „DogLeg“ zurückgeben.Die eigentliche Klasse mag ein DogLeg sein, aber es geht darum, sie zu entkoppeln, sodass der Benutzer von Dog nichts über DogLegs, sondern nur über Legs wissen muss.

Ändern:

class Dog : Animal
{
  public override DogLeg GetLeg() {...}
}

Zu:

class Dog : Animal
{
  public override Leg GetLeg() {...}
}

Tun Sie dies nicht:

 if(a instanceof Dog){
       DogLeg dl = (DogLeg)a.GetLeg();

es macht den Zweck der Programmierung auf den abstrakten Typ zunichte.

Der Grund für das Ausblenden von DogLeg liegt darin, dass die GetLeg-Funktion in der abstrakten Klasse ein abstraktes Bein zurückgibt.Wenn Sie GetLeg überschreiben, müssen Sie ein Leg zurückgeben.Das ist der Sinn einer Methode in einer abstrakten Klasse.Um diese Methode an ihre Kinder weiterzugeben.Wenn Sie möchten, dass die Benutzer des Hundes über DogLegs Bescheid wissen, erstellen Sie eine Methode namens GetDogLeg und geben Sie ein DogLeg zurück.

Wenn Sie tun KÖNNTEN, was der Fragesteller möchte, müsste jeder Benutzer von Animal über ALLE Tiere Bescheid wissen.

Es ist ein absolut berechtigter Wunsch, dass die Signatur einer überschreibenden Methode einen Rückgabetyp hat, der ein Untertyp des Rückgabetyps in der überschriebenen Methode ist (Puh).Schließlich sind sie mit dem Laufzeittyp kompatibel.

Aber C# unterstützt noch keine „kovarianten Rückgabetypen“ in überschriebenen Methoden (im Gegensatz zu C++ [1998] und Java [2004]).

Wie Eric Lippert feststellte, müssen Sie auf absehbare Zeit damit umgehen und auskommen sein Blog[19. Juni 2008]:

Diese Art von Varianz wird als „Rückgabetyp-Kovarianz“ bezeichnet.

Wir haben nicht vor, diese Art von Varianz in C# zu implementieren.

abstract class Animal
{
  public virtual Leg GetLeg ()
}

abstract class Leg { }

class Dog : Animal
{
  public override Leg GetLeg () { return new DogLeg(); }
}

class DogLeg : Leg { void Hump(); }

Machen Sie es so, dann können Sie die Abstraktion in Ihrem Client nutzen:

Leg myleg = myDog.GetLeg();

Dann können Sie es bei Bedarf wie folgt wirken:

if (myleg is DogLeg) { ((DogLeg)myLeg).Hump()); }

Völlig erfunden, aber der Punkt ist, dass Sie Folgendes tun können:

foreach (Animal a in animals)
{
   a.GetLeg().SomeMethodThatIsOnAllLegs();
}

Unter Beibehaltung der Möglichkeit einer speziellen Hump-Methode bei Doglegs.

Sie können Generics und Schnittstellen verwenden, um dies in C# zu implementieren:

abstract class Leg { }

interface IAnimal { Leg GetLeg(); }

abstract class Animal<TLeg> : IAnimal where TLeg : Leg
 { public abstract TLeg GetLeg();
   Leg IAnimal.GetLeg() { return this.GetLeg(); }
 }

class Dog : Animal<Dog.DogLeg>
 { public class DogLeg : Leg { }
   public override DogLeg GetLeg() { return new DogLeg();}
 } 

GetLeg() muss Leg zurückgeben, um eine Überschreibung zu sein.Ihre Dog-Klasse kann jedoch weiterhin DogLeg-Objekte zurückgeben, da sie eine untergeordnete Klasse von Leg sind.Kunden können sie dann als Doglegs anlegen und operieren.

public class ClientObj{
    public void doStuff(){
    Animal a=getAnimal();
    if(a is Dog){
       DogLeg dl = (DogLeg)a.GetLeg();
    }
  }
}

Das Konzept, das Ihnen Probleme bereitet, wird unter beschrieben http://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)

Nicht, dass es viel Nutzen bringt, aber es ist vielleicht interessant zu bemerken, dass Java kovariante Rückgaben unterstützt und dies also genau so funktionieren würde, wie Sie es sich erhofft haben.Außer natürlich, dass Java keine Eigenschaften hat ;)

Vielleicht lässt sich das Problem anhand eines Beispiels leichter erkennen:

Animal dog = new Dog();
dog.SetLeg(new CatLeg());

Nun, das sollte kompiliert werden, wenn Sie Dog kompiliert haben, aber wir wollen wahrscheinlich keinen solchen Mutanten.

Ein damit zusammenhängendes Problem ist: Sollte Dog[] ein Animal[] oder IList<Dog> ein IList<Animal> sein?

C# verfügt über explizite Schnittstellenimplementierungen, um genau dieses Problem zu lösen:

abstract class Leg { }
class DogLeg : Leg { }

interface IAnimal
{
    Leg GetLeg();
}

class Dog : IAnimal
{
    public override DogLeg GetLeg() { /* */ }

    Leg IAnimal.GetLeg() { return GetLeg(); }
}

Wenn Sie über eine Referenz vom Typ Dog einen Hund haben, gibt der Aufruf von GetLeg() ein DogLeg zurück.Wenn Sie dasselbe Objekt haben, die Referenz jedoch vom Typ IAnimal ist, wird ein Bein zurückgegeben.

Richtig, ich verstehe, dass ich einfach wirken kann, aber das bedeutet, dass der Kunde wissen muss, dass Hunde DogLegs haben.Ich frage mich, ob es technische Gründe gibt, warum dies nicht möglich ist, da eine implizite Konvertierung vorliegt.

@Brian Leahy natürlich, wenn Sie nur als Bein dienen, es ist nicht nötig oder Grund zu besetzen.Wenn es jedoch ein DogLeg- oder hundespezifisches Verhalten gibt, gibt es manchmal Gründe, warum die Besetzung notwendig ist.

Sie können auch die Schnittstelle ILeg zurückgeben, die sowohl Leg als auch DogLeg implementieren.

Beachten Sie unbedingt, dass Sie an jeder Stelle, an der Sie den Basistyp verwenden, einen abgeleiteten Typ verwenden können (Sie können Dog an jede Methode/Eigenschaft/Feld/Variable übergeben, die Animal erwartet).

Nehmen wir diese Funktion:

public void AddLeg(Animal a)
{
   a.Leg = new Leg();
}

Eine vollkommen gültige Funktion. Rufen wir die Funktion nun folgendermaßen auf:

AddLeg(new Dog());

Wenn die Eigenschaft Dog.Leg nicht vom Typ Leg ist, enthält die AddLeg-Funktion plötzlich einen Fehler und kann nicht kompiliert werden.

@Luke

Ich denke, dass Sie die Erbschaft vielleicht missverstehen.Dog.GetLeg() gibt ein DogLeg-Objekt zurück.

public class Dog{
    public Leg GetLeg(){
         DogLeg dl = new DogLeg(super.GetLeg());
         //set dogleg specific properties
    }
}


    Animal a = getDog();
    Leg l = a.GetLeg();
    l.kick();

Die tatsächlich aufgerufene Methode ist Dog.GetLeg();und DogLeg.Kick() (ich gehe davon aus, dass es eine Methode Leg.kick() gibt) aus diesem Grund ist der deklarierte Rückgabetyp DogLeg unnötig, da dieser zurückgegeben wird, selbst wenn der Rückgabetyp für Dog.GetLeg() ist Bein.

Sie können das gewünschte Ergebnis erreichen, indem Sie ein generisches Element mit einer entsprechenden Einschränkung verwenden, wie zum Beispiel der folgenden:

abstract class Animal<LegType> where LegType : Leg
{
    public abstract LegType GetLeg();
}

abstract class Leg { }

class Dog : Animal<DogLeg>
{
    public override DogLeg GetLeg()
    {
        return new DogLeg();
    }
}

class DogLeg : Leg { }
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top