Laden...

IList<T> oder List<T> als Parameter- bzw. Rückgabetyp?

Letzter Beitrag vor 17 Jahren 28 Posts 12.283 Views
IList<T> oder List<T> als Parameter- bzw. Rückgabetyp?

hallo,

ich bin derzeit mitten in der Entwicklung einer umfangreichen Business-Logic Komponente mit DAL und getrenntem GUI und allem drum und dran. 😉
Dabei hab ich mir schon öfter überlegt, wie es wohl schöner/besser ist, gewisse Funktionen bereitzustellen.

Man nehme beispielsweise eine Funktion, welche alle vorhandenen Personen liefert (ist wie gesagt ein Beispiel)

Diese könnte nun so aussehen:


// 1.: 
IList<Person> GetPeople();

oder so:


// 2.: 
List<Person> GetPeople();

Der kleine, aber feine Unterschied: Einmal wird ein Interface retourgegeben, einmal eine konkrete Klasse. So spontan und nach meinem Verständnis würde ich sagen, Lösung 1 ist besser - "program against an interface, not an implementation" - es entspricht imho. auch mehr dem "design by contract" ansatz. Jedoch hat die Sache den Nachteil, dass IList z.B. keine "FindAll" Methode enthält (was imho. gut wäre).

Folgendes:


GetPeople().FindAll(new Predicate(...));

ist daher nur möglich, wenn ich Ansatz 2, die zurückgabe einer konkreten Klasse verwende, was mir eingetlich nicht so gut gefällt.

Wie ist eure Meinung dazu?
Ich meine, die zurückgabe einer Liste ist ja etwas sehr oft verwendetes - Wie löst ihr das?

fg
hannes

Hallo Hannes

Mit dem Interface hast du den Vorteil, bspw. irgendwelche Listenklassen von einem OR / Mapper anzusprechen, die IList implementieren.
Wenn du aber immer eine List<OfYourType> hast, könntest du gleich diese zurückgeben.

Gruss Peter

--
Microsoft MVP - Visual Developer ASP / ASP.NET, Switzerland 2007 - 2011

Hallo HannesB,

Interfaces sind bei Parametern von Methoden natürlich viel wichtiger. Wenn du in SetPeople (IList<Person> list) IList statt List verwendest oder sogar noch weniger verlangst z.B. ICollection oder gar nur IEnumerable, dann ermöglichst du es dem Aufrufer, egal was er für eine konkrete Auflistung benutzt, damit die Methode zu füttern. Das ist echt nützliche Flexibilität.

Bei Rückgabewerten liegt die Flexibilität dagegen beim Implementierer der Methode und der Aufrufer hat - wie du ja auch schon erkannt hast - die Nachteile zu tragen. Wenn du die Flexibilität als Implementierer gar nicht brauchst, könntest du - wie Peter Bucher meint - gleich einen konkreten Rückgabetyp wie List<Person> verwenden.

Man könnte sogar noch weitergehen und sagen, dass die Flexibilität, die man sich als Implementierer vorbehält sogar potentiell gefährlich ist. Nimm an der Aufrufer hat sich mit dem Reflector oder im Debugger angeschaut, dass er trotz des Rückgabetyps IList immer eine List zurückbekommt. Dann könnte er auf den Gedanken kommen, einfach auf List zu casten. Wenn du dann in einer neueren Version doch mal was anderes zurückgibst, knallt es.

Nun kann man sagen, dass es die "Schuld" des Aufrufers ist und das stimmt ja auch, aber man hat ihn eben der Versuchung ausgesetzt und im Zweifel schimpf er eben doch auf die blöde geänderte Methode und nicht auf seinen Cast.

Aber nehmen wir mal an, er castet gar nicht, sondern benutzt die Collection tatsächlich nur über das Interface IList, zum Beispiel in dem er mit Add eigene Elemente hinzufügt. Dann könnte man ja später auf Idee kommen, ReadOnlyCollection statt List zurückzugeben und obwohl auch ReadOnlyCollection das Interface IList implementiert, würde es beim Add knallen.

Hier wäre es schon nicht mehr so klar, dass es die "Schuld" des Aufrufers ist.

Deshalb sollte man mindestens ernsthaft erwägen- zumindest bei Collections -, konkrete Rückgabetypen anzugeben.

