C #: Rückgabetypen überschreiben

80

Gibt es eine Möglichkeit, Rückgabetypen in C # zu überschreiben? Wenn ja wie und wenn nicht warum und was ist eine empfohlene Vorgehensweise?

Mein Fall ist, dass ich eine Schnittstelle mit einer abstrakten Basisklasse und deren Nachkommen habe. Ich würde das gerne machen (ok nicht wirklich, aber als Beispiel!):

public interface Animal
{
   Poo Excrement { get; }
}

public class AnimalBase
{
   public virtual Poo Excrement { get { return new Poo(); } }
}

public class Dog
{
  // No override, just return normal poo like normal animal
}

public class Cat
{
  public override RadioactivePoo Excrement { get { return new RadioActivePoo(); } }
}

RadioactivePooerbt natürlich von Poo.

Mein Grund, dies zu wollen, ist, dass diejenigen, die CatObjekte verwenden, die ExcrementEigenschaft nutzen können, ohne sie einwerfen Poozu müssen, RadioactivePoowährend sie beispielsweise Catimmer noch Teil einer AnimalListe sein können, in der Benutzer möglicherweise nicht unbedingt über ihren radioaktiven Kot informiert sind oder sich darum kümmern. Hoffe das hat Sinn gemacht ...

Soweit ich sehen kann, lässt der Compiler dies zumindest nicht zu. Ich denke es ist unmöglich. Aber was würden Sie als Lösung dafür empfehlen?

Svish
quelle
2
Was ist mit Generika? Würden sie nicht helfen?
Arnis Lapsa
3
Sollte Cat.Excrement () nicht einfach eine Instanz von RadioActivePoo als Poo zurückgeben? Sie haben eine gemeinsame Schnittstelle, verwenden Sie diese. (Und danke für das hysterische Beispiel.)
Hometoast
1
Ich möchte mich auch für das Beispiel bedanken: @goodgai unten schlägt vor, abstraktes Poo zu erstellen - ich frage mich, wie ich dies einem Nicht-Programmierer erklären könnte ...
Paolo Tedesco
1
@Konrad: Etwas unsicher, ob ich nur über diesen brillanten Kommentar lachen soll oder ob ich auch fragen soll, ob es sich tatsächlich um einen sehr schrecklichen Codegeruch handelt, auf den hingewiesen werden sollte (denn in diesem Fall würde ich gerne davon erfahren: p)
Svish
3
Ich bin fast enttäuscht von mir selbst, wie lustig ich diese Beispiele fand XD
Gurgadurgen

Antworten:

22

Ich weiß, dass es bereits viele Lösungen für dieses Problem gibt, aber ich denke, ich habe eine gefunden, die die Probleme behebt, die ich mit den vorhandenen Lösungen hatte.

Ich war aus folgenden Gründen mit einigen der vorhandenen Lösungen nicht zufrieden:

  • Paolo Tedescos erste Lösung: Katze und Hund haben keine gemeinsame Basisklasse.
  • Paolo Tedescos zweite Lösung: Es ist etwas kompliziert und schwer zu lesen.
  • Daniel Daranas 'Lösung: Dies funktioniert, aber es würde Ihren Code mit vielen unnötigen Casting- und Debug.Assert () -Anweisungen überladen.
  • Lösungen von hjb417: Mit dieser Lösung können Sie Ihre Logik nicht in einer Basisklasse halten. Die Logik ist in diesem Beispiel (Aufruf eines Konstruktors) ziemlich trivial, in einem Beispiel aus der realen Welt jedoch nicht.

Meine Lösung

Diese Lösung sollte alle oben genannten Probleme lösen, indem sowohl Generika als auch Methoden verwendet werden.

public class Poo { }
public class RadioactivePoo : Poo { }

interface IAnimal
{
    Poo Excrement { get; }
}

public class BaseAnimal<PooType> : IAnimal
    where PooType : Poo, new()
{
    Poo IAnimal.Excrement { get { return (Poo)this.Excrement; } }

    public PooType Excrement
    {
        get { return new PooType(); }
    }
}

public class Dog : BaseAnimal<Poo> { }
public class Cat : BaseAnimal<RadioactivePoo> { }

