Сайт Алексея Муртазина (Star Cat) E-mail: starcat-rus@yandex.ru
Мои программы Новости сайта Мои идеи Мои стихи Форум Об авторе Мой ЖЖ
VB коды Статьи о VB6 API функции Самоучитель по VB.NET
Собрания сочинений Обмен ссылками Все работы с фото и видео
О моём деде Муртазине ГР Картинная галерея «Дыхание души»
Звёздный Кот

Самоучитель по VB.NET
Глава 11.

Рефлексия и атрибуты

В Visual Basic 6 часто встречаются конструкции следующего вида:

Public s

Public X As Integer

Public Y As Integer End

Классы и их отдельные члены обладают характеристиками Public, Private и т. д., которые можно рассматривать как атрибуты класса или члена. Рассмотрим следующий код VB .NET:

<Modified("Dan","l/10/2001")> Public s

<Modified("Dan","l/13/2001")> Public X As Integer

Private Y As Integer

 End

To, что заключено в угловые скобки (<>) — это тоже атрибуты1. Перед нами одно из принципиальных усовершенствований языка Visual Basic, но для чего оно нужно?

Чтобы понять всю важность атрибутов, необходимо сделать небольшой шаг назад.

1 Не пытайтесь использовать атрибут Modified в своих программах, пока не прочтете эту главу и не поймете, откуда он взялся.

Компиляторы и интерпретаторы

Для начала рассмотрим фундаментальные различия между компиляторами и интерпретаторами2.

Компиляторы и интерпретаторы представляют два принципиально разных способа обработки программного кода. Интерпретатором называется программа, которая читает другую программу и выполняет указанные в ней действия (рис. 11.1).

2Дальнейшее описание относится к «эталонным» интерпретаторам и компиляторам. На практике часто встречаются гибридные решения, в которых интерпретатор наделяется некоторыми возможностями компилятора, или наоборот.

Рис. 11.1. Принцип действия интерпретатора

Например, при получении команды

 А = 5

интерпретатор BASIC сохраняет число 5 в некоторой ячейке памяти, с которой связано имя «А». Поскольку интерпретатор фактически читает программный код и выполняет предписанные действия, он в любой момент располагает полной информацией обо всей программе. В частности, это позволяет легко прервать выполнение программы, изменить ее и продолжить работу, просматривать переменные и изменять их текущие значения и даже вводить команды в окне отладки и обрабатывать их вне обычной последовательности выполнения.

Интерпретаторы не всегда работают непосредственно с исходным текстом. Довольно часто они осуществляют предварительное преобразование программы в промежуточный код (иногда называемый «Р-кодом»). Тем не менее интерпретатор всегда может восстановить исходную программу по предварительно преобразованному коду, позволяя программисту работать на уровне исходного текста.

Главным недостатком интерпретатора является низкая скорость работы. Интерпретатор постоянно обрабатывает исходный текст или Р-код и выполняет полученные инструкции.

Компилятор преобразует исходный текст программы в стандартный машинный код (рис. 11.2).

Рис. 11.2. Принцип действия компилятора

Допустим, компилятор встретил следующую команду: 

А = 5

Он генерирует машинный код для выполнения этой операции и записывает его в итоговый исполняемый файл. Завершив свою работу, компилятор уже не участвует в выполнении программы. Поскольку откомпилированная программа состоит из машинного кода, она отличается превосходным быстродействием (в зависимости от качества компилятора).

Для работы откомпилированной программы уже не нужен исходный текст: компилятор не включает его в файл, благодаря чему программы получаются более компактными и эффективными. К сожалению, отсутствие исходного текста затрудняет отладку программы, поскольку исходный текст используется для идентификации переменных и управления выполнением программы во время отладки. По этой причине компиляторы позволяют создавать отладочные версии файлов, содержащие исходный текст программы вместе с информацией о том, какому фрагменту исходной программы соответствует тот или иной блок исполняемого кода. Впрочем, даже при наличии отладочной информации отладчику будет очень трудно продолжить выполнение после модификации прерванной программы, поскольку в результате модификации код обычно перемещается в памяти, а указатели и содержимое памяти становятся недействительными. Многие современные среды программирования обеспечивают ограниченную поддержку «редактирования с продолжением»1, но интерпретаторы позволяют делать это значительно проще и быстрее.

В Visual Basic 6 были объединены лучшие стороны обоих подходов. В среде программирования VB6 поддерживается как интерпретатор со всеми присущими удобствами, так и полноценный компилятор. Одна из самых замечательных особенностей VB6 заключается в том, что любая программа всегда одинаково работает как в интерпретируемом, так и в откомпилированном варианте.

Visual Basic .NET является «чистым» компилятором. Для многих программистов VB6 уже только к этому изменению придется долго привыкать. Я довольно долго работал на Visual C++, поэтому в среде Visual Studio .NET я чувствуют себя вполне комфортно, но должен признаться, что я скучаю по интерпретируемой среде VB6. Я прекрасно понимаю, почему Microsoft решила объединить среды программирования, и новая среда действительно обладает массой классных возможностей... и все же мне немного не хватает интерпретатора VB6.

1По текущей бета-версии трудно сказать, как эта возможность будет реализована в окончательной версии VВ .NET.

 

Раз компилятор, два компилятор...

Как упоминалось в главе 4, Visual Basic .NET и С# компилируют программы не в машинный код. Исходные тексты преобразуются в сборки, содержащие код на промежуточном языке (IL) и манифест с информацией обо всех объектах программы (рис. 11.3).

Рис. 11.3. Принцип действия языков .NET

