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

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

Объекты

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

 

Структура приложения .NET

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

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

 

Приложение

В мире VB6 (и в программировании до появления .NET вообще) исполняемые файлы делились на две основные категории: ЕХЕ-файлы и DLL-файлы. Ниже перечислены их основные характеристики. ЕХЕ-файл:

Используя термин «приложение» или «программа», мы в действительности имеем в виду ЕХЕ-файлы. DLL-файл:

Хотя .NET представляет немало радикальных изменений для разработчиков, эта платформа работает на базе Windows и использует ЕХЕ- и DLL-файлы, которые работают так же, как и прежде. Зачем же тогда вносить дополнительную путаницу с «доменом сборки» и «доменом приложения»? Другими словами, если работа ЕХЕ- и DLL-файлов совершенно не изменилась, а приложения .NET состоят из ЕХЕ- и DLL-файлов, стоит ли усложнять вопрос?

Причины этого решения (как и многих других изменений в .NET) на первый взгляд совершенно не относятся к делу. Более того, они вам уже известны.

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

Интересно заметить, что по этой же причине в 32-разрядных версиях Windows были разделены адресные пространства процессов2. Изоляция адресных пространств является самым важным различием между ЕХЕ и DLL. 

1VB6 DLL могли создавать новые потоки лишь при помощи компонентов независимых фирм.

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

Каждый  ЕХЕ-файл загружается в отдельном адресном пространстве, тогда как каждая DLL загружается в одном адресном пространстве с загрузившим ее ЕХЕ-файлом. Однако с появлением .NET жесткое управление доступом ко всем объектам в CLR позволяет применить аналогичную изоляцию на уровне отдельных составляющих процесса. Иначе говоря, вы можете создать программный компонент, оформленный в виде DLL, и указать, что объекты и переменные этого компонента полностью изолируются от приложения, использующего компонент, и от всех остальных компонентов процесса. Вне архитектуры .NET это можно сделать лишь одним способом — оформить компонент в виде внешнего СОМ-сервера, чтобы при каждом создании экземпляра компонента запускался новый процесс. Несомненно, подход .NET гораздо эффективнее — запуск процессов требует лишних затрат. Более того, при решении с ActiveX EXE с каждым компонентом должен быть связан отдельный поток1. Поскольку компоненты .NET реализованы в виде DLL, они вполне допускают совместное использование потоков. На рис. 10.1, а и 10.1, б изображены возможные варианты реализации приложения, использующего три компонента для решения самостоятельных задач, например реализации правил бизнес-логики или запуска web-приложений.

Рис. 10.1. Разделение процессов гарантирует, что компоненты не будут мешать работе других компонентов (a). DLL загружаются в пространство основного процесса. С точки зрения операционной системы они используют память совместно, но CLR полностью изолирует их друг от друга (б)

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

Домен приложения может быть реализован в виде ЕХЕ-файла с дополнительными DLL-файлами2. Возможно и другое решение — оформление в виде одного или нескольких DLL-файлов. При загрузке в память DLL домены приложения изолируются в процессе от других доменов приложения.

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

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

Домен приложения:

Способ оформления домена приложения зависит от типа приложения. В автономных программах домен приложения обычно оформляется в виде ЕХЕ-файла. В этом случае термин «домен приложения» достаточно близок по смыслу к традиционным ЕХЕ-приложениям.

В web-приложениях домены приложений обычно оформляются в виде DLL. Это обеспечивает безопасную работу web-приложений в процессе web-сервера и одновременно не позволяет им мешать работе друг друга. Отдельные домены приложений даже можно отлаживать из других доменов, работающих под управлением той же программы.

Используя домен приложения, вы используете все его ЕХЕ- и DLL-файлы. Впрочем, в .NET существует особый подход к этим файлам и организации программного кода.

 

Сборки

Вы только что прочитали, что домен приложения состоит из одного или нескольких исполняемых файлов. Означает ли это, что все исполняемые файлы должны загружаться одновременно?

Нет.

По аналогии с тем, как традиционное приложение Windows может загружать отдельные DLL или запускать ЕХЕ-серверы ActiveX по мере надобности, так и домен приложения может загружать отдельные сборки при возникновении необходимости в них. Собственно, термин «сборка» (assembly) и определяет минимальную единицу загрузки в домене приложения.

Сборка:

На рис. 10.2 изображена общая структура приложения .NET. Рисунок относится и к ASP-подобным сценариям, в которых каждый домен приложения представляет отдельное web-приложение или web-службу. Все домены приложения работают в одном процессе и совместно используют потоки по правилам, определяемым процессом. Каждый домен приложения состоит из одной или нескольких сборок, каждая из которых реализуется в виде одного или нескольких исполняемых файлов1. Автономное приложение VB .NET представляет собой отдельный процесс с одним доменом приложения, состоящим из одной или нескольких сборок2.

Рис. 10.2. Взаимосвязь между процессами, доменами приложений и сборками

Домены приложений и сборки подробно рассматриваются в главе 16 при обсуждении размещения и контроля версии.

1В VB .NET сборка может состоять только из одного исполняемого файла.

2VB .NET позволяет создавать приложения, управляющие несколькими доменами, но это нестандартная ситуация.

 

Область видимости в .NET

Как упоминалось выше, понятие «области видимости» в действительности состоит из двух разных аспектов: доступности и продолжительности жизни. Продолжительность жизни определяет сроки существования объекта, а доступность определяет возможность обращения к объекту из программы.

Чтобы понять, как работают области видимости в VB .NET, необходимо сначала разобраться в концепции «пространств имен».

Пространства имен

Домены приложения и сборки определяют структуру программы VB .NET с позиций загрузки, размещения и контроля версии. Пространства имен определяют структуру программы VB .NET с позиций видимости.

Каждый тип объекта в VB .NET обладает именем и входит в некоторое пространство имен.

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

Чтобы лучше понять, как работают пространства имен, попробуйте выполнить следующие действия.

1. Создайте консольное приложение VB .NET.

2. Откройте диалоговое окно Project Properties (щелкните правой кнопкой мыши на проекте в окне Solution Explorer и выберите команду Properties).

3. Очистите текстовое поле Root Namespace.

4. Включите в программу блок Namespace следующего вида:

Namespace MovingToVB.CH10.

 Module Modulel

Sub Main() 

End Sub

End Module 

End Namespace

В VB .NET используемое по умолчанию пространство имен сборки задается в диалоговом окне свойств проекта. Если вы захотите переопределить пространство имен сборки, поле Root Namespace обычно очищается, поскольку все содержимое этого поля становится префиксом имен, определяемых командой Namespace. Другими словами, если бы в нашем примере в текстовом поле остался текст «raquo;, то вместо MovingToVB. CH10. было бы определено пространство имен Сllas slExample.MovingToVB.CH 10. .

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

Namespace MovingToVB.CHlG./font>

 bsp;

End

bsp;

End

Разумеется, попытка завершится неудачей. Компилятор выводит ошибку вида «Modulel.vb(2):and conflict in namespace e» 1.

Короче говоря, создать два класса с одинаковыми именами вы не сможете.

Теперь приведите программу к следующему виду:

Namespace MovingToVB.CHIG.nbsp;

font>

 End

Module Modulel

Dim cl As New

Dim clb As New S2.>

Sub Main()

End Sub

End Module

End Namespace

Namespace MovingToVB.CH10.S2

font>

End ont>

End Namespace

Мы определили два класса в одной сборке! Конфликт предотвращается тем, что каждое имя определяется в отдельном пространстве имен. Как нетрудно убедиться, программный код одного пространства имен может легко работать с типами из другого пространства — для этого достаточно уточнить тип, указав пространство имен, к которому он относится.

Итак, вы убедились, что сборка может определять несколько пространств имен2. Хотя определять разные пространства имен водном файле обычно не рекомендуется, делать это в разных файлах проекта обычно вполне разумно. Более того, само иерархическое строение пространств имен способствует такому подходу. Если взглянуть на пространство System, вы увидите, что под уровнем System располагаются пространства имен второго уровня, причем большинство из них входит в одну DLL (сборку). Пространства имен часто используются для логической группировки классов.

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

1При попытке компиляции также будет выдана ошибка «в пространстве имен не определена функция Sub Main». Вернитесь в диалоговое окно Project Properties и выберите пространство имен для запуска программы.

2В VB .NET проект компилируется в одну сборку.

Размещение приложений

Заглянув в каталог Namespaces главы 10, вы увидите в нем подкаталоги двух проектов Console Application1 и GroupSupport. Корневое пространство имен в обоих проектах было очищено. В листинге 10.1 приведен модуль Modulel проекта Console Application 1

Листинг 10.1. Файл modulel.vb проекта ConsoleApplicationl

Imports MovingToVB.CH10.Organization.Members 

Namespace MovingToVB.CH10.Organization

tion 

End

Module Modulel

 Sub Main()

Dim org As New Organization() 

' Следующие две строки идентичны

' благодаря команде Imports.

Dim grp As New CHlO.Organization.Members.MemberCollection() 

Dim grp2 As New MemberCollection()

' Внимание - это объявление относится к другой сборке,

 ' хотя оно находится в том же пространстве имен! 

Dim sorter As New CH10.Organization.Members.MemberSorter() 

Dim sorter2 As New MemberSorter() 

End Sub

 End Module 

End Namespace

Namespace MovingToVB.CHlO.Organization.Members 

ber 

End

llection 

End ont>

End Namespace

В листинге 10.2 приведен модуль ont> проекта GroupSupport

Листинг 10.2. Файл t> проекта GroupSupport

