ReadOnlyCollection ou IEnumerable para expor coleções de membros?

124

Existe algum motivo para expor uma coleção interna como um ReadOnlyCollection em vez de um IEnumerable se o código de chamada iterar apenas sobre a coleção?

class Bar
{
    private ICollection<Foo> foos;

    // Which one is to be preferred?
    public IEnumerable<Foo> Foos { ... }
    public ReadOnlyCollection<Foo> Foos { ... }
}


// Calling code:

foreach (var f in bar.Foos)
    DoSomething(f);

A meu ver, IEnumerable é um subconjunto da interface do ReadOnlyCollection e não permite que o usuário modifique a coleção. Portanto, se a interface IEnumberable for suficiente, essa é a única a ser usada. Essa é uma maneira adequada de argumentar sobre isso ou estou perdendo alguma coisa?

Obrigado / Erik

Erik Öjebo
fonte
6
Se você estiver usando o .NET 4.5, convém tentar as novas interfaces de coleção somente leitura . Você ainda desejará agrupar a coleção retornada em um ReadOnlyCollectionse for paranóico, mas agora não está vinculado a uma implementação específica.
StriplingWarrior

Respostas:

96

Solução mais moderna

A menos que você precise que a coleção interna seja mutável, você pode usar o System.Collections.Immutablepacote, alterar seu tipo de campo para ser uma coleção imutável e expô-la diretamente - assumindo que Fooela é imutável, é claro.

Resposta atualizada para responder à pergunta mais diretamente

Existe algum motivo para expor uma coleção interna como um ReadOnlyCollection em vez de um IEnumerable se o código de chamada iterar apenas sobre a coleção?

Depende de quanto você confia no código de chamada. Se você tiver controle total sobre tudo o que chamará esse membro e garantir que nenhum código será usado:

ICollection<Foo> evil = (ICollection<Foo>) bar.Foos;
evil.Add(...);

então, certamente, nenhum dano será causado se você apenas devolver a coleção diretamente. Eu geralmente tento ser um pouco mais paranóico do que isso.

Da mesma forma, como você diz: se você só precisa IEnumerable<T> , por que se amarrar a algo mais forte?

Resposta original

Se você estiver usando o .NET 3.5, evite fazer uma cópia e evitar a conversão simples usando uma chamada simples para Ignorar:

public IEnumerable<Foo> Foos {
    get { return foos.Skip(0); }
}

(Existem muitas outras opções para agrupar trivialmente - o Skipmais interessante de Selecionar / Onde é que não há delegado para executar sem sentido para cada iteração.)

Se você não estiver usando o .NET 3.5, você pode escrever um invólucro muito simples para fazer a mesma coisa:

public static IEnumerable<T> Wrapper<T>(IEnumerable<T> source)
{
    foreach (T element in source)
    {
        yield return element;
    }
}
Jon Skeet
fonte
15
Note-se que há uma perda de desempenho para este: AFAIK o método Enumerable.Count é optimizado para Colecções fundidas em IEnumerables, mas não para a varia produzidos por Ir (0)
shojtsy
6
-1 Sou só eu ou isso é uma resposta para uma pergunta diferente? É útil conhecer essa "solução", mas o op pediu uma comparação entre retornar um ReadOnlyCollection e IEnumerable. Esta resposta já pressupõe que você deseja retornar IEnumerable sem nenhum motivo para apoiar essa decisão.
Zaid Masud
1
A resposta mostra a conversão de um ReadOnlyCollectionpara um ICollectione a execução com Addêxito. Tanto quanto sei, isso não deveria ser possível. O evil.Add(...)deve lançar um erro. dotnetfiddle.net/GII2jW
Shaun Luttin
1
@ ShaunLuttin: Sim, exatamente - isso joga. Mas na minha resposta, eu não lancei um ReadOnlyCollection- lancei um IEnumerable<T>que na verdade não é somente leitura ... é em vez de expor umReadOnlyCollection
Jon Skeet
1
Aha. Sua paranóia levaria você a usar um em ReadOnlyCollectionvez de um IEnumerable, a fim de proteger contra o elenco maligno.
Shaun Luttin 26/10/2015
43

Se você apenas precisar percorrer a coleção:

foreach (Foo f in bar.Foos)

devolver IEnumerable é suficiente.

Se você precisar de acesso aleatório aos itens:

Foo f = bar.Foos[17];

Em seguida, envolva-o em ReadOnlyCollection .

