Интерфейсы
Автопарку нужен перевозчик грузов. В объявлении о работе одно требование: «умеет возить груз и говорит, сколько килограммов возьмёт». Подойдёт грузовик, подойдёт и грузчик с тележкой. Объявлению неважно, кто вы и от кого произошли: важно, что вы умеете то, что в нём перечислено.
Базовым классом такое требование не описать. Наследник — разновидность базового класса, а грузчик — не разновидность транспорта. К тому же базовый класс у класса только один: если он уже есть, второй не добавить.
Для таких требований в 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; }Давайте разберём по строкам:
interface ICargoобъявляет интерфейс с одним методомMaxLoad. У метода нет тела: интерфейс говорит, что метод есть, но не говорит, как он работает. Имена интерфейсов в C# по соглашению начинаются с буквыI.class Truck : ICargoреализует интерфейс: в классе обязан быть методMaxLoadс тем же типом результата и теми же параметрами, объявленныйpublic.Porterничем не связан сTruck, кроме интерфейса, и реализует его по-своему.ICargo[] carriers— массив интерфейсного типа: в нём лежат ссылки на объекты любых классов, которые реализуютICargo.c.MaxLoad()выполняет версию объекта:5000для грузовика и50для грузчика.
Договор, который проверяет компилятор
Интерфейс — договор, и компилятор проверяет, что класс выполнил каждый его пункт. Допустим, в ICargo есть ещё метод int MaxDistance();, а Porter его не объявил. Тогда программа не скомпилируется:
error CS0535: 'Porter' does not implement interface member 'ICargo.MaxDistance()'Объект интерфейса создать нельзя: строка new ICargo() даёт ошибку CS0144. Но переменная интерфейсного типа может вести к объекту любого класса, который этот интерфейс реализует.
Здесь аналогия с объявлением неточна. Нанятый работник делает всё, что умеет, даже сверх объявления. А через переменную типа ICargo видны только члены самого интерфейса, даже если у класса есть и другие.
Несколько интерфейсов
Один работник может подойти сразу к нескольким объявлениям. Класс тоже может реализовать несколько интерфейсов: их перечисляют через запятую.
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:
ICargo cart = new Cart();
Console.WriteLine(cart.MaxLoad());
interface ICargo
{
int MaxLoad();
}
class Cart : ICargo
{
int MaxLoad() => 100; // ✗
}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. - Класс может реализовать несколько интерфейсов, а базовый класс у него один.
- Переменная интерфейсного типа ведёт к объекту любого класса, который реализует этот интерфейс.
- У абстрактного класса бывают поля и конструкторы объекта, а у интерфейса — нет.