🔷 C#

ООП: Инкапсуляция и полиморфизм в C# (.NET): подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты — шеф-повар в ресторане. У тебя есть холодильник, плита, ножи и продукты. Если бы каждый посетитель мог зайти на кухню, взять нож и начать резать мясо, наступил бы хаос. Кто-то порежется, продукты испортятся, а блюда получатся невкусными. Чтобы этого избежать, ты используешь инкапсуляцию: ты прячешь сложные процессы (нарезка, жарка) за простым интерфейсом — меню. Посетитель видит только «Стейк с овощами», а как ты его готовишь — это твоя внутренняя кухня.

А теперь представь, что ты умеешь готовить не только стейк, но и рыбу, и пасту. Когда приходит заказ «Приготовить блюдо», ты полиморфно выбираешь нужный рецепт в зависимости от того, что заказали. Для стейка — один способ, для рыбы — другой, но команда одна: «Приготовить». Это и есть полиморфизм: один интерфейс — много реализаций.

В программировании эти два принципа помогают делать код:

  • Безопасным — никто не испортит данные изнутри объекта
  • Гибким — можно добавлять новые типы, не ломая старый код
  • Понятным — сложность спрятана, снаружи только нужное
Важно: Инкапсуляция и полиморфизм — это не просто фишки языка, а способ мышления. Хороший программист всегда думает: «Что я прячу?», «Как я могу расширить это без изменений?»

Теория: как это работает изнутри

Инкапсуляция: защита данных

Инкапсуляция — это объединение данных (полей) и методов (функций) в одном объекте, при этом внутренние детали скрыты от внешнего мира. В C# это реализуется через модификаторы доступа:

Модификатор Доступ Аналогия
public Открыт всем Меню ресторана
private Только внутри класса Рецепт шеф-повара
protected Класс и его наследники Секреты, передаваемые ученикам
internal Внутри сборки (проекта) Кухня для сотрудников ресторана

Когда ты делаешь поле private, ты говоришь: «Никто не может изменить это напрямую». Чтобы дать доступ к данным, ты создаёшь свойства (properties) или методы-геттеры/сеттеры. Это как официант, который принимает заказ, но не лезет в кастрюли.

Полиморфизм: один интерфейс — много форм

Полиморфизм (от греч. «много форм») позволяет объектам разных классов реагировать на одни и те же вызовы по-своему. В C# есть два основных вида:

  1. Полиморфизм наследования — через виртуальные методы (virtual/override)
  2. Полиморфизм интерфейсов — через контракты (interface)

Когда ты вызываешь метод у объекта, C# смотрит на реальный тип объекта, а не на тип переменной. Это называется динамическая диспетчеризация. Представь, что ты говоришь «Подай звук» собаке — она гавкает, кошке — мяукает. Ты не знаешь, кто именно перед тобой, но знаешь, что каждый умеет издавать звук.

Ключевой момент: Полиморфизм работает только через наследование или интерфейсы. Если у двух классов нет общего предка, полиморфизма не будет.

Синтаксис в C# (.NET): все формы и варианты

Инкапсуляция: свойства и поля

В C# есть несколько способов реализовать инкапсуляцию:

// 1. Авто-свойства (самый простой способ)
public class Person
{
    public string Name { get; set; }  // Автоматически создаёт поле
    public int Age { get; private set; }  // Только чтение снаружи
}

// 2. Полные свойства с логикой
public class BankAccount
{
    private decimal _balance;  // Приватное поле
    
    public decimal Balance
    {
        get { return _balance; }
        private set  // Сеттер только внутри класса
        {
            if (value < 0)
                throw new ArgumentException("Баланс не может быть отрицательным");
            _balance = value;
        }
    }
    
    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Сумма должна быть положительной");
        Balance += amount;  // Используем свойство, а не поле
    }
}

// 3. Свойства только для чтения (readonly)
public class Configuration
{
    public string ConnectionString { get; }  // Можно задать только в конструкторе
    
    public Configuration(string connectionString)
    {
        ConnectionString = connectionString;
    }
}

// 4. Вычисляемые свойства
public class Rectangle
{
    public double Width { get; set; }
    public double Height { get; set; }
    
    public double Area => Width * Height;  // Вычисляется каждый раз
}

Полиморфизм: виртуальные методы и переопределение

// Базовый класс с виртуальным методом
public class Animal
{
    public string Name { get; set; }
    
    // virtual — разрешаем переопределять
    public virtual void MakeSound()
    {
        Console.WriteLine("Животное издаёт звук");
    }
}

