Проблема неустойчивости базовых классов и контроль версии


Проблема несовместимости компонентов хорошо известна всем, кому доводилось программировать для Windows. Обычно она выступает в форме так называемого кошмара DLL (DLL Hell) — программа использует определенную версию DLL, a потом установка новой версии компонента нарушает работу программы. Почему? Причины могут быть разными, от очевидных (случайное исключение функции, использовавшейся в программе) до весьма нетривиальных (например, изменение типа возвращаемого значения у функции). В любом случае все сводится к вариациям на одну тему — при изменении открытого интерфейса кода, от которого зависит ваша программа, программа не может использовать новую версию вместо старой, а старая версия уже стерта. В большинстве объектно-ориентированных языков наследование сопряжено с потенциальной угрозой работоспособности вашей программы из-за несовместимости компонентов. Программисту остается лишь надеяться на то, что открытые и защищенные члены классов-предшественников в 1 иерархии наследования не будут изменяться, таким образом, что это нарушит ра-
ботоспособность их программ. Эта ситуация называется проблемой неустойчивости базовых классов. Наследование часто превращает наши программы в некое подобие карточного домика — попробуйте вытащить нижнюю карту, и все сооружение развалится.
Проблему неустойчивости базовых классов желательно рассмотреть на конкретном примере. Разместите приведенное ниже определение класса Payabl eEntity в отдель-ной^библиотеке и откомпилируйте его в сборку с именем PayableEntity Example командой Build (чтобы задать имя сборки, щелкните правой кнопкой мыши на имени проекта в окне решения, выберите в контекстном меню команду Properties и введите нужные значения в диалоговом окне). Если вы не используете архив с примерами, прилагаемый к книге, запомните, в каком каталоге был построен проект:
Public Mustlnherit Class PayableEntity
Private m_Name As String
Public Sub New(ByVal theName As String)
m_Name =theName
End Sub
Public Readonly Property TheName()As String Get
Return m_Name
End Get
End Property
Public MustOverride
Property TaxID()As
String End Class
После построения DLL закройте решение.
Допустим, вы решили включить в класс Employee новый способ получения адреса, зависящий от базового класса PayableEntity; при этом следует помнить, что класс будет использоваться только в откомпилированной форме. Для этого необходимо включить ссылку на сборку, содержащую этот проект (находится в подкаталоге \bin того каталога, в котором была построена DLL PayableEntityExample). Примерный код класса Empl oyee приведен ниже. Обратите внимание на строку, выделенную жирным шрифтом, в которой класс объявляется производным от абстрактного класса, определенного в сборке
PayableEntityExample.
Public Class Employee
' Пространство имен называется PayableEntityExample.
' поэтому полное имя класса записывается в виде
PayableEntityExample.PayableEntity! Inherits
PayableEntityExample.Employee
Private m_Name As String
Private m_Salary As Decimal
Private m_Address As String
Private m_TaxID As String
Private Const LIMIT As Decimal = 0.1D
Public Sub New(ByVal theName As String,
ByVal curSalary As Decimal,
ByVal TaxID As String)
MyBase.New(theName)
m_Name = theName
m_Salary = curSalary
m_TaxID = TaxID
End Sub
Public Property Address()As String
Get
Return m_Address
End Get
Set(ByVal Value As String)
m_Address = Value
End Set
End Property
Public Readonly Property Salary()As Decimal Get
Return m_Salary «
End Get
End Property
Public Overrides Property TaxIDO As String Get
Return m_TaxID
End Get
SetCByVal Value As String)
If Value.Length <> 11 Then
' См. главу 7 Else
m_TaxID = Value
End If
End Set
End Property
End Class
Процедура Sub Main выглядит так:
Sub Main()
Dim torn As New EmployeeC'Tom". 50000)
tom.Address ="901 Grayson"
Console.WriteCtom.TheName & "lives at " & tom.Address)
Console. ReadLine()
End Sub
Результат показан на  7. Программа работает именно так, как предполагалось.
Программа компилируется в исполняемый файл Versiomngl.exe, все идет прекрасно.
Теперь предположим, что класс PayableEntity был разработан независимой фирмой. Гениальные разработчики класса PayableEntity не желают почивать на лаврах! Заботясь о благе пользователей, они включают в свой класс объект с адресом и рассылают новый вариант DLL. Исходный текст они держат в секрете, но мы его приводим ниже. Изменения в конструкторе выделены жирным шрифтом:
Imports Microsoft.Vi sualBasic.Control Chars
Public Class PayableEntity
Private m_Name As String
Private m_Address As Address
Public Sub New(ByVal theName As String,ByVal theAddress As Address)
m_Name = theName
m_Address = theAddress
End Sub
Public Readonly Property TheName()As String Get
Return m_Name End Get
End Property
Public Readonly Property TheAddress() Get
Return
m_Address.DisplayAddress
End Get
End Property
End Class
Public Class Address
Private m_Address As String
Private m_City As String
Private m_State As String
Private m_Zip As String
Public Sub New(ByVal theAddress As String.ByVal theCity As String.
ByVal theState As String.ByVal theZip As String)
m_Address = theAddress
m_City = theCity
m_State = theState
m_Zip = theZip
End Sub
Public Function DisplayAddress() As String
Return m_Address & CrLf & m_City & "." & m_State _
&crLF & m_Zip
End Function
End Class
Перед вами пример редкостной халтуры. В процессе «усовершенствования» авторы умудрились потерять исходный конструктор класса PayableEntity! Конечно, такого быть не должно, но раньше подобные катастрофы все же случались. Старая DLL устанавливалась на жесткий диск пользователя (обычно в каталог Windows\System). Затем выходила новая версия, устанавливалась поверх старой, и вполне благополучная программа Versioningl переставала работать (а как ей работать, если изменился конструктор базового класса?).
Конечно, проектировщики базовых классов так поступать не должны, однако на практике бывало всякое. Но попробуйте воспроизвести этот пример в .NET, и произойдет настоящее чудо: ваша старая программа будет нормально работать, потому что она использует исходную версию Payabl eEnti ty из библиотеки, хранящейся в каталоге \bin решения Versioningl.
Решение проблемы несовместимости версий в .NET Framework в конечном счете основа-но на том, что ваш класс знает версию DLL, необходимую для его работы, и отказывается работать при отсутствии нужной версии. Успешная работа этого механизма зависит от особых свойств сборок (см. главу 13). Тем не менее.в описанной нами ситуации защита .NET Framework преодолевается простым копированием новой DLL на место старой.
Схема контроля версии в .NET позволяет разработчикам компонентов дополнять свои базовые классы новыми членами (хотя на практике делать этого не рекомендуется). Такая возможность сохраняется даже в том случае, если имена новых членов совпадают с именами членов, включенных вами в производный класс. Старый исполняемый файл, созданный на базе производного класса, продолжает работать, поскольку он не использует новую DLL.
Впрочем, это не совсем верно: он действительно продолжает работать — до тех пор, пока вы не откроете исходный текст приложения Versioningl в VS .NET, создадите ссылку на DLL PayableEntityExample и попробуете построить приложение Versioningl заново. Компилятор выдаст сообщение об ошибке:
C:\book to comp\chapter 5\Versioningl\Versioningl\Modu1el.vb(21):
No argument specified or non-optional parameter 'theAddress' of
'Public Sub New(theName As String,theAddress
As PayableEntityExample.Address)'.
Итак, как только вы загрузите старый исходный текст производного класса и создадите ссылку на новую DLL, вам не удастся откомпилировать программу до исправления той несовместимости, на которую вас обрекли разработчики базового класса.
Прежде чем завершить этот раздел, мы хотим разъяснить еще одно обстоятельство. Исключение конструктора из класса и замена его другим конструктором — весьма грубая и очевидная ошибка. Способен ли механизм контроля версии .NET спасти от других, менее тривиальных ошибок? Да, способен.
Рассмотрим самый распространенный (хотя довольно тривиальный) источник ошибок несовместимости при использовании наследования. Имеется производный класс Derived, зависящий от базового класса Parent. В класс Derived включается новый метод Parselt (в следующем примере он просто разделяет строку по словам и выводит каждое слово в отдельной строке):
Imports Microsoft.VisualBasic.ControlChars Module Modulel
SubMain()
Dim myDerived As New Oerived()
myDerived.DisplayIt 0
Console.ReadLine()
End Sub
End Module
Public Class Parent
Public Const MY STRING As String ="this is a test"
Public Overridable Sub Displaylt()
Console.WriteLine(MY_STRING)
End Sub
End Class
Public Class Derived Inherits Parent
Public Overrides Sub Displaylt()
Console.WriteLine(ParseIt(MyBase.MY_STRING))
End Sub
Public Function ParselUByVal aString As String)
Dim tokens() As String
' Разбить строку по пробелам tokens -
aString.Split(Chr(32))
Dim temp As String
' Объединить в одну строку, вставляя между словами
' комбинацию символов CR/LF
temp = Join(tokens.CrLf)
Return temp
End Function
End Class
End Module
Результат показан на  8.
Теперь представьте себе, что класс Parent распространяется не в виде исходных текстов, а в откомпилированной форме. Версия 2 класса Parent содержит собственную версию Parselt, которая широко используется в ее коде. В соответствии с принципом полиморфизма при хранении объекта типа Den ved в объектной переменной типа Parent вызовы Displaylt должны использовать метод Parselt класса Derived вместо метода Parselt базового класса. Однако здесь возникает маловероятная, но теоретически возможная проблема. В нашем сценарии код класса Parent, использующий свою версию функции Parselt, не знает, как функция Parselt реализована в классе Derived. Полиморфный вызов версии Parselt производного класса может нарушить какие-либо условия, необходимые для работы базового класса.
В этой ситуации средства контроля версии VB .NET тоже творят чудеса: код откомпилированного базового класса Parent продолжает использовать свою версию Parselt всегда, даже несмотря на то, что при хранении объектов Derived в переменных типа Parent полиморфизм привел бы к вызову неправильной версии метода. Как упоминалось в предыдущем примере, при открытии кода Derived в Visual Studio компилятор сообщает, что для устранения неоднозначности в объявление метода Parselt производного класса следует включить ключевое слово Override или Shadows.
 
