На собесе по ООП редко спрашивают определения. Чаще дают код с virtual, new и конструкторами и просят предсказать вывод, а потом обсуждают дизайн: интерфейс или абстрактный класс, наследование или композиция.
Четыре принципа на языке C#
- Инкапсуляция: состояние закрыто (
private), меняется только методами, которые держат инварианты. Свойствоpublic decimal Balance { get; private set; }нельзя уменьшить снаружи. - Наследование:
class Admin : User, один базовый класс, сколько угодно интерфейсов. - Полиморфизм:
virtual/overrideи интерфейсы, вызов идёт по реальному типу объекта. - Абстракция:
abstract-классы и интерфейсы описывают контракт без деталей.
virtual, override, new
class Base { public virtual string Name() => "Base"; public string Hi() => "Base"; }
class Derived : Base
{
public override string Name() => "Derived";
public new string Hi() => "Derived";
}
Base b = new Derived();
b.Name(); // "Derived": виртуальный вызов
b.Hi(); // "Base": new скрывает метод, а не переопределяет
override участвует в полиморфизме, new нет: какой метод вызовется, решает статический тип переменной. Без new компилятор предупредит (CS0108). sealed override запрещает переопределять дальше.
Абстрактный класс или интерфейс
| Абстрактный класс | Интерфейс | |
|---|---|---|
| Наследование | один базовый класс | много интерфейсов |
| Поля и конструкторы | есть | нет |
| Реализация методов | есть | с C# 8 методы по умолчанию |
| Статика | есть | с C# 11 static abstract |
Интерфейс описывает возможность («умеет сохраняться»), абстрактный класс общую основу семейства с состоянием («шаблонный метод»).
Модификаторы доступа
public, private (по умолчанию для членов), protected, internal (внутри сборки, по умолчанию для классов), protected internal (наследники ИЛИ сборка), private protected (наследники И сборка), file (C# 11, только в файле).
Порядок инициализации
При new Derived() в C#:
- Статический конструктор
Derived(один раз). - Инициализаторы полей
Derived. - Статический конструктор
Base(при первом обращении к типу). - Инициализаторы полей
Base. - Тело конструктора
Base. - Тело конструктора
Derived.
Отсюда ловушка: виртуальный метод, вызванный из конструктора базы, выполнится в наследнике, у которого тело конструктора ещё не отработало. Поля, заданные в теле, будут нулевыми.
Статика
Статическое поле одно на тип (для дженерика одно на каждый закрытый тип). Статический конструктор вызывается средой один раз, потокобезопасно, перед первым использованием. Исключение в нём делает тип непригодным: дальше TypeInitializationException.
SOLID коротко
- S: одна причина для изменения класса.
- O: расширяем новыми реализациями, а не правкой
switch. - L: наследник подставим вместо базы без сюрпризов (
NotImplementedExceptionв реализации интерфейса это красный флаг). - I: узкие интерфейсы.
- D: зависимость от абстракций, внедрение через конструктор.
Композиция вместо наследования
Наследование жёстко связывает с базовым классом. Чтобы добавить поведение, чаще пишут декоратор: CachedRepository : IRepository принимает внутренний репозиторий и добавляет кеширование. Наследование оставляют для настоящего «является» и шаблонного метода, как у BackgroundService.ExecuteAsync.
Частые ошибки
- Путать
newиoverride. - Вызывать виртуальные методы в конструкторе.
- Делать
protectedполя вместоprotectedметодов: наследники ломают инварианты. - Строить глубокие иерархии ради переиспользования кода.
Что спросят на собесе
- Чем
overrideотличается отnew? - Абстрактный класс или интерфейс: что и когда?
- В каком порядке выполняются конструкторы и инициализаторы полей?
- Расскажите SOLID на примерах из своего кода.
- Почему «предпочитайте композицию наследованию»?