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