Mit dieser Lösung müssen Sie nichts in Dog OR Cat überschreiben! Hier einige Beispiele für die Verwendung:

Cat bruce = new Cat();
IAnimal bruceAsAnimal = bruce as IAnimal;
Console.WriteLine(bruce.Excrement.ToString());
Console.WriteLine(bruceAsAnimal.Excrement.ToString());

Dies wird zweimal "RadioactivePoo" ausgeben, was zeigt, dass der Polymorphismus nicht gebrochen wurde.

Weiterführende Literatur

  • Explizite Schnittstellenimplementierung
  • neuer Modifikator . Ich habe es in dieser vereinfachten Lösung nicht verwendet, aber Sie benötigen es möglicherweise in einer komplizierteren Lösung. Wenn Sie beispielsweise eine Schnittstelle für BaseAnimal erstellen möchten, müssen Sie diese in Ihrer Dekleration von "PooType Excrement" verwenden.
  • out Generic Modifier (Kovarianz) . Wieder habe ich es in dieser Lösung nicht verwendet, aber wenn Sie so etwas wie die Rückkehr MyType<Poo>von IAnimal und die Rückkehr MyType<PooType>von BaseAnimal durchführen möchten, müssen Sie es verwenden, um zwischen den beiden wechseln zu können.
rauben
quelle
6
Alter, das kann super cool sein. Ich habe im Moment keine Zeit, weiter zu analysieren, aber es sieht so aus, als hätten Sie es vielleicht, und wenn Sie dies geknackt haben, High Five und danke fürs Teilen. Leider ist dieses Geschäft mit Kot und Exkrementen eine große Ablenkung.
Nicholas Petersen
1
Ich glaube, ich habe eine Lösung für ein verwandtes Problem gefunden, bei dem eine Methode den geerbten Typ zurückgeben muss - dh eine Methode in 'Dog', die von 'Animal' geerbt wurde, gibt dennoch Dog (this) und nicht Animal zurück. Es wird mit Erweiterungsmethoden gemacht, die ich hier teilen kann, wenn ich dazu komme.
Nicholas Petersen
In Bezug auf die Typensicherheit werden sowohl bruce.Excement als auch bruceAsAnimal.Excrement vom Typ RadioactivePoo ??
Anestis Kivranoglou
@ AnestisKivranoglou ja beide werden vom Typ RadioactivePoo sein
rob
Ich habe SO seit 48 Stunden gepflügt, und diese Antwort hat es gerade geknackt. Und ich dachte ... "Alter, das ist vielleicht super cool."
Martin Hansen Lennox
50

Was ist mit einer generischen Basisklasse?

public class Poo { }
public class RadioactivePoo : Poo { }

public class BaseAnimal<PooType> 
    where PooType : Poo, new() {
    PooType Excrement {
        get { return new PooType(); }
    }
}

public class Dog : BaseAnimal<Poo> { }
public class Cat : BaseAnimal<RadioactivePoo> { }

EDIT : Eine neue Lösung mit Erweiterungsmethoden und einer Marker-Schnittstelle ...

public class Poo { }
public class RadioactivePoo : Poo { }

// just a marker interface, to get the poo type
public interface IPooProvider<PooType> { }

// Extension method to get the correct type of excrement
public static class IPooProviderExtension {
    public static PooType StronglyTypedExcrement<PooType>(
        this IPooProvider<PooType> iPooProvider) 
        where PooType : Poo {
        BaseAnimal animal = iPooProvider as BaseAnimal;
        if (null == animal) {
            throw new InvalidArgumentException("iPooProvider must be a BaseAnimal.");
        }
        return (PooType)animal.Excrement;
    }
}

public class BaseAnimal {
    public virtual Poo Excrement {
        get { return new Poo(); }
    }
}

public class Dog : BaseAnimal, IPooProvider<Poo> { }

public class Cat : BaseAnimal, IPooProvider<RadioactivePoo> {
    public override Poo Excrement {
        get { return new RadioactivePoo(); }
    }
}

class Program { 
    static void Main(string[] args) {
        Dog dog = new Dog();
        Poo dogPoo = dog.Excrement;

        Cat cat = new Cat();
        RadioactivePoo catPoo = cat.StronglyTypedExcrement();
    }
}

