Снова о свойствах
Принципиальное
различие в работе свойств VB6 и VB .NET заключается в том, что секции Get и
Set теперь должны обладать одинаковым уровнем доступа. Определять свойства с
секциями Public Get и Private Set в VB .NET не разрешается.
Это
ограничение легко обходится. Чтобы процедура Set фактически стала закрытой,
объявите свойство с атрибутами Public Readonly и одновременно объявите другое,
внутреннее закрытое свойство для Set.
Кроме того,
в VB6 свойство не могло изменяться в процедуре, даже если оно было передано
по ссылке (то есть с ключевым словом ByRef). В VB .NET свойства, переданные
по ссылке, могут изменяться.
Но самое принципиальное изменение относится к свойствам по умолчанию. В прежних версиях VB существовала концепция свойств по умолчанию, которая на первый взгляд казалась очень удобной. На практике свойства по умолчанию часто становились причиной ошибок в программах. Например, что означает следующая команда?
Me.Text1
= Text2
Отслеживая
ошибки, возникающие в подобных командах, опытные пользователи VB выясняли, что
эта команда задает свойству Text текстового поля с именем Textl значение переменной
Text2. Свойства по умолчанию не только становились источником
ошибок в программах, но и требовали, чтобы при присваивании объектов использовалось
ключевое слово Set, поскольку присваивание объектам нужно было отличать от присваивания
свойствам. В VB .NET проблема свойств по умолчанию решается просто — они разрешены
только там, где это действительно оправдано, а именно при использовании параметров.
Допустим, у вас имеется кэш-таблица aTable; при выборке значений было бы удобно
использовать синтаксис вида aTable("theKey"), но это возможно лишь
в том случае, если Item является свойством по умолчанию для класса HashTable.
Свойства по умолчанию объявляются в классе с ключевым словом Default, причем
это допускается лишь для свойств, получающих минимум один параметр. Если свойство
по умолчанию перегружается, все перегруженные версии также помечаются ключевым
словом Default. Свойства по умолчанию чаще всего используются в ситуации, когда
у объекта имеется свойство, значение которого возвращается в виде массива или
другого объекта, способного вмещать несколько величин (например, хэш-таблицы).
Предположим, у вас имеется класс Sal es и свойство InYear, которое по полученному
индексу возвращает число (объем продаж):
Public Class
Sales
Private m_Sales() As
Decimal = {100, 200. 300}
Default Public Property InYear(ByVal theYear As Integer) As Decimal
Get
Return m_Sales(theYear)
End Get
Set(ByVa1 Value
As Decimal)
m_Sales(theYear)=Value
End Set
End Property
' Остальной код
класса End Class
Свойство
по умолчанию позволяет использовать конструкции вида
Dim ourSales As New Sales()
Console.WriteLine(ourSa1es(1))
вместо
Dim ourSales
As New Sales()
Console.WriteLi
ne(ourSales.InYear(1))
Или, например,
вы можете написать
ourSales (2)
= 3000
вместо
ourSales.InYear(2)
= 3000
Ключевое
слово Set используется в процедурах свойств VB NET.
На первый
взгляд кажется, что свойства очень похожи на открытые поля экземпляров. Если
объявить в классе А открытое поле с именем evil, на него можно сослаться при
помощи конструкции A.evil; ничто не указывает на то, что свойство реализовано
в виде открытой переменной. Может, определить открытое поле и избавиться от
хлопот по определению процедур Get и Set?
Не поддавайтесь
соблазну. Инкапсуляцию данных не стоит нарушать без веских причин (а еще
лучше —те нарушать никогда!).
Но инкапсуляцию
можно случайно нарушить и другими способами — например, если не следить за возвращаемыми
значениями свойств. Каким образом? Если поле представляет собой изменяемый объект
(например, массив), возвращение его в виде значения свойства приведет к нарушению
инкапсуляции, поскольку внешний код сможет изменить состояние поля экземпляра
через полученную объектную переменную. В таких ситуациях следует создать клон
поля (клонирование объектов рассматривается в главе 5). Мораль:
Свойства
не должны возвращать изменяемые объекты, которые представляют собой переменные
классов.
Переменные
класса (в том числе и закрытые поля), объявленные за пределами его методов или
свойств, доступны для всех членов класса. Переменные, объявленные в методе или
свойстве, являются локальными по отношению к этому методу или свойству.
Переменные,
объявленные на уровне модуля, доступны для всех классов, определенных в этом
модуле, и для всего кода, обладающего доступом к этому модулю.
Таким образом,
переменные уровня модуля являются глобальными по отношению к экземплярам классов.
Пример:
Module Modulel
Dim aGlobal As Integer = 37
Sub Main()
Dim anA As New
А()
Dim aB As New
B()
Console. ReadLine()
End Sub
Public Class
A Sub New()
aGlobal =aGlobal +17 Console.WriteLine(aGlobal)
End Sub
End Class
Public Class
В Sub New()
Console.WriteLine(aGlobal)
End Sub
End Class
End Module
В данном
случае целая переменная aGlobal определяется на уровне модуля, поэтому изменения,
вносимые в aGlobal классом А, будут восприняты классом В. Использовать переменные
уровня модуля не рекомендуется — все взаимодействие между классами должно быть
реализовано на уровне обмена сообщениями!
В
прежних версиях VB широко практиковалось хранение общих данных классов в глобальных
переменных. В VB .NET надобность в этом небезопасном приеме отпала. За дополнительной
информацией обращайтесь к разделу «Общие данные в классах» этой
главы.
В программах
VB .NET нередко встречаются ситуации, когда у вас имеются два класса: «внешний»
и «внутренний», фактически принадлежащий первому. Вложенные (nested)
классы обычно выполняют вспомогательные функции, и их код имеет смысл лишь в
контексте внешнего класса. Существует хорошее эмпирическое правило: если при
просмотре внешнего класса код вложенного класса можно свернуть в окне программы
и это не затруднит понимания логики внешнего класса, значит, работа вложенного
класса организована правильно. Конечно, использование вложенных классов всегда
приводит к некоторому нарушению инкапсуляции — вложенный класс может обращаться
к закрытым членам внешнего класса (но не наоборот!). Если это обстоятельство
учитывается в архитектуре вашего приложения, не стоит уделять ему особого внимания,
поскольку внутренний класс всего лишь является специализированным членом внешнего
класса.
VB
.NET не позволяет расширять область видимости вложенного класса посредством
функций. Например, открытый член внешнего класса не может вернуть экземпляр
закрытого или дружественного (Friend) вложенного класса.
Практическое использование вложенных классов на примере связанного списка
Вложенные
классы чаще всего применяются в реализациях различных структур данных. К числу
самых распространенных структур данных принадлежит связанный список. Он представляет
собой цепочку ссылок, которая позволяет легко переходить от текущего объекта
к следующему, однако поиск всегда начинается с конкретной ссылки. Применение
вложенных классов в реализации связанного списка выглядит вполне естественно,
поскольку код объектов-ссылок не представляет интереса для пользователей класса
LinkedList, а объекты Link не могут существовать независимо от содержащего их
объекта LinkedList.
Ниже приведена
очень простая реализация класса для работы со связанными списками. Просмотрите
ее, а затем мы подробно проанализируем листинг. Обратите внимание на важную
строку, выделенную жирным шрифтом (строка 49); в ней используется нетривиальная
особенность объектно-ориентированного программирования, о которой будет рассказано
ниже.
1 Option Strict
On
2 Module Modulel
3 Sub Main()
4 Dim aLinkedList
As New LinkedlistC'first link")
5 Dim aALink
As LinkedList.Link
6 aLink = aLinkedList.MakeLink(aLinkedList.GetFirstLink,"second
link")
7 aLink = aLinkedList.MakeLink(aLink,"third
link")
8 Console.WriteLine(aLinkedList.GetFirstLink.MyData)
9 aLink = aLinkedList.GetNextLink(aLinkedList.GetFirstLink)
10 Console.WriteLine(aLink.MyData)
11 Console.WriteLine(aLink.NextLink.MyData)
12 Console.
ReadLine()
13 End Sub
14 Public Class
LinkedList
15 Private m_CurrentLink
As Link
16 Private m_FirstLink
As Link
17 Sub New(ByVal
theData As String)
18 m_CurrentLink
= New Link(theData)
19 m_FirstLink
= in_CurrentLink
20 End Sub
21 Public Function
MakeLink(ByVal currentLink As Link.ByVal
22 theData As
String) As Link
23 m_CurrentLink
=New Link(currentLink.theData)
24 Return m_CurrentLink
25 End Function
26 Public Readonly
Property GetNextLink(ByVal aLink As Link)_
27 As Link
28 Get
29 Return aLink.NextLink()
30 End Get
31 End Property
32 Public Readonly
Property GetCurrentLink()As Link
33 Get
34 Return m_CurrentLink
35 End Get
36 End Property
37 Public Readonly
Property GetFirstUnkOAs Link
38 Get
39 Return m_FirstLink
40 End Get
41 End Property
42
43 ' Вложенный
класс для ссылок
44 Friend Class
Link
45 Private m_MyData
As String
46 Private m_NextLink
As Link
47 Friend Sub
New(ByVal myParent As Link.ByVal theData As String)
48 m_MyData
- theData
49 myParent.m_NextLink
= Me
50 ' End Sub
51 Friend Sub
New(ByVal theData As String)
52 m_MyData
=theData
53 End Sub
54 Friend Readonly
Property MyData()As String
55 Get
56 Return m_MyData
57 End Get
58 End Property
59 Friend Readonly
Property NextLink()As Link
60 Get
61 Return m_NextLink
62 End Get
63 End Property
64 End Class
65 End Class
66 End Module
Строка 4
создает новый экземпляр связанного списка. В строке 5 определяется объектная
переменная типа Link. Поскольку класс Link является вложенным по отношению к
LinkedList, его тип записывается в виде «полного имени» LinkedList.Link.
Строки 6-12 содержат небольшую тестовую программу.
В строках
17-20 определяется конструктор класса LinkedList, в котором вызывается второй
конструктор класса Link (строки 51-53). Последний объявлен с атрибутом Friend
и потому доступен для внешнего класса Li nkedLi st. Если бы конструктор Link
был объявлен с атрибутом Private, то он стал б"ы недоступным для внешнего
класса.
Также стоит
обратить внимание на то, как в первом конструкторе класса Link (строки 47-50)
организуется ссылка на только что созданный элемент списка из предыдущего элемента.
Для этого используется ключевое слово Me — это очень принципиальный момент,
поэтому строка 49 выделена в листинге жирным шрифтом. На первый взгляд команда
myParent.m_NextLink = Me выглядит
недопустимой, поскольку мы обращаемся к закрытому полю родительского класса
myParent. Однако программа все-таки работает! Итак, запомните очень важное правило:
Для экземпляра
класса всегда доступны закрытые поля других экземпляров этого класса.
При
написании подобных классов в VB .NET можно обойтись и без использования этой
нетривиальной особенности классов. Например, в класс Link можно включить специальный
метод для создания ссылки на следующий элемент списка. В конечном счете выбор
зависит только от вашего стиля программирования. Тем не менее сама возможность
обращения к закрытым членам класса может преподнести неприятные сюрпризы, и
об этом необходимо знать. По этой причине в нашем примере продемонстрирован
именно такой подход.
Вернемся
к классу Еmploуее. Допустим, каждому работнику необходимо присвоить уникальный
номер. В старых версиях VB задача решалась при помощи глобальных переменных,
что приводило к нарушению инкапсуляции и создавало потенциальную угрозу случайного
изменения номеров внешним кодом. Логика подсказывает, что номер должен увеличиваться
только при создании нового объекта Empl оуее.
В VB .NET
наконец-то появились средства для достижения этой цели. Идея проста: в классе
определяются данные, совместно используемые всеми экземплярами данного класса,
однако внешний доступ к этим данным находится под вашим полным контролем (например,
через обращение к свойству). Не стоит и говорить, что эти поля никогда не должны
объявляться открытыми...
Такие поля
называются общими (shared). Они идеально подходят для таких ситуаций,
как в нашем призере с присвоением последовательных номеров. В классах также
могут определяться общие свойства и методы. Недостаток заключается в том, что
общие члены классов не могут работать с обычными полями, свойствами или методами.
Иначе говоря, общие члены работают только с другими общими членами. Дело в том,
что общие данные существуют еще до создания объекта, поэтому было бы нелогично
разрешать общим членам доступ к конкретным объектам.
Ниже приведен
фрагмент новой версии класса Employee с использованием общих данных для присвоения
номеров. В классе определяется закрытая общая переменная типа Integer, которая:
В совокупности
это означает, что работнику никогда не будет присвоен номер 0 и что новый номер
выделяется только при создании нового объекта Empl oyee — именно это нам и требовалось:
Public Class
Employee
Private m_Name
As String
Private m_Salary
As Decimal
Private Shared
m_EmployeeID As Integer = 1
Public Sub New(ByVal
theName As String. ByVal curSalary As Decimal)
m_Name = thename
m_Salary = curSalary
m_EmployeeID = m_EmployeeID + 1
End Sub
Readonly
Property Employeeld()
As Integer
Get
Employeeld =
m_EmployeeID
End Get
End Property
End Class
Ниже приведена
небольшая программа для тестирования класса Empl oyee, а также полный код класса
с общим полем:
Option Strict
On Module Modulel
Sub Main()
Dim Tom As New
Employee("Tom". 100000)
System.Console.WriteLine(Tom.TheName
& "is employee! " & _
Tom. Employee ID & "with salary " & Tom.SalaryO)
Dim Sally As New Employee("Sally". 150000)
System.Console.WriteLine(Sally.TheName
& "is employee!" & _
Sally.EmployeeID &"with salary "SSally.Salary())
System.Console.WriteLine("Please press the Enter key")
System.Console.Read()
End Sub
End Module
Public Class
Employee
Private m_Name
As 'String
Private m_Salary
As Decimal
Private Shared
m_EmployeeID As Integer = 1
Public Sub New(ByVal
theName As String.ByVal curSalary As Decimal)
m_Name = thename
m_Salary = curSalary
m_EmployeeID = m_EmployeeID + 1
End Sub Readonly
Property Employeeld()As Integer
Get
Employeeld =
m_EmployeeID
End Get End Property Readonly
Property TheName()
As String
Get
TheName = m_Name
End Get . End Property Readonly
Property Salary
() As Decimal
Get
Salary = m_Sa1ary
End Get
End Property
End Class
Обращения
к константам в классах не отличаются от обращений к общим полям, но при объявлении
констант вместо Shared используется ключевое слово Const. Конечно, объявление
открытых констант не приводит к нарушению инкапсуляции.
Не
путайте общие данные со статическими. Общие данные существуют в одной копии
для всех экземпляров класса, поэтому с точки зрения экземпляров они неявно обладают
глобальной видимостью. Статическими называются переменные, состояние которых
просто запоминается для повторного использования. Статическими могут быть объявлены
как общие, так и обычные поля класса.