Программа преобразуется в машинный код лишь в фазе JIT-компиляции, происходящей либо во время загрузки (когда сборка загружается в первый раз), либо во время установки. На основании информации об объектах, хранящейся в манифесте, устанавливаются связи с объектами ссылки, с их методами и свойствами. Кроме того, это позволяет сборке устанавливать связи с другими сборками. Данные манифеста и промежуточный код при этом остаются в программе, что позволяет JIT-компилятору восстановить сборку в случае необходимости, например при установке новой зависимой сборки. Возможность связывания методов и свойств с обновленными сборками помогает предотвратить «кошмар DLL», так часто возникающий при работе с компонентами СОМ. Вскоре мы вернемся к этой теме.

 

Стадия компиляции и стадия выполнения

Понятия «стадия компиляции» и «стадия выполнения» очень хорошо знакомы программистам, работающим на C++ и на ассемблере. Рассмотрим следующий фрагмент, написанный на псевдокоде:

If ABooleanConstantOrVariable Then

Блок 1

 End If

#If ABooleanConstant Then 

Блок 2

#End If

Блок 1 выполняется или не выполняется в зависимости от значения ABooleanConstantOrVariable. Даже если этот блок не выполняется (вследствие проверки условия), его код все равно присутствует в программе. Достигнув команды If, программа проверяет значение ABooleanConstantOrVariable и решает, следует ли выполнять следующий блок. Говорят, что условие проверяется на стадии выполнения, потому что это происходит во время работы откомпилированной программы.

Директивы #Ifn#EndIf обрабатываются в процессе компиляции программы. Константа ABooleanConstant определяется в настройках проекта или в параметрах командной строки компилятора. Если значение ABooleanConstant равно True, блок 2 включается в программу, а если оно равно False, блок 2 полностью игнорируется, словно обычный комментарий. В любом случае проверка условия в программу не входит. Происходит так называемая «условная компиляция»: решение о включении блока в программу принимается на основании условия, проверяемого на стадии компиляции (не на стадии выполнения!). Условная компиляция поддерживается и в VB .NET.

Условная компиляция является мощным средством, позволяющим определять разные конфигурации программы в одном исходном файле. С каждой конфигурацией связывается отдельный набор констант компиляции, управляющих включением/исключением кода.

Я часто использую условную компиляцию для включения отладочного кода в отладочные версии программ, для создания специальных конфигураций с трассировочным кодом и для изменений кода при локализации.

 

Атрибуты

Средства условной компиляции VB .NET имеют много общего с аналогичными возможностями VB6, однако в VB .NET также поддерживаются атрибуты, играющие важную роль как на стадии компиляции, так и на стадии выполнения. Прежде всего следует запомнить, что атрибуты доступны для компилятора. Это означает, что компилятор может проанализировать атрибуты класса, метода или свойства и на основании их значений внести изменения в сгенерированный код. Смысл языковых атрибутов (таких, как атрибуты области видимости Public или Private) очевиден, однако сказанное относится и к атрибутам, являющимся частью .NET Framework. Например, если пометить класс атрибутом синхронизации (см. главу 7), компилятор сгенерирует IL-код для поддержки синхронизации класса (далее этот код будет преобразован JIT-компилятором в машинный код).

Однако настоящие возможности атрибутов связаны с тем, что .NET хранит сведения об атрибутах объектов в манифесте. Как было сказано выше, это означает, что данная информация не теряется при компиляции.

А значит, ее можно получить во время выполнения программы.

Процесс получения атрибутов программного кода и других данных, хранящихся в манифесте сборки, называется рефлексией (reflection). Атрибуты будут подробно описаны ниже, а пока давайте разберемся с рефлексией.

 

Рефлексия

Следующий класс и перечисляемый тип взяты из приложения Reflection: 

Public font>

Public s

Public X As Integer

Private Y As Integer 

End

Public Enum TestEnum

FirstMember = 1

SecondMember = 2

 End Enum 

End

Наша цель — открыть сборку и не только обнаружить в ней этот класс и перечисление, но и определить имена и типы всех членов класса, а также получить список значений перечисляемого типа.

 

Исследуем манифест

Прежде всего мы импортируем пространство имен System.Reflection. Это позволит использовать сокращенную запись для всех объектов этого пространства (ссылка на пространство имен System Reflection автоматически включается во все приложения .NET). Пространство System. Reflection содержит объекты, необходимые для чтения манифеста.

' Рефлексия и атрибуты

1Copyright ©2001 by Desaware Inc. All Rights Reserved

Imports System.Reflection

 Module Modulel

Sub Main() 

AssemblyTypes()

 Console.ReadLine()

End Sub

Функция AssemblyTypes получает список всех открытых типов сборки, выполняемой в настоящий момент.

Sub AssemblyTypes()

 Dim Typelndex As Integer

 Dim A As System.Reflection.Assembly

 Dim ATypes() As Type ' Исследовать текущую сборку 

A = A.GetExecutingAssembly()

' Получить все типы, предоставляемые данной сборкой

 ATypes = A.GetTypes()

Метод GetExecuti ngAssembly является статическим. С таким же успехом его можно вызвать без указания конкретного объекта, конструкцией вида

 А = System.Reflection.Assembly.GetExecutingAssembly()

а также Reflection.Assembly.GetExecutingAssembly или [Assembly].GetExecutingAssembly. Существуют и другие статические методы, позволяющие открыть сборку по имени сборки, DLL или пространства имен. Метод GetTypes() возвращает массив всех типов, определенных в сборке. Продолжение метода AssemblyTypes выглядит так:

For Typelndex = 0 То UBound(ATypes)

' Обратите внимание на полные имена типов.

 Console.WriteLine ("Туре: " + ATypes(Typelndex).FullName) 