Auf diese Weise erben Hund und Katze beide von Animal (wie in den Kommentaren erwähnt, hat meine erste Lösung das Erbe nicht bewahrt).
Es ist notwendig, die Klassen explizit mit der Marker-Oberfläche zu markieren, was schmerzhaft ist, aber vielleicht könnte dies Ihnen einige Ideen geben ...

ZWEITE BEARBEITUNG @Svish: Ich habe den Code geändert, um explizit zu zeigen, dass die Erweiterungsmethode die Tatsache, von der iPooProvidererbt , in keiner Weise erzwingt BaseAnimal. Was meinst du mit "noch stärker typisiert"?

Paolo Tedesco
quelle
Dachte nur das Gleiche.
Doktor Jones
9
Verlieren Sie nicht den Pollymorphismus zwischen Hund und Katze?
Almog.ori
Was wäre, wenn es andere Typen gäbe, die Sie ebenfalls überschreiben möchten? Sie könnten technisch gesehen eine ganze Reihe von Typargumenten haben. Was ich vielleicht als nervig empfunden habe ... aber ja, das ist eine Lösung.
Svish
@Svish: Ich stelle mir vor, dass es Zeit ist, ein Abhängigkeitsinjektions-Framework zu verwenden, sobald dies passiert.
Brian
Woher weiß die StronglyTypedExcrement-Methode, dass iPooProvider ein BaseAnimal ist? Vermutet es nur? Oder sehe ich das nicht? Wäre es möglich, diese Methode noch stärker typisiert zu machen?
Svish
31

Dies wird als Kovarianz vom Rückgabetyp bezeichnet und wird in C # oder .NET im Allgemeinen trotz der Wünsche einiger Leute nicht unterstützt .

Was ich tun würde, ist die gleiche Signatur ENSUREbeizubehalten, aber der abgeleiteten Klasse eine zusätzliche Klausel hinzuzufügen, in der ich sicherstelle, dass diese eine zurückgibt RadioActivePoo. Kurz gesagt, ich würde über Design by Contract tun, was ich nicht über Syntax tun kann.

Andere fälschen es lieber. Es ist in Ordnung, denke ich, aber ich neige dazu, "Infrastruktur" -Codezeilen zu sparen. Wenn die Semantik des Codes klar genug ist, bin ich glücklich, und durch vertragliches Design kann ich dies erreichen, obwohl es sich nicht um einen Mechanismus zur Kompilierungszeit handelt.

Gleiches gilt für Generika, die andere Antworten nahe legen. Ich würde sie aus einem besseren Grund verwenden, als nur radioaktiven Kot zurückzugeben - aber das bin nur ich.

Daniel Daranas
quelle
Was ist die ENSURE-Klausel? Wie würde das funktionieren? Ist es ein Attribut in .Net?
Svish
1
In .Net und bevor ich die Codeverträge von .Net 4.0 sehe, schreibe ich ENSURE (x) -Klauseln einfach als "Debug.Assert (x)". Weitere Referenzen finden Sie beispielsweise in archive.eiffel.com/doc/manuals/technology/contract/page.html oder Object Oriented Software Construction, 2. Auflage, von Bertrand Meyer (1994), Kapitel 11.
Daniel Daranas,
5
"Ich würde sie aus einem besseren Grund verwenden, als nur radioaktives Kot zurückzugeben - aber das bin nur ich" gehört in meine Liste der eigenen Lieblingszitate :)
Daniel Daranas
9

Es gibt auch diese Option (explizite Schnittstellenimplementierung)

public class Cat:Animal
{
  Poo Animal.Excrement { get { return Excrement; } }
  public RadioactivePoo Excrement { get { return new RadioactivePoo(); } }
}

Sie verlieren die Fähigkeit, die Basisklasse zum Implementieren von Cat zu verwenden, aber auf der positiven Seite behalten Sie den Polymorphismus zwischen Cat und Dog bei.

Aber ich bezweifle, dass sich die zusätzliche Komplexität lohnt.

Rasmus Faber
quelle
4

Definieren Sie eine geschützte virtuelle Methode, die das 'Exkrement' erstellt, und behalten Sie die öffentliche Eigenschaft bei, die das 'Exkrement' nicht virtuell zurückgibt. Dann können abgeleitete Klassen den Rückgabetyp der Basisklasse überschreiben.

