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