ReadOnlyCollection o IEnumerable per esporre raccolte di membri?


124

C'è qualche motivo per esporre una raccolta interna come ReadOnlyCollection anziché IEnumerable se il codice chiamante scorre solo sulla raccolta?

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 mio avviso, IEnumerable è un sottoinsieme dell'interfaccia di ReadOnlyCollection e non consente all'utente di modificare la raccolta. Quindi, se l'interfaccia IEnumberable è sufficiente, quella è quella da usare. È un modo corretto di ragionare al riguardo o mi sto perdendo qualcosa?

Grazie / Erik


6
Se si utilizza .NET 4.5, è possibile provare le nuove interfacce di raccolta di sola lettura . Avrai comunque bisogno di avvolgere la raccolta restituita in a ReadOnlyCollectionse sei paranoico, ma ora non sei legato a un'implementazione specifica.
StriplingWarrior,

Risposte:


96

Soluzione più moderna

A meno che non sia necessario che la raccolta interna sia mutabile, è possibile utilizzare il System.Collections.Immutablepacchetto, modificare il tipo di campo in una raccolta immutabile e quindi esporlo direttamente - presupponendo che Foosia immutabile, ovviamente.

Risposta aggiornata per rispondere alla domanda in modo più diretto

C'è qualche motivo per esporre una raccolta interna come ReadOnlyCollection anziché IEnumerable se il codice chiamante scorre solo sulla raccolta?

Dipende da quanto ti fidi del codice chiamante. Se hai il controllo completo su tutto ciò che chiamerà mai questo membro e garantisci che nessun codice verrà mai utilizzato:

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

quindi certo, non verrà fatto alcun danno se si restituisce direttamente la raccolta. In genere cerco di essere un po 'più paranoico di quello.

Allo stesso modo, come dici: se ne hai solo bisogno IEnumerable<T> , perché legarti a qualcosa di più forte?

Risposta originale

Se stai usando .NET 3.5, puoi evitare di fare una copia ed evitare il cast semplice usando una semplice chiamata a Skip:

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

