Domanda Buona o cattiva pratica? Inizializzazione di oggetti in getter


Ho una strana abitudine, sembra ... almeno secondo il mio collega. Abbiamo lavorato insieme su un piccolo progetto. Il modo in cui ho scritto le classi è (esempio semplificato):

[Serializable()]
public class Foo
{
    public Foo()
    { }

    private Bar _bar;

    public Bar Bar
    {
        get
        {
            if (_bar == null)
                _bar = new Bar();

            return _bar;
        }
        set { _bar = value; }
    }
}

Quindi, fondamentalmente, inizializzo solo qualsiasi campo quando viene chiamato un getter e il campo è ancora nullo. Ho pensato che questo avrebbe ridotto il sovraccarico non inizializzando alcuna proprietà che non sono utilizzate da nessuna parte.

ETA: La ragione per cui l'ho fatto è che la mia classe ha diverse proprietà che restituiscono un'istanza di un'altra classe, che a sua volta ha anche proprietà con ancora più classi e così via. Chiamare il costruttore per la classe superiore in seguito chiamerebbe tutti i costruttori per tutte queste classi, quando non lo sono sempre tutto necessario.

Ci sono obiezioni contro questa pratica, a parte le preferenze personali?

AGGIORNAMENTO: Ho preso in considerazione le molte opinioni divergenti riguardo a questa domanda e mi schiererò dalla mia risposta accettata. Tuttavia, ora ho una comprensione molto migliore del concetto e sono in grado di decidere quando usarlo e quando no.

Contro:

  • Problemi di sicurezza del filo
  • Non obbedire a una richiesta "setter" quando il valore passato è nullo
  • Micro-ottimizzazioni
  • La gestione delle eccezioni dovrebbe aver luogo in un costruttore
  • È necessario verificare la presenza di null nel codice della classe

Professionisti:

  • Micro-ottimizzazioni
  • Le proprietà non restituiscono mai null
  • Ritarda o evita di caricare oggetti "pesanti"

La maggior parte dei contro non sono applicabili alla mia libreria corrente, tuttavia dovrei testare per vedere se le "micro-ottimizzazioni" stanno effettivamente ottimizzando qualcosa.

ULTIMO AGGIORNAMENTO:

Ok, ho cambiato la mia risposta. La mia domanda iniziale era se questa fosse una buona abitudine. E ora sono convinto che non lo è. Forse lo userò ancora in alcune parti del mio codice attuale, ma non incondizionatamente e sicuramente non sempre. Quindi ho intenzione di perdere la mia abitudine e pensarci prima di usarla. Grazie a tutti!


164
2018-02-08 13:48


origine


risposte:


Quello che hai qui è un - ingenuo - implementazione di "inizializzazione pigra".

Risposta breve:

Utilizzo dell'inizializzazione pigra incondizionatamente non è una buona idea Ha i suoi posti, ma bisogna prendere in considerazione gli impatti di questa soluzione.

Background e spiegazione:

Implementazione concreta:
Diamo prima un'occhiata al tuo campione concreto e perché considero la sua implementazione ingenua:

  1. Viola il Principio di Least Surprise (POLS). Quando un valore viene assegnato a una proprietà, è previsto che questo valore venga restituito. Nella tua implementazione questo non è il caso null:

    foo.Bar = null;
    Assert.Null(foo.Bar); // This will fail
    
  2. Presenta alcuni problemi di threading: due chiamanti di foo.Bar su thread diversi possono potenzialmente ottenere due diverse istanze di Bar e uno di loro sarà senza connessione con il Foo esempio. Qualsiasi modifica apportata a questo Bar l'istanza è silenziosamente persa.
    Questo è un altro caso di violazione di POLS. Quando si accede solo al valore memorizzato di una proprietà, ci si aspetta che sia sicuro per i thread. Mentre si potrebbe obiettare che la classe semplicemente non è thread-safe - incluso il getter della proprietà - si dovrebbe documentarlo correttamente in quanto non è il caso normale. Inoltre, l'introduzione di questo problema non è necessaria come vedremo tra breve.

In generale:
È ora di guardare all'inizializzazione pigra in generale:
L'inizializzazione pigra viene solitamente utilizzata per ritardare la costruzione di oggetti questo richiede molto tempo per essere costruito o che richiede molta memoria una volta completamente costruito.
Questo è un motivo molto valido per l'utilizzo dell'inizializzazione pigra.

