Ist die Rückgabe von IList <T> schlechter als die Rückgabe von T [] oder List <T>?

80

Die Antworten auf Fragen wie diese: List <T> oder IList <T> scheinen immer zuzustimmen, dass die Rückgabe einer Schnittstelle besser ist als die Rückgabe einer konkreten Implementierung einer Sammlung. Aber ich kämpfe damit. Das Instanziieren einer Schnittstelle ist nicht möglich. Wenn Ihre Methode also eine Schnittstelle zurückgibt, gibt sie tatsächlich immer noch eine bestimmte Implementierung zurück. Ich habe ein bisschen damit experimentiert, indem ich 2 kleine Methoden geschrieben habe:

public static IList<int> ExposeArrayIList()
{
    return new[] { 1, 2, 3 };
}

public static IList<int> ExposeListIList()
{
    return new List<int> { 1, 2, 3 };
}

Und benutze sie in meinem Testprogramm:

static void Main(string[] args)
{
    IList<int> arrayIList = ExposeArrayIList();
    IList<int> listIList = ExposeListIList();

    //Will give a runtime error
    arrayIList.Add(10);
    //Runs perfectly
    listIList.Add(10);
}

In beiden Fällen, wenn ich versuche, einen neuen Wert hinzuzufügen, gibt mir mein Compiler keine Fehler, aber offensichtlich gibt die Methode, die mein Array als darstellt, IList<T>einen Laufzeitfehler aus, wenn ich versuche, etwas hinzuzufügen. So können die Leute , die nicht wissen , was in meiner Methode geschieht, und Werte , um es hinzuzufügen müssen, zu gezwungen sind , kopieren Sie zuerst meine IListauf eine ListLage sein , Werte hinzuzufügen , ohne Fehler zu riskieren. Natürlich können sie einen Typcheck durchführen, um festzustellen, ob es sich um ein Listoder ein handelt Array, aber wenn sie dies nicht tun und der Sammlung Elemente hinzufügen möchten, haben sie keine andere Wahl, um das IListin ein zu kopieren List, selbst wenn es ist schon einList . Sollte ein Array niemals als belichtet werden IList?

Ein weiteres Anliegen von mir basiert auf der akzeptierten Antwort auf die verknüpfte Frage (Schwerpunkt Mine):

Wenn Sie Ihre Klasse über eine Bibliothek verfügbar machen, die andere verwenden, möchten Sie sie im Allgemeinen über Schnittstellen und nicht über konkrete Implementierungen verfügbar machen. Dies ist hilfreich, wenn Sie die Implementierung Ihrer Klasse später ändern, um eine andere konkrete Klasse zu verwenden. In diesem Fall müssen die Benutzer Ihrer Bibliothek ihren Code nicht aktualisieren, da sich die Benutzeroberfläche nicht ändert.

Wenn Sie es nur intern verwenden, ist es Ihnen vielleicht nicht so wichtig, und die Verwendung von List ist möglicherweise in Ordnung.

Stellen Sie sich vor, jemand hat meine IList<T>von meiner ExposeListIlist()Methode erhaltenen einfach so verwendet, um Werte hinzuzufügen / zu entfernen. Alles funktioniert gut. Aber jetzt, wie die Antwort andeutet, gebe ich ein Array anstelle einer Liste zurück (kein Problem auf meiner Seite!), Da die Rückgabe einer Schnittstelle flexibler ist.

TLDR:

1) Das Freilegen einer Schnittstelle verursacht unnötige Besetzungen? Ist das nicht wichtig?

2) Manchmal, wenn Benutzer der Bibliothek keine Besetzung verwenden, kann ihr Code beim Ändern Ihrer Methode beschädigt werden, obwohl die Methode vollkommen in Ordnung bleibt.

Ich überdenke das wahrscheinlich, aber ich bin mir nicht einig, dass die Rückgabe einer Schnittstelle der Rückgabe einer Implementierung vorzuziehen ist.