Интерфейсы
Вероятно, вы убедились в том, что наследование занимает важное место в VB .NET, но для полноценного использования объектно-ориентированных средств VB .NET вам также придется освоить реализацию интерфейсов. Этой важной теме посвящены несколько ближайших разделов.
Прежде всего реализацию интерфейса можно рассматривать как контракт, обязательный для любого класса. Интерфейсы занимали важнейшее место в программировании СОМ, а также в реализации объектно-ориентированных средств в прежних версиях VB. При реализации интерфейса класс-обязуется предоставлять некоторую функциональность в соответствии с сигнатурами заголовков членов, отныне и во веки веков. В отличие от объектов при наследовании интерфейсы не связаны никакими взаимными зависимостями — каждая реализация интерфейса существует независимо от других.
В мире ООП часто приходится слышать высказывания типа «композиция предпочтитель-нее наследования» (то есть лучше использовать интерфейсы, а не наследование). Благодаря средствам контроля версии в .NET выбор между наследованием и композицией уже не играет столь принципиальной роли. Используйте наследование всюду, где это уместно, — там, где существует ярко выраженная связь типа «является частным случаем».
Реализация интерфейса предполагает, что ваш класс содержит методы со строго определенными сигнатурами. Эти методы могут быть пустыми, но они обязательно должны присутствовать.
Фактическая реализация методов не фиксируется; как было только что сказано, методы могут вообще ничего не делать. Поддержка интерфейса — всего лишь обязательство определить методы с заданными сигнатурами. Из этого простого факта вытекает множество замечательных следствий. Особый интерес представляют следующие:

  • «умный» компилятор может заменить вызовы функций быстрым поиском по таблице с последующей передачей управления;
  • с точки зрения программиста — разработчики могут вызывать методы вашего класса по сигнатуре, не опасаясь, что указанный метод не существует;
  • при наличии подобных обязательств компилятор может использовать полиморфизм так же, как это делается при наследовании.

