Os níveis de acesso

  • public: acessível onde o tipo estiver visível.
  • private: acessível apenas dentro do tipo que o declara; é o padrão para membros de classes e structs.
  • protected: acessível dentro do tipo declarante e de tipos derivados.
  • internal: acessível dentro do mesmo assembly; é o padrão para tipos de nível superior.
  • protected internal: acessível no mesmo assembly ou por tipos derivados, inclusive fora dele.
  • private protected: acessível apenas por tipos derivados que também estejam no mesmo assembly.
  • file: restringe um tipo de nível superior ao arquivo-fonte atual (C# 11 ou posterior).

Exemplo em uma API de classe

public class Conta
{
    private decimal _saldo;

    public decimal Saldo => _saldo;

    public void Depositar(decimal valor)
    {
        if (valor <= 0)
            throw new ArgumentOutOfRangeException(nameof(valor));

        _saldo += valor;
    }

    protected virtual void RegistrarOperacao(string descricao)
    {
        // Implementação interna extensível pela hierarquia.
    }
}

Consumidores consultam o saldo e pedem um depósito pelo contrato público. Não podem atribuir diretamente a variável privada. O método protegido só deve existir se subclasses legítimas precisarem participar desse ponto de extensão; membros protegidos também são parte do contrato de herança e reduzem a liberdade de mudar a base.

protected internal e private protected

Os dois modificadores combinam dois critérios de acesso, mas com operadores diferentes. protected internal significa “mesmo assembly ou tipo derivado”. private protected significa “tipo derivado e mesmo assembly”. Essa distinção importa quando uma biblioteca é consumida por outro projeto: um derivado externo consegue acessar membros protected internal pela regra protected, mas não membros private protected.

Não os escolha por parecerem mais restritivos; confirme quem realmente deve estender o tipo e em que assembly essa extensão vive.

Acessibilidade de tipos

Tipos de nível superior podem ser public ou internal; private não é permitido nesse nível. Tipos aninhados podem usar os modificadores de acesso aplicáveis. A visibilidade efetiva de um membro não pode ser maior que a de seu tipo contêiner, porque um tipo interno não se torna público por conter um membro público.

Uma declaração de tipo de nível superior sem modificador é internal. Classes e structs também podem declarar tipos aninhados privados. Em soluções com vários projetos, internal é útil para manter detalhes internos ao assembly; InternalsVisibleTo pode conceder acesso a outro assembly, frequentemente a um projeto de testes, e deve ser usado deliberadamente.

Propriedades e acesso assimétrico

Uma propriedade pode ter acessibilidade diferente no setter, desde que o modificador seja mais restritivo que o da propriedade. Isso permite expor leitura ampla e controlar alterações:

public decimal Saldo { get; private set; }
public string Codigo { get; init; }

private set permite escrita apenas dentro do tipo. init permite atribuição durante a inicialização do objeto, mas não depois dela; não transforma automaticamente o objeto referenciado em imutável. Para proteger invariantes complexas, métodos explícitos podem comunicar melhor as transições permitidas.

Escolha uma visibilidade mínima e útil

Comece pelo acesso mais restrito que atende aos consumidores reais; amplie quando houver necessidade concreta. Isso reduz a superfície de API, limita dependências e deixa claro o que pode mudar sem quebrar outros componentes. Não esconda um contrato que deva ser consumido por outros assemblies e não exponha campos ou setters apenas para facilitar serialização ou testes sem avaliar opções próprias dessas ferramentas.

Modificadores de acesso apoiam encapsulamento, mas não o substituem: um método público ainda pode revelar detalhes demais, e um membro privado pode implementar uma abstração ruim. Veja também encapsulamento, abstração e interfaces e herança e membros protegidos.