Namespace MovingToVB.CHl0.Organization.Members 

Public rter

End ont>

End Namespace

Проект ConsoleApplicationl содержит два пространства имен. В пространстве MovingToVB.Chl0.Organization определяется главная программа и классы для управления гипотетической организацией; объекты для управления членами этой организации определяются во втором пространстве имен MovingToVB. Chl0.Organization.Members.

Наибольший интерес в проекте ConsoleApplicationl для нас представляет класс MemberSorter — он не является частью проекта! Класс MemberSorter определен в проекте GroupSupport, на который имеется ссылка в проекте ConsoleApplicationl2. Впрочем, это не мешает ему входить в пространство имен MovingToVB.Сh10. Organization.Members.

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

2Чтобы включить соответствующую ссылку в проект, щелкните правой кнопкой мыши на кнопке References в окне Solution Explorer и выберите команду Add Reference. Перейдите на вкладку Projects и выберите проект GroupSupport.

Да, все верно: в пространство имен может входить несколько сборок!

Зачем это нужно?

Предположим, Member Sorter — довольно большой и сложный класс, используемый относительно редко. Логически он относится к пространству имен MovingToVB. Chl0.Organization.Member, поскольку тесно связан с другими классами этого пространства имен. Однако всегда загружать код редко используемого класса было бы глупо. Если выделить класс MemberSorter в отдельную сборку, он будет загружаться только в случае надобности.

В более изощренной схеме размещения (например, при установке приложения по Интернету) возможны даже такие ситуации, когда сборка GroupSupport не устанавливается до ее использования!

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

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

Команда Imports

Команда Imports позволяет ссылаться на элементы пространств имен в укороченной записи. К объектам импортируемого пространства имен можно обращаться напрямую, не указывая полного имени пространства. Пример встречается в процедуре Sub Main проекта Console Application1, в котором ссылки на классы MemberCollection и MemberSorter задаются как полностью, так и сокращенно.

Что произойдет, если импортируемое пространство имен содержит класс с таким же именем, как и у класса в текущем пространстве имен? В этом случае предпочтение отдается классу из текущего пространства имен. Вы по-прежнему сможете обратиться к классу из другого пространства, но только по полному имени. При попытке импортировать два пространства с одноименными классами (и если класс с таким именем не определен в текущем пространстве имен) произойдет ошибка, поскольку компилятор не сможет определить, какому из двух импортированных пространств следует отдать предпочтение.

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

А что произойдет, если вы дополните существующее пространство имен (например, System)? Ваша сборка и все остальные сборки, ссылающиеся на вашу, увидят это дополнение. И все будет работать совершенно нормально — до того момента, когда Microsoft включит в пространство имен System класс с конфликтующим именем. Мораль: чужие пространства имен лучше не трогать!

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

Например, для обращения к константе CrLf приходится использовать запись вида:

Dim S As String = ControlChars.CrLf

Но если включить в программу строку:

 Imports Microsoft.VisualBasic.ControlChars

 на CrLf можно ссылаться иначе: 

Dim S As String = CrLf

Включение ссылки на пространство имен в проект часто путают с импортированием.

Чтобы включить в проект ссылку на пространство имен (а вернее, на реализующую его сборку), следует воспользоваться окном решения Solution или диалоговым окном свойств проекта. После создания ссылки на пространство имен вы можете обращаться ко всем его объектам и методам по полностью уточненным именам.

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

Имена и пространства имен

В VB6 и COM Microsoft решила проблему конфликтов имен посредством идентификации объектов по глобально-уникальным идентификаторам (GUID). В .NET пространства имен имеют иерархическую, наглядную структуру, с которой вы уже неоднократно встречались. Корневое пространство имен желательно выбирать так, чтобы оно было заведомо уникальным, например, название вашей организации.

Но что произойдет, если в системе установлены две конфликтующие версии пространства имен?

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

Но возможна и другая ситуация: сборка, на которую вы ссылаетесь, может изменить свое пространство имен так, что оно по каким-то причинам станет несовместимо с этой сборкой. Что делать?

Ответ — никто не может изменить сборку, на которую вы ссылаетесь, если только вы специально не разрешите такую модификацию или не выполните ее самостоятельно. Сборкам могут присваиваться «сильные» имена, включающие собственно имя сборки, версию, сигнатуру и другую информацию, по которой эту конкретную сборку можно отличить от других версий. Если приложение связывается с конкретной сборкой по «сильному» имени, вам никогда не придется беспокоиться об изменениях, нарушающих совместимость.

Имена сборок и контроль версий более подробно рассматриваются в главе 16.

 

Область видимости 1: уровень пространства имен

На уровне пространств имен определяются объекты четырех видов: классы, модули, структуры и перечисления.

Всем этим объектам может быть присвоен атрибут области видимости Friend или Public1.

Таблица 10.1. Атрибуты видимости

Сборка/проект

Везде

Public

Видимый

Видимый

Friend

Видимый

Скрытый

Смысл этих атрибутов продемонстрирован в приложении AssemblyScoping2.

Область видимости объекта на этом уровне определяет максимальную общую область видимости для всех элементов объекта. Например, Public-переменная в Public-классе видима и доступна для всех, кто имеет доступ к пространству имен.

Классы, структуры и перечисления знакомы вам как по предыдущему опыту программирования на VB6, так и из предшествующих глав книги, однако на модулях (Module) стоит задержаться чуть подробнее.

Как вы, возможно, заметили, в VB .NET не существует различий между файлами программного кода: все они имеют расширение .VB и одинаковую внутреннюю структуру. Модули VB .NET работают почти так же, как классы, с двумя принципиальными различиями.

1. Невозможно создать экземпляр модуля.

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

Как ни странно, С# не позволяет программисту создавать модули, но может использовать модули, определяемые в приложениях VB .NET (см. приложение ModuleScoping, входящее в примеры программ, но не рассматриваемое в книге).

1Атрибуты Private, Protected и Protected Friend применяются только к элементам классов и рассматриваются ниже. В версии бета-1 поддерживались закрытые элементы уровня пространства имен, видимые только в текущем файле.

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

Правила наследования

Visual Basic .NET не позволяет расширять область видимости базового класса в производных классах. Иначе говоря, Friend-класс может наследовать от Publiс-класса, поскольку видимость всех унаследованных членов будет меньше Public, но Public-класс не может наследовать от Friend-класса, поскольку в этом случае открытые члены класса Friend окажутся видимыми за пределами нормальной области видимости Friend.

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

Контроль наследования

Ключевое слово Mustlnherit указывает на то, что экземпляры данного класса создаваться не могут. Допускается лишь создание экземпляров производных классов.

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

Ключевое слово Shadows при определении вложенного класса указывает на то, что вложенный класс переопределяет соответствующие члены базового класса. Предположим, у вас имеется фрагмент проекта Inheritance 1, приведенный в листинге 10.3.

Листинг 10.3. Приложение Inheritance1 

/font>

Sub Test()

Console.WriteLine ("A.B.Test")

 End Sub

 End ont>

Sub Test()

Console.WriteLine ("A.D.Test")

 End Sub 

End

End

 Inherits 

A Shadows /font>

Sub Test()

Console.WriteLine ("C.B.Test")

 End Sub

 End

End

Module Modulel 

Sub Main()

Dim abref As New A.B() 

Dim cdref As New C.D()

 Dim cbref As New C.B() 

abref.Test() 

cdref .Test() 

cbref .Test()

 Console.ReadLine()

 End Sub

End Module

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

А.в.Test 

А.D.Test

 С.В.Test

Из-за ключевого слова Shadows определение класса В в классе С маскирует определение класса В в классе А.

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

 

Область видимости 1: уровень класса

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

Ниже приведена краткая сводка основных атрибутов доступа.

  •  Public — элемент доступен за пределами класса, в котором он определяется.
  •  Private — элемент недоступен за пределами класса, в котором он определяется.
  •  Friend — элемент доступен за пределами класса, но только в пределах сборки, в которой определяется класс.
  •  Protected — за пределами класса элемент доступен только в классах, производных от данного.
  •  Protected Friend — элемент доступен за пределами класса, но только в пределах сборки, в которой определяется класс, и в производных классах. 

Эта тема уже рассматривалась в главе 5, поэтому табл. 10.2 и 10.3 следует рассматривать как исчерпывающую сводку видимости членов классов. Для тестирования использовалось приложение font> (присутствует среди примеров, но не приводится в книге).

Таблица 10.2. Public-классы

Private

Friend

Protected

Protected Friend

Public

В классе

X

X

X

X

X

Ссылки на базовый класс в производных классах

X

X

X

X

Ссылки на производный класс в производных классах

X

X

X

Ссылки на класс внутри сборки

X

X

X

Ссылки на класс за пределами сборки

X

1Я говорю о классах, однако атрибуты видимости сохраняют смысл также в модулях и структурах (не считая аспектов наследования, относящихся только к классам). Помните, что атрибуты видимости также распространяются на классы и структуры, которые являются членами других классов.

Таблица 10.3. Friend-классы

Private

Friend

Protected

Protected Friend

Public

В классе

X

X

X

X

X

Ссылки на базовый класс в производных классах

X

X

X

X

Ссылки на производный класс в производных классах

X

X

X

Ссылки на класс внутри сборки

X

X

X

Ссылки на класс за пределами сборки

Унаследованный метод может быть скрыт от производного класса (даже если он объявлен с ключевым словом Friend или Publi с) при помощи ключевого слова Shadows.

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

Область видимости и многопоточность