Tuttavia, tali proprietà normalmente non hanno setter, il che elimina il primo problema sopra evidenziato.
Inoltre, sarebbe stata utilizzata un'implementazione thread-safe Lazy<T> - per evitare il secondo problema.

Anche considerando questi due punti nell'implementazione di una proprietà pigra, i seguenti punti sono problemi generali di questo modello:

  1. La costruzione dell'oggetto potrebbe non avere successo, risultando in un'eccezione da un getter di proprietà. Questa è un'altra violazione dei POLS e quindi dovrebbe essere evitata. Persino il sezione sulle proprietà nelle "Linee guida di progettazione per lo sviluppo di librerie di classi" afferma esplicitamente che i getter di proprietà non dovrebbero lanciare eccezioni:

    Evita di fare eccezioni ai getter della proprietà.

    I getter di proprietà dovrebbero essere semplici operazioni senza precondizioni. Se un getter potrebbe lanciare un'eccezione, considera la riprogettazione della proprietà come metodo.

  2. Ottimizzazioni automatiche da parte del compilatore sono ferite, vale a dire inlining e previsione dei rami. Perfavore guarda La risposta di Bill K per una spiegazione dettagliata.

La conclusione di questi punti è la seguente:
Per ogni singola proprietà implementata pigramente, dovresti aver preso in considerazione questi punti.
Ciò significa che si tratta di una decisione per ogni caso e non può essere considerata una best practice generale.

Questo modello ha il suo posto, ma non è una best practice generale quando si implementano le classi. Non dovrebbe essere usato incondizionatamente, per le ragioni sopra esposte.


In questa sezione voglio discutere alcuni dei punti che altri hanno avanzato come argomenti per l'utilizzo dell'inizializzazione pigra incondizionatamente:

  1. serializzazione:
    EricJ afferma in un commento:

    Un oggetto che può essere serializzato non avrà il suo contructor richiamato quando è deserializzato (dipende dal serializzatore, ma molti di quelli comuni si comportano in questo modo). Inserire il codice di inizializzazione nel costruttore significa che devi fornire supporto aggiuntivo per la deserializzazione. Questo modello evita quella codifica speciale.

    Ci sono diversi problemi con questo argomento:

    1. La maggior parte degli oggetti non verrà mai serializzata. Aggiungere una sorta di supporto per questo quando non è necessario violare YAGNI.
    2. Quando una classe ha bisogno di supportare la serializzazione esistono modi per abilitarlo senza una soluzione che non abbia nulla a che fare con la serializzazione a prima vista.
  2. Micro-ottimizzazione: L'argomento principale è che si desidera costruire gli oggetti solo quando qualcuno li accede effettivamente. Quindi stai effettivamente parlando dell'ottimizzazione dell'uso della memoria.
    Non sono d'accordo con questo argomento per i seguenti motivi:

    1. Nella maggior parte dei casi, alcuni oggetti in memoria non hanno alcun impatto su nulla. I computer moderni hanno abbastanza memoria. Questo è senza un caso di problemi reali confermati da un profiler ottimizzazione pre-matura e ci sono buone ragioni contro di esso.
    2. Riconosco il fatto che a volte questo tipo di ottimizzazione è giustificato. Ma anche in questi casi l'inizializzazione pigra non sembra essere la soluzione corretta. Ci sono due ragioni che parlano contro:

      1. L'inizializzazione pigra potenzialmente danneggia le prestazioni. Forse solo marginalmente, ma come ha mostrato la risposta di Bill, l'impatto è maggiore di quanto si possa pensare a prima vista. Quindi questo approccio tratta fondamentalmente le prestazioni rispetto alla memoria.
      2. Se si dispone di un design in cui è un caso di utilizzo comune utilizzare solo parti della classe, questo suggerisce un problema con il design stesso: la classe in questione ha più di una responsabilità. La soluzione sarebbe quella di dividere la classe in diverse classi più focalizzate.

168
2018-02-08 13:50



È una buona scelta di design. Fortemente consigliato per codice di libreria o classi principali.

Viene chiamato da alcune "inizializzazioni pigre" o "inizializzazione ritardata" ed è generalmente considerato da tutti una buona scelta di progettazione.