(Esistono molte altre opzioni per Skipeseguire il wrapping banalmente: la cosa bella di oltre Select / Where è che non c'è delegato da eseguire inutilmente per ogni iterazione.)

Se non stai utilizzando .NET 3.5 puoi scrivere un wrapper molto semplice per fare la stessa cosa:

public static IEnumerable<T> Wrapper<T>(IEnumerable<T> source)
{
    foreach (T element in source)
    {
        yield return element;
    }
}

15
Nota che c'è una penalità prestazionale per questo: AFAIK il metodo Enumerable.Count è ottimizzato per le raccolte lanciate in IEnumerables, ma non per gli intervalli prodotti da Skip (0)
shojtsy,

6
-1 Sono solo io o questa è una risposta a una domanda diversa? È utile conoscere questa "soluzione", ma l'operazione ha richiesto un confronto tra la restituzione di ReadOnlyCollection e IEnumerable. Questa risposta presuppone già che si desidera restituire IEnumerable senza alcun ragionamento a supporto di tale decisione.
Zaid Masud,

1
La risposta mostra il cast a ReadOnlyCollectiona ICollectione quindi l'esecuzione Addcorretta. Per quanto ne so, ciò non dovrebbe essere possibile. Il evil.Add(...)dovrebbe generare un errore. dotnetfiddle.net/GII2jW
Shaun Luttin,

1
@ShaunLuttin: Sì, esattamente - che lancia. Ma nella mia risposta, non lancio un ReadOnlyCollection- Ne lancio uno IEnumerable<T>che in realtà non è di sola lettura ... è invece di esporre unReadOnlyCollection
Jon Skeet,

1
Aha. La tua paranoia ti porterebbe a usare un ReadOnlyCollectioninvece di un IEnumerable, al fine di proteggere dal malefico lancio.
Shaun Luttin,

43

Se hai solo bisogno di scorrere la raccolta:

foreach (Foo f in bar.Foos)

quindi è sufficiente restituire IEnumerable .

Se hai bisogno di un accesso casuale agli oggetti:

Foo f = bar.Foos[17];

quindi avvolgilo in ReadOnlyCollection .


@Vojislav Stojkovic, +1. Questo consiglio vale ancora oggi?
w0051977,

1
@ w0051977 Dipende dalle tue intenzioni. L'uso System.Collections.Immutablecome raccomandato da Jon Skeet è perfettamente valido quando si espone qualcosa che non cambierà mai dopo aver ricevuto un riferimento ad esso. D'altra parte, se stai esponendo una vista di sola lettura di una collezione mutabile, allora questo consiglio è ancora valido, anche se probabilmente lo esporrei come IReadOnlyCollection(che non esisteva quando ho scritto questo).
Vojislav Stojkovic,

@VojislavStojkovic non è possibile utilizzare bar.Foos[17]con IReadOnlyCollection. L'unica differenza è la Countproprietà aggiuntiva . public interface IReadOnlyCollection<out T> : IEnumerable<T>, IEnumerable
Konrad,

@Konrad Giusto, se necessario bar.Foos[17], devi esporlo come ReadOnlyCollection<Foo>. Se vuoi conoscere la dimensione e scorrere attraverso di essa, esponila come IReadOnlyCollection<Foo>. Se non è necessario conoscere le dimensioni, utilizzare IEnumerable<Foo>. Ci sono altre soluzioni e altri dettagli, ma va oltre lo scopo della discussione di questa particolare risposta.
Vojislav Stojkovic,

31

Se lo fai, non c'è niente che impedisce ai chiamanti di riportare IEnumerable su ICollection e di modificarlo. ReadOnlyCollection rimuove questa possibilità, sebbene sia ancora possibile accedere alla raccolta scrivibile sottostante tramite la riflessione. Se la raccolta è piccola, un modo semplice e sicuro per aggirare questo problema è restituire una copia.


14
Questo è il tipo di "design paranoico" con cui sono profondamente in disaccordo. Penso che sia una cattiva pratica scegliere una semantica inadeguata per applicare una politica.
Vojislav Stojkovic,

24
Sei in disaccordo non lo rende sbagliato. Se sto progettando una libreria per la distribuzione esterna, preferirei piuttosto avere un'interfaccia paranoica piuttosto che occuparmi dei bug sollevati dagli utenti che tentano di abusare dell'API. Vedi le linee guida per la progettazione di C # su msdn.microsoft.com/en-us/library/k2604h5s(VS.71).aspx
Stu Mackellar,

5
Sì, nel caso specifico della progettazione di una libreria per la distribuzione esterna, ha senso avere un'interfaccia paranoica per qualsiasi cosa tu stia esponendo. Tuttavia, se la semantica della proprietà Foos è un accesso sequenziale, utilizzare ReadOnlyCollection e quindi restituire IEnumerable. Non c'è bisogno di usare una semantica sbagliata;)
Vojislav Stojkovic,

9
Non è necessario farlo neanche. Puoi restituire un iteratore di sola lettura senza copiare ...
Jon Skeet,

1
@shojtsy La tua riga di codice non sembra funzionare. come genera un null . Un cast genera un'eccezione.
Nick Alexeev,

3

Evito di utilizzare ReadOnlyCollection il più possibile, in realtà è considerevolmente più lento di un semplice elenco. Vedi questo esempio:

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());

15
Ottimizzazione prematura. Secondo le mie misurazioni, l'iterazione su ReadOnlyCollection richiede circa il 36% in più rispetto a un elenco normale, supponendo che non si faccia nulla all'interno del ciclo for. È probabile che non noterai mai questa differenza, ma se questo è un punto critico nel tuo codice e richiede ogni minimo bit di prestazioni che puoi spremere da esso, perché non utilizzare invece un array? Sarebbe ancora più veloce.
StriplingWarrior,

Il tuo benchmark contiene difetti, stai ignorando completamente la previsione del ramo.
Trojaner,

0

A volte potresti voler usare un'interfaccia, forse perché vuoi deridere la raccolta durante i test delle unità. Si prega di consultare il mio blog per l'aggiunta della propria interfaccia a ReadonlyCollection utilizzando un adattatore.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.