Урок #22 – Создание интерфейсов

Интерфейсы очень схожи с абстрактными классами. Между ними есть всего несколько отличий. За урок мы научимся использовать интерфейсы на практике, а также узнаем где и зачем их можно использовать.
Видеоурок
Во многих языках программирования реализована возможность множественного наследования, когда один класс имеет несколько классов родителей. В языке C# такой функциональности нет и чтобы решить эту проблему можно использовать интерфейсы.
Что такое интерфейс?
Интерфейсы очень схожи с абстрактными классами и предоставляют лишь методы без реализации.
В интерфейсах можно записать методы, что должны реализовываться во всех классах, использующих интерфейс. Это удобно, ведь за счёт такого функционала мы можем быть уверены в классах и будем знать что они реализовывают все те функции, что мы предусмотрели заранее.
Как создать интерфейс?
Для создания интерфейса используется ключевое слово Interface :
interface ISomeOne
В интерфейсе можно не прописывать модификаторы доступа и по-умолчанию будет проставлен модификатор public.
Для реализации функционала в интерфейсе необходимо создать класс и указать что он является классом, реализующим определенный интерфейс. Для этого после названия класса пропишите слово : :
class Person : ISomeOne < string name; float happiness; int age; public Person(string name, float happiness, int age) < this.name = name; this.happiness = happiness; this.age = age; >public void Change (string val) < this.name = val; Console.WriteLine("Now his name is - " + val); >>
Интерфейсы