' Если это перечисление, вывести список значений

 If ATypes(Typelndex).IsEnum Then

 Dim EnumStringsO As String 

' Получить имена

EnumStrings = System.Enum.GetNames(ATypes(TypeIndex))

 Console.WriteLine (" Enumeration names are: ")

 Dim estemp As String

 For Each estemp In EnumStrings 

' Вывести имена со значениями 

Console.WriteLine (" " + estemp + " = "+_

 System.Enum.Format(ATypes(ТуреIndex), _ System.Enum.Parse(ATypes(TypeIndex), estemp), "D")) 

Next 

End If

Функция в цикле перебирает элементы массива. Свойство IsEnum переменной Туре равно True, если тип является перечисляемым. В этом случае вызывается статический метод GetNames типа System. Enum (базового типа всех перечислений), возвращающий строковый массив с именами всех значений перечисляемого типа. Статический метод Parse типа System. Enum возвращает значение, соответствующее имени величины перечисляемого типа. Статический метод Format преобразует значение перечисляемого типа в строку для вывода.

Если текущий тип является вложенным (то есть определяется внутри сборки и не принадлежит к числу типов, реализующих саму сборку), вызывается функция ShowMembers, которая выводит список членов заданного типа. И класс Testи перечисление TestEnum будут отнесены к вложенным типам.

' Для вложенных типов (определяемых программистом 

' в сборке) вывести список всех членов. 

If ATypes(Typelndex) .MemberType = _ 

MemberTypes.NestedType Then

' Вывести пользовательские атрибуты, которые будут

' определены в следующем примере.

' ShowCustomAttributes(_ 

ATypes(Typelndex) .GetCustomAttributesQ)

ShowMembers (ATypes(Typelndex))

 End If

Console.WriteLine()

 Next

End Sub

Функция ShowMembers получает список членов заданного типа (в нашем примере возвращается информация только о полях данных). Вы также можете получить список свойств, методов, интерфейсов и т. д.

Sub ShowMembers(ByVal ThisType As Type)

 Dim Index As Integer 

Dim idx2 As Integer 

Dim members() As Memberlnfo 

' Получить все поля данных для типа ThisType.

 ' Также можно получить информацию о методах, 

' свойствах, интерфейсах и т.д. 

' В список включаются закрытые и открытые

 ' (но не статические!) члены.

members = ThisType.FindMembers(MemberTypes.Field, _

 BindingFlags.Public Or BindingFlags.Instance _ 

Or BindingFlags.NonPublic, Type.FiIterName, "*")

Метод FindMembers типа данных Type заполняет массив объектами Memberlnfo, содержащими информацию об искомых членах классов. В нашем примере используется информация только о полях. Параметр BindingFlags определяет искомую категорию членов класса. В нашем примере возвращается информация об открытых и закрытых членах, а также о членах, ассоциированных с экземплярами объектов (в частности, информация о статических членах не включается). Мы используем стандартный фильтр класса Туре и принимаем любые имена полей.

Следующий фрагмент в цикле перебирает содержимое полученного массива и выводит информацию о членах:

For Index = 0 То UBound(members)

 Dim fi As Fieldlnfo 

' Поскольку мы знаем, что это поле 

' данных, возможно безопасное преобразование

' к типу Fieldlnfo.

 fi = CType(members(Index), Fieldlnfo)

Тип Fieldlnfo, производный от Memberlnfo, позволяет получить подробную информацию о полях данных. Оператор СТуре преобразует объект Memberlnfо к типу Fieldlnfo. Такое преобразование заведомо возможно, поскольку каждый найденный объект Memberlnfo фактически является объектом Fieldlnfo (при вызове FindMembers мы запрашивали информацию только о полях данных).

Тип поля определяется при помощи свойства FieldType объекта Fieldlnfo. При помощи свойства Attributes можно узнать, является ли поле закрытым или открытым.

' Получить имя и тип поля

Console.Write (" Member: " + members(Index).Name + _

" Type:" + fi .FieldType.ToString()) 

' Прочитать атрибуты поля и проверить, 

'является ли оно закрытым или открытым.

 If (fi.Attributes And FieldAttributes.Public) <> 0 Then

Console.WriteLine (" - is Public")

 End If

 If (fi.Attributes And FieldAttributes.Private) <> 0 Then

Console.WriteLine (" - is Private")

 End If

' См. следующий пример.

1ShowCustomAttributes(f i .GetCustomAttributes() )

 Next Index 

End Sub

Результат выглядит следующим образом:

Type: Reflection.Modulel

Type: Reflection.

Type: Reflection.ss

Member: X Type:System.Int32 - is Public

Member: Y Type:System.Int32 - is Private

Type: Reflection.m 

Enumeration names are:

 FirstMember = 1

 SecondMember = 2 

Member: value_Type:System.Int32 - is Public

Как видите, пространство имен System. Reflection позволяет легко получить информацию о сборке во время выполнения программы. В оставшейся части этой главы основное внимание уделяется практическому применению этой возможности.

 

Пользовательские атрибуты

Откуда берутся атрибуты?

Одни встраиваются в язык, другие определяются в .NET Frameworks. Впрочем, у вас также есть возможность определять собственные атрибуты.

Проект Attributes показывает, как определить атрибуты для хранения в манифесте служебной информации, например истории вносимых изменений.

Атрибут представляется классом, производным от класса System.Attributes. Имя класса задается в форме MMflAttribute. Так, в нашем примере для создания атрибута Modified определяется класс ModifiedAttribute. При ссылках на атрибут указывается его имя с суффиксом Attribute или без него. Например, на атрибут AttributeUsage можно ссылаться как в виде AttributeUsage, так и в виде AttributeUsageAttribute.

