Pregunta

Recibo una advertencia de ReSharper sobre una llamada a un miembro virtual desde mi constructor de objetos.

¿Por qué esto sería algo que no se debe hacer?

¿Fue útil?

Solución

Cuando se construye un objeto escrito en C#, lo que sucede es que los inicializadores se ejecutan en orden desde la clase más derivada hasta la clase base, y luego los constructores se ejecutan en orden desde la clase base hasta la clase más derivada (consulte el blog de Eric Lippert para obtener detalles sobre por qué esto es así.).

Además, en .NET los objetos no cambian de tipo a medida que se construyen, sino que comienzan como el tipo más derivado, siendo la tabla de métodos para el tipo más derivado.Esto significa que las llamadas a métodos virtuales siempre se ejecutan en el tipo más derivado.

Cuando combinas estos dos hechos, te quedas con el problema de que si realizas una llamada a un método virtual en un constructor, y no es el tipo más derivado en su jerarquía de herencia, se llamará en una clase cuyo constructor no ha sido ejecutarse y, por lo tanto, es posible que no esté en un estado adecuado para llamar a ese método.

Este problema, por supuesto, se mitiga si marca su clase como sellada para asegurarse de que sea el tipo más derivado en la jerarquía de herencia, en cuyo caso es perfectamente seguro llamar al método virtual.

Otros consejos

Para responder a su pregunta, considere esta pregunta:¿Qué se imprimirá el siguiente código cuando Child ¿Se crea una instancia del objeto?

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 respuesta es que en realidad un NullReferenceException será arrojado, porque foo es nulo. El constructor base de un objeto se llama antes que su propio constructor..Al tener un virtual Al llamar al constructor de un objeto, está introduciendo la posibilidad de que los objetos heredados ejecuten código antes de que se hayan inicializado por completo.

Las reglas de C# son muy diferentes a las de Java y C++.

Cuando estás en el constructor de algún objeto en C#, ese objeto existe en una forma completamente inicializada (pero no "construida"), como su tipo completamente derivado.

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

Esto significa que si llama a una función virtual desde el constructor de A, se resolverá en cualquier anulación en B, si se proporciona una.

Incluso si configura intencionalmente A y B de esta manera, entendiendo completamente el comportamiento del sistema, podría sufrir una sorpresa más adelante.Supongamos que llamó a funciones virtuales en el constructor de B, "sabiendo" que B o A las manejarían según corresponda.Luego pasa el tiempo y alguien más decide que necesita definir C y anular algunas de las funciones virtuales allí.De repente, el constructor de B termina llamando al código en C, lo que podría llevar a un comportamiento bastante sorprendente.

Probablemente sea una buena idea evitar funciones virtuales en los constructores de todos modos, ya que las reglas son tan diferente entre C#, C++ y Java.¡Es posible que sus programadores no sepan qué esperar!

Los motivos de la advertencia ya se describen, pero ¿cómo solucionaría la advertencia?Tienes que sellar ya sea clase o miembro virtual.

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

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

Puedes sellar clase A:

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

O puedes sellar el método Foo:

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

    protected sealed override void Foo()
    {
      base.Foo();
    }
  }

En C#, se ejecuta el constructor de una clase base antes constructor de la clase derivada, por lo que cualquier campo de instancia que una clase derivada pueda usar en el miembro virtual posiblemente anulado no se inicializa todavía.

Tenga en cuenta que esto es sólo un advertencia para que prestes atención y te asegures de que todo está bien.Hay casos de uso reales para este escenario, solo tienes que documentar el comportamiento del miembro virtual que no puede usar ningún campo de instancia declarado en una clase derivada debajo de donde está el constructor que lo llama.

