Menu

Абстрактные классы в C#: абстрактные методы, примеры и сравнение с интерфейсом

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

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Абстрактный класс это класс, который существует только для того, чтобы от него наследовались. Его экземпляр нельзя создать, и он может объявлять абстрактные члены: методы и свойства без тела, которые должен реализовать каждый конкретный производный класс. Во всём остальном это обычный класс: у него могут быть поля, конструкторы и полностью реализованные методы, которые наследуют производные классы.

Объявление абстрактного класса

Пометьте класс abstract и пометьте abstract каждый член, который должны предоставить производные классы, закончив его точкой с запятой вместо тела:

Вывод:

Circle: area 7.07, perimeter 9.42
Rectangle: area 10.00, perimeter 13.00

Describe написан один раз, в базовом классе, и вызывает Area и Perimeter, которых в Shape вообще нет. В этом и смысл абстрактного члена: базовый класс может на него полагаться, а компилятор гарантирует, что каждая конкретная фигура его предоставит. Производный класс, который забыл один из них, не компилируется:

class Triangle : Shape
{
    public Triangle() : base("Triangle") { }
    public override double Area() => 6;
}
// error CS0534: 'Triangle' does not implement inherited abstract member 'Shape.Perimeter()'

Исправление: реализовать Perimeter или пометить сам Triangle как abstract, если это промежуточный базовый класс для других классов.

Правила для абстрактных членов

  • У абстрактного члена нет тела, и он неявно виртуальный: производные классы реализуют его через override.
  • Абстрактные члены могут содержать только абстрактные классы. Такой член в обычном классе это ошибка компиляции.
  • Абстрактные члены не могут быть private (никто не смог бы их переопределить) или static в классе.
  • Абстрактными могут быть методы, свойства, индексаторы и события. Поля и конструкторы не могут.
  • Абстрактный класс не обязан содержать ни одного абстрактного члена. Пометка класса abstract сама по себе просто запрещает создание экземпляров, что полезно для базового класса, имеющего смысл только через подклассы.

Абстрактное свойство объявляет, какие методы доступа должен предоставить производный класс:

abstract class Plan
{
    public abstract decimal MonthlyPrice { get; }
    public abstract string Name { get; }
}

class ProPlan : Plan
{
    public override decimal MonthlyPrice => 12.99m;
    public override string Name => "Pro";
}

Конструкторы в абстрактных классах

У абстрактного класса могут быть конструкторы. Их нельзя вызвать через new, но каждый конструктор производного класса выполняет один из них через : base(...), поэтому именно в них инициализируется и проверяется общее состояние. Объявляйте их protected, что говорит ровно это: использовать их могут только производные классы.

Шаблонный метод

Самая частая причина писать абстрактный класс это зафиксировать порядок алгоритма в базовом классе и дать производным классам заполнить отдельные шаги. Базовый метод, выполняющий шаги, не виртуальный, поэтому ни один подкласс не может переставить или пропустить их.

Вывод:

item,qty,price
Coffee beans,2,11.50
Filter papers,1,3.20

| Item | Qty | Price |
| --- | --- | --- |
| Coffee beans | 2 | 11.50 |
| Filter papers | 1 | 3.20 |

Total: 26.20

Каждая строка это именованный кортеж, а вывод собирается через StringBuilder. Обратите внимание на сочетание: Header и Row абстрактные, потому что каждый формат должен определить их сам, а Footer виртуальный с пустой реализацией по умолчанию, потому что большинству форматов там ничего не нужно. Шаги protected, поэтому внешний код может вызвать только Export. Классы фреймворка используют ту же идею: Stream и TextWriter это абстрактные базовые классы, многие удобные методы которых построены на нескольких членах, предоставляемых производным классом (Read и Write для потока, Write(char) для средства записи текста).

Абстрактный класс или интерфейс

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