Wirklich verloren hat der Aufrufer natürlich so oder so nicht. Er könnte, wenn er die Möglichkeiten einer List braucht, aus der IList in einer Zeile eine List machen (new List<Person> (ilist)). Natürlich um den Preis, dass die Collection dabei kopiert wird.

BTW: Get- und Set-Methoden sind bei C# selten. Meistens verwendet man stattdessen Properties.

herbivore

Nachträgliche Anmerkung zur Klarstellung: Es ging und geht mir nicht um Schuld oder um Unschuld, im Sinne von vorwerfbarem Verhalten, sondern um Flexibilität und ihre praktischen Auswirkungen auf die Robustheit der Software.

Man könnte sogar noch weitergehen und sagen, dass die Flexibilität, die man sich als Implementierer vorbehält sogar potentiell gefährlich ist. Nimm an der Aufrufer hat sich mit dem Reflector oder im Debugger angeschaut, dass er trotz des Rückgabetyps IList immer eine List zurückbekommt. Dann könnte er auf den Gedanken kommen, einfach auf List zu casten. Wenn du dann in einer neueren Version doch mal was anderes zurückgibst, knallt es.

Nun kann man sagen, dass es die "Schuld" des Aufrufers ist und das stimmt ja auch, aber man hat ihn eben der Versuchung ausgesetzt und im Zweifel schimpf er eben doch auf die blöde geänderte Methode und nicht auf seinen Cast.

Ist auch seine schuld. Nicht ich hab ihn nicht in Versuchung geführt, sondern er hat der Versuchung "Reflection" nicht widerstanden.

edit: das setz ich unter den Punkt "falsche Verwendung von Reflection" (aber das wäre eher ein anderes Thema.)

Aber nehmen wir mal an, er castet gar nicht, sondern benutzt die Collection tatsächlich nur über das Interface IList, zum Beispiel in dem er mit Add eigene Elemente hinzufügt. Dann könnte man ja später auf Idee kommen, ReadOnlyCollection statt List zurückzugeben und obwohl auch ReadOnlyCollection das Interface IList implementiert, würde es beim Add knallen.

Hier wäre es schon nicht mehr so klar, dass es die "Schuld" des Aufrufers ist.

Ganz eindeutig wäre das hingegen meine Schuld. Schließlich hab ich Funktionalität weggenommen.

Allgemein mach ichs wie schon weiter oben gesagt wurde davon abhängig, wie wichtig mir im speziellen Fall die Flexibilität der Implementation ist. Eher geb ich mal ICollection<T> zurück... wenn ich draufkomm, ich brauch nicht so viel, kann ich das ohne Probleme immer noch in List<T> verwandeln. Umgekeht ist das nicht möglich.

loop:
btst #6,$bfe001
bne.s loop
rts

Aber nehmen wir mal an, er castet gar nicht, sondern benutzt die Collection tatsächlich nur über das Interface IList, zum Beispiel in dem er mit Add eigene Elemente hinzufügt. Dann könnte man ja später auf Idee kommen, ReadOnlyCollection statt List zurückzugeben und obwohl auch ReadOnlyCollection das Interface IList implementiert, würde es beim Add knallen.

Hier wäre es schon nicht mehr so klar, dass es die "Schuld" des Aufrufers ist.

Doch, das waere sehr wohl klar. Schliesslich sollte man, wenn man in eine Auflistung, die als Interface kommt schreibend zugreifen will immer erstmal pruefen, ob die Auflistung ueberhaupt derlei Zugriffe ueberhaupt gestattet.

Aber ich sehe ein anderes Problem: Wenn ich durch eine Auflistung durchenumeriere, dann weiss ich nicht, ob das eine gecachte Auflistung ist, was ja bedeuten wuerde, dass ich mehrfach ohne Probleme durch iterieren kann ohne Performanceprobleme zu bekommen, oder ob eine Streaming Technik zum Einsatz kommt und bei jedem Iterieren ein neuer Datenbankzugriff geschieht und die Auflistung evtl sogar unterschiedliche Elemente enthaelt.

BTW: Get- und Set-Methoden sind bei C# selten. Meistens verwendet man stattdessen Properties.

Wobei die Verwendung von Methoden tw durchaus angebracht ist, um zu unterscheiden, ob es sich einfach um die Wiedergabe eines gecachten Objektes (Property) oder um eine Berechnung bzw Abfrage/sonstwie Beschaffung der Daten handelt.

