Course program← C#: Базовый курс
1Знакомство с C# и .NET
2Данные и типы
3Ветвления и циклы
4Методы и ошибки
5Коллекции и строки
6Классы и объекты

Интерфейсы и абстрактные классы

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#.

Следующие вопросы – не про синтаксис. Кому поручить обязанность, как не сломать работающий код изменением, когда наследование вредит, какие решения повторяются из проекта в проект. Этому посвящён курс «Объектно ориентированное программирование»: он начинается ровно там, где заканчивается этот.