Не забывайте о принципиальном различии между глобальными переменными VB6 и VB .NET. Глобальные переменные VB6 существуют на уровне потока. Следовательно, если вы создаете многопоточный ЕХЕ-файл или DLL в VB6, каждый поток будет обладав собственной копией глобальных переменных. В VB .NET глобальные переменные совместно используются всеми потоками, если только они не были объявлены локальными по отношению к потокам при помощи атрибута ThreadStatic (см. главу 7).

Учтите, что Upgrade Wizard не преобразует глобальные переменные VB6 в переменные VB .NET, локальные по отношению к потокам, и даже не предупреждает об этих различиях.

 

Дополнительно о классах

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

Общие переменные

Любую переменную класса можно снабдить атрибутом Shared. Синтаксис объявления общих переменных: Private Shared SharedVariable As Integer

Общие переменные совместно используются всеми экземплярами классов. У них имеется несколько стандартных применений.

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

Конечно, общие переменные также могут обладать атрибутами Friend, Protected, Friend Protected и Public.

Общие процедуры могут вызываться без указания конкретного экземпляра класса. В них не допускается обращение к членам класса без атрибута Shared.

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

В следующем фрагменте из проекта AssemblyScoping продемонстрировано использование общих переменных и процедур:

Public Demo 

Public Shared SharedVariable As Integer

 Public NotSharedVariable As Integer

 Shared Sub ShowShared()

Console.WriteLine ("Access using Shared Procedure: " & _ SharedVariable.ToString)

 End Sub

 End

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

Dim sh As New Scoping.SharingDemo()

Dim sh2 As New Scoping.SharingDemo()

sh.SharedVariable = 5

Console.WriteLine("5 indicates value was shared: " _

& sh2.SharedVariable.ToString) Console.WriteLine("Access without instance: " _

& Scoping.SharingDemo.SharedVariable.ToString)

 Scoping.SharingDemo.ShowShared()

К общим переменным можно обращаться как через экземпляр, так и напрямую по имени класса.

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

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

 

MyBase и My

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

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

Ключевое слово Myешает противоположную задачу. Если в базовом классе имеется функция, которая должна вызывать функцию базового класса (а не версию производного класса — возможно, переопределенную), ключевое слово Myарантирует, что будет вызвана именно версия базового класса.

Приложение MyBase Myлистинг 10.4) демонстрирует сказанное.

Листинг 10.4. Приложение MyBaseMyont>

Module Modulel

/font>

Overridable Sub Test()

console.WriteLine ("A.Test called")

 End Sub 

Sub ACallsTest()

console.Write ("A Calls Test directly: ")

Test ()

