Этюды в тональности C# 2

Этюды в тональности C#

Продолжение. Начало в КГ №№ 1, 2, 4, 6 (2004 г.)

Введение
В статьях этого цикла я постараюсь рассказать вам о тех конструкциях языка C#, что ранее были оставлены мной за рамками повествования. На тот случай, если вы не читали предыдущие статьи цикла, рекомендую посетить мою авторскую веб-страничку на сайте издательства (короткий путь к ней http://german2004.da.ru ). На ней вы найдете все публиковавшиеся в газете мои статьи по тематике Net Framework, да и на другие темы. Сегодня мы с вами поговорим о понятии интерфейса.

Интерфейсы.
Интерфейсы — это вершина, "пик" объектно-ориентированного программирования. ООП всегда тяготела к абстрагированию используемых в программном проекте "сущностей". Интерфейс же — это полная абстракция, дальше идти уже некуда (хотя кто знает?). Описывая его, вы не вводите ни одной строчки реального кода, подлежащего выполнению, а лишь описываете то, как этот код должен выглядеть. Так сказать, строите предварительный макет, по модели которого впоследствии будете формировать реальный код. С точки зрения синтаксиса языка C# интерфейс описывается следующим образом:

public interface IMyInterface {
}

Внешне описание интерфейса очень похоже на описание обычного объекта, но на этом все сходство между ними заканчивается. Интерфейс, по своей сути, не имеет ничего общего с объектом. Начнем с того, что это вообще не тип данных, а скорее чисто лексическая конструкция. Ее предназначение, подобно делегату, — описать некий набор функциональности, обязательно присутствующий в некоем объекте. В понятие "функциональности" входят такие стандартные для объекта члены, как методы, свойства, индексаторы и события. Описывать поля в интерфейсах нельзя, они не являются голой абстракцией и этим будут нарушать идеологию интерфейса как чистой абстрактной конструкции.
Подобно объектам, интерфейсы могут наследовать функциональность друг от друга. Мы можем описать некий несложный базовый интерфейс, а затем добавить нужную функциональность в его потомках.

public interface IMyNew Interface:IMyInterface{
bool HasPivo{get;}
void Drink();
}

В этом примере мы описываем интерфейс IMyNewInter-face, являющегося потомком интерфейса ImyInterface из моего предыдущего примера. Этот новый интерфейс добавляет своему предку описание доступного только для чтения булевского свойства HasPivo и метода Drink, не имеющего параметров и не возвращающего каких-либо параметров.
Главным достоинством интерфейса является то, что реальный тип объекта, реализующего тот или иной интерфейс, никакой роли не играет. Так, в нашем примере нет никаких ссылок на какие-либо объекты Net Framework. Если класс способен реализовать нужную нам функциональность, нам совершенно безразлично, произошел этот класс, скажем, от System.Windows.Forms.Panel или, к примеру, от System. Data.OleDb. OleDbCommand. Для того чтобы класс мог реализовывать интерфейс IMyIn-terface, требуется лишь, чтобы в нем были реализованы указанные свойства и метод. Все остальное не суть важно, в том числе и место объекта в иерархии классов Net.Framework.

Например, если вы просите зажигалку у прохожего на улице для того, чтобы прикурить сигарету, вам совершенно безразлично, где именно работает этот прохожий, кто его родственники и есть ли у него дети. Является ли этот прохожий главным инженером большого предприятия или продавцом слив на рынке, вам все равно. Для вас, на самом деле, важно только одно, есть ли у него зажигалка, больше вам от него ничего не требуется.
Интерфейс как раз и задумывался для моделирования подобных жизненных ситуаций. Если вы вызываете некий объект только для того, чтобы с его помощью вывести строку текста на экран, вам совершенно не интересно, что этот конкретный объект, возможно, умеет разбирать регулярные выражения. В данном фрагменте кода вам эта его функциональность просто не нужна. Вы хотите получить от него конкретное простое действие, интерфейс помогает вам получить к этому действию доступ. Вот и все.
Подобно тому, как делегат является не более чем "договором" о том, как должен выглядеть некий метод, интерфейс — это примерно такой же "договор" о том, как именно должен выглядеть некий объект. Описывая интерфейс, вы задаете набор свойств или методов, которые гарантированно должны быть в объекте, реализующем этот интерфейс. Предварительное знание об их существовании и позволяет компилятору в дальнейшем к ним спокойно обращаться.