Im folgenden Beispiel mache ich 'Excrement' nicht virtuell, stelle aber die Eigenschaft ExcrementImpl bereit, damit abgeleitete Klassen das richtige 'Poo' bereitstellen können. Abgeleitete Typen können dann den Rückgabetyp von 'Excrement' überschreiben, indem die Implementierung der Basisklasse ausgeblendet wird.

Ex:

namepace ConsoleApplication8

{
public class Poo { }

public class RadioactivePoo : Poo { }

public interface Animal
{
    Poo Excrement { get; }
}

public class AnimalBase
{
    public Poo Excrement { get { return ExcrementImpl; } }

    protected virtual Poo ExcrementImpl
    {
        get { return new Poo(); }
    }
}

public class Dog : AnimalBase
{
    // No override, just return normal poo like normal animal
}

public class Cat : AnimalBase
{
    protected override Poo ExcrementImpl
    {
        get { return new RadioactivePoo(); }
    }

    public new RadioactivePoo Excrement { get { return (RadioactivePoo)ExcrementImpl; } }
}
}
Hasani Blackwell
quelle
Das Beispiel machte es so viel schwieriger zu verstehen. aber toller Code!
Rposky
2

Korrigieren Sie mich, wenn ich falsch liege, aber nicht der springende Punkt des Pollymorphismus ist, um RadioActivePoo zurückgeben zu können, wenn es von Poo erbt. Der Vertrag wäre der gleiche wie die abstrakte Klasse, aber geben Sie einfach RadioActivePoo () zurück.

almog.ori
quelle
2
Sie haben vollkommen recht, aber er möchte die zusätzliche Besetzung und das stärkere Tippen vermeiden, wofür hauptsächlich Generika bestimmt sind ...
Paolo Tedesco
2

Versuche dies:

namespace ClassLibrary1
{
    public interface Animal
    {   
        Poo Excrement { get; }
    }

    public class Poo
    {
    }

    public class RadioactivePoo
    {
    }

    public class AnimalBase<T>
    {   
        public virtual T Excrement
        { 
            get { return default(T); } 
        }
    }


    public class Dog : AnimalBase<Poo>
    {  
        // No override, just return normal poo like normal animal
    }

    public class Cat : AnimalBase<RadioactivePoo>
    {  
        public override RadioactivePoo Excrement 
        {
            get { return new RadioactivePoo(); } 
        }
    }
}
Shiraz Bhaiji
quelle
Was ist der Sinn der Tierschnittstelle hier? Nichts erbt davon.
Rob
1

Ich glaube, ich habe einen Weg gefunden, der nicht von Generika oder Erweiterungsmethoden abhängt, sondern von Methoden, die sich verstecken. Es kann jedoch den Polymorphismus brechen. Seien Sie also besonders vorsichtig, wenn Sie weiter von Cat erben.

Ich hoffe, dieser Beitrag konnte noch jemandem helfen, obwohl er 8 Monate zu spät kam.

public interface Animal
{
    Poo Excrement { get; }
}

public class Poo
{
}

public class RadioActivePoo : Poo
{
}

public class AnimalBase : Animal
{
    public virtual Poo Excrement { get { return new Poo(); } }
}

public class Dog : AnimalBase
{
    // No override, just return normal poo like normal animal
}

public class CatBase : AnimalBase
{
    public override Poo Excrement { get { return new RadioActivePoo(); } }
}

public class Cat : CatBase
{
    public new RadioActivePoo Excrement { get { return (RadioActivePoo) base.Excrement; } }
}
Cybis
quelle
Aack, egal. Ich wusste nicht, dass hjb417 bereits eine ähnliche Lösung veröffentlicht hat. Zumindest muss ich die Basisklasse nicht ändern.
Cybis
Ihre „Lösung“ TUT Pause Polymorphismus, so dass es nicht wirklich eine Lösung. Auf der anderen Seite ist die hjb-Lösung eine echte Lösung und meiner Meinung nach ziemlich klug.
Greenoldman
0

Es kann hilfreich sein, wenn RadioactivePoo von poo abgeleitet ist und dann Generika verwendet.

