Абстрактные классы в C# на примерах
Абстрактный класс в C# - это класс с ключевым словом abstract, экземпляр которого создать нельзя. Он служит заготовкой для наследников: хранит общие поля и готовую логику, а недостающие части оставляет на реализацию в производных классах. Ниже показано, как объявлять абстрактные члены, чем такой класс отличается от интерфейса и в каких случаях выбрать каждый из вариантов.

Содержание
Что такое абстрактный класс
Абстрактный класс описывает общую часть группы родственных типов. Например, у кругов, прямоугольников и треугольников есть имя и площадь, но формула площади у каждой фигуры своя. Базовый класс хранит то, что одинаково, и требует от наследников реализовать то, что различается.
Создать объект абстрактного класса напрямую нельзя: компилятор выдаст ошибку. Зато можно объявлять переменные этого типа и записывать в них объекты наследников, что и даёт полиморфизм.
public abstract class Shape
{
public string Name { get; }
protected Shape(string name) => Name = name;
// абстрактный метод: тела нет, реализация обязательна в наследнике
public abstract double Area();
// обычный метод с готовой реализацией
public void PrintInfo() =>
Console.WriteLine($"{Name}: площадь {Area():F2}");
}
public class Circle : Shape
{
private readonly double _radius;
public Circle(double radius) : base("Круг") => _radius = radius;
public override double Area() => Math.PI * _radius * _radius;
}
public class Rectangle : Shape
{
private readonly double _w, _h;
public Rectangle(double w, double h) : base("Прямоугольник") { _w = w; _h = h; }
public override double Area() => _w * _h;
}
// var s = new Shape("x"); // ошибка CS0144: нельзя создать экземпляр
Shape[] shapes = { new Circle(2), new Rectangle(3, 4) };
foreach (var shape in shapes)
shape.PrintInfo();
Абстрактные и виртуальные члены
В абстрактном классе могут быть как абстрактные, так и обычные члены. Абстрактными бывают методы, свойства, индексаторы и события. У них нет тела, и каждый неабстрактный наследник обязан их переопределить через override. Если наследник тоже оставит что-то нереализованным, он сам должен быть помечен как abstract.
Виртуальный член (virtual) отличается тем, что тело у него есть, и наследник вправе его заменить, но не обязан. Абстрактный член заставляет реализовать поведение, виртуальный - позволяет изменить поведение по умолчанию.
public abstract class Employee
{
public string Name { get; init; } = "";
public decimal BaseSalary { get; init; }
// абстрактное свойство
public abstract string Position { get; }
// виртуальный метод с реализацией по умолчанию
public virtual decimal CalculateSalary() => BaseSalary;
}
public class Manager : Employee
{
public override string Position => "Менеджер";
public override decimal CalculateSalary() => BaseSalary * 1.5m;
}
public class Developer : Employee
{
public override string Position => "Разработчик";
// CalculateSalary не переопределён, используется базовая версия
}
Конструкторы и поля
Конструктор у абстрактного класса есть и вызывается при создании наследника через base(...). Его делают protected, ведь публичный конструктор бессмыслен. Подробности цепочки вызовов описаны в материале про конструкторы при наследовании.
Ещё одно важное отличие от интерфейса: абстрактный класс может хранить состояние, то есть иметь поля и автоматические свойства с данными. Это позволяет не дублировать одинаковые поля в каждом наследнике.
Абстрактный класс или интерфейс
Оба механизма описывают контракт, но решают разные задачи. Интерфейс говорит, что объект умеет делать, а абстрактный класс - чем объект является и какие общие данные у него есть. Реализация интерфейсов подробно разобрана в статье про интерфейсы в C#.
| Признак | Абстрактный класс | Интерфейс |
|---|---|---|
| Наследование | только один базовый класс | класс реализует сколько угодно интерфейсов |
| Поля и состояние | можно хранить поля | полей нет, только свойства без данных |
| Конструктор | есть | нет |
| Готовая реализация | любые методы с телом | реализация по умолчанию с C# 8, ограниченно |
| Модификаторы доступа | public, protected, internal и другие | члены по умолчанию public |
| Смысл связи | "является" (Собака - Животное) | "умеет" (Собака - Бегающее) |
| Структуры | структура не может наследоваться | структура может реализовать интерфейс |
| Изменение контракта | новый абстрактный член ломает наследников | новый член тоже ломает, если нет реализации по умолчанию |
Когда выбрать абстрактный класс
- У типов есть общее состояние: поля, свойства с данными, общий конструктор.
- Нужна готовая общая логика, которую наследники используют без изменений, и несколько точек расширения.
- Требуются защищённые члены (
protected), доступные только наследникам. - Иерархия действительно строится по принципу "является": вид документа, тип платежа, форма фигуры.
Когда выбрать интерфейс
- Нужно одно и то же поведение у несвязанных типов:
IDisposable,IComparable,ILogger. - Класс уже наследуется от другого и не может взять второго родителя.
- Требуется подмена реализации в тестах и внедрение зависимостей.
- Реализовывать контракт будут структуры.
На практике оба подхода часто совмещают: интерфейс описывает контракт для внешнего кода, а абстрактный класс даёт базовую реализацию, от которой удобно наследоваться. Так устроены многие классы библиотеки, например Stream и TextWriter.
public interface INotifier
{
void Send(string message);
}
public abstract class NotifierBase : INotifier
{
protected int SentCount { get; private set; }
public void Send(string message)
{
string prepared = Prepare(message);
Deliver(prepared);
SentCount++;
}
protected virtual string Prepare(string message) => message.Trim();
// единственное, что обязан сделать наследник
protected abstract void Deliver(string message);
}
public class ConsoleNotifier : NotifierBase
{
protected override void Deliver(string message) =>
Console.WriteLine($"[консоль] {message}");
}
INotifier notifier = new ConsoleNotifier();
notifier.Send(" Заказ оформлен ");
Шаблонный метод на примере абстрактного класса
Один из самых частых приёмов с абстрактными классами - шаблонный метод. Базовый класс задаёт порядок шагов алгоритма, а отдельные шаги отдаёт наследникам. Порядок не нарушится, потому что публичный метод не виртуальный.
public abstract class ReportGenerator
{
public string Build()
{
var sb = new System.Text.StringBuilder();
sb.AppendLine(Header());
foreach (var line in Rows())
sb.AppendLine(line);
sb.AppendLine(Footer());
return sb.ToString();
}
protected abstract string Header();
protected abstract IEnumerable<string> Rows();
protected virtual string Footer() => "Конец отчёта";
}
public class SalesReport : ReportGenerator
{
protected override string Header() => "Отчёт по продажам";
protected override IEnumerable<string> Rows()
{
yield return "Январь: 120";
yield return "Февраль: 150";
}
}
Console.WriteLine(new SalesReport().Build());
Ограничения и особенности
Абстрактный класс нельзя сочетать с модификатором sealed: первый требует наследования, второй его запрещает. Нельзя объявить абстрактным статический класс и приватный абстрактный метод, ведь наследник его не увидит. Абстрактный метод не может быть static и не может иметь тела.
Допустимо, что абстрактный класс не содержит ни одного абстрактного члена. Это делают, чтобы запретить создание экземпляров базового типа, оставив при этом полностью готовое поведение. Наследник может также переопределить виртуальный метод как абстрактный, заставив своих потомков реализовать его заново.
Начиная с C# 11 интерфейсы получили статические абстрактные члены, но к абстрактным классам это не относится. Для них по-прежнему действуют обычные правила наследования.
Типичные ошибки
- Создание иерархии ради повторного использования кода, когда отношения "является" нет. Здесь лучше подойдёт композиция, например агрегация.
- Слишком много абстрактных членов: наследникам приходится реализовывать десяток методов, половина которых пустые.
- Вызов виртуального или абстрактного метода из конструктора базового класса. Наследник ещё не инициализирован, и метод может обратиться к пустым полям.
- Забытое ключевое слово
overrideв наследнике: без него метод только скрывает базовый, а не переопределяет. - Публичный конструктор у абстрактного класса. Он не вредит, но вводит в заблуждение, поэтому лучше
protected.
Частые вопросы
Можно ли создать экземпляр абстрактного класса?
Нет, компилятор запретит new для абстрактного типа. Можно создать объект наследника и обращаться к нему через переменную базового типа.
Может ли в абстрактном классе быть обычный метод?
Да, абстрактный класс свободно сочетает методы с реализацией и абстрактные члены. Именно эта возможность и отличает его от классического интерфейса.
Может ли абстрактный класс реализовывать интерфейс?
Да. Если он не хочет реализовывать часть членов интерфейса, он объявляет их абстрактными, и реализация переходит к наследникам.
Можно ли наследоваться от нескольких абстрактных классов?
Нет, в C# допускается только один базовый класс. Если нужно сочетать несколько контрактов, используйте интерфейсы.
Что выбрать, если сомневаюсь?
Начните с интерфейса: он гибче и не навязывает иерархию. Переходите к абстрактному классу, когда появляется общее состояние и повторяющийся код у наследников.
Чем abstract отличается от virtual?
Абстрактный метод не имеет тела и обязателен к переопределению, виртуальный имеет тело по умолчанию и переопределяется по желанию.





