¿Alguien puede explicarme por qué querría usar IList over List en C #?
Pregunta relacionada: ¿Por qué se considera malo exponer?List<T>
¿Alguien puede explicarme por qué querría usar IList over List en C #?
Pregunta relacionada: ¿Por qué se considera malo exponer?List<T>
Respuestas:
Si está exponiendo su clase a través de una biblioteca que otros usarán, generalmente desea exponerla a través de interfaces en lugar de implementaciones concretas. Esto ayudará si decide cambiar la implementación de su clase más tarde para usar una clase concreta diferente. En ese caso, los usuarios de su biblioteca no necesitarán actualizar su código ya que la interfaz no cambia.
Si solo lo está usando internamente, es posible que no le importe tanto, y usarlo List<T>
puede estar bien.
La respuesta menos popular es que a los programadores les gusta fingir que su software se reutilizará en todo el mundo, cuando de hecho la mayoría de los proyectos serán mantenidos por una pequeña cantidad de personas y por agradables que sean los fragmentos de sonido relacionados con la interfaz, estás engañando tú mismo.
Arquitectura Astronautas . Las posibilidades de que escriba alguna vez su propia IList que agregue algo a las que ya están en .NET Framework son tan remotas que son teorías gelatinosas reservadas para "mejores prácticas".
Obviamente, si se le pregunta qué usa en una entrevista, dice ILista, sonríe y ambos se ven complacidos por ser tan listos. O para una API pública, IList. Espero que entiendas mi punto.
IEnumerable<string>
, dentro de la función puedo usar un List<string>
para un almacén de respaldo interno para generar mi colección, pero solo quiero que las personas que llaman enumeren su contenido, no agreguen o eliminen. Aceptar una interfaz como parámetro comunica un mensaje similar: "Necesito una colección de cadenas, sin embargo, no se preocupe, no la cambiaré".
La interfaz es una promesa (o un contrato).
Como siempre sucede con las promesas, cuanto más pequeñas, mejor .
IList
es la promesa. ¿Cuál es la promesa más pequeña y mejor aquí? Esto no es lógico.
List<T>
, porque AddRange
no está definido en la interfaz.
IList<T>
hace promesas que no puede cumplir. Por ejemplo, hay un Add
método que puede lanzar si IList<T>
resulta ser de solo lectura. List<T>
Por otro lado, siempre se puede escribir y, por lo tanto, siempre cumple sus promesas. Además, desde .NET 4.6, ahora tenemos IReadOnlyCollection<T>
y IReadOnlyList<T>
que casi siempre se ajustan mejor que IList<T>
. Y son covariantes. Evitar IList<T>
!
IList<T>
. Alguien podría implementar IReadOnlyList<T>
y hacer que cada método arroje una excepción también. Es algo opuesto a la escritura de patos: lo ha llamado pato, pero no puede graznar como uno. Sin embargo, eso es parte del contrato, incluso si el compilador no puede cumplirlo. Sin embargo, estoy 100% de acuerdo en que IReadOnlyList<T>
o IReadOnlyCollection<T>
debería usarse para acceso de solo lectura.
Algunas personas dicen "siempre usar en IList<T>
lugar de List<T>
".
Quieren que cambie sus firmas de método de void Foo(List<T> input)
a void Foo(IList<T> input)
.
Estas personas están equivocadas.
Es más matizado que eso. Si está devolviendo una IList<T>
parte de la interfaz pública a su biblioteca, se deja opciones interesantes para quizás hacer una lista personalizada en el futuro. Es posible que nunca necesite esa opción, pero es un argumento. Creo que es todo el argumento para devolver la interfaz en lugar del tipo concreto. Vale la pena mencionarlo, pero en este caso tiene una falla grave.
Como un contraargumento menor, puede encontrar que cada persona que llama necesita un de List<T>
todos modos, y el código de llamada está lleno de.ToList()
Pero mucho más importante, si usted está aceptando un IList como parámetro será mejor que tener cuidado, porque IList<T>
, y List<T>
no se comportan de la misma manera. A pesar de la similitud en el nombre, y a pesar de compartir una interfaz , no exponen el mismo contrato .
Supongamos que tiene este método:
public Foo(List<int> a)
{
a.Add(someNumber);
}
Un colega útil "refactoriza" el método para aceptar IList<int>
.
Su código ahora está roto, porque se int[]
implementa IList<int>
, pero es de tamaño fijo. El contrato para ICollection<T>
(la base de IList<T>
) requiere el código que lo utiliza para verificar la IsReadOnly
bandera antes de intentar agregar o eliminar elementos de la colección. El contrato para List<T>
no.
El Principio de sustitución de Liskov (simplificado) establece que un tipo derivado debe poder usarse en lugar de un tipo base, sin condiciones previas o postcondiciones adicionales.
Esto parece que rompe el principio de sustitución de Liskov.
int[] array = new[] {1, 2, 3};
IList<int> ilist = array;
ilist.Add(4); // throws System.NotSupportedException
ilist.Insert(0, 0); // throws System.NotSupportedException
ilist.Remove(3); // throws System.NotSupportedException
ilist.RemoveAt(0); // throws System.NotSupportedException
Pero no lo hace. La respuesta a esto es que el ejemplo usó IList <T> / ICollection <T> incorrectamente. Si utiliza una ICollection <T>, debe verificar el indicador IsReadOnly.
if (!ilist.IsReadOnly)
{
ilist.Add(4);
ilist.Insert(0, 0);
ilist.Remove(3);
ilist.RemoveAt(0);
}
else
{
// what were you planning to do if you were given a read only list anyway?
}
Si alguien le pasa una Matriz o una Lista, su código funcionará bien si revisa la bandera cada vez y tiene una reserva ... Pero en realidad; quien hace eso ¿No sabe de antemano si su método necesita una lista que pueda incluir miembros adicionales? ¿no especificas eso en la firma del método? ¿Qué harías exactamente si te pasaran una lista de solo lectura int[]
?
Puede sustituir un List<T>
código que use IList<T>
/ ICollection<T>
correctamente . No puede garantizar que pueda sustituir un código IList<T>
/ ICollection<T>
into que utilice List<T>
.
Hay una apelación al Principio de responsabilidad única / Principio de segregación de interfaz en muchos de los argumentos para usar abstracciones en lugar de tipos concretos, depende de la interfaz más estrecha posible. En la mayoría de los casos, si está utilizando List<T>
ay cree que podría utilizar una interfaz más estrecha, ¿por qué no IEnumerable<T>
? Esto a menudo es mejor si no necesita agregar elementos. Si es necesario agregar a la colección, utilice el tipo de hormigón, List<T>
.
Para mí IList<T>
(y ICollection<T>
) es la peor parte del marco .NET. IsReadOnly
viola el principio de menor sorpresa. Una clase, como Array
, que nunca permite agregar, insertar o eliminar elementos, no debe implementar una interfaz con los métodos Agregar, Insertar y Eliminar. (consulte también /software/306105/implementing-an-interface-when-you-dont-need-one-of-the-properties )
¿Es IList<T>
una buena opción para su organización? Si un colega le pide que cambie la firma de un método para usar en IList<T>
lugar de List<T>
, pregúntele cómo agregaría un elemento a un IList<T>
. Si no saben IsReadOnly
(y la mayoría de la gente no), entonces no lo use IList<T>
. Siempre.
Tenga en cuenta que el indicador IsReadOnly proviene de ICollection <T> e indica si se pueden agregar o quitar elementos de la colección; pero para confundir realmente las cosas, no indica si se pueden reemplazar, lo que en el caso de las matrices (que devuelven IsReadOnlys == verdadero) puede ser.
Para obtener más información sobre IsReadOnly, consulte la definición de msdn de ICollection <T> .IsReadOnly
IsReadOnly
es true
para un IList
, ¡aún puede modificar sus miembros actuales! Quizás esa propiedad debería ser nombrada IsFixedSize
?
Array
como an IList
, por eso podría modificar a los miembros actuales)
IReadOnlyCollection<T>
y IReadOnlyList<T>
que casi siempre se ajustan mejor, IList<T>
pero no tienen la semántica perezosa peligrosa de IEnumerable<T>
. Y son covariantes. Evitar IList<T>
!
List<T>
es una implementación específica de IList<T>
, que es un contenedor que se puede abordar de la misma manera que una matriz lineal T[]
utilizando un índice entero. Cuando especifica IList<T>
como tipo de argumento del método, solo especifica que necesita ciertas capacidades del contenedor.
Por ejemplo, la especificación de la interfaz no impone una estructura de datos específica para ser utilizada. La implementación de List<T>
sucede con el mismo rendimiento para acceder, eliminar y agregar elementos como una matriz lineal. Sin embargo, podría imaginar una implementación respaldada por una lista vinculada, para la cual agregar elementos al final es más barato (tiempo constante) pero el acceso aleatorio es mucho más costoso. (Tenga en cuenta que .NET LinkedList<T>
no se implementa IList<T>
).
Este ejemplo también le dice que puede haber situaciones en las que necesite especificar la implementación, no la interfaz, en la lista de argumentos: en este ejemplo, siempre que requiera una característica de rendimiento de acceso particular. Esto generalmente está garantizado para una implementación específica de un contenedor ( List<T>
documentación: "Implementa la IList<T>
interfaz genérica utilizando una matriz cuyo tamaño se incrementa dinámicamente según sea necesario").
Además, es posible que desee considerar exponer la menor funcionalidad que necesita. Por ejemplo. Si no necesita cambiar el contenido de la lista, probablemente debería considerar usar IEnumerable<T>
, que se IList<T>
extiende.
LinkedList<T>
vs tomar List<T>
vs IList<T>
todos comunica algo de las garantías de rendimiento requeridas por el código que se llama.
Cambiaría la pregunta un poco, en lugar de justificar por qué debería usar la interfaz sobre la implementación concreta, intente justificar por qué usaría la implementación concreta en lugar de la interfaz. Si no puede justificarlo, use la interfaz.
Un principio de TDD y OOP generalmente es programar para una interfaz, no una implementación.
En este caso específico, ya que esencialmente está hablando de una construcción de lenguaje, no personalizada, generalmente no importará, pero digamos, por ejemplo, que encontró que List no era compatible con algo que necesitaba. Si hubiera utilizado IList en el resto de la aplicación, podría extender List con su propia clase personalizada y aún así poder transmitirlo sin refactorizarlo.
El costo para hacer esto es mínimo, ¿por qué no te ahorras el dolor de cabeza más tarde? De eso se trata el principio de la interfaz.
public void Foo(IList<Bar> list)
{
// Do Something with the list here.
}
En este caso, podría pasar en cualquier clase que implemente la interfaz IList <Bar>. Si utilizó List <Bar> en su lugar, solo se podría pasar una instancia de List <Bar>.
La forma IList <Bar> está más débilmente acoplada que la forma List <Bar>.
Suponiendo que ninguna de estas preguntas (o respuestas) de List vs IList menciona la diferencia distintiva. (¡Por eso busqué esta pregunta en SO!)
Así que aquí están los métodos contenidos por List que no se encuentran en IList, al menos a partir de .NET 4.5 (circa 2015)
Reverse
y ToArray
son métodos de extensión para la interfaz IEnumerable <T>, de la cual se deriva IList <T>.
List
sy no en otras implementaciones de IList
.
El caso más importante para usar interfaces sobre implementaciones está en los parámetros de su API. Si su API toma un parámetro de Lista, cualquiera que lo use tiene que usar Lista. Si el tipo de parámetro es IList, la persona que llama tiene mucha más libertad y puede usar clases de las que nunca ha oído hablar, que pueden no haber existido cuando se escribió el código.
¿Qué pasa si .NET 5.0 reemplaza System.Collections.Generic.List<T>
a System.Collection.Generics.LinearList<T>
. .NET siempre posee el nombre List<T>
pero garantiza que IList<T>
es un contrato. Entonces, en mi humilde opinión, nosotros (al menos I) no deben usar el nombre de alguien (aunque es .NET en este caso) y tener problemas más tarde.
En caso de uso IList<T>
, la persona que llama siempre tiene garantizado que funcionen, y el implementador es libre de cambiar la colección subyacente a cualquier implementación concreta alternativa deIList
Todos los conceptos se expresan básicamente en la mayoría de las respuestas anteriores con respecto a por qué usar la interfaz sobre implementaciones concretas.
IList<T> defines those methods (not including extension methods)
IList<T>
Enlace MSDN
List<T>
implementa esos nueve métodos (sin incluir los métodos de extensión), además de eso tiene alrededor de 41 métodos públicos, lo que pesa en la consideración de cuál usar en su aplicación.
List<T>
Enlace MSDN
Lo haría porque definir una IList o una ICollection se abriría para otras implementaciones de sus interfaces.
Es posible que desee tener un IOrderRepository que defina una colección de pedidos en IList o ICollection. Luego, podría tener diferentes tipos de implementaciones para proporcionar una lista de órdenes siempre que se ajusten a las "reglas" definidas por su IList o ICollection.
IList <> es casi siempre preferible según el consejo del otro póster, sin embargo, tenga en cuenta que hay un error en .NET 3.5 sp 1 al ejecutar un IList <> a través de más de un ciclo de serialización / deserialización con el WCF DataContractSerializer.
Ahora hay un SP para corregir este error: KB 971030
La interfaz garantiza que al menos obtenga los métodos que espera ; siendo consciente de la definición de la interfaz, es decir. todos los métodos abstractos que están allí para ser implementados por cualquier clase que herede la interfaz. así que si alguien hace una clase enorme con varios métodos además de los que heredó de la interfaz para alguna funcionalidad adicional, y esos no son útiles para usted, es mejor usar una referencia a una subclase (en este caso, el interfaz) y asígnele el objeto de clase concreto.
Una ventaja adicional es que su código está a salvo de cualquier cambio en la clase concreta, ya que se está suscribiendo a solo algunos de los métodos de la clase concreta y esos son los que estarán allí siempre que la clase concreta herede de la interfaz que está utilizando. así que es seguridad para usted y libertad para el codificador que está escribiendo una implementación concreta para cambiar o agregar más funcionalidad a su clase concreta.
Puede ver este argumento desde varios ángulos, incluido el de un enfoque puramente OO que dice programar contra una interfaz, no una implementación. Con este pensamiento, usar IList sigue el mismo principio que pasar y usar Interfaces que usted define desde cero. También creo en los factores de escalabilidad y flexibilidad proporcionados por una interfaz en general. Si una clase que implica IList <T> necesita ser extendida o cambiada, el código consumidor no tiene que cambiar; sabe a qué se adhiere el contrato de IList Interface. Sin embargo, el uso de una implementación concreta y la Lista <T> en una clase que cambia, podría hacer que el código de llamada también deba modificarse. Esto se debe a que una clase que se adhiere a IList <T> garantiza un cierto comportamiento que no está garantizado por un tipo concreto que usa List <T>.
También tener el poder de hacer algo como modificar la implementación predeterminada de la Lista <T> en una clase Implementando IList <T> por ejemplo, .Add, .Remove o cualquier otro método IList le da al desarrollador mucha flexibilidad y poder, de lo contrario predefinido por Lista <T>
Por lo general, un buen enfoque es usar IList en su API pública (cuando sea apropiado, y se necesita una lista semántica), y luego Listar internamente para implementar la API. Esto le permite cambiar a una implementación diferente de IList sin romper el código que usa su clase.
La lista de nombres de clase se puede cambiar en el siguiente marco .net, pero la interfaz nunca cambiará ya que la interfaz es un contrato.
Tenga en cuenta que, si su API solo se va a usar en bucles foreach, etc., entonces puede considerar exponer IEnumerable en su lugar.