Ich finde eine allgemeine Antwort kann man (wie meistens bei sowas) nicht geben. List ist einfach, IList oder ICollection ist flexibel. Beides hat seine Vorteile.

Hallo 0815Coder,

Ist auch seine schuld. Nicht ich hab ihn nicht in Versuchung geführt, sondern er hat der Versuchung "Reflection" nicht widerstanden.

letztendlich ist mir egal wer Schuld ist. Fakt ist, dass die Situation durch den Programmier der Methode/Bibliothek vermeidbar ist. Es ist eine Frage der Robustheit. Robustheit: Fähigkeit von Softwaresystemen, auch unter außergewöhnlichen Bedingungen zu funktionieren. Und durch den konkreten Rückgabetyp wird die Software robuster. Insofern ist es zu kurz gesprungen, wenn du sagst "der Aufrufer ist Schuld, um solche Fälle muss ich mich nicht kümmern".

Ganz eindeutig wäre das hingegen meine Schuld. Schließlich hab ich Funktionalität weggenommen.

Ok, ich gebe es ja zu. Ich habe die Schuldfrage indirekt gestellt. Aber es ging mir nicht um Schuld oder um Unschuld. Sondern um Flexibilität und ihre Auswirkungen.

Trotzdem würde ich das nicht ganz so klar sehen. Immerhin ist es die Standard-Vorgehensweise bei .NET, dass readonly Collections bei .NET eben nicht ein reduziertes Interface implementieren, sondern das volle und für die nicht erlaubten Aktionen Exceptions werfen. Insofern musst du, wenn du eine IList bekommst eigentlich immer damit rechnen, dass nur die lesenden Operationen sicher sind.

So klar finde ich die Schuldfrage also auch hier nicht. Aber das ändert nichts daran, dass die Bibliothek durch einen konkreten Rückgabetyp auch diesen Fall vermeiden kann und dadurch die Software insgesamt robuster wird.

herbivore

Bei manchen Auflistungen kann man zur Compile-Zeit auch einfach noch nicht wissen, ob sie schreibgeschuetzt sind, oder nicht. Wenn man ueber einen O/R-Mapper eine Collection implementiert, mit der man Datensaetze hinzufuegen kann, dann entscheidet sich erst zur Laufzeit, ob die Auflistung schreibgeschuetzt ist (naemlich ob der angemeldete User die entsprechenden Berechtigungen auch hat). Meiner Meinung nach ist das ein gewaltiger Vorteil von .NET, flexible Auflistungen zu haben. Und bei statischen Geschichten wie List<T> ist der Aufruf von IsReadonly ja nun wirklich nicht mit irgendwelchen einigermassen relevanten Performanceeinbußen verbunden.

Fakt ist, dass die Situation durch den Programmier der Methode/Bibliothek vermeidbar ist.

Es wäre auch durch den Verwender vermeidbar gewesen, wenn er nicht zu krummen Tricks gegriffen hätte, von welchen er nicht wissen kann, ob sie mit anderen Version noch funktionieren.

Allgemein geb ich dir aber recht.

loop:
btst #6,$bfe001
bne.s loop
rts

Hallo 0815Coder,

Es wäre auch durch den Verwender vermeidbar gewesen, wenn er nicht zu krummen Tricks gegriffen hätte, von welchen er nicht wissen kann, ob sie mit anderen Version noch funktionieren.

nein, das trifft den Kern das Sache nicht. Robustheit betrifft ja immer Fehlersituationen, in diesem Fall Robustheit gegen Fehler, die der Verwender bei der Programmierung macht. Du kannst jetzt nicht einfach sagen, dann soll der Verwender einfach keine Fehler machen. Robustheit, bedeutet in diesem Fall ja gerade zu verhindern, dass ein bestimmter Fehler durch den Verwender überhaupt gemacht werden kann.

herbivore

Ich halte es mit dem Muster:

ParameterÜbergaben so allgemein wie nötig ( Interfaces vor Basisklassen vor Klasse ).

Rückgaben so speziell wie möglich ( Klasse vor Basisklasse vor Interface ).

Rückgaben in Interfaces so allgemein wie möglich ( Interfaces vor Basisklassen vor Klasse ).

