polimorfico - metodo destructor c#




Llamada de miembro virtual en un constructor (12)

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?


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

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.


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


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!


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.


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





virtual-functions