This lesson is not translated yet – the Russian original is below.
Абстрактный класс отвечает на вопрос «чем эти типы похожи». Интерфейс отвечает на другой – «что этот тип умеет». Разница кажется тонкой, но следствия из неё большие.
Интерфейс
Интерфейс – список того, что класс обязуется предоставить. Реализации в нём нет:
public interface IPlayable
{
void Play();
void Stop();
}
Класс объявляет, что его поддерживает, и обязан реализовать всё перечисленное:
public class Song : IPlayable
{
public string Title { get; set; }
public void Play()
{
Console.WriteLine($"Играет {Title}");
}
public void Stop()
{
Console.WriteLine("Остановлено");
}
}
Пропустите один метод – класс не соберётся. Интерфейс – это обещание, и компилятор следит за его выполнением.
Имена интерфейсов в C# принято начинать с большой «I»: IPlayable, IDisposable, IEnumerable. Соглашение соблюдается повсеместно, включая стандартную библиотеку, – по первой букве сразу видно, что перед вами.
Зачем это нужно
Метод может требовать не конкретный класс, а способность:
static void PlayAll(List<IPlayable> items)
{
foreach (var item in items)
{
item.Play();
}
}
Теперь в него подойдёт что угодно, реализующее IPlayable: песня, видео, подкаст, звук
уведомления. Общего предка у них может не быть вовсе – достаточно общей способности.
var playlist = new List<IPlayable> { new Song(), new Video(), new Podcast() };
PlayAll(playlist);
Несколько интерфейсов сразу
Базовый класс может быть только один, интерфейсов – сколько угодно:
public class Song : Media, IPlayable, IDownloadable, IComparable<Song>
{
// реализация всех трёх интерфейсов
}
Так в C# и решают задачи, для которых в C++ применяют множественное наследование. Конфликта не возникает, потому что интерфейс не приносит ни полей, ни готового кода – только требования.
Интерфейс или абстрактный класс
| Абстрактный класс | Интерфейс | |
|---|---|---|
| Сколько можно | один | сколько угодно |
| Поля и состояние | есть | нет |
| Готовая реализация | есть | почти нет |
| Отношение | «является» | «умеет» |
Практическое правило: если у типов есть общая суть и общий код – абстрактный класс. Если общая только способность – интерфейс.
Собака и кошка – животные, у них общий Sleep: это абстрактный класс. Песня и принтер оба умеют
Stop, но ничего общего между ними нет: это интерфейс.
Сомневаетесь – берите интерфейс. Он ни к чему не обязывает и не занимает единственную возможность наследования. Абстрактный класс всегда можно добавить позже, а вот выбраться из неудачной иерархии наследования тяжело.
Как это устроено в других языках
Объявление того, что тип реализует интерфейс:
public class Song : IPlayable // связь объявлена явно
{
public void Play() { }
}
type Song struct{}
func (s Song) Play() {} // связь нигде не объявлена: метод есть – значит, подходит
В C# класс называет интерфейсы, которые реализует. В Go связь неявная: тип подходит под интерфейс автоматически, если у него есть нужные методы. Явная запись многословнее, зато о нарушении контракта компилятор сообщает в самом классе, а не в месте использования.
Интерфейсы, которые вы уже встречали
Стандартная библиотека построена на них, и три встречаются постоянно:
| Интерфейс | Что означает |
|---|---|
IEnumerable<T> |
по объекту можно пройти циклом foreach |
IComparable<T> |
объекты можно сравнивать, значит, и сортировать |
IDisposable |
объект держит ресурс, который надо освободить – работает с using |
Именно поэтому foreach одинаково обходит массив, список и результат LINQ-запроса: все они
реализуют IEnumerable<T>, а цикл не знает и не хочет знать, что перед ним конкретно.
Отсюда полезная привычка: в параметрах методов объявляйте минимальное требование.
static int Sum(IEnumerable<int> numbers) // подойдёт и массив, и список
Метод, требующий List<int>, отвергнет массив без всякой причины: перебрать-то его можно.
Реализация в конструкторе класса
Интерфейс может требовать и свойства, не только методы:
public interface INamed
{
string Name { get; }
}
Класс реализует это обычным свойством – автосвойством, вычисляемым, каким угодно. Интерфейс
задаёт, что снаружи должно быть доступно чтение Name; как оно получено, его не касается.
Что дальше
На этом язык кончается, а проектирование начинается. Вы умеете объявлять типы, наследовать их и описывать контракты – то есть можете читать и писать обычный код на C#.
Следующие вопросы – не про синтаксис. Кому поручить обязанность, как не сломать работающий код изменением, когда наследование вредит, какие решения повторяются из проекта в проект. Этому посвящён курс «Объектно ориентированное программирование»: он начинается ровно там, где заканчивается этот.