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.Instance tem 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

AbordagemSeguro para threadsPreguiçosoComplexidadeRecomendado
Básico (sem lock)NãoSimBaixaNão
Lock simplesSimSimBaixaPara casos simples
Double-check lockingSimSimMédiaCódigo legado
Inicialização estáticaSimNãoBaixaQuando carga antecipada é aceitável
Lazy<T>SimSimBaixaSim
Contêiner DISimConfigurávelBaixaO 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.