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 | |
| C# 8 і новіше | |
Важливі деталі синтаксису
- Для методу з реалізацією обовʼязково пишіть тіло методу у фігурних дужках.
- Методи за замовчуванням не можуть бути 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();
}
}
Не зловживайте!
Методи за замовчуванням рятують зворотну сумісність, але якщо зловживати ними, можна отримати «брудну» архітектуру, де частина логіки розповзається по інтерфейсах. Намагайтеся тримати все важливе в класах, а інтерфейси використовуйте як справжній «контракт».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