На самом деле сравнивать делегат и интерфейс неправильно, между ними есть много общего, но хватает и различий. Так, метод, подставляемый на место делегата, должен полностью совпадать с ним по маске. Объект же, подставляемый на место делегата, должен совпадать с ним лишь в тех методах, которые описаны в этом интерфейсе. Наличие или, напротив, отсутствие других методов или свойств никакой роли не играет. Для того чтобы объект смог реализовать нужный интерфейс, важно лишь, чтобы у него были указанные в шапке интерфейса методы и свойства. Дополнительные методы никакой роли не играют, ну, как не играет роли во время прикуривания сигареты наличие детей у прохожего, предоставившего нам зажигалку.
Для кода, использующего делегат, безразлично реальное название метода, его замещающего. В интерфейсе, напротив, все объявленные конструкции обязательно должны называться точно так же, как указано в самом интерфейсе. Если вы описали в интерфейсе событие onCommand, будьте любезны создать в реальном классе событие, называющееся onCommand и никак иначе. Еще одним требованием является то, что в объекте обязательно должны быть реализованы все методы, объявленные в интерфейсе. Интерфейс нельзя реализовать лишь частично, тут дело обстоит как в известном анекдоте про беременность. Если вы забудете реализовать какой-либо метод, вы получите ошибку времени компиляции. То есть просто не сможете собрать свой проект.

Один и тот же объект может реализовывать сразу несколько интерфейсов. Почему бы и нет? Ведь мы можем спросить у человека, одолжившего нам зажигалку, который час? Разумеется, если у него есть часы. А наличие часов, с точки зрения бытовой логики, — это уже другой интерфейс.
Для того чтобы обыграть возможности, предоставляемые нам интерфейсами, давайте напишем несложное консольное приложение. В нем мы смоделируем вышеописанную ситуацию, при которой мы опрашиваем прохожих на улице с целью прикурить сигарету.
Для начала опишем сам объект зажигалки, обладающий методом Go, ответственным за процесс прикуривания.

// Описываем саму зажигалку
public class Lighter{
public void Go(){Console.Write Line("Прикуриваем...");}
}
Теперь мы можем описать и интерфейс, задающий способность прохожего дать нам зажигалку.
// интерфейс владельца зажигалки
public interface ILighterMan{
Lighter lighter{get;set;}
}

Мы описываем в этом куске кода свойство, возвращающее или устанавливающее объект Lighter, то есть зажигалку. Благодаря возможности брать и отдавать зажигалку мы сможем реализовать более естественную модель взаимоотношений между прохожим и нами. Если бы нам нужно было описать свойство только для чтения или только для записи, следовало бы просто опустить в объявлении тот или иной акцессор.
Следующим шагом мы описываем интерфейс владельца часов. Он у нас будет иметь метод, называющийся GetTime и возвращающий, в теории, текущую дату и время.

// интерфейс владельца часов
public interface IHasTime{
string GetTime();
}

Следующим шагом приступим к описанию классов, описывающих прохожих на нашей гипотетической улице.
Первый вид прохожих нам не интересен. У них нет ни часов, ни зажигалки. С точки зрения программирования это ничем не примечательный потомок класса Object, не обладающий какими-либо методами или свойствами.

// обычный прохожий
class Walker {}

Следующий класс намного любопытнее. Это прямой потомок нашего предыдущего класса, но уже обладающий реализацией интерфейса ILighterMan. Мы указываем этот факт, ссылаясь на название интерфейса, через запятую, в списке предков нашего объекта. Как только мы взяли на себя ответственность за реализацию интерфейса, нам придется добавить к нашему будущему объекту реализацию свойства lighter, объявленного в интерфейсе.
Для этого я описал в теле класса private переменную класса Lighter (lighter1) и добавил публичное свойство для доступа к нему по маске, описанной в интерфейсе. Таким образом, мы выполнили "контракт" по реализации интерфейса ILighterMan. Объявленное в нем обязательное свойство теперь реализовано, и мы можем смело утверждать, что данный класс реализует интерфейс ILighterMan.

// потомок Walker с зажигалкой
class WalkerObject : Walker, ILighterMan {
private Lighter lighter1 =new Lighter();
public Lighter lighter{
get{return lighter1;}
set{lighter1=value;}
}
}

