Pregunta 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?


1146
2017-09-23 07:11


origen


Respuestas:


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 a la clase más derivada (ver el blog de Eric Lippert para detalles sobre 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.


1036
2017-09-23 07:21



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

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, NullReferenceException será arrojado, porque foo es nulo. El constructor base de un objeto se llama antes que su propio constructor. Al tener una virtual 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.


478
2017-09-23 07:17



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 tan diferente entre C #, C ++ y Java. ¡Es posible que tus programadores no sepan qué esperar!


154
2017-09-23 07:36



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

77
2017-09-23 13:20



En C #, se ejecuta un constructor de clase base antes de el 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 hacerte prestar atención y asegurarte 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 a continuación donde está el constructor que lo llama.


16
2017-09-23 07:21



Hay respuestas bien escritas arriba de por qué no lo haría quiero hacer eso Aquí hay un contraejemplo donde tal vez haría quiero hacer eso (traducido a C # desde 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 { }

11
2017-12-28 01:19



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.


5
2017-09-23 07:15



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.


5
2017-09-23 07:25



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 dentro de 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á Dispose en el objeto parcialmente construido si la construcción falla. Vocación Dispose en tales casos, a menudo es necesario para evitar fugas de recursos, pero Dispose los métodos deben prepararse para la posibilidad de que el objeto sobre el que se ejecutan no se haya construido por completo.


5
2017-10-25 20:33