Alexander Derck
quelle
9
Dann kehren IEnumerable<T>Sie zurück und Sie haben Kompilierungszeitsicherheit. Sie können weiterhin alle LINQ-Erweiterungsmethoden verwenden, die normalerweise versuchen, die Leistung zu optimieren, indem Sie sie in einen bestimmten Typ umwandeln (Sie ICollection<T>möchten die CountEigenschaft verwenden, anstatt sie aufzuzählen).
Tim Schmelter
4
Arrays werden IList<T>durch einen "Hack" implementiert . Wenn Sie eine Methode verfügbar machen möchten, die sie zurückgibt, geben Sie einen Typ zurück, der sie tatsächlich implementiert.
CodeCaster
3
Mach einfach keine Versprechen, die du nicht halten kannst. Der Client-Programmierer ist ziemlich wahrscheinlich enttäuscht, wenn er Ihren Vertrag "Ich werde eine Liste zurückgeben" liest. Es ist nicht so, tu das einfach nicht.
Hans Passant
7
Von MSDN : IList ist ein Nachkomme der ICollection-Schnittstelle und die Basisschnittstelle aller nicht generischen Listen. IList-Implementierungen lassen sich in drei Kategorien einteilen: schreibgeschützt, feste Größe und variable Größe. Eine schreibgeschützte IList kann nicht geändert werden. Eine IList mit fester Größe ermöglicht nicht das Hinzufügen oder Entfernen von Elementen, sondern das Ändern vorhandener Elemente. Eine IList variabler Größe ermöglicht das Hinzufügen, Entfernen und Ändern von Elementen.
Hamid Pourjam
3
@AlexanderDerck: Wenn Sie möchten, dass der Verbraucher Artikel zu Ihrer Liste hinzufügen kann, machen Sie es nicht IEnumerable<T>an erster Stelle. Dann ist es in Ordnung zurückzukehren IList<T>oder List<T>.
Tim Schmelter

Antworten:

108

Vielleicht beantwortet dies Ihre Frage nicht direkt, aber in .NET 4.5+ ziehe ich es vor, diese Regeln beim Entwerfen öffentlicher oder geschützter APIs zu befolgen:

  • kehren Sie zurück IEnumerable<T>, wenn nur eine Aufzählung verfügbar ist;
  • Geben Sie zurück, IReadOnlyCollection<T>wenn sowohl die Aufzählung als auch die Anzahl der Elemente verfügbar sind.
  • Geben Sie zurück IReadOnlyList<T>, wenn Aufzählung, Anzahl der Elemente und indizierter Zugriff verfügbar sind.
  • Geben Sie zurück, ICollection<T>wenn Aufzählung, Anzahl der Elemente und Änderung verfügbar sind.
  • Geben Sie zurück IList<T>, wenn Aufzählung, Anzahl der Elemente, indizierter Zugriff und Änderung verfügbar sind.

Die letzten beiden Optionen setzen voraus, dass diese Methode kein Array als IList<T>Implementierung zurückgeben darf .

Dennis
quelle
2
Keine Antwort, aber eine gute Faustregel und sehr hilfreich für mich :) Ich hatte in letzter Zeit große Probleme damit, zu entscheiden, was ich zurückgeben möchte. Prost!
Alexander Derck
1
Dies ist in der Tat das richtige Prinzip +1. Ich würde nur der Vollständigkeit halber hinzufügen , IReadOnlyCollection<T>und ICollection<T>die für nicht lösbaren Behälter ihre Verwendung haben (zum Beispiel der spätere ist de facto ein Standard für EF Entität Unter Sammlung Navigationseigenschaften)
Ivan Stoev
@IvanStoev Kannst du die Antwort bitte mit den hinzugefügten bearbeiten? Es wäre schön, wenn ich endlich eine Liste bekommen würde, wo ich welche Schnittstelle verwenden soll
Alexander Derck
3
Es muss beachtet werden, dass IReadOnlyCollection<T>und IReadOnlyList<T>haben den gleichen Fehler wie die C ++ const. Es gibt zwei Möglichkeiten: Entweder ist die Sammlung unveränderlich, oder es ist nur Ihre Art des Zugriffs auf die Sammlung, die Sie daran hindert, sie zu ändern. Und man kann nicht voneinander unterscheiden.
Ach
1
@Panzercrisis: Ich würde natürlich die zweite Option wählen - IEnumerable<T>. Als API-Entwickler verfügen Sie über umfassende Kenntnisse über zulässige Anwendungsfälle. Wenn Methodenergebnisse nur aufgezählt werden sollen, gibt es keinen Grund zur Offenlegung IList<T>. Je weniger mit dem Ergebnis zu tun haben darf, desto flexibler ist die Methodenimplementierung. Wenn IEnumerable<T>Sie beispielsweise verfügbar machen, können Sie die Implementierung in yield returns ändern , dies ist IList<T>jedoch nicht zulässig.
Dennis
31

