Por qué heredar de List<T> en C# es una mala idea: análisis

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Introducción: la trampa en la que muchos han caído

\n

Imagine la situación: está escribiendo una aplicación para una biblioteca en línea. Necesita su propia clase Library que almacene libros, pueda ordenarlos, buscarlos y mostrar estadísticas. Lo primero que viene a la mente es heredar de List<Book>. Después de todo, ya tiene todo: Add, Remove, indexación, búsqueda. ¿Lógico? Parece que sí.

\n

Pero los desarrolladores experimentados de C# le dirán: «Nunca heredes de List<T>». Esta pregunta en Stack Overflow ha obtenido miles de visitas, y no es sin razón. Detrás de la aparente simplicidad se esconden serios problemas con la arquitectura, el rendimiento y, lo más importante, con la violación de los principios de diseño orientado a objetos.

\n

En este artículo, echaremos un vistazo bajo el capó de List<T>, entenderemos por qué heredar de él es un antipatrón y qué usar en su lugar. ¿Listos? ¡Allá vamos!

\n\n

¿Qué tiene de malo List<T> desde el punto de vista del diseño?

\n

Comencemos con lo principal: List<T> no fue diseñado como una base para una jerarquía, sino como una implementación concreta y de alto rendimiento de un arreglo dinámico. Su API pública es un conjunto de métodos para trabajar con colecciones, no un contrato para la extensión.

\n

Observe su declaración en las fuentes de .NET (simplificada):

\n
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable\n{\n    private T[] _items;\n    private int _size;\n    \n    public void Add(T item) { /* ... */ }\n    public bool Remove(T item) { /* ... */ }\n    // ... docenas de otros métodos\n}
\n

Note que List<T> no está marcado como abstract. No proporciona métodos virtuales para sobrescribir (con la excepción de algunos, de los que hablaremos más adelante). Esto significa que no puede cambiar su comportamiento de forma segura. Si sobrescribe Add, corre el riesgo de romper la lógica interna que depende de este método.

\n\n

Problema n.º 1: Violación del Principio de Sustitución de Liskov (LSP)

\n

El Principio de Sustitución de Liskov dice: «Una clase derivada debe complementar, no reemplazar, el comportamiento de la clase base». Cuando hereda de List<T>, promete que su clase se puede usar en cualquier lugar donde se espere un List<T>. Pero esto rara vez se cumple.

\n

Ejemplo: crea una clase ReadOnlyLibrary : List<Book> y sobrescribe Add para que lance una excepción:

\n
public class ReadOnlyLibrary : List<Book>\n{\n    public new void Add(Book book)\n    {\n        throw new InvalidOperationException("¡Colección de solo lectura!");\n    }\n}\n\n// En algún lugar del código:\nList<Book> list = new ReadOnlyLibrary();\nlist.Add(new Book()); // ¡Ups! ¡No se lanzará ninguna excepción!\n// Porque new no sobrescribe, sino que oculta el método.\n// Se llamará a List.Add, no al nuestro.
\n

Incluso si usa override (y List.Add no es virtual), no podrá prohibir la adición: la interfaz IList no lo prevé. Simplemente romperá el contrato.

\n\n

Problema n.º 2: Violación de la encapsulación

\n

Al heredar de List<T>, expone toda su API pública hacia el exterior. Su clase Library obtendrá métodos como Sort, BinarySearch, Reverse, Insert, etc. Pero, ¿qué pasa si quiere que los libros estén siempre ordenados por autor? El usuario de su clase podrá llamar a library.Reverse() y romper la invariante.

\n

No podrá evitarlo sin reescribir todos los métodos, y hay docenas de ellos. Esto es una violación de la encapsulación: expone la implementación interna que no puede controlar.

\n\n

Echemos un vistazo bajo el capó: cómo funciona List<T> internamente

\n

Para ente

Blogs

Book Recommendations