Нетривиальное
применение интерфейсов
Интерфейсы
также могут объявляться производными от других интерфейсов. В этом случае интерфейс
просто дополняется новыми членами. Предположим, в нашей системе учета кадров
ведущим программистам предоставлено право разрешать модернизацию компьютеров
своих подчиненных. В программе это моделируется методом UpGradeHardware:
Public Interface
ILeadProgrammer
Inherits Head
Public Function UpGradeHardware(aPerson As Programmer)
End Interface
В этом случае
реализация ILeadProgrammer требует дополнительного выполнения контракта интерфейса
Head.
В отличие
от классов, которые могут наследовать лишь от одного базового класса, интерфейс
может быть объявлен производным от нескольких интерфейсов:
Public Interface
ILeadProgrammer
Inherits Head.Inherits
ICodeGuru
Public Function UpGradeHardware(aPerson As Programmer)
End Interface
Поскольку
интерфейс может наследовать от нескольких интерфейсов, реальна ситуация, при
которой в нем потребуется определить два одноименных метода, принадлежащих к
разным интерфейсам, — например, если интерфейсы Head и ICodeGuru содержат методы
с именем SpendMoraleFund. В этом случае вы не сможете обратиться к одному из
этих методов через переменную типа, реализующего такой интерфейс:
Dim tom As New LeadProgrammer("Tom", 65000)
tom.SpendMoraleFund(500)
Интерфейс
должен указываться явно, как в следующем фрагменте:
Dim tom As New
LeadProgrammer("Tom", 65000)
Dim aCodeGuru
As ICodeGuru
aCodeGuru =
tom
aCodeGuru.SpendMoraleFund(500)
Выбор
между интерфейсами и наследованием
Хотя на первый
взгляд интерфейсы чем-то напоминают базовые классы, от этой аналогии больше
вреда, чем пользы. Абстрактный класс может содержать реализованные методы, а
в интерфейсе они недопустимы. Абстрактные базовые классы создаются только в
результате тщательного анализа функциональности с выделением самого примитивного
общего предка, и ни по какой другой причине.
Интерфейсы
существуют вне иерархии наследования, и в этом их достоинство. Вы теряете возможность
автоматического использования существующего кода, но взамен приобретаете свободу
выбора собственной реализации контракта. Интерфейсы используются в тех случаях,
когда вы хотите показать, что поведение класса должно соответствовать определенным
правилам, но фактическая реализация этих правил остается на усмотрение класса.
В .NET структуры не могут наследовать ни от чего, кроме Object, но они могут
реализовывать интерфейсы. Наконец, в .NET интерфейсы становятся единственным
решением в ситуации, когда два класса обладают сходным поведением, но не
имеют общего предка, частными случаями которого они бы являлись.
Важнейшие
интерфейсы .NET Framework
Описать все
интерфейсы .NET Framework на нескольких страницах невозможно, но хотя бы получить
некоторое представление о них вполне реально. Интерфейсы ICloneable и IDisposable
обладают особой декларативной функцией — реализуя их, вы тем самым заявляете,
что ваш класс обладает некой стандартной функциональностью, присутствующей во
многих классах.
Далее в этой
главе рассматриваются базовые интерфейсы для построения специализированных коллекций.
Если вы помните, с какими трудностями была связана
реализация
циклов For-Each в VB6, они станут для вас настоящим подарком!
Как было
показано в разделе «MemberWiseClone», клонирование объекта, содержащего
внутренние объекты, вызывает немало проблем. Разработчики .NET дают вам возможность
сообщить о том, что данная возможность реализована в вашем классе. Для этой
цели используется декларативный интерфейс ICloneable, состоящий из единственной
функции Clone:
Public Interface
ICloneable
Function Clone() As Object
End Interface
Этот интерфейс
(а следовательно, и метод Clone) реализуется в том случае, если вы хотите предоставить
пользователям своего класса средства для клонирования экземпляров. Далее вы
сами выбираете фактическую реализацию метода Clone — не исключено, что она будет
сводиться к простому вызову MemberWiseClone. Как было сказано выше, MemberWiseCl
one нормально клонирует экземпляры, поля которых относятся к структурному типу
или являются неизменяемыми (такие, как String). Например, в классе Empl oyee
клонирование экземпляров может осуществляться методом Clone, поскольку все поля
представляют собой либо строки, либо значения структурных типов. Таким образом,
реализация IC1 опеаЫ е,для класса Empl oyee может выглядеть так:
Public Class Employee Implements ICloneable
Public Function Clone() As Object _
Implements ICloneable.Clone
Return CType(Me.MemberwiseClone, Employee)
End Function
' И т.д.
End Class
В классах,
содержащих внутренние объекты, реализация метода Clone потребует значительно
больших усилий (хотя в главе 9 описан прием, позволяющий достаточно просто решить
эту задачу в большинстве случаев). Так, в приведенном выше классе EmbeddedObject
необходимо клонировать внутренний массив, не ограничиваясь простым копированием.
Как это сделать?
Очень просто. Поскольку класс Array реализует интерфейс ICloneable, он должен
содержать метод для клонирования массивов. Остается лишь вызвать этот метод
в нужном месте. Ниже приведена версия класса Ет-beddedObjects с реализацией
ICloneabl e (ключевые строки выделены жирным шрифтом):
Public Class EmbeddedObjects Implements
ICloneable Private m_Ma() As String
Public Sub New(ByVal
anArray() As String)
m_Data = anArray
End Sub
Public Function Clone() As Object Implements
ICloneable.Clone
Dim temp()As
String
temp = m_Data.Clone
' Клонировать массив
Return New EmbeddedObjects(temp)
End Function
Public Sub DisplayData()
Dim temp As
String
For Each temp In m_Data
Console.WriteLine(temp)
Next End
Sub Public
Sub ChangeDataCByVal
newData As String)
m_Data(0) = newData
End Sub
End Class
Список
классов .NET Framework, реализующих интерфейс ШопеаЫе (а следовательно, поддерживающих
метод Clone), приведен в описании интерфейса ШопеаЫе в электронной документации.
Выше уже
упоминалось о том, что метод Finalize не обеспечивает надежного освобождения
ресурсов, не находящихся под управлением сборщика мусора. В программировании
.NET у этой задачи существует общепринятое решение — класс реализует интерфейс
IDisposable с единственным методом Dispose, освобождающим занятые ресурсы:
Public Interface
IDisposable
Sub Dispose()
End Interface
Итак, запомните
следующее правило:
Если ваш
класс использует другой класс, реализующий IDisposable, то в конце работы с
ним необходимо вызвать метод Dispose.
Как будет
показано в главе 8, метод Dispose должен вызываться в каждом графическом приложении,
зависящем от базового класса Component, поскольку это необходимо для освобождения
графических контекстов, используемых всеми компонентами.
Список
классов .NET Framework, реализующих интерфейс IDisposabe (следовательно, поддерживающих
метод Dispose, который должен вызываться в приложениях), приведен в описании
интерфейса IDisposable в электронной документации.
Коллекцией
(collection) называется объект, предназначенный для хранения других объектов.
Коллекция содержит методы для включения и удаления внутренних объектов, а также
обращения к ним в разных вариантах — от простейшей индексации, как при работе
с массивами, до сложной выборки по ключу, как в классе Hashtable, представленном
в предыдущей главе. .NET Framework содержит немало полезных классов коллекций.
Расширение этих классов посредством наследования позволяет строить специализированные
коллекции, безопасные по отношению к типам. И все же при нетривиальном использовании
встроенных классов коллекций необходимо знать, какие интерфейсы в них реализованы.
Несколько ближайших разделов посвящены стандартным интерфейсам коллекций.
For
Each и интерфейс lEnumerable
Поддержка
For-Each в классах VB6 была недостаточно интуитивной, а ее синтаксис воспринимался
как нечто совершенно инородное (мы упоминали об этом в главе 1). В VB .NET существуют
два способа организации поддержки For-Each в классах коллекций. Первый метод
уже был продемонстрирован выше: новый класс определяется производным от класса
с поддержкой For-Each и автоматически наследует его функциональность. В частности,
этот способ применялся для класса Empl oyees, производного от класса System.
Collections. CollectionBase.
Второй способ,
основанный на самостоятельной реализации интерфейса IEnumerable, обеспечивает
максимальную гибкость. Определение интерфейса выглядит следующим образом:
Public Interface
lEnumerable
Function GetEnumerator() As Enumerator
End Interface
При реализации
lEnumerable класс реализует метод GetEnumerator, который возвращает объект IEnumerator,
обеспечивающий возможность перебора в классе. Метод перехода к следующему элементу
коллекции определяется именно в интерфейсе IEnumerator, который определяется
следующим образом:
Public Interface
lEnumerator
Readonly Property
Current As Object
Function MoveNext()
As Boolean
Sub Reset ()
End Interface
В цикле For-Each
перебор ведется только в одном направлении, а элементы доступны только для чтения.
Этот принцип абстрагирован в интерфейсе lEnumerator — в интерфейсе присутствует
метод для перехода к следующему элементу, но нет методов для изменения данных.
Кроме того, в интерфейс IEnumerator должен входить обязательный метод для перехода
в начало коллекции. Обычно этот интерфейс реализуется способом включения (containment):
в коллекцию внедряется специальный класс, которому перепоручается выполнение
трех интерфейсных методов (один из lEnumerable и два из IEnumerator).
Ниже приведен
пример коллекции Employees, построенной «на пустом месте». Конечно,
класс получается более сложным, чем при простом наследовании от System. Collections.
CollectionBase, но зато он обладает гораздо большими возможностями. Например,
вместо последовательного возвращения объектов Employee можно использовать сортировку
по произвольному критерию:
1 Public Class
Employees
2 Implements
IEnumerable.IEnumerator
3 Private m_Employees()
As Employee
4 Private m_index
As Integer = -1
5 Private m_Count
As Integer = 0
6 Public Function
GetEnumerator() As lEnumerator _
7 Implements
lEnumerable.GetEnumerator
8 Return Me
9 End Function
10 Public Readonly
Property Current() As Object _
11 Implements
IEnumerator.Current
12 Get
13 Return m_Employees(m_Index)
14 End Get
15 End Property
16 Public Function
MoveNext() As Boolean _
17 Implements
lEnumerator.MoveNext
18 If m_Index
< m_Count Then
19 m_Index +=
1
20 Return True
21 Else
22 Return False
23 End If
24 End Function
25 Public Sub
Reset() Implements IEnumerator.Reset
26 m_Index =
0
27 End Sub
28 Public Sub
New(ByVal theEmployees() As Employee)
29 If theEmployees
Is Nothing Then
30 MsgBox("No
items in the collection")
31 ' Инициировать
исключение - см. главу 7
32 ' Throw New
ApplicationException()
33 Else
34 m_Count =
theEmployees.Length - 1
35 m_Employees
= theEmployees
36 End If
37 End Sub
38 End Class
Строка 2
сообщает о том, что класс реализует два основных интерфейса, используемых при
работе с коллекциями. Для этого необходимо реализовать функцию, которая возвращает
объект lEnumerator. Как видно из строк 6-9, мы просто возвращаем текущий объект
Me. Впрочем, для этого класс должен содержать реализации членов IEnumerable;
они определяются в строках 10-27.
В
приведенной выше программе имеется одна тонкость, которая не имеет никакого
отношения к интерфейсам, а скорее связана со спецификой класса. В строке 4 переменная
mjndex инициализируется значением -1, что дает нам доступ к 0 элементу массива,
в результате чего первый вызов MoveNext предоставляет доступ к элементу массива
с индексом 0 (попробуйте инициализировать mjndex значением 0, и вы убедитесь,
что при этом теряется первый элемент массива).
Ниже приведена
небольшая тестовая программа. Предполагается, что Publiс-класс Employee входит
в решение:
Sub Main()
Dim torn As
New Emplpyee("Tom". 50000)
Dim sally As
New Employee("Sally". 60000)
Dim joe As New
Employee("Joe", 10000)
Dim theEmployees(l)
As Employee
theEmployees(0)
= torn
theEmployees(1)
= sally
Dim myEmployees
As New Employees(theEmployees)
Dim aEmployee
As Employee
For Each aEmployee
In myEmployees
Console.WriteLine(aEmployee.TheName)
Next
Console.ReadLine()
End Sub