В предыдущей главе кратко упоминались два столпа объектно-ориентированного программирования
в VB .NET: наследование реализации, обеспечивающее возможность многократного
использования кода, и наследование интерфейсов, то есть «контракт»,
которому должны соответствовать некоторые аспекты поведения класса. Обе разновидности
наследования будут подробно рассмотрены в этой главе.
Для начала
мы покажем, как в VB .NET организовано наследование реализации. К сожалению,
при изучении этой темы никак нельзя ограничиться поверхностным знакомством.
Дело в том, что непродуманные действия программиста при наследовании реализации
приводят к весьма неприятным последствиям, поэтому в этой главе мы потратим
довольно много времени, показывая, как избежать этих опасностей за счет тщательного
проектирования иерархии наследования.
После описания
механики и основных принципов наследования реализации мы во всех подробностях
изучим класс Object, являющийся предком всех объектов .NET. Завершая описание
наследования реализации, мы покажем, как в .NET решается проблема неустойчивости
базовых классов, вызывающая немало хлопот при наследовании реализации в
других объектно-ориентированных языках — таких, как Java и С++. Не пугайтесь
термина «проблема неустойчивости базовых классов»; речь идет всего
лишь о том, что непродуманные изменения базового класса могут нарушить работу
производных классов.
С
этого момента под термином «наследование» будет пониматься наследование
реа-лизации. Если речь пойдет о программном коде, использующем наследование
интерфейсов, мы будем употреблять термин «реализация интерфейсов».
На первый взгляд терминология кажется крайне запутанной, но большинство программистов
быстро привыкают к этим сокращенным обозначениям.
От наследования мы перейдем к реализации интерфейсов в VB .NET. В завершение этой главы вы узнаете, как использовать важнейшие интерфейсы .NET Framework IComparable, ICloneable и IDisposable.
Программисты
с опытом использования интерфейсов BVB5 и VB6 будут приятно удивлены тем, насколько
понятнее выглядят программы при реализации интерфейсов в VB .NET. В синтаксисе
произошли заметные изменения к лучшему.
Хотя наследование
не является панацеей ООП и во многих ситуациях лучше воспользоваться интерфейсами,
не стоит полагать, что без наследования можно как-нибудь обойтись. Наследование
— замечательное средство, способное сэкономить немало времени и сил... если
им правильно пользоваться. Критерий правильного использования прост: не используйте
наследование, если у вас нет абсолютной уверенности в существовании логической
связи типа «является частным случаем».
Класс А объявляется производным от класса В только в том случае, если вы точно
знаете, что сейчас и в сколь угодно отдаленном будущем объект А может использоваться
вместо объекта В и это не вызовет никаких проблем.
(Помните пример из главы 4? Оформляя внештатного работника по правилам для обычных
сотрудников, вы наживете неприятности с налоговой инспекцией. Класс Contractor
не должен объявляться производным от класса Employee даже при том, что "они
обладают рядом сходных черт.)
Ниже этот фундаментальный принцип приведен в слегка измененном, более абстрактном
виде, ориентированном на практическое программирование.
Экземпляр класса А, производного от класса В, должен нормально работать в каждом
фрагменте программы, которому в качестве параметра передается экземпляр базового
типа.
Предположим, у вас имеются функция UseIt(bTh1 ng As В) и объект aThi ng, который
является экземпляром производного класса А. Следующий вызов должен нормально
работать:
Uselt(aThing)
Если все эти рассуждения выглядят слишком абстрактными, ниже приведен вымышленный (и надеемся, забавный) пример. Предположим, вы размышляете над тем, от какого класса следует объявить производным класс ManagerOf Programmers — от Manager или от Programmer? Всем известно, что менеджеры носят аккуратные прически, поэтому класс Manager должен содержать метод SetHalrStyle. А теперь закройте глаза и представьте типичного программиста, которого вдруг назначили управлять другими программистами. Захочет ли он менять свой имидж? Можете ли вы уверенно заявить, что вызов вида
tom.SetHairStyle("sharp razor cut")
всегда имеет смысл? Конечно, среди программистов иногда встречаются экземпляры,
которые заботятся о своей прическе, но обо всех программистах этого никак не
скажешь. Мораль: класс ManagerOf Programmers должен быть производным от класса
Programmer, а не от Manager.
Некоторые
языки позволяют объявить класс ManagerOfProgrammers производным как от Manager,
так и от Programmer. Теоретически такая возможность выглядит вполне логично
и привлекательно, но на практике языки с множественным наследованием (так это
называется по-научному) порождают массу проблем. Вместо множественного наследования
в VB .NET используется реализация нескольких интерфейсов. Как вы вскоре увидите,
этот вариант значительно проще и нагляднее, нежели классическое множественное
наследование.
В сущности,
при программировании в VB .NET вам никак не удастся скрыться от наследования.
Даже если вы привыкли к интерфейсному стилю программирования VB5 и VB6 и считаете,
что для ваших задач достаточно интерфейсов вкупе с включением и делегированием,
ограничиться одними интерфейсами в VB .NET невозможно. Дело в том, что без явного
использования наследования вы не сможете пользоваться .NET Framework.
Наследование заложено в основу любого графического приложения .NET, а также
многих встроенных классов коллекций — даже работа с объектом FolderBrowser связана
с наследованием!
Применение
наследования при построении графических приложений в рекламной литературе VB
.NET иногда именуется визуальным наследованием. Не обращайте внимания — это
самое обычное наследование. То, что ваш класс является производным от Windows.Forms.Form,
сути дела не меняет.
Более того,
сам подход к применению наследования в .NET Framework как нельзя лучше доказывает,
что наследование в объектно-ориентированном программировании не должно полностью
вытесняться интерфейсами. Подход, примененный в .NET Framework, вполне может
применяться и в ваших собственных проектах.
При построении
библиотек, используемых другими программистами, хорошо спроектированные классы,
прошедшие тщательную отладку и тестирование, могут использоваться другими программистами
в качестве базовых.
Начнем с
повторения некоторых терминов. Класс, взятый за основу при определении нового
класса, называется базовым классом. Класс, определяемый на основе базового
класса, называется производным классом. Производный класс автоматически
наследует всю открытую функциональность базового класса, однако эта функциональность
может переопределяться в производном классе и дополняться новыми возможностями.
Следующий
пример наглядно показывает, как это происходит. Допустим, у нас имеется компания
с передовой политикой в области материального стимулирования. Каждый раз, когда
заработная плата всех служащих компании повышается на 5%, для программистов
прибавка составляет 6%. Вам поручено разработать систему учета кадров для этой
компании. Вы решаете определить класс Programmer, производный от Employee, и
переопределить метод RaiseSal агу в классе Programmer, чтобы отразить автоматическую
(и вполне заслуженную!) надбавку.
Итак, приступим
к программированию цепочки наследования Employee—>Programmer. Допустим, у
нас уже имеется класс Publiс Employee, который входит в решение или включается
в него командой Project > References. В этом случае начало кода класса Programmer
будет выглядеть так (ключевая строка выделена жирным шрифтом):
Public Class Programmer
Inherits Employee
End Class
Ключевое
слово Inherits должно находиться в первой не пустой и не содержащей комментария
строке после имени производного класса (кстати, IntelliSense подскажет имена
возможных базовых классов). Учтите, что производный класс не может объявляться
с модификатором Publ i с, если базовый класс объявлялся с модификатором Friend
или Private. Это связано с тем, что модификатор уровня доступа в производном
классе не может быть менее ограничивающим, чем модификатор базового класса.
С другой стороны, он может устанавливать более жесткие ограничения, поэтому
от базового класса с уровнем доступа Publ i с можно объявить производный класс
с уровнем Friend.
Следующим
шагом в построении производного класса должно стать правильное определение конструктора.
Поскольку производный класс должен обладать как минимум теми же возможностями,
что и базовый, конструкторы производных классов часто вызывают конструкторы
базовых классов для правильной инициализации полей базового класса и передают
им аргументы, которые должны использоваться при инициализации. При этом используется
специальное ключевое слово MyBase:
Public Sub New(ByVal
theName As String, ByVal curSalary As Decimal)
MyBase.NewCName.curSalary)
End Sub
Ключевая
строка, выделенная жирным шрифтом, вызывает конструктор базового класса Empl
oyee и инициализирует его поля. Если вы забудете вызвать MyBase. New в том случае,
когда конструктор базового класса вызывается с аргументами, VB .NET выдает сообщение
об ошибке следующего вида:
C:\vb net book \chapter 5 \Examplel \Examplel \Modulel.vb(55):
'Examplel.Programmer'.the base class of 'Examplel.Employee'.
does not have an accessible constructor that can be called with
no arguments. Therefore.the first statement of this constructor
must be a call to a constructor of the base class via 'MyBase.New'
or another constructor
of this class via 'MyClass.New' or 'Me.New'.
Хорошо бы,
чтобы все сообщения об ошибках были настолько содержательными и понятными. Компилятор
напоминает о том, что при отсутствии у базового класса безаргументного конструктора
производный класс должен содержать хотя бы один вызов MyBase. New. После включения
в программу вызова MyBase. New возникает очень интересный вопрос: как обращаться
к полям базового класса? Следующее правило на первый взгляд может вас удивить:
Производный
класс не обладает привилегированным доступом к полям базовою класса.
Из этого
правила следует, что производный класс Programmer не получает доступа к закрытым
полям базового класса Employee. Предположим, заработная плата хранится в
закрытом поле базового класса с именем m_Sal ary и вы пытаетесь включить в код
метода RaiseSalary класса Programmer следующий фрагмент:
Public Sub New(ByVal
theName As String. ByVal curSalary As Decimal)
MyBase.New(theName.
curSalary)
MyBase.m_salary
= 1.2 * curSalary End Sub
Компилятор выдает
сообщение об ошибке:
'Examplel.Employee.m_Salary'is Private.and is not accessible
in this context.
В
повседневной жизни существует хорошая аналогия — родители устанавливают правила
поведения для детей, а не наоборот.
Что же делать?
Если вы хотите, чтобы производный класс получил доступ к некоторым возможностям
базового класса, об этом должен позаботиться сам базовый класс. В следующем
разделе будет показано, как это делается.
Обращение
к функциональности базового класса
В VB .NET
существует модификатор Protected, который автоматически предоставляет доступ
из производных классов к соответствующему члену класса, будь то метод или переменная
класса. Возникает искушение воспользоваться этим модификатором и объявить все
поля базового класса с уровнем доступа Protected, чтобы производные классы могли
легко и быстро работать с ними. Не поддавайтесь соблазну! Хороший стиль проектирования
требует, чтобы модификатор Protected использовался только для методов, но не
для переменных. В противном случае нарушается инкапсуляция и теряется возможность
проверки данных там, где она должна происходить — в базовом классе. Как и в
реальной жизни, здесь действует хороший принцип «доверяй, но проверяй».
Например,
в исходное определение класса Employee входят свойства со следующими сигнатурами:
Public Readonly Property TheName() As String
Public Readonly
Property Salary() As Decimal
Таким образом,
доступ к этим свойствам разрешен всем классам. Чтобы ограничить доступ к свойствам
классами, производными от Empl oyee, замените модификатор Publ ic на Protected.
В табл. 5.1
перечислены различные модификаторы уровня доступа, присваиваемые членам классов
в иерархии наследования.
Как было
сказано выше, функции (но не поля!) с модификаторами Protected и Protected Friend
распространены достаточно широко, поскольку они предотвращают доступ к защищенным
членам со стороны внешнего кода.
При использовании
Protected возникает весьма интересный подвох. К счастью, компилятор вовремя
предупредит вас о возможных проблемах. Рассмотрим конкретный пример: допустим,
у вас имеется класс GeekFest с методом Boast, который пытается обратиться к
свойству Salary класса Programmer (что в конечном счете означает доступ к свойству
Sal агу базового класса Empl oyee). Ниже приведен примерный вид программы:
Public Class
GeekFest
Private m_Programmers()
As Programmer
Sub New(ByVal
Programmers() As Programmer)
m_Programmers = Programmers
End Sub
Public Function
Boast(ByVal aGeek As Programmer) As String
Return "Hey my salary is " & aGeek.Salary
End Function
End Class
Таблица
5.1. Модификаторы уровня доступа при наследовании
Модификатор |
Описание |
||
Friend | Доступ предоставляется только из текущей сборки | ||
Private | Доступ предоставляется только объектам базового класса | ||
Protected | Доступ ограничивается объектами базового класса и объектами любых производных классов | ||
Protected Friend | Доступ предоставляется только из текущей сборки или из классов, производных отданного базового класса (может рассматриваться как комбинация модификаторов Protected и Friend) | ||
Public | Доступ к члену класса предоставляется всем, кто имеет доступ к классу в соответствии с модификатором самого класса | ||
Также допустим,
что в класс Empl oyee входит свойство Sal агу, доступное только для чтения и
помеченное модификатором Protected вместо Public:
Protected Readonly
Property Salary() As Decimal
Get
Return MyClass.m_Salary
End Get End
Property
В результате
компилятор выдает сообщение об ошибке:
C:\vb net book\chapter 5\Examplel\Examplel\Moduleld.vb(19):
'Examplel. Modulel.Employee. Protected Readonly Property Salary()
As Decimal'
is Protected.and is not accessible in this context.
Хотя класс
Programmer обладает доступом к защищенному свойству Salary в своем коде,
объекты Programmer не имеют доступа к этому методу за пределами кода
класса Programmer. Подведем итог:
Обращение
к Protected-методам базового класса возможно только из объектов производного
класса, но не из внешних ссылок на эти объекты за пределами производного класса.
Переопределение
свойств и методов
В нашем примере,
где программист автоматически получает 6-процентное повышение зарплаты вместо
5-процентного, необходимо изменить поведение метода RaiseSalary и отразить в
нем автоматическую надбавку. Это называется переопределением функции.
Общие
члены классов переопределяться не могут.
В отличие
от многих объектно-ориентированных языков синтаксис VB .NET четко показывает,
что метод базового класса должен переопределяться в производном классе. Для
этого используются два специальных ключевых слова.
Естественно,
типы параметров и возвращаемого значения должны совпадать. Если они различаются,
происходит не переопределение, а перегрузка.
Ниже приведен
примерный вид базового класса Employee с методом RaiseSalary, который может
переопределяться в производных классах Programmer, Manager и т. д. Ключевые
строки кода выделены жирным шрифтом:
Option Strict
On Public Class Employee
Private m_Name
As String
Private m_Salary
As Decimal
Private Const
LIMIT As Decimal = 0.1D
Public Sub New(ByVal
theName As String,ByVal curSalary As Decimal)
m_Name =theName
m_Salary =curSalary
End Sub
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
Необязательное
ключевое слово Overloads, упоминавшееся в главе 4, указывает на то, что в классе
определены несколько версий RaiseSalary.
Класс
Employee часто встречается в примерах этой главы. Либо введите его в Visual
Studio, либо скачайте исходный текст с сайта www.piter.com, если вы еще не сделали
этого ранее.
В нашей модели
зарплата программиста повышается вызовом специализированной версии метода RaiseSalary.
Производный класс Programmer приведен ниже (как обычно, ключевые строки выделены
жирным шрифтом):
Public Class
Programmer
Inherits Employee
Public Sub New(ByVal
theName As String, ByVal curSalary As Decimal)
MyBase.New(theName, curSalary)
End Sub
Public Overloads
Overrides Sub RaiseSalaryCByVal Percent As Decimal)
MyBase.RaiseSalary(1.2D *Percent."special")
End Sub
End Class
Обратите
внимание, каким компактным получился производный класс — большая часть функциональности
осталась неизменной, поэтому мы просто наследуем ее от базового класса!
В приведенной
ниже процедуре Sub Main компилятор генерирует вызов правильной версии метода
Rai seSal ary (с 20-процентной надбавкой) для объекта sal 1у, относящегося к
классу Programmer:
Sub Main()
Dim sally As New Programmed"Sally". 150000D) sally.RaiseSalary(0.1D)
' С учетом надбавки для программистов
Console.WriteLine(sally.TheName & " salary is now " & sally.Salary())
Console.ReadLine()
End Sub
Подведем
итог:
Ключевое
слово VB .NET Notlnheritable полностью запрещает наследование от класса. Как
правило, наследование запрещается для классов, выполняющих очень важные функции,
которые ни в коем случае, на должны изменяться. Многие классы .NET Framework
(такие, как String) помечены ключевым словом Notlnheritable именно по этой причине.
Впрочем, если требуется запретить переопределение лишь одного члена класса,
незачем запрещать наследование для всего класса; достаточно пометить ключевым
словом NotOverridable нужный член класса.
По
умолчанию переопределение членов классов запрещается (см. описание ключевого
слова Shadows ниже в этой главе). И все же ключевое слово NotOverridable рекомендуется
использовать, поскольку оно более наглядно выражает намерения программиста.
Иногда при
переопределении метода или свойства возникает необходимость вызвать версию базового
класса. Допустим, имени каждого программиста в классе Programmer должен предшествовать
почетный титул «Code Guru». Ключевое слово MyBase позволяет обратиться
к открытому свойству TheName базового класса в производном классе:
Public Overrides
Readonly Property TheName() As String
Get
Return "Code
Guru " & MyBase.TheName()
End Get
End Property
Учтите, что
ключевое слово MyBase обладает рядом ограничений:
С MyBase
тесно связано другое ключевое слово — MyClass. Оно гарантирует, что даже в случае
переопределения будет вызван метод, определенный в текущем классе, а не какая-то
из его переопределенных версий в производных классах. На ключевое слово MyCl
ass распространяются те же ограничения, что и на ключевое слово MyBase, о котором
упоминалось в предыдущей главе.
На
практике ключевое слово MyClass приносит наибольшую пользу в тех случаях, ког-да
мы хотим указать на модификацию поведения класса. Замена его на Me не дает нужного
эффекта, поскольку ключевое слово Me означает «текущий экземпляр, код
которого выполняется в настоящий момент», и попытки применения его в другом
контексте лишь сбивают с толку.
Предположим,
вы построили замечательную объектно-ориентированную систему учета кадров, в
которой в полной мере используются все преимущества полиморфизма.
А теперь попробуйте ответить на простой вопрос — как в вашей системе реализован
перевод простого работника в менеджеры?
Как ни странно,
в ООП подобные операции (то есть изменение типа текущего экземпляра в объектно-ориентированной
программе) считаются одним из сложнейших аспектов архитектуры приложения, о
котором обычно никто всерьез не думает, пока ситуация не станет критической.
В соответствии со спецификой объектно-ориентированного программирования после
создания объекта изменить его тип невозможно.
В нашей системе
учета кадров существует только одно приемлемое решение — включить в класс Employee
метод, который копирует состояние Employee в новый объект Manager, после чего
помечает старый объект Employee как неиспользуемый.
Просмотр
иерархии наследования
С усложнением
иерархии классов в программе на помощь приходит окно классов и Object Browser.
Например, из окна классов на рис. 5.1 видно, что класс Programmer является производным
от класса Employee и переопределяет только конструктор и метод RaiseSalary.
Рис. 5.1. Иерархия наследования в окне классов
Программы,
основанные на UML (в частности, Visio или Rational Rose), не только ото-бражают
связи между классами в иерархии наследования, но и генерируют «скелет»
программы. Одни программисты в восторге от систем автоматизированного программирования,
другие их ненавидят.
Правила
преобразования и обращения к членам классов в иерархии наследования
Объекты производных
классов могут храниться в переменных базовых классов:
Dim tom As New
Programmer("Tom". 65000)
Dim employeeOfTheMonth
As Employee
employeeOfTheMonth
= torn
В режиме
жесткой проверки типов (Option Strict On), если объект tom хранится в переменной
employeeOfTheMonth, для сохранения его в переменной Programmer приходится использовать
функцию СТуре, поскольку компилятор заранее не знает, что такое преобразование
возможно:
Dim programrnerOnCall
As Programmer
programmerOnCal1
= CType(employeeOfTheMonth,Programmer)
Конечно,
простое сохранение tom в переменной programmerOnCall выполняется простым присваиванием.
При
работе с объектом torn через переменную employeeOfTheMonth вам не удастся использовать
уникальные члены, определенные в классе Programmer и отсутствующие в Employee.
С другой стороны, как будет показано в следующем разделе, при этом сохраняется
доступ ко всем членам класса Programmer, переопределяющим члены класса Employee.
Наследование
часто помогает избавиться от громоздких конструкций Select Case и If-Then-Else,
чтобы вся черновая работа выполнялась компилятором и механизмом полиморфизма.
Например, цикл из следующего фрагмента работает как с экземплярами класса Employee,
так и с экземплярами Programmer:
Sub Maln()
Dim tom As New Employee("Tom". 50000)
Dim sally As New Programmer("Sally", 150000)
Dim ourEmployees(l) As Employee ourEmpl.oyees(0)=tom
ourEmployees(l)= Sally
Dim anEmployee As Employee
For Each anEmployee In ourEmployees
anEmployee.RaiseSalary(0.1D)
Console.WriteLine(anEmployee.TheName
& "salary now is " & _
anEmployee.Salary())
Next
Console. ReadLine()
End Sub
Результат
выполнения этого примера показан на рис. 5.2. Мы видим, что в каждом случае
вызывается правильный метод RaiseSalary, несмотря на то что в массиве типа Employee
хранятся как объекты Employee, так и объекты Programmers.
Рис.
5.2. Использование полиморфизма в программе
Иногда говорят, что в VB .NET по умолчанию методы являются виртуальными. Термин «виртуальный» означает, что при вызове метода компилятор использует истинный тип объекта вместо типа контейнера или ссылки на объект.
В только что рассмотренном примере под виртуальностью следует понимать, что,
хотя все ссылки относятся к типу Empl oyee (поскольку объекты хранятся в массиве
Employee), компилятор проверяет истинный тип объекта sally (это тип Programmer)
для вызова правильного метода Rai seSal агу, обеспечивающего большую прибавку.
Виртуальные методы довольно часто используются в ситуациях, когда в контейнере
базового типа хранятся объекты как базового, так и производного типа. Впрочем,
наш упрощенный подход к вызову виртуальных методов сопряжен с некоторыми опасностями.
Модификация класса Programmer и включение в него уникальных членов нарушают
нормальную работу полиморфизма. В следующем примере класс Programmer дополняется
двумя новыми членами (полем и свойством), выделенными жирным шрифтом:
Public Class Programmer
Inherits Employee
Private m_gadget As String
Public Sub New(ByVal theName As String.
ByVal curSalary
As Decimal)
MyBase.New(theName. curSalary)
End Sub
Public Overloads
Overrides Sub RaiseSalary(ByVal Percent As Decimal)
MyBase.RaiseSalary(1.2D * Percent, "special")
End Sub
Public Property ComputerGadget() As String Get
Return m_Gadget End Get SetCByVal Value As String)
m_Badget = Val ue
End Set
End Property
End Class
В процедуру Sub Main добавляются
новые строчки, выделенные жирным шрифтом:
Sub Main()
Dim tom As New Employee("Tom". 50000)
Dim sally As New Programmed"Sally". 150000)
sally.ComputerGadget = "Ipaq"
Dim ourEmployees.d) As Employee
ourEmployees(0)= tom
ourEmployees(l)= sally
Dim anEmployee As Employee
For Each anEmployee In ourEmployees
anEmployee.RaiseSalary(0.1D)
Console.WriteLine(anEmployee.TheName & "salary now is "
& anEmployee.Salary()) Next
Console.WriteLine(ourEmployeesd).TheName
& "gadget is an "_
& ourEnployees(l).Gadget) Console. ReadLine()
End Sub
При попытке
откомпилировать новый вариант программы будет выдано сообщение об ошибке:
C:\book to comp\chapter
5\VirtualProblems\VirtualProblems\Modulel.vb(17): The name 'Gadget'is not a
member of 'VirtualProblems.Employee1.
Хотя объект
sally, хранящийся в элементе массива ourEmployees(l), относится к типу Programmer,
компилятор этого не знает и потому не может найти свойство ComputerGadget. Более
того, при включенном режиме Option Strict (а отключать его не рекомендуется)
для использования уникальных членов класса Programmer вам придется производить
явное преобразование элементов массива к типу Programmer:
Console.WriteLine(ourEmployees(l).TheName & "gadget is an " & _
CType(ourEmployeesd),
Programmer).ComputerGadget)
Преобразование
объекта, хранящегося в объектной переменной базового типа, в объект производного
класса называется понижающим преобразованием (down-casting); обратное
преобразование называется повышающим (upcasting). Понижающее преобразование
весьма широко распространено, однако использовать его не рекомендуется, поскольку
при этом часто приходится проверять фактический тип объектной переменной в конструкциях
следующего вида: If TypeOf ourEmployees(l)Is Programmer Then
Else If TypeOf
ourEmployees(l)Is Employee Then
End If
Перед вами
те самые конструкции, для борьбы с которыми нам понадобился полиморфизм! (Повышающее
преобразование всегда обходится без проблем, поскольку основополагающее правило
наследования гласит, что объекты производных классов всегда могут использоваться
вместо объектов базовых классов.)
Хороший
стиль программирования требует, чтобы при использовании специальных средств
класса Programmer объекты хранились в контейнере, предназначенном только для
типа Programmer. В этом случае вам не придется проверять возможность преобразования
командой If-TypeOf.
Термин «замещение»
(shadowing) встречался и в ранних версиях VB, и в большинстве языков программирования.
Локальная переменная, имя которой совпадает с именем переменной, обладающей
более широкой областью видимости, замещает (скрывает) эту переменную.
Кстати, это одна из причин, по которой переменным уровня модуля обычно присваиваются
префиксы m_, а глобальные переменные снабжаются префиксами g_ — грамотный выбор
имен помогает избежать ошибок замещения. Переопределение унаследованного метода
тоже можно рассматривать как своего рода замещение. В VB .NET поддерживается
еще одна, чрезвычайно мощная разновидность замещения:
Член производного
класса, помеченный ключевым словом Shadows (которое впервые появилось в бета-версии
2), замещает все одноименные члены базового класса.
При помощи
ключевого слова Shadows можно определить в производном классе функцию, имя которой
совпадает с именем процедуры базового класса. С практической точки зрения
ключевое слово Shadows приводит к тому, что в производном классе появляется
абсолютно новый член с заданным именем, в результате чего все одноименные
унаследованные члены становятся недоступными в производном классе. Из этого
следует, что унаследованные члены класса с ключевым словом Shadows невозможно
переопределить, поэтому полиморфизм перестает работать.
По
умолчанию VB .NET разрешает замещение членов классов, но при отсутствии клю-чевого
слова Shadows выдается предупреждение. Кроме того, если один член класса объявляется
с ключевым словом Shadows или Overloads, это ключевое слово должно использоваться
и для остальных членов класса с тем же именем.
Иногда замещение
усложняет ситуацию и приводит к возникновению нетривиальных ошибок — например,
при полиморфном вызове замещенных методов и свойств через объект базового класса.
Чтобы рассмотреть эти проблемы на конкретном примере, мы внесем некоторые изменения
в класс Programmer (новые строки выделены жирным шрифтом):
Public Class Programmer Inherits Employee Private m_gadget As String
Private m_HowToCallMe As String = "Code guru "
Public Sub NewCByVal
theName As String, ByVal curSalary As Decimal)
MyBase.New(theName,
curSalary)
m_HowToCal1Me = m_HowToCallMe StheName
End Sub
Public Overloads
Overrides Sub RaiseSalary(ByVal Percent As Decimal)
MyBase.RaiseSalary(1.2D * Percent, "special")
End Sub
Public Shadows
Readonly Property TheName() As String
Get
Return mJtowToCallMe
End Get
End Property
End Class
А теперь
попробуйте запустить новый вариант процедуры Sub Main:
Sub Main()
Dim torn As
New Employee('Tom". 50000)
Dim sally As
New Programmer("Sally". 150000)
Console.WriteLinetsally.TheName)
Dim ourEmployees(l)
As Employee
ourEmployees(0)=
tom
ourEmployees(l)=
sally
Dim anEmployee
As Employee
For Each anEmployee In ourEmployees
anEmployee.RaiseSalary(0.lD)
Console.WriteLinetanEmployee.TheName & "salary now is " &
anEmployee. Salary())
Next
Console. ReadLine()
End Sub
Рис.
5.3. Замещение нарушает работу полиморфных вызовов
Результат
показан на рис. 5.3.
Как видно
из рисунка, полиморфный вызов перестал работать. Первая строка, выделенная в
Sub Main жирным шрифтом, правильно ставит перед именем Sally титул «Code
Guru». К сожалению, во второй выделенной строке полиморфизм уже не работает,
вследствие чего не вызывается метод TheName производного класса Programmer.
Результат — имя выводится без титула. Другими словами, при использовании ключевого
слова Shadows обращения к членам объектов осуществляются в соответствии с типом
контейнера, в котором хранится объект, а не их фактическим типом (можно сказать,
что при использовании ключевого слова Shadows в производном классе метод или
свойство становится невиртуальным).
На стадии
проектирования наследственных связей в программе часто выясняется, что многие
классы обладают целым рядом сходных черт. Например, внештатные сотрудники не
относятся к постоянным работникам, но и те и другие обладают рядом общих атрибутов
— именем, адресом, кодом налогоплательщика и т. д. Было бы логично выделить
все общие атрибуты в базовый класс Payabl eEnt i ty. Этот прием, называемый
факторингом, часто используется при проектировании классов и позволяет
довести абстракцию до ее логического завершения.
В классах,
полученных в результате факторинга, некоторые методы и свойства невозможно реализовать,
поскольку они являются общими для всех классов в иерархии наследования. Например,
класс Payabl eEnt i ty, от которого создаются производные классы штатных и внештатных
работников, может содержать свойство с именем TaxID. Обычно в процедуре этого
свойства следовало бы организовать проверку кода налогоплательщика, но для некоторых
категорий внештатных работников эти коды имеют особый формат. Следовательно,
проверка этого свойства должна быть реализована не в базовом классе Payabl eEntity,
а в производных классах, поскольку лишь они знают, как должен выглядеть правильный
код.
В таких ситуациях
обычно определяется абстрактный базовый класс. Абстрактным называется
класс, содержащий хотя бы одну функцию с ключевым словом MustOverride; при этом
сам класс помечается ключевым словом Mustlnherit. Ниже показано, как может выглядеть
абстрактный класс Payabl eEntity:
Public Mustlnherit Class PayableEntity
Private m_Name As String
Public Sub New(ByVal
itsName As String)
m_Name = itsName
End Sub
Readonly Property
TheName()As String
Get
Return m_Name
End Get
End Property
Public MustOverride Property TaxID()As String
End Class
Обратите
внимание: свойство TaxID, помеченное ключевым словом MustOverride, только объявляется
без фактической реализации. Члены классов, помеченные ключевым словом MustOverride,
состоят из одних заголовков и не содержат команд End Property, End Sub и End
Function. Доступное только для чтения свойство TheName при этом реализовано;
из этого следует, что абстрактные классы могут содержать как абстрактные, так
и реализованные члены. Ниже приведен пример класса Егор! оуее, производного
от абстрактного класса PayableEntity (ключевые строки выделены жирным шрифтом):
Public Class
Employee
Inherits PayableEntity
Private m_Salary
As Decimal
Private m_TaxID
As String
Private Const
LIMIT As Decimal = 0.1D
Public Sub NewCByVal
theName As String, ByVal curSalary As Decimal.
ByVal TaxID As String) MyBase.New(theName)
m_Salary = curSalary
m_TaxID = TaxID
End Sub
Public Overrides
Property TaxID() As String Get
Return m_TaxID
End Get
Set(ByVal Value As String)
If Value.Length
<> 11 then
' См. главу
7 Else
m_TaxID = Value
End If
End Set
End Property
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.WriteLineC'NEED
PASSWORD TO RAISE SALARY MORE " & _
"THAN LIMIT!!!!")
Else
m_Salary =(1D + 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 MID + Percent) * m_Salary
End If
End Sub
End Class
Первая ключевая
строка расположена внутри конструктора, который теперь должен вызывать конструктор
абстрактного базового класса для того, чтобы правильно задать имя. Во втором
выделенном фрагменте определяется элементарная реализация для свойства Taxld,
объявленного с ключевым словом MustOverride (в приведенном примере новое значение
свойства не проверяется, как следовало бы сделать в практическом примере).
Ниже приведена
процедура Sub Mai n, предназначенная для тестирования этой программы:
Sub Main()
Dim tom As New
Employee("Tom". 50000. "111-11-1234")
Dim sally As
New Programmed "Sally", 150000. "111-11-2234".)
Console.Wri
teLi ne(sa1ly.TheName)
Dim ourEmployees(l)
As Employee
ourEmployees(0)
= tom
ourEmployees(l)
= sally
Dim anEmployee
As Employee
For Each anEmployee
In ourEmployees anEmployee.RaiseSalary(0.lD)
Console.WriteLine(anEmployee.TheName & "has tax id " & _
anEmployee.TaxID
& ".salary now is " & anEmployee.Salary())
Next
Consol e.ReadLine()
End Sub
В программе
невозможно создать экземпляр класса, объявленного с ключевым словом Mustlnherit.
Например, при попытке выполнения следующей команды:
Dim NoGood As New PayableEntity("can't do")
компилятор
выводит сообщение об ошибке:
Class 'PayableEntity'
is not creatable because it contains at least one member marked as 'MustOverride'
that hasn't been overridden.
Тем не менее
объект производного класса можно присвоить переменной или контейнеру абстрактного
базового класса, что дает возможность использовать в программе полиморфные вызовы:
Dim torn As New Employee("Tom". 50000, "123-45-6789")
Dim whoToPay(13)
As PayableEntity whoToPay(0) = tom
Теоретически
класс Mustlnherit может не содержать ни одного члена с ключевым сло-вом MustOverride
(хотя это будет выглядеть несколько странно).
При использовании
классов коллекций .NET Framework (таких, как ArrayList и HashTable) возникает
неожиданная проблема: эти классы предназначены для хранения обобщенного типа
Object, поэтому прочитанные из них объекты всегда приходится
преобразовывать к исходному типу функцией СТуре. Также возникает опасность того,
что кто-нибудь сохранит в контейнере объект другого типа и попытка вызова СТуре
завершится неудачей. Проблема решается использованием коллекций с сильной
типизацией — контейнеров, позволяющих хранить объекты конкретного типа и
типов, производных от него.
Хорошим примером
абстрактного базового класса .NET Framework является класс CollectionBase. Классы,
производные от Coll ectionBase, используются для построения коллекций с сильной
типизацией (прежде чем создавать собственные классы коллекций, производные от
Coll ectionBase, убедитесь в том, что нужные классы отсутствуют в пространстве
имен System.Collections.Specialized). Коллекции, безопасные по отношению к типам,
строятся на основе абстрактного базового класса System. Collections. CollectionBase;
от вас лишь требуется реализовать методы Add и Remove, а также свойство Item.
Хранение данных во внутреннем списке реализовано на уровне класса System. Collections.
CollectionBase, который и выполняет все остальные операции.
Рассмотрим
пример создания специализированных коллекций (предполагается, что проект содержит
класс Employee или ссылку на него):
1 Public Class
Employees
2 Inherits System.Col
lections.CollectionBase
3 ' Метод Add
включает в коллекцию только объекты класса Employee.
4 ' Вызов перепоручается
методу Add внутреннего объекта List.
5 Public Sub
AddtByVal aEmployee As Employee)
6 List.Add(aEmployee)
7 End Sub
8 Public Sub
Remove(ByVal index As Integer)
9 If index >
Count-1 Or index < 0 Then
10 ' Индекс
за границами интервала, инициировать исключение (глава 7)
11 MsgBox("Can't
add this item")' MsgBox условно заменяет исключение
12 Else
13 List.RemoveAt(index)
14 End If
15 End Sub
16
17 Default Public
Readonly Property Item(ByVal index As Integer)As Employee
18 Get
19 Return CType(List.Item(index).
Employee)
20 End Get
21 End Property
22 End Class
В строках
5-7 абстрактный метод Add базового класса реализуется передачей вызова внутреннему
объекту List; метод принимает для включения в коллекцию только объекты Empl
oyee. В строках 8-10 реализован метод Remove. На этот раз мы также используем
свойство Count внутреннего объекта List, чтобы убедиться в том, что удаляемый
объект не находится перед началом или после конца списка. Наконец, свойство
Item реализуется в строках 17-21. Оно объявляется свойством по умолчанию, поскольку
пользователи обычно ожидают от коллекций именно такого поведения. Свойство объявляется
доступным только для чтения, чтобы добавление новых элементов в коллекцию могло
осуществляться только методом Add. Конечно, свойство можно было объявить и доступным
для чтения/записи, но тогда потребовался бы дополнительный код для проверки
индекса добавляемого элемента.
Следующий фрагмент проверяет работу специализированной коллекции; недопустимая
операция включения нового элемента (в строке, выделенной жирным шрифтом) закомментирована:
Sub Main()
Dim torn As
New Employee("Tom", 50000)
Dim sally As
New Employee("Sally", 60000)
Dim myEmployees
As New Employees()
myEmployees.Add(tom)
myEmployees.Add(sally)
' myEmployees.Add("Tom")
Dim aEmployee
As Employee
For Each aEmployee
In myEmployees
Console.WriteLine(aEmployee.TheName)
Next
Console. ReadLine()
End Sub
Попробуйте
убрать комментарий из строки myEmpl oyees. Add("Tom"). Программа перестанет
компилироваться, и вы получите следующее сообщение об ошибке:
C:\book to comp \chapter 5\EmployeesClass\EmployeesClass\Modulel.vb(9):
A value of type
'String'cannot be converted to 'EmployeesClass.Employee'.
Перед
вами замечательный пример того, какими преимуществами VB .NET обладает перед
включением в прежних версиях VB. Конечно, мы продолжаем перепоручать вызовы
внутреннему объекту, чтобы избавиться от дополнительной работы, но возможность
перебора элементов в цикле For-Each появляется автоматически, поскольку наш
класс является производным от класса с поддержкой For-Each!
Вся работа
.NET Framework (а следовательно, и VB .NET) основана на том, что каждый тип
является производным от корневого класса Object, общего предка всех классов
(в ООП такие классы иногда называются космическими (space) базовыми классами).
К классу Object восходят все типы, как ссылочные (экземпляры классов), так и
структурные (числовые типы и даты, перечисляемые типы и структуры). В частности,
из этого следует, что любой функции, получающей параметр типа Object, можно
передать параметр произвольного типа (поскольку главное правило наследования,
упоминавшееся в начале главы, требует, чтобы переменная производного типа могла
использоваться в любом контексте вместо переменной базового типа).
Программисты
с опытом работы в ранних версиях VB иногда представляют тип Object как аналог
печально известного типа Variant. He поддавайтесь этому искушению! Тип Variant
был всего лишь одним из типов данных, который позволял хранить другие типы данных;
тип Object является корневым базовым классом, на котором завершается вся иерархия
наследования в .NET.
Класс Object
содержит ряд встроенных логических функций, предназначенных для проверки типа
объектной переменной:
Потомки класса
Object делятся на две категории: структурные типы, производные от System. Val
ueType (базовый класс всех структурных типов), и ссылочные типы, производные
непосредственно от Object. Чтобы узнать, принадлежит ли некоторый тип к категории
структурных типов, воспользуйтесь проверкой следующего вида:
Sub Maine)
Dim a As Integer
= 3
Console.Writel_ine("a
is a value type is " & IsValueType(a))
Console. ReadLine()
End Sub
Function IsValueType(ByVal
thing As Object) As Boolean
Return (TypeOf (thing) Is System.ValueType)
End Function
Вероятно,
перед нами одна из ошибок разработчиков VB .NET — функция TypeOf не может вызываться
для структурных переменных без определения вспомогательной функции, получающей
объект указанного типа. Конечно, следовало бы позволить программисту передавать
структурный тип при вызове TypeOf.
Поскольку
класс Object является общим предком всех типов VB .NET, весьма вероятно, что
вам придется часто использовать (или переопределять) методы этого класса. Основные
методы Object описаны в нескольких ближайших разделах.
Довольно
часто возникает желание переопределить защищенный метод Finalize класса Object.
Теоретически код переопределенного метода Finalize выполняется при освобождении
памяти, занимаемой объектом, в процессе сборки мусора. На практике использовать
этот метод нежелательно. Поскольку вы не знаете, когда и в какой последовательности
будут вызваны методы Finalize, использовать их для деинициализа-ции классов
в лучшем случае ненадежно. Вместо этого следует реализовать метод Dispose, описанный
в разделе «IDisposable» этой главы. А если вы все же переопределяете
метод Finalize, учтите, что в нем необходимо вызвать MyBase.Finalize и продублировать
весь код из метода Dispose.
В классе
Object поддерживаются две версии Equals — общая и обычная. Общая версия имеет
следующий синтаксис:
Overloads Public Shared Function Equals(0bject. Object) As Boolean
Пример использования:
Equals(a. b)
Синтаксис
обычной версии:
Overloads Over-ridable
Public Function Equals(Object) As Boolean
Пример использования:
a.Equals(b)
Обе версии
метода Equal s проверяют, обладают ли два объекта одинаковыми данными, но вы
должны быть готовы переопределить Equals, если этого требует специфика вашего
класса. Не забывайте, что общие члены класса не переопределяются, поэтому переопределение
допускается лишь для обычной (не общей) версии Equal s.
Например,
если в вашей программе предусмотрены два способа представления некоторого структурного
типа, позаботьтесь о том, чтобы это обстоятельство учитывалось методом Equals
(именно так разработчики VB .NET поступили с классом String, хотя, строго говоря,
этот класс не относится к структурным типам).
В классе
Object также предусмотрен общий (и потому не переопределяемый) метод ReferenceEquals.
Метод ReferenceEquals проверяет, представляют ли две переменные один экземпляр.
Например, как показывает следующий фрагмент, для двух строк а и b выражение
a.Equals(b) может быть истинным, а выражение Reference-Equals (a. b) — ложным:
Sub Main()
Dim a As String
= "hello"
Dim b As String
= "Hello"
Mid(b.l.D= "h"
Console.Writeline("Is
a.Equals(b)true?" & a.Equals(b))
Console.WriteLine("Is ReferenceEquals(a.b)true?" & _
ReferenceEquals(a.b))
Console. ReadLine()
End Sub
Результат
показан на рис. 5.4.
Рис.
5.4. Различия между методами Equals и ReferenceEquals
Метод ToString
возвращает представление текущего объекта в строковом формате. Вопрос о том,
будет ли это представление удобным при отладке и для пользователей, зависит
от реализации класса. По умолчанию ToString возвращает полное имя типа для заданного
объекта — например, System. Object или Examplel.Programmer.
Постарайтесь
привыкнуть к переопределению ToStnng в ваших классах, чтобы этот метод возвращал
более содержательное строковое представление класса. Например, в классе Employee
из программы EmployeeTestl, приведенной в главе 4, метод ToString может выглядеть
примерно так:
Public Overrides
Function ToString() As String
Dim temp As
String
temp = Me.GetType.ToString()&
"my name is " & Me.TheName
Return temp
End Function
Примерный
результат:
EmployeeTestl+EmployeeTestl+Employee
my name is Tom
Каждый тип
.NET Framework представлен объектом Туре. Класс Туре содержит множество методов
со сложными именами — например, метод GetMembers возвращает информацию об именах
всех методов заданного класса. Метод GetType класса Object возвращает объект
Туре, при помощи которого можно получить информацию о типе во время выполнения
программы. В частности, эта чрезвычайно полезная возможность используется для
выполнения рефлексии (также используется термин «идентификация
типов на стадии выполнения»). Кстати, пространство имен Reflection
занимает столь важное место в работе .NET Framework, что оно автоматически импортируется
в каждый проект VS IDE.
Чтобы увидеть,
как выполняется рефлексия, включите в проект ссылку на сборку System.Windows.Forms
и запустите приведенную ниже программу. Когда через короткий промежуток времени
на экране появится приглашение, нажмите клавишу Enter. Продолжайте нажимать
Enter, и постепенно в консольном окне будет выведена информация обо всех членах
класса Windows. Forms. Form, на основе которого строятся графические приложения
в .NET. Примерный вид окна показан на рис. 5.5.
Рис.
5.5. Информация о членах класса Windows.Forms.Form, полученная
посредством рефлексии
В
этой программе мы ограничиваемся простым вызовомToString, но объекты Memberlnfo
содержат гораздо больше полезной информации. За дополнительными сведениями обращайтесь
к электронной документации.
1 Option Strict
On
2 Imports System.Windows.Forms
3 Module Modulel
4 Sub Main()
5 Dim aForm
As New Windows.Forms.Form()
6 Dim a Type
As Type
7 a Type = aForm.GetType()
8 Dim member
As Object
9 Console.Writellne("This
displays the members of the Form class")
10 Console.WriteLineC'Press
enter to see the next one.")
11 For Each
member In aType.GetMembers
12 Console.ReadLine()
13 Console.
Write(member.ToSthng)
14 Next
15 Console.WriteLine("Press
enter to end")
16 Console.ReadLine()
17 End Sub
18 End Module
В строках
6 и 7 мы получаем объект Туре для класса Windows. Forms. Form. Затем, поскольку
метод GetMembers класса Туре возвращает коллекцию объектов Memberlnfo, описывающих
члены класса, программа просто перебирает все элементы коллекции в строках 11-14.
Замените
Windows.Forms.Form другим классом, и вы получите информацию о членах этого класса.
Для получения объекта Туре также можно передать полное имя класса в строковом
формате версии GetType, оформленной в виде общего метода класса Туре. Рефлексия
позволяет выполнять позднее связывание в VB .NET — методу InvokeMember передается
строка с информацией о вызываемом методе (вероятно, полученной при помощи рефлексии).
За дополнительными сведениями об этой возможности обращайтесь к описанию класса
Туре в документации .NET.
В программировании, как и в современной науке:
Но самое
важное правило клонирования формулируется так:
Последнее
обстоятельство затрудняет клонирование во всех языках ООП, поэтому ме-тод MemberWiseClone
считается потенциально опасным. Дело в том, что объект может содержать другие
объекты. Если внутренние объекты не будут клонированы одновременно с объектом,
их содержащим, вместо пары оригинал-клон вы получите сиамских близнецов, которые
будут зависеть друг от друга. Если класс содержит поля, которые представляют
собой изменяемые объекты, метод MemberWiseClone заведомо создает «сырой»,
неполноценный клон (это называется поверхностным копированием). Метод MemberWiseClone
успешно клонирует только те объекты, поля которых относятся исключительно к
структурным типам.
Следующий
пример наглядно показывает, что имеется в виду под этим предупреждением. Массивы
VB .NET в отличие от массивов VB6 являются объектами.
Допустим,
мы пытаемся клонировать объект класса, одно из полей которого представляет собой
массив:
1 Public Class
EmbeddedObjects
2 Private m_Data()
As String
3 Public Sub
New(ByVa1 anArray() As String)
4 m_Data = anArray
5 End Sub
6 Public Sub
OisplayData()
7 Dim temp As
String
8 For Each temp
In m_Data
9 Console.WriteLine(temp)
10 Next
11 End Sub
12 Public Sub
ChangeData(ByVal newData As String)
13 m_Data(0)
= newData
14 End Sub
15 Public Function
Clone() As EmbeddedObjects
16 Return CType(Me.MemberwiseClone.
EmbeddedObjects)
17 End Function
18 End Class
Выполните
следующую процедуру Sub Main:
Sub Main()
Dim anArray()
As String ={"HELLO"}
Dim a As New
EmbeddedObjects(anArray)
Console.WriteLinet"Am
going to display the data in object a now!")
a.DisplayData()
Dim b As EmbeddedObjects
b =a.Clone()
Dim newData
As String ="GOODBYE"
b.ChangeData(newData)
Console.WriteLine("Am
going to display the data in object b now!")
b.DisplayData()
Console.WriteLine("Am
going to re-display the data in a" & _
"after making a change to object b!!!") a.DisplayData()
Console. ReadLine()
End Sub
Рис.
5.6. Метод MemberWiseClose не работает
Как видно
из рис. 5.6, результат получился весьма неожиданным: изменения клона отражаются
на исходном объекте!
Что происходит
в этом примере? Почему метод MemberWiseClone не работает, как задумано? Почему
изменения в объекте b отражаются на объекте а? Потому что в строках 2 и 4 класса
EmbeddedObjects в качестве значения поля, задаваемого в конструкторе, используется
массив. Массивы являются изменяемыми объектами; как было показано в главе
3, из этого следует, что содержимое массива может изменяться даже при передаче
по значению (ByVal). Состояние внутреннего массива изменяется в строках 12-14
класса EmbeddedObjects. Поскольку объект и псевдоклон связаны ссылкой на массив
m_Data, изменения клона отражаются на исходном объекте.
Решение этой
проблемы рассматривается в разделе «ICloneable» этой главы. А пока
мы просто укажем, что настоящий клон (иногда называемый глубокой копией)
создает клоны всех полей объекта, при необходимости выполняя рекурсивное
кло-нирование. Например, если одно из полей класса является объектом и содержит
еще один внутренний объект, процесс клонирования должен опуститься на два уровня
вглубь.
Также
существует хитроумная методика клонирования, основанная на сериализации объектов.
Подробности приведены в главе 9.
Наконец,
в качестве средства дополнительной защиты разработчики .NET Framework объявили
MemberWiseClone защищенным методом класса Object. Как было показано выше, это
означает, что MemberWi seCI one может вызываться только из производных классов.
Код за пределами производного класса не может клонировать объекты при помощи
этого небезопасного метода. Также обратите внимание на то, что MemberWi seCIone
возвращает тип Object, поэтому в строке 1б класса EmbeddedObjects приходится
использовать функцию СТуре.
Проблема
неустойчивости базовых классов и контроль версии
Проблема
несовместимости компонентов хорошо известна всем, кому доводилось программировать
для 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
Результат
показан на рис. 5.7. Программа работает именно так, как предполагалось.
Рис.
5.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
Результат
показан на рис. 5.8.
Рис.
5.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 еще на стадии
компиляции может вычислить вызываемый метод на основании сигнатуры и типа класса
(это называется ранним связыванием). Этот факт объясняет возможность использования
полиморфизма при реализации интерфейсов.
А теперь
подумайте, что произойдет, если:
Происходит
следующее: в режиме жесткой проверки типов (Option StrictOn) программа вообще
не будет компилироваться. Если этот режим отключить, умный компилятор .NET поймет,
что вызов метода класса не удастся заменить в откомпилированном коде неким подобием
простого вызова функции. Таким образом, компилятору придется сгенерировать значительно
больший объем кода. Фактически он должен во время выполнения программы вежливо
спросить у объекта, поддерживает ли он метод с указанной сигнатурой, и если
поддерживает — не будет ли он возражать против его вызова? Подобное решение
обладает двумя характерными особенностями, из-за которых оно работает значительно
медленнее и гораздо чаще приводит к возникновению ошибок:
Описанный
процесс называется поздним связыванием (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, обязуется содержать:
Как будет
показано ниже, имена методов реализации несущественны — главное, чтобы методы
имели заданную сигнатуру.
Чтобы реализовать
интерфейс в классе, прежде всего убедитесь в том, что он сам или ссылка на него
входит в проект. Далее за именем класса и командой 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
обладают особой декларативной функцией — реализуя их, вы тем самым заявляете,
что ваш класс обладает некой стандартной функциональностью, присутствующей во
многих классах.
Далее в этой
главе рассматриваются базовые интерфейсы для построения специализированных коллекций.
Если вы помните, с какими трудностями была связана
реализация
циклов 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
Интерфейс
ICollection определяется производным от IEnumerable; он дополняет этот интерфейс
тремя свойствами, доступными только для чтения, и одним новым методом. Класс
ICollection редко реализуется самостоятельно. Как правило, он образует базу
для интерфейсов IList и IDictionary (см. ниже). Члены этого интерфейса перечислены
в табл. 5.2.
Таблица
5.2. Члены интерфейса ICollection
Метод/свойство |
Описание |
||
Count (свойство) | Возвращает количество элементов в коллекции | ||
IsSynchronized (свойство) | Используется в многопоточных приложениях (см. главу 12). Свойство возвращает True, если доступ к коллекции синхронизируется с учетом многопоточного доступа | ||
SyncRoot (свойство) | Также используется в многопоточных приложениях (см. главу 12). Свойство возвращает объект для синхронизации доступа к коллекции | ||
СоруТо (метод) | Копирует элементы из коллекции в массив, начиная с заданной позиции | ||
Интерфейс
ICollection реализуется классом System.Collections.CollectionBase.
Интерфейс
IList обеспечивает выборку элементов коллекции по индексу. Разумеется, поскольку
этот интерфейс определяется производным от I Enumerable, при этом сохраняется
возможность использования For-Each. Иерархия наследования IList выглядит следующим
образом:
IEnumerable->ICollection->IList
Интерфейс
IList относительно сложен — он состоит из трех свойств и семи методов (табл.
5.3). Напомним, что некоторые из методов могут быть пустыми, если в каком-то
конкретном классе их реализация не имеет смысла.
Таблица
5.3. Члены интерфейса IList
Метод/свойство |
Описание |
||
IsFixedSize (свойство) | Логическое свойство. Показывает, имеет ли коллекция фиксированный размер | ||
IsReadOnly (свойство) | Логическое свойство. Показывает, доступна ли коллекция только для чтения | ||
Item (свойство) | Свойство доступно для чтения и записи. Используется для получения и присваивания значения объекта с заданным индексом | ||
Add (ByVal value As Object) As Integer (метод) |
Включает объект в текущую позицию списка. Метод должен возвращать индекс, присвоенный добавленному элементу |
||
Clear (метод) |
Удаляет все элементы из списка |
||
Метод/свойство |
Описание |
||
Contains (ByVal
value As Object) As Boolean (метод) |
Метод предназначен
для проверки наличия в списке заданного значения. Эффективная реализация
этого метода иногда бывает весьма нетривиальной задачей. Если элемент
присутствует в списке, метод возвращает True; в противном случае возвращается
False |
||
IndexOf (ByVal
value As Object) As Integer (метод) |
Возвращает индекс
заданного объекта в списке (программист также должен учитывать эффективность
реализации этого метода) |
||
Insert(ByVal
index As Integer, ByVal value As Object) (метод) |
Вставляет объект
в заданную позицию списка |
||
Remove(ByVal
value As Object) (метод) |
Удаляет первое
вхождение заданного объекта в списке |
||
Remove(ByVal
index As Integer) (метод) |
Удаляет элемент,
находящийся в заданной позиции |
||
Интерфейс
IList реализуется классом System.Collections.CollectionBase.
Интерфейс
IDictionary представляет коллекцию, в которой доступ к данным осуществляется
по ключу — как в хэш-таблицах, описанных в предыдущей главе. Более того, класс
хэш-таблиц в числе прочих реализует интерфейсы IDictionary, ICollection, Enumerable
и ICloneable!
Хотя интерфейс
IDictionary объявляется производным от Enumerable и переход к следующему элементу
может осуществляться методом MoveNext, обычно такая возможность не используется
— коллекции, реализующие IDictionary, ориентируются в первую очередь на обращение
по ключу, а не на последовательный перебор элементов. По этой причине интерфейс
IDictionary зависит от интерфейса IDic-tionaryEnumerator, который расширяет
Enumerator и дополняет его тремя новыми свойствами:
В
.NET Framework входит класс DictionaryBase. Определяя класс производным от DictionaryBase,
вы получаете в свое распоряжение всю функциональность интерфейса IDictionary.
Члены класса
IDictionary перечислены в табл. 5.4.
Поскольку
ключи в ассоциативных коллекциях должны быть уникальными, при реали-зации большинства
методов необходимо сначала проверить, не был ли заданный ключ использован ранее.
Свойство Keys возвращает объект, реализующий ICollection; уникальность ключа
проверяется методом,Соп1а1п5 интерфейса ICollection.
Таблица
5.4. Члены интерфейса IDictionary
Метод/свойство |
Описание |
||
IsFixedSize
(свойство) |
Логическое свойство.
Показывает, имеет ли коллекция фиксированный размер |
||
IsReadOnly (свойство) |
Логическое свойство.
Показывает, доступна ли коллекция только для чтения |
||
Item (свойство) |
Свойство доступно
для чтения и записи. Используется для получения и присваивания значения
объекта с заданным индексом |
||
Keys (свойство) |
Возвращает объект,
реализующий интерфейс ICollection и содержащий все ключи ассоциативной
коллекции |
||
Values (свойство) |
Возвращает объект,
реализующий интерфейс ICollection и содержащий все значения ассоциативной
коллекции |
||
Add(ByVal key
As Object, ByVal value As Object) (метод) |
Добавляет объект
с заданным ключом (ключ должен быть уникальным) |
||
Clear (метод) |
Удаляет все
элементы из ассоциативной коллекции |
||
Contains (ByVal
key As Object) As Boolean (метод) |
Ищет значение
с заданным ключом |
||
GetEnumerator
(метод) |
Возвращает объект
IDictionaryEnumerator для работы с ключами и значениями |
||
Remove(ByVal
key As Object) (метод) |
Удаляет элемент
с заданным ключом |
||
Предположим,
коллекцию объектов 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
Результат
показан на рис. 5.9.
Рис.
5.9. Сортировка по нестандартному критерию с использованием IComparable
.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 .
Результат
показан на рис. 5.10,
Рис.
5.10. Сортировка по нескольким критериям с использованием IComparer
В программе можно определить несколько классов, реализующих IComparer. Их последовательное применение позволяет выполнять многоуровневую сортировку произвольной глубины.