ReadOnlyCollection o IEnumerable para exponer colecciones de miembros?

124

¿Hay alguna razón para exponer una colección interna como ReadOnlyCollection en lugar de IEnumerable si el código de llamada solo itera sobre la colección?

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

A mi entender, IEnumerable es un subconjunto de la interfaz de ReadOnlyCollection y no permite al usuario modificar la colección. Entonces, si la interfaz IEnumberable es suficiente, entonces esa es la que hay que usar. ¿Es esa una forma adecuada de razonar al respecto o me estoy perdiendo algo?

Gracias / Erik

Erik Öjebo
fuente
66
Si está utilizando .NET 4.5, es posible que desee probar las nuevas interfaces de colección de solo lectura . Todavía querrás envolver la colección devuelta en un ReadOnlyCollectionsi eres paranoico, pero ahora no estás atado a una implementación específica.
StriplingWarrior

Respuestas:

96

Solución más moderna

A menos que necesite que la colección interna sea mutable, puede usar el System.Collections.Immutablepaquete, cambiar su tipo de campo para que sea una colección inmutable y luego exponerlo directamente, suponiendo que Foosea ​​inmutable, por supuesto.

Respuesta actualizada para abordar la pregunta más directamente

¿Hay alguna razón para exponer una colección interna como ReadOnlyCollection en lugar de IEnumerable si el código de llamada solo itera sobre la colección?

Depende de cuánto confíes en el código de llamada. Si tiene el control total sobre todo lo que alguna vez llamará a este miembro y garantiza que ningún código usará:

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

entonces seguro, no se hará daño si solo devuelve la colección directamente. Sin embargo, generalmente trato de ser un poco más paranoico que eso.

Del mismo modo, como dices: si solo necesitas IEnumerable<T> , ¿por qué atarte a algo más fuerte?

Respuesta original

Si está utilizando .NET 3.5, puede evitar hacer una copia y evitar la conversión simple mediante una simple llamada para Omitir:

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

(Hay muchas otras opciones para ajustar trivialmente; lo bueno de Skipseleccionar / Dónde es que no hay delegado para ejecutar sin sentido para cada iteración).

Si no está utilizando .NET 3.5, puede escribir un contenedor muy simple para hacer lo mismo:

public static IEnumerable<T> Wrapper<T>(IEnumerable<T> source)
{
    foreach (T element in source)
    {
        yield return element;
    }
}
Jon Skeet
fuente
15
Tenga en cuenta que hay una penalización de rendimiento para esto: AFAIK the Enumerable. El método de conteo está optimizado para Colecciones convertidas en IEnumerables, pero no para un rango producido por Skip (0)
shojtsy
66
-1 ¿Soy solo yo o es una respuesta a una pregunta diferente? Es útil conocer esta "solución", pero el operador ha pedido una comparación entre devolver una ReadOnlyCollection y IEnumerable. Esta respuesta ya supone que desea devolver IEnumerable sin ningún motivo para respaldar esa decisión.
Zaid Masud
1
La respuesta muestra enviar un a ReadOnlyCollectiona ICollectiony luego ejecutar con Addéxito. Que yo sepa, eso no debería ser posible. El evil.Add(...)debería arrojar un error. dotnetfiddle.net/GII2jW
Shaun Luttin
1
@ShaunLuttin: Sí, exactamente, eso arroja. Pero en mi respuesta, no lanzo un ReadOnlyCollection- lanzo uno IEnumerable<T>que en realidad no es de solo lectura ... es en lugar de exponer unReadOnlyCollection
Jon Skeet
1
Ajá. Su paranoia lo llevaría a usar un en ReadOnlyCollectionlugar de un IEnumerable, para protegerse contra el lanzamiento del mal.
Shaun Luttin
43

Si solo necesita recorrer la colección:

foreach (Foo f in bar.Foos)

entonces devolver IEnumerable es suficiente.

Si necesita acceso aleatorio a los elementos:

Foo f = bar.Foos[17];

luego envuélvala en ReadOnlyCollection .

Vojislav Stojkovic
fuente
@Vojislav Stojkovic, +1. ¿Este consejo todavía se aplica hoy?
w0051977
1
@ w0051977 Depende de tu intención. Usarlo System.Collections.Immutablecomo recomienda Jon Skeet es perfectamente válido cuando expones algo que nunca va a cambiar después de obtener una referencia. Por otro lado, si está exponiendo una vista de solo lectura de una colección mutable, entonces este consejo sigue siendo válido, aunque probablemente lo expondría como IReadOnlyCollection(que no existía cuando escribí esto).
Vojislav Stojkovic
@VojislavStojkovic no se puede utilizar bar.Foos[17]con IReadOnlyCollection. La única diferencia es la Countpropiedad adicional . public interface IReadOnlyCollection<out T> : IEnumerable<T>, IEnumerable
Konrad
@Konrad Correcto, si lo necesita bar.Foos[17], debe exponerlo como ReadOnlyCollection<Foo>. Si desea conocer el tamaño e iterar a través de él, exponerlo como IReadOnlyCollection<Foo>. Si no necesita saber el tamaño, úselo IEnumerable<Foo>. Hay otras soluciones y otros detalles, pero está más allá del alcance de la discusión de esta respuesta en particular.
Vojislav Stojkovic
31

Si hace esto, no hay nada que impida que las personas que llaman vuelvan el IEnumerable a ICollection y luego lo modifiquen. ReadOnlyCollection elimina esta posibilidad, aunque todavía es posible acceder a la colección de escritura subyacente a través de la reflexión. Si la colección es pequeña, una forma segura y fácil de solucionar este problema es devolver una copia.

Stu Mackellar
fuente
14
Ese es el tipo de "diseño paranoico" con el que no estoy de acuerdo. Creo que es una mala práctica elegir una semántica inadecuada para hacer cumplir una política.
Vojislav Stojkovic
24
No estar de acuerdo no lo hace mal. Si estoy diseñando una biblioteca para distribución externa, preferiría tener una interfaz paranoica que tratar con los errores generados por los usuarios que intentan hacer un mal uso de la API. Consulte las pautas de diseño de C # en msdn.microsoft.com/en-us/library/k2604h5s(VS.71).aspx
Stu Mackellar
55
Sí, en el caso específico de diseñar una biblioteca para distribución externa, tiene sentido tener una interfaz paranoica para lo que sea que esté exponiendo. Aun así, si la semántica de la propiedad Foos es el acceso secuencial, use ReadOnlyCollection y luego devuelva IEnumerable. No es necesario usar una semántica incorrecta;)
Vojislav Stojkovic
9
No necesitas hacer tampoco. Puede devolver un iterador de solo lectura sin copiar ...
Jon Skeet
1
@shojtsy Parece que su línea de código no funciona. como genera un nulo . Un elenco arroja una excepción.
Nick Alexeev
3

Evito usar ReadOnlyCollection tanto como sea posible, en realidad es considerablemente más lento que simplemente usar una Lista normal. Ver este ejemplo:

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());
James Madison
fuente
15
Optimización prematura. Según mis mediciones, iterar sobre una ReadOnlyCollection toma aproximadamente un 36% más de tiempo que una lista normal, suponiendo que no haga nada dentro del ciclo for. Lo más probable es que nunca note esta diferencia, pero si esta es una zona activa en su código y requiere hasta el último rendimiento, puede exprimirla, ¿por qué no usar una matriz en su lugar? Eso sería aún más rápido.
StriplingWarrior
Su punto de referencia contiene fallas, está ignorando por completo la predicción de rama.
Trojaner
0

A veces es posible que desee utilizar una interfaz, tal vez porque quiere burlarse de la colección durante las pruebas unitarias. Consulte mi entrada de blog para agregar su propia interfaz a ReadonlyCollection utilizando un adaptador.


fuente