console.Write ("A Calls Myuot;)

My/font>

 EndSub

End

/font>

Inherits A

' Что произойдет, если объявить Test 

' с ключевым словом Shadows вместо Overrides? 

Overrides Sub Test()

console.WriteLine ("B.Test called")

 End Sub

 Sub BCallsTest()

console.Write ("B Calls Test directly: ")

Test()

console.Write ("B Calls MyBase.Test: ")

MyBase.Test() 

End Sub

 End

Sub Main ()

 Dim aref As New A() 

Dim bref As New B() 

console.WriteLine ("Object A")

 aref.ACallsTest()

console.WriteLine ("Object B") 

bref .ACaUsTest()

 console.WriteLine ("Object B")

 bref .BCallsTest()

 console.ReadLine() 

End Sub

End Module

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

Object A

A Calls Test directly: A.Test called

A Calls MyTest called

Object В

A Calls Test directly: B.Test called

A Calls MyTest called

Object В

В Calls Test directly: B.Test called

В Calls MyBase.Test: A.Test called

Первые три строки очевидны: класс А вызывает свой собственный метод в любом случае, как напрямую, так и через ключевое слово My

С четвертой и пятой строкой дело обстоит несколько сложнее. Объект является экземпляром класса В, однако вызывается метод класса А, унаследованный классом В. При первом вызове Test будет вызвана переопределенная версия. Чтобы вызвать исходную версию Test класса А, код класса А должен воспользоваться ключевым словом My

С двумя последними строками опять все просто. Когда функция Test вызывается из класса В, при отсутствии ключевого слова MyBase будет вызван метод класса В.

В качестве эксперимента попробуйте сменить атрибут функции Test в классе В с Overrides на Shadows. Результат выглядит так:

Object A

A Calls Test directly: A.Test called

A Calls MyTest called

Object В

A Calls Test directly: A.Test called

A Calls MyTest called

Object В

В Calls Test directly: B.Test called

В Calls MyBase.Test: A.Test called

Различия проявляются при вызове метода ACallsTest из объекта производного класса В. В обычных условиях при вызове Test из ACallsTest вызывается переопределенная функция Test производного класса, но при указании ключевого слова Shadows вызывается метод базового класса.

Ключевое слово Overrides означает, что метод производного класса должен вызываться вместо метода базового класса через ссылки как на базовый, так и производный класс. Ключевое слово Shadows означает, что метод производного класса заменяет метод базового класса лишь при вызове через производный класс.

Квалификаторы Myи MyBase не являются ссылками на классы и не могут передаваться в качестве параметров. Они не нарушают правил видимости;

в частности, при помощи MyBase невозможно обратиться к закрытой переменной базового класса. Они могут применяться как к общим членам, так и к членам конкретных экземпляров.

При использовании MyBase член класса не обязан присутствовать непосредственно в базовом классе: VB .NET просматривает цепочку наследования и находит в пей первый метод с заданным именем.

 

Вложенные классы

К числу приятных новшеств VB .NET относится возможность определения вложенных классов и структур в других классах. Вложенные классы и структуры прекрасно подходят для упорядочения данных в классах (особенно больших и сложных).

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

Область видимости вложенных классов определяется по тем же правилам, как и для других членов. Например, закрытый вложенный класс недоступен за пределами своего класса-контейнера. Visual Basic .NET не позволяет расширять видимость вложенного класса посредством обращения к нему через свойства или возвращаемые значения методов. Другими словами, в классе нельзя объявить Public-метод, возвращающий экземпляр Private- или Friend-класса.

 

Методы и свойства

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

Перегрузка функций

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

Sub Print (ByVal X As Integer)

Sub Print (ByVal X As String)

Sub Print (ByVal X As SomeObject)

В VB6 компилятор выдает ошибку, поскольку программа не может содержать функции или процедуры с одинаковыми именами. В VB .NET такие объявления вполне допустимы. Чтобы явно указать на выполняемую перегрузку, можно воспользоваться необязательным ключевым словом Overloads1:

Overloads Sub Print (ByVal X As Integer)

Overloads Sub Print (ByVal X As String)

Overloads Sub Print (ByVal X As SomeObject)

1При определении в классе одноименных методов с разными сигнатурами перегрузка происходит по умолчанию и ключевое слово Overloads указывать не нужно.

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

Перегрузка функций часто используется в .NET Framework. Например, функция Console.WriteLine, часто встречающаяся в примерах программ, определяется следующим образом:

Public Overloads Shared Sub WriteLineO

Public Overloads Shared Sub WriteLine(Boolean)

Public Overloads Shared Sub WriteLine(Char)

Public Overloads Shared Sub WriteLine(Char())

Public Overloads Shared Sub WriteLine(Decimal)

Public Overloads Shared Sub WriteLine(Double)

Public Overloads Shared Sub WriteLine(Integer)

Public Overloads Shared Sub WriteLine(Long)

Public Overloads Shared Sub WriteLine(Object)

Public Overloads Shared Sub WriteLine(Single)

Public Overloads Shared Sub WriteLine(String)

Publi.c Overloads Shared Sub Wri tel_ine(UInt32)

Public Overloads Shared Sub WriteLine(Uint64)

Public Overloads Shared Sub WriteLine(String, Object)

Public Overloads Shared Sub WriteLine(String, Object())

Public Overloads Shared Sub WriteLine(Char(), Integer, Integer)

Public Overloads Shared Sub Wri te!_ine(String, Object, Object)

Public Overloads Shared Sub WriteLine(String, Object, Object, Object)

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

Перегрузка функций хороша прежде всего тем, что она упрощает приложение. Что проще запомнить: одну функцию WriteLine с разными наборами параметров или пятнадцать разных функций с именами WriteStringLine, WritelntLine и т. д.?

Хотя ключевое слово Overloads при перегрузке методов класса необязательно, оно играет важную роль при работе с производными классами. При определении в производном классе метода, имя которого совпадает с именем метода базового класса, возможны три ситуации.

1. Методы базового и производного классов обладают одинаковыми сигнатурами (типами параметров и возвращаемого значения).

  •  Ключевое слово Overrides обеспечивает полиморфный вызов (метод производного класса всегда вызывается даже через ссылку на базовый класс). Если в базовом классе существуют другие методы с тем же именем и другими параметрами, используйте Overrides и Overloads.
  • Ключевые слова Overrides и NotOverridable обеспечивают полиморфный вызов с запретом переопределения метода дальнейшими производными классами. Если в базовом классе существуют другие методы с тем же именем и другими параметрами, используйте NotOverridable, Overrides и Overloads.
  • Ключевое слово Shadows (по умолчанию) обеспечивает вызов метода в зависимости от типа ссылки. Через ссылки на производный класс всегда вызывается метод производного класса, а все одноименные методы базового класса маскируются. Через ссылки на базовый класс всегда вызываются методы базового класса.

2. Методы базового и производного классов обладают разными сигнатурами (типами параметров и возвращаемого значения).

  •  Ключевое слово Shadows (по умолчанию) обеспечивает вызов метода в зависимости от типа ссылки. Через ссылки на производный класс всегда вызывается метод производного класса, а все одноименные методы базового класса маскируются. Через ссылки на базовый класс всегда вызываются методы базового класса.

3. Методы базового и производного классов различаются только по типам параметров.

  •  Ключевые слова Oveloads обеспечивает перегруженный вызов метода производного и базового классов в зависимости от типа используемых параметров.
  •  Ключевое слово Shadows (по умолчанию) обеспечивает вызов метода на основании типа ссылки. Через ссылки на производный класс всегда вызывается метод производного класса, а все одноименные методы базового класса маскируются. Через ссылки на базовый класс всегда вызываются методы базового класса.

Ниже приведен ряд относительно простых правил, помогающих лучше разобраться в смысле ключевых слов Overrides, Shadows и Overloads.

  •  Если класс не является производным от другого класса, об этих ключевых словах вообще можно не беспокоиться. Если в классе определяются два и более методов, различающихся только по типу параметров, перегрузка происходит автоматически.
  •  Используйте ключевое слово Shadows везде, где требуется создать производный класс и скрыть методы базового класса. Вызовы через объект базового класса никогда не будут передаваться методам производного класса, а вызовы через объект производного класса никогда не будут передаваться методам базового класса.
  •  Используйте ключевое слово Overrides в случае, когда метод должен вызываться в зависимости от истинного типа объекта (независимо от того, к какому типу относится переменная, используемая для ссылки на объект, — базовому или производному).
  • Используйте ключевое слово Overloads при добавлении метода, имя которого совпадает с именем метода базового класса, но набор параметров отличается от всех одноименных методов базового класса. При использовании ключевого слова Overloads в производном классе вам придется включать его в объявления всех методов с этим именем.
  •  Используйте ключевое слово NotOverridable, чтобы показать, что метод не может переопределяться в производных классах.

Все еще сомневаетесь? Не беда, компилятор VB .NET поможет во время ввода программы. Следуя его указаниям, я неизменно приходил к желаемому результату.

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

 

Конструкторы

Конструктором называется функция, вызываемая при создании объекта. В VB .NET конструктору присваивается имя New и его общее объявление выглядит так:

Public Sub New()

HyBase.New() 

End Sub

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

Обычно в конструкторе присутствует строка MyBase. New(), обеспечивающая вызов конструктора базового класса. Конструкторы не наследуются производными классами.

Конструкторы, как и другие методы, могут перегружаться. Это позволяет не только определять разные варианты инициализации объекта, но и управлять самим процессом его создания.

Область видимости конструктора

Каталог Overloads содержит подкаталоги двух проектов: библиотеки классов /font> и консольного приложения OverloadsTest. В библиотеке /font> определяются три класса. Первый класс, Internal имеет два конструктора: конструктор по умолчанию объявлен закрытым, а конструктор с параметром объявлен открытым. Поскольку оба конструктора принадлежат одному классу, указывать ключевое слово Overloads не нужно. Класс Internalприведен в листинге 10.5.

Листинг 10.5. Библиотека /font>

 Public l

Private m_Name As String

Shared Sub New() Console.WriteLine ("Shared constructor called")

End Sub

Private Sub New()

m_Name = "Default" 

End Sub

Public Sub New(ByVal NewName As String)

m_Name = NewName 

End Sub

Public Shared Function GetDefaultObject() As Internal

Return New Internal

End Function

Public Sub Test()

Console.WriteLine ("Test in Internal is: " & m_Name) 

End Sub

End

Для чего нужен закрытый конструктор, который может вызываться только в пределах класса?

Закрытые конструкторы полезны в тех случаях, когда объект должен создавать дополнительные экземпляры своего класса или если вы хотите, чтобы создание объекта выполнялось Shared-методом класса (см. метод GetDef aultObject в проекте Overloads).

В чем различие между параметризованным конструктором и созданием структуры Shared-методом?

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

Если нет окончательной ясности в том, будет ли создан объект (например, если создание объекта разрешается лишь для некоторых наборов параметров), лучше воспользоваться Shared-методом. В случае ошибки этот метод может вернуть Nothing или инициировать исключение, обходясь без фактического создания объекта.

Более того, вы можете объявить закрытыми все конструкторы и разрешить создание объекта только Shared-методом. Поскольку сам класс объявлен с атрибутом Public, проблем с видимостью объекта и его методов за пределами файла и даже сборки не будет. Но при отсутствии открытого конструктора объекты этого типа не могут создаваться оператором New! Для создания может использоваться только Shared-метод.

Пример Internalрасширяет эту идею и определяет конструктор по умолчанию с атрибутом Friend.

Public l

 Friend Sub New()

MyBase.New()

 End Sub

Public Overridable Sub Test()

Console.WriteLine ("Internallled") 

End Sub

Public Sub Test(ByVal x As Integer)

Console.WriteLine ("InternalVal x as integer) called")

 End Sub

 End

В классе определяется метод для создания и возвращения объектов класса Internal

Public font>

Public Function GetInternalternalfont>

' Здесь можно провести дополнительную инициализацию 

'и даже выполнить проверку, связанную с безопасностью.

 Return New Internal>

 End Function 

End

Класс Internalпоказывает, как использовать ключевое слово Overloads для перегрузки методов базового класса.

Public

 Inherits Internal

Public Overloads Overrides Sub Test()

Console.WriteLine ("Internalcalled")

 End Sub

 Public Overloads Sub Test(ByVal S As String)

Console.WriteLine ("InternalVal s as String) called")

 End Sub

End

Программа OverloadsTest находится в отдельной сборке. Это означает, что она не сможет создавать объекты Internalнапрямую, поскольку не имеет доступа к конструктору, объявленному с ключевым словом Friend. Программа OverloadsTest (листинг 10.6) показывает, как контролировать возможность создания объектов при помощи ограничения видимости конструктора.

Листинг 10.6. Программа OverloadsTest

Module Modulel

 Sub Hain()

1 Создать объект при помощи закрытого

' конструктора невозможно.

'Dim с As New Internal>

' Можно создавать открытым конструктором.

Dim с As New InternalyTestName")

c.Test()

' К закрытому конструктору можно обратиться 

' при помощи открытого метода.

 Dim c2 As Internalp;

Internal aul.tObject

' Объекты класса Internal

' никогда не создаются напрямую.

'Dim c2 As New Internal>

'Однако это можно сделать при помощи другого класса

 'и конструктора Friend.

 Dim clsl As New >

Dim c3 As InternalGetInternal

 c3.Test()

Console.WriteLine (ControlChars.CrLf & "Internalnbsp;

Dim cls3 As New Internal>

 cls3.Test ("hello")

 cls3.Test (5) 

cls3.Test()

 Console.ReadLine()

 End Sub

End Module

Как видите, мы можем создавать объекты класса Internalдаже несмотря на то, что этот класс является производным от lnternal(объекты которого создаваться не могут из-за отсутствия открытого конструктора). Это объясняется тем, что конструкторы не наследуются, поэтому ограниченная область видимости базового конструктора не влияет на производный класс.

Общие конструкторы

Помимо конструкторов экземпляров, в классах могут определяться общие (Shared) конструкторы. Общий конструктор вызывается при создании первого объекта заданного типа. Определение общих конструкторов в классах продемонстрировано в примере InitAndDestruct из главы 5 и в примере Overloads этой главы.

Снова о создании экземпляров

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

Область видимости объекта (в пределах сборки или везде) определяется атрибутами класса, то есть его объявлением с ключевым словом Public или Friend.

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

Глобальная видимость — возможность обращения к объекту без уточнения имени — достигается импортированием пространства имен, в котором определен этот класс, командой Imports1.

Автоматическое создание объекта при обращении не поддерживается. Для создания объекта необходимо использовать команду New.

1Помните о различиях между импортированием пространства имен и включением ссылки в проект? Включение ссылки дает возможность обращаться к объектам соответствующей сборки. Последующее импортирование пространства имен или любых его объектов позволяет обращаться к ним без указания полностью уточненного имени.

Методы и свойства

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

Однако этот факт помогает принять решение в ситуациях, когда значение не возвращается.

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

В сущности, это единственное правило, а все перечисленное ниже — не более чем настоятельные рекомендации.

Используйте процедуры свойств

При предоставлении доступа к простому элементу данных (скажем, к целому числу или строке) вместо синтаксических конструкций вида:

Public ss

Public A As Integer 

End

следует использовать процедуры свойств:

Public ss 

Private m_B As Integer 

Public Property B() As Integer 

Get

Return (m_B)

 End Get 

Set (ByVal Value As Integer)

m_B = value

 End Set 

End Property 

End

Почему? Потому что такое решение при небольшом объеме дополнительного кода расширяет ваши возможности. Например, позднее вы сможете легко реализовать проверку присваиваемых значений. В VB6 этот выбор особенно важен, поскольку переход от открытых переменных к процедурам свойств сопровождается нарушением совместимости. В VB .NET дело упрощается тем, что связывание осуществляется в процессе работы JIT-компилятора, поэтому переход на процедуры свойств не отразится на работе существующего кода. Тем не менее это полезная привычка.

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

Не усложняйте свойства

Хотя свойства классов могут быть параметризованными, обычно эта возможность используется лишь при создании индексаторов (indexers). В этом случае свойству передается один целочисленный параметр, по которому из массива или набора данных выбирается нужный элемент. Если в вашем случае одного параметра оказывается недостаточно, вероятно, вместо свойства стоит воспользоваться методом.

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

Избегайте побочных эффектов

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

Другие случаи использования методов

Методы часто применяются для преобразования типов данных.

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

 

Процедуры свойств

В VB .NET процедуры свойств несколько изменились по сравнению с VB6. Главное изменение уже неоднократно встречалось в примерах, приводившихся выше. Речь идет об изменении синтаксиса: процедуры Get и Set объединяются, а процедура Let исчезла вместе с различиями между объектами и переменными (вспомните: в VB .NET любая переменная является объектом). Атрибуты Readonly и ReadWrite указывают на то, является ли свойство доступным только для чтения или записи.

Синтаксис, принятый в VB .NET, отличается большей наглядностью. Процедуры Get и Set всегда расположены поблизости друг от друга. С другой стороны, я подозреваю, что любой мало-мальски компетентный программист VB6 старается не отдалять процедуры Property Get и Property Set.

Вероятно, основное недовольство у программистов VB6 вызовет тот факт, что в новом варианте синтаксиса процедуры Property Set/Let и Property Get должны обладать одинаковой видимостью. В VB6 очень часто встречались ситуации с объявлениями Public Property Get и Private/Friend Property Set. Значения свойств, созданных таким образом, могли задаваться только внутри компонента или приложения, однако чтение разрешалось и внешним клиентам.

Честно говоря, меня это изменение тоже не радует, но обвинять в этом разработчиков VB .NET было бы несправедливо. Требование одинаковой видимости процедур Property Get и Property Set входит в спецификацию CLS (Common Language Specification) и действует не только в VB .NET, но и в С# и .NET-вер-сии C++.

Таким образом, если только Microsoft не пойдет на изменение CLS, я рекомендую использовать открытые процедуры свойств с атрибутом Readonly и отдельные методы с видимостью Friend или Private для задания внутренних значений свойств.

Свойства по умолчанию

Рассмотрим следующую команду VB6: 

myVariable = "Hello"

Что делает эта команда? Присваивает строку переменной типа String?

А если переменная my Variable является объектом? Приведет ли это к ошибке времени выполнения? А если у этого объекта имеется свойство по умолчанию, будет ли строка присвоена ему? И какое именно свойство будет использоваться по умолчанию?

А если переменная myVariable относится к универсальному типу Variant? Что делает эта команда: присваивает переменной строку? А может, в универсальной переменной хранится объект, у которого имеется свойство по умолчанию?

Мы не знаем.

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

А теперь рассмотрим ту же строку в VB .NET.

Переменной myVariable типа String или Object присваивается ссылка на объект String, содержащий значение «Hello».

Все. Никакой неоднозначности, никакой путаницы.

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

В своей практике я не припоминаю ни одного случая сознательного использования класса со свойствами по умолчанию. Каждый раз, когда я включал свойство по умолчанию в свои элементы, я об этом жалел. Работая со стандартными объектами (такими, как текстовые поля), я всегда использую полную запись вида txtEdi tControl .Text = "string" вместо того, чтобы полагаться на свойства по умолчанию.

В VB .NET свойства по умолчанию могут быть только параметризованными (поскольку на параметризованные свойства не распространяется неоднозначность простого присваивания). Чаще всего эта возможность используется при создании индексаторов.

Практическое использование свойств по умолчанию продемонстрировано в листинге 10.7.

Листинг 10.7. Приложение Properties

' Использование свойств по умолчанию 

'Copyright ©2001 by Desaware Inc.

Module Modulel

est

Implements IDisposable

Private Shared My ArrayList()

 Public Name As String

Public Sub New(ByVal myname As String)

MyBase.New()

My(Me)

Name = myname

 End Sub

Default Public Readonly Property OtherDefaultTestObjects(ByVal

idx As Integer) As DefaultTest

 Get

Return CType(Mydx), DefaultTest)

 End Get 

End Property

Public Shared Readonly Property OtherObjectCount() As Integer

Get Return my/font>

End Get End Property

Public Readonly Property Mylndex() As Integer

Get

 Return myf(Me)

End Get 

End Property

Public Sub Dispose() Implements IDisposable.Di spose

My (Me) 

End Sub

End

Sub ShowOtherObjects(ByVal obj As DefaultTest)

 Dim x As Integer

Console.WriteLine ("This object is: " & obj.Name)

 Console.WriteLine ("Other objects are: ") 

For x = 0 To DefaultTest.OtherObjectCount - 1

If x <> obj.Mylndex Then 

Console.WriteLine (obj(x).Name)

End If

 Next Console.Wri teLine()

End Sub

Sub Main()

Dim objl As New DefaultTest("Firstobject") 

Dim obj2 As New DefaultTest("Secondobject") 

Dim obj3 As New DefaultTest("Thirdobject")

ShowOtherObjects (obj2)

 obj2.Dispose()

 obj2 = Nothing ShowOtherObjects

(obj3) Console. ReadLine() 

End Sub

End Module

В приложении Properties продемонстрированы некоторые приемы, упоминавшиеся в главах 4 и 5, а также в этой главе.

Класс DefaultTest показывает, как организовать отслеживание всех созданных экземпляров класса с минимальными затратами со стороны клиентского кода.

Общая переменная my типа ArrayList содержит список созданных объектов. Объекты включаются в список при вызове конструктора.

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

Общее свойство OtherObjectCount, доступное только для чтения, возвращает текущее количество объектов в коллекции my. Выбор между свойством и методом в данном случае оказывается непростым. Поскольку свойство объявлено с атрибутом Shared, к нему можно обращаться как через экземпляр объекта DefaultTest, так и непосредственно в записи вида DefaultTest. OtherObjectCount (как показывает метод ShowOtherObjects). Свойство My Index возвращает индекс заданного экземпляра в коллекции my.

Очень важный метод Dispose должен вызываться клиентом при присваивании экземпляру значения Nothing. Почему? Разве VB .NET не обеспечивает автоматического освобождения объектов?

Да, VB .NET автоматически освобождает объекты, на которые не существует ссылок, а точнее, объекты, не присутствующие в цепочках, начинающихся от корневых переменных. Однако коллекция my является корневой переменной! Следовательно, если ссылка на объект встречается в массиве, этот объект не освобождается. Метод Dispose удаляет объект из коллекции my, обеспечивая возможность его нормального освобождения.

Другими словами, VB .NET прекрасно справляется с освобождением переменных, на которые в вашем приложении отсутствуют ссылки, но это не помешает вам устроить утечку памяти, если ваше приложение не позаботится о должном освобождении ненужных объектов. Процедура Main показывает, как организовать освобождение компонентов, реализующих интерфейс IDisposable1.

Метод ShowOtherObjects демонстрирует использование свойства по умолчанию.

1Возможно, вы обратили внимание на то, что в формах метод Di spose помечается ключевыми словами Overloaded и Overrides. Дело в том, что класс Form является производным от класса Control, который не только реализует метод Di spose (который вы можете переопределить), но реализует и вторую версию Dispose с параметром логического типа — отсюда необходимость в ключевом слове Overloads.

Параметризованные свойства

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

Передача свойств по ссылке

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

Рассмотрим пример PropByRef, приведенный в листинге 10.8.

Листинг 10.8. Приложение PropByRef

' Приложение PropByRef

' Copyright ©2001 by Desaware Inc. AU Rights Reserved

s

Private m_Member As String

' Ключевое слово Overloads в данном примере необязательно. 

Overloads Property Member() As String 

Get

Return m_Member 

End Get Set(ByVal Value As String)

m_Member = Value

 End Set 

End Property

Overloads Property Hember(ByVal x As Integer) As String 

Get

Return (m_Member) 

End Get

  Set(ByVal Value As String)

m_Member = Value & " called with " & x.ToString() 

End Set

 End Property

End ont>

Module Modulel

Sub FunctionSetsString(ByRef s As String)

s = "Hello" 

End Sub

Sub Main()

Dim obj As New Prop

FunctionSetsString (obj.Member)

Console.WriteLine (obj.Member)

FunctionSetsString (obj.Member(S))

Console.WriteLine (obj.Member)

Console.ReadLine() 

End Sub

End Module

Класс Propодержит закрытую строковую переменную, для обращения к которой используются две перегруженные функции с именами Member. Обе версии Member возвращают содержимое закрытой строковой переменной, но присваивают ей разные значения. Параметризованный метод Member просто присваивает строку; второй метод Member вместе со строкой сохраняет значение целочисленного параметра. Это позволяет определить, какой из перегруженных методов задал значение свойства.

Метод FunctionSetsString получает по ссылке одну строковую переменную и присваивает ей значение «Hello».

Процедура Main вызывает функцию FunctionSetsString и передает ей по ссылке свойство Member класса Prop

В VB6 при этом создается временная переменная, которой задается значение свойства Member. Именно эта временная переменная передается в качестве параметра функции FunctionSetsString. В результате, хотя параметр передавался по ссылке, свойство Member класса Propак и не изменяется функцией FunctionSetsString, модифицируется только временная переменная.

Однако в VB .NET выводится следующий результат:

Hello

Hello called with 5

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

Другими словами, происходит следующее.

  •  VB .NET читает Member и присваивает значение временной переменной.
  •  VB .NET вызывает FunctionSetsString.
  •  Полученное значение задается свойству той же версией Member и с теми же параметрами, которые использовались при чтении.

Конечно, для сколько-нибудь надежного выполнения этой операции должны выполняться два условия.

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

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

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

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

 

События и делегаты

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

Во-первых, события VB .NET в простейшем случае работают практически так же, как и в VB6. Следующий фрагмент демонстрирует обработку событий мыши в VB .NET.

Private WithEvents buttonl As System.Windows.Forms.Button 

Me.buttonl = New System.Windows.Forms.Button()

Private Sub buttonl_Click(ByVal sender As System.Object,

 ByVal e As System.EventArgs) Handles buttonl.Click 

MsgBox("Simple Event Clicked", MsgBoxStyle.Information, _ 

"Event arrived")

End Sub

В VB .NET формы, кнопки и другие элементы всего лишь представляются разными типами классов. Как и любые другие классы, они могут инициировать события. Таким образом, переменная, представляющая кнопку, объявляется с ключевым словом WithEvents, а затем соответствующий объект создается командой New.

Синтаксис обработчиков событий тоже несколько изменился. Вместо невесть откуда взявшегося обработчика с именем buttonl_Click (как это было в VB6) вы создаете процедуру с произвольным именем и при помощи ключевого слова Handles указываете, какое событие она обрабатывает. Другими словами, приведенную выше процедуру buttonl_Click можно определить в следующем виде:

Private Sub OhNoTheButtonWasClicked(ByVal sender As System.Object,

ByVal e As System.EventArgs) Handles buttonl.Click

MsgBox("Simple Event Clicked", MsgBoxStyle.Information_

"Event arrived") 

End Sub

Имя процедуры значения не имеет. Мастер Form Designer Wizard автоматически создает процедуру с именем элемент_событие, но имя может быть любым — важно лишь имя события, указанное после ключевого слова Handles.

Следовательно, в VB .NET используются те же базовые принципы обработки событий, как и в VB6, поэтому не стоило особенно торопиться с ее описанием в предыдущих главах.

Однако в действительности механизм обработки событий в .NET кардинально отличается не только от VB6, но и от обработки событий в СОМ вообще. Чтобы понять, как работают события, необходимо хорошо разбираться в том, как организовано взаимодействие объектов в .NET — и это другая причина, по которой я рассматриваю события только в этой главе.

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

Чтобы представить события VB .NET в практическом контексте, мы сначала кратко рассмотрим основные принципы обработки событий в VB6 и СОМ. При этом я не стремлюсь дать общее представление о том, как работают события за пределами .NET, а просто хочу свести воедино все, что вы уже знаете, чтобы вам было проще это «все» забыть. Только тогда вы поймете, как же организована обработка событий в .NET.

События, функции обратного вызова и СОМ

Что такое «событие»?

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

Чем непосредственный вызов метода отличается от вызова по событию?

С сугубо концептуальной точки зрения — это всего лишь вопрос восприятия.

Рассмотрим два объекта. Клиентский объект является частью вашей программы и создает серверный объект для выполнения некоторой задачи (рис. 10.3). Серверный объект при необходимости может передать клиенту некоторую информацию. Концептуальное различие между вызовом метода и событием связано с направлением вызова. Вызывая метод из своего приложения, вы сами определяете, когда это происходит, то есть вызов входит в нормальную последовательность выполнения программы. События можно рассматривать как вызовы методов, не входящие в нормальную последовательность выполнения, на которые ваше приложение должно определенным образом отреагировать.

Рис. 10.3. Простейшая схема обработки событий

В VB6 поддерживаются два механизма обработки событий. Первый, стандартный механизм, используется при определении событий в синтаксисе Event и ссылки на серверный объект с ключевым словом WithEvents. Этот механизм рассматривается ниже. Второй механизм называется механизмом обратного вызова OLE (OLE callback). При использовании этого механизма клиентский объект передает серверу ссылку на самого себя, через которую серверный объект может напрямую вызывать методы клиентского объекта.

Мы не будем подробно обсуждать преимущества и недостатки этих двух механизмов, а также тонкости их работы. Главное, что необходимо понять, — что при использовании обратных вызовов OLE сервер получает ссылку на клиентский объект. Поскольку клиент тоже содержит ссылку на сервер, возникает классическая ситуация с циклическими ссылками. Это означает, что до освобождения одной из ссылок ни один из этих объектов не может быть уничтожен. • Хотя в некоторых ситуациях подобный механизм работает достаточно хорошо, не стоит и говорить, что для работы СОМ необходим механизм событий, не подверженный проблеме циклических ссылок. Этот механизм основан на так называемых точках подключения (connection points), при помощи которых объект сообщает информацию о событиях, которые он может инициировать. Эта информация состоит из имени события и списка параметров. Объект ожидает, что клиент (приемник события) предоставит методы с соответствующими параметрами. Точки подключения реализуются при помощи стандартных интерфейсов СОМ, которые не только предоставляют информацию о событиях, но и определяют

способ ведения сервером списка всех подключенных клиентов, чтобы одно событие могло инициироваться сразу в нескольких клиентах. Но самое важное заключается в том, что эти интерфейсы требуют, чтобы серверные объекты хранили «слабую» ссылку на клиентское приложение. «Слабой» называется ссылка, которая позволяет серверу вызывать методы клиентского объекта, но не считается ссылкой на клиента с точки зрения механизма подсчета ссылок СОМ. Это позволяет нормально уничтожить клиентский объект даже в том случае, если он является приемником событий.

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

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

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

 

Делегаты

Одной из основных составляющих любого механизма событий (СОМ, обратных вызовов OLE или .NET) является согласование между клиентом и сервером типов параметров и значений, возвращаемых методами событий. Другая составляющая — механизм для хранения ссылок на объект и вызова методов. Обе задачи решаются при помощи специального объекта, называемого делегатом (рис. 10.4).

Рис. 10.4. Вызов методов при помощи делегата

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

Например, команда

 Delegate Sub DelegateWithStringSignature(ByVal S As String)

из проекта Delegates определяет новый класс с именем DelegateWithStringSignature. Этот класс подходит только для методов, которые получают один строковый параметр и не имеют возвращаемого значения.

Оператор AddressOf в VB .NET возвращает делегата для заданного метода.

1Выражаясь точнее, сигнатуру определяют объекты, производные от базового класса Delegate.

Допустим, у нас имеется класс Calledкоторый определяется следующим образом:

ass

Public Sub WriteHessageCByVal s As String)

  Console.Writel_ine("Called ssage of " & s)

