Chiamata di membri virtuali in un costruttore

Ricevo un avvertimento da ReSharper su una chiamata a un membro virtuale dal mio costruttore di oggetti.

Perché questo dovrebbe essere qualcosa da non fare?

Quando viene costruito un object scritto in C #, ciò che accade è che gli inizializzatori girano in ordine dalla class più derivata alla class base, e quindi i costruttori girano in ordine dalla class base alla class più derivata ( vedi il blog di Eric Lippert per dettagli sul perché questo è ).

Inoltre, gli oggetti .NET non cambiano il tipo così come sono costruiti, ma iniziano come il tipo più derivato, con la tabella del metodo per il tipo più derivato. Ciò significa che le chiamate al metodo virtuale vengono eseguite sempre sul tipo più derivato.

Quando si combinano questi due fatti, si ha il problema che se si effettua una chiamata di metodo virtuale in un costruttore e non è il tipo più derivato nella sua gerarchia di ereditarietà, verrà chiamato su una class il cui costruttore non è stato eseguire, e quindi potrebbe non essere in uno stato adatto per avere chiamato quel metodo.

Questo problema è, naturalmente, mitigato se contrassegni la tua class come sealed per assicurarti che sia il tipo più derivato nella gerarchia dell’eredità – nel qual caso è perfettamente sicuro chiamare il metodo virtuale.

Per rispondere alla tua domanda, considera questa domanda: quale sarà il codice sottostante che verrà stampato quando l’object Child viene istanziato?

 class Parent { public Parent() { DoSomething(); } protected virtual void DoSomething() { } } class Child : Parent { private string foo; public Child() { foo = "HELLO"; } protected override void DoSomething() { Console.WriteLine(foo.ToLower()); } } 

La risposta è che in effetti verrà lanciata una NullReferenceException , perché foo è nullo. Il costruttore di base di un object viene chiamato prima del suo costruttore . Avendo una chiamata virtual nel costruttore di un object si introduce la possibilità che gli oggetti ereditari eseguano il codice prima che siano stati inizializzati completamente.

Le regole di C # sono molto diverse da quelle di Java e C ++.