Поведение атрибута в новом классе также определяется специальными атрибутами. Атрибут AttributeUsage имеет конструктор, которому в качестве параметра передается комбинация флагов перечисляемого типа AttributeTargets. Флаги AttributeTargets определяют типы объектов, к которым может применяться данный атрибут. Как показывает следующий фрагмент, атрибут Modified из нашего примера может устанавливаться для методов, свойств, классов и полей данных.

' Рефлексия и атрибуты, пример II

' Copyright ©2001 by Desaware Inc. All Rights Reserved

Public font>

<AttributeUsage(AttributeTargets.Method Or _

 AttributeTargets.Property Or _

AttributeTargets.buteTargets.Field, 

AllowMultiple:=True)> Public dAttribute

Inherits System.Attribute

Для атрибута AttrubuteUsage можно установить два дополнительных свойства. Свойство Inherited указывает, должен ли новый атрибут наследоваться производными классами, а свойство AllowMultiple определяет возможность многократного применения атрибута к объекту.

Возникает интересный вопрос: как задать значения свойств в конструкторе?

Конструктор атрибута

Рассмотрим простой класс:

Public Sub New()

End Sub

Public X As Integer 

End

Экземпляр этого класса можно создать командой вида:

 Dim С As New My

Однако для того, чтобы значение поля X задавалось в конструкторе, необходимо определить конструктор с целочисленным параметром:

Public Sub New(ByVal NewX As Integer)

X = NewX 

End Sub

Конечно, при создании класса можно определить несколько конструкторов, однако для атрибутов предусмотрена и другая возможность. Ниже приведен простой атрибут MyAttribute, который может устанавливаться только для классов.

