В 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. Определение и использование пользовательских атрибутов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 и даже разберем несколько примеров программ. Главное — помнить о том, что очень многое осталось «за кадром», и быть готовым к самостоятельным исследованиям.
Инфо
|