web-dev-qa-db-fra.com

IList <T> et IReadOnlyList <T>

Si j'ai une méthode qui nécessite un paramètre qui,

  • A une propriété Count
  • A un indexeur entier (get-only)

Quel devrait être le type de ce paramètre? Je choisirais IList<T> avant .NET 4.5 car il n'y avait pas d'autre interface de collection indexable pour cela et les tableaux l'implémentaient, ce qui est un gros plus.

Mais .NET 4.5 introduit la nouvelle interface IReadOnlyList<T> et je veux que ma méthode le supporte aussi. Comment puis-je écrire cette méthode pour prendre en charge IList<T> et IReadOnlyList<T> sans enfreindre les principes de base tels que DRY?

Edit : La réponse de Daniel m'a donné quelques idées:

public void Foo<T>(IList<T> list)
    => Foo(list, list.Count, (c, i) => c[i]);

public void Foo<T>(IReadOnlyList<T> list)
    => Foo(list, list.Count, (c, i) => c[i]);

private void Foo<TList, TItem>(
    TList list, int count, Func<TList, int, TItem> indexer)
    where TList : IEnumerable<TItem>
{
    // Stuff
}

Edit 2: Je pourrais simplement accepter un IReadOnlyList<T> et fournir un assistant comme celui-ci:

public static class CollectionEx
{
    public static IReadOnlyList<T> AsReadOnly<T>(this IList<T> list)
    {
        if (list == null)
            throw new ArgumentNullException(nameof(list));

        return list as IReadOnlyList<T> ?? new ReadOnlyWrapper<T>(list);
    }

    private sealed class ReadOnlyWrapper<T> : IReadOnlyList<T>
    {
        private readonly IList<T> _list;

        public ReadOnlyWrapper(IList<T> list) => _list = list;

        public int Count => _list.Count;

        public T this[int index] => _list[index];

        public IEnumerator<T> GetEnumerator() => _list.GetEnumerator();

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
}

Alors je pourrais l'appeler comme Foo(list.AsReadOnly())


Edit 3: Les tableaux implémentent à la fois IList<T> et IReadOnlyList<T>, ainsi que la classe List<T>. Cela rend assez rare de trouver une classe qui implémente IList<T> mais pas IReadOnlyList<T>.

31
Şafak Gür

Vous n'avez pas de chance ici. IList<T> n'implémente pas IReadOnlyList<T>. List<T> implémente les deux interfaces, mais je pense que ce n'est pas ce que vous voulez.

Cependant, vous pouvez utiliser LINQ: 

  • La méthode d'extension Count() vérifie en interne si l'instance est en fait une collection, puis utilise la propriété Count.
  • La méthode d'extension ElementAt() vérifie en interne si l'instance est en fait une liste et utilise ensuite l'indexeur.
20
Daniel Hilgarth

Si vous êtes plus préoccupé par le maintien du principe de DRY over performance, vous pouvez utiliser dynamic , comme suit:

public void Do<T>(IList<T> collection)
{
    DoInternal(collection, collection.Count, i => collection[i]);
}
public void Do<T>(IReadOnlyList<T> collection)
{
    DoInternal(collection, collection.Count, i => collection[i]);
}

private void DoInternal(dynamic collection, int count, Func<int, T> indexer)
{
    // Get the count.
    int count = collection.Count;
}

Cependant, je ne peux pas dire de bonne foi que je le recommanderais car les pièges sont trop importants:

  • Chaque appel à collection dans DoInternal sera résolu au moment de l'exécution. Vous perdez la sécurité de type, les contrôles au moment de la compilation, etc.
  • Dégradation des performances (même si elle n'est pas grave, mais qu'elle peut être globale) se produira

Votre suggestion d’aide est la plus utile, mais je pense que vous devriez l’inverser; Etant donné que l'interface IReadOnlyList<T> a été introduite dans .NET 4.5, de nombreuses API ne la prennent pas en charge, mais prennent en charge l'interface IList<T> .

Cela dit, vous devez créer un wrapper AsList, qui prend un IReadOnlyList<T> et retourne un wrapper dans une implémentation IList<T>.

Cependant, si vous souhaitez souligner sur votre API que vous prenez un IReadOnlyList<T> (pour souligner le fait que vous ne modifiez pas les données), l'extension AsReadOnlyList que vous avez maintenant serait plus appropriée, suivant l'optimisation à AsReadOnly:

public static IReadOnlyList<T> AsReadOnly<T>(this IList<T> collection)
{
    if (collection == null)
        throw new ArgumentNullException("collection");

    // Type-sniff, no need to create a wrapper when collection
    // is an IReadOnlyList<T> *already*.
    IReadOnlyList<T> list = collection as IReadOnlyList<T>;

    // If not null, return that.
    if (list != null) return list;

    // Wrap.
    return new ReadOnlyWrapper<T>(collection);
}
4
casperOne

Puisque IList<T> et IReadOnlyList<T> ne partagent aucun "ancêtre" utile, et si vous ne voulez pas que votre méthode accepte un autre type de paramètre, vous ne pouvez que fournir deux surcharges.

Si vous décidez que la réutilisation des codes est une priorité absolue, vous pouvez faire en sorte que ces surcharges transmettent l'appel à une méthode private qui accepte IEnumerable<T> et utilise LINQ de la manière suggérée par Daniel, laissant à LINQ le soin de normaliser l'exécution.

Cependant, à mon humble avis, il serait probablement préférable de simplement copier/coller le code une fois et de conserver deux surcharges indépendantes qui diffèrent uniquement par le type d'argument; Je ne crois pas que la micro-architecture de cette échelle offre quelque chose de tangible, mais d'un autre côté, elle nécessite des manœuvres peu évidentes et est plus lente.

1
Jon

Ce dont vous avez besoin est le IReadOnlyCollection<T> disponible dans .Net 4.5, qui est essentiellement un IEnumerable<T> dont la propriété est Count, mais si vous avez également besoin de l’indexation, vous avez besoin de IReadOnlyList<T> qui fournira également un indexeur.

Je ne sais pas pour vous, mais je pense que cette interface est un outil indispensable qui faisait défaut depuis très longtemps.

0
MaYaN