ReadOnlyCollection ou IEnumerable pour exposer des collections de membres?
-
20-08-2019 - |
Question
Existe-t-il une raison quelconque d'exposer une collection interne en tant que ReadOnlyCollection plutôt qu'en tant que IEnumerable si le code d'appel itère uniquement 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 ne permet pas à l'utilisateur de modifier la collection. Donc, si l'interface IEnumberable est suffisante, c'est celle-là à utiliser. Est-ce une bonne façon de raisonner ou est-ce que je manque quelque chose?
Merci / Erik
La solution
Une solution plus moderne
Sauf si vous souhaitez que la collection interne soit modifiable, vous pouvez utiliser le System.Collections.Immutable
, modifiez votre type de champ pour qu'il devienne une collection immuable, puis exposez-le directement - en supposant que Foo
lui-même est immuable, bien sûr.
Réponse mise à jour pour répondre plus directement à la question
Existe-t-il une raison quelconque d'exposer une collection interne en tant que ReadOnlyCollection plutôt qu'en tant que IEnumerable si le code d'appel itère uniquement sur la collection?
Cela dépend du degré de confiance que vous accordez au code d'appel. Si vous contrôlez totalement tout ce qui appellera ce membre et que vous garantissez qu'aucun code ne sera utilisé:
ICollection<Foo> evil = (ICollection<Foo>) bar.Foos;
evil.Add(...);
Ensuite, aucun dommage ne sera causé si vous retournez directement la collection. J'essaie généralement d'être un peu plus paranoïaque que ça.
De même, comme vous dites: si vous n’avez que besoin IEnumerable<T>
, 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 d'éviter la distribution simple en utilisant un simple appel à Ignorer:
public IEnumerable<Foo> Foos {
get { return foos.Skip(0); }
}
(Il existe de nombreuses autres options pour effectuer un wrapping trivial - la bonne chose à propos de Skip
sur Select / Where est qu'il n'y a aucun délégué à exécuter inutilement à 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;
}
}
Autres conseils
Si vous devez uniquement parcourir la collection:
foreach (Foo f in bar.Foos)
puis renvoyer IEnumerable est suffisant.
Si vous avez besoin d'un accès aléatoire aux éléments:
Foo f = bar.Foos[17];
puis enroulez-le dans ReadOnlyCollection .
Si vous faites cela, rien n'empêchera vos appelants de renvoyer IEnumerable à ICollection, puis de le modifier. ReadOnlyCollection supprime cette possibilité, même s'il est toujours possible d'accéder à la collection inscriptible sous-jacente via la réflexion. Si la collection est petite, un moyen simple et sûr de résoudre ce problème consiste à renvoyer une copie à la place.
J'évite autant que possible d'utiliser ReadOnlyCollection, il est en fait considérablement plus lent que la simple utilisation d'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());
Parfois, vous pouvez utiliser une interface, peut-être parce que vous souhaitez simuler la collection lors des tests unitaires. Veuillez consulter mon entrée de blog pour ajouter votre propre interface à ReadonlyCollection à l'aide de un adaptateur.