Wirtualne wywołanie członka w konstruktorze

Otrzymuję Ostrzeżenie od ReSharper o wywołaniu do Wirtualnego członka z mojego konstruktora obiektów.

Dlaczego miałoby to być coś, czego nie można zrobić?

Author: CodeNotFound, 2008-09-23

17 answers

Kiedy obiekt napisany w C# jest konstruowany, to dzieje się tak, że inicjalizatory uruchamiają się w kolejności od najbardziej pochodnej klasy do klasy bazowej, a następnie konstruktory uruchamiają się w kolejności od klasy bazowej do najbardziej pochodnej klasy (Zobacz blog Erica Lipperta, aby dowiedzieć się, dlaczego jest to).

Również w. NET obiekty nie zmieniają typu podczas konstruowania, ale zaczynają jako typ najbardziej Pochodny, przy czym tabela metod jest dla typu najbardziej pochodnego. Oznacza to, że wirtualne wywołania metod są zawsze uruchamiane na najbardziej pochodnym typie.

Kiedy połączysz te dwa fakty, pozostajesz z problemem, że jeśli wykonasz wirtualne wywołanie metody w konstruktorze, a nie jest to najbardziej Pochodny typ w jego hierarchii dziedziczenia, to zostanie on wywołany na klasie, której konstruktor nie został uruchomiony, a zatem może nie być w stanie odpowiednim do wywołania tej metody.

Ten problem jest oczywiście złagodzony, jeśli oznaczysz klasę jako zapieczętowaną, aby upewnić się, że jest to większość typów pochodnych w hierarchii dziedziczenia - w takim przypadku jest całkowicie bezpieczne wywołanie metody wirtualnej.

 1055
Author: Greg Beech,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-03-02 16:22:43

Aby odpowiedzieć na twoje pytanie, rozważ to pytanie: co zostanie wydrukowany poniższy kod, gdy obiekt Child zostanie utworzony?

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!?!
    }
}

Odpowiedź jest taka, że w rzeczywistości a NullReferenceException zostanie wyrzucone, ponieważ foo jest null. konstruktor bazowy obiektu jest wywoływany przed własnym konstruktorem . Poprzez wywołanie virtual w konstruktorze obiektu wprowadzasz możliwość, że dziedziczące obiekty wykonają kod zanim zostaną w pełni zainicjalizowane.

 500
Author: Matt Howells,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-09-13 15:45:08

Zasady C# bardzo różnią się od zasad Javy i C++.

Gdy znajdujesz się w konstruktorze jakiegoś obiektu w C#, Obiekt ten istnieje w pełni zainicjalizowanej (tylko nie "skonstruowanej") formie, jako jego w pełni Pochodny 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"
}

Oznacza to, że jeśli wywołasz wirtualną funkcję z konstruktora a, zostanie ona rozwiązana do dowolnego nadpisania w B, jeśli taka zostanie dostarczona.

Nawet jeśli celowo skonfigurujesz A i B w ten sposób, w pełni rozumiejąc zachowanie systemu, możesz być później w szoku. Powiedzmy, że nazwałeś funkcje wirtualne w konstruktorze B, "wiedząc", że będą one obsługiwane przez B lub A odpowiednio. Potem czas mija, a ktoś inny decyduje, że musi zdefiniować C i nadpisać niektóre z wirtualnych funkcji. Nagle konstruktor B kończy wywołanie kodu w C, co może prowadzić do dość zaskakującego zachowania.

Prawdopodobnie dobrym pomysłem jest unikanie funkcji wirtualnych w konstruktorach, ponieważ reguły więc różni się między C#, C++ i Java. Twoi programiści mogą nie wiedzieć, czego się spodziewać!

 155
Author: Lloyd,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-08-19 08:56:36

Powody ostrzeżenia są już opisane, ale jak naprawić Ostrzeżenie? Musisz zapieczętować klasę lub członka Wirtualnego.

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

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

Można uszczelnić klasę A:

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

Lub możesz uszczelnić metodę Foo:

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

    protected sealed override void Foo()
    {
      base.Foo();
    }
  }
 80
Author: Ilya Ryzhenkov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-23 13:20:22