При вызове метода, реализованного в составе интерфейса, компилятор .NET еще на стадии компиляции может вычислить вызываемый метод на основании сигнатуры и типа класса (это называется ранним связыванием). Этот факт объясняет возможность использования полиморфизма при реализации интерфейсов.
А теперь подумайте, что произойдет, если:

  • вы не будете связаны обязательством на поддержку метода с заданной сигнатурой в результате реализации интерфейса;
  • ваш класс не входит в иерархию наследования, в которой VB .NET сможет найти метод с нужной сигнатурой.

Происходит следующее: в режиме жесткой проверки типов (Option StrictOn) программа вообще не будет компилироваться. Если этот режим отключить, умный компилятор .NET поймет, что вызов метода класса не удастся заменить в откомпилированном коде неким подобием простого вызова функции. Таким образом, компилятору придется сгенерировать значительно больший объем кода. Фактически он должен во время выполнения программы вежливо спросить у объекта, поддерживает ли он метод с указанной сигнатурой, и если поддерживает — не будет ли он возражать против его вызова? Подобное решение обладает двумя характерными особенностями, из-за которых оно работает значительно медленнее и гораздо чаще приводит к возникновению ошибок:

  1. Необходимо предусмотреть обработку ошибок на случай непредвиденных ситуаций.
  2. Поскольку компилятор на стадии компиляции не может определить, по какому адресу следует передать управление в блоке памяти, занимаемом объектом, ему приходится полагаться на косвенные методы передачи управления на стадии выполнения.

