Por que herdar de List<T> em C# é uma má ideia: análise

Treinador Online de Python para Iniciantes

Aprenda Python facilmente sem sobrecarga teórica. Resolva tarefas práticas com verificação automática e escreva código diretamente no navegador.

Start Course

Introdução: a armadilha em que muitos caíram

\n

Imagine a situação: você está escrevendo um aplicativo para uma biblioteca online. Você precisa de sua própria classe Library, que armazena livros, sabe ordená-los, pesquisá-los e exibir estatísticas. A primeira coisa que vem à mente é herdar de List<Book>. Afinal, ela já tem tudo: Add, Remove, indexação, pesquisa. Lógico? Parece que sim.

\n

Mas desenvolvedores C# experientes dirão a você: “Nunca herde de List<T>”. Esta pergunta no Stack Overflow acumulou milhares de visualizações, e não é à toa. Por trás da aparente simplicidade, escondem-se sérios problemas de arquitetura, desempenho e, o mais importante, de violação dos princípios de design orientado a objetos.

\n

Neste artigo, vamos olhar sob o capô do List<T>, entender por que herdar dele é um antipadrão e o que usar em seu lugar. Pronto? Vamos lá!

\n\n

O que há de errado com List<T> do ponto de vista de design?

\n

Vamos começar pelo principal: List<T> foi projetado não como uma base para hierarquia, mas como uma implementação concreta e de alto desempenho de um array dinâmico. Sua API pública é um conjunto de métodos para trabalhar com a coleção, e não um contrato para extensão.

\n

Veja sua declaração nos fontes do .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    // ... dezenas de outros métodos\n}
\n

Observe: List<T> não está marcado como abstract. Ele não fornece métodos virtuais para sobrescrever (com exceção de alguns, sobre os quais falaremos depois). Isso significa que você não pode alterar seu comportamento com segurança. Se você sobrescrever Add, corre o risco de quebrar a lógica interna que depende desse método.

\n\n

Problema nº1: Violação do Princípio de Substituição de Liskov (LSP)

\n

O Princípio de Substituição de Liskov afirma: “Uma classe derivada deve complementar, e não substituir, o comportamento da classe base”. Quando você herda de List<T>, você promete que sua classe pode ser usada em qualquer lugar onde se espera um List<T>. Mas isso raramente se cumpre.

\n

Exemplo: você cria a classe ReadOnlyLibrary : List<Book> e sobrescreve Add para lançar uma exceção:

\n
public class ReadOnlyLibrary : List<Book>\n{\n    public new void Add(Book book)\n    {\n        throw new InvalidOperationException("Coleção somente leitura!");\n    }\n}\n\n// Em algum lugar no código:\nList<Book> list = new ReadOnlyLibrary();\nlist.Add(new Book()); // Opa! Nenhuma exceção será lançada!\n// Porque new não sobrescreve, apenas oculta o método.\n// Será chamado List.Add, e não o nosso.
\n

Mesmo usando override (e List.Add não é virtual), você não conseguirá proibir a adição — a interface IList não prevê isso. Você simplesmente quebrará o contrato.

\n\n

Problema nº2: Violação do encapsulamento

\n

Ao herdar de List<T>, você expõe toda a sua API pública para fora. Sua classe Library ganhará métodos como Sort, BinarySearch, Reverse, Insert, etc. Mas e se você quiser que os livros estejam sempre ordenados por autor? O usuário da sua classe poderá chamar library.Reverse() e quebrar o invariante.

\n

Você não conseguirá evitar isso sem reescrever todos os métodos — e são dezenas deles. Isso é uma violação do encapsulamento: você expõe para fora a implementação interna que não consegue controlar.

\n\n

Olhando sob o capô: como List<T> funciona internamente

\n

Para ente

Blogs

Book Recommendations