Menu

Интерфейсы в C#: объявление, реализация, методы по умолчанию и примеры

Как работают интерфейсы в C#: объявление, реализация в классах и структурах, интерфейс как тип, реализация нескольких интерфейсов сразу, явная реализация, интерфейсы фреймворка, которые вы будете реализовывать чаще всего (IComparable<T>, IEnumerable<T>, IDisposable), и методы интерфейса по умолчанию из C# 8.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Интерфейс это контракт: именованный список членов, которые тип обещает предоставить. Он говорит, что тип умеет делать, и ничего о том, как. Классы и структуры реализуют интерфейсы, а код, написанный для интерфейса, работает с любой реализацией, включая те, что будут написаны позже.

Объявление и реализация интерфейса

Интерфейс объявляет члены без тела. По соглашению его имя начинается с 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». Компилятор этого не требует, но соглашение позволяет сразу узнавать типы интерфейсов, особенно в объявлении класса, где базовый класс и интерфейсы стоят в одном списке.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