Nein, denn der Verbraucher sollte genau wissen, was IList ist:

IList ist ein Nachkomme der ICollection-Schnittstelle und die Basisschnittstelle aller nicht generischen Listen. IList-Implementierungen lassen sich in drei Kategorien einteilen: schreibgeschützt, feste Größe und variable Größe. Eine schreibgeschützte IList kann nicht geändert werden. Eine IList mit fester Größe ermöglicht nicht das Hinzufügen oder Entfernen von Elementen, sondern das Ändern vorhandener Elemente. Eine IList variabler Größe ermöglicht das Hinzufügen, Entfernen und Ändern von Elementen.

Sie können überprüfen IList.IsFixedSizeund IList.IsReadOnlyund tun , was man will.

Ich denke, es IListist ein Beispiel für eine fette Schnittstelle, die in mehrere kleinere Schnittstellen aufgeteilt werden sollte. Außerdem verstößt sie gegen das Liskov-Substitutionsprinzip, wenn Sie ein Array als zurückgeben IList.

Lesen Sie mehr, wenn Sie eine Entscheidung über die Rückgabe der Schnittstelle treffen möchten


AKTUALISIEREN

Graben mehr und ich fand, dass IList<T>nicht implementiert IListund IsReadOnlyüber die Basisschnittstelle zugänglich ist, ICollection<T>aber es gibt keine IsFixedSizefür IList<T>. Lesen Sie mehr darüber, warum generische IList <> nicht generische IList nicht erbt.

Hamid Pourjam
quelle
8
Diese Eigenschaften sind gut, aber es macht meiner Meinung nach immer noch den Zweck einer Schnittstelle zunichte. Für mich ist Schnittstelle ein Vertrag, der nicht von irgendwelchen Überprüfungen oder was auch immer abhängen sollte
Alexander Derck
1
Es wäre für den Entwickler wirklich freundlicher gewesen, wenn er eine zusätzliche Schnittstelle mit (nicht fester Größe / nicht schreibgeschützt) List<T>Semantik bereitgestellt hätte , aber es gibt wahrscheinlich gute Gründe , warum dies nicht enthalten war ... Aber ja, ich denke, Ihre Vorstellung von mehreren Schnittstellen, die die verschiedenen Funktionen abdecken, wäre eine viel fundiertere Wahl.
Spender
1
IList<T>ist eine fette Schnittstelle, aber es ist das Beste, was Sie von Array und Liste bekommen können. Wenn es also in mehrere Schnittstellen aufgeteilt worden wäre, könnten Sie einige Methoden möglicherweise nicht mit Listen und Arrays verwenden, sondern nur mit einer davon, selbst wenn alle Methoden, die Sie verwenden möchten (wie IList.Countoder der Indexer), unterstützt werden. Meiner Meinung nach war es eine gute Entscheidung. Wenn es NotSupportedExceptionin sehr wenigen Fällen zu Addeinem Array kommt, das man verwenden möchte , dann ist das der Deal.
Tim Schmelter
1
@dotctor Was für ein Zufall, ich habe gestern über das Liskov-Substitutionsprinzip gelesen und das hat mich zum Nachdenken gebracht :) Danke für die Antwort
Alexander Derck
6
Nun, diese Antwort in Kombination mit Dennis Regeln kann sicherlich das Verständnis dafür verbessern, wie man mit diesem Dilemma umgeht. Es ist unnötig zu erwähnen, dass das Liskov-Substitutionsprinzip hier und da in .NET Framework verletzt wird und es kein Grund zur Sorge ist.
Fabjan
13

Wie bei allen Fragen zu "Schnittstelle versus Implementierung" müssen Sie erkennen, was das Offenlegen eines öffentlichen Mitglieds bedeutet: Es definiert die öffentliche API dieser Klasse.

Wenn Sie a List<T>als Mitglied verfügbar machen (Feld, Eigenschaft, Methode, ...), teilen Sie dem Verbraucher dieses Mitglieds mit: Der Typ, der durch den Zugriff auf diese Methode erhalten wirdList<T> , ist a oder etwas, das davon abgeleitet ist.

