c# constructores - Llamada de miembro virtual en un constructor




destructores polimorfico (15)

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

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?


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.


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.


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 :)


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.


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

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

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

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

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.


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.


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

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.


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

Creo que puedes llamar a un constructor desde un constructor. Se compilará y correrá. Recientemente vi a alguien hacer esto y funcionó tanto en Windows como en Linux.

Simplemente no hace lo que quieres. El constructor interno construirá un objeto local temporal que se eliminará una vez que el constructor externo regrese. Tendrían que ser diferentes constructores también o crearías una llamada recursiva.

Ref: https://isocpp.org/wiki/faq/ctors#init-methods





c# constructor warnings resharper virtual-functions