Перейти к содержимому
Теория · Наследование и интерфейсы · урок 7 из 10

Интерфейсы

Автопарку нужен перевозчик грузов. В объявлении о работе одно требование: «умеет возить груз и говорит, сколько килограммов возьмёт». Подойдёт грузовик, подойдёт и грузчик с тележкой. Объявлению неважно, кто вы и от кого произошли: важно, что вы умеете то, что в нём перечислено.

Базовым классом такое требование не описать. Наследник — разновидность базового класса, а грузчик — не разновидность транспорта. К тому же базовый класс у класса только один: если он уже есть, второй не добавить.

Для таких требований в C# есть интерфейс — список членов, которые класс обещает иметь. Класс, который выполняет это обещание, реализует интерфейс:

C#
ICargo[] carriers = [new Truck(), new Porter()];
foreach (ICargo c in carriers)
{
    Console.WriteLine(c.MaxLoad()); // 5000
                                    // 50
}

interface ICargo
{
    int MaxLoad();
}

class Truck : ICargo { public int MaxLoad() => 5000; }

class Porter : ICargo { public int MaxLoad() => 50; }

Давайте разберём по строкам:

  1. interface ICargo объявляет интерфейс с одним методом MaxLoad. У метода нет тела: интерфейс говорит, что метод есть, но не говорит, как он работает. Имена интерфейсов в C# по соглашению начинаются с буквы I.
  2. class Truck : ICargo реализует интерфейс: в классе обязан быть метод MaxLoad с тем же типом результата и теми же параметрами, объявленный public.
  3. Porter ничем не связан с Truck, кроме интерфейса, и реализует его по-своему.
  4. ICargo[] carriers — массив интерфейсного типа: в нём лежат ссылки на объекты любых классов, которые реализуют ICargo.
  5. c.MaxLoad() выполняет версию объекта: 5000 для грузовика и 50 для грузчика.

Договор, который проверяет компилятор

Интерфейс — договор, и компилятор проверяет, что класс выполнил каждый его пункт. Допустим, в ICargo есть ещё метод int MaxDistance();, а Porter его не объявил. Тогда программа не скомпилируется:

TEXT
error CS0535: 'Porter' does not implement interface member 'ICargo.MaxDistance()'

Объект интерфейса создать нельзя: строка new ICargo() даёт ошибку CS0144. Но переменная интерфейсного типа может вести к объекту любого класса, который этот интерфейс реализует.

Здесь аналогия с объявлением неточна. Нанятый работник делает всё, что умеет, даже сверх объявления. А через переменную типа ICargo видны только члены самого интерфейса, даже если у класса есть и другие.

Несколько интерфейсов

Один работник может подойти сразу к нескольким объявлениям. Класс тоже может реализовать несколько интерфейсов: их перечисляют через запятую.

C#
var drone = new Drone();
Console.WriteLine(drone.MaxLoad()); // 5
Console.WriteLine(drone.Battery()); // 70

interface IElectric
{
    int Battery();
}

class Drone : ICargo, IElectric
{
    public int MaxLoad() => 5;
    public int Battery() => 70;
}

Этот фрагмент продолжает первую программу. Дрон возит груз и работает от батареи, поэтому реализует оба интерфейса и объявляет методы обоих.

Базовый класс у класса только один, а интерфейсов может быть сколько угодно. Если у класса есть и то и другое, базовый класс идёт в списке первым: class ElectricVan : Vehicle, ICargo, IElectric. При другом порядке компилятор выдаст ошибку CS1722.

На собеседовании это часто спрашивают так: «Есть ли в C# множественное наследование?» Ответ из двух частей:

  • от нескольких классов наследовать нельзя: базовый класс у класса один;
  • реализовать несколько интерфейсов можно, и так один класс подходит к нескольким ролям.

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

Это ещё один классический вопрос собеседования. И абстрактный класс, и интерфейс описывают, что должны уметь классы, которые от них наследуют или их реализуют, но устроены они по-разному:

Абстрактный классИнтерфейс
Сколько их у классаодин, как базовый класссколько угодно
Поля и конструкторы объектабываютне бывают
Кого объединяетродственные классы с общими данными и кодомлюбые классы с общим умением

С версии C# 8 метод интерфейса может иметь тело — реализацию по умолчанию. Но полей и конструкторов объекта у интерфейса по-прежнему нет. Поэтому общие данные хранит абстрактный класс, а интерфейс описывает умение.

Частая ошибка

Реализовали метод интерфейса, но забыли public:

C#
ICargo cart = new Cart();
Console.WriteLine(cart.MaxLoad());

interface ICargo
{
    int MaxLoad();
}

class Cart : ICargo
{
    int MaxLoad() => 100; // ✗
}
TEXT
error CS0737: 'Cart' does not implement interface member 'ICargo.MaxLoad()'. 'Cart.MaxLoad()' cannot implement an interface member because it is not public.

Программа не компилируется. Члены класса по умолчанию private, а член, который реализует интерфейс, должен быть доступен всякому коду, который работает через этот интерфейс. В самом интерфейсе public не пишут: его члены открыты по умолчанию. А в классе слово обязательно: public int MaxLoad() => 100;.

Главное

  • Интерфейс перечисляет члены, которые класс обещает иметь; класс реализует его, объявив каждый такой член public.
  • Класс может реализовать несколько интерфейсов, а базовый класс у него один.
  • Переменная интерфейсного типа ведёт к объекту любого класса, который реализует этот интерфейс.
  • У абстрактного класса бывают поля и конструкторы объекта, а у интерфейса — нет.