@herbivore:
Wer Reflection benutzt, und das um die Schnittstelle einer Klasse zu umgehen,
muss damit leben.
Wenn jemand z.B. private Variablen per Reflection benutzt
kannst du ja auch nicht den Entwickler dafür verantwortlich machen wenn die SW nicht mehr will,
wenn er nach einer restrukturierung des Codes die öffentliche
Schnittstelle der Klasse beibehält aber die internas umstellt.

Wer sich nicht an die öffendliche Schnittstelle einer Klasse hält, macht den fehler, nicht andersrum.

Hallo FZelle,

damit kaprizierst auch du dich leider auf die Schuldfrage, die ich fahrlässigerweise angerissen habe, um die es aber eigentlich überhaupt nicht geht. Wenn es eine Möglichkeit gibt, die Software robuster zu machen, sollte man sie nutzen, anstatt zu sagen, ich bin ja nicht schuld, wenn jemand wegen geringerer Robustheit Fehler macht.

herbivore

Hm, für die meisten Anwendungsfälle reicht das und ist ziemlich optimal, aber was, wenn du virtuelle Methoden aus Basisklassen überschreibst? Dann stehst du vor diesem Problem, ob du willst oder nicht. Die Parameter in der Basismethode wirst du nicht ändern können...

Hallo onlinegurke,

welches Problem meinst du? Und wodurch entsteht es deiner Meinung nach? Reden wir über Rückgabewerte oder doch über Parameter?

herbivore

sorry, hatte ich vergessen mit dazu zu schreiben, ich habe mich auf die Rueckgabewerte bezogen. Mit der Taktik Klassenmethoden immer so genau wie moeglich geht man ja der Thematik sehr galant aus dem Weg, nur leider lässt sich das bei überschriebenen Basisklassenmethoden nicht beeinflussen, weil da evtl die Basisklasse abstrakt ist und der genauen Implememtierung der Methode Spielraum lässt.

Als ich meinen Post geschrieben habe war grad der Beitrag von FZelle der Aktuellste, auf den ich mich auch bezogen habe.

@herbivore:
Deine Argumentation kann ich nicht nachvollziehen.
Wenn ich eine Klasse oder Interface entwerfe, diese zum Programmieren
benutze oder freigebe, bestimme ich damit den Vertrag den ich bereit bin einzuhalten, nämlich alles was Public oder Protected ist.

Wenn Du dann per reflection herbeigehst und etwas benutzt, das ich nicht
freigegeben habe, musst du bei Änderungen der Internas damit leben,
wenn deine SW nicht mehr geht.

Das hat nichts mit robust zu tun, das sind die Regeln die eingehalten werden müssen.

Das wäre genauso, als wenn Du eine E-Klasse nimmst, da nen Flansch in den
Motorraum setzt, irgendetwas am Motor abgreifst, was Du meinst brauchen zu können,
und dann bei einem Modelwechsel den Hersteller verantwortlich machst, wenn dein Flansch nicht mehr passt.

@onlinegurke:
Deshalb hatte ich ja auch geschrieben, so speziell wie möglich.
Wenn durch Vererbung nur Interfaces einen Sinn machen,
dann bleibt dir natürlich nichts anderes übrig als z.B. IList zu benutzen.

Hallo onlinegurke,

ich weiß jetzt was du meinst, sehe aber das Problem nicht. Denn was kann schlimmstenfalls passieren? Die Methode kann ein Objekt der Unterklasse zurückgeben, obwohl der Rückgabetyp die Oberklasse ist. Wenn man Vererbung vernünftig betrieben hat, sollte das aber für den Aufrufer keinen Unterschied machen. Und selbst wenn der Ausrufer explizite Casts geschrieben hat, funktionieren die ja mit einem Unterklassenobjekt in der Regel immer noch.

Hallo FZelle,

Deine Argumentation kann ich nicht nachvollziehen.

dann sind wir und ja einig. 🙂 Ich deine nämlich leider auch nicht.

Das wäre genauso, als wenn Du eine E-Klasse nimmst, da nen Flansch in den Motorraum setzt, irgendetwas am Motor abgreifst, was Du meinst brauchen zu können, und dann bei einem Modelwechsel den Hersteller verantwortlich machst, wenn dein Flansch nicht mehr passt.

darum geht es ja gar nicht. Du bist immer noch bei der Schuldfrage. Der Punkt ist doch der, dass eine E-Klasse, die das abkönnte (also robuster wäre) besser wäre als eine E-Klasse, die das nicht kann. Ich verstehe nicht, was du dagegen haben kannst, Software robuster zu machen, wenn es ohne Mühe geht. Und das ist ja hier so. Deine Argumentation klingt fast so: Ich mache meine Klasse nicht robuster, damit der Aufrufer (früher oder später) ja auf die Fresse fällt, wenn er Mist macht. Ist ja sein Problem und seine Schuld.