W C# konstruktor klasy bazowej uruchamia Przed konstruktorem klasy pochodnej, więc wszelkie pola instancji, które Klasa pochodna może użyć w potencjalnie nadpisanym elemencie wirtualnym, nie są jeszcze inicjalizowane.

Należy pamiętać, że to jest tylko Ostrzeżenie , aby zwrócić uwagę i upewnić się, że wszystko jest w porządku. Istnieją rzeczywiste przypadki użycia tego scenariusza, wystarczy udokumentować zachowanie Wirtualnego członka, że nie może on używać żadnych pól instancji zadeklarowanych w Klasa pochodna poniżej, gdzie konstruktor ją wywołujący.

 16
Author: Alex Lyman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-23 07:21:04

Są dobrze napisane odpowiedzi powyżej, dlaczego nie chcesz tego zrobić. Oto przeciw-przykład, w którym być może chciałbyś to zrobić (przetłumaczone na C# z praktycznego projektowania obiektowego w Ruby autorstwa Sandi Metz, str. 126).

Zauważ, że GetDependency() nie dotyka żadnych zmiennych instancji. Byłoby statyczne, gdyby statyczne metody mogły być wirtualne.

(szczerze mówiąc, są prawdopodobnie mądrzejsze sposoby na zrobienie tego za pomocą kontenerów iniekcji zależności lub inicjalizatory obiektów...)

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
Author: Josh Kodroff,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-12-28 01:19:13

Tak, ogólnie źle jest wywoływać metodę wirtualną w konstruktorze.

W tym momencie obiekt może nie być jeszcze w pełni skonstruowany, a niezmienniki oczekiwane przez metody mogą nie być jeszcze utrzymane.

 5
Author: David Pierre,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-23 07:15:32

Twój konstruktor może (później, w rozszerzeniu Twojego oprogramowania) zostać wywołany z konstruktora podklasy, która nadpisuje metodę wirtualną. Teraz nie zostanie wywołana implementacja funkcji podklasy, ale Implementacja klasy bazowej. Więc nie ma sensu wywoływać tutaj funkcji wirtualnej.

Jednakże, jeśli twój projekt spełnia zasadę substytucji Liskowa, nic się nie stanie. Prawdopodobnie dlatego jest tolerowane-Ostrzeżenie, a nie błąd.

 5
Author: xtofl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-23 07:25:01

Ważnym aspektem tego pytania, którego inne odpowiedzi jeszcze nie rozwiązały, jest to, że jest bezpieczne dla klasy bazowej wywoływanie wirtualnych członków z jej konstruktora , jeśli tego oczekują klasy pochodne. W takich przypadkach projektant klasy pochodnej jest odpowiedzialny za zapewnienie, że wszelkie metody uruchamiane przed ukończeniem budowy będą zachowywać się tak rozsądnie, jak to tylko możliwe w danych okolicznościach. Na przykład w C++/CLI konstruktory są owinięte w kodzie, który wywoła Dispose Na częściowo zbudowanym obiekcie, jeśli Budowa się nie powiedzie. Wywołanie Dispose w takich przypadkach jest często konieczne, aby zapobiec wyciekom zasobów, ale Dispose metody muszą być przygotowane na możliwość, że obiekt, na którym są uruchamiane, może nie być w pełni skonstruowany.

 5
Author: supercat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-10-25 20:33:34

Ponieważ dopóki konstruktor nie zakończy wykonywania, obiekt nie jest w pełni instancjowany. Członkowie, do których odwołuje się funkcja wirtualna, mogą nie zostać zainicjowani. W C++, gdy znajdujesz się w konstruktorze, this odnosi się tylko do statycznego typu konstruktora, w którym się znajdujesz, a nie do rzeczywistego dynamicznego typu tworzonego obiektu. Oznacza to, że wirtualne wywołanie funkcji może nawet nie pójść tam, gdzie tego oczekujesz.

 4
Author: 1800 INFORMATION,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-23 07:14:10

Ostrzeżenie jest przypomnieniem, że członkowie wirtualni mogą zostać nadpisani na klasie pochodnej. W takim przypadku to, co Klasa rodzica zrobiła wirtualnemu członkowi, zostanie cofnięte lub zmienione przez nadpisanie klasy potomnej. Spójrz na mały przykład blow dla jasności

Klasa rodzica poniżej próbuje ustawić wartość na wirtualny element w konstruktorze. A to spowoduje ponowne ostrzejsze Ostrzeżenie, zobaczmy na kodzie:

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

Klasa potomna nadpisuje właściwość rodzica. Jeśli to właściwość nie była zaznaczona wirtualnie kompilator ostrzegałby, że właściwość ukrywa właściwość w klasie nadrzędnej i sugerowałby dodanie słowa kluczowego' new', jeśli jest to zamierzone.

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

Wreszcie wpływ na użycie, wynik poniższego przykładu porzuca wartość początkową ustawioną przez konstruktor klasy nadrzędnej. i to jest to, co Re-sharper próbuje cię ostrzec, wartości ustawione w konstruktorze klasy nadrzędnej są otwarte do nadpisania przez konstruktor klasy podrzędnej, który jest wywoływany tuż po konstruktorze klasy nadrzędnej .

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
Author: BTE,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-28 18:48:23

Strzeżcie się ślepo podążać za radą Resharpera i zapieczętować klasę! Jeśli jest to model w kodzie EF najpierw usunie wirtualne słowo kluczowe, a to wyłączy leniwe Ładowanie jego relacji.

    public **virtual** User User{ get; set; }
 3
Author: typhon04,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-05 19:15:39

Jednym z ważnych brakujących bitów jest, jaki jest prawidłowy sposób rozwiązania tego problemu?

Jak wyjaśnił Greg , głównym problemem jest to, że konstruktor klasy bazowej wywoła Wirtualnego członka przed zbudowaniem klasy pochodnej.

Poniższy kod, zaczerpnięty z MSDN ' s constructor design guidelines , pokazuje ten problem.

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

Kiedy tworzona jest nowa instancja DerivedFromBad, konstruktor klasy bazowej wywołuje DisplayState i pokazuje BadBaseClass ponieważ pole nie zostało jeszcze zaktualizowane przez konstruktor Pochodny.

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

Ulepszona implementacja usuwa metodę wirtualną z konstruktora klasy bazowej i używa metody Initialize. Tworzenie nowej instancji DerivedFromBetter wyświetla oczekiwany "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);
    }
}
 2