Описанный процесс называется поздним связыванием (late binding). Он не только значительно уступает раннему связыванию по скорости, но и вообще не разрешен при включенном режиме Option Strict за исключением позднего связывания, основанного на применении рефлексии.
 
Механика реализации интерфейса
Во многих компаниях, занимающихся программированием (хотя бы в Microsoft), существует должность ведущего программиста или ведущего специалиста по тестированию. Предположим, вы решили расширить систему учета кадров и включить в нее эти новые должности с особыми свойствами — скажем, наличием фонда материального поощрения для особо отличившихся подчиненных.
В описанной выше иерархии классов VB .NET определить новый класс «ведущий специалист» не удастся, поскольку классы Programmer и Tester уже являются производными от класса Empl oyee, а множественное наследование в .NET не поддерживается. Перед нами идеальный пример ситуации, когда вам на помощь приходят интерфейсы.
По общепринятым правилам имена интерфейсов в .NET начинаются с прописной бук-вы «I», поэтому в следующем примере интерфейс называется ILead.
Прежде всего интерфейс необходимо определить. В отличие от VB6, где интерфейс был обычным классом, в VB .NET появилось специальное ключевое слово Interface. Предположим, наши «ведущие» должны оценивать своих подчиненных и тратить средства из фонда материального поощрения. Определение интерфейса выглядит так:
Public Interface ILead
Sub SpendMoraleFund(ByVal amount As Decimal)
Function Rate(ByVal aPerson As Employee) As String
Property MyTeam() As Empl oyee ()
Property MoraleFuod() As Decimal End Interface
Обратите внимание — в определении интерфейса отсутствуют модификаторы уровня доступа Publiс и Private. Разрешены только объявления Sub, Function и Property с ключевыми словами Overloads и Default. Как видите, определение интерфейса выглядит просто. Любой класс, реализующий интерфейс ILead, обязуется содержать:

  • процедуру с параметром типа Decimal;
  • функцию, которая получает объект Empl oyee и возвращает строку;
  • свойство, доступное для чтения и записи, возвращающее массив объектов Employee;
  • свойство, доступное для чтения и записи, возвращающее значение типа Decimа1.