herbivore

Es wäre auch durch den Verwender vermeidbar gewesen, wenn er nicht zu krummen Tricks gegriffen hätte, von welchen er nicht wissen kann, ob sie mit anderen Version noch funktionieren.
nein, das trifft den Kern das Sache nicht. Robustheit betrifft ja immer Fehlersituationen, in diesem Fall Robustheit gegen Fehler, die der Verwender bei der Programmierung macht. Du kannst jetzt nicht einfach sagen, dann soll der Verwender einfach keine Fehler machen. Robustheit, bedeutet in diesem Fall ja gerade zu verhindern, dass ein bestimmter Fehler durch den Verwender überhaupt gemacht werden kann.

Gut. Der Benutzer könnte also Fehler machen weil er Reflection benutzt. Er umgeht damit OO Prinzipien.

Es ist auch möglich, Instanzen zu erzeugen ohne irgendeinen Construktor der Klasse aufzurufen.

System.Runtime.Serialization.FormatterServices.GetSafeUninitializedObject(typeof(AClass));

Prüfst du deshalb in jeder Instanzmethode, ob das objekt ordentlich (mit Construktor) erzeugt wurde? Dann könntest du eine entsprechende Exception werfen und dadurch die Robustheit erhöhen - weil man beim ersten Testlauf draufkommt, dass was nicht stimmt (und was), und nicht erst im schlimmsten Fall im Betrieb. Zugegeben, das zu prüfen wäre unverhältnismässig aufwändig.

Ich seh schon, ich behandle eigentlich wieder die Schuldfrage...

@onlinegurke:
Ich glaub was du meinst heisst Covarianz und wird in c# nur bei delegaten unterstützt (leider).

Du meinst in einer abgeleiteteren Klasse kann eine überschriebene Methode eine Instanz zurückgeben, deren Type spezieller ist als der der Methode der Basisklasse? Die speziellere Instanz tatsächlich zurückzugeben ist möglich. Allerdings ist es in c# nicht möglich (ausser bei delegaten) den Typ des Rückgabewerts spezieller zu definieren, was in anderen Sprachen (auch ausserhalb .net) geht, und auch kein Widerspruch zur OO ist.

loop:
btst #6,$bfe001
bne.s loop
rts

Hallo 0815Coder,

mit der Reflection ist das scheinbar wie mit der Schuldfrage. Irgendwie war es wohl fahrlässig von mir, das zu erwähnen. Vergesst das doch mal beides. Fakt ist doch: Wenn der Rückgabetyp IList ist, aber tatsächlich eine List zurückgegeben wird, kann der Aufrufer ohne Probleme auf List casten. Ob er nun böse war und mit dem Reflector geguckt hat oder ob er einen (unnötigen) Cast auf IList schreiben wollte und einfach nur das I von IList vergessen hat, spielt doch überhaupt keine Rolle. Und Fakt ist, wenn ich List wähle schließe ich diese Fehlerquelle aus.

Dein Konstruktorbeispiel finde ich an den Haaren herbeigezogen. Die Wahrscheinlichkeit, das es auftritt ist gering und der Aufwand es zu prüfen - wie du selbst sagst - groß.

Bei dem Fall über den wir hier reden, geht es um die Wahl eines Rückgabetyps. Wenn ich mich für List entscheide, ist das sogar weniger Aufwand, weil ich ein I weniger tippen muss 🙂 zumindest ist es nicht mehr Aufwand. Warum soll ich mich da nicht für die robustere Lösung entscheiden?

Ich verstehe nicht, wie man - an Stellen, wo es keinen Aufwand macht - ernsthaft und wiederholt gegen robuste Software argumentieren kann, nur weil der Aufrufer "Schuld" an dem Fehler ist. Wie gesagt ist ja Robustheit gerade das Merkmal tolerant gegen Fehler zu sein.

herbivore

hallo,

erstmal danke für die rege diskussionbeteiligung.
Zur Programmrobustheit wurde bereits einiges gesagt und alle Argumente haben ein Für/Wider wie meistens (sonst wäre unsere Tätigkeit ja einfach 😄), brauche glaub ich daher dazu nichts weiter zu sagen.

