it-swarm-eu.dev

Volání virtuálního člena v konstruktoru

Dostávám varování od ReSharperu o volání virtuálního člena z mého konstruktoru objektů.

Proč by to nebylo něco dělat?

1231
JasonS

Když je postaven objekt napsaný v C #, stane se to, že inicializátory běží v pořadí od nejvíce odvozené třídy k základní třídě a konstruktory běží v pořadí od základní třídy k nejvíce odvozené třídě ( viz Eric) Lippertův blog obsahuje podrobnosti o tom, proč je to ).

Také v objektech .NET nemění typ, jak jsou konstruovány, ale začínají jako nejvíce odvozený typ, přičemž tabulka metod je pro nejvíce odvozený typ. To znamená, že volání virtuální metody vždy běží na nejvíce odvozeném typu.

Když zkombinujete tato dvě fakta, zbude vám problém, že pokud zavoláte virtuální metodu v konstruktoru a nejedná se o nejvíce odvozený typ v hierarchii dědičnosti, bude vyvolán na třídu, jejíž konstruktor nebyl spustit, a proto nemusí být ve vhodném stavu pro vyvolání této metody.

Tento problém je samozřejmě zmírněn, pokud svou třídu označíte jako zapečetěnou, aby se zajistilo, že jde o nejrozšířenější typ v hierarchii dědičnosti - v takovém případě je naprosto bezpečné volat virtuální metodu.

1105
Greg Beech

Chcete-li odpovědět na svou otázku, zvažte tuto otázku: co se vytiskne níže uvedený kód, když se vytvoří instance 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!?!
    }
}

Odpověď zní, že NullReferenceException bude ve skutečnosti vyvolána, protože foo je null. Základní konstruktor objektu je volán před vlastním konstruktorem. Zavoláním volání virtual v konstruktoru objektu představujete možnost, že zděděné objekty budou provádět kód dříve, než budou plně inicializovány.

571
Matt Howells

Pravidla C # se velmi liší od pravidel Java a C++.

Když jste v konstruktoru pro nějaký objekt v C #, tento objekt existuje v plně inicializované (prostě ne "vytvořené") formě jako jeho plně odvozený typ.

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

To znamená, že pokud zavoláte virtuální funkci z konstruktoru A, vyřeší se jakékoli přepsání v B, pokud je k dispozici.

I když takto úmyslně nastavíte A a B a plně pochopíte chování systému, mohli byste být šokováni později. Řekněme, že jste ve konstruktoru B nazvali virtuální funkce, „věděli“, že by s nimi B nebo A podle potřeby zacházely. Pak uběhne čas a někdo jiný se rozhodne, že musí definovat C, a potlačit tam některé z virtuálních funkcí. Zcela náhle B konstruktor skončí volajícím kódem v C, což by mohlo vést k docela překvapivému chování.

Je to asi dobrý nápad, jak se vyhnout virtuálním funkcím v konstruktérech, protože pravidla jso tak odlišná mezi C #, C++ a Java. Vaši programátoři nemusí vědět, co mohou očekávat!

157
Lloyd

Důvody varování jsou již popsány, ale jak byste varování opravili? Musíte uzavřít třídu nebo virtuální člen.

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

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

Můžete zapečetit třídu A:

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

Nebo můžete zapečetit metodu Foo:

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

    protected sealed override void Foo()
    {
      base.Foo();
    }
  }
84
Ilya Ryzhenkov

V C # spustí konstruktor základní třídy před konstruktorem odvozené třídy, takže jakákoli pole instance, která by odvozená třída mohla použít v potenciálně přepsané virtuální člen zatím není inicializován.

Uvědomte si, že toto je pouze varování, abyste byli v pozoru a ujistěte se, že je v pořádku. Pro tento scénář existují skutečné případy použití, stačí zdokumentovat chování virtuálního člena, že nemůže použít žádná pole instance deklarovaná v odvozené třídě níže, kde ji konstruktor volá.

17
Alex Lyman