Quando sei nel costruttore per qualche object in C #, quell’object esiste in una forma completamente inizializzata (solo non “costruita”), come il suo tipo completamente derivato.

 namespace Demo { class A { public A() { System.Console.WriteLine("This is a {0},", this.GetType()); } } class B : A { } // . . . B b = new B(); // Output: "This is a Demo.B" } 

Ciò significa che se si chiama una funzione virtuale dal costruttore di A, si risolverà in qualsiasi override in B, se ne viene fornito uno.

Anche se hai impostato intenzionalmente A e B in questo modo, comprendendo appieno il comportamento del sistema, potresti subire uno shock in seguito. Supponi di aver chiamato funzioni virtuali nel costruttore di B, “sapendo” che sarebbero state gestite da B o A a seconda dei casi. Poi passa il tempo, e qualcun altro decide di aver bisogno di definire C, e di sovrascrivere alcune delle funzioni virtuali presenti. All’improvviso il costruttore di B finisce per chiamare il codice in C, il che potrebbe portare a un comportamento abbastanza sorprendente.

Probabilmente è una buona idea evitare le funzioni virtuali nei costruttori, dal momento che le regole sono così diverse tra C #, C ++ e Java. I tuoi programmatori potrebbero non sapere cosa aspettarsi!

I motivi dell’avvertimento sono già stati descritti, ma come risolveresti l’avviso? Devi sigillare una class o un membro virtuale.

  class B { protected virtual void Foo() { } } class A : B { public A() { Foo(); // warning here } } 

Puoi sigillare la class A:

  sealed class A : B { public A() { Foo(); // no warning } } 

Oppure puoi sigillare il metodo Foo:

  class A : B { public A() { Foo(); // no warning } protected sealed override void Foo() { base.Foo(); } } 

In C #, un costruttore della class base viene eseguito prima del costruttore della class derivata, quindi i campi di istanza che una class derivata potrebbe utilizzare nel membro virtuale eventualmente sovrascritto non sono ancora inizializzati.

Prendi nota che questo è solo un avvertimento per farti prestare attenzione e assicurarti che sia giusto. Esistono casi di utilizzo effettivi per questo scenario, è sufficiente documentare il comportamento del membro virtuale che non può utilizzare alcun campo di istanza dichiarato in una class derivata sotto il costruttore che lo chiama.

Ci sono risposte ben scritte sopra per il motivo per cui non vorresti farlo. Ecco un contro-esempio in cui forse vorresti farlo (tradotto in C # da Practical Object-Oriented Design in Ruby di Sandi Metz, pagina 126).

Nota che GetDependency() non sta toccando alcuna variabile di istanza. Sarebbe statico se i metodi statici potessero essere virtuali.

(Per essere onesti, ci sono probabilmente modi più intelligenti per farlo tramite i contenitori di dipendenze o gli inizializzatori di oggetti …)

 public class MyClass { private IDependency _myDependency; public MyClass(IDependency someValue = null) { _myDependency = someValue ?? GetDependency(); } // If this were static, it could not be overridden // as static methods cannot be virtual in C#. protected virtual IDependency GetDependency() { return new SomeDependency(); } } public class MySubClass : MyClass { protected override IDependency GetDependency() { return new SomeOtherDependency(); } } public interface IDependency { } public class SomeDependency : IDependency { } public class SomeOtherDependency : IDependency { } 

Sì, è generalmente male chiamare il metodo virtuale nel costruttore.

A questo punto, objet potrebbe non essere ancora completamente costruito, e gli invarianti attesi dai metodi potrebbero non reggere ancora.

Il tuo costruttore può (più tardi, in un’estensione del tuo software) essere chiamato dal costruttore di una sottoclass che sovrascrive il metodo virtuale. Ora non è l’implementazione della funzione della sottoclass, ma verrà chiamata l’implementazione della class base. Quindi non ha senso chiamare qui una funzione virtuale.

Tuttavia, se il tuo progetto soddisfa il principio di sostituzione di Liskov, non verrà fatto alcun danno. Probabilmente è per questo che è tollerato – un avvertimento, non un errore.

Un aspetto importante di questa domanda che altre risposte non hanno ancora affrontato è che per una class base è sicuro chiamare i membri virtuali dal suo costruttore se questo è ciò che le classi derivate si aspettano che faccia . In questi casi, il progettista della class derivata è responsabile di garantire che tutti i metodi che vengono eseguiti prima che la costruzione sia completa si comportano in modo sensato come possono nelle circostanze. Ad esempio, in C ++ / CLI, i costruttori sono racchiusi nel codice che chiamerà Dispose sull’object parzialmente costruito se la costruzione fallisce. Chiamare lo Dispose in questi casi è spesso necessario per prevenire perdite di risorse, ma i metodi di Dispose devono essere preparati per la possibilità che l’object su cui vengono eseguiti non sia stato completamente costruito.

Perché fino a quando il costruttore non ha completato l’esecuzione, l’object non è completamente istanziato. Qualsiasi membro a cui fa riferimento la funzione virtuale non può essere inizializzato. In C ++, quando ci si trova in un costruttore, this si riferisce solo al tipo statico del costruttore in cui ci si trova e non al tipo dinamico effettivo dell’object che si sta creando. Ciò significa che la chiamata alla funzione virtuale potrebbe non andare nemmeno dove ci si aspetta.

L’avviso è un promemoria per cui è probabile che i membri virtuali vengano sovrascritti sulla class derivata. In tal caso, qualunque sia stata la class genitrice a un membro virtuale verrà annullata o modificata in base alla class secondaria di priorità. Guarda il piccolo esempio di colpo per chiarezza

La class genitore di seguito tenta di impostare il valore su un membro virtuale sul suo costruttore. E questo attiverà l’avvertimento Re-sharper, lascia vedere il codice:

 public class Parent { public virtual object Obj{get;set;} public Parent() { // Re-sharper warning: this is open to change from // inheriting class overriding virtual member this.Obj = new Object(); } } 

La class figlio qui sostituisce la proprietà genitore. Se questa proprietà non è stata contrassegnata come virtuale, il compilatore avverte che la proprietà nasconde la proprietà nella class padre e suggerisce di aggiungere la parola chiave ‘new’ se è intenzionale.

 public class Child: Parent { public Child():base() { this.Obj = "Something"; } public override object Obj{get;set;} } 

Infine, l’impatto sull’utilizzo, l’output dell’esempio seguente, abbandona il valore iniziale impostato dal costruttore della class genitore. E questo è ciò che Re-sharper tenta di avvisarti , i valori impostati sul costruttore della class Parent sono aperti per essere sovrascritti dal costruttore della class figlio che viene chiamato subito dopo il costruttore della class genitore .

 public class Program { public static void Main() { var child = new Child(); // anything that is done on parent virtual member is destroyed Console.WriteLine(child.Obj); // Output: "Something" } } 

Fai attenzione a seguire ciecamente il consiglio di Resharper e a sigillare la class! Se si tratta di un modello in codice EF, prima rimuoverà la parola chiave virtuale e ciò disabiliterà il caricamento lento delle sue relazioni.

  public **virtual** User User{ get; set; } 

Un importante bit mancante è, qual è il modo corretto per risolvere questo problema?

Come spiegato da Greg , il problema alla radice è che un costruttore di classi base invocherà il membro virtuale prima che la class derivata sia stata costruita.

Il seguente codice, tratto dalle linee guida sulla progettazione del costruttore di MSDN , dimostra questo problema.

 public class BadBaseClass { protected string state; public BadBaseClass() { this.state = "BadBaseClass"; this.DisplayState(); } public virtual void DisplayState() { } } public class DerivedFromBad : BadBaseClass { public DerivedFromBad() { this.state = "DerivedFromBad"; } public override void DisplayState() { Console.WriteLine(this.state); } } 

Quando viene creata una nuova istanza di DerivedFromBad , il costruttore della class base chiama a DisplayState e mostra BadBaseClass perché il campo non è stato ancora aggiornato dal costruttore derivato.

 public class Tester { public static void Main() { var bad = new DerivedFromBad(); } } 

Un’implementazione migliorata rimuove il metodo virtuale dal costruttore della class base e utilizza un metodo Initialize . La creazione di una nuova istanza di DerivedFromBetter visualizza l’attesa “DerivedFromBetter”

 public class BetterBaseClass { protected string state; public BetterBaseClass() { this.state = "BetterBaseClass"; this.Initialize(); } public void Initialize() { this.DisplayState(); } public virtual void DisplayState() { } } public class DerivedFromBetter : BetterBaseClass { public DerivedFromBetter() { this.state = "DerivedFromBetter"; } public override void DisplayState() { Console.WriteLine(this.state); } } 

C’è una differenza tra C ++ e C # in questo caso specifico. In C ++ l’object non è inizializzato e quindi non è sicuro chiamare una funzione virutale all’interno di un costruttore. In C # quando viene creato un object class tutti i suoi membri sono inizializzati a zero. È ansible chiamare una funzione virtuale nel costruttore, ma se si potrebbe accedere ai membri che sono ancora zero. Se non è necessario accedere ai membri, è abbastanza sicuro chiamare una funzione virtuale in C #.

Solo per aggiungere i miei pensieri. Se si inizializza sempre il campo privato quando lo si definisce, questo problema dovrebbe essere evitato. Almeno sotto il codice funziona come un incantesimo:

 class Parent { public Parent() { DoSomething(); } protected virtual void DoSomething() { } } class Child : Parent { private string foo = "HELLO"; public Child() { /*Originally foo initialized here. Removed.*/ } protected override void DoSomething() { Console.WriteLine(foo.ToLower()); } } 

Un’altra cosa interessante che ho trovato è che l’errore di ReSharper può essere ‘soddisfatto’ facendo qualcosa di simile al di sotto del quale è stupido da parte mia (tuttavia, come già detto in precedenza, non è comunque una buona idea chiamare i metodi / prop virtuali in Ctor.

 public class ConfigManager { public virtual int MyPropOne { get; private set; } public virtual string MyPropTwo { get; private set; } public ConfigManager() { Setup(); } private void Setup() { MyPropOne = 1; MyPropTwo = "test"; } 

}

Vorrei solo aggiungere un metodo Initialize () alla class base e quindi chiamarlo dai costruttori derivati. Quel metodo chiamerà qualsiasi metodo / proprietà virtuale / astratto dopo che tutti i costruttori sono stati eseguiti 🙂