ParameterÜbergaben so allgemein wie nötig ( Interfaces vor Basisklassen vor Klasse ).
Rückgaben so speziell wie möglich ( Klasse vor Basisklasse vor Interface ).
Rückgaben in Interfaces so allgemein wie möglich ( Interfaces vor Basisklassen vor Klasse ).

Wie meinst du das konkret bei den Rückgaben?
Warum unterscheidest du zwischen Rückgaben in Interfaces und Rückgaben "allgemein"?
Letztendlich definiere ich im Interace ja, wie meine Rückgabe aussieht und die Klasse, welches das Interface impl. muss dann auch den definierten Typ zurückgeben, also nicht "so allgemein wie möglich". Soll heißen: Interface und Impl. müssen vom Rückgabetyp ja überienstimmen.

Hab ein kurzes Beispiel angehängt - kannst du bzw. IHR an diesem demonstrieren, wie ihr die Rückgabeparameter festlegen würdet?


class Program
    {
        static void Main(string[] args)
        {
            IPeopleGetter pg = new PeopleGetter();
            pg.GetPeople();  // returns IList of IPerson, so no "Find" methods available
        }
    }

    public class PeopleGetter : IPeopleGetter
    {
        #region IPeopleGetter Members

        public IList<IPerson> GetPeople()
        {
            return new List<IPerson>()
            {
                new Person() { Name="A", Age=1 },
                new Person() { Name="B", Age=2 }
            };
        }

        #endregion
    }

    public interface IPeopleGetter
    {
        IList<IPerson> GetPeople();
    }

    public class Person : IPerson
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }

    public interface IPerson
    {
        string Name { get; set; }
        int Age { get; set; }
    }

thx
hannes

Im Prinzip ist die Antwort doch einfach:

Kriterium A:

Interfaces definieren Freiheitsgrade. Und was die Freiheit des einen ist, ist die Unfreiheit des anderen.

Beispiel: Verwendet man bei Parametern ein Interfaces, ist der Aufrufer frei, aber die Implementierung unfrei. Bei Rückgabewerten ist es genau umgekehrt. Wenn ich z.B. eine SortedList<> als Parameter in eine Funktion stecke, dann kann ich mich auf die Sortierung verlassen und habe die "Freiheit" einen performanteren Suchalgorithmus zu verwenden, als bei IList<>. Bekomme ich nur IList<> freut sich der Aufrufer, aber ich muss gezwungenermaßen eine O(n)-Suche implementieren (Unfreiheit). Mehr Interfaces zu verwenden, als akut notwendig, bedeutet also einen Nachteil in Hinsicht auf Ressourcen.

Kriterium B:

Die zweite Seite der Medaille ist der Änderungsaufwand. Angenommen eine Änderung erfordert eben doch zusätzlich eine Funktion die auch List<> statt nur SortedList<> verwenden kann. Dann hätte mir IList<T> als Parameter die Änderung erspart. Allerdings wäre die Performance nach wie vor suboptimal, wenn auch nicht mehr in allen Fällen. Aus Sicht des Änderungsaufwandes sind Interfaces in jedem Fall ein Gewinn.

Eine Entscheidung, was im konkreten Fall zu verwenden ist folgt aus der Abwägung beider Kriterien.

Meine Regel:

Sofern beides machbar ist und Performance (bzw. Speicher, diese Fälle sind aber selten) offensichtlich keine Rolle spielt, verwende ich IMMER Interfaces. Und das gilt sowohl für Parameter, als auch für Rückgabewerte. Allerdings muss man bei Rückgabewerten ein wenig mehr aufpassen. Laufzeitprobleme akkumulieren nach oben. Nach unten kann man sie einfacher abschätzen. Aber: Ich hatte noch nie einen Fall, wo dies nicht schon zu Beginn erkennbar gewesen wäre.

Meiner Erfahrung nach sind Änderbarkeitsprobleme heutzutage kritischer als Performanceprobleme - zumindest in den meisten Anwendungstypen. Daher gebe ich Interfaces klar den Vorzug. Zudem beteht in den allermeisten Fällen nichtmal ein potentieller Nachteil bei der Verwendung eines Interfaces, weil die Funktion nur die Funktionalität des Interfaces fordert und diese Anforderung offensichtlich stabil ist (Kriterium A kommt nicht zum Tragen). Komkretes Beispiel: In etwa 80% aller Fälle werden Collections einfach nur abiteriert. Hier reicht das entsprechende Interface als Parameter.

