Введение в С# для программистов (A programmer's Introduction to C#)

Автор - Eric Gunnerson
перевод - Трубецкой А.


На главную
Содержание
Глава 9
Глава 11

Глава 10: Интерфейсы

Обзор

Понятие интерфейса тесно связано с понятием абстрактного класса. Интерфейсы похожи на абстрактный класс, все члены которого являются абстрактными.


Простой пример

В приведенном ниже коде определен интерфейс 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) для интерфейсов. Если к интерфейсу, который уже используется пользователями, будет добавлена какая-нибудь функция, их код будет выдавать ошибки во время выполнения, и их классы будут некорректно реализовывать интерфейс, пока не будут сделаны соответствующие модификации.


Множественная реализация (Multiple Implementation)

Класс может реализовывать несколько интерфейсов:
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). Эта методика позволяет явно указывать, членом какого интерфейса является реализуемый метод.


Явная реализация интерфейса

Чтобы определить, метод какого интерфейса реализуется в данном случае, нужно указать имя интерфейса перед именем метода.

Перепишем предыдущий пример с использованием явной реализации интерфейса:
interface IFoo {
void Execute();
}
interface IBar {
void Execute();
}
class Tester: IFoo, IBar {
void IFoo.Execute() {
Console.WriteLine("IFoo.Execute implementation");
}
void IBar.Execute() {
Console.WriteLine("IBar.Execute 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(), который будет служить оболочкой для нужного нам метода:
interface IFoo {
void Execute();
}
interface IBar {
void Execute();
}
class Tester: IFoo, IBar {
void IFoo.Execute() {
Console.WriteLine("IFoo.Execute implementation");
}
void IBar.Execute() {
Console.WriteLine("IBar.Execute implementation");
}
public void Execute() {
((IFoo)this).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.


На главную
Глава 9
Глава 11


Rambler's Top100
Хостинг от uCoz