Hay respuestas bien escritas arriba sobre por qué no lo haría quiero hacer eso.He aquí un contraejemplo en el que quizás haría quiero hacer eso (traducido a C# desde Diseño práctico orientado a objetos en Ruby por Sandi Metz, pág.126).

Tenga en cuenta que GetDependency() no toca ninguna variable de instancia.Sería estático si los métodos estáticos pudieran ser virtuales.

(Para ser justos, probablemente existan formas más inteligentes de hacer esto mediante contenedores de inyección de dependencia o inicializadores de objetos...)

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

Sí, generalmente es malo llamar a un método virtual en el constructor.

En este punto, es posible que el objeto aún no esté completamente construido y que las invariantes esperadas por los métodos aún no se cumplan.

Porque hasta que el constructor haya completado la ejecución, no se creará una instancia completa del objeto.Es posible que los miembros a los que hace referencia la función virtual no se inicialicen.En C++, cuando estás en un constructor, this solo se refiere al tipo estático del constructor en el que se encuentra, y no al tipo dinámico real del objeto que se está creando.Esto significa que es posible que la llamada a la función virtual ni siquiera llegue a donde espera.

Su constructor puede (más adelante, en una extensión de su software) ser llamado desde el constructor de una subclase que anula el método virtual.Ahora no se llamará a la implementación de la función de la subclase, sino a la implementación de la clase base.Así que realmente no tiene sentido llamar aquí a una función virtual.

Sin embargo, si su diseño satisface el principio de sustitución de Liskov, no se producirá ningún daño.Probablemente por eso se tolera: una advertencia, no un error.

Un aspecto importante de esta pregunta que otras respuestas aún no han abordado es que es seguro para una clase base llamar a miembros virtuales desde su constructor. si eso es lo que las clases derivadas esperan que haga.En tales casos, el diseñador de la clase derivada es responsable de garantizar que cualquier método que se ejecute antes de que se complete la construcción se comportará de la manera más sensata posible según las circunstancias.Por ejemplo, en C++/CLI, los constructores están incluidos en un código que llamará Dispose sobre el objeto parcialmente construido si la construcción falla.Vocación Dispose En tales casos, a menudo es necesario evitar fugas de recursos, pero Dispose Los métodos deben estar preparados para la posibilidad de que el objeto sobre el que se ejecutan no haya sido completamente construido.

La advertencia es un recordatorio de que es probable que los miembros virtuales se anulen en la clase derivada.En ese caso, cualquier cosa que la clase principal haya hecho a un miembro virtual se deshará o cambiará anulando la clase secundaria.Mire el pequeño ejemplo para mayor claridad.

La siguiente clase principal intenta establecer un valor para un miembro virtual en su constructor.Y esto activará la advertencia Re-Sharper, veamos el código:

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 clase secundaria aquí anula la propiedad principal.Si esta propiedad no estuviera marcada como virtual, el compilador advertiría que la propiedad oculta la propiedad en la clase principal y sugeriría que agregue la palabra clave "nueva" si es intencional.

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

Finalmente, el impacto en el uso, el resultado del siguiente ejemplo abandona el valor inicial establecido por el constructor de la clase principal.Y esto es lo que Re-Sharper intenta advertirte., los valores establecidos en el constructor de la clase principal están abiertos para ser sobrescritos por el constructor de la clase secundaria, que se llama justo después del constructor de la clase principal..

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

¡Cuidado con seguir ciegamente los consejos de Resharper y sellar la clase!Si es un modelo en EF Code First, eliminará la palabra clave virtual y eso deshabilitaría la carga diferida de sus relaciones.

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

Una parte importante que falta es: ¿cuál es la forma correcta de resolver este problema?

Como Greg explicó, la raíz del problema aquí es que un constructor de clase base invocaría al miembro virtual antes de que se haya construido la clase derivada.

El siguiente código, tomado de Directrices de diseño del constructor de MSDN, demuestra este problema.

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

Cuando una nueva instancia de DerivedFromBad se crea, el constructor de la clase base llama a DisplayState y muestra BadBaseClass porque el constructor derivado aún no ha actualizado el campo.

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

Una implementación mejorada elimina el método virtual del constructor de la clase base y utiliza un Initialize método.Creando una nueva instancia de DerivedFromBetter muestra el esperado "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);
    }
}

Hay una diferencia entre C++ y C# en este caso específico.En C++ el objeto no se inicializa y, por lo tanto, no es seguro llamar a una función virtual dentro de un constructor.En C#, cuando se crea un objeto de clase, todos sus miembros se inicializan en cero.Es posible llamar a una función virtual en el constructor, pero si puede acceder a miembros que aún son cero.Si no necesita acceder a los miembros, es bastante seguro llamar a una función virtual en C#.

Sólo para agregar mis pensamientos.Si siempre inicializa el campo privado al definirlo, se debe evitar este problema.Al menos el siguiente código funciona de maravilla:

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

Otra cosa interesante que encontré es que el error de ReSharper se puede "satisfacer" haciendo algo como lo siguiente, lo cual es tonto para mí (sin embargo, como muchos mencionaron anteriormente, todavía no es una buena idea llamar a accesorios/métodos virtuales en 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";
   }

}

Simplemente agregaría un método Initialize() a la clase base y luego lo llamaría desde constructores derivados.Ese método llamará a cualquier método/propiedad virtual/abstracto DESPUÉS de que se hayan ejecutado todos los constructores :)

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top