Cамоучитель по VB.NET
Автоматическая сборка мусора: ликвидация утечки памяти
У программистов, работающих на Visual Basic, всегда возникали проблемы с утечкой памяти из-за так называемых циклических ссылок (ситуация, при которой объект А ссылается на объект В, а объект В ссылается на объект А). Если появление циклических ссылок было обусловлено логикой программы, компилятор VB не распознавал их, в результате чего память, занимаемая этими объектами, не освобождалась. Система сборки мусора, встроенная в .NET CLR, решает проблему циклических ссылок иначе — интеллектуальный алгоритм обнаруживает циклические ссылки, разрывает их и освобождает занимаемую память. Конечно, за дополнительные возможности приходится платить; достоинства и недостатки автоматической сборки мусора рассматриваются в главе 4.Common Language Runtime
Исполнительная среда (runtime) всегда присутствовала в Visual Basic, поэтому следующее утверждение поначалу выглядит несколько странно. Итак, одним из самых серьезных новшеств VB .NET является наличие исполнительной среды CLR (Common Language Runtime), общей для всех языков .NET. Хотя на первый взгляд CLR напоминает обычную библиотеку времени выполнения наподобие библиотеки С MSVCRTXX.DLL, библиотека VB MSVBVMXX.DLL имеет значительно большие размеры и обладает гораздо большими возможностями. По этой причине написание программ, в полной мере использующих CLR, больше походит на программирование для API новой операционной системы [ Возможности библиотеки классов .NET Framework настолько широки, что вам практически не придется использовать функции API. ].Поскольку все языки .NET используют одну и ту же среду CLR, необходимость в исполнительных средах для отдельных языков отпадает. Более того, код, предназначенный для выполнения в CLR, может быть написан на любом языке и с одинаковым успехом использоваться во всех языках, соответствующих спецификации CLR [ В этом проявляется главное отличие .NET от Java: на платформе .NET можно использовать любой язык при условии, что он соответствует спецификации CLR. Программа, написанная на Java, работает на любой платформе (по крайней мере теоретически — на практике возникают проблемы), но при условии, что она написана именно на Java. Вероятно, именно языковая интеграция станет одной из составляющих успеха .NET. ]. В частности, код VB может использоваться в программах, написанных на С#, и наоборот, причем это не потребует дополнительных усилий со стороны программиста.
Следующее принципиальное новшество — общий формат исполняемого кода .NET, так называемый Microsoft Intermediate Language (промежуточный язык Microsoft), MSIL или просто IL Он представляет собой частично откомпилированный код, преобразуемый в машинный код средой .NET во время выполнения. Перед нами принципиальное усовершенствование схемы, существовавшей во всех версиях VB до версии 5. Раньше приложения VB компилировались в Р-код (псевдокод, машинный язык абстрактной машины), своего рода промежуточное представление окончательного исполняемого кода. Механизм времени выполнения интерпретировал Р-код при запуске программы пользователем. Пользователи постоянно жаловались на плохое быстродействие [ Вообще-то, в большинстве случаев причины следовало искать в другом месте. Скорость работы с мышью ограничена, так что в большинстве приложений с пользовательским интерфейсом переход на компилированный код особого выигрыша не давал. ]и упрашивали Microsoft включить в VB поддержку компиляции в машинный код. Начиная с версии 5 появилась возможность выбора между компактным Р-кодом и машинным (native) кодом, который занимал больше места, но теоретически быстрее работал. В языках .NET преимущества Р-кода объединились с преимуществами компилируемых языков. Сначала программа, написанная на любом языке, компилируется в IL (отдаленный аналог Р-кода), а затем полученный IL-код преобразуется в машинный код. Подобная двухшаговая схема относительно легко обеспечивает межъязыковую совместимость, а итоговое использование машинного кода обеспечивает хорошее быстродействие.
Многопоточность
Многопоточная программа выполняет несколько функций одновременно. Например, в почтовых клиентах во время приема новых сообщений можно читать старую электронную почту. Пользователи ценят подобные возможности, но в прежних версиях VB написание многопоточных приложений было очень непростой задачей. В главе 10 мы рассмотрим достоинства и недостатки этой новой, чрезвычайно мощной возможности VB .NET.NET и изменение парадигмы
Какое отношение все сказанное имеет к .NET? Самое прямое. Видите ли, .NET изменит подход к проектированию приложений так же сильно, как появление классов в VB некогда повлияло на проектирование приложений VB5 и 6. И переход на .NET вызовет определенные неудобства — как и переход от «бесклассовых» версий VB к поддержке классов! [ В поставку VB .NET входит утилита преобразования программ, но'не стоит возлагать на нее чрезмерные надежды. Ни одна серьезная программа не преобразуется автоматически — возможно, ее будет проще написать с нуля. ]Рассмотрим некоторые факторы, которые следует учитывать при переходе с VB6 на VB .NET.
Сравнение С# с VB .NET
Картина была бы неполной, если бы мы не упомянули о С#. Большая часть .NET Framework написана на С#, поэтому некоторые полагают, что именно С# является настоящим языком .NET. Хотя С# чуть мощнее VB .NET, 99% программистов никогда не будут пользоваться его дополнительными возможностями.Тому, кто никогда не программировал на C/C++, язык С# может показаться непонятным и более сложным, чем VB .NET. Кроме того, VB .NET имеет ряд несомненных преимуществ перед С#. Ниже перечислены пять из них, которые нам кажутся самыми важными:
Структурная обработка ошибок
Во всех версиях Visual Basic использовался механизм обработки ошибок, появившийся в самой первой версии BASIC (с тех пор прошло почти 40 лет!). Мягко говоря, у него есть недостатки. А если выражаться откровенно, использование в современном языке программирования команды On Error Goto, неимоверно усложняющей логику программы, — сущий абсурд. В VB .NET реализована структурная обработка ошибок (см. главу 7), самый современный и мощный механизм обработки ошибок.Трудности перехода на VB .NET
Возникает заманчивая картина: вы запускаете программу преобразования, немного дорабатываете полученный результат, и программа VB автоматически адаптируется для VB .NET!Поверьте, этот путь ведет в тупик. Чтобы в полной мере использовать преимущества VB .NET, необходимо основательно разобраться в объектно-ориентированном программировании и принципах работы .NET Framework. Вам не придется запоминать тысячи методов, входящих в .NET Framework, но для сознательного чтения документации или использования средств IntelliSense, встроенных в IDE, нужно знать, как работает .NET. Без хорошего понимания этого материала вы попросту не сможете работать в IDE с дизайнерами форм Windows и Web.
Чтобы представить масштаб изменений, достаточно рассмотреть простейший пример — форму, на которой находится кнопка. В прежних версиях VB весь код, связанный с кнопкой (и выполнявшийся при ее нажатии), находился в процедуре события Click.
Сразу предупреждаю: при размещении кнопки на форме VB .NET среда программирования генерирует значительно больший объем кода. В этой книге мы постараемся показать, почему в этом коде стоит разобраться — причем так, чтобы он казался не сложнее простого обработчика Click.
Приведенный ниже фрагмент (в основном автоматически сгенерированный) размещает кнопку на форме и выводит окно сообщения при нажатии кнопки. Числа в скобках к программе не относятся, они просто используются для ссылок на строки листинга в тексте книги.
(1) Public Class Form1
(2) Inherits System.Windows.Forms.Form
(3) #Region " Windows Form Designer generated code "
(4) Public Sub New()
(5) MyBase.New()
' Вызов необходим для работы дизайнера форм Windows
(6) InitializeComponent()
' Дальнейшая инициализация выполняется ' после вызова InitializeComponent()
End Sub
' Форма переопределяет Dispose для очистки списка компонентов.
(7) Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean) If disposing Then
If Not (components Is Nothing) Then
components. Dispose() End If End If
MyBase.Dispose(disposing) End Sub
(8) Friend WithEvents Buttonl As System.Windows.Forms.Button
' Необходимо для работы дизайнера форм Windows
Private components As System.ComponentModel.Container
' ВНИМАНИЕ: следующий фрагмент необходим для дизайнера форм Windows
' Для его модификации следует использовать дизайнер форм.
' Не изменяйте его в редакторе!
(9)
InitializeComponent()
'
'Button1
'
Me.Buttonl.Location = New System.Drawing.Point(109, 224)
Me.Buttonl.Name = "Buttonl"
Me.Button1.Size - New System.Drawing.Size(200. 48)
Me.Button1.TabIndex = 0,
Me. Button1. Text = "Click me!"
'
'Form1
'
Me.AutoScaleBaseSize = New System.Drawing.Size(5. 13)
Me.ClientSize = New System.Drawing.Size(292. 216)
Me.Controls.AddRange(New System.windows.Forms.Control() {Me.Button1})
Me.Name = "Forml"
Me.Text = "First windows Application"
Me.ResumeLayout (False)
End Sub #End Region
(10) Private Sub Buttonl_C1ick(ByVal sender As System.Object._
ByVal e As System.EventArgs) Handles Buttonl_Click
MsgBox("Welcome to Visual Basic .NET!")
End Sub
(11) End Class
VB как объектно-ориентированный язык
Объектно-ориентированные средства VB5 и VB6 были, мягко говоря, ограниченными. В частности, эти версии VB не позволяли автоматически инициализировать данные класса при создании экземпляра. В результате объект создавался в неопределенном состоянии, что повышало вероятность ошибок и заставляло программиста принимать дополнительные меры предосторожности при работе с объектами. Для решения этой проблемы в VB .NET появились параметризованные конструкторы (см. главу 4).Другим недостатком было отсутствие полноценного наследования (эта тема рассматривается в главе 5 [ Наследование — штука полезная, но оно не является панацеей для написания объектно-ориентированных программ, как считают некоторые. Бесспорно, это важное, но не самое важное усовершенствование в VB .NET. ]). Наследованием называется особая форма многократного использования кода, при которой программист определяет новые объекты на базе существующих объектов. Наследование очень хорошо подходит для таких задач, как создание нового текстового поля с расширенными возможностями на основании стандартного текстового поля. В VB версий 5 и 6 наследование не поддерживалось, поэтому для построения улучшенного текстового поля приходилось прибегать к услугам неудобной и ненадежной программы-мастера (wizard).
Рассмотрим другой пример, в котором было бы уместно наследование, — создание классов для работы со специализированными коллекциями. Чтобы создать коллекцию, специализировавшуюся на хранении строковых данных, в VB5 и 6 в класс включалось закрытое поле:
Private mCollection As Collection
В обработчиках событий Initialize и Terminate происходило выделение и освобождение памяти, используемой закрытой коллекцией. Затем программировались методы специализированной коллекции, предназначенные для внешнего использования. Большинство таких методов сводилось к простому вызову соответствующего метода закрытой коллекции, например:
Sub Add(Item As String)
mCollection.Add Item
End Sub
Но самое противное начиналось в том случае, если содержимое коллекции требовалось перебирать в цикле For Each. Для этого в модуль класса приходилось включать фрагменты вида:
Public Function NewEnum As lUnknown
Set NewEnum = mCollection.[_NewEnum]
End Function
Но и это не все — этой функции следовало присвоить идентификатор —4!
Принцип «абракадабра — достаем из шляпы кролика!» хорош для фокусника, но не для программиста. При использовании наследования вся эта белиберда не нужна. В VB .NET достаточно написать:
Class MyCollection
Inherits Collection
...и вы получаете автоматическую поддержку For Each (см. главу 5).
Версии Visual Basic
Первые две версии Visual Basic для Windows хорошо подходили для создания прототипов программ и демонстрационных приложений — но этим все и ограничивалось. В обеих версиях отличная среда программирования сочеталась с относительной простотой языка. Сам язык обладал относительно бедными возможностями. С появлением VB3 и новых средств работы с базами данных, требовавших изучения новой модели программирования, первая реакция нередко была обескураживающей: «Зачем они испортили VB?!» Сейчас становится понятно, что включение поддержки баз данных в VB3 было необходимо, чтобы Visual Basic из «игрушечного языка» превратился в серьезный инструмент программирования. В VB4 появились базовые возможности для создания объектов, а следовательно — базовые средства объектно-ориентированного программирования. В VB5 и VB6 объектно-ориентированные аспекты языка были расширены, появились новые возможности создания элементов и использования интерфейсов. Однако сам язык постепенно утрачивал целостность, поскольку объектно-ориентированные средства строились на базовом фундаменте, в котором их поддержка не предусматривалась. Например, правильность создания объектов в Visual Basic не гарантировалась — программисту приходилось применять особые синтаксические конструкции вместо конструкторов, используемых практически во всех объектно-ориентированных языках (конструкторы рассматриваются в главе 4). В итоге разработчики VB пришли к выводу, что поддержка VB на платформе .NET потребует новых изменений — например, использование .NET Framework требует полноценной объектной ориентации языка.Вероятно, при знакомстве с изменениями VB основные трудности возникнут не в связи с новшествами в среде IDE или появлением новых ключевых слов, а из-за необходимости радикального пересмотра парадигмы программирования на VB. В частности, чтобы в полной мере использовать преимущества VB5 и VB6, с объектно-базированного подхода, обладавшего весьма ограниченными возможностями создания новых объектов, приходилось переходить на объектно-ориентированный подход, в инструментарии которого, например, важное место занимают интерфейсы. К сожалению, большинство программистов VB, выросших на этом продукте, ранее никогда не использовали принципы объектно-ориентированного программирования. Когда в VB впервые появились классы, многие программисты VB понятия не имели, что это такое и зачем они нужны.
Но даже эти ограниченные возможности, появившиеся в VB5 и VB6, при правильном применении упрощали работу над большими проектами. Например, они позволяли создавать компоненты многократного использования (такие, как элементы управления), а на более прозаическом уровне — просто приводить код в порядок, упрощая его сопровождение. В некоторых случаях удавалось исключить оператор Sel ect Case, нередко порождавший большие проблемы с сопровождением. Речь идет о конструкциях, более или менее похожих на следующие: [ В поставку VB .NET входит утилита преобразования программ, но не стоит возлагать на нее чрезмерные надежды. Ни одна серьезная программа не преобразуется автоматически — возможно, ее будет проще написать с нуля. ]
Select Case kindOfEmployee
Case Secretary
RaiseSalary 5%
Case Manager
RaiseSalary 10%
Case Programmer
RaiseSalary 15%
Case Architect
RaiseSalary 20 %
' и т. д.
End Select
Сопровождение подобного кода было делом крайне неприятным, поскольку при каждом добавлении нового типа сотрудника (Employee) приходилось изменять все соответствующие операторы Select Case, тогда как эту работу можно было бы поручить компилятору. Начиная с VB5 это наконец стало возможным, поскольку волшебство полиморфизма интерфейсов (см. главу 5) позволяло использовать конструкции вида:
For Each employee in Employees
employee.RaiseSalary
Next
Компилятор анализировал объект и автоматически выбирал нужный метод Rai seSal ary. "Классы заметно повышают эффективность и удобство сопровождения приложений VB. Останетесь ли вы с VB5 или перейдете на VB .NET — без классов трудно представить себе серьезное приложение VB.
| Содержание | Вперед |
Visual Basic: прошлое и настоящее
Visual Basic для Windows появился около 10 лет назад. Дебют состоялся 20 марта 1991 года на выставке «Windows World», хотя своими корнями он уходит к программе Ruby, написанной Аланом Купером (Alan Cooper) в 1988 году.Бесспорно, появление Visual Basic произвело настоящую сенсацию. Стив Гиб-сон (Steve Gibson) в журнале «InfoWorld» назвал Visual Basic «потрясающим новым чудом», которое «радикально изменит подход к программированию для Microsoft Windows». Чарльз Петцольд (Charles Petzold), автор знаменитой книги, посвященной программированию для Windows на языке С, написал в «New York Times»: «Visual Basic представляет настоящую угрозу для благополучия тех, кто зарабатывает себе на жизнь, разъясняя программистам сложности программирования для Windows» (вряд ли к комментарию Петцольда стоит относиться серьезно, поскольку с того знаменательного дня были проданы миллионы книг, посвященных VB). Еще решительнее высказался Стюарт Элсоп (Stewart Alsop): он назвал Visual Basic «идеальной средой программирования для 90-х годов».
Но 90-е годы уже прошли, поэтому никого не удивит тот факт, что Visual Basic .NET отличается от обычного Visual Basic так же сильно, как Visual Basic версии 1 отличается от своего предшественника QuickBasic. Хотя из прежнего опыта использования Visual Basic можно вынести много полезного, переход на платформу [ Microsoft серьезно относится к этому слову. В частности, Windows тоже именуется платформой. ].NET и Visual Basic .NET (сокращенно VB .NET) сопровождается такими же основательными изменениями, как и переход с QuickBasic для DOS на VB1 для Windows.
Cамоучитель по VB.NET
/B> Диалоговое окно New Project

После прокрутки списка в диалоговом окне New Project появляется значок Console Application (Консольное приложение), показанный на Рисунок 2.2. Обратите внимание: при выборе любого значка, кроме последнего — New Project in Existing Solution (Новый проект в существующем решении), — вам не придется подтверждать создание нового решения. Дело в том, что при создании нового проекта за пределами существующего решения IDE создает базовую структуру решения за вас. Большинство программистов .NET предпочитают создавать решения в отдельных каталогах, имена которых совпадают с именами решений. По умолчанию IDE поступает с новыми решениями именно таким образом.
Чтобы сосредоточить все внимание на новых возможностях языка VB .NET, не отвлекаясь на тонкости работы графических приложений в .NET, в первой части книги рассматриваются только консольные приложения. Они работают в текстовом режиме; с некоторым упрощением можно считать, что весь ввод/вывод осуществляется в окне DOS (данные читаются из стандартного входного потока и записываются в стандартный выходной поток).
В нашем примере решение было названо vb_ide_01, но вы можете использовать любое допустимое имя файла. При желании в него можно включить прописные буквы или пробелы. Регистр символов в файловой системе Windows игнорируется при выполнении операций, но сохраняется для удобства пользователя. При установке флажка Create Directory for Solution (Создать каталог для решения) IDE автоматически создает в заданном основном каталоге подкаталог, имя которого совпадает с именем решения. В нашем примере это привело к созданию каталога C:\vb net book\Chater 2\vb_ide_01. Примерный вид IDE показан на Рисунок 2.3.
Дерево каталогов после компиляции

Как упоминалось выше, исходные файлы хранятся на верхнем уровне иерархии, в каталоге vb_ide_01. В каталог bin помещаются двоичные файлы, полученные при компиляции, — в нашем примере создаются файлы с расширениями .ехе и .pdb. Файл с расширением .pdb содержит отладочную информацию и создается только в том случае, если отладочная информация была затребована в диалоговом окне параметров компиляции (Project > Configuration Properties > Build).
Диалоговое окно Add Reference

В приложениях .NET можно использовать традиционные компоненты СОМ, а следовательно, и компоненты ActiveX (в том числе и написанные вами). Взаимодействие технологий .NET/COM основано на механизме COM Interop (см. главу 13). Впрочем, потенциальная возможность еще не означает, что это бледует делать. Использование компонентов СОМ в приложениях .NET сопряжено с основательными затратами ресурсов.
Диалоговое окно Options

Новый редактор поддерживает и такую удобную возможность, как свертка фрагментов программы и отображение на их месте заголовков. Обратите внимание на значки «+» рядом с некоторыми строками на Рисунок 2.11. Если щелкнуть на таком значке, в листинге открывается соответствующая область (region). Если задержать указатель мыши над многоточием (...), на экране будет показан свернутый код. Для управления сверткой используется подменю Edit > Outlining.
Вы можете определять в программе собственные области; для этого достаточно имитировать синтаксис, показанный на Рисунок 2.11. Сворачиваемый блок (регион) начинается с команды «Region "имя_региона" и завершается командой #End Region.
Диалоговое окно свойств проекта

По сравнению с VB6 параметров совсем немного. Впрочем, жалеть об этом не нужно: многое из того, чем в VB6 приходилось заниматься вам, в CLR делается автоматически. В частности, в параметрах компиляции можно указать, нужно ли создавать отладочную информацию (см. следующий раздел), определять константы DEBUG и TRACE и выдавать предупреждения [ Очень трудно представить ситуацию, в которой отключение предупреждений было бы оправдано. ]. Константы DEBUG и TRACE выполняют те же функции, что и в VB6: они позволяют использовать в программе команды условной компиляции:
#If DEBUG Then
Debug.WriteLine("In debug mode")
#End If
#If TRACE Then Trace. WhteLirie( "Tracing")
#End If
Если константа DEBUG не определена, то проверка в первой строке не проходит и команда Debug.WriteLine не выполняется. То же самое происходит и при проверке константы TRACE.
В категории Optimizations списка Configuration Properties можно отключить проверку целочисленного переполнения — впрочем, делать это нежелательно. Вероятно, Microsoft добавит новые способы оптимизации в окончательной версии VB .NET или в дополнениях.
Информация о результатах пакетной компиляции

Из рисунка видно, что оба проекта нашего решения были удИешно построены. Если бы в процессе построения возникли какие-либо ошибки, их описания появились бы в окне вывода.
При выполнении команды Build > Batch Build появляется диалоговое окно, показанное на Рисунок 2.23. Если решение состоит из нескольких проектов и в одном-двух из них возникают ошибки, вы по достоинству оцените эту команду.
Компиляция
Как упоминалось в главе 1, программа .NET в процессе компиляции сначала переводится на промежуточный язык IL, а затем компилируется в машинный код. Допустим, вы решили построить исполняемый файл на базе решения vb_ide_01.В рассмотренном примере задействованы две единицы компиляции — наши два проекта. Исполняемый файл может строиться на основе любого проекта, входящего в решение, а проекты компилируются независимо друг от друга. Чтобы откомпилировать проект, проще всего щелкнуть правой кнопкой мыши на одном из проектов в окне решения и выбрать команду Build или Rebuild в контекстном меню. При выборе команды Build компилятор ограничивается построением частей проекта, изменившихся с момента последнего построения, а команда Rebuild строит заново весь проект. Команда Build используется чаще, поскольку она работает быстрее (при запуске проекта клавишей F5 выполняется команда Build, а не Rebuild).
Результаты компиляции и построения проекта отображаются в окне вывода. На Рисунок 2.19 приведены выходные данные для проекта vb_ide_01.
Контекстная справка

Бросается в глаза другая эффектная особенность IDE — если пристыкованное (docked) окно полностью перекрывает другое окно, это вызовет меньше проблем, чем в VB6, поскольку скрытые окна автоматически отображаются в виде корешков (tabs). Пример показан на Рисунок 2.6 — обратите внимание на корешки в позиции курсора. Чтобы вызвать скрытое окно, достаточно щелкнуть на корешке и перетащить его мышью. Чтобы изменить порядок окон (например, для экономии места), просто перетащите один корешок поверх другого. Подобное использование корешков выгодно отличает VS .NET от VB6 IDE, где в результате неосторожной стыковки окон становилось совершенно невозможно работать и для возвращения к нормальному состоянию приходилось править системный реестр. Также обратите внимание на корешок Start Page главного окна, предназначенный для вызова начальной страницы IDE.
Контекстное меню редактора

Как видите, в этом контекстном меню объединяются команды редактирования и отладки.
Кнопки панелей инструментов снабжены подсказками [ Предусмотрена даже автоматическая нумерация строк программы! Режим нумерации включается в диалоговом окне, вызываемом командой Tools > Option > Text Editor. ]. На некоторых кнопках имеются стрелки, показывающие, что при нажатии кнопки открывается меню. Например, вторая кнопка слева (Add New Item) открывает список элементов, которые можно включить в решение (Рисунок 2.8).
В VB .NET снова стали доступны чрезвычайно удобные команды блочного комментирования/снятия комментариев, впервые появившиеся в VB5. Теперь эти команды вызываются со стандартных панелей инструментов IDE, а не из меню Edit, откуда они таинственно исчезли в VB6.
Панель элементов (toolbox) используется преимущественно при программировании приложений GUI (см. главу 8), но на ней также расположен многоэлементный буфер обмена (clipboard ring), описанный в следующем разделе. Необходимо упомянуть и о такой экзотической возможности, как сохранение фрагментов кода прямо на панели элементов. Эта тема тоже рассматривается в следующем разделе.
Как ни странно, само наличие подсказок убеждает в том, что графический интерфейс не всемогущ. Кто знает, что нас ждет в будущем — может, в каком-нибудь прогрессивном варианте пользовательского интерфейса на кнопках вместо рисунков будет размещаться текст?
Многоэлементный буфер обмена
Буфер обмена рассчитан не на один элемент, а на несколько сразу (аналогичные возможности существуют в Office 2000 и Office XP). Весь вырезанный или скопированный текст передается в многоэлементный буфер обмена, к которому можно обратиться с панели элементов. Чтобы просмотреть текущее содержимое буфера, щелкните на корешке Clipboard Ring на панели элементов. В буфере хранятся 15 последних скопированных или вырезанных фрагментов текста.Комбинация клавиш Ctrl+Shift+V вставляет очередной элемент буфера в текущий документ.
Многократно нажимая клавиши Ctrl+Shift+V, вы перебираете содержимое буфера. При каждом нажатии Ctrl+Shift+V предыдущий вставленный фрагмент заменяется текущим элементом буфера.
Начальная страница Visual Studio

Вы можете настроить клавиатуру и раскладку окон и сохранить разные комбинации параметров в разных профилях. Чтобы сменить профиль, выполните команду Help > Start Page и откройте ссылку My Profile.
В VB .NET каждый проект является частью того, что Microsoft называет решением (solution). Любой код, созданный в VB .NET IDE, относится к некоторому решению. Решение можно рассматривать как хранилище всей информации, необходимой для компиляции программы и ее перевода в форму, пригодную для исполнения. Таким образом, решение состоит из одного или нескольких проектов; различных вспомогательных файлов (графических изображений, ресурсных файлов, метаданных, то есть данных, описывающих другие данные, и т. д.); документации в формате XML и практически всего, что приходит В голову. Программисты с опытом работы на VB5 или 6 могут рассматривать решение как аналог программной группы. На первых порах решения кажутся неудобными; откровенно говоря, при написании мелких программ они действительно слишком громоздки. Но постепенно вы привыкнете к решениям и убедитесь, что они заметно упрощают работу над крупными коммерческими проектами. Дело в том, что решение позволяет легко выбрать файлы, задействованные в решении конкретной проблемы.
| Содержание | Вперед |
Новые возможности отладчика
Отладчик VB .NET обладает некоторыми новыми возможностями, отсутствовавшими в VB6. Краткая сводка этих возможностей приведена ниже.Окно памяти
Окно памяти предназначено для просмотра фактического содержимого заданной области памяти. Ни в одной из прежних версий VB не поддерживалась эта возможность, чрезвычайно полезная в некоторых ситуациях — например, если вы хотите проследить за выполнением низкоуровневого кода и выяснить, что же именно происходит при работе вашей программы. Окно памяти вызывается в IDE командой Debug > Windows > Memory > Memory1 (или 2-4). Примерный вид окна памяти показан на Рисунок 2.26. Если щелкнуть в окне памяти правой кнопкой мыши, появится контекстное меню, в котором выбирается представление выходных данных.Окно решения и окно свойств для файла Module1.vb

В VB .NET всем файлам с кодом Visual Basic независимо от их типа присваивается расширение .vb — расширения .frm, .bas и .cls не используются. Впрочем, одна важная особенность осталась неизменной: файлы с расширением .vb, как и в VB6, содержат обычный текст (причем в бесплатно распространяемый пакет .NET SDK входит автономный компилятор VB для компиляции программ, написанных во внешнем текстовом редакторе).
Позже вы узнаёте, как в IDE конструируются формы и как среда определяет, какие части файла имеют визуальное представление, а какие не имеют. А пока достаточно запомнить, что все файлы VB .NET имеют расширение .vb.
В начале работы можно создать пустое решение без создания проекта — для этого следует выбрать опцию Visual Studio Solutions4Blank Solution в диалоговом окне New Project. Обычно это делается в тех случаях, когда вы не хотите, чтобы имя решения совпадало с именем одного из проектов.
Окно решения
В окне решения (Solution Explorer), показанном на Рисунок 2.14, выводится список файлов, входящих в решение. По умолчанию имя решения совпадает с именем первого созданного в нем проекта. На рисунке изображено решение vb_ide_01 с проектом vb_ide_01, содержащим файл с именем Modulel.vb.Окно свойств
Функции окна свойств в VS .NET (также показанного на Рисунок 2.14) уже не ограничиваются простым заданием свойств элементов управления. Содержимое окна зависит от того, что в настоящий момент выделено в IDE. Имя и тип выделенного элемента указаны в списке, находящемся в верхней части окна. Чтобы изменить значение свойства, щелкните в правой ячейке и начинайте вводить символы. В окне свойств действуют стандартные комбинации клавиш, используемые при редактировании в системе Windows.На Рисунок 2.14 в окне свойств задаются свойства файла Modulel.vb. При помощи этого окна можно задать свойства дизайнера (например, используемого при построении web-приложений или серверных приложений).
| Кнопка | Описание | |
Окно внешних ссылок

Иерархический список файлов в окне решения содержит ветвь References с информацией обо всех сборках (assemblies), используемых в проекте (пока можно считать, что термин «сборка» является аналогом DLL; сборки подробно описаны в главе 13). Диалоговое окно внешних ссылок в решениях VB .NET напоминает похожее окно, которое использовалось для импортирования библиотек СОМ в проектах VB6. Некоторые базовые сборки .NET используются в каждом проекте. Разверните ветвь дерева, щелкнув назначке «+»; примерный результат показан на Рисунок 2.15. Обратите внимание — имена всех базовых сборок начинаются с префикса System.
Теперь щелкните правой кнопкой мыши в строке References окна решения и выберите в контекстном меню команду Add Reference (также можно воспользоваться командой Project > Add Reference главного меню). На экране появляется диалоговое окно, изображенное на Рисунок 2.16. В нем можно добавлять внешние ссылки трех типов: .NET, COM и другие проекты (Projects).
Окно вывода и окно команд
В окне вывода (вызываемом командой View > Other Windows или комбинацией клавиш Ctrl+Alt+О) отображается текущая информация состояния. При построении решения (см. раздел «Компиляция» данной главы) в этом окне компилятор выводит сообщения как об успешном завершении, так и о возникших ошибках.Окно команд (вызывается командой View > Other Windows или комбинацией клавиш Ctrl+Alt+A) напоминает окно отладки (Immediate window) VB6 и применяется в процессе отладки, о котором будет рассказано ниже. К сожалению, пользы от него существенно меньше, чем от окна отладки VB6, — окно команд не поддерживает IntelliSense и не работает в режиме конструирования (а точнее, окно команд обладает крайне ограниченной поддержкой IntelliSense для меню и макросов, но не для объектов и не в процессе отладки).
С другой стороны, окно команд позволяет взаимодействовать со средой IDE. Например, в нем можно ввести команду:
File.AddNewProject
На экране появляется диалоговое окно New Project. Выглядит эффектно, но на практике удобнее работать с меню.
Окно команд работает в двух режимах: в режиме команд (Command) и в режиме непосредственного ввода (Immediate). Для переключения между режимами в окне вводятся строки >cmd или immed (без префикса <<>»!). Ниже перечислены комбинации клавиш, используемые при перемещении в окне команд.
| Операция |
Клавиши |
| Перебор ранее введенных команд | вверх, вниз |
| Прокрутка окна вверх | Ctrl + вверх |
| Прокрутка окна вниз | Ctrl + вниз |
| Назад | Содержание | Вперед |
Окно вывода при обнаружении ошибок

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

На этот раз компиляция прошла успешно, но бывает и иначе. На Рисунок 2.20 показано сообщение об ошибке, полученное при вызове несуществующей функции.
Основные окна IDE
В этом разделе описаны основные окна IDE. Специализированные окна (например, предназначенные для отладки) рассматриваются позже в этой или в одной из последующих глав. Но сначала напомним, что в VS .NET IDE, как и в большинстве современных приложений Windows, контекстные меню вызываются правой кнопкой мыши. Поэкспериментируйте и освойтесь с разными вариантами контекстных меню. Например, контекстное меню редактора показано на Рисунок 2.7.Отладка потоков
В отладчике VB .NET предусмотрено еще одно важное средство — просмотр всех выполняемых потоков (threads) приложения. Переключение потоков в отладчике играет очень важную роль в отладке многопоточных приложений. Мы вернемся к этой теме в главе 10 при знакомстве с многопоточным программированием.Отладка процесса Notepad.exe

Листинг выглядит устрашающе, поскольку мы не располагаем отладочной символической информацией для программы Notepad.exe. При наличии этих данных в окне появился бы фрагмент исходного текста функции, выполнявшейся в момент передачи управления отладчику.
Отладка процессов
С технической точки зрения в любом сеансе отладки всегда задействован некоторый процесс (о процессах рассказано в главе 10). В предыдущих версиях VB отладчик не позволял подключаться к работающим процеЪсам — такая возможность была предусмотрена только в отладчике Visual C++. В VB .NET команда Debug > Processes выводит диалоговое окно, показанное на Рисунок 2!27.Отладка управляемого и неуправляемого кодов
Управляемым (managed) в .NET называется код, выполняемый при участии CLR. В управляемом коде нельзя использовать указатели, а выделением/освобождением памяти занимается CLR. Неуправляемый код не подчиняется этим ограничениям. Он может создаваться в C++ и С#, но в VB .NET такая возможность не поддерживается.Управляемый код усложняет работу некоторых средств отладки. Дело в том, что исполнительная среда CLR прикладывает значительные усилия к оптимизации выполняемого кода, что затрудняет получение правильных кадров стека (то есть адресов всех вызванных функций). Кроме того, в зависимости от специфики программы оптимизация может достичь такой степени, что отображаемый в отладчике код плохо ассоциируется с исходным текстом. Впрочем, по сравнению с преимуществами новой среды отладки VB .NET эти проблемы уходят на второй план.
Отладка в VB.NET
В этом разделе приводится краткий обзор изменений в средствах отладки VB .NET. Мы вернемся к этой важной теме позже, когда у нас появится содержательный код для отладки. К сожалению, начинать приходится с печального известия. Если раньше VB позволял прервать работу программы, отредактировать ее и продолжить выполнение с учетом внесенных изменений, то начиная с бета-версии 2 эта возможность не поддерживается. Программу можно редактировать в процессе отладки, однако изменения вступают в силу лишь после повторной компиляции. Впрочем, различные средства пошагового выполнения и прерывания программ (такие как условные точки прерывания) работают так же, как и прежде.И все же можно с уверенностью сказать, что существование общего отладчика уровня VS .NET, по своим возможностям сравнимого с отладчиком VC++, является одним из самых заметных усовершенствований VB .NET на фоне предыдущих версий VB. Значительно расширились возможности работы со всеми составляющими приложения, вплоть до отладки на уровне загруженных модулей и программных потоков.
Для работы отладчика необходим файл .pdb с отладочной информацией. Чтобы создать этот файл, необходимо установить флажок Generate symbolic debug information в диалоговом окне параметров компиляции. По данным файла .pdb отладчик определяет, какая строка исходного текста соответствует текущей позиции исполняемого файла и какие символические имена были присвоены переменным программы. Без отладочной информации неполадки придется искать в ассемблерном листинге.
Отладочная и окончательная версии
В верхней части диалогового окна Project Properties > Configuration Properties > Build находится раскрывающийся список Configuration, состоящий из трех пунктов: Release (Окончательная версия), Debug (Отладочная версия) и All Configurations (Все конфигурации). При помощи этого списка можно задавать разные наборы параметров для разных типов компиляции. Например, когда работа над приложением близится к концу, в окончательной версии можно изменить некоторые из параметров, установленных ранее в отладочной версии. Для этого следует выбрать в списке пункт Release и произвести дополнительную настройку. Кнопка Configuration Manager позволяет задать параметры компиляции сразу для нескольких проектов.Обычно различия между версиями сводятся к включению отладочной информации или разрешению/запрету оптимизаций. Мы рекомендуем в процессе разработки использовать отладочную конфигурацию, а затем построить итоговый вариант продукта в окончательной конфигурации. Например, в отладочной конфигурации можно включить режим интерпретации предупреждений как ошибок («Treat warnings as errors»), а в окончательной конфигурации — отключить его.
Пакетная компиляция
Иногда бывает удобнее откомпилировать сразу несколько проектов решения вместо того, чтобы компилировать их по отдельности. В таких ситуациях применяются средства пакетной компиляции VB .NET. Команда Build > Build Solution компилирует все проекты, входящие в решение. Она часто используется в конце работы над приложением, когда все готово к формированию окончательного пакета для распространения (эта тема рассматривается в главе 13).Команда Batch Build позволяет выбрать проекты для компиляции. Она особенно удобна при работе над несколькими проектами, если вы не хотите подолгу ожидать выполнения команды Build All или компилировать каждый проект по отдельности. На Рисунок 2.22 показано, как выглядит окно вывода при использовании команды Build Solution для решения vb_ide_01.
Параметры компиляции
Познакомившись с разными способами компиляции проектов и решений, мы переходим к описанию параметров компиляции отдельных проектов. Щелкните правой кнопкой мыши на имени проекта в окне решения и выберите команду Properties > Configuration Properties > Build — на экране появляется окно для настройки параметров компиляции. Например, параметр Debugging позволяет задать аргументы командной строки.Приступаем к работе
Пользователи прежних версий VB обычно предпочитают, чтобы среда IDE по внешнему виду и поведению была как можно ближе к традиционной среде VB6. Для этого откройте на начальной странице VS ссылку My Profile и выберите строку Visual Basic Developer в списке Profile, как показано на Рисунок 2.1.Работа с решением
Вернемся к решению vb_ide_01, о котором говорилось выше. Хотя мы еще не ввели ни одной строки программного кода, VB .NET автоматически создает в каталоге решения несколько файлов и подкаталогов (в этом нетрудно убедиться при помощи Проводника Windows). Ниже приведен полный список содержимого каталога; в вашем случае данные будут похожими, хотя и не совпадающими полностью.943 Assemblylnfo.vb
79 Modulel.vb
obj
1,354 vb_ide_01.sln
7,168 vb_ide_01.suo
3,008 vb_ide_01.vbproj
1.643 vb_ide_01.vbproj.user
6 File(s) 14,195 bytes
Прдкаталоги bin и obj используются при компиляции, а четыре файла образуют решение. В каталоге bin хранится откомпилированный код. В каталоге obj находится подкаталог, задействованный в процессе отладки. Исходный текст программы хранится в файле Modulel.vb. При просмотре файла в текстовом редакторе вы увидите следующий код:
Module Modulel
SubMain()
End Sub
End Module
Файл vb_ide_01.sln аналогичен .vbp-файлу проекта в VB6. В этом файле хранится вся служебная информация, необходимая для компиляции решения, в том числе описание всех проектов и файлов решения. Примерное содержимое этого файла приведено ниже:
Microsoft Visual Studio Solution File. Format Version 7.00
Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "vbjdejl", _
"vbjide_01\vb_ide_01.vbproj". "{F40E94D3-09CA-4E17-90EA-7A514E991F93}"
EndProject
Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "vb_ide_02". _
"vb_ide_02\vbjde_02.vbproj", "{926DC073-167F-49DO-8A30-AF27E27BA2B4}"
EndProject
Global
GlobalSection(SolutionConfiguration) = preSolution ConfigName.0 = Debug ConfigName.l = Release EndGlobalSection
GlobalSection(ProjectDependencies) = postSolution EndGlobalSection GlobalSection(ProjectConfiguration) = postSolution
{F40E9403-09CA-4E17-90EA-7A514E991F93}.Debug.ActiveCfg = Debug|.NET {F40E94D3-09CA-4E17-9DEA-7A514E991F93}.Debug.Bui 1 d.0 = Debug|.NET {F40E94D3-09CA-4E17-9DEA-7A514E991F93}.Release.ActiveCfg = Release].NET {F40E94D3-09CA-4E17-9DEA-7A514E991F93}.Release.Build.0 = Release].NET {926DC073-167F-49В0-8A30-AF27E27BA2B4}.Debug.ActiveCfg = Debug|.NET {926DC073-167F-49D0-8A30-AF27E27BA2B4}.Debug.Build.0 = Debug|.NET {926DC073-167F-49D0-8A30-AF27E27BA2B4}.Release.ActiveCfg = Release!.NET {926DC073-167F-49D0-8A30-AF27E27BA2B4}.Release.Build.0 = Release].NET EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution EndGlobalSection
GlobalSection(ExtensibilityAddlns) = postSolution EndGlobalSection
EndGlobal
Файл vb_ide_01.vbproj написан на языке XML В нем хранится информация о проекте, в том числе значения различных свойств. Для изменения свойств проекта обычно используется команда Properties меню Project или контекстного меню, вызванного щелчком правой кнопщй мыши в окне решения.
Язык XML занимает очень важное место в .NET. Все объекты, создаваемые в .NET, по возможности описываются (и даже передаются в Web) на языке XML.
Ниже приведено содержимое файла проекта в текстовой форме. Обратите внимание на частое повторение ключевого слова Assembly — это ключевое слово вместе с другими важными ключевыми словами Imports и Namespaces рассматривается в главе 4:
ProjectType = "Local"
ProductVersion = "7.0.9254"
SchemaVersion = "1.0"
ProjectGuid = "{E24CC2EA-3E48-4C6E-8F92-ODE603B335D6}"
>
Applicationlcon = ""
AssemblyKeyContainerName = ""
AssemblyName = "vb_ide_01"
AssemblyOhginatorKeyFile = ""
AssemblyOriginatorKeyMode = "None"
DefaultClientScript = "JScript"
DefaultHTMLPageLayout = "Grid"
DefaultTargetSchema = "IE50"
DelaySlgn = "false"
OutputType = "Exe"
OptionCompare = "Binary"
OptionExplicit = "On"
OptionStrlct = "Off"
RootNamespace = "vb_ide_01"
StartupObject = "vb_ide_01.Modulel"
>
Name = "Debug"
BaseAddress = "285212672"
ConfigurationOverrideFile= ""
DeflneConstants = ""
DefineDebug = "true"
DefineTrace = "true"
DebugSymbols = "true"
IncrementalBuild = "true"
Optimize = "false"
OutputPath = "bin\"
RegisterForComlnterop = "false"
RemovelntegerChecks = "false"
TreatWarningsAsErrors = "false"
WarningLevel = "1" />
Name = "Release"
BaseAddress - "285212672"
ConfigurationOverrideFile = ""
DefineConstants = ""
DeflneDebug - "false"
DefineTrace = "true"
DebugSymbols = "false"
IncrementalBuild ="false"
Optimize = "false"
OutputPath = "bin\"
RegisterForComlnterop = "false"
RemovelntegerChecks = "false"
TreatWarningsAsErrors = "false"
WarningLevel = "1"
/>
AssemblyName = "System"
/>
Reference
Name = "System. Data"
AssemblyName = "System. Data"
/>
Name = "System. XML"
AssemblyName = "System. Xml"
/>
RelPath = "Assemblylnfo.vb" SubType = "Code" . BuildAction = "Compile" />
RelPath = "Modulel.vb" SubType = "Code" BuildAction = "Compile"
/>
Двоичный файл vb_ide_01.suo содержит пользовательскую инфьрмацию уровня решения — в частности, сведения об установленных точках прерывания и открытых документах. Если удалить этот файл, все пользовательские настройки будут потеряны, но решение останется работоспособным. В файле vbproj.user хранятся аналогичные параметры уровня проекта (как и когда проект запускается, следует ли при компиляции создавать отладочную версию и т. д.). Этот файл тоже написан на языке XML:
Name = "Debug"
EnableASPDebugging = "false"
EnableASPXDebugging = "false"
EnableUnmanagedDebugging = "false"
EnableSQLServerDebugging = "false"
StartAction = "Project"
StartArguments = ""
StartPage = ""
StartProgram = ""
StartURL = ""
StartWorkingDirectory = ""
StartWithIE = "false" />
Name = "Release"
EnableASPDebugging ="false"
EnableASPXDebugging = "false"
EnableUnmanagedDebugging = "false"
EnableSQLServerDebugging = "false"
StartAction = "Project"
StartArguments = ""
StartPage = ""
StartProgram = ""
StartURL = ""
StartWorkingDirectory = ""
StartWithlE = "false"
/>
CopyProjectDestinationFolder = ""
CopyProjectUncPath = ""
CopyProjectOption = "0"
ProjectView = "ProjectFiles"
/>
Редактор
Редактор обладает полным набором стандартных возможностей, поддерживаемых в редакторах такого рода (вырезание, вставка, поиск/замена и т. д.). Для работы с ними можно использовать стандартные комбинации клавиш Windows (Ctrl+X — вырезать, Ctrl+V — вставить и т. д.). Если вы предпочитаете работать с командами меню, к вашим услугам меню Edit и контекстное меню окна программы. Полный список сочетаний клавиш вызывается из меню Edit; кроме того, он приведен в разделе «Editing, shortcut keys» справочной системы. Например, комбинация Ctrl+I включает режим поиска с приращением.Некоторые команды — такие как Option Explicit— используются по умолчанию и не отображаются в окне программы, как в VB6 (хотя мы все равно включаем их в программу по привычке!). Подробнее об этих командах будет рассказано в следующей главе.
В вашем распоряжении и очень удобное средство IntelliSense, выдающее информацию о методах заданного объекта или параметрах, передаваемых при вызове функции (Рисунок 2.9). Обычно IntelliSense автоматически вызывается при вводе символа «.».
Настройка большинства глобальных параметров редактора выполняется в диалоговом окне — выполните команду Tools > Options и выберите в списке строку Text Editor, как показано на Рисунок 2.10. По сравнению с VB6 диалоговое окно Options основательно изменилось, поэтому мы рекомендуем внимательно изучить его содержимое. Например, чтобы выбрать размер позиций табуляции, щелкните в строке Text Editor (см. Рисунок 2.10) и выберите нужное значение для всех языков или только для VB. Здесь же выбирается режим создания отступов: None (отступы отсутствуют), Block (курсор выравнивается по началу предыдущей строки) или Smart (автоматическое создание отступов в теле цикла, как того требует хороший стиль программирования). Кстати говоря, устанавливать размер позиций табуляции и форматировать отступы можно в готовом тексте, для чего используются комбинации клавиш Ctrl+K, Ctrl+F или- команда Edit > Advanced > Format Selection. Если в режиме Smart выделить фрагмент программы и нажать клавиши Shift+Tab, этот фрагмент будет переформатирован.
Module Modulel
Sub Main()
System.Console.
End Sub
Создание нового решения
Новое решение создается командой File > New. Вам предлагается выбрать один из двух вариантов: создать новый проект (New Project) или пустое решение (Blank Solution). Даже при выборе первого варианта IDE все равно создает решение. Различие между этими вариантами заключается в том, что при выборе определенного типа проекта VS .NET IDE создает несколько служебных файлов и включает их в решение (тип файлов зависит от выбранного типа проекта).Как правило, работа над новой программой начинается с команды New Project. На экране появляется диалоговое окно (Рисунок 2.2; список проектов прокручен примерно до середины). В окне перечислены типы проектов, автоматически создаваемых в VB .NET, — на момент написания книги их было десять. Шаблоны проектов VB .NET работают практически .так же, как в VB6. В частности, они почти всегда содержат «скелет» программы, и в них обязательно присутствует служебная информация о файлах, входящих в решение.
Список задач и комментарии TODO, HACK и UNDONE
В Visual Studio теперь поддерживается список задач (task list), унаследованный из Visual InterDev и Visual J++. Идея состоит в том, что в программу включаются комментарии с описанием действий, которые предполагается выполнить в будущем; тип задачи определяется специальным ключевым словом, следующим после знака комментария. В настоящее время определены три встроенные категории задач — TODO, HACK и UNDONE. Комментарии с задачами выводятся в окне, вызываемом командой View > Other Windows > Task List (или комбинацией клавиш Ctrl+Alt+K). Пример показан на Рисунок 2.13.Для списка задач можно определять пользовательские ключевые слова. Предположим, вы хотите помечать ключевым словом FOR_KEN те фрагменты кода, которыми должен заняться ваш коллега Кен. Определение пользовательских ключевых слов для списка задач происходит следующим образом:
Свернутые области в редакторе

Редактор VS .NET обладает и другими интересными возможностями, незнакомыми даже опытным программистам VB. Мы познакомимся с ними в следующем разделе.
При изучении редактора IDE особенно полезная информация находится в разделе справки «Editing Code and Text». В частности, здесь описано несколько очень удобных средств перемещения по тексту.
Управление исключениями
На первый взгляд управление исключениями кажется экзотикой, не связанной с практической работой. Чтобы оценить эту возможность по достоинству, достаточно оказаться в ситуации, когда на стадии тестирования возникают многочисленные исключения (см. главу 7) и возникает необходимость в тонкой настройке действий, выполняемых при возникновении исключений. Это делается в диалоговом окне, вызываемом командой Debug > Windows > Exceptions. В этом окне вы указываете, как должен действовать отладчик при обнаружении исключений определенного типа. Допустим, вы хотите, чтобы при возникновении ошибок доступа управление передавалось отладчику.Visual Studio IDE в начале работы

При помощи команды View в главном меню всегда можн9 вызвать нужное окно на передний план (и передать ему фокус). Все окна IDE свободно перетаскиваются мышью. Кстати говоря, они не являются дочерними окнами многодокументного интерфейса MDI (Multiple Document Interface), которые должны находиться в границах родительского окна — любое окно можно переместить за пределы главного окна IDE.
Не забывайте о том, что в IDE существует режим контекстной справки. На Рисунок 2.4 показан примерный вид справки, вызванной клавишей F1 при нахождении фокуса в окне решения (Solution Explorer). Также поддерживается режим динамической справки (клавиши Ctrl+Fl), который автоматически отслеживает ваши действия и пытается вызвать соответствующий раздел справки. На Рисунок 2.5 показан список разделов динамической справки, полученный в начале работы над проектом. У динамической справки есть один серьезный недостаток — она интенсивно расходует ресурсы процессора. Когда вы освоитесь в IDE, отключите этот режим, чтобы повысить быстродействие.
Включение новых проектов
В существующее решение можно легко добавить новый проект — откройте решение и выполните команду File > New > Project. На экране появляется знакомое диалоговое окно New Project (Рисунок 2.17), но если внимательно присмотреться к нему, вы заметите два новых переключателя. При установке переключателя Close Solution, как и прежде, новый проект создается в новом решении. Но если установить переключатель Add To Solution, IDE включает новый проект в открытое решение.Включение проекта в существующее решение

Допустим, вы установили переключатель Add to Solution и выбрали, как и прежде, консольное приложение (Console Application). Как видно из Рисунок 2.18, в решение yb_ide_01 включается новый проект с именем vb_ide_02. Таким образом, полученное решение vb_ide_01 состоит из двух проектов: vb_ide_01 и vb_ide_02, и его можно рассматривать как аналог группы проектов VB6. Входящие в решение проекты могут взаимодействовать друг с другом и использоваться для тестирования компонентов (например, в IDE).
Временное хранение фрагментов
Любой фрагмент программного кода можно сохранить для последующего использования на панели элементов (многие программисты используют для этой цели вкладку General, но вы можете создать новую вкладку — щелкните на панели правой кнопкой мыши и выберите команду Add Tab из контекстного меню). Данная возможность очень удобна, поскольку в программах часто встречаются повторяющиеся фрагаенты, а вводить их каждый раз заново слишком долго. Чтобы сохранить фрагмент программы, выделитеего и перетащите мышью на панель элементов (Рисунок 2.12). Фрагменты остаются на панели до тех пор, пока не будут удалены при помощи контекстного меню. Чтобы воспользоваться сохраненным фрагментом, перетащите его мышью в нужную позицию окна программы. Существует и другой способ — выделите позицию вставки и дважды щелкните на сохраненном фрагменте.Выбор процесса для отладки

Чтобы начать отладку, выделите процесс в списке и щелкните на кнопке Attach. После подключения к процессу кнопка Break выводит информацию о текущем состоянии приложения. Если отладочная информация отсутствует, выводится листинг на языке ассемблера. После нажатия кнопки Attach на экране появляется диалоговое окно, в котором вам предлагается выбрать, что же вы собираетесь отлаживать — машинный код, код CLR, сценарий и т. д. В большинстве случаев отлаживается либо машинный код, либо код CLR. Длл примера мы запустили экземпляр приложения Notepad.exe и подключились к нему в отладчике VB .NET. Результат показан на Рисунок 2.28.
Выбор проектов для пакетной компиляции

Многие параметры проектов задаются в диалоговом окне — щелкните на имени проекта в окне решения правой кнопкой мыши и выберите команду Properties (также можно воспользоваться командой Project > Properties). На экране появляется диалоговое окно, изображенное на Рисунок 2.24. В этом разделе рассматриваются важнейшие параметры компиляции, но мы рекомендуем самостоятельно исследовать остальные параметры, находящиеся на вкладках Common Properties и Configuration Properties. В частности, они позволяют:
Выходные файлы
Что же получается в результате компиляции проекта? На Рисунок 2.25 показана структура каталогов, сгенерированных IDE для решения vb_ide_01.Cамоучитель по VB.NET
Цикл For-Each
Содержимое массива часто перебирается в цикле от 0 до UBound(массив), однако вы также можете воспользоваться конструкцией For-Each. Синтаксис For-Each выглядит следующим образом:For Each переменная In массив
[команды]
[Exit For при необходимости]
[команды] Next
Конструкция For-Each универсальна и может использоваться в тех случаях, когда структура данных поддерживает итеративный перебор элементов. За подробностями обращайтесь к главе 4.
Microsoft утверждает, что применение For-Each не будет приводить к существенному снижению быстродействия по сравнению с For-Next (как это было в VB6).
Цикл с определенным условием
Цикл, выполняемый заданное количество раз, определяется при помощи ключевых слов For и Next. Например, следующая программа выводит в консольном окне числа от 1 до 10:Sub Main()
Dim i As Integer
For i = 1 To 10
Console.WriteLine(i)
Next 1
Console.ReadLine()
End Sub
Обычно переменной-счетчику присваивается начальное значение, после чего проверяется, не превышает ли текущее значение счетчика конечное. Если счетчик превысил конечное значение, тело цикла не выполняется. Если текущее значение меньше конечного, VB .NET выполняет последующие команды до тех пор, пока не встретит ключевое слово Next (указывать имя переменной в команде Next необязательно). По умолчанию счетчик увеличивается на 1, и все начинается заново. Процесс продолжается до тех пор, пока при очередной проверке не окажется, что счетчик превысил конечное значение. В этот момент цикл завершается, и управление передается следующей за ним команде.
Хотя в качестве счетчика может использоваться числовая переменная любого типа, ре-комендуется использовать переменные типа Integer. В этом случае VB .NET тратит минимальное количество времени на изменение счетчика, что ускоряет выполнение цикла.
Единичное приращение счетчика, используемое по умолчанию, иногда неудобно—в некоторых ситуациях счетчик должен изменяться на 2, на дробную величину или в обратном направлении. Как и во всех прежних версиях VB, нестандартное приращение указывается в цикле For-Next с ключевым словом Step.
Следующая программа имитирует обратный отсчет перед запуском космического корабля:
Sub Main()
Dim i As Integer
For i = 10 To 1 Step =1
Console.WriteLine("It's t minus " & i & " and counting.")
Next i
Console.WriteLine("Blastoff!")
Console. ReadLine()
End Sub
При отрицательном приращении тело цикла For-Next игнорируется в том случае, если начальное значение счетчика меньше конечного. Это очень удобно при выполнении таких операций, как удаление элементов из списка. Если бы отсчет велся от 0 до ListCount, то на середине произошло бы обращение к удаленному элементу, тогда как при отсчете от ListCount до 0 с шагом -1 элементы нормально удаляются от последнего к первому.
Значение Step может относиться к любому числовому типу. Пример:
for yearlylnterest = .07 То .09 Step .00125D
Цикл перебирает значения от 7 до 9 процентов с приращением в 1/8 процента. Обратите внимание на использование типа Decimal для предотвращения ошибок округления.
VB .NET, как и прежние версии VB, позволяет создавать вложенные циклы практически неограниченной глубины. Следующий фрагмент выводит таблицу умножения с простейшим форматированием:
Sub Main()
Dim i, j As Integer
For j = 2 To 12
For i = 2 To 12
Console.Writed * j & " ")
Next i
Console. WriteLine()
Next j
Console ReadLine()
End Sub
Во внутреннем цикле вместо метода WriteLine используется метод Write, чтобы избежать перевода строки при выводе соседних элементов.
Циклы с неопределенным условием
Довольно часто условие продолжения цикла зависит от результатов, полученных в теле цикла. Следующая конструкция используется в VB .NET для построения цикла с неопределенным условием, тело которого выполняется минимум один раз (завершающая проверка):Do
' Команды VB .NET (0 и более)
Until условие_выполняется
Конечно, условие не ограничивается простой проверкой равенства. В вашем распоряжении операторы сравнения, перечисленные в табл.3.10.
Циклы
В VB .NET, как практически во всех языках программирования, существуют циклы — конструкции, позволяющие выполнять операции заданное количество раз или продолжать, пока выполняется (или наоборот, не выполняется) некоторое логическое условие. По сравнению с прежними версиями VB синтаксис циклов мало изменился. В частности, изменилась конструкция While/Wend, но это изменение к лучшему.Диалоговое окно свойств консольного приложения

Если включить в программу строку, выделенную ниже жирным шрифтом, консольное окно остается на экране до нажатия клавиши Enter (чрезвычайно полезный метод ReadLine() описан ниже).
Module Modulel
Sub Main()
Console.WriteLine("Hello world")
Console. ReadLine()
End Sub
End Module
Несмотря на простоту, эти две программы демонстрируют одну из ключевых особенностей программирования VB .NET (и вообще программирования на любом объектно-ориентированном языке): вы обращаетесь к объектам с запросом на выполнение операций. По аналогии с VB6 точка («.») используется для обращения к членам объектов и классов. Хотя обычно при вызове указывается объект (конкретный экземпляр, созданный на базе класса), в некоторых случаях вместо него указывается имя класса. В качестве примера возьмем следующую строку:
Console.WriteLine("Hellо world")
В ней вызывается метод Wri teLi ne класса Console, предназначенный для вывода текста с последующим переводом строки (в объектно-ориентированном программировании, как и в VB6, функции классов обычно называются методами). Метод WriteLine принадлежит к числу общих (shared) методов, также называемых методами класса. Общие методы подробно описаны в главе4. При вызове WriteLine выводимый текст заключаете в кавычки и помещается в круглые скобки. Во вторую версию программы «Hello world» добавлен вызов метода ReadLi ne, ожидающего нажатия клавиши Enter (метод ReadLi ne обычно используется в правой части команды присваивания, чтобы введенный с консоли текст был сохранен в заданной переменной — см. следующее примечание).
В приведенных программах следует обратить внимание на пару неочевидных обстоятельств. Как было сказано выше, при вызове метода обычно указывается конкретный экземпляр класса. Исключение из этого правила составляют особые методы класса, называемые общими методами. Общие методы существуют на уровне класса, а не его отдельных экземпляров. Другая тонкость заключается в том, что Console входит в пространство имен System, поэтому полный вызов метода выглядит так: System.Console.Writeline("Hello world"). В данном примере это не нужно; причины изложены в главе 4 при более подробном описании пространств имен.
Пользователям предыдущих версий VB следует учесть, что круглые скобки при вызове методов обязательны — обычно IDE добавляет их автоматически, но лучше не забывать об этом. Ключевое слово Call разрешено, но теперь в нем нет необходимости.
Форматирование данных
Все функции форматирования возвращают новую строку в заданном формате. В VB .NET сохранены аналоги старых функций форматирования из VB6 и VBScript, поэтому вы можете продолжать использовать функции Format, Format Number, For-matCurrency, FormatPercent и FormatDateTime. Последние четыре функции неплохо справляются с простым форматированием, но мы все равно предпочитаем использовать более мощные средства форматирования, реализованные в .NET Framework.Синтаксис форматирования в .NET Framework на первый взгляд выглядит несколько странно. Рассмотрим несложный пример:
Dim balance As Decimal = 123456
Dim creditLimit As Decimal = 999999
Console.WriteLine("Customer balance is {0:C}, credit limit is {1:C} ",_
balance. creditLimit = balance)
Результат:
Customer balance is $123,456.00. credit limit is $876.543.00
Попробуйте привести фрагмент, выделенный жирным шрифтом, к следующему виду:
Console.WriteLine("Customer credit is {1:C}, balance is {0:C} ".
balance. creditLimit = balance)
Вы получите следующий результат:
Customer credit is $876.543.00. balance is $123.456.00
Форматируемые переменные перечисляются в порядке их следования в списке. Так, во втором примере {1:С} означает вторую переменную в списке, а {0:С} соответствует первой переменной (напомним, что индексация в .NET Framework начинается с 0). «С» означает форматирование в денежном формате, определенном в параметрах локального контекста Windows.
Функции
Чтобы создать новую функцию или процедуру в окне программы, установите курсор за пределами других процедур и функций и начинайте вводить заголовок процедуры или функции. Как только вы нажмете клавишу Enter, редактор IDE автоматически создаст команду End правильного типа (End Functi on или End Sub). Ниже приведен заголовок функции, которая получает целый параметр по значению и возвращает логическую величину (True или False) в зависимости от того, принадлежит ли переданный параметр интервалу от 1 до 10: Function IsBetweenlAnd10(ByVal num As Integer) As BooleanВ режиме жесткой проверки типов (Option Strict) при объявлении функции необходимо указывать тип возвращаемого значения (в нашем примере — Boolean).
Полный текст модуля с функций Is Between lAnd 10 приведен ниже. Порядок следования функций не важен — функция Sub Mai n может находиться и после определения функции, которая в ней используется.
Module Modulel
Function IsBetweenlAnd10 (ByVal num As Integer) As Boolean
If num >= 1 And num <=10 Then
Return True
Else
Return False
End If
End Function
Sub Main()
Console. WriteLinedsBetweenlAnd100))
Console. ReadLine()
End Sub
End Module
В VB .NET при вызове функции или процедуры непустой список параметров всегда заключается в круглые скобки, как в строке с вызовом Console.WriteLine: IsBetweenlAnd100)
Обратите внимание на ключевое слово Return. При выполнении команды Return функция завершается и возвращает значение, указанное после Return (значение должно быть определенным — возвращение аналога voi d не допускается). Также поддерживается синтаксис с присваиванием имени функции, использовавшийся в прежних версиях VB:
Function IsBetweenlAnd10(ByVal num As Integer) As Boolean
If num >= 1 And num <= 10 Then
IsBetweenlAnd10 = True Else
IsBetweenlAnd10= False
End If
End Function
Использование Return — дело вкуса. Команда Return нагляднее и проще, но старый синтаксис оставляет управление внутри функции, а это иногда бывает удобно.
Обобщенная форма определения функции выглядит следующим образом:
Function имя_функции (аргумент1, аргумент2, ...) As тип
команды
Return выражение ' или имя_функции = выражение
End Function
где аргумент1 и аргумент2 — переменные. Имена функций подчиняются тем же правилам, что и имена переменных. При вызове функции VB .NET выполняет команды, содержащиеся в определении функции. Значение, указанное после Return (или последнее значение, присвоенное имени функции), определяет результат вызова.
Хотя возвращаемое значение обычно используется в программе, VB также позволяет вызвать функцию простой командой вида foo(3) без присваивания.
Обычно количество аргументов, передаваемых при вызове функции, должно совпадать с количеством параметров в ее определении. Типы аргументов должны быть совместимы с типами соответствующих параметров, при этом автоматически выполняются только расширяющие преобразования. Например, следующий фрагмент допустим, поскольку преобразование Short в Integer не приводит к потере данных:
Dim bar As Short = 3
Console.WriteLinedsBetweenlAnd10(bar))
VB .NET позволяет создавать функции с переменным числом аргументов. Дополнительная информация приведена далее в этой главе.
Cамоучитель по VB.NET
Говоря об управляющих конструкциях, нельзя обойти вниманием команду GoTo. Если перефразировать старую шутку, современные программисты делятся на три группы: те, кто не знает, как пользоваться GoTo, и знать не хочет; те, кто не знает, но переживает по этому поводу; и те, кто умеет ею пользоваться.Частое использование GoTo приводит к многократным передачам управления и порождает «спагетти-код», который трудно читать и отлаживать. С другой стороны, в некоторые ситуациях применение GoTo делает программу более понятной и логичной — например, если в какой-то ситуации потребовалось выйти сразу из нескольких вложенных циклов. Команда Exit для этого не подходит, поскольку она завершает только текущий цикл.
В данной ситуации вместо Goto можно воспользоваться перехватом исключений (см. главу 7), но некоторые программисты предпочитают классический подход.
Чтобы воспользоваться командой GoTo в VB .NET, необходимо присвоить метку соответствующей строке. Метка начинается в первой позиции строки, ее первым символом является буква, а последним — двоеточие. Старайтесь присваивать меткам содержательные имена. Пример:
Bad-Input:
' Фрагмент, выполняемый при переходе
Предположим, в нашей программе данные вводятся во вложенном цикле For. Чтобы завершить ввод, пользователь вводит ZZZ :
SubMain()
Dim getData As String
Dim i, j As Integer
For i = 1 To 10
For j = 1 To 100
Console.Write("Type the data, hit the Enter key between " & _
"ZZZ to end: ") getData = Console. ReadLine()
If getData = "ZZZ" Then
Goto Bad Input Else
' Обработка данных
End If
Next j
Next i
Exit Sub
BadInput:
Console.WriteLine("Data entry ended at user request")
Console. ReadLine()
End Sub
Выходить из вложенного цикла командой Exit For неудобно — нам пришлось бы писать дополнительный код для выхода из внешнего цикла. Обратите внимание: команда Exi t Sub предотвращает передачу управления помеченному коду после завершения обоих циклов.
Именованные аргументы
При вызове функций и процедур с большим количеством параметров (особенно необязательных) существует такая элегантная возможность, как передача именованных аргументов. Если значения параметров при вызове передаются в виде «имя -:=значение», вам не придется беспокоиться о соблюдении порядка аргументов (регистр символов в именах игнорируется). В отличие от прежних версий VB, где именованные аргументы то работали, то нет, в VB .NET они работают всегда.Именованные аргументы разделяются запятыми. При разумном выборе имен параметров именованные аргументы заметно упрощают чтение программы, особенно при большом количестве необязательных аргументов. Для примера возьмем приведенный выше заголовок функции ProcessAddress:
Sub ProcessAddress(TheName As String.
Address As String. City As String. State As String,
ZipCode As String, Optional ZipPlus4 As String = "0000")
Вызов этой процедуры может выглядеть так:
ProcessAddress(Address := "The Whitehouse"
Name := "GeorgeW",
City := "DC". _
State:= String.Empty. _
ZipCode:= "12345")
Обратите внимание: порядок перечисления аргументов отличается от заданного в заголовке процедуры.
Изменения в видимости переменных
Область видимости переменных и методов в VB .NET определяется по более сложным правилам, чем в прежних версиях VB. Эта тема подробно описана в главах 4 и 5. В частности, изменения проявляются при объявлении переменных в теле цикла или блока If-Then. Такие переменные невидимы за пределами блока, в котором они были объявлены. Например, в следующем фрагменте мы выбираем одну из двух версий строковой переменной Ri sk и затем пытаемся использовать ее:If income < 100000 Then
Dim risk As String = "too much risk" Else
Dim risk As String = "love to make a deal"
End If
Console.WriteLine("Your risk level is " & Risk)
На экране появляется сообщение об ошибке:
The name 'risk' is not declared.
Видимость обеих версий переменной risk ограничивается блоком, в котором они были объявлены! Мораль: не объявляйте переменные внутри блоков, если для этого нет веских причин.
Команды VB .NET
При вводе программ VB .NET во внешнем редакторе вы не сможете воспользоваться средствами IntelliSense. Мы рекомендуем использовать IDE, поскольку технология IntelliSense значительно упрощает программирование в такой сложной среде, как .NET (конечно, для этого вам придется перейти от бесплатно распространяемого .NET SDK к Visual Studio). Редактор IDE даже исправляет некоторые распространенные ошибки — например, пропуск круглых скобок при вызове некоторых методов.В VB .NET, как и во всех предыдущих версиях BASIC, не учитывается регистр символов (кроме текста, заключенного в кавычки). Пробелы в строках, не заключенных в кавычки, также игнорируются.
Тем не менее VS .NET IDE пытается оформлять программы VB .NET по своим правилам. Первые символы ключевых слов преобразуются к верхнему регистру, а строки дополняются пробелами для удобства чтения (End SUB преобразуется в End Sub и т. д.). Регистр символов в именах методов VB .NET определяется по схеме Pascal (слова начинаются с прописных букв, остальные буквы строчные). Альтернативная схема выбора регистра (writeLine) для методов VB .NET обычно не используется.
Номера строк в командах VB .NET практически не используются, хотя строки программы могут нумероваться, причем каждая команда обычно расположена в отдельной строке. Чтобы продолжить команду в следующей строке, завершите ее символом подчеркивания (_), отделенным одним или несколькими пробелами. Таким образом, если строка не завершается символом подчеркивания, нажатие клавиши Enter является признаком конца команды (в Visual Basic команды не разманд можно разместить в одной строке, разделив их символом «:», но обычно так не поступают. Если введенная строка не помещается в окне, IDE прокручивает строку вправо по мере необходимости.
в любом языке программирования, необязательны.
Комментарии в VB .NET, как и в любом языке программирования, необязательны. Они не обрабатываются компилятором и соответственно не занимают места в откомпилированном коде. В VB .NET существует два варианта оформления комментариев. В первом, более распространенном варианте комментарий начинается с апострофа:Sub Main()
Console.WriteLine("Hello world")
' Игнорировать значение, возвращаемое методом ReadLine
Console. ReadLine()
End Sub
Во втором варианте используется старое ключевое слово Rem, которое появилось в BASIC в начале 1960-х годов!
При включении комментариев в конец строки проще воспользоваться апострофом, поскольку ключевое слово Rem придется отделять двоеточием. В VB .NET не предусмотрено языковых средств для комментирования нескольких строк, хотя на панели инструментов присутствует кнопка, упрощающая создание таких комментариев.
В отличие от языка С#, обладающего встроенными средствами построения комментариев XML, в VB .NET документация XML будет создаваться отдельной надстройкой (add-in).
| Содержание | Вперед |
Консольные приложения
Каждое приложение VB .NET должно иметь точку вто§а. В точке входа содержится код, автоматически выполняемый при запуске, после чего управление передается остальному коду программы. В относительно простых графических приложениях точка входа может ассоциироваться с начальной формой, как в VB6. Но как было показано в главе 1, код форм Windows достаточно сложен и поиск точки входа может вызвать определенные затруднения. В этой главе рассматриваются только консольные приложения, работающие в консольном окне (наподобие окна сеанса DOS). Да, VB .NET позволяет легко создавать традиционные консольные приложения, часто применяемые при программировании серверных сценариев:Точкой входа консольного приложения является процедура Sub Mai n модуля (аналог процедуры Sub Mai n в VB6). Если выбрать в диалоговом окне New Project значок консольного приложения (Console Application), VB .NET автоматически генерирует «скелет» приложения с точкой входа — процедурой Sub Main:
Module Module1
Sub Main()
End Sub
End Module
В отличие от VB6, в первой строке задается имя модуля (команда выделена жирным шрифтом). В данном примере используется имя Modul el, принятое по умолчанию. По правилам имя модуля должно совпадать с именем файла. Допустим, вы изменили имя модуля в первой строке: Module Testl При попытке запустить консольное приложения выводится сообщение об ошибке:
Startup code 'Sub Main' was specified in 'Test.Modulel'.
but 'Test.Modulel' was not found
Переименование модуля после его создания выполняется следующим образом:
Module Modul el
Sub Main()
Console.WriteLine("Hello world")
End Sub End Module
Если запустить эту программу в IDE, на экране очень быстро мелькнет (и тут же исчезнет) окно DOS со словами «Hello world». Окно закрывается по завершении обработки команды End Sub.
Константы
VB .NET позволяет создавать именованные константы для значений, остающихся постоянными на протяжении всей работы программы. Константы объявляются по аналогии с переменными, а их имена подчиняются тем же правилам: до 255 символов, начинаются с буквы, после которой следует произвольная комбинация букв, цифр и символов подчеркивания. В книге имена констант записываются прописными буквами.В VB .NET при активизации жесткой проверки типов необходимо явно указывать тип констант:
Const PIE = 3.14159 ' Не будет компилироваться с Option Strict
Const PIE As Double = 3.14159 ' Правильно, но Math.PI лучше :-)
Значение констант может определяться числовыми выражениями, в том числе и содержащими ранее определенные константы:
Const PIE_OVER_2 As Double = PIE / 2
Аналогичным образом определяются строковые константы:
Const USER_NAME As String = "Bill Gates"
.NET Framework содержит немило встроенных, заранее определенных глобальных констант, которые вы можете использовать в своих программах. Многие из них аналогичны константам VB6 с префиксом vb, но они являются членами различных классов, поэтому обращения к ним выглядят несколько иначе. Например, константа vbCrLf в VB .NET принадлежит классу ControlChars, поэтому при обращении к ней используется запись Control Chars. CrLf.
Круглые скобки и приоритет операций
При обработке сложных выражений последовательность выполнения операций задается двумя способами. При использовании круглых Скобок вам не придется запоминать приоритеты различных операций. В VB .NET, как и во многих языках программирования, операции обладают приоритетом, определяющим последовательность их выполнения. Умножение обладает более высоким приоритетом, чем сложение; следовательно, выражение 3+4*5 равно 23, поскольку умножение (4*5) выполняется раньше, чем сложение.Ниже перечислены математические операции в порядке убывания приоритета.
Сокращенная запись операций с присваиванием
Для некоторых операций, объединенных с присваиванием, в VB. NET предусмотрены сокращенные обозначения, перечисленные в следующей таблице.
| Сокращенная запись |
Эквивалент |
| А*=В |
А = А*В |
| А+=В |
А = А + В |
| А/=В |
А = А/В |
| А-=В |
А = А-В |
| А\=В |
А = А\В |
| А^=В |
А = А^В |
| А&=В |
А = А & В (конкатенация строк) |
Литералы и их соответствие типам данных
Литералом называется последовательность символов, которая может интерпретироваться как значение одного из примитивных типов. Но с типами (даже примитивными) в VB .NET дело обстоит несколько сложнее, чем в более ранних версиях VB.Хотя возможность непосредственной интерпретации данных предусмотрена в любом языке программирования, решить, как именно следует интерпретировать те или иные данные, иногда бывает непросто. Наверное, все согласятся с тем, что 3 — это число 3 и его следует интерпретировать именно так. Но что такое число 3 с точки зрения компилятора? Сколько байт памяти следует под него выделить? Теоретически для хранения числа 3 хватит 2 бит, но в современных языках программирования обычно происходит не так.
Итак, компилятор должен проанализировать литерал и принять необходимые решения, поэтому вы должны по возможности точнее описать, что вы имеете в виду, не полагаясь на разумность компилятора. Вернемся к примеру с простым числом 3. В VB .NET оно может представлять собой (среди прочего):
С точки зрения компилятора простой констатации «это число 3» недостаточно. Разумеется, VB .NET, как и любой язык программирования, позволяет уточнить смысл литерала. Например, 31 — литерал типа Integer со значением 3, а литерал "3" относится к строковому типу String (тип String рассматривается ниже в этой главе; он несколько отличается от строкового типа в прежних версиях VB).
Примитивные типы можно рассматривать как атомарные элементы языка, хотя в VB .NET они представляют собой псевдонимы для классов из библиотеки System.
В переменной, объявленной с примитивным типом, хранятся значения указанного типа. Ниже перечислены примитивные числовые типы VB .NET.
Любой целочисленный литерал можно записать в шестнадцатеричной системе счисления (по основанию 16), для чего он снабжается префиксом &Н. Например, литерал &HF соответствует десятичному числу 15, хранящемуся в формате Integer, поскольку суффикс типа не указан, а число входит в интервал допустимых значений типа Integer. Числа, записанные в восьмеричной системе счисления (по основанию 8), снабжаются префиксом &0.
При выполнении операций с вещественными числами используются следующие типы:
Console.WriteLine(12345678 * 4567)
компилятор выдает ошибку:
This constant expression produces a value that is not representable in type System.Integer.
Проблема решается при помощи суффикса типа Long:
Console.WriteLine(123456781 * 4567)
Общие методы MaxValue и MinValue, ассоциированные с типом, возвращают соответственно верхнюю и нижнюю границы интервала допустимых значений. Пример:
Console.WriteLine(Integer.MaxValue)
В табл. 3.2 собраны данные о соответствии числовых типов VB .NET, типов .NET Framework и их аналогов из VB6 (если они есть).
Логические операторы
Начиная с бета-версии 2 логические операторы (Not, And, Or и т. д.) работают на уровне двоичных разрядов, как и в прежних версиях VB. Допустим, у вас имеются два целых числа X и Y. Каждый бит результата X And Y равен 1 лишь в том случае, если равны 1 соответствующие биты обоих операндов; в противном случае бит результата равен нулю. Таким образом, при вычислении результата X And Y вычисляется каждый бит 32-разрядного целого числа. Пример:X = 7 'В двоичном представлении = 0111
Y = 12 'В двоичном представлении = 1100
Выражение X And Y в двоичной системе равно 0100 (4 в десятичной системе), поскольку лишь во второй позиции оба бита равны 1. Остальные биты результата равны 0, поскольку в этих позициях хотя бы один из битов операндов равен 0. Этот способ позволяет проверить значения отдельных битов целого числа. Примеры:
(X And 1) = 1: проверить, установлен ли младший бит числа.
(X And 2) о 2: проверить, установлен ли предпоследний бит числа (поскольку в
двоичной системе число 2 представляется записью 10).
X And 255: младший байт числа (255 дес. = 11111111 дв.).
X And 65280: старший байт числа (65280 дес. = 1111111100000000 дв.).
Значение, предназначенное для проверки отдельных битов числа, называется маской (mask).
Массивы с индексацией элементов в заданном интервале
Утверждение о том, что индексация массивов всегда начинается с 0, не совсем точно. Теоретически можно определять массивы с заданной верхней и нижней границей индекса, но из-за неудобного синтаксиса и снижения быстродействия вряд ли вам захочется это делать. В следующем фрагменте создается массив с индексацией элементов от 1995 до 2002:Sub Main()
Dim anArray As Array
Dim i As Integer
Dim i(0) As Integer
Dim lowerBounds(0) As Integer
i(O) = 7
lowerBounds(0) = 1995 ' Создать массив с индексами 1995 - 2002
аnАrrау = Array.CreateInstance(GetType(System.Int32). 1. lowerBounds) anArray.SetValue(200000, 1995) anArray.SetValue(1000000. 2001)
Console.WriteLine("The entry in position 1995 is " & _ (anArray.GetValue(1995).ToString))
Console.WriteLine("The entry in position 2002 is " & _ (anArray.GetValue(2001).ToString))
Console. ReadLine()
End Sub
Присваивание выполняется методом SetValue (значение,индекс), а чтение — методом GetValue(индекс). Но если массив создается подобным образом в режиме жесткой проверки типов, вам придется позаботиться о том, чтобы присваиваемое значение было преобразовано к правильному типу!
Массивы
В VB .NET имена массивов должны подчиняться тем же правилам, что и имена переменных. Ссылка на элемент массива выглядит как имя массива, за которым в круглых скобках указывается индекс.Массивы VB .NET во многом отличаются от массивов VB6. Одни изменения видны сразу, другие не столь очевидны. Наиболее заметные изменения перечислены ниже.
Начиная с бета-версии 2 объявление 01m stri ngLi st(7) создает массив из восьми элементов с индексами от 0 до 7. Поскольку в VB .NET индексация всегда начинается с нуля, третий элемент массива обозначается stri ngList(2), а предшествующие элементы обозначаются stringList(0) и stringList(l).
Dim x() As Single
ReDim x(20) ' Начиная с бета-версии 2. создает массив из 21 элемента
ReDim Preserve x(50) ' 21 элемент сохраняется в массиве.
Менее очевидные изменения обусловлены тем, что массивы VB .NET являются экземплярами класса Array. Подробности будут рассмотрены в главе 4, а пока достаточно указать, что это позволяет выполнять операции с массивами вызовом методов класса Array. Ниже продемонстрирован пример сортировки массива методом Sort:
Sub Main()
Dim stuff() As Integer = (9. 7, 5, 4, 2. 1, -37, 6}
Array.Sort(stuff)
Dim i As Integer
For i = 0 To UBound(stuff)
Console.WriteLine(stuff(i))
Next
Console. ReadLine()
End Sub
Программа выводит массив, отсортированный с применением чрезвычайно эффективного алгоритма «быстрой сортировки».
VB.NET наследует от .NET Framework некоторые очень полезные структуры данных, возможности которых выходят далеко за рамки обычных массивов. На фоне этих структур коллекции VB5 и последующих версий выглядят примитивно. В частности, списковые массивы (с динамически изменяемыми размерами) и ассоциативные массивы (с доступом к данным по ключу) часто оказываются удобнее обычных массивов. Многие из новых структур данных рассматриваются в главах 5 и 6.
Математические функции и математические константы
Встроенные математические функции VB6 работают и в VB .NET, но мы предпочитаем использовать методы класса Math, входящего в .NET Framework. В этот класс также входят некоторые полезные константы (например, Math. PI и Math. Е). Основные математические функции класса Math перечислены в табл. 3.9. Все эти функции объявлены общими (shared), поэтому они принадлежат классу Math в целом, а не его отдельным экземплярам (которые, впрочем, все равно невозможно создать — см. главу 4).Все перечисленные методы являются общими методами класса Math, поэтому они должны вызываться с префиксом Math — например, Math.Log10(l0).
В VB .NET предусмотрена целая группа методов для получения разнообразных случай-ных чисел. Мы рассмотрим эти методы в главе 4, когда речь пойдет о создании объектов.
Математические операторы
В табл. 3.7 приведены условные обозначения шести базовых математических операций.Результат стандартного деления (/) всегда относится к типу Double, даже в случае де-ления без остатка. Результат целочисленного деления (\) всегда относится к типу Integer. Это означает, что при работе с типами Decimal и Integer вам придется часто использовать функции преобразования.
Многомерные массивы
Массивы не ограничиваются одним измерением. Допустим, вы хотите сохранить таблицу умножения в матричном виде. Примерное решение может выглядеть так:Dim mulTable(11.11) As Integer
' Создает массив 12x12
Dim i As Integer, j As Integer
For i = 0 To 11
For j = 0 To 11
mulTable(i.j) = (i+l)*(j+l)
Next j
Next i
Размеры массивов в VB .NET могут изменяться, но количество измерений должно оставаться постоянным.
Многомерный массив с неопределенным количеством элементов объявляется при помощи запятых. Следующий пример показывает, как объявить трехмерный массив:
Dim salesByDivision( , , ) As Decimal
Команда ReDim задает или изменяет количество элементов в каждом измерении, но размерность массива не изменяется.
При сохранении содержимого массива командой ReDim Preserve допускается изменение количества элементов только в последнем измерении массива.
Объявление переменных
В VB .NET, как и в VB6, переменные объявляются в процедурах и функциях при помощи ключевых слов Dim и As, а присваивание выполняется знаком =:Dim foo As String
foo = "bar"
Если вы не изменяли стандартную настройку VB .NET, переменные должны объявляться перед использованием (режим Option Explicit, впервые представленный в VB4, теперь используется по умолчанию). В VB .NET поддерживается инициализация переменных при объявлении. Пример:
Dim salesTax As Decimal = 0.0825D
Команда объявляет переменную с именем salesTax и присваивает ей начальное значение 0.0825 типа Decimal. При инициализации могут использоваться любые синтаксически правильные выражения VB .NET. Следующая команда присваивает переменной startAngle встроенное значение математической константы п, используя для этого константу класса System. Math: Dim startAngle As Decimal - Math.PI
Если переменная не была инициализирована при объявлении, ей присваивается стандартное значение, соответствующее ее типу, — например, числовым переменным присваивается 0. При таком удобном синтаксисе, как в VB .NET, всегда лучше инициализировать переменную при объявлении, чем полагаться на значение по умолчанию. В следующем фрагменте используется оператор &, применявшийся в VB6 для конкатенации строк:
Sub Main()
Dim salesTax As Decimal = 0.0825D
Dim state As String = "California"
Console.WriteLine("The sales tax in " & state & " is " & salesTax)
Console. ReadLine()
End Sub
Программа выводит следующий результат:
The sales tax in California is 0.0825
В отличие от предыдущих версий VB, объявление нескольких переменных в одной строке программы работает именно так, как подсказывает здравый смысл. Иначе говоря, следующая команда объявляет три переменные: i, j и k, относящиеся к типу Integer:
Dim i, j, k As Integer
При одновременном объявлении нескольких переменных инициализация выполняться не может, поэтому следующая строка недопустима:
Dim i, j, k As Integer = 1
Как и в прежних версиях VB, вместо указания типа с ключевым словом As может использоваться суффикс типа. Например:
Dim i%, myname$
Приведенная команда объявляет переменную i типа Integer (аналог Long в старом VB) и переменную myName типа String. Программистам VB .NET поступать подобным образом не рекомендуется.
Все программисты хорошо помнят, какие жаркие дискуссии проходили по поводу соглашений об именах переменных. Существует сложная система префиксов (так называемая венгерская запись), позволяющая с первого взгляда определить тип переменных. Согласно общим рекомендациям программирования для .NET Framework применение венгерской записи нежелательно. Мы будем следовать этим рекомендациям, и в книге префиксы встречаются очень редко.
Команды семейства DefType (например, Deflnt) в VB .NET не поддерживаются.
Передача массивов функциям и процедурам
В VB .NET, как и в прежних версиях VB, существуют удобные средства для работы с одномерными и многомерными массивами в процедурах и функциях. Впрочем, существуют некоторые нюансы, обусловленные передачей по ссылке и по значению; мы рассмотрим их в главе 4. Перебор содержимого массива осуществляется конструкцией For Each или (более распространенный вариант) стандартным циклом For с вычислением верхней границы при помощи функции UBound (). Ниже приведен пример функции поиска максимального элемента в массиве:Function FindMax(ByVa1 a() As Integer
Dim finish As Integer = UBound(a)
Dim max As Integer = a(0)
Dim i As Integer
For i = 0 To finish
If a(i) > max Then max = a(i)
Next i
Return max End Function
Обобщенная форма вызова UBound(имя_массива, I) возвращает верхнюю границу по 1-му измерению массива. Для одномерных массивов (списков) параметр 1 является необязательным.
Для проверки также можно воспользоваться методом Length, реализованным в классе массива, но этот метод возвращает количество элементов в массиве вместо верхней границы (в многомерных массивах эти величины не совпадают).
Переменные и присваивание
Имена переменных в VB .NET имеют длину до 255 символов и обычно начинаются с буквы в кодировке Unicode (за дополнительной информацией о Unicode обращайтесь на сайт www.unicode.org), хотя также допускается символ подчеркивания. Далее следует произвольная комбинация букв, цифр и символов подчеркивания. Все символы имени являются значимыми, но регистр символов игнорируется (как вообще в VB .NET); считается, что firstBase и firstbase — одна и та же переменная. Присваивание выполняется при помощи знака =, как и в более ранних версиях VB:theYear = 2001
В .NET Framework используются новые правила присваивания имен переменным, заметно изменившиеся по сравнению с VB6. В соответствии рекомендациями, приведенными в MSDN, применять венгерскую запись нежелательно, а значимые имена переменных (то есть не состоящие из одной буквы, как i или t) должны оформляться в альтернативной схеме выбора регистра. В прежних версиях VB обычно использовалась схема Pascal.
Имена переменных не могут совпадать с последовательностями, зарезервированными VB .NET (список для текущей версии приведен в табл. 3.1), однако это ограничение можно обойти, заключив имя переменной в квадратные скобки. Например, переменная не может называться Loop, а имя [Loop] подойдет — хотя делать это не рекомендуется. Зарезервированные слова внутри имен переменных допустимы (скажем, loopit — вполне нормальное имя). При попытке использования ключевого слова в качестве имени переменной VB .NET подчеркивает его и информирует об ошибке (при помощи экранной подсказки).
Преобразования разнотипных значений
По мнению многих программистов, прежние версии VB6 слишком либерально относились к преобразованию типов. В результате возникало явление «злостного искажения типов» — скажем, VB6 позволял умножить строковое представление числа на Integer.В VB .NET предусмотрен режим жесткой проверки типов Option Strict. Чтобы активизировать его, включите следующую команду в начало программы (также можно воспользоваться флажком Strict Type Checking на вкладке Build диалогового окна Project Properties):
Option Strict On
При активизации этого режима (а это следует делать всегда!) VB .NET требует, чтобы любые преобразования типов, которые могут привести к потере данных, выполнялись явно. Например, при преобразовании Single к типу Integer может произойти потеря точности, поскольку тип Integer не позволяет представить весь интервал допустимых значений типа Single. С другой стороны, если потеря данных исключена (скажем, при преобразовании Integer в тип Long или Decimal), VB .NET выполняет преобразование автоматически. В документации VB .NET преобразования без потери данных называются расширяющими преобразованиями (widening conversions). В табл. 3.3 переделены допустимые расширяющие преобразования для базовых типов данных.
Преждевременный выход из функций или процедур
В некоторых ситуациях требуется покинуть функцию до того, как будет достигнута стандартная точка выхода (например, если проверка покажет, что исходные данные неверны и дальнейшие вычисления бессмысленны). Команда Return немедленно передает управление вызывающей стороне (если не определена секция Finally — см. главу 7):Function BallOut (X As Double) As Double If X < 0 Then
Return 0' Вернуть фиктивное значение Else
' Основные действия
End If
End Function
'Выход из процедур осуществляется командой
Exit Sub.
Процедуры и функции с необязательными аргументами
В VB. NET сохранена возможность определения процедур и функций с необязательными аргументами, но в отличие от VB6 для каждого необязательного параметра должно быть указано значение по умолчанию. Следующий пример демонстрирует синтаксис объявления необязательных параметров:Sub ProcessAddress(TheName As String,
Address As String. City As String. State As String.
ZipCode As String. Optional ZipPlus4 As String = "0000")
В данном примере последний параметр является необязательным (Optional) и по умолчанию равен "0000".
В главе 4 описана перегрузка (overloading) —другой способ определения функций с необязательными параметрами.
VB .NET также позволяет определять процедуры и функции с произвольным количеством аргументов. Для этого в качестве параметра передается массив с ключевым словом РаramАrrау, как в следующем примере:
Function AddThemUp(ByVal ParamArray stuff() As Double) As Double
Dim total As Double = 0
Dim Number As Double = 0
Dim I As Integer
For I = 0 To UBound(stuff)
total = total + stuff(I)
Next
Return total End Function
Пример использования функции:
x = AddThemUp(3, 4. 5. 6)
В результате переменной х присваивается значение 18.
Процедуры и функции
Раньше выбор между процедурой (Sub) и функцией (Function) определялся простым критерием: если вы собирались использовать возвращаемое значение, следовало выбирать функцию, а если нет — процедуру. Мы рекомендуем придерживаться этой модели, хотя ничто не мешает проигнорировать возвращаемое значение функции. В объектно-ориентированных программах функции и процедуры обычно включаются в классы и называются методами.В VB .NET, как и во многих языках программирования, существуют два способа передачи параметров функциям и процедурам: передача по ссылке и передача по значению. Когда параметр передается по ссылке, его изменения внутри функции приведут к изменению исходного аргумента после выхода из функции. По умолчанию в VB .NET параметры передаются по значению (а в VB6 — по ссылке).
Было бы наивно полагать, что при передаче по значению исходный аргумент после выхода из функции всегда сохраняет прежнее значение. В VB .NET состояние объекта может измениться даже в том случае, если он передавался по значению. В главе 4 эта ситуация, часто приводящая к возникновению хитроумных ошибок, рассматривается более подробно.
Процедуры
В отличие от функций, процедуры не возвращают конкретных значений. Вызов процедур осуществляется по имени. Непустые списки аргументов всегда заключаются в круглые скобки. В приведенном ниже примере строка с вызовом процедуры выделена жирным шрифтом:Option Strict On
Module Modulel
Sub ShowBottlesOfBeer(ByVal nbot As Integer)
Console.WriteLine(nbot & " bottles of beer on the wall")
Console.Writeline(nbot & " bottles of beer.")
Console.WriteLine("if one of those bottles hsould happen to fall")
Console.WriteLine(nbot -1&" bottles of beer on the wall")
End Sub
Sub Main()
Dim I As Integer
For I = 10 To 1 Step -1
ShowBottlesOfBeer(I)
Next
Console.WriteLine("All beer gone...")
Console. ReadLine()
End Sub
End Module
При вызове процедур указывать ключевое слово Sub не обязательно. Строку с вызовом процедуры из приведенного выше примера можно было записать и в таком виде:
Call ShowBottlesOfBeer(I)
Заголовок процедуры должен содержать объявления всех параметров с ключевыми словами ByVal или ByRef (по умолчанию используется ByVal, то есть передача по значению):
Sub имя_процедуры(В(ByVа1 аргумент1 As тип. ByVal аргумент2 As тип, ....)
команды
End Sub
При вызове процедуры в форме имя_процедуры(аргумент1, аргумент2, ...) или Call имя_процедуры(аргумент1. аргумент2, ...) VB .NET создает копии данных-аргументов и выполняет код, содержащийся в теле процедуры (поскольку в отличие от предыдущих версий по умолчанию параметры передаются по значению).
Прочие литералы
Кроме числовых литералов также существуют литералы типов Boolean, Date и Char. Тип данных Bool ean принимает значения True и Fal se. В VB .NET он представляется 4 байтами (в отличие от 2 байт в VB6).В VB .NET бета-версии 1 значение True было равно +1 (как и в других языках .NET). Начиная с бета-версии 2 оно снова стало равно -1. Говоря точнее, в поразрядных операциях и при преобразовании к числовым типам значение True равно -1, а не 1. Но если логическая величина VB .NET передается за пределы VB, при приведении к числовому типу нового языка она считается равной 1. Пожалуй, это решение было ошибочным, поскольку одной из целей, поставленных при разработке .NET, было обеспечение максимальной межъязыковой совместимости. Пока вы ограничиваетесь встроенными константами True и False, все будет нормально, но стоит перейти к конкретным числовым значениям — и у вас могут возникнуть проблемы.
Тип данных Date представляет дату и/или время. Как ИГ в VB5, такие литералы заключаются между символами #...# — например, #Jan 1. 2001#. Если время не указано), предполагается, что литерал соответствует полуночи указанной даты.
Тип Date в VB .NET не преобразуется к типу Double. В частности, из этого следует, что с датами нельзя производить математические вычисления — например, вычислить завтрашнюю дату командой Today+1.
Тип данных Char представляет один символ Unicode. Объем кодировки Unicode (65 536 символов) вполне достаточен для представления всех существующих алфавитов. Обычно символ заключается в кавычки, за которыми следует префикс С (например, "Н"С), но вы также можете воспользоваться встроенной функцией Chr и указать числовой код символа Unicode. Например, запись Chr(&H2153) представляет символ 1/3 кодировке Unicode, хотя в некоторых операционных системах этот символ не будет отображаться во время работы программы. Если заключить один символ в кавычки без суффикса «С», вы получите тип Stri ng вместо Char, а автоматическое преобразование между этими типами не поддерживается (команда Opti on Strict описана ниже в этой главе).
Рекурсия
В VB .NET, как и в любом сколько-нибудь серьезном языке программирования, поддерживается рекурсия — решение задач посредством сведения их к более простым задачам того же типа. Одним из стандартных примеров рекурсивного решения является перебор дерева каталогов на диске (см. главу 9).На концептуальном уровне рекурсивное решение выглядит следующим образом:
Решение задачи с применением рекурсии
If задача тривиальна
Решить Else
Упростить задачу, сводя ее к однотипной, но более простой задаче
Решить более простую задачу с применением рекурсии
End If
(Возможно) Объединить решение простой задачи (или задач)
с решением исходной задачи.
Рекурсивная функция или процедура постоянно вызывает сама себя, непрерывно упрощая задачу до тех пор, пока ее решение не станет тривиальным; в этот момент задача решается, и происходит возврат к предыдущему уровню. Применение рекурсии нередко связано с принципиальным изменением подхода к некоторым задачам, порождающим особенно элегантные решения и столь же элегантные программы (кстати, многие алгоритмы сортировки — такие, как встроенный метод Sort класса .NET Array, — основаны на принципе рекурсии).
В качестве примера мы рассмотрим программу поиска наибольшего общего делителя двух целых чисел (то есть наибольшего целого числа, на которое они оба делятся без остатка). Пример:
Около 2000 лет назад Евклид предложил следующий алгоритм вычисления НОД двух целых чисел а и b:
НОД(126.12)=НОД (12. 126 Mod 12)=НОД (12,6) - 6
Ниже приведен пример рекурсивной функции для вычисления НОД. В строке, выделенной жирным шрифтом, функция GCD вызывает сама себя для более простого случая:
Option Strict On Module Modulel
Function GCD(ByVal p As Long, ByVal q As Long) As Long
If Q Mod P = 0 Then
Return P Else
Return GCD(Q, P Mod Q)
End If
End Function Public Sub Main()
Console.WriteLine("The GCD of 36 and 99 is " & GCD(36. 99))
Console. ReadLine()
End Sub
End Module
Сначала обрабатывается тривиальный случай Q Mod P = 0. Если это условие не выполняется, функция GCD снова вызывает себя для более простого случая, поскольку в результате применения Mod мы переходим к меньшим числам. В приведенном примере объединять результаты не нужно (как, например, в функции сортировки).
Select Case
В качестве альтернативы для громоздких конструкций с множеством Elself в VB .NET была сохранена команда Select Case, упрощающая принятие решений в зависимости от состояния числовой или строковой переменной. Пример:Select Case average
Case Is > 90
Console.WriteLine("A")
Case Is > 80
Console. Wri teLi ne("B")
Case Is > 70
Console.WriteLine("C")
Case Else
Console.WriteLine("You fail")
End Select
Программисты с опытом работы на С и Java, обратите внимание — команда break не нужна, поскольку выполняется только одна секция Case. Дискретные наборы значений перечисляются через запятую, а ключевое слово То позволяет задавать интервалы:
Select Case yourChoice
Case 1 To 9
' Порядок
Case -1. 0
' Неправильный ввод
End Select
Строки
Строковая переменная содержит текст в кодировке Unicode длиной до 231 (более 2 миллиардов!) символов. Как было показано выше, значения строкового типа заключаются в кавычки:Dim message As String
message = "Help"
Конкатенация (объединение) строк обычно выполняется оператором &. Старый вариант с оператором + тоже работает, но при отключении жесткой проверки типов он может вызвать серьезные проблемы, поэтому использовать знак + при конкатенации не рекомендуется. Старый способ идентификации строковых переменных с суффиксом $ (например, aStringVariableS) иногда используется для временных переменных.
В VB .NET строковые переменные не относятся к базовому типу, а являются экземпляра-ми класса String. Некоторые нюансы, связанные с их применением, будут рассмотрены в главе 4, а пока мы упомянем лишь одну особенность, о которой необходимо знать для эффективной работы со строками в VB .NET: при любой модификации строки в VB .NET создается новый экземпляр строки. Частая модификация строки требует больших затрат ресурсов, поэтому в VB .NET имеется класс StringBuilder для выполнения подобных операций (например, выборки данных из буфера и объединения их в строковой переменной).
В отличие от предыдущих версий VB, в VB .NET не поддерживаются строки фиксированной длины.
Строковые функции
В вашем распоряжении остались все классические строковые функции VB6 (Left, Right, Mid и т. д.), но версии этих функций с суффиксом $ теперь не поддерживаются. В табл. 3.5 перечислены важнейшие функции класса String, заменяющие строковые функции VB6. Не забывайте, что при многократной модификации строк (например, при вызове Mid в цикле) следует использовать класс StringBuilder, описанный в главе 4. Некоторые из приведенных методов используют массивы, которые будут рассмотрены ниже в этой главе.При программировании для .NET рекомендуется по возможности использовать методы и свойства класса Stri ng, входящего в .NET Framework. Самые распространенные строковые методы перечислены в табл. 3.6.
Ключевые слова текущей версии VB .NET
| AddHandler |
AddressOf |
Alias |
And |
Ansi |
| As |
Assembly |
Auto |
Binary |
BitAnd |
| BitNot |
BitOr |
BitXor |
Boolean |
ByRef |
| Byte |
ByVal |
Call |
Case |
Catch |
| CBool |
CByte |
CChar |
CDate |
CDec |
| CDbl |
Char |
CInt |
Class |
CLng |
| CObj |
Compare |
Const |
CShort |
CSng |
| CStr |
Ctype |
Date |
Decimal |
Declare |
| Default |
Delegate |
Dim |
Do |
Double |
| Each |
Else |
Elself |
End |
Enum |
| Erase |
Error |
Event |
Exit |
Explicit |
| ExternalSource |
False |
Finally |
For |
Friend |
| Function |
Get |
GetType |
GoTo |
Handles |
| If |
Implements |
Imports |
In |
Inherits |
| Integer |
Interface |
Is |
Lib |
Like |
| Long |
Loop |
Me |
Mod |
Module |
| Mustlnherit |
MustOverride |
MyBase |
MyClass |
Namespace |
| Next |
New |
Not |
Nothing |
Notlnheritable |
| NotOverridable |
Object |
Off |
On |
Option |
| Optional |
Or |
Overloads |
Overridable |
Overides |
| Pa ram Array |
Preserve |
Private |
Property |
Protected |
| Public |
RaiseEvent |
Readonly |
Re Dim |
REM |
| RemoveHandler |
Resume |
Return |
Select |
Set |
| Shadows |
Shared |
Short |
Single |
Static |
| Step |
Stop |
Strict |
String |
Structure |
| Sub |
SyncLock |
Text |
Then |
Throw |
| To |
True |
Try |
TypeOf |
Unicode |
| Until |
When |
While |
With |
With Events |
| WriteOnly |
Xor |
Операторы сравнения
| Символ |
Проверяемое условие |
| <> |
Не равно |
| < |
Меньше |
| <= |
Меньше или равно |
| > |
Больше |
| >= |
Больше или равно |
Как и в VB6, вы можете игнорировать регистр символов во всех сравнениях модуля или формы; для этого в начало модуля или формы включается команда Option Compare Text. Команда Option Compare Binary возвращается к стандартному сравнению строк в соответствии с положением символов в кодировке ANSI. При активном режиме Option Compare Text используется порядок символов для страны, указанной при установке системы Windows.
Ключевое слово Unti 1 можно заменить ключевым словом Whi I e (при этом следует заменить условие на противоположное). Например, фрагмент
Do
' Команды VB .NET (0 и более)
Loop Until X <> String.Empty
эквивалентен следующему фрагменту:
Do
' Команды VB .NET (0 и более)
Loop While X = String.Empty
Обратите внимание на использование константы String.Empty вместо пустой строки "", менее наглядной и чаще приводящей к ошибкам. Если переместить ключевое слово Whi 1е или Unti 1 в секцию Do, проверка будет выполняться в начале цикла (и при ложном условии цикл не будет выполнен ни одного раза). Пример:
Do While Text1.Text <> String.Empty
' Обработать непустой текст Loop
Условия объединяются при помощи операторов Or, Not и And. Пример:
Do While count < 20 And savings < 1000000
Если вы предпочитаете использовать старую конструкцию While-Wend, учтите, что клю-чевое слово Wend было заменено командой End While.
Соответствие между числовыми типами
| Тип VB. NET |
Тип .NET Framework |
Тип VB6 |
| Byte |
System. Byte |
Byte |
| Boolean |
System. Boolean |
Boolean |
| Decimal |
System. Decimal |
— |
| — |
— |
Currency |
| Double |
System. Double |
Double |
| Short |
System. Intl6 |
Integer |
| Integer |
System.Int32 |
Long |
| Long |
System.Int64 |
— |
| Single |
System. Single |
Single |
Допустимые расширяющие преобразования для базовых типов VB .NET
| Тип |
Допустимое расширение |
| Byte | Byte, Short, Integer, Long, Decimal Single, Double |
| Short | Short, Integer, Long, Decimal, Single, Double |
| Integer | Integer, Long, DecimaL Single, Double |
| Long | Long, DecimaL Single, Double |
| Single | Single, Double |
| Date | Date, String |
Dim foo As Boolean
foo = 3
В этом фрагменте логической переменной foo значение True присваивается в виде ненулевого числа (в VB6 это было вполне распространенным явлением). Подобные преобразования должны выполняться явно:
Dim foo As Boolean
foo =СВооl(З)
VB .NET автоматически выполняет преобразование между символьным типом и строкой, состоящей из одного символа.
При желании вы можете вернуться к доисторическим временам и отказаться от жесткой проверки типов. Для этого достаточно начать модуль со следующей команды:
Option Strict Off
Впрочем, поступать подобным образом не рекомендуется.
Если содержимое переменной одного типа может быть преобразовано к другому типу, можно воспользоваться функцией явного преобразования, как в только что приведенном примере с СВооl. Функции явного преобразования типов перечислены в табл. 3.4.
Функции явного преобразования типов
| Функция |
Описание |
| CBool | Преобразует выражение к типу Boolean |
| CByte | Преобразует выражение к типу Byte |
| CInt | Преобразует выражение к типу Integer с округлением |
| CIng | Преобразует выражение к типу Long с округлением |
| CSng | Преобразует выражение к типу Single |
| CDate | Преобразует выражение к типу Date |
| СDbl | Преобразует выражение к типу Double |
| CDec | Преобразует выражение к типу Decimal |
| CStr | Преобразует выражение к типу String |
| CChar | Преобразует первый символ строки к типу Char |
На первый взгляд кажется, что тип Char можно интерпретировать как короткое целое без знака (то есть целое в интервале от 0 до 65 535), но делать этого не следует. Начиная с бета-версии 2 было запрещено преобразование Char в число функциями семейства CInt; вместо этого используется встроенная функция Asc.
Тип object и исчезновение типа Variant
Вероятно, вы заметили, что при описании типов нигде не упоминается тип Variant. BVB .NET этот тип не поддерживается — и это очень хорошо! В VB6 переменные Variant допускали хранение данных произвольного типа. Программисты часто злоупотребляли этой возможностью, что приводило к возникновению нетривиальных ошибок в программах. В VB .NET все типы данных (даже числовые, как Integer) являются частными случаями типа Object. Может показаться, что тип Object стал аналогом Variant в VB .NET, но это не так. Как будет показано в главах 4 и 5, тип Object занимает в программировании .NET значительно более важное место и обладает множеством интересных возможностей. Мы вернемся к типу Object в главах 4 и 5.
Ниже приведен хрестоматийный пример — преобразование температуры по Цельсию в температуру по шкале Фаренгейта. Мы руководствуемся следующими предположениями:
Option Strict On Module
Modulel Sub Main()
Dim cdeg As Decimal
Console. Writer Enter the degrees in centigrade...")
cdeg = CDec(Console.ReadLine())
Dim fdeg As Decimal
fdeg = (((9@ / 5) * cdeg) + 32)
Console.WriteLine(cdeg & " is " & fdeg & " degrees Fahrenheit.")
Console. ReadLine()
End Sub
End Module
Обратите внимание на суффикс @ — он гарантирует, что при вычислениях будет использоваться тип Decimal. Если убрать этот суффикс, то при включенной жесткой проверке типов будет выдано сообщение об ошибке!
При использовании простого оператора / для деления в VB .NET необходимо учитывать некоторые нюансы. За дополнительной информацией обращайтесь к разделу «Математические операторы».
Основные строковые функции
| Функция |
Описание |
| Asc |
Возвращает код первого символа в строке |
| Chr |
Преобразует число в символ Unicode |
| Filter |
Получает строковый массив и искомую строку; возвращает одномерный массив всех элементов, в которых был найден заданный текст |
| GetChar |
Возвращает символ строки с заданным индексом в формате Char. Индексация символов начинается с 1. Например, команда GetChar("Hello",2) возвращает символ «е» в виде типа Char |
| InStr |
Возвращает позицию первого вхождения одной строки в другой строке |
| InStrRev |
Возвращает позицию последнего вхождения одной строки в другой строке |
| Join |
Строит большую строку из меньших строк |
| LCase |
Преобразует строку к нижнему регистру |
| Left |
Находит или удаляет заданное количество символов от начала строки |
| Len |
Возвращает длину строки |
| LTrim |
Удаляет пробелы в начале строки |
| Mid |
Находит или удаляет символы в строке |
| Replace |
Заменяет одно или более вхождений одной строки в другой строке |
| Right |
Находит или удаляет заданное количество символов в конце строки |
| RTrim |
Удаляет пробелы в конце строки |
| Space |
Генерирует строку заданной длины, состоящую из пробелов |
| Split |
Позволяет разбивать строку по заданным разделителям (например, пробелам) |
| Str |
Возвращает строковое представление числа |
| StrComp |
Альтернативный способ сравнения строк |
| StrConv |
Преобразует строку из одной формы в другую (например, с изменением регистра) |
| String |
Создает строку, состоящую из многократно повторяющегося символа |
| Trim |
Удаляет пробелы в начале и конце строки |
| UCase |
Преобразует строку к верхнему регистру |
Основные строковые методы и свойства .NET Framework
| Метод/свойство | Описание |
| Chars | Возвращает символ, находящийся в заданной позиции строки |
| Compare | Сравнивает две строки |
| Copy | Копирует существующую строку |
| Copy To | Копирует заданное количество символов, начиная в заданную позицию массива символов |
| Empty | Константа, представляющая пустую строку |
| EndsWith | Проверяет, завершается ли заданная строка определенной последовательностью символов |
| IndexOf | Возвращает индекс первого вхождения подстроки в заданной строке |
| Insert |
Возвращает новую строку, полученную вставкой подстроки в заданную позицию |
| Join |
Объединяет массив строк с заданным разделителем |
| LastlndexOf |
Возвращает индекс последнего вхождения заданного символа или подстроки в строке |
| Length |
Возвращает количество символов в строке |
| PadLeft |
Выравнивает символы строки по правому краю. Строка дополняется слева пробелами или другими символами до заданной длины |
| PadRight |
Выравнивает символы строки по левому краю. Строка дополняется справа пробелами или другими символами до заданной длины |
| Remove |
Удаляет из строки заданное количество символов, начиная с заданной позиции |
| Replace |
Заменяет все вхождения подстроки другой подстрокой |
| Split |
Разбивает строку, превращая ее в массив подстрок |
| Starts With |
Проверяет, начинается ли заданная строка определенной последовательностью символов |
| Substring |
Возвращает подстроку, начинающуюся с заданной позиции |
| ToCharArray |
Копирует символы строки в символьный массив |
| ToLower |
Возвращает копию строки, преобразованную к нижнему регистру |
| ToUpper |
Возвращает копию строки, преобразованную к верхнему регистру |
| Trim |
Удаляет пробелы или все символы из набора, заданного в виде массива символов Unicode, в начале и конце строки |
| TrimEnd |
Удаляет пробелы или все символы из набора, заданного в виде массива символов Unicode, в конце строки |
| TrimStart |
Удаляет пробелы или все символы из набора, заданного в виде массива символов Unicode, в начале строки |
Поскольку в .NET Framework строки являются объектам, синтаксис вызова этих методов достаточно логичен и удобен. Выполните следующую программу:
Sub Main()
Dim river As String =" Mississippi Missippi"
'один пробел слева
Consol e. Wri teLi ne( ri ver. Tollpper ())
Console.Wri teLi net ri ver.ToLower())
Console.WriteLineCriver.Trim())
Console. WriteLinetri ver. EndsWith("I"))
Consol e.Wri teLi ne С ri ver.EndsWith("i"))
Console.WriteLine(river.IndexOf("s"))
'Индексация начинается с 0!
Console.WriteLineCriver.Insert(9. " river"))
'Индексация
' начинается с 0!
Consol e.ReadLine() End Sub
Результат выглядит так:
MISSISSIPPI MISSIPPI
mississippi missippi
Mississippi Missippi
False
True
3
Mississi riverppi Missippi
| Назад | Содержание | Вперед |
Математические операции
| Оператор |
Операция |
| + | Сложение |
| - | Вычитание (и обозначение отрицательных чисел) |
| / | Деление (преобразование к Double — не может вызвать исключение DivideByZero; см. главу 7) |
| \ | Целочисленное деление (без преобразования — может вызвать исключение DivideByZero) |
| * | Умножение |
| ^ | Возведение в степень |
Module Modulel
Sub Main()
Console.WriteLine((4 / 2).GetType())
Console. ReadLine()
End Sub
End Module
В консольном окне выводится строка
System.Double
Возможности метода GetType не ограничиваются простым выводом имени — в частности, он используется в процессе рефлексии. Механизм рефлексии описан в главе 4.
Ниже приведен пример ситуации, в которой необходимо учитывать тип значения, возвращаемого оператором деления. Перед нами простая (но нерабочая) версия программы, преобразующей температуру по Цельсию в температуру по Фаренгейту. В выделенной строке отсутствует суффикс @, преобразующий результат деления к типу Decimal:
Option Strict On
Module Modulel Sub Main()
Dim cdeg As Decimal
Console.. Writer Enter the degrees in centigrade...")
cdeg=CDec(Console.ReadLine())
Dim fdeg As Decimal
fdeg = (((9 / 5) * cdeg) + 32)
Console.WriteLine(cdeg & " is " & fdeg & " degrees Fahrenheit.")
Console. ReadLine()
End Sub
End Module
Из-за присутствия знака / в выделенной строке переменной fdeg присваивается результат типа Double. В режиме жесткой проверки типов это приводит к тому, что на стадии компиляции будет выдано следующее сообщение об ошибке:
Option Strict disallows implicit conversions from Double to Decimal.
Как исправить ошибку? Только не надо убирать команду Option Strict — это одно из лучших новшеств VB .NET, которое избавляет вас от злостного искажения типов. Лучше воспользуйтесь суффиксом @ или преобразуйте выражение (полностью или частично) к типу Decimal. Пример:
fdeg = ((CDec(9 / 5) * cdeg) + 32)
Поскольку результат деления преобразуется к типу Decimal, результат тоже относится к типу Decimal.
Остается лишь заметить, что в этом простом примере мы используем метод Write вместо Wri teLi ne, чтобы предотвратить перевод строки после вывода текста. Кроме того, в реальной программе введенные данные следовало бы предварительно проанализировать, потому что пользователи часто ошибаются при вводе.
Наконец, вещественное деление в VB .NET соответствует стандарту IEEE, поэтому вместо ошибки деления на ноль теперь происходит нечто странное. Пример:
Sub Main()
Dim getData As String
Dim x, у As Double
x = 4
У = 0
Console.WriteLine("What is 4/0 in VB .NET? " & x / y)
Console.ReadLine()
End Sub
Результат выглядит так:
What is 4/0 in VB. NET? Infinity
Результат деления 0/0 равен
NaN (Not A Number, «не является числом»).
В табл. 3.8 перечислены операторы, используемые только при делении чисел типа Integer и Long.
Математические операторы целочисленного деления
| Оператор |
Операция |
| \ |
Целочисленное деление любых целых чисел |
| Mod |
Остаток от целочисленного деления |
Оператор Mod дополняет оператор целочисленного деления и возвращает остаток от целочисленного деления. Например, 7 Mod 3 = 1. Если целые числа делятся без остатка, оператор Mod возвращает 0: 8 Mod 4 = 0.
Общие математические функции класса Math
| Математическая функция |
Описание |
| Abs |
Возвращает абсолютное значение (модуль) числа |
| Acos |
Возвращает угол, косинус которого равен заданному числу |
| Asin |
Возвращает угол, синус которого равен заданному числу |
| Atan |
Возвращает угол, тангенс которого равен заданному числу |
| Ceiling |
Возвращает наименьшее целое число, большее либо равное заданному числу |
| Cos |
Возвращает косинус заданного угла |
| Exp | Возвращает число е (приблизительно 2,71828182845905), возведенное в заданную степень |
| Floor | Возвращает наибольшее целое число, большее либо равное заданному числу |
| Log |
Возвращает натуральный логарифм |
| Log10 |
Возвращает десятичный логарифм |
| Max |
Возвращает большее из двух заданных чисел |
| Min |
Возвращает меньшее из двух заданных чисел |
| Round |
Возвращает целое число, ближайшее к заданному числу |
| Sign |
Возвращает величину, определяющую знак числа |
| - Sin |
Возвращает синус заданного угла |
| Sqrt |
Возвращает квадратный корень |
| Tan |
Возвращает тангенс заданного угла |
| Назад | Содержание | Вперед |
Ускоренная проверка
Если компилятор обнаруживает, что проверенная часть сложного логического условия однозначно определяет результат, он не проверяет остаток выражения. Это называется ускоренной проверкой (short curcuiting). Например, если в следующем примере переменная foo ложна, компилятор не проверяет переменную bar:If foo And bar Then...
Так было в VB .NET бета-версии 1, но в прежних версиях VB ускоренная проверка не применялась. После многочисленных жалоб разработчики Microsoft вернули старую интерпретацию логических операторов And и Or и добавили новые ключевые слова AndAlso и OrElse, поддерживающие ускоренную проверку:
If foo AndAlso Then...
Условные команды и принятие решений
" В VB .NET условная команда If, как и в VB6, существует в двух версиях — однострочной и многострочной:If X < 0 Then Console.WriteLine("Number must be positive!")
Условие конструкции If-Then может содержать логические операторы And, Or и Not. Довольно часто программа выполняет разные действия в зависимости от того, окажется ли условие истинным или ложным. В этом случае базовая форма команды
If-Then:
If условие Then
' Команды VB .NET (0 и более) End If
дополняется одной или несколькими секциями El se:
If условие Then
' Команды VB .NET (0 и более) Else
' Команды VB .NET (0 и более)
End If
Несколько последовательных проверок в секциях Else можно оформить в виде конструкции Elself:
If условие Then
' Команды
Elself условие Then
' Команды
Elself условие Then
' Команды
Else
' Команды
End If
Конструкция If-Then может использоваться для преждевременного выхода из цикла — для этого она объединяется с командой Exit Do или Exit For. Встретив команду Exit Do или Exit For, VB .NET немедленно завершает цикл и продолжает выполнение программы с команды, следующей за ключевым словом Loop или Next (в зависимости от типа цикла).
Cамоучитель по VB.NET
Абстракция
Абстракцией называется моделирование объектов в программе. Другими словами, речь идет об имитации реально существующих объектов, отражающей особенности их взаимодействия в окружающем мире. Так, первый объектно-ориентированный язык Simula (http://java.sun.com/people/jag/SimulaHistory.html) разрабатывался специально для задач имитации и моделирования. Впрочем, модные концепции виртуальной реальности выводят принцип абстракции на совершенно новый уровень, не связанный с физическими объектами. Абстракция необходима, потому что успешное использование ООП возможно лишь в том случае, если вы сможете выделить содержательные аспекты своей проблемы.Приступая к построению объектной модели, всегда задавайте себе вопрос: какие свойства и методы должны входить в объект, чтобы он адекватно моделировал ситуацию для решения поставленной задачи?
ArrayList

Класс ArrayList реализует динамический массив, размеры которого автоматически увеличиваются и уменьшаются по мере надобности. Динамические массивы работают чуть медленнее обычных массивов, но они заметно упрощают многие задачи программирования. Кроме того, в отличие от большинства массивов класс ArrayLi st является гетерогенным, то есть позволяет хранить объекты разных типов. В главе 5 будет показано, как создать класс ArrayList для хранения объектов лишь одного типа; вы также узнаете о некоторых нюансах, связанных с хранением обобщенных объектов в ArrayLi St.
Использование ArrayList вместо базового массива означает, что вам не придется часто вызывать ReDim Preserve для сохранения существующих данных. Достаточно вызвать метод Add, и класс ArrayList сам выполнит всю черновую работу. Класс ArrayList содержит ряд других полезных методов. Например, метод AddRange позволяет перенести в динамический массив все содержимое существующего массива всего одной командой. После завершения обработки элементы можно скопировать обратно. В частности, это позволяет легко объединить содержимое двух массивов. В табл. 4.3 перечислены основные члены класса ArrayList (полный список приведен в электронной документации).
Рисунок 4.6 . Метод GetDirectories в справочной системе
Атрибуты уровня доступа и создание объектов
Атрибуты уровня доступа, установленные для класса, управляют возможностью создания объектов соответствующего типа. Грубо говоря, они являются отдаленным аналогом свойства Instancing в VB6, хотя для некоторых значений Instancing приходится дополнительно учитывать уровень доступа конструктора. В табл. 4.5 описано соответствие между свойством Instancing VB6 и комбинациями атрибутов уровня доступа класса и конструктора.
Две разновидности циклических ссылок Структурные типы

Традиционно в объектно-ориентированных языках возникало немало проблем с простейшими типами данных — такими, как обычные целые числа. Дело в том, что в объектно-ориентированном языке все данные должны быть объектами. С другой стороны, создание объекта сопряжено с определенными затратами на выполнение служебных операций (таких, как выделение блока памяти для объекта). Обработка сообщения «сложить» также уступает по скорости простой математической операции сложения и т. д. Стоит добавить, что в языках с автоматической сборкой мусора некоторое время расходуется на уничтожение неиспользуемых объектов.
Ранние объектно-ориентированные языки пошли по самому прямолинейному пути. Скажем, в Smalltalk все данные интерпретировались как объекты. В результате такие языки работали медленнее, чем языки с разделением примитивных типов и объектов. С другой стороны, подобное разделение приводило к усложнению программ, поскольку программный код, работавший с числами, приходилось отделять от кода, работавшего с объектами. Чтобы интерпретировать число в объектном контексте, его приходилось «заворачивать» в объект. Например, в Java сохранение числа в эквиваленте динамического массива выглядело примерно так:
anArrayList.Add(Integer(5));
Число 5 «заворачивалось» в объект Integer. Такие программы плохо читались и медленно работали.
В .NET Framework были объединены лучшие стороны обоих решений. В общем случае числа интерпретируются как примитивные типы, но при "необходимости они автоматически интерпретируются как объекты. Таким образом, для обычного числового литерала можно вызвать метод или занести его в хэш-таблицу без дополнительных усилий. Это называется автоматической упаковкой (boxing); обратный процесс называется распаковкой (unboxing).
Для нас, программистов, из этого вытекает важное следствие: хотя в VB .NET все данные являются объектами, не каждая переменная в программе содержит манипулятор блока памяти и создается оператором New. Разумеется, ничто не дается даром: программисту приходится помнить о различиях между структурными и ссылочными типами. Первое, наиболее очевидное различие заключается в том, что новые экземпляры структурных типов создаются без ключевого слова New. Вам не придется (да и не удастся) использовать конструкции вида Dim a As New Integer(5).
Более серьезное различие связано с передачей переменных процедурам по значению. Как было сказано выше, при передаче изменяемого объекта по значению процедура может изменить состояние объекта. Переданные по значению структурные типы ведут себя вполне традиционно — все изменения теряются при выходе из вызванной процедуры или функции (иногда это называется структурной семантикой в отличие от ссылочной семантики).
Все числовые типы VB .NET являются структурными типами; к этой же категории относится и такой тип, как дата. Как будет показано ниже, VB .NET позволяет определить пользовательские структурные типы, если по соображениям быстродействия вы хотите свести к минимуму затраты на работу с объектами или же предпочитаете работать с объектами, обладающими структурной семантикой.
Чтобы узнать, обладает ли некий тип структурной семантикой, передайте переменную этого типа следующей функции.
Function IsValueType(ByVal foo As Object)As Boolean
If TypeOf (foo)Is System.ValueType Then
Return True Else
Return False
End If
End Function
Для объектов структурного типа оператор Equal s всегда возвращает True, если структурные объекты содержат одинаковые данные. Синтаксис вызова выглядит так:
a..Fquals(b)
Учтите, что для ссылочных типов этот принцип в общем случае не выполняется. Например, два массива могут содержать одинаковые элементы, но равными при этом они не считаются.
В VB .NET структурные типы делятся на две категории: структуры и перечисляемые типы. Мы начнем с перечисляемых типов, а затем перейдем к структурам, которые представляют собой «облегченные» варианты объектов.
Хэш-таблицы
Простые и динамические массивы удобны прежде всего тем, что вы можете напрямую обратиться к любому элементу по индексу. Конечно, для этого необходимо знать индекс. В следующей структуре данных — хэш-таблице — произвольный доступ к данным осуществляется по ключу. Допустим, у вас имеется хэш-таблица с именем theData. Команда theData("Bill 's Address") позволяет извлечь из хэш-таблицы нужный элемент без циклического перебора всего содержимого. Хэш-таблицы очень удобны в ситуациях, когда вы хотите получить быстрый доступ к значению по связанному с ним уникальному атрибуту, то есть ключу. Разумеется, программирование хэш-таблицы — задача непростая [ Для этого необходимо построить хорошую функцию хэширования для вычисления индекса данных по ключу, а также решить неизбежную проблему коллизий, то есть совпадения хэш-кодов у двух разных элементов. Даже терминология выглядит устрашающе... ], но, к счастью, эта работа уже выполнена за вас разработчиками .NET Framework.Другую категорию структур данных, предназначенных для выборки значения по ключу, составляют ассоциативные массивы (словари). Они часто реализуются в виде хэш-таблиц с дополнительным кодом для выполнения особых операций (например, обнаружения повторяющихся значений или ключей).
В табл. 4.4 перечислены важнейшие методы класса Hashtable (за полным списком обращайтесь к электронной документации).
Методы класса HashTable учитывают регистр символов в строковых ключах, и действие команды Option Compare Text на них не распространяется. О том, как создать хэш-таб-лицу, игнорирующую регистр символов, рассказано в главе 5.
Хронометраж — насколько быстрее работает класс StringBuilder?
Хотя Microsoft не разрешает публиковать точные результаты хронометража для бета-версий (и это вполне разумно, поскольку в них содержится большой объем отладочного кода), отношение результатов, полученных в ходе измерений, почти всегда остается более или менее постоянным. Иногда в окончательной версии это отношение слегка изменяется, но в гораздо меньшей степени, чем абсолютные значения показателей.Хронометраж в VB .NET реализуется легко — достаточно объединить метод Now с методом Ticks класса DateTlme. Как подсказывает само название, метод Now возвращает текущие показания системных часов. Метод Ti cks возвращает число типа Long, равное количеству 100-наносекундных интервалов, прошедших с 00:00 1 января 0001 года (1 наносекунда = 1/1 000 000 000 секунды).
Следующая программа использовалась для оценки того, насколько быстрее класс StringBuilder выполняет присоединение символов в конец строки по сравнению с классом String. Выигрыш растет с увеличением количества символов; при 50 000 символов эффективность возрастала более чем в 800 раз!
Option Strict On Module Modulel
Sub Main()
Dim i As Integer
Dim StartTime As New DateTime()
Dim EndTime As New DateTime()
StartTime =DateTime.Now()
Dim theText As New System.Text.SthngBuilder()
For i =1 To 50000
theText =theText.Append("A")
Next
EndTime =DateTime.Now
Dim answerl,answer2 As Long
' Количество 100-наносекундных интервалов
answer1 =EndTi me.Ticks()-StartTime.Ticks()
StartTime =DateTime.Now()
Dim aString As String
For i =1 To 50000
aString =aString & "A"
Next
EndTime =DateTime.Now
' Количество 100-наносекундных интервалов
answer2 =(EndTime.Ticks()-StartTime.Ticks())
Console.WriteLine("StringBuilder was " & _ answer? /answerl & "times faster.")
Console.ReadLine()
End Sub
End Module
Импортирование
Обращаясь к хорошим знакомым, мы не называем их по имени-отчеству и фамилии. В .NET предусмотрена возможность импортирования пространств имен командой Imports, что позволяет избавиться от громоздких полных имен. После правильной команды Imports все ссылки видаSystem.Text.StringBuilder .
могут заменяться простым именем StringBuilder.
Пространство имен System автоматически импортируется в каждое решение, созданное в Visual Studio. Благодаря этому методы этого пространства имен могут вызываться в виде Console.WriteLine() вместо полного имени System.Console.WrlteLlne().
Список пространств имен, автоматически импортируемых в решение, находится на странице Imports окна свойств решения (Рисунок 4.2).
Загрузите в Object Brewser автоматически импортируемое пространство имен Microsoft. Visual Basic — вы увидите, что оно содержит различные функции, существовавшие в Visual Basic и сохраненные в VB .NET (Рисунок 4.3).
Импортирование пространства имен Microsoft. Visual Basic. Constants позволяет использовать старые константы VB — такие, как vbCrtf. .NET-версии многих констант не имеют префикса vb (например, CrLf) и находятся в пространстве имен
Microsoft.VIsualBasi с.Control Chars.
Команда Imports должна располагаться перед всеми остальными объявлениями, включая объявление имени модуля, но после директив Option (таких, как Option Strict On или Option Compare Text).
Инкапсуляция
В ООП термин «инкапсуляция» означает то, что мы обычно называем маскировкой данных. Скрывая данные, вы определяете свойства и методы для работы с ними. Вспомните, что говорилось выше, — успешное применение ООП возможно лишь в том случае, если все операции с внутренними данными объекта осуществляются посредством обмена сообщениями. Данные объекта хранятся в полях экземпляра; также часто встречается термин «переменные экземпляра». В сущности, это одно и то же, и выбор зависит в основном от того, к какому термину вы привыкли; в этой книге обычно используется термин «поля экземпляра». Текущее состояние объекта определяется текущими значениями полей экземпляра. Не забывайте главное правило: никогда не предоставляйте прямой доступ извне к полям экземпляра (внутренним данным объекта).Вернемся к примеру с объектно-ориентированной программой для отдела кадров, в которой мы определили класс Employee. В переменных класса Еmplоуее могут храниться следующие сведения:
Рискуя надоесть частыми повторениями, мы все же еще раз подчеркнем: успешное применение инкапсуляции возможно, если другие части вашей программы никогда не получают прямого доступа к полям экземпляра (переменным) ваших классов. Программа должна взаимодействовать с ними только через вспомогательные члены класса. Только при наличии закрытых данных, недоступных извне, объект превращается в «черный ящик» с четкими правилами поведения и неизвестным внутренним устройством. Ограничение доступа к данным имеет определяющее значение как для повторного использования, так и для надежности объекта при долгосрочном использовании.
Is и Nothing
Оператор Is проверяет, ссылаются ли две объектные переменные на одну область памяти. Следующий фрагмент в обоих случаях выводит True, поскольку в результате операций присваивания все объектные переменные ссылаются на одну область памяти:Dim Objectl As New Object()
Dim ObjectZ As New Object()
Dim Objects As New Object()
ObjectZ =Object1
Objects Object2
Console.WriteLine(Objectl Is Object2)
Console.WriteLine(Object1 Is Object3)
Как и в прежних версиях VB, присваивание объектной переменной значения Nothi ng разрывает ее связь с блоком памяти. Когда объектная переменная равна Nothing, она не ассоциируется ни с каким объектом. В этом состоянии находятся все объектные переменные, которые были объявлены в программе, но еще не инициализировались. В программе часто встречаются проверки следующего вида:
If anObject Is Nothing Then
' Переменная не связана с объектом, присвоить значение
Else
' Значение было присвоено ранее
End If
Дополнительная информация о том, что происходит при присваивании объектным переменным значения Nothing, приведена в разделе «Сборка мусора и завершение».
Экземпляры
Итак, вы решили, какие классы должны входить в ваш проект. На следующем этапе построения объектной модели рассматриваются конкретные экземпляры этих классов. Попробуйте ответить на три вопроса:Текущее состояние объекта не обеспечивает его однозначной идентификации. Два объекта могут находиться в одинаковом состоянии, одинаково реагировать на внешние воздействия, но при этом они все равно остаются разными объектами (как два массива с одинаковым содержимым). Таким образом, должен существовать некий критерий, по которому объект можно отличить от других похожих объектов. К поведению объекта относится информация о том, что он делает в данный момент и что он теоретически может сделать в будущем. В VB .NET поведение объекта определяется его свойствами, методами и событиями.
Несомненно, эти три характеристики влияют друг на друга, и это обстоятельство должно учитываться в коде класса. Например, поведение объекта зависит от его текущего состояния: заблокированное текстовое поле ведет себя совсем не так, как доступное, и об этом следует помнить при проектировании класса.
Как объекты взаимодействуют друг с другом?
Согласно одному из ключевых принципов ООП каждый класс (= шаблон для построения объектов) отвечает за выполнение небольшой группы взаимосвязанных задач. Проектирование и отладка специализированного класса, предназначенного для создания простых объектов с небольшим количеством задач, занимает гораздо меньше времени, чем разработка классов со сложными внутренними структурами данных, многочисленными методами и свойствами. Если потребуется сделать нечто такое, на что ваш объект не рассчитан, не стоит вносить изменения в исходный класс и усложнять его — лучше определите новый класс, оптимизированный для решения этой задачи. Предоставьте старому объекту доступ к объектам нового типа, и старый объект сможет обратиться к новому объекту с запросом на выполнение нужной операции.Пользователи прежних версий VB хорошо знакомы с подобным взаимодействием между объектами. В частности, именно это и происходит при вызове методов или задании свойств элементов. С точки зрения пользователя, запрос к другому объекту реализуется обращением к свойству или вызовом метода этого объекта. На внутреннем уровне в обработке запросов участвуют обобщенные версии функций и процедур VB .NET (см. главу 3). Таким образом, все подробности внутренней реализации остаются скрытыми от внешнего мира.
На жаргоне ООП этот принцип формулируется так: объекты взаимодействуют с другими объектами только посредством обмена сообщениями. Еще раз подчеркнем — объект никогда не должен напрямую работать с внутренними данными другого объекта. Все взаимодействие должно происходить на уровне сообщений (задания свойств и вызова методов). Проектируйте свои объекты по принципу «черного ящика»: объект реагирует на сообщения из определенного набора, а его фактическая реализация остается неизвестной.
Подведем итог. Все операции с объектами в программе должны сводиться к изменению их свойств и вызову методов. Не используйте открытые (глобальные) переменные в классах или объектах, созданных на их основе.
| Содержание | Вперед |
Классы как пользовательские типы
Класс также можно рассматривать как «интеллектуальный» пользовательский тип, обладающий расширенными возможностями — например, перед изменением своих внутренних данных класс может проверить их новые значения. При обращении к данным класс может проверить полномочия стороны, от которой поступил вызов. Наконец, представьте себе пользовательский тип, который возвращает данные в определенном формате вместо того, чтобы просто выдавать их внутреннее представление.С этой точки зрения объект представляет собой переменную пользовательского типа, с которой ассоциированы функции обращения к данным и их проверки. Главный принцип работы с данными класса — замена прямого доступа к данным вызовом вспомогательных функций.
На первый взгляд организация доступа к данным при помощи функций только прибавляет работы программисту, но это небольшое усложнение с лихвой компенсируется преимуществами от проверки и контроля доступа к данным.
По мере освоения ООП перед вами откроются новые возможности, не ограничивающиеся простой проверкой или возвращением внутренних данных объекта.
Классы коллекций в .NET Framework
Чтобы пробудить в вас интерес к .NET Framework, мы кратко рассмотрим некоторые классы коллекций. В этих классах реализуются стандартные структуры данных, часто используемые в нетривиальных программах. Коллекции настолько важны, что они по умолчанию автоматически импортируются в каждое решение VB .NET (в пространстве имен System.Collections).
Me
Если класс используется как шаблон для создания однотипных объектов, программист должен иметь возможность сослаться на текущий объект, которому принадлежит выполняемый код. Зарезервированное слово Me всегда интерпретируется как объектная переменная, обозначающая текущий экземпляр. Применение Me гарантирует, что неоднозначная конструкция будет интерпретирована в контексте текущего класса.Также стоит заметить, что один из самых распространенных (и самых нелепых) примеров использования Me встречается в ситуациях вроде следующей:
Public Class Point
Private x As Integer
Private у As Integer
Public Sub New(ByVal x As Integer.ByVal у As Integer)
Me.x = x
Me.у = у End Sub
' И т.д.
End Class
Запись Me. x используется для того, чтобы отличить поле х экземпляра от параметра х, передаваемого при вызове метода New. Конечно, проблема легко решается добавлением префикса m_ перед именем переменной класса, однако подобные конструкции часто используются в С#; возможно, они встретятся в программе, сопровождение которой вам будет поручено.
Наследование
В качестве примера наследования представьте себе классы для отдельных категорий работников (класс Programmer, класс Manager и т. д.). Механизм, используемый для создания таких классов на базе класса Empl oyee, называется наследованием. В иерархии наследования класс Employee называется базовым, а класс Programmer — производным классом. Производные классы:Производный класс может содержать новые методы, не имеющие аналогов в базовом классе.
Например, в класс Manager может быть включено новое свойство Secretary.
Разработчики давно хотели видеть наследование в VB и громко жаловались на его отсутствие. Нельзя сказать, что шум был поднят на пустом месте, однако многие склонны переоценивать важность наследования. Дело в том, что наследование, если хорошенько разобраться, всего лишь избавляет программиста от необходимости заново писать готовый код. В наследовании нет никакой мистики — это лишь способ упростить повторное использование программного кода. В нашем примере классы Employee и Manager обладали рядом сходных черт (наличие даты найма, зарплаты и т. д.). Зачем программировать свойство Salary в двух местах, если код будет абсолютно одинаковым? При полноценной реализации наследования использование функциональности базового класса в производном классе практически не требует дополнительных усилий — производный класс изначально наследует все члены своего предка. Программист может переопределить некоторые члены базового класса в соответствии со спецификой производного класса. Например, если менеджер автоматически получает 8-процентную прибавку к зарплате, тогда как для большинства работников прибавка составляет всего 4%, метод RaiseSalагу класса Manager должен заменить метод RaiseSalary базового класса Employee. С другой стороны, методы вроде GetName в изменении не нуждаются и остаются в прежнем виде.
Многие влиятельные теоретики ООП полагают, что от наследования вообще стоит дер-жаться подальше, и рекомендуют заменить его использованием интерфейсов (конечно, в VB .NET поддерживаются обе возможности). Такое отношение связано с проблемой неустойчивости базовых классов, которая в VB .NET отошла на второй план (за подробностями обращайтесь к главе 5). Использование интерфейсов вместо классического наследования иногда называется наследованием интерфейсов, тогда как за классическим наследованием закреплен термин «наследование реализации».
Напоследок мы хотим предупредить: не используйте наследование, если вы твердо не уверены в существовании логической связи «является частным случаем». Например, не создавайте класс Contractor (внештатный работник), производный от Employee, только для того, чтобы избавиться от хлопот по дублированию кода свойств имени или номера социального страхования. Внештатный работник не является служащим компании, и бухгалтер, простоты ради оформивший его по общим правилам, только наживет себе неприятности с налоговой инспекцией. То же относится и к вам: применение наследования при отсутствии логической связи «является частным случаем» приведет к печальным последствиям (см. главу 5).
Объектные переменные
Рассмотрим следующий фрагмент:Dim thing As New Object
Dim aRandomlnstance As New Random
В нем объявляются и создаются две переменные: thing и aRandomlnstance. Первая переменная содержит ссылку на тип Object, а вторая — ссылку на экземпляр класса Random. Следующая команда вполне допустима даже в режиме жесткой проверки типов (Option Strict On), поскольку в VB .NET все переменные в конечном счете представляют собой объекты:
thing = aRandomlnstance
С другой стороны, обратное присваивание (aRandomlnstance = thing) недопустимо, поскольку не каждый объект является экземпляром класса Random.
Объектную переменную можно рассматривать как манипулятор блока памяти (причем не фиксированного, а перемещаемого). Объектные переменные также часто называют ссылками (references) или интеллектуальными указателями (smart pointers). Обычно при использовании знака = с ключевым словом New манипулятор связывается с блоком памяти, в котором хранится соответствующий объект (при работе с так называемыми структурными типами возникают некоторые тонкости, которые будут рассматриваться далее в этой главе).
Как будет показано в следующей главе, общим предком всех типов VB .NET является тип Object. Именно поэтому в VB .NET любую величину можно сохранить в переменной типа Object, а любой созданный объект поддерживает методы класса Object. Например, поскольку в классе Object определен метод ToString, каждый класс позволяет получить строковое представление объекта (полезность которого зависит от реализации). Метод ToString автоматически вызывается при использовании конструкций вида Console. WriteLine(foo).
Если объектная переменная содержит манипулятор блока памяти, в результате операции присваивания второй объектной переменной будет присвоен манипулятор того же блока памяти. Но если вы забудете о том, что для работы с одним блоком памяти используются две разные переменные, это может привести к печальным последствиям — изменения в состоянии объекта, внесенные через одну переменную, автоматически повлияют на другую переменную. Для примера рассмотрим следующий фрагмент:
Sub Maln()
Dim A As New ArrayList()
Dim В As ArrayList
В = А
B.Add("foo")
Console.WriteLine(A.Count)
Console.ReadLine() End Sub
Динамический массив А также будет содержать строку foo, поэтому выведенное значение A.Count будет равно 1.
Если вы знакомы с языками, в которых широко используются указатели (например, С или Pascal), вы увидите, что у объектных переменных есть много общего с указателями. Главное различие состоит в том, что разыменование (dereferencing) объектных переменных происходит автоматически и с ними не могут выполняться математические операции.
Поскольку в VB .NET строки и массивы являются объектами, следует помнить, что для работы с ними используются объектные переменные. Как было показано в главе 3, это позволяет использовать встроенные возможности соответствующих классов при помощи синтаксиса «.». Например, при работе с массивом через переменную апАггау команда anArray.Sort() отсортирует массив чрезвычайно эффективным методом быстрой сортировки.
К сожалению, за все хорошее приходится платить. Передача объектных переменных по значению связана с определенными трудностями, которые теперь распространяются и на стандартные объекты вроде массивов. Данная тема рассматривается в. разделе «Проблемы с передачей объектных переменных по значению» этой главы.
Как и в прежних версиях VB, объектные переменные могут использоваться для получения более компактной записи. Например, в следующем фрагменте определяется короткое имя аВох, которое будет использоваться вместо длинного Му-
Form.TextBoxl:
Dim aBox As System.Windows.Forms.TextBox aBox = MyForm.TextBoxl
Подобные сокращения часто используются в сочетании с ключевым словом With:
With aBox
.AutoSize =False
.Height =1000
.Width =200
.Text ="Hello"
End With
Область видимости переменных
Переменные класса (в том числе и закрытые поля), объявленные за пределами его методов или свойств, доступны для всех членов класса. Переменные, объявленные в методе или свойстве, являются локальными по отношению к этому методу или свойству.Переменные, объявленные на уровне модуля, доступны для всех классов, определенных в этом модуле, и для всего кода, обладающего доступом к этому модулю.
Таким образом, переменные уровня модуля являются глобальными по отношению к экземплярам классов. Пример:
Module Modulel
Dim aGlobal As Integer = 37
Sub Main()
Dim anA As New А()
Dim aB As New B()
Console. ReadLine()
End Sub
Public Class A Sub New()
aGlobal =aGlobal +17 Console.WriteLine(aGlobal)
End Sub
End Class
Public Class В Sub New()
Console.WriteLine(aGlobal)
End Sub
End Class
End Module
В данном случае целая переменная aGlobal определяется на уровне модуля, поэтому изменения, вносимые в aGlobal классом А, будут восприняты классом В. Использовать переменные уровня модуля не рекомендуется — все взаимодействие между классами должно быть реализовано на уровне обмена сообщениями!
В прежних версиях VB широко практиковалось хранение общих данных классов в глобальных переменных. В VB .NET надобность в этом небезопасном приеме отпала. За дополнительной информацией обращайтесь к разделу «Общие данные в классах» этой главы.
Общие члены классов
Закрытые общие поля классов в сочетании со ReadOnly-свойствами очень удобны, но этим область применения ключевого слова Shared не исчерпывается. В классе можно объявлять общие свойства и методы. Как было показано на примере класса Math, при обращении к общим средствам класса указывается либо имя класса, либо имя конкретного экземпляра. Допустим, в класс Employee включается общая функция Calcul ateFICA, зависящая от двух открытых констант:Public Const FICA_LIMIT As Integer = 76200
Public Const FICA_PERCENTAGE As Decimal = 0.062D
Функция CalculateFICA выглядит так:
Public Shared Function CalculateFICA(ByVal aSalary As Decimal) As Decimal
If aSalary > FICA_LIMIT Then
Return FICA_LIMIT * FICA_PERCENTAGE
Else
Return aSalary * FICA_PERCENTAGE
End If
End Function
Общие члены класса могут использоваться без создания экземпляров Empl oyee, только по имени класса. Пример:
System.Console.WriteLine(Employee.
CalculateFICA(100000))
С другой стороны, метод мджно вызвать и для конкретного экземпляра Employee:
System.Console.WriteLine
(Tom.CalculateFICA
(Tom.GetSalary())
Конструкторы тоже можно объявлять общими, для этого в объявление метода
New включается ключевое слово Shared. Общие конструкторы:
Общие данные в классах
Вернемся к классу Еmploуее. Допустим, каждому работнику необходимо присвоить уникальный номер. В старых версиях VB задача решалась при помощи глобальных переменных, что приводило к нарушению инкапсуляции и создавало потенциальную угрозу случайного изменения номеров внешним кодом. Логика подсказывает, что номер должен увеличиваться только при создании нового объекта Empl оуее.В VB .NET наконец-то появились средства для достижения этой цели. Идея проста: в классе определяются данные, совместно используемые всеми экземплярами данного класса, однако внешний доступ к этим данным находится под вашим полным контролем (например, через обращение к свойству). Не стоит и говорить, что эти поля никогда не должны объявляться открытыми...
Такие поля называются общими (shared). Они идеально подходят для таких ситуаций, как в нашем призере с присвоением последовательных номеров. В классах также могут определяться общие свойства и методы. Недостаток заключается в том, что общие члены классов не могут работать с обычными полями, свойствами или методами. Иначе говоря, общие члены работают только с другими общими членами. Дело в том, что общие данные существуют еще до создания объекта, поэтому было бы нелогично разрешать общим членам доступ к конкретным объектам.
Ниже приведен фрагмент новой версии класса Employee с использованием общих данных для присвоения номеров. В классе определяется закрытая общая переменная типа Integer, которая:
Public Class Employee
Private m_Name As String
Private m_Salary As Decimal
Private Shared m_EmployeeID As Integer = 1
Public Sub New(ByVal theName As String. ByVal curSalary As Decimal)
m_Name = thename
m_Salary = curSalary
m_EmployeeID = m_EmployeeID + 1
End Sub
Readonly
Property Employeeld() As Integer
Get
Employeeld = m_EmployeeID
End Get
End Property
End Class
Ниже приведена небольшая программа для тестирования класса Empl oyee, а также полный код класса с общим полем:
Option Strict On Module Modulel
Sub Main()
Dim Tom As New Employee("Tom". 100000)
System.Console.WriteLine(Tom.TheName & "is employee! " & _
Tom. Employee ID & "with salary " & Tom.SalaryO)
Dim Sally As New Employee("Sally". 150000)
System.Console.WriteLine(Sally.TheName & "is employee!" & _
Sally.EmployeeID &"with salary "SSally.Salary())
System.Console.WriteLine("Please press the Enter key")
System.Console.Read()
End Sub
End Module
Public Class Employee
Private m_Name As 'String
Private m_Salary As Decimal
Private Shared m_EmployeeID As Integer = 1
Public Sub New(ByVal theName As String.ByVal curSalary As Decimal)
m_Name = thename
m_Salary = curSalary
m_EmployeeID = m_EmployeeID + 1
End Sub Readonly Property Employeeld()As Integer
Get
Employeeld = m_EmployeeID
End Get End Property Readonly
Property TheName() As String
Get
TheName = m_Name
End Get . End Property Readonly
Property Salary () As Decimal
Get
Salary = m_Sa1ary
End Get
End Property
End Class
Обращения к константам в классах не отличаются от обращений к общим полям, но при объявлении констант вместо Shared используется ключевое слово Const. Конечно, объявление открытых констант не приводит к нарушению инкапсуляции.
Не путайте общие данные со статическими. Общие данные существуют в одной копии для всех экземпляров класса, поэтому с точки зрения экземпляров они неявно обладают глобальной видимостью. Статическими называются переменные, состояние которых просто запоминается для повторного использования. Статическими могут быть объявлены как общие, так и обычные поля класса.
Определение классов в программе
От использования готовых классов .NET Framework мы переходим к определению собственных классов в программе. Код класса можно разместить в отдельном файле при помощи команды Project > Add Class, как в VB6, или же просто ввести его в нужном модуле — например, в стартовом модуле, содержащем точку входа в консольное приложение.В процессе тестирования мы предпочитаем связывать каждый класс с процедурой Sub Main, в которой он используется. Таким образом, код классов не оформляется в виде отдельных модулей классов, а выделяется в программный модуль с отдельной процедурой Sub Main, предназначенной для их тестирования. Если вы последуете нашему примеру, учтите, что код, определяемый на уровне модуля, доступен везде, где доступен сам модуль. Таким образом, мы создаем некий аналог глобальных переменных и функций VB .NET — со всеми опасностями, присущими глобальным данным.
VB .NET не смотрит на то, сколько классов определяется в одном файле. В большинстве классов определяются один или два конструктора, свойства для чтения и изменения состояния объекта, а также методы для выполняемых действий. Для примера возьмем простейший класс Empl oyee с двумя полями (имя и зарплата) и небольшую тестовую программу. В классе определяются два свойства, доступных только для чтения; эти свойства возвращают значения полей. Методы в этом классе отсутствуют:
1 Module EmployeeTestl
2 Sub Main()
3 Dim Tom As New Employee("Tom". 100000)
4 Console.WriteLine(Tom.TheName & "salary is " & Tom.Salary)
5 Console. ReadLine()
6 End Sub
7 ' Определение класса
8 Public Class Employee
9 Private m_Name As String
10 Private m_Salary As Decimal
11 Public Sub New(ByVa1 sName As String.ByVal curSalary As Decimal)
12 m_Name = Sname
13 m_Salary = curSalary
14 End Sub
15 Public Readonly Property TheName()As String
16 Get
17 Return m_Name
18 End Get
19 End Property
20 Public Readonly Property Salary() As Decimal
21 .Get .
22 Return m_Salary
23 End Get
24 End Property
25 End Class
26 End Module
В строках 2—6 определяется процедура Sub Main, используемая компилятором в качестве точки входа. Если эта процедура выбрана в качестве стартового объекта (это происходит по умолчанию, но вообще стартовый объект выбирается в диалоговом окне Project Properties), она отвечает за создание исходных экземпляров. Далее созданные объекты обычно создают другие объекты в ответ на получение ими сообщений. Конечно, в нашей простой программе ничего такого не происходит.
Непосредственное создание объекта происходит в строке 3, играющей ключевую роль в процессе тестирования программы. В этой строке при создании нового объекта Empl oyee методу New передаются два параметра — имя и начальная зарплата. В строке 4 мы выводим значения свойств TheName и Salагу, чтобы убедиться в том, что исходное состояние созданного объекта было задано верно.
Класс Empl oyee определяется в строках 8-25. Как упоминалось выше, для удобства тестирования код класса определяется в исходном модуле, хотя мы с таким же успехом могли воспользоваться командой Project > Add Class и выделить его в отдельный файл.
Давайте внимательно рассмотрим каждую строку в определении класса. В строке 8 ключевое слово Publiс является атрибутом уровня доступа и определяет, кому разрешено создавать экземпляры этого класса. В нашем примере класс объявлен открытым, поэтому теоретически любой желающий сможет создавать его экземпляры после компиляции — для этого в программу достаточно включить ссылку на сборку, содержащую этот класс (сборки рассматриваются в главе 13). Чтобы класс мог использоваться только в рамках нашего проекта и оставался недоступным для внешних программ, ключевое слово Public следует заменить ключевым словом Friend.
В строках 9 и 10 определяются закрытые поля для хранения информации о состоянии объекта. В очередной раз напомним, что переменные всегда должны объявляться закрытыми (Private). В своих определениях классов и модулей мы всегда начинаем имена полей с префикса m_ или m.
В строках 11-14 определяется конструктор, предназначенный для создания экземпляров класса. Конструктор задает значения закрытых полей экземпляра в соответствии со значениями полученных параметров.
В строках 15-19 и 20-24 определяются два открытых свойства, доступных только для чтения. Они предназначены для получения информации о текущем состоянии объекта. В приведенном примере использовано ключевое слово Return, однако с таким же успехом можно было применить старый синтаксис с присваиванием имени свойства:
Get
TheName = m_Name
End Get
Впрочем, даже в этой форме синтаксис процедуры свойства несколько изменился по сравнению с VB6 — исчезли старые конструкции Property Get/Property Set.
Следующая версия программы показывает, как сделать свойство Salary доступным для чтения и записи. Для этого достаточно удалить ключевое слово Readonly и добавить небольшой фрагмент:
Public Property Salary()As Decimal Get
Return m_Salary End Get Set(ByVal Value As Decimal)
m_Salary = Value
End Set
End Property
В этом фрагменте прежде всего следует обратить внимание на чтение нового значения свойства с применением ключевого слова Value. Иначе говоря, когда в программе встречается строка вида Tom.Salary = 125000, параметру Value автоматически присваивается значение 125 000.
Иногда встречаются ситуации, когда свойство требуется объявить доступным только для записи. Для этого перед именем свойства ставится ключевое слово WriteOnly, a затем определяется секция Set без секции Get.
Допустим, мы решили объявить свойство Salary доступным только для чтения и включить в класс метод для повышения зарплаты. Метод объявляется как обычная процедура или функция. В нашем примере метод не возвращает значения, поэтому выбор стоит остановить на процедуре:
Public Sub RaiseSalary(ByVal Percent As Decimal)
m_Salary =(1 + Percent) * m_salary
End Sub
Члены класса объявляются с модификаторами Public, Private или Friend. Ключевое слово Pri vate означает, что член класса используется только внутри класса.
В классах иногда объявляются закрытые конструкторы. «Какая польза от закрытого конструктора?» — спросите вы. Конструктор объявляется закрытым в том случае, если он вызывается только самим классом исходя из логики его работы.
По умолчанию в VB .NET для классов и их членов используется уровень доступа Friend (доступ разрешается только из текущей программы). Впрочем, пропускать атрибуты уровня доступа при объявлении класса не рекомендуется — особенно если учесть, что уровень доступа, принятый по умолчанию, не является минимальным.
Определение структур в программе
Определение структуры начинается с модификатора уровня доступа и ключевого слова Structure:Public Structure NameOfStructure
' Код структуры End Structure
Для каждого члена структуры должен быть указан модификатор доступа (например, Public или Private). Поля, объявленные с ключевым словом Dim вне процедур и функций, считаются открытыми. Ниже приведен простейший вариант структуры для работы с комплексными числами:
Public Structure ComplexNumber
Private m_real As Double
Private m_complex As Double
Public Property real () As Double Get
Return m_real
End Get Set(ByVal Value As Double)
m_real = Value
End Set
End Property
Public Property complex()As Double Get
Return m_complex End Get Set(ByVal Value As Double)
m_complex = Value
End Set
End Property
Public Sub New(ByVal x As Double. ByVal у As Double)
real = x complex = у
End Sub
Public Function Add(ByVal zl As ComplexNumber) As ComplexNumber
Dim z As ComplexNumber
z.real = Me.real + zl.real
Z.complex = Me.complex + zl.complex
Return z End Function
' И т.д. End Structure
Обратите внимание на возвращение структуры функцией Add. Кстати, поля структур не могут инициализироваться при объявлении:
Private m_real As Double = 0 ' Ошибка
Между структурами и ссылочными объектами существует еще одно принципиальное различие: использование открытых полей вместо свойств Get-Set в структурах широко распространено и не считается проявлением плохого стиля программирования, как для объектов. Это связано с тем, что поля экземпляров обычно относятся к базовым типам. Например, переопределение приведенной выше структуры ComplexNumber с открытыми полями Real и Imaginary не вызовет особых проблем.
Структуры создаются вызовом New или при присваивании значений их полям. Обращения к полям структур осуществляются так же, как и обращения к свойствам объектов. Ниже приведен пример использования структуры Compl exNumber:
Sub Main()
Dim Z1 As New ComplexNumber(2.3. 2.4)
Dim Z2.Z3 As ComplexNumber
Z2.real = 1.3
Z2.complex =1.4
Z3 = Zl.Add(Z2)
Console. WriteLine(Z3. real)
Console.ReadLine()
End Sub
Текущая версия VB .NET не позволяет переопределять смысл операторов (то есть про-изводить перегрузку операторов), поэтому нам пришлось определить метод Add вместо того, чтобы задать новое определение для оператора «+». Возможность перегрузки операторов должна появиться в будущих версиях VB .NET. Если вы хотите, чтобы в сегодняшней версии вашего пакета для работы с комплексными числами сложение выполнялось знаком «+», придется использовать С#.
Структуры могут содержать любые объекты VB .NET, в том числе другие структуры, перечисляемые типы, массивы и т. д. Таким образом, на VB .NET можно написать пакет для работы с матрицами, в котором основная структура данных будет определяться следующим образом:
Public Structure Matrix
Private TheOata(,) As Double
' И т.д. End Structure
Отладка объектно-ориентированных программ
Отладка объектно-ориентированных программ всегда начинается с анализа объектных переменных и проверки того, соответствует ли их состояние предполагаемому. Именно по этой причине в VS IDE предусмотрены средства для получения информации о закрытых полях ваших классов — окна просмотра (Watch) и локальных переменных (Locals). Применение этих средств отладки будет рассмотрено на простом примере. Допустим, мы решили перейти от связанного списка к двусвязному. Проще говоря, в каждом элементе должна храниться не одна ссылка, а две — на следующий и на предыдущий элемент списка, чтобы перебор мог осуществляться не только в прямом, но и в обратном направлении. Ниже приведен первый вариант класса двусвязного списка, содержащий ошибку. На этом примере будут продемонстрированы основные приемы отладки объектно-ориентированных программ:1 Option Strict On
2 Module Modulel
3 Sub Main()
4 Dim alinkList As New LinkedList("first link")
5 Dim aLink As LinkedList.Link
6 aLink = aLinklist.MakeLink(aLinkList.GetFirstLink, "second link")
7 aLink = aLinkList.MakeLinktaLink, "third link")
8 Console.WriteLine(aLinkList.GetFirstLink.MyData)
9 aLink = aLinkList.GetNextLink(aLinkList.GetFirstLink)
10 Console.Wri teLine(aLi nk.MyData)
11 Console.WriteLineCaLink.NextLink.MyData)
12 Console. ReadUne()
13 End Sub
14 Public Class LinkedList
15 Private m_CurrentLink As Link
16 Private nfFirstUnk As Link
17 Sub New(ByVal theData As String)
18 m_CurrentLink = New Link(theData)
19 m_FirstLink = m CurrentLink
20 End Sub
21 Public Function MakeLinktByVal currentLink As Link. ByVal _
22 theData As String) As Link
23 m_CurrentLink = New LinkCcurrentLink.theData)
24 Return m_CurrentLink
25 End Function
26 Public Readonly Property GetNextLink(ByVal aLink As Link)_
27 As Link
28 Get
29 Return aLink.NextLink()
30 End Get
31 End Property
32 Public Readonly Property GetCurrentLink() As Link
33 Get
34 Return m_CurrentLink
35 End Get
36 End Property
37 Public Readonly Property GetFirstLink() As Link
38 Get
39 Return m_FirstLink
40 End Get
41 End Property
42
43 ' Вложенный класс для ссылок
44 Friend Class Link
45 Private m_MyData As String
46 Private m_NextLink As Link
47' Private m_ParentLink As Link
48 Friend Sub New(ByVal myParent As Link. ByVal theData As String)
49 m_MyData = theData
50 m_Parentlink = Me
51 m_NextLink = myParent
52 End Sub
53 Friend Sub New(ByVal theData As String)
54 m_MyData = theData
55 End Sub
56 Friend Readonly Property MyData() As String
57 Get
58 Return m_MyData
59 End Get
60 End Property
61 Friend Readonly Property NextLink() As Link
62 Get
63 Return m_NextLink
64 End Get
65 End Property
66 End Class
67 End Class
68 End Module
Результат работы программы показан на Рисунок 4.11. Конечно, это совсем не то, что мы ожидали получить.
Отношения между классами в программах
В традиционном ООП предусмотрены три типа отношений между классами:Вернемся к классической тройке. Отношение использования, самое очевидное и распространенное, всего лишь означает, что один класс зависит от другого. Во всех ситуациях, когда один объект посылает сообщение другому объекту, можно говорить о зависимости между этими объектами. В обобщенном случае класс А использует класс Б, если:
Термин «включение» (агрегирование) означает, что объект класса А содержит внутренние объекты класса Б.
На базе включения реализуется методика делегирования, когда поставленная перед внешним объектом задача перепоручается внутреннему объекту, специализирующемуся на решении задач такого рода. Агрегирование с делегированием методов было очень распространенным явлением в прежних версиях VB, поскольку этот принцип использовался при создании новых элементов (вспомните, как создавались новые, специализированные текстовые поля — вы размещали текстовое иоле внутри формы пользовательского элемента, а затем запускали программу-мастер, которая автоматически генерировала код делегирования).
Агрегирование по-прежнему широко используется в VB .NET, но во многих ситуациях ему на смену приходит наследование — третий тип отношений между классами. Наследование считается одним из четырех «краеугольных камней» ООП наряду с абстракцией, инкапсуляцией и полиморфизмом. Все четыре концепции будут рассмотрены в ближайших четырех разделах.
Параметризованные конструкторы

На первый взгляд конструктор New работает так же, как в предыдущих версиях VB. В действительности изменилось очень многое, и самое принципиальное изменение заключается в том, что при вызове New теперь могут передаваться параметры. Как вы вскоре увидите, в пользовательских классах переопределенная версия New замещает событие Initial ize из прежних версий VB, которое не поддерживало параметров.
Например, для класса Random определены две версии конструктора. Первая версия вызывается без параметров, как показано выше. В этом случае вы получаете случайные числа, полученные в результате случайной инициализации генератора по показаниям системных часов. Другая версия выглядит так:
Dim aRandomlnstance As Random
aRandomlnstance = New Random(42)
Эта версия класса Random генерирует одну и ту же последовательность случайных чисел, начинающуюся с числа 42 (эта возможность абсолютно необходима в процессе отладки).
Как ни странно, появление параметризованных конструкторов в VB сделало для полноценной реализации ООП едва ли не больше, чем поддержка наследования. Если наследование еще можно заменить в программе другими средствами (обычно агрегированием), то компенсировать отсутствие параметризованных конструкторов гораздо труднее. Параметризованные конструкторы нужны прежде всего для того, чтобы предотвратить случайное создание объекта в неопределенном состоянии. В прежних версиях VB это всегда порождало массу проблем, поскольку событие Initialize вызывалось без параметров. Оставалось лишь следовать общепринятой схеме — включать в класс функцию инициализации объектов (обычно этой функции присваивалось имя Create) и надеяться на то, что пользовать класса не забудет вызвать эту функцию. В противном случае объект не инициализировался, а поля экземпляра сохраняли значения по умолчанию, что приводило к появлению тонких, неуловимых ошибок.
В VB .NET, как во всех объектно-ориентированных языках, объект создается только конструктором. Более того, ниже будет показано, как потребовать обязательной передачи параметров при вызове конструктора — это гарантирует, что объект не будет создан в неопределенном состоянии.
Определение нескольких версий одной функции, различающихся только типом пара-метров, называется перегрузкой (overloading). Как будет показано ниже, в VB .NET перегрузка поддерживается не только для конструкторов New, но для любых функций и процедур. Перегрузка также используется для решения проблемы с передачей необязательных параметров.
Перечисляемые типы
Перечисляемые типы обычно используются для определения набора именованных целочисленных констант. При определении перечисляемого типа используется пара ключевых слов Enum-End Enum вместе с модификатором доступа. Перечисляемый тип может содержать только целочисленные типы вроде Integer или Long (тип Char недопустим). Например, в следующем фрагменте определяется открытый перечисляемый тип с именем BonusStructure:Public Enum BonusStructure
None = 0
FirstLevel = 1
SecondLevel = 2
End Enum
После этого в любом месте программы можно объявить переменную типа BonusStructure: Dim bonusLevel As BonusStructure
При работе с перечисляемыми типами, как и с другими структурными типами, ключевое слово New не используется.
Если в перечисляемом типе указаны только имена без числовых значений, .NET начи-нает отсчет с 0 и увеличивает значение на 1 для каждой новой константы. Если задано только первое число, то каждое следующее значение вычисляется увеличением предыдущего на 1.
Определив в проекте перечисляемый тип, вы можете использовать конструкции вида
Bonus =Tom.Sales * bonusLevel.SecondLevel
Поскольку перечисляемые типы неявно интерпретируются как общие, в ссылках на них можно указывать имя перечисляемого типа вместо имени переменной:
Public Function Calcu1ateBonus(ByVal theSales As Decimal) As Decimal
Return theSales * BonusStructure.SecondLevel
End Function
Одним из традиционных недостатков перечисляемых типов было отсутствие удобных средств для получения имени по значению, что затрудняло отладку программ. В классе Enum, базовом для всех перечисляемых типов, определены очень полезные методы для получения подобной информации. Например, следующая команда возвращает строку FirstLevel :
BonusStructure.GetName(bonusLevel .GetType.l)
Данный фрагмент выводит все имена, входящие в перечисляемый тип:
Dim enumNames As String().s As String
enumNames = BonusStructure.GetNames(bonusLevel.GetType)
For Eachs In enumNames
System.Console.WriteLine(s) Next
Перегрузка членов класса
Метод RaiseSalary класса Employee можно сделать и поинтереснее. Предположим, повышения зарплаты до 10% происходят автоматически, но для больших сумм требуется специальный пароль. В прежних версиях VB такие задачи решались при помощи необязательных параметров. Хотя эта возможность сохранилась и в VB .NET, существует более изящное решение с определением двух версий RaiseSalary. Используя возможность перегрузки методов, мы определяем два разных метода для разных случаев.В VB .NET синтаксис перегрузки методов очень прост: для этого в программе просто определяются два метода с одинаковыми именами и разными параметрами. Тем не менее мы настоятельно рекомендуем использовать ключевое слово Over! oads. По нему пользователи вашего кода узнают о том, что метод перегружается намеренно, а не в результате ошибки. В следующем фрагменте приведены две версии метода RaiseSalary, о которых говорилось выше:
Public Overloads Sub RaiseSalary(ByVal Percent As Decimal)
If Percent > 0.1 Then
' Операция запрещена - необходим пароль
Console.WhteLineC'MUST HAVE PASSWORD TO RAISE SALARY " & _
"MORE THAN 10*!!!!") Else X
m_Salary =(1 + Percent) * m_salary End If End Sub
Public Overloads
Sub RaiseSalary(ByVal Percent As Decimal._
ByVal Password As Stqng)
If Password -"special Then
m_Salary = (1 + Percent) * m_Salary
End If End Sub
При перегрузке методы класса различаются только по типам параметров. Методы не могут перегружаться по типу возвращаемого значения или уровню доступа.
Ниже приведен пример класса Empl oyee с перегруженным методом Rai seSalany, а также небольшая тестовая программа. Обратите внимание: 10%-ный порог не кодируется в программе, а определяется в виде константы:
Option Strict On Module Modulel Sub Main()
Dim Tom As New Employee("Tom". 100000)
Console.WhteLineCTom.TheName & " has salary " & Tom.Salary)
Tom.RaiseSalary(0.2D)
' Суффикс D - признак типа Decimal
Console.WriteLine(Tom.TheName & " still has salary " & Tom.Salary)
Console. WhteLine()
Dim Sally As New Employee("Sally", 150000)
Console.WriteLine(Sally.TheName & " has salary " & Sally.Salary)
Sally.RaiseSalary(0.2D,"special")
' Суффикс D - признак типа Decimal
Console.WriteLine(Sally.TheName & "has salary "SSally.Salary)
Console. WriteLine()
Console.WriteLine("Please press the Enter key")
Console. ReadLine()
End Sub
End Module
Public Class Employee
Private m_Name As String
Private m_Salary As Decimal
Private Const LIMIT As Decimal = 0.1D
Public Sub New(ByVal theName As String,ByVal curSalary As Decimal)
m_Name = thename
m_Salary = curSalary
End Sub
Readonly Property TheName()As String Get
Return m_Name
End Get '
End Property
Readonly Property Salary()As Decimal Get
Return m_Salary
End Get
End Property
Public Overloads
Sub RaiseSalary(ByVal Percent As Decimal)
If Percent > LIMIT Then
' Операция запрещена - необходим пароль
Console.WriteLine("MUST HAVE PASSWORD TO RAISE SALARY " & _
"MORE THAN LIMIT!!!!")
Else
m_Salary =(1 +Percent)*m_salary End If End Sub
Public Overloads
Sub RaiseSalary(ByVal Percent As Decimal._
ByVal Password As String)
If Password = "special" Then
m_Salary =(1 + Percent) * m_Salary
End If
End Sub
End Class
Переход к использованию объектов
С давних времен в программировании использовалась структурная, процедурно-ориентированная модель. Сначала программист разбирался, что должна делать программа, а затем выбирал одно из двух:Между ООП и процедурно-ориентированным программированием существуют два важных различия:
Сейчас стоит повторить золотое правило программирования, нисколько не изменившееся с переходом на ООП: будьте проще. Использование простых классов заметно упрощает объектно-ориентированное программирование. Класс с простой внутренней структурой и небольшим числом внешних связей проще понять, а следовательно, и запрограммировать.
Описание логических связей между классами играет в ООП настолько важную роль, что появилась целая наука о построении диаграмм, иллюстрирующих отношения между классами. Чаще всего для описания логических связей применяется язык UML (Uniform Model Language). Средства построения диаграмм входят во многие системы автоматизированной разработки программ — такие, как Microsoft Visual Modeler и Visio, а также Rational Rose компании Rational Software (Visual Modeler входит в некоторые версии VS .NET).
Некоторые пакеты на основании диаграммы автоматически генерируют базовый код классов. За общими сведениями о UML мы рекомендуем обращаться на web-сайт Rational (www.rational.com/uml).
Полиморфизм
В традиционной трактовке термин «полиморфизм» (от греческого «много форм») означает, что объекты производных классов выбирают используемую версию метода в зависимости от своего положения в иерархии наследования. Например, и в базовом классе Employee, и в производном классе Manager присутствует метод для повышения зарплаты работника. Тем не менее метод RaiseSalаrу для объектов класса Manager работает не так, как одноименный метод базового объекта Employee.Классическое проявление полиморфизма при работе с классом Manager, производным от Empl oyee, заключается в том, что при вызове метода по ссылке на Empl oyee будет автоматически выбрана нужная версия метода (базового или производного класса). Допустим, в программе метод RaiseSalary вызывается по ссылке на Employee.
В обоих случаях объект выбирает метод в зависимости от полученного сообщения. При отправке сообщения не нужно знать, к какому классу фактически принадлежит объект; достаточно разослать сообщение всем объектам Employee и поручить выбор полиморфного метода компилятору.
Следующий пример показывает, почему полиморфизму придается такое большое значение. Одному из авторов доводилось консультировать компанию, занимавшуюся компьютерной обработкой медицинских анализов. Каждый раз, когда в процесс тестирования включался новый реактив, программистам приходилось просматривать многие тысячи строк кода, искать команды Select Case и добавлять в них секции Case для нового реактива. Стоило пропустить хотя бы одну... и нам бы не хотелось, чтобы этот реактив испытывался на наших анализах крови. Конечно, исправление многих команд Select Case превращало сопровождение в настоящий кошмар, требующий долгих часов тестирования.
Полиморфизм позволяет написать программу, которая в подобной ситуации ограничивается единственным изменением. Все, что от вас потребуется, — определить для нового реактива новый класс и правильно запрограммировать в нем переопределяемые или добавленные методы.
Почему? Потому что в главной программе можно будет использовать конструкции следующего вида:
For Each reagent in Reagents
reagent.Method
Next
Показанный цикл будет автоматически работать с новым реактивом, а необходимость в долгих поисках Sel ect Case отпадет.
Select Case reagent Case iodine
' Действия с йодом Case benzene
' Действия с бензолом
' И т. д. для 100 разных случаев в 100 местах
В приведенном выше фрагменте цикл For Each перебирает все возможные реактивы, и благодаря волшебному свойству полиморфизма компилятор найдет метод, который должен вызываться для каждого конкретного реактива. Правильное использование полиморфизма избавит вас от громоздких команд Select Case, выбирающих нужное действие в зависимости от типа объекта.
Практическое использование вложенных классов на примере связанного списка
Вложенные классы чаще всего применяются в реализациях различных структур данных. К числу самых распространенных структур данных принадлежит связанный список. Он представляет собой цепочку ссылок, которая позволяет легко переходить от текущего объекта к следующему, однако поиск всегда начинается с конкретной ссылки. Применение вложенных классов в реализации связанного списка выглядит вполне естественно, поскольку код объектов-ссылок не представляет интереса для пользователей класса LinkedList, а объекты Link не могут существовать независимо от содержащего их объекта LinkedList.Ниже приведена очень простая реализация класса для работы со связанными списками. Просмотрите ее, а затем мы подробно проанализируем листинг. Обратите внимание на важную строку, выделенную жирным шрифтом (строка 49); в ней используется нетривиальная особенность объектно-ориентированного программирования, о которой будет рассказано ниже.
1 Option Strict On
2 Module Modulel 3 Sub Main()
4 Dim aLinkedList As New LinkedlistC'first link")
5 Dim aALink As LinkedList.Link
6 aLink = aLinkedList.MakeLink(aLinkedList.GetFirstLink,"second link")
7 aLink = aLinkedList.MakeLink(aLink,"third link")
8 Console.WriteLine(aLinkedList.GetFirstLink.MyData)
9 aLink = aLinkedList.GetNextLink(aLinkedList.GetFirstLink)
10 Console.WriteLine(aLink.MyData)
11 Console.WriteLine(aLink.NextLink.MyData)
12 Console. ReadLine()
13 End Sub
14 Public Class LinkedList
15 Private m_CurrentLink As Link
16 Private m_FirstLink As Link
17 Sub New(ByVal theData As String)
18 m_CurrentLink = New Link(theData)
19 m_FirstLink = in_CurrentLink
20 End Sub
21 Public Function MakeLink(ByVal currentLink As Link.ByVal
22 theData As String) As Link
23 m_CurrentLink =New Link(currentLink.theData)
24 Return m_CurrentLink
25 End Function
26 Public Readonly Property GetNextLink(ByVal aLink As Link)_
27 As Link
28 Get
29 Return aLink.NextLink()
30 End Get
31 End Property
32 Public Readonly Property GetCurrentLink()As Link
33 Get
34 Return m_CurrentLink
35 End Get
36 End Property
37 Public Readonly Property GetFirstUnkOAs Link
38 Get
39 Return m_FirstLink
40 End Get
41 End Property
42
43 ' Вложенный класс для ссылок
44 Friend Class Link
45 Private m_MyData As String
46 Private m_NextLink As Link
47 Friend Sub New(ByVal myParent As Link.ByVal theData As String)
48 m_MyData - theData
49 myParent.m_NextLink = Me
50 ' End Sub
51 Friend Sub New(ByVal theData As String)
52 m_MyData =theData
53 End Sub
54 Friend Readonly Property MyData()As String
55 Get
56 Return m_MyData
57 End Get
58 End Property
59 Friend Readonly Property NextLink()As Link
60 Get
61 Return m_NextLink
62 End Get
63 End Property
64 End Class
65 End Class
66 End Module
Строка 4 создает новый экземпляр связанного списка. В строке 5 определяется объектная переменная типа Link. Поскольку класс Link является вложенным по отношению к LinkedList, его тип записывается в виде «полного имени» LinkedList.Link. Строки 6-12 содержат небольшую тестовую программу.
В строках 17-20 определяется конструктор класса LinkedList, в котором вызывается второй конструктор класса Link (строки 51-53). Последний объявлен с атрибутом Friend и потому доступен для внешнего класса Li nkedLi st. Если бы конструктор Link был объявлен с атрибутом Private, то он стал б"ы недоступным для внешнего класса.
Также стоит обратить внимание на то, как в первом конструкторе класса Link (строки 47-50) организуется ссылка на только что созданный элемент списка из предыдущего элемента. Для этого используется ключевое слово Me — это очень принципиальный момент, поэтому строка 49 выделена в листинге жирным шрифтом. На первый взгляд команда myParent.m_NextLink = Me выглядит недопустимой, поскольку мы обращаемся к закрытому полю родительского класса myParent. Однако программа все-таки работает! Итак, запомните очень важное правило:
Для экземпляра класса всегда доступны закрытые поля других экземпляров этого класса.
При написании подобных классов в VB .NET можно обойтись и без использования этой нетривиальной особенности классов. Например, в класс Link можно включить специальный метод для создания ссылки на следующий элемент списка. В конечном счете выбор зависит только от вашего стиля программирования. Тем не менее сама возможность обращения к закрытым членам класса может преподнести неприятные сюрпризы, и об этом необходимо знать. По этой причине в нашем примере продемонстрирован именно такой подход.
Преимущества ООП
На первый взгляд классы ООП и связанные с ними методы и свойства имеют много общего с процедурным подходом и модульным строением программ. Ключевое различие заключается в следующем:Класс представляет собой шаблон для создания объектов, состояние которых изменяется со временем.
Выглядит слишком абстрактно? И вроде бы не имеет никакого отношения к программированию VB? Вспомните панель элементов Visual Basic. В прежних версиях VB каждая кнопка панели создавала объект, являющийся экземпляром класса соответствующего элемента.
А если бы панель элементов, готовая в любой момент создать новое текстовое поле или кнопку по вашему запросу, куда-то исчезла? Только представьте, какими сложными станут программы VB, если каждое текстовое поле придется оформлять в виде отдельного модуля! Кстати говоря, один модуль нельзя подключить к программе дважды, поэтому создание формы с двумя одинаковыми текстовыми полями потребует довольно изощренного программирования.
Благодаря существованию панели элементов VB всегда был объектно-ориентированным языком. Начиная с версии 4 в нем появилась возможность создавать некоторые типы объектов. Но только в VB .NET программист может определять классы для любых объектов и в полной мере использовать средства ООП на том же уровне, что и в C++ и С#. Более того, все языки .NET обеспечивают примерно равную эффективность п$и работе с классами.
класс String
Другим хорошим примером класса с несколькими конструкторами является класс String. Хотя для конструирования строк предусмотрена сокращенная запись (последовательность символов, заключенная в кавычки), в более сложных случаях лучше перейти на использование конструкторов. Например, один из конструкторов создает строку, состоящую из нескольких копий одного символа. Следующая команда создает строку из 37 пробелов:Dim str As String = New String((CChar(" "), 37)
В данном случае вместо конструктора можно воспользоваться функцией Space (учтите, что в режиме жесткой проверки типов Option Strict On строка, состоящая из одного символа «пробел», должна преобразовываться в символ специальной функцией).
Обобщенная форма этого конструктора выглядит так:
New (ByVal с as Char, ByVal count As Integer)
При работе с классом Stri ng также часто используется конструктор New(ByVal val ue() As Char, который получает массив символов и преобразует их в строку.
Поскольку строковые переменные в VB .NET стали объектными, при вводе «.» после имени строковой переменной появляется подсказка IntelliSense со списком членов класса String.
класс StringBuilder
Чрезвычайно полезный класс StringBuilder входит в пространство имен System.Text (пространства имен рассматриваются в следующем разделе). Этот класс следует использовать вместо обычного класса String в тех случаях, когда вы собираетесь внести постоянные изменения в строку. Дело в том, что при каждом изменении строки (даже при простом добавлении нового символа) VB .NET приходится создавать новую строку, а эта операция требует времени. При работе с экземпляром класса StringBuilder VB .NET обходится модификацией исходного объекта.При создании пустого объекта Stri ngBui I der методом New VB .NET резервирует блок памяти для 16 символов и автоматически наращивает его при включении новых символов. Объект StringBuilder можно рассматривать как «интеллектуальный» массив символов, который увеличивается и уменьшается по мере надобности и поэтому в каком-то смысле напоминает тип Stri ng в V-B6. Текущий размер объекта Stri ngBui I der называется вместимостью (capacity). В классе Stri ngBui I der определены шесть конструкторов, перечисленных в табл. 4.1.
Проблемы с передачей объектных переменных по значению
Большинство языков программирования требует четкого понимания, чем передача параметров по ссылке отличается от передачи по значению. Не забывайте, что в VB .NET параметры по умолчанию передаются по значению (ByVal).Большинство программистов руководствуется простым правилом: если параметр передавался по ссылке, его изменения сохраняются в исходной переменной, а если по значению — изменения теряются после выхода из функции или процедуры. К сожалению, в случае с объектными переменными это правило не всегда истинно. Попробуйте выполнить следующий фрагмент, в котором массив передается в процедуру по значению. Вы убедитесь в том, что исходный массив изменяется после вызова процедуры!
Module Modulel Sub Main()
Dim a() As String ={"HELLO"."GOODBYE"}
Console.WriteLineC'Original first item in array is:" & a(0))
Console.WriteLineC'Original second item in array is:" & a(1))
Yikes(a) ' Массив передается по значению!
Console.WriteLineC'After passing by value first item in array now is:"_
&A(0))
Console.WriteLine("After passing by value second item in array is:"_
&АШ)
Console. ReadLine()
End Sub
Sub Yikes(ByVal Foo As String())
Foo(0) = "GOODBYE"
Food) = "HELLO"
End Sub
End Module
Пространства имен для создаваемых классов
Классы, перечисляемые типы, структуры или модули включаются в пространства имен. Конечно, создать экземпляр модуля невозможно — только экземпляры классов, определяемых в модуле. В диалоговом окне Project Properties, показанном на Рисунок 4.9, присутствуют текстовые поля для имени сборки и корневого пространства имен,На Рисунок 4.9 указано корневое пространство имен Apress. При объявлении пространства имен в программе можно использовать иерархию произвольной глубины, отражающую логическую структуру нашей программы. Рассмотрим пример определения класса:
Namespace Cornell.Morrison.VB.NET.CH4
Module Module1
Sub Main()
Console.filriteLine("test code goes here")
End Sub
Public Class"EmployeeExamplel
' Код класса End Class
End Module
End Namespace
Пространства имен
При использовании кода, написанного другими программистами, всегда возникает потенциальная опасность конфликтов имен. Но даже если забыть об этих конфликтах, все равно необходимо каким-то образом сгруппировать тысячи методов в соответствии с их функциональным назначением. Следовательно, в огромной библиотеке классов .NET должен существовать какой-либо способ логической группировки информации. В .NET эта задача решается при помощи пространств имен. Конечно, вы можете создавать собственные пространства имен; ниже в этой главе мы покажем, как это делается.В каждом городе есть своя Главная Улица, а в каждой библиотеке непременно найдется метод с именем Open. Пространства имен позволяют различать эти методы. Например, в пространстве имен System. 10 собраны методы для выполнения файловых операций; в него входит класс Fil е, а в этом классе имеется метод Open. Полное имя метода выглядит так:
System.I0.File.Open
Класс File является частью пространства имен System. I0, поэтому он не конфликтует с другим классом File из пространства имен Cornell .Morrison.NiftyClasses, который также может содержать собственный метод Open.
Пространство имен и имя сборки

Теперь, когда вы знаете, как определять собственные классы, вам будет проще работать с окном классов, в котором члены классов вашего решения отображаются в виде удобного иерархического дерева. Окно классов помогает ориентироваться в коде вашего решения: при двойном щелчке в одной из строк окна классов в окне программы автоматически открывается код соответствующего члена. Окно классов открывается командой View > Class View или комбинацией клавиш Ctrl+Shift+C. На Рисунок 4.10 показано, как выглядит окно классов для одной из версий нашего класса Employee.
Рисунок 4.10. Окно классов для класса Employee
В левом верхнем углу окна расположена пара кнопок. Кнопка New Folder создает новую папку, но чаще используется кнопка Class View Sort By Type. Она открывает список, в котором выбирается режим представления информации в окне.
Результат работы программы с

Глава получилась очень длинной. В ней вы познакомились с некоторыми встроенными классами .NET Framework, но главной темой была специфика работы с объектами в VB .NET. По сравнению с прежними версиями VB в этой области произошло много изменений, в основном принципиальных. В частности, были рассмотрены параметризованные конструкторы, значительно повышающие надежность создания объектов и их инициализацию. Короче говоря, в этой главе был изложен базовый материал, абсолютно необходимый для дальнейшего освоения VB .NET.
Снова о конструкторах
Если в вашем классе не определен конструктор, VB .NET автоматически генерирует для него конструктор, вызываемый без аргументов. Работа этого конструктора сводится к инициализации всех полей экземпляра значениями по умолчанию. Такой конструктор называется конструктором по умолчанию или безаргумеитнъм конструктором. Если в классе определен хотя бы один пользовательский конструктор, VB .NET не станет генерировать конструктор по умолчанию.Ничто не мешает вам определить в классе несколько конструкторов с разными уровнями доступа. Например, можно определить абсолютно безопасный конструктор с атрибутом Public и конструктор с атрибутом Friend, использование которого сопряжено с чуть большим риском. Конечно, эти конструкторы должны вызываться с разными параметрами, поскольку VB .NET различает методы по списку параметров, а не по модификаторам уровня доступа.
Конструкторы перегружаются, как и остальные методы, однако при этом нельзя использовать ключевое слово Overloads. Ниже приведен фрагмент обновленной версии класса Employee с конструктором, позволяющим задать значение нового поля.
Public Class Employee
Private m_Name As String
Private m_NickName As String
Private m_Salary As Decimal
Public Sub NewCByVal sName As String.ByVal curSalary As Decimal)
m_Name = sName
m_Salary = curSalary
End Sub
Public SubNewCByVal theName As String.ByVal nickName As String._
ByVal curSalary As Decimal)
m_Name = theName
m_NickName = nickName
m_Salary = curSalary
End Sub
Компилятор выбирает вторую версию конструктора лишь в том случае, если при вызове передаются два строковых параметра и один числовой. При передаче одной строки и числа выбирается первый конструктор.
Перегрузка конструкторов приводит к дублированию кода в программе. Так, в приведенном выше фрагменте значения m_Name и fli_Sa,lary присваивались в обоих конструкторах. В VB .NET для таких ситуаций предусмотрена специальная сокращенная запись: конструкция MyClass.New вызывает другой конструктор класса [ На момент написания книги также можно было воспользоваться ключевым словом Me, но вариант с MyClass является предпочтительным. ]. Пример:
Public Sub New(ByVal sName As String.ByVal curSalary As Decimal)
m_Name = Sname
mJSalary = curSalary End Sub
Public Sub New(ByVal sName As String, ByVal nickName As String._ ByVal curSalary As Decimal)
MyClass.Newt sName.curSalary)
m_NickName =nickName
End Sub
При вызове другого конструктора конструкцией MyClass. New порядок определения конструкторов в программе не важен. VB .NET выбирает конструктор по типу переданных параметров независимо от его места в определении класса.
Помните, что MyClass — ключевое слово, а не объект. Значение MyCLass нельзя присвоить переменной, передать процедуре или использовать в операторе Is. В подобных ситуациях используется ключевое слово Me; оно обозначает конкретный объект, код которого выполняется в настоящий момент.
Снова о свойствах
Принципиальное различие в работе свойств VB6 и VB .NET заключается в том, что секции Get и Set теперь должны обладать одинаковым уровнем доступа. Определять свойства с секциями Public Get и Private Set в VB .NET не разрешается.Это ограничение легко обходится. Чтобы процедура Set фактически стала закрытой, объявите свойство с атрибутами Public Readonly и одновременно объявите другое, внутреннее закрытое свойство для Set.
Кроме того, в VB6 свойство не могло изменяться в процедуре, даже если оно было передано по ссылке (то есть с ключевым словом ByRef). В VB .NET свойства, переданные по ссылке, могут изменяться.
Но самое принципиальное изменение относится к свойствам по умолчанию. В прежних версиях VB существовала концепция свойств по умолчанию, которая на первый взгляд казалась очень удобной. На практике свойства по умолчанию часто становились причиной ошибок в программах. Например, что означает следующая команда?
Me.Text1 = Text2
Отслеживая ошибки, возникающие в подобных командах, опытные пользователи VB выясняли, что эта команда задает свойству Text текстового поля с именем Textl значение переменной Text2. Свойства по умолчанию не только становились источником ошибок в программах, но и требовали, чтобы при присваивании объектов использовалось ключевое слово Set, поскольку присваивание объектам нужно было отличать от присваивания свойствам. В VB .NET проблема свойств по умолчанию решается просто — они разрешены только там, где это действительно оправдано, а именно при использовании параметров. Допустим, у вас имеется кэш-таблица aTable; при выборке значений было бы удобно использовать синтаксис вида aTable("theKey"), но это возможно лишь в том случае, если Item является свойством по умолчанию для класса HashTable. Свойства по умолчанию объявляются в классе с ключевым словом Default, причем это допускается лишь для свойств, получающих минимум один параметр. Если свойство по умолчанию перегружается, все перегруженные версии также помечаются ключевым словом Default. Свойства по умолчанию чаще всего используются в ситуации, когда у объекта имеется свойство, значение которого возвращается в виде массива или другого объекта, способного вмещать несколько величин (например, хэш-таблицы). Предположим, у вас имеется класс Sal es и свойство InYear, которое по полученному индексу возвращает число (объем продаж):
Public Class Sales
Private m_Sales() As
Decimal = {100, 200. 300}
Default Public Property InYear(ByVal theYear As Integer) As Decimal
Get
Return m_Sales(theYear)
End Get
Set(ByVa1 Value As Decimal)
m_Sales(theYear)=Value
End Set
End Property
' Остальной код класса End Class
Свойство по умолчанию позволяет использовать конструкции вида
Dim ourSales As New Sales()
Console.WriteLine(ourSa1es(1))
вместо
Dim ourSales As New Sales()
Console.WriteLi ne(ourSales.InYear(1))
Или, например, вы можете написать
ourSales (2) = 3000
вместо
ourSales.InYear(2) = 3000
Ключевое слово Set используется в процедурах свойств VB NET.
Создание объектов в VB .NET
В VB .NET, как и в прежних версиях VB, объекты создаются ключевым словом New (исключение составляют строки и массивы — для создания этих объектов предусмотрена сокращенная запись).Рассмотрим практический пример — в .NET Framework входит полезный класс Random для работы со случайными числами. По своим возможностям этот класс превосходит функцию Rnd, сохраненную в языке для обеспечения обратной совместимости. Например, класс Random позволяет заполнить байтовый массив случайными числами от 0 до 255 или сгенерировать положительное случайное число в заданном интервале. Однако Random — не функция, а класс, методы которого вызываются с указанием конкретного экземпляра. А для этого необходимо предварительно создать экземпляр (проще говоря, объект) класса Random.
Сделать это можно несколькими способами, и в любом варианте используется ключевое слово New. Самый наглядный, хотя и не самый компактный способ заключается в отделении объявления класса от вызова New:
Dim aRandomlnstance As Random
' Объявление aRandomlnstance = New Random()
' Создание экземпляра
Многие программисты предпочитают использовать сокращенную запись:
Dim aRandomlnstance As New Random
' Экземпляр создается при объявлении
Эта команда эквивалентна приведенному выше фрагменту; в ней используется такая возможность VB .NET, как инициализация переменных при объявлении.
На языке ООП метод New называется конструктором, поскольку он предназначен для создания (конструирования) экземпляров класса.
Программисты, работавшие с предыдущими версиям VB, должны обратить внимание на следующее: в VB .NET не поддерживается ключевое слово Set (некоторые побочные эффекты его исчезновения описаны в разделе «Свойства» настоящей главы). Два варианта синтаксиса New различаются только реакцией на исключения, возникающие при создании объектов (см. главу 7).
В прежних версиях VB между полной и сокращенной формой вызова конструктора существовали тонкие различия, связанные с тем, что при сокращенной записи создание объекта откладывалось до момента первого использования. В VB .NET эта особенность была исключена.
Некоторые программисты (особенно работающие на С# и Java) предпочитают третий вариант синтаксиса, который выглядит как комбинация первых двух:
Dim foo As Random = New Random()
' В стиле C#/Java
Он ничем не отличается от второго варианта синтаксиса.
Метод New позволяет конструировать объекты в любом выражении VB .NET, если результат соответствует контексту. Следующая команда VB .NET вполне допустима (хотя понять ее непросто, поэтому использовать подобный стиль программирования не рекомендуется):
Consolе.WriteLi net New Random().Next())
Впрочем, подобные конструкции могут встретиться в чужих программах, которые вам придется сопровождать. Особенно часто они используются программистами с опытом работы на C++/Java.
Создав экземпляр класса Random, вы можете пользоваться его методами и свойствами при помощи знакомого «точечного» синтаксиса. Библиотека .NET Framework содержит множество классов; технология IntelliSense всегда напомнит вам, что можно сделать с тем или иным экземпляром класса (Рисунок 4.1).
Справочная система и .NET Framework
В библиотеку .NET Framework входят сотни пространств имен, каждое из которых содержит множество полезных классов. По масштабам и возможностям .NET Framework сравнима с полным интерфейсом Win32 API. Библиотека настолько огромна, что описать ее в одной книге попросту невозможно. Хотя эта глава дает начальное представление о некоторых классах .NET Framework, как можно скорее приступайте к чтению документации .NET. Начните с раздела «.NET Framework Class Library» и найдите описания пространств имен, представляющих для вас интерес. Как показано на Рисунок 4.4, в справочной системе перечислены все классы каждого пространства имен.Каждое имя класса в левом столбце представляет собой гиперссылку, ведущую к подробному описанию класса. В нижней части описания класса перечислены имена его членов. Если щелкнуть на любом из этих имен, вы перейдете к подробному описанию соответствующего члена. Обратите внимание: VB .NET уже не считается второстепенным языком — синтаксис всех членов приводится для VB, VC и С#. На Рисунок 4.5 показан пример документации класса Directorylnfo в бета-версии 2.
Чтобы получить подробное описание метода GetDi rectories, использованного в предыдущем примере, щелкните на ссылке Directorylnfo в нижней части страницы, а затем щелкните на ссылке GetDi rectories. Внешний вид страницы показан на Рисунок 4.6. Некоторые термины, встречающиеся на этой странице (такие, как Pri vate), рассматриваются далее в этой главе,
Страница Imports окна свойств

Чтобы рассмотреть пример использования Imports на сколько-нибудь нетривиальном примере, мы возьмем класс Directorylnfo из пространства имен System. IO. Как подсказывает само название, класс Directorylnfo содержит методы для получения информации о содержимом каталогов, вывода полного имени каталога и т. д. Один из конструкторов этого класса получает строку с именем каталога, который вы хотите проанализировать (если переданное имя не является абсолютным, конструктор считает, что оно задается относительно текущего каталога программы). Правильно написанная команда Imports позволяет заменить длинную команду Dim dirlnfo As New System.IO.Directory!nfo("C:\") более компактной и понятной командой
Dim dirlnfo As New DirectoryInfo("C:\")
Следующая программа выводит список всех каталогов на жестком диске, в ее работе используются рекурсия и класс Directorylnfo. Ключевую роль в ней играет метод GetDi rectories (), возвращающий коллекцию подкаталогов. Функция ListDi rectories перебирает содержимое коллекции и рекурсивно вызывается для каждого элемента:
Option Strict On Imports System.IO Module Modulel
Sub Main())
Dim dirlnfo As New DirectoryInfo("C:\")
ListDirectories(dirInfo)
End Sub
Sub ListDirectories(ByVal theDirectory
As Directorylnfo)
Dim tempDir*As DirectoryInfo
Console. Wri.teLi net theDi rectory .Full Name())
For Each terrain In theDi rectory. GetDi rectories ()
ListDirectories(tempOir) Next End Sub End Module
Если вы привыкли к рекурсивному перебору каталогов с использованием старой функции Dir, вы оцените, насколько упростил эту программу в .NET замечательный метод Directorylnfo.
Во время работы над этим примером мы легкомысленно назвали свое решение D1-rectorylnfo. В результате команда Imports перестала работать! Причины так и остались неизвестными, но мораль ясна: не присваивайте своим решениям имена, совпадающие с именами классов библиотеки .NET.
Структуры
Некоторые полагают, что структуры VB .NET аналогичны пользовательским типам прежних версий VB или многих других языков программирования. Конечно, структуры VB .NET могут использоваться как пользовательские типы, но этим область их возможного применения не исчерпана. Структура может обладать всеми признаками традиционного класса, включая конструкторы и члены с атрибутами Private/Friend/Public. Единственное отличие структур от обычных объектов заключается в том, что структуры обладают структурной семантикой. Вспомните, какой смысл вкладывается в этот термин:Некоторые программисты используют структуры чаще, чем следует, полагая, что структура как1 облегченный объект работает эффективнее, чем объекты обычных классов. К сожалению, этот подход не лишен недостатков: два объекта, обладающие одинаковым состоянием, далеко не всегда должны считаться равными, тогда как при использовании структур это неизбежно. Кроме того, пользователи вашего кода обычно ожидают, что структуры (и структурные типы вообще) по своему поведению близки к встроенным структурным типам вроде Integer и Double.
Все стандартные числовые типы (Integer, Long, и т.д.) реализованы B.NETFramewdrke виде структур.
Свойства и инкапсуляция
На первый взгляд кажется, что свойства очень похожи на открытые поля экземпляров. Если объявить в классе А открытое поле с именем evil, на него можно сослаться при помощи конструкции A.evil; ничто не указывает на то, что свойство реализовано в виде открытой переменной. Может, определить открытое поле и избавиться от хлопот по определению процедур Get и Set?Не поддавайтесь соблазну. Инкапсуляцию данных не стоит нарушать без веских причин (а еще лучше —те нарушать никогда!).
Но инкапсуляцию можно случайно нарушить и другими способами — например, если не следить за возвращаемыми значениями свойств. Каким образом? Если поле представляет собой изменяемый объект (например, массив), возвращение его в виде значения свойства приведет к нарушению инкапсуляции, поскольку внешний код сможет изменить состояние поля экземпляра через полученную объектную переменную. В таких ситуациях следует создать клон поля (клонирование объектов рассматривается в главе 5). Мораль:
Свойства не должны возвращать изменяемые объекты, которые представляют собой переменные классов.
Конструкторы класса
Сколько времени теряется при создании новых экземпляров строки? Мы провели тестирование (при этом использовалась программа, приведенная в подразделе «Хронометраж — насколько быстрее работает класс StringBuilder?» ниже). Оказалось, что класс StringBuilder обычно работает в сотни раз быстрее, чем класс String. На практике это соотношение может быть и выше, поскольку в наших тестах не использовались дополнительные возможности класса StringBuilder, позволяющие избежать частого выделения памяти. В результате оптимизации класс StringBuilder может работать еще быстрее. С другой стороны, если вы просто обращаетесь к отдельным символам строки и не собираетесь изменять ее, обычный класс String оказывается эффективнее класса StringBuitder.В следующем фрагменте показано, как быстро создать строку, состоящую из 25 000 повторений буквы А:
Dim bar As New String("A" .25000)
Dim foo As New System.Text.SthngBuilder(Bar)
Свойство Chars позволяет прочитать или записать символ, находящийся в заданной позиции Stri ngBui I der. Индексация начинается с 0, поэтому для экземпляра StringBuilder с именем foo команда foo.Chars(l) = "b" заменяет второй символ строки символом «b».
При помощи свойства Length можно получить или задать текущий размер объекта Stri ngBuilder. Если указанное значение меньше текущего размера, VB усекает объект Stri ngBuilder. Если при создании объекта StringBuilder была задана максимальная вместимость, в случае ее превышения инициируется исключение (исключения рассматриваются в главе 7).
Часто используемые члены класса StrlngBuilder очень сильно перегружены. Они существуют в большом количестве версий, что позволяет выполнять со строками разнообразные операции — включать и удалять в них строки, символы, массивы символов и т. д. Например, метод Append добавляет символы в конец объекта
StringBuilder:
Dim foo As New System.Text.StringBuilder()
foo = foo.Append("A")
foo.Appenc("hello")
' Добавляет 5 символов
foo.Append(37)
' Добавляет 2 символа
foo. Append (new Random()) '??
Как показывает последняя строка приведенного фрагмента, к объекту Stri ngBui I der можно присоединить произвольный объект. При этом VB автоматически вычисляет строковое представление объекта (а точнее, вызывает его метод ToStri ng) и присоединяет полученную строку к StringBuilder. Конечно, осмысленность строкового представления объекта зависит от реализации класса. В приведенном примере вместо случайного числа будет добавлена бесполезная строка System.Random (но команда foo. Append (New Random(). Next приведет к желаемому результату).
Метод Insert вставляет объект или значение в заданную позицию объекта
StringBuilder:
Insert(ByVal index As Integer.ByVal thing As Object)
Похожую сигнатуру имеет и метод Remove, удаляющий заданное количество символов с заданной позиции:
Remove(ByVal startlndex As Integer.ByVal length As Integer)
Перегруженные версии метода Replace выполняют несколько полезных операций:
Repliсе(ByVal oldChar As Char.ByVal newChar As Char.ByVal startlndex As Integer._ ByVal count A's Integer)
ReplacefoldValue As String.ByVal newValue As String.ByVal startlndex As Integer._ ByVal count As Integer)
В классе StringBuilder определен метод Equals, но в отличие от строк два объекта StringBuilder с одинаковым содержимым не обязательно считаются равными. Это объясняется тем, что в .NET Framework истинное выражение a.Equals(b) должно оставаться истинным всегда, а для объектов StringBuilder это невозможно, поскольку они изменяются. Использовать метод Equals для объектов StringBuilder не рекомендуется.
Метод ToStri ng преобразует объект Stri ngBui1der в String. Это делается лишь после того, как все необходимые изменения будут внесены и в дальнейшем вы собираетесь только читать содержимое4 строки.
Важнейшие члены класса
Consolе.WriteLinediiyList.Item( 1))
Свойство Item является свойством по умолчанию класса ArrayList. Это означает, что при использовании его имя может не указываться, Например, приведенная выше команда эквивалентна следующей команде:
Console. WriteLine(myList(1))
В разделе «Свойства» настоящей главы вы узнаете, чем отличаются свойства по умолчанию в VB .NET и прежних версиях VB.
В следующем коротком примере массив ArrayLi st используется для ввода и сохранения неизвестного количества строк. При этом удается обойтись без команды ReDim Preserve, необходимой при работе с обычными массивами.
Option Strict On Module Modulel
Sub Main()
Dim myList As New ArrayList()
Dim theData As String
Console.Write("Please enter each item and hit Enter key,"_
& "enter ZZZ when done:") theData =Console.ReadLine()
Do Until theData ="ZZZ" myList.Add(theData)
Console.WriteC'Please enter each item and hit Enter,"_
& "enter ZZZ when done:") theData =Console.ReadLine() Loop
Console.WriteLine("You entered "SmyList.Count() & "ITEMS.")
Console.ReadLine()
End Sub
End Module
| Назад | Содержание | Вперед |
Терминология ООП
Отправной точкой во всей терминологии ООП является понятие класса. Классом называется шаблон, по которому создаются объекты.Каждый объект, созданный на основе класса, называется экземпляром этого класса. Методы, свойства и процедуры событий, определенные внутри класса, называются членами. Предположим, вы пишете программу для работы с информацией о сотрудниках компании. Несомненно, в такой программе будет определен класс Employee; каждый экземпляр класса Employee будет соответствовать конкретному человеку. Члены класса Employee должны соответствовать специфике решаемых задач (например, в свойстве Name будет храниться имя работника, а метод Raise-Salary будет использоваться для повышения зарплаты).
TypeName и TypeOf
Переменные, объявленные с типом Object, могут использоваться для хранения произвольных объектов. Следовательно, программисту необходимы средства для определения типа объекта, связанного с объектной переменной. В VB .NET эта задача решается двумя способами: функцией TypeName и оператором TypeOf ...Is.Функция TypeName возвращает строку с описанием типа. Для всех типов, кроме базовых, должен быть предварительно вызван оператор New; в противном случае функция возвращает строку Nothing. Например, следующий фрагмент выводит в консольном окне строку Nothing:
Dim anSBuilder As System.Text.StringBuilder
Console.WriteLineC'My type name is " & TypeName(anSBuilder))
Но после вызова New в окне будет выведена строка StringBuilder:
Dim anSBuilder As New System.Text.StringBuilder
Console.WriteLineC'My type name is " & TypeName(anSBuilder))
Функция TypeName возвращает короткое имя класса, поэтому не рассчитывайте получить полное имя вида System.Text.StringBuilder.
Если вызвать функцию TypeName для массива, вы получите строковое имя, за которым следует пустая пара круглых скобок. Пример:
Dim aThing(5)As Integer
Console.WriteLine("My type Harness " & TypeName(aThing))
Полученная строка имеет вид Integer().
Функция TypeName удобна в процессе отладки, но в окончательных версиях программ обычно используется оператор TypeOf...Is. Он работает гораздо эффективнее, поскольку обходится без сравнений строк, необходимых при использовании TypeName. Синтаксис проверки выглядит следующим образом:
If TypeOf aThing Is System.Text.SthngBuilder Then
' Объект относится к типу StringBuilder End If
Оператор TypeOf...Is возвращает True, если объект относится к заданному типу или является производным от него. Поскольку в .NET все объекты являются производными от общего предка Object проверка вида TypeOf...Is Object всегда возвращает True, даже если переменная относится к типу, производному от Object. Если вам потребуется узнать точный тип объектной переменной, воспользуйтесь методом GetType.
Уничтожение объектов
В VB .NET объекты не умирают «естественной смертью»; в каком-то смысле они постепенно «уходят в небытие» со временем. Главное отличие от предыдущих версий VB заключается в том, что вы не можете явно освободить память, занимаемую объектом. Встроенный сборщик мусора когда-нибудь заметит, что эти блоки памяти не используются в программе, и автоматически освободит их. Автоматическая сборка мусора оказывает сильное влияние на программирование в VB .NET. В частности, сборку мусора следует рассматривать как полностью автоматизированный процесс, на который вы абсолютно не можете повлиять.Хотя в программе можно провести принудительную сборку мусора вызовом метода System. GC. Collect(), считается, что это не соответствует хорошему стилю программирования .NET. Мы рекомендуем всегда полагаться на автоматическую сборку мусора.
Вспомните, что в прежних версиях VB в каждом классе существовало событие Termi nate, которое гарантированно вызывалось в тот момент, когда количество ссылок уменьшалось до 0 (в терминологии ООП это называется детерминированным завершением). В VB .NET (как бы вы к этому ни относились) поддерживается только недетерминированное завершение, из чего следует, что вы не можете рассчитывать на то, что некий аналог события Termi nate будет вызван в определенный момент времени. Более того, не гарантировано даже то, что он вообще будет когда-нибудь вызван!
Некоторые программисты считают Finalize аналогом события Terminate в VB .NET, однако эта аналогия неверна. Метод Finalize всего лишь содержит код, который должен выполняться при освобождении памяти вашего объекта в процессе сборки мусора. Но поскольку вы не можете явно управлять тем, когда это произойдет, мы настоятельно рекомендуем использовать Finalize лишь как дополнительную меру безопасности — например, для дублирования метода Dispose, который должен вызываться пользователем класса. Метод Dispose рассматривается ниже.
Возникает вопрос: если раньше мы проводили деинициализацию класса в событии Terminate, как же это делается сейчас? Для решения этой проблемы в VB .NET существует очень важное правило:
Если ваш класс должен освобождать какие-либо внешние ресурсы, кроме обычной памяти (например, подключения к базе данных, графические контексты, файловые манипуляторы и т.. д.), он должен содержать метод с именем Di spose, вызываемый из внешнего кода.
Мы вернемся к методу Dispose при рассмотрении интерфейса IDisposabl e в главе 5. А пока достаточно сказать, что любое графическое приложение — даже самое простое, вроде продемонстрированного в главе 1, — относится к категории программ, в которых необходим метод Dispose. Это связано с тем, что графические программы захватывают так называемые графические контексты, которые должны быть освобождены для возвращения ресурсов в систему (графические контексты не являются блоками памяти, поэтому автоматическая сборка мусора в данном случае не поможет). Теперь становится ясно, почему в автоматически сгенерированный код, приведенный в главе 1, входит вызов Dispose. Недетерминированное завершение относится к числу самых неоднозначных нововведений .NET, однако автоматическая сборка мусора является неотъемлемой частью .NET. Разработчикам при всем желании не удалось бы сохранить прежний, детерминированный вариант управления памятью и обеспечить совместимость с .NET. Кроме того, механизму, использованному в старых версиях VB (подсчет ссылок), присущи проблемы с утечкой памяти, вызванной существованием циклических ссылок, когда объект А ссылается на объект В и наоборот, как показано на Рисунок 4.8.
Такие языки, как Java, наглядно доказали, что преимущества от автоматической сборки мусора оправдывают небольшие изменения в стиле программирования, связанные с отсутствием детерминированного завершения.
Вложенные классы
В программах VB .NET нередко встречаются ситуации, когда у вас имеются два класса: «внешний» и «внутренний», фактически принадлежащий первому. Вложенные (nested) классы обычно выполняют вспомогательные функции, и их код имеет смысл лишь в контексте внешнего класса. Существует хорошее эмпирическое правило: если при просмотре внешнего класса код вложенного класса можно свернуть в окне программы и это не затруднит понимания логики внешнего класса, значит, работа вложенного класса организована правильно. Конечно, использование вложенных классов всегда приводит к некоторому нарушению инкапсуляции — вложенный класс может обращаться к закрытым членам внешнего класса (но не наоборот!). Если это обстоятельство учитывается в архитектуре вашего приложения, не стоит уделять ему особого внимания, поскольку внутренний класс всего лишь является специализированным членом внешнего класса.VB .NET не позволяет расширять область видимости вложенного класса посредством функций. Например, открытый член внешнего класса не может вернуть экземпляр закрытого или дружественного (Friend) вложенного класса.
Введение в ООП
VB .NET значительно расширяет событийную модель программирования прежних версий VB, в основу которой были заложены элементы управления. В VB .NET вся программа состоит из самостоятельных объектов, взаимодействующих друг с другом. Объекты создаются по шаблонам, называемым классами. Объекты, построенные на базе одного класса:Программистам VB хорошо знакома концепция автономных фрагментов программного кода с четко определенной функциональностью. Конечно, речь идет об элементах управления. Возможность многократного использования кода, оформленного в виде элементов, повышает эффективность программирования на VB по сравнению с традиционной (процедурной) моделью.
Одно из величайших преимуществ .NET заключается в том, что вы можете программировать классы на любом языке по своему выбору, и они будут нормально работать в любом другом языке. Например, написанный на VB .NET элемент можно будет использовать в С#, и наоборот, а благодаря среде Common Language Runtime выбор языка практически не отразится на быстродействии.
Жизненный цикл объекта
Итак, при создании экземпляра класса оператором New вызывается соответствующий метод-конструктор New из определения класса (также может быть вызван общий конструктор, если он есть). Версия конструктора выбирается в соответствии с типом переданных параметров. Конструктор можно рассматривать как аналог события Class_Initiall ze в VB6.Не в каждом классе определяется открытый конструктор. Более того, в некоторых ситуациях все конструкторы класса объявляются закрытыми и экземпляры создаются только общими методами. Конструктор объявляется закрытым в одном из следующих случаев:
Dim Tom As New EmployeeC'Tom ", 100000)
Tom = New Employee("Tom ". 125000)
В этом фрагменте создаются два разных объекта Empl oyee, причем после присваивания во второй строке первый объект Тот теряется. Иногда это соответствует намерениям программиста, иногда — нет. Например, если идентификатор работника хранится в общей переменной Empl oyeeID, то вторая строка присвоит второму объекту Тот идентификатор на 1 больше первоначального. Так или иначе, следующий фрагмент заведомо невозможен:
Dim Tom As New Employee("Tom ", 100000)
Dim Tom As New Employee("Tom ", 125000)
Компилятор выдает следующее сообщение об ошибке:
The local variable 'Tom' is defined multiple times in the same method.
Cамоучитель по VB.NET
Абстрактные базовые классы
На стадии проектирования наследственных связей в программе часто выясняется, что многие классы обладают целым рядом сходных черт. Например, внештатные сотрудники не относятся к постоянным работникам, но и те и другие обладают рядом общих атрибутов — именем, адресом, кодом налогоплательщика и т. д. Было бы логично выделить все общие атрибуты в базовый класс Payabl eEnt i ty. Этот прием, называемый факторингом, часто используется при проектировании классов и позволяет довести абстракцию до ее логического завершения.В классах, полученных в результате факторинга, некоторые методы и свойства невозможно реализовать, поскольку они являются общими для всех классов в иерархии наследования. Например, класс Payabl eEnt i ty, от которого создаются производные классы штатных и внештатных работников, может содержать свойство с именем TaxID. Обычно в процедуре этого свойства следовало бы организовать проверку кода налогоплательщика, но для некоторых категорий внештатных работников эти коды имеют особый формат. Следовательно, проверка этого свойства должна быть реализована не в базовом классе Payabl eEntity, а в производных классах, поскольку лишь они знают, как должен выглядеть правильный код.
В таких ситуациях обычно определяется абстрактный базовый класс. Абстрактным называется класс, содержащий хотя бы одну функцию с ключевым словом MustOverride; при этом сам класс помечается ключевым словом Mustlnherit. Ниже показано, как может выглядеть абстрактный класс Payabl eEntity:
Public Mustlnherit Class PayableEntity
Private m_Name As String
Public Sub New(ByVal itsName As String)
m_Name = itsName
End Sub
Readonly Property TheName()As String
Get
Return m_Name
End Get
End Property
Public MustOverride Property TaxID()As String
End Class
Обратите внимание: свойство TaxID, помеченное ключевым словом MustOverride, только объявляется без фактической реализации. Члены классов, помеченные ключевым словом MustOverride, состоят из одних заголовков и не содержат команд End Property, End Sub и End Function. Доступное только для чтения свойство TheName при этом реализовано; из этого следует, что абстрактные классы могут содержать как абстрактные, так и реализованные члены. Ниже приведен пример класса Егор! оуее, производного от абстрактного класса PayableEntity (ключевые строки выделены жирным шрифтом):
Public Class Employee
Inherits PayableEntity
Private m_Salary As Decimal
Private m_TaxID As String
Private Const LIMIT As Decimal = 0.1D
Public Sub NewCByVal theName As String, ByVal curSalary As Decimal.
ByVal TaxID As String) MyBase.New(theName)
m_Salary = curSalary
m_TaxID = TaxID End Sub
Public Overrides Property TaxID() As String Get
Return m_TaxID
End Get
Set(ByVal Value As String)
If Value.Length <> 11 then
' См. главу 7 Else
m_TaxID = Value
End If
End Set
End Property
Readonly Property Salary() As Decimal Get
Return MyClass.m_Salary
End Get
End Property
Public Overridable Overloads Sub RaiseSalary(ByVal Percent As Decimal)
If Percent > LIMIT Then
' Операция запрещена - необходим пароль
Console.WriteLineC'NEED PASSWORD TO RAISE SALARY MORE " & _
"THAN LIMIT!!!!") Else
m_Salary =(1D + Percent) * m_Salary
End If
End Sub
Public Overridable Overloads Sub RaiseSalary(ByVal Percent As
Decimal. ByVal Password As String) If Password ="special" Then
m_Salary MID + Percent) * m_Salary
End If
End Sub
End Class
Первая ключевая строка расположена внутри конструктора, который теперь должен вызывать конструктор абстрактного базового класса для того, чтобы правильно задать имя. Во втором выделенном фрагменте определяется элементарная реализация для свойства Taxld, объявленного с ключевым словом MustOverride (в приведенном примере новое значение свойства не проверяется, как следовало бы сделать в практическом примере).
Ниже приведена процедура Sub Mai n, предназначенная для тестирования этой программы:
Sub Main()
Dim tom As New Employee("Tom". 50000. "111-11-1234")
Dim sally As New Programmed "Sally", 150000. "111-11-2234".)
Console.Wri teLi ne(sa1ly.TheName)
Dim ourEmployees(l) As Employee
ourEmployees(0) = tom
ourEmployees(l) = sally
Dim anEmployee As Employee
For Each anEmployee In ourEmployees anEmployee.RaiseSalary(0.lD)
Console.WriteLine(anEmployee.TheName & "has tax id " & _
anEmployee.TaxID & ".salary now is " & anEmployee.Salary())
Next
Consol e.ReadLine() End Sub
В программе невозможно создать экземпляр класса, объявленного с ключевым словом Mustlnherit. Например, при попытке выполнения следующей команды:
Dim NoGood As New PayableEntity("can't do")
компилятор выводит сообщение об ошибке:
Class 'PayableEntity' is not creatable because it contains at least one member marked as 'MustOverride' that hasn't been overridden.
Тем не менее объект производного класса можно присвоить переменной или контейнеру абстрактного базового класса, что дает возможность использовать в программе полиморфные вызовы:
Dim torn As New Employee("Tom". 50000, "123-45-6789")
Dim whoToPay(13) As PayableEntity whoToPay(0) = tom
Теоретически класс Mustlnherit может не содержать ни одного члена с ключевым сло-вом MustOverride (хотя это будет выглядеть несколько странно).
Equals и ReferenceEquals
В классе Object поддерживаются две версии Equals — общая и обычная. Общая версия имеет следующий синтаксис:Overloads Public Shared Function Equals(0bject. Object) As Boolean
Пример использования:
Equals(a. b)
Синтаксис обычной версии:
Overloads Over-ridable Public Function Equals(Object) As Boolean
Пример использования:
a.Equals(b)
Обе версии метода Equal s проверяют, обладают ли два объекта одинаковыми данными, но вы должны быть готовы переопределить Equals, если этого требует специфика вашего класса. Не забывайте, что общие члены класса не переопределяются, поэтому переопределение допускается лишь для обычной (не общей) версии Equal s.
Например, если в вашей программе предусмотрены два способа представления некоторого структурного типа, позаботьтесь о том, чтобы это обстоятельство учитывалось методом Equals (именно так разработчики VB .NET поступили с классом String, хотя, строго говоря, этот класс не относится к структурным типам).
В классе Object также предусмотрен общий (и потому не переопределяемый) метод ReferenceEquals. Метод ReferenceEquals проверяет, представляют ли две переменные один экземпляр. Например, как показывает следующий фрагмент, для двух строк а и b выражение a.Equals(b) может быть истинным, а выражение Reference-Equals (a. b) — ложным:
Sub Main()
Dim a As String = "hello"
Dim b As String = "Hello"
Mid(b.l.D= "h"
Console.Writeline("Is a.Equals(b)true?" & a.Equals(b))
Console.WriteLine("Is ReferenceEquals(a.b)true?" & _
ReferenceEquals(a.b))
Console. ReadLine()
End Sub
Результат показан на Рисунок 5.4.
For Each и интерфейс lEnumerable
Поддержка For-Each в классах VB6 была недостаточно интуитивной, а ее синтаксис воспринимался как нечто совершенно инородное (мы упоминали об этом в главе 1). В VB .NET существуют два способа организации поддержки For-Each в классах коллекций. Первый метод уже был продемонстрирован выше: новый класс определяется производным от класса с поддержкой For-Each и автоматически наследует его функциональность. В частности, этот способ применялся для класса Empl oyees, производного от класса System. Collections. CollectionBase.Второй способ, основанный на самостоятельной реализации интерфейса IEnumerable, обеспечивает максимальную гибкость. Определение интерфейса выглядит следующим образом:
Public Interface lEnumerable
Function GetEnumerator() As Enumerator
End Interface
При реализации lEnumerable класс реализует метод GetEnumerator, который возвращает объект IEnumerator, обеспечивающий возможность перебора в классе. Метод перехода к следующему элементу коллекции определяется именно в интерфейсе IEnumerator, который определяется следующим образом:
Public Interface lEnumerator
Readonly Property Current As Object
Function MoveNext() As Boolean
Sub Reset ()
End Interface
В цикле For-Each перебор ведется только в одном направлении, а элементы доступны только для чтения. Этот принцип абстрагирован в интерфейсе lEnumerator — в интерфейсе присутствует метод для перехода к следующему элементу, но нет методов для изменения данных. Кроме того, в интерфейс IEnumerator должен входить обязательный метод для перехода в начало коллекции. Обычно этот интерфейс реализуется способом включения (containment): в коллекцию внедряется специальный класс, которому перепоручается выполнение трех интерфейсных методов (один из lEnumerable и два из IEnumerator).
Ниже приведен пример коллекции Employees, построенной «на пустом месте». Конечно, класс получается более сложным, чем при простом наследовании от System. Collections. CollectionBase, но зато он обладает гораздо большими возможностями. Например, вместо последовательного возвращения объектов Employee можно использовать сортировку по произвольному критерию:
1 Public Class Employees
2 Implements IEnumerable.IEnumerator
3 Private m_Employees() As Employee
4 Private m_index As Integer = -1
5 Private m_Count As Integer = 0
6 Public Function GetEnumerator() As lEnumerator _
7 Implements lEnumerable.GetEnumerator
8 Return Me
9 End Function
10 Public Readonly Property Current() As Object _
11 Implements IEnumerator.Current
12 Get
13 Return m_Employees(m_Index)
14 End Get
15 End Property
16 Public Function MoveNext() As Boolean _
17 Implements lEnumerator.MoveNext
18 If m_Index < m_Count Then
19 m_Index += 1
20 Return True
21 Else
22 Return False
23 End If
24 End Function
25 Public Sub Reset() Implements IEnumerator.Reset
26 m_Index = 0
27 End Sub
28 Public Sub New(ByVal theEmployees() As Employee)
29 If theEmployees Is Nothing Then
30 MsgBox("No items in the collection")
31 ' Инициировать исключение - см. главу 7
32 ' Throw New ApplicationException()
33 Else
34 m_Count = theEmployees.Length - 1
35 m_Employees = theEmployees
36 End If
37 End Sub
38 End Class
Строка 2 сообщает о том, что класс реализует два основных интерфейса, используемых при работе с коллекциями. Для этого необходимо реализовать функцию, которая возвращает объект lEnumerator. Как видно из строк 6-9, мы просто возвращаем текущий объект Me. Впрочем, для этого класс должен содержать реализации членов IEnumerable; они определяются в строках 10-27.
В приведенной выше программе имеется одна тонкость, которая не имеет никакого отношения к интерфейсам, а скорее связана со спецификой класса. В строке 4 переменная mjndex инициализируется значением -1, что дает нам доступ к 0 элементу массива, в результате чего первый вызов MoveNext предоставляет доступ к элементу массива с индексом 0 (попробуйте инициализировать mjndex значением 0, и вы убедитесь, что при этом теряется первый элемент массива).
Ниже приведена небольшая тестовая программа. Предполагается, что Publiс-класс Employee входит в решение:
Sub Main()
Dim torn As New Emplpyee("Tom". 50000)
Dim sally As New Employee("Sally". 60000)
Dim joe As New Employee("Joe", 10000)
Dim theEmployees(l) As Employee
theEmployees(0) = torn
theEmployees(1) = sally
Dim myEmployees As New Employees(theEmployees)
Dim aEmployee As Employee
For Each aEmployee In myEmployees
Console.WriteLine(aEmployee.TheName)
Next
Console.ReadLine()
End Sub
Функция GetType и рефлексия
Каждый тип .NET Framework представлен объектом Туре. Класс Туре содержит множество методов со сложными именами — например, метод GetMembers возвращает информацию об именах всех методов заданного класса. Метод GetType класса Object возвращает объект Туре, при помощи которого можно получить информацию о типе во время выполнения программы. В частности, эта чрезвычайно полезная возможность используется для выполнения рефлексии (также используется термин «идентификация типов на стадии выполнения»). Кстати, пространство имен Reflection занимает столь важное место в работе .NET Framework, что оно автоматически импортируется в каждый проект VS IDE.Чтобы увидеть, как выполняется рефлексия, включите в проект ссылку на сборку System.Windows.Forms и запустите приведенную ниже программу. Когда через короткий промежуток времени на экране появится приглашение, нажмите клавишу Enter. Продолжайте нажимать Enter, и постепенно в консольном окне будет выведена информация обо всех членах класса Windows. Forms. Form, на основе которого строятся графические приложения в .NET. Примерный вид окна показан на Рисунок 5.5.
ICloneable
Как было показано в разделе «MemberWiseClone», клонирование объекта, содержащего внутренние объекты, вызывает немало проблем. Разработчики .NET дают вам возможность сообщить о том, что данная возможность реализована в вашем классе. Для этой цели используется декларативный интерфейс ICloneable, состоящий из единственной функции Clone:Public Interface ICloneable
Function Clone() As Object
End Interface
Этот интерфейс (а следовательно, и метод Clone) реализуется в том случае, если вы хотите предоставить пользователям своего класса средства для клонирования экземпляров. Далее вы сами выбираете фактическую реализацию метода Clone — не исключено, что она будет сводиться к простому вызову MemberWiseClone. Как было сказано выше, MemberWiseCl one нормально клонирует экземпляры, поля которых относятся к структурному типу или являются неизменяемыми (такие, как String). Например, в классе Empl oyee клонирование экземпляров может осуществляться методом Clone, поскольку все поля представляют собой либо строки, либо значения структурных типов. Таким образом, реализация IC1 опеаЫ е,для класса Empl oyee может выглядеть так:
Public Class Employee Implements ICloneable
Public Function Clone() As Object _
Implements ICloneable.Clone
Return CType(Me.MemberwiseClone, Employee)
End Function ' И т.д.
End Class
В классах, содержащих внутренние объекты, реализация метода Clone потребует значительно больших усилий (хотя в главе 9 описан прием, позволяющий достаточно просто решить эту задачу в большинстве случаев). Так, в приведенном выше классе EmbeddedObject необходимо клонировать внутренний массив, не ограничиваясь простым копированием.
Как это сделать? Очень просто. Поскольку класс Array реализует интерфейс ICloneable, он должен содержать метод для клонирования массивов. Остается лишь вызвать этот метод в нужном месте. Ниже приведена версия класса Ет-beddedObjects с реализацией ICloneabl e (ключевые строки выделены жирным шрифтом):
Public Class EmbeddedObjects Implements
ICloneable Private m_Ma() As String
Public Sub New(ByVal anArray() As String)
m_Data = anArray
End Sub
Public Function Clone() As Object Implements
ICloneable.Clone
Dim temp()As String
temp = m_Data.Clone ' Клонировать массив
Return New EmbeddedObjects(temp)
End Function
Public Sub DisplayData()
Dim temp As String
For Each temp In m_Data
Console.WriteLine(temp)
Next End
Sub Public
Sub ChangeDataCByVal
newData As String)
m_Data(0) = newData
End Sub
End Class
Список классов .NET Framework, реализующих интерфейс ШопеаЫе (а следовательно, поддерживающих метод Clone), приведен в описании интерфейса ШопеаЫе в электронной документации.
ICollection
Интерфейс ICollection определяется производным от IEnumerable; он дополняет этот интерфейс тремя свойствами, доступными только для чтения, и одним новым методом. Класс ICollection редко реализуется самостоятельно. Как правило, он образует базу для интерфейсов IList и IDictionary (см. ниже). Члены этого интерфейса перечислены в табл. 5.2.
IComparable
Предположим, коллекцию объектов Employee потребовалось отсортировать по заработной плате. Конечно, операцию сортировки было бы удобно реализовать непосредственно в классе Emplоуее, чтобы сортировка простого или динамического массива объектов этого класса выполнялась так же просто, как сортировка строковых массивов. Оказывается, порядок сортировки элементов, используемый методом Sort классов Array и ArrayList, определяется интерфейсом IComparable (строковые массивы интерфейс IComparabl e сортирует в порядке ASCII-кодов). Интерфейс состоит из единственного метода CompareTo: Function CompareTo(ByValobj As Object) As Integer Метод возвращает следующие значения:Public Class Employee
Implements IComparable
Private m_Name As String
Private m_Salary As Decimal
Private Const LIMIT As Decimal =0.10
Public Sub New(ByVal theName As String,ByVal curSalary As Decimal)
m_Name = theName m_Salary = curSalary
End Sub
Public Function CompareTo(ByVal anEmployee As Object) As Integer _
Implements IComparable.CompareTo
If CType(anEmployee,Employee).Salany < Me.Salary Then Return -1
El self CTypetanEmployee.Employee).Salary = Me.Salary Then
Return 0
Elself CTypeCanEmployee,Employee).Salary > Me.Salary Then
Return 1
End If
End Function
Public Readonly Property TheName() As String Get
Return m_Name End Get End Property
Public Readonly Property Salary() As Decimal Get
Return MyClass.m_Salary
End Get End Property
Public Overridable Overloads Sub RaiseSalary(ByVal Percent As Decimal)
If Percent > LIMIT Then
' Операция запрещена - необходим пароль
Console.WriteLine("NEED PASSWORD TO RAISE SALARY MORE " & _
"THAN LIMIT!!!!")
Else
m_Salary =(1 + Percent) * m_Salary
End If
End Sub
Public Overridable Overloads Sub RaiseSalary(ByVal Percent As Decimal._
ByVal Password As String) If Password = "special" Then
m_Salary =(1 + Percent) * m_Salary
End If
End Sub
End Class
Для тестирования новой версии класса можно воспользоваться следующей программой:
Sub Main()
Dim torn As New Employee("Tom". 50000)
Dim sally'As New Employee("Sally", 60000)
Dim joe As New Employee("Joe", 20000)
Dim gary As New Employее("Gary", 1)
Dim theEmployees() As Employee = _
{torn, sally, joe. gary}
Array.Sort(theEmployees)
' Порядок сортировки определяется CompareTo!
Dim aEmployee As Employee
For Each aEmployee In theEmployees
Console.WriteLine(aEmployee.TheName & "has yearly salary $"
& FormatNumbertaEmployee.Salary)) Next
Console.ReadLine()
End Sub
Результат показан на Рисунок 5.9.
IDisposable
Выше уже упоминалось о том, что метод Finalize не обеспечивает надежного освобождения ресурсов, не находящихся под управлением сборщика мусора. В программировании .NET у этой задачи существует общепринятое решение — класс реализует интерфейс IDisposable с единственным методом Dispose, освобождающим занятые ресурсы:Public Interface IDisposable
Sub Dispose()
End Interface
Итак, запомните следующее правило:
Если ваш класс использует другой класс, реализующий IDisposable, то в конце работы с ним необходимо вызвать метод Dispose.
Как будет показано в главе 8, метод Dispose должен вызываться в каждом графическом приложении, зависящем от базового класса Component, поскольку это необходимо для освобождения графических контекстов, используемых всеми компонентами.
Список классов .NET Framework, реализующих интерфейс IDisposabe (следовательно, поддерживающих метод Dispose, который должен вызываться в приложениях), приведен в описании интерфейса IDisposable в электронной документации.
Иерархия наследования в окне классов

Объекты производных классов могут храниться в переменных базовых классов:
Dim tom As New Programmer("Tom". 65000)
Dim employeeOfTheMonth As Employee
employeeOfTheMonth = torn
В режиме жесткой проверки типов (Option Strict On), если объект tom хранится в переменной employeeOfTheMonth, для сохранения его в переменной Programmer приходится использовать функцию СТуре, поскольку компилятор заранее не знает, что такое преобразование возможно:
Dim programrnerOnCall As Programmer
programmerOnCal1 = CType(employeeOfTheMonth,Programmer)
Конечно, простое сохранение tom в переменной programmerOnCall выполняется простым присваиванием.
При работе с объектом torn через переменную employeeOfTheMonth вам не удастся использовать уникальные члены, определенные в классе Programmer и отсутствующие в Employee. С другой стороны, как будет показано в следующем разделе, при этом сохраняется доступ ко всем членам класса Programmer, переопределяющим члены класса Employee.
IList Интерфейс IList обеспечивает
IDictionary
Интерфейс IDictionary представляет коллекцию, в которой доступ к данным осуществляется по ключу — как в хэш-таблицах, описанных в предыдущей главе. Более того, класс хэш-таблиц в числе прочих реализует интерфейсы IDictionary, ICollection, Enumerable и ICloneable!
Хотя интерфейс IDictionary объявляется производным от Enumerable и переход к следующему элементу может осуществляться методом MoveNext, обычно такая возможность не используется — коллекции, реализующие IDictionary, ориентируются в первую очередь на обращение по ключу, а не на последовательный перебор элементов. По этой причине интерфейс IDictionary зависит от интерфейса IDic-tionaryEnumerator, который расширяет Enumerator и дополняет его тремя новыми свойствами:
Члены класса IDictionary перечислены в табл. 5.4.
Поскольку ключи в ассоциативных коллекциях должны быть уникальными, при реали-зации большинства методов необходимо сначала проверить, не был ли заданный ключ использован ранее. Свойство Keys возвращает объект, реализующий ICollection; уникальность ключа проверяется методом,Соп1а1п5 интерфейса ICollection.
Информация о членах класса Windows.Forms.Form

В программировании, как и в современной науке:
Следующий пример наглядно показывает, что имеется в виду под этим предупреждением. Массивы VB .NET в отличие от массивов VB6 являются объектами.
Допустим, мы пытаемся клонировать объект класса, одно из полей которого представляет собой массив:
1 Public Class EmbeddedObjects
2 Private m_Data() As String
3 Public Sub New(ByVa1 anArray() As String)
4 m_Data = anArray
5 End Sub
6 Public Sub OisplayData()
7 Dim temp As String
8 For Each temp In m_Data
9 Console.WriteLine(temp)
10 Next
11 End Sub
12 Public Sub ChangeData(ByVal newData As String)
13 m_Data(0) = newData
14 End Sub
15 Public Function Clone() As EmbeddedObjects
16 Return CType(Me.MemberwiseClone. EmbeddedObjects)
17 End Function
18 End Class
Выполните следующую процедуру Sub Main:
Sub Main()
Dim anArray() As String ={"HELLO"}
Dim a As New EmbeddedObjects(anArray)
Console.WriteLinet"Am going to display the data in object a now!")
a.DisplayData()
Dim b As EmbeddedObjects
b =a.Clone()
Dim newData As String ="GOODBYE"
b.ChangeData(newData)
Console.WriteLine("Am going to display the data in object b now!")
b.DisplayData()
Console.WriteLine("Am going to re-display the data in a" & _
"after making a change to object b!!!") a.DisplayData()
Console. ReadLine() End Sub
Интерфейсы
Вероятно, вы убедились в том, что наследование занимает важное место в VB .NET, но для полноценного использования объектно-ориентированных средств VB .NET вам также придется освоить реализацию интерфейсов. Этой важной теме посвящены несколько ближайших разделов.Прежде всего реализацию интерфейса можно рассматривать как контракт, обязательный для любого класса. Интерфейсы занимали важнейшее место в программировании СОМ, а также в реализации объектно-ориентированных средств в прежних версиях VB. При реализации интерфейса класс-обязуется предоставлять некоторую функциональность в соответствии с сигнатурами заголовков членов, отныне и во веки веков. В отличие от объектов при наследовании интерфейсы не связаны никакими взаимными зависимостями — каждая реализация интерфейса существует независимо от других.
В мире ООП часто приходится слышать высказывания типа «композиция предпочтитель-нее наследования» (то есть лучше использовать интерфейсы, а не наследование). Благодаря средствам контроля версии в .NET выбор между наследованием и композицией уже не играет столь принципиальной роли. Используйте наследование всюду, где это уместно, — там, где существует ярко выраженная связь типа «является частным случаем».
Реализация интерфейса предполагает, что ваш класс содержит методы со строго определенными сигнатурами. Эти методы могут быть пустыми, но они обязательно должны присутствовать.
Фактическая реализация методов не фиксируется; как было только что сказано, методы могут вообще ничего не делать. Поддержка интерфейса — всего лишь обязательство определить методы с заданными сигнатурами. Из этого простого факта вытекает множество замечательных следствий. Особый интерес представляют следующие:
А теперь подумайте, что произойдет, если:
Как стать начальником?
Предположим, вы построили замечательную объектно-ориентированную систему учета кадров, в которой в полной мере используются все преимущества полиморфизма. А теперь попробуйте ответить на простой вопрос — как в вашей системе реализован перевод простого работника в менеджеры?Как ни странно, в ООП подобные операции (то есть изменение типа текущего экземпляра в объектно-ориентированной программе) считаются одним из сложнейших аспектов архитектуры приложения, о котором обычно никто всерьез не думает, пока ситуация не станет критической. В соответствии со спецификой объектно-ориентированного программирования после создания объекта изменить его тип невозможно.
В нашей системе учета кадров существует только одно приемлемое решение — включить в класс Employee метод, который копирует состояние Employee в новый объект Manager, после чего помечает старый объект Employee как неиспользуемый.
Коллекции
Коллекцией (collection) называется объект, предназначенный для хранения других объектов. Коллекция содержит методы для включения и удаления внутренних объектов, а также обращения к ним в разных вариантах — от простейшей индексации, как при работе с массивами, до сложной выборки по ключу, как в классе Hashtable, представленном в предыдущей главе. .NET Framework содержит немало полезных классов коллекций. Расширение этих классов посредством наследования позволяет строить специализированные коллекции, безопасные по отношению к типам. И все же при нетривиальном использовании встроенных классов коллекций необходимо знать, какие интерфейсы в них реализованы. Несколько ближайших разделов посвящены стандартным интерфейсам коллекций.Корневой базовый класс Object
Вся работа .NET Framework (а следовательно, и VB .NET) основана на том, что каждый тип является производным от корневого класса Object, общего предка всех классов (в ООП такие классы иногда называются космическими (space) базовыми классами). К классу Object восходят все типы, как ссылочные (экземпляры классов), так и структурные (числовые типы и даты, перечисляемые типы и структуры). В частности, из этого следует, что любой функции, получающей параметр типа Object, можно передать параметр произвольного типа (поскольку главное правило наследования, упоминавшееся в начале главы, требует, чтобы переменная производного типа могла использоваться в любом контексте вместо переменной базового типа).Программисты с опытом работы в ранних версиях VB иногда представляют тип Object как аналог печально известного типа Variant. He поддавайтесь этому искушению! Тип Variant был всего лишь одним из типов данных, который позволял хранить другие типы данных; тип Object является корневым базовым классом, на котором завершается вся иерархия наследования в .NET.
Класс Object содержит ряд встроенных логических функций, предназначенных для проверки типа объектной переменной:
Sub Maine)
Dim a As Integer = 3
Console.Writel_ine("a is a value type is " & IsValueType(a))
Console. ReadLine()
End Sub
Function IsValueType(ByVal thing As Object) As Boolean
Return (TypeOf (thing) Is System.ValueType)
End Function
Вероятно, перед нами одна из ошибок разработчиков VB .NET — функция TypeOf не может вызываться для структурных переменных без определения вспомогательной функции, получающей объект указанного типа. Конечно, следовало бы позволить программисту передавать структурный тип при вызове TypeOf.
Механика реализации интерфейса
Во многих компаниях, занимающихся программированием (хотя бы в Microsoft), существует должность ведущего программиста или ведущего специалиста по тестированию. Предположим, вы решили расширить систему учета кадров и включить в нее эти новые должности с особыми свойствами — скажем, наличием фонда материального поощрения для особо отличившихся подчиненных.В описанной выше иерархии классов VB .NET определить новый класс «ведущий специалист» не удастся, поскольку классы Programmer и Tester уже являются производными от класса Empl oyee, а множественное наследование в .NET не поддерживается. Перед нами идеальный пример ситуации, когда вам на помощь приходят интерфейсы.
По общепринятым правилам имена интерфейсов в .NET начинаются с прописной бук-вы «I», поэтому в следующем примере интерфейс называется ILead.
Прежде всего интерфейс необходимо определить. В отличие от VB6, где интерфейс был обычным классом, в VB .NET появилось специальное ключевое слово Interface. Предположим, наши «ведущие» должны оценивать своих подчиненных и тратить средства из фонда материального поощрения. Определение интерфейса выглядит так:
Public Interface ILead
Sub SpendMoraleFund(ByVal amount As Decimal)
Function Rate(ByVal aPerson As Employee) As String
Property MyTeam() As Empl oyee ()
Property MoraleFuod() As Decimal End Interface
Обратите внимание — в определении интерфейса отсутствуют модификаторы уровня доступа Publiс и Private. Разрешены только объявления Sub, Function и Property с ключевыми словами Overloads и Default. Как видите, определение интерфейса выглядит просто. Любой класс, реализующий интерфейс ILead, обязуется содержать:
Чтобы реализовать интерфейс в классе, прежде всего убедитесь в том, что он сам или ссылка на него входит в проект. Далее за именем класса и командой Inherits в программу включается строка с ключевым словом Implements, за которым следует имя интерфейса. Пример:
Public Class LeadProgrammer
Inherits Programmer
Implements Head
End Class
Имя Head подчеркивается синей волнистой чертой, свидетельствующей о возникшей проблеме. Тем самым компилятор настаивает на выполнении обязательств по реализации интерфейса хотя бы пустыми методами.
Как это сделать? В отличие от ранних версий VB, где члены классов, входящие в реализацию интерфейса, обозначались особой формой сигнатуры, в VB .NET используется более наглядный синтаксис. В следующем фрагменте соответствующая строка выделена жирным шрифтом.
Public Function Rate(ByVal aPerson As Employee) As String _
Implements ILead.Rate End Function
Конечно, имена членов интерфейса обычно совпадают с именами методов, их реализующих, но это не обязательно. Например, следующий фрагмент вполне допустим.
Public Property OurMoraleFund() As Decimal Implements
Head.MoraleFund Get
Return m_Moral e Fund
End Get
Set(ByVal Value As Decimal)
m_MoraleFund =Value
End Set
End Property
Главное, чтобы типы параметров и возвращаемого значения соответствовали сигнатуре данного члена интерфейса. При объявлении метода могут использоваться любые допустимые модификаторы, не мешающие выполнению контракта, — Overloads, Overrides, Overridable, Public, Private, Protected, Friend, Protected Friend, MustOverride, Default и Static. Запрещается только использовать атрибут Shared, поскольку члены интерфейса должны принадлежать конкретному экземпляру, а не классу в целом.
Если в реализации интерфейса используется член класса с модификатором Pri vate, обращения к этому члену возможны только через переменную, объявленную с типом данного интерфейса. В отличие от предыдущих версий VB в остальных случаях к членам интерфейса всегда можно обращаться через объекты класса. Теперь вам не придется присваивать их промежуточным интерфейсным переменным. Пример:
Dim tom As New LeadProgrammer("Tom",65000) tom.SpendMoraleFund(500)
Однако в обратных преобразованиях приходится использовать функцию СТуре:
Dim tom As New LeadProgrammer("Tom". 65000)
Dim aLead As ILead.aName As String
aLead = tom
aName = Ctype(aLead. Programmer).TheName 'OK
Следующая строка недопустима:
aName =tom.TheName ' ЗАПРЕЩЕНО!
Ниже перечислены общие правила преобразования между типом объекта и интерфейсом, им реализуемым.
Dim torn As New LeadProgrammer("tom". 50000)
Console.WriteLine((TypeOf (tom) Is Head))
Вторая строка выводит значение True.
Один метод может реализовывать несколько функций, определенных в одном интерфейсе:
Public Sub itsOK Implements
Interface1.Ml.Interfacel.M2,Interfacel.M3
Ниже приведена полная версия класса LeadProgrammer. Конечно, реализация методов интерфейса выглядит несколько условно, однако опадает представление о том, что можно сделать при реализации интерфейса:
Public Class LeadProgrammer
Inherits Programmer Implements Head
Private m_MoraleFund As Decimal
Private m_MyTeam As Employee()
Public Function Rate(ByVal aPerson As Employee) As String _
Implements Head.Rate
Return aPerson.TheName & "rating to be done"
End Function
Public Property MyTeam() As Employee()
Implements ILead.MyTeam
Get
Return m_MyTeam
End Get
SeUByVal Value As Employee()) X.
m_MyTeam = Value
End Set End Property
Public Sub SpendMoraleFund(ByVal
amount As Decimal)_
Implements ILead.SpendMocaleFund
' Израсходовать средства из фонда мат. поощрения
Console.WriteLine("Spent " & amount.ToString())
End Sub
Public Property OurMoraleFund()As Decimal
Implements ILead.MoraleFund
Get
Return m_MoraleFund
End Get
SettByVal Value As Decimal)
m_MoraleFund = Value
End Set End Property
Public Sub New(ByVal theName As String. ByVal curSalary As Decimal)
MyBase.New(theName. curSalary)
End Sub
End Class
Метод ToString
Метод ToString возвращает представление текущего объекта в строковом формате. Вопрос о том, будет ли это представление удобным при отладке и для пользователей, зависит от реализации класса. По умолчанию ToString возвращает полное имя типа для заданного объекта — например, System. Object или Examplel.Programmer.Постарайтесь привыкнуть к переопределению ToStnng в ваших классах, чтобы этот метод возвращал более содержательное строковое представление класса. Например, в классе Employee из программы EmployeeTestl, приведенной в главе 4, метод ToString может выглядеть примерно так:
Public Overrides Function ToString() As String
Dim temp As String
temp = Me.GetType.ToString()& "my name is " & Me.TheName
Return temp
End Function
Примерный результат:
EmployeeTestl+EmployeeTestl+Employee my name is Tom
Нетривиальное применение интерфейсов
Интерфейсы также могут объявляться производными от других интерфейсов. В этом случае интерфейс просто дополняется новыми членами. Предположим, в нашей системе учета кадров ведущим программистам предоставлено право разрешать модернизацию компьютеров своих подчиненных. В программе это моделируется методом UpGradeHardware:Public Interface ILeadProgrammer
Inherits Head
Public Function UpGradeHardware(aPerson As Programmer)
End Interface
В этом случае реализация ILeadProgrammer требует дополнительного выполнения контракта интерфейса Head.
В отличие от классов, которые могут наследовать лишь от одного базового класса, интерфейс может быть объявлен производным от нескольких интерфейсов:
Public Interface ILeadProgrammer
Inherits Head.Inherits ICodeGuru
Public Function UpGradeHardware(aPerson As Programmer)
End Interface
Поскольку интерфейс может наследовать от нескольких интерфейсов, реальна ситуация, при которой в нем потребуется определить два одноименных метода, принадлежащих к разным интерфейсам, — например, если интерфейсы Head и ICodeGuru содержат методы с именем SpendMoraleFund. В этом случае вы не сможете обратиться к одному из этих методов через переменную типа, реализующего такой интерфейс:
Dim tom As New LeadProgrammer("Tom", 65000)
tom.SpendMoraleFund(500)
Интерфейс должен указываться явно, как в следующем фрагменте:
Dim tom As New LeadProgrammer("Tom", 65000)
Dim aCodeGuru As ICodeGuru
aCodeGuru = tom
aCodeGuru.SpendMoraleFund(500)
Обращение к функциональности базового класса
В VB .NET существует модификатор Protected, который автоматически предоставляет доступ из производных классов к соответствующему члену класса, будь то метод или переменная класса. Возникает искушение воспользоваться этим модификатором и объявить все поля базового класса с уровнем доступа Protected, чтобы производные классы могли легко и быстро работать с ними. Не поддавайтесь соблазну! Хороший стиль проектирования требует, чтобы модификатор Protected использовался только для методов, но не для переменных. В противном случае нарушается инкапсуляция и теряется возможность проверки данных там, где она должна происходить — в базовом классе. Как и в реальной жизни, здесь действует хороший принцип «доверяй, но проверяй».Например, в исходное определение класса Employee входят свойства со следующими сигнатурами:
Public Readonly Property TheName() As String
Public Readonly Property Salary() As Decimal
Таким образом, доступ к этим свойствам разрешен всем классам. Чтобы ограничить доступ к свойствам классами, производными от Empl oyee, замените модификатор Publ ic на Protected.
В табл. 5.1 перечислены различные модификаторы уровня доступа, присваиваемые членам классов в иерархии наследования.
Как было сказано выше, функции (но не поля!) с модификаторами Protected и Protected Friend распространены достаточно широко, поскольку они предотвращают доступ к защищенным членам со стороны внешнего кода.
При использовании Protected возникает весьма интересный подвох. К счастью, компилятор вовремя предупредит вас о возможных проблемах. Рассмотрим конкретный пример: допустим, у вас имеется класс GeekFest с методом Boast, который пытается обратиться к свойству Salary класса Programmer (что в конечном счете означает доступ к свойству Sal агу базового класса Empl oyee). Ниже приведен примерный вид программы:
Public Class GeekFest
Private m_Programmers() As Programmer
Sub New(ByVal Programmers() As Programmer)
m_Programmers = Programmers
End Sub
Public Function Boast(ByVal aGeek As Programmer) As String
Return "Hey my salary is " & aGeek.Salary
End Function
End Class
>
Основные методы класса Object
Поскольку класс Object является общим предком всех типов VB .NET, весьма вероятно, что вам придется часто использовать (или переопределять) методы этого класса. Основные методы Object описаны в нескольких ближайших разделах.Довольно часто возникает желание переопределить защищенный метод Finalize класса Object. Теоретически код переопределенного метода Finalize выполняется при освобождении памяти, занимаемой объектом, в процессе сборки мусора. На практике использовать этот метод нежелательно. Поскольку вы не знаете, когда и в какой последовательности будут вызваны методы Finalize, использовать их для деинициализа-ции классов в лучшем случае ненадежно. Вместо этого следует реализовать метод Dispose, описанный в разделе «IDisposable» этой главы. А если вы все же переопределяете метод Finalize, учтите, что в нем необходимо вызвать MyBase.Finalize и продублировать весь код из метода Dispose.
Основы наследования
Хотя наследование не является панацеей ООП и во многих ситуациях лучше воспользоваться интерфейсами, не стоит полагать, что без наследования можно как-нибудь обойтись. Наследование — замечательное средство, способное сэкономить немало времени и сил... если им правильно пользоваться. Критерий правильного использования прост: не используйте наследование, если у вас нет абсолютной уверенности в существовании логической связи типа «является частным случаем».Класс А объявляется производным от класса В только в том случае, если вы точно знаете, что сейчас и в сколь угодно отдаленном будущем объект А может использоваться вместо объекта В и это не вызовет никаких проблем.
(Помните пример из главы 4? Оформляя внештатного работника по правилам для обычных сотрудников, вы наживете неприятности с налоговой инспекцией. Класс Contractor не должен объявляться производным от класса Employee даже при том, что "они обладают рядом сходных черт.)
Ниже этот фундаментальный принцип приведен в слегка измененном, более абстрактном виде, ориентированном на практическое программирование.
Экземпляр класса А, производного от класса В, должен нормально работать в каждом фрагменте программы, которому в качестве параметра передается экземпляр базового типа.
Предположим, у вас имеются функция UseIt(bTh1 ng As В) и объект aThi ng, который является экземпляром производного класса А. Следующий вызов должен нормально работать:
Uselt(aThing)
Если все эти рассуждения выглядят слишком абстрактными, ниже приведен вымышленный (и надеемся, забавный) пример. Предположим, вы размышляете над тем, от какого класса следует объявить производным класс ManagerOf Programmers — от Manager или от Programmer? Всем известно, что менеджеры носят аккуратные прически, поэтому класс Manager должен содержать метод SetHalrStyle. А теперь закройте глаза и представьте типичного программиста, которого вдруг назначили управлять другими программистами. Захочет ли он менять свой имидж? Можете ли вы уверенно заявить, что вызов вида
tom.SetHairStyle("sharp razor cut")
всегда имеет смысл? Конечно, среди программистов иногда встречаются экземпляры, которые заботятся о своей прическе, но обо всех программистах этого никак не скажешь. Мораль: класс ManagerOf Programmers должен быть производным от класса Programmer, а не от Manager.
Некоторые языки позволяют объявить класс ManagerOfProgrammers производным как от Manager, так и от Programmer. Теоретически такая возможность выглядит вполне логично и привлекательно, но на практике языки с множественным наследованием (так это называется по-научному) порождают массу проблем. Вместо множественного наследования в VB .NET используется реализация нескольких интерфейсов. Как вы вскоре увидите, этот вариант значительно проще и нагляднее, нежели классическое множественное наследование.
В сущности, при программировании в VB .NET вам никак не удастся скрыться от наследования. Даже если вы привыкли к интерфейсному стилю программирования VB5 и VB6 и считаете, что для ваших задач достаточно интерфейсов вкупе с включением и делегированием, ограничиться одними интерфейсами в VB .NET невозможно. Дело в том, что без явного использования наследования вы не сможете пользоваться .NET Framework. Наследование заложено в основу любого графического приложения .NET, а также многих встроенных классов коллекций — даже работа с объектом FolderBrowser связана с наследованием!
Применение наследования при построении графических приложений в рекламной литературе VB .NET иногда именуется визуальным наследованием. Не обращайте внимания — это самое обычное наследование. То, что ваш класс является производным от Windows.Forms.Form, сути дела не меняет.
Более того, сам подход к применению наследования в .NET Framework как нельзя лучше доказывает, что наследование в объектно-ориентированном программировании не должно полностью вытесняться интерфейсами. Подход, примененный в .NET Framework, вполне может применяться и в ваших собственных проектах.
При построении библиотек, используемых другими программистами, хорошо спроектированные классы, прошедшие тщательную отладку и тестирование, могут использоваться другими программистами в качестве базовых.
Переопределение свойств и методов
В нашем примере, где программист автоматически получает 6-процентное повышение зарплаты вместо 5-процентного, необходимо изменить поведение метода RaiseSalary и отразить в нем автоматическую надбавку. Это называется переопределением функции.Общие члены классов переопределяться не могут.
В отличие от многих объектно-ориентированных языков синтаксис VB .NET четко показывает, что метод базового класса должен переопределяться в производном классе. Для этого используются два специальных ключевых слова.
Ниже приведен примерный вид базового класса Employee с методом RaiseSalary, который может переопределяться в производных классах Programmer, Manager и т. д. Ключевые строки кода выделены жирным шрифтом:
Option Strict On Public Class Employee
Private m_Name As String
Private m_Salary As Decimal
Private Const LIMIT As Decimal = 0.1D
Public Sub New(ByVal theName As String,ByVal curSalary As Decimal)
m_Name =theName
m_Salary =curSalary End Sub
Public Readonly Property TheName()As String
Get
Return m_Name
End Get End Property
Public Readonly Property Salary()As Decimal
Get
Return MyClass.m_Salary
End Get End Property
Public Overridable Overloads Sub RaiseSalary(ByVal Percent As Decimal)
If Percent > LIMIT Then
' Операция запрещена - необходим пароль
Console.WriteLine('NEED PASSWORD TO RAISE SALARY MORE " & _
"THAN LIMIT!!!!") Else
m_Salary =(1 + Percent) * m_Salary
End If
End Sub
Public Overridable Overloads Sub RaiseSalary(ByVal Percent As _
Decimal.ByVal Password As String) If Password ="special"Then
m_Salary =(1 + Percent) * m_Salary
End If
End Sub
End Class
Необязательное ключевое слово Overloads, упоминавшееся в главе 4, указывает на то, что в классе определены несколько версий RaiseSalary.
Класс Employee часто встречается в примерах этой главы. Либо введите его в Visual Studio, либо скачайте исходный текст с сайта www.piter.com, если вы еще не сделали этого ранее.
В нашей модели зарплата программиста повышается вызовом специализированной версии метода RaiseSalary. Производный класс Programmer приведен ниже (как обычно, ключевые строки выделены жирным шрифтом):
Public Class Programmer
Inherits Employee
Public Sub New(ByVal theName As String, ByVal curSalary As Decimal)
MyBase.New(theName, curSalary)
End Sub
Public Overloads Overrides Sub RaiseSalaryCByVal Percent As Decimal)
MyBase.RaiseSalary(1.2D *Percent."special")
End Sub
End Class
Обратите внимание, каким компактным получился производный класс — большая часть функциональности осталась неизменной, поэтому мы просто наследуем ее от базового класса!
В приведенной ниже процедуре Sub Main компилятор генерирует вызов правильной версии метода Rai seSal ary (с 20-процентной надбавкой) для объекта sal 1у, относящегося к классу Programmer:
Sub Main()
Dim sally As New Programmed"Sally". 150000D) sally.RaiseSalary(0.1D)
' С учетом надбавки для программистов
Console.WriteLine(sally.TheName & " salary is now " & sally.Salary())
Console.ReadLine()
End Sub
Подведем итог:
По умолчанию переопределение членов классов запрещается (см. описание ключевого слова Shadows ниже в этой главе). И все же ключевое слово NotOverridable рекомендуется использовать, поскольку оно более наглядно выражает намерения программиста.
Иногда при переопределении метода или свойства возникает необходимость вызвать версию базового класса. Допустим, имени каждого программиста в классе Programmer должен предшествовать почетный титул «Code Guru». Ключевое слово MyBase позволяет обратиться к открытому свойству TheName базового класса в производном классе:
Public Overrides Readonly Property TheName() As String
Get
Return "Code Guru " & MyBase.TheName()
End Get
End Property
Учтите, что ключевое слово MyBase обладает рядом ограничений:
Полиморфизм на практике
Наследование часто помогает избавиться от громоздких конструкций Select Case и If-Then-Else, чтобы вся черновая работа выполнялась компилятором и механизмом полиморфизма. Например, цикл из следующего фрагмента работает как с экземплярами класса Employee, так и с экземплярами Programmer:Sub Maln()
Dim tom As New Employee("Tom". 50000)
Dim sally As New Programmer("Sally", 150000)
Dim ourEmployees(l) As Employee ourEmpl.oyees(0)=tom
ourEmployees(l)= Sally
Dim anEmployee As Employee
For Each anEmployee In ourEmployees
anEmployee.RaiseSalary(0.1D)
Console.WriteLine(anEmployee.TheName & "salary now is " & _
anEmployee.Salary()) Next
Console. ReadLine()
End Sub
Результат выполнения этого примера показан на Рисунок 5.2. Мы видим, что в каждом случае вызывается правильный метод RaiseSalary, несмотря на то что в массиве типа Employee хранятся как объекты Employee, так и объекты Programmers.
класс CollectionBase
При использовании классов коллекций .NET Framework (таких, как ArrayList и HashTable) возникает неожиданная проблема: эти классы предназначены для хранения обобщенного типа Object, поэтому прочитанные из них объекты всегда приходится преобразовывать к исходному типу функцией СТуре. Также возникает опасность того, что кто-нибудь сохранит в контейнере объект другого типа и попытка вызова СТуре завершится неудачей. Проблема решается использованием коллекций с сильной типизацией — контейнеров, позволяющих хранить объекты конкретного типа и типов, производных от него.Хорошим примером абстрактного базового класса .NET Framework является класс CollectionBase. Классы, производные от Coll ectionBase, используются для построения коллекций с сильной типизацией (прежде чем создавать собственные классы коллекций, производные от Coll ectionBase, убедитесь в том, что нужные классы отсутствуют в пространстве имен System.Collections.Specialized). Коллекции, безопасные по отношению к типам, строятся на основе абстрактного базового класса System. Collections. CollectionBase; от вас лишь требуется реализовать методы Add и Remove, а также свойство Item. Хранение данных во внутреннем списке реализовано на уровне класса System. Collections. CollectionBase, который и выполняет все остальные операции.
Рассмотрим пример создания специализированных коллекций (предполагается, что проект содержит класс Employee или ссылку на него):
1 Public Class Employees
2 Inherits System.Col lections.CollectionBase
3 ' Метод Add включает в коллекцию только объекты класса Employee.
4 ' Вызов перепоручается методу Add внутреннего объекта List.
5 Public Sub AddtByVal aEmployee As Employee)
6 List.Add(aEmployee)
7 End Sub
8 Public Sub Remove(ByVal index As Integer)
9 If index > Count-1 Or index < 0 Then
10 ' Индекс за границами интервала, инициировать исключение (глава 7)
11 MsgBox("Can't add this item")' MsgBox условно заменяет исключение
12 Else
13 List.RemoveAt(index)
14 End If
15 End Sub
16
17 Default Public Readonly Property Item(ByVal index As Integer)As Employee
18 Get
19 Return CType(List.Item(index). Employee)
20 End Get
21 End Property
22 End Class
В строках 5-7 абстрактный метод Add базового класса реализуется передачей вызова внутреннему объекту List; метод принимает для включения в коллекцию только объекты Empl oyee. В строках 8-10 реализован метод Remove. На этот раз мы также используем свойство Count внутреннего объекта List, чтобы убедиться в том, что удаляемый объект не находится перед началом или после конца списка. Наконец, свойство Item реализуется в строках 17-21. Оно объявляется свойством по умолчанию, поскольку пользователи обычно ожидают от коллекций именно такого поведения. Свойство объявляется доступным только для чтения, чтобы добавление новых элементов в коллекцию могло осуществляться только методом Add. Конечно, свойство можно было объявить и доступным для чтения/записи, но тогда потребовался бы дополнительный код для проверки индекса добавляемого элемента. Следующий фрагмент проверяет работу специализированной коллекции; недопустимая операция включения нового элемента (в строке, выделенной жирным шрифтом) закомментирована:
Sub Main()
Dim torn As New Employee("Tom", 50000)
Dim sally As New Employee("Sally", 60000)
Dim myEmployees As New Employees()
myEmployees.Add(tom)
myEmployees.Add(sally)
' myEmployees.Add("Tom")
Dim aEmployee As Employee
For Each aEmployee In myEmployees
Console.WriteLine(aEmployee.TheName)
Next
Console. ReadLine()
End Sub
Попробуйте убрать комментарий из строки myEmpl oyees. Add("Tom"). Программа перестанет компилироваться, и вы получите следующее сообщение об ошибке:
C:\book to comp \chapter 5\EmployeesClass\EmployeesClass\Modulel.vb(9):
A value of type 'String'cannot be converted to 'EmployeesClass.Employee'.
Перед вами замечательный пример того, какими преимуществами VB .NET обладает перед включением в прежних версиях VB. Конечно, мы продолжаем перепоручать вызовы внутреннему объекту, чтобы избавиться от дополнительной работы, но возможность перебора элементов в цикле For-Each появляется автоматически, поскольку наш класс является производным от класса с поддержкой For-Each!
Проблема неустойчивости базовых классов и контроль версии

Проблема несовместимости компонентов хорошо известна всем, кому доводилось программировать для Windows. Обычно она выступает в форме так называемого кошмара DLL (DLL Hell) — программа использует определенную версию DLL, a потом установка новой версии компонента нарушает работу программы. Почему? Причины могут быть разными, от очевидных (случайное исключение функции, использовавшейся в программе) до весьма нетривиальных (например, изменение типа возвращаемого значения у функции). В любом случае все сводится к вариациям на одну тему — при изменении открытого интерфейса кода, от которого зависит ваша программа, программа не может использовать новую версию вместо старой, а старая версия уже стерта. В большинстве объектно-ориентированных языков наследование сопряжено с потенциальной угрозой работоспособности вашей программы из-за несовместимости компонентов. Программисту остается лишь надеяться на то, что открытые и защищенные члены классов-предшественников в 1 иерархии наследования не будут изменяться, таким образом, что это нарушит ра-
ботоспособность их программ. Эта ситуация называется проблемой неустойчивости базовых классов. Наследование часто превращает наши программы в некое подобие карточного домика — попробуйте вытащить нижнюю карту, и все сооружение развалится.
Проблему неустойчивости базовых классов желательно рассмотреть на конкретном примере. Разместите приведенное ниже определение класса Payabl eEntity в отдель-ной^библиотеке и откомпилируйте его в сборку с именем PayableEntity Example командой Build (чтобы задать имя сборки, щелкните правой кнопкой мыши на имени проекта в окне решения, выберите в контекстном меню команду Properties и введите нужные значения в диалоговом окне). Если вы не используете архив с примерами, прилагаемый к книге, запомните, в каком каталоге был построен проект:
Public Mustlnherit Class PayableEntity
Private m_Name As String
Public Sub New(ByVal theName As String)
m_Name =theName
End Sub
Public Readonly Property TheName()As String Get
Return m_Name
End Get
End Property
Public MustOverride
Property TaxID()As
String End Class
После построения DLL закройте решение.
Допустим, вы решили включить в класс Employee новый способ получения адреса, зависящий от базового класса PayableEntity; при этом следует помнить, что класс будет использоваться только в откомпилированной форме. Для этого необходимо включить ссылку на сборку, содержащую этот проект (находится в подкаталоге \bin того каталога, в котором была построена DLL PayableEntityExample). Примерный код класса Empl oyee приведен ниже. Обратите внимание на строку, выделенную жирным шрифтом, в которой класс объявляется производным от абстрактного класса, определенного в сборке
PayableEntityExample.
Public Class Employee
' Пространство имен называется PayableEntityExample.
' поэтому полное имя класса записывается в виде
PayableEntityExample.PayableEntity! Inherits
PayableEntityExample.Employee
Private m_Name As String
Private m_Salary As Decimal
Private m_Address As String
Private m_TaxID As String
Private Const LIMIT As Decimal = 0.1D
Public Sub New(ByVal theName As String,
ByVal curSalary As Decimal,
ByVal TaxID As String)
MyBase.New(theName)
m_Name = theName
m_Salary = curSalary
m_TaxID = TaxID
End Sub
Public Property Address()As String
Get
Return m_Address
End Get
Set(ByVal Value As String)
m_Address = Value
End Set
End Property
Public Readonly Property Salary()As Decimal Get
Return m_Salary «
End Get
End Property
Public Overrides Property TaxIDO As String Get
Return m_TaxID
End Get
SetCByVal Value As String)
If Value.Length <> 11 Then
' См. главу 7 Else
m_TaxID = Value
End If
End Set
End Property
End Class
Процедура Sub Main выглядит так:
Sub Main()
Dim torn As New EmployeeC'Tom". 50000)
tom.Address ="901 Grayson"
Console.WriteCtom.TheName & "lives at " & tom.Address)
Console. ReadLine()
End Sub
Результат показан на Рисунок 5.7. Программа работает именно так, как предполагалось.
Рисунок 5.7. Демонстрация неустойчивости базовых классов (контроль версии отсутствует)
Программа компилируется в исполняемый файл Versiomngl.exe, все идет прекрасно.
Теперь предположим, что класс PayableEntity был разработан независимой фирмой. Гениальные разработчики класса PayableEntity не желают почивать на лаврах! Заботясь о благе пользователей, они включают в свой класс объект с адресом и рассылают новый вариант DLL. Исходный текст они держат в секрете, но мы его приводим ниже. Изменения в конструкторе выделены жирным шрифтом:
Imports Microsoft.Vi sualBasic.Control Chars
Public Class PayableEntity
Private m_Name As String
Private m_Address As Address
Public Sub New(ByVal theName As String,ByVal theAddress As Address)
m_Name = theName
m_Address = theAddress
End Sub
Public Readonly Property TheName()As String Get
Return m_Name End Get
End Property
Public Readonly Property TheAddress() Get
Return
m_Address.DisplayAddress
End Get
End Property
End Class
Public Class Address
Private m_Address As String
Private m_City As String
Private m_State As String
Private m_Zip As String
Public Sub New(ByVal theAddress As String.ByVal theCity As String.
ByVal theState As String.ByVal theZip As String)
m_Address = theAddress
m_City = theCity
m_State = theState
m_Zip = theZip
End Sub
Public Function DisplayAddress() As String
Return m_Address & CrLf & m_City & "." & m_State _
&crLF & m_Zip
End Function
End Class
Перед вами пример редкостной халтуры. В процессе «усовершенствования» авторы умудрились потерять исходный конструктор класса PayableEntity! Конечно, такого быть не должно, но раньше подобные катастрофы все же случались. Старая DLL устанавливалась на жесткий диск пользователя (обычно в каталог Windows\System). Затем выходила новая версия, устанавливалась поверх старой, и вполне благополучная программа Versioningl переставала работать (а как ей работать, если изменился конструктор базового класса?).
Конечно, проектировщики базовых классов так поступать не должны, однако на практике бывало всякое. Но попробуйте воспроизвести этот пример в .NET, и произойдет настоящее чудо: ваша старая программа будет нормально работать, потому что она использует исходную версию Payabl eEnti ty из библиотеки, хранящейся в каталоге \bin решения Versioningl.
Решение проблемы несовместимости версий в .NET Framework в конечном счете основа-но на том, что ваш класс знает версию DLL, необходимую для его работы, и отказывается работать при отсутствии нужной версии. Успешная работа этого механизма зависит от особых свойств сборок (см. главу 13). Тем не менее.в описанной нами ситуации защита .NET Framework преодолевается простым копированием новой DLL на место старой.
Схема контроля версии в .NET позволяет разработчикам компонентов дополнять свои базовые классы новыми членами (хотя на практике делать этого не рекомендуется). Такая возможность сохраняется даже в том случае, если имена новых членов совпадают с именами членов, включенных вами в производный класс. Старый исполняемый файл, созданный на базе производного класса, продолжает работать, поскольку он не использует новую DLL.
Впрочем, это не совсем верно: он действительно продолжает работать — до тех пор, пока вы не откроете исходный текст приложения Versioningl в VS .NET, создадите ссылку на DLL PayableEntityExample и попробуете построить приложение Versioningl заново. Компилятор выдаст сообщение об ошибке:
C:\book to comp\chapter 5\Versioningl\Versioningl\Modu1el.vb(21):
No argument specified or non-optional parameter 'theAddress' of
'Public Sub New(theName As String,theAddress
As PayableEntityExample.Address)'.
Итак, как только вы загрузите старый исходный текст производного класса и создадите ссылку на новую DLL, вам не удастся откомпилировать программу до исправления той несовместимости, на которую вас обрекли разработчики базового класса.
Прежде чем завершить этот раздел, мы хотим разъяснить еще одно обстоятельство. Исключение конструктора из класса и замена его другим конструктором — весьма грубая и очевидная ошибка. Способен ли механизм контроля версии .NET спасти от других, менее тривиальных ошибок? Да, способен.
Рассмотрим самый распространенный (хотя довольно тривиальный) источник ошибок несовместимости при использовании наследования. Имеется производный класс Derived, зависящий от базового класса Parent. В класс Derived включается новый метод Parselt (в следующем примере он просто разделяет строку по словам и выводит каждое слово в отдельной строке):
Imports Microsoft.VisualBasic.ControlChars Module Modulel
SubMain()
Dim myDerived As New Oerived()
myDerived.DisplayIt 0
Console.ReadLine()
End Sub
End Module
Public Class Parent
Public Const MY STRING As String ="this is a test"
Public Overridable Sub Displaylt()
Console.WriteLine(MY_STRING)
End Sub
End Class
Public Class Derived Inherits Parent
Public Overrides Sub Displaylt()
Console.WriteLine(ParseIt(MyBase.MY_STRING))
End Sub
Public Function ParselUByVal aString As String)
Dim tokens() As String
' Разбить строку по пробелам tokens -
aString.Split(Chr(32))
Dim temp As String
' Объединить в одну строку, вставляя между словами
' комбинацию символов CR/LF
temp = Join(tokens.CrLf)
Return temp
End Function
End Class
End Module
Результат показан на Рисунок 5.8.
Рисунок 5.8. Простейшее разбиение строки по словам
Теперь представьте себе, что класс Parent распространяется не в виде исходных текстов, а в откомпилированной форме. Версия 2 класса Parent содержит собственную версию Parselt, которая широко используется в ее коде. В соответствии с принципом полиморфизма при хранении объекта типа Den ved в объектной переменной типа Parent вызовы Displaylt должны использовать метод Parselt класса Derived вместо метода Parselt базового класса. Однако здесь возникает маловероятная, но теоретически возможная проблема. В нашем сценарии код класса Parent, использующий свою версию функции Parselt, не знает, как функция Parselt реализована в классе Derived. Полиморфный вызов версии Parselt производного класса может нарушить какие-либо условия, необходимые для работы базового класса.
В этой ситуации средства контроля версии VB .NET тоже творят чудеса: код откомпилированного базового класса Parent продолжает использовать свою версию Parselt всегда, даже несмотря на то, что при хранении объектов Derived в переменных типа Parent полиморфизм привел бы к вызову неправильной версии метода. Как упоминалось в предыдущем примере, при открытии кода Derived в Visual Studio компилятор сообщает, что для устранения неоднозначности в объявление метода Parselt производного класса следует включить ключевое слово Override или Shadows.
Просмотр иерархии наследования
С усложнением иерархии классов в программе на помощь приходит окно классов и Object Browser. Например, из окна классов на Рисунок 5.1 видно, что класс Programmer является производным от класса Employee и переопределяет только конструктор и метод RaiseSalary.
Сортировка по нестандартному критерию с использованием IComparable Интерфейс IComparer

.NET Framework позволяет выполнять сортировку по нескольким критериям. Например, чтобы упорядочить массив работников сначала по заработной плате, а затем по имени (в группах с одинаковой зарплатой) следует реализовать интерфейс IComparer, содержащий единственный метод СотрагеТо. При этом вы сможете воспользоваться одной из перегруженных версий Array. Sort (или ArrayList. Sort), которая имеет следующую сигнатуру:
Public Shared Sub Sort(ByVal array As Array. ByVal comparer As IComparer)
Обычно в программе создается отдельный класс, реализующий IComparer, и экземпляр этого класса передается методу Sort. Пример такого класса приведен ниже. Обратите внимание на выделенную строку — в ней имена работников передаются в виде строк методу Compare класса String:
Public Class SortByName
Implements IComparer
Public Function CompareTo(ByVal firstEmp As Object.ByVal
secondEmp=As Object) As Integer Implements IComparer.Compare
Dim temp1 As Employee = CType(firstEmp,Employee)
Dim temp2 As Employee = CType(secondEmp.Employee)
Return
String.Compare(templ.TheName. temp2.TheName)
End Function
End Class
Пример процедуры Sub Main с использованием этого класса:
SubMain()
Dim torn As New Employee("Tom", 50000)
Dim sally As New Employee("Sally". 60000)
Dim sam As New Employee("Sam". 60000)
Dim ted As New Employee("Ted". 50000)
Dim theEmployees() As Employee = _
{torn.sally,sam.ted}
Array.Sort(theEmployees)
Dim SortingByName As SortByName = New SortByName()
Array.Sort(theEmployees,SortingByName)
Dim aEmployee As Employee
For Each aEmployee In theEmployees
Console.WriteLine(aEmployee.TheName & "has yearly salary $" &
FormatNumberCaEmployee.Salary))
Next
Console. ReadLine() End Sub .
Результат показан на Рисунок 5.10,
Рисунок 5.10. Сортировка по нескольким критериям с использованием IComparer
В программе можно определить несколько классов, реализующих IComparer. Их последовательное применение позволяет выполнять многоуровневую сортировку произвольной глубины.
Члены интерфейса
Интерфейс ICollection реализуется классом System.Collections.CollectionBase.Важнейшие интерфейсы .NET Framework
Описать все интерфейсы .NET Framework на нескольких страницах невозможно, но хотя бы получить некоторое представление о них вполне реально. Интерфейсы ICloneable и IDisposable обладают особой декларативной функцией — реализуя их, вы тем самым заявляете, что ваш класс обладает некой стандартной функциональностью, присутствующей во многих классах.реализация циклов For-Each в VB6, они станут для вас настоящим подарком!
Выбор между интерфейсами и наследованием
Хотя на первый взгляд интерфейсы чем-то напоминают базовые классы, от этой аналогии больше вреда, чем пользы. Абстрактный класс может содержать реализованные методы, а в интерфейсе они недопустимы. Абстрактные базовые классы создаются только в результате тщательного анализа функциональности с выделением самого примитивного общего предка, и ни по какой другой причине.Интерфейсы существуют вне иерархии наследования, и в этом их достоинство. Вы теряете возможность автоматического использования существующего кода, но взамен приобретаете свободу выбора собственной реализации контракта. Интерфейсы используются в тех случаях, когда вы хотите показать, что поведение класса должно соответствовать определенным правилам, но фактическая реализация этих правил остается на усмотрение класса. В .NET структуры не могут наследовать ни от чего, кроме Object, но они могут реализовывать интерфейсы. Наконец, в .NET интерфейсы становятся единственным решением в ситуации, когда два класса обладают сходным поведением, но не имеют общего предка, частными случаями которого они бы являлись.
Замещение

Термин «замещение» (shadowing) встречался и в ранних версиях VB, и в большинстве языков программирования. Локальная переменная, имя которой совпадает с именем переменной, обладающей более широкой областью видимости, замещает (скрывает) эту переменную. Кстати, это одна из причин, по которой переменным уровня модуля обычно присваиваются префиксы m_, а глобальные переменные снабжаются префиксами g_ — грамотный выбор имен помогает избежать ошибок замещения. Переопределение унаследованного метода тоже можно рассматривать как своего рода замещение. В VB .NET поддерживается еще одна, чрезвычайно мощная разновидность замещения:
Член производного класса, помеченный ключевым словом Shadows (которое впервые появилось в бета-версии 2), замещает все одноименные члены базового класса.
При помощи ключевого слова Shadows можно определить в производном классе функцию, имя которой совпадает с именем процедуры базового класса. С практической точки зрения ключевое слово Shadows приводит к тому, что в производном классе появляется абсолютно новый член с заданным именем, в результате чего все одноименные унаследованные члены становятся недоступными в производном классе. Из этого следует, что унаследованные члены класса с ключевым словом Shadows невозможно переопределить, поэтому полиморфизм перестает работать.
По умолчанию VB .NET разрешает замещение членов классов, но при отсутствии клю-чевого слова Shadows выдается предупреждение. Кроме того, если один член класса объявляется с ключевым словом Shadows или Overloads, это ключевое слово должно использоваться и для остальных членов класса с тем же именем.
Иногда замещение усложняет ситуацию и приводит к возникновению нетривиальных ошибок — например, при полиморфном вызове замещенных методов и свойств через объект базового класса. Чтобы рассмотреть эти проблемы на конкретном примере, мы внесем некоторые изменения в класс Programmer (новые строки выделены жирным шрифтом):
Public Class Programmer Inherits Employee Private m_gadget As String
Private m_HowToCallMe As String = "Code guru "
Public Sub NewCByVal theName As String, ByVal curSalary As Decimal)
MyBase.New(theName, curSalary)
m_HowToCal1Me = m_HowToCallMe StheName
End Sub
Public Overloads Overrides Sub RaiseSalary(ByVal Percent As Decimal)
MyBase.RaiseSalary(1.2D * Percent, "special")
End Sub
Public Shadows Readonly Property TheName() As String
Get
Return mJtowToCallMe
End Get
End Property
End Class
А теперь попробуйте запустить новый вариант процедуры Sub Main:
Sub Main()
Dim torn As New Employee('Tom". 50000)
Dim sally As New Programmer("Sally". 150000)
Console.WriteLinetsally.TheName)
Dim ourEmployees(l) As Employee
ourEmployees(0)= tom
ourEmployees(l)= sally
Dim anEmployee As Employee
For Each anEmployee In ourEmployees
anEmployee.RaiseSalary(0.lD)
Console.WriteLinetanEmployee.TheName & "salary now is " &
anEmployee. Salary())
Next
Console. ReadLine()
End Sub
Знакомство с наследованием
Начнем с повторения некоторых терминов. Класс, взятый за основу при определении нового класса, называется базовым классом. Класс, определяемый на основе базового класса, называется производным классом. Производный класс автоматически наследует всю открытую функциональность базового класса, однако эта функциональность может переопределяться в производном классе и дополняться новыми возможностями.Следующий пример наглядно показывает, как это происходит. Допустим, у нас имеется компания с передовой политикой в области материального стимулирования. Каждый раз, когда заработная плата всех служащих компании повышается на 5%, для программистов прибавка составляет 6%. Вам поручено разработать систему учета кадров для этой компании. Вы решаете определить класс Programmer, производный от Employee, и переопределить метод RaiseSal агу в классе Programmer, чтобы отразить автоматическую (и вполне заслуженную!) надбавку.
Итак, приступим к программированию цепочки наследования Employee—>Programmer. Допустим, у нас уже имеется класс Publiс Employee, который входит в решение или включается в него командой Project > References. В этом случае начало кода класса Programmer будет выглядеть так (ключевая строка выделена жирным шрифтом):
Public Class Programmer
Inherits Employee
End Class
Ключевое слово Inherits должно находиться в первой не пустой и не содержащей комментария строке после имени производного класса (кстати, IntelliSense подскажет имена возможных базовых классов). Учтите, что производный класс не может объявляться с модификатором Publ i с, если базовый класс объявлялся с модификатором Friend или Private. Это связано с тем, что модификатор уровня доступа в производном классе не может быть менее ограничивающим, чем модификатор базового класса. С другой стороны, он может устанавливать более жесткие ограничения, поэтому от базового класса с уровнем доступа Publ i с можно объявить производный класс с уровнем Friend.
Следующим шагом в построении производного класса должно стать правильное определение конструктора. Поскольку производный класс должен обладать как минимум теми же возможностями, что и базовый, конструкторы производных классов часто вызывают конструкторы базовых классов для правильной инициализации полей базового класса и передают им аргументы, которые должны использоваться при инициализации. При этом используется специальное ключевое слово MyBase:
Public Sub New(ByVal theName As String, ByVal curSalary As Decimal)
MyBase.NewCName.curSalary)
End Sub
Ключевая строка, выделенная жирным шрифтом, вызывает конструктор базового класса Empl oyee и инициализирует его поля. Если вы забудете вызвать MyBase. New в том случае, когда конструктор базового класса вызывается с аргументами, VB .NET выдает сообщение об ошибке следующего вида:
C:\vb net book \chapter 5 \Examplel \Examplel \Modulel.vb(55):
'Examplel.Programmer'.the base class of 'Examplel.Employee'.
does not have an accessible constructor that can be called with
no arguments. Therefore.the first statement of this constructor
must be a call to a constructor of the base class via 'MyBase.New'
or another constructor of this class via 'MyClass.New' or 'Me.New'.
Хорошо бы, чтобы все сообщения об ошибках были настолько содержательными и понятными. Компилятор напоминает о том, что при отсутствии у базового класса безаргументного конструктора производный класс должен содержать хотя бы один вызов MyBase. New. После включения в программу вызова MyBase. New возникает очень интересный вопрос: как обращаться к полям базового класса? Следующее правило на первый взгляд может вас удивить:
Производный класс не обладает привилегированным доступом к полям базовою класса.
Из этого правила следует, что производный класс Programmer не получает доступа к закрытым полям базового класса Employee. Предположим, заработная плата хранится в закрытом поле базового класса с именем m_Sal ary и вы пытаетесь включить в код метода RaiseSalary класса Programmer следующий фрагмент:
Public Sub New(ByVal theName As String. ByVal curSalary As Decimal)
MyBase.New(theName. curSalary)
MyBase.m_salary = 1.2 * curSalary End Sub
Компилятор выдает сообщение об ошибке:
'Examplel.Employee.m_Salary'is Private.and is not accessible
in this context.
В повседневной жизни существует хорошая аналогия — родители устанавливают правила поведения для детей, а не наоборот.
Что же делать? Если вы хотите, чтобы производный класс получил доступ к некоторым возможностям базового класса, об этом должен позаботиться сам базовый класс. В следующем разделе будет показано, как это делается.
Cамоучитель по VB.NET
Делегаты и события
Мы рассмотрели разнообразные примеры использования делегатов, однако ни один из них не имел отношения к обработке событий. Впрочем, связь между делегатами и событиями в VB .NET весьма проста. При каждом использовании сокращенного синтаксиса обработки событий, описанного в первой половине главы, VB .NET незаметно определяет класс делегата для обработки события, а команда AddressOf создает экземпляр делегата для этого обработчика. Например, следующие две строки эквивалентны (EventHandler — имя неявно определяемого делегата):AddHandler Buttonl.Click.AddressOf Me.Buttonl_Click
AddHandler Buttonl.Click.New EventHandler(AddressOf Buttonl Click)
В сущности, каждое событие соответствует делегату следующего вида:
Public Delegate Event (sender As Object.evt As EventArgs)
Вызов RaiseEvent просто приводит к вызову Invoke для автоматически сгенерированного делегата.
Делегаты
При использовании механизма обратного вызова приходится выполнять вспомогательные операции для регистрации вызываемых функций. В оставшейся части главы будет показано, что при этом происходит и как при помощи этих операций добиться максимальной эффективности обратного вызова.Механизм обратного вызова (а следовательно, и события) в VB .NET зависит от особой разновидности объектов .NET, называемых делегатами. Делегат является экземпляром класса System.Delegate. В простейшем случае в делегате инкапсулируется объект и адрес заданной функции или процедуры этого объекта. Такие делегаты идеально подходят для схем обратного вызова вроде той, что используется при обработке событий. Почему? Потому что делегат содержит всю информацию, необходимую для обратного вызова, и может использоваться для вызова нужного метода объекта-приемника.
Но прежде, чем переходить к описанию работы с делегатами, стоит подчеркнуть одно важное обстоятельство. Хотя обработка событий на платформе .NET основана на использовании делегатов, в подавляющем большинстве случаев вам не придется работать непосредственно с делегатами. Команда AddHandl ег предоставляет в ваше распоряжение все необходимое для гибкой обработки событий в VB .NET (впрочем, как вы вскоре увидите, у делегатов есть и другие применения).
В делегатах также могут инкапсулироваться общие методы класса без привязки ккон-кретному экземпляру. Кроме того, в групповых делегатах инкапсулируется сразу несколько объектов с несколькими процедурами.
Традиционные средства вызова функции по адресу были основаны на языковой поддержке указателей на функции. Как показано в следующей врезке, применение указателей на функции сопряжено с потенциальным риском. По сравнению с указателями на функции делегаты обеспечивают дополнительную защиту, значение которой не стоит недооценивать.
Указатели на функции в VB6 . При вызовах функций API часто передается адрес функции для обратного вызова, поэтому в VB6 поддерживался оператор AddressOf. В VB6 адрес функции мог передаваться при любом вызове API. Но что происходило, если список параметров функции, адрес которой передавался при вызове, отличался от предполагаемого? Обычно это приводило к общей ошибке защиты (GPF) и даже к фатальным сбоям с появлением синего экрана.
Таким образом, произвольные указатели на функции обладают принципиальным недостатком: компилятор не может проверить, что такой указатель относится к функции правильного типа. Делегаты представляют собой разновидность указателей на функции, безопасных по отношению к типам. Следуя принципу «доверяй, но проверяй», компилятор автоматически проверяет сигнатуру вызываемой функции — такой вариант работает гораздо надежнее.
Динамическая обработка событий

Основной проблемой синтаксиса WithEvents является его недостаточная гибкость. Обработчики событий нельзя динамически устанавливать и отключать на программном уровне — фактически вся схема обработки событий жестко фиксируется в программе. Однако в VB .NET поддерживается другой способ динамической обработки событий, значительно более гибкий. Он основан на возможности указания процедуры класса-приемника, вызываемой при возникновении события (исключение добавленных обработчиков также происходит динамически).
Конечно, для установки обработчика события необходимо зарегистрировать не только класс-приемник, но и метод, который должен вызываться при возникновении события. Для этой цели применяется команда AddHandler, которой при вызове передаются два параметра:
tom:
AddHandler tom.SalarySecurityEvent.AddressOf anEmp1oyee_SalarySecurityEvent
В результате тестовая программа будет обнаруживать событие Sal arySecuri tyEvent объекта tom и в случае его возникновения — вызывать процедуру anEmployee_SalarySecurityEvent текущего модуля (разумеется, процедура anEmployee_SalarySecurityEvent должна обладать правильной сигнатурой!).
Ниже приведен фрагмент решения AddHandlerExamplel (ключевые строки выделены жирным шрифтом):
Module Modulel
Private WithEvents anEmployee As EmployeeWithEvents Sub Main()
Dim torn As New EmployeeWithEvents("Tom". 100000)
Console.WriteLine(tom.TheName & "has salary " & tom.Salary)
AddHandler tom.SalarySecurityEvent,
AddressOf anEmployee_SalarySecurityEvent
tom.RaiseSalary(0.2D) ' Суффикс D - признак типа Decimal
Console.WriteLine(tom.TheName & "still has salary " & tom.Salary)
Console.WriteLine("Please press the Enter key")
Console. ReadLine()
End Sub
Public Sub anEmployee_SalarySecurity£vent(ByVal Sender _
As AddHandlerExamplel.EmployeeWi thEvents,_
ByVal e As AddHandlerExamplel.ImproperSalaryRaiseEvent)_
Handles anEmployee.SalarySecurityEvent
MsgBox(Sender.TheName & "had an improper salary raise of " & _
FormatPercent(e.theRaise) & "with INCORRECT PASSWORD!")
End Sub
End Module
Команда AddHandler обладает просто невероятной гибкостью. Например, установка обработчиков событий может зависеть от имени типа:
If TypeName(tom)="Manager" Then
AddHandler tom.SalarySecurityEvent.AddressOf _
anEmployee_SalarySecurityEvent e
End If
Кроме того, один обработчик событий можно связать с несколькими разными событиями, происходящими в разных классах. Это позволяет выполнять в VB .NET централизованную обработку событий с динамическим назначением обработчиков — в VB такая возможность встречается впервые. В приведенном ниже листинге инициируются разные события в зависимости от переданных параметров командной строки. Главное место в нем занимают фрагменты вида
Case "first"
AddHandler m_EventGenerator.TestEvent,_
AddressOf m_EventGenerator_TestEventl
При передаче в командной строке аргумента first устанавливается соответствующий обработчик события.
В программе используется полезный метод GetCommandLineArgs класса System.Environment. Как упоминалось в главе 3, этот метод возвращает массив аргументов командной строки. Начальный элемент массива содержит имя исполняемого файла; поскольку индексация массива начинается с 0, для получения первого аргумента используется вызов System.Environment.GetComman3LineArgs(l), однако предварительно необходимо убедиться в существовании аргументов командной строки, для чего проверяется длина массива System.Environment.GetCommandLineArgs. Перед запуском программы перейдите на страницу Configuration Properties диалогового окна Project Properties и укажите аргументы командной строки для тестирования.
Ниже приведен полный исходный текст программы:
Option Strict On Module Modulel
Private m_EventGenerator As EventGenerator
Sub Main()
m_EventGenerator= New EventGenerator()
Dim commandLinesOAs String = System.Environment.GetCommandLineArgs
If commandLines.Length = 1 Then
MsgBox("No command argument.program ending!")
Environment.Exit(-l) Else
Dim theCommand As String = commandLines(l)
Console.WriteLine("Thecommand lineoption is" StheCommand)
' Проверить параметр командной строки и назначить
' соответствующий обработчик события.
Select Case theCommand Case "first"
AddHandler m_EventGenerator.TestEvent. AddressOf
m_EventGenerator_TestEvent1
Case "second"
AddHandler m_EventGenerator.TestEvent,_ AddressOf
m_EventGenerator_TestEvent2
Case Else
AddHandler m_EventGenerator.TestEvent. AddressOf
m_EventGenerator_TestEventDefault
End Select
' Инициировать события
m_EventGenerator.TriggerEvents()
End If
Console.WriteLine("Press enter to end.")
Console. ReadLine()
End Sub
'Обработчик по умолчанию для непустой командной строки
Public Sub m_EventGenerator_TestEventDefault(_
ByVal sender As Object.ByVal evt As EventArgs) System.Console.WriteLine("Default choice " & _
m_EventGenerator.GetDescri pti on()) End Sub
' Обработчик 12 для строки "first"
Public Sub m_EventGenerator_TestEvent1(_
ByVal sender As Object.ByVal evt As EventArgs)
System.Console.WriteLineC'lst choice " & _
m_EventGenerator.GetDescription()) End Sub
'Обработчик 13 для строки "second"
Public Sub m_EventGenerator_TestEvent2(
ByVal sender As Object.ByVal evt As EventArgs)
System.Console.WriteLinet"2nd choice " & _
m_EventGenerator.GetDescri pti on ())
End Sub
End Module
Public Class EventGenerator
' В классе определяется только одно событие
Public Event TestEvent(ByVal sender As Object, ByValevt As EventArgs)
' Также можно было использовать конструктор по умолчанию
Public Sub New()
' Пустой конструктор
End Sub
.Public Function GetDescription() As String
Return "EventGenerator class"
End Function
' Процедура вызывается для инициирования событий
Public Sub TriggerEvents()
Dim e As System.EventArgs = New System.EventArgs()
RaiseEvent TestEvent(Me.e)
End Sub
End Class
Групповые делегаты как члены классов

В предыдущем примере все модули имеют доступ ко всем функциям остальных модулей. Такую архитектуру нельзя признать удачной — правильнее было бы оформить делегат в виде члена класса, нежели в виде открытого объекта. Это позволит выполнить перед его созданием проверку, аналогичную той, которая выполняется для других членов класса. Ниже приведен слегка измененный вариант предыду-
щей архитектуры, где перед дополнением группового делегата новыми функциями выполняется проверка (в данном примере — весьма тривиальная). Соответствующий фрагмент выделен жирным шрифтом:
Option Strict On
Public Class DelegateServer
Public Delegate Sub ClientCallback(ByVal IngVal As Long)
Private m_Clients As ClientCallback
' Использовать конструктор по умолчанию
Public Sub RegisterDelegate(ByVal aDelegate As
ClientCallback.ByVal dolt As Boolean)
' Обычно здесь выполняется полноценная проверка.
' В данном примере функция обратного вызова регистрируется
' лишь в том случае, если второй параметр равен
True. If dolt Then
m_Clients = CType(System.Delegate.Combine(m_ Clients.aDelegate)._
ClientCallback)
End If
End Sub
Public Sub CallClients(ByVal IngVal As Long)
m_Clients( IngVal)
End Sub
End Class
Module Modulel
Sub Main()
Dim delsrv As New DelegateServer()
delsrv.RegisterDelegate(AddressOf DelegateCallbackHandlerl.True)
' He вызывается - второй параметр равен False!
delsrv.RegisterDelegate(AddressOf DelegateCal1backHandler2.False)
' Инициировать обращение к клиентам
delsrv.CallClients(125)
Console.WriteLine("Press enter to end.")
Console.ReadLine()
End Sub
Public Sub DelegateCallbackHandlerKByValIngVal As Long)
System.Console.WriteLine("DelegateCa11backHandlerl cal1ed")
End Sub
Public Sub DelegateCallbackHandler2(ByVal IngVal As Long)
System.Console.Wri teLine("DelegateCal1backHandler2 cal1ed")
End Sub
End Module
Групповые делегаты
В приведенных выше примерах в делегате инкапсулировался адрес одной функции или процедуры. Нередко в делегатах требуется инкапсулировать сразу несколько процедур (инкапсуляция нескольких функций особого смысла не имеет — каким должно быть возвращаемое значение?). Подобные делегаты называются групповыми (multicast) и реализуются в виде делегата, содержащего несколько однотипных делегатов. При наличии группового делегата все инкапсулированные процедуры вызываются одним методом Invoke, причем это происходит в соответствии с порядком занесения их делегатов в групповой делегат.Чтобы создать групповой делегат, следует объединить минимум двух делегатов одного типа и присвоить результат переменной того же типа. Задача решается статическим методом Combine класса System.Delegate, который возвращает новый делегат.
Допустим, firstDel и secDel — экземпляры класса MyMultiCastDelegate. Следующая команда объединяет firstDel и secDel в групповой делегат, хранящийся в
firstDel: firstDel =System.Delegate.Combine(firstDel,secDel)
Ниже приведено простое приложение, объединяющее адреса нескольких функций в групповом делегате:
1 Option Strict On
2 Module Modulel
3 Sub Main()
4 Console.WriteLine("Calling delegate function...")
5 RegisterDelegate(AddressOf CallBackHandlerl)
6 RegisterDelegate(AddressOf CallBackHandler2)
7 Call Delegates ()
8 Console.WriteLine(
9 "Finished calling.delegate function...")
10 Console.ReadLine()
11 End Sub
12 Public Sub CallBackHandlerHByVal lngVal As RETURNJALUES)
13 Console.WriteLine("Callback 1 returned " & IngVal)
14 End Sub
15 Public Sub CallBackHandler2(ByVallngVal As RETURNJALUES)
16 Console.WriteLine("Callback 2 returned " & IngVal)
17 End Sub
18 End Module
19 Module Module2
20 Public Delegate Sub CallBackFunc(ByVallngValAs RETURN_VALUES)
21 Private m_cbFunc As CallBackFunc
22 Public Enum RETURN_VALUES
23 VALUE_SUCCESS
24 VALUE_FAILURE
25 End Enum
26 Public Sub RegisterDelegate(ByRef cbFunc As CallBackFunc)
27 m_cbFunc = CType(System.Delegate.Combine(_
28 m_cbFunc.cbFunc).CallBackFunc)
29 End Sub
30 Public Sub Call Delegates ()
31 Dim IngCounter As Long = 0
32 ' Вызвать процедуры через делегата
33 ' и вернуть признак успешного вызова
34 m_cbFunc(RETURN VALUES.VALUE_SUCCESS)
35 End Sub
36 End Module
В строках 5 и 6 вызывается процедура модуля Module2 (строки 26-28), где и происходит фактическое построение группового делегата. Это возможно благодаря тому, что делегат передается по ссылке, а не по значению. Обратите внимание на преобразование типа метода Combine к типу делегата в строке 27. Непосредственный вызов функций группового делегата происходит в строках 30-35. Всем зарегистрированным функциям передается значение перечисляемого типа RETURNJALUES . VALUE_SUCCESS. Результат выполнения программы показан на рисунке.
Обработка событий с точки зрения ООП
Поскольку сущность объектно-ориентированного программирования в конечном счете сводится к обмену сообщениями между взаимодействующими объектами, события должны занимать определенное место в этой схеме. В каком-то отношении они нормально вписываются в нее: объект-источник отправляет сообщение, которое указывает на возникновение события.Но при этом возникает очевидная проблема: каким объектам следует отправлять сообщения? Оповещать о каждом событии все объекты, в настоящий момент существующие в программе? Это было бы слишком неэффективно. Для большийства объектов событие не представляет ни малейшего интереса, а быстродействие станет неприемлемо низким.
Вместо этого VB .NET пытается ограничить число получателей события, для чего используется модель «подписка/публикация». В этой модели объекты-приемники событий регистрируют объекты-источники тех событий, которые представляют для них интерес. На события от одного источника могут подписаться сразу несколько объектов-приемников. О том, что источник инициировал событие, оповещаются только зарегистрированные получатели.
Впрочем, реализовать подобную схему не так просто. Какие сообщения должны передаваться приемнику от источника? Как организовать их отправку? Что должно происходить при получении сообщения? Как говорилось выше, взаимодействие между объектами на базе обмена сообщениями строится на вызове методов класса-приемника. В конечном счете обработка событий строится по тому же принципу, но при этом приходится учитывать ряд дополнительных тонкостей.
Общий смысл происходящего заключается в том, что при возникновении события объект-источник вызывает заранее определенные функции объектов-приемников. Вызываемая функция приемника регистрируется источником события одновременно с регистрацией объекта-приемника. Такая схема называется оповещением посредством обратного вызова (callback notification), потому что источник события вызывает метод приемника по заранее известному ему адресу. На Рисунок 6.1 показан объект-«начальник» с событием HighRating, при возникновении вызываются разные методы объектов-приемников. Во второй половине этой главы будет рассказано, как это происходит в VB .NET.
Чтобы регистрация прошла успешно, методы объекта-приемника должны иметь строго определенную форму (например, обладать конкретной сигнатурой). По этой причине в некоторых языках (например, в Java) механизм обратного вызова реализуется с применением интерфейсов — это гарантирует наличие метода с правильной сигнатурой.
Обработка событий в иерархии наследования
Производный класс может в любой момент инициировать открытые или защищенные события своего базового класса, при этом событие идентифицируется ключевым словом MyBase. Кроме того, производные классы автоматически наследуют все обработчики открытых и защищенных событий своих предков. Время от времени в производном классе возникает необходимость в переопределении методов, используемых при обработке открытых и защищенных событий базового класса. Для этой цели используется конструкция Handles MyBase. Пример:Public Class ParentClass
Public Event ParentEventtByVal aThing As Object.
ByVal E As System.EventArgs)
' Программный код End Class
' Производный класс
Public Class ChildClass
Inherits ParentClass
Sub EventHandler(ByVal x As Integer)
Handles MyBase ParentEvent
'Обработка событий базового класса
End Sub
End Class
Отключение обработчиков событий
Обработчики событий, динамически назначаемые командой AddHandler, отключаются командой RemoveHandler, которой должны передаваться точно такие же аргументы, как и при соответствующем вызове AddHandlеr. Обычно для удаления динамически назначаемых обработчиков хорошо подходит метод Dispose. По этой причине в каждом классе, использующем динамическое назначение обработчиков, рекомендуется реализовать интерфейс IDisposable — это напомнит пользователям класса о необходимости вызова Dispose.Подключение приемников к источнику
В нашем распоряжении имеется весь код, необходимый для рассылки событий, но пока нет ни одного заинтересованного получателя. Существует несколько способов, которыми класс может сообщить VB .NET о своем желании получать события от другого класса. Простейший способ очень похож на тот, который использовался в VB6: на уровне модуля (или класса) объявляется переменная класса-приемника с ключевым словом WithEvents. Например, если включить в класс следующую строку, не входящую ни в один из членов: Private WithEvents anEmployee As Employeeобъекты этого класса становятся потенциальными приемниками событий, инициируемых классом Employee. Обратите особое внимание на некоторые особенности этого объявления:
Построение классов событий
В предыдущем примере мы воспользовались готовым классом System.EventArgs. Возможности этого класса весьма ограничены, поскольку его конструктор вызывается без аргументов. При более профессиональном подходе в программе определяется новый класс события, дополняющий этот базовый класс. Например, в него можно включить ReadOnly-свойство, возвращающее информацию о предполагаемом повышении зарплаты, и другое свойство для текста сообщения. Пример подобного класса приведен ниже (решение CustomEventArgExample в архиве). Запрашиваемый рост зарплаты и сообщение инкапсулируются в конструкторе события. В дальнейшем для получения этих данных используются два свойства, доступных только для чтения:Public Class ImproperSalaryRaiseEvent
Inherits System.EventArgs
Private m_Message As String
Private m_theRaise As Decimal
Sub New(ByVal theRaise As Decimal. ByVal theReason As String)
MyBase.New()
m_Message = theReason
m_theRaise = theRaise
End Sub
Readonly Property Message() As String
Get
Return m_Message
End Get End Property Readonly Property theRaise() As Decimal
Get
Return m_theRaise
End Get
End Property
End Class
После того как этот класс будет включен в решение, следует внести небольшие изменения в объявление события в классе Empl oyee:
Public Event SalarySecurityEvent(ByVal Sender As
CustomEventArgExample.EmployeeWithEvents. ByVale As
ImproperSalaryRaiseEvent)
Теперь во втором аргументе передается переменная класса ImproperSalaryRai seEvent. Следующие изменения вносятся во фрагмент, в котором непосредственно вызывается событие:
Public Overloads Sub RaiseSalary(ByVal Percent As Decimal)
If Percent > LIMIT Then
' Операция запрещена - необходим пароль
RaiseEvent SalarySecurityEvent(Me,
New ImproperSalaryRaiseEvent(Percent, "INCORRECT PASSWORD!"))
Else
m_Salary =(1 + Percent) * m_Salary
End If
End Sub
Остается лишь слегка исправить код обработчика события (изменения выделены жирным шрифтом).
Module Modulel
Private WithEvents anEmployee As EmployeeWithEventsII Sub Maine)
Dim tom As New EmployeeWithEventsII("Tom". 100000)
anEmployee = tom
Console.Wntel_ine(tom.TheName &"has salary " & tom.Salary)
anEmployee.RaiseSalary(0.2D)'Суффикс D - признак типа Decimal
Console.WriteLine(tom.TheName & "still has salary " & tom.Salary)
Console.Writeline("Please press the Enter key")
Console.ReadLine()
End Sub
Public Sub anEmployee_SalarySecuhtyEvent(ByVal Sender _ As
CustomEventArgExample.EmployeeWithEvents. ByVal e As
CustomEventArgExample.ImproperSalaryRaiseEvent) Handles
anEmployee.SalarySecurityEvent
MsgBox(Sender.TheName & "had an improper salary raise of " & _ FormatPercent(e.theRaise) & "with INCORRECT PASSWORD!")
End Sub
End Module
Результат показан на следующем рисунке. Как видно из рисунка, данные о запрошенном росте заработной платы доступны в обработчике события.
Практический пример: специализированная сортировка
Предыдущие примеры выглядят искусственно и относятся к категории «игрушечных программ». В этом разделе мы покажем, как использовать делегаты при специализированной сортировке — одной из стандартных областей применения функций обратного вызова. Общая идея заключается в том, что один метод сортировки в зависимости от ситуации может использовать разные критерии сортировки. Предположим, у вас имеется массив имен: «Mike Item», «Dave Mendlen», «Alan Carter», «Tony Goodhew», «Ari Bixhorn», «Susan Warren»-.Если вызвать метод Sort класса Array, сортировка будет произведена по именам. А если вы хотите отсортировать массив по фамилиям?
Возможный подход к решению этой задачи описан в главе 5 — вы можете написать собственную реализацию интерфейса IComparer и передать ее Sort. Применение обратного вызова на базе делегатов выглядит чуть более элегантно и теоретически обладает большей гибкостью, поскольку программист может определить собственные процедуры сортировки, которые в конкретной ситуации работают эффективнее стандартных алгоритмов.
Чтобы массив поддерживал сортировку по именам, следует определить класс с несколькими методами Compare и при помощи делегата связать алгоритм сортировки с нужным методом Compare через механизм обратного вызова. В частности, это позволит динамически изменять критерий сортировки во время работы программы.
Прежде всего определяется класс, выполняющий сортировку. Чтобы избежать подробного обсуждения различных алгоритмов сортировки, мы воспользуемся простейшим алгоритмом волновой сортировки:
If Stuff(j) < Stuff(i))Then
temp = Stuff(i)
Stuff(i) = Stuff(j)
Stuff(j) = temp
End If
Next j
Next I
Чтобы реализовать этот алгоритм с применением функций обратного вызова, необходимо определить класс Special Sort с делегатом, используемым при обратном вызове. Код этого класса приведен ниже:
1 Public Class Special Sort
2 ' Определение делегата
3 Public Delegate Function SpecialCompareCallback(ByVal flrstString _
As String,ByVal secondString As String) As Boolean
4 ' Определение процедуры, вызываемой делегатом
5 Public Shared Sub IfySort(ByVal Stuff As String()._
ByVal MyCompare As SpecialCompareCallback)
6 Dim i, j As Integer
7 Dim temp As String
8 Dim bottom As Integer = Stuff.GetLowerBound(0)
9 Dim top As Integer = Stuff.GetUpperBound(0)
10 For i = bottom To (top = bottom)
11 For j = i + 1 To top
12 If MyCompare(Stuff(j). Stuff(i)) Then
13 temp = Stuff(i)
14 Stuff(1) - Stuff (j)
15 Stuff(j) = temp
16 End If
17 Next j
18 Next i
19 End Sub
20 End Class
В строке З определяется делегат, при помощи которого классу передается информация об используемом порядке сортировки. Делегат может инкапсулировать любую функцию, которая, как и все нормальные функции сравнения строк, получает два строковых параметра и возвращает логическую величину.
В строке 5 определяется общая процедура, одним из параметров которой является переменная с типом делегата. Таким образом, в ключевой строке 12:
If MyCompare(Stuff(j). Stuff(i)) Then
функция сравнения, инкапсулированная в делегате MyCompare, может относиться к другому классу! Например, если определить приведенный ниже класс, эта схема позволит использовать любой из его методов Compare (обратите внимание: методы Compare объявлены общими, поэтому для их вызова нам даже не нужно создавать конкретный экземпляр класса):
Public Class MyCustomCompare
Public Shared Function TheBasicComparetByVal firstString As String,
ByVal secondString As String) As Boolean
Return (firstString <- secondString)
End Function
Public Shared Function TheSpecialCompare(ByVal firstString As String.
ByVal secondString As String)As Boolean Dint tokensl,tokens2 As String()
tokensl = firstString.Split(Chr(32))
tokens2 = secondString.Split(Chr(32))
Return (tokensl(l) <- tokens2(l))
' Сравнение по фамилии!
End Function
End Class
Класс содержит две общие функции, которые ниже будут использованы для создания делегатов. Первая функция, TheBasicCompare, просто сравнивает строки в алфавитном порядке. Более интересная функция TheSpecialCompare предполагает, что строка передается в формате «имя фамилия», и сравнивает фамилии, выделяя их при помощи удобной функции Split.
Остается лишь создать экземпляры класса SpecialSort и делегаты. Это происходит в следующей функции Main (ключевые строки выделены жирным шрифтом):
1 Module Modulel
2 Sub Main()
3 Dim test()As String ={"Mike Iem"."Dave Mendlen"."Alan Carter".
4 "Tony Goodhew","An Bixhorn"."Susan Warren"}
5 ' Объявить переменную обратного вызова в форме класс.делегат
6 Dim MyCallBack As Special Sort.SpecialCompareCal1back
7 MyCallBack = AddressOf MyCustomCompare.TheBasicCompare
8 SpecialSort.MySort(test,MyCallBack)
9 Console.WriteLine("Here is a basic sort by FIRST name")
10 Dim temp As String
11 For Each temp In test
12 Console.WriteLine(temp)
13 Next
14 ' Передать другую процедуру сравнения
15 MyCallBack = AddressOf MyCustomCompare.TheSpecialCompare
16 Sped al Sort. MySort (test. MyCallBack)
17 Console.WriteLine()
18 Console.WriteLineC'Here is a sort by LAST name")
19 For Each temp In test
20 Console.WriteLine(temp)
21 Next
22 Console. ReadLine()
23 End Sub
24 End Module
В строке 6 объявляется «псевдоуказатель на функцию». Чтобы задать его значение, мы передаем адрес функции с правильной сигнатурой (строки 7-15). Поскольку функции объявлены общими, создавать экземпляр класса MyCustomCompare для этого не нужно. После создания делегата в строках 8 и 16 вызывается нужная процедура сортировки класса Special Sort. Поскольку при вызове MySort передается делегат, процедура обращается к классу MyCustomCompare и узнает, по какому критерию должно осуществляться сравнение.
Простейшее инициирование событий
Давайте вернемся к простому классу Empl oyee и подробно, шаг за шагом разберем все, что необходимо сделать для определения и инициирования событий. Предположим, событие должно инициироваться при попытке увеличения заработной платы более чем на 10 процентов без ввода пароля. В главе 4 метод RaiseSalary выглядел так:Public Overloads Sub RaiseSalary(ByVal percent As Decimal)
If percent > LIMIT Then
' Операция запрещена - Необходим пароль
Console.WriteLine("MUST HAVE PASSWORD TO RAISE SALARY " & _
"MORE THAN LIMIT!!!!") Else
m_Sa1ary =(1 + percent) * m_salary
End If
End Sub
Вместо выделенной команды, выводящей текстовое сообщение на консоль, должно инициироваться событие. Задача решается в несколько этапов. В простейшем случае в классе сначала объявляется открытая переменная с ключевым словом Event, с указанием имени события и его параметров. Например, следующая строка весьма близка к синтаксису VB6: Public Event SalarySecurityEventdnessage as String) В этой строке объявляется открытое событие с параметром строкового типа.
События обычно объявляются с модификатором Public, никогда не возвращают значений и могут получать параметры произвольного типа, кроме ParamArray.
После того как переменная события будет определена, событие инициируется командой следующего вида (впрочем, для того, чтобы событие действительно произошло, потребуются еще кое-какие дополнительные действия):
RaiseEvent SalarySecurityEventC'MUST HAVE PASSWORD TO RAISE " & _
"Salary MORE THAN LIMIT!! !!")
Однако из этого не следует, что для любого события следует ограничиваться одним строковым параметром. В соответствии с парадигмой программирования .NET в качестве параметров любого события передается объект-источник и информация о событии, инкапсулированная в объекте события. На первых порах вполне достаточно объявления вида
Public Event SalarySecurityEvent(ByVal who As Employee, ByVale As system.EventArgs)
Событие инициируется следующей командой RaiseEvent:
RaiseEvent SalarySecurityEvent(Me,New System.EventArgs())
Хотя события обычно объявляются открытыми, это не является обязательным требованием — событие может иметь любой модификатор уровня доступа. Закрытыми (Private) объявляются события, представляющие интерес только для объектов этого класса, а защищенные (Protected) события также могут обрабатываться объектами производных классов. Допускается даже объявление общих (Shared) событий, которые, как и общие члены классов, существуют на уровне класса в целом, а не его отдельных членов (в частности, общие методы могут инициировать только общие события).
По сигнатуре события приемник узнает, от какого источника поступило событие (в данном примере это объект-работник, которому попытались неправильно повысить заработную плату); сам объект передается в виде ключевого слова Me. Впрочем, приведенное объявление не использует возможностей передачи данных в переменной события е. Вскоре мы разработаем класс, производный от System. EventArgs, в объектах которого будет содержаться строка предупреждения вместе с данными о попытке повышения заработной платы.
Схема оповещения посредством обратного

Конечно, вы можете определить собственную сигнатуру для методов объекта-приемника, вызываемых источником, однако в .NET существует практически общепринятое правило, согласно которому функции приемника передаются два параметра:
Private Sub Buttonl_Click(ByVal sender As System.Object.
ByValeAs System.EventArgs) Handles Button1.Click
End Sub
Параметры имеют следующий смысл:
Таким образом, процедура-обработчик может однозначно определить, какой объект был источником события.
В данном примере объект события е не представляет интереса, поскольку он не содержит сколько-нибудь полезной информации о событии. С другой стороны, в некоторых ситуациях он может пригодиться. Например, из объектной переменной класса MouseEventArgs можно узнать, в какой точке был сделан щелчок мышью. В общем случае программист определяет собственные классы событий, производные от класса System. EventArgs и содержащие любую полезную информацию о событии (о том, как это делается, будет рассказано ниже).
Также обратите внимание на новое ключевое слово Hand! es в определении процедуры события. Как нетрудно догадаться, это ключевое слово указывает, какие события обрабатываются данной процедурой. Возможно, в данном примере ключевое слово Handl es выглядит излишним, однако оно предоставляет программисту дополнительные возможности, поскольку теперь обработчики события не обязаны обладать жестко заданными именами (фиксируются только сигнатуры). Следовательно, одна процедура может обрабатывать несколько событий, для чего в конец объявления процедуры включаются несколько секций Handl es. Новый подход обладает большей гибкостью по сравнению с массивами управляющих элементов, использовавшимися в прежних версиях VB (в VB .NET массивы элементов не поддерживаются).
Хотя IDE генерирует процедуры событий со стандартными именами, в VB .NET это уже не является обязательным требованием. Если процедура имеет правильный набор параметров и в ее заголовке присутствует ключевое слово Handles, эта процедура может использоваться для обработки событий. Пример:
Private Sub MyClickProcedure(ByVal sender As System.Object,_
ByValeAs System.EventArgs) Handles Buttonl.Click
Процедура MyClickProcedure может обрабатывать событие Buttonl. Click благодаря наличию правильных параметров. Она обрабатывает это событие, поскольку в заголовке указано ключевое слово Handles. Главное новшество заключается в явном указании обрабатываемых событий с ключевым словом Handles.
Рассмотрим другой пример. Допустим, предыдущий фрагмент был приведен к следующему виду:
Private Sub MyClickProcedureCByVal sender As System.Object._
ByVal e As System.EventArgs) Handles Buttonl.Click. Button2.Click._
mnuTHing.Click
Теперь одна процедура обрабатывает события сразу от двух разных кнопок и команды меню! В VB6 подобная универсальность была невозможна, поскольку в прежних версиях VB обработчики событий вызывались по имени элемента. Надеемся, читатель согласится с тем, что ключевое слово Handl es обладает значительно большим потенциалом, чем массивы управляющих элементов.
| Содержание | Вперед |
Создание делегата
Начнем с создания простейшего делегата, инкапсулирующего объект и «указатель» на процедуру этого объекта. Как показано ниже, синтаксис создания объектов чуть сложнее синтаксиса, используемого при создании простых объектов. Прежде всего нам понадобится класс, содержащий процедуру с определенной сигнатурой:Class ClassForStringSubDelegate
' Использовать конструктор по умолчанию
Public Sub TestSub(ByVal aString As String)
Console. WriteLine(aString SaString)
End Sub
End Class
Чтобы создать делегат для обратного вызова этой процедуры, необходимо сообщить компилятору об использовании делегата для процедуры с одним строковым параметром. Первый шаг этого сценария выполняется за пределами Sub Main следующей строкой:
Public Delegate Sub StringSubDelegate(ByVal
aString As String)
Обратите внимание: в этой строке мы не объявляем делегат, а определяем его. Компилятор VB .NET автоматически создает новый класс StringSubDel egate, производный от System . Delegate1.
Далее в процедуре Sub Main экземпляр класса делегата создается оператором AddressOf для адреса процедуры, имеющей правильную сигнатуру. VB .NET автоматически вычисляет объект по полному имени процедуры. Команда создания экземпляра выглядит так:
aDel egate = AddressOf test.TestSub
Компилятор VB .NET понимает, что делегат создается для объекта test. Также можно воспользоваться ключевым словом New, однако это делается редко, поскольку New неявно вызывается в первой форме:
aDelegate = New StringSubDelegate(AddressOf test.TestSub)
После того как делегат будет создан, инкапсулированная в нем процедура вызывается методом Invoke класса Delegate, как в следующем фрагменте:
Sub Main( )
Dim test As New ClassForStri ngSubDelegate()
Dim aDelegate As StringSubDelegate
aDelegate = AddressOf test.TestSub
aDelegate.Invoke( "Hello" )
Console. ReadLineb
End Sub
На самом деле использовать Invoke необязательно — достаточно передать делегату нужные параметры. VB .NET поймет команду aDelegate(" Hello"), которая выглядит значительно проще.
В этом нетрудно убедиться, просматривая полученный IL-код при помощи программы ILDASM.
Согласитесь, такой способ вывода в консольном окне строки «HelloHello» выглядит несколько необычно!
Впрочем, «если это и безумие, то в своем роде последовательное». Предположим, вы решили усовершенствовать свой класс, чтобы вместо простого вывода текста в консольном окне на экране появлялось окно сообщения. Для этого достаточно внести изменения, выделенные жирным шрифтом в следующем листинге:
Module Modulel
Public Delegate Sub StringSubDelegate(ByVal aString As String)
Sub Main()
Dim test As New ClassForStringSubDelegate()
Dim aDelegate As StringSubDelegate
aDelegate - AddressOf test.TestMsgBox
aDelegate("Hello")
Console. ReadLine()
End Sub
Class ClassForStringSubDelegate
' Использовать конструктор по умолчанию
Public Sub TestSub(ByVal aString As String)
Console.WriteLine(aString SaString)
End Sub
Public Sub TestMsgBox(ByVal aString As String)
MsgBox(aString &aString)
End Sub
End Class End Module
Поскольку для делегата важна только сигнатура инкапсулированного метода, он легко «переключается» на другой метод. Потребовалось создать новую версию для вывода информации в окне отладки (вместо консоли и окна сообщения)? Достаточно внести несколько изменений в делегат и добавить в класс функцию, инкапсулируемую делегатом.
Важнейшая особенность делегатов заключается в том, что связывание с методом производится на стадии выполнения. Таким образом, делегаты в сочетании с явным или неявным вызовом метода Invoke по своим возможностям значительно превосходят функцию VB6 CallByName.
Все вместе
А теперь давайте объединим все сказанное на практическом примере. Создайте консольное приложение и включите следующий фрагмент в первый (стартовый) модуль:Module Modulel
Private WithEvents anEmployee As EmployeeWithEvents
Sub Main()
Dim tom As New EmployeeWithEvents("Tom". 100000)
anEmployee = tom
Console.WriteLine(tom.TheName & "has salary " & tom.Salary)
anEmployee.RaiseSalary(0.2D) ' Суффикс D - признак типа Decimal
Console.WriteLinettom.TheName & "still has salary " & tom.Salary)
Console.WritelineC'Please press the Enter key")
Console.ReadLine() End Sub End Module
Cамоучитель по VB.NET
Анализ исключений
Следующим шагом должен быть перехват и последующий анализ исключения. Для этого команда Catch приводится к следующему виду:Catch excep As Exception
(имя может быть любым, поскольку упоминание в заголовке секции Catch считается объявлением переменной). Теперь объект исключения, на который ссылается ехсер, автоматически заполняется данными. Например, в следующей секции Catch используется встроенный метод ToString объекта исключения ехсер:
Catch ехсер As Exception
Console.WriteLine(excep)
Результат выглядит примерно так:
System.IndexOutOfRangeException:
An exception of type_ System.IndexOutOfRangeException
was thrown, at Exception_l.Exception!.Main() in
C:\Documents and_ Settings\x20\My DocumentsWisual Studio
Projects\ConsoleApplication!4\Exception.vb:1ine 6
Из описания видно, что ошибка произошла в строке 6 при обращении к элементу массива. Конечно, если вы не хотите пугать пользователя, выводить эту информацию в окончательной версии программы не рекомендуется, но в процессе отладки она очень полезна.
При знакомстве с этим примером возникает логичный вопрос. Допустим, пользователь ввел имя файла, но метод ProcessFile не может его обработать. Что тогда? Можно ли отличить одно исключение от другого? Как вы вскоре увидите, небольшое усложнение секции Catch позволяет различать исключения по категориям. Более того, в секции Catch можно даже заново инициировать перехваченное исключение командой Throw, чтобы продолжить его обработку.
Две основные ветви иерархии исключений

Обработка исключений в сочетании с определением собственных классов исключений позволяет полностью отказаться от использования GoTo. Например, в главе 3 был приведен пример оправданного применения GoTo для прерывания вложенных циклов, когда ошибка происходит во внутреннем цикле. Программист VB .NET в подобной ситуации просто заключает весь цикл в блок Try-Catch, как показано ниже:
Sub Main()
Dim getData As String
Dim i, j As Integer
Dim e As System.I0.I0Exception
Try
For i = 1 To 10
For j = 1 To 100 Console.WriteC'Type the data, hit the Enter key between " & _
"ZZZ to end: ") getData _
Console.ReadLine() If getData = "ZZZ" Then
e New System.I0.I0Exception("Data entry ended " & _
"at user request") Throw e Else
' Обработка данных
End If
Next j
Next i
Catch
Console.WriteLinete.Message)
Console. Readline()
End Try
End Sub
В приведенном выше фрагменте выделенные строки нельзя объединить конструкцией следующего вида:
Dim e As New System.IO.IOException("Data entry ended at user request")
Вследствие правил видимости VB .NET объект исключения окажется недоступным в секции Catch.
Иерархия исключений
Мы создали новый класс исключений, производный от IOExcepti on, потому что потенциальная проблема явно относилась к категории ввода-вывода. Допустим, ситуация имеет более общий характер и для базового класса не существует других очевидных кандидатов, кроме класса Exception. Впрочем, это не совсем верно — лучший выбор существует всегда. Мы настоятельно рекомендуем выбирать в качестве базового не сам класс Exceptlon, а производный от него класс AppllcationException.Дело в том, что .NET Framework различает исключения, возникшие в результате проблем исполнительной среды (например, нехватки памяти или дискового пространства) и проблем, обусловленных работой вашего приложения. Именно исключения второй категории должны быть производными от AppllcationExcepti on, поэтому именно этот класс следует выбирать базовым при определении обобщенных исключений в программе.
Учтите, что класс IOException, как и многие стандартные исключения, является произ-водным от Exception, а не от ApplicationException.
Исполнительная среда помогает сделать следующий шаг. Иерархия исключений расходится на две ветви, показанные на Рисунок 7.1.
Инициирование исключений
Выше уже говорилось о том, что метод ProcessFilе просто передает исключение в процедуру Sub Main, из которой он был вызван. В процедуре Sub Mai n команда вызова тоже заключена в блок Try-Catch, поэтому исключение будет обработано. С другой стороны, такое решение выглядит немного наивно, а если написанные вами классы будут использоваться другими программистами, оно становится попросту опасным. Но даже если дело как-нибудь обойдется, пользователи вашего кода вряд ли будут довольны тем, что вы без разбора передаете исключения, не пытаясь их обработать.Лучше попытаться по возможности «прибрать» за собой, а затем воспользоваться ключевым словом Throw, чтобы передать объект исключения вызывающей стороне. В главе 4 упоминалось о том, что в VB .NET не поддерживается детерминированное завершение. Следовательно, если вы создали объект с методом D1 spose, этот метод следует вызвать перед тем, как инициировать исключение. Сказанное относится и к открытию файлов, и к получению графического контекста. В следующем фрагменте представлена условная структура подобного кода:
Try
' Создание локального объекта с методом Dispose
' Код. который может инициировать исключения
Catch(e As Exception)
local Object.dispose()
Throw e;
End Try
Если вы не вызовете метод Dispose для своего локального объекта, то захваченные ресурсы так и не будут освобождены. Ведь ссылка на объект существует лишь в локальном коде; остальные части программы не обладают доступом к методу Dispose! С другой стороны, причина, по которой возникло исключение, остается в силе, поэтому о возникшей проблеме (например, о неудачной операции с файлом) нужно сообщить вызывающему коду. Для этого следует заново инициировать исключение командой Throw, как это сделано во второй выделенной строке.
Впрочем, если вы действительно хотите программировать «как положено», не ограничивайтесь простым перезапуском исключения. Постарайтесь сделать свой код как можно более информативным и включите в объект исключения дополнительную информацию. Для этого есть три возможности.
Для примера представьте такую ситуацию: из источника данных читаются пары «ключ/значение», и для последнего ключа не находится парного значения. Программа предполагает, что значение ассоциируется с каждым ключом, поэтому при попытке чтения возникает неожиданно'е исключение ввода-вывода (чтение данных из файла описано в главе 9).
Теперь вы хотите сообщить о происходящем вызывающей стороне. Чтобы добавить в исключение строку, можно воспользоваться специальной версией конструктора класса Exception:
Public Sub New(ByVal message As String)
В следующем фрагменте в объект IOException добавляется новая строка с сообщением об отсутствии значения для последнего ключа, после чего исключение инициируется заново.
Dim excep As New IQException("Missing value for last key") Throw excep
Получив инициированное исключение, внешний код получает текст сообщения методом Message класса Exception и узнает о возникшей проблеме.
На практике в подобных ситуациях чаще возникает исключение класса EndOfStream-Exception, производного от IOException. Операции с потоками данных рассматриваются в главе 9.
Вторая ситуация реализуется элементарно благодаря главному правилу наследования: производный класс всегда может использоваться вместо базового класса. Вам лишь остается инициировать исключение производного класса, которое лучше подходит для данной ситуации.
Последний случай требует некоторой дополнительной работы, поскольку для этого потребуется определить класс, производный от существующего класса исключения. Предположим, вы хотите определить новый класс исключения, производный от System. 10. lOException. Новый класс отличается от старого лишь одним ReadOnly-свойством, возвращающим ключ, с которым не ассоциируется парное значение:
Public Class LastValueLostException Inherits System.I0.I0.Exception
Private mKey As String
Public Sub New(ByVal theKey As String)
MyBase.New("No value found for last key")
mKey = theKey
End Sub
Public Readonly Property LastKey() As String Get
Return mKey
End Get
End Property
End Class
Обратите внимание: имя созданного класса исключения завершается словом Exception. Это стандартное правило, которому мы настоятельно рекомендуем следовать. Получив исключение LastValueLostException, программист может воспользоваться свойством LastKey, значение которого передается в конструкторе нового класса исключения, и получить ключ, не ассоциируемый со значением. Следующая строка обеспечивает выдачу правильной информации методом Message базового класса Exception: MyBase.New("No value found for last key")
В этой строке вызывается конструктор базового класса (и в конечном счете конструктор предка Exception).
Возможно, вы заметили, что в классе LastValueLostException не переопределяются другие методы — такие, как метод ToString, унаследованный от Exception. В стандартных ситуациях объекты исключений всегда должны выводить стандартные сообщения.
Как использовать созданный класс в программе? Например, если последний ключ без парного значения был равен «oops», исключение будет инициироваться следующей командой:
Throw New LastValueLostException("oops")
Наличие нескольких секций Catch
Одной секции Try в VB .NET может соответствовать несколько секций Catch. Каждая секция перехватывает определенную категорию исключений, при этом для идентификации ошибок используются объекты классов, производных от базового класса Exception. Пример:Sub Main()
Dim args(). argument As String Try
args = Environment.GetCormandLineArgs()
ProcessFile(argsd))
Catch indexProblem As IndexOutOfRangeException
Console.WriteLine("ERROR - No file name supplied")
Catch ioProblem As System.10.I0Exception
Console.WriteLine("ERROR - can't process file named " & args(D)
Catch except As Exception
' Прочие исключения
End Try
Console.WriteLine("Press enter to end")
Console. ReadLine()
End Sub
В данном примере программа последовательно просматривает все секции Catch, пытаясь найти совпадение. Если пользователь не указал имя файла, совпадение будет найдено в первой секции. Вторая секция должна совпадать в том случае, если при вызове ProcessFile не удастся обработать файл (возможные причины рассматриваются далее). Если первые два случая не подошли, остальные исключения перехватываются последней секцией Catch.
Обнаружив подходящую секцию Catch, VB выполняет ее. Код других секций Catch при этом не выполняется.
Совпадение считается обнаруженным, если текущее исключение относится к типу, указанному в заголовке секции Catch, или производному от него. Например, класс FileNotFoundException является производным от I0Exception, поэтому следующий фрагмент неправилен:
Try
ProcessFile(args(1))
Catch indexProblem As IndexOutOfRangeException
Console.WriteLinet"ERROR = No file name supplied")
Catch IOProblem As System.IO.l0Exception
Console. WriteLinet "ERROR = can't process file named " & args(D)
Catch fileNotFound As System.IO.FileNotFoundException
End Try
Специализированное исключение FileNotFoundException будет поглощено предыдущей секцией, перехватывающей исключение базового класса l0Exception.
Из сказанного следует, что размещать секции Catch после секции Catch e As Exception бесполезно. Указание типа Exception в первой секции Catch автоматически перекрывает все остальные секции (кстати говоря, секция Catch без явного указания типа исключения считается эквивалентной Catch e As Exception). Также следует учитывать, что пустая секция с условием Catch e As Exception напоминает очень опасную конструкцию On Error Resume из прежних версий VB.
Несмотря на все опасности, связанные с перехватом обобщенных исключений Catch e As Exception, эту проверку рекомендуется включать в последнюю секцию Catch любого блока Try — особенно на стадии разработки и тестирования, поскольку эта проверка помогает лучше изолировать ошибки. Если все остальные способы не помогают, попробуйте вывести содержимое стека на консоль или в файл методом StackTrace класса обобщенного исключения Exception. Пример:
Try
ProcessFile(argsd))
Catch indexProblem As IndexOutOfRangeException
Console.WriteLine("ERROR - No file name supplied")
Catch fnf As System.I0.FileNotFoundException
Console.WriteLinet"ERROR - FILE NOT FOUND")
Catch ioProblem As System.I0.lOException
Console.WriteLine("ERROR - can't process file named " & args(1))
Catch e As Exception
Console.WriteLinet"Please inform the writer of this program " & _
"of this message")
Console.Writete.StackTrace)
End Try
Что произойдет, если возникшее исключение не подойдет ни к одной из секций Catch, а в конце блока Try-Catch отсутствует универсальная секция Catch e As Exception? В этом случае исключение передается в секцию Try верхнего уровня, заключающую код внутренней секции Try. Если подходящая секция Catch не будет найдена и во внешней секции Try, поиск продолжается в методе, от которого поступил вызов. Вероятно, именно это и произойдет при вызове метода ProcessFi I e из предыдущего примера — метод ProcessFi 1е передает все необработанные исключения (в форме объекта Exception) в процедуру Sub Main.
Если исключение не будет перехвачено ни одной секцией Try в методе, управление переходит в секцию Finally, а затем немедленно передается за пределы метода. Таким образом, обработку исключений можно рассматривать как невероятно мощную (и притом интеллектуальную) разновидность GoTo. Интеллектуальность заключается в автоматическом выполнении завершающего кода в секции Finally.
В общем случае, если исключение не было обработано программой вплоть до точки входа в приложение, .NET выводит сообщение с описанием исключения и содержимое стека с информацией обо всех вызванных методах на момент возникновения исключения.
В VB .NET секция Catch может дополняться условием When, расширяющим возможности ее применения. Синтаксис выглядит следующим образом:
Catch badnameException When theName - String.Empty
Подготовка к структурной обработке исключений
Прежде чем переходить к примерам, демонстрирующим обработку исключений на практике, необходимо познакомиться с некоторыми обстоятельствами. Во-первых, при структурной обработке исключений в программу включается дополнительная ветвь, которая автоматически выполняется при возникновении каких-либо аварийных ситуаций. Кроме того, при обработке исключений VB .NET автоматически создает объект, содержащий информацию об ошибке.Когда в программе происходит исключение, встроенный механизм начинает искать обработчик, подходящий для данного объекта исключения (то есть для конкретной причины ошибки). Речь идет не о наборе GoTo, запутывающих логику программы, — обработка исключения больше напоминает запасную дорогу, идущую параллельно главной магистрали и связанную с ней несколькими переездами — настоящей мечте любого водителя, попавшего в пробку. Если в программе что-то пойдет не так, управление автоматически передается ветви, содержащей логику обработки исключений (если, конечно, вы ее запрограммировали). После этого исключение либо рассматривается одним из обработчиков, либо передается дальше по цепочке.
В VB .NET для обработки исключений существует синтаксическая конструкция, называемая блоком Try-Catch. Допустим, у нас имеется консольное приложение ProcessFile. Предполагается, что пользователь запускает его в режиме командной строки командой вида ProcessFile имя_файла
Имя файла передается в виде параметра. Как это обычно бывает, пользователи будут делать все, чтобы сбить бедную программу с толку. В частности, они могут:
Module Exceptionl Sub Main()
Dim args() As String Try
args = Environment.GetCommandLineArgs()
ProcessFile(argsd))
Catch
Console.WriteLine("ERROR")
End Try
Console.WriteLine("Press enter to end")
Console. ReadLine()
End Sub
Sub ProcessFiletByVal fileName As String)
' Обработка файла
Console.WriteLine("Am processing " & fName)
End Sub
End Module
Секция Try блока Try-Catch содержит «правильный» код — в данном примере это вызов ProcessFile (вызов Environment.GetCommandLingArgs() заключен в секцию Try, потому что он тоже может инициировать исключение — например, если ваша программа работает на платформе, не поддерживающей передачи аргументов в командной строке).
Секция Catch в блоке Try-Catch необходима, потому что некоторые невнимательные пользователи не обращают внимания на указания. Если в приведенном фрагменте пользователь забывает ввести имя файла, программа пытается обратиться к имени файла, что приводит к исключению IndexOutOfRangeExceptl on, поскольку элемент с указанным индексом отсутствует в файле. При возникновении исключения управление передается в дополнительную ветвь, то есть в блок Catch, который в нашем примере просто выводит строку ERROR в консольном окне.
Из блока Try, как и из других управляющих конструкций VB .NET (таких, как циклы For и Do), можно выйти немедленно командой Exit Try. Впрочем, применение Exit Try обычно считается проявлением плохого стиля программирования.
| Содержание | Вперед |
Проверка ошибок и обработка исключений
Традиционный механизм обработки ошибок, использовавшийся в прежних версиях VB, а также в программировании СОМ и Windows, основан на проверке возвращаемого значения функции и выборе действий. Обычно для проверки возвращаемого значения в программе создается аналог конструкции Select Case, причем значения интерпретируются абсолютно произвольно. Например, в одном случае 0 означает успех, а в другом — неудачу. А в приведенном ниже фрагменте кода VB6 коды выглядят и вовсе странно:Select Case Error-Number
Case 57
MsgBox "Your printer may be off-line."
Case 68
MsgBox "Is there a printer available?"
' Другие секции Case
Case Else
' Все остальные случаи
End Select
Подобные конструкции работают, но их трудно читать и еще труднее изменять в процессе сопровождения программы. Можно уверенно сказать, что эта схема таит в себе широкие возможности для ошибок программирования. Например, вы можете перепутать коды ошибок или забыть проверить некоторые из возвращаемых значений. Кроме того, писать один и тот же код проверки при каждом вызове функции Windows API, по крайней мере, утомительно. Хотя в некоторых ситуациях возвращаемое значение приходится проверять независимо от выбранной схемы обработки ошибок, не стоит превращать это в постоянную практику. Также следует учитывать фактор эффективности: структурная обработка исключений быстрее программируется, отнимает меньше времени при сопровождении, а нередко и выполняется быстрее!
Рекомендации по использованию исключений
Исключения выглядят эффектно, и новички часто склонны злоупотреблять ими. В самом деле, стоит ли тратить время на анализ пользовательского ввода, когда можно просто инициировать исключение? Не поддавайтесь соблазну. При неправильном использовании обработка исключений существенно замедляет работу программы. Ниже приведены некоторые рекомендации по использованию исключений в программе.Секция Finally
При использовании блоков Try-Catch нередко существует код, который должен выполняться как при нормальном завершении, так и при возникновении исключения. Например, в обоих случаях следует закрыть файлы, вызвать методы Dispose и т. д. Даже в простом примере, приведенном в начале главы, потребовалась команда ReadLine, чтобы консольное окно оставалось на экране до нажатия клавиши Enter.Чтобы некоторый фрагмент выполнялся независимо от того, возникнет ли в программе исключение или нет, в блок Try-Catch включается секция Finally, выделенная в следующем примере жирным шрифтом:
Sub Main()
Dim args(). argument As String
args = Environment. GetCommandLineArgs()
Try
ProcessFile(argsd))
Catch
Console.WriteLine("ERROR")
Finally
Console.WriteLine("Press enter to end")
Console.ReadLine()
End Try
End Sub
Код секции Finally выполняется до передачи исключений внешнему.коду и до возвра-щения из функции .
Cамоучитель по VB.NET
ColorDialog

При использовании диалогового окна выбора цвета (Col orDialog) программа обычно запрашивает свойство Color и назначает его свойству ForeColor или BackColor элемента или формы. Например, приведенная ниже процедура изменяет фоновый цвет формы при нажатии на кнопку:
Private Sub btnCo1or_Click(ByVal sender As System.Object. _
ByVale As System.EventArgs)Handles btnColor.Click
Dim myDialog As New ColorDialog()
Dim Temp As Color = btnColor.BackColor
If myDialog.ShowDialog() = DialogResult.OK Then
Me.BackColor = myDialog.Color btnColor.BackColor = Temp
End If
End Sub
На экране появляется стандартное диалоговое окно, в котором выбирается нужный цвет.
Диалоговые формы и диалоговые окна

В .NET Framework включена поддержка стандартных диалоговых окон; соответствующие элементы расположены в нижней части панели элементов (Рисунок 8. 14).
Дизайнер форм
Программисты VB6 легко привыкают к особенностям работы с формами и элементами в VS .NET IDE. В вашем распоряжении оказывается пара новых (притом весьма полезных) инструментов, кратко описанных далее, однако общие принципы работы с панелью элементов (toolbox) почти не изменились.Для читателей, никогда не работавших в старой версии VB IDE, мы кратко опишем процесс включения нового элемента в окно формы.
Добавление новых событий

Добавить поддержку нового события в элемент ничуть не сложнее, чем включить обработчик события в класс (эта тема рассматривалась в главе 6). Предположим, каждый раз, когда пользователь вводит нечто отличное от положительного целого числа, элемент должен инициировать событие BadDataEntered. Новый код класса выделен жирным шрифтом:
Public Class PositivelntegerTextBox
Inherits System.Windows.Forms.TextBox
Public Event BadDataEntered(ByVal Sender As Object, _
ByVal e As EventArgs)
Protected Overrides Sub OnTextChanged(ByVal e As EventArgs)
MyBase.OnTextChanged(e)
If Not (IsNumeric(Me.Text)) Then Me.Text = String.Empty
RaiseEvent BadDataEntered(Me. New System.EventArgs()) Else
Dim temp As Decimal temp = CType(Me.Text,Decimal)
If temp = Math.Round(temp.0) <> 0 Then
Me.Text = String.Empty
RaiseEvent BadDataEntered(Me, New System.EventArgs())
End If
End If
End Sub
End Class
В элементах VB существует понятие события по умолчанию (default event). Событие по умолчанию срабатывает автоматически при двойном щелчке на экземпляре элемента в дизайнере форм. Событие по умолчанию задается при помощи специального атрибута. Атрибуты соответствуют экземплярам класса System.Attribute; атрибут Def aul tEvent входит в пространство имен System. ComponentModel. В программе атрибуты заключаются в угловые скобки, а при установке атрибута Defaul tEvent указывается имя события в кавычках. Чтобы назначить событие BadDataEntered событием по умолчанию для данного элемента, приведите начало класса к следующему виду:
Imports System.ComponentModel
PositivelntegerTextBox
Inherits System.Windows.Forms.TextBox
Добавление новых свойств
На следующем шаге класс будет дополнен свойствами MinValue и MaxValue. Программная реализация этих свойств выглядит весьма прямолинейно. При создании экземпляра переменным присваиваются значения 1 и максимальное значение типа Long соответственно. В дальнейшем необходимо проследить за тем, чтобы свойство MinValue не оказалось меньше 1, а свойство MaxVal ue не превышало предельной величины:Private m_Min As Long = 1
Private m_Max As Long = Long.MaxValue
Public Property MinValue()As Long Get
Return m_Min End Get SetCByVal Value As Long)
m_Min = Math.Maxd,Value)
End Set
End Property
Public Property MaxValue()As Long Get
Return m_Max End Get Set(ByVal Value As Long)
m_Max =Math.Min(m_Min.Value)
End Set
End Property
Если включить этот код в проект и откомпилировать его, свойство будет поддерживаться элементом, но не будет отображаться в окне свойств. Проблема решается установкой атрибута Browsable для имени свойства:
Примерный вид окна свойств при установке атрибута Browsable для свойств MinValue и MaxValue показан на Рисунок 8.18.
FileDialog
Абстрактный класс FileDialog является базовым для двух специализированных подклассов:Private Sub mnuOpen_Click(ByVal sender As System.Object,_
ByVal e As System.EventArgs)Handles mnuOpen.Click
Dim myDialog As New OpenFileDialog()
myDialog.Filter = "text (*.txt),RTF (*.rtf)|*.txt:*rtf"
If myDialog.ShowDialog =DialogResult.OK Then
' При загрузке текстовых файлов во втором параметре
' необходимо передавать признак типа файла.
Dim Temp As String = myDialog.FileName.Tollpper
If Temp.EndsWith("TXT") Then
RichTextBoxl.LoadFi1e(myDialog.FileName,
Ri chTextBoxStreamType.Plai nText) Else
Ri chTextBoxl. LoadFiletmyDialog.File_Name.
Ri ChTextBoxStreamType.RichText)
End If
End If End Sub
FontDialog
Диалоговое окно выбора шрифта (FontDialog) хорошо знакомо всем пользователям текстовых редакторов Windows. В свойстве Font объекта диалогового окна возвращается объект Font, обычно назначаемый свойству Font элемента или формы. В следующем примере предполагается, что на форме находится текстовое поле TextBoxl и кнопка Buttonl:Private Sub Buttonl_Click_l(ByVal sender As System.Object. _
ByVal e As System.EventArgs) Handles Buttonl.Click
Dim myDialog As New FontDialog()
If myDialog.ShowDialog() = DialogResult.OK Then TextBoxl.Font
myDialog.Font
End If
End Sub
(обратите внимание, как однострочное текстовое поле автоматически подстраивается под размеры нового шрифта).
Формы MDI
В прежних версиях VB при программировании приложений с интерфейсом MDI (Multiple Document Interface) родительская форма MDI выбиралась на стадии конструирования. В .NET эта задача решается иначе — свойству IsMdiContainer формы задается значение True. Программист создает дочерние формы MDI на стадии конструирования или выполнения, а затем заносит в их свойство Mdi Parent ссылку на форму со свойством I sMdi Conta i пег, равным True. Таким образом, в программах VB .NET можно сделать то, что было практически нереально в предыдущих версиях VB, — изменять связи MDI во время работы программы. Кроме того, приложение может содержать несколько родительских форм MDI; в VB6 такая возможность не поддерживалась.Рассмотрим пример. Создайте приложение со следующей процедурой Forml_Load:
Private Sub Forml_Load(ByVal sender As System.Object._
ByVal e As System.EventArgs) Handles MyBase.Load
Me.Text = "I'm an MDI Parent"
Me.IsMdiContainer - True
Dim MyChild As New System.Windows.Forms.Form()
MyChiId.MdiParent = Me
MyChild.Show()
MyChild.Text ="MDI Child" End Sub
Примерный вид окна показан на Рисунок 8.12.
Конечно, это весьма жалкое подобие приложения MDI. В приложениях MDI обычно присутствует меню Window, позволяющее расположить открытые дочерние окна «черепицей» или «мозаикой», а также активизировать любое дочернее окно. Меню Window реализуется в родительском окне, причем его код может быть достаточно простым:
Public Sub InitializeMenu()
Dim mnuWindow As New MenuItem("&Window")
MainMenu1.MenuIterns.Add(mnuWindow)
mnuWindow.MenuItems.AddCNew Menultem _
("&Cascade", AddressOf WindowCascade_Clicked))
mnuWindow.MenuItems.Add(New Menultem
("Tile &Horizontal", AddressOf WindowTileHoriz_C1icked))
mnuWindow.MenuItems.Add(New Menultem _
("Tile &Vertical". AddressOf WindowTileVert_Clicked))
mnuWindow.MdiList = True
End Sub
Protected Sub WindowCascade_Clicked(ByVal
Sender As Object. ByValeAs System.EventArgs)
Me.LayoutMdi(MdiLayout.Cascade)
End Sub
Protected Sub WindowTileHoriz_Clicked(ByVal Sender As Object._
ByVal e As System.EventArgs)
Me.LayoutMdi(MdiLayout.TileHorizonta1)
End Sub
Protected Sub WindowTileVert_Clicked(ByVal Sender As Object,
ByVal e As System.EventArgs)
Me.LayoutMdi(MdiLayout.TileVertica1)
End Sub
Примерный вид окна, полученного при вызове InitializeMenu для формы из предыдущего примера, показан на Рисунок 8.13.
Графика: GDI+
Графическое программирование в .NET Framework полностью отличается от всего, что было реализовано в прежних версиях VB. Знакомые графические команды (частично позаимствованные еще из QuickBasic) исчезли. Из числа принципиальных изменений также следует обратить внимание на отсутствие свойства AutoRedraw или его аналогов. В прежних версиях VB свойство AutoRedraw, равное True, избавляло программиста от необходимости программировать процедуру события Pal nt для того, чтобы обеспечить восстановление графического изображения в элементе.Программирование графики в VB .NET основано на концепции графического контекста — отдаленного родственника контекстов устройств Windows GDI. Любопытная подробность: новая система называется GDI+, хотя с GDI она имеет очень мало общего.
У программистов с опытом работы в GDI переход на GDI+ часто вызывает шок, поскольку графический вывод в .NET происходит без сохранения состояния. Иначе говоря, каждая графическая команда должна содержать полную информацию о выполняемой операции. Скажем, если вы использовали черную кисть в первой строке программы и хотите снова воспользоваться ею во второй строке, необходимо указать графической системе, что операция должна выполняться черной кистью. GDI+ «не помнит» об операциях, выполнявшихся ранее.
Классы GDI+ находятся в пространствах имен System.Drawing, System.Drawing. Drawing2D, System. Drawing. Imagi ng и System. Drawing. Text [ Каждое из этих пространств имен заслуживает отдельной книги, и здесь мы ограничимся лишь кратким упоминанием. ]. Эти пространства имен входят в сборку System.Drawing, ссылка на которую создается автоматически при выборе типа приложения Windows Application в диалоговом окне New Project.
Большая часть графического вывода в GDI+ осуществляется переопределением процедуры [ Это не событие, хотя в конечном счете перерисовка и приводит к |ызову события OnPaint базового класса Form. ]OnPaint формы или элемента. Процедура OnPaint играет столь же важную роль, как и в прежних версиях VB: она обеспечивает восстановление изображения при временном скрытии или свертывании формы. Сигнатура этой важной процедуры выглядит следующим образом: Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
Вывод осуществляется на графической поверхности GDI+, представленной экземпляром класса Graphics. Процедура OnPaint класса Form инкапсулирует такую поверхность в виде значения свойства e.Graphics.
Хотя любая форма или элемент (в том числе и PictureBox) с поддержкой вывода позволяет получить доступ к своему графическому содержимому при помощи вызова ControlName.CreateGraphics, будьте очень внимательны, если это происходит за пределами процедуры Paint. Между выводом в графическом контексте, полученным вызовом e.Graphics в процедуре OnPaint и написанием кода, использующего CreateGraphics, существуют тонкие различия. Мы столкнулись с этой проблемой при создании программы вывода всех шрифтов (см. ниже).
Элемент PrintDialog и конфигурация печати
Пользователь программы должен иметь возможность выбрать принтер и внести другие изменения в конфигурацию печати. Такую возможность предоставляет элемент PrintDialog. В программе соответствующий фрагмент может выглядеть примерно так:Private Sub Buttonl_Click(ByVal sender As System.Object,_
ByVal e As System.EventArgs)Handles Buttonl.Click
Dim PhntDialogl As New PrintDialog()
' Следующая строка необходима, потому что информация
' объекта PrinterSettings нужна объекту PrintDialog перед выводом
PrintDialogl.Document = PrintDocumentl
If PrintDialogl.ShowDialog() = DialogResult.OK Then
PrintDocumentl. PrintO
End If
End Sub
Выделенная строка сообщает экземпляру PrintOialog, что связанный с ним документ должен быть экземпляром PrintDocumentl (предполагается, что этот объект был создан ранее). Эта строка необходима, поскольку элемент PrintDialog должен получить некоторые параметры печати (в виде объекта Pri ntSetti ngs) перед выводом окна. Чтобы передать ему эту информацию, проще всего назначить объект PrintDocument свойству Document.
Элементы меню и новый редактор меню Visual Studio
Хотя визуальное конструирование форм в книге почти не рассматривается, мы просто не могли не упомянуть новый редактор меню. Программисты VB уже давно ждали чего-то подобного. Впрочем, при всем удобстве нового редактора вы извлечете из него максимум пользы лишь при полном понимании кода, сгенерированного IDE.Построить меню в редакторе меню (Menu Editor) несложно. Сначала с панели элементов на форму перетаскивается элемент MainMenu; на форме появляется заготовка меню (Рисунок 8.10).
Теперь можно переходить к вводу команд меню. При вводе очередной команды в редакторе появляются поля для ввода следующей команды или команд, находящихся на других уровнях иерархии меню (Рисунок 8.11). Чтобы отредактировать команду меню, созданную ранее, достаточно щелкнуть на ней (для изменения существующих меню щелчок делается на элементе MainMenu на панели под формой). Порядок команд меню изменяется операциями вырезания/вставки (эти операции работают даже в главной строке меню). Как и в прежних версиях VB, перед клавишами ускоренного вызова ставится префикс &. На Рисунок 8.11 показано меню с клавишей ускоренного вызова и разделительной чертой.
Прежде чем переходить к коду, сгенерированному для меню на Рисунок 8.11, необходимо знать, что меню формы инкапсулируется в классе System.Windows.Forms.MainMenu. Объект MainMenu выполняет функции контейнера для экземпляров Menu Item. Для организации подменю в экземплярах Menultem определяется свойство Menultems; значение этого свойства представляет собой коллекцию класса Menu.MenuItemCollection, содержащую другие объекты Menultem.
Что касается кода, соответствующего Рисунок 8.11, то он начинается с объявления команд меню. Имена, принятые по умолчанию, были заменены более содержательными: вместо Menulteml используется имя mnuFile и т. д.
Маркеры изменения размеров элемента

В прежних версиях VB6 случайное изменение размеров формы приводило к тому, что все старания по размещению элементов на стадии конструирования шли прахом. Чтобы решить эту проблему, программисту приходилось либо самостоятельно программировать масштабирование формы, либо тратиться на специальный элемент управления. При использовании класса Windows. Forms проблема исчезает, поскольку в каждый видимый элемент добавляются два чрезвычайно полезных свойства: Anchor и Dock.
Значение свойства Anchor задается в окне свойств при помощи небольшого мини-редактора, показанного на Рисунок 8.3.
Свойство Anchor привязывает элемент к одному или нескольким краям формы. В этом случае расстояние между элементом и краем остается постоянным при любых изменениях размеров контейнера. Элемент, привязанный к противоположным краям контейнера, автоматически масштабируется с изменением размеров контейнера. Ниже приведен пример кода, сгенерированного при установке свойства Anchor с привязкой элемента по левому и правому краю:
Me.Button1.Anchor = (System.Windows.Forms.AnchorStyles.Left _
Or System.Windows.Forms.AnchorStyles.Right)
Свойство Dock обладает значительно большей гибкостью по сравнению со старым свойством Al ign, которое оно заменяет. Элемент, пристыкованный к краю формы, остается совмещенным с этим краем при любых изменениях размеров формы. Значение свойства Dock задается в мини-редакторе, показанном на Рисунок 8.4.
Меню Tab Order

Изменение порядка перебора элементов (tab order) в прежних версиях VB было делом утомительным и неприятным, которое несколько упрощалось только специальной надстройкой (add-in). В VB .NET существует команда View > Tab Order, которая делает эту задачу элементарной. Все, что от вас потребуется, — ввести нужную позицию элемента в небольшом поле, которое при выполнении команды View > Tab Order появляется рядом с элементом (Рисунок 8.7). Для элементов, находящихся внутри контейнера, позиция задается в формате «х.у». Например, если групповому полю в порядке перебора была присвоена позиция 3, то расположенные внутри него элементы будут иметь номера 3.0, 3.1 и т. д. (чтобы отключить режим ввода порядка перебора, снова выполните команду Tab Order).
Рисунок 8.7. Команда Tab Order
Многостраничный вывод
Процесс многостраничной печати основан на небольшой хитрости: если процедура обработки события Pri ntPage задает свойству HasMorePages объекта Pri ntPageEventArgs значение True, то объект PrintDocument узнает о наличии дополнительных страниц для печати и автоматически инициирует заново событие PagePri n't.Проблемы, возникающие при многостраничной печати, имеют мало общего с самой печатью. Они присущи любому сложному выводу на графической поверхности: программа должна помнить обо всем. Предположим, вы хотите написать небольшую программу для вывода содержимого текстового поля или текстового файла. В псевдокоде все выглядит просто.
Начало построения меню в редакторе

В прежних версиях VB создавать контекстные меню было неудобно. В .NET контекстное меню представляется экземпляром класса ContextMenu и редактируется в визуальном режиме. Чтобы связать контекстное меню с элементом или формой, достаточно задать значение свойства ContextMenu этого элемента или формы. По стандартам Windows контекстные меню вызываются щелчком правой кнопки мыши; в .NET это происходит автоматически благодаря наследованию. Вам не придется программировать событие MouseDown — поддержка контекстных меню реализована в классе Control , производными от которого являются формы и элементы. При перетаскивании на форму элемента ContextMenu IDE генерирует почти такой же код, как для команд главного меню:
Friend WithEvents ContextMenul As System.Windows.Forms.ContextMenu
Me.ContextMenul =New System,Windows.Forms.ContextMenu()
Так же вызывается метод AddRange:
Me.ContextMenul.MenuIterns.AddRange(New System.Windows.Forms.MenuItem() {Me.MenuIteml})
Чтобы быстро задать значение свойства Name команд контекстного меню, перейдите в режим редактирования имен щелчком правой кнопкой мыши.
Поскольку обычные и контекстные меню управляются на программном уровне, ваше приложение может построить меню с нуля на стадии выполнения программы!
Немного истории
Для работы с формами в прежних версиях VB использовался механизм Ruby Forms. Он представлял собой версию программы Ruby, написанной Аланом Купером (Alan Cooper); в результате объединения Ruby с QuickBasic появился VB11. Таким образом, разработка графических приложений в прежних версиях VB зависела от механизма форм, работа которого была практически полностью скрыта от программиста. Стартовая форма, словно по волшебству, появлялась на экране, а элементы размещались на форме программистом на стадии конструирования [ В VB6 элементы могли добавляться и во время работы программы, но данная возможность почти не использовалась из-за крайне неудобного механизма перехвата событий в этих элементах. ]. В программах С/С++ процесс ручного создания окон и элементов на формах был делом в лучшем случае непростым, и в этой области VB обладал несомненными преимуществами. Впрочем, у этого волшебства была и оборотная сторона — механизм Ruby Forms был недостаточно гибким. Он плохо расширялся, и программисту приходилось мириться со всеми недочетами в его реализации. Возможности использования форм и элементов VB были ограничены; в любых нестандартных ситуациях программисту приходилось широко использовать функции API и перехватывать стандартные сообщения Windows в уродливых конструкциях с субклассированием [ Некоторые пакеты независимых фирм (например, Desa ware Spy Works) упрощали процесс субклас-сирования, но задача все равно оставалась непростой. ]. Даже такая простая задача, как создание списка с ускоренным поиском, требовала вызова функции API; стандартная операция включения новых строк в список усложнялась тем, что свойство Items списка было доступно только для чтения (к счастью, в VB .NET эти проблемы решаются элементарно [ Например, чтобы быстро заполнить список, достаточно присвоить коллекцию свойству DataSource. ]).Более того, многие программисты не понимали, чем же в действительности были формы VB — классами или экземплярами классов? В каком-то смысле и тем и другим. В результате программа выглядела весьма странно:
Forml.Show ' Я - экземпляр
Dim newForm As New Forml ' А теперь я - класс
newForm.Show
Программисты предпочитают работать с логичными и последовательными моделями, а с точки зрения ООП механизм форм в ранних версиях часто выглядел как нагромождение искусственных и несогласованных «заплат». В VB .NET ситуация полностью изменилась. Здесь формы являются экземплярами класса Windows. Forms. Form, и поведение форм может изменяться посредством наследования точно так же, как и поведение любых других классов .NET Framework. Например, код специализированной формы может начинаться со следующего заголовка:
Public Class MyForm
Inherits System.Windows.Forms.Form
После этого форма наделяется новыми возможностями, для чего программист либо переопределяет члены родительского класса Form, либо добавляет в него новые члены, как для любого другого класса (см. главу 5).
В прежних версиях VB элементы даже не считались полноценными классами, поэтому команды вроде следующей были недопустимыми:
Dim myButton As New CommandButton
Перед нами классический пример логической связи «является частным случаем», идеально подходящий для наследования, однако программист не мог определить новый вариант кнопки как производный от базового класса стандартной кнопки.
Возможности создания нестандартных элементов, появившиеся в VB5, были основаны на включении и делегировании, причем в этом процессе использовалась одна из самых непредсказуемых программ-мастеров (wizards) — не говоря уже о том, что элементы, построенные в VB5 и 6, несколько отличались от элементов, построенных на других языках (например, C++ и Delphi).
В .NET элементы и формы являются классами. Как будет показано ниже в этой главе, VB .NET позволяет определить новый класс текстового поля, начинающийся с заголовка следующего вида:
Public Class PositivelntegerTextBox
Inherits System.Windows.Forms.TextBox
Далее программист переопределяет или добавляет методы, как в любом другом классе, причем полученный элемент ничем не отличается от элементов, написанных на С# или управляемом C++.
Короче говоря, в предыдущих версиях VB графические приложения строились на весьма шаткой и ненадежной основе, которая к тому же не была в полной мере объектно-ориентированной. Переход к единой программной модели в рамках объектно-ориентированной версии VB потребовал коренного пересмотра этой структуры. Так появилось новое пространство имен Windows.Forms.
| Содержание | Вперед |
О классе PrintPageEventArgs
Объект PrintPageEventArgs содержит два ReadOnly-свойства, при помощи которых можно получить информацию о размерах страницы. Значения обоих свойств задаются в сотых долях дюйма.
Печать
Печать в .NET — дело непростое, однако богатство возможностей окупает все хлопоты. В этом разделе мы подробно опишем процесс печати одной страницы (в основном на примере автоматически сгенерированного кода), а затем покажем, как напечатать несколько страниц. Также будет показано, как запрограммировать собственную процедуру печати с использованием делегата. Начнем с печати изображения, хранящегося в графическом поле.Чтобы обойти некоторые ограничения, действующие в GDI+, мы будем предполагать, что изображение задается свойством Image, а не прямым копированием в графическое поле.
Печать в конечном счете сводится к выводу информации в графическом контексте, но вместо экранного контекста используется контекст, ассоциированный с принтером или окном предварительного просмотра печати.
Как при выводе на принтер, так и при использовании поддержки предварительного просмотра (Print Preview) в .NET работа всегда начинается с создания объекта класса System. Drawl ng. Pri nti ng. Pri ntDocument. Для получения этого объекта можно применить один из следующих способов:
Dim aPrintDocument As New PrintDocument()
При использовании панели элементов на форме размещается элемент Pri ntDocument, не обладающий визуальным интерфейсом. При этом генерируется фрагмент следующего вида:
Friend WithEvents PrintDocumentl As System.Drawing.Printing.PrintDocument
Непосредственное создание экземпляра происходит в следующей строке, включенной в процедуру
InitializeComponent: Me.PrintDocumentl = New
System.Drawing.Printing.PrintDocument()
При необходимости можно запрограммировать обработчики и для двух других событий, но фактическая печать выполняется именно в обработчике PrintPage. Два других события обычно используются для оповещения о начале и завершении печати.
Во втором параметре события Pri ntPage передается объект PagePri ntEventArgs. В этом объекте хранится много полезных данных, в том числе:
Private Sub Buttonl_Click(ByVal sender As System.Object,_
ByVal e As System.EventArgs)
Handles Buttonl.Click
PrintDocumentl.Print()
End Sub
Метод Print вызывает событие PrintPage, поэтому на следующем этапе следует запрограммировать обработчик события PrintDocumentl_PrintPage, в котором и происходит непосредственная печать. Если обработчик был сгенерирован с помощью дизайнера, в заголовок автоматически включается соответствующая секция Handles:
1 Private Sub Pri ntDocumentl_PrintPage(
ByVal sender As System.Object. ByVal e As
System.Drawing.Printing.PrintPageEventArgs)
Handles PrintDocument1.PrintPage
2 Dim g As Graphics
3 g = e.Graphics
4 g.DrawImageCPictureBoxl.Image. 0. 0)
5 g.Dispose()
6 e.HasMorePages = False
7 End Sub
При выполнении этого фрагмента изображение печатается на принтере по умолчанию (о том, как сменить принтер, будет рассказано в следующем разделе). Хотя наш пример относительно прост, каждая строка в нем играет важную роль и заслуживает объяснения. В строке 1 событие связывается с процедурой, имеющей правильную сигнатуру, не требуя от вас прямого использования делегата (см. главу 6).
В строке 3 мы получаем объект Graphics, представляющий поверхность вывода текущего принтера. Строка 4 выводит изображение начиная с левого верхнего угла. Вывод происходит на принтере, с которым связан графический контекст. Присутствие вызова D1 spose в строке 5 связано с тем, что графические контексты (как было сказано выше) не освобождаются сборщиком мусора. Строка 6 сообщает об отсутствии дальнейших страниц для печати.
Переопределение события
Начнем с переопределения событий базового класса. Например, событие OnChange поможет предотвратить ввод в поле всего, что не является целым числом. Следующий обработчик разрешает вводить или вставлять из буфера только положительные целые числа:1 Public Class PositivelntegerTextBox
2 Inherits System.Windows.Forms.TextBox
3 Protected Overrides Sub OnTextChanged(ByVal e As EventArgs)
4 MyBase.OnTextChanged(e)
5 If Not (IsNumeric(Me.Text))Then
6 Me.Text - String.Empty
7 Else
8 Dim temp As Decimal
9 temp = CType(Me.Text.Decimal)
10 If temp - Math.Round(temp.0) <> 0 Then
11 Me.Text = String.Empty
12 End If
13 End If
14 End Sub
15 End Class
В строках 1 и 2 объявляется специализированная версия обычного текстового поля. Поскольку при специализации на базе наследования сохраняются все члены базового класса, которые не подверглись явным изменениям, вам не придется прибегать к услугам программы-мастера (как в VB6), чтобы решить вопрос с неизменными свойствами — например, с основным цветом. При переопределении событий в производном классе обычно вызывается обработчик базового класса, как в строке 4. Необходимость этой строки связана с тем, что мы не программируем обработку этого события от начала и до конца, а хотим воспользоваться унаследованными аспектами поведения базового класса. В строках 5-6 предотвращается ввод нечисловых данных типа 32Skiddoo. В строках 9-12 из текстового поля удаляются дробные числа; при помощи встроенной функции Round программа убеждается в том, что округленное число совпадает с исходным. Следует заметить, что простое уничтожение введенного текста выглядит несколько жестоко по отношению к пользователю. В более изощренной программе следовало бы сохранить предыдущий текст, чтобы восстановить его при необходимости. В этом случае одна ошибка ввода не будет приводить к полной потере введенных данных.
На этой стадии приведенный выше фрагмент можно откомпилировать в DLL и получить вполне работоспособный элемент. Чтобы протестировать элемент после компиляции, создайте новое приложение Windows и поместите элемент на панель элементов:
Построение нестандартных элементов на базе наследования
Наследование применяется при построении не только новых форм, но и новых элементов. Допустим, вы хотите создать текстовое поле, предназначенное для ввода только целых чисел. Как обычно, все начинается с объявления нового класса:Public Class PositivelntegerTextBox
Inherits System . Windows . Forms . TextBox
Остается лишь запрограммировать поле на нужное поведение. В оставшейся части этого раздела будет показано, как класс Positi velntegerTexBox наполняется специализированными свойствами, событиями и методами.
Чтобы сократить объем этого примера, мы ограничимся упрощенной версией элемента. В частности, проблемы лицензирования и безопасности вообще не рассматриваются — за дополнительной информацией обращайтесь к более серьезным книгам, посвященным программированию элементов в .NET.
Итак, создайте новую библиотеку классов и включите в решение ссылку на сборку Windows.Forms.dll.
Мы начинаем работу с библиотеки классов, а не с проекта типа User Control, потому что он лучше подходит для элементов, написанных «на пустом месте». Если вы захотите построить элемент, содержащий несколько других элементов, выберите в диалоговом окне New Project тип Windows Controls Library — в вашем распоряжении окажется контейнер, предназначенный для построения сложного элемента посредством включения.
вывод всех шрифтов в системе
Для демонстрации вывода текста была написана программа, которая воспроизводит в графическом поле все установленные шрифты с указанием имен (попутно мы столкнулись с проблемой, описанной в конце раздела). Программа состоит из нестандартного элемента и формы с прокруткой (Рисунок 8.24).
Простейшее Windows-приложение

Ограниченный объем книги не позволяет нам рассмотреть все свойства класса Form, но мы хотим выделить существенные различия между поведением форм в прежних версиях VB и VB .NET. Одно из самых принципиальных изменений связано с использованием шрифтов. Вместо старых шрифтовых свойств (например, FontBold) используется класс Font пространства имен System.Drawing, самый распространенный конструктор которого выглядит следующим образом:
Sub New(ByVal family As FontFamily.ByVal emSize As Single._
ByVal style As FontStyle)
Термин «семейство шрифтов» (font family) хорошо знаком пользователям Word. Семейством называется группа шрифтов (Times New Roman, Courier New, Arial и т. д.), объединенных сходным графическим стилем, но обладающих разным кеглем и атрибутами начертания (курсив, жирный шрифт и т. д.).
Например, поскольку оператор Or объединяет значения отдельных битов, следующая команда присваивает переменной MyFont объект полужирного курсивного шрифта Arial с кеглем 12 пунктов:
myFont =New System.Drawing.Font("Arial". 12._
FontStyle.Bold Or FontStyle.Italic)
Следующая процедура события Button 1_C1ick изменяет шрифт при нажатии кнопки:
Private Sub Buttonl_Click(ByVal sender As System.Object. _
ByVal e As System.EventArgs) Handles Buttonl.Click
Dim myFont As System.Drawing.Font
myFont = New System.Drawing.Font("Arial".12.
FontStyle.Bold Or FontStyle.Italic)
Me.Font = myFont End Sub
На Рисунок 8.9 показано, как выглядит новый шрифт на кнопке.
Рисунок 8.9. Кнопка с текстом, оформленным полужирным курсивным шрифтом
При изменении свойства Font формы новый шрифт автоматически используется для вывода свойства Text всех элементов, расположенных на форме. Исключение составляют элементы, которым были назначены собственные шрифты.
В сочетании со свойствами Anchor и Dock часто используются свойства MinimumSize и MaximumSize, определяющие соответственно минимальные и максимальные размеры формы. Значения этих свойств представляют собой объекты Size. Например, следующая команда запрещает уменьшать форму до размеров, меньших размеров кнопки:
Me.MimmumSize =New Size(Buttonl.Size)
Поскольку свойство MinimumSize управляет изменением свойства Size, в заданные размеры включается размер заголовка окна. Таким образом, после выполнения предыдущей команды в уменьшенном окне почти не останется места для кнопки. Лучше воспользоваться командой вида
Me.MinimumSize = New Size(Buttonl.Size.Width * 2, Button1.Size.Height * 2)
При изменении свойства MaximumSize часто используется класс System.Windows. Forms.Screen, предназначенный для работы с экранами (с поддержкой нескольких мониторов). Этот класс также используется при изменении свойств DesktopBounds и DesktopLocation.
Новое свойство ClientSi ze возвращает информацию о клиентской области формы (области, не включающей заголовок и рамку). Свойство Bounds предназначено для чтения/записи структуры Rectangle, содержащей ширину и высоту формы и позицию ее левого верхнего угла.
Класс Rectangle пространства имен System.Drawing содержит немало полезных мето-дов; подробное описание этой вспомогательной структуры данных приведено в документации. Мы часто используем метод Inflate, предназначенный для увеличения прямоугольников с заданным приращением.
Многие свойства, методы и события форм отличаются от своих прототипов из VB6. Важнейшие изменения перечислены в табл. 8.1.
Таблица 8.1. Изменения в свойствах, методах и событиях форм
| Старый элемент формы | Новый элемент формы |
| Activate/Deactivate (события) | Переименованы в Activated/Deactivated |
| Container (свойство) | Переименовано в Parent |
| DblClick (событие) | Переименовано в DoubleClick |
| hWnd (свойство) | Переименовано в Handle |
| MouseCursor (свойство) | Переименовано в Cursor и возвращает экземпляр класса Cursor |
| Parent (свойство) | Заменено методом FindForm |
| Picture (свойство) | Заменено свойством Backgroundlmage |
| SetFocus (метод) | Переименован в Focus |
| Startup (свойство) | Заменено свойством StartPosition |
| ToolTip (свойство) | Заменено элементом ToolTip, который связывается с элементами через свойство ToolTip элемента |
| Unload (команда) Unload (событие) | Заменена методом Close Заменено событием hosing (также существует новое событие Closed, инициируемое после закрытия формы) |
| ZOrder (метод) | Заменен методами BriflgToFront и SendToBack |
| Назад | Содержание | Вперед |
Простейший вывод
Рассмотрим очень простой пример графического вывода. Следующая программа выводит растровый файл sample.bmp (находящийся в каталоге \bin решения) в левом верхнем углу формы:Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
MyBase.OnPaint(e)
Dim g As Graphics
g = e.Graphics()
g.Draw!mage(New Bitmap("sample.bmp"). 0. 0)
g.Dispose()
End Sub
Обратите внимание на вызов Dispose в строке, выделенной жирным шрифтом. Поскольку сборщик мусора не освобождает графические контексты, привыкайте делать это самостоятельно в конце процедуры Paint.
Напоминаем: если объект поддерживает метод Dispose, этот метод следует вызвать по завершении работы с объектом.
Следующий этап — рисование линий, прямоугольников и других фигур. Перед операциями такого рода следует получить объект пера, который является экземпляром класса System.Drawing.Pen. Самый распространенный конструктор класса Реп имеет следующий синтаксис:
Public Sub New(Color.Single)
Первый параметр определяет цвет пера (и входит в перечисляемый тип System. DrawingColor), а второй определяет толщину пера (другие конструкторы также позволяют задать кисть для заполнения внутренней части объекта). Например, чтобы нарисовать прямоугольник, вы определяете его размеры и вызываете g. DrawRectangle. Результат выполнения следующей программы показан на Рисунок 8.22:
Protected Overrides Sub OnPaint(ByVa1 e As PaintEventArgs)
MyBase.OnPaint(e)
Dim g As Graphics
g = e.Graphics()
Dim myPen As New PerKColor,Purple. 6)
Dim aRectangle As New Rectangle(Me.ClientRectangle.Width \4,_
Me.ClientRectangle.Height \ 4. . Me.ClientRectangle.Height \2,_
Me.ClientRectangle.Width \ 2))
g.DrawRectangle(myPen,aRectangle)
g.Dispose()
End Sub
Размещение элементов на форме во время выполнения
До выхода VB6 существовал лишь один способ размещения элементов на форме во время выполнения программы — массивы элементов. В VB6 появился более удобный режим динамического добавления элементов, но и в этом случае не обходилось без проблем с обработкой событий для добавленных элементов. В VB .NET этот процесс подвергся существенным изменениям. Например, создайте новое приложение Windows и включите в него следующий фрагмент:1 Private Sub Forml_Load(ByVal sender As System.Object,ByVal e As _
2 System.EventArgs) Handles MyBase.Load
3 Dim newButton As New System.Windows.Forms.Button()
4 ' Задать свойства newButton
5 With newButton
6 .Visible =True
7 .Size =New Size(l00.l00)
8 ' .Text ="I'm a new button"
9 ' Обычно здесь задаются и другие свойства
10 End With
11 Me.Controls.Add(newButton)
12 AddHandler newButton.Click.AddressOf Me.newButton_Click
13 End Sub
14 Public Sub newButton_Click(ByVal sender As _
15 System.Object.ByVal e As System.EventArgs)
16 MsgBox("You clicked on my new button")
17 End Sub
В строке З создается новая кнопка, а в строках 5-10 удобная сокращенная запись With используется для задания ряда свойств объекта newButton. Только в строке 11 новая кнопка размещается на форме. Строка 12 снова демонстрирует замечательную гибкость механизма обработки событий .NET: код, содержащийся в строках 14-17, назначается обработчиком события для кнопки. Возможный результат выполнения программы показан на Рисунок 8.15.
Результат вызова DrawRectangle: прямоугольник в рамке толщиной 6 пикселов Вывод текста

Метод DrawString объекта Graphics предназначен для вывода текста. При вызове этого метода задается объект шрифта, цвет, кисть и начальная точка вывода. Например, следующий фрагмент выводит текст «Hello World» — в современных книгах по программированию это превратилось в традицию. При выводе используется текущий шрифт формы, текст выводится фиолетовой кистью на белом фоне:
Protected Overrides Sub OnPaint(ByVal e As _
System.Wi ndows.Forms.PaintEventArgs)
MyBase.OnPaint(e)
Dim g As Graphics = e.Graphics
Dim theColor As Color = Color.Purple
Dim theFont As New Font("Arial", 22._
FontStyle.Bold Or FontStyle.Italic)
Me.BackColor = Col or.White
g.DrawString("Hello World!". theFont.New SolidBrush(theColor). 0, 0)
g.Dispose()
End Sub
Рисунок 8.23. Вывод текста «Hello World!» средствами GDI+
В GDI+ полностью поддерживается кодировка Unicode, что позволяет выводить текст на любом языке.
Самостоятельное программирование печати
Во многих случаях вполне достаточно элемента Pri ntDialog и автоматически сгенерированного кода, но время от времени требуется реализовать нестандартные возможности печати. Для этого необходимо написать процедуру со следующей сигнатурой:Private Sub ProcedureToDoThePrinting(ByVal Sender As Object,_
ByVal e As System.Drawing.Printing.PrintPageEventArgs)
Затем процедура при помощи делегата подключается к событию PrintPage класса PrintDocument. Например, для вызова объекта aPrintDocument класса PrintDocument с приведенной выше процедурой aPri ntDocument_PrintPage используется команда следующего вида:
AddHandler aPrintDocument.PrintPage, AddressOf Me.aPrintDocument_PrintPage
В следующем фрагменте показано, как выглядит возможная реализация печати в обработчике команды меню Print:
Private Sub mnuPrint_Click(ByVal sender As System.Object,_
ByVal e As System.EventArgs)Handles mnuPrint.Click
Dim aPrintDocument As New PrintDocument()
AddHandler aPrintDocument.PrintPage.
AddressOf Me.aPrintDocument_PrintPage
aPrintDocument.Print()
End Sub
Снова о простой программе
В главе 1 был приведен довольно сложный код Windows-приложения, автоматически сгенерированный IDE. Приложение выполняло простейшую функцию — оно реагировало на нажатие кнопки. Тогда же мы пообещали, что вскоре вы поймете, как работает эта программа. Настало время вернуться к старому примеру (последовательность действий при построении этого приложения описана в главе 1). Прежде всего обратите внимание на то, что при построении приложений на базе форм Windows VS .NET IDE автоматически включает в решение ссылки на сборки System.Drawing и System.Windows.Forms; в этом нетрудно убедиться при помощи Object Browser (Рисунок 8.8). Сборка System.Drawing состоит из одного пространства имен, классы которого предназначены для изменения размеров и позиционирования форм и элементов. Кроме того, эта сборка используется при выводе или размещении на форме графических изображений. Сборка System.Windows.Forms тоже состоит из одного пространства имен и содержит классы всех элементов управления, а также класс Wi ndows. Forms. Form, представляющий экземпляры форм.
События клавиатуры
Три события клавиатуры инициируются в тех же ситуациях, что и их аналоги из прежних версий VB.Private Sub TextBoxl_KeyPress(ByVal sender As Object._
ByVal e As System.Windows.Forms.KeyPressEventArgs)
Handles TextBoxl.KeyPress
If e.KeyChar < "0" Or e.KeyChar > "9" Then
MsgBox("only digits allowed")
End If
End Sub
События KeyDown и KeyUp, как и в VB6, могут использоваться для проверки клавиш-модификаторов (таких, как Ctrl и Alt). Класс KeyEventArgs, передаваемый этим событиям, обладает несколько большими возможностями, чем класс KeyPressEventArgs события KeyPress. Класс KeyEventArgs содержит свойство KeyData, в котором при помощи перечисляемого типа Key передается полная информация о комбинациях клавиш и о состоянии клавиш-модификаторов в момент нажатия. Свойства Modi f i ers и Shi ft позволяют узнать об одновременном нажатии трех клавиш (Alt+ +Shift+другая клавиша). Например, следующая команда проверяет, была ли нажата клавиша-модификатор Alt: If e.Modifiers =Keys.Alt Then
Если вас не интересуют такие мелочи, как различия между левой и правой клавишей Shift, удобнее воспользоваться свойствами Control Shift и Alt класса KeyEventArgs.
К сожалению, значения свойств KeyChar и KeyData нельзя сбросить, поскольку они доступны только для чтения [ Возможно, этот недочет будет исправлен в окончательной версии. ]. Впрочем, введенный символ можно «поглотить» и тем самым предотвратить его появление в элементе; для этого свойству Handled объекта события задается значение True. Пример:
If e.KeyChar < "0" Or e.KeyChar >"9" Then
e.Handled = True
End If
В результате неверный символ не появится в текстовой поле.
События проверки
В .NET вместо одного события проверки существуют целых два: Validating и Validated. Событие Validating инициируется перед утратой фокуса элементом. Например, следующий фрагмент гарантирует, что пользователь не оставит текстовое поле пустым:Public Sub TextBoxl_Validating(ByVa1 sender As Object.
ByVal e As System.ComponentModel.CancelEventArgs)
Handles TextBoxl.Validating
' Если текстовое поле не содержит символов, отменить передачу фокуса
If TextBoxl.Text.Trim = String.Empty Then e.Cancel = True
End Sub
Команда e.Cancel = True отменяет передачу фокуса от текстового поля другому элементу, если в поле нет ни одного символа.
С другой стороны, событие Val idated инициируется после утраты фокуса элементом, но до передачи его другому элементу. Таким образом, в обработчике события Validated можно обновить состояние других элементов формы.
Если свойство CausesValidation элемента равно False, события Validating и Validated не инициируются.
Создание диалоговых окон
Чтобы вывести собственное диалоговое окно, создайте форму, задайте ее свойствам ControlBox, MinimizeBox и MaximizeBox значение False, а свойству Modal — значение True. Форму следует выводить методом ShowDialog в режиме модального диалогового окна. Если при этом задать свойство TopMost равным True, диалоговое окно будет располагаться поверх всех окон на экране (и вам уже не придется использовать функцию API SetWindowPos).Однако поведение стандартных кнопок несколько изменилось по сравнению с VB6. Свойства Default и Cancel не поддерживаются, поэтому соответствующие элементы-кнопки назначаются свойствам AcceptButton и Cancel Button:
Me.AcceptButton = btnOK Me.Cancel Button = btnCancel
После вызова ShowDialog программа может узнать, какая кнопка была нажата на форме, при помощи свойства Dial ogResult кнопки или самой формы (нажатие кнопки с заданным свойством DialogResul t приводит к автоматическому закрытию формы, на которой эта кнопка находится).
Создание кнопки во время выполнения программы Наследование форм

Прежде всего следует сказать, что «визуальное наследование», часто упоминаемое в рекламных материалах по VB .NET, существует лишь в больном воображении специалистов по маркетингу. На самом деле речь идет о том, что формы, созданные в программе, могут использоваться как основа для определения новых форм посредством наследования. Конечно, это весьма удобно и полезно, но ничего принципиально нового в таком наследовании нет. Класс формы, производный от Windows. Forms. Form и дополненный специализированными свойствами, методами и событиями, в дальнейшем может использоваться в качестве базового для определения новых классов.[ История с «визуальным наследованием» как нельзя лучше демонстрирует тупость специалистов по рекламе. Возможно, эффектный термин поразит некомпетентноге менеджера, но у программистов он лишь вызывает раздражение. ]
Предположим, вы хотите создать для своей организации окно-заставку (splash screen), которое отдельные подразделения будут дополнять своими данными. Базовая форма создается следующим образом:
Public Class Form1
Inherits SplashScreenBase. Form1
Все остальное за вас сделает механизм наследования! Этот способ особенно хорош тем, что работа дизайнера автоматически синхронизируется с унаследованной формой.
Средства базового класса Control

Класс Control содержит более 300 членов, и описать их все (или хотя бы большую часть) в одной главе просто невозможно. Следовательно, вам все равно придется обратиться к электронной документации [ Кстати говоря, пакет форм Windows автоматически опознает операции с колесом мыши и обеспечивает прокрутку формы/элемента там, где это имеет смысл. Таким образом, в большинстве случаев вам не придется использовать новое событие Control.MouseWheel. ]. Однако события клавиатуры и события проверки, занимающие важное место в работе элементов, несколько отличаются от своих прототипов из VB6, и при работе с ними следует помнить о некоторых нюансах.
Свойства MaxValue и MinValue

Рассмотрев простейшие возможности использования пространства имен Windows . Forms на конкретных примерах, мы переходим к иерархии классов, показанной на Рисунок 8.20.
Диаграмма выглядит весьма устрашающе, но мы уделим основное внимание главной линии наследования:
System.ComponentModel .Component > Control
Из Рисунок 8.20 видно, что классы форм и элементов являются производными от класса Control. Например, генеалогия класса Form выглядит так:
ScrollableControl
>ContainerControl
> Form
Вывод списка установленных шрифтов

в строках 27 и 28 очень полезной функцией MeasureString: Public Function MeasureString(String.Font) As SizeF
Функция возвращает объект класса Si zeF — разновидность структуры Si ze, в которой вместо типа Integer используется тип Single. Поскольку класс SizeF содержит вещественные числа, в строках 30 и 31 преобразование осуществляется функцией CInt. В строке 30 происходит наращивание высоты графического поля, а строка 31 гарантирует, что ширина поля позволяет вместить самую длинную из выводимых строк. Проверка осуществляется методом Мах класса Math.
Cамоучитель по VB.NET
Атрибуты файла
Операции с атрибутами файлов и каталогов выполняются достаточно часто, поэтому в .NET Framework был включен удобный класс FileAttri bute. Вероятно, правильнее было бы назвать его FileDi rectoryAttri bute, поскольку все атрибуты относятся не только к файлам, но и к каталогам.Значения перечисляемого типа обычно объединяются поразрядными операциями, чтобы избежать всевозможных ошибок в программе. Не используйте команды следующего вида:
If File.GetAttributes("c:\foo.txt") = FileAttributes.Readonly Then...
В проверяемом условии не учитывается тот факт, что у файла могут быть установлены и другие атрибуты. Правильная команда должна выглядеть так:
If File.GetAttributes("c:\foo.txt") And FileAttributes.Readonly _
= FileAttributes.Readonly Then...
При необходимости атрибуты объединяются оператором Оr. Пример:
File.SetAttributes( "с: \foo.txt".
Not (FileAttributes.Archive) Or FileAttributes.Hidden)
Команда назначает атрибуты C:\foo.txt таким образом, что файл становится скрытым (Hidden), а архивный бит (Archive) сбрасывается. Ниже перечислены важнейшие значения этого перечисляемого типа:
Archive
Compressed
Di rectory
Encrypted
Hidden
Normal (атрибуты не установлены)
Readonly
System
Чтение и запись двоичных данных: классы BinaryReader и BinaryWriter
Операции чтения и записи на уровне отдельных байтов слишком примитивны, и пользоваться ими неудобно. По этой причине в .NET Framework предусмотрены гораздо более практичные способы чтения и записи данных в файловые потоки. В этом разделе мы покажем, как использовать классы BinaryReader и BinaryWriter для чтения и записи строк и примитивных типов данных. Эти классы автоматически преобразуют примитивные типы в двоичный формат, подходящий для сохранения на диске или пересылки по сети. XОбъекты Bi naryReader и BinaryWriter создаются посредством многоуровневого объединения конструкторов потоков. Иначе говоря, конструктору класса потока более высокого уровня вместо строки передается существующий объект потока. Пример приведен ниже (в строке выделенной жирным шрифтом):
Dim aFileStream As FileStream Try
aFileStream = New FileStream("c:\data.txt".FileMode.OpenOrCreate._
FileAccess.Write)
Dim myBinaryWriter As New BinaryWriter(aFileStream)
myBinaryWriter.Write("Hello world")
myBinaryWriter.writed) Catch e as Exception
Console.Writeline(e.stacktrace) Finally
If not(aFileStream is Nothing) Then aFileStream.Close()
End Try
Конструктору класса Bi naryWriter передается объект файлового потока aFileStream. Полученный в результате поток обладает расширенными возможностями и поддерживает запись текстовых и числовых данных в файл в двоичном формате. Пример записи с использованием класса BinaryWriter:
myBinaryWriter.Write("Hello world") myBinaryWriter.wri ted)
Работа этого фрагмента основана на перегрузке метода Write в классе Bi naryWriter, позволяющей легко записывать в поток любые базовые типы данных. Ниже перечислены основные перегруженные версии:
Sub Write(Byte)
Sub Write(Byte())
Sub Write(Char)
Sub Write(Char())
Sub Write(Decifnal)
Sub Write(Double)
Sub Write(Short)
Sub Write(Integer)
Sub Write(Long)
Sub Write(Byte)
Sub Write(Single)
Sub Write(String)
На Рисунок 9.3 показано, как созданный файл выглядит в шестнадцатеричном редакторе. Как видно из рисунка, строка записана в виде кодов отдельных символов, но число кодируется четырьмя байтами.
К сожалению, хотя для записи в поток существуют различные перегруженные версии метода Write, при чтении записанной информации средствами класса BinaryReader не существует аналогичных перегруженных методов Read. Вместо этого для каждого типа данных определяется собственная версия Read — ReadString, Readlnt32 (для типа Integer), ReadChar и т. д. Вы должны знать, что и в каком порядке было записано в файл; в противном случае восстановить исходные данные не удастся. Следующий фрагмент показывает, как выполняется чтение в приведенном выше примере:
aFileStream = New FileStream("с:\data.txt", FileMode.Open. FileAccess.Read)
Dim myBinaryReader As New BinaryReader(aFileStream) Console._
WriteLine( myBinaryReader.ReadString)
Console.WriteLine(myBinaryReader.Readlnt32)
Файл, записанный с применением

Двоичные потоки чтения/записи хорошо подходят для случаев, когда программисту точно известен порядок следования данных в двоичном формате, но прочитать полученный файл бывает непросто. Таким образом, для хранения обычного текста в файле лучше поискать другой вариант. В этой стандартной ситуации вместо пары BinaryReader/BinaryWriter следует использовать пару StreamReader/StreamWriter. По функциональным возможностям классы StreamReader и StreamWriter близки к традиционным средствам последовательного доступа к файлам из прежних версий VB (если не считать того, что в этих классах появилась поддержка Unicode). В классе StreamReader помимо метода Read также имеется удобный метод ReadToEnd, позволяющий прочитать весь файл за одну операцию.
Обратите внимание, что эти классы объявлены производными от абстрактных классов TextReader и TextWriter, а не от Stream. Эти абстрактные классы, объявленные с атрибутом Must Inherit, содержат общие средства чтения/записи текста. Их методы перечислены в табл. 9.11 и 9.12.
Каталоги и файлы
В VB .NET существуют два класса для работы с каталогами и два класса для работы с файлами.Другое различие между этими парами заключается в том, что классы Directory и File являются производными непосредственно от Object, а классы Directory-Info и FileInfo объявлены производными от абстрактного (Mustlnherit) класса FileSystemInfo, содержащего универсальные методы вроде LastAccessTime и FullName.
И все же самое принципиальное различие состоит в другом. Классы Directorylnfo и Filelnfo гораздо лучше подходят для рекурсивного использования результатов, как было показано в примере, приведенном в главе 4. Дело в том, что члены классов Directory и File обычно возвращают строки с описанием каталогов или файлов, тогда как члены классов Di rectorylnfo и Filelnfo обычно возвращают экземпляры своих классов. Как было показано в главе 4, эта особенность упрощает написание рекурсивных программ.
Между этими парами существует еще одно тонкое различие: они обладают разными профилями безопасности. Хотя в этой книге нам не удастся сколько-нибудь подробно описать вопросы безопасности при программировании для .NET, вы должны хотя бы в общих чертах понимать, что классы Directory и File проверяют привилегии вашего кода для обращения или модификации файла или каталога при каждом использовании, а классы Directorylnfo и Filelnfo проверяют их всего один раз при создании экземпляра объекта. Именно этим и объясняется повышение их эффективности при многократном выполнении операций с одним файлом или каталогом.
Поскольку существование данных, к которым вы обращаетесь, не гарантировано, обращения к файлам или каталогам часто заключаются в блоки Try-Catch. Впрочем, на эти классы распространяется одно из основных правил при работе с исключениями: не используйте исключения там, где можно ограничиться простой проверкой. Например, в обычных условиях незачем перехватывать исключения Di rectoryNotFoundExcepti on — проще предварительно вызвать метод Exists и убедиться в том, что каталог существует. Ниже перечислены основные исключения, встречающиеся при операциях с файлами и каталогами. Иерархию возглавляет базовый класс IOException:
IOException
>DirectoryNotFoundException
> EndOfStreamException
>FileLoadException
>FileNotFoundException
Класс Directory
Большинство методов класса Directory идентифицирует каталоги при помощи возвращаемых строк. Поскольку все члены класса объявлены общими, при обращении к ним не обязательно указывать конкретный экземпляр. Пример:System.IO.Directory.GetCurrentDirectory()
Эта команда возвращает строку с описанием текущего каталога. Метод GetDirectories(pathString) возвращает массив строк с описанием подкаталогов каталога, заданного параметром pathString. Описание интерпретируется либо как путь, заданный относительно каталога текущего приложения, либо как путь в схеме UNC (Universal Naming Convention). Следующая программа выводит имя текущего каталога и имена всех его подкаталогов.
Imports System.IO Module Modulel
Sub Main()
Dim curDir.nextDir As String Try
curDir =Directory.GetCurrentDirectory ()
Console.WriteLine(curDir)
For Each nextDir In Directory.GetDirectories(curDir)
Console.WriteLine(nextDir) Next
Catch ioe As IOException
Console.WriteLine("eeeks -i/o problems!" & ioe.message)
Catch e As Exception
Consol e. Write(e.stacktrace) Finally
Console.ReadLine()
End Try
End Sub
End Module
Если ваши потребности не ограничиваются простым выводом имен каталогов, лучше воспользоваться классом DirectoryInfo. Более подробное описание этого класса приводится ниже.
Помимо передачи строки с описанием каталога методу GetDirectories можно передать шаблон с метасимволами, используемыми в DOS [ «?» обозначает один символ, а «*» — несколько символов. ]. Важнейшие методы класса Di rectory перечислены в табл. 9.2. Во всех случаях параметры передаются по значению (с ключевым словом ByVal).
Таблица 9.2. Важнейшие методы класса Directory
| Метод | Описание |
| Create Directory (ByVal pathName As String) | Создает каталог с заданным именем и возвращает объект Directory Info для созданного каталога. При необходимости также создаются все промежуточные каталоги |
| Delete(ByVal pathName As String) | Удаляет пустой каталог. Чтобы удалить непустой каталог вместе со всеми каталогами и файлами, воспользуйтесь командой Delete (pathName As String, True) |
| Exists(ByVal pathName As String) | Возвращает логический признак существования каталога |
| GetCreationTime (ByVal pathName As String) | Возвращает объект даты, содержащий информацию о дате и времени создания каталога |
| GetCurrentDi rectory | Возвращает строку с описанием текущего каталога |
| GetDirectories (ByVaL pathName As String) | Возвращает массив строк с описанием подкаталогов. При вызове может передаваться второй строковый параметр, содержащий шаблон |
| GetDi rectoryRoot •(ByVal pathName As String) | Возвращает строку с описанием корневой части заданного пути |
| GetFiles(ByVal pathName As String) | Возвращает массив строк с описаниями файлов каталога. При вызове может передаваться второй строковый параметр, содержащий шаблон |
| GetLastAccessTime (ByVal pathName As String) | Возвращает объект даты, содержащий информацию о времени последнего обращения к каталогу |
| GetLastWriteTime (ByVal pathName As String) | Возвращает объект даты, содержащий информацию о времени последней записи в каталог |
| GetLogicalDrives | Возвращает строковый массив с именами логических дисков в формате «диск:\» (например, С:\) |
| GetParent (ByVal pathName As String) | Возвращает строку с описанием каталога, родительского по отношению к заданному |
| Move(ByVal sourceDirName As String,ByVal destDirName As String) | Перемещает каталог со всем содержимым в пределах диска |
| SetCurrentDirectory (ByVal pathName As String) | Задает текущий каталог |
| Содержание | Вперед |
Класс Path
Прежде чем рассматривать операции с каталогами и файлами, следует познакомиться с классом Path. Этот класс содержит несколько общих методов, предназначенных для обработки уточненных имен файлов [ Любопытная подробность: в описании этого класса, приведением в документации VB .NET, упоминаются некоторые аспекты кросс-платформенных операций. В частности, упоминается о различиях между символом «/» и разделителем каталогов «\», используемым в системах семейства UNIX (в том числе и в системе BSD, для которой Microsoft анонсировала поддержку CLR). ]. Сетевые имена файлов устроены несколько сложнее локальных имен, поэтому методы класса Path приносят несомненную пользу (кстати говоря, анализ даже локальных имен — занятие на любителя). Основные члены класса Path перечислены в табл. 9.1.
Классе File
Класс File, как и класс Directory, состоит из общих методов, которым при вызове обычно передается имя файла. Эти методы применяктея при копировании, удалении и перемещении файлов. Основные методы класса File перечислены в табл. 9.3. Обратите внимание,— все параметры передаются по значению (в таблице отсутствуют методы класса File, предназначенные для работы с потоками данных, — они будут рассмотрены ниже).
Классы DirectoryInfo и FileInfo
В отличие от обобщенных классов Directory и Filе классы Directory Info и FileInfо инкапсулируют конкретные (или потенциально существующие) каталоги и файлы. Чтобы использовать их, необходимо предварительно создать экземпляр класса. Под потенциальным существованием мы имеем в виду, что объект Di rectorylnfo или Fi lelnfo может быть создан даже в том случае, если файл или каталог с заданным именем еще не существует и создается при последующем вызове метода Create.Как правило, при создании экземпляров этих классов при вызове конструктора указывается имя каталога или файла. Пример:
Dim myDirectory As Directorylnfo
myDirectory = New Directorylnfo("C:\Test Directory")
Текущий каталог обозначается символом «.»:
Dim currentDir As New Directorylnfo(".")
После создания объекта Directorylnfo можно запросить различные сведения о соответствующем каталоге — например, время создания:
MsgBox(myDirectory.GreatienTime)
Как упоминалось выше, одна из самых замечательных особенностей этих классов заключается в том, что их члены возвращают объекты, а не строки. Например, в следующей программе вызов GetFiles в выделенной строке возвращает коллекцию объектов Filelnfo, что позволяет при необходимости вызвать методы этих объектов.
Imports System.IO
Module Modulel Sub Main()
Dim myDi rectory As Directorylnfo Try
myDirectory =New DirectoryInfo("C:\Test Directory")
Dim aFile As File Info
For Each aFile In myDirectory.GetFiles
Consol e. WriteLi ne( "The fi1e named " & aFile. Full Name & _
"has length " & aFile.Length) Next Catch e As Exception
MsgBox("eeks -an exception " & e.StackTrace) Finally
Console.WriteLine("Press enter to end")
Console.ReadLine()
End Try
End Sub
End Module
Монитор файловой системы
К числу принципиальных новшеств, отличающих VB .NET от предыдущих версий VB, относится и возможность сделать на сервере то, что в VB давно делалось для клиентов. Речь идет об инкапсуляции общей функциональности в элементах и многократном использовании кода. В завершение этой главы мы покажем, как использовать класс FileSystemMonitor для написания программы, которая отслеживает изменения в заданном каталоге и сигнализирует о них при помощи событий.Программа может следить за каталогом или набором файлов, соответствующих заданному фильтру. Элемент Fil eSystemMoni tor даже может произвести рекурсивный перебор всех подкаталогов заданного каталога. Инициируемые события перечислены в табл. 9.13.
Объект, сохраненный в формате SOAP Применение сериализации при клонировании объектов

У сериализации имеется и такое нетривиальное применение, как клонирование сложных объектов. Фокус заключается в том, чтобы записать объект в поток памяти MemoryStream и затем восстановить его (потоки MemoryStream позволяют работать с данными в быстрой оперативной памяти по аналогии с тем, как поток FileStream работает с файлом на диске). Ниже приведен типичный код клониро-вания:
Public Function Clone()As Object Implements ICloneable.Clone
Dim myBinaryFormatter As New Formatters.Binary.BinaryFormatter()
Try
Seriali'zeToBinary() mSTream.Position = 0
Return myBinaryFormatter.Deserialize(mSTream) Finally
mSTream.Close()
End Try
End Function
Sub SerializeToBinary()
Dim myBinaryFormatter As New Formatters.Binary.BinaryFormatter()
Try
mSTream = New MemoryStream()
myBinaryFormatter.Serialize(mSTream.Me)
Catch
Throw
End Try
End Sub
Объектные потоки: сохранение и восстановление объектов
Объектно-ориентированное программирование вряд ли получило бы столь широкое признание, если бы программист не мог сохранить объект в текущем состоянии и восстановить его позднее. Запись объекта в поток данных называется сериализацией (serialization), а обратный процесс называется десериализацией (deserialization). В нескольких ближайших разделах мы познакомим читателя с основными принципами сериализации и десериализации.Но прежде, чем переходить к рассмотрению новой темы, следует заметить, что это более сложная и тонкая проблема, чем кажется на первый взгляд. Почему? Одна из причин заключается в том, что объект может содержать другие объекты (вспомните классы Manager и Secretary из главы 5). Следовательно, процесс сохранения должен поддерживать рекурсивное сохранение внутренних объектов. Более того, при этом необходимо позаботиться об отсутствии дублирования. Если на 100 программистов в отделе приходится одна секретарша, было бы нежелательно сохранять данные секретарши в 100 экземплярах, когда вполне достаточно одного экземпляра с соответствующей настройкой ссылок (нечто похожее происходит при приведении баз данных к нормальной форме с исключением избыточных данных).
К счастью, в .NET Framework сохранение объектов не требует особых усилий со стороны программиста. Как будет вскоре показано, объекты можно сохранять даже в понятном для человека формате SOAP (Simple Object Access Protocol), основанном на языке XML.
Потоки данных
Как упоминалось во вступительной части, одной из целей проектирования класса System. I0.Stream было абстрагирование примитивных операций при работе с потоками байтов. В соответствий с этой концепцией каждая конкретная реализация класса Stream должна предоставить свои версии следующих методов:В реализации метода Seek обычно используются три значения (Begin, Current и End), входящие в удобный перечисляемый тип SeekOrigin.
В табл. 9.7 перечислены основные методы абстрактного класса Stream, смысл которых должен сохраниться и в производных классах.
Практический пример: динамический список с поддержкой сериализации
Прежде чем приводить полный код примера, мы хотим предупредить об одной потенциальной трудности, которая постоянно возникает при восстановлении сохраненных объектов, а особенно объектов, хранящихся в динамических списках. Итак, после завершения восстановления мы получаем набор обобщенных объектов, хранящихся в динамическом массиве. Но как определить истинный тип этих объектов, чтобы выполнить правильное преобразование? В следующем примере эта информация жестко фиксируется в процессе восстановления, поскольку мы точно знаем порядок занесения объектов иерархии Employee в массив. В более общей ситуации эти сведения пришлось бы сохранять в отдельном файле.В настоящем примере мы создаем менеджера (класс Manager) с именем Sally и секретаря (класс Secretary) с именем Тот. Класс Manager содержит внутренний объект класса Secretary в одной из переменных; класс Secretary содержит ссылку на Manager.
Не забудьте включить в решение ссылку на сборку System.Runtime.Serialization.For-matters.Soap, это необходимо для работы программы.
Ниже приведен код тестовой части программы. Три ключевые строки выделены жирным шрифтом:
Option Strict On
' Использует сборку System.Runtime.Serialization.Formatters.Soap
Imports System.IO
Imports System.Runtime.Serialization
Imports System.Runtime.Serialization.Formatters
Module Modulel
Sub Main()
Dim Sally As New Manager("Sally". 150000)
Dim Tom As Secretary
Tom = New Secretary("Tom". 100000, Sally)
Sally.MySecretary = Tom
Dim Employees As New ArrayList() Employees. Add(Tom)
Employees.Add(Sally)
Console.WriteLine(Tom.TheName & "is employee " & _
Tom.ThelD & "and has salary " & Tom.Salary)
Console.WriteLine("Tom's boss is " & Tom.MyManager.TheName)
Console.WriteLine("Sally's secretary is " & Sally.MySecretary.TheName)
Console. WriteLine() Console.Writel_ine(Sally.TheName & "is employee " & _
Sally.ThelD & "has salary " & Sally.Salary) Sally.RaiseSalary(0.lD)
Console.WriteLinet"After raise " & Sally.TheName &_ "has salary "_
& Sally.Salary)

Рисунок 9.5. Сериализация динамического массива
Ниже приведена остальная часть кода этого примера.
Sub SerializeToSoap(ByVal myEmployees As ArrayList._
ByVal fName As String)
Dim fStream As FileStream
Dim mySoapFormatter As New Formatters.Soap.SoapFormatter()
Try
fStream = New FileStreamtfName. FileMode.Create.FileAccess.Write)
mySoapFormatter.Serialize(fStream. myEmployees)
Catch
Throw Finally
If Not (fStream Is Nothing) Then fStream.Close()
End Try
End Sub
Function DeSerializeFromSoap(ByVal fName As String) As ArrayList
Dim fStream As New FileStream(fName. Fi1eMode.Open. FileAccess.Read)
Dim mySoapFormatter As New Formatters.Soap.SoapFormatter()
Try
fStream = New FileStream(fName, FileMode.Open. FileAccess.Read)
Return
CType(mySoapFormatter.Deserialize(fStream), ArrayList)
Catch
Throw Finally
If Not (fStream Is Nothing) Then fStream.Close()
End Try
End Function
End Module
Private m_Name As String
Private m_Salary As Decimal
Private Const LIMIT As Decimal = 0.1D
Private Shared m_EmployeeId As Integer = 1000
Private m_myID As Integer
Public Sub New(ByVal sName As String. ByVal curSalary As Decimal)
m_Name = sName
m_Salary = curSalary
m_myID = m_EmployeeId
m_EmployeeId = m_EmployeeId + 1
End Sub
Readonly Property TheIDO As Integer
Get
Return mjnyID
End Get
End Property Readonly Property TheName()As String
Get
Return m_Name
End Get
End Property Readonly Property Salary()As Decimal
Get
Return MyClass.m_Salary
End Get
End Property Public Overridable Overloads
Sub RaiseSalary(ByVal Percent As Decimal)
If Percent > LIMIT Then
' Недопустимая операция
Console.WriteLineC'MUST HAVE PASSWORD " & _
"TO RAISE SALARY MORE THAN LIMIT!!!!") Else
m_Salary = (1 + Percent) * m_Salary
End If
End Sub
Public Overridable Overloads
Sub RaiseSa1ary(ByVal Percent As Decimal._
ByVal Password As String) If Password = "special" Then
m_Salary = (1 + Percent) * m_Salary
End If
End Sub
End Class
Inherits Employee
Private m_Sec As Secretary
Private m_Salary As Decimal
Public Sub New(ByVal sName As String,_
ByVal curSalary As Decimal)
MyBase.New(sName. curSalary)
End Sub
Public Sub New(ByVal sName As String.ByVal curSalary As Decimal.
ByVal mySec As Secretary)
MyBase.New(sName.curSalary)
m_Sec = mySec
End Sub
Property MySecretary()As Secretary Get
Return m_Sec End Get Set(ByVal Value As Secretary)
m_Sec = Value
End Set
End Property
Public Overloads Overrides
Sub RaiseSalary(ByVal percent As Decimal)
MyBase.RaiseSalary(2 * percent, "special")
End Sub
End Class
Public Class Secretary Inherits Employee
Private m_Boss As Manager
Public Sub New(ByVal sName As String. ByVal curSalary As Decimal,
ByVal myBoss As Manager) MyBase.New(sName, curSalary)
m_Boss = myBoss
End Sub
Property MyManager() As Manager Get
Return m_Boss
End Get Set(ByVal Value As Manager)
m_Boss = Value
End Set
End Property
End Class
Простая сериализация
Прежде всего импортируйте пространство имен System.Runtime.Serialization, это сэкономит немало времени на вводе имен. В типичной ситуации поддержка сериализации включается простым добавлением атрибута в заголовок класса:
Атрибут
В .NET Framework сериализация поддерживается в классах, реализующих интерфейс ISerializable.
После пометки класса атрибутом
Следующий пример показывает, как организовать сериализацию для массива. Массив ArrayList является объектом и может содержать другие объекты (в нашем примере это объекты иерархии Employee). Поскольку динамические массивы сери-ализуются автоматически, остается лишь пометить атрибутом
Sub SerializeToBinary(ByVal myEmployees As ArrayList._
ByVal fName As String)
Dim fStream As FileStream
Dim myBinaryFormatter As New Formatters.Binary.BinaryFormatter()
Try
fStream = New FileStreamtfName,FileMode.Create, FileAccess.Write)
myBinaryFormatter.Serialize(fStream, myEmployees)
Catch e As Exception
Throw e Finally
If Not (fStream Is Nothing) Then fStream.Close()
End Try
End Sub
Чтобы вместо двоичного формата массив сохранялся в формате SOAP, достаточно включить в проект ссылку на сборку System.Runtime.Serialization.Formatters.Soap (это делается в диалоговом окне Project > References) и привести выделенные строки к следующему виду:
Dim mySoapFormatter As New Formatters.Soap.SoapFormatter()
и
mySoapFormatter.Serialize(fStream. myEmployees)
На Рисунок 9.4 показано, как выглядит полученный файл в формате SOAP.
Отдельные поля класса можно пометить атрибутом
Простое восстановление
С восстановлением сохраненного объекта дело обстоит сложнее: поскольку при десериализации возвращается тип Object, приходится выполнять явное преобразование к нужному типу, как в выделенной строке следующего фрагмента:
Function DeSerializeFromSoap(ByVal fName As String) As ArrayList
Dim fStream As New FileStreamtfName.FileMode.Open. FileAccess.Read)
Dim mySoapFormatter As New Formatters.Soap.SoapFormatter()
Try
fStream = New FileStream("C:\test.xml". FileMode.Open.
FileAccess.Read)
Return CType(mySoapFormatter.Deserialize(fStream), ArrayList)
Catch e As Exception
Throw e Finally
If Not (fStream Is Nothing) Then fStream.Close()
End Try
End Function
Рекурсивный просмотр дерева каталогов
Класс Directorylnfo удобен тем, что на его основе легко строятся обобщенные процедуры для рекурсивного перебора дерева каталогов. Как было показано в главе 4, при этом удобно использовать вспомогательную процедуру, которая, в свою очередь, вызывает другую процедуру для работы с файлами заданного каталога. Ниже приведена одна из возможных реализаций этого рекурсивного процесса:
Option Strict On Imports System.IO Module Modulel
SubMain()
Dim nameOfDirectory As String ="C:\"
Dim myDirectory As DirectoryInfo
myDirectory = New DirectoryInfo(nameOfDirectory)
WorkWithDirectory(myDirectory)
End Sub
Public Sub WorkWithDirectory(ByVal aDir As Directorylnfo)
Dim nextDir As Directorylnfo WorkWithFilesInDir(aDir)
For Each nextDir In aDir.GetDirectories
WorkWithDirectory(nextDir) Next
End Sub
Public Sub WbrkWithFilesInDir(ByVal aDir As Directorylnfo)
Dim aFile As Filelnfo For Each aFile In aDir.GetFiles()
' Выполнить операцию с файлом.
' В нашем примере просто выводится уточненное имя.
Consolе.WriteLine(aFi1e.Ful1 Name) Next
End Sub
End Module
Следующий, более реалистичный пример активизирует форму, показанную на Рисунок 9.1. Программа заносит все скрытые файлы заданного каталога в список и продолжает рекурсивную обработку дерева каталогов. Курсор мыши заменяется изображением песочных часов; по этому признаку пользователь узнает о том, что программа выполняет какую-то длительную операцию.
В действительности эту программу следовало бы реализовать в многопоточной модели, чтобы форма реагировала на действия пользователя, — о том, как это делается, рассказано в следующей главе. Конечно, проблему можно решить включением команды DoEvents в код обновления списка, однако многопоточное решение выглядит более профессионально.
Private Sub Buttonl_Click(ByVal sender As System.Object,_
ByVal e As System.EventArgs) Handles Buttonl.Click
'Заменить курсор изображением песочных часов
Me.Cursor = Cursors.WaitCursor ListBoxl. Items. Clear()
WorkWithDirectory(New Directorylnfo(TextBoxl.Text))
Me.Cursor = Cursors.Default
End Sub
Public Sub WorkWithDirectory(ByVal aDir As Directorylnfo)
Dim nextDir As Directorylnfo Try
WorkWithFilesInDir(aDir)
For Each nextDir In aDir.GetDirectories
WorkWithDirectory(nextDi r) Next
Catch e As Exception
MsgBox(e.message SvbCrLf Se.StackTrace)
End Try
End Sub
Public Sub WorkWithFilesInDir(ByVal aDir As Directorylnfo)
Dim aFile As Filelnfo For Each aFile In aDir.GetFiles()
If aFile.Attributes And _
FileAttributes.Hidden = FileAttributes.Hidden Then
ListBoxl. Items. Add( "FOUND hidden filenamed " & aFile. FullName)
End If
Next
End Sub
Сетевые потоки
Среди областей, в которых особенно наглядно проявляются возможности абстрактной модели потока, особое место занимает пересылка информации в Интернете. Работа с низкоуровневым кодом HTML и XML почти не требует усилий со стороны программиста. Хотя в этом разделе мы сможем дать лишь общее представление об этой важной теме и о задействованных пространствах имен, по крайней мере вы увидите, как потоковая интерпретация сетевых данных реализуется на практике. В рассмотренном ниже примере мы передаем информацию на web-сайт и получаем непосредственный HTML-код новой страницы в качестве результата запроса. Анализ полученного HTML-кода приносит нужную информацию.
Мы не смогли устоять перед искушением: наше маленькое приложение обращается на сайт Amazon.com и возвращает текущие сведения о количестве проданных экземпляров нашей книги. Обобщенный алгоритм выглядит следующим образом:
http://www.amazon.com/exec/obidos/ASIN/1893115992
Следующий конструктор создает экземпляр класса с номером ISBN, переданным в виде строкового параметра:
Public Sub New(ByVal ISBN As String)
m_URL ="http://wvM.amazon.com/exec/obidos/ASIN/" & ISBN
End Sub
Доступное только для чтения свойство GetRank нашего класса просто вызывает закрытую функцию, основной код которой приведен в следующих восьми строках:
1 Dim theURL As New URI(m_URL)
2 Dim theRequest As WebRequest
3 theRequest = WebRequest.Create(theURL)
4 Dim theResponse As WebResponse
5 theResponse = theRequest.GetResponse
6 Dim aReader As New StreamReader(theResponse.GetResponseStream())
7 Dim theData As String .
8 theData = aReader.ReadToEnd
В строке 1 создается объект класса URI. В строках 2 и 3 генерируется web-запрос, передаваемый на сайт Amazon.com. Строки 4 и 5 принимают ответ на запрос, а в строке 6 метод GetResponseStream класса Response конструирует объект StreamReader для полученного потока. На этой стадии строковая переменная theData содержит низкоуровневый HTML-код web-страницы нашей книги.
Amazon.com Sales Rank:
5.776
Остается лишь проанализировать переменную theData и извлечь из нее данные о продажах. Для этого мы воспользуемся вспомогательной функцией Analyze:
Private Function Analyze(ByVal theData As String)As Integer
Dim Location As Integer
Location - theData.IndexOf("Amazon.com Sales Rank:")
+ "Amazon.com Sales Rank:".Length
Dim temp As String
Do Until theData.Substring(Location.l) = "<" temp = temp
StheData.Substring(Location.l)
Location += 1
Loop
Return CInt(temp)
End Function
Для анализа строковой переменной также можно воспользоваться классом регулярных выражений из пространства имен System.Text.
Ниже приведен полный код тестового модуля (разумеется, для тестирования вам также понадобится Интернет-соединение):
Option Strict On Imports System.IO Imports System.Net
Module Module1
Sub Main()
Dim myBook As New AmazonRanker("1893115992")
MsgBox("This book's current rank is " & myBook.GetRank)
End Sub
End Module
Public Class AmazonRanker
Private m_URL As String
Private m_Rank As Integer
Public Sub New(ByVal ISBN As String)
m_URL = "http://www.amazon.com/exec/obidos/ASIN/" & ISBN
End Sub
Public Readonly
Property GetRank() As Integer
Get Return ScrapeAmazon()
End Get End Property
Private Function ScrapeAmazon() As Integer Try
Dim theURL As New URI(m_URL)
Dim theRequest As WebRequest
theRequest = WebRequest.Create(theURL)
Dim theResponse As WebResponse
theResponse = theRequest.GetResponse
DimaReaderAsNew
StreamReader(theResponse.GetResponseStream())
Dim theData As String
theData = aReader.ReadToEnd
Return Analyze(theData) Catch E As Exception
Console.WriteLine(E.StackTrace)
Console. ReadLine()
End Try
End Function
Private Function Analyze(ByVal theData As String) As Integer
Dim Location As Integer
Location = theData.IndexOf("Amazon.com Sales Rank:") + "Amazon.com
Sales Rank:".Length Dim temp As String
Do Until theData.Substring(Location.l) = "<" temp - temp
&theData.Substring(Location,l) Location += 1 Loop
Return CInt(temp)
End Function
End Class
Пример этой программы наглядно показывает, какие неуловимые проблемы порой возникают в результате локализации. Когда наш друг запустил эту программу в Европе, она отказалась работать. Оказалось, что на сайте Amazon по вполне понятным причинам используется американский числовой формат, а программа запускалась в европейской версии Windows, в результате чего символ «,» интерпретировался неверно. Разумеется, проблема легко решается — достаточно, чтобы функция возвращала значение строкового типа.
Основные методы
Read Читает один символ из входного потока. Перегруженная версия читает в символьный массив определенное количество символов начиная с заданной позиции ReadLine Читает символы до комбинации CR+LF и возвращает их в виде строкового значения. Если текущая позиция находится в конце файла, метод возвращает Nothing ReadToEnd Читает все символы от текущей позиции до конца TextReader и возвращает их в виде одной строки (метод особенно удобен при работе с небольшими файлами)
Члены базового класса
GetFileSystemlnfos Хороший пример использования абстрактных классов: метод возвращает массив объектов FileSystemlnfo, представляющих все файлы и подкаталоги текущего каталога MoveTo(ByVal destDirName As String) Перемещает Directorylnfo и все его содержимое Root (свойство) Объект DirectoryIlnfo для корневого каталога в иерархии текущего каталога
Члены класса Filelnfo
Копирует существующий файл и возвращает объект Filelnfo для копии. Необязательный логический параметр управляет перезаписью существующих файлов Create Создает файл по имени, указанному при конструировании объекта Filelnfo, и возвращает объект FileSystem для нового файла Delete Удаляет файл, представленный объектом FileInfo MoveTo(ByVal destFileName As String) Перемещает файл
Идея выделения общей функциональности в абстрактный базовый класс выглядит впол-не логично, однако в данном случае она реализована не лучшим образом. Например, свдйство Length присутствует в файле FileInfo, но не поддерживается в FileSystemlnfo, поэтому для вычисления размера дерева каталогов приходится прибегать к услугам другого объекта — а именно вызывать метод Size объекта Folder, входящего в модель FileSystemObject. Эта модель впервые была представлена в VBScript, поэтому в решение приходится включать ссылку на библиотеку сценарной поддержки на базе СОМ.
Основные методы класса
В .NET Framework входят классы для работы с XML, спроектированные по образцу класса Stream. Впрочем, пространства имен XML в .NET велики и сложны, и о них вполне можно было бы написать отдельную книгу.
Значения перечисляемого
Объекты FHeStream также возвращаются следующими методами классов File и FHelnfo: File.Create, File.Open, File.OpenRead, File.OpenWrite, FHeInfo.Create, FHelnfo.Open, FHelnfo.OpenRead.
Хотя файловые потоки поддерживают произвольный доступ методом Seek, базовый класс-FileStream ориентирован исключительно на операции с байтами, поэтому его возможности ограничиваются простой записью байта или массива байтов методами WriteByte и Write. Приведенный ниже фрагмент создает файл, показанный на Рисунок 9.2:
Option Strict On Imports System.IO
Module Modulel
Sub Main()
Dim i As Integer
Dim theBytes(255) As Byte
For i = 0 To 255
theBytes(i) = CByte(i)
Next
Dim myFileStream As FileStream
Try
myFileStream = New FileStream("C:\foo",
Fi1eMode.OpenOrCreate. FileAccess.Write)
myFlleStream.Write(theBytes, 0. 256) Finally
If Not (myFileStream Is Nothing) Then
myFileStream.Close()
End Try
DisplayAFile("C:\foo")
End Sub
End Module
Важнейшие члены классов FileSystemInfo, FileInfo и DirectoryInfo
Класс FileSystemlnfo является базовым для классов Directorylnfo и Filelnfo и содержит большую часть их общей функциональности. Перед нами хороший пример тех возможностей, которые открываются при использовании абстрактных базовых классов. В классе Directory Info существует метод GetFileSystemlnfos, который возвращает массив объектов FileSystemlnfо, представляющих файлы и подкаталоги заданного каталога. Такое становится возможным только благодаря существованию класса FileSystemlnfo. Важнейшие члены базового класса FileSystemlnf о перечислены в табл. 9.4.
Запись в файл
Начнем с рассмотрения команды, часто встречающейся при работе с файловыми потоками:
Dim myFileStream As New FileStream("MyFile.txt". FileMode.OpenOrCreate, FileAccess.Write)
Как видно из приведенного фрагмента, эта версия конструктора FileStream получает имя файла (заданное по отношению к текущему каталогу, если не указано полное имя) и два параметра, значения которых относятся к перечисляемым типам FileMode и FileAccess соответственно. Таким образом, в нашем примере конструктор Fi1eStream либо создает файл с именем MyFile.txt в текущем каталоге, либо открывает его, если файл с таким именем уже существует. В любом случае программа сможет записывать данные в файл. Часто встречаются и другие конструкторы класса Fi leStream:
Cамоучитель по VB.NET
Анализ взаимной блокировки в окне потоков

Следовательно, если убрать вызов Rnd в строке 98 и заменить его фрагментом
mFork.GrabFork(Me)
mKnife.GrabKnife(Me)
взаимная блокировка исчезает!
Более серьезный пример: извлечение данных из кода HTML
Мы рекомендуем использовать потоки лишь в том случае, когда функциональность программы четко делится на несколько операций. Хорошим примером является программа извлечения данных из кода HTML из главы 9. Наш класс выполняет две операции: выборку данных с сайта Amazon и их обработку. Перед нами идеальный пример ситуации, в которой многопоточное программирование действительно уместно. Мы создаем классы для нескольких разных книг и затем анализируем данные в разных потоках. Создание нового потока для каждой книги повышает эффективность программы, поскольку во время приема данных одним потоком (что может потребовать ожидания на сервере Amazon) другой поток будет занят обработкой уже полученных данных.
Многопоточный вариант этой программы работает эффективнее однопоточного варианта лишь на компьютере с несколькими процессорами или в том случае, если прием дополнительных данных удается эффективно совместить с их анализом.
Как говорилось выше, в потоках могут запускаться только процедуры, не имеющие параметров, поэтому в программу придется внести небольшие изменения. Ниже приведена основная процедура, переписанная с исключением параметров:
Public Sub FindRank()
m_Rank = ScrapeAmazon()
Console.WriteLine("the rank of " & m_Name & "Is " & GetRank)
End Sub
Поскольку нам не удастся воспользоваться комбинированным полем для хранения и выборки информации (написание многопоточных программ с графическим интерфейсом рассматривается в последнем разделе настоящей главы), программа сохраняет данные четырех книг в массиве, определение которого начинается так:
Dim theBook(3.1) As String theBook(0.0) = "1893115992"
theBook(0.l) = "Programming VB .NET" ' И т.д.
Четыре потока создаются в том же цикле, в котором создаются объекты AmazonRanker:
For i= 0 То 3
Try
theRanker = New AmazonRanker(theBook(i.0). theBookd.1))
aThreadStart = New ThreadStar(AddressOf theRanker.FindRan()
aThread = New Thread(aThreadStart)
aThread.Name = theBook(i.l)
aThread.Start() Catch e As Exception
Console.WriteLine(e.Message)
End Try
Next
Ниже приведен полный текст программы:
Option Strict On Imports System.IO Imports System.Net
Imports System.Threading
Module Modulel
Sub Main()
Dim theBook(3.1) As String
theBook(0.0) = "1893115992"
theBook(0.l) = "Programming VB .NET"
theBook(l.0) = "1893115291"
theBook(l.l) = "Database Programming VB .NET"
theBook(2,0) = "1893115623"
theBook(2.1) = "Programmer 's Introduction to C#."
theBook(3.0) = "1893115593"
theBook(3.1) = "Gland the .Net Platform "
Dim i As Integer
Dim theRanker As =AmazonRanker
Dim aThreadStart As Threading.ThreadStart
Dim aThread As Threading.Thread
For i = 0 To 3
Try
theRanker = New AmazonRankerttheBook(i.0). theBook(i.1))
aThreadStart = New ThreadStart(AddressOf theRanker. FindRank)
aThread = New Thread(aThreadStart)
aThread.Name= theBook(i.l)
aThread.Start()
Catch e As Exception
Console.WriteLlnete.Message)
End Try Next
Console.ReadLine()
End Sub
End Module
Public Class AmazonRanker
Private m_URL As String
Private m_Rank As Integer
Private m_Name As String
Public Sub New(ByVal ISBN As String. ByVal theName As String)
m_URL = "http://www.amazon.com/exec/obidos/ASIN/" & ISBN
m_Name = theName End Sub
Public Sub FindRank() m_Rank = ScrapeAmazon()
Console.Writeline("the rank of " & m_Name & "is "
& GetRank) End Sub
Public Readonly Property GetRank() As String Get
If m_Rank <> 0 Then
Return CStr(m_Rank) Else
' Проблемы
End If
End Get
End Property
Public Readonly Property GetName() As String Get
Return m_Name
End Get
End Property
Private Function ScrapeAmazon() As Integer Try
Dim theURL As New Uri(m_URL)
Dim theRequest As WebRequest
theRequest =WebRequest.Create(theURL)
Dim theResponse As WebResponse
theResponse = theRequest.GetResponse
Dim aReader As New StreamReader(theResponse.GetResponseStream())
Dim theData As String
theData = aReader.ReadToEnd
Return Analyze(theData)
Catch E As Exception
Console.WriteLine(E.Message)
Console.WriteLine(E.StackTrace)
Console. ReadLine()
End Try End Function
Private Function Analyze(ByVal theData As String) As Integer
Dim Location As.Integer Location = theData.IndexOf("Amazon.com
Sales Rank:") _
+ "Amazon.com Sales Rank:".Length
Dim temp As String
Do Until theData.Substring(Location.l) = "<" temp = temp
&theData.Substring(Location.l) Location += 1 Loop
Return Clnt(temp)
End Function
End Class
Многопоточные операции часто используются в .NET и пространствах имен ввода-вы-вода, поэтому в библиотеке .NET Framework для них предусмотрены специальные асинхронные методы. Дополнительная информация о применении асинхронных методов при написании многопоточных программ приведена в описании методов BeginGetResponse и EndGetResponse класса HTTPWebRequest
Домены приложений
Программные потоки .NET работают в так называемых доменах приложений, определяемых в документации как «изолированная среда, в которой выполняется приложение». Домен приложения можно рассматривать как облегченный вариант процессов Win32; один процесс Win32 может содержать несколько доменов приложений. Главное отличие между доменами приложений и процессами заключается в том, что процесс Win32 обладает самостоятельным адресным пространством (в документации домены приложений также сравниваются с логическими процессами, работающими внутри физического процесса). В .NET все управление памятью осуществляется исполнительной средой, поэтому в одном процессе Win32 могут работать несколько доменов приложений. Одним из преимуществ этой схемы является улучшение возможностей масштабирования (scaling) приложений. Средства для работы с доменами приложений находятся в классе AppDomain. Рекомендуем изучить документацию по этому классу. С его помощью можно получить информацию об окружении, в котором работает ваша программа. В частности, класс AppDomain применяется при выполнении рефлексии для системных классов .NET. Следующая программа выводит список загруженных сборок.
Imports System.Reflection
Module Modulel
Sub Main()
Dim theDomain As AppDomain
theDomain = AppDomain.CurrentDomain
Dim Assemblies()As [Assembly ]
Assemblies = theDomain.GetAssemblies
Dim anAssemblyxAs [Assembly ]
For Each anAssembly In Assemblies
Console.WriteLinetanAssembly.Full Name) Next
Console.ReadLine()
End Sub
End Module
Фоновые потоки (демоны)
Некоторые потоки, работающие в фоновом режиме, автоматически прекращают работу в тот момент, когда останавливаются другие компоненты программы. В частности, сборщик мусора работает в одном из фоновых потоков. Обычно фоновые потоки создаются для приема данных, но это делается лишь в том случае, если в других потоках работает код, способный обработать полученные данные. Синтаксис: имя потока.IsBackGround = True
Если в приложении остались только фоновые потоки, приложение автоматически завершается.
Форма с заблокированной кнопкой

Предполагается, что отдельный поток выполняет подсчет и разблокирует недоступную кнопку. Конечно, это можно сделать; более того, такая задача возникает достаточно часто. К сожалению, вы не сможете действовать наиболее очевидным образом — организовать связь вторичного потока с потоком графического интерфейса, сохраняя ссылку на кнопку ShowCount в конструкторе, или даже с использованием стандартного делегата. Иначе говоря, никогда не используйте вариант, приведенный ниже (основные ошибочные строки выделены жирным шрифтом).
Public Class RandomCharacters
Private m_0ata As StringBuilder
Private m_CountDone As Boolean
Private mjength. m_count As Integer
Private m_Button As Windows.Forms.Button
Public Sub New(ByVa1 n As Integer,_
ByVal b As Windows.Forms.Button)
m_length = n - 1
m_Data = New StringBuilder(mJength)
m_Button = b MakeString()
End Sub
Private Sub MakeString()
Dim I As Integer
Dim myRnd As New Random()
For I = 0 To m_length
m_Data.Append(Chr(myRnd.Next(65. 90)))
Next
End Sub
Public Sub StartCount()
GetEes()
End Sub
Private Sub GetEes()
Dim I As Integer
For I = 0 To mjength
If m_Data.Chars(I) = CChar("E") Then
m_count += 1
End If Next
m_CountDone =True
m_Button.Enabled=True
End Sub
Public Readonly
Property GetCount()As Integer
Get
If Not (m_CountDone) Then
Throw New Exception("Count not yet done") Else
Return m_count
End If
End Get
End Property
Public Readonly Property IsDone() As Boolean
Get
Return m_CountDone
End Get
End Property
End Class
Вполне вероятно, что в некоторых случаях этот код будет работать. Тем не менее:
Организовать взаимодействие объектов с применением событий тоже не удастся. 06-работник события выполняется в том же потоке, в котором произошел вызов RaiseEvent поэтому события вам не помогут.
И все же здравый смысл подсказывает, что в графических приложениях должны существовать средства модификации элементов из другого потока. В .NET Framework существует поточно-безопасный способ вызова методов приложений GUI из другого потока. Для этой цели используется особый тип делегатов Method Invoker из пространства имен System.Windows. Forms. В следующем фрагменте приведен новый вариант метода GetEes (измененные строки выделены жирным шрифтом):
Private Sub GetEes()
Dim I As Integer
For I = 0 To m_length
If m_Data.Chars(I) = CChar("E")Then
m_count += 1
End If Next
m_CountDone = True Try
Dim mylnvoker As New Methodlnvoker(AddressOf UpDateButton)
myInvoker.Invoke() Catch e As ThreadlnterruptedException
'Неудача
End Try
End Sub
Public Sub UpDateButton()
m_Button.Enabled =True
End Sub
Межпоточные обращения к кнопке осуществляются не напрямую, а через Method Invoker. .NET Framework гарантирует, что этот вариант безопасен по отношению к потокам.
Главная опасность (общие данные)
До настоящего момента рассматривался единственный безопасный случай использования потоков — наши потоки не изменяли общих данных. Если разрешить изменение общих данных, потенциальные ошибки начинают плодиться в геометрической прогрессии и избавить от них программу становится гораздо труднее. С другой стороны, если запретить модификацию общих данных разными потоками, многопоточное программирование .NET практически не будет отличаться от ограниченных возможностей VB6.
Вашему вниманию предлагается небольшая программа, которая демонстрирует возникающие проблемы, не углубляясь в излишние подробности. В этой программе моделируется дом, в каждой комнате которого установлен термостат. Если температура на 5 и более градусов по Фаренгейту (около 2,77 градусов по Цельсию) меньше положенной, мы приказываем системе отопления повысить температуру на 5 градусов; в противном случае температура повышается только на 1 градус. Если текущая температура больше либо равна заданной, изменение не производится. Регулировка температуры в каждой комнате осуществляется отдельным потоком с 200-миллисекундной задержкой. Основная работа выполняется следующим фрагментом:
If mHouse.HouseTemp < mHouse.MAX_TEMP = 5 Then Try
Thread.Sleep(200)
Catch tie As ThreadlnterruptedException
' Пассивное ожидание было прервано
Catch e As Exception
' Другие исключения End Try
mHouse.HouseTemp +- 5 ' И т.д.
Ниже приведен полный исходный текст программы. Результат показан на Рисунок 10.6: температура в доме достигла 105 градусов по Фаренгейту (40,5 градуса по Цельсию)!
1 Option Strict On
2 Imports System.Threading
3 Module Modulel
4 Sub Main()
5 Dim myHouse As New House(l0)
6 Console. ReadLine()
7 End Sub
8 End Module
9 Public Class House
10 Public Const MAX_TEMP As Integer = 75
11 Private mCurTemp As Integer = 55
12 Private mRooms() As Room
13 Public Sub New(ByVal numOfRooms As Integer)
14 ReDim mRooms(numOfRooms = 1)
15 Dim i As Integer
16 Dim aThreadStart As Threading.ThreadStart
17 Dim aThread As Thread
18 For i = 0 To numOfRooms -1
19 Try
20 mRooms(i)=NewRoom(Me, mCurTemp,CStr(i) &"throom")
21 aThreadStart - New ThreadStart(AddressOf _
mRooms(i).CheckTempInRoom)
22 aThread =New Thread(aThreadStart)
23 aThread.Start()
24 Catch E As Exception
25 Console.WriteLine(E.StackTrace)
26 End Try
27 Next
28 End Sub
29 Public Property HouseTemp()As Integer
30 . Get
31 Return mCurTemp
32 End Get
33 Set(ByVal Value As Integer)
34 mCurTemp = Value 35 End Set
36 End Property
37 End Class
38 Public Class Room
39 Private mCurTemp As Integer
40 Private mName As String
41 Private mHouse As House
42 Public Sub New(ByVal theHouse As House,
ByVal temp As Integer, ByVal roomName As String)
43 mHouse = theHouse
44 mCurTemp = temp
45 mName = roomName
46 End Sub
47 Public Sub CheckTempInRoom()
48 ChangeTemperature()
49 End Sub
50 Private Sub ChangeTemperature()
51 Try
52 If mHouse.HouseTemp < mHouse.MAX_TEMP - 5 Then
53 Thread.Sleep(200)
54 mHouse.HouseTemp +- 5
55 Console.WriteLine("Am in " & Me.mName & _
56 ".Current temperature is "&mHouse.HouseTemp)
57 . Elself mHouse.HouseTemp < mHouse.MAX_TEMP Then
58 Thread.Sleep(200)
59 mHouse.HouseTemp += 1
60 Console.WriteLine("Am in " & Me.mName & _
61 ".Current temperature is " & mHouse.HouseTemp)
62 Else
63 Console.WriteLine("Am in " & Me.mName & _
64 ".Current temperature is " & mHouse.HouseTemp)
65 ' Ничего не делать, температура нормальная
66 End If
67 Catch tae As ThreadlnterruptedException
68 ' Пассивное ожидание было прервано
69 Catch e As Exception
70 ' Другие исключения
71 End Try
72 End Sub
73 End Class
Имена потоков, CurrentThread и ThreadState
Перед запуском каждому потоку рекомендуется присвоить содержательное имя, поскольку имена значительно упрощают отладку многопоточных программ. Для этого следует задать значение свойства Name командой следующего вида:
bThread.Name = "Subtracting thread"
Свойство Thread.CurrentThread возвращает ссылку на объект потока, выполняемого в настоящий момент.
Хотя для отладки многопоточных приложений в VB .NET существует замечательное окно потоков, о котором рассказано далее, нас очень часто выручала команда
MsgBox(Thread.CurrentThread.Name)
Нередко выяснялось, что код выполняется совсем не в том потоке, в котором ему полагалось выполняться.
Напомним, что термин «недетерминированное планирование программных потоков» означает очень простую вещь: в распоряжении программиста практически нет средств, позволяющих влиять на работу планировщика. По этой причине в программах часто используется свойство ThreadState, возвращающее информацию о текущем состоянии потока.
Команда SyncLock и класс Monitor
Использование команды SyncLock связано с некоторыми тонкостями, не проявившимися в приведенных выше простых примерах. Так, очень важную роль играет выбор объекта синхронизации. Попробуйте запустить предыдущую программу с командой SyncLock(Me) вместо SyncLock(mHouse). Температура снова поднимается выше пороговой величины!
Помните, что команда SyncLock производит синхронизацию по объекту, переданному в качестве параметра, а не по фрагменту кода. Параметр SyncLock играет роль двери для обращения к синхронизируемому фрагменту из других потоков. Команда SyncLock(Me) фактически открывает несколько разных «дверей», а ведь именно этого вы и пытались избежать при помощи синхронизации. Мораль:
Для защиты общих данных в многопоточном приложении команда SyncLock должна синхронизироваться по одному объекту.
Поскольку синхронизация связана с конкретным объектом, в некоторых ситуациях возможна непреднамеренная блокировка других фрагментов. Допустим, у вас имеются два синхронизированных метода first и second, причем оба метода синхронизируются по объекту bigLock. Когда поток 1 входит в метод first и захватывает bigLock, ни один поток не сможет войти в метод second, потому что доступ к нему уже ограничен потоком 1!
Функциональность команды SyncLock можно рассматривать как подмножество функциональности класса Monitor. Класс Monitor обладает расширенными возможностями настройки, и с его помощью можно решать нетривиальные задачи синхронизации. Команда SyncLock является приближенным аналогом методов Enter и Exi t класса Moni tor:
Try
Monitor.Enter(theObject) Finally
Monitor.Exit(theObject)
End Try
Для некоторых стандартных операций (увеличение/уменьшение переменной, обмен содержимого двух переменных) в .NET Framework предусмотрен класс Interlocked, методы которого выполняют эти операции на атомарном уровне. С использованием класса Interlocked данные операции выполняются значительно быстрее, нежели при помощи команды SyncLock.
Метод Join
Иногда программный поток требуется приостановить до момента завершения другого потока. Допустим, вы хотите приостановить поток 1 до тех пор, пока поток 2 не завершит свои вычисления. Для этого из потока 1 вызывается метод Join для потока 2. Иначе говоря, команда
thread2.Join()
приостанавливает текущий поток и ожидает завершения потока 2. Поток 1 переходит в заблокированное состояние.
Если присоединить поток 1 к потоку 2 методом Join, операционная система автоматически запустит поток 1 после завершения потока 2. Учтите, что процесс запуска является недетерминированным: нельзя точно сказать, через какой промежуток времени после завершения потока 2 заработает поток 1. Существует и другая версия Join, которая возвращает логическую величину:
thread2.Join(Integer)
Этот метод либо ожидает завершения потока 2, либо разблокирует поток 1 после истечения заданного интервала времени, вследствие чего планировщик операционной системы снова будет выделять потоку процессорное время. Метод возвращает True, если поток 2 завершается до истечения заданного интервала тайм-аута, и False в противном случае.
Не забывайте основное правило: независимо оттого, завершился ли поток 2 или про-изошел тайм-аут, вы не можете управлять моментом активизации потока 1.
Многопоточность в графических программах
Наше обсуждение многопоточности в приложениях с графическим интерфейсом начнется с примера, поясняющего, для чего нужна многопоточность в графических приложениях. Создайте форму с двумя кнопками Start (btnStart) и Cancel (btnCancel), как показано на Рисунок 10.9. При нажатии кнопки Start создается класс, который содержит случайную строку из 10 миллионов символов и метод для подсчета вхождений буквы «Е» в этой длинной строке. Обратите внимание на применение класса StringBuilder, повышающего эффективность создания длинных строк.
Шаг 1
Поток 1 замечает, что данных для него нет. Он вызывает Wait, снимает блокировку и переходит в очередь ожидания
Многопоточность в простом приложении с графическим интерфейсом

Imports System.Text
Public Class RandomCharacters
Private m_Data As StringBuilder
Private m_CountDone As Boolean
Private mjength, m_count As Integer
Public Sub New(ByVal n As Integer)
m_Length = n -1
m_Data = New StringBuilder(m_length) MakeString()
End Sub
Private Sub MakeString()
Dim i As Integer
Dim myRnd As New Random()
For i = 0 To m_length
' Сгенерировать случайное число от 65 до 90,
' преобразовать его в прописную букву
' и присоединить к объекту StringBuilder
m_Data.Append(Chr(myRnd.Next(65.90)))
Next
End Sub
Public Sub StartCount()
GetEes()
End Sub
Private Sub GetEes()
Dim i As Integer
For i = 0 To m_length
If m_Data.Chars(i) = CChar("E") Then
m_count += 1
End If Next
m_CountDone = True
End Sub
Public Readonly
Property GetCount() As Integer Get
If Not (m_CountDone) Then
Throw New Exception("Count not yet done") Else
Return m_count
End If
End Get End Property
Public Readonly
Property IsDone()As Boolean Get
Return
m_CountDone
End Get
End Property
End Class
С двумя кнопками на форме связывается весьма простой код. В процедуре btn-Start_Click создается экземпляр приведенного выше класса RandomCharacters, инкапсулирующего строку с 10 миллионами символов:
Private Sub btnStart_Click(ByVal sender As System.Object.
ByVal e As System.EventArgs) Handles btnSTart.Click
Dim RC As New RandomCharacters(10000000)
RC.StartCount()
MsgBox("The number of es is " & RC.GetCount)
End Sub
Кнопка Cancel выводит окно сообщения:
Private Sub btnCancel_Click(ByVal sender As System.Object._
ByVal e As System.EventArgs)Handles btnCancel.Click
MsgBox("Count Interrupted!")
End Sub
При запуске программы и нажатии кнопки Start выясняется, что кнопка Cancel не реагирует на действия пользователя, поскольку непрерывный цикл не позволяет кнопке обработать полученное событие. В современных программах подобное недопустимо!
Возможны два решения. Первый вариант, хорошо знакомый по предыдущим версиям VB, обходится без многопоточности: в цикл включается вызов DoEvents. В .NET эта команда выглядит так:
Application.DoEvents()
В нашем примере это определенно нежелательно — кому захочется замедлять программу десятью миллионами вызовов DoEvents! Если вместо этого выделить цикл в отдельный поток, операционная система будет переключаться между потоками и кнопка Cancel сохранит работоспособность. Реализация с отдельным потоком приведена ниже. Чтобы наглядно показать, что кнопка Cancel работает, при ее нажатии мы просто завершаем программу.
Окно потоков
Окно потоков (Threads window) Visual Studio .NET оказывает неоценимую помощь в отладке многопоточных программ. Оно активизируется командой подменю Debug > Windows в режиме прерывания. Допустим, вы назначили имя потоку bThread следующей командой:
bThread.Name = "Subtracting thread"
Примерный вид окна потоков после прерывания программы комбинацией клавиш Ctrl+Break (или другим способом) показан на Рисунок 10.5.
Стрелкой в первом столбце помечается активный поток, возвращаемый свойством Thread.CurrentThread. Столбец ID содержит числовые идентификаторы потоков. В следующем столбце перечислены имена потоков (если они были присвоены). Столбец Location указывает выполняемую процедуру (например, процедура WriteLine класса Console на Рисунок 10.5). Остальные столбцы содержат информацию о приоритете и приостановленных потоках (см. следующий раздел).
Окно потоков (а не операционная система!) позволяет управлять потоками вашей программы при помощи контекстных меню. Например, вы можете остановить текущий поток, для чего следует щелкнуть в соответствующей строке правой кнопкой мыши и выбрать команду Freeze (позже работу остановленного потока можно возобновить). Остановка потоков часто используемая при отладке, чтобы неправильно работающий поток не мешал работе приложения. Кроме того, окно потоков позволяет активизировать другой (не остановленный) поток; для этого следует щелкнуть правой кнопкой мыши в нужной строке и выбрать в контекстном меню команду Switch To Thread (или просто сделать двойной щелчок на строке потока). Как будет показано далee, это очень удобно при диагностике потенциальных взаимных блокировок (deadlocks).
Переключение между потоками в простой многопоточной программе

При прерывании потоков и передаче управления другим потокам операционная система использует принцип вытесняющей многопоточности посредством квантования времени. Квантование времени также решает одну из распространенных проблем, возникавших прежде в многопоточных программах, — один поток занимает все процессорное время и не уступает управления другим потокам (как правило, это случается в интенсивных циклах вроде приведенного выше). Чтобы предотвратить монопольный захват процессора, ваши потоки должны время от времени передавать управление другим потокам. Если программа окажется «несознательной», существует другое, чуть менее желательное решение: операционная система всегда вытесняет работающий поток независимо от уровня его приоритета, чтобы доступ к процессору был предоставлен каждому потоку в системе.
Поскольку в схемах квантования всех версий Windows, в которых работает .NET, каждо-му потоку выделяется минимальный квант времени, в программировании .NET проблемы с монопольным захватом процессора не столь серьезны. С другой стороны, если среда .NET когда-нибудь будет адаптирована для других систем, ситуация может измениться.
Если включить следующую строку в нашу программу перед вызовом Start, то даже потоки, обладающие минимальным приоритетом, получат некоторую долю процессорного времени:
bThread.Priority = ThreadPriority.Highest
Почему при многопоточном программировании возникает столько проблем?
Теперь, когда вы получили некоторое представление о многопоточном программировании и о потенциальных проблемах, с ним связанных, мы решили, что в конце этой главы будет уместно ответить на вопрос, вынесенный в заголовок подраздела.
Одна из причин заключается в том, что многопотрчность — процесс нелинейный, а мы привыкли к линейной модели программирования. На первых порах трудно привыкнуть к самой мысли о том, что выполнение программы может прерываться случайным образом, а управление будет передаваться другому коду.
Однако существует и другая, более фундаментальная причина: в наши дни программисты слишком редко программируют на ассемблере или хотя бы просматривают дизассемблированные результаты работы компилятора. Иначе им было бы гораздо проще привыкнуть к мысли, что одной команде языка высокого уровня (такого, как VB .NET) могут соответствовать десятки ассемблерных инструкций. Поток может прерываться после любой из этих инструкций, а следовательно — и посреди команды высокого уровня.
Но и это не все: современные компиляторы оптимизируют быстродействие программ, а оборудование компьютера может вмешиваться в процесс управления памятью. Как следствие, компилятор или оборудование может без вашего ведома изменить порядок команд, указанный в исходном тексте программы [ Многие компиляторы оптимизируют циклические операции копирования массивов вида for i=0 to n:b(i)=a(i):ncxt. Компилятор (или даже специализированное устройство управления памятью) может просто создать массив, а потом заполнить его одной операцией копирования вместо многократного копирования отдельных элементов! ].
Надеемся, эти пояснения помогут вам лучше понять, почему многопоточное программирование порождает столько проблем, — или по крайней мере меньше удивляться при виде странного поведения ваших многопоточных программ!
Приостановка и уничтожение потоков
Пространство имен Threading содержит и другие методы, прерывающие нормальное функционирование потоков:
В результате вызова Abort инициируется исключение ThreadAbortException. Чтобы вы поняли, почему это странное исключение не следует обрабатывать в программах, мы приводим отрывок из документации .NET SDK:
«...При уничтожении потока вызовом Abort исполнительная среда инициирует исключение ThreadAbortException. Это особая разновидность исключений, которая не может перехватываться программой. При инициировании этого исключения перед тем, как уничтожить поток, исполнительная среда выполняет все блоки Finally. Поскольку в блоках Finally могут выполняться любые действия, вызовите Join, чтобы убедиться в уничтожении потока».
Мораль: Abort и Suspend использовать не рекомендуется (а если без Suspend все же не обойтись, возобновите приостановленный поток методом Resume). Безопасно завершить поток можно только путем опроса синхронизируемой условной переменной или вызовом метода Interrupt, о котором говорилось выше.
Приостановка потока
Временно неиспользуемые потоки можно перевести в пассивное состояние методом Slеер. Пассивный поток также считается заблокированным. Разумеется, с переводом потока в пассивное состояние на долю остальных потоков достанется больше ресурсов процессора. Стандартный синтаксис метода Slеер выглядит следующим образом: Thread.Sleep(интервал_в_миллисекундах)
В результате вызова Sleep активный поток переходит в пассивное состояние как минимум на заданное количество миллисекунд (впрочем, активизация сразу же после истечения заданного интервала не гарантируется). Обратите внимание: при вызове метода ссылка на конкретный поток не передается — метод Sleep вызывается только для активного потока.
Другая версия Sleep заставляет текущий поток уступить оставшуюся часть выделенного процессорного времени:
Thread.Sleep(0)
Следующий вариант переводит текущий поток в пассивное состояние на неограниченное время (активизация происходит только при вызове Interrupt):
Thread.Slеер(Timeout.Infinite)
Поскольку пассивные потоки (даже при неограниченном времени ожидания) могут прерываться методом Interrupt, что приводит к инициированию исключения ThreadlnterruptExcepti on, вызов Slеер всегда заключается в блок Try-Catch, как в следующем фрагменте:
Try
Thread.Sleep(200)
Catch tie As ThreadlnterruptedException
' Пассивное состояние потока было прервано
Catch e As Exception
'Остальные исключения
End Try
Каждая программа .NET работает в программном потоке, поэтому метод Sleep также используется для приостановки работы программ (если пространство имен Threadipg не импортируется программой, приходится использовать полное имя Threading.Thread. Sleep).
Проблемы многопоточности

В процедуре Sub Main (строки 4-7) создается «дом» с десятью «комнатами». Класс House устанавливает максимальную температуру 75 градусов по Фаренгейту (около 24 градусов по Цельсию). В строках 13-28 определяется довольно сложный конструктор дома. Ключевыми для понимания программы являются строки 18-27. Строка 20 создает очередной объект комнаты, при этом конструктору передается ссылка на объект дома, чтобы объект комнаты при необходимости мог к нему обратиться. Строки 21-23 запускают десять потоков для регулировки температуры в каждой комнате. Класс Room определяется в строках 38-73. Ссылка на объект House coxpaняется в переменной mHouse в конструкторе класса Room (строка 43). Код проверки и регулировки температуры (строки 50-66) выглядит просто и естественно, но как вы вскоре убедитесь, это впечатление обманчиво! Обратите внимание на то, что этот код заключен в блок Try-Catch, поскольку в программе используется метод Sleep.
Вряд ли кто-нибудь согласится жить при температуре в 105 градусов по Фаренгейту (40,5 24 градусов по Цельсию). Что же произошло? Проблема связана со следующей строкой:
If mHouse.HouseTemp < mHouse.MAX_TEMP - 5 Then
А происходит следующее: сначала температуру проверяет поток 1. Он видит, что температура слишком низка, и поднимает ее на 5 градусов. К сожалению, перед повышением температуры поток 1 прерывается и управление передаётся поток 2. Поток 2 проверяет ту же самую переменную, которая еще не была изменена потоком 1. Таким образом, поток 2 тоже готовится поднять температуру на 5 градусов, но сделать этого не успевает и тоже переходит в состояние ожидания. Процесс продолжается до тех пор, пока поток 1 не активизируется и не перейдет к следующей команде — повышению температуры на 5 градусов. Повышение повторяется при активизации всех 10 потоков, и жильцам дома придется плохо.
Процессор предоставляется и потокам с более низким приоритетом

Команда назначает новому потоку максимальный приоритет и уменьшает приоритет главного потока. Из Рисунок 10.3 видно, что новый поток начинает работать быстрее, чем прежде, но, как показывает Рисунок 10.4, главный поток тоже получает управление (правда, очень ненадолго и лишь после продолжительной работы потока с вычитанием). При запуске программы на ваших компьютерах будут получены результаты, похожие на показанные на Рисунок 10.3 и 10.4, но из-за различий между нашими системами точного совпадения не будет.
В перечисляемый тип ThreadPrlority входят значения для пяти уровней приоритета:
ThreadPriority.Highest
ThreadPriority.AboveNormal
ThreadPrlority.Normal
ThreadPriority.BelowNormal
ThreadPriority.Lowest
Простая многопоточная программно время работы

Если программа будет работать в течение большегошромежутка времени, результат будет выглядеть примерно так, как показано на Рисунок 10.2. Мы видим, что выполнение запущенного потока приостанавливается и управление снова передается главному потоку. В данном случае имеет место проявление вытесняющей мно-гопоточности посредством квантования времени. Смысл этого устрашающего термина разъясняется ниже.
Решение проблемы: синхронизация
В предыдущей программе возникает ситуация, когда результат работы программы зависит от порядка выполнения потоков. Чтобы избавиться от нее, необходимо убедиться в том, что команды типа
If mHouse.HouseTemp < mHouse.MAX_TEMP - 5 Then...
полностью отрабатываются активным потоком до того, как он будет прерван. Это свойство называется атомарностыд — блок кода должен выполняться каждым потоком без прерывания, как атомарная единица. Группа команд, объединенных в атомарный блок, не может быть прервана планировщиком потоков до ее завершения. В любом многопоточном языке программирования существуют свои способы обеспечения атомарности. В VB .NET проще всего воспользоваться командой SyncLock, при вызове которой передается объектная переменная. Внесите в процедуру ChangeTemperature из предыдущего примера небольшие изменения, и программа заработает нормально:
Private Sub ChangeTemperature() SyncLock (mHouse)
Try
If mHouse.HouseTemp < mHouse.MAXJTEMP -5 Then
Thread.Sleep(200)
mHouse.HouseTemp += 5
Console.WriteLine("Am in " & Me.mName & _
".Current temperature is " & mHouse.HouseTemp)
Elself
mHouse.HouseTemp < mHouse. MAX_TEMP Then
Thread.Sleep(200) mHouse.HouseTemp += 1
Console.WriteLine("Am in " & Me.mName &_ ".Current temperature is " & mHouse.HomeTemp) Else
Console.WriteLineC'Am in " & Me.mName & _ ".Current temperature is " & mHouse.HouseTemp)
' Ничего не делать, температура нормальная
End If Catch tie As ThreadlnterruptedException
' Пассивное ожидание было прервано Catch e As Exception
' Другие исключения
End Try
End SyncLock
End Sub
Код блока SyncLock выполняется атомарно. Доступ к нему со стороны всех остальных потоков будет закрыт, пока первый поток не снимет блокировку командой End SyncLock. Если поток в синхронизируемом блоке переходит в состояние пассивного ожидания, блокировка сохраняется вплоть до прерывания или возобновления работы потока.
Правильное использование команды SyncLock обеспечивает потоковую безопасность вашей программы. К сожалению, злоупотребление SyncLock отрицательно сказывается на быстродействии. Синхронизация кода в многопоточной программе уменьшает скорость ее работы в несколько раз. Синхронизируйте лишь самый необходимый код и снимайте блокировку как можно скорее.
Базовые классы коллекций небезопасны в многопоточных приложениях, но в .NET Framework входят поточно-безопасные версии большинства классов коллекций. В этих классах код потенциально опасных методов заключается в блоки SyncLock. Поточно-безопасные версии классов коллекций следует использовать в многопоточных программах везде, где возникает угроза целостности данных.
Остается упомянуть о том, что при помощи команды SyncLock легко реализуются условные переменные. Для этого потребуется лишь синхронизировать запись в общее логическое свойство, доступное для чтения и записи, как это сделано в следующем фрагменте:
Public Class ConditionVariable
Private Shared locker As Object= New Object()
Private Shared mOK As Boolean Shared
Property TheConditionVariable()As Boolean
Get
Return mOK
End Get
Set(ByVal Value As Boolean) SyncLock (locker)
mOK= Value
End SyncLock
End Set
End Property
End Class
Следующий шаг: кнопка Show Count
Допустим, вы решили проявить творческую фантазию и придать форме вид, показанный на Рисунок 10.9. Обратите внимание: кнопка Show Count пока недоступна.
Совместная работа с данными по мере их создания
В многопоточных приложениях часто встречается ситуация, когда потоки не только работают с общими данными, но и ожидают их появления (то есть поток 1 должен создать данные, прежде чем поток 2 сможет их использовать). Поскольку данные являются общими, доступ к ним необходимо синхронизировать. Также необходимо предусмотреть средства для оповещения ожидающих потоков о появлении готовых данных.
Подобная ситуация обычно называется проблемой «поставщик/потребитель». Поток пытается обратиться к данным, которых еще нет, поэтому он должен передать управление другому потоку, создающему нужные данные. Проблема решается кодом следующего вида:
Связи «поставщик/потребитель» встречаются очень часто, поэтому в библиотеках классов многопоточного программирования для таких ситуаций создаются специальные примитивы. В .NET эти примитивы называются Wait и Pulse-PulseAl 1 и являются частью класса Monitor. Рисунок 10.8 поясняет ситуацию, которую мы собираемся запрограммировать. В программе организуются три очереди потоков: очередь ожидания, очередь блокировки и очередь выполнения. Планировщик потоков не выделяет процессорное время потокам, находящимся в очереди ожидания. Чтобы потоку выделялось время, он должен переместиться в очередь выполнения. В результате работа приложения организуется гораздо эффективнее, чем при обычном опросе условной переменной.
На псевдокоде идиома потребителя данных формулируется так:
' Вход в синхронизированный блок следующего вида
While нет данных
Перейти в очередь ожидания
Loop
Если данные есть, обработать их.
Покинуть синхронизированный блок
Сразу же после выполнения команды Wait поток приостанавливается, блокировка снимается, и поток переходит в очередь ожидания. При снятии блокировки поток, находящийся в очереди выполнения, получает возможность работать. Со временем один или несколько заблокированных потоков создадут данные, необходимые для работы потока, находящегося в очереди ожидания. Поскольку проверка данных осуществляется в цикле, переход к использованию данных (после цикла) происходит лишь при наличии данных, готовых к обработке.
На псевдокоде идиома поставщика данных выглядит так:
' Вход в синхронизированный блок вида
While данные НЕ нужны
Перейти в очередь ожидания
Else Произвести данные
После появления готовых данных вызвать Pulse-PulseAll.
чтобы переместить один или несколько потоков из очереди блокировки в очередь выполнения. Покинуть синхронизированный блок (и вернуться в очередь выполнения)
Предположим, наша программа моделирует семью с одним родителем, который зарабатывает деньги, и ребенком, который эти деньги тратит. Когда деньги кончаются, ребенку приходится ждать прихода новой суммы. Программная реализация этой модели выглядит так:
1 Option Strict On
2 Imports System.Threading
3 Module Modulel
4 Sub Main()
5 Dim theFamily As New Family()
6 theFamily.StartltsLife()
7 End Sub
8 End fjodule
9
10 Public Class Family
11 Private mMoney As Integer
12 Private mWeek As Integer = 1
13 Public Sub StartltsLife()
14 Dim aThreadStart As New ThreadStarUAddressOf Me.Produce)
15 Dim bThreadStart As New ThreadStarUAddressOf Me.Consume)
16 Dim aThread As New Thread(aThreadStart)
17 Dim bThread As New Thread(bThreadStart)
18 aThread.Name = "Produce"
19 aThread.Start()
20 bThread.Name = "Consume"
21 bThread. Start()
22 End Sub
23 Public Property TheWeek() As Integer
24 Get
25 Return mweek
26 End Get
27 Set(ByVal Value As Integer)
28 mweek - Value
29 End Set
30 End Property
31 Public Property OurMoney() As Integer
32 Get
33 Return mMoney
34 End Get
35 Set(ByVal Value As Integer)
36 mMoney =Value
37 End Set
38 End Property
39 Public Sub Produce()
40 Thread.Sleep(500)
41 Do
42 Monitor.Enter(Me)
43 Do While Me.OurMoney > 0
44 Monitor.Wait(Me)
45 Loop
46 Me.OurMoney =1000
47 Monitor.PulseAll(Me)
48 Monitor.Exit(Me)
49 Loop
50 End Sub
51 Public Sub Consume()
52 MsgBox("Am in consume thread")
53 Do
54 Monitor.Enter(Me)
55 Do While Me.OurMoney = 0
56 Monitor.Wait(Me)
57 Loop
58 Console.WriteLine("Dear parent I just spent all your " & _
money in week " & TheWeek)
59 TheWeek += 1
60 If TheWeek = 21 *52 Then System.Environment.Exit(0)
61 Me.OurMoney =0
62 Monitor.PulseAll(Me)
63 Monitor.Exit(Me)
64 Loop
65 End Sub
66 End Class
Метод StartltsLife (строки 13-22) осуществляет подготовку к запуску потоков Produce и Consume. Самое главное происходит в потоках Produce (строки 39-50) и Consume (строки 51-65). Процедура Sub Produce проверяет наличие денег, и если деньги есть, переходит в очередь ожидания. В противном случае родитель генерирует деньги (строка 46) и оповещает объекты в очереди ожидания об изменении ситуации. Учтите, что вызов Pulse-Pulse All вступает в силу лишь при снятии блокировки командой Monitor.Exit. И наоборот, процедура Sub Consume проверяет наличие денег, и если денег нет — оповещает об этом ожидающего родителя. Строка 60 просто завершает программу по прошествии 21 условного года; вызов System. Environment.Exit(0) является .NET-аналогом команды End (команда End тоже поддерживается, но в отличие от System. Environment. Exit она не позволяет вернуть код завершения операционной системе).
Потоки, переведенные в очередь ожидания, должны быть освобождены другими час-тями вашей программы. Именно по этой причине мы предпочитаем использовать PulseAll вместо Pulse. Поскольку заранее неизвестно, какой именно поток будет активизирован при вызове Pulse 1 , при относительно небольшом количестве потоков в очереди с таким же успехом можно вызвать PulseAll.
Создание потоков
Начнем с элементарного примера. Допустим, вы хотите запустить в отдельном потоке процедуру, которая в бесконечном цикле уменьшает значение счетчика. Процедура определяется в составе класса:
Public Class WillUseThreads
Public Sub SubtractFromCounter()
Dim count As Integer
Do While True count -= 1
Console.WriteLlne("Am in another thread and counter ="
& count)
Loop
End Sub
End Class
Поскольку условие цикла Do остается истинным всегда, можно подумать, что ничто не помешает выполнению процедуры SubtractFromCounter. Тем не менее в многопоточном приложении это не всегда так.
В следующем фрагменте приведена процедура Sub Main, запускающая поток, и команда Imports:
Option Strict On Imports System.Threading Module Modulel
Sub Main()
1 Dim myTest As New WillUseThreads()
2 Dim bThreadStart As New ThreadStart(AddressOf _
myTest.SubtractFromCounter)
3 Dim bThread As New Thread(bThreadStart)
4 ' bThread.Start()
Dim i As Integer
5 Do While True
Console.WriteLine("In main thread and count is " & i) i += 1
Loop
End Sub
End Module
Давайте последовательно разберем наиболее принципиальные моменты. Прежде всего процедура Sub Man n всегда работает в главном потоке (main thread). В програм-мах .NET всегда работают минимум два потока: главный и поток сборки мусора. В строке 1 создается новый экземпляр тестового класса. В строке 2 мы создаем делегат ThreadStart и передаем адрес процедуры SubtractFromCounter экземпляра тестового класса, созданного в строке 1 (эта процедура вызывается без параметров). Благодаря импортированию пространства имен Threading длинное имя можно не указывать. Объект нового потока создается в строке 3. Обратите внимание на передачу делегата ThreadStart при вызове конструктора класса Thread. Некоторые программисты предпочитают объединять эти две строки в одну логическую строку:
Dim bThread As New Thread(New ThreadStarttAddressOf _
myTest.SubtractFromCounter))
Наконец, строка 4 «запускает» поток, для чего вызывается метод Start экземпляра класса Thread, созданного для делегата ThreadStart. Вызывая этот метод, мы указываем операционной системе, что процедура Subtract должна работать в отдельном потоке.
Слово «запускает» в предыдущем абзаце заключено в кавычки, поскольку в этом случае наблюдается одна из многих странностей многопоточного программирования: вызов Start не приводит к фактическому запуску потока! Он всего лишь сообщает, что операционная система должна запланировать выполнение указанного потока, но непосредственный запуск находится вне контроля программы. Вам не удастся начать выполнение потоков по своему усмотрению, потому что выполнением потоков всегда распоряжается операционная система. В одном из дальнейших разделов вы узнаете, как при помощи приоритета заставить операционную систему побыстрее запустить ваш поток.
На Рисунок 10.1 показан пример того, что может произойти после запуска программы и ее последующего прерывания клавишей Ctrl+Break. В нашем случае новый поток запустился лишь после того, как счетчик в главном потоке увеличился до 341!
Взаимная блокировка
В процессе синхронизации блокировка устанавливается для объектов, а не потоков, поэтому при использовании разных объектов для блокировки разных фрагментов кода в программах иногда возникают весьма нетривиальные ошибки. К сожалению, во многих случаях синхронизация по одному объекту просто недопустима, поскольку она приведет к слишком частой блокировке потоков.
Рассмотрим ситуацию взаимной блокировки (deadlock) в простейшем виде. Представьте себе двух программистов за обеденным столом. К сожалению, на двоих у них только один нож и одна вилка. Если предположить, что для еды нужны и нож и вилка, возможны две ситуации:
Диагностика взаимных блокировок затрудняется тем, что они могут возникать в отно-сительно редких случаях. Все зависит от того, в каком порядке планировщик выделит им процессорное время. Вполне возможно, что в большинстве случаев объекты синхронизации будут захватываться в порядке, не приводящем к взаимной блокировке.
Ниже приведена реализация только что описанной ситуации взаимной блокировки. После краткого обсуждения наиболее принципиальных моментов мы покажем, как опознать ситуацию взаимной блокировки в окне потоков:
1 Option Strict On
2 Imports System.Threading
3 Module Modulel
4 Sub Main()
5 Dim Tom As New Programmer( "Tom")
6 Dim Bob As New Programmer( "Bob")
7 Dim aThreadStart As New ThreadStart(AddressOf Tom.Eat)
8 Dim aThread As New Thread(aThreadStart)
9 aThread.Name= "Tom"
10 Dim bThreadStart As New ThreadStarttAddressOf Bob.Eat)
11 Dim bThread As New Thread(bThreadStart)
12 bThread.Name = "Bob"
13 aThread.Start()
14 bThread.Start()
15 End Sub
16 End Module
17 Public Class Fork
18 Private Shared mForkAvaiTable As Boolean = True
19 Private Shared mOwner As String = "Nobody"
20 Private Readonly Property OwnsUtensil() As String
21 Get
22 Return mOwner
23 End Get
24 End Property
25 Public Sub GrabForktByVal a As Programmer)
26 Console.Writel_ine(Thread.CurrentThread.Name &_
"trying to grab the fork.")
27 Console.WriteLine(Me.OwnsUtensil & "has the fork.") . .
28 Monitor.Enter(Me) 'SyncLock (aFork)'
29 If mForkAvailable Then
30 a.HasFork = True
31 mOwner = a.MyName
32 mForkAvailable = False
33 Console.WriteLine(a.MyName&"just got the fork.waiting")
34 Try
Thread.Sleep(100) Catch e As Exception Console.WriteLine (e.StackTrace)
End Try
35 End If
36 Monitor.Exit(Me)
End SyncLock
37 End Sub
38 End Class
39 Public Class Knife
40 Private Shared mKnifeAvailable As Boolean = True
41 Private Shared mOwner As String ="Nobody"
42 Private Readonly Property OwnsUtensi1() As String
43 Get
44 Return mOwner
45 End Get
46 End Property
47 Public Sub GrabKnifetByVal a As Programmer)
48 Console.WriteLine(Thread.CurrentThread.Name & _
"trying to grab the knife.")
49 Console.WriteLine(Me.OwnsUtensil & "has the knife.")
50 Monitor.Enter(Me) 'SyncLock (aKnife)'
51 If mKnifeAvailable Then
52 mKnifeAvailable = False
53 a.HasKnife = True
54 mOwner = a.MyName
55 Console.WriteLine(a.MyName&"just got the knife.waiting")
56 Try
Thread.Sleep(100)
Catch e As Exception
Console.WriteLine (e.StackTrace)
End Try
57 End If
58 Monitor.Exit(Me)
59 End Sub
60 End Class
61 Public Class Programmer
62 Private mName As String
63 Private Shared mFork As Fork
64 Private Shared mKnife As Knife
65 Private mHasKnife As Boolean
66 Private mHasFork As Boolean
67 Shared Sub New()
68 mFork = New Fork()
69 mKnife = New Knife()
70 End Sub
71 Public Sub New(ByVal theName As String)
72 mName = theName
73 End Sub
74 Public Readonly Property MyName() As String
75 Get
76 Return mName
77 End Get
78 End Property
79 Public Property HasKnife() As Boolean
80 Get
81 Return mHasKnife
82 End Get
83 Set(ByVal Value As Boolean)
84 mHasKnife = Value
85 End Set
86 End Property
87 Public Property HasFork() As Boolean
88 Get
89 Return mHasFork
90 End Get
91 Set(ByVal Value As Boolean)
92 mHasFork = Value
93 End Set
94 End Property
95 Public Sub Eat()
96 Do Until Me.HasKnife And Me.HasFork
97 Console.Writeline(Thread.CurrentThread.Name&"is in the thread.")
98 If Rnd() < 0.5 Then
99 mFork.GrabFork(Me)
100 Else
101 mKnife.GrabKnife(Me)
102 End If
103 Loop
104 MsgBox(Me.MyName & "can eat!")
105 mKnife = New Knife()
106 mFork= New Fork()
107 End Sub
108 End Class
Основная процедура Main (строки 4-16) создает два экземпляра класса Programmer и затем запускает два потока для выполнения критического метода Eat класса Programmer (строки 95-108), описанного ниже. Процедура Main задает имена потоков и занускает их; вероятно, все происходящее понятно и без комментариев.
Интереснее выглядит код класса Fork (строки 17-38) (аналогичный класс Knife определяется в строках 39-60). В строках 18 и 19 задаются значения общих полей, по которым можно узнать, доступна ли в данный момент вилка, и если нет — кто ею пользуется. ReadOnly-свойство OwnUtensi1 (строки 20-24) предназначено для простейшей передачи информации. Центральное место в классе Fork занимает метод «захвата вилки» GrabFork, определяемый в строках 25-27.
Однако наибольший интерес представляет код класса Programmer (строки 61-108). В строках 67-70 определяется общий конструктор, что гарантирует наличие в программе только одной вилки и ножа. Код свойств (строки 74-94) прост и не требует комментариев. Самое главное происходит в методе Eat, выполняемом двумя отдельными потоками. Процесс продолжается в цикле до тех пор, пока какой-либо поток не захватит вилку вместе с ножом. В строках 98-102 объект случайным образом захватывает вилку/нож, используя вызов Rnd, — именно это и порождает взаимную блокировку. Происходит следующее:
Поток, выполняющий метод Eat объекта Тот, активизируется и входит в цикл. Он захватывает нож и переходит в состояние ожидания.
О возникновении взаимной блокировки можно узнать и в окне потоков. Запустите программу и прервите ее клавишами Ctrl+Break. Включите в окно просмотра переменную Me и откройте окно потоков. Результат выглядит примерно так, как показано на Рисунок 10.7. Из рисунка видно, что поток Bob захватил нож, но вилки у него нет. Щелкните правой кнопкой мыши в окне потоков на строке Тот и выберите в контекстном меню команду Switch to Thread. Окно просмотра показывает, что у потока Тот имеется вилка, но нет ножа. Конечно, это не является стопроцентным доказательством, но подобное поведение по крайней мере заставляет заподозрить неладное.
Если вариант с синхронизацией по одному объекту (как в программе с повышением -температуры в доме) невозможен, для предотвращения взаимных блокировок можно пронумеровать объекты синхронизации и всегда захватывать их в постоянном порядке. Продолжим аналогию с обедающими программистами: если поток всегда сначала берет нож, а потом вилку, проблем с взаимной блокировкой не будет. Первый поток, захвативший нож, сможет нормально поесть. В переводе на язык программных потоков это означает, что захват объекта 2 возможен лишь при условии предварительного захвата объекта 1.
Завершение или прерывание программных потоков
Поток автоматически завершается при выходе из метода, указанного при создании делегата ThreadStart, но иногда требуется завершить метод (следовательно, и поток) при возникновении определенных факторов. В таких случаях в потоках обычно проверяется условная переменная, в зависимости от состояния которой принимается решение об аварийном выходе из потока. Как правило, для этого в процедуру включается цикл Do-While:
Sub ThreadedMethod()
' В программе необходимо предусмотреть средства для опроса
' условной переменной.
' Например, условную переменную можно оформить в виде свойства
' и использовать ссылку на это свойство в программе.
Do While conditionVariable = False And MoreWorkToDo
' Основной код
Loop End Sub
На опрос условной переменной уходит некоторое время. Постоянный опрос в условии цикла следует использовать лишь в том случае, если вы ожидаете преждевременного завершения потока.
Если проверка условной переменной должна происходить в строго определенном месте, воспользуйтесь командой If-Then в сочетании с Exit Sub внутри бесконечного цикла.
Доступ к условной переменной необходимо синхронизировать, чтобы воздействие со стороны других потоков не помешало ее нормальному использованию. Этой важной теме посвящен раздел «Решение проблемы: синхронизация».
К сожалению, код пассивных (или заблокированных иным образом) потоков не выполняется, поэтому вариант с опросом условной переменной для них не подходит. В этом случае следует вызвать метод Interrupt для объектной переменной, содержащей ссылку на нужный поток.
Метод Interrupt может вызываться только для потоков, находящихся в состоянии Wait, Sleep или Join. Если вызвать Interrupt для потока, находящегося в одном из перечисленных состояний, то через некоторое время поток снова начнет работать, а исполнительная среда инициирует в потоке исключение ThreadlnterruptedExcepti on. Это происходит даже в том случае, если поток был переведен в пассивное состояние на неопределенный срок вызовом Thread.Sleepdimeout. Infinite). Мы говорим «через некоторое время», поскольку планирование потоков имеет недетерминированную природу. Исключение ThreadlnterruptedExcepti on перехватывается секцией Catch, содержащей код выхода из состояния ожидания. Тем не менее секция Catch вовсе не обязана завершать поток по вызову Interrupt — поток обрабатывает исключение по своему усмотрению.
В.NET метод Interrupt может вызываться даже для незаблокированных потоков. В этом случае поток прерывается при ближайшей блокировке.
Знакомство с многопоточностью
Каждая программа работает в определенном контексте, описывающем распределение кода и данных в памяти. При сохранении контекста фактически сохраняется состояние программного потока, что позволяет в будущем восстановить его и продолжить выполнение программы.
Сохранение контекста сопряжено с определенными затратами времени и памяти. Операционная система запоминает состояние программного потока и передает управление другому потоку. Когда программа захочет продолжить выполнение приостановленного потока, сохраненный контекст приходится восстанавливать, на что уходит еще больше времени. Следовательно, многопоточность следует использовать лишь в тех случаях, когда преимущества компенсируют все затраты. Ниже перечислены некоторые типичные примеры.
В программном потоке выполнятся процедура, а не объект.
Трудно сказать, что следует понимать под выражением «выполняется объект», но один из авторов часто ведет семинары по многопоточному программированию и этот вопрос задают чаще других. Возможно, кто-то полагает, что работа программного потока начинается с вызова метода New класса, после чего поток обрабатывает все сообщения, передаваемые соответствующему объекту. Такие представления абсолютно неверны. Один объект может содержать несколько потоков, выполняющих разные (а иногда даже одинаковые) методы, при этом сообщения объекта передаются и принимаются несколькими разными потоками (кстати, это одна из причин, затрудняющих многопоточное программирование: чтобы отладить программу, необходимо узнать, какой поток в данный момент выполняет ту или иную процедуру!).
Поскольку программные потоки создаются на базе методов объектов, сам объект обычно создается раньше потока. После успешного создания объекта программа создает поток, передавая ему адрес метода объекта, и только после этого отдает распоряжение о начале выполнения потока. Процедура, для которой создавался поток, как и все процедуры, может создавать новые объекты, выполнять операции с существующими объектами и вызывать другие процедуры и функции, находящиеся в ее области видимости.
В программных потоках также могут выполняться общие методы классов. В этом слу-Также помните о другом важном обстоятельстве: поток завершается с выходом из процедуры, для которой он был создан. До выхода из процедуры нормальное завершение программного потока невозможно.
Потоки могут завершаться не только естественно, но и аварийно. Обычно делать это не рекомендуется. За дополнительной информацией обращайтесь к разделу «Завершение и прерывание потоков».
Основные средства .NET, относящиеся к использованию программных потоков, сосредоточены в пространстве имен Threading. Следовательно, большинство многопоточных программ должно начинаться со следующей строки:
Imports System.Threading
Импортирование пространства имен упрощает ввод программы и позволяет использовать технологию IntelliSense.
Непосредственная связь потоков с процедурами наводит на предположение о том, что в этой картине важное место занимают делегаты (см. главу 6). В частности, в пространство имен Threading входит делегат ThreadStart, обычно используемый при запуске программных потоков. Синтаксис использования этого делегата выглядит так:
Public Delegate Sub ThreadStart()
Код, вызываемый при помощи делегата ThreadStart, не должен иметь параметров и возвращаемого значения, поэтому потоки не могут создаваться для функций (которые возвращают значение) и для процедур с параметрами. Для передачи информации из потока тоже приходится искать альтернативные средства, поскольку выполняемые методы не возвращают значений и не могут использовать передачу по ссылке. Например, если процедура ThreadMethod находится в классе WilluseThread, то ThreadMethod может передавать информацию посредством изменения свойств экземпляров класса WillUseThread.
Cамоучитель по VB.NET
Автономные наборы данных: новый подход к работе с базами данных
В VB6 типичное приложение, использовавшее базы данных, открывало соединение с базой и использовало его для всех запросов на протяжении жизненного цикла программы. В VB .NET доступ к базам данных средствами ADO .NET обычно основан на автономных (отсоединенных) операциях. За этим высокопарным выражением кроется простой смысл: в большинстве случаев после выборки данных из базы соединение разрывается. В ADO .NET постоянная связь с источником данных встречается очень редко (при желании вы можете использовать постоянные соединения классической модели ADO, прибегнув к услугам .NET COM Interop, однако при этом неизбежно возникают проблемы масштабируемости, издавна присущие ADO).
Поскольку программа обычно работает с автономными данными, типичному приложению .NET для обработки каждого запроса приходится заново подключаться к базе данных. На первый взгляд это кажется большим шагом назад, но такое впечатление обманчиво. Старый способ поддержания соединений плохо подходит для мира распределенных систем: если ваше приложение открывает соединение с базой данных и оставляет его открытым, серверу приходится поддерживать это соединение до тех пор, пока клиент его не закроет. Учитывая интенсивную загрузку современных серверов и пересылку огромных объемов данных, поддержание всех клиентских соединений отрицательно сказывается на пропускной способности сервера.
Кроме того, в web-комплексах [ Web-комплексом называется группа компьютеров, обрабатывающих трафик одного URL. Большинство крупных сайтов обслуживается web-комплексами, обеспечивающими более эффективное распределение нагрузки. ](Web farm) запросы могут обрабатываться разными компьютерами. Постоянные соединения с web-комплексами бесполезны, поскольку вы не знаете, какой сервер будет обрабатывать последующие запросы.
Форма результатов приложения

'frmMain.vb
Imports System.Data.SqlClient
Public Class frmMain
Inherits System.Windows.Forms.Form #Region "Windows Form Designer generated code "
Public Sub New()
MyBase.New()
'Вызов необходим для работы дизайнера форм Windows
InitializeComponent()
' Дальнейшая инициализация выполняется
' после вызова InitializeComponent()
End Sub
' Форма переопределяет Dispose для очистки списка компонентов.
Protected Overloads Overrides
Sub Dispose(ByVal disposing As Boolean)
If Disposing Then
If Not (components Is Nothing) Then
components. Dispose()
End If
End If
MyBase.Dispose(Disposing) End Sub
Private WithEvents Label1 As System.Windows.Forms.Label
Private WithEvents Label2 As System.Windows.Forms.Label
Private WithEvents Label3 As System.Windows.Forms.Label
Private WithEvents Label4 As System.Windows.Forms.Label
Private WithEvents btnConnect As System.Windows.Forms.Button
Private WithEvents txtUID As System.Windows.Forms.TextBox
Private WithEvents txtPassword As System.Windows.Forms.TextBox
Private WithEvents txtDatabase As System.Windows.Forms.TextBox
Private WithEvents txtServer As System.Windows.Forms.TextBox
' Необходимо для работы дизайнера форм Windows
Private components As System.ComponentModel.Container
' ВНИМАНИЕ: следующий фрагмент необходим для дизайнера форм Windows
' Для его модификации следует использовать дизайнер форм.
' Не изменяйте его в редакторе!
Private Sub _ Initial izeComponent()
Me.Label4 = New System.Windows.Forms.Label ()
Me.txtPassword = New System.Windows.Forms.TextBox()
Me.Label 1 = New System.Windows.Forms.Label ()
Me.txtServer = New System.Windows.Forms.TextBox()
Me.Label2 = New System.Windows.Forms.Label ()
Me.Labels = New System.Windows.Forms.Label ()
Me.txtUID - New System.Windows.Forms.TextBox()
Me.txtDatabase = New System.Windows.Forms.TextBox()
Me.btnConnect = New System.Windows.forms.Button()
Me.SuspendLayout()
'Label4
Me.Label4.Location = New System.Drawing.Point(24.176)
Me.Label 4.Name = "Label4"
Me.Label4.Size = New System.Drawing.Size(82.19)
Me.Label4.TabIndex = 0
Me.Label4.Text = "Password:"
Me.Label4.TextAlign = System.Drawi ng.ContentAlignment.MiddleRight
'txtPassword
Me.txtPassword.Location = New System.Drawing.Point(168.168)
Me ..txtPassword. Name = "txtPassword"
Me.txtPassword.PasswordChar = ChrW(42)
Me.txtPassword.Size = New System.Drawing.Size(205.22)
Me.txtPassword.Tablndex = 3
Me.txtPassword.Text = ""
'Label 1
Me.Label 1.Location = New System.Drawing.Point(24. 32)
Me.Label 1.Name = "Label1"
Me.Label 1.Size = New System.Drawing.SizeC82. 20)
Me.Label 1.Tablndex =0
Me.Label 1.Text = "Server:"
Me.Label 1.TextAli gn = System.Drawi ng.ContentAlignment.Mi ddleRight
'txtServer
Me.txtServer.Location - New System.Drawing.Point(168, 24}
Me.txtServer.Name = "txtServer"
Me.txtServer.Size = New System.Drawing.Size(205. 22)
Me.txtServer.Tablndex = 0
Me.txtServer.Text = ""
'Label 2
Me.Label2.Location = New System.Drawing.Point(24. 80)
Me.Label 2.Name = "Label 2"
Me.Label2.Size = New System.Drawing.Size(82, 20)
Me.Label2.Tablndex = 0
Me.Label 2.Text = "Database:"
Me.Label 2.TextAlign = System.Drawi ng.ContentAlignment.Mi ddleRight
'Label3
Me. Labels.Anchor = System.Windows.Forms.AnchorStyles.None
Me.Label3.Location = New System.Drawing.Point(24. 128)
Me.Labels.Name = "Label 3"
Me.Labels.Size = New System.Drawing.Size(82. 20)
Me.Labels.Tablndex = 0
Me.Labels.Text = "User ID:"
Me.Label 3.TextAli gn = System.Drawi ng.ContentAlignment.Mi ddleRi ght
'txtUID
Me.txtUID.Location = New System.Drawing.Point(168, 120)
Me.txtUID.Name = "txtUID"
Me.txtUID.Size - New System.Drawing.Size(205, 22)
Me.txtUID.Tablndex = 2
Me.txtUID.Text = ""
'txtDatabase
Me.txtDatabase.Location = New System.Drawing.Point(168. 72)
Me.txtDatabase.Name = "txtDatabase"
Me.txtDatabase.Size = New System.Drawing.Size(205. 22)
Me.txtDatabase.Tablndex = 1
Me.txtDatabase.Text = ""
'btnConnect
Me.btnConnect.Location = New System.Drawing.Point(160. 232)
Me.btnConnect.Name = "btnConnect"
Me.btnConnect.Size = New System.Drawing.Size(92, 30)
Me.btnConnect.Tablndex = 4
Me. btnConnect.Text = "SConnect"
'frmMain
Me.AutoScaleBaseSize = New System.Drawing.Size(6. 15)
Me.ClientSize - New System.Drawing.Size(408, 280)
Me.Controls.AddRange(New _
System.Wi ndows.Forms.Control(){Me.btnConnect,_
Me.txtPassword. Me.txtUID. Me.txtDatabase.
Me.txtServer.Me
.Label 4.
Me.Label3.Me
.Label 2.
Me.Label 1})
Me.Name - "frmMain" Me.Text = "DB Connector"
Me.ResumeLayout(False) End Sub
#End Region
Private Sub btnConnect_C1ick(ByVal sender As System.Object,_
ByVal e As System.EventArgs) Handles btnConnect.Click
Try
mySQLConn = New SqlConnectionC'user id=" & txtUID.Text &
";password="&txtPassword.Text & _ ";database="&txtDatabase.Text & _
";server="&txtServer.Text)
mySQLConn.Open() dbCmd.Connect!on = mySQLConn
Dim frmChild As New frmResults() frmChild.Show()
Catch except As Exception MsgBox(_
"Failed to connect for the following reason:<" & _ except.Message & ">")
End Try
End Sub
End Class
Модуль содержит следующий код:
Imports System.Data.SqlClient Module main
' Глобальные определения
Public mySQLConn As SqlConnection
Public dbReader As SqlDataReader
Public dbCmd As SqlCommand = New SqlCommand()
End Module
Модуль Modulel содержит только глобальные определения различных объектов SQL, которые должны быть доступны для обеих форм. Хотя обычно подобное использование глобальных данных в окончательных версиях программ не рекомендуется, в данном случае это позволяет сосредоточить основное внимание на выполнении операций с базой данных.
Классы сборки System.Data.DLL
Сборка System.Data.DLL содержит большое количество классов, разделенных на пять пространств имен работы с данными с дополнительным пространством
System.Xml. Вспомогательное пространство System.Data.SqlTypes содержит структурные типы, соответствующие типам данных SQL Server (например, Sql Money и SqlDateTime).
Поскольку типы данных SQL реализованы в виде структурных типов, их преобразования отличаются большей эффективностью по сравнению с другими языками — например, по сравнению с Java, где типы SQL реализованы в виде ссылочных типов.
Другое вспомогательное пространство имен, System.Data .Common, содержит классы, часто используемые при обращениях к источнику данных. В этой главе основное внимание уделяется пространствам имен System. Data.OleDb и System. Data. SqlCLient, выполняющим непосредственную работу. Классы этих пространств имен используют средства System. Data. Common, включая класс DataAdapter. Класс DataAdapter представляет соединение с базой данных, используемое при заполнении набора данных или обновлении источника, а также некоторые стандартные команды при операциях с базами данных.
Пространства имен System.Data.OleDb и System.Data.SqtCLient обладают сходной функциональностью, с одним исключением — классы System.Data.OleOb предназначены для подключения к источникам данных OLE DB, а классы System.Data.SqlCHent ориентированы на Microsoft SQL Server версии 7 и выше.
Содержание
Вперед
Нетривиальный пример работы с базами данных в VB .NET (часть 1)
В этом разделе представлено графическое приложение, при помощи которого пользователь может подключиться к выбранной базе данных SQL, выполнить запрос и получить его результаты в виде списка. Простоты ради мы отказались от проверки пользовательского ввода. Программа состоит из трех файлов: двух форм (frmMain и frmResults, см. Рисунок 11.2 и 11.3 соответственно) и стандартного модуля Modulel.
Несмотря на свою длину, программа не содержит ничего принципиально нового. На главной форме размещены четыре текстовых гюля для ввода имени сервера, имени базы данных, идентификатора пользователя и пароля. При нажатии кнопки Connect программа динамически выполняет введенную команду во фрагменте, выделенном жирным шрифтом.
Вероятно, наибольший интерес представляет форма frmResults (комментарии следуют после листинга). Ключевое место в этой форме занимает метод btnQuery_Click, выделенный жирным шрифтом:
' frmResults.vb
Imports System.Data.SqlClient
Public Class frmResults
Inherits System.Windows.Forms.Form fRegion "Windows Form Designer generated code "
Public Sub New() MyBase.New()
'Вызов необходим для работы дизайнера форм Windows
InitializeComponent()
' Дальнейшая инициализация выполняется
' после вызова InitializeComponent()
End Sub
' Форма переопределяет Dispose для очистки списка компонентов.
Public Overrides Sub Dispose()
MyBase.Dispose()
If Not (components Is Nothing) Then components.
Dispose()
End If
End Sub
Private WithEvents txtQuery As System.Windows.Forms.TextBox
Private WithEvents btnQuery As System.Windows.Forms.Button
Private WithEvents IstData As System.Windows.Forms.ListBox
' Необходимо для работы дизайнера форм Windows
Private components As System.ComponentModel.Container
' ВНИМАНИЕ: следующий фрагмент необходим для дизайнера форм Windows
' Для его модификации следует использовать дизайнер форм.
' Не изменяйте его в редакторе!
Private Sub _
Initial izeComponent()
Me.btnQuery = New System.Windows.Forms.Button()
Me.txtQuery = New System.Windows.Forms.TextBox()
Me.IstData = New System.Windows.Forms.ListBox()
Me.SuspendLayout()
'btnQuery
Me. btnQuery. Font = NewSystem. Orawing. Font ("Microsoft Sans Serif"._
8.5!.System.Drawing.FontStyle.Regular,
System.Drawing.GraphicsUnit.Point,CType(0. Byte))
Me.btnQuery.Location = New System.Drawing.Point(440. 0)
Me.btnQuery.Name = "btnQuery"
Me.btnQuery.Size = New System.Drawing.Size(56. 24)
Me.btnQuery.Tablndex = 2
Me.btnQuery.Text = "&Execute"
'txtQuery
Me. txtQuery. Font=New System. Drawing. Font ("Microsoft Sans Serif", _
8.5!. System.Drawing.FontStyle.Regular.
System.Drawi ng.Graphi csUnit.Point.CTypet 0. Byte))
Me.txtQuery.Location = New System.Drawing.Point(8. 0)
Me.txtQuery.Name = "txtQuery"
Me.txtQuery.Size = New System.Drawing.Size(432, 20)
Me.txtQuery.Tablndex = 1
Me.txtQuery.Text = "TextBox1"
'IstData
Me.lstData.ColumnWidth = 120
Me.IstData.Location = New System.Drawing.Point(8. 32)
Me.lstData.MultiColumn = True
Me.lstData.Name = "IstData"
Me.lstData.Size = New System.Drawing.Size(488. 355)
Me.lstData.Tablndex = 3
'frmResults
Me.AutoScaleBaseSize = New System.Drawing.Size(5. 13)
Me.ClientSize = New System.Drawing.Size(504. 397)
Me.Controls.AddRange(New System.Windows.Forms.Control()
{Me.lstOata. Me.btnQuery, Me.txtQuery})
Me.Name = "frmResults"
Me.Text = "Query Window"
Me.ResumeLayout(False)
End Sub
#End Region
Private Sub btnQuery_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)
Handles btnQuery.Click
Try
dbCmd.CommandText = txtQuery.Text
dbReader=dbCmd. ExecuteReader (CoimandBehavior. Singl eResult)
' Получить схему таблицы
Dim dtbllnfo As DataTable = dbReader.GetSchemaTable()
' Служебная переменная для перебора записей
Dim rwRow As DataRow
Dim strHeaders As System.Text.StringBuilder - _
New System.Text.StringBuilder()
Dim strData As System.Text.StringBuilder = New _
System.Text.StringBuilder()
Dim typTypesCdtbllnfo.Columns.Count) As Type
Dim intCounter As Integer = 0
' Перебрать все записи метаданных
For Each rwRow In dtblInfo.Rows
' Определить тип
typTypes(intCounter)= rwRow("DataType") intCounter +=1
' Включить в строку имя поля
strHeaders.Append("<" & rwRow(0) & ">" & vbTab) Next
' Занести в список заголовочную строку
1stData.Items.Add(strHeaders.ToString())
' Перебор записей данных
Do While dbReader.Read()
' Перебор полей записи
For intCounter = 0 To (dbReader.FieldCount - 1)
' Включить содержимое поле в выходную строку
strData.Append(GetProperType(dbReader,intCounter,_
typTypes(intCounter)) & vbTab) Next
' Включить строку в список
1stData.Items.Add(strData.ToString())
' Очистить объект StringBuilder strData = New System.Text.StringBuilder()
Loop Catch except As Exception
MsgBoxt"Error:" & except.Message)
End Try
End Sub
' Функция получает данные конкретного столбца.
Private Function GetProperType(ByVal dr As SqlDataReader.
ByVal intPos As Integer, ByVal typType As Type) As Object
' Проверить тип поля, затем получить значение Select
Case typType.Name Case "String"
' Преобразовать и вернуть
Return CType(dr.GetString(intPos).String)
Case "Int32"
' Преобразовать и вернуть
Return
CType(dr.Get!nt32(intPos). Int32)
' Здесь следовало бы организовать проверку всех
' остальных типов и возврат соответствующих значений.
' Мы выбрали простой путь и ограничились проверкой
' двух самых распространенных типов
Case Else
Return "
End Select
End Function
End Class
'При нажатии кнопки в объект команды SQL
'заносится текст, введенный пользователем в текстовом поле:
dbCmd.CommandText = txtQuery.Text
(в настоящем примере пропущена проверка данных, необходимая в любой реальной программе).
Далее объявляются объекты, используемые при чтении и выводе имен полей и их значений:
Dim dtbllnfo As DataTable = dbReader.GetSchemaTable()
Dim rwRow As DataRow
Dim strHeaders As System.Text.StringBuilder = New _
System.Text.StringBuilder()
Dim strData As System.Text.StringBuilder = New _
System.Text.Stri ngBui1der()
Dim typTypes(dtblInfo.Columns.Count) As Type
Поскольку в этом приложении структура базы данных не известна заранее, мы получаем ее описание при помощи метода GetSchemaTable(). Этот метод возвращает объект DataTable с метаданными (описаниями полей записей полученного набора). Метаданные содержат информацию о количестве полей в записи, их именах и типах. На основании этой информации можно запросить и вывести данные из любой доступной базы данных. Помните, что в режиме Option Strict On (который всегда должен быть активным) для вызова правильной функции GetXXX() объекта DataReader необходимо знать тип поля. По соображениям эффективности в приведенном примере использованы две переменные типа Stri ngBui I der (см. ниже). Информация, необходимая для вывода данных в списке, извлекается в цикле:
Dim intCounter As Integer =0 For Each rwRow In dtbllnfo.Rows
typTypes(intCounter) = rwRow("'DataType")
intCounter += 1
strHeaders.Append("<" & rwRow(0) & ">" & vbTab) Next
Записи DataTable перебираются в цикле For Each. Типы полей сохраняются в массиве typTypes и затем присоединяются к объекту StringBuilder для последующего вывода всех имен столбцов за одну операцию (однократное обновление свойства выполняется быстрее многократных). Также обратите внимание на использование имени поля в вызове rwRow( "DataType") — структура таблицы может измениться, что приведет к изменению номера поля DataType. После завершения цикла у нас появится вся необходимая информация об именах и типах всех полей, и мы сможем перейти к ее выводу в конструкции с вложенным циклом:
Do While dbReader.Read()
For intCounter = 0 To (dbReader.FieldCount = 1)
strData.Append(GetProperType(dbReader.intCounter,
typTypes(intCounter)) & vbTab) Next
1stData.Items.Add(strData.ToString()) strData = New
System.Text.StrlngBuilder() Loop
Первая часть цикла напоминает аналогичные конструкции из предыдущих примеров — мы перебираем все поля записи, определяем тип каждого поля и выводим данные в списке перед следующим вызовом Read. Для упрощения этой задачи была написана вспомогательная функция GetProperType().
На Рисунок 11.4 показан результат выборки данных из базы Northwind.
Почему ADO .NET — не ADO++
В каждой из предыдущих версий VB появлялась новая модель поддержки баз данных. VB .NET следует этой давней традиции и представляет новый способ работы с данными — ADO .NET. При ближайшем рассмотрении выясняется, что название выбрано крайне неудачно. Почему? Потому что ADO .NET просто не является следующим поколением ADO! Это совершенно новая модель, не имеющая ничего общего с классическим вариантом ADO. В частности, для работы с результатами вам придется освоить новую объектную модель, основанную на объекте DataSet (объект ADO .NET DataSet не привязан к одной таблице и поэтому обладает значительно большими возможностями, чем, например, объект ADO RecordSet). Кроме того, модель ADO .NET:
Пространство имен System.Data.OleDb
Пространство имен System.Data.OleDb содержит классы, используемые при взаимодействии с OLE DB-совместимыми базами данных (такими, как Microsoft Access или Microsoft Fox Pro). Обычно в программах используются классы OleDbConnectl on, OleDbCommand и OleDbDataReader этого пространства имен. Ниже приведены краткие описания этих важных классов.
1 Imports System.Data.OleDb
2 Module Modulel
3 Sub Maint)
4 Dim myAccessConn As OleDbConnection
5 Dim dbReader As OleDbDataReader
6 Dim dbCmd As OleDbCommand =New OleDbCommand(
7 "SELECT Employees.FirstName.Employees.LastName FROM Employees")
8 Try
9 ' Открыть соединение
10 myAccessConn = New OleDbConnection(
11 "Provider=Microsoft.Jet.OLEDB.4.0;" &_
12 "Data Source=C:\Program Files \Microsoft _
Office\0ffice\SamplesNNorthwind.mdb")
13 myAccessConn.Open()
14 dbCmd.Connection = myAccessConn
15 dbReader = dbCmd.ExecuteReader(CommandBehavior.SingleResult)
16 Do While dbReader.Read()
17 Console.WriteLine(dbReader.GetString(0) & " " & _
dbReader.GetString(1))
18 Loop
19 Console.ReadLine()
20 Catch e As Exception
21 MsgBox(e.Message)
22 End Try
23 End Sub
24 End Module
Результаты, полученные при запуске этого приложения, показаны на Рисунок 11.1.
Результат обработки запроса к базе данных Northwind

В этой главе мы постарались дать представление о работе с ADO .NET, однако читатель должен помнить, что перед ним лишь предельно краткий обзор. В частности, мы совершенно не коснулись таких тем, как обновление данных в хранимых процедурах, элементы, связанные с данными, или объекты DataAdapter/DataSet. За подробностями обращайтесь к специализированной литературе.
Результаты выполнения простого запроса SQL

Хотя наше приложение всего лишь выводит список работников Northwind, его код типичен для подключения к любой базе данных при помощи .NET-провайдера OLE DB, предоставленного VB .NET. В строке 1 для упрощения дальнейших ссылок импортируется пространство имен System. Data. 0leDb. В строках 4 и 5 объявляются две объектные переменные. Объект 0leDbConnecti on инкапсулирует текущее соединение к провайдеру OLE DB и в конечном счете к базе данных (строки 10-12). Объект 0leDbDataReader инкапсулирует рабочие данные. В отличие от объектов RecordSet эти данные не обязаны относиться к одной таблице (хотя в нашем примере это именно так). Строка 6 определяет запрос SQL, хранящийся в объекте OleDbCommand. Использована версия конструктора с параметром типа Stri ng, в котором передается команда SQL, — в нашем случае это простейший из всех возможных запросов. В строке 10 создается соединение с базой данных. При вызове конструктора передается строка с именем провайдера OLE DB. Значение берется из реестра Windows и не является частью .NET (в нашем примере используется стандартный провайдер для Access). Также обратите внимание на жесткую кодировку местонахождения базы данных Northwind; в нашем примере выбран каталог, используемый по умолчанию при установке Office. Если на вашем компьютере база данных Northwind находится в другом каталоге, отредактируйте эту строку.
Затем созданное соединение открывается. Поскольку эта операция по различным причинам может завершиться неудачей, программный код открытия и чтения из базы данных заключается в блок Try-Catch. После успешного вызова Ореn() (строка 13) соединение можно использовать в программе (выполнение этих операций в конструкторе позволило бы сократить программу на несколько строк). Объект OleDbCommand пока не знает, какое соединение он должен использовать, поэтому открытое соединение с базой данных назначается свойству Connecti on объекта OleDbCommand (строка 14). Одно из преимуществ подобного решения заключается в том, что оно позволяет использовать один объект команды с несколькими соединениями.
Команда выполняется методом ExecuteReader() объекта 0leDbCommand (строка 15). Мы используем метод ExecuteReader, поскольку остальные методы Execute возвращают данные в формате XML и традиционные наборы записей, обрабатываемые менее эффективно. В строке 14 значение перечисляемого типа CommandBehavior. SingleResul t передается методу ExecuteReader в качестве параметра. Флаг Si ngl eResult означает, что команда должна выбрать из базы данных все записи результата. Другие флаги позволяют ограничить выборку одной или несколькими записями. Прочитанные записи перебираются в цикле в строках 16-18.
Код перебора записей эквивалентен следующему фрагменту VB6/ADO:
Do While Not rs.EOF
Print rs(0)
rs.MoveNext() Loop
Использование метода Read предотвращает одну распространенную ошибку, часто допускаемую программистами VB6 ADO, которые забывают перейти к следующей записи методом MoveNext. Все операции с одной записью выполняются между вызовами Read, поскольку после вызова Read вернуться к содержимому предыдущей записи уже не удастся.
В цикле вызываются различные методы GetXXX объекта 01 eDbReader, возвращающие значение поля с заданным индексом (нумерация полей записи начинается с 0). Таким образом, вызов
dbReader.GetString()
возвращает значение второго столбца в формате String. Вместо индекса при вызове GetStrlng можно указать имя столбца, но этот вариант менее эффективен.
Перед получением значения поля необходимо указать правильный тип. Мы всегда про-граммируем в режиме Option Strict On, поэтому преобразования данных с потерей точности допускаются лишь с явным приведением к заданному типу.
System. Data.SqlClient
Чтение данных из базы SQL Server происходит аналогичным образом — пространства имен OleDb и SqlClient имеют практически одинаковый синтаксис. Ниже приведена версия предыдущей программы для SQL Server:
Imports System.Data.SqlClient
Module Modulel
Sub Main()
Dim mySQLConnString As String
Dim mySQLConn As SqlConnection
Dim dbReader As SqlDataReader
Dim dbCmd As SqlCommand = New SqlCommand(
"SELECT Employees.FirstName.Employees.LastName FROM
Employees") Try
mySQLConnString = _
"uid-test:password=apress;
database=northwind:server=Apress"
mySQLConn = New SqlConnection(mySQLConnString)
mySQLConn.Open()
dbCmd.Connection = mySQLConn
dbReader = dbCmd.ExecuteReader(CommandBehavior.SingleResult)
Do While dbReader.Read()
' Вывести данные в консольном окне
Console.WriteLinetdbReader.GetString(0) & "." &_
dbReader.GetString(1))
Loop
Catch e As Exception MsgBox(e.Message)
End Try
Console. ReadLine()
End Sub
End Module
Основное различие (помимо имен классов) наблюдается в формате строки соединения. Предполагается, что на сервере Apress имеется учетная запись с паролем apress. При подключении к SQL Server в строке соединения указывается идентификатор пользователя, пароль, сервер и имя базы данных. Передавая эту информацию, мы получаем объект соединения. Конечно, лишь простейшие запросы формулируются в виде простой строки; в любом сколько-нибудь нетривиальном случае строку запроса приходится строить из отдельных фрагментов.
Несмотря на разный формат строк соединения, в приложениях SQL и OLE DВ используется одна и та же программная модель ADO .NET — это весьма существенное преимущество. Наличие общих интерфейсов IDbConnection, IdbCommand и т. д. значительно упрощает написание обобщенного кода в ADO .NET.
Вызов хранимой процедуры
В следующем примере используется хранимая процедура с именем getalbumname. Процедура вызывается с одним параметром и выбирает из базы данных albums запись альбома с заданным именем:
create procedure getalbumbyname
@albumname varchar(255) As
select *from albums where albumname = @albumname
Выборка данных с использованием хранимой процедуры организована аналогично простому запросу к базе данных Northwind:
Dim dbCmd As SqlCommand = New SqlCommand(
"execute getalbumbyname 'Operation Mindcrime'")
Try
mySQLConn =New SqlConnection(
"user id=sa:password=password;" & _
"database=albums;server=i-ri3")
mySQLConn.Open()
dbCmd.Connection = mySQLConn
dbReader = dbCmd.ExecuteReader(CommandBehavior.SingleResult)
' И т.д.
End Try
Как видите, программа почти не изменилась, разве что команда SQL, использовавшаяся для создания объекта Sql Command, превратилась в команду вызова хранимой процедуры getalbumbyname, которой в качестве параметра передается имя интересующего нас альбома. Конечно, после вызова ExecuteReader цикл перебора записей не нужен, поскольку мы точно знаем, что хранимая процедура возвращает всего одну запись.
Вместо того чтобы передавать параметр хранимой процедуры в строке вызова, можно воспользоваться коллекцией Parameters объекта SQLCommand. Мы решили, что вариант с непосредственной передачей параметров в команде SQL проще. Конечно, это возможно лишь в том случае, если значение параметра известно во время написания программы, в противном случае приходится использовать коллекцию Parameters.
Cамоучитель по VB.NET
Автоматически сгенерированная страница с описанием web-службы

При создании web-службы VS.NET IDE автоматически генерирует XML-файл с описанием службы, написанный на языке WSDL (Web Service Language Description). Программисты COM могут рассматривать его как аналог библиотеки типов. Редактирование сгенерированного файла .vsdisco позволяет изменить информацию о web-службе, содержащуюся в файле WSDL. В частности, Рисунок 12.4 был получен на основании кода WSDL.
Что передается клиенту?
Сценарии ASP .NET программируются в обычном стиле VB .NET, однако в зависимости от типа клиентского броузера генерируется разный код HTML. Например, если клиент работает в последней версии Internet Explorer, в сгенерированном коде используются конструкции DHTML, а интервальная проверка осуществляется на стороне клиента. Но если в качестве броузера используется сотовый телефон с поддержкой WAP (Wireless Application Protocol), будет сгенерирован код WML (версия HTML для этой платформы), а все необходимые проверки будут выполняться сервером. И все это происходит автоматически, совершенно не требуя особых действий со стороны программиста![ Если, конечно, вы не займетесь разработкой нестандартных элементов для форм Web — в этом случае вам придется изрядно потрудиться. Элементы форм ^Veb должны знать, какой код следует генерировать для каждой конкретной платформы. ]
Ниже приведен код предыдущего примера, сгенерированный для клиентского броузера Internet Explorer, работающего в Windows XP. Ключевые строки выделены жирным шрифтом:
content="http://schemas.microsoft.com/intel1isense/ie5">