Нетривиальное применение интерфейсов

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

  • ICloneable: в классе реализуется метод Clone, обеспечивающий глубокое копирование.
  • IDisposable: класс потребляет ресурсы, которые не могут автоматически освобождаться сборщиком мусора.
  • Далее в этой главе рассматриваются базовые интерфейсы для построения специализированных коллекций. Если вы помните, с какими трудностями была связана

    реализация циклов For-Each в VB6, они станут для вас настоящим подарком!

     

    ICloneable

    Как было показано в разделе «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), приведен в описании интерфейса ШопеаЫе в электронной документации.

     

    IDisposable

    Выше уже упоминалось о том, что метод 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