[c#] Chiamata di membri virtuali in un costruttore


Answers

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

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.

Question

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?




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



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



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.




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.




Un'altra cosa interessante che ho trovato è che l'errore di ReSharper può essere 'soddisfatto' facendo qualcosa di simile di seguito che è stupido per me (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";
   }

}




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



Related