JavaRush /Курсы /C# SELF /Ошибки с модификаторами доступа и инкапсуляцией

Ошибки с модификаторами доступа и инкапсуляцией

C# SELF
25 уровень , 2 лекция
Открыта

1. Введение

Модификаторы доступа — это как заборы и замки в вашем доме: они определяют, кто может войти в ту или иную комнату, а кто — только посмотреть через замочную скважину. Напоминаем, в C# они бывают такими:

Модификатор Кто видит
public
Все
private
Только внутри текущего класса
protected
Текущий класс и наследники
internal
Весь проект (сборка/assembly)
protected internal
Весь проект + наследники
private protected
Только наследники в этом же проекте

Задача инкапсуляции — скрывать детали реализации класса, чтобы никто не мог испортить ваш объект злым выражением 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 поля "для теста".
  • Используйте свойства вместо прямых полей даже для публичного чтения — их легко потом доработать.
2
Задача
C# SELF, 25 уровень, 2 лекция
Недоступна
Ошибки с уровнями доступа и инкапсуляцией
Ошибки с уровнями доступа и инкапсуляцией
2
Задача
C# SELF, 25 уровень, 2 лекция
Недоступна
Реализация инкапсуляции и защита состояния класса
Реализация инкапсуляции и защита состояния класса
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