Абстрактный классИнтерфейс
Класс может наследовать или реализоватьодинмного
Поля экземпляра (состояние)данет
Конструкторыданет
Методы с теломдада, начиная с C# 8 (методы интерфейса по умолчанию)
Статические членыдада, начиная с C# 8; статические абстрактные члены начиная с C# 11
Модификаторы доступа у членовлюбыеpublic по умолчанию; другие разрешены начиная с C# 8
Могут использовать структурынетда
Добавление члена позжебезопасно, если он не абстрактныйломает реализации, если у него нет тела по умолчанию

Методы интерфейса по умолчанию (C# 8) сократили разрыв, но не главное различие:

public interface ILogger
{
    void Write(string message);
    void Error(string message) => Write("ERROR: " + message);   // default implementation: C# 8, .NET Core 3.0+
}

Метод по умолчанию всё равно не может обращаться к полям экземпляра, потому что у интерфейса их нет; он может только вызывать другие члены интерфейса.

Практический способ выбора:

  • Интерфейс, когда вы описываете возможность, которая может быть у несвязанных типов (IComparable<T>, IDisposable, IShippingProvider), когда в этом должны участвовать структуры или когда классу нужно несколько таких ролей. По этой причине большая часть кода с внедрением зависимостей пишется для интерфейсов.
  • Абстрактный класс, когда производные типы действительно являются вариациями одного и того же и разделяют состояние или фиксированный алгоритм, как экспортёры выше.
  • Оба это обычное сочетание: интерфейс, от которого зависят вызывающие, и абстрактный базовый класс, который его реализует и избавляет реализующих от переписывания общих частей. Сторона интерфейсов подробно описана на странице об интерфейсах.

Частые ошибки

  • Попытка создать экземпляр базового класса. new Shape(...) это CS0144. Создавайте экземпляр производного класса.
  • Забытый override в реализации. Запись public double Area() в производном классе не реализует абстрактный член; компилятор сообщает CS0534 об отсутствующей реализации плюс предупреждение, что новый метод скрывает унаследованный.
  • Открытый конструктор у абстрактного класса. Безвредно, но вводит в заблуждение. Используйте protected.
  • Абстрактные классы без общего кода. Если в базовом классе только абстрактные члены и нет состояния, интерфейс скажет то же самое и не займёт единственное место для базового класса.
  • Вызов абстрактных членов из базового конструктора. Реализация производного класса выполняется до тела его конструктора, поэтому поля, которые присваивает это тело, всё ещё равны 0 или null.

Часто задаваемые вопросы

Что такое абстрактный класс в C#?

Класс с пометкой abstract предназначен только для роли базового класса: создать его экземпляр через new нельзя (ошибка CS0144). Он может содержать обычные поля, конструкторы и методы, а также члены abstract, у которых нет тела и которые должен реализовать каждый неабстрактный производный класс.

Чем абстрактный класс отличается от интерфейса в C#?

Класс может наследоваться только от одного абстрактного класса, но реализовать много интерфейсов. Абстрактный класс может хранить состояние (поля экземпляра), иметь конструкторы и члены с любым уровнем доступа, и это естественное место для общей реализации. Интерфейс описывает возможность без состояния экземпляра; начиная с C# 8 он может включать тела методов по умолчанию, но по-прежнему не может иметь полей экземпляра и конструкторов.

Чем абстрактные методы отличаются от виртуальных в C#?

У метода virtual есть тело, которое производные классы могут переопределить. У метода abstract тела нет, и производные классы обязаны его переопределить, иначе они сами становятся абстрактными (ошибка CS0534 для конкретного класса). Абстрактные методы могут находиться только в абстрактных классах.

Может ли у абстрактного класса быть конструктор в C#?

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

Почему нельзя создать экземпляр абстрактного класса?

Потому что у него могут быть абстрактные члены без реализации: у вызова new Shape().Area() не было бы кода для выполнения. Компилятор сообщает CS0144 («Cannot create an instance of the abstract type or interface»). Создайте экземпляр конкретного производного класса и при желании храните его в переменной абстрактного типа: Shape s = new Circle(2);.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