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









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

| Кнопка | Описание | |

| Операция |
Клавиши |
| Перебор ранее введенных команд | вверх, вниз |
| Прокрутка окна вверх | Ctrl + вверх |
| Прокрутка окна вниз | Ctrl + вниз |
| Назад | Содержание | Вперед |









| Содержание | Вперед |
| Сокращенная запись |
Эквивалент |
| А*=В |
А = А*В |
| А+=В |
А = А + В |
| А/=В |
А = А/В |
| А-=В |
А = А-В |
| А\=В |
А = А\В |
| А^=В |
А = А^В |
| А&=В |
А = А & В (конкатенация строк) |
| 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 |
| Символ |
Проверяемое условие |
| <> |
Не равно |
| < |
Меньше |
| <= |
Меньше или равно |
| > |
Больше |
| >= |
Больше или равно |
| Тип 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 |
| Тип |
Допустимое расширение |
| 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 |
| Функция |
Описание |
| CBool | Преобразует выражение к типу Boolean |
| CByte | Преобразует выражение к типу Byte |
| CInt | Преобразует выражение к типу Integer с округлением |
| CIng | Преобразует выражение к типу Long с округлением |
| CSng | Преобразует выражение к типу Single |
| CDate | Преобразует выражение к типу Date |
| СDbl | Преобразует выражение к типу Double |
| CDec | Преобразует выражение к типу Decimal |
| CStr | Преобразует выражение к типу String |
| CChar | Преобразует первый символ строки к типу Char |
| Функция |
Описание |
| 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 |
Преобразует строку к верхнему регистру |
| Метод/свойство | Описание |
| 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, в начале строки |
| Назад | Содержание | Вперед |
| Оператор |
Операция |
| + | Сложение |
| - | Вычитание (и обозначение отрицательных чисел) |
| / | Деление (преобразование к Double — не может вызвать исключение DivideByZero; см. главу 7) |
| \ | Целочисленное деление (без преобразования — может вызвать исключение DivideByZero) |
| * | Умножение |
| ^ | Возведение в степень |
| Оператор |
Операция |
| \ |
Целочисленное деление любых целых чисел |
| Mod |
Остаток от целочисленного деления |
| Математическая функция |
Описание |
| Abs |
Возвращает абсолютное значение (модуль) числа |
| Acos |
Возвращает угол, косинус которого равен заданному числу |
| Asin |
Возвращает угол, синус которого равен заданному числу |
| Atan |
Возвращает угол, тангенс которого равен заданному числу |
| Ceiling |
Возвращает наименьшее целое число, большее либо равное заданному числу |
| Cos |
Возвращает косинус заданного угла |
| Exp | Возвращает число е (приблизительно 2,71828182845905), возведенное в заданную степень |
| Floor | Возвращает наибольшее целое число, большее либо равное заданному числу |
| Log |
Возвращает натуральный логарифм |
| Log10 |
Возвращает десятичный логарифм |
| Max |
Возвращает большее из двух заданных чисел |
| Min |
Возвращает меньшее из двух заданных чисел |
| Round |
Возвращает целое число, ближайшее к заданному числу |
| Sign |
Возвращает величину, определяющую знак числа |
| - Sin |
Возвращает синус заданного угла |
| Sqrt |
Возвращает квадратный корень |
| Tan |
Возвращает тангенс заданного угла |
| Назад | Содержание | Вперед |

Рисунок 4.6 . Метод GetDirectories в справочной системе

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


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


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


>

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

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




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

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





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

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

Рисунок 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 |
| Назад | Содержание | Вперед |

Рисунок 8.23. Вывод текста «Hello World!» средствами GDI+
В GDI+ полностью поддерживается кодировка Unicode, что позволяет выводить текст на любом языке.





Таблица 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) | Задает текущий каталог |
| Содержание | Вперед |


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

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

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

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

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

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

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

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

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

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

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

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