Метод ToString

Метод 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

 

Функция GetType и рефлексия

Каждый тип .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 заведомо создает «сырой», неполноценный клон (это называется поверхностным копированием). Метод 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.