< Attri but ells age (Attri buteTargets. ssMyAttributeAttribute

Inherits System.Attribute

Public Sub New()

End Sub

Public X As Integer 

End

Естественно, для применения этого атрибута можно воспользоваться конструктором по умолчанию:

<MyAttribute()> Public /font>

End

Как видите, один из недостатков атрибутов заключается в том, что в момент установки атрибута нельзя вызывать методы соответствующего класса. Да, методы атрибутов можно вызывать при обращении к ним посредством рефлексии, но в момент непосредственного применения атрибута это не помогает. Тем не менее вы можете задавать значения свойств и полей данных, используя знакомый синтаксис именованных свойств: 

<MyAttribute(X:=5)> Public

End

Таким образом, на практике бывает удобно определять атрибуты с различными свойствами и полями, значения которых задаются в момент установки атрибута без применения длинных и сложных конструкторов.

Оператор присваивания : =, используемый при передаче именованных параметров функциям, знаком многим программистам VB. Приведенный выше синтаксис, при котором он задает значения свойств, может использоваться только для атрибутов.

Снова об атрибуте Modified

Давайте вернемся к атрибуту Modified. В соответствии со значениями AttributeUsage этот атрибут может устанавливаться для методов, свойств, классов и полей данных. Также допускается многократное применение этого атрибута. Атрибут Modified предназначен для пометки изменений программного кода, поэтому многократное применение выглядит вполне логично: элемент программы может изменяться несколько раз.

Помимо кода, приведенного выше, для этого атрибута определяются три открытых поля: Author, ModDate и SomelntValue. В листинге 11.1 приведен конструктор, при вызове которого передается автор и дата изменения.

Листинг 11.1. Определение и использование пользовательских атрибутов

Public font>

<AttributeUsage(AttributeTargets.Method Or

 AttributeTargets.Property Or _

AttributeTargets.buteTargets.Field,

  AllowMultiple:=True)> Public dAttribute

 Inherits System.Attribute 

Public Author As String 

Public ModDate As String

Public Sub New(ByVal SetAuthor As String, _ 

ByVal SetModDate As String)

 MyBase.New()

 Author = SetAuthor

 ModDate = SetModDate

 End Sub

Public Overrides Function ToString() As String

Return ("Modified by " + Author + " on " + ModDate)

 End Function

Public SomelntValue As Integer 

End

1Все исходные тексты можно найти на сайте издательства «Питер» www.piter.conp. —Примеч. ред.

Класс ModifiedAttribute также переопределяет метод ToString для вывода сообщений об изменениях. В следующем фрагменте определяется класс с именем Testизмененный автором Dan 10 октября 2001 года. Класс содержит поле X, которое изменялось дважды (авторы изменений — Dan и Joe). Атрибут также присваивает полю SomelntValue значение 5 (просто для того, чтобы вы лучше поняли, как это делается).

<Modif iedC'Dan", "1/10/2001") > Public

 Testfied("Dan", "1/13/2001"), Modified("Joe", "1/25/2001",

SomeIntValue:=5)> Public X As Integer

 Private Y As Integer

End

Public Enum TestEnum

FirstMember = 1

SecondMember = 2

 End Enum 

End

Чтение пользовательских атрибутов

Программа, демонстрирующая операции с пользовательскими атрибутами, построена на базе примера Reflection (см. раздел «Рефлексия» этой главы). В функцию ShowMembers добавляется команда 

ShowCustomAttributes(fi.GetCustomAttributes(True))

Метод GetCustomAttributes класса Fieldlnfo возвращает массив пользовательских атрибутов для заданного поля.

В метод AssemblyTypes добавляется команда

  ShowCustomAttributes(ATypes(TypeIndex).GetCustomAttributes(True))

При помощи метода GetGustomAttributes мы получаем массив пользовательских атрибутов для классов, найденных методом AssemblyTypes. Метод ShowCustomAttributes определяется следующим образом:

Private Sub ShowCustomAttributes (ByVal TheAttributes() As Object) 

Dim ca As System.Attribute

 Dim idx As Integer

For idx = 0 To UBound(TheAttributes) 

ca = CType(TheAttributes(idx), System.Attribute)

 Console.WriteLine (" Attribute: " + ca .ToString() )

 Next

 End Sub

Методу ShowCustomAttributes в качестве параметра передается массив объектов, возвращаемый методом GetCustomAttributes. Объекты массива соответствуют пользовательским атрибутам, которые, как и все пользовательские атрибуты, являются производным от класса System. Attributes. Функция перебирает все элементы массива, обращается к каждому объекту через переменную типа System.Attributes и затем выводит пользовательский атрибут методом ToString.

Хотя механизм вызова методов при установке атрибутов в Visual Basic .NET не предусмотрен, при работе с объектами атрибутов можно легко организовать вызов методов посредством рефлексии.

При выполнении программы будет получен следующий результат:

  Type: Attributes.

Type: Attributes.dAttribute

Attribute: System.AttributeUsageAttribute

  Member: Author Type:System.String - is Public

  Member: ModDate Type:System.String - is Public

  Member: SomelntValue Type:System.Int32 - is Public

Type: Attributes.ss

Attribute: Modified by Dan on 1/10/2001 

Member: X Type:System.Int32 - is Public

Attribute: Modified by Joe on 1/25/2001

Attribute: Modified by Dan on 1/13/2001 

Member: Y Type:System.Int32 - is Private

Type: Attributes.m 

Enumeration names are:

FirstMember = 1

SecondMember = 2 Member: value_Type:System.Int32 - is Public

Type: Attributes.Modulel

В данном примере мы воспользовались рефлексией для чтения атрибута Modified. Представьте себе утилиту, которая использует рефлексию для документирования сборки и получает не только имена и свойства объектов, но и информацию об авторах и датах модификаций, а также любые другие сведения, для которых вы определите атрибуты. По аналогии с тем, как исполнительная среда использует атрибуты .NET Framework для управления поведением объектов и приложений, вы тоже можете читать атрибуты и управлять поведением своих программ.

При программировании компонентов и управляющих элементов в VB .NET вам часто придется использовать такие атрибуты, как Browsable, Category, Description и Bindable, определенные в классе System.ComponentModel. От этих атрибутов зависит интерпретация свойств средой разработки .NET. Например, если вы при помощи атрибута Category укажете, что свойство принадлежит к категории Appearance, то при работе с компонентом на стадии конструирования это свойство появится в группе Appearance окна свойств. IDE использует рефлексию для получения данных о принадлежности свойств к категориям1.

1 Да, помимо стадий компиляции и выполнения в VB .NET также приходится учитывать поведение компонентов на стадии конструирования. Все программисты VB, которым приходилось программировать элементы ActiveX, хорошо знакомы с этой концепцией.

Приложение DumpLib дополняет приложения Reflection и Attributes и показывает, как получить информацию о методах, свойствах и параметрах для каждого объекта в сборке. Оно строит простую базу данных с информацией о членах классов и выводит ее содержимое в стандартном формате CSV (разделение данных запятыми)1.

1Я создал программу DumpLib для того, чтобы мне было проще сравнивать методы и константы в VB6 и VB .NET во время работы над книгой. Эта программа приводится как дополнительный пример использования рефлексии без каких-либо пояснений.

 

Связывание

Во время первой загрузки сборки JIT-компилятор на основании информации манифеста компилирует IL-код в машинный код. Обращения к членам классов в полученном коде реализуются очень эффективно: компилятор может установить местонахождение каждого члена и сгенерировать прямое обращение по соответствующим адресам2. Такой механизм называется ранним связыванием (early binding).

Раннее связывание используется в тех случаях, когда компилятору известны типы объекта и его члена.

2Подробности низкоуровневой реализации раннего связывания совершенно несущественны. Неважно, вызывается ли функция напрямую, через v-таблицу (таблицу виртуальных функций), через смещение указателя и т. д. Достаточно знать, что раннее связывание работает быстро, а указатели и смещения при нем жестко фиксируются.

Раннее связывание и «кошмар DLL»

СОМ тоже использует раннее связывание в тех случаях, когда компилятору известны типы объекта3 и члена. Допустим, у вас имеется СОМ-программа, использующая объект из COM DLL. Приложение просматривает библиотеку типов DLL и осуществляет связывание членов на основании прочитанных данных. Таким образом, если приложение располагает указателем на объект, находящийся в DLL, оно будет обращаться к его свойствам и методам через этот указатель.

Но что произойдет, если изменить исходный текст DLL с изменением порядка членов в объекте и построить DLL заново?

Приложение СОМ о перемещении ничего не знает, поэтому при попытке вызвать метод с фиксированным смещением из предыдущей версии в итоге может быть вызван другой метод новой DLL или попытка вызова несуществующего метода завершится сбоем. Поэтому в СОМ очень важно, чтобы новые DLL были полностью совместимы со своими предыдущими версиями. Ошибки совместимости часто приводят к ошибкам защиты памяти. Даже простая перестановка членов в объекте может стать причиной несовместимости. Переименование членов и параметров приводит к еще худшим бедам.

Приложения .NET тоже используют раннее связывание, хотя машинный код генерируется на основании манифеста вызываемой .NET DLL, а не библиотеки типов. Однако при создании сборки приложения компилятор .NET сохраняет манифест используемой .NET DLL в виде сигнатуры4. При каждой загрузке приложение проверяет, соответствует ли сигнатура DLL той, что была сохранена при создании приложения.

3В терминологии СОМ правильнее было бы сказать «тип интерфейса».

4Несколько упрощенное представление. На самом деле сохраняемая информация зависит от конфигурации, использованной при построении. Дополнительная информация приведена в главе 16.

Допустим, автор .NET DLL создает новую версию DLL, изменяет порядок следования методов и добавляет несколько новых членов.

Приложение, использующее эту DLL, в процессе загрузки обнаруживает, что сигнатура DLL изменилась. Если конфигурация приложения допускает использование обновленных компонентов1, CLR понимает, что все вызовы с ранним связыванием могут завершиться неудачей. Кэшированная версия приложения на машинном коде уничтожается, и приложение обрабатывается заново JIT-компи-лятором с использованием данных манифеста из новой DLL.

Что если создатель обновленной DLL допустил ошибку и изменил тип параметра при вызове метода?

В этом случае JIT-компилятор обнаружит ошибку на стадии компиляции и приложение не загрузится.

Теперь вы понимаете, почему в сборках .NET данные манифеста хранятся вместе с IL-кодом даже после их компиляции в машинный код?

Безопасное применении раннего связывания в приложениях .NET позволяет решить многие проблемы, из-за которых в приложениях СОМ приходилось прибегать к позднему связыванию.

 1Сборку можно настроить так, чтобы она работала только с конкретной версией DLL.

 

Позднее связывание

Термин «позднее связывание» (late binding) означает, что при вызове метода приложение вычисляет его местонахождение во время выполнения программы. ' Иначе говоря, программа знает только имя метода и определяет его местонахождение по информации типа того объекта, к которому оно обращается. При позднем связывании может оказаться, что вызываемый метод не существует, поэтому в программе необходимо организовать перехват и обработку ошибок.

Позднее связывание в СОМ реализуется при помощи интерфейса IDispatch, реализованного всеми классами VB6. Этот интерфейс содержит метод Invoke, который получает имя метода/свойства и вызывает его с заданными параметрами2. В Visual Basic 6 позднее связывание используется при вызове методов обобщенного типа Object.

Visual Basic .NET поддерживает позднее связывание, хотя при этом он не использует ни средства СОМ, ни интерфейс IDispatch. Чуть позже в этой главе вы узнаете, как позднее связывание реализовано в .NET, а пока давайте рассмотрим один из способов (неправильный!) применения позднего связывания в .NET (листинг 11.2).

2Для экспертов в области СОМ такое объяснение выглядит несколько упрощенно, но мы не будем вдаваться в тонкости.

Листинг 11.2. Неверный подход к позднему связыванию в VB .NET

' Позднее связывание пример #1

' Copyright ©2001 by Desaware Inc. All Rights Reserved

Option Strict Off

Interface ITestlnterfacel 

Sub Test()

End Interface

Interface ITestlnterface2 

Sub Test()

End Interface

Implements ITestlnterfacel

Implements ITestInterface2

Sub TestK) Implements ITestlnterfacel.Test

 Console.WriteLine ("Testl called")

