Интерфейс это контракт: именованный список членов, которые тип обещает предоставить. Он говорит, что тип умеет делать, и ничего о том, как. Классы и структуры реализуют интерфейсы, а код, написанный для интерфейса, работает с любой реализацией, включая те, что будут написаны позже.
Объявление и реализация интерфейса
Интерфейс объявляет члены без тела. По соглашению его имя начинается с I. Класс реализует его, указав после двоеточия и предоставив открытый член для каждого:
Вывод:
15% off: 68
10 off: 70
Best for 80: 68
Best for 40: 30
BestPrice ничего не знает о PercentOff или FixedOff. Он зависит только от IDiscount, поэтому добавление позже класса BuyOneGetOne не требует его изменения. Это разделение и есть вся причина существования интерфейсов.
Несколько правил объявления:
- Члены интерфейса по умолчанию открытые, и реализующие их члены тоже должны быть открытыми (если только они не реализованы явно, см. ниже).
- Интерфейс может объявлять методы, свойства, индексаторы и события. Поля экземпляра и конструкторы он объявлять не может.
- Класс, пропустивший член, не компилируется: ошибка CS0535, «'FixedOff' does not implement interface member 'IDiscount.Apply(decimal)'».
- Интерфейс нельзя создать через
new. Вы создаёте реализующий класс и можете хранить его в переменной типа интерфейса:IDiscount d = new FixedOff(5);.
Интерфейс как тип
Переменную, параметр, поле или возвращаемый тип можно объявить интерфейсом. Тогда он принимает любой объект, реализующий интерфейс, и открывает только члены интерфейса. Так пишут код, которому всё равно, какую реализацию он получит, и так тесты подставляют подделку:
public class Checkout
{
private readonly IPaymentGateway gateway; // not StripeGateway, not PayPalGateway
public Checkout(IPaymentGateway gateway) { this.gateway = gateway; }
public bool Pay(decimal amount) => gateway.Charge(amount);
}
// Production: new Checkout(new StripeGateway(apiKey))
// Unit test: new Checkout(new FakeGateway(alwaysSucceeds: true))
На этом построены контейнеры внедрения зависимостей в ASP.NET Core: сервисы регистрируются и запрашиваются по интерфейсу.
Реализация нескольких интерфейсов
У класса не больше одного базового класса, но реализовать он может любое число интерфейсов. Сначала базовый класс, затем интерфейсы через запятую:
Вывод:
INVOICE Studio rent March: 950.00
True
True
Интерфейсы могут наследоваться и от других интерфейсов: interface IRepository<T> : IReadRepository<T> добавляет члены к унаследованным, и класс, реализующий IRepository<T>, должен предоставить оба набора.
Явная реализация интерфейса
Иногда два интерфейса объявляют член с одинаковым именем и разным смыслом, или член интерфейса не имеет смысла на открытой поверхности самого класса. Реализуйте его явно, поставив впереди имя интерфейса и опустив модификатор доступа:
Вывод:
Desk lamp,34.90
Desk lamp for 34.90
Явно реализованный член доступен только через интерфейс. Фреймворк использует это, чтобы убрать лишнее с распространённых типов: массивы явно реализуют IList.Add (он выбрасывает NotSupportedException, поскольку у массивов фиксированный размер), поэтому метод не появляется, когда вы набираете array..
Интерфейсы фреймворка, которые вы будете реализовывать
Реализация стандартного интерфейса подключает ваш тип к существующему коду фреймворка. Три из них встречаются постоянно.
IComparable<T> даёт типу естественный порядок, которым пользуются List<T>.Sort(), Array.Sort и Max(). CompareTo возвращает отрицательное число, ноль или положительное число:
Вывод:
1.4, 2.9, 2.10, 10.0
Сортировка тех же версий как строк дала бы 1.4, 10.0, 2.10, 2.9. Без IComparable<T> метод Sort() выбрасывает InvalidOperationException, потому что не знает, как сравнить два объекта Version.
IEnumerable<T> делает тип пригодным для foreach и LINQ. Проще всего реализовать GetEnumerator через yield return, это описано на странице об IEnumerable и yield.
IDisposable помечает тип, который держит что-то, требующее освобождения (дескриптор файла, подключение, таймер). Его единственный метод, Dispose, вызывается оператором using в конце блока, даже если выброшено исключение:
Вывод:
open sales.txt
write to sales.txt: March total: 12400
close sales.txt
after using
Методы интерфейса по умолчанию (C# 8)
Добавление члена в опубликованный интерфейс раньше ломало каждый класс, который его реализовывал. Начиная с C# 8 у члена интерфейса может быть тело, которое реализующие классы наследуют, если не предоставят своё:
public interface ILogger
{
void Write(string message);
// New in version 2 of the library. Existing implementers keep compiling.
void Error(string message) => Write("ERROR: " + message);
}
public class ConsoleLogger : ILogger
{
public void Write(string message) => Console.WriteLine(message);
}
ILogger log = new ConsoleLogger();
log.Error("disk full"); // ERROR: disk full
var direct = new ConsoleLogger();
// direct.Error("x"); // does not compile: the default method belongs to the interface
Последняя строка удивляет людей: метод по умолчанию не наследуется в собственные члены класса, поэтому его можно вызвать только через тип интерфейса. Методы по умолчанию также не могут использовать поля экземпляра (их у интерфейсов нет); они работают через другие члены интерфейса. Для них нужен .NET Core 3.0 или новее, в .NET Framework они недоступны.
C# 8 также разрешил статические члены в интерфейсах, а C# 11 добавил статические абстрактные члены, которые позволяют обобщённому коду вызывать статический метод или оператор у T. На этом построена обобщённая математика в .NET 7 (INumber<T>, where T : INumber<T>).
Интерфейс или абстрактный класс?
Используйте интерфейс для возможности, которую могут разделять несвязанные типы, когда в этом должны участвовать структуры или когда классу нужно несколько таких ролей. Используйте абстрактный класс, когда реализации являются вариациями одного и того же и разделяют состояние или фиксированный алгоритм. На странице об абстрактных классах есть сравнительная таблица. Многие решения используют оба: интерфейс для потребителей и абстрактный базовый класс, который реализует скучные части за реализующих.
Частые ошибки
- Отсутствующая или неоткрытая реализация. Каждому члену интерфейса нужен открытый член с совпадающей сигнатурой или явная реализация. Иначе CS0535 (или CS0737, когда метод есть, но не открытый).
- Интерфейсы с одной реализацией «на всякий случай». Интерфейс оправдан, когда реализаций несколько или есть подделка для тестов. Иначе это ещё один файл, по которому приходится ходить.
- Толстые интерфейсы. Интерфейс с двадцатью членами заставляет каждую реализацию писать двадцать членов. Разбейте его на меньшие роли (
IReader,IWriter), которые классы сочетают. - Приведение интерфейса обратно к классу.
((StripeGateway)gateway).Refund()отменяет разделение. Если вызывающим нуженRefund, ему место в интерфейсе. - Ожидание, что метод интерфейса по умолчанию можно вызвать у класса. Он доступен через тип интерфейса.
Часто задаваемые вопросы
Что такое интерфейс в C#?
Интерфейс это именованный набор членов (методов, свойств, событий, индексаторов), которые тип обещает предоставить, без состояния экземпляра. interface IDiscount { decimal Apply(decimal price); } говорит: «всё, что является IDiscount, умеет применять себя к цене». Классы и структуры реализуют его через двоеточие, и затем код может работать с любым из них через тип интерфейса.
Как реализовать интерфейс в C#?
Укажите его после двоеточия в объявлении класса (после базового класса, если он есть) и предоставьте открытый член для каждого члена интерфейса: class HolidayDiscount : IDiscount { public decimal Apply(decimal price) => price * 0.9m; }. Отсутствующий член это ошибка CS0535. Visual Studio и Rider могут сгенерировать заготовки быстрым исправлением.
Может ли класс C# реализовать несколько интерфейсов?
Да, сколько нужно, через запятую: class Invoice : IPrintable, IComparable<Invoice>, IDisposable. Это ответ C# на множественное наследование: у класса один базовый класс, но он может играть много ролей. Если два интерфейса объявляют одинаковый член, один открытый метод может удовлетворить оба, или можно использовать явную реализацию, чтобы у каждого был свой.
Что такое явная реализация интерфейса в C#?
Реализация члена с именем интерфейса впереди и без модификатора доступа: string IExportable.Format() { ... }. Тогда член можно вызвать только через переменную типа интерфейса, а не через класс. Это используют, чтобы разрешить конфликт имён между двумя интерфейсами и убрать редко используемые члены интерфейса с открытой поверхности класса.
Что такое методы интерфейса по умолчанию в C#?
Начиная с C# 8 у члена интерфейса может быть тело: void Error(string m) => Write("ERROR: " + m);. Реализующие классы получают его бесплатно и могут предоставить свою версию. Возможность существует, чтобы авторы библиотек могли добавлять члены в опубликованный интерфейс, не ломая все реализации. Метод по умолчанию доступен через тип интерфейса, а не через класс.
Почему имена интерфейсов в C# начинаются с I?
Это соглашение об именовании в .NET (IEnumerable, IDisposable, IComparable<T>): заглавная I, за которой следует имя в PascalCase, часто прилагательное на «-able». Компилятор этого не требует, но соглашение позволяет сразу узнавать типы интерфейсов, особенно в объявлении класса, где базовый класс и интерфейсы стоят в одном списке.