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.

NotaExistem 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 :

  1. GetLista() que retorna uma coleção interna definida como uma List<int>;
  2. Exibir() para exibir o conteúdo da lista interna no console;

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:


José Carlos Macoratti