web-dev-qa-db-fra.com

Renvoyer IList <T> est-il pire que renvoyer T [] ou List <T>?

Les réponses à des questions comme celle-ci: List <T> ou IList <T> semblent toujours convenir que le retour d'une interface est meilleur que le retour d'une implémentation concrète d'une collection. Mais je me bats avec ça. L'instanciation d'une interface est impossible, donc si votre méthode retourne une interface, elle retourne en fait toujours une implémentation spécifique. J'expérimentais un peu avec ceci en écrivant 2 petites méthodes:

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

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

Et utilisez-les dans mon programme de test:

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);
}

Dans les deux cas, lorsque j'essaye d'ajouter une nouvelle valeur, mon compilateur ne me donne aucune erreur, mais évidemment la méthode qui expose mon tableau comme un IList<T> Donne une erreur d'exécution lorsque j'essaye d'y ajouter quelque chose. Ainsi, les personnes qui ne savent pas ce qui se passe dans ma méthode et doivent y ajouter des valeurs sont obligées de copier d'abord mon IList dans un List pour pouvoir ajouter des valeurs sans risquer d'erreurs . Bien sûr, ils peuvent faire une vérification de type pour voir s'ils ont affaire à un List ou un Array, mais s'ils ne le font pas, et ils veulent ajouter des éléments à la collection ils n'ont pas d'autre choix pour copier le IList dans un List, même s'il s'agit déjà d'un List. Un tableau ne doit-il jamais être exposé en tant que IList?

Une autre de mes préoccupations est basée sur la réponse acceptée de la question liée (accentuation de la mienne):

Si vous exposez votre classe via une bibliothèque que d'autres utiliseront, vous voulez généralement l'exposer via des interfaces plutôt que des implémentations concrètes. Cela vous aidera si vous décidez de modifier l'implémentation de votre classe ultérieurement pour utiliser une classe concrète différente. Dans ce cas, les utilisateurs de votre bibliothèque n'auront pas besoin de mettre à jour leur code car l'interface ne change pas.

Si vous ne l'utilisez qu'en interne, vous pouvez ne pas vous en soucier autant, et utiliser List peut être correct.

Imaginez que quelqu'un utilise réellement mon IList<T> Obtenu de ma méthode ExposeListIlist() comme ça pour ajouter/supprimer des valeurs. Tout fonctionne bien. Mais maintenant, comme la réponse le suggère, parce que le retour d'une interface est plus flexible, je retourne un tableau au lieu d'une liste (pas de problème de mon côté!), Alors ils sont prêts pour un régal ...

TLDR:

1) L'exposition d'une interface provoque des conversions inutiles? Cela n'a-t-il pas d'importance?

2) Parfois, si les utilisateurs de la bibliothèque n'utilisent pas de transtypage, leur code peut se casser lorsque vous modifiez votre méthode, même si la méthode reste parfaitement correcte.

J'y pense probablement trop, mais je n'ai pas le consensus général que le retour d'une interface est préférable au retour d'une implémentation.

80
Alexander Derck

Peut-être que cela ne répond pas directement à votre question, mais dans .NET 4.5+, je préfère suivre ces règles lors de la conception d'API publiques ou protégées:

  • retourne IEnumerable<T>, si seule l'énumération est disponible;
  • retourne IReadOnlyCollection<T> si l'énumération et le nombre d'articles sont disponibles;
  • retourne IReadOnlyList<T>, si l'énumération, le nombre d'éléments et l'accès indexé sont disponibles;
  • retourne ICollection<T> si l'énumération, le nombre d'articles et la modification sont disponibles;
  • retourne IList<T>, si l'énumération, le nombre d'éléments, l'accès indexé et la modification sont disponibles.

Les deux dernières options supposent que cette méthode ne doit pas retourner de tableau en tant que IList<T> la mise en oeuvre.

107
Dennis

Non, car le consommateur doit savoir exactement ce que IList est:

IList est un descendant de l'interface ICollection et est l'interface de base de toutes les listes non génériques. Les implémentations IList se répartissent en trois catégories: lecture seule, taille fixe et taille variable. Un IList en lecture seule ne peut pas être modifié. Une IList de taille fixe ne permet pas l'ajout ou la suppression d'éléments, mais elle permet la modification d'éléments existants. Une IList de taille variable permet l'ajout, la suppression et la modification d'éléments.

Vous pouvez vérifier IList.IsFixedSize et IList.IsReadOnly et faites ce que vous voulez avec.

Je pense que IList est un exemple d'une interface fat et qu'elle aurait dû être divisée en plusieurs interfaces plus petites et qu'elle viole également principe de substitution Liskov lorsque vous retournez un tableau en tant que IList.

En savoir plus si vous souhaitez prendre une décision concernant le retour de l'interface


