1. Коротко про класичні відмінності
Якщо хтось каже: «Інтерфейс — це просто набір сигнатур», запитайте: «А на якій версії C# ви пишете?» Починаючи з C# 8 і новіших версій, інтерфейси стали значно потужнішими. Час порівняти їх з абстрактними класами — не лише за класичними властивостями, а й з урахуванням усіх нових можливостей платформи .NET.
Якщо повернутися на кілька років тому — до C# 7, — усе було просто. Абстрактний клас може визначати поля й частково реалізовані методи, інтерфейс — лише сигнатури (методів, властивостей, подій, індексаторів).
Наслідування абстрактного класу реалізується через відношення "є" (Is-a), інтерфейси реалізують множинне наслідування поведінки ("вміє", can-do).
| Характеристика | Абстрактний клас | Інтерфейс (до C# 8) |
|---|---|---|
| Відношення | is-a | can-do |
| Наслідування | Лише одне | Множинне |
| Поля | Може містити | Не може |
| Реалізація методів | Може | Не може |
| Конструктори | Може | Не може |
| Модифікатори доступу | Різні (public, protected, ...) | Лише неявно public |
Як бачите, раніше абстрактні класи були, по суті, «старшими братами» інтерфейсів — потужнішими й гнучкішими. Але все змінюється!
2. Інтерфейси з реалізацією за замовчуванням
З виходом C# 8 (а тим більше в C# 14 та .NET 9) інтерфейси здобули вагому можливість — методи з реалізацією за замовчуванням, які називають «Default Interface Methods» (DIM).
Як це виглядає?
public interface IAnimal
{
void SayHello();
// Метод з реалізацією за замовчуванням!
void Walk()
{
Console.WriteLine("Я йду...");
}
}
Зверніть увагу: інтерфейс тепер може містити реалізацію метода. Причому не одну, а скільки потрібно. Втім, є нюанс: такі методи обовʼязково мають бути явно оголошені з тілом, а все інше (поля, приватні методи, конструктори) — як і раніше заборонено.
3. Сучасні можливості інтерфейсів
Нові можливості, про які варто знати сучасному .NET-розробнику:
- Методи з реалізацією за замовчуванням.
- Приватні методи всередині інтерфейсу (лише для допоміжних цілей, доступні тільки іншим методам цього ж інтерфейсу).
- Статичні методи (починаючи з C# 8).
- Властивості з реалізацією за замовчуванням.
- Статичні поля (починаючи з C# 14 — «static interface members»).
- Абстрактні статичні члени («abstract static members» — так, тепер інтерфейс може вимагати від реалізації певні статичні методи!).
Приклад повного сучасного інтерфейсу:
public interface ILogger
{
static int LoggerCount { get; set; } // C# 14
void Log(string message); // Сигнатура (контракт)
// Default implementation
void LogWarning(string warning)
{
Log("[WARNING]: " + warning);
}
// Приватний допоміжний метод в інтерфейсі (C# 8+)
private void FormatAndLog(string level, string msg)
{
Log($"{level}: {msg}");
}
// Статичний метод в інтерфейсі (C# 8+)
static void PrintLoggerInfo()
{
Console.WriteLine("Інтерфейс ILogger — ваш найкращий помічник!");
}
}
Раніше таке здавалося неможливим.
4. Абстрактні класи: що нового?
Абстрактні класи… як би це сказати… майже не змінилися за останнє десятиліття. Вони, як і раніше, можуть містити:
- Поля (зокрема приватні, захищені та статичні).
- Реалізовані й абстрактні методи.
- Конструктори (так, можна створювати абстрактні класи з логікою ініціалізації).
- Властивості, події, індексатори.
- Статичні та екземплярні члени.
Приклад абстрактного класу:
public abstract class Animal
{
public string Name { get; set; }
public abstract void Speak();
public virtual void Walk()
{
Console.WriteLine($"{Name} крокує на лапках!");
}
protected void Eat()
{
Console.WriteLine($"{Name} їсть корм.");
}
}
Абстрактний клас і надалі є доречним вибором для зберігання спільної логіки, стану та поведінки в ієрархіях класів.
5. Сучасне порівняння: таблиця з урахуванням нових можливостей
| Характеристика | Абстрактний клас | Інтерфейс (C# 14+, .NET 9) |
|---|---|---|
| Відношення | is-a (є) | can-do (вміє) |
| Наслідування | Лише одне | Множинне |
| Поля | Так, будь-які | Лише статичні* (C# 14+) |
| Конструктори | Так | Ні |
| Реалізація методів | Так (virtual/abstract) | Так (default, static, abstract static) |
| Властивості з реалізацією | Так | Так (реалізація за замовчуванням) |
| Приватні члени | Так | Так (лише методи, C# 8+) |
| Статичні члени | Так | Так (C# 8+, з обмеженнями) |
| Статичні поля | Так | Так* (C# 14+) |
| Модифікатори доступу | Будь-які | За замовчуванням public або private |
* — У інтерфейсах статичні поля зазвичай використовують у виняткових випадках, і це зовсім свіжа можливість мови.
6. Де використовувати інтерфейс, а де — абстрактний клас: сучасні рекомендації
Інтерфейси (а тепер і з реалізацією за замовчуванням) — інструмент для створення контрактів між компонентами. Їхня ключова особливість — множинність. Ваш клас може реалізовувати скільки завгодно різних інтерфейсів, що робить його універсальним.
Абстрактний клас залишається вашим вибором, якщо:
- Потрібні спільний стан (поля), логіка й поведінка, які успадковуватимуть інші класи.
- Потрібна типова, але така, що може бути перевизначена, логіка (скористайтеся virtual).
- Потрібно централізувати ініціалізацію через конструктор.
У реальних проєктах часто трапляється така схема: «чисті контракти» формулюють в інтерфейсах, а якщо потрібен спільний код чи інфраструктура для підкласів — створюють абстрактний базовий клас.
. ┌────────────────────────┐
│ Інтерфейс │
│ (контракт: що вміє) │
└─────────┬──────────────┘
│
┌──────────────┼──────────────┐
│ │ │
Реалізація 1 Реалізація 2 ... Реалізація N
MyLogger CloudLogger FileLogger
(можна комбінувати з наслідуванням абстрактного класу)
7. Сценарії — коли що виграє
Множинна реалізація:
Припустімо, у вас є інтерфейс IDrivable і абстрактний клас Vehicle. Тепер клас Car може наслідувати базу — Vehicle — і водночас реалізовувати кілька інтерфейсів (IDrivable, IRepairable, IInsurable). Якби у вас був абстрактний клас Repairable, довелося б обирати — або Vehicle, або Repairable! Інтерфейси тут очевидно перемагають.
Спільна логіка й стан:
Припустімо, у всього «автотранспорту» є поле «номер». Це має бути полем абстрактного класу. В інтерфейсі поля (за винятком статичних) недоступні.
Еволюція API:
Одна з ключових переваг Default Interface Methods — тепер інтерфейси можна розширювати, не ризикуючи зламати наявні реалізації.
Наприклад, додали в інтерфейс новий метод із реалізацією за замовчуванням — усе працює: усі старі реалізації інтерфейсу залишаються працездатними. Раніше це було болісно або й узагалі неможливо.
8. Приклади на практиці
У нашому навчальному застосунку поступово зʼявляється логування. Створімо свій інтерфейс ILogger з реалізацією за замовчуванням:
public interface ILogger
{
void Log(string message);
// Реалізація за замовчуванням доступна всім реалізаціям інтерфейсу!
void LogInfo(string info)
{
Log("[INFO] " + info);
}
// Статичний метод інтерфейсу
static void PrintHelp()
{
Console.WriteLine("Використовуйте ILogger для логування подій");
}
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
// Десь у коді:
ILogger logger = new ConsoleLogger();
logger.LogInfo("Система запущена!"); // працює завдяки default-реалізації
// Виклик статичного методу інтерфейсу
ILogger.PrintHelp();
Якби ми додали новий метод із реалізацією за замовчуванням в інтерфейс, усі наявні реалізації (наприклад, ConsoleLogger) автоматично отримали б цей метод — без паніки й поломки коду.
9. Помилки та нюанси: практика й підводні камені
Варто знати, що не все так просто, як може здатися на перший погляд. Наприклад, якщо інтерфейс містить реалізацію за замовчуванням, але звертаються до обʼєкта через тип класу, а не через тип інтерфейсу, реалізація за замовчуванням доступна лише через інтерфейс.
ConsoleLogger log = new ConsoleLogger();
log.LogInfo("Hello"); // Не компілюється: LogInfo не визначено в класі!
ILogger log2 = log;
log2.LogInfo("Hello"); // Усе гаразд!
Це схоже на приватний випадок явної реалізації інтерфейсу. Іноді така поведінка зручна для приховування «зайвого» API, іноді — несподівана для новачків.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