Bobbyalex
quelle
0

Zu Ihrer Information. Dies ist in Scala recht einfach zu implementieren.

trait Path

trait Resource
{
    def copyTo(p: Path): Resource
}
class File extends Resource
{
    override def copyTo(p: Path): File = new File
    override def toString = "File"
}
class Directory extends Resource
{
    override def copyTo(p: Path): Directory = new Directory
    override def toString = "Directory"
}

val test: Resource = new Directory()
test.copyTo(null)

Hier ist ein Live-Beispiel, mit dem Sie spielen können: http://www.scalakata.com/50d0d6e7e4b0a825d655e832

jedesah
quelle
0

Ich glaube, Ihre Antwort heißt Kovarianz.

class Program
{
    public class Poo
    {
        public virtual string Name { get{ return "Poo"; } }
    }

    public class RadioactivePoo : Poo
    {
        public override string Name { get { return "RadioactivePoo"; } }
        public string DecayPeriod { get { return "Long time"; } }
    }

    public interface IAnimal<out T> where T : Poo
    {
        T Excrement { get; }
    }

    public class Animal<T>:IAnimal<T> where T : Poo 
    {
        public T Excrement { get { return _excrement ?? (_excrement = (T) Activator.CreateInstance(typeof (T), new object[] {})); } } 
        private T _excrement;
    }

    public class Dog : Animal<Poo>{}
    public class Cat : Animal<RadioactivePoo>{}

    static void Main(string[] args)
    {
        var dog = new Dog();
        var cat = new Cat();

        IAnimal<Poo> animal1 = dog;
        IAnimal<Poo> animal2 = cat;

        Poo dogPoo = dog.Excrement;
        //RadioactivePoo dogPoo2 = dog.Excrement; // Error, dog poo is not RadioactivePoo.

        Poo catPoo = cat.Excrement;
        RadioactivePoo catPoo2 = cat.Excrement;

        Poo animal1Poo = animal1.Excrement;
        Poo animal2Poo = animal2.Excrement;
        //RadioactivePoo animal2RadioactivePoo = animal2.Excrement; // Error, IAnimal<Poo> reference do not know better.


        Console.WriteLine("Dog poo name: {0}",dogPoo.Name);
        Console.WriteLine("Cat poo name: {0}, decay period: {1}" ,catPoo.Name, catPoo2.DecayPeriod);
        Console.WriteLine("Press any key");

        var key = Console.ReadKey();
    }
}
Pierre Grondin
quelle
0

Sie können einfach eine Schnittstelle zurückgeben. In Ihrem Fall IPoo.

Dies ist in Ihrem Fall der Verwendung eines generischen Typs vorzuziehen, da Sie eine Kommentarbasisklasse verwenden.

David Shaskin
quelle
0

Im Folgenden werden einige der besten Aspekte mehrerer anderer Antworten sowie eine Technik kombiniert, um den Schlüsselaspekt Cateiner ExcrementEigenschaft des erforderlichen RadioactivePooTyps zu ermöglichen. Sie können dies jedoch nur dann zurückgeben, Poowenn wir nur wissen, dass wir eine AnimalBaseeher als haben speziell aCat .

Aufrufer müssen weder Generika verwenden, obwohl sie in den Implementierungen vorhanden sind, noch eine Funktion mit einem anderen Namen aufrufen, um etwas Besonderes zu erhalten Poo.

Die Zwischenklasse AnimalWithSpecialisationsdient nur zum Versiegeln der ExcrementEigenschaft und verbindet sie über eine nicht öffentliche SpecialPooEigenschaft mit der abgeleiteten Klasse, AnimalWithSpecialPoo<TPoo>die eine ExcrementEigenschaft eines abgeleiteten Rückgabetyps hat.

Wenn Cates das einzige Tier Pooist, das in irgendeiner Weise etwas Besonderes ist, oder wenn wir nicht möchten, dass der Typ Excrementdas Hauptdefinitionsmerkmal von a ist Cat, könnte die generische Zwischenklasse in der Hierarchie übersprungen werden, sodass sie Catdirekt von abgeleitet wird AnimalWithSpecialisations, aber wenn vorhanden Es gibt mehrere verschiedene Tiere, deren Hauptmerkmal darin besteht, dass sie Pooin irgendeiner Weise etwas Besonderes sind. Die Aufteilung der "Kesselplatte" in Zwischenklassen hilft dabei, die Tiere zu haltenCat Klasse selbst ziemlich sauber , wenn auch auf Kosten zusätzlicher virtueller Funktionsaufrufe.

