O padrão Singleton garante que uma classe tenha exatamente uma instância durante toda a vida de uma aplicação e fornece um ponto de acesso global a essa instância. Este guia abrange múltiplas abordagens de implementação em C# .NET, desde a mais simples até a mais robusta, junto com orientação sobre quando os singletons são apropriados e quando você deve buscar alternativas.
Quando usar um Singleton
Os singletons são úteis quando:
- Você precisa de exatamente uma instância de uma classe para coordenar ações em todo o sistema (por exemplo, um gerenciador de configuração, serviço de log ou pool de conexões).
- Criar múltiplas instâncias causaria conflitos ou desperdiçaria recursos.
- A instância deve ser acessível globalmente sem passá-la por cada chamada de método.
Implementação 1: Básica (não segura para threads)
Esta é a implementação mais simples, mas não é segura para aplicações multithread:
public sealed class Singleton
{
private static Singleton _instance;
private Singleton() { }
public static Singleton Instance
{
get
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
}
Problema: Se duas threads verificarem _instance == null ao mesmo tempo, ambas criarão uma instância, violando a garantia do singleton.
Implementação 2: Segura para threads com Lock
Adicionar uma instrução lock impede que múltiplas threads criem a instância simultaneamente:
public sealed class Singleton
{
private static Singleton _instance;
private static readonly object _lock = new object();
private Singleton() { }
public static Singleton Instance
{
get
{
lock (_lock)
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
}
}
Isso é seguro para threads, mas cada chamada a Instance adquire o bloqueio, o que introduz um custo de desempenho mesmo depois que a instância foi criada.
Implementação 3: Double-check locking
O double-check locking reduz a sobrecarga adquirindo o bloqueio apenas quando a instância ainda não foi criada:
public sealed class Singleton
{
private static volatile Singleton _instance;
private static readonly object _lock = new object();
private Singleton() { }
public static Singleton Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new Singleton();
}
}
}
return _instance;
}
}
}
A palavra-chave volatile garante que a atribuição a _instance seja concluída antes que a variável seja acessada por outras threads. A verificação null externa evita o bloqueio uma vez que a instância existe. A verificação null interna trata a condição de corrida entre duas threads que passaram pela verificação externa.
Implementação 4: Inicialização estática (eager)
O runtime do .NET garante que um construtor estático é executado exatamente uma vez por domínio de aplicação, tornando esta abordagem inerentemente segura para threads:
public sealed class Singleton
{
private static readonly Singleton _instance = new Singleton();
// Explicit static constructor tells the runtime
// not to mark the type with beforefieldinit
static Singleton() { }
private Singleton() { }
public static Singleton Instance => _instance;
}
A instância é criada quando a classe é referenciada pela primeira vez. O construtor estático explícito impede que o runtime use a otimização beforefieldinit, que poderia fazer com que a instância fosse criada antes do esperado.
Compromisso: A instância é criada assim que qualquer membro estático é acessado, mesmo que Instance nunca seja chamado. Para a maioria das aplicações, isso não é um problema.
Implementação 5: Lazy (recomendada)
A classe Lazy<T>, introduzida no .NET 4, fornece a inicialização preguiçosa mais limpa e segura para threads:
public sealed class Singleton
{
private static readonly Lazy<Singleton> _lazy =
new Lazy<Singleton>(() => new Singleton());
private Singleton() { }
public static Singleton Instance => _lazy.Value;
}
Lazy<T> gerencia toda a sincronização de threads internamente. A instância não é criada até a primeira vez que Instance é acessado. Esta é a abordagem recomendada para a maioria das aplicações C# porque é concisa, correta e fácil de entender.
Você também pode controlar o modo de segurança de threads:
// Default: thread-safe (LazyThreadSafetyMode.ExecutionAndPublication)
new Lazy<Singleton>(() => new Singleton());
// Not thread-safe (use only if single-threaded)
new Lazy<Singleton>(() => new Singleton(), isThreadSafe: false);
// Thread-safe, but each thread may create its own instance; only one is kept
new Lazy<Singleton>(() => new Singleton(),
LazyThreadSafetyMode.PublicationOnly);
Tornando a classe Singleton selada
Todos os exemplos acima usam a palavra-chave sealed. Isso impede que subclasses criem instâncias adicionais através de herança. Sem sealed, uma classe derivada poderia expor seu próprio construtor e quebrar a garantia de instância única.
Adicionando funcionalidade
Um singleton pode manter estado e fornecer métodos como qualquer outra classe:
public sealed class AppSettings
{
private static readonly Lazy<AppSettings> _lazy =
new Lazy<AppSettings>(() => new AppSettings());
public static AppSettings Instance => _lazy.Value;
public string ConnectionString { get; private set; }
public int MaxRetries { get; private set; }
private AppSettings()
{
// Load settings from configuration file
ConnectionString = "Server=.;Database=MyApp;Trusted_Connection=true;";
MaxRetries = 3;
}
public void Reload()
{
// Re-read settings from configuration file
}
}
Uso:
var connStr = AppSettings.Instance.ConnectionString;
Por que você deve considerar alternativas
Embora os singletons resolvam problemas reais, eles apresentam desvantagens bem conhecidas:
- Dependências ocultas. O código que chama
Singleton.Instancetem uma dependência implícita que não é visível na assinatura do seu construtor. - Difícil de testar. Você não pode facilmente substituir um mock ou implementação falsa em testes unitários.
- Estado global mutável. Se o singleton mantém estado mutável, pode introduzir problemas difíceis de depurar em aplicações concorrentes.
- Gerenciamento de tempo de vida. O singleton vive durante toda a vida da aplicação, o que pode não ser apropriado para todos os recursos.
Alternativa preferida: Injeção de dependências
Aplicações .NET modernas tipicamente usam um contêiner de injeção de dependências (DI) para gerenciar os tempos de vida dos objetos. Você pode registrar qualquer classe com um tempo de vida singleton:
// In Program.cs or Startup.cs
builder.Services.AddSingleton<IAppSettings, AppSettings>();
Então injete-a através do construtor:
public class OrderService
{
private readonly IAppSettings _settings;
public OrderService(IAppSettings settings)
{
_settings = settings;
}
}
Esta abordagem lhe dá o mesmo comportamento de instância única enquanto mantém as dependências explícitas, suporta design baseado em interfaces e permite testes unitários fáceis com mocks.
Resumo comparativo
| Abordagem | Seguro para threads | Preguiçoso | Complexidade | Recomendado |
|---|---|---|---|---|
| Básico (sem lock) | Não | Sim | Baixa | Não |
| Lock simples | Sim | Sim | Baixa | Para casos simples |
| Double-check locking | Sim | Sim | Média | Código legado |
| Inicialização estática | Sim | Não | Baixa | Quando carga antecipada é aceitável |
| Lazy<T> | Sim | Sim | Baixa | Sim |
| Contêiner DI | Sim | Configurável | Baixa | O melhor para apps modernas |
Resumo
Para a maioria das aplicações C# .NET, a abordagem Lazy<T> é a melhor implementação de singleton independente: é segura para threads, preguiçosa e fácil de ler. No entanto, em aplicações modernas usando injeção de dependências, prefira registrar seu serviço com AddSingleton<T>() no contêiner DI. Isso lhe dá a mesma garantia de instância única com os benefícios adicionais de dependências explícitas, testabilidade e arquitetura mais limpa.