Innanzitutto, se si inizializza nella dichiarazione delle variabili a livello di classe o del costruttore, quando viene costruito l'oggetto, si ha il sovraccarico di creare una risorsa che non può mai essere utilizzata.

In secondo luogo, la risorsa viene creata solo se necessario.

In terzo luogo, si evita la raccolta di dati inutili che non è stata utilizzata.

Infine, è più semplice gestire le eccezioni di inizializzazione che possono verificarsi nella proprietà, quindi le eccezioni che si verificano durante l'inizializzazione delle variabili a livello di classe o del costruttore.

Ci sono delle eccezioni a questa regola.

Per quanto riguarda l'argomento prestazioni del controllo aggiuntivo per l'inizializzazione nella proprietà "get", è insignificante. L'inizializzazione e lo smaltimento di un oggetto rappresentano un colpo di performance più significativo rispetto a un semplice controllo del puntatore nullo con un salto.

Linee guida di progettazione per lo sviluppo di librerie di classi a http://msdn.microsoft.com/en-US/library/vstudio/ms229042.aspx

per quanto riguarda Lazy<T>

Il generico Lazy<T> la classe è stata creata esattamente per ciò che il manifesto vuole, vedi Inizializzazione pigra a http://msdn.microsoft.com/en-us/library/dd997286(v=vs.100).aspx. Se hai versioni precedenti di .NET, devi usare il modello di codice illustrato nella domanda. Questo modello di codice è diventato così comune che Microsoft ha ritenuto opportuno includere una classe nelle ultime librerie .NET per semplificare l'implementazione del modello. Inoltre, se la tua implementazione richiede la sicurezza del thread, devi aggiungerla.

Tipi di dati primitivi e classi semplici

Ovviamente, non userai l'inizializzazione pigra per il tipo di dati primitivi o per l'uso di classi semplici come List<string>.

Prima di commentare su Lazy

Lazy<T>è stato introdotto in .NET 4.0, quindi per favore non aggiungere un altro commento riguardante questa classe.

Prima di commentare le micro-ottimizzazioni

Quando si creano le librerie, è necessario considerare tutte le ottimizzazioni. Ad esempio, nelle classi .NET vedrete bit array utilizzati per le variabili di classe booleane in tutto il codice per ridurre il consumo di memoria e la frammentazione della memoria, solo per citare due "micro-ottimizzazioni".

Per quanto riguarda le interfacce utente

Non si utilizzerà l'inizializzazione pigra per le classi direttamente utilizzate dall'interfaccia utente. La scorsa settimana ho trascorso la parte migliore di un giorno rimuovendo il carico pigro di otto raccolte utilizzate in un modello di visualizzazione per le caselle combinate. Ho un LookupManager che gestisce il caricamento e la cache pigri delle raccolte necessarie a qualsiasi elemento dell'interfaccia utente.

"Setter"

Non ho mai usato una proprietà set ("setter") per qualsiasi proprietà lazy loaded. Pertanto, non permetteresti mai foo.Bar = null;. Se hai bisogno di impostare Bar quindi vorrei creare un metodo chiamato SetBar(Bar value) e non usare l'inizializzazione pigra

collezioni

Le proprietà delle raccolte di classi vengono sempre inizializzate quando dichiarate perché non dovrebbero mai essere nulle.

Classi complesse

Permettetemi di ripeterlo in modo diverso, usate l'inizializzazione pigra per classi complesse. Che di solito sono classi mal progettate.

da ultimo

Non ho mai detto di farlo per tutte le classi o in tutti i casi. È una cattiva abitudine.


48
2018-02-08 14:16



Prendi in considerazione l'implementazione di tale modello usando Lazy<T>?

Oltre alla facile creazione di oggetti con carico pigro, si ottiene la sicurezza del thread mentre l'oggetto viene inizializzato:

Come altri hanno detto, caricate pigramente gli oggetti se sono veramente pesanti in termini di risorse o ci vuole del tempo per caricarli durante la costruzione dell'oggetto.


17
2018-02-08 14:08



Penso che dipenda da cosa stai iniziando. Probabilmente non lo farei per un elenco dato che il costo di costruzione è piuttosto ridotto, quindi può andare nel costruttore. Ma se fosse una lista pre-compilata, probabilmente non lo farei fino a quando non fosse necessario per la prima volta.

