C# - IEnumerable<T> ou IReadOnlyList<T>
Hoje veremos uma comparação entre IEnumerable<T> e IReadOnlyList<T> e qual delas devemos retornar. |
Existe um dilema
comum que os desenvolvedores enfrentam ao criar métodos que retornam coleções
que é escolher o tipo de retorno correto dentre as coleções disponíveis.
Como existem muitas coleções disponíveis este artigo vai focar na escolha entre as coleções IEnumerable<T> e IReadOnlyList<T> mostrando o impacto que essa escolha pode ter.
Vamos começar entendendo por que a preferência por IEnumerable<T> ou IReadOnlyList<T> ao retornar uma coleção de um método, especialmente quando se deseja que tal coleção seja imutável.
Nota: Existem outras opções como IReadOnlyCollection<T> e IReadOnlyDictionary<TKey,TValue>, mas para simplificar, vamos nos restringir a IReadOnlyList<T>.
As definições destas duas interfaces são:
Vamos agora definir o seguinte código para ilustrar o problema :
Teste teste =
new(); teste.Exibir(); var lista = teste.GetLista(); lista.Add(10); teste.Exibir(); Console.ReadKey(); public class Teste{ private IList<int> _colecaoInterna; public Teste() { _colecaoInterna = new List<int> {1,2,3,4}; } public IList<int> GetLista() => _colecaoInterna; public void Exibir() => Console.WriteLine($"Lista => [{string.Join(", ",_colecaoInterna)}]"); } |
Definimos uma classe Teste, que expõe dois métodos :
No código que usa
a classe Teste estamos criando uma instância da
classe e usando o método Exibir().
Depois disso, recuperamos a coleção usando o método
GetLista() e adicionamos um novo item à lista recebida exibindo o
conteúdo novamente via método Exibir().
Ao executar o projeto teremos o seguinte resultado :
Lista => [1,2,3,4] //
Antes de adicionar elemento no código do cliente
Lista => [1,2,3,4,10] // Após adicionar o elemento
no código do cliente
Note que não expomos intencionalmente nossa coleção interna fora da classe
Teste.
No entanto, como estamos retornando uma coleção mutável List<T> usando o método GetLista(), ela expõe involuntariamente nossa coleção interna para corrupção devido ao consumo de código.
Conforme demonstrado, quem for consumir o código agora está livre para modificar a coleção interna, o que pode não ser exatamente o que desejamos que aconteça.
As interfaces IEnumerable<T> e IReadOnlyList<T> são as únicas interfaces em .NET que permitem que você declare sua intenção antecipadamente e sinalize ao cliente de seu código que uma coleção não pode ser alterada.
Esta seria a primeira razão pela qual os programadores gostam de usar IEnumerable e IReadOnlyList. Elas ajudam a "selar" as coleções, marcando-as como imutáveis. A segunda razão é o Princípio OCP ou Aberto-Fechado. A programação para uma interface permite que você troque facilmente as coleções subjacentes, desde que implementem a mesma interface. Tudo isso sem alterar nenhum código do cliente.
Então qual delas devemos usar ?
A interface
IEnumerable<T> promete ao código de consumo buscar os itens à medida que
são acessados (avaliação preguiçosa). Essa não seria uma escolha ideal se as
coleções precisassem ser iteradas várias vezes.
Nesses cenários, o código de consumo teria que armazenar os resultados
internamente, materializando os resultados usando ToList()
ou ToArray(). Além disso, se for esperado que o código de consumo acesse
os resultados usando o índice, pode ser uma escolha melhor retornar o
resultado como IReadOnlyList<T>.
No entanto, por outro lado, se você espera que o código de consumo pare de avaliar a coleção prematuramente (pode não exigir a coleção inteira), IEnumerable<T> pode ser uma ótima opção. Mas é preciso ter cuidado com certas limitações de IEnumerable<T>.
Nota: Apenas retornar IReadOnlyList ou IEnumerable não garante que a coleção subjacente seja imutável. Se o cliente souber qual tipo de coleção está realmente sendo retornado, ele pode implementar um hack como este:
IReadOnlyList<string> nomes = ExtrairNomes(data);
var nomesMutaveis = (List<string>)nomes;
|
E ainda alterar essa coleção. Isso, no entanto, é uma violação grosseira de um princípio OOP básico: você não deve supor nada mais sobre um objeto do que o que seu tipo está lhe dizendo. Tem uma interface de coleção somente leitura? Lide com isso de forma adequada ou então tenha problemas de compatibilidade quando o autor do método decidir alterar o tipo da coleção subjacente. (retirado de : https://enterprisecraftsmanship.com/posts/ienumerable-vs-ireadonlylist/ )
Voltando ao nosso
exemplo, vamos reescrever o método GetLista() agora
retornando um IEnumerable<int>.
private IList<int> _internalCollection;
public IEnumerable<int> GetLista() =>
_internalCollection;
Ao fazer isso o compilador lançara um erro no código de consumo, reclamando que
'IEnumerable<int>' não contém uma definição para
'Add'.
Além disso, como IEnumerable<T> não pode ser acessado via índice, os itens também não podem ser substituídos.
Mas isso o torna verdadeiramente imutável ?
Bem, pense
novamente.
Se o código consumidor estiver ciente da coleção subjacente (neste caso,
List<int>), ele pode literalmente invadir a
coleção.
Vamos reescrever o código que consume a classe Teste para mostrar isso:
Teste teste =
new(); teste.Exibir(); var lista = (List<int>)teste.GetLista(); lista.Add(10); teste.Exibir(); Console.ReadKey(); |
Observe como
convertemos o IEnumerable<int> retornado pelo
método GetLista() para a coleção subjacente.
Isso nos permite ainda modificar a coleção interna, o que novamente é
indesejável.
O resultado obtido ao executar o código será o mesmo que o anterior.
Pois é aqui que a interface IReadOnlyList() (ou IReadOnlyCollection<T>) realmente entra em jogo.
Ela informa ao código de consumo que a coleção é realmente imutável e proíbe modificações.
Se você precisa de uma coleção verdadeiramente imutável, sem o risco de consumir código sabendo e usando o tipo de coleção subjacente para modificar sua coleção, escolha a interface IReadOnlyList<T> (ou IReadOnlyCollection<T>)
No entanto, se você deseja que a coleção não seja realmente armazenada em cache e seja executada de forma adiada (deferred execution), você precisa escolher IEnumerarble<T> e produzir os resultados, pois o código de consumo requer dados na coleção.
Aplicando uma regra mais liberal e geral , podemos dizer que ambas as interfaces de coleção são úteis, assim, prefira IEnumerable ao aceitar uma coleção e prefira IReadOnlyList ao retornar uma.
E estamos conversados...
"Então disse Jesus aos doze: Quereis vós também
retirar-vos?
Respondeu-lhe, pois, Simão Pedro: Senhor, para quem iremos nós?
Tu tens as palavras da vida eterna."
João 6:67-68
Referências:
NET - Unit of Work - Padrão Unidade de ...