c# - trailer - ¿Devolver IList<T> es peor que devolver T[] o List<T>?
volver cancion (5)
Al igual que con todas las preguntas de "interfaz versus implementación", tendrá que darse cuenta de lo que significa exponer a un miembro público: define la API pública de esta clase.
Si expone una
List<T>
como miembro (campo, propiedad, método, ...), le dice al consumidor de ese miembro: el tipo obtenido al acceder a este método
es
una
List<T>
, o algo derivado de eso .
Ahora, si expone una interfaz, oculta el "detalle de implementación" de su clase utilizando un tipo concreto.
Por supuesto, no puede crear una instancia de
IList<T>
, pero puede usar una
Collection<T>
,
List<T>
, derivaciones de la misma o su propio tipo que implementa
IList<T>
.
La pregunta
real
es
"¿Por qué
Array
implementa
IList<T>
"
o
"¿Por qué la interfaz
IList<T>
tantos miembros?"
También depende de lo que quiera que hagan los consumidores de ese miembro.
Si realmente devuelve un miembro interno a través de su miembro
Expose...
, querrá devolver una
new List<T>(internalMember)
todos modos, de lo contrario, el consumidor puede intentar enviarlos a
IList<T>
y modificar su interno miembro a través de eso.
Si solo espera que los consumidores repitan los resultados, exponga
IEnumerable<T>
o
IReadOnlyCollection<T>
lugar.
Las respuestas a preguntas como esta: Lista <T> o IList <T> siempre parecen estar de acuerdo en que devolver una interfaz es mejor que devolver una implementación concreta de una colección. Pero estoy luchando con esto. Es imposible crear instancias de una interfaz, por lo que si su método devuelve una interfaz, en realidad sigue devolviendo una implementación específica. Estaba experimentando un poco con esto escribiendo 2 pequeños métodos:
public static IList<int> ExposeArrayIList()
{
return new[] { 1, 2, 3 };
}
public static IList<int> ExposeListIList()
{
return new List<int> { 1, 2, 3 };
}
Y usarlos en mi programa de prueba:
static void Main(string[] args)
{
IList<int> arrayIList = ExposeArrayIList();
IList<int> listIList = ExposeListIList();
//Will give a runtime error
arrayIList.Add(10);
//Runs perfectly
listIList.Add(10);
}
En ambos casos cuando trato de agregar un nuevo valor, mi compilador no me da errores, pero obviamente el método que expone mi matriz como
IList<T>
da un error de tiempo de ejecución cuando intento agregarle algo.
Por lo tanto, las personas que no saben lo que está sucediendo en mi método y tienen que agregarle valores, se ven obligadas a copiar primero mi
IList
a una
List
para poder agregar valores sin arriesgarse a cometer errores.
Por supuesto, pueden hacer una verificación de tipo para ver si se trata de una
List
o una
Array
, pero si no lo hacen, y quieren agregar elementos a la colección,
no tienen otra opción para copiar la
IList
a un
List
, incluso si ya es una
List
.
¿Una matriz nunca debe exponerse como
IList
?
Otra preocupación mía se basa en la respuesta aceptada de la pregunta vinculada (énfasis mío):
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á utilizando internamente, es posible que no le importe tanto, y usar List puede estar bien.
Imagine que alguien realmente usara mi
IList<T>
que obtuvieron de mi método
ExposeListIlist()
simplemente para agregar / eliminar valores.
Todo funciona bien
Pero ahora, como sugiere la respuesta, dado que devolver una interfaz es más flexible, devuelvo una matriz en lugar de una Lista (¡no hay ningún problema de mi parte!), Entonces se van a tratar ...
TLDR:
1) ¿La exposición de una interfaz provoca lanzamientos innecesarios? ¿Eso no importa?
2) A veces, si los usuarios de la biblioteca no usan una conversión, su código puede romperse cuando cambias tu método, aunque el método permanezca perfectamente bien.
Probablemente estoy pensando demasiado en esto, pero no obtengo el consenso general de que devolver una interfaz es preferible a devolver una implementación.
Devolver una interfaz no es necesariamente mejor que devolver una implementación concreta de una colección. Siempre debe tener una buena razón para usar una interfaz en lugar de un tipo concreto. En su ejemplo, parece inútil hacerlo.
Las razones válidas para usar una interfaz pueden ser:
-
No sabe cómo será la implementación de los métodos que devuelven la interfaz y puede haber muchos desarrollados a lo largo del tiempo. Pueden ser otras personas escribiéndolas, de otras compañías. Por lo tanto, solo desea ponerse de acuerdo sobre las necesidades básicas y dejarles a ellos cómo implementar la funcionalidad.
-
Desea exponer algunas funciones comunes independientemente de su jerarquía de clases de una manera segura. Los objetos de diferentes tipos básicos que deberían ofrecer los mismos métodos implementarían su interfaz.
Se podría argumentar que 1 y 2 son básicamente la misma razón. Son dos escenarios diferentes que finalmente conducen a la misma necesidad.
"Es un contrato". Si el contrato es con usted y su aplicación está cerrada tanto en funcionalidad como en tiempo, a menudo no tiene sentido usar una interfaz.
Tal vez esto no responda directamente a su pregunta, pero en .NET 4.5+, prefiero seguir estas reglas al diseñar API públicas o protegidas:
-
devuelva
IEnumerable<T>
, si solo está disponible la enumeración; -
devuelva
IReadOnlyCollection<T>
si tanto la enumeración como el recuento de elementos están disponibles; -
devuelva
IReadOnlyList<T>
, si la enumeración, el recuento de elementos y el acceso indexado están disponibles; -
devuelva
ICollection<T>
si la enumeración, el recuento de elementos y la modificación están disponibles; -
devuelva
IList<T>
, si la enumeración, el recuento de elementos, el acceso indexado y la modificación están disponibles.
Las dos últimas opciones suponen que ese método no debe devolver la matriz como implementación
IList<T>
.
Tenga cuidado con las citas generales que se toman fuera de contexto.
Devolver una interfaz es mejor que devolver una implementación concreta
Esta cita solo tiene sentido si se usa en el contexto de los principios SÓLIDOS . Hay 5 principios, pero a los fines de esta discusión, hablaremos de los últimos 3.
Principio de inversión de dependencia
uno debe "depender de abstracciones. No dependas de concreciones.
En mi opinión, este principio es el más difícil de entender. Pero si miras la cita cuidadosamente, se parece mucho a tu cita original.
Depende de las interfaces (abstracciones). No dependa de implementaciones concretas (concreciones).
Esto sigue siendo un poco confuso, pero si comenzamos a aplicar los otros principios juntos, comenzará a tener mucho más sentido.
Principio de sustitución de Liskov
"Los objetos en un programa deben ser reemplazables con instancias de sus subtipos sin alterar la corrección de ese programa".
Como señaló, devolver una
Array
es un comportamiento claramente diferente a devolver una
List<T>
a pesar de que ambas implementan
IList<T>
.
Esto es sin duda una violación de LSP.
Lo importante es darse cuenta de que las interfaces son sobre el consumidor . Si está devolviendo una interfaz, ha creado un contrato para que cualquier método o propiedad en esa interfaz pueda usarse sin cambiar el comportamiento del programa.
Principio de segregación de interfaz
"Muchas interfaces específicas del cliente son mejores que una interfaz de uso general".
Si está devolviendo una interfaz, debe devolver la interfaz más específica del cliente que su implementación admita.
En otras palabras, si no espera que el cliente llame al método
Add
, no debe devolver una interfaz con un método
Add
.
Desafortunadamente, las interfaces en .NET Framework (particularmente las versiones anteriores) no siempre son interfaces ideales para clientes específicos. Aunque como señaló @Dennis en su respuesta, hay muchas más opciones en .NET 4.5+.
No, porque el consumidor debe saber qué es exactamente IList :
IList es un descendiente de la interfaz ICollection y es la interfaz base de todas las listas no genéricas. Las implementaciones de IList se dividen en tres categorías: solo lectura, tamaño fijo y tamaño variable. Una IList de solo lectura no se puede modificar. Una IList de tamaño fijo no permite la adición o eliminación de elementos, pero permite la modificación de elementos existentes. Una IList de tamaño variable permite la adición, eliminación y modificación de elementos.
Puede verificar
IList.IsFixedSize
e
IList.IsReadOnly
y hacer lo que quiera con él.
Creo que
IList
es un ejemplo de una interfaz gruesa y debería haberse dividido en varias interfaces más pequeñas y también viola el
principio de sustitución de Liskov
cuando devuelve una matriz como
IList
.
Lea más si desea tomar una decisión sobre la interfaz de retorno
ACTUALIZAR
Excavando más y descubrí que
IList<T>
no implementa
IList
e
IsReadOnly
es accesible a través de la interfaz base
ICollection<T>
pero no hay
IsFixedSize
para
IList<T>
.
Lea más sobre
por qué IList genérico <> no hereda IList no genérico.