Pergunta

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

Por que isso seria algo que não fazer?

Foi útil?

Solução

Quando um objeto escrito em C # é construído, o que acontece é que os inicializadores de executar, a fim da classe mais derivada para a classe base, e depois construtores executados em ordem da classe base para a classe mais derivada ( consulte o blog de Eric Lippert para obter mais detalhes a respeito de porque este é ).

Também em objetos .NET não digitar a mudança como eles são construídos, mas começam como o tipo mais derivado, com a tabela método seja para o tipo mais derivado. Isto significa que chamadas de método virtual sempre executados no tipo mais derivado.

Quando você combinar esses dois fatos que são deixados com o problema que se você fizer uma chamada de método virtual em um construtor, e não é o tipo mais derivado em sua hierarquia de herança, que ele será chamado em uma classe cujo construtor não tiver sido executado, e, portanto, pode não estar em condições adequadas para que tenha método chamado.

Este problema é, naturalmente, mitigados se você marcar a sua classe como selada para garantir que ele é o tipo mais derivado na hierarquia de herança -. Nesse caso, é perfeitamente seguro para chamar o método virtual

Outras dicas

A fim de responder à sua pergunta, considere esta questão: qual será o código abaixo imprimir quando o objeto Child é 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()); //NullReferenceException!?!
    }
}

A resposta é que, de facto, um NullReferenceException será lançada, porque foo é nulo. construtor base de Um objeto é chamado antes de seu próprio construtor . Por ter uma chamada virtual no construtor de um objeto que você está introduzindo a possibilidade de que os objetos herdando irá executar código antes de terem sido totalmente inicializado.

As regras de C # é muito diferente da de Java e C ++.

Quando você está no construtor para um objeto em C #, esse objeto existe numa forma totalmente inicializado (não apenas "construída"), como seu tipo totalmente derivada.

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

Isto significa que se você chamar uma função virtual a partir do construtor de A, ele vai resolver a qualquer substituição em B, se for fornecido.

Mesmo se você intencionalmente configurado A e B assim, a plena compreensão do comportamento do sistema, você poderia ser dentro para um choque mais tarde. Digamos que você chama funções virtuais no construtor do B, "saber" eles seriam manipulados por B ou A conforme apropriado. Em seguida, o tempo passa, e alguém decide que precisam para definir C, e substituir algumas das funções virtuais lá. Todos extremidades do construtor repente do B-se chamar código em C, o que poderia levar a um comportamento bastante surpreendente.

É provavelmente uma boa idéia para evitar funções virtuais em construtores de qualquer maneira, uma vez 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 da advertência já estão descritos, mas como você corrigir o aviso? Você tem que selar qualquer classe ou membro virtual.

  class B
  {
    protected virtual void Foo() { }
  }

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

Você pode selar classe A:

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

Ou você pode selar método Foo:

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

    protected sealed override void Foo()
    {
      base.Foo();
    }
  }

Em C #, uma classe base do construtor é executado antes a classe derivada construtor, portanto, quaisquer campos de instância que uma classe derivada pode usar no membro virtual possivelmente-substituído ainda não são inicializados.

Note que este é apenas um aviso para fazer você prestar atenção e ter certeza que é tudo direito. Há casos de uso reais para este cenário, você só tem que documento o comportamento do membro virtual que ele não pode usar quaisquer campos de instância declarada em uma classe derivada abaixo de onde o construtor chamado é.

