Sé que ha habido muchas publicaciones sobre esto, pero todavía me confunde por qué debería pasar una interfaz como IList y devolver una interfaz como IList en lugar de la lista concreta.
Leí muchas publicaciones que dicen cómo esto hace que sea más fácil cambiar la implementación más adelante, pero no veo completamente cómo funciona eso.
Di si tengo este método
public class SomeClass
{
public bool IsChecked { get; set; }
}
public void LogAllChecked(IList<SomeClass> someClasses)
{
foreach (var s in someClasses)
{
if (s.IsChecked)
{
// log
}
}
}
No estoy seguro de cómo el uso de IList me ayudará en el futuro.
¿Qué tal si ya estoy en el método? ¿Debería seguir usando IList?
public void LogAllChecked(IList<SomeClass> someClasses)
{
//why not List<string> myStrings = new List<string>()
IList<string> myStrings = new List<string>();
foreach (var s in someClasses)
{
if (s.IsChecked)
{
myStrings.Add(s.IsChecked.ToString());
}
}
}
¿Qué obtengo por usar IList ahora?
public IList<int> onlySomeInts(IList<int> myInts)
{
IList<int> store = new List<int>();
foreach (var i in myInts)
{
if (i % 2 == 0)
{
store.Add(i);
}
}
return store;
}
¿Que tal ahora? ¿Hay alguna nueva implementación de una lista de int que tendré que cambiar?
Básicamente, necesito ver algunos ejemplos de código reales de cómo el uso de IList habría resuelto algún problema sobre simplemente incluir List en todo.
Por mi lectura, creo que podría haber usado IEnumberable en lugar de IList ya que solo estoy repasando cosas.
Editar Así que he estado jugando con algunos de mis métodos sobre cómo hacer esto. Todavía no estoy seguro del tipo de devolución (si debo hacerlo más concreto o una interfaz).
public class CardFrmVm
{
public IList<TravelFeaturesVm> TravelFeaturesVm { get; set; }
public IList<WarrantyFeaturesVm> WarrantyFeaturesVm { get; set; }
public CardFrmVm()
{
WarrantyFeaturesVm = new List<WarrantyFeaturesVm>();
TravelFeaturesVm = new List<TravelFeaturesVm>();
}
}
public class WarrantyFeaturesVm : AvailableFeatureVm
{
}
public class TravelFeaturesVm : AvailableFeatureVm
{
}
public class AvailableFeatureVm
{
public Guid FeatureId { get; set; }
public bool HasFeature { get; set; }
public string Name { get; set; }
}
private IList<AvailableFeature> FillAvailableFeatures(IEnumerable<AvailableFeatureVm> avaliableFeaturesVm)
{
List<AvailableFeature> availableFeatures = new List<AvailableFeature>();
foreach (var f in avaliableFeaturesVm)
{
if (f.HasFeature)
{
// nhibernate call to Load<>()
AvailableFeature availableFeature = featureService.LoadAvaliableFeatureById(f.FeatureId);
availableFeatures.Add(availableFeature);
}
}
return availableFeatures;
}
Ahora estoy devolviendo IList por el simple hecho de que luego agregaré esto a mi modelo de dominio que tiene una propiedad como esta:
public virtual IList<AvailableFeature> AvailableFeatures { get; set; }
Lo anterior es un IList en sí mismo, ya que parece ser el estándar para usar con nhibernate. De lo contrario, podría haber devuelto IEnumberable, pero no estoy seguro. Aún así, no puedo entender qué necesitaría el usuario al 100% (ahí es donde devolver un concreto tiene una ventaja).
Editar 2
También estaba pensando ¿qué pasa si quiero pasar por referencia en mi método?
private void FillAvailableFeatures(IEnumerable<AvailableFeatureVm> avaliableFeaturesVm, IList<AvailableFeature> toFill)
{
foreach (var f in avaliableFeaturesVm)
{
if (f.HasFeature)
{
// nhibernate call to Load<>()
AvailableFeature availableFeature = featureService.LoadAvaliableFeatureById(f.FeatureId);
toFill.Add(availableFeature);
}
}
}
¿tendría problemas con esto? ¿Ya que no podrían pasar en una matriz (que tiene un tamaño fijo)? ¿Sería mejor quizás para una lista concreta?
Count
funciona en O (1) siIEnumerable
es unICollection
. Por supuesto, también puedes usar unforeach
bucle.items as IList<T>
y si obtiene un valor no nulo, utilice su código mejorado. De esa manera, aprovechará si puede, al mismo tiempo que le permite al cliente flexibilidad en lo que pasa.La razón más práctica que he visto la dio Jeffrey Richter en CLR a través de C #.
El patrón es tomar la clase o interfaz más básica posible para sus argumentos y devolver la clase o interfaz más específica posible para sus tipos de retorno. Esto les da a las personas que llaman la mayor flexibilidad para pasar tipos a sus métodos y la mayor cantidad de oportunidades para emitir / reutilizar los valores devueltos.
Por ejemplo, el siguiente método
public void PrintTypes(IEnumerable items) { foreach(var item in items) Console.WriteLine(item.GetType().FullName); }
permite que se llame al método pasando cualquier tipo que pueda convertirse en un enumerable . Si fueras más específico
public void PrintTypes(List items)
luego, digamos, si tuviera una matriz y deseara imprimir sus nombres de tipo en la consola, primero tendría que crear una nueva Lista y llenarla con sus tipos. Y, si usó una implementación genérica, solo podría usar un método que funcione para cualquier objeto solo con objetos de un tipo específico.
Cuando se habla de tipos de devolución, cuanto más específico sea, más flexibles pueden ser las personas que llaman.
public List<string> GetNames()
puede usar este tipo de retorno para iterar los nombres
foreach(var name in GetNames())
o puede indexar directamente en la colección
Console.WriteLine(GetNames()[0])
Mientras que, si recuperara un tipo menos específico
public IEnumerable GetNames()
tendrías que masajear el tipo de retorno para obtener el primer valor
Console.WriteLine(GetNames().OfType<string>().First());
fuente
IEnumerable<T>
le permite iterar a través de una colección.ICollection<T>
se basa en esto y también permite agregar y eliminar elementos.IList<T>
también permite acceder a ellos y modificarlos en un índice específico. Al exponer aquel con el que espera que trabaje su consumidor, puede cambiar su implementación.List<T>
pasa a implementar las tres de esas interfaces.Si expone su propiedad como un
List<T>
o incluso unIList<T>
cuando todo lo que desea que su consumidor tenga es la capacidad de recorrer la colección. Entonces podrían llegar a depender del hecho de que pueden modificar la lista. Luego, más tarde, si decide convertir el almacén de datos real de aList<T>
aDictionary<T,U>
ay exponer las claves del diccionario como el valor real de la propiedad (he tenido que hacer exactamente esto antes). Entonces, los consumidores que hayan llegado a esperar que sus cambios se reflejen dentro de su clase ya no tendrán esa capacidad. ¡Eso es un gran problema! Si expone elList<T>
comoIEnumerable<T>
, puede predecir cómodamente que su colección no se modificará externamente. Ese es uno de los poderes de exponerList<T>
como cualquiera de las interfaces anteriores.Este nivel de abstracción va en la otra dirección cuando pertenece a los parámetros del método. Cuando pasa su lista a un método que acepta
IEnumerable<T>
, puede estar seguro de que su lista no se modificará. Cuando eres la persona que implementa el método y dices que aceptas unIEnumerable<T>
porque todo lo que necesitas hacer es recorrer esa lista. Entonces, la persona que llama al método es libre de llamarlo con cualquier tipo de datos que sea enumerable. Esto permite que su código se use de formas inesperadas, pero perfectamente válidas.De esto se deduce que la implementación de su método puede representar sus variables locales como desee. Los detalles de implementación no están expuestos. Dejándolo libre para cambiar su código a algo mejor sin afectar a las personas que llaman a su código.
No se puede predecir el futuro. Asumir que el tipo de una propiedad siempre será beneficioso ya que
List<T>
limita inmediatamente su capacidad para adaptarse a expectativas imprevistas de su código. Sí, es posible que nunca cambie ese tipo de datos de a,List<T>
pero puede estar seguro de que si es necesario. Tu código está listo para ello.fuente
Respuesta corta:
Si utiliza una implementación concreta de la lista, su código no admitirá otra implementación de la misma lista.
Lea un poco sobre herencia y polimorfismo .
fuente
Aquí hay un ejemplo: una vez tuve un proyecto en el que nuestras listas se volvieron muy grandes y la fragmentación resultante del montón de objetos grandes estaba afectando el rendimiento. Reemplazamos List con LinkedList. LinkedList no contiene una matriz, por lo que, de repente, casi no usamos el montón de objetos grandes.
Principalmente, usamos las listas como
IEnumerable<T>
, de todos modos, por lo que no hubo más cambios necesarios. (Y sí, recomendaría declarar las referencias como IEnumerable si todo lo que está haciendo es enumerarlas). En un par de lugares, necesitábamos el indexador de listas, por lo que escribimos unIList<T>
contenedor ineficiente alrededor de las listas vinculadas. Con poca frecuencia necesitábamos el indexador de listas, por lo que la ineficiencia no fue un problema. Si lo hubiera sido, podríamos haber proporcionado alguna otra implementación de IList, tal vez como una colección de arreglos lo suficientemente pequeños, que hubieran sido indexables de manera más eficiente y al mismo tiempo evitando objetos grandes.Al final, es posible que deba reemplazar una implementación por cualquier motivo; el rendimiento es solo una posibilidad. Independientemente del motivo, el uso del tipo derivado menos posible reducirá la necesidad de realizar cambios en su código cuando cambie el tipo de tiempo de ejecución específico de sus objetos.
fuente
Dentro del método, debe usar
var
, en lugar deIList
oList
. Cuando su fuente de datos cambia para provenir de un método, suonlySomeInts
método sobrevivirá.La razón para usar en
IList
lugar deList
como parámetros es porque se implementan muchas cosasIList
(List y [], como dos ejemplos), pero solo una se implementaList
. Es más flexible codificar en la interfaz.Si solo está enumerando los valores, debería usar
IEnumerable
. Cada tipo de tipo de datos que puede contener más de un valor se implementaIEnumerable
(o debería) y hace que su método sea enormemente flexible.fuente
El uso de IList en lugar de List facilita considerablemente la escritura de pruebas unitarias. Le permite usar una biblioteca 'Mocking' para pasar y devolver datos.
La otra razón general para usar interfaces es exponer la cantidad mínima de conocimiento necesaria al usuario de un objeto.
Considere el caso (artificial) en el que tengo un objeto de datos que implementa IList.
public class MyDataObject : IList<int> { public void Method1() { ... } // etc }
Sus funciones anteriores solo se preocupan por poder iterar sobre una lista. Idealmente, no deberían necesitar saber quién implementa esa lista o cómo la implementa.
En su ejemplo, IEnumerable es una mejor opción como pensaba.
fuente
Siempre es una buena idea reducir las dependencias entre su código tanto como sea posible.
Teniendo esto en cuenta, tiene más sentido pasar tipos con el menor número posible de dependencias externas y devolver lo mismo. Sin embargo, esto podría ser diferente según la visibilidad de sus métodos y sus firmas.
Si sus métodos forman parte de una interfaz, los métodos deberán definirse utilizando los tipos disponibles para esa interfaz. Los tipos concretos probablemente no estarán disponibles para las interfaces, por lo que tendrían que devolver tipos no concretos. Querría hacer esto si estuviera creando un marco, por ejemplo.
Sin embargo, si no está escribiendo un marco, puede ser ventajoso pasar parámetros con los tipos más débiles posibles (es decir, clases base, interfaces o incluso delegados) y devolver tipos concretos. Eso le da a la persona que llama la capacidad de hacer todo lo posible con el objeto devuelto, incluso si se lanza como una interfaz. Sin embargo, esto hace que el método sea más frágil, ya que cualquier cambio en el tipo de objeto devuelto puede romper el código de llamada. Sin embargo, en la práctica, eso generalmente no es un problema importante.
fuente
Aceptas una Interfaz como parámetro para un método porque eso permite a la persona que llama enviar diferentes tipos concretos como argumentos. Dado su método de ejemplo LogAllChecked, el parámetro someClasses podría ser de varios tipos, y para la persona que escribe el método, todos pueden ser equivalentes (es decir, escribiría exactamente el mismo código independientemente del tipo de parámetro). Pero para la persona que llama al método, puede hacer una gran diferencia: si tiene una matriz y usted solicita una lista, debe cambiar la matriz a una lista o vv cada vez que llama al método, una pérdida total de tiempo tanto de un programador como del punto de vista del rendimiento.
El hecho de que devuelva una interfaz o un tipo concreto depende de lo que desee que las personas que llaman hagan con el objeto que creó; esta es una decisión de diseño de API y no existe una regla estricta y rápida. Debe sopesar su capacidad para hacer un uso completo del objeto con su capacidad para usar fácilmente una parte de la funcionalidad de los objetos (y, por supuesto, si DESEA que hagan un uso completo del objeto). Por ejemplo, si devuelve un IEnumerable, entonces los está limitando a iterar: no pueden agregar o eliminar elementos de su objeto, solo pueden actuar contra los objetos. Si necesita exponer una colección fuera de una clase, pero no desea permitir que la persona que llama cambie la colección, esta es una forma de hacerlo. Por otro lado, si devuelve una colección vacía que espera / desea que llenen,
fuente
Aquí está mi respuesta en este mundo .NET 4.5+ .
IList <T> parece tan consistente con IReadonlyList <T>
debido a que List <T> implementa IReadonlyList <T> , no necesita ninguna conversión explícita.
Una clase de ejemplo:
// manipulate the list within the class private List<int> _numbers; // callers can add/update/remove elements, but cannot reassign a new list to this property public IList<int> Numbers { get { return _numbers; } } // callers can use: .Count and .ReadonlyNumbers[idx], but cannot add/update/remove elements public IReadOnlyList<int> ReadonlyNumbers { get { return _numbers; } }
fuente