Содержание
Можно встретить достаточно много различных определений, что такое интерфейс в программировании и, в частности, в языке C#. От самых общих, например, «интерфейс — это то по средствам чего что-то взаимодействует с чем-то». До более понятных и конкретных: интерфейс — это контракт о взаимодействии между различными частями системы. При этом, под «системой» понимается не только одна единственная программа, но и совокупность программных средств, разработанных, в том числе, и разными группами разработчиков. Тема интерфейсов в C# — это довольно интересная и, что главное, одна из наиболее важных тем в изучении основ программирования на C#. И сегодня мы начнем эту тему изучать.
Язык C# — это довольно динамично развивающийся язык программирования и очередные нововведения, например, в C# 8.0 достаточно серьезно меняют концепцию интерфейсов, в принципе, однако, мы будем разбирать тему постепенно — от самых основ и до последних нововведений. Начнем как полагается — с классики.
Интерфейсы в C#
До выхода C# 8.0, что такое интерфейс, с точки зрения программирования, можно было определить следующим образом — это именованная область кода, содержащая сигнатуры методов, свойств, индексаторов и событий. То есть, интерфейс, в его классическом понимании и представлении, не должен содержать никаких реализаций чего-либо — только сигнатуры.
Выше, мы определились с тем, что интерфейс — это также некий контракт о взаимодействии. Зачем такой «контракт» нужен и когда его следует использовать?
Рассмотрим следующую задачу: нам необходимо наделить несколько классов одними и теми же методами. Здесь мы можем пойти следующим путем: если эти классы наши и классы родственны по своей сути, то мы можем выделить для них класс-предок, в котором реализовать необходимые нам методы и, соответственно, все наследники эти методы унаследуют и, при необходимости перегрузят или переопределят. Вполне простой и понятный ход работы. Но как решать такую задачу, если: а) классы не родственны по своей сути; б) мы хотим, чтобы нашим кодом (например, библиотекой классов) могли воспользоваться другие разработчики и написать свои реализации методов? И именно с этих двух моментов и начинается практическая значимость интерфейсов в C#.
Объявление интерфейса в C#
Для того, чтобы объявить интерфейс в C# используется ключевое слово interface
. В общем случае, объявление интерфейса выглядит следующим образом:
interface имя_интерфейса { сигнатуры методов; сигнатуры свойств и т.д. }
Имя интерфейса обычно начинается с заглавной буквы I
. Например, вы можете встретить такие названия интерфейсов .NET как IComparable
, IEnumerable
и т.д. Такое именование сразу дает нам понять, что перед нами находится интерфейс, а не класс. Технически, мы можем задать вообще любое имя интерфейса, не используя I в имени, но лучше придерживаться правил.
Для примера работы с интерфейсами в C# рассмотрим следующую ситуацию: есть человек, магазин и банк. Три неродственные сущности. И у человека, и у магазина и у банка в распоряжении могут находиться деньги, например, у человека деньги хранятся в кошельке, у магазина — в кассе, у банка — в сейфе. Нам необходима сделать так, чтобы у всех трех классов были одинаковые методы работы с деньгами: метод, позволяющий положить деньги, например, в кошелек и метод, позволяющий изъять деньги из кошелька. Для этого объявим вот такой интерфейс:
interface IMoneyVault { int MoneyAdd(int count); int MoneyRemove(int count); }
Как можно видеть, наш интерфейс, во-первых, не содержит никаких реализаций методов — только сигнатуры и, во-вторых, методы интерфейса не имеют никаких модификаторов доступа — все они по умолчанию публичны и имеют модификатор public
(по крайней мере до версии C# 8.0). После того, как мы объявили интерфейс, классы его (интерфейс) реализующие должны в обязательном порядке реализовать всё, что есть у интерфейса. В нашем случае — это два метода: MonewAdd
и MonewRemove
.
Реализация интерфейсов в C#
Чтобы реализовать интерфейс мы должны создать класс и указать у этого класса какой интерфейс (или интерфейсы) он реализуется. Например, начнем с класса человека:
Обратите внимание, что Visual Studio сразу же, как только вы напишете имя реализуемого интерфейса, подчеркнет его, а в списке ошибок появятся одна или несколько ошибок следующего содержания:
IMoneyVault.MoneyAdd(int)
«. Такие ошибки будут появляться до тех пор, пока мы не реализуем всё, что есть в интерфейсе. Напишем следующие реализации методов:
public class Person : IMoneyVault { private int currentMoney; public int MoneyAdd(int count) { return currentMoney += count; } public int MoneyRemove(int count) { return currentMoney -= count; } }
Конечно, здесь немного теряется логика в том плане, что у нас человек, образно говоря, становится хранилищем денег (реализует интерфейс IMoneyVault
) и, по-хорошему, надо было бы назвать наш класс несколько иначе, но для примера оставим всё так как есть. После того, как интерфейс реализован, мы можем использовать его методы. Например:
Person Person = new Person();//создаем объект Console.WriteLine(Person.MoneyAdd(10)); //добавили деньги Console.WriteLine(Person.MoneyRemove(5));//изъяли деньги
5
Аналогичным образом мы можем реализовать этот же интерфейс и в другом классе и написать для методов MoneyAdd
и MoneyRemove
. Например, путь у класса магазина при добавлении денег будет считаться сразу и сумма НДС:
public class Shop : IMoneyVault { private int currentMoney; public int CurrentMoney { get=>currentMoney; } private double nalog; public double Nalog { get=>nalog; } public int MonewAdd(int count) { currentMoney += count; nalog += 0.13 * currentMoney; return currentMoney; } public int MonewRemove(int count) { currentMoney -= count; nalog -= 0.13 * currentMoney; return currentMoney; } }
Проверим работу:
Shop Shop = new Shop(); Shop.MoneyAdd(1000); Console.WriteLine($"Всего денег: {Shop.CurrentMoney}"); Console.WriteLine($"В том числе НДС: {Shop.Nalog}");
В том числе НДС: 130
Таким образом, как мы будем реализовывать интерфейс в нашем классе — это наше дело, но сигнатуры методов и других сущностей объявленных в интерфейсе должны в точности совпадать. Например, вот такая реализация метода MoneyAdd
ошибочна:
public double MonewAdd(int count) { currentMoney += count; nalog += 0.13 * currentMoney; return currentMoney; }
так как в интерфейсе этот метод имеет другую сигнатуру — возвращаемое значение должно быть int
, а не double
.
Использование интерфейсов в C#
В примерах выше, мы явно создавали класс Person
или Shop
и вызывали методы. Однако, это не единственный способ использования интерфейсов в C#. Рассмотрим такой пример:
Shop Shop = new Shop(); object data = Shop; //теперь просто так вызвать MoneyAdd нельзя IMoneyVault moneyVault = Shop; //создавать интерфейсные переменные - можно if (data is IMoneyVault) { int money = ((IMoneyVault)data).MoneyAdd(10); Console.WriteLine(money); } int money2 = moneyVault.MoneyAdd(100); Console.WriteLine(money2);
Здесь происходит следующее:
- вначале мы создаем переменную типа
Shop
так как мы привыкли это делать; - далее, мы присваиваем полученное значение переменой типа
object
, т.е. переменной самого базового типа. - и, наконец, мы создаем переменную типа интерфейса. Сам интерфейс мы так создать не можем, т.к. конструктора у него нет никакого (даже по умолчанию), то есть вот такой код вызовет ошибку:
IMoneyVault vault = new IMoneyVault();
но, при этом, в C# мы можем завести переменную интерфейсного типа и спокойно присваивать такоё переменной значения, которое является любым объектом, реализующим указанный интерфейс, что мы и сделали. Далее, так как переменная data
, хоть и хранит ссылку на Shop
, но о методах интерфейса эта переменная уже ничего не знает, поэтому мы:
- используя ключевое слово
is
проверяем действительно ли в переменной дата содержится объект класса, реализующего интерфейсIMoneyVault
- вызываем метод
MoneyAdd
Ну и, наконец, так как мы оперируем переменными ссылочных типов, то результат выполнения этого кода будет следующим:
110
так как переменные data
и moneyVault
по сути содержат ссылки на один и тот же объект Shop
.
Итого
Сегодня мы рассмотрели основные моменты работы с интерфейсами в C#. Научились объявлять интерфейсы и реализовывать их в наших классах. Всё, что мы рассмотрели в этой части справедливо для всех версий .NET и C#, однако, возможности интерфейсов были значительно расширены в C# 8.0 и более новых версиях, о чем мы и поговорим в следующих частях учебника.