Não são respostas bem escrito acima para por que você que não quer fazer isso. Aqui é um contra-exemplo onde talvez você se quer fazer isso (traduzido em C # a partir de Prático Object-Oriented projeto em ruby ?? por Sandi Metz, p. 126).

Note que GetDependency() não está tocando quaisquer variáveis ??de instância. Seria estático se métodos estáticos pode ser virtual.

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

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 para chamar o método virtual no construtor.

Neste ponto, o objet pode não ser ainda totalmente construído, e as invariantes esperados pelos métodos não podem exercer ainda.

Porque até o construtor tenha concluído a execução, o objeto não está totalmente instanciado. Quaisquer membros referenciados pela função virtual não pode ser inicializado. Em C ++, quando você está em um construtor, this se refere apenas ao tipo estático do construtor em que está, e não o tipo de dinâmica real do objeto que está sendo criado. Isto significa que a chamada de função virtual não pode mesmo ir para onde você espera que ele.

Seu construtor pode (mais tarde, em uma extensão de seu software) ser chamado a partir do construtor de uma subclasse que substitui o método virtual. Agora não implementação das subclasse da função, mas a implementação da classe base será chamado. Portanto, não faz muito sentido para chamar uma função virtual aqui.

No entanto, se os seus satisfaz projeto o princípio Liskov Substituição, nenhum dano será feito. Provavelmente é por isso que ele é tolerado -. Um aviso, não um erro

Um aspecto importante desta questão que outras respostas ainda não ter abordado é que ele é seguro para uma base de classe para chamar membros virtuais de dentro de seu construtor se é isso que as classes derivadas estão esperando que ele faça . Nesses casos, o designer da classe derivada é responsável por garantir que todos os métodos que são executados antes que a construção está completa se comportará como forma sensata quanto eles podem dadas as circunstâncias. Por exemplo, em C ++ / CLI, construtores são embrulhados em código que irá chamar Dispose no objecto parcialmente construído-se a construção falhar. Chamando Dispose, nesses casos, é muitas vezes necessário para evitar vazamentos de recursos, mas os métodos Dispose deve estar preparado para a possibilidade de que o objeto sobre o qual estão sendo executados podem não ter sido totalmente construído.

O aviso é um lembrete de que membros virtuais são susceptíveis de ser substituído na classe derivada. Nesse caso, qualquer que seja a classe pai fez para um membro virtual será desfeita ou alterado, substituindo classe filha. Olhe o exemplo pequeno golpe para maior clareza

A classe pai abaixo tentativas de valor ajustado a um membro virtual em seu construtor. E isso vai desencadear aviso Re-nítida, vamos ver em 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 classe criança aqui substitui a propriedade pai. Se esta propriedade não foi marcado virtual compilador iria avisar que a propriedade peles de propriedade na classe pai e sugerem que você adicionar 'nova' palavra-chave se é intencional.

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

Finalmente, o impacto da utilização, a saída do exemplo abaixo abandona o conjunto valor inicial pelo construtor da classe pai. E é isso que as tentativas Re-nítidas para avisá-lo , valores definidos em construtor da classe pai estão abertos para ser substituído pelo construtor da classe criança que é chamado logo após o construtor da classe 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 com os seguintes cegamente o conselho do ReSharper e fazer a classe selada! Se é um modelo no EF Code First ele irá remover a palavra-chave virtual e que iria desativar o carregamento lento de seu relacionamento.

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

Um pouco ausente importante é, qual é a maneira correta de resolver este problema?

Como Greg explicou , a raiz do problema aqui é que um construtor de classe base deveria invocar o membro virtual antes da classe derivada foi construído.

O código a seguir, tirada diretrizes de design do construtor do MSDN , demonstra esta questão.

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 é criado, as chamadas do construtor classe base para DisplayState e mostra BadBaseClass porque o campo ainda não tenha sido atualização pelo construtor derivado.

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

Uma implementação melhorada remove o método virtual do construtor da classe base, e usa um método Initialize. Criando uma nova instância do DerivedFromBetter exibe o 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);
    }
}

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

Apenas para adicionar meus pensamentos. Se você sempre inicializar o campo privado quando defini-lo, este problema deve ser evitado. Pelo menos o código abaixo 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 encontrei é que o erro ReSharper pode ser 'satisfeito' por fazer algo como abaixo do qual é burro para mim (no entanto, como mencionado por muitos no início, ainda não é uma boa idéia para chamar prop / métodos virtuais em 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 acrescentar um método Initialize () para a classe base e, em seguida, chamar isso de construtores derivados. Esse método vai chamar nenhum / métodos abstratos / propriedades virtuais depois de todos os construtores foram executados:)

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top