Для начала ознакомимся с формальным определением типа интерфейса. представляет собой не более чем просто именованный набор абстрактных членов. Абстрактные методы являются чистым протоколом, поскольку не имеют никакой стандартной реализации. Конкретные члены, определяемые интерфейсом, зависят от того, какое поведение моделируется с его помощью. Это действительно так. Интерфейс выражает поведение, которое данный класс или структура может избрать для поддержки. Более того, каждый класс (или структура) может поддерживать столько интерфейсов, сколько необходимо, и, следовательно, тем самым поддерживать множество поведений.
Нетрудно догадаться, что в библиотеках базовых классов .NET поставляются сотни предопределенных типов интерфейсов, которые реализуются в различных классах и структурах. Например, в состав ADO.NET входит множество поставщиков данных, которые позволяют взаимодействовать с определенной системой управления базами данных. Это означает, что в ADO.NET на выбор доступно множество объектов соединения (SqlConnection, OracleConnection, OdbcConnection и т.д.).
В интерфейсе ни у одного из методов не должно быть тела. Это означает, что в интерфейсе вообще не предоставляется никакой реализации. В нем указывается только, что именно следует делать, но не как это делать. Как только интерфейс будет определен, он может быть реализован в любом количестве классов. Кроме того, в одном классе может быть реализовано любое количество интерфейсов.
Для реализации интерфейса в классе должны быть предоставлены тела (т.е. конкретные реализации) методов, описанных в этом интерфейсе. Каждому классу предоставляется полная свобода для определения деталей своей собственной реализации интерфейса. Следовательно, один и тот же интерфейс может быть реализован в двух классах по-разному. Тем не менее в каждом из них должен поддерживаться один и тот же набор методов данного интерфейса. А в том коде, где известен такой интерфейс, могут использоваться объекты любого из этих двух классов, поскольку интерфейс для всех этих объектов остается одинаковым. Благодаря поддержке интерфейсов в C# может быть в полной мере реализован главный принцип полиморфизма: один интерфейс — множество методов.
Интерфейсы объявляются с помощью ключевого слова interface. Ниже приведена упрощенная форма объявления интерфейса:
interface имя< возвращаемый_тип имя_метода_1 (список_параметров); возвращаемый_тип имя_метода_2 (список_параметров); // . возвращаемый_тип имя_метода_N (список_параметров); >
где имя — это конкретное имя интерфейса. В объявлении методов интерфейса используются только их возвращаемый_тип и сигнатура. Они, по существу, являются абстрактными методами. Как пояснялось выше, в интерфейсе не может быть никакой реализации. Поэтому все методы интерфейса должны быть реализованы в каждом классе, включающем в себя этот интерфейс. В самом же интерфейсе методы неявно считаются открытыми, поэтому доступ к ним не нужно указывать явно.
Помимо методов, в интерфейсах можно также указывать свойства, индексаторы и события. Интерфейсы не могут содержать члены данных. В них нельзя также определить конструкторы, деструкторы или операторные методы. Кроме того, ни один из членов интерфейса не может быть объявлен как static.
Как только интерфейс будет определен, он может быть реализован в одном или нескольких классах. Для реализации интерфейса достаточно указать его имя после имени класса, аналогично базовому классу. Ниже приведена общая форма реализации интерфейса в классе:
class имя_класса : имя_интерфейса < // тело класса >
где имя_интерфейса — это конкретное имя реализуемого интерфейса. Если уж интерфейс реализуется в классе, то это должно быть сделано полностью. В частности, реализовать интерфейс выборочно и только по частям нельзя.
В классе допускается реализовывать несколько интерфейсов. В этом случае все реализуемые в классе интерфейсы указываются списком через запятую. В классе можно наследовать базовый класс и в тоже время реализовать один или более интерфейс. В таком случае имя базового класса должно быть указано перед списком интерфейсов, разделяемых запятой.
Методы, реализующие интерфейс, должны быть объявлены как public. Дело в том, что в самом интерфейсе эти методы неявно подразумеваются как открытые, поэтому их реализация также должна быть открытой. Кроме того, возвращаемый тип и сигнатура реализуемого метода должны точно соответствовать возвращаемому типу и сигнатуре, указанным в определении интерфейса.
Давайте рассмотрим пример:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 < // Создаем два интерфейса, описывающих абстрактные методы // арифметических операций и операций Sqrt и Sqr public interface IArOperation < // Определяем набор абстрактных методов int Sum(); int Otr(); int Prz(); int Del(); >public interface ISqrSqrt < int Sqr(int x); int Sqrt(int x); >// Данный класс реализует интерфейс IArOperation class A : IArOperation < int My_x, My_y; public int x < set < My_x = value; >get < return My_x; >> public int y < set < My_y = value; >get < return My_y; >> public A() < >public A(int x, int y) < this.x = x; this.y = y; >// Реализуем методы интерфейса public virtual int Sum() < return x + y; >public int Otr() < return x - y; >public int Prz() < return x * y; >public int Del() < return x / y; >// В данном классе так же можно реализовать собственные методы public virtual void rewrite() < Console.WriteLine("Переменная x: \nПеременная y: ",x,y); > > // Данный класс унаследован от класса А, но при этом в нем не нужно // заново реализовывать интерфейс, но при этом можно переопределить // некоторые его методы class Aa : A < public int z; public Aa(int z, int x, int y) : base(x, y) < this.z = z; >// Переопределим метод Sum public override int Sum() < return base.x + base.y + z; >public override void rewrite() < base.rewrite(); Console.WriteLine("Переменная z: " + z); >> // Данный класс унаследован от класса А, и при этом // реализует интерфейс ISqrSqrt class Ab : A, ISqrSqrt < public int Sqr(int x) < return x * x; >public int Sqrt(int x) < return (int)Math.Sqrt((double)(x)); >> class Program < static void Main() < A obj1 = new A(x: 10, y: 12); Console.WriteLine("obj1: "); obj1.rewrite(); Console.WriteLine("+ = ",obj1.x,obj1.y,obj1.Sum()); Console.WriteLine(" * = ", obj1.x, obj1.y, obj1.Prz()); Aa obj2 = new Aa(z: -3, x: 10, y: 14); Console.WriteLine("\nobj2: "); obj2.rewrite(); Console.WriteLine(" + + = ", obj2.x, obj2.y, obj2.Sum(), obj2.z); Console.ReadLine(); > > >

