Llamada de miembro virtual en un constructor

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

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

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 a la clase base, y luego los constructores se ejecutan en orden desde la clase base hasta la clase más derivada ( ver el blog de Eric Lippert para más detalles en cuanto a por qué esto es ).

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

Cuando combina estos dos hechos, se queda con el problema de que si realiza 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 invocará en una clase cuyo constructor no haya sido invocado. ejecutar, y por lo tanto puede no estar en un estado adecuado para que se llame ese método.

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

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

 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 respuesta es que, de hecho, se lanzará una NullReferenceException , porque foo es nulo. El constructor base de un objeto se llama antes que su propio constructor . Al tener una llamada virtual en el 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 de las de Java y C ++.

Cuando se encuentra en el constructor para algún objeto en C #, ese objeto existe en una forma totalmente inicializada (simplemente 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á con cualquier anulación en B, si se proporciona una.

Incluso si configura intencionalmente A y B de esta manera, comprendiendo completamente el comportamiento del sistema, podría recibir un shock más adelante. Digamos que llamaste funciones virtuales en el constructor de B, “sabiendo” que serían manejadas por B o A, según corresponda. Luego pasa el tiempo y alguien más decide que necesitan definir C y anulan algunas de las funciones virtuales allí. De repente, el constructor de B termina llamando código en C, lo que podría llevar a un comportamiento bastante sorprendente.

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

Las razones de la advertencia ya están descritas, pero ¿cómo arreglarías la advertencia? Tienes que sellar una clase o un miembro virtual.

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

Puedes sellar la clase A:

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

O puede sellar el método Foo:

  class A : B { public A() { Foo(); // no warning } protected sealed override void Foo() { base.Foo(); } } 

En C #, el constructor de una clase base se ejecuta antes del constructor de la clase derivada, por lo que aún no se han inicializado los campos de instancia que una clase derivada pueda usar en el miembro virtual posiblemente anulado.

Tenga en cuenta que esto es solo una advertencia para que preste atención y asegúrese de que todo esté bien. Hay casos de uso reales para este escenario, solo debe documentar el comportamiento del miembro virtual de que no puede usar ningún campo de instancia declarado en una clase derivada a continuación donde está el constructor que lo llama.

Hay respuestas bien escritas arriba sobre por qué no querrías hacer eso. Aquí hay un contraejemplo en el que tal vez le gustaría hacer eso (traducido a C # del Diseño práctico orientado a objetos en Ruby por Sandi Metz, p 126).

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

(Para ser justos, probablemente hay formas más inteligentes de hacerlo a través de contenedores de dependency injection 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 al método virtual en el constructor.

En este punto, el objeto puede no estar completamente construido aún, y las invariantes esperadas por los métodos pueden no mantenerse todavía.

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

Sin embargo, si su diseño satisface el principio de Sustitución de Liskov, no se dañará. Probablemente es por eso que 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 bajo las circunstancias. Por ejemplo, en C ++ / CLI, los constructores están envueltos en código que llamará a Dispose en el objeto parcialmente construido si falla la construcción. Llamar a Dispose en tales casos a menudo es necesario para evitar fugas de recursos, pero los métodos de Dispose deben prepararse para la posibilidad de que el objeto sobre el que se ejecutan no se haya construido por completo.

Porque hasta que el constructor haya completado la ejecución, el objeto no está completamente instanciado. Los miembros a los que hace referencia la función virtual no se pueden inicializar. En C ++, cuando estás en un constructor, this solo se refiere al tipo estático del constructor en el que te encuentras, y no al tipo dynamic real del objeto que se está creando. Esto significa que la llamada a la función virtual puede que ni siquiera vaya donde usted espera.

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 le haya hecho a un miembro virtual se deshará o modificará al anular la clase secundaria. Mira el pequeño ejemplo de golpe para mayor claridad

La siguiente clase principal intenta establecer el valor de 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 se marcó como virtual, el comstackdor advertirá que la propiedad oculta la propiedad de la clase principal y le sugerirá 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 padre. Y esto es lo que Re-sharper intenta advertirle , los valores establecidos en el constructor de la clase Parent están abiertos para ser sobreescritos por el constructor de clase hijo que se llama justo después del constructor de la clase padre .

 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 el consejo de Resharper y hacer que la clase sea sellada! Si se trata de un modelo en EF Code First, eliminará la palabra clave virtual y eso inhabilitará la carga diferida de sus relaciones.

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

Un aspecto importante que falta es, ¿cuál es la forma correcta de resolver este problema?

Como Greg explicó , el problema de raíz 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 las pautas 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 se crea una nueva instancia de DerivedFromBad , 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 método Initialize . La creación de 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 virutal 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 todavía son cero. Si no necesita acceder a los miembros, es bastante seguro llamar a una función virtual en C #.

Solo para agregar mis pensamientos. Si siempre inicializa el campo privado cuando lo define, este problema debe evitarse. Al menos debajo del código funciona como un encanto:

 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 puede ser “satisfecho” haciendo algo como lo que está por debajo, lo cual es estúpido para mí (sin embargo, como mencioné anteriormente, todavía no es una buena idea llamar a prop / methods 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 / propiedades virtuales / abstractos DESPUÉS de que se hayan ejecutado todos los constructores 🙂