Domanda

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

Perché questo dovrebbe essere qualcosa da non fare?

È stato utile?

Soluzione

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

Anche in .NET gli oggetti non cambiano tipo man mano che vengono costruiti, ma iniziano come il tipo più derivato, con la tabella dei metodi per il tipo più derivato. Ciò significa che le chiamate al metodo virtuale vengono sempre eseguite sul tipo più derivato.

Quando si combinano questi due fatti, rimane il problema che se si effettua un metodo virtuale che chiama in un costruttore, e non è il tipo più derivato nella sua gerarchia ereditaria, verrà chiamato in una classe il cui costruttore non è stato eseguito e pertanto potrebbe non essere in uno stato adatto per il richiamo di tale metodo.

Questo problema è, ovviamente, mitigato se si contrassegna la classe come sigillata per garantire che sia il tipo più derivato nella gerarchia dell'ereditarietà, nel qual caso è perfettamente sicuro chiamare il metodo virtuale.

Altri suggerimenti

Per rispondere alla tua domanda, considera questa domanda: cosa verrà stampato il codice seguente quando viene istanziata l'oggetto Child ?

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()); //NullReferenceException!?!
    }
}

La risposta è che in effetti verrà lanciato un NullReferenceException , perché foo è nullo. Il costruttore di base di un oggetto viene chiamato prima del suo costruttore . Avendo una virtuale nel costruttore di un oggetto stai introducendo la possibilità che l'ereditarietà degli oggetti eseguirà il codice prima che siano stati completamente inizializzati.

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

Quando sei nel costruttore per qualche oggetto in C #, quell'oggetto esiste in una forma completamente inizializzata (solo non " built "), 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 disponibile.

Anche se hai impostato intenzionalmente A e B in questo modo, comprendendo appieno il comportamento del sistema, potresti essere sorpreso in seguito. Supponiamo che tu abbia chiamato funzioni virtuali nel costruttore di B, " conoscere " verrebbero gestiti da B o A, a seconda dei casi. Quindi il tempo passa e qualcun altro decide che devono definire C e sovrascrivere alcune delle funzioni virtuali lì. Tutto ad un tratto il costruttore di B finisce per chiamare il codice in C, il che potrebbe portare a comportamenti abbastanza sorprendenti.

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

I motivi dell'avviso sono già descritti, ma come si corregge l'avviso? Devi sigillare la classe o il membro virtuale.

  class B
  {
    protected virtual void Foo() { }
  }

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

Puoi sigillare la classe 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 classe base esegue prima del costruttore della classe derivata, quindi tutti i campi di istanza che una classe derivata potrebbe usare nel membro virtuale eventualmente sovrascritto non sono ancora inizializzati.

Nota che questo è solo un avvertimento per farti prestare attenzione e assicurarti che vada bene. Esistono casi d'uso reali per questo scenario, devi solo documentare il comportamento del membro virtuale che non può usare nessun campo di istanza dichiarato in una classe 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 Disegno pratico orientato agli oggetti in rubino di Sandi Metz, p. 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 di farlo tramite contenitori di iniezione di dipendenza o 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ì, in genere è male chiamare il metodo virtuale nel costruttore.

A questo punto, l'oggetto potrebbe non essere ancora completamente costruito e gli invarianti previsti dai metodi potrebbero non essere ancora validi.

Perché fino a quando il costruttore non ha completato l'esecuzione, l'oggetto 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'oggetto che viene creato. Ciò significa che la chiamata di funzione virtuale potrebbe non andare nemmeno dove ti aspetti.

Il costruttore può (in seguito, in un'estensione del software) essere chiamato dal costruttore di una sottoclasse che sovrascrive il metodo virtuale. Ora non verrà implementata l'implementazione della funzione della sottoclasse, ma verrà chiamata l'implementazione della classe base. Quindi non ha davvero senso chiamare una funzione virtuale qui.

Tuttavia, se il tuo progetto soddisfa il principio di sostituzione 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 classe base è sicuro chiamare membri virtuali dal suo costruttore se questo è ciò che le classi derivate si aspettano che faccia . In tali casi, il progettista della classe derivata ha la responsabilità di garantire che qualsiasi metodo che viene eseguito prima del completamento della costruzione si comporti nel modo più ragionevole possibile in tali circostanze. Ad esempio, in C ++ / CLI, i costruttori sono racchiusi nel codice che chiamerà Dispose sull'oggetto parzialmente costruito se la costruzione non riesce. In questi casi è spesso necessario chiamare Dispose per prevenire perdite di risorse, ma i metodi Dispose devono essere preparati per la possibilità che l'oggetto su cui vengono eseguiti potrebbe non essere stato completamente costruito .

L'avvertimento ricorda che è probabile che i membri virtuali vengano sovrascritti sulla classe derivata. In tal caso, qualunque cosa la classe genitore abbia fatto a un membro virtuale verrà annullata o modificata sostituendo la classe figlio. Guarda il piccolo esempio di chiarezza

La seguente classe padre tenta di impostare il valore su un membro virtuale nel suo costruttore. E questo attiverà un avvertimento più nitido, vediamo sul 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 classe figlio qui sovrascrive la proprietà parent. Se questa proprietà non fosse contrassegnata come virtuale, il compilatore avviserebbe che la proprietà nasconde la proprietà nella classe genitore e suggerirebbe di aggiungere una "nuova" parola chiave se è intenzionale.

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

Finalmente l'impatto sull'uso, l'output dell'esempio seguente abbandona il valore iniziale impostato dal costruttore della classe genitore. E questo è ciò che Re-sharper tenta di avvisarti , i valori impostati sul costruttore della classe Parent sono aperti per essere sovrascritti dal costruttore della classe figlio che viene chiamato subito dopo il costruttore della classe 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 classe! Se si tratta di un modello nel codice EF In primo luogo rimuoverà la parola chiave virtuale e ciò disabiliterebbe 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 Greg ha spiegato , il problema alla radice qui è che un costruttore di classi base invocherebbe il membro virtuale prima della classe derivata è stato costruito.

Il seguente codice, tratto da Le linee guida per la progettazione dei costruttori di MSDN dimostrano 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 classe di base chiama DisplayState e mostra BadBaseClass perché il campo non è stato ancora aggiornamento dal costruttore derivato.

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

Un'implementazione migliorata rimuove il metodo virtuale dal costruttore della classe base e utilizza un metodo Initialize . La creazione di una nuova istanza di DerivedFromBetter visualizza l'atteso " 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'oggetto non è inizializzato e quindi non è sicuro chiamare una funzione virutale all'interno di un costruttore. In C # quando viene creato un oggetto classe, tutti i suoi membri vengono inizializzati a zero. È possibile chiamare una funzione virtuale nel costruttore ma se è possibile accedere a 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 scoperto è che l'errore ReSharper può essere 'soddisfatto' facendo qualcosa di simile al di sotto che è stupido per me (tuttavia, come menzionato da molti prima, non è ancora una buona idea chiamare 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";
   }

}

Aggiungerei semplicemente un metodo Initialize () alla classe base e lo chiamerei dai costruttori derivati. Tale metodo chiamerà qualsiasi metodo / proprietà virtuale / astratto DOPO che tutti i costruttori sono stati eseguiti :)

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top