Лекция № 10
Интерфейсы
Интерфейс - это набор семантически связанных абстрактных членов.
Возможность трактовать объект как относящийся ко многим типам часто называют множественным наследованием (multiple inheritance). CLR поддерживает единичное реализационное наследование и множественное интерфейсное наследование. CLR допускает наследование типа только от одного другого типа, имеющего в качестве своего корневого базового типа System.Object. Такое наследование называется реализационным (implementation inheritance), поскольку производный тип наследует все возможности и поведение своего базового класса: производный класс функционирует, точно как базовый. Однако производный тип может переопределить поведение базового класса. Такое переопределение поведения базового класса (его реализации) делает новый производный тип уникальным.
При интерфейсном наследовании (interface inheritance) тип наследует сигнатуры методов своих интерфейсов, но не их реализацию. Наследуя интерфейс, тип обязуется обеспечить собственную реализацию методов. Если он не реализует методы интерфейса, то считается абстрактным, и создать экземпляры этого типа невозможно.
Интерфейсы не порождаются ни от каких System.Object-производных типов. Интерфейс - просто абстрактный тип, состоящий из набора виртуальных методов, каждый из которых имеет свое имя, параметры и возвращаемый тип. Методы интерфейса не могут иметь реализации, так что они являются незавершенными (абстрактными).
Интерфейсы могут также определять события, свойства без параметров, свойства с параметрами (индексаторы), так как все это лишь синтаксические изыски, которым в конечном счете ставятся в соответствие методы. CLR также допускает наличие в интерфейсах статических методов, статических полей, констант и статических конструкторов.
Но CLS-совместимый интерфейс не может иметь подобных статических членов, поскольку некоторые языки не могут определять или обращаться к ним. На самом деле С# не допускает определения статических членов интерфейса. Кроме того, CLR не позволяет интерфейсу иметь экземплярные поля и конструкторы.
Интерфейсы синтаксически подобны абстрактным классам. Однако в интерфейсе ни один метод не может включать тело, т.е. интерфейс в принципе не предусматривает какой бы то ни было реализации. Он определяет, что должно быть сделано, но не уточняет, как. Коль скоро интерфейс определен, его может реализовать любое количество классов. При этом один класс может реализовать любое число интерфейсов. Для реализации интерфейса класс должен обеспечить тела (способы реализации) методов, описанных в интерфейсе. Каждый класс может определить собственную реализацию. Таким образом, два класса могут реализовать один и тот же интерфейс различными способами, но все классы поддерживают одинаковый набор методов. Следовательно, код, "осведомленный" о наличии интерфейса, может использовать объекты любого класса, поскольку интерфейс для всех объектов одинаков. Предоставляя программистам возможность применения такого средства программирования, как интерфейс, С# позволяет в полной мере использовать аспект полиморфизма, выражаемый как "один интерфейс - много методов".
Интерфейсы объявляются с помощью ключевого слова interface.
Упрощенная форма объявления интерфейса:
interface имя {
тип_возврата имя_метода1 (список_параметров);
тип_возврата имя_метода2 (список_параметров);
//...
тип_возврата имя_методаN (список_параметров) ;}
Имя интерфейса задается элементом имя. Методы объявляются с использованием лишь типа возвращаемого ими значения и сигнатуры. Все эти методы, по сути, - абстрактные.
В интерфейсе методы неявно являются открытыми (public-методами), при этом не разрешается явным образом указывать спецификатор доступа.
Рассмотрим пример интерфейса для класса, который генерирует ряд чисел.
public interface ISeries {
int getNext(); // Возвращает следующее число ряда,
void setStart(int х); // Устанавливает начальное значение.
}
Этот интерфейс имеет имя ISeries. Интерфейс ISeries объявлен открытым, поэтому он может быть реализован любым классом в любой программе.
Хотя префикс "I" необязателен, многие программисты его используют, чтобы отличать интерфейсы от классов.
Помимо сигнатур методов интерфейсы могут объявлять сигнатуры свойств, индексаторов и событий. Интерфейсы не могут иметь членов данных. Они не могут определять конструкторы, деструкторы или операторные методы. Кроме того, ни один член интерфейса не может быть объявлен статическим.
Реализация интерфейсов
Если интерфейс определен, один или несколько классов могут его реализовать. Чтобы реализовать интерфейс, нужно указать его имя после имени класса подобно тому, как при создании производного указывается базовый класс. Формат записи класса, который реализует интерфейс, таков:
class имя_класса : имя_интерфейса {// тело класса }
Имя реализуемого интерфейса задается с помощью элемента имя_интерфейса. Если класс реализует интерфейс, он должен это сделать в полном объеме, т.е. реализация интерфейса не может быть выполнена частично.
Классы могут реализовать несколько интерфейсов. В этом случае имена интерфейсов отделяются запятыми. Класс может наследовать базовый класс и реализовать один или несколько интерфейсов. В этом случае список интерфейсов должно возглавлять имя базового класса.
Методы, которые реализуют интерфейс, должны быть объявлены открытыми. Дело в том, что методы внутри интерфейса неявно объявляются открытыми, поэтому их реализации также должны быть открытыми. Кроме того, сигнатура типа в реализации метода должна в точности совпадать с сигнатурой типа, заданной в определении интерфейса.
Рассмотрим пример реализации интерфейса ISeries, объявление которого приведено выше. Создается класс с именем ByTwos, генерирующий ряд чисел, в котором каждое следующее число больше предыдущего на два.
using System;
public interface ISeries {
int getNext(); // Возвращает следующее число ряда,
void setStart(int x); // Устанавливает начальное значение.
}
// Реализация интерфейса ISeries,
class ByTwos : ISeries {
int val;
public ByTwos() {val =0;}
public int getNext() {val += 2; return val;}
public void setStart(int x) {val = x;}
}
class Demo
{public static void Main()
{ ByTwos ob = new ByTwos();
ob.setStart(100);
for (int i = 0; i < 5; i++)
Console.WriteLine(ob.getNext());
}
}
Результаты работы программы:
Класс ByTwos реализует все три метода, определенные интерфейсом ISeries.
В классах, которые реализуют интерфейсы, можно определять дополнительные члены.
Интерфейсные свойства и индексаторы.
Как и методы, свойства определяются в интерфейсе без тела. Формат спецификации свойства.
тип имя{ get;
set;}
Свойства, предназначенные только для чтения или только для записи, содержат только get- или set-элемент, соответственно.
В интерфейсе можно определить и индексатор. Объявление индексатора в интерфейсе имеет следующий формат записи:
тип_элемента this[int индекс] { get;
set;
}
Индексаторы, предназначенные только для чтения или только для записи, содержат только get- или set-метод, соответственно.
Определение четырех интерфейсов из библиотеки классов .NET Framework Class Library (FCL):
public interface System.IComparable {Int32 CompareTo(Object obj);}
public interface System.Collections.IEnumerable {IEnumerator GetEnumerator();}
public interface System.Collections.IEnumerator {Boolean MoveNext();
void Reset();
Object Current {get;} // Свойство только для чтения
}
public interface System.Collections.ICollection : IEnumerable {
void CopyTo(Array array, Int32 index);
Int32 Count {get;} // Свойство только для чтения
Boolean IsSynchronized {get;} // Свойство только для чтения
Object SyncRoot {get;} // Свойство только для чтения
}
Согласно принятым соглашениям имя типа-интерфейса начинается с префикса I. Определение интерфейса может содержать модификаторы - public, protected, internal и prívate - по аналогии с классами и структурами. Разумеется, в 99% случаев применяется public. Эти модификаторы определяют видимость интерфейса извне. Нестатические члены интерфейса всегда рассматриваются как открытые и виртуальные. В С#, если вы реализуете метод интерфейса в типе и не указываете ключевое слово virtual, этот метод рассматривается как виртуальный и изолированный - производные типы не смогут его переопределить.
Интерфейс не может наследовать реализацию другого типа, но может «наследовать» контракт другого интерфейса (как в случае ICollection, имеющего в качестве базового IEnumerable). Интерфейс может включать контракт нескольких интерфейсов. Наследующий интерфейс тип должен реализовать все методы, определенные этим интерфейсом и все методы «унаследованных» интерфейсом контрактов.
Например, любой тип, реализующий интерфейс ICollection, должен обеспечить реализацию членов СоруТо, Count, IsSynchronized и SyncRoot. Кроме того, этот тип должен также предоставить реализацию метода GetEnumerator интерфейса IEnumerable .
Интерфейс System.ICloneable определяется (в MSCorLib.dll) так:
public interface ICloneable { Object Clone();}
Следующий код демонстрирует определение типа, реализующего этот интерфейс, а также клонирование объекта:
Использование интерфейсных ссылок
В программе можно объявить ссылочную переменную интерфейсного типа. Другими словами, можно создать переменную ссылку на интерфейс. Такая переменная может ссылаться на любой объект, который реализует ее интерфейс. При вызове метода для объекта посредством интерфейсной ссылки будет выполнена та версия указанного метода, которая реализована этим объектом. Этот процесс аналогичен использованию ссылки на базовый класс для доступа к объекту производного класса.
Например, FCL-тип System.String наследует реализацию System.Object и реализует интерфейсы IComparable, ICloneable, IConvertible и IEnumerable. Это значит, что типу String не требуется реализовывать методы, предлагаемые его базовым типом Object. Если в типе String явно не реализуются методы Object, значит, они просто наследуются от этого типа. Между тем тип String должен реализовать методы, объявленные во всех интерфейсах, иначе он будет незавершенным (абстрактным) типом. Интерфейсы позволяют интерпретировать объект как относящийся к разным типам. Любой тип, реализующий интерфейсы, позволяет рассматривать свои объекты как любой из этих интерфейсов.
Пример: // Создаем объект String.
String s = "Jeffrey";
// Используя s, я могу вызвать любой метод, определенный в
// String, Object, IComparable, ICloneable, IConvertible, IEnumerable.
// Создадим переменную типа IComparable, ссылающуюся на s.
IComparable comparable = s;
// Используя comparable, я могу вызвать лишь методы, объявленные IComparable,
// Создадим перененную типа ICloneable, ссылающуюся на s.
ICloneable cloneable = s;
// Используя cloneable, я могу вызвать лишь методы, объявленные ICloneable.
// Создадим переменную типа IEnumerable, ссылающуюся на s.
IEnumerable enumerable = (IEnumerable) cloneable;
// Можно приводить переменную из одного интерфейса
// к другому, так как тип реализует оба интерфейса.
В этом коде неважно, какая переменная используется: воздействию всегда подвергается объект типа String, на который указывает s. Однако тип переменной определяет допустимые по отношению к этому строковому объекту действия.