End Sub 

End

Затем мы создаем делегата типа DelegateWithStringSignature:

Dim с As New Called

Dim dl As DelegateWithStringSignature

dl = AddressOf с.WriteMessage

Делегат, созданный этим фрагментом, не только имеет правильную сигнатуру (один строковый параметр), но и ссылается на метод WriteMessage конкретного объекта Callednbsp;

Вызов метода WriteMessage этого объекта осуществляется любой из двух команд, приведенных ниже:

dl. InvokeC'Test")

с.WriteMessage("Test")

Делегаты без объектов

Но при чем здесь события? Ведь класс Calledз приведенного выше примера не имеет к событиям никакого отношения?

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

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

Листинг 10.9. Приложение Delegates

ass

Shared Sub SharedMessage(ByVal s As String) 

Console.WriteLine (_

"Called Calledssage with parameter: " & s)

 End Sub

Public Sub WriteMessage(ByVal s As String) 

Console.WriteLine (_

"Called Calledsage method with parameter: " & s)

 End Sub End

led

Sub WriteMessage(ByVal s As String)

 Console.WriteLine ("Called OtherCalledfont>

& "WriteMessage method with parameter: " & s)

 End Sub

 End

Module StdModule

Sub ModuleWriteMessage(ByVal s As String)

Console.WriteLine ("AddresssOf works in standard module too")

 End Sub

 End Module

