JavaRush /Курси /C# SELF /Методи інтерфейсу за замовчуванням

Методи інтерфейсу за замовчуванням

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

1. Від «чистого контракту» до гнучкої архітектури

До C# 8 інтерфейс був як суворий контракт: хочете реалізувати інтерфейс — реалізуйте все до останньої коми. Якщо в ньому зʼявлялися нові члени, усі наявні реалізації мали терміново їх додати, інакше компілятор не дозволяв зібрати проєкт.

Але життя складніше. Уявіть: ви підтримуєте бібліотеку, якою користуються сотні проєктів, і раптом треба додати новий метод до інтерфейсу. Не хочете ламати зворотну сумісність? Саме тут на сцену виходять методи за замовчуванням (Default Interface Methods, DIM)!

У чому суть?

Методи за замовчуванням дозволяють оголосити реалізацію методу прямо в інтерфейсі. Тепер контракт стає гнучкішим: якщо клас не реалізує «новинку», буде використано реалізацію за замовчуванням. Це як дублер у кіно: якщо актор не хоче стрибати з мосту, його замінює каскадер.

2. Синтаксис Default Interface Methods

Як оголошувати методи з реалізацією в інтерфейсі?

Усе дуже схоже на звичайні методи, тільки тепер тіло методу можна (і треба) писати прямо в інтерфейсі:


public interface ILogger
{
    void Log(string message);

    // Новий метод з реалізацією за замовчуванням!
    void LogWarning(string message)
    {
        Log("[WARNING] " + message);
    }
}

Тут LogWarning уже містить реалізацію! Будь-який клас, що реалізує ILogger, зобовʼязаний реалізувати лише Log, а LogWarning матиме реалізацію за замовчуванням (якщо не матиме власної).

Порівняймо: класична й сучасна сигнатури

Версія Оголошення в інтерфейсі
До C# 8
void DoSomething();
C# 8 і новіше
void DoSomething() { Console.WriteLine("Працюю!"); }

Важливі деталі синтаксису

  • Для методу з реалізацією обовʼязково пишіть тіло методу у фігурних дужках.
  • Методи за замовчуванням не можуть бути abstract.
  • Усі методи інтерфейсу, як і раніше, неявно public.
  • Можна оголошувати і властивості з get/set за замовчуванням (див. нижче).

3. Практичні приклади

Приклад 1. Забезпечуємо зворотну сумісність

Припустімо, у вашому застосунку є інтерфейс для збереження даних:


public interface ISaveable
{
    void Save(string filePath);
}

Згодом ви вирішили додати збереження у хмару. Не хочете змінювати сотню класів? Додайте метод за замовчуванням!


public interface ISaveable
{
    void Save(string filePath);

    // Новий метод з реалізацією "за замовчуванням"!
    void SaveToCloud(string cloudService)
    {
        Console.WriteLine($"Зберігаю у хмару {cloudService} (за замовчуванням — нічого не роблю)");
    }
}

Тепер усі старі класи автоматично «вміють» зберігати у хмару (поки що лише виводять повідомлення).

Приклад 2. Розширюємо інтерфейс логера

Раніше ми мали простий інтерфейс для логування:


public interface ILogger
{
    void Log(string message);
}

Додаймо метод за замовчуванням для логування помилок:


public interface ILogger
{
    void Log(string message);

    void LogError(string message)
    {
        Log("[ERROR] " + message);
    }
}

Клас, що реалізує ILogger, може не реалізовувати LogError — спрацює версія за замовчуванням:


public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
    // LogError не реалізуємо — працюватиме реалізація за замовчуванням!
}

ILogger logger = new ConsoleLogger();
logger.Log("Усе добре!");
logger.LogError("Ой, щось пішло не так!"); // викличе реалізацію за замовчуванням

Приклад 3. Методи за замовчуванням + розширюваний застосунок

Ваш застосунок підтримує різні способи експорту: у файл, у базу даних, у мережу. Інтерфейс:


public interface IExporter
{
    void Export(string data, string destination);

    // Новий функціонал — експорт в архів
    void ExportToArchive(string data, string archivePath)
    {
        Console.WriteLine("Архівація за замовчуванням не підтримується.");
    }
}

Плагіни, написані іншими розробниками, і надалі працюватимуть, навіть якщо вони нічого не знають про новий метод.

