Encapsulamento significa que o estado de um objeto só ocorre por meio de uma interface definida, e por isso a classe pode garantir que esse estado seja sempre válido e de acordo com o propósito da classe.
Em alguns casos, portanto, está perfeitamente de acordo com o princípio de encapsulamento apenas expor um campo publicamente - todos os valores possíveis para o campo são válidos com todos os outros valores possíveis de todos os outros campos e, portanto, o programador pode decidir ativamente permitir o campo para ser manipulado livremente por código externo.
No entanto, esses casos são restritos principalmente a classes que são, em sua maioria, "dados simples e antigos". Eles também não são muito interessantes a esse respeito, então chega de falar deles.
Em outros casos, em outras linguagens, haveria um método getter e setter, algo como int getId()obter um valor e void setId(int val)atualizá-lo.
As propriedades nos permitem usar a mesma sintaxe para ler e escrever por meio de métodos que usaríamos para ler e escrever um campo. Este é um bom açúcar sintático, embora não seja vital.
(Na verdade, devido à forma como a reflexão funciona e casos como DataBinder.Evalesse, pode ser útil ter uma propriedade mesmo quando um campo funcionaria bem, mas isso é outro assunto).
Até que os setters privados fossem introduzidos (na verdade, o que mudou com C # 2 é a sintaxe para ter um setter privado e um getter público ou protegido no mesmo bloco), poderíamos ter um método privado para fazer o trabalho do setter privado, então setters privados não são realmente necessários. Eles são úteis, portanto, embora sejam apenas açúcar sintático, eles são muito úteis.
O encapsulamento não é uma questão de se seus setters (ou getters) são públicos, privados, protegidos ou internos, mas uma questão de se eles são apropriados . Comece com um padrão de cada campo sendo privado (e nesse caso readonly) e então, conforme necessário, adicione membros (sejam propriedades ou métodos) que alteram esses campos e certifique-se de que o objeto permaneça válido conforme eles mudam . Isso garante que a invariante de uma classe seja mantida, o que significa que as regras que descrevem o conjunto válido de estados em que ela pode estar nunca são quebradas (os construtores também ajudam, certificando-se de que ela comece nesse estado válido).
Quanto à sua última pergunta, ser imutável significa que uma classe não tem setters públicos, protegidos ou internos e nenhum método público, protegido ou interno que altere quaisquer campos. Existem graus disso, em C #, existem três graus possíveis:
Todos os campos de instância de uma classe são readonly, portanto, nem mesmo o código privado pode alterá-los. É garantido que seja imutável (qualquer coisa que tente alterá-lo não será compilado) e possivelmente otimizações podem ser feitas por trás disso.
Uma classe é imutável de fora porque nenhum membro público muda nada, mas não é garantido pelo uso de readonlypara não ser alterado de dentro.
Uma classe é imutável quando vista de fora, embora algum estado seja alterado como um detalhe de implementação. Por exemplo, um campo pode ser memoizado e, portanto, embora do lado de fora uma tentativa de obtê-lo apenas recupere o mesmo valor, a primeira tentativa realmente o calcula e, em seguida, o armazena para recuperação em tentativas subsequentes.
private File settingsFile = null;e, em seguida, em um dos construtores:if (settingsFile == null) { settingsFile = GetSettingsFile() };. Refatorar código assim me fazia chorar às vezes :). Só porque você pode definir um membro antes do construtor, não significa que você deva, pois, com vários construtores, isso torna difícil seguir a lógica. Os setters privados o forçam a definir valores dentro do construtor ou posteriormente.