Author: Gustavo Mori,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-02 19:47:23

Jest różnica między C++ i C# w tym konkretnym przypadku. W C++ Obiekt nie jest inicjowany i dlatego nie jest bezpieczne wywołanie funkcji virutal wewnątrz konstruktora. W C# gdy obiekt klasy jest tworzony, wszystkie jego członkowie są inicjalizowane zerem. Możliwe jest wywołanie funkcji wirtualnej w konstruktorze, ale jeśli będziesz mógł uzyskać dostęp do członków, które nadal są zerowe. Jeśli nie potrzebujesz dostępu do członków, jest całkiem bezpieczne wywołanie funkcji wirtualnej w C#.

 1
Author: Yuval Peled,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-23 07:58:58

Dodam tylko moje przemyślenia. Jeśli zawsze inicjalizujesz pole prywatne, gdy je zdefiniujesz, tego problemu należy unikać. Przynajmniej poniższy kod działa jak urok:

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
Author: Jim Ma,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-10-14 16:23:58

Kolejną ciekawą rzeczą, którą odkryłem, jest to, że błąd ReSharper może być "spełniony", robiąc coś takiego jak poniżej, co jest dla mnie głupie (jednak, jak wspomniano wcześniej, nadal nie jest dobrym pomysłem, aby wywoływać wirtualne prop/metody w 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";
   }

}

 0
Author: adityap,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-05-22 16:50:58

Dodałbym metodę Initialize() do klasy bazowej, a następnie wywołałbym ją z pochodnych konstruktorów. Ta metoda wywoła dowolne wirtualne / abstrakcyjne metody/właściwości po wykonaniu wszystkich konstruktorów:)

 -1
Author: Ross,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-13 21:14:46