Как будет показано ниже, имена методов реализации несущественны — главное, чтобы методы имели заданную сигнатуру.
Чтобы реализовать интерфейс в классе, прежде всего убедитесь в том, что он сам или ссылка на него входит в проект. Далее за именем класса и командой Inherits в программу включается строка с ключевым словом Implements, за которым следует имя интерфейса. Пример:
Public Class LeadProgrammer
Inherits Programmer
Implements Head
End Class
Имя Head подчеркивается синей волнистой чертой, свидетельствующей о возникшей проблеме. Тем самым компилятор настаивает на выполнении обязательств по реализации интерфейса хотя бы пустыми методами.
Как это сделать? В отличие от ранних версий VB, где члены классов, входящие в реализацию интерфейса, обозначались особой формой сигнатуры, в VB .NET используется более наглядный синтаксис. В следующем фрагменте соответствующая строка выделена жирным шрифтом.
Public Function Rate(ByVal aPerson As Employee) As String _
Implements ILead.Rate End Function
Конечно, имена членов интерфейса обычно совпадают с именами методов, их реализующих, но это не обязательно. Например, следующий фрагмент вполне допустим.
Public Property OurMoraleFund() As Decimal Implements
Head.MoraleFund Get
Return m_Moral e Fund
End Get
Set(ByVal Value As Decimal)
m_MoraleFund =Value
End Set
End Property
Главное, чтобы типы параметров и возвращаемого значения соответствовали сигнатуре данного члена интерфейса. При объявлении метода могут использоваться любые допустимые модификаторы, не мешающие выполнению контракта, — Overloads, Overrides, Overridable, Public, Private, Protected, Friend, Protected Friend, MustOverride, Default и Static. Запрещается только использовать атрибут Shared, поскольку члены интерфейса должны принадлежать конкретному экземпляру, а не классу в целом.
Если в реализации интерфейса используется член класса с модификатором Pri vate, обращения к этому члену возможны только через переменную, объявленную с типом данного интерфейса. В отличие от предыдущих версий VB в остальных случаях к членам интерфейса всегда можно обращаться через объекты класса. Теперь вам не придется присваивать их промежуточным интерфейсным переменным. Пример:
Dim tom As New LeadProgrammer("Tom",65000) tom.SpendMoraleFund(500)
Однако в обратных преобразованиях приходится использовать функцию СТуре:
Dim tom As New LeadProgrammer("Tom". 65000)
Dim aLead As ILead.aName As String
aLead = tom
aName = Ctype(aLead. Programmer).TheName 'OK
Следующая строка недопустима:
aName =tom.TheName ' ЗАПРЕЩЕНО!
Ниже перечислены общие правила преобразования между типом объекта и интерфейсом, им реализуемым.

  • Переменную, объявленную с типом класса, всегда можно присвоить переменной, объявленной с типом любого из интерфейсов, реализуемых классом. В частности, если метод получает в качестве параметра переменную интерфейсного типа, ему можно передать переменную любого из типов, реализующих этот интерфейс (данное правило напоминает основной принцип наследования, в соответствии с которым производные типы всегда могут использоваться вместо базовых). Запомните следующее правило:
  • При переходе от переменной типа интерфейса к переменной типа, реализующего интерфейс, необходимо использовать функцию СТуре.

Чтобы определить, реализует ли объект некоторый интерфейс, воспользуйтесь ключевым словом TypeOf в сочетании с Is. Пример:
Dim torn As New LeadProgrammer("tom". 50000)
Console.WriteLine((TypeOf (tom) Is Head))
Вторая строка выводит значение True.
Один метод может реализовывать несколько функций, определенных в одном интерфейсе:
Public Sub itsOK Implements
Interface1.Ml.Interfacel.M2,Interfacel.M3
Ниже приведена полная версия класса LeadProgrammer. Конечно, реализация методов интерфейса выглядит несколько условно, однако опадает представление о том, что можно сделать при реализации интерфейса:
Public Class LeadProgrammer
Inherits Programmer Implements Head
Private m_MoraleFund As Decimal
Private m_MyTeam As Employee()
Public Function Rate(ByVal aPerson As Employee) As String _
Implements Head.Rate
Return aPerson.TheName & "rating to be done"
End Function
Public Property MyTeam() As Employee()
Implements ILead.MyTeam
Get
Return m_MyTeam
End Get
SeUByVal Value As Employee()) X.
m_MyTeam = Value
End Set End Property
Public Sub SpendMoraleFund(ByVal
amount As Decimal)_
Implements ILead.SpendMocaleFund
' Израсходовать средства из фонда мат. поощрения
Console.WriteLine("Spent " & amount.ToString())
End Sub
Public Property OurMoraleFund()As Decimal
Implements ILead.MoraleFund
Get
Return m_MoraleFund
End Get
SettByVal Value As Decimal)
m_MoraleFund = Value
End Set End Property
Public Sub New(ByVal theName As String. ByVal curSalary As Decimal)
MyBase.New(theName. curSalary)
End Sub
End Class
 