Третий вид прохожих, попадающихся на нашей улице, выглядит еще вычурнее. Для того чтобы подчеркнуть достоинство от использования интерфейсов, я унаследовал его не от вновь создаваемой нами иерархии классов производных от Walker, а от класса System.Data.DataColumn. Я намеренно выбрал столь неестественный для нашей программы класс. Надеюсь, вы еще не забыли о том, что понятие реализации интерфейса никак не связано с понятием наследования в иерархии классов Net.Framework? Благодаря этому обстоятельству мы можем оформлять реализацию нужного нам интерфейса в самых непредсказуемых, на первый взгляд, объектах.
Также этот вид объекта реализует сразу два интерфейса: ставший привычным для нас интерфейс ILighterMan, а также интерфейс IHasTime, отвечающий за наличие часов. Наследование дополнительного интерфейса накладывает на нас обязательство реализовать, помимо свойства lighter, описанного в интерфейсе ILighterMan, еще и метод GetTime, описанный в интерфейсе IHasTime.

// DataColumn "с зажигалкой" и часами
class WalkerDataColumn:
System.Data.DataColumn,ILighterMan,IHasTime {
private Lighter lighter1=new Lighter();
public Lighter lighter{
get { return lighter1; }
set { lighter1=value; }
}
public string GetTime(){
return System.DateTime.Now. ToString();
}
}

Описав всех действующих лиц, можем приступать к реализации основного потока программы, моделирующего поведение прохожих на улице. Основной поток, для простоты примера, мы реализуем прямо в методе Main нашей программы. Если бы я писал реальную программу, а не пример, я бы его оформил, скорее всего, в виде отдельного класса. Я намеренно подробно расписал в комментариях каждый шаг нашей программы. Обратите внимание на то, что, получая доступ к объекту, мы приводим его к типу нужного интерфейса, а не к типу какого-либо объекта.

static void Main(string[] args){
// создаем динамический массив
System.Collections.ArrayList Street =
new System.Collections.Array List();
int i=0;
// в цикле добавляем в массив 15 объектов,
// по 5 штук каждого вида
for(i=0;i<5;i++){
Street.Add(new Walker());
Street.Add(new WalkerObject());
Street.Add(new WalkerDataColumn());
}
//перебираем все объекты в массиве
foreach(object walker in Street){
// выводим строчку с описанием типа прохожего
Console.WriteLine("\nВстретили : "+walker.GetType());
if(walker is ILighterMan){
// вот тут смотрите, к чему мы приводим объект. Уловили идею?
ILighterMan lighterMan = (walker as ILighterMan);
//берем зажигалку у прохожего
Lighter lighter = lighter Man.lighter;
// прикуриваем
lighter.Go();
// отдаем зажигалку обратно
lighterMan.lighter = lighter;
// выясняем, есть ли у прохожего часы
if(walker is IHasTime){
string time = (walker as IHasTime).GetTime();
Console.WriteLine("Выяснили время : " +time);
}
}else {
// встреченный объект не поддерживает ILighterMan
Console.WriteLine("Тут нам ничего не светит...");
}
}
// Ждем ввода Enter
Console.WriteLine("Press Enter for continue...");
Console.ReadLine();
}

Обратите свое внимание на довольно любопытный нюанс этой программы. Мы с вами добавляем в массив совершенно разнородные объекты. В дальнейшем мы вовсе не интересуемся их настоящим типом! Этот самый "настоящий тип объекта" нам просто неинтересен. Мы просто выясняем с помощью команды is, реализован ли в этом объекте нужный нам интерфейс. Если такой интерфейс присутствует, мы приводим этот объект к виду интерфейса. Внешне все это выглядит как создание переменной типа интерфейса. Дальше мы совершенно спокойно работаем с объектом таким образом, как будто это экземпляр некоего гипотетического объекта ILighterMan. Но заметьте, в реальности это объект совершенно другого типа, а объекта типа ILigherMan так и вовсе не существует в природе! Тем не менее, мы пользуемся реализуемыми им методами и свойствами в рамках объявленного нами интерфейса. Этот программный прием широко используется в иерархии классов Net.Framework, сошлюсь в качестве примера на пространство имен System.Collection. В нем имеется множество интерфейсов, предназначенных для создания списков, коллекций, а также сравниваемых и клонируемых классов.

Продолжение следует


Компьютерная газета. Статья была опубликована в номере 09 за 2004 год в рубрике программирование :: разное

©1997-2024 Компьютерная газета