Fondamentalmente, se il costo di costruzione supera il costo di fare un controllo condizionale su ogni accesso, allora lo crei pigro. Altrimenti, fallo nel costruttore.


9
2018-02-08 13:51



Il lato negativo che posso vedere è che se vuoi chiedere se Bars è nullo, non lo sarebbe mai, e tu dovresti creare la lista lì.


9
2018-02-08 13:51



L'istanziazione / inizializzazione pigra è un modello perfettamente fattibile. Tieni presente, tuttavia, che, in generale, i consumatori della tua API non si aspettano che getter e setter prendano tempo distinguibile dal POV dell'utente finale (o che falliscano).


8
2018-02-08 14:26



Stavo per commentare la risposta di Daniel ma sinceramente non penso che vada abbastanza lontano.

Sebbene questo sia un ottimo schema da usare in determinate situazioni (ad esempio, quando l'oggetto è inizializzato dal database), è un'abitudine ORRIBILE entrare.

Una delle cose migliori di un oggetto è che offre un ambiente sicuro e affidabile. Il caso migliore è se fai il maggior numero possibile di campi "Finale", riempiendoli tutti con il costruttore. Questo rende la tua classe abbastanza a prova di proiettile. Permettere ai campi di essere cambiati attraverso i setter è un po 'meno, ma non è terribile. Per esempio:

classe SafeClass
{
    String name = "";
    Integer age = 0;

    public void setName (String newName)
    {
        assert (newName! = null)
        name = newName;
    } // segui questo schema per età
    ...
    public String toString () {
        String s = "Safe Class ha il nome:" + name + "e age:" + age
    }
}

Con il tuo pattern, il metodo toString sarebbe simile a questo:

    se (nome == null)
        lancia nuova IllegalStateException ("SafeClass è entrato in uno stato illegale! name is null")
    se (età == null)
        lancia nuova IllegalStateException ("SafeClass è entrato in uno stato illegale! age is null")

    public String toString () {
        String s = "Safe Class ha il nome:" + name + "e age:" + age
    }

Non solo questo, ma hai bisogno di controlli nulli ovunque tu possa usare quell'oggetto nella tua classe (Al di fuori della classe è sicuro a causa del controllo nullo nel getter, ma dovresti principalmente usare i membri delle classi all'interno della classe)

Anche la tua classe è perennemente in uno stato incerto - ad esempio se hai deciso di rendere quella classe una classe di ibernazione aggiungendo alcune annotazioni, come faresti?

Se prendi qualche decisione basata su una micro-optomizzazione senza requisiti e test, è quasi certamente la decisione sbagliata. In effetti, c'è davvero una buona possibilità che il tuo pattern stia effettivamente rallentando il sistema anche nelle circostanze più ideali, perché l'istruzione if può causare un errore di previsione del ramo sulla CPU che rallenterà molte molte molte più volte rispetto a semplicemente assegnando un valore nel costruttore a meno che l'oggetto che si sta creando sia piuttosto complesso o provenga da un'origine dati remota.

Per un esempio del problema di previsione brance (che stai ripetutamente ripetendo, nost solo una volta), vedi la prima risposta a questa fantastica domanda: Perché è più veloce elaborare una matrice ordinata rispetto a una matrice non ordinata?


8
2018-02-09 00:09



Permettetemi di aggiungere un altro punto a molti buoni punti fatti da altri ...

Il debugger (per impostazione predefinita) valutare le proprietà quando si passa attraverso il codice, che potrebbe potenzialmente creare un'istanza di Bar prima di quanto accadrebbe normalmente semplicemente eseguendo il codice. In altre parole, il semplice atto di debug sta cambiando l'esecuzione del programma.

Questo può o non può essere un problema (a seconda degli effetti collaterali), ma è qualcosa di cui essere a conoscenza.


4
2018-02-28 00:44



Sei sicuro che Foo dovrebbe istanziare qualcosa?

A me sembra puzzolente (anche se non necessariamente sbagliato) per permettere a Foo di istanziare qualcosa. A meno che non sia lo scopo espresso di Foo di essere una fabbrica, non dovrebbe istanziare i propri collaboratori, ma invece farli iniettare nel suo costruttore.

Se tuttavia lo scopo di Foo è di creare istanze di tipo Bar, allora non vedo nulla di sbagliato nel farlo pigramente.


2
2018-02-09 11:57