// Производный класс переопределяет метод
public class Dog : Animal
{
    // override — переопределяем виртуальный метод
    public override void MakeSound()
    {
        Console.WriteLine("Гав-гав!");
    }
}

public class Cat : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Мяу-мяу!");
    }
}

// Использование полиморфизма
public class Zoo
{
    public void LetAllAnimalsSpeak(Animal[] animals)
    {
        foreach (var animal in animals)
        {
            animal.MakeSound();  // Вызовется правильный метод для каждого
        }
    }
}

Полиморфизм через интерфейсы

// Интерфейс — контракт, который обязывает реализовать метод
public interface IShape
{
    double GetArea();
    void Draw();
}

// Классы реализуют интерфейс по-своему
public class Circle : IShape
{
    public double Radius { get; set; }
    
    public double GetArea() => Math.PI * Radius * Radius;
    
    public void Draw()
    {
        Console.WriteLine($"Рисуем круг радиусом {Radius}");
    }
}

public class Square : IShape
{
    public double Side { get; set; }
    
    public double GetArea() => Side * Side;
    
    public void Draw()
    {
        Console.WriteLine($"Рисуем квадрат со стороной {Side}");
    }
}

// Использование
public class DrawingApp
{
    public void RenderShapes(IShape[] shapes)
    {
        foreach (var shape in shapes)
        {
            shape.Draw();  // Полиморфный вызов
            Console.WriteLine($"Площадь: {shape.GetArea()}");
        }
    }
}

Примеры кода — от простого к сложному

Пример 1: Простейшая инкапсуляция

// Создаём класс "Студент" с защищёнными данными
public class Student
{
    // Приватное поле — никто не может изменить напрямую
    private string _name;
    
    // Публичное свойство для доступа
    public string Name
    {
        get { return _name; }
        set 
        { 
            // Проверка: имя не может быть пустым
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("Имя не может быть пустым");
            _name = value; 
        }
    }
    
    // Авто-свойство для возраста
    public int Age { get; set; }
    
    // Метод для вывода информации
    public void PrintInfo()
    {
        Console.WriteLine($"Студент: {Name}, возраст: {Age}");
    }
}

// Использование
Student student = new Student();
student.Name = "Иван";  // Работает через свойство
student.Age = 20;
student.PrintInfo();  // Вывод: Студент: Иван, возраст: 20

// student._name = "Пётр";  // Ошибка! _name — private

Пример 2: Банковский счёт с защитой

public class BankAccount
{
    private decimal _balance;  // Баланс — только внутри
    
    public string AccountNumber { get; }  // Номер счёта — только для чтения
    
    public BankAccount(string accountNumber, decimal initialBalance)
    {
        AccountNumber = accountNumber;
        _balance = initialBalance >= 0 ? initialBalance : 0;
    }
    
    // Метод для пополнения
    public void Deposit(decimal amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("Сумма пополнения должна быть положительной");
            return;
        }
        _balance += amount;
        Console.WriteLine($"Пополнено на {amount}. Баланс: {_balance}");
    }
    
    // Метод для снятия
    public bool Withdraw(decimal amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("Сумма снятия должна быть положительной");
            return false;
        }
        if (amount > _balance)
        {
            Console.WriteLine("Недостаточно средств");
            return false;
        }
        _balance -= amount;
        Console.WriteLine($"Снято {amount}. Баланс: {_balance}");
        return true;
    }
    
    // Метод для просмотра баланса (только чтение)
    public decimal GetBalance()
    {
        return _balance;
    }
}

// Использование
BankAccount account = new BankAccount("12345", 1000);
account.Deposit(500);    // Пополнено на 500. Баланс: 1500
account.Withdraw(200);   // Снято 200. Баланс: 1300
account.Withdraw(2000);  // Недостаточно средств
// account._balance = 1000000;  // Ошибка! Поле private

Пример 3: Полиморфизм с животными

// Базовый класс
public class Animal
{
    public string Name { get; set; }
    
    public Animal(string name)
    {
        Name = name;
    }
    
    // Виртуальный метод — можно переопределить
    public virtual void Speak()
    {
        Console.WriteLine($"{Name} издаёт звук");
    }
}

// Наследники
public class Dog : Animal
{
    public Dog(string name) : base(name) { }
    
    public override void Speak()
    {
        Console.WriteLine($"{Name} говорит: Гав-гав!");
    }
}

public class Cat : Animal
{
    public Cat(string name) : base(name) { }
    
    public override void Speak()
    {
        Console.WriteLine($"{Name} говорит: Мяу-мяу!");
    }
}

public class Cow : Animal
{
    public Cow(string name) : base(name) { }
    
