| « Поставить закладку » « Сделать стартовой » | |||
|
|||
|
Глава 5. Наследование и интерфейсы
Глава №5. Наследование и интерфейсы В предыдущей
главе кратко упоминались два столпа объектно-ориентированного программирования в
VB .NET: наследование реализации, обеспечивающее возможность
многократного использования кода, и наследование интерфейсов, то есть
«контракт», которому должны соответствовать некоторые аспекты поведения класса.
Обе разновидности наследования будут подробно рассмотрены в этой
главе. Для начала мы
покажем, как в VB .NET организовано наследование реализации. К сожалению, при
изучении этой темы никак нельзя ограничиться поверхностным знакомством. Дело в
том, что непродуманные действия программиста при наследовании реализации
приводят к весьма неприятным последствиям, поэтому в этой главе мы потратим
довольно много времени, показывая, как избежать этих опасностей за счет
тщательного проектирования иерархии наследования. После описания
механики и основных принципов наследования реализации мы во всех подробностях
изучим класс Object, являющийся предком всех объектов .NET. Завершая описание
наследования реализации, мы покажем, как в .NET решается проблема
неустойчивости базовых классов, вызывающая немало хлопот при наследовании
реализации в других объектно-ориентированных языках — таких, как Java и С++. Не
пугайтесь термина «проблема неустойчивости базовых классов»; речь идет всего
лишь о том, что непродуманные изменения базового класса могут нарушить работу
производных классов.
От наследования мы перейдем к реализации интерфейсов в VB .NET. В завершение этой главы вы узнаете, как использовать важнейшие интерфейсы .NET Framework IComparable, ICloneable и IDisposable.
Хотя
наследование не является панацеей ООП и во многих ситуациях лучше
воспользоваться интерфейсами, не стоит полагать, что без наследования можно
как-нибудь обойтись. Наследование — замечательное средство, способное сэкономить
немало времени и сил... если им правильно пользоваться. Критерий правильного
использования прост: не используйте наследование, если у вас нет абсолютной
уверенности в существовании логической связи типа «является частным
случаем».
Если все эти рассуждения выглядят слишком абстрактными, ниже приведен вымышленный (и надеемся, забавный) пример. Предположим, вы размышляете над тем, от какого класса следует объявить производным класс ManagerOf Programmers — от Manager или от Programmer? Всем известно, что менеджеры носят аккуратные прически, поэтому класс Manager должен содержать метод SetHalrStyle. А теперь закройте глаза и представьте типичного программиста, которого вдруг назначили управлять другими программистами. Захочет ли он менять свой имидж? Можете ли вы уверенно заявить, что вызов вида tom.SetHairStyle("sharp razor cut")
В сущности, при
программировании в VB .NET вам никак не удастся скрыться от наследования. Даже
если вы привыкли к интерфейсному стилю программирования VB5 и VB6 и считаете,
что для ваших задач достаточно интерфейсов вкупе с включением и делегированием,
ограничиться одними интерфейсами в VB .NET невозможно. Дело в том, что без
явного использования наследования вы не сможете пользоваться .NET
Framework. Наследование заложено в основу любого графического приложения .NET, а
также многих встроенных классов коллекций — даже работа с объектом FolderBrowser
связана с наследованием!
Более того, сам
подход к применению наследования в .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. Модификаторы уровня доступа при наследовании
Также допустим,
что в класс Empl oyee входит свойство Sal агу, доступное только для чтения и
помеченное модификатором Protected вместо Public: Protected Readonly
Property Salary() As Decimal Get Return
MyClass.m_Salary End Get End
Property В результате
компилятор выдает сообщение об ошибке: C:vb net bookchapter 5ExamplelExamplelModuleld.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.
В нашей модели
зарплата программиста повышается вызовом специализированной версии метода
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 нужный член класса.
Иногда при
переопределении метода или свойства возникает необходимость вызвать версию
базового класса. Допустим, имени каждого программиста в классе 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, о котором
упоминалось в предыдущей главе.
Предположим, вы
построили замечательную объектно-ориентированную систему учета кадров, в которой
в полной мере используются все преимущества полиморфизма. А теперь попробуйте ответить
на простой вопрос — как в вашей системе реализован перевод простого работника в
менеджеры? Как ни странно,
в ООП подобные операции (то есть изменение типа текущего экземпляра в
объектно-ориентированной программе) считаются одним из сложнейших аспектов
архитектуры приложения, о котором обычно никто всерьез не думает, пока ситуация
не станет критической. В соответствии со спецификой объектно-ориентированного
программирования после создания объекта изменить его тип
невозможно. В нашей системе
учета кадров существует только одно приемлемое решение — включить в класс
Employee метод, который копирует состояние Employee в новый объект Manager,
после чего помечает старый объект Employee как неиспользуемый. Просмотр иерархии наследования С усложнением
иерархии классов в программе на помощь приходит окно классов и Object Browser.
Например, из окна классов на рис. 5.1 видно, что класс Programmer является
производным от класса Employee и переопределяет только конструктор и метод
RaiseSalary.
Рис. 5.1. Иерархия наследования в окне классов
Правила преобразования и обращения к членам классов в иерархии
наследования Объекты
производных классов могут храниться в переменных базовых классов: 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 выполняется простым
присваиванием.
Наследование
часто помогает избавиться от громоздких конструкций 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.
Использование полиморфизма в программе
Public Class Programmer Inherits Employee Private m_gadget As String Public Sub New(ByVal theName As String. ByVal curSalary As
Decimal) End Sub Public Overloads
Overrides Sub RaiseSalary(ByVal Percent As Decimal) End Sub End Set End Property End Class
Console.WriteLine(ourEmployeesd).TheName & "gadget is an "_ End Sub
При попытке
откомпилировать новый вариант программы будет выдано сообщение об ошибке:
C:book to
compchapter 5VirtualProblemsVirtualProblemsModulel.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 Перед вами те
самые конструкции, для борьбы с которыми нам понадобился полиморфизм!
(Повышающее преобразование всегда обходится без проблем, поскольку
основополагающее правило наследования гласит, что объекты производных классов
всегда могут использоваться вместо объектов базовых классов.)
Термин
«замещение» (shadowing) встречался и в ранних версиях VB, и в большинстве
языков программирования. Локальная переменная, имя которой совпадает с именем
переменной, обладающей более широкой областью видимости, замещает
(скрывает) эту переменную. Кстати, это одна из причин, по которой переменным
уровня модуля обычно присваиваются префиксы m_, а глобальные переменные
снабжаются префиксами g_ — грамотный выбор имен помогает избежать ошибок
замещения. Переопределение унаследованного метода тоже можно рассматривать как
своего рода замещение. В VB .NET поддерживается еще одна, чрезвычайно мощная
разновидность замещения: Член
производного класса, помеченный ключевым словом Shadows (которое впервые
появилось в бета-версии 2), замещает все одноименные члены базового
класса. При помощи
ключевого слова Shadows можно определить в производном классе функцию, имя
которой совпадает с именем процедуры базового класса. С практической
точки зрения ключевое слово Shadows приводит к тому, что в производном классе
появляется абсолютно новый член с заданным именем, в результате чего все
одноименные унаследованные члены становятся недоступными в производном классе.
Из этого следует, что унаследованные члены класса с ключевым словом Shadows
невозможно переопределить, поэтому полиморфизм перестает
работать.
Иногда замещение
усложняет ситуацию и приводит к возникновению нетривиальных ошибок — например,
при полиморфном вызове замещенных методов и свойств через объект базового
класса. Чтобы рассмотреть эти проблемы на конкретном примере, мы внесем
некоторые изменения в класс 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
При
использовании классов коллекций .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 5EmployeesClassEmployeesClassModulel.vb(9): A value of type
'String'cannot be converted to 'EmployeesClass.Employee'.
Вся работа .NET
Framework (а следовательно, и VB .NET) основана на том, что каждый тип является
производным от корневого класса Object, общего предка всех классов (в ООП такие
классы иногда называются космическими (space) базовыми классами). К
классу Object восходят все типы, как ссылочные (экземпляры классов), так и
структурные (числовые типы и даты, перечисляемые типы и структуры). В частности,
из этого следует, что любой функции, получающей параметр типа Object, можно
передать параметр произвольного типа (поскольку главное правило наследования,
упоминавшееся в начале главы, требует, чтобы переменная производного типа могла
использоваться в любом контексте вместо переменной базового типа). Класс 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
Поскольку класс
Object является общим предком всех типов VB .NET, весьма вероятно, что вам
придется часто использовать (или переопределять) методы этого класса. Основные
методы Object описаны в нескольких ближайших разделах.
В классе 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, полученная
посредством рефлексии
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.
В программировании, как и в современной науке:
Но самое важное
правило клонирования формулируется так:
Следующий пример
наглядно показывает, что имеется в виду под этим предупреждением. Массивы 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» этой главы. А пока мы просто
укажем, что настоящий клон (иногда называемый глубокой копией) создает
клоны всех полей объекта, при необходимости выполняя рекурсивное кло-нирование.
Например, если одно из полей класса является объектом и содержит еще один
внутренний объект, процесс клонирования должен опуститься на два уровня
вглубь.
Наконец, в
качестве средства дополнительной защиты разработчики .NET Framework объявили
MemberWiseClone защищенным методом класса Object. Как было показано выше, это
означает, что MemberWi seCI one может вызываться только из производных классов.
Код за пределами производного класса не может клонировать объекты при помощи
этого небезопасного метода. Также обратите внимание на то, что MemberWi seCIone
возвращает тип Object, поэтому в строке 1б класса EmbeddedObjects приходится
использовать функцию СТуре. Проблема неустойчивости базовых классов и контроль
версии Проблема несовместимости компонентов хорошо известна всем, кому доводилось программировать для Windows. Обычно она выступает в форме так называемого кошмара DLL (DLL Hell) — программа использует определенную версию DLL, a потом установка новой версии компонента нарушает работу программы. Почему? Причины могут быть разными, от очевидных (случайное исключение функции, использовавшейся в программе) до весьма нетривиальных (например, изменение типа возвращаемого значения у функции). В любом случае все сводится к вариациям на одну тему — при изменении открытого интерфейса кода, от которого зависит ваша программа, программа не может использовать новую версию вместо старой, а старая версия уже стерта. В большинстве объектно-ориентированных языков наследование сопряжено с потенциальной угрозой работоспособности вашей программы из-за несовместимости компонентов. Программисту остается лишь надеяться на то, что открытые и защищенные члены классов-предшественников в 1 иерархии наследования не будут изменя |