Question

ReSharper me prévient d'un appel à un membre virtuel à partir du constructeur de mes objets.

Pourquoi cela serait-il quelque chose à ne pas faire?

Était-ce utile?

La solution

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 ( consultez le blog d'Eric Lippert pour savoir pourquoi. ).

De plus, les objets .NET 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écuté et, par conséquent, peut ne pas être dans un état approprié pour que cette méthode soit appelée.

Ce problème est, bien sûr, 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 de l’héritage - auquel cas il est parfaitement sûr d’appeler la méthode virtuelle.

Autres conseils

Afin de répondre à votre question, posez-vous cette question: qu'est-ce que le code ci-dessous affichera lorsque l'objet Enfant 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()); //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.

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, elle sera résolue en une substitution dans B, si elle est fournie.

Même si vous avez intentionnellement configuré A et B comme ceci, tout en comprenant parfaitement le comportement du système, vous pourriez vous retrouver avec un choc plus tard. Dites que vous avez appelé des fonctions virtuelles dans le constructeur de B, "savoir" ils seraient traités par B ou A selon le 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 conduire à un comportement assez surprenant.

C’est quand même une bonne idée d’éviter les fonctions virtuelles dans les constructeurs, car les règles sont très différentes entre C #, C ++ et Java. Vos programmeurs peuvent ne pas savoir à quoi s'attendre!

Les raisons de l'avertissement sont déjà décrites, mais comment remédieriez-vous à 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();
    }
  }

En C #, le constructeur d'une classe de base 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 qu'il ne s'agit que d'un avertissement pour vous faire prêter attention et vous assurer que tout va bien. Il existe des cas d'utilisation réels pour ce scénario. Il vous suffit de documenter le comportement du membre virtuel pour qu'il ne puisse utiliser aucun champ d'instance déclaré dans une classe dérivée où le constructeur qui l'appelle est <. / p>

Il existe des réponses bien écrites ci-dessus expliquant pourquoi vous ne souhaitez pas faire cela. Voici un contre-exemple dans lequel vous voudriez peut-être faire cela (traduit en C # à partir de Conception pratique orientée objet en Ruby de 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 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 { }

Oui, il est généralement mauvais d'appeler une 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 peuvent ne pas encore être conservés.

Etant donné 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 fait uniquement référence 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 la fonction virtuelle pourrait même ne pas aller où vous vous attendez.

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 ici une fonction virtuelle.

Toutefois, si votre conception respecte le principe de substitution de Liskov, aucun préjudice ne sera causé. C’est probablement pour cela qu’il est toléré - un avertissement, pas une erreur.

Un autre 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 s'attendent à ce qu'elle fasse . Dans de tels cas, le concepteur de la classe dérivée est responsable de l’assurance que toutes les méthodes exécutées avant l’achèvement de la construction se comporteront de la manière la plus judicieuse possible. Par exemple, dans C ++ / CLI, les constructeurs sont encapsulés dans un code qui appellera Dispose sur l'objet partiellement construit si la construction échoue. 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. .

L'avertissement est un rappel que les membres virtuels risquent d'être remplacés par 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 exemple 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 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 dans le constructeur de la classe Parent peuvent être écrasées par le constructeur de la classe enfant, appelé juste 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"
    }
} 

Attention à ne pas suivre aveuglément les conseils de Resharper et à sceller le cours! 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; }

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

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

Le code suivant, extrait de Les instructions de conception de constructeur de MSDN illustrent 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);
    }
}

Quand 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é mise à 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 résultat attendu "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);
    }
}

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 #.

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

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

}

Je voudrais simplement ajouter une méthode Initialize () à la classe de base, puis l’appeler à partir de constructeurs dérivés. Cette méthode appellera toutes les méthodes / propriétés virtuelles / abstraites APRÈS que tous les constructeurs aient été exécutés:)

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top