Membre virtuel appel à un constructeur

Je reçois un avertissement de ReSharper à propos d'un appel à un membre virtuel de mon constructeur d'objets.

pourquoi ne pas le faire?

1166
demandé sur CodeNotFound 2008-09-23 11:11:30

17 réponses

quand un objet écrit en C# est construit, ce qui se passe c'est que les initialiseurs s'exécutent dans l'ordre de la classe la plus dérivée à la classe de base, puis les constructeurs s'exécutent dans l'ordre de la classe de base à la classe la plus dérivée ( voir le blog D'Eric Lippert pour plus de détails sur la raison pour laquelle c'est ).

aussi dans .net les objets ne changent pas de type car ils sont construits, mais commencent comme le type le plus dérivé, avec la table de méthode étant pour le plus type dérivé. Cela signifie que les appels de méthode virtuelle tournent toujours sur le type le plus dérivé.

lorsque vous combinez ces deux faits, vous vous retrouvez avec le problème que si vous faites un appel de méthode virtuelle dans un constructeur, et qu'il ne s'agit pas du type le plus dérivé dans sa hiérarchie d'héritage, il sera appelé sur une classe dont le constructeur n'a pas été lancé, et donc ne peut pas être dans un état approprié pour avoir cette méthode appelée.

Ce problème est, de bien sûr, atténués si vous marquez votre classe comme scellées pour s'assurer que c'est la plus type dérivé dans la hiérarchie d'héritage - dans ce cas, il est parfaitement possible d'appeler la méthode virtuelle.

1055
répondu Greg Beech 2017-03-02 16:22:43

pour répondre à votre question, considérez cette question: Quel sera le code imprimé ci-dessous lorsque l'objet Child sera instancié?

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 réponse est qu'en fait un NullReferenceException sera lancé, parce que foo est nul. le constructeur de base d'un objet est appelé avant son propre constructeur . En ayant un appel virtual dans le constructeur d'un objet vous introduisez la possibilité qu'hériter des objets exécutera le code avant qu'ils n'aient été entièrement initialisés.

500
répondu Matt Howells 2018-09-13 15:45:08

les règles de C# sont très différentes de celles de Java et C++.

lorsque vous êtes dans le constructeur pour un objet en C#, Cet objet existe sous une forme entièrement initialisée (mais pas" construite"), comme son type entièrement dérivé.

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

cela signifie que si vous appelez une fonction virtuelle du constructeur de A, elle se résoudra à n'importe quel override dans B, si une est fournie.

même si vous avez intentionnellement mis en place un et B comme ceci, comprendre pleinement le comportement du système, vous pourriez être en état de choc plus tard. Disons que vous avez appelé les fonctions virtuelles dans le constructeur de B, "sachant" qu'elles seraient gérées par B ou a comme approprié. Puis le temps passe, et quelqu'un d'autre décide qu'ils doivent définir C, et de remplacer certaines des fonctions virtuelles. Tout d'un coup le constructeur de B finit par appeler du code en C, ce qui pourrait conduire à un comportement assez surprenant.

il est probablement une bonne idée d'éviter fonctions virtuelles dans les constructeurs de toute façon, puisque les règles sont si différentes entre le C#, C++ et Java. Vos programmeurs ne peuvent pas savoir à quoi s'attendre!

155
répondu Lloyd 2011-08-19 08:56:36

les Raisons de l'avertissement sont déjà décrites, mais comment voulez-vous résoudre l'avertissement? Vous devez sceller soit classe ou membre virtuel.

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

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

vous pouvez sceller Classe A:

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

ou vous pouvez sceller la méthode Foo:

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

    protected sealed override void Foo()
    {
      base.Foo();
    }
  }
80
répondu Ilya Ryzhenkov 2008-09-23 13:20:22

dans C#, Un constructeur de classe de base exécute avant le constructeur de classe dérivé, de sorte que les champs d'instance qu'une classe dérivée pourrait utiliser dans le membre virtuel éventuellement surchargé ne sont pas encore initialisés.

notez que ce n'est qu'un avertissement pour vous faire prêter attention et s'assurer que tout va bien. Il ya des cas d'utilisation réelle pour ce scénario, vous avez juste à document le comportement de le membre virtuel qu'il ne peut pas utiliser n'importe quels champs d'instance déclarés dans une classe dérivée ci-dessous où le constructeur l'appelle.

16
répondu Alex Lyman 2008-09-23 07:21:04