Обратите внимание на структуру реализации данных интерфейсов в приведенном примере. Диаграмма классов, иллюстрирующая вышеуказанный пример показана ниже:
interface (справочник по C#)
Интерфейс определяет контракт. record Любой class или struct реализующий этот контракт должен предоставлять реализацию элементов, определенных в интерфейсе. Интерфейс может определить реализацию по умолчанию для членов. Он также может определять члены static , чтобы обеспечить единую реализацию для общих функциональных возможностей. Начиная с C# 11 интерфейс может определить static abstract или static virtual члены, чтобы объявить, что тип реализации должен предоставлять объявленные члены. Как правило, методы объявляют, static virtual что реализация должна определять набор перегруженных операторов.
В следующем примере класс ImplementationClass должен реализовать метод с именем SampleMethod , не имеющий параметров и возвращающий значение void .
Дополнительные сведения и примеры см. в разделе Интерфейсы.
Пример интерфейса
interface ISampleInterface < void SampleMethod(); >class ImplementationClass : ISampleInterface < // Explicit interface member implementation: void ISampleInterface.SampleMethod() < // Method implementation. >static void Main() < // Declare an interface instance. ISampleInterface obj = new ImplementationClass(); // Call the member. obj.SampleMethod(); >>
Интерфейс может быть членом пространства имен или класса. Объявление интерфейса может содержать объявления (сигнатуры без реализации) следующих членов.
Члены интерфейса по умолчанию
Эти предыдущие объявления элементов обычно не содержат текст. Элемент интерфейса может объявить текст. Органы-члены в интерфейсе — это реализация по умолчанию. Члены с телом позволяют интерфейсу предоставлять реализацию по умолчанию для классов и структур, которые не предоставляют реализацию с переопределением. Интерфейс может включать:
- Константы
- Операторы
- Статический конструктор
- Вложенные типы
- Статические поля, методы, свойства, индексаторы и события
- Объявления членов с помощью явного синтаксиса реализации интерфейса.
- Явные модификаторы доступа (доступ по умолчанию — public ).
Статические абстрактные и виртуальные члены
Начиная с C# 11 интерфейс может объявлять static abstract и static virtual члены для всех типов элементов, кроме полей. Интерфейсы могут объявлять, что реализация типов должна определять операторы или другие статические члены. Эта функция позволяет универсальным алгоритмам указывать поведение, подобное числу. Примеры можно увидеть в числовых типах во время выполнения .NET, например System.Numerics.INumber . Эти интерфейсы определяют общие математические операторы, реализованные многими числовыми типами. Компилятор должен разрешать вызовы static virtual и static abstract методы во время компиляции. Методы static virtual , static abstract объявленные в интерфейсах, не имеют механизма диспетчеризации среды выполнения, аналогичного virtual методам или abstract объявленным в классах. Вместо этого компилятор использует сведения о типе, доступные во время компиляции. static virtual Поэтому методы почти исключительно объявляются в универсальных интерфейсах. Кроме того, большинство интерфейсов, объявляющих или методы, static virtual объявляют, что один из параметров типа должен реализовать объявленный интерфейс. static abstract Например, интерфейс объявляет, INumber что T должен реализовываться INumber . Компилятор использует аргумент типа для разрешения вызовов методов и операторов, объявленных в объявлении интерфейса. Например, int тип реализует INumber . Когда параметр T типа обозначает аргумент int типа, вызываются члены, static объявленные в int ней. Кроме того, если double аргумент типа является аргументом типа, вызываются члены, static объявленные в типе double .
Диспетчеризация static abstract методов и static virtual методов, объявленных в интерфейсах, разрешается с помощью типа времени компиляции выражения. Если тип среды выполнения выражения является производным от другого типа времени компиляции, будет вызываться статические методы базового типа (время компиляции).
Эту функцию можно попробовать, работая с руководством по статическим абстрактным членам в интерфейсах.
Наследование интерфейса
Интерфейсы не могут содержать состояние экземпляра. Хотя статические поля теперь разрешены, поля экземпляров не допускаются в интерфейсах. Автоматические свойства экземпляра не поддерживаются в интерфейсах, так как они неявно объявляют скрытое поле. Это правило оказывает незначительное воздействие на объявления свойств. В объявлении интерфейса следующий код не объявляет автоматически реализованное свойство, как это делается в class или struct . Вместо этого он объявляет свойство, которое не имеет реализации по умолчанию, но должно быть реализовано в любом типе, реализующем интерфейс.
public interface INamed < public string Name >
Интерфейс может наследовать от одного или нескольких базовых интерфейсов. Когда интерфейс переопределяет метод, реализованный в базовом интерфейсе, он должен использовать синтаксис явной реализации интерфейса.
Если список базовых типов содержит базовый класс и интерфейсы, базовый класс должен стоять первым в списке.
Класс, реализующий интерфейс, может явно реализовывать члены этого интерфейса. Явным образом реализованный член нельзя получить через экземпляр класса, но только через экземпляр интерфейса. Кроме того, обращение к членам интерфейса по умолчанию можно осуществлять только через экземпляр интерфейса.
Дополнительные сведения о явной реализации интерфейса см. в статье Явная реализация интерфейса.
Пример реализации интерфейса
В следующем примере показана реализация интерфейса. В этом примере интерфейс содержит объявление свойства, а класс содержит реализацию. Любой экземпляр класса, который реализует IPoint , имеет целочисленные свойства x и y .
interface IPoint < // Property signatures: int X < get; set; >int Y < get; set; >double Distance < get; >> class Point : IPoint < // Constructor: public Point(int x, int y) < X = x; Y = y; >// Property implementation: public int X < get; set; >public int Y < get; set; >// Property implementation public double Distance => Math.Sqrt(X * X + Y * Y); > class MainClass < static void PrintPoint(IPoint p) < Console.WriteLine("x=, y=", p.X, p.Y); > static void Main() < IPoint p = new Point(2, 3); Console.Write("My Point: "); PrintPoint(p); >> // Output: My Point: x=2, y=3
Спецификация языка C#
См. также
- Справочник по C#
- Руководство по программированию на C#
- Ключевые слова в C#
- Ссылочные типы
- Интерфейсы
- Использование свойств
- Использование индексаторов
Совместная работа с нами на GitHub
Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.
Создание вариантных универсальных интерфейсов (C#)
Параметры универсального типа можно объявить в интерфейсах как ковариантные или контравариантные. Ковариация позволяет методам интерфейса иметь тип возвращаемого значения, степень наследования которого больше, чем указано в параметрах универсального типа. Контравариантность позволяет методам интерфейса иметь типы аргументов, степень наследования которых меньше, чем указано в параметре универсального типа. Универсальный интерфейс, который имеет ковариантные или контравариантные параметры универсального типа, называется вариантным.
В платформе .NET Framework 4 появилась поддержка вариативности для нескольких существующих универсальных интерфейсов. Список вариативных интерфейсов в .NET см. в статье Вариативность в универсальных интерфейсах (C#).
Объявление вариантных универсальных интерфейсов
Вариантные универсальные интерфейсы можно объявить с помощью ключевых слов in и out для параметров универсального типа.
Параметры ref , in и out в C# не могут быть вариантными. Типы значений также не поддерживают вариативность.
Для объявления ковариантного параметра универсального типа можно использовать ключевое слово out . Ковариантный тип должен удовлетворять следующим условиям:
-
Тип используется только в качестве типа значения, возвращаемого методами интерфейса, и не используется в качестве типа аргументов метода. Это показано в следующем примере, в котором тип R объявлен ковариантным.
interface ICovariant < R GetSomething(); // The following statement generates a compiler error. // void SetSomething(R sampleArg); >
Существует одно исключение из данного правила. Если в качестве параметра метода используется контравариантный универсальный делегат, этот тип можно использовать в качестве параметра универсального типа для этого делегата. Это продемонстрировано ниже на примере типа R . Дополнительные сведения см. в разделах Вариативность в делегатах (C#) и Использование вариативности в универсальных методах-делегатах Func и Action (C#).
interface ICovariant < void DoSomething(Actioncallback); >
interface ICovariant < // The following statement generates a compiler error // because you can use only contravariant or invariant types // in generic constraints. // void DoSomething() where T : R; >
Для объявления контравариантного параметра универсального типа можно использовать ключевое слово in . Контравариантный тип можно использовать только в качестве типа аргументов метода, но не в качестве типа значения, возвращаемого методами интерфейса. Контравариантный тип можно также использовать для универсальных ограничений. В следующем примере кода показано объявление контравариантного интерфейса и использование универсального ограничения для одного из его методов.
interface IContravariant < void SetSomething(A sampleArg); void DoSomething() where T : A; // The following statement generates a compiler error. // A GetSomething(); >
Кроме того, можно реализовать поддержку ковариации и контравариации в одном интерфейсе, но для разных параметров типа, как показано в следующем примере кода.
interface IVariant
Реализация вариантных универсальных интерфейсов
Для реализации вариантных универсальных интерфейсов в классах используется тот же синтаксис, что и для инвариантных интерфейсов. В следующем примере кода показана реализация ковариантного интерфейса в универсальном классе.
interface ICovariant < R GetSomething(); >class SampleImplementation : ICovariant < public R GetSomething() < // Some code. return default(R); >>
Классы, которые реализуют вариантные интерфейсы, являются инвариантными. Например, рассмотрим следующий код.
// The interface is covariant. ICovariant ibutton = new SampleImplementation(); ICovariant iobj = ibutton; // The class is invariant. SampleImplementation button = new SampleImplementation(); // The following statement generates a compiler error // because classes are invariant. // SampleImplementation obj = button;
Расширение вариантных универсальных интерфейсов
При расширении вариантных универсальных интерфейсов необходимо использовать ключевые слова in и out для явного указания того, поддерживает ли вариативность производный интерфейс. Компилятор не подразумевает вариативность интерфейса, который расширяется. Например, рассмотрим следующие интерфейсы.
interface ICovariant < >interface IInvariant : ICovariant < >interface IExtCovariant : ICovariant
В интерфейсе IInvariant параметр универсального типа T является инвариантным, тогда как в IExtCovariant параметр типа является ковариантным, хотя оба интерфейса расширяют один и тот же интерфейс. То же правило применяется к контравариантным параметрам универсального типа.
Можно создать интерфейс, который расширяет и интерфейс, в котором параметр универсального типа T является ковариантным, и интерфейс, где он является контравариантным, если в расширяемом интерфейсе параметр универсального типа T является инвариантным. Это показано в следующем примере кода.
interface ICovariant < >interface IContravariant < >interface IInvariant : ICovariant, IContravariant
Тем не менее, если параметр универсального типа T объявлен ковариантным в одном интерфейсе, его нельзя объявить контравариантным в расширенном интерфейсе и наоборот. Это показано в следующем примере кода.
interface ICovariant < >// The following statement generates a compiler error. // interface ICoContraVariant : ICovariant
Недопущение неоднозначности
При реализации вариантных универсальных интерфейсов вариативность может приводить к неоднозначности. Такой неоднозначности следует избегать.
Например, если вы явно реализуете один вариантный универсальный интерфейс с разными параметрами универсального типа в одном классе, это может создавать неоднозначность. Компилятор не сообщает об ошибке в данном случае, но и не указывает, какая реализация интерфейса будет выбрана во время выполнения. Такая неоднозначность может привести к возникновению неявных ошибок в коде. Рассмотрим следующий пример кода.
// Simple class hierarchy. class Animal < >class Cat : Animal < >class Dog : Animal < >// This class introduces ambiguity // because IEnumerable is covariant. class Pets : IEnumerable, IEnumerable < IEnumeratorIEnumerable.GetEnumerator() < Console.WriteLine("Cat"); // Some code. return null; >IEnumerator IEnumerable.GetEnumerator() < // Some code. return null; >IEnumerator IEnumerable.GetEnumerator() < Console.WriteLine("Dog"); // Some code. return null; >> class Program < public static void Test() < IEnumerablepets = new Pets(); pets.GetEnumerator(); > >
В этом примере не указано, каким образом метод pets.GetEnumerator делает выбор между Cat и Dog . Это может вызвать проблемы в вашем коде.
См. также
- Вариативность в универсальных интерфейсах (C#)
- Использование вариативности в универсальных методах-делегатах Func и Action (C#)
Совместная работа с нами на GitHub
Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.