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 имеет ряд несомненных преимуществ перед С#. Ниже перечислены пять из них, которые нам кажутся самыми важными:

  • Присутствие многих знакомых функций VB/VBScript (таких, как Mid, Sin(x) вместо Math.Sin(x) или FormatNumber) вместо сложных и порой менее удобных функций .NET Framework.
  • Наглядность. В VB .NET многие понятия записываются простым естественным языком. Например, вместо конструкции С# «:» в VB .NET используются слова Inherits или Implements. В С# используются мудреные слова abstract, sealed и virtual, а в VB .NET — Must Inherit, Nonlnheritable, Overridable, Overrides, Shadows. Несомненно, второй набор выглядит понятнее, даже если не знать, что означают эти термины.
  • Компиляция кода происходит в фоновом режиме, поэтому вы получаете немедленный отклик от компилятора (что гораздо лучше простого лексического анализа кода, как в С#).
  • В VB .NET не учитывается регистр символов, а интеллектуальный редактор автоматически изменяет регистр в соответствии с объявлениями. С#, как и все языки семейства С, чувствителен к регистру символов. Людей, привыкших работать в VB, это сильно раздражает. Мы подошли к последней, самой главной причине:
  • VB .NET сохранил общее сходство с Visual Basic 6, самым популярным языком программирования в мире!




  • Структурная обработка ошибок

    Во всех версиях 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) Private Sub _
    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

  • Классы рассматриваются в главе 4.
  • Ключевое слово Inherits описано в главе 5, а формы Windows — в главе 8.
  • Новая среда программирования позволяет определять сворачиваемые фрагменты кода (глава 2).
  • Конструктор New рассматривается в главе 4.
  • В этой строке используется механизм наследования (глава 5).
  • Описание форм Windows приведено в главе 8.
  • Наследование рассматривается в главе 5, а метод Dispose упоминается в главах 4 и 5.
  • События описаны в главе 6. Специфика обработки событий в приложениях GUI рассматривается в главе 8.
  • Весь содержательный код этой процедуры проанализирован в главе 8.
  • См. главу 8.
  • Классы рассматриваются в главе 4.


  • 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


    /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


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



    Диалоговое окно Options


    Диалоговое окно 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


    Начальная страница 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


    Окно решения и окно свойств для файла 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 + вниз
    Чтобы скопировать ранее введенную команду (полностью или частично) в строку текущей команды, перейдите к нужной команде, выделите ее и нажмите Enter.

    Назад Содержание Вперед




    Окно вывода при обнаружении ошибок


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

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

    Окно вывода при успешном построении


    Окно вывода при успешном построении
    На этот раз компиляция прошла успешно, но бывает и иначе. На Рисунок 2.20 показано сообщение об ошибке, полученное при вызове несуществующей функции.

    Основные окна IDE

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

    Отладка потоков

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



    Отладка процесса Notepad.exe


    Отладка процесса 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

    bin

    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 те фрагменты кода, которыми должен заняться ваш коллега Кен. Определение пользовательских ключевых слов для списка задач происходит следующим образом:

  • Выполните команду Tools > Options > Environment > Task List.
  • Введите текст FOR_KEN (при этом становится доступной кнопка Add).
  • Выберите приоритет.
  • Нажмите кнопку Add и кнопку ОК.




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


    Свернутые области в редакторе
    Редактор VS .NET обладает и другими интересными возможностями, незнакомыми даже опытным программистам VB. Мы познакомимся с ними в следующем разделе.

    При изучении редактора IDE особенно полезная информация находится в разделе справки «Editing Code and Text». В частности, здесь описано несколько очень удобных средств перемещения по тексту.



    Управление исключениями

    На первый взгляд управление исключениями кажется экзотикой, не связанной с практической работой. Чтобы оценить эту возможность по достоинству, достаточно оказаться в ситуации, когда на стадии тестирования возникают многочисленные исключения (см. главу 7) и возникает необходимость в тонкой настройке действий, выполняемых при возникновении исключений. Это делается в диалоговом окне, вызываемом командой Debug > Windows > Exceptions. В этом окне вы указываете, как должен действовать отладчик при обнаружении исключений определенного типа. Допустим, вы хотите, чтобы при возникновении ошибок доступа управление передавалось отладчику.

  • Выберите исключение Win32Exceptions >0xc0000005.
  • Установите в группе When the exception is thrown переключатель Break into the debugger.
  • В результате отладчик будет автоматически вызываться при возникновении ошибок доступа (0xc0000005), и вы сможете точно определить, в какой строке программы это произошло.



    Visual Studio IDE в начале работы


    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. В частности, они позволяют:

  • Задать значок приложения (Common Properties > Build).
  • Просмотреть или изменить список автоматически импортируемых библиотек (Common Properties > Imports).
  • Задать различные параметры сборки и пространства имен, к которым относится ваш проект (Commpn Properties > General). Эти два важных понятия рассматриваются в главах 4 и 13.
  • По умолчанию сборке и пространству имен назначаются имена, соответствующие имени вашего решения. Пробелы в этих именах недопустимы, поэтому VB .NET автоматически заменяет их символами подчеркивания.





    Выходные файлы

    Что же получается в результате компиляции проекта? На Рисунок 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

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

  • Измените имя модуля в окне программы.
  • Измените имя файла модуля в окне решения.
  • Щелкните правой кнопкой мыши в строке ConsoleApplication окна решения и выберите в контекстном меню команду Properties.
  • Убедитесь в том, что в списке Startup object появившегося диалогового окна (Рисунок 3.1) выбрано имя модуля.
  • По аналогии с VB6 программа VB .NET (решение) может состоять из нескольких модулей, но наличие процедуры Sub Main допускается только в одном модуле. Приложение завершается по достижении команды End Sub процедуры Sub Mai n. Например, легендарная программа «Hello world» выглядит следующим образом:

    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) выполняется раньше, чем сложение.

    Ниже перечислены математические операции в порядке убывания приоритета.

  • Возведение в степень (^).
  • Унарный минус (изменение знака числа).
  • Умножение и деление.
  • Целочисленное деление.
  • Вычисление остатка (Mod).
  • Сложение и вычитание.
  • Если две операции обладают одинаковым приоритетом, порядок выполнения определяется порядком их следования в выражении (слева направо).

    Сокращенная запись операций с присваиванием

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

    Сокращенная запись
    Эквивалент
    А*=В
    А = А*В
    А+=В
    А = А + В
    А/=В
    А = А/В
    А-=В
    А = А-В
    А\=В
    А = А\В
    А^=В
    А = А^В
    А&=В
    А = А & В (конкатенация строк)


    Литералы и их соответствие типам данных

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

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

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

  • Байт: фактически вы сообщаете компилятору, что для хранения числа следует выделить минимальный объем памяти.
  • Короткое целое: старый тип Integer из VB6.
  • Целое .NET: старый тип Long из VB6 (компилятор выделяет для хранения числа 4 байта).
  • К счастью, символ 3 никогда не будет автоматически интерпретироваться как строковая константа (если не переопределять стандартную логику VB). В VB .NET строки и числа по умолчанию не смешиваются — более подробно эта тема рассматривается в разделе «Преобразования разнотипных значений» этой главы.

    С точки зрения компилятора простой констатации «это число 3» недостаточно. Разумеется, VB .NET, как и любой язык программирования, позволяет уточнить смысл литерала. Например, 31 — литерал типа Integer со значением 3, а литерал "3" относится к строковому типу String (тип String рассматривается ниже в этой главе; он несколько отличается от строкового типа в прежних версиях VB).

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

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

  • Byte: 1-байтовое целое без знака в интервале от 0 до 255.
  • Short: 2-байтовое целое со знаком в интервале от -32 768 до 32 767, аналог типа Integer в прежних версиях VB. Признаком типа Short в литералах является суффикс S — например, 237S.
  • Integer: 4-байтовое целое со знаком в интервале от -2 147 483 648 до 2 147 483 647, аналог типа Long в прежних версиях VB. Признаком типа Integer в литералах является суффикс I — например, 2371.
  • Если суффикс не указан, а число входит в интервал допустимых значений типа Integer, по умолчанию оно сохраняется в формате Integer. Это связано с тем, что на 32-разрядных процессорах тип Integer обрабатывается эффективнее остальных типов.

  • Long: 8-байтовое целое со знаком в интервале от -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807. Не имеет аналогов в прежних версиях VB. Признаком типа Long в литералах является суффикс L — например, 2371.
  • При объявлении числовых переменных можно использовать старые суффиксы типов %, & и т. д. — например, литерал 1234% относится к типу Long. Но при этом следует помнить, что в VB6 и VB .NET эти суффиксы имеют разный смысл, поскольку тип Integer VB .NET соответствует типу Long V86. По этой причине использовать старый синтаксис не рекомендуется.

    Любой целочисленный литерал можно записать в шестнадцатеричной системе счисления (по основанию 16), для чего он снабжается префиксом &Н. Например, литерал &HF соответствует десятичному числу 15, хранящемуся в формате Integer, поскольку суффикс типа не указан, а число входит в интервал допустимых значений типа Integer. Числа, записанные в восьмеричной системе счисления (по основанию 8), снабжаются префиксом &0.

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

  • Single: 4-байтовое вещественное число. Признаком типа Single в литералах является суффикс F — например, 1.23F или 3F.
  • Double: 8-байтовое вещественное число. Если в числе с десятичной точкой не указан суффикс, по умолчанию оно сохраняется в формате Double. Это связано с тем, что Double работает эффективнее Single; на 32-разрядных процессорах Double является основным типом для выполнения вещественных операций. Признаком типа Double в литералах является суффикс R (или #).
  • Новый тип Decimal пришел на смену старому типу Currency, использовавшемуся в прежних версиях VB. Он используется в ситуациях, когда ошибки округления недопустимы.

  • Decimal: 12-байтовое вещественное число, гарантирующее отсутствие ошибок округления в громадном интервале допустимых значений с 28 значащими цифрами. Формальное определение гласит, что тип Decimal предназначен для хранения чисел с мантиссой в интервале ±79 228 162 514 264 337 593 543 950 335, масштабируемых до произвольного порядка при условии, что количество значащих цифр не превышает 28. Таким образом, наименьшее число, представляемое типом Decimal, равно ±0.0000000000000000000000000001. Признаком типа Decimal в литералах является суффикс D.
  • Применение суффикса типа в литералах помогает избежать путаницы и случайных ошибок переполнения, возникающих при умножении двух чисел. При выполнении следующей команды:

    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. Одни изменения видны сразу, другие не столь очевидны. Наиболее заметные изменения перечислены ниже.

  • Индексация-элементов в массивах начинается с 0. На момент написания книги ключевое слово То не поддерживалось — будем надеяться, что оно еще вернется!
    Начиная с бета-версии 2 объявление 01m stri ngLi st(7) создает массив из восьми элементов с индексами от 0 до 7. Поскольку в VB .NET индексация всегда начинается с нуля, третий элемент массива обозначается stri ngList(2), а предшествующие элементы обозначаются stringList(0) и stringList(l).
  • Все массивы VB .NET являются динамическими. Во время работы программы их можно переобъявить с новым размером при помощи команд ReDim (с потерей текущего содержимого) и ReDim Preserve (с сохранением текущего содержимого). Пример:
    Dim x() As Single

    ReDim x(20) ' Начиная с бета-версии 2. создает массив из 21 элемента

    ReDim Preserve x(50) ' 21 элемент сохраняется в массиве.

  • Команда ReDim не позволяет изменять тип массива; также не допускается использование ReDim при объявлении. Перед вызовом ReDim массив должен быть объявлен при помощи Dim или аналогичной команды.

  • Массивы могут инициализироваться при объявлении, как показывает следующий пример:
  • Dim weekend() As String = {Saturday. Sunday}

    Менее очевидные изменения обусловлены тем, что массивы 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, — основаны на принципе рекурсии).

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

  • НОД(4,6) = 2 (наибольшее число, на которое 4 и 6 делятся без остатка).
  • НОД(12,7) - 1 (числа 12 и 7 не делятся ни на одно целое число, большее 1).
    Около 2000 лет назад Евклид предложил следующий алгоритм вычисления НОД двух целых чисел а и b:

  • Если а делится на b, НОД= b. В противном случае НОД (а, b) = НОД (b, a Mod b)
  • Вспомним, что функция Mod возвращает остаток от целочисленного деления, а выражение a Mod b,равно 0 лишь в том случае, если а кратно 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





    Операторы сравнения

    Символ
    Проверяемое условие
    <>
    Не равно
    <
    Меньше
    <=
    Меньше или равно
    >
    Больше
    >=
    Больше или равно
    Строковые операнды по умолчанию сравниваются в соответствии с порядком символов Unicode. Таким образом, «А» предшествует «В», но «В» предшествует «а» (а пробел предшествует любому печатному символу). Строка «aBCD» предшествует строке «CDE» (то есть считается «меньше» ее), поскольку прописные буквы в кодировке стоят раньше строчных.

    Как и в 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
    ,VB .NET выполняет числовые преобразования только в том случае, если преобразуемое число входит в интервал допустимых значений нового типа; в противном случае выдается сообщение об ошибке.

    На первый взгляд кажется, что тип 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.

    Ниже приведен хрестоматийный пример — преобразование температуры по Цельсию в температуру по шкале Фаренгейта. Мы руководствуемся следующими предположениями:

  • Пользователь завершает ввод текста нажатием клавиши Enter.
  • Все введенные символы воспринимаются методом ReadLine().
  • Пользователь ввел число, поэтому введенный текст преобразуется к числовому типу функцией CDec (конечно, на практике введенные данные следовало бы предварительно проанализировать):
  • ' Преобразование температуры по Цельсию в температуру по Фаренгейту
    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, в начале строки
    В отличие от VB6, где индексация символов в строке начиналась с 1, в методах .NET Framework индекс первого символа равен 0.

    Поскольку в .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)
    * Умножение
    ^ Возведение в степень
    Чтобы лучше разобраться в разных типах деления, можно воспользоваться методом .NET GetType. В командах вывода (таких как WriteLine) этот метод возвращает имя типа в строковом представлении. Рассмотрим следующую программу:

    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
    Остаток от целочисленного деления
    Оператор \ игнорирует остаток от деления и возвращает результат типа Integer (если он относится к интервалу допустимых значений этого типа). Например, 7\3=21. Напомним, что оператор / дает результат типа Double; если вы хотите, чтобы частное относилось к типу Integer — воспользуйтесь оператором \ или функцией преобразования типа.

    Оператор 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
    Класс ArrayList реализует динамический массив, размеры которого автоматически увеличиваются и уменьшаются по мере надобности. Динамические массивы работают чуть медленнее обычных массивов, но они заметно упрощают многие задачи программирования. Кроме того, в отличие от большинства массивов класс ArrayLi st является гетерогенным, то есть позволяет хранить объекты разных типов. В главе 5 будет показано, как создать класс ArrayList для хранения объектов лишь одного типа; вы также узнаете о некоторых нюансах, связанных с хранением обобщенных объектов в ArrayLi St.

    Использование ArrayList вместо базового массива означает, что вам не придется часто вызывать ReDim Preserve для сохранения существующих данных. Достаточно вызвать метод Add, и класс ArrayList сам выполнит всю черновую работу. Класс ArrayList содержит ряд других полезных методов. Например, метод AddRange позволяет перенести в динамический массив все содержимое существующего массива всего одной командой. После завершения обработки элементы можно скопировать обратно. В частности, это позволяет легко объединить содержимое двух массивов. В табл. 4.3 перечислены основные члены класса ArrayList (полный список приведен в электронной документации).

    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оуее могут храниться следующие сведения:

  • Имя.
  • Дата приема на работу.
  • Текущая зарплата.
  • Чтобы изменить значения полей экземпляра, пользователи не обращаются к ним напрямую, а изменяют свойства и вызывают методы типа Rai seSalаrу. Разумеется, метод RaiseSalary будет изменять поле с текущей зарплатой, но в нетривиальном классе Employee он может работать с несколькими полями. Например, легко представить себе метод Rai seSalагу, который принимает решение о повышении зарплаты с учетом ее текущего уровня, рабочего стажа и личных достижений работника. Подведем итог. Инкапсуляция определяет функциональность объекта с точки зрения пользователя. Ее непосредственными проявлениями в VB .NETвыступают члены класса (методы, события и свойства).

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



    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 — производным классом. Производные классы:

  • всегда решают более специализированные задачи, чем базовые классы;
  • содержат все члены базового класса (хотя поведение этих членов может быть совершенно иным).
  • Например, метод RaiseSalary класса Manager может давать менеджеру большую прибавку к жалованию, чем метод RaiseSalary класса 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. Общие конструкторы:

  • не обладают атрибутами Publiс или Private;
  • вызываются без параметров;
  • могут работать только с общими полями класса. Как правило, общие конструкторы применяются только для инициализации общих данных. Код общего конструктора выполняется при создании первого экземпляра указанного класса, перед вызовом всех остальных конструкторов.


  • Общие данные в классах

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

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

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

    Ниже приведен фрагмент новой версии класса Employee с использованием общих данных для присвоения номеров. В классе определяется закрытая общая переменная типа Integer, которая:

  • имеет начальное значение 1;
  • ассоциируется со ReadOnly-свойством, возвращающим ее текущее значение;
  • изменяется (увеличивается) только в конструкторе класса.
  • В совокупности это означает, что работнику никогда не будет присвоен номер 0 и что новый номер выделяется только при создании нового объекта Empl oyee — именно это нам и требовалось:

    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 .NET, C# и Java, кроме классических типов существует четвертый тип отношений между классами — реализация интерфейса (отношение типа «поддерживает»). Суть реализации интерфейса заключается в том, что для поддержки некоторых функциональных возможностей ваш класс принимает на себя обязательства, по которым он должен содержать определенные члены. Интерфейсы существуют в VB начиная с версии 5 и часто используются в VB .NET. В главе 5 эта тема рассматривается гораздо подробнее.

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

  • член класса А отправляет сообщение объекту класса Б
  • или

  • член класса А создает или возвращает объекты класса Б.
  • Постарайтесь свести к минимуму количество взаимодействующих классов. Иначе говоря, избегайте лишних связей между классами, без которых можно обойтись. Если класс А не использует класс Б, то изменения в классе Б никак не отразятся на работе класса А (а следовательно, модификация класса Б не станет причиной ошибок в классе А!).

    Термин «включение» (агрегирование) означает, что объект класса А содержит внутренние объекты класса Б.

    На базе включения реализуется методика делегирования, когда поставленная перед внешним объектом задача перепоручается внутреннему объекту, специализирующемуся на решении задач такого рода. Агрегирование с делегированием методов было очень распространенным явлением в прежних версиях 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



    Переход к использованию объектов

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

  • Задача разбивалась на подзадачи; те, в свою очередь, делились на подзадачи следующего уровня и т. д. Это продолжалось до тех пор, пока упрощение подзадач не позволяло реализовать их непосредственно (подход «сверху вниз»).
  • Программист писал процедуры для решения простых задач и последовательно объединял их в более сложные процедуры, пока недобивался нужного эффекта (подход «снизу вверх»).
  • Конечно, многие опытные программисты не следовали рекомендациям теоретиков, выступавших за первый способ, и предпочитали решать практические задачи комбинацией этих двух стратегий [ В программировании это обычно называется встречным движением. ].

    Между ООП и процедурно-ориентированным программированием существуют два важных различия:

  • В ООП программист сначала выделяет классы, образующие объектную модель, и только после этого переходит к анализу их методов и свойств.
  • Методы и свойства ассоциируются с классом, предназначенным для выполнения соответствующих операций.
  • Возникает очевидный вопрос: по каким критериям выделять классы в программе? Для этого имеется хорошее эмпирическое правило, которое связывает компоненты объектной модели с частями речи. Классы соответствуют существительным в постановке задачи. В нашем примере центральное место занимает существительное «работник» (Employee). Методы объектов соответствуют глаголам — например, работнику можно повысить зарплату (метод RaiseSalary). Свойства соответствуют прилагательным, описывающим существительные. Разумеется, это соответствие лишь намечает контуры объектной модели. Только практический опыт поможет вам решить, какие существительные, глаголы и прилагательные важны, а какие являются второстепенными.

    Сейчас стоит повторить золотое правило программирования, нисколько не изменившееся с переходом на ООП: будьте проще. Использование простых классов заметно упрощает объектно-ориентированное программирование. Класс с простой внутренней структурой и небольшим числом внешних связей проще понять, а следовательно, и запрограммировать.

    Описание логических связей между классами играет в ООП настолько важную роль, что появилась целая наука о построении диаграмм, иллюстрирующих отношения между классами. Чаще всего для описания логических связей применяется язык 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.

  • Если ссылка на Empl oyee в действительности относится к объекту Manager, будет вызван метод RalseSalary класса Manager.
  • В противном случае вызывается стандартный метод RaiseSalary базового класса.
  • В VB5 и VB6 смысл термина «полиморфизм» был расширен, и к традиционному полимор-физму на базе наследования добавился полиморфизм на базе интерфейсов (объект, реализующий интерфейс, вызывал метод интерфейса вместо другого метода с тем же именем). Объект, реализующий интерфейс Manager, правильно выберет метод RaiseSalary в зависимости от контекста использования.

    В обоих случаях объект выбирает метод в зависимости от полученного сообщения. При отправке сообщения не нужно знать, к какому классу фактически принадлежит объект; достаточно разослать сообщение всем объектам 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. Она открывает список, в котором выбирается режим представления информации в окне.

  • Sort Alphabetically. Классы и члены упорядочиваются по алфавиту (a-z).
  • Sort By Type. Классы и члены упорядочиваются по типу. Например, в этом режиме удобно сгруппировать все свойства (базовых классов, интерфейсов, методов и т. д.).
  • Sort By Access. Классы и члены упорядочиваются по уровню доступа.
  • Group By Type. Классы и члены группируются в разных узлах в зависимости от типа. Например, все свойства объединяются в узле Properties, а все поля — в узле Fields.



  • Результат работы программы с


    Результат работы программы с
    Глава получилась очень длинной. В ней вы познакомились с некоторыми встроенными классами .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 окна свойств
    Чтобы рассмотреть пример использования 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. Единственное отличие структур от обычных объектов заключается в том, что структуры обладают структурной семантикой. Вспомните, какой смысл вкладывается в этот термин:

  • передача по значению не изменяет состояния исходной переменной;
  • структуры создаются без использования оператора New, поэтому для них всегда определено значение по умолчанию, образованное значениями по умолчанию всех полей экземпляра;
  • в структуре определен метод Equals, который возвращает True, если две структуры содержат одинаковые внутренние данные (метод Equals используется в форме А.Еquals(В)).
  • В текущей версии VB .NET равенство двух экземпляров структурного типа не может быть проверено при помощи знака равенства (=). Вместо этого следует использовать метод Equals. По умолчанию метод Equals выполняет так называемое поверхностное (shallow) сравнение — смысл этого термина рассматривается в разделе «Клонирование объектов» главы 5. Если вы хотите, чтобы ваша версия Equals отличалась каким-то особым поведением, метод можно переопределить в определении структуры.

    Некоторые программисты используют структуры чаще, чем следует, полагая, что структура как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 выполняют несколько полезных операций:

  • Replace(ByVal oldChar As Char. ByVal newChar As Char): заменяет все вхождения старого символа в строке новым символом;
  • Replace (ByVal oldValue As String. ByVal newValue As String): заменяет все вхождения старой подстроки новой подстрокой.
  • У этого метода существуют еще две версии, позволяющие заменить все вхождения заданной подстроки или символа в заданном фрагменте объекта Stri ngBuilder (параметр count определяет длину фрагмента):

    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 строки.



    Важнейшие члены класса

    Capacity Получает или задает максимальное количество элементов, на которое рассчитан объект ArrayList. Конечно, вместимость массива изменяется по мере добавления новых элементов, но по соображениям эффективности вместимость наращивается большими «порциями» BinarySearch Выполняет бинарный поиск заданного элемента в отсортированном динамическом массиве или в его части AddRange Позволяет добавить содержимое другого массива (динамического или обычного) в текущий динамический массив. В сочетании с методом InsertRange позволяет быстро объединять массивы с использованием Arraylist в качестве вспомогательного класса
    Add Добавляет новый объект в конец динамического массива Имя

    Описание

    Count

    Возвращает количество элементов, фактически хранящихся в массиве

    GetRange

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

    IndexOf

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

    Insert

    Вставляет элемент в заданную позицию объекта ArrayList

    InsertRange

    Вставляет элементы коллекции в объект ArrayList начиная с заданной позиции

    Item

    Получает или задает значение элемента, находящегося в заданной позиции. Является свойством по умолчанию для класса ArrayList

    LastlndexOf

    Возвращает индекс последнего вхождения заданного элемента в динамический массив (индексация начинается с нуля)

    Length

    Возвращает количество элементов в динамическом массиве

    Readonly

    Возвращает новый объект ArrayList, доступный только для чтения (проверка возможности записи в динамический массив осуществляется методом IsReadOnly)

    Remove

    Удаляет из массива первое вхождение заданного элемента

    Re move At

    Удаляет элемент, находящийся в заданной позиции

    RemoveRange

    Удаляет последовательность смежных элементов

    RepeatRange

    Возвращает объект ArrayList, содержащий заданное количество дубликатов



    одного элемента

    Reverse

    Переставляет элементы в объекте ArrayList в противоположном порядке (во всем массиве или в его части)

    SetRange

    Копирует элементы коллекции поверх интервала элементов ArrayList

    Sort

    Сортирует элементы в объекте ArrayList (во всем массиве или в его части)

    ToArray

    Копирует элементы из объекта ArrayList в массив

    TrimToSize

    Используется после завершения операций с объектом ArrayList; вместимость динамического массива уменьшается до фактического количества элементов, хранящихся в нем в настоящий момент (разумеется, позднее массив снова может увеличиться)

    Среди свойств класса ArrayList наибольший интерес представляет свойство Item, которое представляет элемент с заданным индексом. Пример:

    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 .NET это происходит при вызове метода или изменении свойства).
  • Если объект ведет себя по определенным правилам, образующим открытый интерфейс, и должным образом реагирует на внешние воздействия, пользователю не нужно думать о внутренней реализации этого объекта.

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

    Одно из величайших преимуществ .NET заключается в том, что вы можете программировать классы на любом языке по своему выбору, и они будут нормально работать в любом другом языке. Например, написанный на VB .NET элемент можно будет использовать в С#, и наоборот, а благодаря среде Common Language Runtime выбор языка практически не отразится на быстродействии.



    Жизненный цикл объекта

    Итак, при создании экземпляра класса оператором New вызывается соответствующий метод-конструктор New из определения класса (также может быть вызван общий конструктор, если он есть). Версия конструктора выбирается в соответствии с типом переданных параметров. Конструктор можно рассматривать как аналог события Class_Initiall ze в VB6.

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

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

    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 Метод возвращает следующие значения:

  • отрицательное число, если текущий экземпляр меньше заданного объекта;
  • ноль, если текущий экземпляр равен заданному объекту;
  • положительное число, если текущий экземпляр больше заданного объекта.
  • Следующая версия класса Employee реализует интерфейсы lEnumerable и IComparable и сортирует массив по убыванию заработной платы:

    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 обеспечивает

    Clear (метод) Удаляет все элементы из списка
    Contains (ByVal value As Object) As Boolean (метод)

    Метод предназначен для проверки наличия в списке заданного значения. Эффективная реализация этого метода иногда бывает весьма нетривиальной задачей. Если элемент присутствует в списке, метод возвращает True; в противном случае возвращается False

    IndexOf (ByVal value As Object) As Integer (метод)

    Возвращает индекс заданного объекта в списке (программист также должен учитывать эффективность реализации этого метода)

    Insert(ByVal index As Integer, ByVal value As Object) (метод)

    Вставляет объект в заданную позицию списка

    Remove(ByVal value As Object) (метод)

    Удаляет первое вхождение заданного объекта в списке

    Remove(ByVal index As Integer) (метод)

    Удаляет элемент, находящийся в заданной позиции

    Интерфейс IList реализуется классом System.Collections.CollectionBase.

    IDictionary
    Интерфейс IDictionary представляет коллекцию, в которой доступ к данным осуществляется по ключу — как в хэш-таблицах, описанных в предыдущей главе. Более того, класс хэш-таблиц в числе прочих реализует интерфейсы IDictionary, ICollection, Enumerable и ICloneable!

    Хотя интерфейс IDictionary объявляется производным от Enumerable и переход к следующему элементу может осуществляться методом MoveNext, обычно такая возможность не используется — коллекции, реализующие IDictionary, ориентируются в первую очередь на обращение по ключу, а не на последовательный перебор элементов. По этой причине интерфейс IDictionary зависит от интерфейса IDic-tionaryEnumerator, который расширяет Enumerator и дополняет его тремя новыми свойствами:

  • Entry: возвращает пару «ключ/значение» для текущего элемента словаря.
  • Key: возвращает текущий ключ.
  • Value: возвращает ссылку на текущее значение.
  • В .NET Framework входит класс DictionaryBase. Определяя класс производным от DictionaryBase, вы получаете в свое распоряжение всю функциональность интерфейса IDictionary.

    Члены класса IDictionary перечислены в табл. 5.4.

    Поскольку ключи в ассоциативных коллекциях должны быть уникальными, при реали-зации большинства методов необходимо сначала проверить, не был ли заданный ключ использован ранее. Свойство Keys возвращает объект, реализующий ICollection; уникальность ключа проверяется методом,Соп1а1п5 интерфейса ICollection.



    Информация о членах класса Windows.Forms.Form


    Информация о членах класса Windows.Forms.Form
    В программировании, как и в современной науке:
  • Клоном называется точная копия объекта.
  • Состояние клона может измениться и стать отличным от состояния исходного объекта.
  • Но самое важное правило клонирования формулируется так:

  • Изменения в клоне не должны отражаться на исходном объекте, на основе которого клон создавался.
  • Последнее обстоятельство затрудняет клонирование во всех языках ООП, поэтому ме-тод MemberWiseClone считается потенциально опасным. Дело в том, что объект может содержать другие объекты. Если внутренние объекты не будут клонированы одновременно с объектом, их содержащим, вместо пары оригинал-клон вы получите сиамских близнецов, которые будут зависеть друг от друга. Если класс содержит поля, которые представляют собой изменяемые объекты, метод MemberWiseClone заведомо создает «сырой», неполноценный клон (это называется поверхностным копированием). Метод MemberWiseClone успешно клонирует только те объекты, поля которых относятся исключительно к структурным типам.

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

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

    1 Public Class EmbeddedObjects

    2 Private m_Data() As String

    3 Public Sub New(ByVa1 anArray() As String)

    4 m_Data = anArray

    5 End Sub

    6 Public Sub OisplayData()

    7 Dim temp As String

    8 For Each temp In m_Data

    9 Console.WriteLine(temp)

    10 Next

    11 End Sub

    12 Public Sub ChangeData(ByVal newData As String)

    13 m_Data(0) = newData

    14 End Sub

    15 Public Function Clone() As EmbeddedObjects

    16 Return CType(Me.MemberwiseClone. EmbeddedObjects)

    17 End Function

    18 End Class

    Выполните следующую процедуру Sub Main:

    Sub Main()

    Dim anArray() As String ={"HELLO"}

    Dim a As New EmbeddedObjects(anArray)

    Console.WriteLinet"Am going to display the data in object a now!")

    a.DisplayData()

    Dim b As EmbeddedObjects

    b =a.Clone()

    Dim newData As String ="GOODBYE"

    b.ChangeData(newData)

    Console.WriteLine("Am going to display the data in object b now!")

    b.DisplayData()

    Console.WriteLine("Am going to re-display the data in a" & _

    "after making a change to object b!!!") a.DisplayData()
    Console. ReadLine() End Sub



    Интерфейсы

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

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

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

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

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

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

    А теперь подумайте, что произойдет, если:

  • вы не будете связаны обязательством на поддержку метода с заданной сигнатурой в результате реализации интерфейса;
  • ваш класс не входит в иерархию наследования, в которой VB .NET сможет найти метод с нужной сигнатурой.
  • Происходит следующее: в режиме жесткой проверки типов (Option StrictOn) программа вообще не будет компилироваться. Если этот режим отключить, умный компилятор .NET поймет, что вызов метода класса не удастся заменить в откомпилированном коде неким подобием простого вызова функции. Таким образом, компилятору придется сгенерировать значительно больший объем кода. Фактически он должен во время выполнения программы вежливо спросить у объекта, поддерживает ли он метод с указанной сигнатурой, и если поддерживает — не будет ли он возражать против его вызова? Подобное решение обладает двумя характерными особенностями, из-за которых оно работает значительно медленнее и гораздо чаще приводит к возникновению ошибок:

  • Необходимо предусмотреть обработку ошибок на случай непредвиденных ситуаций.
  • Поскольку компилятор на стадии компиляции не может определить, по какому адресу следует передать управление в блоке памяти, занимаемом объектом, ему приходится полагаться на косвенные методы передачи управления на стадии выполнения.
  • Описанный процесс называется поздним связыванием (late binding). Он не только значительно уступает раннему связыванию по скорости, но и вообще не разрешен при включенном режиме Option Strict за исключением позднего связывания, основанного на применении рефлексии.



    Как стать начальником?

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

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

    В нашей системе учета кадров существует только одно приемлемое решение — включить в класс 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 содержит ряд встроенных логических функций, предназначенных для проверки типа объектной переменной:

  • IsArray: функция проверяет, содержит ли объектная переменная массив.
  • IsDate: функция проверяет, можно ли интерпретировать объект как дату и время.
  • IsNumeri с: функция проверяет, можно ли интерпретировать объект как число.
  • Потомки класса Object делятся на две категории: структурные типы, производные от System. Val ueType (базовый класс всех структурных типов), и ссылочные типы, производные непосредственно от 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, обязуется содержать:

  • процедуру с параметром типа Decimal;
  • функцию, которая получает объект Empl oyee и возвращает строку;
  • свойство, доступное для чтения и записи, возвращающее массив объектов Employee;
  • свойство, доступное для чтения и записи, возвращающее значение типа Decimа1.
  • Как будет показано ниже, имена методов реализации несущественны — главное, чтобы методы имели заданную сигнатуру.

    Чтобы реализовать интерфейс в классе, прежде всего убедитесь в том, что он сам или ссылка на него входит в проект. Далее за именем класса и командой 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 ' ЗАПРЕЩЕНО!

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

  • Переменную, объявленную с типом класса, всегда можно присвоить переменной, объявленной с типом любого из интерфейсов, реализуемых классом. В частности, если метод получает в качестве параметра переменную интерфейсного типа, ему можно передать переменную любого из типов, реализующих этот интерфейс (данное правило напоминает основной принцип наследования, в соответствии с которым производные типы всегда могут использоваться вместо базовых). Запомните следующее правило:
  • При переходе от переменной типа интерфейса к переменной типа, реализующего интерфейс, необходимо использовать функцию СТуре.
  • Чтобы определить, реализует ли объект некоторый интерфейс, воспользуйтесь ключевым словом TypeOf в сочетании с Is. Пример:

    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 четко показывает, что метод базового класса должен переопределяться в производном классе. Для этого используются два специальных ключевых слова.

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

    Ниже приведен примерный вид базового класса 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

    Подведем итог:

  • Переопределение допускается только для членов базовых классов, объявленных с ключевым словом Overridable.
  • Если на некоторой стадии построения иерархии классов потребуется запретить дальнейшее переопределение метода в производных классах, этот метод помечается ключевым словом NotOverridable.
  • Ключевое слово VB .NET Notlnheritable полностью запрещает наследование от класса. Как правило, наследование запрещается для классов, выполняющих очень важные функции, которые ни в коем случае, на должны изменяться. Многие классы .NET Framework (такие, как String) помечены ключевым словом Notlnheritable именно по этой причине. Впрочем, если требуется запретить переопределение лишь одного члена класса, незачем запрещать наследование для всего класса; достаточно пометить ключевым словом NotOverridable нужный член класса.

    По умолчанию переопределение членов классов запрещается (см. описание ключевого слова 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 обладает рядом ограничений:

  • многократное повторение MyBase не позволяет получить доступ к «дедушкам» и «прадедушкам» класса в иерархии наследования; запись вида MyBase.MyBase. MemberFunction недопустима.
  • MyBase является ключевым словом языка. В отличие от Me, MyBase нельзя использовать с оператором Is, присваивать объектным переменным или передавать процедурам в качестве параметра.
  • С MyBase тесно связано другое ключевое слово — MyClass. Оно гарантирует, что даже в случае переопределения будет вызван метод, определенный в текущем классе, а не какая-то из его переопределенных версий в производных классах. На ключевое слово MyCl ass распространяются те же ограничения, что и на ключевое слово MyBase, о котором упоминалось в предыдущей главе.

  • MyBase — ключевое слово, а не реальный объект. Следовательно, MyClass, как и MyBase, нельзя использовать с оператором Is, присваивать объектным переменным или передавать процедурам в качестве параметра (для ссылки на конкретный экземпляр используется ключевое слово Me).
  • Ключевое слово MyClass не позволяет получить доступ к закрытым (Private) членам класса, тогда как Me предоставляет такую возможность.
  • На практике ключевое слово MyClass приносит наибольшую пользу в тех случаях, ког-да мы хотим указать на модификацию поведения класса. Замена его на Me не дает нужного эффекта, поскольку ключевое слово Me означает «текущий экземпляр, код которого выполняется в настоящий момент», и попытки применения его в другом контексте лишь сбивают с толку.





    Полиморфизм на практике

    Наследование часто помогает избавиться от громоздких конструкций 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


    Сортировка по нестандартному критерию с использованием 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,

    Сортировка по нестандартному критерию с использованием IComparable Интерфейс IComparer

    Рисунок 5.10. Сортировка по нескольким критериям с использованием IComparer

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




    Члены интерфейса

    Интерфейс ICollection реализуется классом System.Collections.CollectionBase.



    Важнейшие интерфейсы .NET Framework

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

  • ICloneable: в классе реализуется метод Clone, обеспечивающий глубокое копирование.
  • 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, которой при вызове передаются два параметра:

  • имя события в классе-источнике;
  • адрес метода (процедуры событий) класса-приемника, вызываемого при возникновении события.
  • Код AddHandl ег включается в класс-приемник, а не в класс-источник. Адрес метода, вызываемого при возникновении события, определяется оператором AddressOf. При вызове AddressOf передается имя метода объекта класса-приемника. Например, следующая команда устанавливает динамический обработчик события для объекта

    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. Обратите особое внимание на некоторые особенности этого объявления:

  • Класс источника должен быть указан явно, объявления вида As Object недопустимы.
  • Объявление располагается на уровне модуля или класса и не содержит ключевого слова New.
  • После включения этой строки в программу объектная переменная anEmpl oyee может использоваться всюду, где вас интересует событие SalarySecurityEvent. Как показано на Рисунок 6.2, IDE автоматически создает обработчик события с именем, построенным по схеме А_В, для каждой объектной переменной, объявленной с ключевым словом Wi thEvents. Чтобы вызвать автоматически сгенерированный «скелет» события, достаточно выбрать его в раскрывающемся списке, как на Рисунок 6.2.



    Построение классов событий

    В предыдущем примере мы воспользовались готовым классом 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 через механизм обратного вызова. В частности, это позволит динамически изменять критерий сортировки во время работы программы.

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

  • Начать с первого элемента.
  • Последовательно просмотреть все остальные элементы. Если очередной элемент окажется меньше текущего первого элемента, поменять их местами.
  • Начать со второго элемента, просмотреть все остальные элементы.
  • Продолжать до последнего элемента. Основной код волновой сортировки выглядит так:
  • For i =bottom To (top - bottom) For j =i + 1 To top

    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 существует практически общепринятое правило, согласно которому функции приемника передаются два параметра:

  • Объектная переменная, содержащая ссылку на объект-источник события.
  • Объект события (класса, производного от System.EventArgs), содержащий информацию о событии (разные классы, производные от System.Event.Args, обладают разными свойствами, ориентированными на разные обработчики событий).
  • Пример приводился ранее в главе 1. При размещении кнопки на форме генерировалась процедура события Click:

    Private Sub Buttonl_Click(ByVal sender As System.Object.

    ByValeAs System.EventArgs) Handles Button1.Click
    End Sub

    Параметры имеют следующий смысл:

  • Объектная переменная sender содержит ссылку на объект, то есть кнопку, нажатую пользователем. Следовательно, процедура события располагает информацией об источнике события.
  • Объектная переменная е содержит объект события, который (по крайней мере теоретически) содержит дополнительную информацию о событии.
  • Традиционно в VB источник (отправитель) события не идентифицировался в процедуре события. Единственным исключением были массивы управляющих элементов, когда конкретный элемент-отправитель выделялся из массива при помощи параметра-индекса. Смысл дополнительной объектной переменной sender в обобщенной процедуре события VB .NET становится очевидным, если вспомнить, что одна процедура может обрабатывать несколько событий, поступающих от разных объектов. Попробуйте вызвать встроенный метод ToString в приведенной выше процедуре события: MsgBox(sender.ToString) Результат будет выглядеть так:

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

    В данном примере объект события е не представляет интереса, поскольку он не содержит сколько-нибудь полезной информации о событии. С другой стороны, в некоторых ситуациях он может пригодиться. Например, из объектной переменной класса 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, как это сделано во второй выделенной строке.

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

  • Добавьте в исключение содержательное сообщение и инициируйте его заново. Возможно, новая информация окажется полезной.
  • Инициируйте исключение одного из стандартных типов, производных от типа текущего исключения, чтобы оно лучше описывало ситуацию.
  • Создайте новый класс исключения, производный от типа текущего исключения, который будет описывать ситуацию лучше, чем любой из стандартных классов.
  • Решения расположены по возрастанию приоритета, и в идеальном случае следует всегда использовать пункт 3. На практике программисты при выборе руководствуются своей оценкой того, какую информацию об исключении необходимо передать для дальнейшей обработки.

    Для примера представьте такую ситуацию: из источника данных читаются пары «ключ/значение», и для последнего ключа не находится парного значения. Программа предполагает, что значение ассоциируется с каждым ключом, поэтому при попытке чтения возникает неожиданно'е исключение ввода-вывода (чтение данных из файла описано в главе 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 имя_файла

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

  • забыть о вводе имени файла;
  • ввести имя несуществующего файла;
  • ввести имя файла, недоступного для выполнения данной операции.
  • Программа должна быть написана так, чтобы учитывать все возможные ошибки со стороны пользователя. Ниже приведен пример простого блока Try-Catch, который может входить в приложение 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, по крайней мере, утомительно. Хотя в некоторых ситуациях возвращаемое значение приходится проверять независимо от выбранной схемы обработки ошибок, не стоит превращать это в постоянную практику. Также следует учитывать фактор эффективности: структурная обработка исключений быстрее программируется, отнимает меньше времени при сопровождении, а нередко и выполняется быстрее!



    Рекомендации по использованию исключений

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

  • Исключение является признаком аварийной ситуации; не используйте исключения для простой передачи информации (мы видели программу, в которой при успешном завершении функции инициировалось исключение SUCCESS_EXCEPTION).
  • Не заменяйте тривиальные проверки обработкой исключений. Например, исключения не стоит применять для проверки достижения конца файла (EOF).
  • Избегайте раздробленной обработки исключений, при которой едва ли не каждая команда заключается в отдельный блок Try-Catch. Заключение всей операции в один блок Try-Catch обычно предпочтительнее использования нескольких блоков.
  • Не поглощайте исключения конструкциями вида Catch e As Excepti on с пустым блоком команд, если для этого нет достаточно веских причин. Такая конструкция эквивалентна бездумному применению On Error Resume в старых программах VB, и пользоваться ею нежелательно по тем же причинам. Если в программе произошло исключение, обработайте его или передайте для дальнейшей обработки.
  • Последнюю рекомендацию скорее можно назвать «правилом хорошего тона». Передавая исключение во внешний код для последующей обработки, добавьте в него новую информацию (или определите новый класс исключений), чтобы внешний код мог точно определить, что произошло и какие меры были приняты для того, чтобы исправить ситуацию.



  • Секция 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


    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, мы кратко опишем процесс включения нового элемента в окно формы.

  • Дважды щелкните на элементе или перетащите его с панели элементов на форму.
  • Расположите элемент в нужной позиции (щелкните внутри элемента и перетащите его мышью).
  • Измените размеры элемента при помощи маленьких квадратных маркеров, показанных на Рисунок 8.1 (при необходимости можно выполнить более точную настройку размеров при помощи комбинации Shift+клавиши со стрелками).


  • Добавление новых событий


    Добавление новых событий
    Добавить поддержку нового события в элемент ничуть не сложнее, чем включить обработчик события в класс (эта тема рассматривалась в главе 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
    Public Class _

    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 для имени свойства:

    Public Property MinValue

    Примерный вид окна свойств при установке атрибута Browsable для свойств MinValue и MaxValue показан на Рисунок 8.18.



    FileDialog

    Абстрактный класс FileDialog является базовым для двух специализированных подклассов:

  • OpenFileDialog;
  • SaveFileDialog.
  • Рассмотрим использование этих диалоговых окон на простом примере. Элемент RichTextbox .NET, как и его аналоги из предыдущих версий VB, поддерживает методы LoadFilе и SaveFile для быстрого открытия и сохранения файлов. Чтобы пример стал более реалистичным, свойство Filter диалогового окна будет ограничивать вывод файлов .txt и .rtf (в следующем фрагменте эта строка выделяется жирным шрифтом). Также обратите внимание на то, как при вызове метода LoadFi 1е указывается текстовый формат файла (при загрузке файлов RTF второй параметр указывать не обязательно):

    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
    Изменение порядка перебора элементов (tab order) в прежних версиях VB было делом утомительным и неприятным, которое несколько упрощалось только специальной надстройкой (add-in). В VB .NET существует команда View > Tab Order, которая делает эту задачу элементарной. Все, что от вас потребуется, — ввести нужную позицию элемента в небольшом поле, которое при выполнении команды View > Tab Order появляется рядом с элементом (Рисунок 8.7). Для элементов, находящихся внутри контейнера, позиция задается в формате «х.у». Например, если групповому полю в порядке перебора была присвоена позиция 3, то расположенные внутри него элементы будут иметь номера 3.0, 3.1 и т. д. (чтобы отключить режим ввода порядка перебора, снова выполните команду Tab Order).

    Меню Tab Order

    Рисунок 8.7. Команда Tab Order




    Многостраничный вывод

    Процесс многостраничной печати основан на небольшой хитрости: если процедура обработки события Pri ntPage задает свойству HasMorePages объекта Pri ntPageEventArgs значение True, то объект PrintDocument узнает о наличии дополнительных страниц для печати и автоматически инициирует заново событие PagePri n't.

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

  • Прочитать строку из поля или из файла.
  • Не превышает ли длина строки предельное допустимое значение? Если превышает, разбить на несколько фрагментов.
  • Помещаются ли эти фрагменты на текущей странице?
  • Если помещаются, напечатать их функцией DrawString начиная с текущей позиции. Если не помещаются, напечатать сколько удастся, начать новую страницу и вывести на ней остальные фрагменты.
  • Повторить процесс до тех пор, пока не будут обработаны все строки текстового поля или файла.
  • Однако проверка возможности размещения новой строки на странице не имеет ничего общего с печатью; в ней используются различные метрики, вычисляемые на основании ширины и высоты текстовой строки. В свою очередь, эти параметры зависят от семейства и размера используемого шрифта. К счастью, вы можете положиться на такие методы, как MeasureStri ng; эти методы используют метрики, связанные с текущим контекстом устройства.



    Начало построения меню в редакторе


    Начало построения меню в редакторе
    В прежних версиях 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-свойства, при помощи которых можно получить информацию о размерах страницы. Значения обоих свойств задаются в сотых долях дюйма.

  • PageBounds: возвращает размеры прямоугольной области всей страницы.
  • MarginBounds: возвращает размеры прямоугольной области, ограниченной полями.
  • В свойстве PageSettings объекта PrintPageEventArgs хранится дополнительная информация. В табл. 8.2 перечислены важнейшие свойства класса PageSetti ngs (большинство принтеров позволяет читать эти свойства, но не все принтеры поддерживают запись).



    Печать

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

    Чтобы обойти некоторые ограничения, действующие в GDI+, мы будем предполагать, что изображение задается свойством Image, а не прямым копированием в графическое поле.
    Печать в конечном счете сводится к выводу информации в графическом контексте, но вместо экранного контекста используется контекст, ассоциированный с принтером или окном предварительного просмотра печати.
    Как при выводе на принтер, так и при использовании поддержки предварительного просмотра (Print Preview) в .NET работа всегда начинается с создания объекта класса System. Drawl ng. Pri nti ng. Pri ntDocument. Для получения этого объекта можно применить один из следующих способов:

  • Воспользоваться элементом Pri ntDocument на панели элементов и положиться на автоматически сгенерированный код или воспользоваться оператором New в конструкции вида
    Dim aPrintDocument As New PrintDocument()
  • Присвоить значение свойства Document экземпляра класса объекту, объявленному с типом Pri ntDocument.
    При использовании панели элементов на форме размещается элемент Pri ntDocument, не обладающий визуальным интерфейсом. При этом генерируется фрагмент следующего вида:
    Friend WithEvents PrintDocumentl As System.Drawing.Printing.PrintDocument

    Непосредственное создание экземпляра происходит в следующей строке, включенной в процедуру
    InitializeComponent: Me.PrintDocumentl = New
    System.Drawing.Printing.PrintDocument()
  • Объявление объекта PrintDocument с ключевым словом WithEvents играет важней^ шую роль для понимания автоматически сгенерированного кода печати. Дело в том, что при вызове метода Print для экземпляра класса Pri ntDocument .NET инициирует по крайней мере три события:

  • BeginPrint;
  • PrintPage (при печати нескольких страниц может инициироваться многократно);
  • EndPrint.
  • Минимальная поддержка печати в программе требует программирования как минимум со бытия PrintPage.

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

    Во втором параметре события Pri ntPage передается объект PagePri ntEventArgs. В этом объекте хранится много полезных данных, в том числе:

  • Графический объект, определяемый значением свойства Graphi cs. С этим объектом выполняются все операции вывода, и его содержимое будет в итоге напечатано на принтере.
  • Объект PageSetti ngs содержит инструкции, относящиеся к печати страниц. Среди свойств этого объекта — признак печати в альбомной (landscape) ориентации, разрешение принтера, размеры полей и т. д.
  • В следующем простом примере при нажатии кнопки вызывается метод Print класса PrintDocument:

    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 и поместите элемент на панель элементов:

  • Выполните команду Tools > Customize Toolbox (или нажмите Ctrl+T).
  • Перейдите на вкладку .NET Framework Components.
  • Нажмите кнопку Browse и выберите DLL нужнопа^лемента (библиотека находится в подкаталоге \bin основного каталога решения).
  • Элемент помещается на вкладку .NET Framework Components (Рисунок 8.16).



    Построение нестандартных элементов на базе наследования

    Наследование применяется при построении не только новых форм, но и новых элементов. Допустим, вы хотите создать текстовое поле, предназначенное для ввода только целых чисел. Как обычно, все начинается с объявления нового класса:

    Public Class PositivelntegerTextBox

    Inherits System . Windows . Forms . TextBox

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

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

    Итак, создайте новую библиотеку классов и включите в решение ссылку на сборку Windows.Forms.dll.

    Мы начинаем работу с библиотеки классов, а не с проекта типа User Control, потому что он лучше подходит для элементов, написанных «на пустом месте». Если вы захотите построить элемент, содержащий несколько других элементов, выберите в диалоговом окне New Project тип Windows Controls Library — в вашем распоряжении окажется контейнер, предназначенный для построения сложного элемента посредством включения.



    вывод всех шрифтов в системе

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



    Простейшее Windows-приложение


    Простейшее 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 показано, как выглядит новый шрифт на кнопке.

    Простейшее Windows-приложение

    Рисунок 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 пикселов Вывод текста


    Результат вызова 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

    Результат вызова DrawRectangle: прямоугольник в рамке толщиной 6 пикселов Вывод текста

    Рисунок 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.

  • KeyPress: происходит при нажатии клавиши, когда элемент обладает фокусом.
  • KeyDown: происходит при переходе клавиши в нажатое состояние, когда элемент обладает фокусом.
  • Key Up: происходит при отпускании клавиши, когда элемент обладает фокусом.
  • По аналогии с VB6 событие KeyDown инициируется раньше события KeyPress, которое, в свою очередь, предшествует KeyUp. Событие KeyPress использует объект KeyPressEventArgs, в свойстве KeyChar которого передается информация о нажатой клавише. Например, следующий фрагмент выводит окно сообщения в том случае, если введенный символ не является цифрой:

    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), которое отдельные подразделения будут дополнять своими данными. Базовая форма создается следующим образом:

  • Выполните команду File > New > Project.
  • Выберите тип приложения Windows Application, введите в поле Name строку SplashScreeriBase и нажмите кнопку ОК.
  • Предположим, вы хотите преобразовать стандартное приложение Windows в библиотеку классов, чтобы откомпилировать его в DLL вместо ЕХЕ-файла. Проще всего это делается так:

  • Щелкните правой кнопкой мыши в строке SplashScreenBase окна решения и выберите в контекстном меню команду Properties.
  • Выберите в раскрывающемся списке Output Type строку Class Library (вместо Windows Application). Нажмите кнопку ОК.
  • Сконструируйте форму, разместите на ней нужные элементы, реализуйте свойства, методы и события.
  • Откомпилируйте программу.
  • После построения библиотеки DLL остается лишь включить ссылку на нее в решение, после чего классы DLL используются в программе наравне с остальными классами. При выполнении команды Project > Add Inherited Form можно поручить IDE включить в проект весь необходимый код, для чего достаточно ответить на несколько вопросов в диалоговых окнах. С другой стороны, возня с диалоговыми окнами выглядит немного глупо, поскольку после включения ссылки на DLL в проект остается лишь привести первую строку приложения к следующему виду:

    Public Class Form1

    Inherits SplashScreenBase. Form1

    Все остальное за вас сделает механизм наследования! Этот способ особенно хорош тем, что работа дизайнера автоматически синхронизируется с унаследованной формой.



    Средства базового класса Control


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




    Свойства MaxValue и MinValue


    Свойства 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 и Directorylnfo.
  • Классы File и Filelnfo.
  • Обращение к функциональным возможностям классов Directory и File происходит при помощи общих методов. Поскольку методы классов Di rectory и Fi1е являются общими, они могут вызываться и без предварительного создания экземпляра оператором New. Конечно, это повышает их эффективность при разовых обращениях к конкретному файлу или каталогу. Тем не менее при многократном обращении к файлу или каталогу эти методы становятся менее эффективными. Классы Di rectorylnfo и Filelnfo содержат обычные методы, поэтому обращение к их членам происходит через конкретные экземпляры.

    Другое различие между этими парами заключается в том, что классы 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 Применение сериализации при клонировании объектов


    Объект, сохраненный в формате 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 должна предоставить свои версии следующих методов:

  • Read — метод чтения данных из потока. Иногда сводится к простейшему чтению одного байта, но во многих производных классах используются сложные методы для чтения данных большими порциями.
  • Write — метод записи данных в поток. Как и предыдущий метод, может сводиться к простейшей записи одного байта, но может задействовать и фрагменты данных большего размера.
  • Впрочем, этим возможности не ограничиваются. Кроме простого перемещения от первого байта к последнему реализация класса Stream может поддерживать и другие способы — например, перемещение в обратном направлении или непосредственный переход к заданной позиции в потоке. Такое возможно для файловых потоков, но не имеет смысла (а следовательно, и не реализуется) для потоков, основанных на сетевых соединениях. Свойство CanSeek позволяет узнать, поддерживает ли поток произвольный доступ. Если свойство равно True, значит, в производном классе поддерживаются реализации методов Seek и SetLength, а также свойств Position и Length.

    В реализации метода 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

    Public Class Employee

    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

    Public Class Manager

    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, это сэкономит немало времени на вводе имен. В типичной ситуации поддержка сериализации включается простым добавлением атрибута в заголовок класса:

    Public Class Employee

    Атрибут должен быть установлен и во всех классах, производных от вашего класса, а также во всех вложенных классах, объекты которых содержатся в исходном классе. В противном случае рекурсивный процесс будет нарушен с исключением System.Runtime.Serialization.Serial izationException.

    В .NET Framework сериализация поддерживается в классах, реализующих интерфейс ISerializable.

    После пометки класса атрибутом следует решить, в каком формате должен сохраняться объект — в XML-формате SOAP или в более компактном двоичном формате. Используемый по умолчанию двоичный формат доступен всегда. Чтобы воспользоваться форматом SOAP, необходимо добавить ссылку на сборку System. Runti me. Sena1izati on. Formatters. Soap.

    Следующий пример показывает, как организовать сериализацию для массива. Массив ArrayList является объектом и может содержать другие объекты (в нашем примере это объекты иерархии Employee). Поскольку динамические массивы сери-ализуются автоматически, остается лишь пометить атрибутом различные классы иерархии Empl oyee. Вся содержательная работа выполняется в двух выделенных строках:

    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 и возвращает текущие сведения о количестве проданных экземпляров нашей книги. Обобщенный алгоритм выглядит следующим образом:

  • Создать объект URI (Universal Resource Locator) передачей строкового параметра конструктору класса URI.
  • Передать объект URI методу Create класса HttpWebRequest, чтобы инициировать выдачу запроса HTTP.
  • Вызвать метод GetResponse класса HttpWebRequest и получить поток.
  • Проанализировать полученный поток, содержащий HTML-код, и извлечь из него нужную информацию, для чего необходимо знать структуру страницы. Кстати, это одна из причин, по которым для получения данных удобнее использовать web-службы: если Amazon неожиданно сменит структуру своих страниц, наше приложение перестанет работать.
  • В данном случае страница генерируется следующей строкой запроса, которая и будет использована для создания объекта URI (в конце строки приведен номер ISBN нашей книги):

    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:

  • Sub New(String, FileMode): создает объект FileStream с заданным именем и в заданном режиме (см. ниже описание FileMode).
  • Sub NewCString, FileMode, FileAccess): создает объект FileStream в заданном режиме, с заданными правами чтения/записи и совместного доступа.
  • Допустимыми значениями перечисляемого типа FileAccesS являются Read, Write и ReadWri te. Основные значения перечисляемого типа Fi I eMode перечислены в табл. 9.9. Учтите, что некоторые из них требуют особых привилегий для операций с файлами.



    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 содержит и другие методы, прерывающие нормальное функционирование потоков:

  • Suspend;
  • Abort.
  • Трудно сказать, зачем в .NET была включена поддержка этих методов — при вызове Suspend и Abort программа, скорее всего, начнет работать нестабильно. Ни один из методов не позволяет нормально провести деинициализацию потока. Кроме того, при вызове Suspend или Abort невозможно предсказать, в каком состоянии поток оставит объекты после приостановки или аварийного завершения.

    В результате вызова 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 сможет их использовать). Поскольку данные являются общими, доступ к ним необходимо синхронизировать. Также необходимо предусмотреть средства для оповещения ожидающих потоков о появлении готовых данных.

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

  • Поток 1 (потребитель) активизируется, входите синхронизированный метод, ищет данные, не находит их и переходит в состояние ожидания. Предварителъно он должен снять блокировку, чтобы не мешать работе потока- поставщика.
  • Поток 2 (поставщик) входит в синхронизированный метод, освобожденный потоком 1, создает данные для потока 1 и каким-то образом оповещает поток 1 о наличии данных. Затем он снимает блокировку, чтобы поток 1 смог обработать новые данные.
  • Не пытайтесь решить эту проблему постоянной активизацией потока 1 с проверкой состояния условной переменной, значение которой>устанавливается потоком 2. Такое решение серьезно повлияет на быстродействие вашей программы, поскольку в большинстве случаев поток 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 и входит во фрагмент программы, защищенный этим объектом. К сожалению, для работы ему необходим доступ к коду, защищенному другим блоком Sync Lock с другим объектом синхронизации. Но прежде, чем он успевает войти во фрагмент, синхронизируемый другим объектом, в него входит поток В и захватывает этот объект. Теперь поток А не может войти во второй фрагмент, поток В не может войти в первый фрагмент, и оба потока обречены на бесконечное ожидание. Ни один поток не может продолжить работу, поскольку необходимый для этого объект так и не будет освобожден.

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

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

    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.

  • Строки 26 и 27 просто выводят на консоль отладочную информацию. В основном коде метода (строки 28-36) доступ к вилке синхронизируется по объектной переменной Me. Поскольку в нашей программе используется только одна вилка, синхронизация по Me гарантирует, что два потока не смогут одновременно захватить ее. Команда Slee'p (в блоке, начинающемся в строке 34) имитирует задержку между захватом вилки/ножа и началом еды. Учтите, что команда Sleep не снимает блокировку с объектов и лишь ускоряет возникновение взаимной блокировки!
    Однако наибольший интерес представляет код класса Programmer (строки 61-108). В строках 67-70 определяется общий конструктор, что гарантирует наличие в программе только одной вилки и ножа. Код свойств (строки 74-94) прост и не требует комментариев. Самое главное происходит в методе Eat, выполняемом двумя отдельными потоками. Процесс продолжается в цикле до тех пор, пока какой-либо поток не захватит вилку вместе с ножом. В строках 98-102 объект случайным образом захватывает вилку/нож, используя вызов Rnd, — именно это и порождает взаимную блокировку. Происходит следующее:
    Поток, выполняющий метод Eat объекта Тот, активизируется и входит в цикл. Он захватывает нож и переходит в состояние ожидания.
  • Поток, выполняющий метод Eat объекта Bob, активизируется и входит в цикл. Он не может захватить нож, но захватывает вилку и переходит в состояние ожидания.
  • Поток, выполняющий метод Eat объекта Тот, активизируется и входит в цикл. Он пытается захватить вилку, однако вилка уже захвачена объектом Bob; поток переходит в состояние ожидания.
  • Поток, выполняющий метод Eat объекта Bob, активизируется и входит в цикл. Он пытается захватить нож, однако нож уже захвачен объектом Тот; поток переходит в состояние ожидания.
  • Все это продолжается до бесконечности — перед нами типичная ситуация взаимной блокировки (попробуйте запустить программу, и вы убедитесь в том, что поесть так никому и не удается).
    О возникновении взаимной блокировки можно узнать и в окне потоков. Запустите программу и прервите ее клавишами 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:

  • проектировалась как модель с полиостью автономной архитектурой (хотя классы DataAdapter, Connection, Command и DataReader остаются ориентированными на соединение);.
  • не поддерживает курсоры на стороне сервера. Динамические курсоры ADO в ней не поддерживаются;
  • базируется на языке XML [ Во внутреннем представлении классов ADO .NET используется оптимизированный формат, но весь обмен данными происходит в формате XML. ] (что позволяет работать через Интернет, даже если клиент находится за. брандмауэром (firewall));
  • входит в сборку .NET System. Data. DLL, а не реализуется на уровне языка;
  • вряд ли будет поддерживать старые клиенты Windows 95.
  • Еще одна интересная особенность ADO .NET заключается в том, что для таких важных средств, как двухфазная актуализация данных (commit), потребуется использовать Enterprise Services (то есть фактически COM+/MTS с .NET-оболочкой).



    Пространство имен System.Data.OleDb

    Пространство имен System.Data.OleDb содержит классы, используемые при взаимодействии с OLE DB-совместимыми базами данных (такими, как Microsoft Access или Microsoft Fox Pro). Обычно в программах используются классы OleDbConnectl on, OleDbCommand и OleDbDataReader этого пространства имен. Ниже приведены краткие описания этих важных классов.

  • Класс OleDbConnection: можно считать, что этот класс представляет соединение с источником данных OLE DC и содержит свойства, необходимые для подключения к базе данных (данные провайдера OLE DB, имя пользователя и пароль). После соединения в экземпляре класса сохраняются дополнительные метаданные о базе данных.
  • Класс OleDbCommand: класс представляет команды SQL, применяемые к базе данных OLE DB. Вместе с командой хранятся все параметры и дополнительная информация, необходимая для обработки запроса.
  • Класс OleDbDataReader: используется после получения данных от источника при помощи двух классов, описанных выше. Является специализированной формой класса потока ввода (см. главу 9) и умеет только читать данные, возвращаемые объектом OleDbCommand. Аналогом объекта DataReader в ADO является набор записей, хранящийся на сервере, доступный только для чтения и поддерживающий только перебор в прямом направлении.
  • Ниже приведен пример использования этих трех классов. Наше приложение подключается к базе данных Northwind, входящей в поставку Access и современных версий SQL Server.

    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


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





    Результаты выполнения простого запроса SQL


    Результаты выполнения простого запроса 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-службы
    При создании 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">










    style="height:67px;width:123px;Z-INDEX: 101: LEFT :311px:

    POSITION:absolute;TOP:212px"/>








    Из выделенного фрагмента видно, что в сгенерированный для клиентской стороны HTML-код включена форма HTML с атрибутом post и скрытым полем _VIEWSTATE. Эти строки позволяют ASP .NET сохранять информацию состояния, не требуя специальных действий со стороны клиента. Скрытое поле _VIEWSTATE содержит зашифрованные данные, по которым ASP .NET может восстановить страницу и ее состояние. Механизм шифрования представляет собой особую разновидность сериализации; чтобы отключить сохранение состояния, задайте свойству ЕпаЫе-ViewState значение False.

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




    value="dDwxMDA3MzE2MzEy03Q802w8aTwxPjs+
    02w8dDw7bDxpPDM+Oz47bDxOPHA,

    8cDxsPEZvbnRfU216ZTtUZXh0018hUOI7PjtsPFN5
    c3RlbS5XZWIuVUkuV2Vi029udHJvbH

    MuRm9udFVuaXQsIFN5c3RlbS5XZWIsIFZlcnNpb
    249MS4wLjIOMTEuMCwgQ3VsdHVyZT

    luZXVOcmFsLCBQdWJsaWNLZXlUb2tlbjliMDNmN
    WY3ZjExZDUwYTNhPFgtT6FyZ2U+0

    ldlbGNvbWUgdG8gQVNQLk5FVCBAOC8xOS8yMDAx
    IDEyOjM20jAwIFBN02k8MTQwOT

    47Pj47Pjs7Pjs+Pjs+Pjs+"/>

    Хотя базовый файл .aspx, указанный в атрибуте тега , остался прежним, скрытая переменная _VIEWSTATE изменилась весьма основательно. Фактически в этом поле сохранено полное состояние элементов HTML и данных формы. Одно из преимуществ этого подхода заключается в том, что программист избавляется от хлопот с сохранением состояния. Он просто задает свойству EnableViewState значение True и переходит к программированию логики страницы. С другой стороны, с каждой страницей приходится пересылать дополнительные данные. В большинстве случаев их объем относительно невелик, поэтому преимущества компенсируют недостатки.

    Элементы ввода (тег ) на стороне клиента всегда запоминают свое состояние даже после пересылки данных на сервер и обратно. ASP .NET не позволяет отключать автоматическое сохранение состояния для элементов ввода.

    Наконец, у каждой страницы .aspx имеется свойство Sessi on, возвращающее объект с информацией о текущей странице. Объект Session можно рассматривать как своего рода «cynepcookie» с возможностью сохранения данных по ключу (как в хэш-таблицах). Пример:

    Session("user name") = TextBox1.Text

    Содержимое текстового поля TextBoxl сохраняется в объекте Session, а при обращениях к -нему используется строка "user name".

    Нехватка места не позволяет развивать эту тему, однако мы хотим обратить внимание читателя на одно обстоятельство. В особо сложных ситуациях, когда сохранение всех данных в объекте Session, находящемся в памяти сервера, нежелательно, информация сохраняется в базе данных SQL Server. Более того, если сайт обслуживается web-комплексом, вы даже можете указать, на каком сервере должна храниться эта информация! Эти две особенности значительно улучшают масштабируемость приложений ASP .NET.



    Файл Web.config

    В ранних версиях Windows конфигурационные данные хранились в ini-файлах, на смену которым пришел глобальный реестр. В ASP .NET приложения фактически возвращаются к временам текстовых ini-файлов — конфигурационные данные хранятся в текстовом файле Web.config, находящемся в каталоге приложения. Этот файл определяет условия выполнения приложений ASP .NET — такие как параметры отладки и системы безопасности. Конфигурационные файлы имеют довольно сложную структуру. Ниже приведено содержимое файла Web.config для предыдущего примера:

    1

    2

    3

    4

    11 compilation defaultl_anguage="vb" debug="true"/>

    12

    17

    18

    22

    23

    29

    30

    31

    36


    37

    44
    45 pageOutput = "false" traceMode = "SortByTime"

    46 localOnly- "true" />

    47 (--SESSION STATE SETTINGS

    48 By default ASP .NET uses cookies to identify which requests belong to a

    49 particular session.If cookies are not available.a session can be

    50 tracked by adding a session identifier session.To disable cookies.set

    51 sessionState cookieless = "true"'

    52 -->

    53
    54 mode="InProc"

    55 stateConnectionString - "tcpip=127.0.0.1:42424"

    56 sqlConnectionSthng = "data source=127.0.0.1:user id=sa:password="

    57 cookieless="false"

    58 timeout="20"

    59 />

    60

    68

    69
    70 type= "System.Web.HttpNotFoundHandler.System.Web"/>

    71
    72 type= "System.Web.HttpNotFoundHandler.System.Web"/>

    73
    74 type= "System.Web.HttpNotFoundHandler.System.Web"/>

    75
    76 type= "System.Web.HttpNotFoundHandler.System.Web"/>

    77
    78 type= "System.Web.HttpNotFoundHandler.System.Web"/>

    79


    80

    83

    84


    85


    Из строки 1 видно, что страницы ASP .NET, как и многие объекты .NET, строятся на базе XML. В строках 2-11 содержится часть конфигурационного файла, которая управляет настройками компиляции и определяет режим использования страницы — тестирование/разработка или нормальная работа. В этой части файла также указываются языки, использованные в странице (как видно из строки И, в приведенном примере это VB).

    При переводе приложения в рабочий режим установите в файле Web.config параметр debug=false. Отладочный режим играет важную роль в процессе разработки, но на стадии использования он может серьезно замедлить работу приложения ASP .NET — в некоторых случаях на порядок и даже более.

    Следующий раздел относится к проверке привилегий пользователя (строки 18-36). В приведенном листинге параметрам аутентификации присвоено значение «*»; это означает, что работа с приложением разрешена любому пользователю. Как видно из автоматически сгенерированных комментариев к этому разделу, вы также можете разрешить или запретить доступ к приложению при помощи ключевых слов allow и deny.

    Хорошая схема аутентификации занимает важное место в программировании распределенных приложений. Каким образом страница узнает, кем является ее пользователь? В ASP .NET поддерживаются три разновидности схем аутентификации, перечисленные в табл. 12.1.

    Использование web-службы на стороне клиента

    Если щелкнуть на ссылке GetWeather на Рисунок 12.4, в броузере загружается страница, показанная на Рисунок 12.5. На странице приведен код, который может использоваться для обращения к web-службе. В разделе «SOAP» описывается доступ к службе через протокол SOAP, основанный на XML. Этот протокол отличается наибольшей гибкостью, но простым его не назовешь. В простейшем варианте обращения к web-службе используется запрос HTTP GET. Прототип выглядит следующим образом:

    /WebServicel/Servicel.asmx/
    GetWeather?city=string НTTР/1.1
    Host:Local Host

    Проект web-службы в IDE


    Проект web-службы в IDE
    Дважды щелкните на дизайнере и обратите внимание на автоматически сгенерированный код:

    Public Class Servicel

    Inherits System.Web.Services.WebService

    Класс System. Web. Services. WebServi се является базовым для всех web-служб .NET. Благодаря наследованию в вашем распоряжении оказываются все возможности этого класса, в том числе и свойство Context для получения запроса HTTP, использованного для обращения к странице через Web.

    Включите следующий фрагмент перед командой End Class:

    Public Function GetWeather(ByVal City As String)
    As String Select Case City

    ' Получить информацию о погоде в Сиэттле
    Case "Seattle"

    Return "The current temperature is 64 degrees. " & _

    "and raining of course."
    Case Else

    Return "Can't find data for " & City & "."
    End Select
    End Function

    При нажатии клавиши F5 VS .NET IDE автоматически генерирует web-страницу наподобие показанной на Рисунок 12.4. На этой странице приводится общая информация о web-службе.

    Простая web-служба

    Как упоминалось в главе 9, непосредственное извлечение информации из web-страниц — процесс медленный (из-за необходимости анализировать всю страницу) и ненадежный, поскольку структура страницы может измениться. Удобным средством получения таких данных с web-сайта является web-служба (Web service).

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

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

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

    Для простоты в нашем примере возвращаемые данные жестко кодируются в функции GetWeather.

    Создайте новый проект web-службы, для чего следует выбрать значок ASP .NET Web Service в диалоговом окне New Project. Результат показан на Рисунок 12.3.

    Простейший вариант использования web-службы


    Простейший вариант использования web-службы
    Введите в текстовом поле строку Seattle и нажмите кнопку Invoke. Примерный вид страницы показан на Рисунок 12.6.

    Простое приложение ASP .NET в IDE


    Простое приложение ASP .NET в IDE
    В первой выделенной строке атрибуту runat присваивается значение server, указывающее на то, что код элемента должен выполняться на стороне сервера, а не на стороне клиента. В принципе код ASP .NET может работать и на стороне клиента, но это делается очень редко, поскольку многие возможности ASP .NET становятся недоступными (в сущности, при этом выполняется сценарий клиентской стороны, не имеющий ничего общего с ASP .NET).

    Обратите внимание: все теги элементов страницы снабжаются префиксом
    Если дважды щелкнуть на кнопке, в окне программы открывается процедура события Cl i ck. Открывшаяся страница имеет много общего с приложениями, созданными на базе форм Windows; она тоже содержит автоматически сгенерированный код, который будет описан ниже. В приложениях ASP .NET код обычно отделяется от средств визуального представления и хранится в отдельном файле с двойным расширением .aspx.vb.

    Включите в процедуру события Cl ick следующий фрагмент:

    Private Sub Buttonl_Click(ByVal sender As System.Object,_
    ByVal e As System.EventArgs)
    Handles Buttonl.Click

    Me.Label 1.Font.Size = FontUnit.XLarge

    Me.Label 1.Text = "Welcome to ASP .NET @" & Now
    End Sub

    Обратите внимание на сходство этого кода с обработчиками событий в приложениях форм Windows, включая наличие параметров sender и EventArgs. Свойство Font в web-элементах несколько отличается от одноименного свойства форм Windows, и это вполне естественно, поскольку количество шрифтов, отображаемых в страницах HTML, заметно ограничено по сравнению с формами Windows. Также обратите внимание на возможность использования встроенных функций .NET (таких, как Now) в коде ASP .NET. Приложения ASP .NET обладают полноценным доступом к .NET Framework. В частности, это означает, что для обращения к данным в ASP .NET могут использоваться все элементы и вся программная поддержка баз данных .NET (см. главу И).

    При нажатии клавиши F5 VS .NET IDE генерирует web-страницу и автоматически отображает ее в Internet Explorer. После нажатия кнопки окно приобретает вид, показанный на Рисунок 12.2.

    Приложения ASP .NET, как и другие классы приложений .NET, компилируются в би-блиотеки DLL. IDE генерирует для DLL файл описания сборки, хранящийся на сервере. В приложение ASP .NET также входит страница .aspx и другие сопутствующие файлы.

    Ниже приведен полный код страницы с разверткой регионов:

    Public Class WebForml

    Inherits System.Web.UI.Page

    Protected WithEvents Labell As System.Web.UI.WebControls.Label
    Protected WithEvents Buttonl As System.Web.UI.WebControls.Button #Region "Web Form Designer Generated Code "

    'This call is required by the Web Form Designer.
    Private Sub _

    InitializeComponent()
    End Sub
    Private Sub
    Page_Init(ByVal sender As System.Object.

    ByVal e As System.EventArgs)

    Handles MyBase.Init

    'CODEGEN: следующий вызов необходим для дизайнера форм Web.

    ' Не изменяйте его в редакторе!

    InitializeComponent()
    End Sub
    #End Region

    Private Sub Page_Load(ByVal sender As System.Object._
    ByVal e As System.EventArgs) Handles MyBase.Load

    ' Здесь размещается пользовательский код инициализации страницы
    End Sub

    Private Sub Bultonl_Click(ByVal sender As System.Object._
    ByVal e As System.EventArgs) Handles Buttonl.Click Me.Label1.Font.Size_
    FontUnit.XLarge Me.Label1.Text = "Welcome to ASP .NET @" & Now
    End Sub
    End Class

    Простое web-приложение для ASP .NET

    Выберите в диалоговом окне New Project значок приложения ASP .NET.

    После небольшой задержки VS IDE создает страницу, которой по умолчанию присваивается имя WebFormsl.aspx. Сгенерированная страница содержит код вывода для приложения ASP .NET. Примерный вид VS .NET IDE показан на Рисунок 12.1. Обратите внимание, сколько файлов было создано для такой простой страницы (имена файлов перечислены в окне решения). Посмотрите на каскадный список стилей, определяющий общие параметры внешнего вида страницы. Дизайнер очень похож на дизайнер форм Windows, описанный в главе 8. Слева расположена панель, с которой элементы перетаскиваются на web-страницу (конечно, web-страницы на стадии конструирования обладают меньшими возможностями по сравнению с формами Windows, поскольку они должны работать в броузере).

    Создайте на форме надпись и кнопку. Растяните надпись по ширине страницы, расположите кнопку под надписью и выровняйте по центру, при этом генерируются экземпляры классов из пространства System. Web. UI. WebControl. Ссылка на это пространство имен автоматически включается в решение при создании «скелета» нового приложения ASP .NET. В свойстве Text надписи следует ввести пустую строку, а в свойстве Text кнопки вводится текст «Click me!». Изменения свойств сохраняются в HTML-коде, содержащемся в странице .aspx. Чтобы просмотреть базовый HTML-код, можно выполнить команду View > HTML Source (клавиши Ctrl+PageDown) или выбрать вкладку HTML в IDE. В следующем фрагменте ключевые строки выделяются жирным шрифтом1.

    <%@Page Language="vb"AutoEventWireup="false"
    Codebehind="WebForml.aspx.vb"
    Inherits="WebApplicationl.WebForml'l>
    DTD HTML 4.0 Transitional//EN">





    content="Microsoft Visual Studio.NET 7.0">





    content= "http://schemes.microsoft.com/intel1i sense/ie5">








    style="Z-INDEX:102;LEFT:
    15px;POSITION:absolute: TOP:40px"runat="server
    "Width="631px"Height="132px">





    Результат обращения к web-службе с использованием запроса GET


    Результат обращения к web-службе с использованием запроса GET
    С запросами SOAP дело обстоит несколько сложнее. Вряд ли кому-нибудь захочется генерировать их вручную. Вместо этого можно воспользоваться командой Project > Add Web Reference или утилитой командной строки wsdl.exe, входящей в поставку .NET Framework. Оба варианта приводят к одному результату — генерируется вспомогательный класс-посредник, который используется в программе.

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

    "С:\Program Fi1es\Microsoft.NET\FrameworkSDK\Bin\wsdl"/language:VB"
    http://localhost/WebServicel/Servicel.asmx?wsdl

    При запуске с ключом /language: VB утилита создает файл с кодом вспомогательного класса Servicel.vb, по умолчанию находящийся в одном каталоге с wsdl.exe (выходной каталог задается ключом out). Основной код сгенерированного класса выглядит так (ключевые строки выделены жирным шрифтом):

    Option Strict Off

    Option Explicit On

    Imports System

    Imports System.Diagnostics

    Imports System.Web.Services

    Imports System.Web.Servi ces.Protocols

    Imports System.Xml.Serialization

    'This source code was auto-generated by wsdl,Version=l.0.2914.16.
    _
    Public Class Servicel

    Inherits System.Web.Services.Protocols.SoapHttpClientProtocol

    _

    Public Sub New() MyBase.New Me.Url ="http://localhost/WebServicel/Servicel.asmx"

    End Sub


    System.Web.Services.Protocols.
    SoapDocumentMethodAttri bute _
    ("http://tempuri.org/GetWeather".

    Use:=System.Web.Services.Description.
    SoapBindingUse.Literal.
    ParameterStyle:=System.Web.Services.
    Protocols.SoapParameterStyle.Wrapped)> _

    Public Function GetWeather(ByVal city
    As String)As String

    Dimresults() As Object=Me.Invoke
    ("GetWeather". New Object 0 {city})
    Return CType(results(0). String)

    End Function

    DebuggerStepThroughAttribute()> _

    Public Function BeginGetWeather(ByVal city As String._

    ByVal callback As System.AsyncCallback.
    ByVal asyncState As Object) As System.IAsyncResult
    Return
    Me.Beginlnvoke("GetWeather", New Object()

    {city}.call back.asyncState)
    End Function

    _
    Public Function EndGetWeather(ByVal asyncResult

    As System.IAsyncResult) As String
    Dim results()As Object = Me.EndInvoke(asyncResult)
    Return
    CType(results(0), String)
    End Function
    End Class

    Затем в проект включается код класса (или ссылка на него) и ссылки на сборки System. Web, System. XML и System. Web.Services. Остается лишь создать экземпляр вспомогательного класса и вызвать функцию GetWeather!

    Теперь, когда вы знаете, как получить результаты обращения к web-службе, нетрудно представить себе следующие возможные шаги:

  • объединение результатов нескольких обращений к web-службам в одном файле;
  • применение к результату запроса преобразований XSL и построение новых страниц с HTML-кодом при помощи web-служб, работающих на разных сайтах.
  • Перед нами одно из проявлений той концепции, которую сейчас активно внедряет Microsoft, — Web как глобальная сеть, через которую пользователь легко получает доступ к любым необходимым данным.





    Результат выполнения простейшего кода ASP .NET


    Результат выполнения простейшего кода ASP .NET
    В ASP .NET используется файл globalasax — аналог файла global.asa из ASP. Главное различие между этими файлами заключается в том, что обращения к файлу global.asax происходят:

  • в начале каждого запроса;
  • при возникновении ошибки;
  • при попытке аутентификации пользователя (процесс аутентификации рассматривается ниже).
  • Если вы захотите добавить в global.asax код инициализации приложения, включите его в функцию InitializeComponent(), поскольку именно эта функция вызывается при загрузке приложения.

    Содержание Вперед




    Результаты обращения к web-службе


    Результаты обращения к web-службе
    Результат представлен в формате XML, что позволяет легко проанализировать данные в программе. Также обратите внимание на то, что в автоматически сгенерированном коде показано, как обращаться к web-службе за пределами IDE. Для этого необходимо лишь сгенерировать правильный запрос HTTP GET или SOAP. Ниже приведен пример построения запроса GET в консольном приложении, использующем для отправки запроса GET классы WebRequest и WebResponse пространства имен System.Net:

    1 Imports System.Net

    2 Imports System.I0

    3 Module Module1

    4 Sub Main()

    5 Dim myResponse As WebResponse

    6 Try

    7 Dim myWebServiceRequest As WebRequest

    8 myWebServiceRequest - WebRequest.Create _

    9 ("http://localhost/WebServicel/Servicel.
    asmx/GetWeather?dty=SeattIe")

    10 myResponse = _

    11 myWebServiceRequest.GetResponse()

    12 Dim theAnswer As String

    13 Dim aStream As New StreamReader
    (myResponse.GetResponseStream)

    14 theAnswer = aStream.ReadToEnd

    15 MsgBox(theAnswer)

    16 Catch e As Exception

    17 Console.WriteLine(e.Message)

    18 Finally

    19 myResponse. Close()

    20 End Try

    21 End Sub

    22 End Module

    Ключевая роль в этом листинге принадлежит строке 8 (продолжающейся в строке 9), в которой серверу передается запрос GET. Как было показано в главе 9, результат представляет собой поток данных, используемый для построения StreamReader (строка 13). Строка 14 читает в строковую переменную весь текст потока. Строка 19 закрывает объект запроса HTTP и освобождает все связанные с ним ресурсы. Кстати, переменная myResponse была объявлена в строке 5 именно потому, что при объявлении ее в блоке Try (строки 6-15) переменная оказалась бы недоступной для секции Fi nal ly. Результат выполнения программы показан на Рисунок 12.7.

    Схемы аутентификации в ASP .NET

    Схема аутентификации
    Описание
    Windows Используется встроенная схема аутентификации IIS (на экране появляется диалоговое окно для ввода имени и пароля). Пользователь должен иметь учетную запись на данном компьютере
    Forms Наиболее распространенная форма аутентификации. После успешной регистрации сервер генерирует cookie, автоматически включаемое в заголовок сообщения на протяжении сеанса. При настройке этой схемы аутентификации указывается имя регистрационной формы и управляющая ею страница ASP .NET
    Passport Новая схема аутентификации, активно внедряемая компанией Microsoft
    Первые две схемы вполне стандартные, только третья относительно нова. Поиск на сайте Googte показывает, что на момент написания книги существовали тысячи web-страниц с обсуждением достоинств и недостатков схемы аутентификации Microsoft Passport. При желании вы можете самостоятельно изучить эту тему и принять собственное решение.

    Помните, что при любой схеме аутентификации HTTP остается текстовым протоколом, поэтому информация передается клиенту в незашифрованном виде и может быть похищена методом перехвата пакетов (packet sniffing). Для борьбы с перехватом используется транспортный механизм SSL (Secure Socket Layer).

    Классы .NET WebRequest и WebResponse автоматически используют SSL для URL, начинающихся с префикса «https».

    Строки 47-59 управляют состоянием сеанса. В большинстве случаев используются стандартные механизмы ASP .NET, обладающие достаточно мощными возможностями. Начиная со строки 60 и до конца файла реализуется одна занятная возможность ASP .NET, представляющая особый интерес для тех, кто хочет защитить свою интеллектуальную собственность. В этом разделе приводится список типов файлов, которые не должны приниматься клиентом с сервера.

    Назад Содержание Вперед


    Cамоучитель по VB.NET

    COM Interop и вызовы функций DLL

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

    В Visual Studio .NET взаимодействие с СОМ почти не требует усилий со стороны программиста. Просто выберите нужный объект СОМ на вкладке СОМ диалогового окна ссылок, и с ним можно будет работать как с классом .NET. Для этого IDE читает библиотеку типов СОМ и создает для объекта вспомогательный класс («обертку») .NET. Открытыми членами этого класса являются все открытые члены объекта СОМ. Кстати, технология IntelliSense работает и для экземпляров этих классов.

    Классы объектов СОМ также создаются утилитой tlbimp.exe, входящей в. NET SDK. В частности, эта утилита удобна при одновременном построении нескольких «оберток» для использования в будущем.



    Исследование манифеста

    При двойном щелчке на строке Manifest из Рисунок 13.1 открывается окно, показанное на Рисунок 13.2. Обратите внимание на перечисление всех сборок, от которых зависит данная сборка, а также на описание класса Employee.

    Манифест сборки всегда содержит два обязательных атрибута, указанных в верхней и нижней части Рисунок 13.2:

  • имя сборки;
  • основной и дополнительный номер версии.
  • В качестве имени сборки может использоваться любое допустимое имя файла. Обычно имя сборки задается в диалоговом окне — выполните команду Project > Properties и перейдите на страницу General в категории Common Properties.

    Манифест класса Employee


    Манифест класса Employee
    Сильное имя (strong name) можно считать аналогом GUID, хотя оно устроено несколько сложнее. Сильные имена используются лишь для общих сборок. Дополнительная информация приведена в следующем разделе.

    Многофайловые сборки

    На момент написания книги в среде программирования не поддерживалось создание сборок, состоящих из нескольких файлов. Если такая необходимость возникала, программисту приходилось обращаться к документации и использовать соответствующие утилиты .NET SDK. Все компиляторы командной строки .NET позволяют создавать многофайловые сборки. Хотя в книге данная тема не рассматривается, мы хотим обратить ваше внимание на одну интересную особенность многофайловых сборок: они тоже могут устанавливаться простым копированием, причем устанавливать сразу все файлы на компьютер конечного пользователя не обязательно — компоненты могут копироваться по мере надобности. Эта возможность очень удобна для установки по Интернету, где приходится учитывать объем пересылаемой информации. Например, в многофайловой сборке прием компонента справочной системы можно отложить до того момента, когда пользователь захочет вызвать справку.





    Манифест

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

  • информация, необходимая для поиска модулей, от которых зависит работа сборки;
  • имена всех файлов, входящих в сборку;
  • имена и метаданные всех сборок и файлов, используемых сборкой;
  • данные о версии сборки;
  • информация о типах, используемая исполнительной средой для экспортирования типов из сборки (по аналогии с информацией, находящейся в библиотеке типов СОМ).
  • Именно благодаря наличию манифеста появляется возможность создания сборок, состоящих из нескольких файлов. Кроме того, данные манифеста заменяют сложную систему регистрации компонентов в реестре. Первое представление о сборке и ее манифесте дает файл Assemblylnfo.vb; чтобы просмотреть содержимое этого файла, дважды щелкните на соответствующей строке окна решения VS .NET. Как видно из приведенного ниже примера, этот текстовый файл содержит многочисленные атрибуты сборки. Большинство атрибутов (например, название организации) можно редактировать вручную, хотя чаще значения задаются в IDE при помощи диалоговых окон свойств проекта.

    Imports System.Reflection

    Imports System.Runtime.InteropServices

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

    'Review the values of the assembly attributes








    ' Следующий QUID используется для идентификации библиотеки типов.
    ' если проект будет использоваться в СОМ

    ' Данные версии для сборки состоят из следующих четырех величин:

    ' Основная версия

    ' Дополнительная версия

    ' Ревизия

    ' Номер построения

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


    Если задать эти атрибуты и построить сборку, такая информация становится доступ-ной в Проводнике Windows. Щелкните правой кнопкой мыши на значке ЕХЕ-файла в окне Проводника, выберите в контекстном меню команду Properties (Свойства) и перейдите на вкладку Version (Версия).

    В каталоге \bin .NET SDK находится полезная программа ILDASM, которая может использоваться для исследования сборок и их манифестов. На Рисунок 13.1 показано, какую информацию выдает ILDASM для программы Employee из главы 4.



    Общие сборки и GAC

    Общие сборки .NET хранятся в глобальном кэше сборок (GAC). Наличие глобального кэша экономит дисковое пространство и память, поскольку на стадии выполнения программы на диске или в памяти достаточно хранить лишь один экземпляр сборки. Конечно, при совместном использовании сборок возникают некоторые проблемы, присущие старому механизму совместного использования DLL на базе реестра. К счастью, средства контроля версии .NET позволяют хранить в GAC разные версии одной сборки, поэтому каждое приложение может использовать нужную версию. Постарайтесь как можно реже использовать глобальный кэш сборок и ограничиться следующими ситуациями:

  • если использование сборки в разных приложениях вызвано абсолютной необходимостью, но по соображениям экономии места хранение нескольких локальных копий нежелательно;
  • если сборка требует особого уровня защиты (удаление сборок из GAC разрешено только администратору).
  • Список сборок, находящихся в GAC, выводится утилитой gacutil.exe из каталога \bin .NET SDK. Команда имеет следующий синтаксис: gacutil.exe -1

    Ниже приведено начало списка для одного из наших компьютеров. Полный список имеет внушительные размеры; даже на этой ранней стадии существования .NET он занимает около трех страниц:

    Microsoft (R).NET Global Assembly Cache Utility.Version 1.0.2914.16
    Copyright (C)Microsoft Corp,1998-2001,All rights reserved.

    The Global Assembly Cache contains the following assemblies:
    Accessibili ty.Version=1.0.2411.0,Culture=neutral,
    PublicKeyToken-b03f5f7flld50a3a,Custom=null

    ADODB,Version=2. 7.0.0.Culture-neutral,
    PublicKeyToken-b03f5f7fIld50a3a, Custom=null

    CRVsPackageLib.Version=1.0.0.0.Culture-neutral,
    PublicKeyToken=4f3430cff154c24c,Custom=nul1

    Crystal Deci si ons.Crystal
    Reports.Engine.Version=9.1.0.0.Culture-neutral,
    PublicKeyToken=4f3430cff154c24c,Custom=nul 1

    Для общих сборок контроль версии играет гораздо более важную роль, чем для закрытых сборок, поэтому в списке указаны номера версий каждой сборки. Последнее из Четырех чисел в номере версии определяет номер ежедневного построения, изменение которого считается непринципиальным. Далее следует номер ревизии, изменение которого тоже считается непринципиальным. Изменения следующих двух чисел (дополнительного и основного номеров) принципиальны. Это означает, что если программа запрашивает сборку версии 2.0.0.0, а в GAC находится только версия 2.5.0.0, программа не будет работать, если не внести специальные изменения в конфигурационный файл. С другой стороны, версия 2.0.0.37 считается совместимой с версией 2.0.0.0 и успешно загружается.

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



    Построение ключей

    При программировании в .NET ключи (открытый и закрытый) обычно создаются утилитой sn.exe, входящей в .NET SDK (сокращение «sn» означает «strong name», то есть «сильное имя»).

    Ключи хранятся в двоичных файлах. Команда построения ключей имеет следующий синтаксис:

    sn -k <имя_файла>

    Файлам должно быть присвоено расширение .snk. В нашем примере пара ключей была создана командой

    sn -k c:\keys\pair.snk

    Файл .snk можно сгенерировать и в VS .NET IDE (команда Strong Name в диалоговом окне Project Properties), но большинство программистов предпочитает создавать ключи в отдельном процессе с максимальным уровнем защиты. Файл .snk должен быть защищен от несанкционированного доступа; если закрытый ключ станет известен посторонним, проку от него будет немного.



    Принципы работы СОМ

    Технология СОМ упрощает создание программ, сохраняющих совместимость в разных версиях платформы Windows и более или менее независимых от языка программирования. Компоненты СОМ могут создаваться на разных языках, включая классический С (вариант для мазохистов), C++, Delphi, VB5 и 6. Технология СОМ с большим успехом применялась для создания объектов, предназначенных для решения специализированных задач, таких как элементы VB OCX.

    Технология СОМ была задумана как механизм, при помощи которого программные компоненты получают информацию о возможностях других компонентов и обращаются к ним с запросами, не беспокоясь о подробностях внутренней реализации [ Существуют и другие технологии, ориентированные на повторное использование программного кода (например, CORBA), но пока наибольшего успеха добилась именно модель СОМ. ]. Для этого был выработан стандартный протокол получения информации об интерфейсах, поддерживаемых другими компонентами, наряду со стандартизацией средств для обращения к конкретной реализации интерфейса в экземплярах.

    Тем не менее у СОМ были свои недостатки. Во-первых, реализация СОМ для Windows требовала, чтобы в системном реестре хранилась вся информация обо всех компонентах в системе. Пользователю приходилось регистрировать компоненты при установке программ и стирать соответствующую информацию при удалении программ. При попытке удаления программ возникала опасность того, что изменения, внесенные в реестр, повлияют на работу других программ. Стоило серьезно повредить реестр, и система вообще переставала работать. Более того, установка новой версии компонента нередко нарушала работу программ, рассчитанных на более раннюю версию компонента.

    В Windows 98 была впервые представлена концепция параллельного выполнения (side-by-side execution); это означало, что приложение могло использовать локальный экземпляр компонента СОМ, находящийся в каталоге приложения, вместо экземпляра, зарегистрированного в системе. Справедливости ради следует сказать, что параллельное выполнение так и не решило проблемы с «кошмаром DLL», вдобавок оно работает только в Windows 98, 2000 и ХР — и то если об этом специально позаботится разработчик программы.

    Давайте посмотрим, что происходит на уровне реестра при регистрации компонентов СОМ.

  • Разработчик создает для компонента глобально-уникальный идентификатор (GUID).
  • Разработчик создает для компонента программный идентификатор (ProgID).
  • Утилита регистрации связывает ProgID компонента с GUID, создавая соответствующую запись в реестре.
  • Утилита регистрации заносит полный путь к двоичному файлу компонента в реестр и связывает его с GUID компонента.
  • Утилита регистрации также может сохранить в реестре дополнительные сведения о компоненте — например, тип потоковой модели.
  • При попытке использования компонента происходит следующее:

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



    Программа ILDASM в действии


    Программа ILDASM в действии
    Номера версии (основной, дополнительный, ревизия и построение) хранятся в следующем формате:

    <0сн>.<дополн>.<ревизия>.<построение>

    Эти значения можно задать прямо в файле Assemblylnfo.vb. Чтобы включить режим автоматической нумерации, введите версию в формате «х.у .*». Знак «*» указывает VS на то, что номера ревизии и построения должны генерироваться автоматически.

    Для закрытых сборок версия не проверяется.

    Во многих сборках также встречаются еще два атрибута:
  • локальный контекст;
  • сильное имя.
  • Локальный контекст (culture) содержит информацию о национальных стандартах, поддерживаемых сборкой. Не путайте локальный контекст с языком. Например, и в Великобритании и в США говорят на\нглийском языке, но локальные контексты в этих странах различаются (так, в них используются разные форматы вывода дат и денежных сумм).

    Сборки.NET

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

    С технической точки зрения сборка (assembly) в .NET представляет собой минимальную устанавливаемую единицу программного кода. Сборка оформляется в виде автономного ЕХЕ-файла или в виде библиотеки DLL, на которую можно ссылаться из других приложений. Однако сборка содержит нечто большее, чем обычный IL-код, компилируемый и выполняемый исполнительной средой .NET. Как минимум, сборка состоит из одного или нескольких модулей и классов, откомпилированных в IL-код, и метаданных (данных, описывающих данные [ Префикс «мета» для подобных абстракций второго порядка позаимствован из метаматематики — области математики, посвященной описанию самих математических объектов. ]), которые описывают сборку и функциональность входящих в нее классов. Метаданные являются частью сборки, поэтому в документации сборки названы самодокументируемыми. Во многих ситуациях сборка состоит из одного файла, но встречаются и многофайловые сборки. Например, в сборку могут входить ресурсные файлы, графические изображения и даже дополнительные EXE/DLL-файлы. В любом случае сборка является минимальным объектом .NET, для которого производится контроль версии или задаются привилегии.

    В большинстве случаев создаются однофайловые сборки, состоящие из одного ЕХЕ-или DLL-файла.

    Сборки бывают закрытыми (private) и общими (shared). Закрытые сборки всегда находятся в каталоге приложения или в одном из его подкаталогов. Общие сборки хранятся в глобальном кэше сборок (GAC, global assembly cache). Начнем с закрытых сборок, поскольку именно они используются по умолчанию для решений, построенных в VS .NET IDE. С общими сборками дело обстоит сложнее, и мы займемся ими позже.

    Обычно у закрытых сборок не бывает проблем с несовместимостью версий, однако они требуют дополнительных затрат дискового пространства, если в системе приходится хранить несколько копий одного файла в разных каталогах [ В наше время дисковое пространство обходится так дешево, что эти затраты с избытком компенсируются удобствами, связанными с использованием закрытых сборок. ]. При создании ссылок на сборку командой Project > Add Reference по умолчанию в каталоге приложения создается новый экземпляр закрытой сборки. Мы рекомендуем по возможности ограничиваться использованием закрытых сборок.

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

    Содержание Вперед




    Сертификация сборки

    При наличии пары ключей в виде файла .snk можно сертифицировать сборку закрытым ключом. При этом .NET включает открытый ключ в манифест сборки [ Точнее говоря, в манифест включается хэшированная версия ключа, которая в .NET называется образцом (token) открытого ключа. Вероятно, хэширование применяется для экономии места, хотя нам кажется, что в манифесте следовало бы просто опубликовать открытый ключ. ] и вычисляет хэш-код всех данных сборки по закрытому ключу. Результат тоже включается в манифест. По этим данным другие сборки, использующие ваш код, смогут убедиться в том, что сборка не подверглась постороннему вмешательству. Для этого открытый ключ применяется к зашифрованному хэш-коду, а результат сравнивается с приведенным в манифесте.

    Чтобы сертифицировать сборку, включите в программу атрибут AssemblyKeyFile-Attribute с именем файла .snk после всех команд импортирования или же воспользуйтесь вкладкой Sharing диалогового окна Project Settings. Пример:

    Imports System.Reflection

    \

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



    Сильные имена и совместное использование сборок

    Сильные имена, как и GUID, должны быть уникальными в пространстве и времени. В отличие от GUID, которые теоретически могут быть похищены, сильные имена защищены от несанкционированного использования при помощи механизма шифрования с открытым ключом [ При условии надежного хранения закрытого ключа. ]. Конкретные схемы шифрования бывают очень сложными, однако в целом шифрование с открытым ключом построено на довольно простой идее — в некоторых ситуациях вернуться от конечного результата к исходным данным бывает очень, очень сложно. Например, вы можете легко перемножить два целых числа и получить результат, но узнать исходные множители по произведению невозможно — для этого нужно знать хотя бы один из них [ Этот принцип заложен в основу RSA, распространенного алгоритма шифрования с открытым ключом. ].

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

    Во всех схемах с открытым ключом используется пара ключей: открытый и закрытый. Открытый ключ (public key) может свободно распространяться, поскольку без знания закрытого ключа зашифрованное сообщение невозможно восстановить за сколько-нибудь приемлемый промежуток времени. Применение закрытого ключа к данным манифеста позволяет сертифицировать их. Другие пользователи при помощи открытого ключа убеждаются в том, что сборка поступила именно от вас, а не из постороннего источника. А в некоторых случаях (например, при использовании Verisign) они даже могут убедиться в том, что открытый ключ принадлежит именно вам, а не кому-то другому (шифрование с открытым ключом защищает целостность данных, но для проверки открытого ключа необходимы услуги третьей стороны).





    Включение и удаление сборок из GAC

    Чтобы общая сборка автоматически включалась в GAC в процессе установки, проще всего воспользоваться программой установки с поддержкой GAC — например, последней версией пакета Microsoft Installer (MSI). Описание этой программы выходит за рамки книги, но мы укажем, что эту программу можно бесплатно загрузить с сайта MSDN (http://msdn.microsoft.com).

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

    gacutil.exe. Синтаксис командной строки:

    gacutil -1 <имя_сборки>

    Сборка с заданным именем помещается в GAC.



    Вызовы функций DLL

    Хотя при вызове функций DLL можно использовать старый синтаксис Declare, в .NET рекомендуется использовать другой способ — атрибут Oil Import, позволяющий создавать общие точки входа. Для этого в программе определяется пустая функция, совпадающая по сигнатуре с вызываемой функцией. Ниже приведен пример использования атрибута Dll Import.

    Imports System.Drawing

    Imports System.Runtime.InteropServices

    Module Modulel

    ' Импортировать функцию CreateDC из Win32 API

    Public Function CreateDC(ByVal strDhver _
    As String. ByVal strDeviceName As String,_
    ByVal strOutput As String. ByVal nullDEVICE As Integer _ )
    As 'IntPtr
    End Function
    Sub Main()

    ' Создать прямоугольник

    Dim rctWindow As Rectangle = New Rectangle(100, 100, 200, 200)

    Dim penBlack As Pen = New PerUColor.Black)

    penBlack.Brush = Brushes.DarkKham

    Dim grfx As Graphics

    Dim hDC As IntPtr = CreateDC("DISPLAY". vbNullString,

    vbNullString. vbNullString)

    grfx = Graphics.FromHdc(hDC)

    Do While (True)

    grfx.FillRectangle(penBlack.Brush, rctWindow)
    System.Threading.Thread.Sleep(0)
    Loop
    End Sub
    End Module

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





    

        Программирование: Языки - Технологии - Разработка