web-dev-qa-db-fra.com

ReadOnlyCollection ou IEnumerable pour exposer les collections de membres?

Y a-t-il une raison d'exposer une collection interne en tant que ReadOnlyCollection plutôt que IEnumerable si le code appelant ne fait qu'itérer sur la collection?

class Bar
{
    private ICollection<Foo> foos;

    // Which one is to be preferred?
    public IEnumerable<Foo> Foos { ... }
    public ReadOnlyCollection<Foo> Foos { ... }
}


// Calling code:

foreach (var f in bar.Foos)
    DoSomething(f);

Comme je le vois, IEnumerable est un sous-ensemble de l'interface de ReadOnlyCollection et il ne permet pas à l'utilisateur de modifier la collection. Donc, si l'interface IEnumberable est suffisante, c'est celle à utiliser. Est-ce une bonne façon de raisonner ou est-ce que je manque quelque chose?

Merci/Erik

115
Erik Öjebo

Solution plus moderne

Sauf si vous avez besoin que la collection interne soit modifiable, vous pouvez utiliser System.Collections.Immutable package, changez votre type de champ pour être une collection immuable, puis exposez cela directement - en supposant que Foo lui-même est immuable, bien sûr.

Réponse mise à jour pour répondre plus directement à la question

Y a-t-il une raison d'exposer une collection interne en tant que ReadOnlyCollection plutôt que IEnumerable si le code appelant ne fait qu'itérer sur la collection?

Cela dépend de la confiance que vous accordez au code appelant. Si vous contrôlez complètement tout ce qui appellera ce membre et que vous garantissez qu'aucun code n'utilisera jamais:

ICollection<Foo> evil = (ICollection<Foo>) bar.Foos;
evil.Add(...);

alors bien sûr, aucun mal ne sera fait si vous retournez simplement la collection directement. J'essaie généralement d'être un peu plus paranoïaque que ça.

De même, comme vous le dites: si vous seulement besoinIEnumerable<T>, alors pourquoi vous attacher à quelque chose de plus fort?

Réponse originale

Si vous utilisez .NET 3.5, vous pouvez éviter de faire une copie et éviter la distribution simple en utilisant un simple appel à Skip:

public IEnumerable<Foo> Foos {
    get { return foos.Skip(0); }
}

(Il existe de nombreuses autres options pour encapsuler trivialement - ce qui est bien avec Skip sur Select/Where, c'est qu'il n'y a pas de délégué à exécuter inutilement pour chaque itération.)

Si vous n'utilisez pas .NET 3.5, vous pouvez écrire un wrapper très simple pour faire la même chose:

public static IEnumerable<T> Wrapper<T>(IEnumerable<T> source)
{
    foreach (T element in source)
    {
        yield return element;
    }
}
92
Jon Skeet

Si vous avez seulement besoin de parcourir la collection:

foreach (Foo f in bar.Foos)

puis retourner IEnumerable suffit.

Si vous avez besoin d'un accès aléatoire aux articles:

Foo f = bar.Foos[17];

puis envelopper dans ReadOnlyCollection.

40
Vojislav Stojkovic

Si vous faites cela, rien n'empêche vos appelants de restituer l'IEnumerable à ICollection, puis de le modifier. ReadOnlyCollection supprime cette possibilité, bien qu'il soit toujours possible d'accéder à la collection inscriptible sous-jacente via la réflexion. Si la collection est petite, un moyen sûr et facile de contourner ce problème est de renvoyer une copie à la place.

31
Stu Mackellar

J'évite d'utiliser ReadOnlyCollection autant que possible, c'est en fait considérablement plus lent que d'utiliser simplement une liste normale. Voir cet exemple:

List<int> intList = new List<int>();
        //Use a ReadOnlyCollection around the List
        System.Collections.ObjectModel.ReadOnlyCollection<int> mValue = new System.Collections.ObjectModel.ReadOnlyCollection<int>(intList);

        for (int i = 0; i < 100000000; i++)
        {
            intList.Add(i);
        }
        long result = 0;

        //Use normal foreach on the ReadOnlyCollection
        TimeSpan lStart = new TimeSpan(System.DateTime.Now.Ticks);
        foreach (int i in mValue)
            result += i;
        TimeSpan lEnd = new TimeSpan(System.DateTime.Now.Ticks);
        MessageBox.Show("Speed(ms): " + (lEnd.TotalMilliseconds - lStart.TotalMilliseconds).ToString());
        MessageBox.Show("Result: " + result.ToString());

        //use <list>.ForEach
        lStart = new TimeSpan(System.DateTime.Now.Ticks);
        result = 0;
        intList.ForEach(delegate(int i) { result += i; });
        lEnd = new TimeSpan(System.DateTime.Now.Ticks);
        MessageBox.Show("Speed(ms): " + (lEnd.TotalMilliseconds - lStart.TotalMilliseconds).ToString());
        MessageBox.Show("Result: " + result.ToString());
3
James Madison

Parfois, vous souhaiterez peut-être utiliser une interface, peut-être parce que vous souhaitez simuler la collection pendant les tests unitaires. Veuillez consulter mon entrée de blog pour ajouter votre propre interface à ReadonlyCollection en utilisant un adaptateur.

0
Nick Taylor