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 получит дефолтную реализацию (если не окажется своей).
Сравните: классическая vs. современная сигнатура
| Версия | Объявление в интерфейсе |
|---|---|
| До 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("Hello from 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();
}
}
Не злоупотребляйте!
Методы по умолчанию спасают обратную совместимость, но если злоупотреблять ими, можно получить "грязную" архитектуру, где часть логики расползается по интерфейсам. Старайтесь держать всё важное — в классах, а интерфейс использовать как действительно "контракт".
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