Virtual member call in a constructor: quando o compilador C# avisa, e não é à toa
Todo desenvolvedor C# que já escreveu classes com herança certamente já viu este aviso do ReSharper ou Visual Studio: «Virtual member call in a constructor». Parece inofensivo, mas na verdade é uma bandeira vermelha que pode levar a bugs difíceis de detectar e queda de desempenho. Hoje vamos analisar o que está por trás desse aviso, como ele se relaciona com o desempenho e por que até programadores experientes às vezes cometem esse erro.
O tema não é aleatório: a pergunta «Virtual member call in a constructor» no Stack Overflow acumulou milhares de visualizações e dezenas de respostas. Isso significa que o problema é real e afeta muitos. Mas vamos além: não apenas explicaremos como corrigir, mas mostraremos como otimizar o código, evitando alocações desnecessárias e comportamentos inesperados.
O que é «Virtual member call in a constructor»?
Imagine a situação: você cria uma classe base, cujo construtor chama um método virtual. O que poderia dar errado? Afinal, métodos virtuais foram criados para serem sobrescritos. Mas o problema é que, ao chamar um método virtual a partir do construtor da classe base, o objeto da classe derivada ainda não foi totalmente inicializado. Isso pode levar à chamada do método sobrescrito em um objeto "cru", onde os campos ainda não foram definidos e as dependências não foram injetadas.
public class BaseClass{ public BaseClass() { // Atenção! Chamada virtual no construtor VirtualMethod(); }
public virtual void VirtualMethod() { Console.WriteLine("BaseClass.VirtualMethod"); }}
public class DerivedClass : BaseClass{ private readonly string _data;
public DerivedClass(string data) { _data = data; }
public override void VirtualMethod() { // _data ainda não foi inicializado! Console.WriteLine($"DerivedClass: {_data?.Length ?? 0}"); }}
// Usovar obj = new DerivedClass("Hello"); // Exibirá: DerivedClass: 0, e não 5Neste exemplo, _data será null no momento da chamada de VirtualMethod, porque o construtor da classe derivada ainda não foi executado. Este é um caso clássico em que a chamada virtual no construtor leva a um comportamento não óbvio.
Por que isso é um problema de desempenho?
À primeira vista, é apenas um erro lógico. Mas vamos mais fundo. Quando você chama um método virtual a partir do construtor, o ambiente de execução (CLR) é forçado a realizar verificações e despachos adicionais. Em uma situação normal, a tabela virtual de métodos (vtable) já está pronta e a chamada é rápida. No entanto, no construtor da classe base, a vtable pode ainda não estar completamente construída para o tipo derivado. Isso leva a:
- Custos adicionais de despacho — a CLR gasta tempo verificando a correção da chamada.
- Potencial compilação JIT de código não otimizado — o JIT pode não conseguir embutir (inline) essa chamada, reduzindo o desempenho.
- Verificações extras de null — se você sobrescreveu o método, precisa escrever código defensivo, o que aumenta o tamanho do IL e o tempo de execução.
Em sistemas de alta carga, onde construtores são chamados milhares de vezes por segundo, esses micro-atrasos podem se acumular. Portanto, a otimização de código C# deve começar por detalhes aparentemente pequenos como este.
Quando a chamada virtual no construtor pode ser útil?
Spoiler: quase nunca. Mas há raras exceções quando você sabe exatamente o que está fazendo. Por exemplo, no padrão Template Method, onde a classe base define o esqueleto de um algoritmo e as classes derivadas