Vojislav Stojkovic
fonte
@Vojislav Stojkovic, +1. Esse conselho ainda se aplica hoje?
w0051977
1
@ w0051977 Depende da sua intenção. Usar System.Collections.Immutablecomo Jon Skeet recomenda é perfeitamente válido quando você expõe algo que nunca vai mudar depois de obter uma referência a ele. Por outro lado, se você estiver expondo uma visão somente leitura de uma coleção mutável, esse conselho ainda será válido, embora eu provavelmente a exponha como IReadOnlyCollection(que não existia quando escrevi isso).
Vojislav Stojkovic
@VojislavStojkovic você não pode usar bar.Foos[17]com IReadOnlyCollection. A única diferença é Countpropriedade adicional . public interface IReadOnlyCollection<out T> : IEnumerable<T>, IEnumerable
219 Konrad
@ Konrad Certo, se você precisar bar.Foos[17], tem que expô-lo como ReadOnlyCollection<Foo>. Se você deseja saber o tamanho e iterar através dele, exponha-o como IReadOnlyCollection<Foo>. Se você não precisa saber o tamanho, use IEnumerable<Foo>. Existem outras soluções e outros detalhes, mas está além do escopo da discussão dessa resposta específica.
Vojislav Stojkovic
31

Se você fizer isso, nada impedirá que os chamadores convertam o IEnumerable de volta ao ICollection e o modifiquem. O ReadOnlyCollection remove essa possibilidade, embora ainda seja possível acessar a coleção gravável subjacente por meio de reflexão. Se a coleção for pequena, uma maneira fácil e segura de contornar esse problema é retornar uma cópia.

Stu Mackellar
fonte
14
Esse é o tipo de "design paranóico" com o qual discordo totalmente. Eu acho que é uma má prática escolher semântica inadequada para impor uma política.
Vojislav Stojkovic
24
Você discordar não faz errado. Se estou projetando uma biblioteca para distribuição externa, prefiro ter uma interface paranóica do que lidar com bugs gerados por usuários que tentam abusar da API. Consulte as diretrizes de design do C # em msdn.microsoft.com/en-us/library/k2604h5s(VS.71).aspx
Stu Mackellar
5
Sim, no caso específico de projetar uma biblioteca para distribuição externa, faz sentido ter uma interface paranóica para o que você estiver expondo. Mesmo assim, se a semântica da propriedade Foos for acesso seqüencial, use ReadOnlyCollection e, em seguida, retorne IEnumerable. Não há necessidade de usar semânticas erradas;)
Vojislav Stojkovic
9
Você também não precisa fazer. Você pode retornar um read-only iterador sem copiar ...
Jon Skeet
1
@shojtsy Sua linha de código parece não funcionar. como gera um nulo . Um elenco lança uma exceção.
Nick Alexeev
3

Evito usar o ReadOnlyCollection, tanto quanto possível, é realmente consideravelmente mais lento do que apenas usar uma lista normal. Veja este exemplo:

List<int> intList = new List<int>();
        //Use a ReadOnlyCollection around the List
        System.Collections.ObjectModel.ReadOnlyCollection<int> mValue = new System.Collections.ObjectModel.ReadOnlyCollection<int>(intList);

        for (int i = 0; i < 100000000; i++)
        {
            intList.Add(i);
        }
        long result = 0;

        //Use normal foreach on the ReadOnlyCollection
        TimeSpan lStart = new TimeSpan(System.DateTime.Now.Ticks);
        foreach (int i in mValue)
            result += i;
        TimeSpan lEnd = new TimeSpan(System.DateTime.Now.Ticks);
        MessageBox.Show("Speed(ms): " + (lEnd.TotalMilliseconds - lStart.TotalMilliseconds).ToString());
        MessageBox.Show("Result: " + result.ToString());

        //use <list>.ForEach
        lStart = new TimeSpan(System.DateTime.Now.Ticks);
        result = 0;
        intList.ForEach(delegate(int i) { result += i; });
        lEnd = new TimeSpan(System.DateTime.Now.Ticks);
        MessageBox.Show("Speed(ms): " + (lEnd.TotalMilliseconds - lStart.TotalMilliseconds).ToString());
        MessageBox.Show("Result: " + result.ToString());
James Madison
fonte
15
Otimização prematura. Pelas minhas medidas, a iteração em um ReadOnlyCollection leva cerca de 36% mais tempo que uma lista regular, supondo que você não faça nada dentro do loop for. As chances são de que você nunca notará essa diferença, mas se esse é um hot spot do seu código e requer todo o desempenho que você puder obter, por que não usar uma matriz? Isso seria mais rápido ainda.
StriplingWarrior
Seu benchmark contém falhas, você está ignorando completamente a previsão de ramificações.
Trojaner 15/02/19
0

Às vezes, você pode querer usar uma interface, talvez porque queira zombar da coleção durante o teste de unidade. Consulte a entrada do meu blog para adicionar sua própria interface ao ReadonlyCollection usando um adaptador.


fonte