Hallo HannesB,

Wie meinst du das konkret bei den Rückgaben?

Was verstehst du denn nicht. Der Typ sollte so speziell wie möglich sein. Also genau das umkehrte wie bei Parametern.

Warum unterscheidest du zwischen Rückgaben in Interfaces und Rückgaben "allgemein"?

Weil es nicht gut kommt, wenn in einem Interface konkrete Typen vorkommen (von den Basistypen mal abgesehen). Interfaces schreibt man ja gerade, um von konkreten Typen zu abstrahieren.

Letztendlich definiere ich im Interace ja, wie meine Rückgabe aussieht und die Klasse, welches das Interface impl. muss dann auch den definierten Typ zurückgeben, also nicht "so allgemein wie möglich". Soll heißen: Interface und Impl. müssen vom Rückgabetyp ja überienstimmen.

Wenn man Interfaces nicht auch in irgendwelche Klassen implementieren würde, hätte man nichts davon. Und wenn eine Klasse ein Interface implementiert, müssen die Signaturen 1:1 stimmen. Insofern geht die Regel für Interfaces vor denen für Klassen.

herbivore

hallo + guten morgen an alle,

na, habts euch schon den ersten kaffe reingestellt? (programmierernahrung) 😉
komme gerade von diesem - zurück zum thema.

@herbivore:
Ich verstehe die Aussage zu den Rückgabetypen und mir ist auch klar, dass ein Interface keine konkreten Typen enthalten soll - sonst würde ja der Sinn eines Interfaces (=Contract) ad absurdum geführt werden.

Meine Frage bezog sich darauf, dass sich Regel 2 und 3 widersprechen, sobald eine Klasse ein Interface implementiert (was wohl immer der Fall ist, da das Interface sonst zwecklos wäre).

Das schriebst:

Und wenn eine Klasse ein Interface implementiert, müssen die Signaturen 1:1 stimmen. Insofern geht die Regel für Interfaces vor denen für Klassen.

darum ging es mir.
Ich halte es für "good practice", für alle klassen interfaces zu definieren, sodass eine andere Klasse, welche diese verwendet ein interface dieser enthalen kann. Daraus ergibt sich, bezogen auf mein Beispiel:

Interface: IList<IPerson> GetPeople();
Implementierung: public IList<IPerson> GetPeople() {...}
=> in der Verwendung wird dann das Interface zurückgegeben mit den bereits genannten Konsequenzen.

Als Schlussfolgerung ergibt sich für mich:
Es ist (fast immer) besser, als Parameter ein Inteface anzubieten (flexibler) und als Rückgabewert (meistens) besser, ein Interface bereitzustellen.

fg
hannes

Hallo HannesB,

da List ein ziemlich grundlegender Typ ist, könnte man m.E. auch gut schreiben:

List<IPerson> GetPeople();

Womit dann wieder die Robustheit mindestens in Bezug auf die Collection selbst gegeben wäre. Das würde ich präferieren.

Als Schlussfolgerung ergibt sich für mich:
Es ist (fast immer) besser, als Parameter ein Inteface anzubieten (flexibler) und als Rückgabewert (meistens) besser, ein Interface bereitzustellen.

Entweder du hast dich verschieben oder wir sind unterschiedlicher Meinung. Meine Schlussfolgerung wäre:

Es ist (fast immer) besser, als Parametertypen (außer bei Basistypen) Interfaces zu verwenden (flexibler) und als Rückgabewert (meistens) besser, einen konkreten Typ zu verwenden. Es sei denn, die Methode ist in einem Interface definiert, dann sollte auch der Rückgabetyp ein Interface sein.

herbivore

Hallo "herbivore", (bin es aus anderen foren gewohnt, dass man sich mit vornamen anspricht, nicht mit dem Username, ist aber auch ok)

Es ist (fast immer) besser, als Parametertypen (außer bei Basistypen) Interfaces zu verwenden (flexibler)

Hier stimmen wir ja überein.

als Rückgabewert (meistens) besser, einen konkreten Typ zu verwenden. Es sei denn, die Methode ist in einem Interface definiert, dann sollte auch der Rückgabetyp ein Interface sein.

