JavaRush /Курси /C# SELF /Порівняння інтерфейсів та абстрактних класів

Порівняння інтерфейсів та абстрактних класів

C# SELF
Рівень 24 , Лекція 2
Відкрита

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, іноді — несподівана для новачків.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