    public override void Speak()
    {
        Console.WriteLine($"{Name} говорит: Му-у-у!");
    }
}

// Демонстрация полиморфизма
public class AnimalShow
{
    public static void Main()
    {
        // Создаём массив разных животных
        Animal[] animals = new Animal[]
        {
            new Dog("Бобик"),
            new Cat("Мурка"),
            new Cow("Зорька"),
            new Dog("Шарик")
        };
        
        // Полиморфный вызов — каждый говорит по-своему
        foreach (Animal animal in animals)
        {
            animal.Speak();  // Вызовется правильный метод
        }
    }
}

Пример 4: Интерфейсы для разных устройств

// Интерфейс для всех устройств, которые можно включить
public interface ISwitchable
{
    void TurnOn();
    void TurnOff();
    bool IsOn { get; }
}

// Телевизор
public class TV : ISwitchable
{
    private bool _isOn;
    public bool IsOn => _isOn;
    
    public void TurnOn()
    {
        _isOn = true;
        Console.WriteLine("Телевизор включён. Показывает каналы.");
    }
    
    public void TurnOff()
    {
        _isOn = false;
        Console.WriteLine("Телевизор выключен.");
    }
}

// Лампа
public class Lamp : ISwitchable
{
    private bool _isOn;
    public bool IsOn => _isOn;
    
    public void TurnOn()
    {
        _isOn = true;
        Console.WriteLine("Лампа светит.");
    }
    
    public void TurnOff()
    {
        _isOn = false;
        Console.WriteLine("Лампа погасла.");
    }
}

// Пульт управления — работает с любым ISwitchable
public class RemoteControl
{
    public void TogglePower(ISwitchable device)
    {
        if (device.IsOn)
            device.TurnOff();
        else
            device.TurnOn();
    }
}

// Использование
RemoteControl remote = new RemoteControl();
TV tv = new TV();
Lamp lamp = new Lamp();

remote.TogglePower(tv);   // Телевизор включён
remote.TogglePower(lamp); // Лампа светит
remote.TogglePower(tv);   // Телевизор выключен

Пример 5: Абстрактный класс и фабрика

// Абстрактный класс — нельзя создать экземпляр, только наследовать
public abstract class Shape
{
    public string Color { get; set; }
    
    // Абстрактный метод — обязательно переопределить
    public abstract double GetArea();
    
    // Обычный метод
    public void Display()
    {
        Console.WriteLine($"Фигура цвета {Color}, площадь: {GetArea()}");
    }
}

public class Circle : Shape
{
    public double Radius { get; set; }
    
    public override double GetArea()
    {
        return Math.PI * Radius * Radius;
    }
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    
    public override double GetArea()
    {
        return Width * Height;
    }
}

// Фабрика — создаёт объекты по типу
public class ShapeFactory
{
    public static Shape CreateShape(string type, string color)
    {
        switch (type.ToLower())
        {
            case "circle":
                return new Circle { Color = color, Radius = 5 };
            case "rectangle":
                return new Rectangle { Color = color, Width = 4, Height = 6 };
            default:
                throw new ArgumentException("Неизвестный тип фигуры");
        }
    }
}

// Использование
Shape shape1 = ShapeFactory.CreateShape("circle", "красный");
Shape shape2 = ShapeFactory.CreateShape("rectangle", "синий");

shape1.Display();  // Фигура цвета красный, площадь: 78.539...
shape2.Display();  // Фигура цвета синий, площадь: 24

Пример 6: Сложная инкапсуляция с событиями

public class TemperatureSensor
{
    private double _temperature;
    private double _minTemperature;
    private double _maxTemperature;
    
    // Событие — уведомление о превышении температуры
    public event EventHandler TemperatureExceeded;
    
    public TemperatureSensor(double minTemp, double maxTemp)
    {
        _minTemperature = minTemp;
        _maxTemperature = maxTemp;
        _temperature = 20;  // Начальная температура
    }
    
    // Свойство с проверкой
    public double Temperature
    {
        get { return _temperature; }
        set
        {
            if (value < -273.15)  // Абсолютный ноль
                throw new ArgumentException("Температура ниже абсолютного нуля");
            
            _temperature = value;
            
            // Проверяем, не вышла ли температура за пределы
            if (_temperature > _maxTemperature || _temperature < _minTemperature)
            {
                OnTemperatureExceeded(new TemperatureEventArgs(_temperature));
            }
        }
    }
    
    // Защищённый метод для вызова события
    protected virtual void OnTemperatureExceeded(TemperatureEventArgs e)
    {
        TemperatureExceeded?.Invoke(this, e);
    }
}

