Design-Problem: Kovarianz und Parameter für Methoden



  • Guten Abend liebe Community

    Ich bin auf ein Design-Problem gestossen, welches mir zum einen recht gut einleuchtet, wofür ich aber keine Lösung finde. Angenommen, es sei folgende Schnittstelle gegeben:

    public interface ISet<out T>
    {
        // Inhalt ist hier nicht von Bedeutung.
    };
    

    Nach den Regeln der Kovarianz kann man ein ISet<T> als ISet<U> verwenden, wenn T von U erbt bzw. dieses implementiert. Als nächstes möchte ich jedoch Methoden hinzufügen, welche als Argumente von ISet<T> immer nur T fressen - klassische Invarianz - was aber nicht funktionieren kann, da die Kovarianz ISet<T> -> ISet<U> gelten kann:

    public interface ISet<out T>
    {
        void Add(T inv); // Das ist verboten.
    };
    

    Es ist klar, dass sowas nicht gehen kann, aber ich weiss nicht, wie ich das Problem beheben soll. Es ist so, dass ich für ein ISet<T> diverse Methoden mit T-Argumenten bräuchte. Gleichzeitig ist die Kovarianz von ISet<T> notwendig, da die T erst zur Laufzeit kompiliert (!) werden und ich darum nur Basisklassen U von T kenne.

    Wie löse ich diesen Knoten auf? Hoch experimental mal wieder... 😃 👍

    Mit freundlichen Grüssen



  • Ist es denn Notwendig die Kovarianz anzubieten?
    Wenn nicht, umgehst du doch diese Problematik oder nicht?



  • Firefighter schrieb:

    Ist es denn Notwendig die Kovarianz anzubieten?
    Wenn nicht, umgehst du doch diese Problematik oder nicht?

    /rant/ schrieb:

    Gleichzeitig ist die Kovarianz von ISet<T> notwendig, da die T erst zur Laufzeit kompiliert (!) werden und ich darum nur Basisklassen U von T kenne.

    Ohne die Kovarianz kann ich die Generics gleich ganz spülen. Was ich will ist ein wenig kompliziert. Die T sind Klassen, welche per Reflection erzeugt werden und von U erben. Benutzt werden können direkt nur ISet<U>, wobei tatsächlich jedoch ISet<T> benutzt werden.

    Das ist, so wie ich es verstanden habe, die Kovarianz. Oder gibt es hier eine Variante ohne Kovarianz, bei welcher ich zur Kompilierzeit ein maximum an Informationen zur Verfügung habe?



  • Also da ich dir keine Lösung für die Kovarianz geben kann, da ich mich damit nicht ganz so gut auskenne, kann ich nur versuchen einen anderen Weg zu beschreiben.
    Wäre es denn nicht möglich, die Sachen die von T-Abhängig sind in ein extra Interface zu packen und in dem ISet<T> , T über ein Constraint einzuschränken oder geht damit auch die Vererbung flöten? Ist ein recht exotischer Fall den du uns da zeigst 😃



  • Ich glaube, momentan mache ich genau das. Vielleicht ein wenig mehr Code:

    public interface IObject
    {
    }
    
    public interface ISet<out T> where T: IObject
    {
    }
    
    public class Set<T> : ISet<T> where T: IObject
    {
    }
    
    /*
     * Beispiel einer Anwendung
     */
    
    public interface IAddress : IObject
    {
        // Properties...
    }
    
    /* Mittels Reflection zur Laufzeit erzeugt:
     *
     * public class Address : IAddress
     * {
     * }
     */
    
    public ISet<IAddress> Addresses
    {
        get
        {
            // MySet ist ein ISet<IAddress>, welches über Kovarianz aus einem Set<Address> geschält worden ist.
            return this.MySet;
        }
    }
    

    Was ich nun möchte ist, dass man auf dem ISet<IAddress> von ThisSet Methoden aufrufen kann, welche IAddress-Argumente fressen (im Prinzip brauche ich Methoden ähnlich denen von ICollection, aber mit ein wenig anderer Semantik), welche aber vom Typ Address sein müssen. Das angestrebte System ist derart dynamisch, dass gewisse Kontrollen erst zur Laufzeit greifen können, aber ich will das Maximum an compile-time Sicherheit/Information herausholen.

    Da nun aber in ISet<T> die Kovarianz für T gilt, darf ich keine Methoden mit T-Argumenten deklarieren; in der Folge gibt es also für ISet<IAddress> keine Methoden, welche IAddress entgegen nehmen. Gibt es in C# oder sagen wir .NET keine Sprachmittel, die sowas erlauben? Die reale Situation ist im übrigen noch ein wenig Komplexer, da IObject Basis für ein IObject<S> where S: ISet<IObject> ist, wovon dann beispielsweise Address : IAddress, IObject<ISet<Address>> wirklich abgeleitet wird 😃

    Ich habe bis jetzt nur eine Möglichkeit im Kopf, wie ich es realisieren könnte:

    • ISet wieder invariant definieren, sprich den out-Modifier wieder entfernen.
    • In ISet<T> eine Methode ExposeVariant<U> anbieten, welche eine Implementierung von ISet<U> zurückgibt, die ISet<T> wrappt.
    • Alle Varianz-Konvertierungen müssen explizit durchgeführt werden. Das ist vielleicht auch besser so, da weniger verwirrend.

    Das ist eine schöne Machbarkeitsstudie 🤡



  • Tja an der Stelle bin ich leider raus. Da reichen meine Kenntnisse in Sachen Ko/Kontravarianz nicht ganz aus.
    Aber rein Gefühlstechnisch würde ich eher zu dem tendieren was du als mögliche Lösungsansätze prsäentiert hast. Du umgehst deine Probleme aber behälst dein System bei was du verfolgst. Ich finde das ist ne faire Lösung oder nicht?



  • Gut, dann werde ich es wohl so versuchen müssen. Danke für deine Bemühungen.

    PS: Arrays haben das Verhalten, welches ich suche:

    string[] Strings = {"Das", "ist", "ein", "Test"};
    object[] Objects = Strings;
    
    Objects[0] = "Dies"; // funktioniert problemlos, da Objects[0] zwar als object rauskommt, aber eigentlich ein string ist.
    Objects[0] = 7; // funktioniert nicht mehr
    


  • Hallo /rant/,

    unter http://blogs.msdn.com/b/ericlippert/archive/2007/10/17/covariance-and-contravariance-in-c-part-two-array-covariance.aspx erklärt Eric Lippert, warum diese Covariance bei Arrays eigentlich ein Fehler im Design von C# gewesen ist.
    Auch dein Beispiel zeigt ja, daß der Code ja einwandfrei compiliert, aber eben zur Laufzeit einen Fehler wirft (so ähnlich wie ein ungeprüfter Zugriff auf die veraltete ArrayList).



  • U ist deine Basisklasse, die du zur Kompilezeit kennst?
    T ist eine abgeleitete Klasse, die zur Laufzeit erstellt wird? (Es gilt T : U)
    Das Interface soll nur T als generischen Typ akzeptieren?

    Warum machst du dann nicht einfach:

    interface ISet<T> where T : U { ... }
    

    Oder verstehe ich dein Problem nicht?



  • T ist ein Interface, welches zur Laufzeit von U implementiert wird. Ich habe ein ISet<U>, welches zur Laufzeit erzeugt wird. Damit ich möglichst elegant dagegen programmieren kann, möchte ich dieses ISet<U> als ISet<T> benutzen können. Insofern hast du es leider genau umgekehrt 😉


Anmelden zum Antworten