
14.3. Реализация интерфейсов
Прежде чем рассмотреть реализацию интерфейса, уточним отличия интерфейсов от абстрактных классов. Наиболее важное отличие состоит в том, что при наследовании классов у каждого класса может быть только один базовый класс - множественное наследование классов в языке С# невозможно. При построении класса на основе интерфейсов их может быть любое количество. Другими словами класс может реализовать сколько угодно интерфейсов, но при этом может иметь только один базовый класс. В интерфейсе не определяются поля и не может быть конструкторов. В интерфейсе нельзя объявлять статические методы.
Как и для абстрактных классов невозможно определить объект с помощью интерфейса.
Чтобы интерфейсом можно было пользоваться, он должен быть реализован классом или структурой. В этой главе рассмотрим реализацию интерфейсов с помощью классов. Синтаксически отношение реализации интерфейса обозначается включением имени интерфейса в спецификацию базы класса. Напомним формат объявления класса со спецификацией базы (для простоты не указаны модификаторы класса):
class имя_класса спецификация_базы { объявления_ членов_класса
}
Спецификация базы класса в этом случае имеет вид:
:имя_базового_классаоpt список_интерфейсовоpt
Имя базового класса (и следующая за ним запятая) могут отсутствовать. В списке интерфейсов через запятые помещаются имена тех интерфейсов, которые должен реализовать класс. В спецификацию базы класса может входить только один базовый класс и произвольное число имён интерфейсов. При этом должно выполняться обязательное условие - класс должен реализовать все члены всех интерфейсов, включённых в спецификацию базы. Частный случай - класс, реализующий только один интерфейс:
class имя_класса: имя_интерфейса { объявления_ членов_класса
}
Реализацией члена интерфейса является его полное определение в реализующем классе, снабженное модификатором доступа public.
Сигнатуры и типы возвращаемых значений методов, свойств и индексаторов в реализациях и в интерфейсе должны полностью совпадать.
Покажем на примерах, как при построении класса на основе интерфейса класс реализует его методы, свойства и индексаторы. Реализацию событий нужно рассмотреть позднее в главе, посвященной событиям.
В следующей программе (14_01.cs) интерфейс IPublication, реализуется классом Item – "заметка в газете".
// 14_01.cs - Интерфейс и его реализация
interface IPublication { // интерфейс публикаций
void write(); // готовить публикацию
void read(); // читать публикацию
string Title { set; get; } // название публикации
}
class Item : IPublication
{ // заметка в газете
string newspaper = "Известия"; // название газеты
string headline; // заголовок статьи
public string Title
{ // реализация свойства
set { headline = value; }
get { return headline; }
}
public void write()
{ // реализация метода
/* операторы, имитирующие подготовку статьи */
}
public void read()
{ // реализация метода
/*cоператоры, имитирующие чтение статьи */
Console.WriteLine(@"Прочел в газете ""{0}"", статью ""{1}"".", newspaper, Title);
}
}
public static void Main()
{
Console.WriteLine("Publication!");
Item article = new Item();
article.Title = "О кооперации";
article.read();
}
Результат выполнения программы:
Publication!
Прочел в газете "Известия", статью "О кооперации".
Класс Item кроме реализаций членов интерфейса включает объявления закрытых полей: newspaper (название газеты) и headline (заголовок статьи). Для простоты в класс не включён конструктор и только условно обозначены операторы методов write() и read(). Реализация свойства Title приведена полностью - аксессеры get и set позволяют получить и задать название статьи, представляемой конкретным объектом класса Item. В методе Main() нет ничего незнакомого читателю - определён объект класса Item и ссылка articte на него. С помощью обращения arcticle.Title задано название статьи.
В UML для изображения интерфейсов применяется та же символика, что и для классов. Конечно, имеется отличие - в верхней части, после имени интерфейса помещается служебное слово Interface. Тот факт, что класс реализует интерфейс, отображается с помощью специального символа и имени интерфейса над прямоугольником, представляющим класс.
В качестве второго примера рассмотрим интерфейс, реализация членов которого позволит получать целочисленные значения членов числовых рядов (Шилдт[14]):
interface ISeries {
void setBegin(); // восстановить начальное состояние
int GetNext {get; } // вернуть очередной член ряда
int this[int k] {get;} // вернуть k-й член ряда
}
Договоримся о ролях прототипов, входящих в этот интерфейс. В приведённом объявлении: setBegin() – прототип метода; GetNext – имя свойства, позволяющего получить значение очередного члена ряда и настроить объект на следующий член. Индексатор в этом примере позволяет получить значение не очередного, а произвольного k-го члена ряда и перевести объект в состояние, при котором свойство GetNext вернёт значение (k+1)-го члена.
Те функциональные возможности, которые приписаны членам интерфейса ISeries, при реализации в конкретных классах могут быть изменены. Но в этом случае нарушается общий принцип применения интерфейсов. Поэтому в примерах конкретных классов будем придерживаться описанных соглашений о ролях членов интерфейса ISeries.
Интерфейс ISeries можно реализовать для представления разных числовых рядов. Вот, например, регулярные числовые последовательности, которые можно представить классами, реализующими интерфейс ISeries: 1, 1, 2, 3, 5, 8, 13 ... – ряд Фибоначчи: аi=аi-2+аi-1, где i>2; a1=1, а2=1; 1, 3, 4, 7, 11, 18, 29 ... – ряд Лукаса: аi=аi-2+аi-1, где i>2; a1=1, a2=3; 1, 2, 5, 12, 29, 70, 169 ... – ряд Пелла: аi=аi-2+2*аi-1, где i>2; a1=1, a2=2.
В следующей программе на основе интерфейса ISeries определен класс, представляющий в виде объектов фрагменты ряда Пелла (см. также рис 14.2).
// 14_02.cs - Интерфейс и его реализация
interface ISeries
{
void setBegin(); // восстановить начальное состояние
int GetNext { get; } // вернуть очередной член ряда
int this[int k] { get;} // вернуть к-й член ряда
}
class Pell : ISeries // Ряд Пелла: 1, 2, 5, 12,...
{
int old, last; // два предыдущих члена ряда
public Pell() // конструктор
{ setBegin(); }
public void setBegin() // задать начальное состояние
{
old = 1;
last = 0;
}
public int GetNext // вернуть следующий после last
{
get
{
int now = old + 2 * last;
old = last;
last = now;
return now;
}
}
public int this[int k] // вернуть к-й член ряда
{
get
{
int now = 0;
setBegin();
if (k <= 0)
return -1;
for (int j = 0; j < k; j++)
now = GetNext;
return now;
}
}
public void seriesPrint(int n)
{ // вывести n членов, начиная со следующего
for (int i = 0; i < n; i++)
Console.Write(GetNext + "\t");
Console.WriteLine();
}
}
public static void Main()
{
Pell pell = new Pell();
pell.seriesPrint(9);
Console.WriteLine("pell[3] = " + pell[3]);
pell.seriesPrint(4);
pell.seriesPrint(3);
}
Результат выполнения программы:
1 2 5 12 29 70 169 408 985
pell[3] = 5
12 29 70 169
408 985 2378
169 408 985
Кроме реализации членов интерфейса ISeries в классе Pell объявлен метод seriesPrint(). Он выводит значения нескольких членов ряда, следующих за текущим. Количество членов определяет аргумент метода seriesPrint(). После выполнения метода состояние ряда изменится – текущим членом станет последний выведенный член ряда. Обратите внимание, что при реализации индексатора нумерация членов ряда начинается с 1.
Обратите внимание, что в реализации интерфейса заголовок метода должен полностью совпадать с заголовком прототипа этого метода в интерфейсе (за исключением появления модификатора доступа). Соответствие должно быть и при реализации других членов интерфейса.
В наших примерах интерфейсы и реализующие их классы размещены в одном файле. Если интерфейс объявлен не в том файле, где выполняется его реализация, то объявление интерфейса необходимо снабдить соответствующим модификатором доступа.
В приведённых выше программах классов Item и Pell была использована неявная реализация членов интерфейсов. Термин "неявная" употребляется для обозначения того, что в объявлении класса, реализующего интерфейс, не применяются квалифицированные имена членов интерфейса и их реализации снабжаются обязательным модификатором public.
Когда один класс реализует несколько интерфейсов, возможны совпадения имён членов из разных интерфейсов. Для разрешения такой конфликтной ситуации в классе, реализующем интерфейс, используется квалифицированное имя члена интерфейса. Здесь существует ограничение – такая реализация члена называется явной иона не может быть открытой, то есть для неё нельзя использовать модификатор public. Подробнее об особенностях явной реализации интерфейсов можно узнать из работ [1, 2, 8].