// Класс для данных события
public class TemperatureEventArgs : EventArgs
{
    public double CurrentTemperature { get; }
    
    public TemperatureEventArgs(double temperature)
    {
        CurrentTemperature = temperature;
    }
}

// Использование
TemperatureSensor sensor = new TemperatureSensor(0, 100);
sensor.TemperatureExceeded += (sender, e) =>
{
    Console.WriteLine($"Внимание! Температура {e.CurrentTemperature}°C вышла за пределы!");
};

sensor.Temperature = 25;   // Нормально
sensor.Temperature = 150;  // Событие: Внимание! Температура 150°C вышла за пределы!

Пример 7: Полиморфизм с generic-типами

// Базовый класс для всех репозиториев
public abstract class Repository
{
    protected List _items = new List();
    
    public abstract void Add(T item);
    public abstract T GetById(int id);
    public abstract List GetAll();
}

// Репозиторий для пользователей
public class UserRepository : Repository
{
    public override void Add(User user)
    {
        if (user == null)
            throw new ArgumentNullException(nameof(user));
        _items.Add(user);
        Console.WriteLine($"Пользователь {user.Name} добавлен");
    }
    
    public override User GetById(int id)
    {
        return _items.FirstOrDefault(u => u.Id == id);
    }
    
    public override List GetAll()
    {
        return new List(_items);
    }
}

// Класс пользователя
public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

// Использование
UserRepository userRepo = new UserRepository();
userRepo.Add(new User { Id = 1, Name = "Анна", Email = "anna@mail.com" });
userRepo.Add(new User { Id = 2, Name = "Борис", Email = "boris@mail.com" });

User found = userRepo.GetById(1);
Console.WriteLine($"Найден пользователь: {found.Name}");  // Анна

Сравнение с Python: чем похоже и чем отличается

Аспект C# Python
Инкапсуляция Строгая: private, protected, public Соглашение: _private, __private (name mangling)
Свойства Встроенные get/set Через @property декоратор
Полиморфизм Через virtual/override или интерфейсы Утиная типизация — любой объект с нужным методом
Интерфейсы Явные: interface IMyInterface Неявные: через абстрактные классы или протоколы
Наследование Одиночное (один базовый класс) Множественное (несколько базовых классов)
Статическая типизация Да — типы проверяются на этапе компиляции Нет — типы проверяются во время выполнения
Совет: Если ты знаешь Python, помни: в C# нужно явно указывать, что метод можно переопределить (virtual). В Python все методы по умолчанию виртуальные. Это делает C# более безопасным, но требует больше кода.

Типичные ошибки и подводные камни

Ошибка 1: Прямой доступ к приватным полям

public class BadExample
{
    public int _data;  // Ошибка: поле public, нарушает инкапсуляцию
}

// Правильно:
public class GoodExample
{
    private int _data;
    public int Data 
    { 
        get { return _data; }
        set { _data = value; }
    }
}

Ошибка 2: Забыли про virtual или override

public class Base
{
    public void Method() { Console.WriteLine("Base"); }
}

public class Derived : Base
{
    public void Method() { Console.WriteLine("Derived"); }  // Предупреждение! Скрывает метод
}

// Использование
Base obj = new Derived();
obj.Method();  // Выведет "Base", а не "Derived"! Полиморфизм не работает

Ошибка 3: Изменение коллекции через свойство

public class Team
{
    public List Members { get; set; } = new List();
}

// Проблема: кто угодно может изменить список
Team team = new Team();
team.Members.Clear();  // Удалили всех членов команды!

// Решение: возвращать копию или использовать ReadOnlyCollection
public class SafeTeam
{
    private List _members = new List();
    
    public IReadOnlyList Members => _members.AsReadOnly();
    
    public void AddMember(string name)
    {
        _members.Add(name);
    }
}

Ошибка 4: Нарушение принципа подстановки Лисков

public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }
    public int Area => Width * Height;
}

public class Square : Rectangle
{
    public override int Width
    {
        set { base.Width = base.Height = value; }
    }
    public override int Height
    {
        set { base.Width = base.Height = value; }
    }
}

// Проблема: квадрат нарушает поведение прямоугольника
Rectangle rect = new Square();
rect.Width = 5;
rect.Height = 10;
Console.WriteLine(rect.Area);  // Ожидаем 50, получим 100!

Ошибка 5: Чрезмерное использование public

// Плохо: всё открыто
public class User
{
    public string Name;
    public int Age;
    public string Password;  // Опасность! Пароль в открытом доступе
}

