¿Por qué usar IList o List?

82

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?

chobo2
fuente

Respuestas:

156

Aquí hay tres preguntas: ¿qué tipo debo usar para un parámetro formal? ¿Qué debo usar para una variable local? y ¿qué debo usar para un tipo de devolución?

Parámetros formales:

El principio aquí es que no pida más de lo que necesita . IEnumerable<T>comunica "Necesito obtener los elementos de esta secuencia de principio a fin". IList<T>comunica "Necesito obtener y establecer los elementos de esta secuencia en orden arbitrario". List<T>comunica "Necesito obtener y configurar los elementos de esta secuencia en orden arbitrario y solo acepto listas; no acepto matrices".

Al pedir más de lo que necesita, (1) hace que la persona que llama haga un trabajo innecesario para satisfacer sus demandas innecesarias y (2) comunica falsedades al lector. Pregunte solo por lo que va a utilizar. De esa forma, si la persona que llama tiene una secuencia, no es necesario que llame a ToList para satisfacer su demanda.

Variables locales:

Usa lo que quieras. Es tu método. Eres el único que puede ver los detalles de implementación interna del método.

Tipo de retorno:

El mismo principio que antes, invertido. Ofrezca lo mínimo que requiere su interlocutor. Si la persona que llama solo requiere la capacidad de enumerar la secuencia, solo dele un IEnumerable<T>.

Eric Lippert
fuente
9
Sin embargo, ¿cómo sabe lo que necesita la persona que llama? Por ejemplo, estaba cambiando uno de mis tipos de retorno a un IList <>, entonces, bueno, probablemente solo los enumeraré de todos modos, devolvamos un IEnumberable. Luego miré en mi vista (mvc) y descubrí que realmente necesitaba el método de conteo, ya que necesitaba usar un bucle for. Entonces, en mi propia aplicación, subestimé lo que realmente necesitaba, cómo anticipar lo que otra persona necesitará o no necesitará.
Chobo2
@ chobo2: En su ejemplo específico, LINQ Countfunciona en O (1) si IEnumerablees un ICollection. Por supuesto, también puedes usar un foreachbucle.
Brian
6
@ chobo2: Bueno, ¿cómo anticipa qué métodos necesitará la persona que llama? Ese parece ser el problema a resolver primero. Es de suponer que de alguna manera tiene una forma de saber qué métodos escribir para las personas que los van a llamar. Pregúnteles a esas personas qué métodos les gustaría que regresaran. Su pregunta es fundamentalmente "¿cómo sé qué software escribir?" Usted sabe al conocer qué problemas tiene que resolver su cliente y al escribir código que resuelva sus problemas.
Eric Lippert
1
@Eric: debe actualizar su respuesta para que los parámetros formales incluyan un ejemplo de ICollection donde solo necesita agregar un elemento. Además, la explicación del tipo de devolución debe decir algo como "ofrecer solo lo mínimo de lo que permite que realice la persona que llama". Si es una lista de solo lectura, devuelva solo IEnumerable, etc.
Charles Lambert
4
@kvb: Casi. Considere su primer escenario. Puede realizar una mejora algorítmica haciendo 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.
Eric Lippert
33

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
10
Tenga en cuenta que este consejo contradice la respuesta de Eric Lippert en la recomendación para tipos de devolución. El enfoque de Jeffrey Richter brinda a los consumidores del método la mayor flexibilidad para usar el objeto devuelto como deseen, mientras que el de Eric brinda a los mantenedores del método la mayor flexibilidad para cambiar la implementación sin modificar la superficie pública del método. Tiendo a seguir los consejos de Jeffrey para el código interno, pero para una biblioteca pública, probablemente estaría más inclinado a seguir los de Eric.
phoog
3
@phoog: Teniendo en cuenta de dónde viene Eric, no sería sorprendente que sea más cauteloso con los cambios importantes. Pero definitivamente es un punto válido.
Muy rudo. Parece que hay gente para ambos lados (regrese mínimo o no). Entiendo más por qué lo hace por los parámetros que ayuda a detener la conversión innecesaria. Todavía no estoy seguro de qué hacer con el tipo de devolución.
chobo2
@ chobo2: Realmente, solo debe considerar si es posible que necesite cambiar el tipo de retorno en el futuro si especifica algo más exacto en lugar de general. Si puede considerar su método, determine que probablemente no cambiará el tipo de colección de devolución, entonces probablemente sea seguro devolver un tipo más exacto. Si no está seguro, o tiene miedo de que si lo cambia en el futuro estará rompiendo el código de otras personas, entonces vaya de manera más general.
1
+1 Trivia: esta respuesta es un buen ejemplo específico de CLR de la ley de Postel . :)
Dan J
13

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 un IList<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 a List<T>a Dictionary<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 el List<T>como IEnumerable<T>, puede predecir cómodamente que su colección no se modificará externamente. Ese es uno de los poderes de exponer List<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 un IEnumerable<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.

Charles Lambert
fuente
9

Respuesta corta:

Pasas la interfaz para que no importa qué implementación concreta de esa interfaz uses, tu código la admitirá.

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 .

Adrian
fuente
8

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 un IList<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.

phoog
fuente
3

Dentro del método, debe usar var, en lugar de IListo List. Cuando su fuente de datos cambia para provenir de un método, su onlySomeIntsmétodo sobrevivirá.

La razón para usar en IListlugar de Listcomo parámetros es porque se implementan muchas cosas IList(List y [], como dos ejemplos), pero solo una se implementa List. 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 implementa IEnumerable(o debería) y hace que su método sea enormemente flexible.

Bryan Boettcher
fuente
13
Decir que debería usar "var" es completamente incorrecto. No importa lo que use allí, es una cuestión de estilo. No afecta la firma del método y está escrito en piedra en el momento de la compilación. En su lugar, debería ayudarlo a superar su confusión acerca de declarar su lista local como IList foo = new - aquí es donde claramente radica su confusión.
x0n
Entonces, básicamente, estás diciendo que aceptes IList solo por el hecho de que si quieren enviar una matriz que no tienen que hacer .ToList primero. ¿Qué tal devolverlo? No entiendo lo que quieres decir con "el método sobrevivirá" cuando usas vars. Sé que será un poco más de trabajo, pero ¿no tendrás que cambiar eso también al nuevo tipo? Entonces, ¿tal vez IList <int> a IList <String>?
chobo2
Es cierto que el punto de "use var" fue más una sugerencia de no preocuparse por eso dentro del método mismo y centrarse más en cómo se ve a los consumidores.
Bryan Boettcher
Estoy de acuerdo con @ x0n: var se usa en exceso y no ayudará a aclarar nada.
Ese Chuck Guy
1

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.

Natanael
fuente
1

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.

Ricardo
fuente
1

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,

jmoreno
fuente
0

Aquí está mi respuesta en este mundo .NET 4.5+ .

Utilice IList <T> e IReadonlyList <T> , en
              lugar de List <T> , porque ReadonlyList <T> no existe.

IList <T> parece tan consistente con IReadonlyList <T>

  • Use IEnumerable <T> para la exposición mínima (propiedad) o el requisito (parámetro) si foreach es la única forma de usarlo.
  • Use IReadonlyList <T> si también necesita exponer / usar Count y [] indexer.
  • Use IList <T> si también permite que las personas que llaman agreguen / actualicen / eliminen elementos

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; } }
detale
fuente