Existují dobře napsané odpovědi výše, proč byste to nechtěli dělat. Zde je protiklad, kde byste chtěli to udělat (přeloženo do C # z Praktický objektově orientovaný design v Ruby Sandi Metz, str. 126).

Všimněte si, že GetDependency() se nedotýká žádných proměnných instance. Bylo by statické, pokud by statické metody mohly být virtuální.

(Abychom byli spravedliví, existují pravděpodobně chytřejší způsoby, jak toho dosáhnout pomocí kontejnerů na závislostní injekce nebo inicializátorů objektů ...)

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 { }
11
Josh Kodroff

Ano, obecně je špatné volat virtuální metodu v konstruktoru.

V tomto okamžiku nemusí být objekt ještě zcela vytvořen a invarianty očekávané metodami se zatím nemusí držet.

6
David Pierre

Váš konstruktor může být (později v rozšíření vašeho softwaru) vyvolán z konstruktoru podtřídy, která přepíše virtuální metodu. Nyní nebude implementována funkce podtřídy, ale bude implementována základní třída. Takže nemá smysl říkat virtuální funkci zde.

Pokud však váš návrh vyhovuje principu substituce Liskov, nedojde k žádné újmě. Pravděpodobně proto je tolerován - varování, nikoli chyba.

5
xtofl

Jedním důležitým aspektem této otázky, na který se ostatní odpovědi dosud nezabývají, je to, že je bezpečné, aby základní třída mohla volat virtuální členy zvenčí svého konstruktoru pokud to je to, co od nich odvozené třídy očekávají, že to udělá . V takových případech je konstruktér odvozené třídy zodpovědný za zajištění toho, aby se všechny metody, které se provádějí před dokončením stavby, chovaly za těchto okolností tak rozumně, jak mohou. Například v C++/CLI jsou konstruktory zabaleny do kódu, který v případě selhání konstrukce zavolá Dispose na částečně vytvořeném objektu. Volání Dispose v takových případech je často nezbytné, aby se zabránilo únikům zdrojů, ale metody Dispose musí být připraveny na možnost, že objekt, na kterém jsou spuštěny, nemusí být zcela vytvořen.

5
supercat

Protože dokud konstruktor nedokončí provádění, objekt není plně instancován. Jakýkoli člen, na který odkazuje virtuální funkce, nesmí být inicializován. V C++, když jste v konstruktoru, this odkazuje pouze na statický typ konstruktoru, ve kterém se nacházíte, a nikoli na skutečný dynamický typ vytvářeného objektu. To znamená, že volání virtuální funkce nemusí jít ani tam, kde to očekáváte.

5
1800 INFORMATION

Jedním důležitým chybějícím bitem je, jaký je správný způsob, jak tento problém vyřešit?

Jak vysvětlil Greg , hlavní problém je v tom, že konstruktor základní třídy vyvolá virtuální člen před vytvořením odvozené třídy.

Následující kód, převzatý z pokyny konstruktéra MSDN pro konstruktor , demonstruje tento problém.

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

Když je vytvořena nová instance DerivedFromBad, konstruktor základní třídy volá DisplayState a zobrazí BadBaseClass, protože pole dosud nebyl odvozeným konstruktorem aktualizován.

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

Vylepšená implementace odebere virtuální metodu z konstruktoru základní třídy a používá metodu Initialize. Vytvoření nové instance DerivedFromBetter zobrazí očekávané "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);
    }
}
3
Gustavo Mori

Varování je připomínkou toho, že virtuální členové budou pravděpodobně v odvozené třídě přepsáni. V takovém případě bude vše, co nadřazená třída udělala virtuálnímu členovi, zrušeno nebo změněno přepsáním podřízené třídy. Podívejte se na malý příklad rány pro přehlednost

Nadřazená třída níže se pokouší nastavit hodnotu virtuálního člena na svém konstruktoru. A tím se spustí opětovné ostřejší varování, podívejte se na kód:

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

Podřízená třída zde přepíše nadřazenou vlastnost. Pokud tato vlastnost nebyla označena jako virtuální, kompilátor by varoval, že vlastnost skrývá vlastnost nadřazené třídy, a pokud je to záměrné, navrhněte přidání 'nového' klíčového slova.

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

Konečně dopad na použití výstup z níže uvedeného příkladu opustí počáteční hodnotu nastavenou konstruktorem nadřazené třídy. A to je to, co se vás znovu pokouší varovat, hodnoty nastavené na konstruktoru Rodičovská třída jsou otevřené, aby je mohl přepsat konstruktor podřízené třídy, který se nazývá právo za konstruktorem nadřazené třídy .

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"
    }
} 
3
BTE

Dejte pozor, abyste slepě následovali radu Resharpera a zapečetili třídu! Pokud je to model v EF Code First, odstraní virtuální klíčové slovo a to by zakázalo líné načítání jeho vztahů.

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

Jen pro doplnění mých myšlenek. Pokud při definování soukromého pole vždy inicializujete, tomuto problému byste se měli vyhnout. Alespoň pod kód funguje jako kouzlo:

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

V tomto konkrétním případě je rozdíl mezi C++ a C #. V C++ není objekt inicializován, a proto není bezpečné volat virutální funkci uvnitř konstruktoru. V C #, když je vytvořen objekt třídy, jsou všechny jeho členy inicializovány nula. V konstruktoru je možné volat virtuální funkci, ale pokud budete mít přístup k členům, kteří jsou stále nulové. Pokud nepotřebujete přístup ke členům, je docela bezpečné zavolat virtuální funkci v C #.

1
Yuval Peled

Další zajímavou věcí, kterou jsem našel, je, že chyba ReSharper může být „uspokojena“ tím, že dělám něco jako níže, které je pro mě hloupé (jak již bylo zmíněno v mnoha dřívějších případech, stále není dobrý nápad volat virtuální prop/metody v ctoru).

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

}

0
adityap