c# - Chiamata di membri virtuali in un costruttore





constructor warnings resharper virtual-functions (16)


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

}

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?




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.




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.




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 #.




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

La classe 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 classe figlio qui sostituisce la proprietà genitore. Se questa proprietà non è stata contrassegnata come virtuale, il compilatore avverte che la proprietà nasconde la proprietà nella classe 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 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"
    }
} 



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.




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



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.




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 :)




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.




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



Quando viene costruito un oggetto scritto in C #, ciò che accade è che gli inizializzatori girano in ordine dalla classe più derivata alla classe base, e quindi i costruttori girano in ordine dalla classe base alla classe 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 classe 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 classe 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'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.




È possibile chiamare un altro costruttore tramite la parola chiave this(...) (quando è necessario chiamare un costruttore della stessa classe) o la parola chiave super(...) (quando è necessario chiamare un costruttore da una superclasse).

Tuttavia, tale chiamata deve essere la prima dichiarazione del costruttore. Per superare questa limitazione, usa questa risposta .







c# constructor warnings resharper virtual-functions