Il ya des réponses bien écrites ci-dessus pour la raison pour laquelle vous ne serait pas veulent faire cela. Voici un contre-exemple où peut-être vous voudrait faire cela (traduit en C# de Design orienté objet pratique dans Ruby par Sandi Metz, p. 126).

notez que GetDependency() ne touche Aucune variable d'instance. Ce serait statique si les méthodes statiques pouvaient être virtuelles.

(Pour être honnête, il y a probablement des façons plus intelligentes de le faire via des conteneurs d'injection de dépendances ou des initialisateurs d'objets...)

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
répondu Josh Kodroff 2012-12-28 01:19:13

Oui, il est généralement mauvais d'appeler méthode virtuelle dans le constructeur.

à ce point, l'objet peut ne pas être entièrement construit encore, et les invariants attendus par les méthodes peuvent ne pas tenir encore.

5
répondu David Pierre 2008-09-23 07:15:32

Votre constructeur peut (plus tard, dans une extension de votre logiciel) s'appelle le constructeur d'une sous-classe qui surcharge la méthode virtuelle. Maintenant, pas la mise en œuvre de la fonction par la sous-classe, mais la mise en œuvre de la classe de base sera appelée. Il n'est donc pas vraiment logique d'appeler une fonction virtuelle ici.

cependant, si votre dessin satisfait au principe de substitution de Liskov, aucun mal ne sera fait. Probablement c'est pourquoi il est toléré un avertissement, pas une erreur.

5
répondu xtofl 2008-09-23 07:25:01

un aspect important de cette question que d'autres réponses n'ont pas encore abordé est qu'il est sûr pour une classe de base d'appeler des membres virtuels à partir de son constructeur si c'est ce que les classes dérivées s'attendent à ce qu'elle fasse . Dans ce cas, le concepteur de la classe dérivée est responsable de s'assurer que toutes les méthodes qui sont utilisées avant que la construction ne soit terminée se comporteront aussi judicieusement qu'elles le peuvent dans les circonstances. Par exemple, en C++ / CLI, les constructeurs sont enveloppés dans un code qui appellera Dispose sur l'objet partiellement construit si la construction échoue. Appeler Dispose dans de tels cas est souvent nécessaire pour prévenir les fuites de ressources, mais les méthodes Dispose doivent être préparées pour la possibilité que l'objet sur lequel elles sont exécutées peut ne pas avoir été entièrement construit.

5
répondu supercat 2012-10-25 20:33:34

parce que tant que le constructeur n'a pas terminé l'exécution, l'objet n'est pas pleinement instancié. Les membres référencés par la fonction virtuelle ne peuvent pas être initialisés. En C++, lorsque vous êtes dans un constructeur, this se réfère uniquement au type statique du constructeur dans lequel vous êtes, et non au type dynamique réel de l'objet qui est créé. Cela signifie que l'appel de fonction virtuelle pourrait même ne pas aller là où vous l'attendez.

4
répondu 1800 INFORMATION 2008-09-23 07:14:10

L'avertissement est un rappel que les membres virtuels sont susceptibles d'être remplacés sur la classe dérivée. Dans ce cas, tout ce que la classe des parents a fait à un membre virtuel sera défait ou modifié par l'annulation de la classe des enfants. Regardez le petit exemple de coup pour la clarté

La classe parente ci-dessous tente de définir la valeur d'un membre virtuel sur son constructeur. Et cela déclenchera un avertissement re-sharper, voyons sur le code:

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 des enfants ici remplace la propriété parent. Si cette propriété n'était pas marquée virtual, le compilateur avertirait que la propriété se cache sur la classe parent et suggérerait d'ajouter le mot-clé 'new' si c'est intentionnel.

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

enfin l'impact sur l'utilisation, la sortie de l'exemple ci-dessous abandonne la valeur initiale fixée par le constructeur de classe parent. et c'est ce que re-sharper tente de vous prévenir , valeurs définies sur le Parent les constructeurs de classe sont ouverts pour être écrasés par le constructeur de classe enfant qui est appelé juste après le constructeur de classe parent .

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
répondu BTE 2016-08-28 18:48:23

méfiez-vous de suivre aveuglément les conseils de Resharper et de faire sceller la classe! Si c'est un model en code EF en premier, il supprimera le mot-clé virtuel et cela désactivera le chargement paresseux de ses relations.

    public **virtual** User User{ get; set; }
3
répondu typhon04 2017-10-05 19:15:39

Un important morceau est, quelle est la bonne façon de résoudre ce problème?

comme Greg a expliqué , le problème racine ici est qu'un constructeur de classe de base invoquerait le membre virtuel avant que la classe dérivée ait été construite.

Le code suivant, tiré de MSDN constructeur de lignes directrices de conception , illustre ce problème.

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

Lorsqu'un une nouvelle instance de DerivedFromBad est créée, la classe de base du constructeur appelle DisplayState et affiche BadBaseClass parce que le champ n'a pas encore été mis à jour par le constructeur dérivé.

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

une implémentation améliorée supprime la méthode virtuelle du constructeur de classe de base, et utilise une méthode Initialize . La création d'une nouvelle instance de DerivedFromBetter affiche le "DerivedFromBetter" attendu

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
répondu Gustavo Mori 2018-04-02 19:47:23

il y a une différence entre C++ et C# dans ce cas précis. En C++ , l'objet n'est pas initialisé et il est donc dangereux d'appeler une fonction virutale à l'intérieur d'un constructeur. Dans C# quand un objet de classe est créé, tous ses membres sont initialisés à zéro. Il est possible d'appeler une fonction virtuelle dans le constructeur, mais si vous peut-accès membres qui sont toujours de zéro. Si vous n'avez pas besoin d'accéder aux membres, il est tout à fait possible d'appeler une fonction virtuelle en C#.

1
répondu Yuval Peled 2008-09-23 07:58:58

Juste pour ajouter mes pensées. Si vous initialisez toujours le champ privé lorsque vous le définissez, ce problème devrait être évité. Au moins en dessous du code fonctionne comme un charme:

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
répondu Jim Ma 2015-10-14 16:23:58

une autre chose intéressante que j'ai trouvé est que L'erreur de ReSharper peut être 'satisfait' en faisant quelque chose comme ci-dessous qui est stupide pour moi (cependant, comme mentionné par beaucoup plus tôt, il n'est toujours pas une bonne idée d'appeler prop/méthodes virtuelles dans 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
répondu adityap 2014-05-22 16:50:58

je voudrais juste ajouter une méthode Initialize() à la classe de base et ensuite appeler cela des constructeurs dérivés. Cette méthode appellera n'importe quelles méthodes/propriétés virtuelles/abstraites après que tous les constructeurs auront été exécutés:)

-1
répondu Ross 2017-12-13 21:14:46