c# - Chiamata di membri virtuali in un costruttore




constructor warnings resharper virtual-functions (15)

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?


Answers

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 sono inizializzati a zero. È possibile 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 #.


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


Per rispondere alla tua domanda, considera questa domanda: quale sarà il codice sottostante che verrà stampato quando l'oggetto 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()); //NullReferenceException!?!
    }
}

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


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

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 "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!


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 si sta creando. Ciò significa che la chiamata alla funzione virtuale potrebbe non andare nemmeno dove ci si aspetta.


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 { }

I motivi dell'avvertimento sono già stati descritti, ma come risolveresti l'avviso? Devi sigillare una classe o un 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();
    }
  }

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 classe 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 classe 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 classe 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);
    }
}

Il tuo costruttore può (più tardi, in un'estensione del tuo software) essere chiamato dal costruttore di una sottoclasse che sovrascrive il metodo virtuale. Ora non è l'implementazione della funzione della sottoclasse, ma verrà chiamata l'implementazione della classe 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.


In C #, un costruttore della classe base viene eseguito prima del costruttore della classe derivata, quindi i campi di istanza che una classe 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 classe derivata sotto il costruttore che lo chiama.


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";
   }

}


Fai attenzione a seguire ciecamente il consiglio di Resharper e a sigillare la classe! 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 aspetto importante di questa domanda che altre risposte non hanno ancora affrontato è che per una classe 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 classe 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'oggetto 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'oggetto su cui vengono eseguiti non sia stato completamente costruito.


class C
{
    int y,z;

    C()
    {
        y=10;
    }

    C(int x)
    {
        C();
        z=x+y;
        System.out.println(z);
    }
}

class A
{
    public static void main(String a[])
    {
        new C(10);
    }
}

Vedere l'esempio se stiamo chiamando il costruttore, C(int x)allora il valore di z dipende da y se non chiamiamo C()nella prima riga, allora sarà il problema per z. z non sarebbe in grado di ottenere il valore corretto.





c# constructor warnings resharper virtual-functions