В процедуру Sub Main проекта Delegates входит следующий фрагмент:

Dim с As New Called

Dim о As New OtherCalled

Dim BadObject As New ObjectWi thNoWri teMessage()

Dim dl As DelegateWithStringSignature

Dim obj As Object

Dim Params() As Object = {"DynamicParam"}

' Следующие две строки эквивалентны.

dl = New DelegateWithStringSignature(AddressOf с.WriteMessage)

'dl = AddressOf с.WriteMessage

dl.Invoke ("Test")

dl.Dynamiclnvoke (Params)

Dim d2 As DelegateWithStringSignature = AddressOf

 Calledssage

Dim d3 As DelegateWithStringSignature = AddressOf o.WriteMessage

Dim d4 As DelegateWithStringSignature = AddressOf

StdModule.ModuleWriteMessage

d2.Invoke ("Test2")

d3.Invoke ("Test3")

d4.Invoke ("Test4")

Три делегата dl, d2 и d3 демонстрируют вызов метода конкретного объекта, общего метода класса и функции стандартного модуля. Метод Dynamiclnvoke позволяет динамически задавать параметры во время работы программы. Вскоре вы увидите, зачем это может понадобиться. Результат выглядит следующим образом:

Called Calledsage method with parameter: Test

Called Calledsage method with parameter: DynamicParam

Called Calledssage with parameter: Test2

Called OtherCalledsage method with parameter: Tests

AddressOf works in standard module too

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

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

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

Ниже приведено определение нового класса, не поддерживающего метод WriteMessage:

thNoWriteMessage 

Sub BadWri teMessageO

End Sub

 End

Также определяется тип делегата без параметров:

Delegate Sub DelegateWithNoParam()

В настоящем примере делегат создается методом CreateDelegate, который позволяет задать тип делегата, объект и имя метода следующим образом: 

Console.WriteLine (ControlChars.CrLf & "Late binding example 1")

Dim d5 As DelegateWithStringSignature

Dim с As New Called

d5 = CType(System.Delegate.CreateDelegate(GetType(_

DelegateWithStringSignature), c, "WriteMessage"),

DelegateWithStringSignature) 

dS.Invoke ("Test") 

Try