4. Як працює виклик методів інтерфейсу за замовчуванням?

Сценарій «старий клас — новий інтерфейс»

Якщо клас не реалізує метод за замовчуванням, під час виклику через посилання на інтерфейс спрацює реалізація інтерфейсу. Якщо реалізує — буде використано його власну.


public class FileExporter : IExporter
{
    public void Export(string data, string destination)
    {
        Console.WriteLine("Зберігаю у файл...");
    }
    // ExportToArchive не реалізуємо — буде виведення за замовчуванням
}

IExporter exporter = new FileExporter();
exporter.Export("дані", "file.txt");        // Спрацює реалізація FileExporter
exporter.ExportToArchive("дані", "file.zip"); // Спрацює реалізація за замовчуванням!

Сценарій «клас перекриває метод за замовчуванням»


public class AdvancedExporter : IExporter
{
    public void Export(string data, string destination)
    {
        Console.WriteLine("Зберігаю у розширеному режимі...");
    }

    public void ExportToArchive(string data, string archivePath)
    {
        Console.WriteLine("Архівація підтримується!");
    }
}

IExporter exporter = new AdvancedExporter();
exporter.ExportToArchive("дані", "file.zip"); // Тепер буде викликано реалізацію класу!

5. Що ще можна робити з Default Interface Methods?

Властивості та події за замовчуванням

Можна оголошувати властивості з реалізацією за замовчуванням, якщо вони мають тіло get або set:


public interface IHasId
{
    // Автоматично повертає 42, доки не перевизначено
    int Id => 42;
}

public class Person : IHasId {}
Console.WriteLine(new Person().Id); // 42

Виклики методів за замовчуванням із коду інтерфейсу

Усередині інтерфейсу методи за замовчуванням та інші члени інтерфейсу можуть викликати один одного:


public interface IDemo
{
    void Foo() => Bar();
    void Bar() => Console.WriteLine("BAR");
}

6. Обмеження та особливості Default Interface Methods

Чи можна оголошувати поля, конструктори?

Ні. Навіть із методами за замовчуванням інтерфейс — усе ще не клас. Полів, конструкторів і деструкторів бути не може.

Чи можна застосувати base до інтерфейсу?

Можна, але з нюансами. Усередині методу за замовчуванням інтерфейсу можна викликати метод батьківського інтерфейсу через явне зазначення інтерфейсу:


public interface IBase
{
    void Greet() => Console.WriteLine("Привіт від IBase");
}

public interface IDerived : IBase
{
    void IBase.Greet()
    {
        Console.WriteLine("Привіт від IDerived!");
        IBase.Greet(this); // Викликаємо метод базового інтерфейсу явно
    }
}

Але це рідко потрібно для базових сценаріїв.

Що відбувається при конфлікті реалізацій за замовчуванням?


public interface IA { void Foo() { Console.WriteLine("A"); } }
public interface IB { void Foo() { Console.WriteLine("B"); } }

// Клас не реалізує Foo явно:
public class C : IA, IB { }
// Помилка компіляції: незрозуміло, яку реалізацію вибрати!

7. Типові помилки, обмеження та особливості

Поширена кумедна помилка:
Дехто намагається оголосити поля в інтерфейсі після знайомства з DIM — але поля, як і раніше, не можна. Також, якщо ви раптом спробуєте реалізувати статичний метод за замовчуванням — до C# 8 це неможливо. (Із 8-ї версії вже можна мати статичні методи в інтерфейсах, але дефолтні реалізації статичних методів — окрема тема.)

Особливість: Diamond Problem («проблема ромба»)
Якщо ваш клас реалізує два інтерфейси з однаковим методом за замовчуванням, ви зобовʼязані явно реалізувати цей метод:


public class ConflictClass : IA, IB
{
    public void Foo() // потрібно самостійно обрати варіант!
    {
        // Виклик потрібної реалізації явно через інтерфейс (якщо треба)
        ((IA)this).Foo();
        // або
        ((IB)this).Foo();
    }
}

Не зловживайте!
Методи за замовчуванням рятують зворотну сумісність, але якщо зловживати ними, можна отримати «брудну» архітектуру, де частина логіки розповзається по інтерфейсах. Намагайтеся тримати все важливе в класах, а інтерфейси використовуйте як справжній «контракт».

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