Wenn Sie nun eine Schnittstelle verfügbar machen, verbergen Sie die "Implementierungsdetails" Ihrer Klasse mithilfe eines konkreten Typs. Natürlich kann man nicht instantiate IList<T>, aber man kann ein verwenden Collection<T>, List<T>, Ableitungen davon oder Ihre eigene Art der Umsetzung IList<T>.

Die eigentliche Frage lautet "Warum Arrayimplementiert IList<T>" oder "Warum hat die IList<T>Schnittstelle so viele Mitglieder" .

Es hängt auch davon ab, was die Verbraucher dieses Mitglieds tun sollen. Wenn Sie tatsächlich ein internes Mitglied über Ihr Expose...Mitglied zurückgeben, möchten Sie new List<T>(internalMember)trotzdem ein internes Mitglied zurückgeben , da der Verbraucher andernfalls versuchen kann, es zu übertragen IList<T>und Ihr internes Mitglied dadurch zu ändern.

Wenn Sie nur erwarten, dass Verbraucher die Ergebnisse wiederholen, legen Sie sie offen IEnumerable<T>oder IReadOnlyCollection<T>stattdessen.

CodeCaster
quelle
3
"Die eigentliche Frage lautet" Warum implementiert Array IList <T> "oder" Warum hat die IList <T> -Schnittstelle so viele Mitglieder ".", Damit hatte ich in der Tat zu kämpfen.
Alexander Derck
2
@Fabjan - IList<T>verfügt über Methoden zum Hinzufügen und Entfernen von Elementen, weshalb die Implementierung von Arrays schlecht ist. Die IsReadOnlyEigenschaft ist ein Hack, um die Unterschiede zu beschönigen, wenn sie in (mindestens) zwei Schnittstellen aufgeteilt werden sollte.
Lee
@ Lee Whoops, Sie haben Recht, löschen Sie meinen ungenauen Kommentar
Fabjan
1
@ AlexanderDerck es ist etwas, was ich zuvor gefragt habe stackoverflow.com/q/5968708/706456
oleksii
@oleksii es ist in der Tat ähnlich, aber ich würde es nicht als Duplikat bezeichnen
Alexander Derck
8

Seien Sie vorsichtig mit pauschalen Zitaten, die aus dem Zusammenhang gerissen werden.

Die Rückgabe einer Schnittstelle ist besser als die Rückgabe einer konkreten Implementierung

Dieses Zitat ist nur dann sinnvoll, wenn es im Kontext der SOLID-Prinzipien verwendet wird . Es gibt 5 Prinzipien, aber für die Zwecke dieser Diskussion werden wir nur über die letzten 3 sprechen.

Prinzip der Abhängigkeitsinversion

man sollte sich auf Abstraktionen verlassen. Verlassen Sie sich nicht auf Konkretionen. “

Meiner Meinung nach ist dieses Prinzip am schwierigsten zu verstehen. Wenn Sie sich das Zitat jedoch genau ansehen, ähnelt es Ihrem ursprünglichen Zitat.

Abhängig von Schnittstellen (Abstraktionen). Verlassen Sie sich nicht auf konkrete Implementierungen (Konkretionen).

Das ist immer noch etwas verwirrend, aber wenn wir anfangen, die anderen Prinzipien zusammen anzuwenden, macht es viel mehr Sinn.

Liskov-Substitutionsprinzip

"Objekte in einem Programm sollten durch Instanzen ihrer Untertypen ersetzt werden können, ohne die Richtigkeit dieses Programms zu ändern."

Wie Sie bereits betont haben, unterscheidet sich die Rückgabe von an Arraydeutlich von der Rückgabe von a List<T>, obwohl beide implementiert sind IList<T>. Dies ist mit Sicherheit eine Verletzung von LSP.

Es ist wichtig zu erkennen, dass es bei Schnittstellen um den Verbraucher geht . Wenn Sie eine Schnittstelle zurückgeben, haben Sie einen Vertrag erstellt, in dem alle Methoden oder Eigenschaften dieser Schnittstelle verwendet werden können, ohne das Verhalten des Programms zu ändern.

Prinzip der Schnittstellentrennung

"Viele kundenspezifische Schnittstellen sind besser als eine Allzweckschnittstelle."

Wenn Sie eine Schnittstelle zurückgeben, sollten Sie die clientseitigste Schnittstelle zurückgeben, die Ihre Implementierung unterstützt. Mit anderen Worten, wenn Sie nicht erwarten, dass der Client die AddMethode aufruft, sollten Sie keine Schnittstelle mit einer AddMethode darauf zurückgeben.