dS = CType(System.Delegate.CreateDelegate(GetType(_

  DelegateWithStringSignature), BadObject, _ 

"WriteMessage"), DelegateWithStringSignature)

d5.Invoke ("Test")

 Catch e As Exception

Console.WriteLine (e.Message) 

End Try

Имя метода может быть представлено в виде переменной. В этом случае связывание объекта и метода с делегатом откладывается до момента выполнения программы. Попытка связать делегата с объектом BadObject завершается неудачей, поскольку у этого объекта отсутствует метод WriteMessage. To же самое происходит и при несовпадении сигнатуры метода WriteMessage объекта с сигнатурой типа DelegateWithStringSignature (например, если метод не вызывается с одним строковым параметром). При выполнении этого фрагмента будет выведен следующий результат:

Late binding example 1

Called Calledsage method with parameter: Test

Error binding to target method

В программе можно определить универсальный метод, позволяющий вызвать любого переданного делегата независимо от типа параметров. Эта возможность продемонстрирована в функции LateBoundCaller:

Public Sub LateBoundCaller(ByVal d As Delegate) 

Dim params() As Object = {"Test"}

 Try

d.Dynamiclnvoke (Params) Catch e As Exception

Console.WriteLine (e.Message)

End Try 

End Sub

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

Console.WriteLine (ControlChars.CrLf & "Late binding example 2")

Dim d6 As DelegateWithNoParam = AddressOf BadObject.BadWriteMessage

 La-teBoundCaller (CType(dl, System.Delegate))

 LateBoundCaller (CType(d6, System.Delegate))

Первый вызов завершается успешно, поскольку делегат dl относится к типу DelegateWithStringSignature. Вторая попытка завершается неудачей, поскольку делегат DelegateWithNoParam вызывается без параметров. Результат:

Late binding example 2

Called Calledsage method with parameter: Test

Parameter count mismatch

Другие применения делегатов

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

В качестве примера мы рассмотрим реализацию в VB .NET классической задачи — перечисления всех окон верхнего уровня в системе. Наше решение основано на передаче делегатных параметров и на применении делегатов для реализации обратного вызова при работе с функциями API2.

При вызове функции API EnumWindows передается функция обратного вызова с двумя параметрами: манипулятором (handle) окна и значением, смысл которого определяется пользователем. Определение делегата выглядит так:

Delegate Function EnumWindowsCallback(ByVal hWnd As Integer, _

 ByVal IParam As Integer) As Integer

Обратите внимание: параметры и возвращаемое значение определяются с типом Integer (32-разрядным!), а не с типом Long3.

1Функция VB6 Calllndirect позволяет указать имя и параметры метода во время работы программы, но количество и типы параметров фиксируются. В статье «Implementing Indirect Calls on ActiveX/COM Objects» по адресу http://www.desaware.com показано, как организовать обобщенные косвенные вызовы с использованием пакета SpyWorks, но и это решение значительно уступает VB .NET по простоте.

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

3Вероятно, это будет самая распространенная ошибка программирования, связанная с использованием функций API в VB .NET.

В проект входит модуль с объявлениями функции EnumWindows и функции GetWindowText, используемой для получения текста заголовков окон (там, где они присутствуют). Кроме того, определяется функция обратного вызова Show-WindowNamesCallback, совпадающая по сигнатуре с делегатом EnumWindows-Callback. Метод ShowWindowsNames при помощи оператора AddressOf получает делегата для ссылки на функцию ShowWindowNamesCallback и передает его функции EnumWindows. Определения этих функций выглядят следующим образом:

Module StdModule

Public Dection EnumWindows Lib "User32" _ 

(ByVal proc As EnumWindowsCallback, ByVal pval As Integer) As Integer

 Public Dection GetWindowText Lib "User32" Alias _

 "GetWindowTextA" (ByVal hWnd As Integer, ByVal WindowName As 

String, ByVal BufferLength As Integer) As Integer

Public Function ShowWindowNamesCallback(ByVal hwnd As Integer, _ 

ByVal IParam As Integer) As Integer

Dim windowname As New String(Chr(32), 255)

Dim TrimmedName As String

GetWindowText(hWnd, windowname, 254)

TrimmedName = Lett$(windowname, InStr(windowname, Chr(0)) - 1)

If TrimmedName <> "" Then Console.WriteLine (TrimmedName)

Return (1)

 End Function

Public Sub ShowWindowNames()

EnumWindows(AddressOf ShowWindowNamesCallback, 0)

 End Sub

End Module

Пока все выглядело примерно так же, как в VB6, но дальше начинается самое интересное. Далее следует определение класса WindowHandles:

ndles

Private col As New Collection()

 Public Sub New()

MyBase.New()

EnumWindows(AddressOf GetWindowHandlesCallback, 0)

 End Sub

Private Function GetWindowHandlesCallback(ByVal hwnd As Integer, _

 ByVal IParam As Integer) As Integer

col.Add (hwnd)

Return (1)

 End Function

Public Sub ShowAllWindows()

 Dim hwnd As Integer

 For Each hwnd In col

 Console.Write (HexS(hwnd))

Console.Write (", ") 

Next

Console.WriteLine()

 End Sub

End

При создании экземпляра этого класса вызывается функция EnumWindows, которой передается делегат для метода GetWindowHandlesCallback. При каждом вызове GetWindowHandlesCallback полученный манипулятор окна сохраняется в коллекции, содержимое которой выводится на консоль методом ShowAlIWindows.

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

превосходит VB6, где оператор AddressOf возвращал лишь указатели на функции в стандартных модулях1.

1 Интересно, а как .NET это делает, если функция EnumWindows работает только с указателями на функции и понятия не имеет об объектах и экземплярах? Что ж, в VB6 за кулисами часто творились всякие чудеса — вот и считайте это новой разновидностью чуда для VB .NET.

 

События

Наконец-то пришло время разобраться с тем, как события работают в VB .NET. Прежде всего следует отметить, что делегаты обладают еще одной особенностью, играющей важную роль при работе с событиями. При определении делегаткой переменной в VB .NET вы в действительности определяете новый делегатный тип, производный от класса System.MulticastDelegate (который, в свою очередь, является производным от типа System.Delegate). Групповые (multicast) делегаты позволяют хранить ссылки на несколько методов, причем при вызове метода Invoke вызываются все эти методы (рис. 10.5).

Рис. 10.5. Групповые делегаты

Варианты обработки событий

Приложение EventExample содержит одну форму с четырьмя кнопками и тремя переключателями. Каждый элемент демонстрирует некоторый аспект обработки событий в VB .NET.

Кнопка SimpleEvent работает так, как описано в разделе «События и делегаты» в последнем разделе этой главы. Дизайнер форм создает объявление кнопки с ключевым словом WithEvents, указывающим на то, что VB .NET должен автоматически организовать обработку событий. Для кнопки генерируется метод buttonl_Click, а ключевое слово Handles указывает на то, что язык должен автоматически создать делегата для метода buttonl_Click и связать его с событием buttonl.Click.

Private WithEvents buttonl As System.Windows.Forms.Button

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

 ByVal e As System.EventArgs) Handles buttonl.Click

MsgBox("Simple Event Clicked", MsgBoxStyle.Information, _

 "Event arrived")

 End Sub

Кнопка button.2 с надписью «Other Approaches» показывает, что существует несколько возможных подходов к обработке событий.

В классе Eventпределяются два события, FirstEvent и SecondEvent. Событие SecondEvent соответствует общепринятой схеме, согласно которой в первом параметре передается ссылка на объект, инициировавший событие. Событие FirstEvent эту схему игнорирует.

Для обоих событий определяются делегаты, хотя в нашем примере это неочевидно.

  •  При вызове функции RaiseEvent в действительности вызывается метод Invoke делегата.

Впрочем, все это происходит за кулисами VB .NET1.

1В С# обработка событий организована посложнее, но сейчас это неважно.

Public ass 

Public Event FirstEvent(ByVal myData As Integer)

Public Event SecondEvent(ByVal Sender As Object, ByVal myData As Integer)

Public Sub Testl()

RaiseEvent FirstEvent(5)

 End Sub

Public Sub Test2()

RaiseEvent SecondEvent(Me, 5)

 End Sub

End

На примере этого класса мы рассмотрим два варианта обработки событий. Процедура события button2_Click просто вызывает методы Testl и Test2 для двух разных экземпляров класса Event

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

ByVal e As System.EventArgs) Handles button2.Click

ec.Testl()

ec2.Test2()

 End Sub

Объекты еc и еc2 определяются по-разному:

Private ec As New Event

 Private WithEvents ec2 As Event

Как видите, событие еc2 связывается с функцией Eventent при помощи ключевого слова Handles — точно так же, как событие buttonl. Click связывалось с методом buttonl_Click.

Private Sub Eventent(ByVal obj As Object,_

 ByVal i As Integer) Handles ec2.SecondEvent

MsgBox("Eventent called", _

MsgBoxStyle.Information, "Event arrived")

 End Sub

Объект ее не определяется с ключевым словом WithEvents. В VB6 это вызвало бы большие трудности, но в VB .NET поддерживается динамическое подключение событий командой AddHandler:

AddHandler еc. FirstEvent, AddressOf Eventnt 

Private Sub Eventnt(ByVal i As Integer)

MsgBox("Eventnt called",

MsgBoxStyle.Information, "Event arrived")

 End Sub

Упрощенное объяснение — функция AddHandler связывает метод Eventvent с событием ее. FirstEvent.

На самом деле происходит следующее.

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

Мы также знаем, что оператор AddressOf возвращает делегата.