Hmm - hier stimmen wir nicht überein jedoch deshalb da meiner Meinung nach die von dir beschriebene Einschränkung "Es sei denn, die Methode ist in einem Interface definiert" fast immer zutreffen sollte, außer es handelt sich um eine Methode, die nur intern verwendet wird. Das führt natürlich direkt zur Diskussion, ob man für alle Klassen ein Interface definieren sollte, was wiederum Vor- und (wenige) Nachteile hat, ist aber ein anderes, auch interessantes Thema.

Ich will jedenfalls nicht sagen, dass deine oder meine Ansicht "falsch" ist - es ging mir in dem Thread darum, unterschiedliche Meinungen und Erfahrungen zu dem Thema einzuholen. 8)

fg
hannes

Hallo HannesB,

Hmm - hier stimmen wir nicht überein jedoch deshalb da meiner Meinung nach die von dir beschriebene Einschränkung "Es sei denn, die Methode ist in einem Interface definiert" fast immer zutreffen sollte

trotzdem trifft es mein Fazit dann doch besser. Wenn der "es-sei-denn-Fall" bei dir immer zutrifft, ist meine Aussagen, genau die gleiche wie deine. Aber wenn bei dir oder vor allem bei jemand anders der "es-sei-denn-Fall" nicht zutrifft, würde er mit deiner Version zu eine falchen/ungüstigen Schluss kommen.

Das führt natürlich direkt zur Diskussion, ob man für alle Klassen ein Interface definieren sollte, was wiederum Vor- und (wenige) Nachteile hat, ist aber ein anderes, auch interessantes Thema.

Zumindest ist entgegen deiner Annahme kaum zu vermuten, dass wirklich alle Methoden (zuerst) in einem Interface definiert sind. Schau mal ins Framework. Ich denke 90% der Methoden sind nicht in einem Interface, sondern direkt in einer Klasse definiert.

Ich halte es auch für falsch und unnötig für alle Klassen Interfaces zu definieren. Die Diskussion hatten wir auch schon in mindestens einem anderen Thread. Ein Interface braucht man nur da, wo überhaupt die realistische Chance besteht, dass tatsächlich mehr als eine Klasse dieses Interface definiert.

herbivore

hallo,

trotzdem trifft es mein Fazit dann doch besser. Wenn der "es-sei-denn-Fall" bei dir immer zutrifft, ist meine Aussagen, genau die gleiche wie deine. Aber wenn bei dir oder vor allem bei jemand anders der "es-sei-denn-Fall" nicht zutrifft, würde er mit deiner Version zu eine falchen/ungüstigen Schluss kommen.

Das stimmt wohl. 👍

Zumindest ist entgegen deiner Annahme kaum zu vermuten, dass wirklich alle Methoden (zuerst) in einem Interface definiert sind. Schau mal ins Framework. Ich denke 90% der Methoden sind nicht in einem Interface, sondern direkt in einer Klasse definiert.

Hmm - Annahme hatte ich hier eigentlich garkeine, mir ging es nur darum, was ihr aus Architektursicht für sinnvoll haltet. Ad. Framework: Da ist es wohl sicher so, sonst hätte ich ja in IList<> eine z.B. Find Methode. Pfuh, das kann ich jetzt aber echt nicht einschätzen, wieviele % es wohl sind, soweit mir bekannt ist es zumindest so, dass es für die meisten Klassen im Framework eine abstrakte Basis gibt, was ja schon was. =)

Den Thread "für alle Klassen ein Interface" muss ich mir raussuchen - interssiert mich, was dazu gesagt wurde.

fg
hannes

Hallo HannesB,

soweit mir bekannt ist es zumindest so, dass es für die meisten Klassen im Framework eine abstrakte Basis gibt, was ja schon was.

das halte ich für ein Gerücht.

herbivore

Hallo,

😁
ok - man sollte sagen: Viele Klassen implementieren ein Interface und/oder haben eine abstrakte Basis.

Auflistungen: IListe, IEnumerable
Streams : abstrakte Basis
Datenbank: abstract DBConnection bzw. IDBConnection interface
Alle Exception: _Exception Interface
Xml Verarbeitung: abstrakte Basis XmlNode
WF: abstrakte Basis DependencyObject
Winforms: alle Contols - ICOmponent

bringt jetzt aber nichts mehr zum Thema.

Jedenfalls thx für eure Antworten + Meinungen!

fg
hannes