.net - namespace - system collections objectmodel
¿Las pautas de diseño de.NET sugieren devolver List<T> sobre IEnumerable<T>? (4)
Contexto: Envié un correo electrónico a mis colegas Enumerable.Empty<T>()
acerca de Enumerable.Empty<T>()
como una forma de devolver colecciones vacías sin hacer algo como return new List<T>();
Recibí una respuesta diciendo que la desventaja es que no expone un tipo específico:
Eso presenta un pequeño problema. Siempre es bueno ser lo más específico posible con respecto a su tipo de devolución * (de modo que si devuelve una Lista <>, haga que sea el tipo de devolución); eso es porque cosas como listas y matrices tienen métodos adicionales que son útiles. Además, también puede ser útil para hacer consideraciones de rendimiento cuando se usa la colección del método de llamada.
El truco a continuación, por desgracia, te obliga a devolver un IEnumerable, que es lo menos específico posible, ¿verdad? :(
* Esto es realmente de .NET Design Guidelines. Los motivos expuestos en las directrices son los mismos que menciono aquí, creo.
Esto parecía ser todo lo contrario de lo que había aprendido, y por más que lo intenté, no pude encontrar este consejo exacto en las pautas de diseño. Encontré una pequeña pieza como esta:
DO devuelve una subclase de
Collection<T>
oReadOnlyConnection<T>
partir de métodos y propiedades muy utilizados.
Con un siguiente fragmento de código, pero no más justificación.
Entonces, dicho esto, ¿es esta una guía real y aceptada (como se describió en la primera cita del bloque)? ¿O ha sido malinterpretado? Todas las demás preguntas de SO que pude encontrar tienen respuestas que prefieren IEnumerable<T>
como el tipo de devolución. ¿Tal vez las pautas originales de .NET están desactualizadas?
¿O tal vez no es tan claro? ¿Hay algunas concesiones para considerar? ¿Cuándo sería una buena idea devolver un tipo más específico? ¿Alguna vez se recomienda devolver un tipo genérico concreto, o solo devolver una interfaz más específica como IList<T>
y ReadOnlyCollection<T>
?
El problema con las directrices sobre decisiones como esta es que los desarrolladores somos susceptibles de seguirlos a ciegas, sin pensar. Yo diría que estas cuestiones deberían considerarse caso por caso, sopesando las ventajas y desventajas de los diferentes enfoques posibles en cada caso.
Con los tipos de devolución, hay realmente (al menos) dos formas de verlo. Una filosofía que veo aquí en a menudo prefiere elegir tipos más genéricos para darle -el autor del código- más flexibilidad en el camino. Por ejemplo, si escribe un método que devuelve un IList<T>
y el valor de retorno es en realidad una List<T>
, se reserva el derecho de implementar posteriormente su propia estructura de datos optimizada que implementa IList<T>
y devolverlo en un versión futura.
Una filosofía alternativa alternativa, pero en mi opinión igualmente válida, sería que devolver tipos más genéricos simplemente hace que su biblioteca sea menos útil, por lo que debería devolver tipos más específicos . Por ejemplo, devolver agresivamente IEnumerable<T>
todo el tiempo dará lugar al escenario en el que los usuarios finales de su biblioteca están llamando constantemente a ToArray()
, ToList()
, etc. Creo que este es el punto básico que su compañero estaba haciendo cuando escribió "cosas como listas y matrices tienen métodos adicionales que son útiles".
A veces escuché el último enfoque defendido con palabras como "Sé específico en lo que ofreces, pero genérico en lo que aceptas". En otras palabras, tome los parámetros genéricos pero ofrezca valores de retorno específicos. Ya sea que esté de acuerdo o no con este principio, es fácil al menos ver la razón detrás de esto.
En cualquier caso, como dije, cada caso diferirá en los detalles. Por ejemplo, en mi primer ejemplo de IList<T>
versus List<T>
, es posible que ya esté planeando implementar una estructura de datos personalizada y simplemente aún no la haya escrito. En este caso, devolver una List<T>
probablemente sería una mala elección ya que ya sabe que la interfaz será diferente en unas pocas semanas o meses.
Por otro lado, puede tener algún método que siempre devuelva un T[]
que elija exponer solo como un IEnumerable<T>
aunque la probabilidad de que esta implementación cambie constantemente es muy baja. En este caso, optar por el tipo más genérico puede ser más agradable estéticamente, pero no es probable que beneficie a nadie y, de hecho, priva al resultado de una funcionalidad muy útil: acceso aleatorio.
Por lo tanto, siempre será una solución de compromiso, y la mejor guía que creo que puede seguir es considerar siempre sus elecciones y usar su criterio para elegir la más sensata para el caso en cuestión.
Hay razones para ambos estilos:
- Sea específico : está garantizando a las personas que llaman que siempre devolverá este tipo. Incluso cuando su implementación cambia. Puedes mantener la promesa? En caso afirmativo, sea específico para que los llamadores se beneficien. Más tipos derivados tienen más características.
- Sea genérico : si es probable que cambie la implementación subyacente del método o la propiedad en cuestión, no puede prometer una
List<T>
. Tal vez pueda prometer unIList<T>
o una clase de colección personalizada (que puede cambiar más adelante).
.NET framework BCL va con matrices o clases de colección personalizadas. Necesitan proporcionar una API 100% estable, por lo que deben tener cuidado.
En los proyectos de software normales, puede cambiar la persona que llama (que los chicos de .NET Framework no pueden), por lo que puede ser más indulgente. Si prometes demasiado y necesitas cambiar eso un año después, puedes hacerlo (con un poco de esfuerzo).
Solo hay una cosa que siempre está mal: decir que siempre se debe hacer (1) o (2). Siempre siempre está mal.
Aquí hay un ejemplo para un caso donde la especificidad es claramente la elección correcta:
public static T[] Slice<T>(this T[] list, int start, int count)
{
var result = new T[count];
Array.Copy(list, start, result, 0, count);
return result;
}
Solo hay una implementación razonable posible, por lo que podemos prometer claramente que el tipo de devolución es una matriz. Nunca tendremos que devolver una lista.
Por otro lado, un método para devolver a todos los usuarios de una tienda persistente puede cambiar mucho internamente. Los datos pueden ser incluso más grandes que la memoria disponible, que requiere transmisión. Probablemente deberíamos elegir IEnumerable<User>
.
IMO es importante recordar que List<T>
e IEnumerable<T>
tienen una semántica diferente.
Devolver IEnumerable<T>
simplemente le da al que llama una secuencia de algo que puede ser enumerado (y posiblemente convertido en una colección). Sin embargo, no hay garantía de que la secuencia se haya materializado. Por lo tanto, devolver IEnumerable<T>
admite la ejecución diferida. Además, es implícitamente "solo lectura".
La List<T>
vuelta List<T>
significa que aquí hay una colección. Todo el trabajo se ha realizado cuando el método retorna. Además, la interfaz de List<T>
permite que la persona que llama también modifique la colección de inmediato.
La lista de regreso en lugar de IEnumerable (o al menos IList) esencialmente viola el Principio de Sustitución de Liskov
Devolver "Lista" lo bloquea para que necesite devolver ese tipo, y le impide refactorizar las partes internas del método a una implementación potencialmente más eficiente (prometió devolver Lista, pero ahora quiere usar "rendimiento" - ¡vaya! no lo hagas sin código externo-churn).
En muchos (¿la mayoría?) Casos, el requisito predominante de este tipo de métodos es iterar sobre los resultados y hacer algo ... IEnumerable es exactamente apropiado para estos usos.
Si el usuario necesita tener un conjunto mutable, esto se logra trivialmente usando "ToList ()" u otras extensiones en IEnumerable en el otro extremo.
El código que quiere que expongas una lista interna para la mutación directa puede ser una señal de que también estás violando la encapsulación. Ten cuidado. Cada vez encontramos más y más inmutabilidad en muchos lenguajes de programación funcionales que pueden ser una calidad extremadamente valiosa en su código.