Понятие интерфейса тесно связано с понятием абстрактного класса.
Интерфейсы похожи на абстрактный класс, все члены которого являются
абстрактными.
Простой пример
В приведенном ниже коде определен интерфейс IScalable и класс TextObject, который реализует этот
интерфейс. Это означает, что класс содержит реализацию всех функций,
объявленных в интерфейсе:
public class DiagramObject { public DiagramObject() {} } interface IScalable { void ScaleX(float factor); void ScaleY(float factor); }
// Объект диаграммы, который также реализует интерфейс IScalable public class TextObject: DiagramObject, IScalable { public TextObject(string text) { this.text = text; } // реализуем ISclalable.ScaleX() public void ScaleX(float factor) { // ... } // реализуем ISclalable.ScaleY() public void ScaleY(float factor) { // ... } private string text; }
class Test { public static void Main() { TextObject text = new TextObject("Hello"); IScalable scalable = (IScalable) text; scalable.ScaleX(0.5F); scalable.ScaleY(0.5F); } }
Этот код описывает систему отображения диаграмм. Все объекты
наследуются от DiagramObject,
поэтому они содержат стандартный набор функций для отображения диаграмм
(они не показаны в этом примере). Некоторые из объектов могут быть
масштабированы, поэтому они реализуют интерфейс IScalable.
Имя интерфейса упоминается вместе с именем базового класса для TextObject. Это указывает на то,
что TextObject реализует данный
интерфейс. Это означает, что для каждой функции, содержащейся в
интерфейсе, TextObject должен
иметь соответствующую функцию. У членов интерфейса нет модификаторов
доступа; класс, который реализует интерфейс, должен сам устанавливать
уровень доступа члена интерфейса.
Когда объект реализует интерфейс, ссылка на интерфейс может быть
получена с помощью приведения типа ссылки к типу интерфейса (как это
было сделано в приведенном выше примере). Эту ссылку можно использовать,
чтобы вызвать функции интерфейса.
Этот пример мог быть сделан и с использованием абстрактных методов.
Можно было поместить методы ScaleX()
и ScaleY() в класс DiagramObject и сделать их
виртуальными. В разделе "Рекомендации по проектированию" (позже в этой
главе) обсуждаются случаи, когда следует использовать абстрактные
методы, а когда использовать интерфейс.
Работа с интерфейсами
Как правило заранее неизвестно, поддерживает ли объект какой-нибудь
интерфейс. Поэтому прежде чем выполнить приведение к типу интерфейса
необходимо убедиться, что объект реализует нужный интерфейс.
interface IScalable { void ScaleX(float factor); void ScaleY(float factor); } public class DiagramObject { public DiagramObject() {} } public class TextObject: DiagramObject, IScalable { public TextObject(string text) { this.text = text; } // реализуем ISclalable.ScaleX() public void ScaleX(float factor) { Console.WriteLine("ScaleX: {0} {1}", text, factor); // здесь делаем необходимые вычисления } // реализуем ISclalable.ScaleY() public void ScaleY(float factor) { Console.WriteLine("ScaleY: {0} {1}", text, factor); // здесь делаем необходимые вычисления } private string text; }
class Test { public static void Main() { DiagramObject[] dArray = new DiagramObject[100]; // инициализируем массив с помощью классов, наследующих DiagramObject. // некоторые из них реализуют IScalable. dArray[0] = new DiagramObject(); dArray[1] = new TextObject("Text Dude"); dArray[2] = new TextObject("Text Backup");
foreach (DiagramObject d in dArray) { if (d is IScalable) { IScalable scalable = (IScalable) d; scalable.ScaleX(0.1F); scalable.ScaleY(10.0F); } } } }
Прежде чем выполнить приведение типа, мы проверяем, поддерживается ли
этим типом данный интерфейс, чтобы удостовериться, что приведение будет
проведено успешно. Если тип поддерживает данный интерфейс, объект
приводится к типу интерфейса, и вызываются функции масштабирования.
К сожалению, этот код проверяет тип объекта на совместимость с типом
интерфейса дважды: в первый раз - при вызове оператора is и второй раз - непосредственно
перед приведением (эта проверка является частью процесса приведения).
Это расточительно, так как в данном случае приведение в любом случае
всегда будет успешным.
Один из способов решения этой проблемы мог бы состоять в том, чтобы
добавить в код обработку исключений. Но это не самая лучшая идея, потому
что это сделало бы код более сложным. Обработку исключений вообще нужно
использовать только в крайних случаях. Кроме того, вряд ли такой код
работал бы быстрее, так как использование исключений само по себе
несколько накладно.
Оператор as
В C# существует специальный оператор для решения подобных задач -
оператор as. С
использованием этого оператора можно переписать цикл следующим образом:
class Test { public static void Main() { DiagramObject[] dArray = new DiagramObject[100]; dArray[0] = new DiagramObject(); dArray[1] = new TextObject("Text Dude"); dArray[2] = new TextObject("Text Backup");
foreach (DiagramObject d in dArray) { IScalable scalable = d as IScalable; if (scalable != null) { scalable.ScaleX(0.1F); scalable.ScaleY(10.0F); } } } }
Оператор as проверяет
тип левого операнда, и если он может быть явно преобразован к типу
правого операнда, результатом действия оператора будет объект,
преобразованный к типу правого операнда. Если преобразование невозможно,
оператор as возвращает
пустой указатель.
И оператор is, и
оператор as можно
также использовать при работе с классами.
Интерфейсы и наследование
При преобразовании типа объекта к типу интерфейса, компилятор ищет в
иерархии наследования класс, для которого данный интерфейс указан в
списке базовых классов. Одного наличия в классе функций этого интерфейса
не достаточно:
interface IHelper { void HelpMeNow(); } public class Base: IHelper { public void HelpMeNow() { Console.WriteLine("Base.HelpMeNow()"); } } // Does not implement IHelper, though it has the right form. public class Derived: Base { public new void HelpMeNow() { Console.WriteLine("Derived.HelpMeNow()"); } }
class Test { public static void Main() { Derived der = new Derived(); der.HelpMeNow(); IHelper helper = (IHelper) der; helper.HelpMeNow(); } }
В результате будут выведены следующие строки: Derived.HelpMeNow() Base.HelpMeNow()
Во втором случае компилятор не вызовет функцию HelpMeNow() из класса Derived (хотя класс Derived содержит эту функцию),
поскольку Derived не реализует
интерфейс IHelper.
Рекомендации по
проектированию
Интерфейсы и абстрактные классы имеют в поведении много общего и могут
быть использованы в похожих ситуациях. Однако из-за особенностей их
работы существуют ситуации, в которых более уместно использование
интерфейсов или, наоборот, абстрактных классов. Приведем несколько
рекомендаций, помогающих определить, стоит ли пользоваться в данном
случае интерфейсом или абстрактным классом.
В первую очередь нужно проверить, можно ли описать объект должным
образом, используя отношение "является" ("is-a" relationship). Другими
словами, действительно ли описываемая возможность является объектом, и
могут ли производные классы считаться примерами этого объекта?
Другой способ состоит в том, чтобы перечислить, какие объекты
собираются использовать описываемую возможность. Если эта возможность
была бы полезна самым различным объектам, которые логически не связаны
друг с другом, для ее описания следует использовать интерфейс.
Примечание:
Поскольку в C# можно иметь только один базовый класс, это решение может
быть довольно важным. Если описываемая возможность реализована в виде
базового класса, пользователи могут быть очень разочарованы, если в
своей иерархии они уже имеют базовый класс и тем самым окажутся
неспособными использовать предлагаемую возможность.
Когда вы используете интерфейсы, помните, что не существует никакой
поддержки версий (versioning support) для интерфейсов. Если к
интерфейсу, который уже используется пользователями, будет добавлена
какая-нибудь функция, их код будет выдавать ошибки во время выполнения,
и их классы будут некорректно реализовывать интерфейс, пока не будут
сделаны соответствующие модификации.
interface IFoo { void ExecuteFoo(); } interface IBar { void ExecuteBar(); } class Tester: IFoo, IBar { public void ExecuteFoo() {} public void ExecuteBar() {} }
Этот код будет работать замечательно, если не совпадают названия
функций в интерфейсах. Но если пример немного изменить, появятся
некоторые проблемы:
// error interface IFoo { void Execute(); } interface IBar { void Execute(); } class Tester: IFoo, IBar { // IFoo or IBar implementation? public void Execute() {} }
Является ли Tester.Execute()
реализацией IFoo.Execute() или IBar.Execute()?
Эта ситуация неоднозначна, поэтому компилятор выдает сообщение об
ошибке. Если пользователь имеет доступ хотя бы к одному из этих
интерфейсов, он может изменить имя метода в интерфейсе. Однако это - не
лучшее решение: почему интерфейс IFoo
должен изменять имена своих методов только из-за того, что в интерфейсе IBar есть методы с такими же именами?
Кроме того, если IFoo и IBar созданы разными
производителями, имена их методов, как правило, изменить невозможно.
.NET Runtime и C# поддерживает методику, известную как "явная
реализация интерфейса" (explicit interface implementation). Эта методика
позволяет явно указывать, членом какого интерфейса является реализуемый
метод.
Явная реализация интерфейса
Чтобы определить, метод какого интерфейса реализуется в данном случае,
нужно указать имя интерфейса перед именем метода.
Перепишем предыдущий пример с использованием явной реализации
интерфейса:
class Test { public static void Main() { Tester tester = new Tester(); IFoo iFoo = (IFoo) tester; iFoo.Execute(); IBar iBar = (IBar) tester; iBar.Execute(); } }
При запуске программы на экран будет выведено: IFoo.Execute implementation IBar.Execute implementation
Это как раз то, что нам было нужно.
Но что появится на экране, если переписать класс Test таким образом?
class Test { public static void Main() { Tester tester = new Tester(); tester.Execute(); } }
Будет вызван метод IFoo.Execute()
или IBar.Execute()?
Ответ прост: не будет вызван ни один из этих методов. Для реализаций
методов IFoo.Execute() и IBar.Execute() в классе Tester не указаны модификаторы
доступа, поэтому эти методы являются закрытыми (private) и не могут быть
вызваны.
В этом случае такое поведение вызвано не тем, что для метода не был
указан модификатор public, а
тем, что использование модификаторов доступа вообще запрещено в явных
реализациях интерфейса. Поэтому единственным способом, который позволяет
обращаться к методам интерфейса, является приведение типа объекта к типу
интерфейса.
Чтобы использовать один из методов Execute(),
объявленных в интерфейсах, добавим в класс Tester соответствующий метод Execute(), который будет служить
оболочкой для нужного нам метода:
class Test { public static void Main() { Tester tester = new Tester(); tester.Execute(); } }
Теперь вызов метода Execute()
для экземпляра класса Tester
будет равносилен вызову Tester.IFoo.Execute().
Сокрытие реализации
(Implementation Hiding)
Могут возникнуть ситуации, в которых имеет смысл скрыть реализацию
интерфейса от пользователей класса. В некоторых случаях это может
сделать объект более легким в использовании. Например:
class DrawingSurface { } interface IRenderIcon { void DrawIcon(DrawingSurface surface, int x, int y); void DragIcon(DrawingSurface surface, int x, int y, int x2, int y2); void ResizeIcon(DrawingSurface surface, int xsize, int ysize); } class Employee: IRenderIcon { public Employee(int id, string name) { this.id = id; this.name = name; } void IRenderIcon.DrawIcon(DrawingSurface surface, int x, int y) { } void IRenderIcon.DragIcon(DrawingSurface surface, int x, int y, int x2, int y2) { } void IRenderIcon.ResizeIcon(DrawingSurface surface, int xsize, int ysize) { } int id; string name; }
Если бы интерфейс был реализован обычным образом, методы DrawIcon(), DragIcon() и ResizeIcon() были бы доступны как
часть класса Employee, что
могло бы запутать пользователей класса. Если интерфейс реализован с
помощью "явной реализации", к этим методам можно обращаться только через
интерфейс.
Интерфейсы, наследуемые от
интерфейсов
Интерфейсы можно комбинировать для получения новых интерфейсов.
Интерфейсы ISortable и ISerializable комбинируются друг с
другом и, кроме того, в полученный интерфейс можно добавлять новые
методы:
using System.Runtime.Serialization; using System; interface IComparableSerializable : IComparable, ISerializable { string GetStatusString(); }
Класс, который реализует интерфейс IComparableSerializable,
должен будет реализовать все методы IComparable
и ISerializable, а также метод GetStatusString(), который был
добавлен в IComparableSerializable.