End Sub

Sub Test2() Implements ITestlnterface2.Test 

Console.WriteLine ("Test2 called")

End Sub

 End

Module Modulel

Sub Main()

 Dim obj As Object

 Dim A()

 Dim itl As ITestlnterfacel

 Dim it2 As ITestlnterface2

A/font>

 A/font>

 obj = A

 obj.Test1()

 obj.Test2()

 Try

obj.Test3()

 Catch e As Exception

Console.WriteLine ("Late binding error: " & e.Message)

 End Try 

itl = Aont>

itl.Test()

 obj = itl 

Try

obj .Test()

 Catch e As Exception

Console.WriteLine ("Can't late bind to implemented interface")

 End Try

obj.TestK) Console.ReadLine() 

End Sub

End Module

 Результат:

Testl called 

Test2 called

Testl called

Test2 called

Late binding error: Method "LateBinding.A.TestB" not found

Testl called

Can't late bind to implemented interface

Testl called

Вызовы Aont> и Aont> проходят раннее связывание с непосредственным вызовом методов Testl и Test2, реализованных классом.

Методы obj.Testl и obj.Test2 проходят позднее связывание. Поскольку эти методы вызываются для типа Object (который сам по себе не содержит методов Test и Test2), CLR приходится искать этот метод во время выполнения программы и вызывать его. Как видите, попытка вызова Test3 завершается неудачей с исключением «Method not found» («метод не найден»).

Как и в VB6, на объекты можно ссылаться по реализованным ими интерфейсам, однако для реализованного интерфейса нельзя использовать позднее связывание. В VB6 при присваивании интерфейса переменной типа Object вы получаете доступ к интерфейсу и можете вызывать его методы через объектную переменную. В VB .NET этот вариант не работает.

Почему такой подход к позднему связыванию завершается неудачей?

Во всем виновата первая строка программы: Option Strict Off

Как было сказано выше, в приложениях VB .NET всегда следует включать жесткую проверку типов, и этот пример лишь подтверждает правило: недопустимый вызов obj.Test3() обнаруживается лишь во время выполнения. Подобные проблемы всегда должны обнаруживаться на стадии компиляции, поэтому Visual Basic .NET не разрешает позднее связывание такого рода при установленном флажке Option Strict.

Позднее связывание: правильный подход

Итак, флажок Option Strict установлен. Теперь я покажу, как правильно организовать позднее связывание в VB .NET.

