Question Appel de membre virtuel dans un constructeur


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

Pourquoi serait-ce quelque chose à ne pas faire?


1146
2017-09-23 07:11


origine


Réponses:


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 le blog d'Eric Lippert pour plus de détails sur les raisons pour lesquelles).

Les objets .NET ne changent pas non plus de type car ils sont construits, mais commencent comme le type le plus dérivé, la table de méthode étant pour le type le plus dérivé. Cela signifie que les appels de méthode virtuelle s'exécutent toujours sur le type le plus dérivé.

Lorsque vous combinez ces deux faits, le problème est que si vous faites un appel de méthode virtuelle dans un constructeur et que ce n'est pas le 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é courir, et ne peut donc pas être dans un état approprié pour avoir cette méthode appelée.

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


1036
2017-09-23 07:21



Afin de répondre à votre question, réfléchissez à la question suivante: que signifiera le code ci-dessous lorsque le Child l'objet est 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());
    }
}

La réponse est que, en fait, un NullReferenceException sera jeté, parce que foo est nul. Le constructeur de base d'un objet est appelé avant son propre constructeur. En ayant un virtual appeler dans le constructeur d'un objet vous introduisez la possibilité que les objets héritiers exécuteront du code avant qu'ils aient été entièrement initialisés.


478
2017-09-23 07:17



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

Lorsque vous êtes dans le constructeur d'un objet en C #, cet objet existe dans un formulaire entièrement initialisé (pas simplement "construit"), en tant que 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 à partir du constructeur de A, elle se résoudra à toute substitution dans B, si une fonction est fournie.

Même si vous configurez intentionnellement A et B de cette façon, en comprenant parfaitement le comportement du système, vous pourriez être en état de choc plus tard. Supposons que vous appeliez des fonctions virtuelles dans le constructeur de B, "sachant" qu'elles seraient traitées par B ou A, selon le cas. Puis le temps passe, et quelqu'un d'autre décide qu'ils ont besoin de définir C, et remplacer certaines fonctions virtuelles. Tout à coup, le constructeur de B finit par appeler le code en C, ce qui pourrait conduire à un comportement assez surprenant.

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


154
2017-09-23 07:36



Les raisons de l'avertissement sont déjà décrites, mais comment corrigeriez-vous l'avertissement? Vous devez sceller soit une classe ou un 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();
    }
  }

77
2017-09-23 13:20



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

Notez que c'est juste un Attention pour vous faire attention et assurez-vous 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 en dessous de l'endroit où le constructeur l'appelle.


16
2017-09-23 07:21



Il y a des réponses bien écrites ci-dessus pour la raison pour laquelle vous ne serait pas vouloir le faire. Voici un contre-exemple où peut-être vous aurait vouloir le faire (traduit en C # de Conception pratique orientée objet en 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 juste, il y a probablement des façons plus intelligentes de le faire via des conteneurs d'injection de dépendances ou des initialiseurs 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
2017-12-28 01:19



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

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


5
2017-09-23 07:15



Votre constructeur peut (plus tard, 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 de la sous-classe, mais l'implémentation de la classe de base sera appelée. Donc, cela n'a pas vraiment de sens d'appeler une fonction virtuelle ici.

Cependant, si votre conception répond au principe de substitution de Liskov, aucun mal ne sera fait. C'est probablement pourquoi c'est toléré - un avertissement, pas une erreur.


5
2017-09-23 07:25