polimorfico metodo destructores constructores c# constructor warnings resharper virtual-functions

c# - destructores - metodo constructor



Llamada de miembro virtual en un constructor (17)

¡Cuidado con seguir ciegamente el consejo 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 lenta de sus relaciones.

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

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

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


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

También en .NET, los objetos no cambian el tipo a medida que se construyen, sino que comienzan como el tipo más derivado, con 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 combina estos dos hechos, queda con el problema de que si realiza una llamada de 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 haya sido ejecutar, y por lo tanto no puede estar en un estado adecuado para tener ese método llamado.

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


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

Tenga en cuenta que esto es solo una advertencia para hacerle prestar atención y asegurarse de que todo está bien. Hay casos de uso reales para este escenario, solo tiene que 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 para que no quieras hacer eso. Aquí hay un contraejemplo donde quizás querrí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 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 { }


Hay una diferencia entre C ++ y C # en este caso específico. En C ++, el objeto no está inicializado y, por lo tanto, no es seguro llamar una función viral dentro de un constructor. En C # cuando se crea un objeto de clase, todos sus miembros tienen cero inicialización. 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 #.


La advertencia es un recordatorio de que es probable que los miembros virtuales se sobrescriban en la clase derivada. En ese caso, todo lo que la clase principal haya hecho a un miembro virtual se deshará o cambiará al anular la clase secundaria. Mira el pequeño ejemplo de golpe para mayor claridad.

La clase principal a continuación intenta establecer un valor para un miembro virtual en su constructor. Y esto activará una advertencia Re-más aguda, veamos en 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 una 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, la salida del ejemplo a continuación abandona el valor inicial establecido por el constructor de la clase principal. Y esto es lo que Re-agudo intenta advertirle , 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 primaria .

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


Las razones de la advertencia ya están descritas, pero ¿cómo arreglaría la advertencia? Tienes que sellar la clase o el 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 puedes sellar el método Foo:

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


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

Cuando está en el constructor para algún objeto en C #, ese objeto existe en una forma completamente inicializada (simplemente no "construida"), como su tipo totalmente 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á a cualquier anulación en B, si se proporciona una.

Incluso si configura A y B intencionalmente de esta manera, entendiendo completamente el comportamiento del sistema, podría sufrir un shock más adelante. Digamos que llamó a las 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 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 las funciones virtuales en los constructores, ya que las reglas son muy diferentes entre C #, C ++ y Java. ¡Sus programadores pueden no saber qué esperar!


Otra cosa interesante que encontré es que el error ReSharper puede "satisfacerse" haciendo algo como lo siguiente que es estúpido para mí (sin embargo, como mencioné anteriormente, todavía no es una buena idea llamar prop virtual / métodos 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"; }

}


Porque hasta que el constructor no haya terminado de ejecutarse, el objeto no está totalmente instanciado. Cualquier miembro referenciado por la función virtual no puede ser inicializado. En C ++, cuando estás en un constructor, this solo se refiere al tipo estático del constructor en el que estás, y no al tipo dinámico real del objeto que se está creando. Esto significa que la llamada a la función virtual puede que ni siquiera llegue a donde espera que lo haga.


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

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


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 / abstracta DESPUÉS de que todos los constructores hayan sido ejecutados :)


Solo para añadir mis pensamientos. Si siempre inicializa el campo privado cuando lo define, este problema debería evitarse. Al menos el código de abajo funciona como un amuleto:

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


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

Sin embargo, si su diseño cumple con el principio de sustitución de Liskov, no se hará daño. Probablemente es por eso que es tolerado: 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 en las circunstancias. Por ejemplo, en C ++ / CLI, los constructores están envueltos en un código que llamará a Dispose en el objeto parcialmente construido si la construcción falla. En estos casos, llamar a Dispose es a menudo necesario para evitar fugas de recursos, pero los métodos de Dispose deben estar preparados para la posibilidad de que el objeto sobre el que se ejecutan no se haya construido completamente.


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

Como explicó Greg , 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 de 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); } }


Para responder a su pregunta, considere esta pregunta: ¿qué se imprimirá el siguiente código cuando se ejemplifique el 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()); //NullReferenceException!?! } }

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 el código antes de que se hayan inicializado por completo.