Как упоминалось выше, в СОМ позднее связывание основано на интерфейсе IDispatch, поэтому объекты с поддержкой позднего связывания должны реализовать IDispatch. Реализация должна знать все члены, к которым объект желает предоставить доступ посредством позднего связывания, и обеспечить вызовы этих методов и свойств при вызове метода IDispatch. Invoke. Эта задача бывает довольно сложной, хотя при создании компонентов СОМ в ATL и MFC построение необходимого кода автоматизировано. Конечно, в VB6 все делается автоматически.

В приложениях .NET вся информация, необходимая для вызова методов объектов, находится в манифесте, поэтому вполне логично предположить, что в VB .NET позднее связывание основано на применении рефлексии1.

1В главе 10 было показано, что позднее связывание может быть реализовано с применением делегатов. Делегаты обладают всей информацией, необходимой для позднего связывания, поскольку они ассоциируются с конкретным объектом и/или конкретным методом и сигнатурой.

На форме приложения IndirectCalls (рис. 11.4) пользователь вводит имя и значение параметра вызываемой функции.

Рис. 11.4. Форма приложения IndirectCalls

Текстовым полям присвоены имена txtFunсtiоn и txtРаrametеr. Кнопка называется cmdCalllt, а статическая надпись под кнопкой, предназначенная для вывода результата — IblResult. Приведенный ниже класс myTestемонстрирует позднее связывание (или косвенный вызов функции — в данном контексте эти термины имеют одинаковый смысл).

Public ass

 Public Function A(ByVal InputValue As Integer) As Integer

Return InputValue * 2

 End Function 

Public Function B(ByVal InputValue As Integer) As Integer

Return InputValue * 3 

End Function 

Public Function C(ByVal InputValue As Integer) As Integer

Return InputValue * 4 

End Function

End

Процедура события cmdCallIt_Click (листинг 11.3) показывает, как использовать рефлексию для косвенного вызова функции.

Листинг 11.3. Позднее связывание на базе метода InvokeMethod

Private Sub cmdCallIt_Click(ByVal sender As System.Object, _

 ByVal e As System.EventArgs) Handles cmdCalllt.Click

 Dim obj As New myTest/font>

Dim T As Type 

Dim Params(0) As Object 

Dim result As Integer

 ' Целое число упаковывается в объект.

 Try

Params(0) = CInt(txtParameter().Text)

 Catch ex As Exception

MsgBox ("Must enter a number")

Exit Sub 

End Try

T = obj .GetType() 

Try

result = CInt(Т.InvokeMember(txtFunction().Text, _

 Reflection.BindingFlags.Default Or _

Reflection.BindingFlags.InvokeMethod, Nothing, obj, Params)) 

IblResult() .Text = "Result is " + result.ToString()

 Catch ex As Exception

MsgBox (ex.ToString())

 End Try

End Sub

Метод GetType класса myTestозвращает информацию типа для объекта (информация берется из манифеста). Вызов функции осуществляется методом InvokeMember объекта Туре.

Первый параметр InvokeMember определяет имя члена класса. Флаги BindingFlags передают CLR рекомендации о том, как должно осуществляться связывание. В нашем примере выбирается стандартный поиск с последующим вызовом метода. Следующий параметр определяет объект, выполняющий связывание. Мы передаем Nothing, чтобы использовать стандартное связывание (нестандартные объекты связывания создаются в тех случаях, когда вам по какой-либо причине требуется дополнительно управлять выбором члена при вызове InvokeMember). Следующий параметр определяет объект, метод которого вы хотите вызвать. Помните, что метод InvokeMethod вызывается не для объекта класса myTestа для объекта Туре, содержащего информацию типа для объекта myTest Следовательно, вы должны предоставить ссылку на объект для вызова метода. Наконец, в последнем параметре передается массив объектов. У InvokeMethod существуют дополнительные перегруженные версии, обеспечивающие поддержку именованных параметров и данных локального контекста1. Они используются в ситуациях, когда метод должен вызываться в другом локальном контексте.

1В .NET вместо термина «локальный контекст» (locale) используется термин «культура» (culture).

Динамическая загрузка

В последнем примере этой главы концепция позднего связывания доведена до логического завершения. Приложение DynamicLoading показывает, что динамически выбирать можно не только метод, но и объекты со сборками.

Каталог DynamicLoading содержит подкаталоги двух вспомогательных проектов. В первом каталоге находится сборка LaterBinding со следующим классом:

' Демонстрация "очень позднего" связывания

 ' Copyright ©2001 by Desaware Inc. All Rights Reserved 

Public ynamically 

Public Sub Test()

MsgBox("The LoadltDyamically Test method was invoked", _

 MsgBoxStyle . Information, "Important message")

 End Sub

End

В сборке LaterBinding выбрано корневое пространство имен MovingToVB. LaterBinding. Сборка компилируется в отдельную DLL. Мы хотим загрузить DLL во время выполнения программы, создать экземпляр класса LoadltDynamically и вызвать метод Test. Задача решается во втором проекте LaterBindingCaller при помощи кода, приведенного в листинге 11.4.

Листинг 11.4. Проект LaterBindingCaller

1Пример позднего связывания

' Copyright ©2001 by Desaware Inc. All Rights Reserved.

Imports System.Reflection

Module Modulel

Sub Main()

Dim A As Reflection.Assembly 

Dim LaterBindingDLL As String

 Dim obj As Object 

Dim Params() As Object

' Navigate to the other DLL 

LaterBindingDLL = CurDir() & _ 

"\. .\. .\laterbinding\bin\laterbinding.dll"

 A = A.LoadFrom(LaterBindingDLL)

obj = A. CreatelnstanceC1MovingToVB. LaterBinding. LoadltDynamically")

 obj .GetTypeO . InvokeMember("Test", BindingFlags.Default Or _

  BindingFlags.InvokeMethod, Nothing, obj, Params) 