Нетривиальное применение интерфейсов
Интерфейсы также могут объявляться производными от других интерфейсов. В этом случае интерфейс просто дополняется новыми членами. Предположим, в нашей системе учета кадров ведущим программистам предоставлено право разрешать модернизацию компьютеров своих подчиненных. В программе это моделируется методом 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
ICollection
Интерфейс ICollection определяется производным от IEnumerable; он дополняет этот интерфейс тремя свойствами, доступными только для чтения, и одним новым методом. Класс ICollection редко реализуется самостоятельно. Как правило, он образует базу для интерфейсов IList и IDictionary (см. ниже). Члены этого интерфейса перечислены в табл. 5.2.
Таблица 5.2. Члены интерфейса ICollection

Интерфейс ICollection реализуется классом System.Collections.CollectionBase.
 
IList
Интерфейс IList обеспечивает выборку элементов коллекции по индексу. Разумеется, поскольку этот интерфейс определяется производным от I Enumerable, при этом сохраняется возможность использования For-Each. Иерархия наследования IList выглядит следующим образом:
IEnumerable->ICollection->IList
Интерфейс IList относительно сложен — он состоит из трех свойств и семи методов (табл. 5.3). Напомним, что некоторые из методов могут быть пустыми, если в каком-то конкретном классе их реализация не имеет смысла.
Таблица 5.3. Члены интерфейса IList

Интерфейс IList реализуется классом System.Collections.CollectionBase.
 
IDictionary
Интерфейс IDictionary представляет коллекцию, в которой доступ к данным осуществляется по ключу — как в хэш-таблицах, описанных в предыдущей главе. Более того, класс хэш-таблиц в числе прочих реализует интерфейсы IDictionary, ICollection, Enumerable и ICloneable!
Хотя интерфейс IDictionary объявляется производным от Enumerable и переход к следующему элементу может осуществляться методом MoveNext, обычно такая возможность не используется — коллекции, реализующие IDictionary, ориентируются в первую очередь на обращение по ключу, а не на последовательный перебор элементов. По этой причине интерфейс IDictionary зависит от интерфейса IDic-tionaryEnumerator, который расширяет Enumerator и дополняет его тремя новыми свойствами:

  • Entry: возвращает пару «ключ/значение» для текущего элемента словаря.
  • Key: возвращает текущий ключ.
  • Value: возвращает ссылку на текущее значение.

В .NET Framework входит класс DictionaryBase. Определяя класс производным от DictionaryBase, вы получаете в свое распоряжение всю функциональность интерфейса IDictionary.
Члены класса IDictionary перечислены в табл. 5.4.
Поскольку ключи в ассоциативных коллекциях должны быть уникальными, при реали-зации большинства методов необходимо сначала проверить, не был ли заданный ключ использован ранее. Свойство Keys возвращает объект, реализующий ICollection; уникальность ключа проверяется методом,Соп1а1п5 интерфейса ICollection.
Таблица 5.4. Члены интерфейса IDictionary

 
IComparable
Предположим, коллекцию объектов Employee потребовалось отсортировать по заработной плате. Конечно, операцию сортировки было бы удобно реализовать непосредственно в классе Emplоуее, чтобы сортировка простого или динамического массива объектов этого класса выполнялась так же просто, как сортировка строковых массивов. Оказывается, порядок сортировки элементов, используемый методом Sort классов Array и ArrayList, определяется интерфейсом IComparable (строковые массивы интерфейс IComparabl e сортирует в порядке ASCII-кодов). Интерфейс состоит из единственного метода CompareTo: Function CompareTo(ByValobj As Object) As Integer Метод возвращает следующие значения:

  • отрицательное число, если текущий экземпляр меньше заданного объекта;
  • ноль, если текущий экземпляр равен заданному объекту;
  • положительное число, если текущий экземпляр больше заданного объекта.