// Хорошо: только нужное
public class SafeUser
{
    public string Name { get; private set; }
    public int Age { get; private set; }
    private string _password;
    
    public void SetPassword(string password)
    {
        if (password.Length < 6)
            throw new ArgumentException("Пароль слишком короткий");
        _password = password;
    }
}

Лучшие практики и советы профессионалов

  1. Принцип наименьших привилегий: делай поля private по умолчанию. Открывай только то, что действительно нужно.
  2. Используй авто-свойства для простых случаев, полные свойства — когда нужна логика.
  3. Предпочитай интерфейсы абстрактным классам, если нет общей реализации.
  4. Не делай методы virtual без необходимости — это усложняет тестирование и понимание кода.
  5. Используй sealed для классов, которые не должны наследоваться.
  6. Возвращай IReadOnlyList<T> вместо List<T>, чтобы защитить данные.
  7. Проверяй аргументы в публичных методах — это часть инкапсуляции.
  8. Используй паттерн "Стратегия" для полиморфизма вместо длинных switch-case.
Золотое правило: Класс должен быть закрыт для изменений, но открыт для расширения (Принцип открытости/закрытости). Инкапсуляция и полиморфизм — главные инструменты для этого.

Вопросы для самопроверки

  1. Что такое инкапсуляция и зачем она нужна?

    Ответ: Инкапсуляция — это скрытие внутренних данных объекта и предоставление доступа к ним только через методы/свойства. Нужна для защиты данных, упрощения использования и возможности изменять внутреннюю реализацию без влияния на внешний код.

  2. Какие модификаторы доступа есть в C#?

    Ответ: public (доступен всем), private (только внутри класса), protected (класс и наследники), internal (внутри сборки), protected internal (наследники или внутри сборки).

  3. Чем отличается virtual от abstract?

    Ответ: virtual — метод имеет реализацию по умолчанию, которую можно переопределить. abstract — метод не имеет реализации, его обязательно переопределить в наследнике. Класс с abstract методом тоже должен быть абстрактным.

  4. Что такое полиморфизм и как он реализуется в C#?

    Ответ: Полиморфизм — способность объектов разных классов реагировать на один и тот же вызов по-своему. Реализуется через наследование (virtual/override) или интерфейсы.

  5. Можно ли переопределить статический метод?

    Ответ: Нет. Статические методы не являются виртуальными и не участвуют в полиморфизме. Они принадлежат классу, а не объекту.

  6. Что произойдёт, если вызвать метод у объекта через базовый класс?

    Ответ: Если метод виртуальный, вызовется реализация из реального типа объекта (динамическая диспетчеризация). Если метод не виртуальный, вызовется реализация из типа переменной.

  7. Как защитить коллекцию от изменений снаружи?

    Ответ: Использовать IReadOnlyList<T> или ReadOnlyCollection<T>. Возвращать копию коллекции или использовать AsReadOnly().

  8. В чём разница между интерфейсом и абстрактным классом?

    Ответ: Интерфейс содержит только объявления методов (без реализации). Абстрактный класс может содержать реализацию, поля, конструкторы. Класс может реализовать много интерфейсов, но наследовать только один абстрактный класс.

  9. Что такое принцип подстановки Лисков?

    Ответ: Объекты базового класса должны заменяться объектами производного класса без нарушения работы программы. Например, квадрат не должен наследовать от прямоугольника, если меняет его поведение.

  10. Как в C# реализовать свойство только для чтения?

    Ответ: Использовать авто-свойство без set (public int Id { get; }) или с private set. Значение задаётся в конструкторе.

Итог и что изучить дальше

Мы разобрали два фундаментальных принципа ООП — инкапсуляцию и полиморфизм. Ты узнал:

  • Как защищать данные с помощью модификаторов доступа и свойств
  • Как создавать гибкие системы через виртуальные методы и интерфейсы
  • Как избегать типичных ошибок и писать качественный код

Теперь ты готов к более сложным темам:

  1. SOLID — пять принципов объектно-ориентированного проектирования
  2. Паттерны проектирования — фабрика, стратегия, наблюдатель и другие
  3. DI (Dependency Injection) — внедрение зависимостей для слабой связанности
  4. LINQ — язык запросов для работы с коллекциями
  5. Асинхронное программирование — async/await для неблокирующих операций
Помни: ООП — это не про синтаксис, а про мышление. Каждый раз, когда пишешь класс, спрашивай себя: «Что я прячу?», «Как это можно расширить?», «Не нарушаю ли я принципы?». С практикой это войдёт в привычку.

Удачи