Следовательно, команда AddHandler на самом деле берет делегата для метода Event>rstEvent и добавляет его к делегату ее. FirstEvent. Я знаю, что само выражение «добавить одного делегата к другому» выглядит довольно странно. Попробуйте взглянуть на происходящее так:

  • Делегат представляет собой объект, позволяющий вызвать заданный метод.
  • Групповой делегат представляет собой объект, позволяющий вызвать несколько методов.
  • При добавлении делегата к групповому делегату вы просто добавляете очередной метод в список методов, вызываемых групповым делегатом.

 При добавлении делегата к событию (реализованному групповым делегатом) вы просто включаете очередной метод в список методов, вызываемых при инициировании события. А что произойдет, если воспользоваться командой AddHandler для добавления делегата к уже обрабатываемому событию? Мы знаем, что объект ес2 связан с методом Eventent, а теперь мы связываем его с дополнительным методом.

AddHandler ec2.SecondEvent, AddressOf AnotherSecondEventHandler

 Private Sub AnotherSecondEventHandler(ByVal obj As Object, _

ByVal i As Integer)

MsgBoxC'AnotherSecondEventHandler called", _

MsgBoxStyle.Information, "Event arrived")

 End Sub

Если щелкнуть на кнопке button2, на экране появятся три сообщения о вызове методов Eventnt, Eventent и AnotherSecondEventHandler.

Метод AnotherSecondEventHandler также демонстрирует один важный факт: имя метода абсолютно несущественно. Синтаксис «класс_событие» используется лишь для наглядности.

Наследование событий

События, как и методы со свойствами, наследуются производными классами.

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

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

Public Delegate Sub EventTemplate(ByVal Obj As Object, ByVal i As Integer)

Public EventCtass

 Inherits Eventont>

Shared Event ASharedEvent()

' Обратите внимание на другой вариант -

' эти объявления эквивалентны.

'Event DerivedEvent(ByVal obj As Object, ByVal i As Integer)

Event DerivedEvent As EventTemplate

Метод InternalHandler обрабатывает событие SecondEvent своего базового класса. Производные классы вообще обладают способностью принимать события базовых классов. Клиент, использующий этот класс, получает как события, унаследованные от базового класса, так и события, инициированные производным классом. Чтобы пользователь производного класса не мог получать события базового класса, воспользуйтесь ключевым словом Shadows и замаскируйте событие базового класса, определив закрытое событие с тем же именем. Закрытое событие недоступно за пределами класса.

Метод Tes2 класса DerivedEventаскирует одноименный метод базового класса. Он вызывает метод Test2 базового класса и инициирует общее событие.

Public Sub InternalHandler(ByVal obj As Object,_

 ByVal i As Integer) Handles MyBase.SecondEvent

RaiseEvent DerivedEvent(Me, i * 2)

 End Sub 

Private Shadows Event FirstEvent(ByVal MyData As Integer)

Public Shadows Sub Test2()

 MyBase.Test2() 

RaiseEvent ASharedEvent() 

End Sub 

End

Объект DerivedEventпределяется в форме с ключевым словом WithEvents. Он связывается с методом DerivedEventHandler при помощи ключевого слова Handles. Команда AddHandler связывает его с методом AnotherSecond-EventHandler. Да, все верно — метод AnotherSecondEventHandler обрабатывает сразу два события от двух разных объектов. Вообще говоря, любой метод может принимать любое число событий от любого числа объектов — при условии, что сигнатура метода соответствует сигнатуре делегата события. Вскоре мы рассмотрим еще один пример подобного рода.

Для наглядности ссылка на общее событие осуществляется по имени класса, а не по имени объекта. Я мог бы сослаться на него и через экземпляр объекта (dec. ASharedEvent), однако имя класса подчеркивает тот факт, что речь идет об общем событии. Общее событие связывается с методом точно так же, как и событие объекта. Программный код, демонстрирующий использование DerivedEventвызывается событием button3.Click.

Private WithEvents dec As DerivedEvent

AddHandler dec.SecondEvent, AddressOf AnotherSecondEventHandler

 AddHandler DerivedEventvent, AddressOf SharedEventHandler

Private Sub DerivedEventHandler(ByVal obj As Object, _

 ByVal i As Integer) Handles dec.DerivedEvent

MsgBox("DerivedEventHandler called", MsgBoxStyle.Information,_

"Event arrived")

 End Sub

Private Sub SharedEventHandler()

MsgBoxC'Shared event handler", MsgBoxStyle.Information, "Event arrived")

 End Sub

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

 ByVal e As System.EventArgs) Handles buttons.Click

dec.Test2() 

End Sub

Снова о делегатах

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

Public SpecialEventHandler As EventTemplate

 Public Sub TestSpecialEvents()

SpecialEventHandler. Invoke (He,' 6) 

End Sub

Сначала делегату SpecialEventHandler присваивается делегат, указывающий на метод FirstSpecialEventHandler формы. Затем создается другой делегат, ссылающийся на метод SecondSpecialEventHandler формы. Наконец, делегату SpecialEventHandler присваивается комбинация двух предыдущих делегатов, объединенных методом Combine класса Delegate.

В результате методы FirstSpecialEventHandler и SecondSpecialEventHandler связываются с делегатом dec.SpecialEventHandler. При вызове объектом dec метода SpecialEventHandler. Invoke будут вызваны оба метода формы.

Описанный механизм работает практически так же, как механизм событий, если не считать того, что при использовании событий VB .NET берет на себя часть работы, упрощая задачу программиста. Следующий фрагмент демонстрирует этот «не-событийный» подход.

dec.SpecialEventHandler = AddressOf FirstSpecialEventHandler

 Dim EventToCombine As EventTemplate = AddressOf _

 SecondSpecialEventHandler

dec.SpecialEventHandler = CType(dec.SpecialEventHandler.Combine(_

  .dec.SpecialEventHandler, EventToCombine), EventTemplate)

Private Sub FirstSpecialEventHandler(ByVal obj As Object, 

ByVal i As Integer)

MsgBox("FirstSpecial event handler", MsgBoxStyle.Information,

"Event arrived")

 End Sub

Private Sub SecondSpecialEventHandler(ByVal obj As Object, 

ByVal i As Integer)

MsgBox("SecondSpecial event handler", MsgBoxStyle.Information, _

"Event arrived")

 End Sub

Private Sub button4_Click(ByVal sender As System.Object,

 ByVal e As System.EventArgs) Handles buttonA.Click

dec.TestSpecialEvents() 

End Sub

Обработка нескольких событий

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

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

 ByVal e As System.EventArgs) Handles radioButton3.CheckedChanged, _

 radioButton2.CheckedChanged, radioButtonl.CheckedChanged 

Dim params() As Object 

Dim NameValue As Object

NameValue = sender.GetType.InvokeMember("Text", _

 Reflection.BindingFlags.Public Or _ 

Reflection.BindingFlags.Instance Or _

Reflection.BindingFlags.GetProperty, Nothing, sender, params)

  Debug.WriteLineCCStr(NameValue))

Dim rb As RadioButton

rb = CType(sender, RadioButton)

Debug.Writeline(rb.Name & " " & rb.Text)

End Sub

Метод Options_CheckedChange связывается с событием CheckedChanged трех разных переключателей, для чего все события перечисляются после ключевого слова Handles. Вероятно, вы уже поняли, что аналогичного эффекта можно было добиться и командой AddHandler,

Hо этот вариант вам придется кодировать самостоятельно, так как в Visual Studio отсутствует механизм, который бы позволял сделать это автоматически.

Параметр sender указывает, каким переключателем было инициировано событие. Поскольку массивы элементов в VB .NET не поддерживаются, вам не удастся идентифицировать источник события по значению свойства Index. В зависимости от типа приложения для идентификации источника можно воспользоваться любым другим подходящим свойством, например свойствами Name, Text или позицией элемента. Для получения свойств от sender используется механизм рефлексии, описанный в следующей главе. С загадочным методом InvokeMember (см. выше) мы разберемся чуть позже, а пока достаточно сказать, что он возвращает заданное свойство элемента sender.

Если вы твердо уверены, что все элементы относятся к одному типу, параметр sender можно объявить с типом соответствующего элемента и обращаться к свойству напрямую, как показано во втором варианте с переменной rb, объявленной с типом RadioButton (кнопка-переключатель).

Отключение обработчиков

Метод Dispose формы содержит следующие команды:

RemoveHandler ее.FirstEvent, AddressOf Eventnt RemoveHandler ec2.SecondEvent, AddressOf AnotherSecondEventHandler RemoveHandler DerivedEventvent, AddressOfSharedEventHandler RemoveHandler dec.SecondEvent, AddressOf AnotherSecondEventHandler

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

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

Поскольку проблема циклических ссылок в VB .NET решена, клиентские объекты будут успешно освобождены, верно?

Неверно.

Серверный объект продолжает хранить ссылки на эти объекты с своем делегате события. Пока в вашем приложении сохраняется ссылка на серверный объект, VB .NET будет считать делегатов ссылками на клиентские объекты.

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

А как же события, объявленные с ключевым словом WithEvents? С ними проблем быть не должно, поскольку ссылка на источник события автоматически освобождается при освобождении модуля, принимающего события. Например, если при помощи ключевого слова WithEvents связать событие с элементом формы, то при удалении ссылки на форму будет освобождена сама форма и все ее объекты, поскольку ни одна из этих ссылок не ведет к переменным корневого уровня вашего приложения.

 

Итоги

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

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

Глава завершается описанием событий и делегатов — новой концепции, на которой построен механизм событий .NET.

Назад   Вперёд

 


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