web-dev-qa-db-fra.com

Appel de membre virtuel dans un constructeur

ReSharper m'avertit qu'un appel à un membre virtuel a été appelé par le constructeur de mes objets.

Pourquoi serait-ce quelque chose à ne pas faire?

1244
JasonS

Lorsqu'un objet écrit en C # est construit, 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 Eric Le blog de Lippert pour savoir pourquoi c'est ).

De plus, dans .NET, les objets ne changent pas de type lors de leur construction, mais commencent comme type le plus dérivé, la table de méthodes correspondant au type le plus dérivé. Cela signifie que les appels de méthodes virtuelles s'exécutent toujours sur le type le plus dérivé.

Lorsque vous combinez ces deux faits, vous vous retrouvez avec le problème suivant: si vous appelez une méthode virtuelle dans un constructeur et que ce n'est pas le type le plus dérivé de sa hiérarchie d'héritage, il sera appelé par une classe dont le constructeur n'a pas été exécuter, et peut donc ne pas être dans un état approprié pour faire appeler cette méthode.

Bien entendu, ce problème est atténué si vous marquez votre classe comme étant scellée afin de s'assurer qu'il s'agit du type le plus dérivé de la hiérarchie d'héritage - auquel cas il est parfaitement sûr d'appeler la méthode virtuelle.

1116
Greg Beech

Pour répondre à votre question, réfléchissez à cette question: que le code ci-dessous affichera-t-il 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 une NullReferenceException sera levée, car 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é que les objets hérités exécutent le code avant leur initialisation complète.

593
Matt Howells

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

Lorsque vous êtes dans le constructeur d'un objet en C #, cet objet existe sous une forme entièrement initialisée (mais pas "construite"), en tant que 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 à partir du constructeur de A, le problème sera résolu en une substitution dans B, si elle est fournie.

Même si vous avez intentionnellement configuré A et B comme ceci, en comprenant parfaitement le comportement du système, vous pourriez vous retrouver avec un choc plus tard. Supposons que vous appeliez des fonctions virtuelles dans le constructeur de B, "sachant" qu'elles seraient gérées par B ou A selon les cas. Ensuite, le temps passe et quelqu'un d'autre décide de définir C et de remplacer certaines des fonctions virtuelles. Tout à coup, le constructeur de B finit par appeler le code en C, ce qui pourrait entraîner un comportement assez surprenant.

Quoi qu’il en soit, c’est probablement une bonne idée d’éviter les fonctions virtuelles dans les constructeurs, car les règles are sont si différentes entre C #, C++ et Java. Vos programmeurs peuvent ne pas savoir à quoi s'attendre!

157
Lloyd

Les raisons de l'avertissement sont déjà décrites, mais comment remédier à l'avertissement? Vous devez sceller la classe ou le membre virtuel.

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

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

Vous pouvez sceller la 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();
    }
  }
84
Ilya Ryzhenkov

En C #, le constructeur d'une classe de base s'exécute avant le constructeur de la classe dérivée, ainsi tous les champs d'instance qu'une classe dérivée pourrait utiliser dans la variable virtuelle éventuellement remplacée Les membres ne sont pas encore initialisés.

Notez que ceci est juste un warning pour vous faire prêter attention et vous assurer que tout va bien. Il y a des cas d'utilisation réels pour ce scénario, il vous suffit de documenter le comportement du membre virtuel qu'il ne peut utiliser aucun champ d'instance déclaré dans une classe dérivée où se trouve le constructeur qui l'appelle.

17
Alex Lyman