Следующая версия класса Employee реализует интерфейсы lEnumerable и IComparable и сортирует массив по убыванию заработной платы:
Public Class Employee
Implements IComparable
Private m_Name As String
Private m_Salary As Decimal
Private Const LIMIT As Decimal =0.10
Public Sub New(ByVal theName As String,ByVal curSalary As Decimal)
m_Name = theName m_Salary = curSalary
End Sub
Public Function CompareTo(ByVal anEmployee As Object) As Integer _
Implements IComparable.CompareTo
If CType(anEmployee,Employee).Salany < Me.Salary Then Return -1
El self CTypetanEmployee.Employee).Salary = Me.Salary Then
Return 0
Elself CTypeCanEmployee,Employee).Salary > Me.Salary Then
Return 1
End If
End Function
Public Readonly Property TheName() As String Get
Return m_Name End Get End Property
Public Readonly Property Salary() As Decimal Get
Return MyClass.m_Salary
End Get End Property
Public Overridable Overloads Sub RaiseSalary(ByVal Percent As Decimal)
If Percent > LIMIT Then
' Операция запрещена - необходим пароль
Console.WriteLine("NEED PASSWORD TO RAISE SALARY MORE " & _
"THAN LIMIT!!!!")
Else
m_Salary =(1 + Percent) * m_Salary
End If
End Sub
Public Overridable Overloads Sub RaiseSalary(ByVal Percent As Decimal._
ByVal Password As String) If Password = "special" Then
m_Salary =(1 + Percent) * m_Salary
End If
End Sub
End Class
Для тестирования новой версии класса можно воспользоваться следующей программой:
Sub Main()
Dim torn As New Employee("Tom". 50000)
Dim sally'As New Employee("Sally", 60000)
Dim joe As New Employee("Joe", 20000)
Dim gary As New Employее("Gary", 1)
Dim theEmployees() As Employee = _
{torn, sally, joe. gary}
Array.Sort(theEmployees)
' Порядок сортировки определяется CompareTo!
Dim aEmployee As Employee
For Each aEmployee In theEmployees
Console.WriteLine(aEmployee.TheName & "has yearly salary $"
& FormatNumbertaEmployee.Salary)) Next
Console.ReadLine()
End Sub
Результат показан на  9.
 
Интерфейс IComparer
.NET Framework позволяет выполнять сортировку по нескольким критериям. Например, чтобы упорядочить массив работников сначала по заработной плате, а затем по имени (в группах с одинаковой зарплатой) следует реализовать интерфейс IComparer, содержащий единственный метод СотрагеТо. При этом вы сможете воспользоваться одной из перегруженных версий Array. Sort (или ArrayList. Sort), которая имеет следующую сигнатуру:
Public Shared Sub Sort(ByVal array As Array. ByVal comparer As IComparer)
Обычно в программе создается отдельный класс, реализующий IComparer, и экземпляр этого класса передается методу Sort. Пример такого класса приведен ниже. Обратите внимание на выделенную строку — в ней имена работников передаются в виде строк методу Compare класса String:
Public Class SortByName
Implements IComparer
Public Function CompareTo(ByVal firstEmp As Object.ByVal
secondEmp=As Object) As Integer Implements IComparer.Compare
Dim temp1 As Employee = CType(firstEmp,Employee)
Dim temp2 As Employee = CType(secondEmp.Employee)
Return
String.Compare(templ.TheName. temp2.TheName)
End Function
End Class
Пример процедуры Sub Main с использованием этого класса:
SubMain()
Dim torn As New Employee("Tom", 50000)
Dim sally As New Employee("Sally". 60000)
Dim sam As New Employee("Sam". 60000)
Dim ted As New Employee("Ted". 50000)
Dim theEmployees() As Employee = _
{torn.sally,sam.ted}
Array.Sort(theEmployees)
Dim SortingByName As SortByName = New SortByName()
Array.Sort(theEmployees,SortingByName)
Dim aEmployee As Employee
For Each aEmployee In theEmployees
Console.WriteLine(aEmployee.TheName & "has yearly salary $" &
FormatNumberCaEmployee.Salary))
Next
Console. ReadLine() End Sub .
Результат показан на  10,
В программе можно определить несколько классов, реализующих IComparer. Их последовательное применение позволяет выполнять многоуровневую сортировку произвольной глубины.

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г. Яндекс.Метрика