Der Beispielcode zeigt, dass die meisten erwarteten Vorgänge "wie erwartet" funktionieren.

public interface IExcretePoo<out TPoo>
  where TPoo : Poo
{
  TPoo Excrement { get; }
}

public class Poo
{ }

public class RadioactivePoo : Poo
{ }

public class AnimalBase : IExcretePoo<Poo>
{
  public virtual Poo Excrement { get { return new Poo(); } }
}

public class Dog : AnimalBase
{
  // No override, just return normal poo like normal animal
}

public abstract class AnimalWithSpecialisations : AnimalBase
{
  // this class connects AnimalBase to AnimalWithSpecialPoo<TPoo>
  public sealed override Poo Excrement { get { return SpecialPoo; } }

  // if not overridden, our "special" poo turns out just to be normal animal poo...
  protected virtual Poo SpecialPoo { get { return base.Excrement; } }
}

public abstract class AnimalWithSpecialPoo<TPoo> : AnimalWithSpecialisations, IExcretePoo<TPoo>
  where TPoo : Poo
{
  sealed protected override Poo SpecialPoo { get { return Excrement; } }
  public new abstract TPoo Excrement { get; }
}

public class Cat : AnimalWithSpecialPoo<RadioactivePoo>
{
  public override RadioactivePoo Excrement { get { return new RadioactivePoo(); } }
}

class Program
{
  static void Main(string[] args)
  {
    Dog dog = new Dog();
    Poo dogPoo = dog.Excrement;

    Cat cat = new Cat();
    RadioactivePoo catPoo = cat.Excrement;

    AnimalBase animal = cat;

    Poo animalPoo = catPoo;
    animalPoo = animal.Excrement;

    AnimalWithSpecialPoo<RadioactivePoo> radioactivePooingAnimal = cat;
    RadioactivePoo radioactivePoo = radioactivePooingAnimal.Excrement;

    IExcretePoo<Poo> pooExcreter = cat; // through this interface we don't know the Poo was radioactive.
    IExcretePoo<RadioactivePoo> radioactivePooExcreter = cat; // through this interface we do.

    // we can replace these with the dog equivalents:
    animal = dog;
    animalPoo = dogPoo;
    pooExcreter = dog;

    // but we can't do:
    // radioactivePooExcreter = dog;
    // radioactivePooingAnimal = dog;
    // radioactivePoo = dogPoo;
  }
Steve
quelle
0

C # 9 gibt uns kovariante Override-Rückgabetypen. Grundsätzlich gilt: Was Sie wollen, funktioniert einfach .

Marc Gravell
quelle
-1

Nun, es ist tatsächlich möglich, einen konkreten Typ zurückzugeben, der vom geerbten Rückgabetyp abweicht (auch für statische Methoden), dank dynamic:

public abstract class DynamicBaseClass
{
    public static dynamic Get (int id) { throw new NotImplementedException(); }
}

public abstract class BaseClass : DynamicBaseClass
{
    public static new BaseClass Get (int id) { return new BaseClass(id); }
}

public abstract class DefinitiveClass : BaseClass
{
    public static new DefinitiveClass Get (int id) { return new DefinitiveClass(id);
}

public class Test
{
    public static void Main()
    {
        var testBase = BaseClass.Get(5);
        // No cast required, IntelliSense will even tell you
        // that var is of type DefinitiveClass
        var testDefinitive = DefinitiveClass.Get(10);
    }
}

Ich habe dies in einem API-Wrapper implementiert, den ich für mein Unternehmen geschrieben habe. Wenn Sie vorhaben, eine API zu entwickeln, kann dies in einigen Anwendungsfällen die Benutzerfreundlichkeit und die Entwicklererfahrung verbessern. Die Verwendung von dynamicwirkt sich jedoch auf die Leistung aus. Versuchen Sie daher, dies zu vermeiden.

Wobuntu
quelle