Il existe des réponses bien écrites ci-dessus expliquant pourquoi vous ne voudriez pas vouloir faire cela. Voici un contre-exemple dans lequel vous voudriez peut-être (traduit en C # à partir de Conception pratique orientée objet en Ruby par Sandi Metz, page 126).

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

(Pour être juste, il existe probablement des moyens plus intelligents de le faire via des conteneurs d'injection de dépendance ou des initialiseurs d'objet ...)

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

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

À ce stade, l'objet peut ne pas être entièrement construit et les invariants attendus par les méthodes peuvent ne pas encore être vérifiés.

6
David Pierre

Votre constructeur peut (ultérieurement, dans une extension de votre logiciel) être appelé à partir du constructeur d’une sous-classe qui remplace la méthode virtuelle. Maintenant, pas l'implémentation de la fonction par la sous-classe, mais l'implémentation de la classe de base sera appelée. Il n’a donc pas de sens d’appeler une fonction virtuelle ici.

Toutefois, si votre conception respecte le principe de substitution de Liskov, aucun préjudice ne sera causé. C'est probablement pourquoi c'est toléré - un avertissement, pas une erreur.

5
xtofl

Un aspect important de cette question, auquel d’autres réponses n’ont pas encore répondu, 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 attendent de lui . Dans de tels cas, le concepteur de la classe dérivée est responsable de s'assurer que toutes les méthodes qui sont exécutées avant la fin de la construction se comporteront de manière aussi judicieuse que possible dans les circonstances. Par exemple, en C++/CLI, les constructeurs sont encapsulés dans un code qui appellera Dispose sur l'objet partiellement construit en cas d'échec de la construction. L'appel de Dispose dans de tels cas est souvent nécessaire pour éviter les fuites de ressources, mais les méthodes Dispose doivent être préparées à la possibilité que l'objet sur lequel elles sont exécutées n'ait pas été entièrement construit.

5
supercat

Tant que le constructeur n'a pas terminé son exécution, l'objet n'est pas totalement instancié. Tous 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 ne fait référence qu'au type statique du constructeur dans lequel vous vous trouvez, et non au type dynamique réel de l'objet en cours de création. Cela signifie que l'appel de fonction virtuelle peut même ne pas aller où vous vous attendez.

5
1800 INFORMATION

Il manque un élément important: quelle est la bonne façon de résoudre ce problème?

Comme Greg a expliqué , le problème fondamental est qu'un constructeur de classe de base invoquerait le membre virtuel avant la construction de la classe dérivée.

Le code suivant, extrait de consignes de conception du constructeur de MSDN , 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'une nouvelle instance de DerivedFromBad est créée, le constructeur de la classe de base appelle DisplayState et affiche BadBaseClass car 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 la 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);
    }
}
3
Gustavo Mori

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 parent a fait à un membre virtuel sera annulé ou modifié en remplaçant la classe enfant. Regardez le petit exemple du coup pour plus de clarté

La classe parent ci-dessous tente de définir la valeur sur un membre virtuel dans son constructeur. Et cela déclenchera un avertissement Re-sharper, voyons dans 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 enfant ici remplace la propriété parent. Si cette propriété n'était pas marquée comme virtuelle, le compilateur avertirait que la propriété masque la propriété de la classe parente et vous suggère d'ajouter le mot clé "new" s'il est intentionnel.

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

Enfin, pour l’impact sur l’utilisation, la sortie de l’exemple ci-dessous abandonne la valeur initiale définie par le constructeur de la classe parent. Et c'est ce que Re-sharper tente de vous avertir, les valeurs définies sur le constructeur de la classe Parent peuvent être écrasées par le constructeur de la classe enfant, appelé droit après le constructeur de la 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
BTE

Attention à ne pas suivre aveuglément les conseils de Resharper et à sceller la classe! Si c'est un modèle dans EF Code First, il supprimera le mot clé virtual et désactivera le chargement paresseux de ses relations.

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

Juste pour ajouter mes pensées. Si vous initialisez toujours le champ privé lorsque vous le définissez, ce problème doit être évité. Au moins au-dessous de 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
Jim Ma

Il y a une différence entre C++ et C # dans ce cas particulier. En C++, l'objet n'est pas initialisé et il est donc dangereux d'appeler une fonction virtuelle dans un constructeur. En C #, lorsqu'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 pouviez accéder à des membres encore nuls. Si vous n'avez pas besoin d'accéder aux membres, il est assez sûr d'appeler une fonction virtuelle en C #.

1
Yuval Peled

Une autre chose intéressante que j'ai trouvée est que l'erreur ReSharper peut être "satisfaite" en faisant quelque chose comme ci-dessous qui est stupide pour moi (cependant, comme mentionné par de nombreuses personnes plus tôt, ce n'est toujours pas une bonne idée d'appeler prop/methods virtuel 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
adityap