[~ # ~] mise à jour [~ # ~]

En creusant plus, j'ai trouvé que IList<T> n'implémente pas IList et IsReadOnly est accessible via l'interface de base ICollection<T> mais il n'y a pas de IsFixedSize pour IList<T>. En savoir plus sur pourquoi IList générique <> n'hérite pas d'IList non générique?

31
Hamid Pourjam

Comme pour toute question "interface versus implémentation", vous devrez comprendre ce que signifie exposer un membre public: il définit l'API publique de cette classe.

Si vous exposez un List<T> En tant que membre (champ, propriété, méthode, ...), vous dites au consommateur de ce membre: le type obtenu en accédant à cette méthode is a List<T>, Ou quelque chose qui en dérive.

Maintenant, si vous exposez une interface, vous masquez le "détail d'implémentation" de votre classe à l'aide d'un type concret. Bien sûr, vous ne pouvez pas instancier IList<T>, Mais vous pouvez utiliser un Collection<T>, List<T>, Des dérivations de ceux-ci ou votre propre type implémentant IList<T>.

La question réelle est "Pourquoi Array implémente-t-elle IList<T>", ou "Pourquoi le IList<T> interface tant de membres ".

Cela dépend aussi de ce que vous voulez que les consommateurs de ce membre fassent. Si vous renvoyez réellement un membre interne via votre membre Expose..., Vous voudrez quand même renvoyer une new List<T>(internalMember), sinon le consommateur peut essayer de les convertir en IList<T> Et modifier votre membre interne grâce à cela.

Si vous vous attendez à ce que les consommateurs répètent les résultats, exposez plutôt IEnumerable<T> Ou IReadOnlyCollection<T>.

12
CodeCaster

Soyez prudent avec les citations générales qui sont prises hors contexte.

Renvoyer une interface est mieux que renvoyer une implémentation concrète

Cette citation n'a de sens que si elle est utilisée dans le contexte des principes SOLIDES . Il y a 5 principes, mais pour les besoins de cette discussion, nous allons simplement parler des 3 derniers.

Principe d'inversion de dépendance

il faut "Dépendre des abstractions. Ne dépendez pas des concrétions. "

À mon avis, ce principe est le plus difficile à comprendre. Mais si vous regardez attentivement le devis, il ressemble beaucoup à votre devis d'origine.

Dépend des interfaces (abstractions). Ne dépendez pas des implémentations concrètes (concrétions).

C'est encore un peu déroutant, mais si nous commençons à appliquer les autres principes ensemble, cela commence à avoir beaucoup plus de sens.

Principe de substitution de Liskov

"Les objets d'un programme doivent pouvoir être remplacés par des instances de leurs sous-types sans altérer l'exactitude de ce programme."

Comme vous l'avez souligné, le renvoi d'un Array est clairement un comportement différent de celui d'un List<T> même s'ils implémentent tous les deux IList<T>. Il s'agit très certainement d'une violation de LSP.

La chose importante à réaliser est que les interfaces concernent le consommateur . Si vous retournez une interface, vous avez créé un contrat selon lequel toutes les méthodes ou propriétés de cette interface peuvent être utilisées sans modifier le comportement du programme.

Principe de ségrégation des interfaces

"De nombreuses interfaces spécifiques au client valent mieux qu'une interface à usage général."

Si vous renvoyez une interface, vous devez renvoyer l'interface la plus spécifique au client prise en charge par votre implémentation. En d'autres termes, si vous ne vous attendez pas à ce que le client appelle la méthode Add, vous ne devez pas renvoyer une interface avec une méthode Add dessus.

Malheureusement, les interfaces dans le framework .NET (en particulier les premières versions) ne sont pas toujours des interfaces spécifiques au client idéales. Bien que @Dennis ait souligné dans sa réponse, il existe beaucoup plus de choix dans .NET 4.5+.

8
craftworkgames

Renvoyer une interface n'est pas nécessairement mieux que renvoyer une implémentation concrète d'une collection. Vous devriez toujours avoir une bonne raison d'utiliser une interface au lieu d'un type concret. Dans votre exemple, cela semble inutile.

Les raisons valables d'utiliser une interface peuvent être:

  1. Vous ne savez pas à quoi ressemblera l'implémentation des méthodes renvoyant l'interface et il peut y en avoir beaucoup, développées au fil du temps. Il peut s'agir d'autres personnes qui les écrivent, d'autres sociétés. Donc, vous voulez juste vous mettre d'accord sur le strict nécessaire et leur laisser le soin de mettre en œuvre la fonctionnalité.

  2. Vous souhaitez exposer certaines fonctionnalités communes indépendantes de votre hiérarchie de classe de manière sécurisée. Les objets de différents types de base qui devraient offrir les mêmes méthodes implémenteraient votre interface.

On pourrait faire valoir que 1 et 2 sont essentiellement la même raison. Ce sont deux scénarios différents qui conduisent finalement au même besoin.

"C'est un contrat". Si le contrat est avec vous-même et que votre application est fermée à la fois en termes de fonctionnalité et de temps, il est souvent inutile d'utiliser une interface.

4
Martin Maat