1. Введение
Модификаторы доступа — это как заборы и замки в вашем доме: они определяют, кто может войти в ту или иную комнату, а кто — только посмотреть через замочную скважину. Напоминаем, в C# они бывают такими:
| Модификатор | Кто видит |
|---|---|
|
Все |
|
Только внутри текущего класса |
|
Текущий класс и наследники |
|
Весь проект (сборка/assembly) |
|
Весь проект + наследники |
|
Только наследники в этом же проекте |
Задача инкапсуляции — скрывать детали реализации класса, чтобы никто не мог испортить ваш объект злым выражением obj.Field = 99999;, если вы этого не хотите.
2. Классические ошибки с уровня доступа
Открытие всего через public
Самая распространённая ошибка — объявлять все поля и методы класса как public. Логика примерно такая: "А вдруг пригодится? Пусть другие обращаются к моим членам, где хотят!". Такой подход кажется удобным, пока кто-то не присваивает вашему объекту недопустимое состояние, или внезапно не начинает использовать ваши внутренние детали так, что менять код становится страшно.
Пример ошибки:
public class BankAccount
{
public string Owner;
public decimal Balance;
}
Теперь любой может сделать вот так:
var acc = new BankAccount();
acc.Owner = "";
acc.Balance = -1000000M; // Внезапно, долг в миллион!
Что не так?
Вы нарушили все принципы безопасности и здравого смысла. Даже если ваш банк содержит только листочки с деревьев или цветные фантики из игры "Монополия", отрицательный баланс "по желанию" — странно.
Превращение класса в "structure без структуры"
Вторая типовая ошибка — делать публичными не только поля, но и внутренние свойства и методы, которые вообще не должны быть доступны снаружи.
public class Vault
{
public string DoorPIN = "1234";
public void OpenDoor()
{
// Какая-то магия
}
}
Что теперь? А вот что: теперь кто угодно может узнать PIN и открыть дверь в ваш сейф. Даже если это «тестовый сейф», через месяц кто-нибудь забудет про эту дырку, и случится неприятность.
Правильный подход:
Интерфейс класса должен быть минимален и защищён. То, что нужно другим, — делаем public. То, что — только для себя, — private. Если что-то надо наследникам — protected.
3. Ошибки инкапсуляции
В .NET есть строгий консенсус: никакие данные класса (состояние) не должны храниться в публичных полях! Все должно быть закрыто или хотя бы обёрнуто в свойства. Прямой доступ к полям — классика плохого стиля и источник трудноуловыимых ошибок.
Почему поля должны быть private
Поля — основа внутреннего состояния объекта. Если дать к ним публичный доступ, вы теряете контроль над тем, когда и какие значения могут туда быть записаны. Ваш объект становится уязвимым для некорректных значений, а при изменении их формата все публичные обращения сломаются.
Вместо этого:
Объявляйте поля как private, а наружу выдавайте только необходимые свойства или методы:
public class BankAccount
{
private decimal balance;
public decimal Balance
{
get { return balance; }
private set
{
if (value < 0)
throw new ArgumentException("Баланс не может быть отрицательным");
balance = value;
}
}
public BankAccount(decimal initialBalance)
{
Balance = initialBalance;
}
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("Нельзя пополнять на ноль или меньше!");
Balance += amount;
}
}
Грубая ошибка: public set для данных, которые нельзя менять
Иногда необходим только get, но программисты пишут public { get; set; } "по умолчанию", потому что так быстрее. В результате любой может сделать:
account.Balance = 99999999; // Почему бы и нет?
Правильнее так:
Если свойство должно устанавливаться только внутри класса, делайте set с модификатором:
public decimal Balance { get; private set; }
или, если значение должно задаваться только при создании объекта (C# 14):
public decimal Balance { get; init; }
4. Когда protected тоже ловушка
Переоткрытые двери для всех наследников
protected — хороший способ передать функциональность наследнику, но некоторые данные лучше не делать доступными даже наследникам! Например, если ваши дети (наследники класса) не должны ломать критические поля напрямую.
Пример:
public class SecureVault
{
protected string secretCode = "1234";
}
Теперь любой наследник может вот так:
public class HackerVault : SecureVault
{
public void Hack()
{
secretCode = "0000"; // Легко поменял секрет!
}
}
Рекомендация:
Используйте protected только для тех вещей, которые реально должны быть доступны для наследников. Всё остальное оставляйте приватным, а для нужных операций пишите защищённые методы.
5. Ошибки с internal и перепутанные сборки
Модификатор internal кажется заманчивым: "пусть всё будет доступно внутри проекта". Однако как только проект становится больше одного файла или подключается к библиотекам, появляются конфликты. Часто студенты случайно делают что-то публичным внутри сборки, хотя класс должен быть полностью скрыт.
Пример:
internal class Logger
{
internal void Write(string msg) { /* ... */ }
}
Теперь любой может вызвать Logger.Write из любого файла проекта. А если через год кто-то подключит вашу DLL к другому проекту? Вся "внутренняя кухня" будет видна, если вы оставили что-то public.
Совет:
Делайте internal для технических классов, но старайтесь оставлять чувствительные детали всё-таки private.
6. Практика: банковское приложение
Продолжим наш пример с банковским приложением, чтобы на практике увидеть, как легко ошибиться и что с этим делать.
Пример ошибки №1: Public поля
public class BankAccount
{
public decimal Balance;
public string Owner;
}
Проблема: любой может это сломать.
Исправляем через свойства
public class BankAccount
{
private decimal _balance;
private string _owner;
public string Owner => _owner; // Только для чтения
public decimal Balance
{
get => _balance;
private set
{
if (value < 0) throw new ArgumentException("Баланс не может быть отрицательным");
_balance = value;
}
}
public BankAccount(string owner, decimal initialBalance)
{
if (string.IsNullOrWhiteSpace(owner))
throw new ArgumentException("Имя владельца обязательно");
_owner = owner;
Balance = initialBalance;
}
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Сумма пополнения должна быть положительной");
Balance += amount;
}
}
Теперь если вы попробуете вот так:
var acc = new BankAccount("Лена", 1000m);
acc.Balance = -700m; // Ошибка! set приватный.
Или даже так:
acc.Owner = "Хакер"; // Ошибка компиляции, нет set
— ничего не выйдет. Только через конструктор или методы.
Ошибка: Свойства "для красоты"
Многие пишут:
public int Age { get; set; }
Хотя возраст должен меняться только через метод IncreaseAge (например, 1 января), а напрямую — нет.
7. Визуальное напоминание: как нельзя и как нужно
graph TD
A[Public Fields] -->|Any code| D[Uncontrolled state]
B[Private Fields + Public Methods] -->|Well-defined| E[Controlled state]
F[Public Setters] -->|Anyone can change| D
G[Private Setters] -->|Only class| E
| Подход | Состояние защищено? | Можно валидировать? | Легко расширять? |
|---|---|---|---|
| Public fields | ❌ | ❌ | ❌ |
| Properties (get/set public) | ❌ | Частично | Да, но небезопасно |
| Private fields + property с set private | ✅ | ✅ | ✅ |
8. Советы по стилю работы с инкапсуляцией
- Открывайте наружу только то, что реально нужно пользователю класса.
- Всегда валидируйте данные, попадающие в объект.
- Не стремитесь к избыточности: если свойство не должно изменяться извне — делайте set приватным.
- Для нестандартной логики — всегда используйте методы, а не прямой доступ.
- Даже если очень хочется — не делайте public поля "для теста".
- Используйте свойства вместо прямых полей даже для публичного чтения — их легко потом доработать.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