Leider sind die Schnittstellen im .NET Framework (insbesondere die frühen Versionen) nicht immer ideale clientspezifische Schnittstellen. Obwohl, wie @Dennis in seiner Antwort hervorhob, in .NET 4.5+ viel mehr Auswahlmöglichkeiten bestehen.

Handwerksspiele
quelle
Das Lesen über Liskovs Prinzip hat mich in erster Linie verwirrtIList<T>
Alexander Derck
Im speziellen Fall ist es das Array, das gegen LSP verstößt. Wenn Sie eine IList zurückgeben, sollten Sie niemals ein Array zurückgeben, da dies ein Verstoß ist. Sie haben bereits den Vertrag mit dem Anrufer geschlossen, dass er eine Liste zurückerhält, in der Elemente hinzugefügt und entfernt werden können.
Craftworkgames
5

Die Rückgabe einer Schnittstelle ist nicht unbedingt besser als die Rückgabe einer konkreten Implementierung einer Sammlung. Sie sollten immer einen guten Grund haben, eine Schnittstelle anstelle eines konkreten Typs zu verwenden. In Ihrem Beispiel erscheint es sinnlos, dies zu tun.

Gültige Gründe für die Verwendung einer Schnittstelle können sein:

  1. Sie wissen nicht, wie die Implementierung der Methoden aussehen wird, die die Schnittstelle zurückgeben, und es kann viele geben, die im Laufe der Zeit entwickelt wurden. Es können andere Leute sein, die sie schreiben, von anderen Firmen. Sie möchten sich also nur auf das Nötigste einigen und es ihnen überlassen, wie die Funktionalität implementiert wird.

  2. Sie möchten einige allgemeine Funktionen unabhängig von Ihrer Klassenhierarchie typsicher machen. Objekte verschiedener Basistypen, die dieselben Methoden bieten sollten, würden Ihre Schnittstelle implementieren.

Man könnte argumentieren, dass 1 und 2 im Grunde der gleiche Grund sind. Es sind zwei verschiedene Szenarien, die letztendlich zu demselben Bedarf führen.

"Es ist ein Vertrag". Wenn der Vertrag mit Ihnen abgeschlossen ist und Ihre Anwendung sowohl hinsichtlich der Funktionalität als auch der Zeit geschlossen ist, macht es häufig keinen Sinn, eine Schnittstelle zu verwenden.

Martin Maat
quelle
1
Ich stimme nicht wirklich zu, wenn es möglich ist, eine Schnittstelle zurückzugeben, würde ich eine Schnittstelle zurückgeben, da dies das einzige ist, was benötigt wird. Wenn eine Methode eine Schnittstelle erwartet, kann ich eine Klasse übergeben, die nur diese Schnittstelle implementiert, aber auch eine Klasse, die die Schnittstelle + viele andere Dinge implementiert. Mit der Schnittstelle ist es einfacher, Ihre Anwendung zu erweitern, ohne vorhandene Dinge neu zu schreiben
Alexander Derck
2
Es kann sehr aufschlussreich sein, Ihre gesamte Software (und insbesondere Ihre Klassen und Klassenbibliotheken) mit der Einstellung "Was wäre, wenn jemand anderes diese Klasse (Bibliothek) verwenden würde?" Zu entwickeln. und "Was ist, wenn ich dem konkreten Typ, den ich zurückgeben möchte, Funktionalität hinzufügen möchte?" . BCL-Typen können versiegelt werden, sodass Sie sie nicht erweitern können. Dann stecken Sie fest, wenn Sie etwas anderes zurückgeben möchten, da Ihre API verspricht, genau diesen Typ im Gegensatz zu einer Schnittstelle verfügbar zu machen. Die Rückgabe einer Schnittstelle ist fast immer besser als die Rückgabe einer konkreten Implementierung.
CodeCaster
1
Ich stimme (wirklich) zu, ich hätte einen dritten Grund hinzufügen sollen: "Beschränken Sie die Funktionalität eines zurückgegebenen Objekts auf das Wesentliche, um die Nachricht an den Anrufer zu klären". Ich gebe auch IReadOnlyXxx-Schnittstellen zurück, wann immer dies möglich ist, und nicht das vollwertige Objekt, das zum Zusammenstellen der Sammlung verwendet wird.
Martin Maat