Chamada de membro virtual em um construtor

Estou recebendo um aviso do ReSharper sobre uma chamada para um membro virtual do meu construtor de objects.

Por que isso seria algo a não fazer?

    Quando um object escrito em C # é construído, o que acontece é que os inicializadores são executados em ordem da class mais derivada para a class base e, em seguida, construtores são executados em ordem da class base para a class mais derivada ( consulte o blog de Eric Lippert para detalhes por que isso acontece ).

    Também em objects .NET não mudam de tipo à medida que são construídos, mas começam como o tipo mais derivado, com a tabela de método sendo para o tipo mais derivado. Isso significa que as chamadas de método virtual sempre são executadas no tipo mais derivado.

    Quando você combina esses dois fatos, fica com o problema de que, se fizer uma chamada de método virtual em um construtor, e não for o tipo mais derivado em sua hierarquia de inheritance, ela será chamada em uma class cujo construtor não foi executado e, portanto, pode não estar em um estado adequado para ter esse método chamado.

    Este problema é, naturalmente, mitigado se você marcar sua class como selada para garantir que seja o tipo mais derivado na hierarquia de inheritance – nesse caso, é perfeitamente seguro chamar o método virtual.

    Para responder à sua pergunta, considere esta pergunta: o que o código abaixo imprimirá quando o object Child for instanciado?

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

    A resposta é que, de fato, um NullReferenceException será lançado, porque foo é nulo. O construtor base de um object é chamado antes de seu próprio construtor . Por ter uma chamada virtual no construtor de um object, você está introduzindo a possibilidade de que objects herdados executem código antes de serem totalmente inicializados.

    As regras do C # são muito diferentes das do Java e C ++.

    Quando você está no construtor para algum object em C #, esse object existe em um formulário totalmente inicializado (não apenas “construído”), como seu 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" } 

    Isso significa que, se você chamar uma function virtual do construtor de A, ela será resolvida para qualquer substituição em B, se uma for fornecida.

    Mesmo que você intencionalmente configure A e B assim, entendendo completamente o comportamento do sistema, você poderá ter um choque mais tarde. Digamos que você chamou funções virtuais no construtor de B, “sabendo” que elas seriam manipuladas por B ou A, conforme apropriado. Então o tempo passa, e outra pessoa decide que precisa definir C e replace algumas das funções virtuais lá. De repente, o construtor de B acaba chamando o código em C, o que pode levar a um comportamento bastante surpreendente.

    Provavelmente, é uma boa idéia evitar funções virtuais em construtores, já que as regras são tão diferentes entre C #, C ++ e Java. Seus programadores podem não saber o que esperar!

    As razões do aviso já estão descritas, mas como você consertaria o aviso? Você tem que selar qualquer class ou membro virtual.

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

    Você pode selar a class A:

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

    Ou você pode selar o método Foo:

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

    Em C #, o construtor de uma class base é executado antes do construtor da class derivada, portanto, quaisquer campos de instância que uma class derivada possa usar no membro virtual possivelmente substituído ainda não foram inicializados.

    Observe que isso é apenas um aviso para fazer com que você preste atenção e verifique se está tudo certo. Existem casos de uso reais para este cenário, você só precisa documentar o comportamento do membro virtual que não pode usar campos de instância declarados em uma class derivada abaixo de onde o construtor está chamando.

    Há respostas bem escritas acima para por que você não gostaria de fazer isso. Aqui está um contra-exemplo onde talvez você queira fazer isso (traduzido em C # de Practical Object-Oriented Design em Ruby por Sandi Metz, p. 126).

    Observe que GetDependency() não está tocando em nenhuma variável de instância. Seria estático se os methods estáticos pudessem ser virtuais.

    (Para ser justo, provavelmente existem maneiras mais inteligentes de fazer isso através de containers de injeção de dependência ou inicializadores de objects …)

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

    Sim, geralmente é ruim chamar o método virtual no construtor.

    Neste ponto, o object pode não estar totalmente construído ainda, e os invariantes esperados por methods podem não se sustentar ainda.

    Seu construtor pode (mais tarde, em uma extensão de seu software) ser chamado a partir do construtor de uma subclass que substitui o método virtual. Agora não a implementação da subclass da function, mas a implementação da class base será chamada. Então, não faz sentido chamar uma function virtual aqui.

    No entanto, se o seu design satisfizer o princípio da substituição de Liskov, nenhum dano será feito. Provavelmente é por isso que é tolerado – um aviso, não um erro.

    Um aspecto importante dessa questão que outras respostas ainda não abordaram é que é seguro para uma class base chamar membros virtuais de dentro de seu construtor se é isso que as classs derivadas esperam que ele faça . Nesses casos, o projetista da class derivada é responsável por garantir que quaisquer methods executados antes da conclusão da construção se comportem da maneira mais sensata possível sob as circunstâncias. Por exemplo, em C ++ / CLI, os construtores são agrupados em código, o que chamará Dispose no object parcialmente construído se a construção falhar. Chamar Dispose em tais casos é frequentemente necessário para evitar vazamentos de resources, mas os methods Dispose devem estar preparados para a possibilidade de que o object sobre o qual eles são executados possa não ter sido totalmente construído.

    Porque até que o construtor tenha concluído a execução, o object não está totalmente instanciado. Quaisquer membros referenciados pela function virtual podem não ser inicializados. Em C ++, quando você está em um construtor, this se refere apenas ao tipo estático do construtor em que você está, e não ao tipo dynamic real do object que está sendo criado. Isso significa que a chamada de function virtual pode nem chegar onde você espera.

    O aviso é um lembrete de que membros virtuais provavelmente serão substituídos na class derivada. Nesse caso, o que quer que a class pai tenha feito com um membro virtual será desfeito ou alterado pela substituição da class filha. Olhe para o pequeno golpe de exemplo para maior clareza

    A class pai abaixo tenta definir o valor para um membro virtual em seu construtor. E isso acionará o aviso Re-sharper, deixe ver no 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(); } } 

    A class filha aqui substitui a propriedade pai. Se esta propriedade não estiver marcada como virtual, o compilador avisará que a propriedade oculta a propriedade na class pai e sugerirá que você adicione a palavra-chave ‘new’ se for intencional.

     public class Child: Parent { public Child():base() { this.Obj = "Something"; } public override object Obj{get;set;} } 

    Finalmente, o impacto sobre o uso, a saída do exemplo abaixo abandona o valor inicial definido pelo construtor da class pai. E é isso que o Re-sharper tenta avisá-lo , os valores definidos no construtor da class Parent estão abertos para serem sobrescritos pelo construtor da class filha, que é chamado logo após o construtor da class pai .

     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 ao seguir cegamente o conselho de Resharper e selar a class! Se for um modelo no EF Code First, ele removerá a palavra-chave virtual e desativará o carregamento lento de seus relacionamentos.

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

    Um importante bit ausente é: qual é a maneira correta de resolver esse problema?

    Como Greg explicou , o problema raiz aqui é que um construtor de class base invocaria o membro virtual antes que a class derivada fosse construída.

    O código a seguir, retirado das diretrizes de design do construtor do MSDN , demonstra esse 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); } } 

    Quando uma nova instância de DerivedFromBad é criada, o construtor da class base chama o DisplayState e mostra BadBaseClass porque o campo ainda não foi atualizado pelo construtor derivado.

     public class Tester { public static void Main() { var bad = new DerivedFromBad(); } } 

    Uma implementação aprimorada remove o método virtual do construtor da class base e usa um método Initialize . Criar uma nova instância de DerivedFromBetter exibe o “DerivedFromBetter” esperado

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

    Há uma diferença entre C ++ e C # neste caso específico. Em C ++, o object não é inicializado e, portanto, não é seguro chamar uma function virutal dentro de um construtor. Em C # quando um object de class é criado, todos os seus membros são zero inicializados. É possível chamar uma function virtual no construtor, mas se você puder acessar membros que ainda são zero. Se você não precisa acessar membros, é seguro chamar uma function virtual em C #.

    Apenas para adicionar meus pensamentos. Se você sempre inicializar o campo privado ao defini-lo, esse problema deve ser evitado. Pelo menos abaixo o código funciona como um 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()); } } 

    Outra coisa interessante que descobri é que o erro ReSharper pode ser ‘satisfeito’ fazendo algo como abaixo, o que é idiota para mim (no entanto, como mencionado anteriormente, ainda não é uma boa idéia chamar virtual prop / methods in 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"; } 

    }

    Gostaria apenas de adicionar um método Initialize () à class base e, em seguida, chamar isso de construtores derivados. Esse método irá chamar qualquer método virtual / abstrato / propriedades após todos os construtores foram executados 🙂