End Sub

End Module

Как видите, в программе нет ничего сложного. Метод Assembly. LoadFrom получает имя DLL в виде параметра и загружает сборку. Метод Createlnstance создает объект по полностью уточненному имени.

После того как объект будет создан, мы вызываем метод InvokeMember, как это было сделано в проекте IndirectCall.

 

Итоги

В начале этой главы мы рассмотрели основные различия между компиляторами и интерпретаторами, играющие важную роль для понимания различий между стадией компиляции и стадией выполнения. Вы узнали, что атрибуты определяются на стадии компиляции и обладают значительно большими возможностями, чем традиционный механизм условной компиляции. Также было показано, что хотя атрибуты в первую очередь предназначены для управления компиляцией, они также могут использоваться для управления поведением программ на стадии выполнения.

Далее было показано, как читать из манифеста сборки различную информацию, включая имена типов и методов, а также значения атрибутов. Мы рассмотрели процесс определения пользовательских атрибутов, их применение для документирования сборок и чтение на стадии выполнения.

Завершающая часть главы посвящена связыванию. В архитектурах СОМ и .NET раннее связывание обеспечивает более высокую эффективность, однако в СОМ оно часто порождает проблемы совместимости. В .NET эти проблемы решаются хранением манифеста и IL-кода, что позволяет перекомпилировать сборки по мере необходимости.

В конце главы представлены два подхода к позднему связыванию в .NET: правильный и неправильный. Развивая концепцию позднего связывания, мы рассмотрели возможность динамической загрузки сборки и объекта на стадии выполнения.

 

Лирическое отступление

Прежде чем продолжать, я хочу поговорить с вами по душам.

Я хочу, чтобы вы знали, чего ожидать от оставшихся глав, а чего ожидать не стоит.

Видите ли, до сих пор моя задача была относительно простой.

В части 1 были рассмотрены основные стратегические факторы, которые следует учитывать при переходе на VB .NET.

В части 2 излагались ключевые концепции, необходимые для проектирования приложений VB .NET.

В предыдущих четырех главах было приведено подробное, добросовестное описание изменений в синтаксисе языка VB .NET.

Но с остальными главами дело обстоит иначе. Они посвящены изменениям языка, связанным с классами .NET Framework и с исполнительной средой. Иногда речь идет о замене некоторых средств VB6 целыми пространствами имен .NET, но многие изменения обусловлены изменениями базовой архитектуры (например, новый механизм форм, новые web-технологии и т. д.) или появлением новых концепций .NET Framework, совершенно незнакомых программистам VB6. Во всех перечисленных случаях соответствующая функциональность представлена десятками объектов, каждый из которых обладает многочисленными свойствами и методами, а иногда она складывается из нескольких пространств имен с десятками объектов.

Если вы рассчитываете получить подробное описание по всем подобным темам, вы будете разочарованы. Более того, по многим из них вполне можно написать отдельную книгу!1

Что мне делать?

Единственное, что я могу, — придерживаться того курса, который я выбрал с первых страниц. Как было сказано выше, я не собираюсь пересказывать документацию Microsoft. В своей книге я хочу познакомить читателя с VB .NET, описать некоторые базовые концепции и поделиться своим мнением.

1 Это замечание будет часто встречаться в следующих главах. Кстати, если вам вдруг захочется написать такую книгу, пожалуйста, дайте мне знать.

А теперь запомните самое главное правило для всех следующих глав.

  •  Обязательно прочитайте электронную документацию. Вы должны хорошо ориентироваться в пространствах имен, в объектах и их методах.

Многие программисты VB6 не привыкли читать документацию. У них есть для этого веские основания: документация Microsoft для программистов Visual Basic написана очень неудобно. Объявления C++ во многих случаях так плохо переносились в VB6, что мне пришлось написать отдельную книгу лишь с одной целью: научить программистов VB читать MSDN и создавать объявления функций для программ VB6. Многие функции, определенные в MSDN, несовместимы с Visual Basic, причем иногда было трудно отличить совместимые функции от несовместимых.

Теперь ситуация кардинально изменилась.

Язык VB .NET обеспечивает полную CLS-совместимость. Классы .NET Framework полностью совместимы с VB .NET. В документацию Microsoft включены синтаксические конструкции VB .NET для вызовов методов и обращений к свойствам. Многие примеры написаны на VB .NET, а там, где код VB .NET отсутствует, примеры С# практически построчно переводятся на VB .NET — код вызова методов/свойств в С# и VB .NET выглядит практически одинаково.

Что касается меня, то я постараюсь сразу выделить основные концепции и ключевые классы, с которыми вам предстоит работать. Но я при всем желании не смогу привести подробные описания всех технических мелочей, которые приходится учитывать при переходе с VB6 на VB .NET. У каждого элемента, у каждого объекта имеются свои особенности. Вы должны быть готовы к самостоятельному чтению документации MSDN. Co временем на рынке непременно появятся книги, которые помогут вам разобраться в отдельных темах.

А пока можете рассматривать следующие главы как введение в .NET-состав-ляющую VB .NET (тогда как до настоящего момента речь шла об основной функциональности языка). В этой части книги я постараюсь дать представление о доступных возможностях и заложить основу для дальнейшего изучения VB .NET. Мы рассмотрим стратегические и концептуальные аспекты разных компонентов .NET и даже разберем несколько примеров программ. Главное — помнить о том, что очень многое осталось «за кадром», и быть готовым к самостоятельным исследованиям.

Назад   Вперёд

 


Инфо
Сайт создан: 20 июня 2015 г.
Рейтинг@Mail.ru
Главная страница