Ключ к пониманию интерфейсов лежит в их сравнении с классами. Классы — это объекты, обладающие свойствами и методами, которые на эти свойства воздействуют. Хотя классы проявляют некоторые характеристики, связанные с поведением (методы), они представляют собой предметы, а не действия, присущие интерфейсам. Интерфейсы позволяют определять характеристики или возможности действий и применять их к классам независимо от иерархии последних. Допустим, у вас есть дистрибьюторское приложение, сущности которого можно упорядочить. Среди них могут быть классы Customer, Supplier и Invoice. Некоторые другие, скажем, MaintenanceView или Document, упорядочивать не надо. Как упорядочить только выбранные вами классы? Очевидный способ — создать базовый класс с именем типа Serializable. Но у этого подхода есть крупный минус. Одна ветвь наследования здесь не подходит, так как нам не требуется наследование всех особенностей поведения. С# не поддерживает множественное наследование, так что невозможно произвести данный класс от нескольких классов. А вот интерфейсы позволяют определять набор семантически связанных методов и свойств, способные реализовать избранные классы независимо от их иерархии.
Концептуально интерфейсы представляют собой связки между двумя в корне отличными частями кода. Иначе говоря, при наличии интерфейса и класса, определенного как реализующий данный интерфейс, клиентам класса дается гарантия, что у класса реализованы все методы, определенные в интерфейсе. Скоро вы это поймете на примерах.
Прочитав эту главу, вы поймете, почему интерфейсы являются такой важной частью С# в частности и программирования на основе компонентов вообще. Затем мы познакомимся с объявлением и реализацией интерфейсов в приложениях на С#. В завершение мы углубимся в специфику использования интерфейсов и преодоления врожденных проблем с множественным наследованием и конфликтами имен.
ПРИМЕЧАНИЕ Когда вы создаете интерфейс и в определении класса задаете его использование, говорят, что класс реализует интерфейс, или наследует его. Лично я считаю термин "реализовать" корректнее. Интерфейсы — это набор характеристик поведения, а класс определяется как реализующий их в противоположность наследования их от другого класса.
Чтобы понять, где интерфейсы приносят пользу, рассмотрим традиционную проблему программирования в Windows, когда нужно обеспечить универсальное взаимодействие двух совершенно различных фрагментов кода без использования интерфейсов. Представьте себе, что вы работаете на Microsoft и являетесь ведущим программистом команды по разработке панели управления. Вам надо предоставить универсальные средства, которые дают возможность клиентским апплетам "закрепляться" на панели управления, показывая при этом свой значок и позволяя клиентам выполнять их. Если учесть, что эта функциональность разрабатывалась до появления СОМ, возникает вопрос: как создать средства интеграции любых будущих приложений с панелью управления? Задуманное решение долгие годы было стандартной частью разработки Windows.
Как ведущий программист по разработке панели управления, вы создаете и документируете функцию (функции), которая должна быть реализована в клиентском приложении, и некоторые правила. В случае апплетов панели управления, Microsoft определила, что для их написания вам нужно создать динамически подключаемую библиотеку, которая реализует и экспортирует функцию CPIApplet. Вам также потребуется добавить к имени этой DLL расширение .ср! и поместить ее в папку Windows System32 (для Windows ME или Windows 98 это будет Win-do ws\System32, а для Windows 2000 — WINNT\System32). При загрузке панель управления загружает все DLL с расширением .cpl из папки System32 (с помощью функции LoadLibrary), а затем вызывает функцию GetProcAddress для загрузки функции CPIApplet, проверяя таким образом выполнение вами соответствующих правил и возможность корректного взаимодействия с панелью управления.
Как я уже говорил, эта стандартная модель программирования в Windows позволяет выйти из ситуации, когда вы хотите, чтобы ваш код универсальным образом взаимодействовал с кодом, который будет разработан в будущем. Однако это не самое элегантное в мире решение.
Главный недостаток этой методики в том, что она вынуждает включать в состав Клиента — в данном случае в код панели управления — большие порции проверяющего кода. Например, панель управления не может просто полагаться на допущение, что каждый .cpl-файл в папке является DLL Windows. Панель управления также должна проверить наличие в этой DLL функций коррекции и что эти функции делают именно то, что описано в документации. Здесь-то интерфейсы и вступают в дело. Интерфейсы позволяют создавать такие же средства, связывающие несовместимые фрагменты кода, но при этом они более объектно-ориентированны и гибки. Кроме того, поскольку интерфейсы являются частью языка С#, компилятор гарантирует, что если класс определен как реализующий данный интерфейс, то он выполняет именно те действия, о которых он заявляет, что должен их выполнять.
В С# интерфейс — понятие первостепенной важности, объявляющее ссылочный тип, который включает только объявления методов. Но что значит "понятие первостепенной важности"? Я хотел сказать, что эта встроенная функция является неотъемлемой частью языка. Иначе говоря, это не то, что было добавлено позже, после того как разработка языка была закончена. А давайте поподробнее познакомимся с интерфейсами, узнаем, что они собой представляют и как их объявлять.
ПРИМЕЧАНИЕ Разработчики на C++ могут воспринимать
интерфейс в основном как абстрактный класс, в котором объявлены только чисто
виртуальные методы в дополнение к другим типам членов классов С#, таким как
свойства, события и индексаторы.
Интерфейсы могут содержать методы, свойства, индексаторы и события, но ни одна из этих сущностей не реализуется в самом интерфейсе. Рассмотрим их применение на примере. Допустим, вы создаете для вашей компании редактор, содержащий элементы управления Windows. Вы пишете редактор и тестовые программы для проверки элементов управления, размещаемых пользователями на форме редактора. Остальная часть команды пишет элементы управления, которыми будет заполнена форма. Вам почти наверняка понадобятся средства проверки на уровне формы. В определенное время, скажем, когда пользователь явно приказывает форме проверить все элементы управления или при обработке формы, последняя циклически проверяет все прикрепленные к ней элементы управления, или, что более подходяще, заставляет элемент управления проверить самого себя.
Как предоставить такую возможность проверки элемента управления? Именно здесь проявляется превосходство интерфейсов. Вот пример простого интерфейса, единственным методом Validate:
interface IValidate {
boo! ValidateO; }
Теперь вы можете задокументировать тот факт, что если элемент управления реализует интерфейс IValidate, то этот элемент управления может быть проверен.
Рассмотрим пару аспектов, связанных с приведенным кодом. Во-первых, вы не должны задавать для метода интерфейса модификатор доступа, такой как public. При указании модификатора доступа перед объявлением метода возникает ошибка периода компиляции. Дело в том, что все методы интерфейса открыты по умолчанию (разработчики на C++ могут также заметить, что, поскольку интерфейсы по определению — это абстрактные классы, не требуется явно объявлять метод как "чисто" виртуальный (pure virtual), прибавляя =0к его определению).
Кроме методов, интерфейсы могут определять свойства, индексаторы и события:
interface lExamplelnterface {
// Пример объявления свойства.
int testProperty { get; }
// Пример объявления события, event testEvent Changed;
// Пример объявления индексатора, string this[int index] { get; set; } }
Поскольку интерфейс определяет связь между фрагментами кода, любой класс, реализующий интерфейс, должен определять любой и каждый элемент этого интерфейса, иначе код не будет компилироваться. Используя IValidate из нашего примера, клиентский класс должен реализовать лишь методы интерфейса. В следующем примере есть базовый класс FancyControl и интерфейс IValidate. Кроме того, в нем имеется класс
MyControl, производный от FancyControl, реализующий интерфейс IValidate. Обратите внимание на синтаксис и способ приведения объекта MyControl к интерфейсу IValidate для ссылки на его члены.
using System;
public class FancyControl
{
protected string Data; public string data {
get {
return this.Data; }
set {
this.Data = value; } } }
interface IValidate {
bool ValidateO; }
class MyControl : FancyControl, IValidate {
public MyControlQ
{
data = "my grid data";
}
public bool ValidateO {
Console.WriteLine("Validating...{0}", data); return true;
} >
class InterfaceApp {
public static void Main() {
MyControl myControl = new MyControK);
// Вызов функции для размещения элемента управления // в форме. Теперь для проверки элемента управления // редактор может выполнить такие действия:
IValidate val = (IValidate)myControl; bool success = val.ValidateO;
Console.WriteLineC'The validation of '{0}' was {1 Successful", myControl.data,
(true == success ? "" : "not ")); } }
С помощью определения такого класса и интерфейса редактор может запрашивать у элемента управления, реализует ли он интерфейс IValidate (ниже я покажу, как это сделать). Если это так, редактор может проверить этот элемент управления, а затем вызывать реализованные методы интерфейса. Возможно, вы спросите: "А почему бы мне просто не определить базовый класс для использования в этом редакторе, у которого есть чисто виртуальная функция Validate! Ведь после этого редактор будет принимать только элементы управления, производные от этого базового класса, да?"
Да, но... это решение повлечет суровые ограничения. Допустим, вы создаете собственные элементы управления, каждый из которых является производным от гипотетического базового класса. Как следствие, все они реализуют этот виртуальный метод Validate. Это будет работать, пока в один прекрасный день вы не найдете по-настоящему замечательный элемент управления и вам захочется использовать его в редакторе. Допустим, вы нашли компонент "сетка", написанный кем-то другим и поэтому не являющийся производным от нужного редактору базового класса "элемент управления". На C++ решение заключается в том, чтобы с помощью множественного наследования сделать сетку производной от компонента стороннего разработчика и базового класса редактора одновременно. Но С# не поддерживает множественное наследование.
Интерфейсы позволяют реализовать несколько характеристик поведения в одном
классе. На С# можно создавать объекты, производные от одного класса, и в дополнение
к этой унаследованной функциональности реализовать столько интерфейсов, сколько
нужно для класса. Например, чтобы приложение-редактор проверяло содержимое элемента
управления, связывало элемент управления с базой данных и последовательно направляло
его содержимое на диск, объявите свой класс так:
public class MyGrid : ThirdPartyGrid, IValidate. ISerializable, IDataBound {
}
И все же вопрос остался: "Как отдельный фрагмент кода узнает, реализован ли классом данный интерфейс?"
Запрос о реализации интерфейса с помощью is
В примере InterfaceApp вы видели код, использованный для приведения объекта (MyControl) к одному из реализованных в нем интерфейсов (IValidate) и затем для вызова одного из членов этого интерфейса (Validate):
MyControl myControl = new MyControlO;
IValidate val = (IValidate)myControl; bool success = val.ValidateO;
Что будет, если клиент попытается использовать класс так, как если бы в последнем был реализован метод, на самом деле в нем не реализованный? Следующий пример будет скомпилирован, поскольку интерфейс ISerializable является допустимым. И все же в период выполнения будет передано исключение System.InvalidCastException, так как в MyGrid не реализован интерфейс ISerializable. После этого выполнение приложения прервется, если только исключение не будет явно уловлено.
using System;
public class FancyControl
{
protected string Data; public string data {
get {
return this.Data; }
set {
this.Data = value;
> } }
interface ISerializable {
bool Save(); }
interface IValidate {
bool ValidateO; }
class MyControl : FancyControl, IValidate {
public MyControlO
{
data = "my grid data";
>
public bool ValidateO {
Console.WriteLine("Validating...{0}", data);
return true; > }
class IsOperatorlApp {
public static void Main()
{
MyControl rayControl = new MyControlO;
ISerializable ser = (ISerializable)myControl;
// Заметьте: в результате этого будет сгенерировано // исключение System.InvalidateCastException, поскольку // в классе не реализован интерфейс ISerializable. bool success = ser.Save();
Console.WriteLine("The saving of '{0}' was {1}successful",
myControl.data,
(true == success ? "" : "not "));
} }
Конечно, улавливание исключения не повлияет на то, что предназначенный для выполнения код в этом случае не будет исполнен. Способ запроса объекта перед попыткой его приведения — вот что вам нужно. Один из способов — задействовать оператор is. Он позволяет в период выполнения проверять совместимость одного типа с другим. Оператор имеет следующий вид, где выражение — ссылочный тип:
выражение is тип
Результат оператора is — булево значение, которое затем можно использовать с условными операторами. В следующем примере я изменил код, чтобы проверять совместимость между классом MyControl и интерфейсом ISerializable перед попыткой вызова метода ISerializable:
using System;
public class FancyControl
{
protected string Data; public string data {
get
{
return this.Data;
}
set
{
this.Data = value;
} }
}
interface ISerializable {
bool Save(); >
interface IValidate {
bool ValidateO;
}
class MyControl : FancyControl, IValidate {
public MyControlO
{
data = "my grid data";
}
public bool ValidateQ {
Console.WriteLine("Validating...{0}", data);
return true; > }
class IsOperator2App {
public static void Main()
{
MyControl myControl = new MyControlO;
if (myControl is ISerializable) {
ISerializable ser = (ISerializable)myControl;
bool success = ser.SaveO;
Console. WriteLinef'The saving of '{0}' was "+ "{1}successful", myControl.data,
(true == success ? "" : "not ")); }
else {
Console.WriteLine("The ISerializable interface "+ "is not implemented."); > } }
Вы увидели, как оператор is позволяет проверить совместимость двух типов, чтобы гарантировать их корректное использование. А теперь рассмотрим его близкого родственника — оператор as и сравним их.
Запрос о реализации интерфейса с помощью as
Приглядевшись к MSIL-коду, сгенерированному из предыдущего примера IsOperator2App (см. MSIL-код после этого абзаца), вы заметите проблему, связанную с оператором is. Сразу после выделения объекта и подготовки стека вызывается isinst. Код операции isinst, генерируемый компилятором для оператора С# is, проверяет, чем является объект: экземпляром класса или интерфейсом. Заметьте, что лишь через несколько строк, при условии, что проверка условий пройдена, компилятор генерирует код операции castclass. Этот код выполняет собственную проверку, и, поскольку он работает несколько иначе, чем isinst, в результате сгенерированный IL-код выполняет неэффективную работу, дважды проверяя правильность приведения.
.method public hidebysig static void MainQ il managed {
.entrypoint
// Code size 72 (0x48)
.maxstack 4
.locals (class MyControl V_0,
class ISerializable V_1, bool V_2)
IL_0000: newobj instance void MyControl::.ctor()
IL_0005: stloc.O
IL_0006: ldloc.0
ILJ)007: isinst ISerializable
IL_OOOc: brfalse.s IL_003d
IL_OOOe: ldloc.0
IL_OOOf: castclass ISerializable
IL_0014: stloc.1
IL_0015: ldloc.1
IL_0016: callvirt instance bool ISerializable::Save()
IL_001b: stloc.2
IL_001c: Idstr "The saving of '{0}' was {1}successful"
IL_0021: ldloc.0
IL_0022: call instance class System.String FancyControl::get_data()
IL_0027: ldloc.2
IL_0028: brtrue.s IL_0031
IL_002a: Idstr "not "
IL_002f: br.s IL_0036
IL_0031: Idstr
IL_0036: call void [mscorlib]System.Console::WriteLLne(class System. String,
class System.Object,
class System.Object) IL_003b: br.s IL_0047
IL_003d: Idstr "The ISerializable interface is not implemented." IL_0042: call void [mscorlib]System.Console::WriteLine(class System. String) IL_0047: ret } // end of method IsOperator2App::Hain
Мы можем повысить эффективность процесса проверки с помощью оператора as, который преобразует совместимые типы и принимает такой вид:
объект = выражение as тип
где выражение — любой ссылочный тип
Можно думать, что оператор as представляет собой комбинацию оператора is и, если рассматриваемые типы совместимы, приведения. Важное различие между as и is в том, что если выражение и тип несовместимы, то вместо возврата булевского значения оператор as устанавливает объект в null. Теперь наш пример можно переписать:
using System;
public class FancyControl {
protected string Data; public string data {
get {
return this.Data; }
set {
this.Data = value; > } }
interface ISerializable {
bool Save(); >
interface IValidate {
bool ValidateQ; }
class HyControl : FancyControl, IValidate
{
public MyControlO
{
data = "my grid data";
}
public bool ValidateO
{
Console.WriteLine("Validating...{0}", data);
return true; > >'
class AsOperatorApp
{
public static void Main()
{
MyControl myControl = new MyControlO;
ISerializable ser = myControl as ISerializable; if (null != ser)
<
bool success = ser.SaveO;
Console.WriteLine("The saving of -{0}' was "+
"{1}successful",
myControl.data,
(true == success ? "" : "not "));
} else
{ Console.WriteUne("nie ISerializable interface is not implemented.");
} } }
Теперь проверка, гарантирующая правильность приведения, производится только раз, что, думаю, намного эффективнее. А сейчас вновь обратимся к MSIL-коду, чтобы увидеть результаты применения оператора as:
.method public hidebysig static void Main() 11 managed {
.entrypoint
// Code size 67 (0x43) .maxstack 4 .locals (class MyControl V_0,
class ISeriallzable V_1,
bool V_2)
IL_0000: newobj Instance void MyControl::.ctor() IL_0005: stloc.O IL_0006: ldloc.0
IL_0007: isinst ISerializable IL_OOOc: stloc.1 IL_OOOd: ldloc.1 IL_OOOe: brfalse.s IL_0038 IL_0010: ldloc.1
IL_0011: callvirt instance bool ISerializable::Save() IL_0016: stloc.2
IL_0017: Idstr . "The saving of '{0}' was {1}successful" IL_001c: ldloc.0
IL_001d: call instance class System.String FancyControl::get_data()
IL_0022: ldloc.2
IL_0023: brtrue.s IL_002c
IL_0025: Idstr "not "
IL_002a: br.s IL_0031
IL_002c: Idstr
IL_0031: call void [mscorlib]System.Console::WriteLine(class System.String,
class System.Object, class System.Object)
IL_0036: br.s IL_0042
IL_0038: Idstr "The ISerializable interface is not implemented." IL_003d: call void [mscorlib]System.Console::WriteLine(class System.String) IL_0042: ret } // end of method AsOperatorApp::Main
Явная квалификация имени члена интерфейса
Вы уже познакомились с классами, реализующими интерфейсы путем задания модификатора
доступа public, за которым следует сигнатура метода интерфейса. Однако
иногда у вас будет возникать желание (или даже необходимость) явно квалифицировать
имя члена именем интерфейса. В этом разделе мы изучим две причины, которые могут
заставить нас поступить таким образом.
Сокрытие имени с помощью интерфейсов
'Чтобы вызвать метод, реализованный в интерфейсе, необходимо привести экземпляр этого класса к типу интерфейса и вызвать нужный ме-цод — это самый распространенный подход. Хотя это работает и многие (включая и меня) используют эту методику, формально вы вовсе не обязаны приводить объект к реализованному им интерфейсу, чтобы вы-зывать методы этого интерфейса. Этот так, потому что методы интерфейса, реализованные классом, также являются открытыми методами Цасса. Взгляните на код на С#, особенно на метод Main, чтобы понять, что я имею в виду:
using System;
public interface IDataBound {
void BindQ; }
public class EditBox : IDataBound
{
// Реализация IDataBound. public void BindQ
{
Console.WriteLineC'Binding to data store...");
} }
class NameHidingUpp
{
II Точка входа Main, public static void MainQ {
Console.WriteLine();
EditBox edit = new EditBox();
Console.WriteLine("Calling EditBox.Bind()...");
edit.BindO;
Console.WriteLine();
IDataBound bound = (IDataBound)edlt;
Console.WriteLine("Calling (IDataBound)EditBox.Bind()...");
bound.Bind();
> } I Теперь этот пример выдаст следующее: /
Calling EditBox.Bind()... J Binding to data store... •
/'
Calling (IDataBound))EditBox.Bind()... ; Binding to data store... '
Заметьте: хотя это приложение вызывает реализованный метод Btyid двумя способами — с помощью приведения и без него, оба вызова кс|р-ректно функционируют, в том смысле, что Bind выполняется. Хотя цо-началу возможность прямого вызова реализованного метода без приведения объекта к интерфейсу может показаться неплохой, порой это более чем нежелательно. Самая очевидная причина этого то, что реализация нескольких интерфейсов, каждый из которых может содержать массу членов, может привести к быстрому засорению отрытого пространства имен вашего класса членами, не имеющими значения за пределами видимости реализующего эти интерфейсы класса. Вы можете не позволять реализованным членам интерфейсов становиться открытыми членами класса, применяя методику сокрытия имен (name hiding).
В простейшем случае сокрытие имен представляет собой возможность скрывать имя унаследованного члена от любого кода за пределами производного или реализующего его класса [его часто называют внешним миром (outside world)]. Возьмем тот пример, где класс EditBox должен был реализовывать интерфейс IDataBound, но на этот раз EditBox не должен предоставлять методы IDataBound внешнему миру. Этот интерфейс нужен ему скорее для собственных нужд, или, возможно, программист просто не хочет засорять пространство имен класса многочисленными методами, которые обычно не используются клиентом. Чтобы скрыть член реализованного интерфейса, нужно лишь удалить модификатор доступа члена public и квалифицировать имя члена именем интерфейса:
using System;
public interface IDataBound {
void Bind();
}
public class EditBox : IDataBound
1 // реализация IDataBound. I void IDataBound.Bind()
| <
| Console.WriteLine("Binding to data store...");
\ }
}\
cl ss NameHiding2App {
public static void MainQ
{
Console.WriteLine();
EditBox edit = new EditBoxO;
Console.WriteLine("Calling EditBox. BindQ...");
// ОШИБКА: эта строка не будет компилироваться, так как // метод Bind более не существует в пространстве имен // класса EditBox. edit.BindQ;
Console. WriteLineO;
IDataBound bound = (IDataBound)edit;
Console.WriteLine("Calling (IDataBound)EditBox.BindQ...");
// Это правильно, так как объект был сначала приведен
// к IDataBound.
bound.Bind(); } >
Этот код не будет компилироваться, так как имя члена Bind более не является частью класса EditBox. Поэтому данная методика позволяет вам удалять член из пространства имен класса, в то же время разрешая явный доступ к нему с помощью операции приведения.
Повторю: при сокрытии члена вы не можете применять модификатор доступа. При
попытке использования модификатора доступа с членом реализованного интерфейса
вы получите ошибку периода компиляции. Может, это покажется странным, но поймите,
что общая причина, заставляющая скрыть что-то, — желание сделать эту сущность
невидимой за пределами текущего класса. Так как модификаторы доступа существуют
лишь для определения уровня видимости за пределами базо- , вого класса, при
сокрытии имен они не имеют смысла.
Избежание неоднозначности имен
Одна из главных причин, по которой С# не поддерживает множестве"-ное наследование, — проблема конфликта имен, результатом котором является неоднозначность имен. Хотя С# не поддерживает множественное наследование на уровне объектов (создание производных классов), он поддерживает наследование от одного класса и дополнительную реализацию нескольких интерфейсов. Однако за дополнительные возможности приходится расплачиваться конфликтами имен. I
Ниже интерфейсы ISerializable и IDataStore поддерживают чтение и хранение данных в разных форматах: в двоичной форме в виде объектов для хранения на диске и для хранения в БД. Проблема в том, что рни оба содержат методы с именем SaveData:
using System;
interface ISerializable {
void SaveDataO; }
interface IDataStore {
void SaveDataQ; }
class Test : ISerializable, IDataStore {
public void SaveDataO
{
Console.Writel_ine("Test. SaveData called");
} }
class NameCollisionslApp {
public static void Main()
{
Test test = new Test();
Console.WriteLine("Calling Test.SaveDataO"); ! test. SaveDataO;
\ >
>|
\ Во время написания этот код компилировался. Однако, как я уже говорил, в будущей версии компилятора С# этот код вызовет ошибку периода компиляции из-за неоднозначности реализованного метода Save-Ddta. Независимо от того, компилируется ли этот код, у вас будет про-бл^ма, так как поведение в результате вызова метода SaveData будет нео-пр^деленным для программиста, пытающегося задействовать этот класс. Получите ли вы метод SaveData, который последовательно за-пвдывает данные объекта на диск, или метод SaveData, который сохраняет их в БД?
В дополнение взгляните на такой код:
using System;
interface ISerializable {
void SaveDataO;
>
interface IDataStore {
void SaveDataO; }
class Test : ISerializable. IDataStore <
public void SaveDataO
{
Console.WriteLine("Test.SaveData called");
> }
class NameCollisions2App {
public static void Main()
{
Test test = new Test();
if (test is ISerializable)
{
Console.WriteLineC'ISerializable is implemented"); I
> / if (test is IDataStore)
< / Console.WriteLine("IDataStore is implemented"); /
}
> I }
В этом примере оператор is успешно выполняется в обоих интерфейсах, а значит, реализованы оба интерфейса, хотя мы знаем, что'это не так! При компиляции данного примера компилятор даже выдаст предупреждения:
NameCollisions2.cs(27,7): warning CS0183: The given expression is always of the provided ('ISerializable') type NameCollisions2.cs(32,7): warning CS0183; The given expression is always of the provided ('IDataStore') type
Проблема в том, что в классе реализованы или сериализованная версия метода Bind, или версия, использующая БД (но не обе сразу). Однако клиент получит положительный результат при проверке на реализацию обеих версий этого интерфейса. При попытке задействовать интерфейс, который на самом деле не реализован, получится непредсказуемый результат.
Чтобы решить эту проблему, можно обратиться к явной квалификации имени члена: уберем модификатор доступа и поставим перед именем члена (в данном случае перед SaveData) имя интерфейса:
using System;
interface ISerializable {
void SaveDataQ;
}
interface IDataStore
{
void SaveDataO;
}
class Test : ISerializable, IDataStore {
void ISerializable.SaveDataO
{
Console.WriteLine("Test.ISerializable.SaveData called");
}
void IDataStore. SaveDataO
{
Console.WriteLine("Test.IDataStore.SaveData called");
} }
class NameCollisionsSApp {
public static void MainO {
Test test = new Test(); if (test is ISerializable) {
Console.WriteLlne("ISerializable is Implemented"); ((ISe rializable)test). SaveDataO; >
Console.WriteLineO; if (test is IDataStore) {
Console.WriteLine("IDataStore is implemented"); ((IDataSto re)test).SaveData(); } } }
Теперь можно сказать однозначно, какой метод будет вызван. Оба метода реализованы с полностью квалифицированными именами, а приложение выдает именно тот результату что вы ожидаете:
ISerializable is implemented Test.ISe rializable.SaveData called
IDataStore is implemented Test.IDataStore.SaveData called
С интерфейсами и наследованием связаны две распространенные проблемы. Первая, проиллюстрированная приведенным ниже кодом, связана с созданием производного класса, содержащего метод, чье имя идентично имени метода интерфейса, который должен быть реализован классом:
using System;
public class Control {
public void SerializeO
<
Console.WriteLine("Control.Serialize called");
> >
public interface IDataBound {
void SerializeO;
}
public class EditBox : Control, IDataBound
{
>
class InterfacelnhlApp
{
public static void Main()
{
EditBox edit = new EditBoxQ; edit. SerializeO; } }
Как вы знаете, чтобы реализовать интерфейс, нужно предоставить определение
каждого члена в определении интерфейса. Однако в предыдущем примере мы этого
не сделали, а код все равно компилируется! Причина в том, что компилятор С#
ищет метод Serialize, реализованный в классе EditBox, и находит
его. Однако компилятор неверно определяет, что это реализованный метод. Метод
Serialize, найденный компилятором, унаследован от класса Control методом
Serialize, но не является настоящей реализацией метода IDataBound.Serialize.
Поэтому, хоть он и компилируется, этот код не будет функционировать, как
задумано, в чем мы убедимся позже.
Теперь внесем дополнительную интригу. Следующий код сначала проверяет оператором as, реализован ли интерфейс, затем пытается вызвать реализованный метод Serialize. Этот код компилируется и работает. Однако, как мы знаем, в классе EditBox метод Serialize на самом деле не реализован из-за наследования IDataBound. В EditBox уже есть метод Serialize (унаследованный) от класса Control. Это значит, что клиент, по всей вероятности, не получит ожидаемых результатов.
using System;
public class Control {
public void SerializeO
{
Console.WriteLine("Control.Serialize called");
} }
public interface IDataBound {
void SerializeO; }
public class EditBox : Control, IDataBound
{
}
class InterfaceInh2App {
public static void MainО
<
EditBox edit = new EditBoxO;
IDataBound bound = edit as IDataBound;
if (bound != null)
{
Console.WriteLine("IDataBound is supported...");
bound. SerializeO; }
else {
Console.WriteLineC'IDataBound is NOT supported...");
>
> >
Возникновение другой потенциальной проблемы ожидается, когда у производного класса есть метод с тем же именем, что и у реализации метода интерфейса из базового класса. Давайте рассмотрим этот код:
using System;
interface ITest
{
void Foo();
}
// В классе Base реализован интерфейс ITest. class Base : ITest
<
public void Foo()
{
Console.WriteLine("Base.Foo (ITest implementation)");
> }
class HyDerived : Base
{
public new void Foo()
{
Console.WriteLine("MyDerived.Foo");
} }
public class InterfacelnhSApp
{
public static void MainQ
{
MyDe rived myDe rived = new MyDerivedO;
myDerived.Foo();
ITest test = (ITest)myDerived; test.FooQ; }
}
В результате выполнения этот код выводит информацию:
MyDerived.Foo
Base.Foo (ITest implementation)
В этой ситуации в классе Base реализован интерфейс ITest и его метод Foo. Однако производный от Base класс My Derived реализует для этого класса новый метод Foo. Какой из методов Foo будет вызван? Это зависит от имеющейся у вас ссылки. Если есть ссылка на объект My Derived, вызывается его метод Foo. Это так, даже несмотря на то, что у объекта myDerived есть унаследованная реализация ITest.Foo: в период выполнения будет исполнен MyDerived.Foo, так как ключевым словом new задана подмена унаследованного метода.
Однако когда вы явно выполняете приведение объекта myDerived к интерфейсу ITest, компилятор разрешает реализацию интерфейса. У класса MyDerived есть метод с тем же именем, но это не тот метод, что ищет компилятор. Когда вы приводите объект к интерфейсу, компилятор проходит по дереву наследования, пока не найдет класс, содержащий в своем базовом списке интерфейс. Именно поэтому в результате выполнения последних двух строк кода метода Main вызывается метод Foo, реализованный в ITest.
Хочется надеяться, что некоторые из этих потенциальных ловушек, включая конфликты имен и наследование интерфейсов, убедили вас следовать моей настоятельной рекомендации: всегда приводите объекты к интерфейсу, член которого вы пытаетесь использовать.
Еще одна мощная функция С# — возможность комбинирования двух или более интерфейсов, и в итоге класс должен реализовать только результат комбинирования. Пусть вы хотите создать новый класс TreeView, реализующий оба интерфейса: IDragDrop и ISortable. Поскольку резонно предполагать, что и другим элементам управления, таким как ListView и ListBox, понадобится скомбинировать эти функции вам, возможно, захочется скомбинировать интерфейсы IDragDrop и ISortable в единое целое:
using System;
public class Control
{
}
public interface IDragDrop {
void DragQ; void Drop(); }
public interface ISerializable
{
void SerlalizeO;
}
public interface ICombo : IDragDrop, ISerializable
{
// Этот интерфейс не дает ничего нового // (что касается поведения), так как единственное его // назначение - скомбинировать интерфейсы IDragDrop // и ISerializable в единый интерфейс.
}
public class MyTreeView : Control, ICombo
{
public void DragQ
{
Console.WriteLineC'MyTreeView.Drag called");
}
public void Drop()
{
Console.WriteLineC'MyTreeView. Drop called");
>
public void Serialize()
{
Console.WriteLineC'MyTreeView.Serialize called");
} }
class CombiningApp
{
public static void MainQ
{
MyTreeView tree = new MyTreeViewO;
tree.DragO; tree.DropQ; tree.SerializeO; }
Комбинируя интерфейсы, вы не только упростите возможность агрегирования семантически связанных интерфейсов в единый интерфейс, но при необходимости сможете добавлять к новому "композитному" интерфейсу дополнительные методы.
Подведем итоги
Интерфейсы на С# обеспечивают разработку классов, у которых могут быть общие функции, но при этом они не являются частями одной и той же иерархии классов. Интерфейсы играют особую роль в разработке на С#, поскольку С# не поддерживает множественное наследование. Чтобы совместно использовать методы и свойства, классы могут реализовывать несколько интерфейсов. Операторы is и as позволяют определить, реализован ли конкретный интерфейс конкретным объектом, что помогает предотвратить ошибки, связанные с использованием членов интерфейса. Наконец, явное именование членов и сокрытие имен позволяют управлять реализацией интерфейса и избежать многих ошибок.