Best practice Restituzione di oggetto di sola lettura


11

Ho una domanda sulle "migliori pratiche" su OOP in C # (ma in qualche modo si applica a tutte le lingue).

Prendi in considerazione la possibilità di avere una classe di libreria con un oggetto che deve essere esposto al pubblico, ad esempio tramite l'accesso alla proprietà, ma non vogliamo che il pubblico (le persone che usano questa classe di libreria) lo cambi.

class A
{
    // Note: List is just example, I am interested in objects in general.
    private List<string> _items = new List<string>() { "hello" }

    public List<string> Items
    {
        get
        {
            // Option A (not read-only), can be modified from outside:
            return _items;

            // Option B (sort-of read-only):
            return new List<string>( _items );

            // Option C, must change return type to ReadOnlyCollection<string>
            return new ReadOnlyCollection<string>( _items );
        }
    }
}

Ovviamente l'approccio migliore è "opzione C", ma pochissimi oggetti hanno la variante ReadOnly (e certamente nessuna classe definita dall'utente ce l'ha).

Se tu fossi l'utente di classe A, ti aspetteresti modifiche a

List<string> someList = ( new A() ).Items;

propagarsi all'oggetto originale (A)? O va bene restituire un clone purché sia ​​stato scritto in commenti / documentazione? Penso che questo approccio al clone potrebbe portare a bug piuttosto difficili da rintracciare.

Ricordo che in C ++ potevamo restituire oggetti const e potevi chiamare solo metodi contrassegnati come const su di esso. Immagino che non ci sia tale caratteristica / modello nel C #? In caso contrario, perché non dovrebbero includerlo? Credo che si chiama const correttezza.

Ma ancora una volta la mia domanda principale riguarda "cosa ti aspetteresti" o come gestire l'opzione A vs B.


2
Dai un'occhiata a questo articolo su questa anteprima di nuove raccolte immutabili per .NET 4.5: blogs.msdn.com/b/bclteam/archive/2012/12/18/… Puoi già ottenerle tramite NuGet.
Kristof Claes,

Risposte:


10

Oltre alla risposta di @ Jesse, che è la migliore soluzione per le collezioni: l'idea generale è quella di progettare la tua interfaccia pubblica di A in modo che ritorni solo

  • tipi di valore (come int, double, struct)

  • oggetti immutabili (come "stringa" o classi definite dall'utente che offrono solo metodi di sola lettura, proprio come la tua classe A)

  • (solo se necessario): copie dell'oggetto, come dimostra la tua "Opzione B"

IEnumerable<immutable_type_here> è immutabile da solo, quindi questo è solo un caso speciale di quanto sopra.


19

Si noti inoltre che si dovrebbe programmare su interfacce e, in generale, ciò che si desidera restituire da quella proprietà è un IEnumerable<string>. A List<string>è un po 'no-no perché è generalmente mutevole e andrò così lontano a dire che ReadOnlyCollection<string>come un implementatore di ICollection<string>sentirsi come se violasse il Principio di Sostituzione di Liskov.


6
+1, usare un IEnumerable<string>è esattamente ciò che si desidera qui.
Doc Brown,

2
In .Net 4.5, puoi anche usare IReadOnlyList<T>.
svick,

3
Nota per le altre parti interessate. Quando si considera l'accesso alla proprietà: se si restituisce semplicemente IEnumerable <stringa> invece di Elenco <stringa> restituendo _list (+ cast implicito in IEnumerable), questo elenco può essere ricondotto in Elenco <stringa> e modificato e le modifiche verranno propagarsi nell'oggetto principale. È possibile restituire il nuovo Elenco <stringa> (_list) (+ successivo cast implicito a IEnumerable) che proteggerà l'elenco interno. Maggiori informazioni su questo può essere trovato qui: stackoverflow.com/questions/4056013/...
NeverStopLearning

1
È meglio richiedere che tutti i destinatari di una raccolta che devono accedervi tramite indice debbano essere preparati a copiare i dati in un tipo che lo faciliti, oppure è meglio richiedere che il codice che restituisce la raccolta li fornisca in una forma che è accessibile per indice? A meno che il fornitore non lo abbia probabilmente in una forma che non faciliti l'accesso per indice, considererei che il valore di liberare i destinatari dalla necessità di fare la propria copia ridondante eccederebbe il valore di liberare il fornitore dalla necessità di fornire un modulo ad accesso casuale (ad esempio di sola lettura IList<T>)
supercat

2
Una cosa che non mi piace di IEnumerable è che non sai mai se funziona come una routine o se è solo un oggetto dati. Se funziona come un coroutine, potrebbe causare bug sottili, quindi è bene saperlo a volte. Questo è il motivo per cui tendo a preferire ReadOnlyCollection o IReadOnlyList ecc.
Steve Vermeulen,

3

Ho riscontrato un problema simile un po 'indietro e sotto era la mia soluzione, ma funziona davvero solo se controlli anche la libreria:


Inizia con la tua lezione A

public class A
{
    private int _n;
    public int N
    {
        get { return _n; }
        set { _n = value; }
    }
}

Quindi ricava un'interfaccia da questo con solo la porzione get delle proprietà (e tutti i metodi di lettura) e disponi di un attrezzo che

public interface IReadOnlyA
{
    public int N { get; }
}
public class A : IReadOnlyA
{
    private int _n;
    public int N
    {
        get { return _n; }
        set { _n = value; }
    }
}

Quindi, naturalmente, quando si desidera utilizzare la versione di sola lettura, è sufficiente un semplice cast. Questo è proprio ciò che la soluzione C è, in realtà, ma ho pensato di offrire il metodo con cui l'ho raggiunto


2
Il problema è che è possibile riportarlo alla versione mutabile. E la sua violazione di LSP, in quanto lo stato osservabile attraverso i metodi della classe base (in questo caso l'interfaccia) può essere modificato con nuovi metodi dalla sottoclasse. Ma almeno comunica l'intento, anche se non lo impone.
user470365

1

Per chiunque trovi questa domanda e sia comunque interessato alla risposta, ora c'è un'altra opzione che sta esponendo ImmutableArray<T>. Questi sono alcuni punti per cui penso che sia meglio allora IEnumerable<T>o ReadOnlyCollection<T>:

  • afferma chiaramente che è immutabile (API espressiva)
  • afferma chiaramente che la raccolta è già formata (in altre parole non è inizializzata pigramente ed è OK per iterarla più volte)
  • se il conteggio degli oggetti è noto, non nasconde quel knowlegde come IEnumerable<T>fa
  • poiché il cliente non sa quale sia l'implementazione IEnumerableo IReadOnlyCollectnon può presumere che non cambi mai (in altre parole, non vi è alcuna garanzia che gli articoli di una collezione non siano stati cambiati mentre il riferimento a quella collezione rimane invariato)

Inoltre, se l'APi dovesse notificare al cliente eventuali cambiamenti nella raccolta, esponerei un evento come membro separato.

Si noti che non sto dicendo che IEnumerable<T>è male in generale. Lo userei ancora quando si passa la raccolta come argomento o si restituisce una raccolta inizializzata pigramente (in questo caso particolare ha senso nascondere il conteggio degli oggetti perché non è ancora noto).

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.