Текст
                    Официальное пособие для самоподготовки
курс
Microsoft
W0
MCTS
Основы разработки на платфор Microsoft
.NS



«.РУССКАЯ РЕДАКЦИЯ
MKrosoft1
£ ПИТЕР
Официальное пособие для самоподготовки
Учебный курс
Microsoft’
Основы разработки приложений на платформе
Microsoft*
.NET Framework
Тони Нортроп, Шон Уилдермьюс, Билл Райан
Экзамен 70-536
MCTS
Москва * Санкт-Петербург * Нижний Новгород * Воронеж Новосибирск * Ростов-на-Дону * Екатеринбург * Самара Киев * Харьков * Минск
2007
га.РУССКАЯ РЕДАКЦИЯ	Е^ППТЕР
УДК 004.45
ББК 32.973.26-018.2
Н82
Нортроп Тони, Уилдермьюс Шон, Райан Билл
Н82 Основы разработки приложений на платформе Microsoft .NET Framework. Учебный курс Microsoft / Пер. с англ. — М. : «Русская Редакция», СПб. : «Питер», 2007. — 864 стр. : ил.
ISBN 978-5-469-01659-5 («Питер»)
ISBN 978-5-7502-0297-3 («Русская Редакция»)
Этот учебный курс посвящен разработке приложений с использованием .NET Framework (любой версии) на языках Visual Basic .NET и Visual C# .NET. Книга содержит введение в .NET Framework, описание создания и применения консольных и GUI-приложений. Авторы делятся с читателями бесценным опытом решения различных задач, стоящих перед программистами. Значительное внимание уделяется вопросам безопасности, глобализации и развертывания приложений. Освоив теоретические материалы и выполнив практические задания курса, вы получите знания и навыки, необходимые разработчику приложений, использующих современную платформу Microsoft .NET.
Книга адресована всем, кто хочет научиться создавать Windows-приложения и серверные компоненты, использующие инфраструктуру .NET Framework, на языках Visual Basic .NET и С#. Настоящий учебный курс поможет вам самостоятельно подготовиться к сдаче экзамена № 70-536: «Microsoft NET Framework 2.0 — Application Development Foundation» — базового экзамена по нескольким cepj ификациям MCTS (Microsoft Certified Technology Specialist).
Книга состоит из 16 глав, содержит множество иллюстраций и примеров из практики. На прилагаемом компакт-диске находятся электронная версия книги, вопросы пробного экзамена, словарь терминов и другие справочные материалы на английском языке.
УДК 004.45
ББК 32.973.26-018.2
Подготовлено к изданию по лицензионному договору с Microsoft Corporation, Редмонд, Вашингтон, США.
Microsoft, Active Directory, Excel, IntelliSense, Internet Explorer, Microsoft Press, MSDN, OpenType, Outlook, Visual Basic, Visual C#, Visual Studio, Win32, Windows, Windows Server и Windows Vista являются товарными знаками или охраняемыми товарными знаками корпорации Microsoft в США и/или других странах. Вое другие товарные знаки являются собственностью соответствующих фирм.
Все адреса, названия компаний, организаций и продуктов, а также имена лиц, используемые в примерах, вымышлены и не имеют никакого отношения к реальным компаниям, организациям, продуктам и лицам.
Нортроп Тони, Уилдермьюс Шон, Райан Билл
Основы разработки приложений на платформе Microsoft .NET Framework
Учебный курс Microsoft
Перевод с английского под общей редакцией А. Е. Соловченко
Совместный проект издательства «Русская Редакция» и издательства «Питер»
Н.РУССШ РЦАЦИ С^ППТЕР*
Налоговая льгота — общероссийский классификатор продукции ОК 005-93, том 2; 95 3005 — чит.-ратура учсона» Подписано в печать 19.02.07. Формат 70x100/16. Усл. п. л. 69,66. Тираж 3000. За ui 101.
Отпечатано по технологии CtP в ОАО «Печатный двор» им. А. М. Горького.
197110, Санкт-Петербург, Чкаловский пр., д. 15.
ISBN 878-0-7356-2277-7 (англ.) ©
ISBN 878-5-469-01659-5 («Питер») ©
ISBN 878-5-7502-0297-3	©
Оригинальное издание на английском языке, Microsoft Corporation, 2006
Перевод на русский язык, Microsoft Corporation, 2007
Оформление и подготовка к изданию, издательство «Русская Редакция», 2007
(«Русская Редакция»)
Содержание
Введение ..............................................................XVII
Благодарности .........................................................XXII
Глава 1 Основы .NET Framework ............................................1
Занятие 1. Применение значимых типов .....................................2
Встроенные значимые типы ..............................................3
Объявление значимых типов .............................................4
Создание пользовательских типов .......................................6
Создание перечислимых типов............................................9
Практикум. Объявление и использование значимых типов .................10
Резюме ...............................................................13
Закрепление материала ................................................13
Занятие!. Использование общих ссылочных типов ...........................15
Введение в ссылочные типы ............................................15
Сравнение ссылочных и значимых типов .................................15
Встроенные ссылочные типы ............................................16
Строки и построители строк ...........................................17
Создание и сортировка массивов .......................................19
Использование потоков ................................................19
Генерация и перехват исключений ......................................20
Практикум. Работа со ссылочными типами ...............................23
Резюме ...............................................................27
Закрепление материала ................................................27
Занятие 3. Конструирование классов ......................................28
Введение в наследование ..............................................28
Введение в интерфейсы ................................................30
Частичные классы .....................................................33
Обобщения ............................................................33
События ..............................................................38
Введение в атрибуты ..................................................41
Введение в переадресацию типов .......................................43
Практикум. Создание производного класса с делегатами .................43
Резюме ...............................................................46
Закрепление материала ................................................46
Занятие 4. Преобразование типов .........................................47
Преобразование типов в Visual Basic и C# .............................47
Упаковка и распаковка ................................................49
Реализация преобразования в собственных типах ........................49
Практикум. Безопасное преобразование типов ...........................52
Резюме ...............................................................53
Закрепление материала ................................................53
Закрепление материала главы .............................................54
Резюме главы ............................................................54
Основные термины ......................................................  55
Лабораторная работа .....................................................55
Проектирование приложения ............................................55
Рекомендуемые упражнения ................................................56
Управление данными в приложении .NET Framework 2.0 при помощи системных типов ................................................................56
Реализация интерфейсов .NET Framework для унификации компонентов .....56
Управление взаимодействием компонентов .NET-приложений при помощи событий и делегатов ..................................................56
Пробный экзамен ......................................................   57
VI
Содержание
Глава 2 Программирование ввода-вывода .....................................58
Занятие 1. Навигация по файловой системе ..................................59
Введение в классы для работы с файловой системой .......................60
Класс FileSystemlnfo ...................................................60
Класс File Info ........................................................61
Получение сведений о файле .............................................62
Как скопировать файл ...................................................62
Класс Directoryinfo ....................................................63
Перечисление файлов в папке ............................................63
Класс Driveinfo ........................................................64
Перечислимое DriveType .................................................65
Перечисление дисков ....................................................65
Класс Path .............................................................66
Как изменить расширение файла ..........................................66
Класс FileSystemVvhtcher ...............................................67
Мониторинг изменений в каталоге ........................................68
Практикум. Перечисление и мониторинг файлов ............................71
Резюме .................................................................74
Закрепление материала ..................................................74
Занятие!. Чтение и запись файлов ..........................................75
Введение в потоки ......................................................75
Классы, упрощающие чтение и запись данных ..............................78
Класс File .............................................................78
Класс Directory ...........................................................79
Перечислимое FileAccess ...................................................80
Перечислимое FileMode .....................................................80
Класс FileStream ..........................................................80
Класс StreamReader ...........................................................82
Как читать из файла ..........................................................82
Класс Streamwriter ...........................................................84
Как выполнять запись в файл ..........................
Введение в объекты чтения и записи ...................
Класс Memorystream ...................................
Применение класса MemoryStream .......................
Класс BufferedStream .................................
Применение класса BufferedStream .....................
Практикум. Чтение и запись файлов ....................
Резюме ...............................................
Закрепление материала ................................
Занятие 3. Сжатие потоков ...............................
Введение в сжатие потоков ............................
Класс GZipStream .....................................
Класс DeflateStream ..................................
Как сжать данные .....................................
Декомпрессия данных *.................................
Практикум. Сжатие и декомпрессия существующего файла Резюме ...............................................
Закрепление материала ................................
Занятие 4. Работа с изолированным хранилищем ............
Введение в изолированное хранилище ...................
Класс IsolatedStorageFile ............................
Как создать хранилище ................................
Класс IsolatedStorageFileStream ......................
Чтение-запись в изолированное хранилище ..............
Работа с папками изолированного хранилища ............
Класс IsolatedStorageFilePermission ..................
Разрешения для изолированного хранилища ..............
Практикум. Чтение-запись в изолированное хранилище .. Резюме ...............................................
Закрепление материала ................................
85 . 87
90 . 91 . 93 . 94 . 94 . 96 . 96 . 96
. 97 . 97 . 98
. 99 101 102 105
105 105 106 106 108 109 ПО
112 113 113 114 116
116
Содержание
VII
Закрепление материала главы ...........................................116
Резюме главы ..........................................................117
Основные термины ......................................................117
Лабораторная работа ...................................................117
Занятие 1. Сохранение пользовательских настроек ....................117
Занятие 2. Отслеживание активности старых серверов .................118
Рекомендуемые упражнения ..............................................118
Создание приложения для поиска файлов ..............................118
Создание простого хранилища конфигурационных данных ................119
Пробный экзамен .......................................................119
Глава 3 Поиск, изменение и перекодирование текста ......................
Занятие 1. Построение регулярных выражений .............................
Применение регулярных выражений для поиска по шаблону ...............
Извлечение найденных данных .........................................
Замена подстрок с помощью регулярных выражений ......................
Использование регулярных выражений для ограничения ввода строк ......
Практикум. Создание объекта Regex для проверки текста ...............
Резюме ..............................................................
Закрепление материала ...............................................
Занятие 2. Кодирование-декодирование строк .............................
Введение в кодировки ................................................
Использование класса Encoding .......................................
Проверка поддерживаемых кодовых страниц .............................
Определение кодировки при записи файла...............................
Определение кодировки при чтении файла ..............................
Практикум. Чтение и запись файлов в разных кодировках ...............
Резюме ..............................................................
Закрепление материала ...............................................
Закрепление материала главы ............................................
Резюме главы ...........................................................
Основные термины .......................................................
Лабораторная работа ....................................................
Занятие 1. Проверка ввода ...........................................
Занятие 2. Обработка данных, введенных на унаследованном ПК .........
Рекомендуемые упражнения ...............................................
Дополнение обработки текстов в .NET-приложениях поиском с заменой на основе регулярных выражений ......................................
Пробный экзамен ........................................................
120
121
121
131
132
134
135
138
138
142
142
144
145
145
146
147
148
148
149
149
150
150
150
151
151
151
152
Глава 4 Наборы и обобщения ......................................153
Занятие 1. Сбор элементов данных .........................................154
Типы наборов .........................................................155
Добавление и удаление элементов ......................................155
Перебор элементов в цикле ............................................159
Согласованные интерфейсы наборов .....................................161
Упорядочение элементов ...............................................162
Практикум. Сортировка строк таблицы ..................................163
Резюме ...............................................................165
Закрепление материала ................................................166
Занятие!. Работа с последовательными списками ............................166
Введение в последовательные списки ...................................166
Класс Queue ..........................................................167
Класс Stack ..........................................................169
Практикум. Построение FIFO- и LIFO-списков .........................170
Резюме .............................................................172
Закрепление материала ................................................173
Занятие 3. Работа со словарями ...........................................173
Применение объекта Dictionary ..................................173
Концепция равенства ............................................177
VIII
Содержание
Применение интерфейса lEqualityComparer .............................179
Применение класса SortedList ........................................181
Специализированные словари ..........................................183
Практикум. Создание таблицы поиска ..................................186
Резюме ..............................................................187
Закрепление материала ...............................................188
Занятие 4. Применение специализированных наборов .......................188
Работа с двоичными значениями (битами) ..............................189
Хранение строк в наборах ............................................194
Класс NameValueCollection .........................................  197
Практикум. Нечувствительная к регистру таблица поиска, поддерживающая локализацию .........................................................199
Резюме ..............................................................201
Закрепление материала ...............................................201
Занятие 5. Обобщенные наборы ...........................................201
Работа с обобщениями ................................................202
Улучшение контроля типов и повышение производительности .............207
Структура класса обобщенного набора .................................215
Практикум. Создание и использование обобщенных наборов ..............218
Резюме ..............................................................219
Закрепление материала ...............................................219
Закрепление материала главы ............................................220
Резюме главы ...........................................................220
Основные термины .....................................................  220
Лабораторная работа ....................................................221
Занятие 1. Использование Array List для хранения кодов состояний ....221
Занятие 2. Выбор подходящего набора .................................221
Занятие 3. Добавление наборов с поддержкой контроля типов ...........221
Рекомендуемые упражнения ...............................................222
Применение обобщенных наборов .......................................222
Сравнение классов Dictionary ........................................222
Пробный экзамен ......................................................  223
Глава 5 Сериализация ..............................
Занятие 1. Сериализация объектов ..................
Введение в сериализацию ........................
Как сериализовать объект .......................
Как десериализовать объект .....................
Создание сериализуемых классов..................
Выбор формата сериализации .....................
Использование SoapFormatter ....................
Управление сериализацией SOAP...................
Советы по сериализации .........................
Практикум. Сериализация и десериализация объектов Резюме .............ч...........................
Закрепление материала ..........................
Занятие 2. Сериализация XML .......................
Применение сериализация XML ....................
Использование XML для сериализации объекта .....
Использование XML для десериализации объекта .... Создание классов, сериализуемых в формат XML .... Управление сериализацией XML ...................
Реализация требований схемы XML ................
Как сериализовать объект DataSet ...............
Практикум. Использование сериализации XML ......
Резюме .........................................
Закрепление материала .................................................
Занятие 3. Собственные методы сериализации ...............................
Как реализовать Собственную.сериализацию ..............................
Обработка событий сериализации ........................................
224
225
225
226
228
230
234
235
235
236
236
240
240
241
242
242
243
244
245
248
249
250
252
252
253
254
256
Содержание
IX
Изменение сериализации по контексту .................................259
Как создать собственный форматирующий объект ........................260
Практикум. Реализация собственной сериализации ......................260
> Резюме ...............................................................262
Закрепление материала ...............................................262
Закрепление материала главы ............................................263
Резюме главы ...........................................................263
Основные термины .......................................................263
Лабораторная работа ....................................................264
Выбор способа сериализации ..........................................264
Вопросы .............................................................264
Сериализация в различных версиях приложения .........................264
Вопросы ..........................................*..................264
Рекомендуемые упражнения ...............................................265
Сериализация и десериализация объектов и графов объектов ............265
Управление сериализацией в XML с использованием пространства имен System.Xml.Serialization ............................................265
Реализация собственной сериализации при помощи классов форматирования ... 265
Пробный экзамен ........................................................266
Глава 6 Программирование графических элементов .........................267
Занятие 1. Рисование графических элементов .............................268
Пространство имен System. Drawing ...................................268
Определение положения и размера элементов управления .................271
Определение цветов элементов управления .............................272
Рисование линий и фигур .............................................272
Настройка перьев ....................................................276
Заливка фигур .......................................................278
Практикум. Создание метода для рисования круговой диаграммы .........280
Резюме ..............................................................285
Закрепление материала ...............................................286
Занятие!. Работа с изображениями .......................................287
Классы Image и Bitmap ...............................................287
Вывод изображений на экран ..........................................288
Создание и сохранение рисунков ......................................289
Работа со значками ..................................................290
Практикум. Сохранение круговой диаграммы ............................290
Резюме ..............................................................291
Закрепление материала ...............................................291
Занятие 3. Форматирование текста .....................................  292
Добавление текста на графические элементы ...........................293
Создание объекта Font................................................293
Прорисовка текста ...................................................294
Форматирование текста ...............................................295
Практикум. Добавление текста на изображение .........................297
Резюме ............................................................  303
Закрепление материала ...............................................303
Закрепление материала главы ............................................304
Резюме главы ...........................................................304
Основные термины .......................................................305
Лабораторная работа ....................................................305
Занятие 1. Выбор метода работы с графикой ...........................305
Занятие 2. Создание простых диаграмм ................................306
Рекомендуемые упражнения ...............................................306
Улучшение пользовательского интерфейса с помощью кистей, перьев, , цветов и шрифтов ...................................................  306
v Улучшение пользовательского интерфейса с помощью графических элементов и значков ............................307
. * Улучшение пользовательского интерфейса с помощью фигур .............307
Пробный экзамен ........................................................307
X
Содержание
Глава? Потоки ......................................................308
Занятие 1. Создание потоков ............................................309
Введение в потоки ...................................................309
Передача данных потокам .............................................316
Остановка потоков ...................................................318
Контекст выполнения .................................................320
Практикум. Многопоточный код, использующий класс Thread ........322
Резюме ..............................................................324
Закрепление материала ...............................................324
Занятие 2. Общий доступ к данным .......................................324
Предотвращение коллизий .............................................325
Синхронизация с помощью блокировок ..................................328
Практикум. Использование объекта Mutex для создания приложения,
исполняемого в единственном экземпляре ..............................344
Резюме ..............................................................346
Закрепление материала ...............................................347
Занятие 3. Асинхронная модель программирования .........................347
Суть асинхронного программирования ..................................348
Использование ThreadPool ............................................355
Применение объектов Timer .......................................361
Практикум. Создание очереди рабочих элементов при помощи ThreadPool .362
Резюме ..............................................................363
Закрепление материала ...............................................364
Закрепление материала главы ............................................364
Резюме главы ...........................................................364
Основные термины .......................................................365
Лабораторная работа ....................................................365
Занятие 1. Повышение эффективности обработки данных на сервере ......365
Занятие 2. Работа с несколькими приложениями	........................366
Рекомендуемые упражнения ...............................................366
Создание приложения с ThreadPool ....................................366
Пробный экзамен ........................................................367
Глава 8 Домены приложений и службы .......................................
Занятие!. Создание доменов приложений ....................................
Введение в домены приложения ..........................................
Класс AppDomain .......................................................
Как создать домен приложения ..........................................
Загрузка сборок в домен приложения ....................................
Выгрузка доменов приложений ...........................................
Практикум. Создание доменов и загрузка сборок .........................
Резюме ................................................................
Закрепление материала .................................................
Занятие 2. Конфигурирование доменов приложений ...........................
Запуск сборок с ограниченными привилегиями ............................
Настройка свойств домена приложения ...................................
Практикум. Управление привилегиями домена приложения ..................
Резюме ................................................................
Закрепление материала .................................................
Занятие 3. Создание служб Windows ........................................
Введение в службы Windows .............................................
Создание проекта службы ...............................................
Реализация службы .....................................................
Создание установочной программы для службы ............................
Управление службами и их настройка ....................................
Практикум. Создание, установка и запуск службы мониторинга Wfeb-сайта .
Резюме ................................................................
Закрепление материала .................................................
Закрепление материала главы ..............................................
Резюме главы .............................................................
368
369
369
371
373
374
374
374
376
376
377
377
379
381
382
382
384
384
386
387
387
390
392
396
396
397
397
Содержание
XI
Основные термины .......................................................397
Лабораторная работа ....................................................398
Занятие 1. Создание инструмента для тестирования ....................398
Занятие 2. Мониторинг файлов ........................................399
Рекомендуемые упражнения ...............................................399
Создание блоков изоляции для CLR при помощи доменов приложений ......400
Создание, установка и управление службой ............................400
Пробный экзамен ........................................................400
Глава 9 Установка и конфигурирование приложений ........................401
Занятие 1. Настройка конфигурации ......................................402
Конфигурирование в .NET Framework 2.0................................403
Общие параметры .....................................................408
Параметры настройки приложения ...................4..................419
Практикум. Получение строки подключения к базе данных ...............425
Резюме ..............................................................431
Закрепление материала ...............................................432
Занятие!. Создание установщика .........................................433
Использование базового установщика ..................................433
Фиксация установки ..................................................437
Откат установки .....................................................438
Практикум. Создание и удаление разделов реестра .....................439
Резюме ..............................................................440
Закрепление материала ...............................................440
Занятие 3. Использование утилиты .NET Framework 2.0 Configuration ......441
Просмотр конфигураций ...............................................442
Изменение конфигурации ..............................................443
Сброс конфигурации ..................................................447
Практикум. Изменение и восстановление параметров настройки приложения .... 448
Резюме ..............................................................449
Закрепление материала ...............................................449
Занятие 4. Управление конфигурацией ....................................450
Получение и хранение параметров настройки ...........................450
Реализация интерфейсов конфигурации .................................452
Практикум. Чтение и запись параметров конфигурации ..................460
Резюме ..............................................................462
Закрепление материала ...............................................463
Закрепление материала главы ............................................463
Резюме главы ...........................................................464
Основные термины .......................................................464
Лабораторная работа. Установка и настройка приложения ..................464
Рекомендуемые упражнения ...............................................465
Создание строк подключения в файле конфигурации .....................465
Пробный экзамен ........................................................466
Глава 10 Инструментарий для мониторинга ................................467
Занятие 1. Регистрация событий .........................................468
Работа с событиями Microsoft Windows и их регистрация ...............468
Создание и удаление журнала событий .................................470
Запись в журнал событий .............................................471
Чтение из журнала событий ...........................................474
Практикум. Создание и использование журнала событий в приложении ....475
Резюме ..............................................................476
Закрепление материала ...............................................477
Занятие 2. Отладка и трассировка .......................................478
Запись вывода, генерируемого кодом ..................................478
Отладочные атрибуты .................................................485
Создание слушателей трассировки .....................................492
. Объекты Listener .....................................................494
Практикум. Создание журнала событий приложения и работа с ним .......499
XII
Содержание
Резюме ................................................................
Закрепление материала .................................................
Занятие 3. Мониторинг производительности ..................................
Введение в процессы ...................................................
Класс Process .........................................................
Перечисление процессов ................................................
Применение счетчиков производительности ...............................
Класс CounterCreationData .............................................
Класс PerformanceCounterCategory ......................................
Класс Performancecounter ..............................................
Запуск процессов ......................................................
Классы StackTrace и StackFrame ........................................
Практикум. Мониторинг производительности приложений ...................
Резюме ................................................................
Закрепление материала .................................................
Занятие 4. Обнаружение управляющих событий ................................
Перечисление управляющих объектов .....................................
Перечисление логических дисков ........................................
Перечисление сетевых адаптеров ........................................
Поиск информации о приостановленных сервисах ..........................
Подписка на уведомление об управляющих событиях с помощью ManagementEventWatcher ................................................
Практикум. Регистрация управляющих событий в журнале ..................
Резюме ................................................................
Закрепление материала .................................................
Закрепление материала главы ...............................................
Резюме главы ..............................................................
Основные термины ..........................................................
Лабораторная работа .......................................................
Лабораторная работа. Перенаправление вывода ...........................
Рекомендуемые упражнения ..................................................
Добавление функций настройки, управления и установки в .NET-приложения .. Пробный экзамен ...........................................................
Глава 11 Безопасность приложений ..........................................
Занятие!. Защита доступа по правам кода ...................................
Что такое защита доступа по правам кода? ..............................
Элементы CAS ..........................................................
Политика безопасности .................................................
Взаимодействие CAS и системы безопасности операционной системы ........
Настройка CAS при помощи утилиты .NET Framework Configuration .........
Утилита Caspol ........................................................
Практикум. Настройка CAS ..............................................
Резюме ................................................................
Закрепление материала .................................................
Занятие2. Декларативная защита сборок .....................................
Причины использования объявлений сборок CAS ...........................
Классы для реализации разрешений CAS ..................................
Типы объявлений разрешений сборки .....................................
Создание объявлений сборок ............................................
Рекомендации по использованию объявлений сборок .......................
Практикум. Использование запросов на разрешения сборок ................
Резюме ................................................................
Закрепление материала .................................................
Занятие 3. Декларативная и императивная защита методов ....................
Типы запросов на разрешения методов ...................................
Инструкции по применению запросов на разрешения методов ...............
Способы запроса разрешений ............................................
Способы ограничения разрешений ........................................
Ослабление строгости разрешений для повышения производительности ........
500
500
501
502
503
503
506
508
509
511
512
515
517
519
520
521
521
522
523
525
525
526
528
528
529
529
529
529
530
530
530
530
531
533
533
534
540
541
542
546
551
553
554
554
555
556
558
559
561
561
563
563
567
567
569
569
575
577
Содержание XIII
Вызов доверенного кода из кода с частичным доверием ....................581
Использование наборов разрешений .......................................581
Практикум. Защита методов при помощи запросов CAS ......................582
Резюме .................................................................587
Закрепление материала ..................................................587
Закрепление материала главы ...............................................588
Резюме главы ..............................................................589
Основные термины ..........................................................589
Лабораторная работа .......................................................589
Упражнение 1. Объяснение модели CAS ....................................589
Упражнение 2. Настройка защиты CAS .....................................590
Рекомендуемые упражнения ..................................................590
Реализация защиты доступа по правам кода ...........,...................590
Управление разрешениями на доступ к ресурсам с применением классов System. Security. Permission ...........................................591
Управление привилегиями кода при помощи классов System.Security.Policy .591
Пробный экзамен ...........................................................591
Глава 12 Безопасность пользователя и данных ...............................592
Занятие 1. Аутентификация и авторизация пользователей .....................594
Введение в аутентификацию и авторизацию ................................594
Класс Windowsldentity ..................................................595
Класс WindowsPrincipal .................................................597
Класс PrincipalPennission...............................................600
Декларативное ограничение доступа к методам по ролям ...................600
Императивная RBS для фрагментов кода ...................................602
Реализация нестандартных пользователей и ролей .........................604
Обработка исключений аутентификации в потоках ..........................613
Практикум. Добавление в приложение защиты, основанной на ролях .........614
Резюме .................................................................618
Закрепление материала ..................................................619
Занятие!. Работа со списками управления доступом ..........................620
Введение в DACL ........................................................621
Введение в SACL ........................................................622
Программный просмотр и конфигурирование ACL ............................623
Практикум. Работа с DACL ...............................................626
Резюме .................................................................627
Закрепление материала ..................................................627
Занятие 3. Шифрование и расшифровка данных ................................629
Шифрование и расшифровка с симметричным ключом .........................629
Асимметричное шифрование и расшифровка данных ..........................638
Проверка целостности данных с помощью хэш-функций ......................644
Подписывание файлов ....................................................648
Практикум. Шифрование и расшифровкам файлов ............................652
Резюме .................................................................656
Закрепление материала ..................................................657
Закрепление материала главы ...............................................658
Резюме главы ..............................................................658
Основные термины ..........................................................658
Лабораторная работа .......................................................659
’ Упражнение I. Создание нестандартных методов аутентификации ...........659
Упражнение 2. Криптографическая защита данных ..........................660
' Рекомендуемые упражнения ................................................661
Реализация нестандартной аутентификации с помощью классов System.Security.Authentication .........................................661
Модификация удостоверений с помощью классов System.Security.Pnncipal ...661
Реализация управления доступом с помощью классов System.Security.AccessControl ..........................................661
Шифрование, расшифровка и хэширование с помощью классов System. Security.Cryptography ..........................................661
Пробный экзамен ...........................................................662
XIV
Содержание
Глава 13 Технологии Interop ............................................663
Занятие 1. Применение СОМ-объектов .....................................664
Импорт библиотек типов ..............................................665
Импорт типа при помощи Visual Studio 2005 .......................... 666
Применение Tlblmp.exe для импорта типа ..............................666
Инструменты для COM Interop .........................................668
Использование в коде СОМ-объектов ...................................669
Обработка исключений в COM Interop ..................................670
Ограничения COM Interop .............................................671
Практикум. Работа с COM-приложением в .NET ..........................672
Резюме ..............................................................673
Закрепление материала ...............................................673
Занятие!. Предоставление COM-компоненту доступа к .NET-компонентам .....674
Создание .NET-компонентов для использования в СОМ ...................674
Сокрытие открытых классов .NET от СОМ ...............................676
Развертывание сборок д ля СОМ .......................................678
Практикум. Создание сборки, пригодной для использования кодом СОМ ...679
Резюме ..............................................................681
Закрепление материала ...............................................681
Занятие 3. Работа с неуправляемым кодом ................................681
Platform Invoke .....................................................682
Инкапсулирование функций DLL ........................................684
Преобразование типов данных .........................................684
Маршалинг структур ..................................................686
Использование обратных вызовов с неуправляемым кодом ................690
Обработка исключений ................................................692
Ограничения при работе с неуправляемым кодом ........................695
Практикум. Вызов Windows-функций из DLL .............................696
Резюме ..............................................................696
Закрепление материала ...............................................696
Закрепление материала главы ............................................697
Резюме главы ...........................................................697
Основные термины .......................................................698
Лабораторная работа. Встраивание унаследованного кода в проект .NET ....698
Результаты опроса ...................................................698
Рекомендуемые упражнения ...............................................699
Пробный экзамен ........................................................700
Глава 14 Отражение .....................................................701
Занятие 1. Введение в отражение ........................................702
Введение в сборки и модули ..........................................702
Анализ сборки .......................................................704
Практикум. Изучение сборки средствами .NET ..........................707
Резюме ..............................................................709
Закрепление материала ...............................................710
Занятие!. Атрибуты сборки ..............................................710
Системные атрибуты ..................................................710
Получение атрибутов сборки ..........................................715
Практикум. Определение и вывод атрибутов сборки во время выполнения .716
Резюме ..............................................................717
Закрепление материала ...............................................717
Занятие 3. Отражение и типы ............................................718
Получение типов .....................................................718
Члены типа-перечислителя ............................................723
Работа с BindingFlags ...............................................726
Практикум. Загрузка сборки и вывод ее информации о типе .............728
Резюме ..............................................................730
Закрепление материала ...............................................730
Содержание XV
Занятие 4. Динамическая генерация кода..................................730
Работа с динамическим кодом .........................................731
Практикум. Вызов членов посредством отражения .......................734
Резюме ..............................................................736
Закрепление материала ...............................................736
Занятие 5. Генерация кода во время выполнения ..........................737
Сборка с собственным кодом ..........................................738
Практикум. Создание динамической сборки .............................743
Резюме ..............................................................745
Закрепление материала ...............................................745
Закрепление материала главы ............................................746
Резюме главы ...........................................................746
Основные термины .......................................................746
Лабораторная работа. Конструирование архитектуры подключаемых модулей ..747
Рекомендуемые упражнения ...............................................747
Приложение Assembly Explorer ......................................  747
Пробный экзамен ........................................................748
Глава 15 Электронная почта .............................................749
Занятие 1. Создание почтового сообщения ................................750
Процедура создания и отправки сообщений электронной почты ...........750
Создание объекта MailMessage .........................................751
Прикрепление файлов .................................................753
Создание сообщений в формате HTML ...................................753
Практикум. Создание объекта MailMessage .............................756
Резюме ..............................................................758
Закрепление материала ...............................................759
Занятие!. Отправка почты ...............................................760
Отправка сообщений ..................................................760
Обработка исключений, возникающих при отправке почты ..............  761
Настройка учетных данных ............................................762
Настройка SSL .......................................................763
Асинхронная отправка сообщений ......................................763
Практикум. Отправка почтового сообщения .............................765
Резюме ..............................................................769
Закрепление материала ...............................................770
Закрепление материала главы ............................................771
Резюме главы ...........................................................771
Основные термины ........................................................771
Лабораторная работа. Добавление функций отправки почты .................771
Рекомендуемые упражнения ...............................................772
Отправка почтового сообщения на SMTP-сервер из приложения .NET Framework ........................................772
Пробный экзамен ......................................................  773
Глава 16 Глобализация ................................................. 774
Занятие 1. Использование информации о культуре .........................775
Класс Cultureinfo ...................................................775
Перечислимое CultureTypes ...........................................779
Класс Regioninfo ....................................................780
Классы DateTimeFormatlnfo и NumberFormatlnfo ........................781
Использование класса Compareinfo и перечислимого CompareOptions для сравнения с учетом культуры .....................................783
Практикум. Создание кода, учитывающего культуру .....................786
Резюме ..............................................................787
Закрепление материала ...............................................788
Занятие!. Создание собственной культуры ................................789
Практикум. Создание собственной культуры ............................791
Резюме ..............................................................792
Закрепление материала ...............................................792
XVI Содержание
Закрепление материала главы ...........................................793
Резюме главы ..................................................7	...«.. 793
Основные термины ......................................................794
Лабораторная работа ...................................................794
Установка и настройка нового приложения ............................794
Рекомендуемые упражнения ..............................................795
Использование информации о культуре ................................795
Создание собственной культуры ......................................795
Пробный экзамен .......................................................795
Ответы ................................................................796
Словарь терминов ......................................................837
ИЙ

Введение
Этот учебный курс предназначен для разработчиков, планирующих сдать сертификационный экзамен 70-536 по программе Microsoft MCTS, а также желающих научиться создавать приложения, использующие .NET Framework 2.0. Предполагается, что читатели имеют практический опыт работы с Windows и программирования на языке Visual Basic или С#.
Освоив материал этого учебного курса, вы научитесь:
	создавать приложения, использующие системные типы и наборы;
	создавать реализацию служб, процессов, потоков и доменов приложений с целью изоляции приложений и поддержки многопоточности;
	создавать и развертывать управляемые приложения;
	писать классы, поддерживающие сериализацию для облегчения их передачи и хранения;
	создавать надежно защищенные приложения, устойчивые к атакам и поддерживающие разграничение доступа на основе ролей пользователей и групп;
	организовывать взаимодействие с другими приложениями и использовать унаследованный код при помощи технологий взаимодействия и отражения;
	писать приложения, способные отправлять сообщения электронной почты;
	создавать приложения, пригодные для использования в различных регионах с населением, говорящих на разных языках;
	программно рисовать диаграммы и создавать другие изображения для вывода на экран и сохранения в файлы.
Требования к оборудованию
Для выполнения практических заданий этого курса необходимо следующее оборудование:
	компьютер, оснащенный процессором с тактовой частотой 600 МП1 или более быстрый (рекомендуется 1-ГГц процессор);
	192 Мб ОЗУ или больше (рекомендуется 512 Мб);
	2 Гб свободного места на жестком диске;
	привод DVD-ROM;
	дисплей с разрешением 1,024 х 768 пикселов, способный отображать минимум 256 цветов;
» клавиатура, мышь или другое совместимое указывающее устройство.
XVIII Введение
Требования к программному обеспечению
Для выполнения практических заданий этого курса необходимо следующее ПО:
 одна из следующих операционных систем:
□	Windows 2000 Service Pack 4;
□	Windows XP Service Pack 2;
□	Windows XP Professional x64 Edition (WOW);
□	Windows Server 2003 Service Pack 1;
□	Windows Server 2003, x64 Edition (WOW);
□	Windows Server 2003 R2;
□	Windows Server 2003 R2, x64 Edition (WOW);
□	Microsoft Windows Vista.
	Visual Studio 2005.
О прилагаемом компакт-диске
В состав учебного курса входит компакт-диск со следующим содержимым:
	Пробный экзамен
Проверить и закрепить полученные знания и навыки создания приложений, использующих .NET Framework 2.0, помогут электронные тесты, составленные из вопросов раздела «Закрепление материала» этой книги; читатели смогут самостоятельно выбирать вопросы. Также можно пройти пробный сертификационный экзамен 70-536. Для этого на диске содержится более 300 вопросов реального экзамена — этого достаточно, чтобы освоить процедуру опроса и оценить степень своей готовности к экзамену.
	Примеры кода
Для большинства практических занятий в этой книге имеются файлы примеров кода и другие дополнительные материалы. Перед выполнением ряда упражнений вам потребуется загрузить соответствующий проект-пример. В других случаях вы создадите проект самостоятельно; при возникновении затруднений можно сравнить свой проект с готовым проектом, имеющимся на прилагаемом диске.
	Электронная книга
На прилагаемом диске также находится электронная версия этой книги (на английском языке), которую можно носить с собой вместо печатной копии. Электронная книга записана в формате PDF, для ее просмотра необходима программа Adobe Acrobat или Adobe Reader.
Установка заданий пробного экзамена
Чтобы установить задания пробного экзамена с прилагаемого диска на жесткий диск своего компьютера, выполните следующие действия:
	Вставьте прилагаемый диск в привод компьютера, примите лицензионное соглашение — откроется меню компакт-диска.
Введение
XIX
ПРИМЕЧАНИЕ Если меню не открывается
Если меню не открывается и лицензионное соглашение не выводится, возможно, на вашем компьютере отключена функция AutoRun. О других способах установки см. в файле Readme.txt на прилагаемом диске.
	Щелкните пункт Practice Tests и следуйте инструкциям программы установки.
О пробном экзамене
г
Чтобы запустить пробный экзамен, выполните следующие действия:
	Щелкните Пуск | Все программы | Microsoft Press Training Kit Exam Prep (Start | All Programs | Microsoft Press Training Kit Exam Prep) — откроется окно co списком пробных экзаменов из учебных курсов Microsoft, установленных на вашем компьютере.
	Дважды щелкните занятие, материал которого необходимо закрепить (lesson review) либо пробный экзамен (practice test).
ПРИМЕЧАНИЕ Закрепление материала и пробный экзамен
Закрепление материала (lesson review) включает выборку вопросов из одноименного раздела книги, а пробный экзамен (practice test) — выборку из 300 вопросов, близких к вопросам сертификационного экзамена 70-536.
Закрепление материала
При выборе закрепления материала открывается окно Custom Mode, которое позволяет настроить процедуру тестирования. Щелкнув ОК, можно принять параметры по умолчанию либо настроить число вопросов, режим опроса, выбрать темы, а также включить опрос «на время». При повторном закреплении материала можно ответить на все вопросы либо только на те, на которые вы не смогли ответить правильно в прошлый раз.
После щелчка ОК начнется опрос.
	Во время теста для навигации по вопросам используются кнопки Next, Previous и Go lb.
	Ответив на вопрос, вы можете проверить правильность своего ответа и получить правильный ответ с пояснениями, щелкнув кнопку Explanation.
	Для просмотра результатов теста ответьте на все вопросы и щелкните Score Test. Вы увидите процент правильных ответов по каждой из тем экзамена, а также сводные итоги. Далее можно распечатать итоги теста, проанализировать свои ответы либо повторить тест.
Пробный экзамен
Пробный экзамен поддерживает режим сертификационного экзамена (Certification Mode), режим обучения (Study Mode) и пользовательский режим (Custom Mode):
	Certification Mode
— воспроизводит реальный сертификационный экзамен. В этом режиме необходимо ответить на предлагаемые вопросы за ограниченное время, при этом невозможно ни приостановить, ни перезапустить таймер.
XX
Введение
 Study Mode
— тест без ограничения по времени и возможностью просмотра правильных ответов с пояснениями для каждого вопроса.
 Custom Mode
— допускает произвольную настройку параметров тестирования.
Пользовательский интерфейс разных режимах практически идентичен, отличаются лишь доступные параметры (см. выше).
С пояснениями правильных ответов для каждого вопроса отображается список ссылок (References) на соответствующие разделы учебного курса и источники дополнительной информации. Щелчок Test Results выводит итоги теста, на вкладке Learning Plan отображается список материалов, которые необходимо изучить для освоения каждой темы.
Удаление ПО пробного экзамена
Чтобы удалить ПО пробного экзамена, воспользуйтесь аплетом Установка и удаление программ (Add Or Remove Programs) Панели управления Windows.
Программа сертификации специалистов Microsoft
Программа сертификации специалистов Microsoft (Microsoft Certified Professional, MCP) — отличная возможность подтвердить Ваше знание современных технологий и программных продуктов этой фирмы. Лидер отрасли в области сертификации, Microsoft разработала современные методы тестирования; экзамены и программы сертификации подтвердят Вашу квалификацию разработчика или специалиста по реализации решений на основе технологий и программных продуктов Microsoft. Сертифицированные Microsoft профессионалы квалифицируются как эксперты и высоко ценятся на рынке труда. Сертифицированным специалистам и организациям доступен ряд преимуществ (подробнее см. по ссылке, приведенной ниже).
ПРИМЕЧАНИЕ Сведения о сертификации специалистов Microsoft
Полные сведения о сертификатах Microsoft см. по адресу to www.microsoft.com/leaming/ mcp/default. asp.
Техническая поддержка
Мы постарались сделать все от нас зависящее, чтобы и учебный курс, и ппилагаемый к нему компакт-диск не содержали ошибок. Если все же у вас возникнут вопросы или вы захотите поделиться своими предложениями или комментариями, обращайтесь в издательство Microsoft Press по одному из указанных ниже адресов:
Введение )Q(|
Электронная почта: tkinput@microsoft.com
Почтовый адрес:
Microsoft Press
Attn: MCTS Self-Paced Training Kit (Exam 70-536): Microsoft .NET Framework 2.0 —
Application Development Foundation Editor
One Microsoft Way
Redmond, WA 98052-6399
Издательство Microsoft Press публикует постоянно обновляемый список исправлений и дополнений к своим книгам по адресу http://mspress.microsoft.com/support/books/. База знаний Microsoft Knowledge Base доступна по ссылке http://support.microsoft.com/search/. Сведения о технической поддержке программных продуктов Microsoft см. на сайте http://support.microsoft.com.
Благодарности
На обложке читатель увидит имена авторов, но авторы — лишь часть большой команды, работавшей над книгой. Прежде всего, благодарим Кена Джонса (Ken Jones) из Microsoft за то, что он пригласил нас участвовать в проекте по изданию этой книги. Во время работы над текстом мы тесно сотрудничали с Лорой Сэкерман (Laura Sackerman) и Морин Циммерман (Maureen Zimmerman), также из Microsoft. Мы благодарим их за то, что они тактично и терпеливо учили нас делиться своим опытом с читателями. Не все сотрудники Microsoft работали с нами непосредственно, но помощь следующих людей заслуживает искренней благодарности:
Розмэри Кэйпертон (Rosemary Caperton) и Сэнди Резник (Sandi Resnick) — за координацию и корректуру;
Карл Дилтц (Carl Diltz) — за разработку оригинал-макета книги;
Пэтти Мэссерман (Patty Masserman) — за составление предметного указателя;
Билл Тил (Bill Teel) — за подготовку иллюстраций.
Нам помогала не только команда Microsoft, так, Джим Фухс (Jim Fuchs) и Дэвид Робинсон (David Robinson) работали над книгой в качестве технических редакторов. Ребята, огромное спасибо за выловленные вами ошибки — иначе о них бы сообщили наши внимательные читатели! Благодарим также нашего литературного редактора, Роджера Леблана (Roger LeBlanc), за избавление от грамматических ошибок.
Авторы также благодарят:
Тони Нортроп: в работе над книгой также тем или иным образом приняли участие следующие лица. Мои друзья, особенно Тара, Джон и Эмили Бэнкс, Кристин Касциа-то, Курт и Беатрис Диллард, Эрик и Алиса Фолкнер; Крис и Дайана Геггис, Боб Хоган, Сэм Джексон, Том Киган, Эрик и Элисон Паруки, а также Тодд Уэши скрашивали мое существование, когда я был не за клавиатурой. Благодарю также родственников за то, что время от времени отвлекали меня (а отвлекался я, лишь когда они приносили печенье): Майк, Мишель, Рэй, Сэнди Эдсон — спасибо вам! Больше же всех я благодарен своей жене Эрике за ее безмерное терпение во время моей работы над книгой.
Шон Уилдермьюс: благодарю Криса Селлза (Chris Sells) за то, что он пригласил меня и Кена Джонса поучаствовать в этом проекте.
ГЛАВА 1
Основы .NET Framework
Занятие 1.	Применение значимых типов	2
Занятие 2.	Использование общих ссылочных типов	15
Занятие 3.	Конструирование классов	28
Занятие 4.	Преобразование типов	47
Инфраструктура .NET Framework — неотъемлемая часть ОС Windows, разработанная для поддержки приложений и служб нового поколения. Многое в .NET Framework будет уже знакомо программистам с опытом использования других объектно-ориентированных средств разработки; однако, .NET Framework включает множество уникальных элементов, которые будут в новинку даже самым опытным разработчикам. В этой главе приводятся основные сведения об инфраструктуре .NET Framework, необходимые для изучения остального материала.
ПРИМЕЧАНИЕ .NET 2.0
Если вы работали с прежними версиями .NET Framework (ниже 2.0), многое будет вам знакомо. Однако в версии 2.0 появились новшества: обобщения, частичные классы и переадресация типов (см. занятие 3).
Темы экзамена:
 Управление данными приложений .NET Framework с использованием системных типов .NET Framework 2.0. (см. пространство имен System):
□	значимые типы;
□	ссылочные типы;
□	атрибуты;
□	обобщения;
□	классы исключений;
□	упаковка и распаковка;
□	класс TypeForwardedToAitribute.
2
Основы .NET Framework
Глава 1
 Реализация интерфейсов .NET Framework с целью обеспечения совместимости компонентов и стандартных контрактов (см. пространство имен System):
□	интерфейс 1СотрагаЫе\
□	интерфейс IDisposable;
□	интерфейс IConvertible;
□	интерфейс ICloneable;
□	интерфейс lEquatable;
□	интерфейс IFormattable.
 Управление прикладными компонентами .NET Framework при помощи событий и делегатов (см. пространство имен System):
□	класс Delegate',
□	класс EventArgs',
□	делегаты EventHandler.
Прежде всего
Предполагается, что читатель имеет минимум двухлетний опыт разработки Web-, Windows- или распределенных приложений с использованием .NET Framework 1.0, .NET Framework 1.1 и .NET Framework 2.0 Также требуется умение работы в Microsoft Visual Studio 2005, применения Microsoft Visual Basic или C# и твердые навыки решения следующих задач:
	создание консольных приложений и приложений Windows Forms в Visual Studio при помощи Visual Basic или С#;
	добавление в проект пространств имен и ссылок на системные библиотеки классов;
	запуск проекта в Visual Studio, установка точек прерывания, пошаговое выполнение кода и наблюдение за значениями переменных.
Занятие 1. Применение значимых типов
В .NET Framework простейшие типы, в основном числовые и Булевы, являются значимыми. Значимые типы — это переменные, содержащие сами данные, а не ссылку на них. Экземпляры значимых типов хранятся в области памяти под названием стек (stack), в которой исполняющая среда создает, читает, обновляет и удаляет их быстро и с минимальной затратой ресурсов.
-----------------------4 ......... .......... , ........................ ..
К СВЕДЕНИЮ Ссылочные типы
Подробнее о ссылочных типах см. в занятии 2.
Существует три основных значимых типа:
	встроенные типы;
	пользовательские типы;
	перечислимые.
Все они — потомки базового типа System. Value. В следующих разделах будет показано, как ими пользоваться.
Занятие 1
Применение значимых типов
3
Изучив материал этого занятия, вы сможете:
J выбирать наиболее подходящий встроенный значимый тип;
J объявлять значимые типы;
Z создавать собственные типы;
J использовать перечислимые.
Продолжительность занятия — 30 минут.
Встроенные значимые типы
Встроенные типы — это базовые типы, поставляемые с .NET Framework, на их основе строятся все остальные типы. Все встроенные числовые типы являются значимыми. Числовой тип следует выбирать, исходя из размера его значения и желаемой точности вычислений. В табл. 1-1 перечислены наиболее востребованные числовые типы в порядке возрастания размера. Первые шесть типов используются для работы с целочисленными значениями, а последние три представляют действительные числа (упорядочены по возрастанию точности).
Табл. 1-1. Встроенные значимые типы
Ъш (Visual Basic/ псевдоним С#)	Размер в байтах	Диапазон значений	Применение
System.SByte байт со знаком (SByte/sbyte)	1	-128-127	Значение длиной один
System.Byte байт без знака (Byte/byte)	1	0-255	Значение длиной один
System.Intl6 (Short/short)	2	-32768-32767	Взаимодействие между компонентами и другие особые задачи
System.Int32 (Integer/ini')	4	-2147483648-2147483647	Целые числа и счетчики
System. UInt32 (Ulnteger/uint)	4	0-4294967295	Положительные целые числа и счетчики
System.Int64 (Long/long)	8	—9223372036854775808— 9223372036854775807	Большие целые числа
System.Single (Single/float)	4	—3.402823Е+38— 3 402823Е+38	Числа с плавающей запятой
System.Double (Double/double)	8	-1.79769313486232Е+308— 1.79769313486232Е+308	Точные или большие числа с плавающей запятой
System.Decimal (Decimal/decimal)	16	-79228162514264337593543950335- 79228162514264337593543950335	Финансовые и научные вычисления, требующие высокой точности
4
Основы .NET Framework
Глава 1
СОВЕТ Оптимизация производительности встроенных типов
Исполняющая среда оптимизирует производительность 32-разрядных целочисленных типов (Int32 и UInt32), поэтому их следует использовать для счетчиков и других часто используемых переменных. Для операций с плавающей запятой лучше всего подходит тип Double, поскольку такие операции оптимизированы аппаратно.
Эти числовые типы используются настолько часто, что в Visual Basic и C# для них определены псевдонимы. Псевдоним — это сокращенный эквивалент полного имени типа; большинство программистов предпочитает использовать более короткие псевдонимы. Существуют и другие значимые типы, кроме числовых (см. табл. 1-2).
Табл. 1-2. Другие значимые типы
Тйп (Visual Basic/ псевдоним С#)	Размер в байтах	Диапазон значений	Применение
System.Char (Char/char)	2	-	Одиночные символы Unicode
System. Boolean (Boolean/bool)	4	-	Значения True/False
System.IntPtr (нет)	Зависит от платформы	-	Указатель на адрес памяти
System.DateTime (Date/date)	8	1/1/0001 12:00:00 АМ-12/31/9999 11:59:59 РМ	Время
В .NET Framework существует более 300 значимых типов, но обычно удается обойтись приведенными выше. При присваивании переменной значения другой переменной данные копируются из второй переменной в первую, в стеке остаются две копии этих данных. Иначе обстоят дела со ссылочными типами, которые рассматриваются на занятии 2.
Хотя значимые типы часто представляют простые значения, они все равно функционируют как объекты, то есть поддерживают методы. В действительности, для вывода значения в виде текста обычно используется метод ToString значимого типа. Обычно для этих целей переопределяется метод ToString базового типа System.Object.
ПРИМЕЧАНИЕ Базовый класс Object
В .NET Framework все типы порождаются от типа System.Object. Такое родство позволяет создать общую систему типов, используемую в . NET Framework.
Объявление значимых типов
Чтобы использовать тип, сначала нужно объявить символ как экземпляра этого типа. Значимые типы поддерживают неявные конструкторы, так что при объявлении такого типа его экземпляр создается автоматически, без употребления ключевого слова New (в отличие от классов, где оно необходимо). Конструктор присваивает новому экземпляру значение по умолчанию (обычно null или 0), но переменную следует явно инициализировать внутри объявления:
Занятие 1
Применение значимых типов
5
ПРИМЕЧАНИЕ Различия в ключевых словах Visual Basic и C#
Одним из незначительных различий ключевых слов в Visual Basic и C# является то, что в Visual Basic их принято писать с заглавной буквы, а в C# — со строчной. Для удобочитаемости в тексте этой книги все ключевые слова пишутся с заглавной буквы, а примеры кода всегда приводятся для обоих языков.
’ VB
Dim b As Boolean = False
// C#
bool b = false;
ПРИМЕЧАНИЕ Регистр символов в именах переменных в Visual Basic и C#
В C# имеет значение регистр символов, а в Visual Basic это не важно. По традиции имена переменных в C# начинаются со строчной буквы, а в Visual Basic с заглавной. Ради единообразия листингов в большинстве примеров Visual Basic-кода здесь имена переменных будут записываться строчными буквами. При желании можете использовать писать имена переменных в коде на Visual Basic с заглавной буквы — это никак не отразится на работе исполняющей среды.
Чтобы узнать, присвоено ли переменной значение, объявите ее как nullable. Например, если пользователь не выбрал на форме алтернативные варианты («да/нет»), нужно сохранить значение null. В следующем примере переменная типа Boolean может иметь значения true, false или other.
' VB
Dim b As Nullable(Of Boolean) = Nothing
// C#
Nullaole<bool> b = null;
// Shorthand notation, only for C# bool? b = null;
ПРИМЕЧАНИЕ .NET 2.0
Тип Nullable появился в .NET 2.0.
Объявление переменной как nullable включает члены HasValue и Value. Член HasValue указывает, было ли присвоено значение:
' VB
If b.HasValue Then Console.WriteLine("b is {0}.", b.Value)
Else Console.WriteLine("b is not set.")
// at
if (b HasValue)Console WriteLine("b is {0}.", b.Value);
else Console.WriteLine("b is not set.");
6
Основы .NET Framework
Глава 1
Создание пользовательских типов
Пользовательские типы также называются структурами (structures), для их создания используется соответствующее ключевое слово struct. Подобно другим значимым типам, экземпляры пользовательских типов хранятся в стеке и содержат данные, а не ссылки на них. В остальном структуры почти полностью аналогичны классам.
Структуры состоят из других типов, что облегчает работу с их данными. Простейшим примером является структура System.Drawing.Point со свойствами X и Y, определяющими горизонтальную и вертикальную координаты точки. Структура Point упрощает работу с координатами за счет поддержки конструктора и членов:
' VB - Требуется ссылка на System.Drawing
Создание точки
Dim р As New System Drawing.Point(20, 30)
’ Перемещение точки по диагонали
p.0ffset(-1, -1)
Console.WriteLine("Point X {0}, Y {1}”, p.X, p Y)
/I C# - Требуется ссылка на System.Drawing
// Создание точки
System.Drawing.Point p = new System.Drawing.Point(20, 30);
// Перемещение точки по диагонали p.0ffset(-1, -1),
Console.WriteLine("Point X {0}, Y {1}”, p.X, p.Y);
Собственные структуры определяются при помощи ключевого слова Structure в Visual Basic и ключевого слова struct в С#. Например, в следующем фрагменте кода создается тип, обрабатывающий в цикле целые числа между минимальным и максимальным значениями, заданными в конструкторе:
 VB
Structure Cycle
' Закрытые поля
Dim _val, _min, _max As Integer
Конструктор
Public Sub New(ByVal min As Integer, ByVai max As Integer)
_val = min : _min = min : _max = max
End Sub
' Открытые члены
Public Property Value() As Integer
Get
Return _val
End Get
Set(ByVal value As Integer)
' Новый параметр должен находиться между _min и _max.
Занятие 1
Применение значимых типов
7
If value > _max Then _val = jnin
Else If value < jnin Then _val = _max Else _val = value
End Set
End Property
Public Overrides Function ToStringO As String Return Value.ToString
End Function
Public Function Tolnteger() As Integer
Return Value
End Function
Операторы (новинка .NET 2.0)
Public Shared Operator +(ByVal arg1 As Cycle,
ByVai arg2 As Integer) As Cycle
arg1.Value += arg2
Return arg1
End Operator
Public Shared Operator -(ByVai arg1 As Cycle,
ByVai arg2 As Integer) As Cycle
arg1.Value -= arg2
Return arg1
End Operator
End Structure
// C#
struct Cycle
{
// Закрытые поля int _val, _min, _max;
// Конструктор
public Cycle(int min, int max) {
_val = min;
_min = min;
_max = max;
}
public int Value
{
get { return _val; } set
8
Основы .NET Framework
Глава 1
{
if (value > _max)
_val = _min;
else
if (value < _min)
_val = _max;
else
_val = value;
}
}
}
public override string ToStringO
return Value.ToStringO;
}
public int Tolnteger()
{
return Value;
}
// Операторы (новинка .NET 2.0)
public static Cycle operator +(Cycle arg1, int arg2) {
arg1.Value += arg2;
return arg1;
}
public static Cycle operator -(Cycle arg1, int arg2) {
arg1.Value -= arg2;
return arg1;
}
}
ПРИМЕЧАНИЕ .NET 2.0
Ключевое слово Operator появилось в .NET 2.0.
Эту структуру можно использовать для представления элементов с циклически меняющимися значениями, таких как углы или номера периодов спортивного матча: ‘
VB
Dim degrees As New Cycle(0, 359), quarters As New Cycle(1, 4)
For i As Integer = 0 To 8
degrees += 90 : quarters += 1
Console.WriteLine("degrees = {0}, quarters = {1}", degrees, quarters)
Next
Занятие 1
Применение значимых типов
9
// с#
Cycle degrees = new Cycle(0, 359);
Cycle quarters = new Cycle(1, 4);
for (int i = 0; i <= 8; i++)
{
degrees += 90; quarters += 1;
Console.WriteLine(’degrees = {0}, quarters = {1}", degrees, quarters), }
Экземпляр Cycle можно без труда преобразовать в значимый тип и из значимого типа — обратно в ссылочный, заменив ключевые слова Structure/struct на Class. Если сделать это, экземпляры класса Cycle будут размещаться в управляемой куче, а не в стеке (где они займут 12 байтов, по 4 для каждого закрытого целочисленного поля), а присваивание экземпляра Cycle другой переменной приведет к тому, что и он, и переменная будут ссылаться на одну и ту же копию данных.
Хотя по функциональности структуры не отличаются от классов, первые обычно более эффективны. Если тип лучше работает как значимый, а не ссылочный, следует определять структуру, а не класс. Структурные типы должны удовлетворять следующим требованиям:
	логически представлять одиночное значение;
	размер их экземпляра не должен превышать 16 байтов;
	после создания они не должны изменяться;
	не должны приводиться к ссылочным типам.
Создание перечислимых типов
Перечислимые типы — это наборы связанных символов с фиксированными значениями. Перечислимые используются для представления альтернативных значений, доступных разработчикам, использующих класс, в котором объявлено перечислимое. Так, следующее перечислимое хранит возможные обращения:
’ VB
Enum Titles As Integer
Mr
Ms
Mrs
Dr
End Enum
// C#
enum Titles : int { Mr, Ms, Mrs, Dr };
Если создать экземпляр типа Titles, Visual Studio покажет список возможных значений переменной этого типа. Хотя это целочисленная переменная, можно вывести одно из ее связанных значений-строк, как показано в следующем примере:
’ VB
Dim t As Titles = Titles.Dr
Console WriteLine("{0}.", t) ' Выводит "Dr."
10
Основы .NET Framework
Глава 1
// C#
Titles t = Titles.Dr;
Console WriteLine("{0}.", t); // Выводит "Dr."
Перечисления предназначены просто для упрощения кодирования и улучшения читаемости кода, они позволяют использовать понятные символы вместо обычных численных значений. Используйте перечислимые, чтобы предоставить разработчикам, которые будут пользоваться вашими типами, выбор из ограниченного набора значений переменной.
Практикум. Объявление и использование значимых типов
Выполнив следующие упражнения, вы научитесь создавать и использовать структуры и перечислимые. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение 1. Создание структуры
В этом упражнении вы создадите простую структуру с открытыми членами.
1.	В Visual Studio создайте проект консольного приложения. Назовите проект CreateStruct.
2.	Создайте структуру с именем Person, как показано в следующем примере кода:
* VB Structure Person End Structure
// C# struct Person { }
3.	В структуре Person определите общие члены:
□	firstName (тип String)',
□	lastName (тип String);
□	age (тип Integer).
Это показано в следующем фрагменте кода: ' VB
Public firstName As String
Public lastName As String
Public age As Integer
// C#
public string firstName;
public string lastName;
public int age;
4.	Создайте конструктор, определяющий все три члена, как показано ниже:
• VB
Public Sub New(ByVal .firstName As String, ByVai .lastName As String, ByVai _age As
Занятие 1
Применение значимых типов
11
Integer)
firstName = _firstName
lastName = „lastName age = „age End Sub
П C#
public Person(string _firstName, string _lastName, int _age) {
firstName = _firstName;
lastName = „lastName; age = _age;
}
5.	Переопределите метод ToString, чтобы отобразить имя, фамилию и возраст человека. Это показано в следующем фрагменте кода: ' VB
Public Overloads Overrides Function ToStringO As String
Return firstName + " " + lastName + ", age ” + age.ToString End Function
// C#
public override string ToStringO {
return firstName + ” " + lastName + ", age ” + age; }
6.	В методе Main консольного приложения напишите код, создающий экземпляр структуры и передающий его методу Console. WriteLine'.
' VB
Dim p As Person = New Person("Tony", "Allen", 32) Console.WriteLine(p)
// C#
Person p = new Person("Tony", "Allen", 32); Console.WriteLine(p);
7.	Запустите консольное приложение, чтобы убедиться, что оно работает корректно.
Упражнение 2. Добавление перечислимого типа в структуру
В этом упражнении вы расширите возможности структуры, созданной в первом упражнении, добавив к ней перечислимый тип.
1.	Откройте проект, созданный в упражнении 1.
2.	Объявите в структуре Person новое перечислимое. Дайте ему имя Genders и укажите два возможных значения: Male и Female. Это показано в следующем фрагменте:
* VB
Enum Genders
12
Основы .NET Framework
Глава 1
Male
Female
End Enum
// C#
public enum Genders : int { Male, Female };
3.	Добавьте открытый член типа Genders и измените конструктор Person так, чтобы он принимал экземпляры этого типа. Это показано ниже:
’ VB
Public firstName As String
Public lastName As String
Public age As Integer
Public gender As Genders
Public Sub New(ByVal _firstName As String, ByVai „lastName As String,
ByVai _age As Integer, ByVai „gender As Genders)
firstName = „firstName
lastName = „lastName
age = „age
gender = „gender
End Sub
// C#
public string firstName;
public string lastName;
public int age;
public Genders gender;
public Person(string „firstName, string „lastName, int „age, Genders „gender) {
firstName = „firstName;
lastName = „lastName;
age = „age;
gender = „gender;
}
4.	Измените метод Person. ToString, чтобы он отображал пол, как показано в следующем фрагменте кода:
' VB
Public Overloads Overrides Function ToStringO As String
Return firstName + " " + lastName + ” (" + gender.ToStringO + "), age
" +
age.ToString
End Function
// C#
public override string ToStringO
Занятие 1
Применение значимых типов
13
{
return firstName + " " + lastName + " (" + gender + "), age " + age;
}
5.	Измените код Main, чтобы экземпляр класса Person создавался как положено:
’ VB
Sub Main()
Dim р As Person = New Person("Tony", "Aller.", 32, Person.Genders.Male) Console.WriteLine(p)
End Sub
// C#
static void Main(string[] args)
{
Person p = new Person(”Tony", "Allen", 32, Person.Genders.Male); Console.WriteLine(p.ToString());
}
6.	Запустите консольное приложение, чтобы проверить его.
Резюме
	В .NET Framework имеется множество встроенных типов, которые можно использовать как есть либо создавать на их основе собственные типы.
	Значимые типы хранят данные, а не ссылки на них, обеспечивая отличную производительность. Однако значимые типы способны хранить лишь небольшие объемы данных. В .NET Framework размер экземпляра значимого типа ограничен 16 байтами.
	Можно создавать пользовательские типы, хранящие несколько значений и методов. В объектно-ориентированных средах разработки значительная часть прикладной логики хранят в виде пользовательских типов.
	Перечислимые типы делают код понятнее, позволяя сопоставлять символы наборам значений переменных.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Какие из перечисленных типов являются значимыми? (Укажите все верные ответы.) A. Decimal', В. String',
С. System. Drawing.Point',
D. Integer.
14
Основы .NET Framework
Глава 1
2.	Вы передаете процедуре переменную значимого типа в качестве аргумента Процедура изменяет эту переменную, но после возврата значение переменной не измени-илось. Почему? (Выберите один ответ.)	£,
А. Переменная не была инициализирована перед передачей.
В. При передаче процедуре переменной значимого типа данные копируются.
С. Переменная была заново объявлена внутри процедуры.
D. Процедура обработала переменную как ссылочный тип.
3.	Выберите правильное объявление целочисленной переменной, способной принимать значение null.
А.
’ VB
Dim i As Nullable<Of Integer> = Nothing
// C#
Nullable(int) i = null;
B.
' VB
Dim i As Nullable(Of Integer) = Nothing
11 C# Nullable<int> i = null;
C.
’ VB
Dim i As Integer = Nothing
11 C# int i = null;
D.
1 VB
Dim i As Integer(Nullable) = Nothing /«К
// C# int<Nullable> i = null; *
4.	Нужно создать простой класс или структуру, содержащую только значимые типы, Этот класс или структура должна быть максимально оптимизирована. Также при передаче вашего типа процедурам последние не должны их модифицировать. Что выбрать: класс или структуру, ссылочный тип или значимый?
А. Ссылочный класс.
В. Ссылочную структуру.
С. Значимый класс.
D. Значимую структуру.
Занятие 2
Использование общих ссылочных типов
15
Занятие 2. Использование общих ссылочных типов
В .NET Framework большинство типов — ссылочные. Ссылочные типы обеспечивают гибкость и отличную производительность как аргументы методов. В следующих разделах рассматриваются ссылочные типы, о создании классов, интерфейсов и делегатов см. в занятии 4.
Изучив материал этого занятия, вы сможете:
t
J объяснить различие между значимыми и ссылочными типами;
J рассказать о различиях значимых и ссылочных типов в плане присваивания им значений;
J перечислить встроенные ссылочные типы;
J определить, когда нужно использовать тип StringBuilder,
J создавать и упорядочивать массивы;
J открывать и читать файлы, а также вести запись и закрывать их;
J определять возникновение исключений и обрабатывать их.
Продолжительность занятия — 40 минут.
Введение в ссылочные типы
В стеке хранятся только адреса (указатели, pointers) значений ссылочных типов. Сами значения, на которые указывает ссылка в стеке, хранятся в другой области памяти, называемой кучей (heap). Исполняющая среда автоматически управляет памятью кучи, используя процедуру под названием сбор мусора. Эта процедура периодически освобождает память, удаляя элементы, ссылок на которые не осталось.
СОВЕТ Сбор мусора
Сбор мусора запускается, только когда памяти остается слишком мало или вызовом метода GC.Collect. Автоматическое управление памятью оптимизировано для приложений, в которых большинство объектов — короткоживущие (за исключением созданных при запуске приложения). Такой подход к проектированию.кода обеспечивает максимальную производительность.
Сравнение ссылочных и значимых типов
Так как ссылочные типы, по сути, представляют адреса данных, а не сами данные, при присваивании ссылочной переменной значения другой ссылочной переменной копируются не данные, а ссылка на область памяти в куче, принадлежащей второй переменной.
Рассмотрим объявление простой структуры:
' VB
Structure Numbers
Public val As Integer
16
Основы .NET Framework
Глава 1
Public Sub New(ByVal _val As Integer) val = _val
End Sub
Public Overloads Overrides Function ToStringO As String
Return val.ToString
End Function
End Structure
// C#
struct Numbers
{
public int val;
public Numbers(int _val)
{ val = _val; }
public override string ToStringO
{ return val.ToStringO; }
}
Теперь рассмотрим код, который создает экземпляр структуры Numbers, копирует его и модифицирует оригинал и копию, отображая результат на экране: ’ VB
Dim n1 As Numbers = New Numbers(O)
Dim n2 As Numbers = n1
nl.val += 1
n2.val += 2
Console.WriteLine("n1 = {0}, n2 = {1}", n1, n2)
// C#
Numbers n1 = new Numbers(O);
Numbers n2 = n1;
nl.val += 1;
n2.val += 2;
Console.WriteLine("n1 = {0}, n2 = {1}", n1, n2);
В результате выполнения этого кода на экране появятся строки «п1 = 1, п2 = 2», так как структура является значимым типом, а копирование значимого типа приводит к созданию нового экземпляра данных. Однако если объявить тип Numbers к ак класс, а не структуру, то же приложение выведет на экран «п1 = 3, п2 = 3». Дело в том, что тип Numbers стал из значимого ссылочным, а при изменении ссылочного типа изменяются и все его копии.
Встроенные ссылочные типы
В .NET Framework включено около 2500 встроенных ссылочных типов. Все типы, не порожденные от System. Value Туре, — ссылочные, включая и эти 2500. В табл. 1-3 пере
Занятие 2
Использование общих ссылочных типов
17
числены наиболее распространенные типы, от которых порождаются многие другие ссылочные типы.
Табл. 1-3. Часто используемые ссылочные типы
Тип	Применение
System.Object	Тип Object — наиболее общий в .NET Framework. Любой тип можно преобразовать в System.Object, а члены ToString, GetType и Equals, унаследованные от этого типа, доступны у любого типа
System.String	Текст
System. Text.StringBuilder	Динамический текст
System.Array	Массивы данных. Это базовый класс для всех массивов. Синтаксис объявления массива зависит от языка
System.IO.Stream	Буфер для ввода-вывода с использованием файлов, устройств и сети. Это абстрактный базовый класс, от него порождают классы, выполняющие конкретные задачи
System.Exception	Обрабатывает системные исключения и исключения, определенные приложениями. Специализированные классы исключений порождают от этого типа
Строки и построители строк
Типы — это не просто контейнеры для данных, они также поддерживают члены (методы и свойства), позволяющие манипулировать этими данными. Тип System.String поддерживает методы для работы с текстом. Например, следующий код выполняет быстрый поиск с заменой:
’ VB
Dim s As String = "this is some text to search"
s = s.Replace("search", "replace") Console.WriteLine(s).
// C#
string s = "this is some text to search";
s = s.Replace("search", "replace");
Console.WriteLine(s);
Строки типа System.String в .NET являются неизменяемыми. Это значит, что любая попытка изменения строки приводит к тому, что исполняющая среда создает новый экземпляр строки, а старый — уничтожает. Это происходит незаметно, и для многих окажется сюрпризом, что следующий код размещает в памяти четыре строки:
' VB
Dim s As String
s = "wombat"	' "wombat"
s += " kangaroo" ' "wombat kangaroo"
Ji?
18
Основы .NET Framework
Глава 1
s += " wallaby"
s += " koala"
Console.WriteLine(s)
' "wombat kangaroo wallaby"
' "wombat kangaroo wallaby koala"
// C# string s;
s = "wombat";
s += ” kangaroo";
s += " wallaby";
s += " koala";
Console.WriteLine(s);
// "wombat"
// "wombat kangaroo"
// "wombat kangaroo wallaby"
// "wombat kangaroo wallaby koala"
Ссылка останется только на последнюю строку, остальные три будут уничтожены в процессе сбора мусора. Это пример того, как НЕ надо хранить строковые переменные, отказавшись от такого подхода, удастся избежать ненужного сбора мусора и повысить производительность. Решить проблему можно так:
	использовать методы Concat, Join win Format класса String для объединения подстрок в строку;
	использовать класс StringBuilder для создания динамических (изменяемых) строк.
Наиболее гибким решением будет использование StringBuilder, так как оно охватывает несколько операторов. Конструктор по умолчанию создает буфер длиной 16 байтов, который при необходимости можно увеличить, в конструкторе следует указать исходный и максимальный размер буфера. Ниже приведен пример использования StringBuilder.
’ VB
Dim sb As New System.Text.StringBuilder(30)
sb.Append("wombat")	’ Построение строки
sb.Append(" kangaroo") sb.Append(" wallaby") sb.Append(" koala")
Dim s as String = sb.ToString ’ Копирование результата в строку Console.WriteLine(s)
// C#
System.Text.StringBuilder sb = new System.Text.StringBuilder(30);
sb.Append("wombat”);	// Построение строки
sb.Append(" kangaroo");
sb.Append(" wallaby");
sb.AppendC koala");
string s = sb.ToStringO; // Копирование результата в строку Console.WriteLine(s);
Другая важная тонкость класса String в том, что он переопределяет операторы класса System.Object (см. табл. 1-4).
Занятие 2
Использование общих ссылочных типов
19
Табл. 1-4. Операторы класса String
Оператор	Visual Basic	C#	Действие на System.String
Сложение	+ или &	+	Объединяет две строки в новую строку
Равенство	—	==	Возвращает True, если строки идентичны, и False в противном случае
Неравенство	о	!=	Выполняет операцию, обратную оператору равенства
Присваивание		*	Копирует одну строку в другую. В результате строки ведут себя как значимые типы, хоть и реализованы как ссылочные типы
Создание и сортировка массивов
Массивы объявляются при помощи круглых (в Visual Basic) или квадратных скобок (в С#). Как и тип String, тип System.Array предоставляет члены для работы с содержащимися в нем данными. В следующем фрагменте кода объявляется заполненный массив, который затем сортируется:
' VB
Объявление и инициализация массива
Dim аг() As Integer = {3, 1, 2}
’ Вызов общего или статического метода
Array,Sort(ar)
’ Отображение результата
Console.WriteLine("{O}, {1}, {2}", ar(0), ar(1). аг(2))
// C#
// Объявление и инициализация массива int[] аг = { 3, 1, 2 };
// Вызов общего или статического метода
Array.Sort(ar);
И Отображение результата
Console.WriteLine("{0}. {1}, {2}", аг[0], аг[1], аг[2]);
Использование потоков
Потоки — это еще один весьма востребованный тип, применяемый для чтения-записи на диск и взаимодействия по сети. Тип System.IO.Stream является базовым для всех специализированных потоковых типов. В табл. 1-5 показаны некоторые наиболее распространенные потоковые типы. Типы, представляющие потоки сетевого ввода-вывода, находятся в пространстве имен System.Network.Sockets, а шифрованные потоки — в пространстве имен System.Securlty.Cryptography.
20
Основы .NET Framework
Глава 1
Табл. 1-5. Часто используемые потоковые типы
System. ТО Тип	Использование
FileStream	Создание базового потока для чтения-записи в файл
MemoryStream	Создание базового потока для чтения-записи в память
StreamReader	Чтение из потока
StreamWriter	Запись в поток
Простейшие потоковые классы — StreamReader и Stream Writer, позволяющие читать и вести запись в текстовые файлы. Имя файла можно передать как аргумент конструктора, в этом случае достаточно единственной строки кода, чтобы открыть файл. После обработки файла вызовите метод Close, чтобы освободить файл. Следующий фрагмент кода, использующий пространства имен System. 10, иллюстрирует чтение-запись в текстовый файл: ' VB
Создание текстового файла и запись в него
Dim sw As StreamWriter = New StreamWriter("text.txt") sw.WriteLine(”Hello, World!”) sw.Close
Чтение и отображение текстового файла
Dim sr As StreamReader = New StreamReader("text.txt”)
Console.WriteLine(sr.ReadToEnd) sr.Close
// C#
// Создание текстового файла и запись в него
StreamWriter sw = new StreamWriterCtext.txt");
sw.WriteLine("Hello, World!");
sw.Close();
// Чтение и отображение текстового файла
StreamReader sr = new StreamReader("text txt");
Console.WriteLine(sr.ReadToEnd());
sr.CloseO;
К СВЕДЕНИЮ Потоки
Подробнее о потоках см. в главе 2.
Генерация и перехват исключений
Исключения — это неожиданные события, прерывающие нормальную работу кода. Например, если при чтении большого текстового файла со съемного диска пользователь извлечет диск, исполняющая среда сгенерирует исключение. Это логично, так как нормальное продолжение работы кода при этом невозможно
Исключения ни в коем случае не должны полностью останавливать выполнение кода. Задача разработчика — предусмотреть возникновение исключений, перехватывать их и
Занятие 2
Использование общих ссылочных типов
21
корректно обрабатывать. В предыдущем примере можно уведомить пользователя о том, что файл недоступен, и запросить дальнейших инструкций. Вот как это делается (на простом примере, использующем System. 10):
' VB
Try
Dim sr As StreamReader = New StreamReader("C:\boot.ini")
Console.WriteLine(sr.ReadToEnd)
Catch ex As Exception
' Если при чтении файла возникнут сбои, появится сообщение об ошибке
Console.WriteLine("Error reading file: " + ex.Message)
End Try
// C#
try
{
StreamReader sr = new StreamReader(@”C:\boot.ini");
Console.WriteLine(sr.ReadToEnd());
}
catch (Exception ex)
{
// Если при чтении файла возникнут сбои, появится сообщение об ошибке Console.WriteLine("Error reading file: " + ex.Message);
}
Если в предыдущем примере возникнет какая-либо ошибка (не найден файл, недостаточно привилегий, ошибки при чтении), управление будет передано блоку Catch. Если проблем не возникает, исполняющая среда игнорирует этот блок.
Базовый класс Exception очень полезен, он содержит сообщения об ошибках и другие прикладные данные. В дополнение к этому классу в .NET Framework определены сотни других классов исключений. Все они — потомки System.SystemException. Кроме того, всегда можно определить собственные классы, производные от System.ApplicationException, более полно соответствующие специфическим исключениям, чем стандартные классы.
Наличие разных классов исключений позволяет адекватно реагировать на ошибки разных типов. Однако исполняющая среда обработает только первый подходящий по типу исключения блок Catch, так что блоки Catch нужно выстраивать в порядке «от частного к общему». Следующий пример выводит различные сообщения при отсутствии файла, недостатке привилегий и других возможных сбоях:
' VB
Try
Dim sr As StreamReader = New StreamReader("text.txt")
Console.WriteLine(sr.ReadToEnd)
Catch ex As System.10.FileNotFoundException
Console WriteLine("The file could not be found ")
Catch ex As System.UnauthorizedAccessException
Console.WriteLine("You do not have sufficient permissions.")
Catch ex As Exception
Console.WriteLine("Error reading file: " + ex.Message)
End Try
22
Основы .NET Framework
Глава 1
Этот процесс иногда называют фильтрацией исключений. Обработка исключений также поддерживает блок Finally. Этот блок выполняется после блоков Try и Catch, выполнение которых было завершено, независимо от того, сгенерировано ли исключение. Следовательно, в блоке Finally нужно закрывать потоки и освобождать любые объекты, которые могли остаться открытыми после возникновения исключения. В следующем примере объект StreamReader будет освобожден при любых обстоятельствах:
’ VB
Dim sr As StreamReader = New StreamReaderCtext.txt")
Try
Console.WriteLine(sr.ReadToEnd)
Catch ex As Exception
' Если возник сбой при чтении файла, выводится сообщение об ошибке
Console.WriteLineC'Error reading file: “ + ex.Message)
Finally
' Закрываем StreamReader, независимо от того, возникло исключение или нет sг.Close
End Try
И C#
StreamReader sr = new StreamReaderCtext.txt"):
try
{
Console.WriteLine(sr.ReadToEnd());
}
catch (Exception ex)
11 Если возник сбой при чтении файла, выводится сообщение об ошибке Console.WriteLineC'Error reading file: " + ex.Message);
}
finally
{
// Закрываем StreamReader, независимо от того, возникло исключение или нет
sr.CloseO:
}
Обратите внимание: в предыдущем примере объявление StreamReader было вынесено за блок Try. Это необходимо, так как у блока Finally нет доступа к переменным, объявленным в блоке 7>у. Это логично, поскольку не факт, что на момент возникновения исключения уже будут обработаны объявления переменных в блоке Try. ДДя перехвата исключений, возникших как во время, так и после объявления StreamReader, используйте вложенные блоки Try/Catch/Finally.	*
Обычно весь код, за исключением объявлений переменных, желательно заключать в блоки Try. Надежная обработка ошибок облегчает пользователю жизнь при возникновении сбоев и значительно упрощает отладку программы. Цена обработки ошибок — небольшое снижение производительности. Чтобы сэкономить место и сосредоточиться на основном материале, в большинстве примеров мы опустим код обработки ошибок.
Занятие 2
Использование общих ссылочных типов
23
Практикум. Работа со ссылочными типами
Следующие упражнения предназначены для закрепления знаний о ссылочных типах, обработке строк и исключений. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение 1. Определение типов как значимых или ссылочных
В этом упражнении вы создадите консольное приложение, определяющее тип объект (значимый или ссылочный).
1.	В Visual Studio создайте проект консольного приложения. Назовите этот проект List -Value-Types.
2.	Создайте экземпляры следующих классов:
□	SByte;
□	Byte;
□	Intl6;
□	Int32;
□	Int64;
□	String;
□	Exception.
Как показано в следующем фрагменте кода:
' VB
Dim a	As SByte = 0
Dim b	As Byte = 0
Dim c	As Int16 = 0
Dim d	As Int32 = 0
Dim e	As Int64 = 0
Dim s	As String =
Dim ex As Exception = New Exception
// C#
SByte a = 0;
Byte b = 0;
Int16 c = 0;
Int32 d = 0;
Int64 e = 0;
string s =
Exception ex = new Exception()
3.	Добавьте созданные экземпляры в массив:
' VB
Dim types As ObjectO = {a, b, c, d, e, s, ex}
// C#
objectf] types = { a, b, c, d. e, s, ex (;
24
Основы .NET Framework
Глава 1
4.	В цикле foreach проверьте свойство object.GetType().IsValueType, чтобы определить, является ли тип значимым. Напишите код для вывода на экран имени и типа каждого объекта:
’ VB
For Each о As Object In types
Dim type As String
If o.GetType.IsValueType Then
type = "Value type"
Else
type = "Reference Type"
End If
Console.WriteLine(”{0}: {1}", o.GetType, type)
Next
// C#
foreach ( object о in types )
{
string type;
if (o.GetType().IsValueType)
type = "Value type";
else
type = "Reference Type";
Console.WriteLine("{0}: {1}", o.GetType(), type );
}
5.	Запустите консольное приложение и проверьте, правильно ли определяются типы.
Упражнение 2. Работа со строками и массивами
В этом упражнении вы создадите функцию сортировки строк.
1.	В Visual Studio создайте проект консольного приложения. Назовите этот проект SortString.
2.	Определите строковый тип и воспользуйтесь методом String.Split, чтобы разделить строку слова и поместить их в массив:
' VB
Dim s As String = "Microsoft NET Framework 2.0 Application Development Foundation"
Dim sa As StringO = s.Split(" ")
// C#
string s = "Microsoft .NET Framework 2.0 Application Development Foundation";
string[] sa = s.Split(' ');
3.	Вызовите метод Array. Sort для сортировки массива слов:
' VB
Array.Sort(sa)
Занятие 2
Использование общих ссылочных типов
25
// с#
Array.Sort(sa);
4.	Вызовите метод String.Join, чтобы преобразовать массив слов в единую строку, а затем выведите эту строку на консоль. Это делается следующим образом:
’ VB
s = String.Join(” ", sa)
Console.WriteLine(s)
// C#
s = string.Join(" ", sa);
Console.WriteLine(s);
5.	Запустите консольное приложение, чтобы убедиться в корректности его работы.
Упражнение 3. Работа с потоками и исключениями
Разработчик написал приложение Windows Forms для просмотра текстовых файлов. Однако пользователи жалуются, что оно «весьма капризно». Опечатки при вводе имен файла, а также недоступные файлы приводят к остановке приложения из-за необработанного исключения. Вы должны добавить к приложению код обработки исключений, который будет показывать пользователям понятные сообщения об ошибках, если файл окажется недоступным.
1.	Скопируйте на жесткий диск папку Chapter01\Lesson2-ViewFile с компакт-диска, прилагаемого к книге, и откройте версию проекта ViewFile для C# или Visual Basic .NET.
2.	Исключения возникают при попытке открыть файл. Следовательно, нужно изменить код, выполняющийся в ответ на событие show But ton.С Иск. Добавьте код, перехватывающий все типы исключений и отображающий пользователю сообщение об ошибке. Если исключение возникает после инициализации объекта TextReader, нужно закрыть этот объект, независимо от возникновения исключения. Для этого понадобится два вложенных блока Тгу\ один для перехвата исключений при инициализации объекта TextReader, а второй — для перехвата исключений, возникающих при чтении файла. Это показано на следующем примере кода:
’ VB
Try
Dim tr As TextReader = New StreamReader(locationTextBox.Text)
Try
displayTextBox.Text = tr.ReadToEnd
Catch ex As Exception
MessageBox.Show(ex.Message)
Finally
tr.CloseO
End Try
Catch ex As Exception
MessageBox.Show(ex.Message)
End Try
26 Основы .NET Framework	Глава 1
// C#
try
{
TextReader tr = new StreamReader(locationTextBox.Text);
try
{ displayTextBox.Text = tr ReadToEndO; }
catch (Exception ex)
{ MessageBox.Show(ex.Message); }
finally
{ tr.CloseO; }
}
catch (Exception ex)
{ MessageBox.Show(ex.Message); }
3.	Запустите приложение. Сначала убедитесь, что оно успешно отображает содержимое файла. Затем введите недопустимое имя файла, чтобы проверить, появится ли сообщение об ошибке.
4.	Добавьте код обработки исключений System.IO.FileNotFoundException и System.Un-authorizedAccessException, как показано в следующем фрагменте кода:
' VB
Try
Dim tr As TextReader = New StreamReader(locationTextBox.Text)
Try
displayTextBox.Text = tr.ReadToEnd
Catch ex As Exception
MessageBox.Show(ex.Message)
Finally
tr.CloseO
End Try
Catch ex As System.10.FileNotFoundException
MessageBox.Show("Sorry, the file does not exist.")
Catch ex As System.UnauthorizedAccessException
MessageBox.Show("Sorry, you lack sufficient privileges.")
Catch ex As Exception
MessageBox Show(ex.Message)
End Try
11 C#
try
TextReader tr = new StreamReader(locationTextBox.Text);
try
{ displayTextBox.Text = tr.ReadToEndO; }
catch (Exception ex)
{ MessageBox Show(ex.Message); }
finally
{ tr.CloseO; }
Занятие 2
Использование общих ссылочных типов
27
}
catch (System.10.FileNotFoundException ex)
{ MessageBox.Show("Sorry, the file does not exist."); } catch (System.UnauthorizedAccessException ex)
{ MessageBox.Show("Sorry, you lack sufficient privileges."); } catch (Exception ex)
{ MessageBox.Show(ex.Message); }
5. Снова запустите приложение и проверьте, появляется ли новое сообщение об ошибке при вводе недопустимого имени файла.
Резюме
	Ссылочные типы содержат ссылку на данные, а не сами данные.
	При копировании экземпляра значимого типа создается его копия. При копировании ссылочного типа копируется только указатель (ссылка на данные). Следовательно, если скопировать ссылочное значение, а затем изменить копию, то изменится и копия, и исходная переменная.
	В .NET Framework включено множество встроенных ссылочных типов, которые можно использовать как есть или создавать на их основе собственные типы.
	Строки являются неизменяемыми; для динамического создания строк используется класс StringBuilder.
	Для чтения и записи в файлы, память и сеть используются потоки.
	Для фильтрации исключений по типу используются секции Catch блоков Try. Для освобождения и уничтожения ненужных более ресурсов используется секция Finally блока Try.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Какие из перечисленных ниже типов являются ссылочными? (Укажите все верные ответы.)
А. Типы, объявленные как Nullable}
В. String}
С. Exception}
D. Все типы, порожденные от System.Object.
2.	Укажите правильный порядок секций Catch при обработке различных типов исключений.
А. От общих к специфичным.
В. От наиболее вероятных к наименее вероятным.
С. От специфичных к общим.
D. От менее вероятных к более вероятным.
28
Основы .NET Framework
Глава 1
3.	В каких случаях вместо класса String следует использовать класс StringBuilder?
А. При построении строки из более коротких строк.
В. При работе с текстовыми данными длиной более 256 байтов.
С. При поиске с заменой в строках.
D. Когда строка обрабатывается как значимый тип.
4.	Почему ресурсы следует освобождать в блоке Finally, а не Catch?
А. Чтобы не повторять эту операцию в каждом блоке Catch.
В. Блоки Finally выполняются независимо от возникновения исключения.
С. Компилятор генерирует ошибку, если не освободить ресурсы в блоке Finally.
D. Освобождать ресурсы в блоке Catch нельзя.
Занятие 3. Конструирование классов
В объектно-ориентированных языках большой объем работы должен выполняться внутри объектов. Для любого приложения, кроме простейших, приходится конструировать собственные классы со своими свойствами и методами, необходимых для раешения прикладных задач. На этом занятии обсуждается создание собственных классов.
Изучив материал этого занятия, вы сможете:
описать и использовать наследование;
J описать и использовать интерфейсы;
J описать и использовать частичные классы;
J создавать собственные и использовать встроенные обобщения;
J обрабатывать и генерировать события;
J добавлять атрибуты для описания сборок и методов;
J перемещать тип из одной библиотеки классов в другую, используя переадресацию типов.
Продолжительность занятия — 40 минут.
Введение в наследование
В .NET Framework включены тысячи классов со множеством методов и свойств. Отслеживать огромное количество классов и членов было бы невозможно, если бы не исключительная согласованность объектной модели .NET Framework. Например, метод ToString у разных классов решает в сущности одну и ту же задачу — преобразование экземпляра класса в строку. Аналогично, многие классы поддерживают одинаковые операторы, например, оператор сравнения экземпляров.
Такая согласованность оказалась возможной благодаря наследованию (inheritance) и интерфейсам (interfaces). Наследование применяется для создания новых классов на основе существующих. Например, в главе 6 говорится, что класс Bitmap наследует часть функциональности класса Image и дополняет его собственными методами. Следовательно, экземпляры класса Bitmap можно использовать так же, как и экземпляры класса Image, но класс Bitmap поддерживает дополнительные методы для работы с растровыми изображениями.
Можно без труда создавать собственные классы исключений на основе System.ApplicationExcaption при помощи наследования, как показано ниже:
Занятие 3
Конструирование классов
29
 VB
Class DerivedException
Inherits System.ApplicationException
Public Overrides Readonly Property MessageO As String
Get
Return "An error occurred in the application."
End Get
End Property	.v
End Class
// C#
class DerivedException : System.ApplicationException
public override string Message
{
get { return "An error occurred in the application."; }
}
}
Новый класс позволяет генерировать и перехватывать новое исключение, так как наследует соответствующие функции от своего базового класса:
’ VB
Try
Throw New DerivedException
Catch ex As DerivedException
Console.WriteLine("Source: {0}, Error: {1}", ex.Source, ex.Message)
End Try
// C# try
throw new DerivedExceptionO;
}
catch (DerivedException ex)
{
Console.WriteLine("Source; {0}, Error: {1}", ex Source, ex.Message);
Обратите внимание, что он не только поддерживает генерацию и перехват, но и включает, среди прочих, член Source, унаследованный от System.ApplicationException.
Другим преимуществом наследования является взаимозаменяемость производных классов. Например, существует пять классов, производных от System.Drawing. Brush: HatchBrush, LinearGradientBrush, PathGradientBrush, SolidBrush и TextureBrush. Метод Graphics.Draw Rectangle принимает объект Brush как один из параметров, но ему никогда не передают экземпляр базового класса Brush, а только один из производных классов. Метод Graphics.DrawRectangle принимает любые классы, производные от Brush. Аналогично, Graphics.DrawRectangle будет принимать экземпляры любого вашего класса, производного от Brush.
30
Основы .NET Framework
Глава 1
Введение в интерфейсы
Интерфейс или контракт определяет общий набор членов, который должны быть во всех классах, реализующих данный интерфейс. К примеру, интерфейс IComparable определяет метод СотрагеТо, проверяющий равенство экземпляров класса. Все классы, реализующие интерфейс IComparable, как пользовательские, так и встроенные, поддерживают проверку равенства.
Интерфейс IDisposable имеет единственный метод, Dispose, позволяющий сборкам, создавшим экземпляр класса, освобождать любые ресурсы, занятые этим экземпляром. Чтобы создать в Visual Studio 2005 класс, реализующий интерфейс IDisposable, выполните следующее:
1.	Объявите класс, например так:
 VB
Class BigClass
End Class
// C#
class BigClass
{
2.	Добавьте объявление интерфейса:
’ VB
Class BigClass
Implements IDisposable
End Class
// C#
class BigClass : IDisposable
{
}
3.	Если вы используете Visual Basic, Visual Studio автоматически сгенерирует объявления всех необходимых методов. Если этого не произошло, удалите команду Implements и попробуйте снова; может быть, запуск Visual Studio еще не завершился. Если вы пишете на С#, щелкните правой кнопкой мыши объявление Interface, выберите Implement Interface, а затем снова щелкните Implement Interface, как показано на рис. 1-1.
class BigClass : ^Bisposaf^
Рис. 1-1. Visual Studio упрощает реализацию интерфейсов
Занятие 3
Конструирование классов
31
4.	Напишите код для каждого из методов интерфейса. В этом примере вы напишете код для метода Dispose, освобождающего ранее выделенные ресурсы.
В табл. 1-6 перечислены наиболее востребованные интерфейсы .NET Framework.
Табл. 1-6. Часто используемые интерфейсы
Класс	Описание
IComparable 1'	Реализуется типами, значения которых можно упорядочивать; например, числовыми и строковыми классами. Класс IComparable требуется для сортировки
IDisposable	Определяет методы для ручной очистки объекта. Этот интерфейс важен для больших ресурсоемких объектов, а также объектов, использующих блокирующих ресурсы (например, базы данных)
IConvertible	Позволяет преобразовывать класс в базовый тип, такой как Boolean, Byte, Double или String
ICloneable	Поддерживает копирование объекта
lEquatable	Позволяет проверять равенство экземпляров класса. Например, типы с этим интерфейсом можно использовать в выражениях типа «if (а ~= Ь)»
IFormattable	Позволяет преобразовывать значение объекта в строку со специальным форматом. Более гибок, чем базовый метод TbString
Также можно создавать собственные интерфейсы. Это удобно, когда нужно создать несколько взаимозаменяемых классов со сходной функциональностью. Например, в следующем примере определяется интерфейс, содержащий три члена:
’ VB
Interface IMessage
’ Отправка сообщения. Возвращает True при успехе и False в противном случае.
Function Send() As Boolean
' Отправляемое сообщение.
Property MessageO As String
' Адрес для отправки.
Property AddressO As String
End Interface
11 C#
interface IMessage
{
// Отправка сообщения. Возвращает True при успехе и False в противном случае.
bool Send();
// Отправляемое сообщение.
string Message { get; set; }
// Адрес для отправки.
string Address { get; set; }
}
Если реализовать этот интерфейс в новом классе, Visual Studio сгенерирует для членов интерфейса следующий шаблон:
32
Основы .NET Framework
Глава 1
' VB
Class EmailMessage
Implements IMessage
Public Property AddressO As String Implements IMessage.Address Get End Get
Set(ByVal value As String) End Set
End Property
Public Property MessageO As String Implements IMessage.Message Get End Get
Set(ByVal value As String) End Set
End Property
Public Function Send() As Boolean Implements IMessage.Send End Function
End Class
// C#
class EmailMessage : IMessage
public bool SendO {
throw new Exception("The method or operation is not implemented."); }
public string Message {
get {
throw new Exception("The method or operation is not implemented."); } set {
throw new Exception("The method or operation is not implemented.”); }
public string Address {
get {
Занятие 3
Конструирование классов
33
t throw new Exception("The method or operation is not implemented.");
} set { throw new ExceptionC'The method or operation is not implemented."); }
}
}
Если вы, создав собственный класс, решите, что удобно было бы иметь несколько классов с одинаковыми членами, знайте: в Visual Studio есть способ быстро извлечь интерфейс из класса. Просто выполните следующее:
1.	В Visual Studio 2005 щелкните класс правой кнопкой мыши.
2.	Щелкните Refactor, а затем Extract Interface.
3.	Укажите имя интерфейса, выберите открытые члены для добавления в интерфейс и щелкните ОК.
В классах может быть реализовано несколько интерфейсов. Следовательно, в классе можно реализовать оба интерфейса: IComparable и IDisposable.
Частичные классы
ПРИМЕЧАНИЕ .NET 2.0
Частичные классы появились в .NET 2.0.
Частичные классы позволяют разбить определение класса по нескольким файлам. Преимущество такого подхода — в скрытии подробностей определения класса, это позволяет сосредоточиться на более важных задачах при разработке производных классов.
Примером встроенного частичного класса является класс Windows-формы Form. В Visual Studio 2003 и ранних версиях классы форм включали код, сгенерированный проектировщиком форм. Теперь этот код скрыт в частичном классе с именем form.Designer.vb или form.Designer.es.
Чтобы увидеть файлы частичного класса в Visual Basic, нужно выбрать в Solution Explorer команду Show All Files. В C# они отображаются по умолчанию. Частичные классы не включены в темы экзамена, но нужно знать о них, скажем, чтобы найти код формы при создании приложений Windows Form.
Обобщения
Обобщения — это часть системы типов .NET Framework, которая позволяет определять тип, опуская некоторые подробности. Например, можно не указывать типы параметров или членов классов, а оставить эту задачу реализующему тип коду. Это позволяет коду, использующему тип, настраивать его для собственных нужд.
Подготовка к экзамену
Обобщения появились лишь в .NET 2.0, но будьте готовы: экзамен включает довольно много вопросов по обобщениям.
В .NET Framework 2.0 пространство имен System.Collections.Generic содержит несколько классов обобщений, включая Dictionary, Queue, Sorted Dictionary и SortedList. Эти клас
34
Основы .NET Framework
Глава 1
сы работают аналогично их эквивалентам в System.Collections, но лучше с точки зрения производительности и контроля типов.
К СВЕДЕНИЮ Обобщенные наборы
В .NET Framework 2.0 имеется пространство имен System.Collections.Generic с более производительными по сравнению со стандартными наборами. Подробнее см. в главе 4.
Применение обобщений
В .NET Framework 1.0 и 1.1 поддержки обобщений не было. Вместо этого разработчики использовали класс Object для определения параметров и членов, а потом приводили другие типы к этому классу. Обобщения, по сравнению с классом Object, предоставляют два значительных преимущества:
	Уменьшают число ошибок во время выполнения При приведении типов к классу Object (и обратном приведении) компилятор не определяет несоответствие типов. Например, если привести объект-строку к Object, а результат — к Integer, компилятор не перехватит ошибку. Вместо этого исключение сгенерирует исполняющая среда. Обобщения позволяют перехватывать такие ошибки. Кроме того, можно указывать ограничения на классы, используемые в обобщении, что даст компилятору возможность определять несовместимые типы;
	Улучшенная производительность Приведение типов требует упаковки и распаковки (см. главу 4), а эти операции загружают процессор и снижают производительность. Использование обобщений не требует приведения типов или упаковки, что повышает производительность.
Пример из практики
Тони Нортроп (Топу Northrup)
Я не заметил выигрыша в производительности от использования обобщений, хотя, если верить Microsoft, обобщения быстрее приведения типов. На практике же приведение типов оказывается в несколько раз быстрее. Впрочем, различий в производительности можно и не заметить (в моих тестах выполнение 100 000 таких операций заняло лишь несколько секунд). И все же обобщения обеспечивают более строгий контроль типов.
Создание обобщений *
Сначала рассмотрим классы Obj и Gen, которые выполняют одинаковые задачи, но Obj для передачи типов использует класс Object, a Gen — обобщения:
' VB
Class Obj
Public VI As Object
Public V2 As Object
Public Sub New(ByVal _V1 As Object, ByVai _V2 As Object)
V1 = _V1
V2 = _V2
Занятие 3
Конструирование классов
35
End Sub
End Class
Class Gen(0f T, U)
Public V1 As T o
Public V2 As U
Public Sub New(ByVal _V1 As T, ByVai _V2 As U)
V1 = _V1
V2 = _V2
End Sub
End Class
// C#
class Obj
{
public Object t;
public Object u;
public Obj(Object _t, Object _u)
t = _t; u = _u;
}
}
class Gen<T, U>
{
public T t;
public U u;.
public Gen(T _t, U _u)
{
t = _t;
u = _u;
}
}
Видно, что класс Obj имеет два члена типа Object. Класс Gen имеет два члена типа Ти U. Код, использующий этот класс, определит типы для Т и U. В зависимости от того, как класс Gen используется в коде, Т и U могут иметь тип string, int, пользовательский тип или любую их комбинацию.
При создании классов обобщений есть существенное ограничение: код обобщения действителен, только если он компилируется без ошибок во всех вариантах обобщений, «достроенных» как с использованием классов Int и string, так и любых других. В сущности, при написании кода обобщения мы ограничены возможностями базового класса Object. Следовательно, вы можете вызвать метод ToString или GetHashCode в своем классе,
36
Основы .NET Framework
Глава 1
но не можете использовать оператор + или >. Эти ограничения не действуют для кода, использующего обобщение, так как в нем для обобщения объявлен свой тип
Применение обобщений
Для использования обощений нужно указать его тип. Рассмотрим код консольного приложения, использующего классы Gen и Obj\
’ VB
' Добавляем две строки, используя класс Obj
Dim оа As Obj = New ObjC'Hello, ", "World!")
Console.WriteLine(CType(oa.V1, String) + CType(oa.V2, String))
’ Добавляем две строки, используя класс Gen
Dim да As New Gen(0f String, String)("Hello, ", "World!")
Console.WriteLine(ga.V1 + ga.V2)
’ Добавляем Double и Integer, используя класс Obj
Dim ob As Obj = New Obj(10.125, 2005)
Console.WriteLine(CType(ob.VI, Double) + CType(ob.V2, Integer))
’ Добавляем Double и Integer, используя класс Gen
Dim gb As New Gen(0f Double, Integer)(10.125, 2005)
Console.WriteLine(gb.V1 + gb.V2)
// C#
// Добавляем две строки, используя класс Obj
Obj оа = new ObjC'Hello, ", "World!”);
Console.WriteLine((string)oa.t + (string)oa.u);
If Добавляем две строки, используя класс Gen
Gen<string, string> да = new Gen<string, string>("Hello, ", "World!"); Console.WriteLine(ga.t + ga.u);
// Добавляем double и int, используя класс Obj
Obj ob = new Obj(10.125, 2005);
Console.WriteLine((double)ob.t + (int)ob.u);
// Добавляем double и int, используя класс Gen
Gen<double, int> gb = new Gen<double, int>(10.125, 2005);
Console.WriteLine(gb.t + gb.u);
Классы Obj и Gen дадут одинаковые результаты. Однако, код, использующий класс Gen, работает быстрее, так как не требует упаковки-распаковки в Object. Кроме этого, разработчикам гораздо проще использовать класс Gen. Во-первых, им не нужно вручную приводить класс Object к соответствующим типам. Во-вторых, ошибки из-за несоответствия типов будут перехвачены во время компиляции, а не «всплывут» во таремя выполнения. Чтобы продемонстрировать это преимущество, рассмотрим следующий код с ошибкой:
Занятие 3
Конструирование классов
37
• VB г
Добавляем Double и Integer при помощи класса Gen
Dim gb As New Gen(0f Double, Integer)(lO.125, 2005)
Console WriteLine(gb V1 + gb.V2)
Добавляем Double и Integer при помощи класса Obj
Dim ob As Obj = New Obj(10.125, 2005)
Console.WriteLine(CIype(ob.V1, Integer) + CType(ob.V2, Integer))
// C#
// Добавляем double и int при помощи класса Gen
Gen<double, int> gc = new Gen<double, int>(10.125, 2005);
Console.WriteLine(gc.t + gc.u);
// Добавляем double и int при помощи класса Obj
Obj ос = new Obj(10.125, 2005);
Console.WriteLine((int)oc.t + (int)oc.u);
Последняя строка этого кода содержит ошибку — значение oc.t приведено к типу Int, а не double. К сожалению, компилятор не перехватил эту ошибку. Вместо этого в C# во время выполнения генерируется исключение при попытке исполняющей среды привести double к Int. В Visual Basic, где сужающие преобразования по умолчанию разрешены, результат еще хуже — возникает ошибка в вычислениях. Ошибку компиляции исправить на порядок проще, чем ошибку времени выполнения, так что в этом преимущество класса с обобщениями очевидно.
Использование ограничений
Возможности обобщений были бы очень ограничены, если бы разрешалось писать только код, компилирующийся для любого «достроенного» класса, ведь в этом случае его возможности зависят от базового класса Object. Чтобы избежать этого, используйте ограничения — они позволяют определить требования к типам, которыми разрешено заменять обобщения в коде.
Обобщения поддерживают четыре типа ограничений:
	По интерфейсу Разрешает использовать обобщение только типам, в которых реализованы определенные интерфейсы.
	По базовому классу Разрешает использовать обобщение только типам, соответствующим определенному базовому классу или производным от него.
	По конструктору Требует, чтобы в типе, использующем обобщение, был реализован конструктор без параметров.
	По ссылочному или значимому типу Требует, чтобы типы, использующие обобщение, были только ссылочными или только значимыми.
Для применения ограничений к обобщению в Visual Basic используется секция As, а в C# - where. Например, следующий класс обобщения может использоваться только типами, реализующими интерфейс IComparable'.
' VB
Class CompGen(0f T As IComparable)
Public t1 As T
Public t2 As T
38
Основы .NET Framework
Глава 1
Public Sub New(ByVal _t1 As T, ByVai _t2 As T) t1 = _t1 t2 = _t2
End Sub
Public Function Max() As T
If t2.CompareTo(t1) < 0 Then
Return t1
Else
Return t2
End If
End Function
End Class
// C#
class CompGen<T>
where T : IComparable {
public T t1;
public T t2;
public CompGen(T _t1, T _t2)
{
t1 = _t1;
t2 = _t2;
}
public T Max()
{
if (t2 CompareTo(tl) < 0)
return t1;
else
return t2;
}
} *
Предыдущий класс будет скомпилирован без ошибок. Однако, если удалить секцию where, компилятор вернет ошибку с сообщением, что метод СотрагеТо не определен в типе Т. Ограничение обобщения классами, реализующими интерфейс IComparable, гарантирует, что метод СотрагеТо будет доступен всегда.
События
Большинство современных программ выполняется нелинейно. Так, в приложениях Windows Forms иногда нужно выполнять какое-либо действие только по щелчку кнопки или нажатию клавиши. В серверных приложениях часто приходится ожидать сетевого запроса. В .NET Framework реализация таких сценариев возможна с использованием событий, о которых рассказывается в следующих разделах.
Занятие 3
Конструирование классов
39
Введение в события
•
Событие (event) — это отправленное объектом уведомление о совершении какого-либо действия. Действие может быть выполнено пользователем (пример — щелчок мышью), или программой. Объект, сгенерировавший событие, называется отправителем события. Объект, перехватывающий событие и реагирующий на него, называется получателем события.
При обработке событий класс объект-отправитель «не знает», какой объект или метод получит (обработает) событие. Поэтому нужен посредник (механизм, сходный с указателями) между источником и получателем события. В .NET Framework для этого предусмотрен специальный тип — Delegate.
Введение в делегаты
Делегат — это класс, содержащий ссылку на метод. В отличие от других классов, у класса-делегата есть сигнатура, и он может содержать ссылки только на методы, соответствующие этой сигнатуре. Делегат — это эквивалент указателя функции с поддержкой контроля типов, обеспечивающий обратный вызов. Хотя делегаты служат и для других целей, здесь мы обсудим только их роль в обработке событий. Чтобы определить класс делегата, достаточно объявить его. Объявление предоставляет сигнатуру делегата, а общеязыковая исполняющая среда обеспечивает реализацию. Ниже показано объявление делегата события:
' VB
Public Delegate Sub AlarmEventHandler(sender As Object, e As EventArgs)
// C#
public delegate void AlarmEventHandler(object sender, EventArgs e);
Стандартная сигнатура делегата обработчика события определяет метод, не возвращающий значение, чей первый параметр имеет тип Object и ссылается на объект, сгенерировавший событие, а второй параметр, производный от EventArgs, содержит данные события. Если событие не генерирует данных, второй параметр содержит просто экземпляр EventArgs. В противном случае второй параметр содержит объект специализированного типа, производного от EventArgs, содержащего поля и свойства, необходимые для хранения данных события.
EventHandler — это предопределенный делегат, представляющий метод-обработчик события, не генерирующих данных. Если же событие генерирует данные, вы должны определить собственный тип для хранения данных события и либо создать делегат, где второй параметр будет содержать объект этого типа, либо использовать обобщенный делегат EventHandler и подставить собственный тип как параметр обобщенного делегата.
Чтобы связать событие с методом, который будет его обрабатывать, добавьте экземпляр делегата к объекту события. Обработчик события вызывается каждый раз при генерации события, пока делегат не будет удален.
Обработка событий
Чтобы обработать событие, необходимо выполнить следующее:
	, создать метод-обработчик события, соответствующий сигнатуре делегата. Обычно это означает, что он должен возвращать пустое значение (void) и принимать два параметра: Object и EventArgs (или производного типа). Это показано в этом примере кода:
40
Основы .NET Framework
Глава 1
' VB
Public Sub Button1_Click(sender As Object, e As EventArgs)
' Код метода End Sub
// C#
private void button1_Click(object sender, EventArgs e)
// Код метода }
	добавить обработчик события, чтобы указать метод-получатель события:
' VB
AddHandler Me.Buttonl.Click, AddressOf Me.Button1_Click
11 C#
this.buttonl.Click += new System.EventHandler(this.button1_Click);
ПРИМЕЧАНИЕ .NET 2.0
В .NET Framework 2.0 включена новая версия обобщенного обработчика EventHandler.
При генерации события будет вызван указанный метод.
Как сгенерировать событие
Для генерации события нужно сделать как минимум следующее:  создать делегат:
' VB
Public Delegate Sub MyEventHandler(ByVal sender As Object, ByVai e As EventArgs)
11 C#
public delegate void MyEventHandler(object sender, EventArgs e);
	создать член-событие: ' VB
Public Event MyEvent As MyEventHandler
П C#
public event MyEventHandler MyEvent;
	вызвать делегат в методе, когда потребуется сгенерировать событие: ’ VB
Dim е As EventArgs = New EventArgs
RaiseEvent MyEvent(Me, e)
// C#
MyEventHandler handler = MyEvent;
EventArgs e = new EventArgsO;
Занятие 3
Конструирование классов
41
if (handler != null)
{ '
// Вызов делегатов. handler(this, е);
}
// Заметьте, что C# проверяет обработчик на null.
// В Visual Basic это не обязательно.
Если нужно передать какую-либо информацию обработчику события, можно породить собственный класс от EventArgs.
ПРИМЕЧАНИЕ Различия между вызовом событий в Visual Basic и C#
В Visual Basic и C# события вызываются по-разному. В C# перед вызовом события нужно проверить его объект на равенство null. В Visual Basic такая проверка не обязательна.
Введение в атрибуты
Атрибуты описывают типы, методы и свойства, позволяя программно запрашивать описания этих объектов и членов при помощи технологии, называемой отражением (reflection). Типичные задачи, для решения которых используются атрибуты, таковы:
	указание разрешений и привилегий, необходимых для использования класса; указание разрешений и привилегий, в предоставлении которых нужно отказать по соображениям безопасности;
	объявление возможностей, таких как поддержка сериализации;
	описание сборки (заголовок, описания и сведения об авторском праве).
Типы атрибутов происходят от базового класса System.Attribute. На них ссылаются с использованием знаков О и []. Следующий пример кода иллюстрирует добавление атрибутов к сборке:
VB - Assemblyinfo.vb
<Assembly Assembly!itle("ch01vb )>
<Assembly: AssemblyDescription("Chapter 1 Samples")>
<Assembly: AssemblyCompany("Microsoft Learning")>
<Assembly: AssemblyProduct("chOlvb")>
<Assembly: AssemblyCopyright("Copyright © 2006')>
<Assembly: Assembly!radema rk("")>
11 C# - Assemblylnfo.es
[assembly: AssemblyTitle(”chO1cs")]
[assembly: AssemblyDescription( Chapter 1 Samples")]
[assembly: AssemblyConfiguration(" )]
[assembly: AssemblyCompany("Microsoft Learning")]
[assembly: AssemblyProduct("chOlcs")]
[assembly: AssemblyCopyright("Copyright © 2006")]
[assembly: Assembly!rademark()]
При создании проекта Visual Studio автоматически генерирует ряд стандартных атрибутов сборки, включая заголовок, описание, название компании, имя руководителя и
42
Основы .NET Framework
Глава 1
версию. Эти атрибуты следует настраивать для каждого нового проекта, так как по умолчанию они не включают важные сведения, например, текст описания.
Атрибуты не только позволяют снабдить сборку описанием для других разработчиков, но и сопроводить ее информацией о требованиях и возможностях. Например, чтобы разрешить сериализацию класса, нужно добавить атрибут Serializable'.
' VB
<Serializable()> Class ShoppingCartltem
End Class
11 C#
[Serializable]
class ShoppingCartltem
{
}
Без атрибута Serializable сериализовать класс нельзя. В следующем примере атрибуты указывают, что необходимо прочитать файл C:\boot.ini. Из-за этого исполняющая среда перед выполнением сгенерирует исключение, если текущих привилегий недостаточно для доступа к этому файлу:
’ VB
Imports System.Security.Permissions
<Assembly: FileIOPermissionAttrlbute(SecurityAction.RequestMinimum, Read := "C:\boot.ini")>
Module Modulel
Sub Main()
Console.WriteLine("Hello, World!")
End Sub
End Module
// C#
using System;
using System.Security.Permissions;
[assembly:FileIOPermissionAttribute(SecurityAction.RequestMinimum, Read=@"C:\boot.ini")]
namespace DeclarativeExample
{
class Classi
{
[STAThread]
static void Main(string[] args)
{
Console.WriteLine("Hello, World1");
}
}
Занятие 3
Конструирование классов
43
Введение в переадресацию типов
Перенаправление типов — это атрибут (TypeForwardedTo), позволяющий перемещать тип из одной сборки в другую (скажем, из сборки А в сборку В), без перекомпиляции программ, использующих сборку А. После выпуска компонента (сборки) для использования клиентскими приложениями передцресация типов позволяет обновлять компоненты (и все необходимые сборки) без перекомпиляции приложений, использующих эти компоненты и сборки. Перенаправление тшюв работает только для компонентов, на которые ссылаются существующие приложения. При перекомпиляции приложения потребуются ссылки на сборки со всеми типам, которые использует приложение.
Чтобы переместить тип из одной библиотеки классов в другую, выполните следующее:
ПРИМЕЧАНИЕ .NET 2.0
Перенаправление типов — новинка .NET 2.0. --------------- i	---------------------- ---- ------ 
1.	Добавьте атрибут TypeForwardedTo к исходной сборке библиотеки классов.
2.	Вырежьте определение типа из исходной библиотеки классов.
3.	Вставьте определение типа в нужную библиотеку классов.
4.	Соберите обе библиотеки заново.
Следующий фрагмент кода иллюстрирует перемещение типа ТуреА в библиотеку DestLib'.
’ VB
Imports System.Runtime.Compilerservices
<Assembly:TуpeFo rwa rdedTо(GetType(Dest Lib.ТуpeA))]>
// C#
using System.Runtime.Compilerservices;
[assembly:TypeForwardedTo(typeof(DestLib.TypeA))]
Практикум. Создание производного класса с делегатами
Выполнив следующие упражнения, вы научитесь работать с событиями и наследованием. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение 1. Порождение нового класса от существующего
В этом упражнении вы объявите класс, производный от класса Person, созданного на занятии 1.
1.	Скопируйте на жесткий диск папку Chapter01\Lesson3-Person с компакт-диска, прилагаемого к книге, и откройте версию проекта CreateStruct для C# или Visual Basic.
2.	Преобразуйте структуру Person в класс.
3.	Напишите новое определение класса Manager, производного от Person.
' VB
Class Manager
End Class
44
Основы .NET Framework
Глава 1
П C#
class Manager : Person
{
}
4.	Добавьте открытые члены типа string: phoneNumber и officeLocation.
5.	Переопределите конструктор, добавив новые члены, хранящие номер телефона и адрес офиса. Конструктор базового типа нужно вызвать, как показано в следующем фрагменте:
' VB
Public Sub New(ByVal _firstName As String, ByVai _lastName As String, ByVai _age As Integer, ByVai _gender As Genders, ByVal _phoneNumber As String,
ByVai ..officeLocation As String)
MyBase.New(_firstName, JLastName, _age, _gender)
phoneNumber = _phoneNumber
officeLocation = _officeLocation
End Sub
11 C#
public Manager(string _firstName, string _lastName, int _age, Genders _gender, string _phoneNumber, string _officeLocation)
: base (_firstName, _lastName, _age, _gender)
{
phoneNumber = _phoneNumber;
officeLocation = _officeLocation;
}
6.	Переопределите метод ToString, чтобы добавить номер телефона и адрес офиса, как показано ниже:
' VB
Public Overloads Overrides Function ToStringO As String
Return MyBase.ToString + ", " + phoneNumber + ”, " + officeLocation
End Function
// C#
public override string ToStringO
{
return base.ToStringO + ", " + phoneNumber + ”,	+ officeLocation;
}
7.	Измените метод Main, заменив объект Person объектом Manager. Запустите приложение, чтобы проверить его работу.
Упражнение 2. Обработка событий
В этом упражнении вы создадите класс, обрабатывающий событие, генерируемое таймером.
Занятие 3
Конструирование классов
45
1.	В Visual Studio создайте проект приложения Windows Forms. Назовите этот проект TimerEvents.
2.	Добавьте на форму элемент управления ProgressBar, как показано на рис. 1-2.
Рис. 1-2. Этот элемент управления будет обрабатывать события таймера
3.	В объявлении класса формы объявите экземпляр объекта System. Windows .Forms. Timer. Объекты Timer применяют для генерации событий с указанной периодичностью (в миллисекундах). Следующий код иллюстрирует объявление объекта Timer.
' VB
Dim t As Timer
П c#
System.Windows.Forms.Timer t;
4.	В проектировщике просмотрите свойства формы. Затем просмотрите список событий. Дважды щелкните событие Load, чтобы автоматически создать его обработчик, который будет выполняться при инициализации формы. В этом методе инициализируйте объект Timer, установите для него интервал в одну секунду, создайте обработчик для события Tick и запустите таймер. Это показано на следующем листинге: • VB
Private Sub Form1_Shown(ByVal sender As System.Object, ByVai e As System,EventArgs)
Handles MyBase.Shown
t = New System.Windows.Forms.Timer
t Interval = 1000
AddHandler t.Tick, AddressOf Me.t_Tick
t.Start()
End Sub
// Cfi
private void Timer_Shown(object sender, EventArgs e) {
t = new System.Windows.Forms.Timer();
t.Interval = 1000;
t.Tick += new EventHandler(t_Tick);
t.Start();
}
5.	Напишите обработчик события Timer.Tick. При генерации события прибавьте 10 к атрибуту ProgressBar. Value. Когда значение этого атрибута достигнет 100, остановите таймер:
-h- ' VB
Private Sub t_Tick(ByVal sender As Object, ByVai e As EventArgs)
ProgressBarl.Value +=10
46
Основы .NET Framework
Глава 1
If ProgressBarl.Value = 100 Then t.StopO
End If
End Sub
// C#
void t_Tick(object sender, EventArgs e)
{
progressBar.Value += 10;
if (progressBar.Value >= 100) t.StopO;
}
6.	Запустите приложение и проверьте, реагирует ли оно на событие таймера каждую секунду.
Резюме
	Наследование используется для создания новых типов на основе существующих.
	Интерфейсы определяют набор членов, общий для логически связанных типов.
	Частичные классы позволяют разбить определение класса на несколько файлов кода.
	События могут вызывать заданный метод в ответ на некоторые обстоятельства, возникшие при исполнении программы.
	Атрибуты используются для описания сборок, типов и членов.
	Атрибут TypeForwardedTo используется для перемещения типа между библиотеками классов.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Какое из следующих выражений верно? (Укажите все верные ответы.)
А. Наследование определяет общие члены, которые должны быть реализованы в связанных типах.
В. Интерфейсы определяют общие члены, которые должны быть реализованы в связанных типах.
С. Наследование позволяет порождать новые типы на основе базового типа.
D. Интерфейсы порождают тип от базового типа.
2.	Что из перечисленного является примером встроенных обобщений? (Укажите все верные ответы.) A. Nullable}
В. Boolean}
Занятие 4
Преобразование типов
47
С^- EventHandler,
D. System Drawing. Point.
3.	Вы создаете класс обобщения и хотите запрограммировать освобождение объектов обобщений. Как это сделать?
А. Вызвать метод Object.Dispose.
В. Реализовать интерфейс IDisposable.
С. Породить класс обобщения от класса IDisposable.
D. Использовать ограничения, чтобы требовать реализацию интерфейса IDisposable в классе обобщения.
4.	Вы реализовали делегат события в классе, но при подключении обработчика этого события компилятор сообщил об ошибке из-за отсутствия перегруженного метода, соответствующего делегату. В чем проблема?
А. Сигнатура обработчика события не совпадает с сигнатурой делегата.
В.	Обработчик события объявлен как Shared/static, а не как член экземпляра.
С.	Вы допустили опечатку в имени обработчика события при подключении его к делегату.
D.	Класс был создан на другом языке.
Занятие 4. Преобразование типов
Часто требуется преобразовывать одни типы в другие, например, чтобы сравнить значения типа Integer и Double. В этом случае можно передать значение Double методу, требующему параметр типа Integer. Также нередко требуется вывести число как текстовую строку на экран.
На этом занятии вы узнаете, как преобразовывать типы в Visual Basic и С#. Преобразование типов — это один из немногих аспектов, в которых различия между Visual Basic и C# значительны.
Изучив материал этого занятия, вы сможете:
J преобразовывать типы;
J объяснить принципы упаковки и рассказать, почему ее следует избегать;
J использовать операторы преобразования.
Продолжительность занятия — 20 минут.
Преобразование типов в Visual Basic и C#
По умолчанию в Visual Basic разрешено неявное преобразование типов, а в C# неявное преобразование с потерей точности запрещено. Чтобы отключить неявное преобразование в Visual Basic, добавьте Option Strict On в начало каждого файла с кодом или (в Visual Studio) выберите команду меню Project\Properties\Compile, а затем выберите Option Strict On для всего проекта.
Как в Visual Basic, так и в C# неявное преобразование разрешено, если целевой тип способен хранить значения исходного типа без потери точности. Такое преобразование называется расширяющим. Проиллюстрировать это можно следующим примером:
48
Основы .NET Framework
Глава 1
' VB
Dim i As Integer = 1
Dim d As Double = 1.0001
d = i ' Преобразование допускается
П C#
int i = 1;
double d = 1.0001;
d = i; // Преобразование допускается
Если диапазон или точность значений исходного типа выше, чем у целевого, преобразование называется сужающим, обычно такое преобразование разрешено выполнять только явно. В табл. 1-7 перечислены способы явных преобразований.
Табл. 1-7. Методы явного преобразования
Системный тип	Visual Basic	C#	Преобразуемые типы
System.Convert	CType	(Ope)	Типы, в которых реализован интерфейс System.IConvertible Типы, определяющие операторы
type. ToString, type.Parse type.TryParse, type. TryParseExact	CBool, CInt, CStr, и т. д. DirectCast, TryCast	оператор приведения	преобразования Строковые и базовые типы; если преобразование невозможно, генерируется исключение Строковый тип в базовый; если преобразование невозможно, генерируется исключение Базовые типы Visual Basic; для оптимизации компилируется как встраиваемая функция (только в Visual Basic) Преобразование типов. DirectCast генерирует исключение, если типы не связаны через наследование или общий интерфейс; TryCast в таких случаях возвращает Nothing (только в Visual Basic)
ПРИМЕЧАНИЕ .NET 2.0
Типы TryParse, TryParseExact и TryCast появились в .NET 2.0. В предыдущих версиях нужно было выполнять синтаксический разбор, и перехватывать исключение в случае его неудачи.
Сужающее преобразование выполнить не удастся, если диапазон или точность исходного типа выше, чем у целевого либо не определено, поэтому такое преобразование следует заключать в блоки Try либо использовать TryCast или TryParse и проверять возвращенное значение.
Занятие 4
Преобразование типов
49
Упаковка и распаковка
Упаковка преобразует значимый тип в ссылочный, а распаковка — ссылочный тип в значимый. В следующем листинге приводится пример упаковки значимого типа Integer в ссылочный тип Object: ' VB
Dim i As Integer = 123
Dim о As Object = CType(i, Object)
// C# int i = 123; object о = (object) i;
Распаковка выполняется во время присвоения значимому типу ссылочного объекта. Ниже приведен пример распаковки: ’ VB
Dim о As Object =123
Dim i As Integer = CType(o, Integer)
// C#
object о = 123; int i = (int) o;
ПРИМЕЧАНИЕ Упаковка и распаковка
Упаковка и распаковка требует дополнительных усилий, так что старайтесь избегать их при программировании циклических задач. Упаковка также выполняется при вызове виртуальных методов, которые структура наследует от System.Object, например, ToString. Чтобы избежать ненужной упаковки, придерживайтесь следующих рекомендаций:  пишите перегруженные методы, содержащие отдельные версии для обработки аргументов различных типов. Рекомендуется создавать несколько перегруженных версий метода, а не одну, принимающую аргумент типа Object,
	по возможности используйте обобщения вместо аргументов типа Object',
	переопределяйте виртуальные члены ToString, Equals и GetHash при написании структур.
Реализация преобразования в собственных типах
Есть несколько способов преобразования в собственных типах. Выбор способа зависит от типа выполняемого преобразования:
	определить операторы преобразования, чтобы упростить сужающие и расширяющие преобразования числовых типов;
	переопределить ToString для преобразования в строки и Parse для преобразования из строк;
	реализовать System.IConvertible, чтобы разрешить преобразование через Sy stem.Convert. Этот способ используется для разрешения преобразований, специфичных для культуры;
	реализовать класс TypeConverter, чтобы разрешить преобразования в окне свойств дизайнера Visual Studio. Преобразование во время конструирования не входит в темы экзамена, поэтому класс TypeConverter в этой книге не рассматривается.
50
Основы .NET Framework
Глава 1
К СВЕДЕНИЮ Преобразование во время конструирования
Подробнее о преобразовании во время проектирования см. в статье «Extending Design-
Time Support» по адресу http://msdn2.microsoft.com/en-us/library/37899azc(en-US,VS.80).aspx.
ПРИМЕЧАНИЕ .NET 2.0
Операторы преобразования появились в .NET 2.0.
Определение операторов преобразования позволяет напрямую присваивать значимый тип пользовательскому. Для преобразований без потери точности используйте ключевое слово Widening/implicit, для преобразований с потерей точности — Narrowing/ explicit. Например, следующая структура определяет операторы присваивания с целочисленными значениями:
1 VB
Structure ТуреА
Public Value As Integer
’ Разрешает неявное преобразование из целочисленного типа
Public Shared Widening Operator CType(ByVal arg As Integer) As TypeA
Dim res As New TypeA
res.Value = arg
Return res
End Operator
' Разрешает явное преобразование в целочисленный тип
Public Shared Narrowing Operator CType(ByVal arg As TypeA) As Integer Return arg.Value
End Operator
' Обеспечивает преобразование строк (избегая упаковки)
Public Overrides Function ToStringO As String
Return Me.Value.ToString
End Function
End Structure
// C#
struct TypeA	-i
{
public int Value;
// Разрешает неявное преобразование из целочисленного типа
public static implicit operator TypeA(int arg) {
TypeA res = new TypeA();
res.Value = arg;
return res;
}
Занятие 4
Преобразование типов
51
// Разрешается явное преобразование в целочисленный тип
public static explicit operator int(TypeA arg)
<
return arg.Value,
}
// Обеспечивает преобразование строк (избегая упаковки)
public override string ToStringO
{
return this.Value.ToStringO;
}
}
В предыдущем типе ToString также переопределяется для преобразования строк без упаковки. Теперь можно напрямую присваивать типу целочисленные значения:
' VB
Dim a As ТуреА, i As Integer
’ Неявное расширяющее преобразование разрешено
а = 42 ’ Rather than a.Value = 42
' Сужающее преобразование должно быть явным
i = Clnt(a) ’ Rather than i = a.Value
Синтаксис правильный
i = CType(a, Integer)
Console.WriteLine("a = {0}, i = {0}", a.ToString, i.ToString)
// C#
TypeA a; int i;
// Неявное расширяющее преобразование разрешено
а = 42; // но не a.Value = 42
// Сужающее преобразование должно быть явным
i = (int)a; // но не i = a.Value
Console.Writel_ine("а = {0}, i = {0}”, a.ToStringO, i.ToStringO);
Чтобы реализовать интерфейс System. IConvertible, добавьте в определение типа интерфейс IConvertible. Затем при помощи Visual Studio автоматически реализуйте этот интерфейс. Visual Studio вставляет объявления членов для 17 методов, включая GetTypeCode, ChangeType и ТЬТуре, для каждого базового типа. Вам не придется реализовывать каждый метод, а некоторые (такие как ToDateTime), возможно, просто окажутся недопустимыми. Для недопустимых методов просто генерируется исключение — Visual Studio автоматически добавляет код генерации исключений для всех не реализованных методов преобразования.
После реализации IConvertible собственный тип можно будет преобразовывать при помощи стандартного класса System.Convert
VB
Dim a As ТуреА, b As Boolean a = 42
Преобразование при помощи ToBoolean.
b = Convert.ToBoolean(a)
Console.WriteLineC'a = {0}, b = {1}”, a.ToString, b.ToString)
52
Основы .NET Framework
Глава 1
// C#
TypeA a; bool b;
a = 42;
// Преобразование при помощи ToBoolean.
b = Convert.ToBoolean(a);
Console.WriteLine("a = {0}, b = {1}”, a.ToStringO, b.ToStringO),
Практикум. Безопасное преобразование типов
В следующих упражнениях вы научитесь избегать проблем при неявном преобразовании типов и добиваться предсказуемой работы своего кода. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение 1. Проверка неявного преобразования
В этом упражнении вы проверите преобразование, чтобы определить, какие числовые типы допускают неявное преобразование.
1.	В Visual Studio создайте консольное приложение.
2.	Объявите экземпляры трех значимых типов: Int 16, Int32 и double, как показано в следующем фрагменте кода:
' VB
Dim i16 As Int16 = 1
Dim i32 As Int32 = 1
Dim db As Decimal = 1
// C#
Int16 i16 = 1;
Int32 i32 = 1;
double db = 1;
3.	Попытайтесь присвоить значения переменных друг другу:
' VB
i16 = i32
i16 = db
i32 = i16
i32 = db
db = i16
db = i32
// C#
i16 = 132;
i16 = db,
i32 = i16;
i32 = db;
Занятие 4
Преобразование типов
53
db = i16;
db = i32;
4.	Попытайтесь собрать проект. Какие неявные преобразования разрешил компилятор? Почему?
Упражнение 2. Включение Option Strict (только в Visual Basic)
В этом упражнении вы измените параметры компилятора и вновь соберете проект, созданный в упражнении 1.
1.	Откройте в Visual Studio проект, созданный в упражнении 1.
2.	В меню Project выберите команду имя_проекта Properties.
3.	Перейдите на вкладку Compile. В секции Implicit Conversion измените Notification на Error.
4.	Попытайтесь собрать проект. Какие неявные преобразования разрешил компилятор? Почему?
Резюме
 В .NET Framework возможно автоматическое преобразование встроенных типов. Расширяющие преобразования выполняются неявно как в Visual Basic, так и в С#. Сужающие преобразования в C# должны быть явными, тогда как в Visual Basic они разрешены по умолчанию.
 Упаковка позволяет обрабатывать любой тип как ссылочный.
 Чтобы разрешить преобразования в пользовательских типах, нужно реализовать в них операторы преобразования.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Почему следует избегать упаковки? (Выберите один ответ.)
А. Она требует дополнительных затрат ресурсов.
В. Пользователи должны иметь административные привилегии для запуска приложения.
С. Она ухудшает читабельность кода.
2.	Структуры наследуют метод ToString от System.Object. Зачем нужно переопределять этот метод в структурах? (Укажите все правильные ответы.) А. Чтобы избежать упаковки.
В. Чтобы возвращать не только имя типа.
С. Этого требует компилятор.
D. Чтобы избежать ошибок времени выполнения из-за недопустимых преобразований строк.
3.	Если преобразовать типы напрямую нельзя, что нужно сделать при реализации интерфейса IConvertible!
54
Основы .NET Framework
Глава 1
А. Удалить член ТоТуре, выполняющий преобразование.
В. Сгенерировать исключение InvalidCastException.
С. Сгенерировать пользовательское исключение, сообщающее об ошибке.
D. Оставить тело члена пустым.
4.	Какие неявные преобразования будут разрешены после включения Option Strict? (Укажите все верные ответы.)
A.	Int 16 в Int32.
В.	nt32 в Int 16.
С.	Intl6 в Double.
D.	Double в Intl6.
Закрепление материала главы
Для закрепления знаний и навыков, полученных при изучении этой главы, выполните следующее:
	изучите резюме главы;
	повторите основные термины, использованные в этой главе;
	выполните задания лабораторной работы по темам этой главы;
	выполните предложенные упражнения;
	сдайте пробный экзамен.
Резюме главы
	Значимые типы — это небольшие переменные, хранящие сами данные, а не ссылки на области памяти, в которых они хранятся. Во время присваивания значимых типов данные копируются, в результате создается новый экземпляр данных. В значимых типах можно хранить null-значения, используя обобщение Nullable. Кроме того, можно создавать структуры, комбинирующие несколько значимых типов.
	Ссылочные типы содержат ссылки на данные, а не сами данные. В .NET Framework имеются тысячи ссылочных типов, позволяющие решать практически любые задачи. Из всех ссылочных типов чаще всего используется класс String. Так как класс String неизменяемый, он отличается от других ссылочных типов: при копировании строки создается уникальный экземпляр данных. При копировании большинства ссылочных классов копируется только указатель, то есть изменения, внесенные в один из экземпляров этого типа, отражаются и на остальных его экземплярах. При непредвиденных событиях .NET Framework генерирует исключения. Для обработки исключений в коде используются блоки TrylCatch.
	Классы в языках .NET Framework — это особые типы, которые могут включать значимые и ссылочные типы, методы, атрибуты и свойства. Согласованность классов обеспечивается наследованием (механизмом, позволяющим порождать новые классы от существующих) и применением интерфейсов (которые требуется реализовывать в классах). Обобщения позволяют создавать классы и методы, работающие с различными типами. Чтобы приложения могли обрабатывать предопределенные события, нужно генерировать эти события и вызывать их обработчики. .
Лабораторная работа
55
 Преобразование позволяет сравнивать и копировать значения разных типовНеявное преобразование выполняется автоматически и различается в Visual Basic и С#. В C# неявное преобразование разрешено, только если оно не ведет к потере информации. В Visual Basic разрешено как сужающее, так и расширяющее преобразование. Преобразование значимых типов в ссылочные называется упаковкой,распаковка происходит при обратном преобразовании (присваивании объекта значимому типу).
Основные термины
	упаковка;
	приведение типов;
	ограничение;
	контракт (интерфейс);
	исключение;
	фильтрация исключений;
	сбор мусора
	обобщение;
	куча;
	интерфейс;
	сужающее преобразование,
	тип, принимающий ««//-значения;
	сигнатура;
	стек;
	структура;
	распаковка;
	расширяющее преобразование.
Лабораторная работа
Сейчас вы примените на практике все, что узнали о типах. Ответы на вопросы см. в разделе «Ответы» в конце книги.
Проектирование приложения
Вас недавно приняли на должность разработчика в медицинское учреждение. Вам поручено спроектировать информационную систему, которая будет использоваться служащими для обработки информации о пациентах, историях болезни, назначенном лечении и врачах. Ответьте на вопросы руководства, касающиеся проекта приложения.
1.	Мы должны обрабатывать информацию о пациентах и о врачах. Как это обеспечить? Сколько классов для этого потребуется?
2.	Сотрудникам требуется возможность поиска по группам пациентов и врачей. Например, при увольнении врача нужно уведомить всех его пациентов и подобрать для них нового врача. Кроме того, мы ежегодно связываемся с докторами для продления их трудовых контрактов. Как хранить сведения о пациентах и врачах?
56
Основы .NET Framework
Глава 1
3.	Кроме прочего, приложение должно печатать адреса на конвертах для подписчиков и врачей. Возможно ли реализовать единый метод для обработки адресов как пациентов, так и врачей? Как это сделать?
4.	Для нас очень важно сохранить конфиденциальность нашей информации. Разработчик баз данных собирается внедрить систему разрешений, чтобы предотвратить несанкционированный доступ к базе. Пользователи, которым отказано в доступе, должны получать сообщение о том, что для получения доступа ему следует обратиться к своему руководителю. Как это сделать?
Рекомендуемые упражнения
Чтобы лучше проработать темы экзамена, рассмотренные в этой главе, выполните следующие упражнения.
Управление данными в приложении -NET Framework 2.0 при помощи системных типов
Выполните как минимум упражнения 1 и 2. Чтобы освоить применение обобщений на практике, также выполните упражнение 3.
 Упражнение 1 Откройте последний созданный проект и добавьте в код обработку исключений. Весь код вне объявлений значимых типов должен быть заключен в блок Try (если производительность не является приоритетом).
	Упражнение 2 Создайте класс обобщения, представляющий связный список, позволяющий создавать цепочку различных типов объектов.
	Упражнение 3 Создайте два класса с идентичной функциональностью. В первом классе используйте обобщения, а во втором — приведение к типам Object. Напишите цикл For, выполняющий тысячу операций над экземплярами созданного класса. Измерьте время работы обоих классов и определите, какой работает быстрее. Для измерения времени можно использовать тип Datelime. Now.Ticks.
Реализация интерфейсов .NET Framework для унификации компонентов
Выполните все упражнения, чтобы освоить реализацию общих интерфейсов.
	Упражнение 1 Создайте собственный класс, реализующий необходимые интерфейсы для сортировки массива.
	Упражнение 2 Создайте собственный класс, который можно преобразовывать в общие значимые типы.
	Упражнение 3 Создайте собственный класс, экземпляры которого можно уничтожать при помощи метода IDisposable .Dispose.
Управление взаимодействием компонентов .NET-приложений при помощи событий и делегатов
Выполните оба упражнения.
	Упражнение 1 Откройте последнее созданное приложение Windows Forms и изучите код обработчиков событий пользовательского интерфейса, автоматически сгенерированный Visual Studio.
Пробный экзамен
57
	Упражнение 2 Создайте класс, генерирующий событие, и класс, производный от EventArgs. Затем создайте сборку обработчика этого события.
Пробный экзамен
На прилагаемом компакт-диске, содержится пробный экзамен, обеспечивающий широкие возможности по самоподготовке. Например, можно проверить усвоение тем этой главы либо всех тем экзамена 70-536. Можно сымитировать настоящий экзамен либо выбрать режим обучения, когда после каждого вопроса приводятся правильные ответы с пояснениями.
Пробный экзамен
Подробнее о пробном экзамене рассказано во введении.
Программирование ввода-вывода
Занятие 1.	Навигация по файловой системе	59
Занятие 2.	Чтение и запись файлов	75
Занятие 3.	Сжатие потоков	96
Занятие 4.	Работа с изолированным хранилищем	105
В этой главе описана работа со средствами ввода-вывода в Microsoft .NET Framework. Основы системы ввода-вывода составляют доступ к файлам и папкам файловой системы, работа с потоками для чтения и записи, сжатие потоков и механизм изолированного хранения.
Темы экзамена:
	Доступ к файлам и папкам с использованием классов для работы с файловой системой (file system) (см. пространство имен System.IО).
□	Классы File и Filelnfo\
□	классы Directory и Directory Infо\
□	класс Driveinfo и перечислимое DriveType',
□	классы FileSystemlnfo и FileSystemWatcher,
□	класс Path',
□	класс ErrorEventArgs и делегат ErrorEventHandler,
□	класс RenamedEventArgs и делегат RenamedEventHandler.
	Управление потоками байтов с использованием классов Stream (см. пространство имен System.IO).
□	Класс FileStream',
□	класс Stream (классы Reader и Writer здесь не рассматриваются, поскольку выделены в отдельную тему);
Занятие 1
Навигация по файловой системе
59
ст класс MemoryStream',
□	класс BufferedStream.
 Управление данными приложений .NET Framework с использованием классов Reader и Writer (см. пространство имен System.IO).
□	Классы StringReader и StringWriter;
□	классы TextReader и TextWriter,
□	классы StreamReader и StreamWriter,
□	Классы BinaryReader и BinaryWriter.
 Сжатие и декомпрессия потоковой информации в приложениях .NET Framework (см. пространств имен System.IO.Compression) и укрепление защиты и данных приложений с помощью изолированного хранения, (см. пространство имен System.IO.IsolatedStorage).
□	Класс IsolatedStorageFile',
□	Класс IsolatedStorageFileStream',
□	Класс DeflateStream',
□	Класс GZipStream.
Прежде всего
Чтобы освоить материал этой главы, необходимо иметь навыки работы с Microsoft Visual Basic или C# и уметь:
1.	создавать консольные приложения в Microsoft Visual Studio, используя Visual Basic или С#;
2.	добавлять в проект ссылки на системные библиотеки классов;
3.	Создавать текстовые файлы.
Пример из практики
Шон Уилдермъюс (Shawn Wildermuth)
Я написал не одну тысячу строк кода, моя специальность — работа с файловой системой. Средства ввода-вывода .NET Framework помогают мне решать бесчисленное множество задач. Значительная часть функциональности ввода-вывода является основой как других разделов .NET Framework, так и продуктов сторонних разработчиков. После того, как я овладел основами работы с потоками и файлами, мой труд стал существенно легче.
Занятие 1. Навигация по файловой системе
Одной из самых распространенных задач в рутинной работе программиста является работа с файловой системой. Она включает как навигацию и сбор информации о дисках, папках и файлах, так и отслеживание изменений, происходящих в файловой системе.
60
Программирование ввода-вывода
Глава 2
Изучив материал этого занятия, вы сможете:
S использовать в коде классы File и Fileinfo-,
использовать в коде классы Directory и Directoryinfo.,
J использовать классы Driveinfo и DriveType;
J перечислять файлы, каталоги и диски, используя классы, производные от класса FileSystemlnfo',
J использовать класс Path для управления путями файловой системы;
J отслеживать изменения в файловой системе, используя класс FileSystem Watcher.
Продолжительность занятия — 20 минут.
Введение в классы для работы с файловой системой
Пространство имен System.10 включает набор классов, предназначенных для навигации и управления файлами, папками и дисками. Классы файловой системы подразделяются на два типа: информационные и вспомогательные.
Большинство информационных классов являются производными от базового класса FileSystemlnfo. Такие классы позволяют работать с любой системной информацией объектов файловой системы, включая сведения о файлах, папках и дисках. Эти классы называются Fileinfo и Directory Info.
В дополнение к этому класс Driveinfo представляет дисковые устройства; этот класс также информационный, но он не является производным от класса FileSystemlnfo, поскольку имеет несколько иную функциональность (например, файлы и папки можно удалять, а диски — нет).
Вспомогательные классы поддерживают статические (static) [в Visual Basic — общие (shared)] методы для работы с такими объектами файловой системы, как файлы, папки и пути. К вспомогательным классам относятся классы File, Directory и Path.
Класс FileSystemlnfo
Класс FileSystemlnfo предоставляет базовую функциональность для всех информационных классов. В табл. 2-1 приводятся наиболее важные свойства класса FileSystemlnfo.
Табл. 2-1. Свойства FileSystemlnfo
Имя	Описание
Attributes	Возвращает или устанавливает FileAttributes для текущего файла или папки
CreationTime	Возвращает или устанавливает время создания текущего файла или папки
Exists	Определяет, существует ли файл или папка
Extension	Возвращает строку, представляющую расширение файла или папки
FullName	Возвращает полный путь к файлу или папке
LastAccessTime	Возвращает или устанавливает время последнего обращения к файлу или папке
Занятие 1
Навигация по файловой системе
61
Таил. 2-1. (окончание)
Имя	Описание
LastWriteTime Возвращает или устанавливает время последней записи в файл или папку
Имя	Возвращает «простое» имя файла или папки (имя файла в папке или
имя последней папки в иерархии каталогов)
В табл. 2-2 приводятся наиболее важные методы класса FileSystemInfo.
Табл. 2-2. Методы FileSystemInfo
Имя	Описание
Delete	Удаляет файл или папку
Refresh	Обновляет экземпляр класса последними данными о файловой системе
Класс Fileinfo
Класс Fileinfo предоставляет базовую функциональность для доступа к отдельным файлам и управления ими.
В табл. 2-3 приводятся наиболее важные свойства класса Fileinfo.
Табл. 2-3.	Свойства Fileinfo
Имя	Описание
Directory	Возвращает объект Directory Info, представляющий папку, в которой расположен файл
DirectoryName	Возвращает имя папки, в которой расположен файл	
IsReadOnly	Возвращает или устанавливает флаг, определяющий возможность изменения или удаления файла
Length	Возвращает размер файла
В Табл. 2-4 приводятся наиболее важные методы класса Fileinfo.	
Табл. 2-4.	Методы Filelttfo
Имя	Описание
AppendText	Создает новый объект StreamWriter, позволяющий добавлять текст в файл (см. занятие 2)
Сору То	Создает копию файла в другом каталоге
Create	Создает файл с параметрами текущего файла.
CreateText	Создает новый объект StreamWriter и новый файл для записи текста (см. занятие 2)
Decrypt	Расшифровывает файл, зашифрованный текущим пользователем
Encrypt	Зашифровывает файл, делая его содержимое доступным только текущему пользователю
MoveTo	Перемещает файл в другой каталог
g2 Программирование ввода-вывода
Глава 2
Табл. 2-4.	(окончание)
Имя	Описание
Open	Открывает файл с заданными разрешениями (на чтение, чтение и запись и т. п.)
OpenRead OpenText	Открывает файл только для чтения Открывает файл и возвращает объект StreamReader для чтения текста из файла
Open Write Replace	Открывает файл только для записи Заменяет файл файлом, созданным по параметрам текущего объекта Fileinfo
Получение сведений о файле
Чтобы получить информацию о выбранном файле, выполните следующие действия:
1. Используя путь к выбранному файлу, создайте новый объем File Info.
2. Обратитесь к свойствам объекта Fileinfo.
Например, можно проверить, существует ли файл, запросив свойство Exist объекта Fileinfo, как показано в приведенном ниже коде:
' VB
Dim ourFile As Fileinfo = New FilelnfoC'c \boot.ini")
If ourFile.Exists Then
Console.WriteLine("Filename : {0}", ourFile.Name)
Console.WriteLine("Path : {0}", ourFile.FullName) End If
11 C#
Filclnfo ourFile = new Filelnfo(@"c:\boot.ini ");
if (ourFile.Exists) {
Console.WriteLine("Filename : {0}", ourFile.Name);
Console.WriteLine("Path : {0}", ourFile.FullName);
}
Используя в коде объект Fileinfo подобным образом, можно запрашивать информацию о любом файле файловой системе.
Как скопировать файл
Объект File Info позволяет не только получать информацию о файле, но и выполнять над файлом различные операции После получения допустимого объекта Fileinfo для создания копии файла достаточно вызвать метод Сору То, как в приведенном ниже примере:
VB
Dim ourFile As Fileinfo = New Filelnfo("c:\boot.ini")
ourFile.CopyTo("c:\boot.bak")
Занятие 1
Навигация по файловой системе
63
// C#
Fileinfo ourFile = new Filelnfo(@"c:\boot.ini");
ourFile.CopyTo(@"c:\boot.bak");
Для перемещения и создания файлов используется аналогичная процедура. Как только допустимый объект Fileinfo создан, можно обращаться ко всем его свойствам и вызывать любые его методы.
Класс Directoryinfo
Класс Directoryinfo предоставляет базовую функциональность для доступа к отдельным каталогам и управления ими. В табл. 2-5 приводятся важнейшие свойства класса Directory Info.
Табл. 2-5. Свойства Directoryinfo
Имя	Описание
Parent	Возвращает объект Directoryinfo для родительской папки
Root	Возвращает строку, представляющую корневой каталог в пути к текущему каталогу
В Табл. 2-6. приводятся наиболее важные методы класса Directory Info.	
Табл. 2-6. Методы Directoryinfo
Имя	Описание
Create	Создает папку, описанную в текущем объекте Directory Info
CreateSubdirectory	Создает новую папку, вложенную в текущую папку в иерархии каталогов
GetDirectories	Возвращает массив объектов Directory Info, представляющих вложенные папки текущего каталога
GetFiles	Возвращает массив объектов Fileinfo, представляющих все файлы в текущем каталоге
GetFileSystemlnfos	Возвращает массив объектов FileSystemlnfo, представляющих как файлы и папки в текущем каталоге
MoveTo	Перемещает текущую папку в другой каталог
Перечисление файлов в папке
Получение информации о файлах в папке во многом похоже на получение информации о файле. Ниже говорится, как перечислить файлы в папке:
1. Используя путь к выбранной папке, создайте объект Directory Info.
2. Чтобы перечислить файлы в этой папке, вызовите метод GetFiles.
Этот пример кода демонстрирует решение этой задачи:
’ VB
Dim ourDir As Directoryinfo = New Directorylnfo("c:\windows")
64 Программирование ввода-вывода	Глава 2
Console.Writel_ine("Directory: {0}", ourDir.FullName)
Dim file As Fileinfo
For Each file In ourDir.GetFilesO
Console.WriteLine("File: {0}", file.Name)
Next
// C# *
Directoryinfo ourDir = new Directorylnfo(@"c:\windows");
Console.WriteLine("Directory: {0}”, ourDir.FullName);
foreach (Fileinfo file in ourDir.GetFilesO) {
Console.WriteLine("File: {0}", file.Name);
}
Метод GetFiles объекта Directoryinfo позволяет перечислить файлы в отдельной папке.
Класс Driveinfo
Класс Driveinfo предоставляет базовую функциональность для доступа к отдельным дискам и управления ими. В табл. 2-7 приводятся наиболее важные свойства класса Driveinfo.
Табл. 2-7. Свойства Driveinfo
Имя	Описание
AvailableFreeSpace	Возвращает размер свободного места на диске. В зависимости от дисковых квот значение, возвращаемое этим методом, может отличаться от значения, возвращаемого методом TotalFreeSpace (см. ниже)
DriveFormat	Возвращает формат диска, например NTFS или FAT32
DriveType	Возвращает тип диска как перечислимое DriveType (см. следующий раздел)
IsReady	Возвращает состояние диска (готов ли он)
Имя	Возвращает имя диска
RootDirectory	Возвращает объект Directory Info, представляющий корневой каталог диска
TotalFreeSpace	Возвращает общий объем свободного места на диске.
TotalSize	Возвращает размер диска
VolumeLabel	Возвращает или устанавливает метку диска. Установить метку можно, если диск доступен не только для чтения
В табл. 2-8 приодится важный метод класса Driveinfo.
Занятие 1
Навигация по файловой системе 55
Табл. 2-8. Метод Driveinfo
Имя	Описание
GetDrives	Статический (static) [в Visual Basic — общий (shared)] метод,
возвращающий все диски текущей системы
Перечислимое DriveType
Перечислимое DriveType определяет типы дисков, которые может представлять объект Driveinfo. В табл. 2-9 приводятся члены перечислимого DriveType.
Табл. 2-9. Члены DriveType
Имя	Описание
CDRom	Оптический диск, CD-ROM, DVD и т. п.
Fixed	Фиксированный жесткий диск
Network	Подключенный сетевой диск
NoRootDirectory	Диск, не имеющий корневого каталога
Ram	Виртуальный диск
Removable	Съемный диск
Unknown	Дисковое устройство неопределенного типа
Перечисление дисков
Чтобы перечислить дисковые устройства системы, следуйте процедуре, приведенной ниже:
1. Вызовите статический (в Visual Basic — общий) метод GetDrives класса Driveinfo.
2. Переберите в цикле все элементы массива объектов Driveinfo, возвращенного методом GetDrives
Приведенный ниже код иллюстрирует эту процедуру:
VB
Dim drives() As Driveinfo = Driveinfo. GetDrivesO
Dim drive As Driveinfo
For Each drive In drives
Console WriteLine("Drive {0}", drive.Name)
Console WriteLineC'Type {0}", drive.DriveTyoe)
Next
// C#
Drivelnfo[] drives = Driveinfo.GetDrivesO;
foreach (Driveinfo drive in drives)
{
Console WriteLine("Drive {0}", drive.Name);
66
Программирование ввода-вывода
Глава 2
Console.WriteLine("Type: {0}", drive.DriveType); }
Видно, что с помощью метода GetDrives объекта Driveinfo решить задачу перечисления дисковых устройств системы максимально просто.
ПРИМЕЧАНИЕ Оптические диски
Все типы оптических дисков (CD, CD-R, DVD, DVD-R и т. п.) представлены объектом
Drivelnfo.CDRom.
Класс Path
Класс Path предоставляет методы для управления путями файловой системы. В табл. 2-10 приводятся наиболее важные методы класса Path.
Табл. 2-10. Статические (static) методы Path
Имя	Описание
ChangeExtension	Принимает в качестве параметра путь и возвращает путь к файлу с другим расширением (внимание: изменяется только строка пути, расширение настоящего файла не изменяется)
Combine	Объединяет две совместимые строки с путями
GetDirectoryName GetExtension	Возвращает имя папки, соответствующей заданному пути Возвращает расширение файла, соответствующего заданному пути
GetFileName	Возвращает имя файла, соответствующего заданному пути
GetFileName WithoutExtension	Возвращает имя файла, соответствующего заданному пути (без расширения)
GetFullPath	Возвращает полный путь, соответствующий заданному пути. Этот метод можно использовать для разрешения относительных путей
GetPathRoot	Возвращает корневой каталог из заданного пути
GetRandomFileName	Генерирует случайное имя файла
GetTempFileName GetTempPath	Создает временный файл и возвращает полный путь к нему Возвращает путь к папке временных файлов текущего пользователя или системы
HasExtension	Определяет, содержит ли имя файла, соответствующего заданному пути, расширение
IsPathRooted	Определяет, входит ли в заданный путь корневой каталог
Как изменить расширение файла
Класс Path позволяет запрашивать и разбирать пути файловой системы. Класс Path освобождает программиста от написания собственного кода для разбора строк и предоставляет большинство сведений о путях файловой системы. К примеру, можно воспользоваться классом Path, чтобы получить и изменить расширение файла, как показано в следующем фрагменте кода:
Занятие 1
Навигация по файловой системе
67
• VB
Dim ourPath As String = "c:\boot.ini"
Console.WriteLine(ou rPath)
Console.WriteLine( Ext: {0}", Path.GetExtension(ourPath))
Console.WriteLine("Change Path: {0}",
Path.ChangeExtension(ou rPath, ”bak”))
// C#
string ourPath = @"c:\boot.ini";
Console.WriteLine(ourPath);
Console.WriteLine("Ext: {0}”, Path.GetExtension(ourPath));
Console.WriteLine("Change Path: {0}",
Path.ChangeExtension(ourPath, "bak"));
Используя метод GetExtension класса Path, можно получить текущее расширение файла, соответствующего заданному пути файловой системы. Изменить расширение можно с помощью метода ChangeExtension класса Path.
Класс FileSystemWatcher
Класс FileSystemWatcher предоставляет методы для отслеживания изменений в каталогах файловой системы. В табл. 2-11 приводятся важнейшие свойства класса FileSystemWatcher.
Табл. 2-11. Свойства FileSystemWatcher
Имя	Описание
EnableRaisingEvents	Возвращает или устанавливает значение, свойства, которое определяет, будет ли генерировать события отслеживаемый объект. Обычно используется для включения и выключения мониторинга папок и файлов
Filter	Возвращает или устанавливает фильтр файлов для мониторинга изменений. Пустой фильтр включает мониторинг всех файлов
IncludeSubdirectories	Возвращает или устанавливает свойство, которое включает мониторинг вложенных папок (либо только папки, указанной в свойстве Path)
NotifyFilter	Возвращает или устанавливает тип отслеживаемых изменений. По умолчанию приходят уведомления обо всех изменениях (создании, удалении, переименовании и изменении файлов)
Path	Возвращает или устанавливает путь к отслеживаемой папке
В табл. 2-12 описан наиболее важный метод класса FileSystemWatcher.	
Табл. 2-12. Метод FileSystemWatcher	
Имя	Описание
WaitForChanged	Синхронный метод для мониторинга изменений папок, возвращает структуру с информацией обо всех изменениях
68
Программирование ввода-вывода
Глава 2
В табл. 2-13 приводятся наиболее важные события класса FileSystemWatcher.
Табл. 2-13. События FileSystemWatcher
Имя	Описание
Changed	Генерируется при изменении файла или папки в отслеживаемом каталоге
Created	Генерируется при создании файла или папки в отслеживаемом каталоге
Deleted	Генерируется при удалении файла или папки в отслеживаемом каталоге
Renamed	Генерируется при переименовании файла или папки в отслеживаемом каталоге
Мониторинг изменений в каталоге
Чтобы отслеживать изменения в каталоге, выполните следующие действия:
1.	Создайте новый объект FileSystem Watcher, указав выбранный каталог в свойстве Path.
2.	Добавьте обработчики событий Created и Deleted.
3.	Включите события, установив свойство EnableRaisingEvents равным true.
Делается это так:
' VB
Dim watcher As FileSyslemWatcher = New FileSystemWatcherO watcher.Path = "c:\"
Зарегистрировать обработчики событий
AddHandler watcher.Created,
New FileSystemEventHandler(AddressOf watcher_Changed) AddHandler watcher.Deleted,
New FileSystemEventHandler(AddressOf watcher_Changed)
Начать мониторинг watcher.EnableRaisingEvents = True
’ Обработчик события
Sub watcher_Changed(ByVal sender As Object, ByVai e As FileSystemEventArgs)
Console.WriteLine("Directory changed({0}): {1}", e.ChangeType, e.FullPath)
End Sub
// C#
FileSystemWatcher watcher = new FileSystemWatcherO;
watcher.Path = @"c:\";
Занятие 1
Навигация по файловой системе
69
// Зарегистрировать обработчики событий watcher.Created +=
new FileSystemEventHandler(watcher_Changed);
watcher.Deleted +=
new FileSystemEventHandler(watcher_Changed);
// Начать мониторинг watcher.EnableRaisingEvents = true;
// Обработчик события
static void watcher_Changed(object sender, FileSystemEventArgs e)
Console.WriteLine("Directory changed({0}): {1}”, e.ChangeType, e.FullPath);
}
Такой обработчик события просто выдает сообщение о каждом изменении, используя информацию из объекта FileSystemEventArgs, который метод-обработчик получает как параметр.
Помимо событий Added и Changed, можно контролировать переименование файлов, например так:
1.	Создайте новый объект FileSystem Watcher, указав выбранный каталог в свойстве Path.
2.	Добавьте обработчик события Renamed.
3.	Включите события, установив свойство EnableRaisingEvents равным true. Делается это так:
' VB
Dim watcher As FileSystemWatcher = New FileSystemWatcher() watcher.Path = "c:\"
Зарегистрировать обработчики событий
AddHandler watcher.Renamed,
New RenameEventHandler(AddressOf watcher_Renamed)
Начать мониторинг
watcher.EnableRaisingEvents = True
Обработчик события
Sub watcher_Renamed(ByVal sender As Object, ByVai e As RenamedEventArgs)
Console.WriteLine("Renamed from {0} to {1}", e.OldFullPath, e.FullPath)
End Sub
70
Программирование ввода-вывода
Глава 2
// C#
FileSystemWatcher watcher = new FileSystemWatcherO; watcher.Path = @"c:\";
И Зарегистрировать обработчики событий watcher.Renamed +=
new RenamedEventHandler(watcher_Renamed);
// Начать мониторинг watcher.EnableRaisingEvents = true;
// Обработчик события
static void watcher_Renamed(object sender, RenamedEventArgs e)
Console.WriteLine("Renamed from {0} to {1}", e.OldFullPath, e.FullPath);
}
При наблюдении за файловой системой число изменений может превысить возможности FileSystemWatcher. Когда происходит слишком много событий, FileSystemWatcher генерирует событие Error. Ниже рассказывается, как обрабатывать событие Error.
1.	Создайте новый объект FileSystemWatcher, указав выбранный каталог в свойстве Path.
2.	Добавьте обработчик события Error.
3.	Включите события, установив свойство EnableRaisingEvents равным true.
Делается это так:
' VB
Dim watcher As FileSystemWatcher = New FileSystemWatcherO watcher.Path = ”c:\”
' Зарегистрировать обработчики событий
AddHandler watcher.Error,
New ErrorEventHandler(AddressOf watcher_Error)
' Начать мониторинг watcher.EnableRaisingEvents = True
' Обработчик событий
Sub watcher_Error(ByVal sender As Object, ByVai e As ErrorEventArgs)
Console.WriteLineC’Error: {0}", e.GetExceptionO)
End Sub
Занятие 1	Навигация по файловой системе
// C#
FileSystemWatcher watcher = new FileSystemWatcher();
watcher.Path = @"c:\";
// Зарегистрировать обработчики событий
watcher.Error +=
new ErrorEventHandler(watcher_Error);
// Начать мониторинг
watcher.EnableRaisingEvents = true;
11 Обработчик событий
static void watcher_Error(object sender, ErrorEventArgs e)
{
Console.WriteLine("Error: {0}",
e.GetExceptionO);
}
Практикум. Перечисление и мониторинг файлов
В этой лабораторной работе вы перечислите файлы в папке и затем будете отслеживать их изменения Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение 1. Перечисление файлов в папке
Сейчас вы перечислите файлы на выбранном диске.
1.	Со ‘дайте новое консольное приложение с именем ShowFilesDemo.
2.	Добавьте директиву Import (в C# — using) для импорта в новый проект пространства имен System.IO.
3.	Добавьте метод Show Directory, принимающий в качестве параметра объект Directory Info.
4.	В добавленном методе напишите цикл для перебора всех файлов в папке и вывода их имен на консоль, Полученный код выглядит примерно так:
' VB
Sub ShowDirectory(ByVal dir As Directoryinfo)
' Показать все файлы
Dim file As Fileinfo
For Each file In dir.GetFilesO
Console.WriteLine("File: {0}", file.FullName)
Next
End Sub
// C#
static void ShowDirectory(Directoryinfo dir)
72
Программирование ввода-вывода
Глава 2
{
// Показать все файлы
foreach (Fileinfo file in dir.GetFilesO)
{
Console.WriteLine("File: {0}", file.FullName);
}
}
5.	В теле метода ShowDirectory переберите в цикле все вложенные папки, вызывая метод ShowDirectory. Рекурсивно вызываемый метод ShowDirectory найдет все файлы в каждой папке. Полученный код выглядит примерно так:
' VB
' Рекурсивный перебор
' вложенных папок
Dim subDir As Directoryinfo
For Each subDir In dir.GetDirectoriesO
ShowDi recto ry(subDiг)
Next
// C#
I/ Рекурсивный перебор
// вложенных папок
foreach (Directoryinfo subDir in dir.GetDirectoriesO)
{
ShowDirectory(subDir);
}
6.	Поместите в тело метода Main код, создающий новый экземпляр объекта Direc torylnfo для каталога Windows, и вызовите новый метод ShowDirectory, передав ему этот объект в качестве параметра. Ниже приводится соответствующий пример кода
' VB
Dim dir As Directoryinfo = New Directorylnfo(Environment.SystemDirectory) ShowDirectory(dir)
// C#
Directoryinfo dir = new Directorylnfo(Environment.SystemDirectory); ShowDirectory(dir);
7.	Соберите проект и исправьте ошибки. Убедитесь, что консольное приложение правильно перечисляет все файлы в системном каталоге (Environment.SystemDirectory).
Упражнение 2. Мониторинг изменений файловой системы
Ваша задача — отследить изменения всех файлов с расширением .ini.
1.	Создайте новое консольное приложение с именем FileWatchingDemo.
2.	Импортируйте пространство имен System.IO namespace в новый файл.
3.	Создайте новый экземпляр класса FileSystem Watcher для системного каталога. Например, это можно сделать так:
Занятие 1
Навигация по файловой системе
73
7VB
Dim watcher As New FileSystemWatcher(Environment.SystemDirectory)
// Cit
FileSystemWatcher watcher =
new FileSystemWatcher(Environment.SystemDirectory);
4.	Установите свойства созданного экземпляра объекта-наблюдателя за файловой системой, чтобы он контролировал только .ini-файлы во всех вложенных папках и уведомлял только об изменении атрибутов или размера файла. Полученный код может выглядеть примерно так:
1 VB
watcher.Filter = "* ini"
watcher.IncludeSubdirectories = True
watcher.NotifyFilter =
NotifyFilters.Attributes Or NotifyFilters.Size
// C#
watcher.Filter = "*.ini";
watcher.IncludeSubdirectories = true;
watcher.NotifyFilter =
NotifyFilters.Attributes | NotifyFilters.Size;
5.	Чтобы отслеживать изменения, добавьте к объекту-наблюдателю обработчик события Changed. Например, это можно сделать так:
• VB
AddHandler watcher.Changed,
New FileSystemEventHandler(AddressOf watcher_Changed)
// C#
watcher.Changed +=
new FileSystemEventHandler(watcher_Changed);
6.	Далее потребуется создать метод, обрабатывающий событие Changed. Добавьте в тело этого метода код, выводящий на консоль имя измененного файла. Полученный код выглядит примерно так:
' VB
Sub watcher_Changed(ByVal sender As Object,
ByVai e As FileSystemEventArgs)
Console.WriteLine("Changed. {0}", e.FullPath)
End Sub
// C#
static void watcher_Changed(object sender, FileSystemEventArgs e)
{
74
Программирование ввода-вывода
Глава 2
Console.WriteLine("Changed: {0}", е.FullPath); }
7.	Чтобы объект-наблюдатель начал генерировать события, установите свойство EnablingRaisingEvents равным true.
8.	Соберите проект и исправьте ошибки. Убедитесь, что при изменениях атрибутов или размера любого .ini-файла консольное приложение выдает сообщение.
Резюме
	Для перечисления объектов файловой системы и получения подробной информации об их свойствах можно использовать классы Fileinfo, Directoryinfo и Driveinfo.
	Класс Path позволяет получать подробную информацию о путях файловой системы, его следует использовать вместо ручного разбора путей.
	Для отслеживания изменений файловой системы, таких как добавление, удаление и переименование файлов и папок, можно использовать класс FileSystemWatcher.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе <Ответы» в конце книги.
1.	Как правильно открыть файл для записи? (Укажите все верные ответы.) А.
' VB
File.Open("somefile. txt",
FileMode.Create)
// C#
File.Open("somefile.txt", FileMode.Create);
B.
• VB
File.OpenCsomefile.txt", FileMode.Create, FileAccess.Write)
// C#
File.Open("somefile.txt", FileMode.Create, FileAccess.Write);
C.
VB
File.OpenCsomefile.txt", FileMode.Create, FileAccess.Read)
// C#
File.OpenCsomefile.txt", FileMode.Create, FileAccess.Read);
D.
' VB
Dim file As new FileInfoCsomefile.txt")
Занятие 2
Чтение и запись файлов
75
f£le. Open(FileMode.Create)
// C#
Fileinfo file = new FileInfoCsoniefile.txt");
file.Open(FileMode.Create);
2.	Какие изменения можно отслеживать посредством FileSystemWatcher (Укажите все верные ответы.)
А.	Создание файлов.
В.	Создание папок.
С.	Изменение файлов.
D.	Переименование файлов.
Е.	None.
3.	Верно ли, что следующий код изменяет расширение файла?
’ VB
Dim ourPath As String = "c:\boot.ini"
Path.ChangeExtension(ourPath, "bak”)
// C#
string ourPath = ©"c:\boot.ini";
Path.ChangeExtension(ourPath, "bak");
А. Да., В. Нет.
Занятие 2. Чтение и запись файлов
Чтение и запись файлов — одни из наиболее распространенных задач в программировании. .NET-разработчик должен уметь программировать чтение и запись в файлы. Средства .NET Framework существенно упрощают решение этих задач.
Изучив материал этого занятия, вы сможете:
J открывать файлы и читать их содержимое;
Z создавать потоки в памяти;
J выполнять запись в файлы и закрывать их.
Продолжительность занятия — 20 минут.
Введение в потоки
Потоки — универсальное средство .NET Framework, позволяющее реализовать как последовательный, так и произвольный доступ к данным. Потоки используются в самых различных разделах Framework. В основе работы с потоками лежит абстрактный базовый класс (объявленный как Mustlnherit в Visual Basic), предоставляющий базовый интерфейс и реализацию для всех потоков в .NET Framework. Некоторые свойства и методы класса Stream приводятся в табл. 2-14 и 2-15, соответственно.
76
Программирование ввода-вывода
Глава 2
Табл. 2-14.	Свойства Stream
Имя	Описание
CanRead	Определяет, поддерживает ли поток чтение
CanSeek CanTimeout	Определяет, поддерживает ли поток поиск Определяет, можно ли для потока задать время ожидания
CanWrite Length Position	Определяет, под держивает ли поток запись Возвращает длину потока (в байтах) Возвращает или устанавливает виртуальный курсор, определяющий текущее положение в потоке Значение свойства Position не должно превышать значения свойства Length (длины потока)
ReadTimeout WriteTimeout	Возвращает или устанавливает время ожидания потока при чтении Возвращает или устанавливает время ожидания потока при записи
Табл. 2-15.	Методы Stream
Имя	Описание
Close	Закрывает поток и освобождает все занятые с ним ресурсы
Flush	Очищает все буферы потока и сбрасывает модифицированный данные на накопитель, с которым работает поток
Read	Последовательно считывает указанное число байтов, начиная с текущего положения, перемещая курсор в конец прочитанной поледовательности
ReadByte	Считывает один байт и перемещает курсор на один байт Эквивалент вызова метода Read для чтения единственного байта
Seek	Устанавливает положение курсора в потоке
SetLength	Задает длину потока. Усекает поток, если новая длина меньше старой, и расширяет поток в противном случае
Write	Записывает в поток данные как серию байтов, перемещая курсор в конец записанной последовательности
WriteByte	Записывает один байт в поток и перемещает курсор. Равносилен вызову метода Write для записи единственного байта
Другие классы .NET Framework для работы с потоками являются потомками класса Stream'.
1.	FileStream {System. IO).
2.	MemoryStream {System.IO).
3.	CryptoStream {System.Security).
4.	Networkstream {System.Net).
5.	GZipStream {System.Compression).
Все потоки происходят от общего базового класса, поскольку при работе с данными принято представлять их в виде потоков. Освоив концепцию потоков, вы сможете работать с любыми типами потоков. К примеру, для вас не составит труда разработать простой метод для вывода содержимого потока на консоль, как показано ниже:
Занятие 2
Чтение и запись файлов
77
 VB
Shared Sub DumpStream(ByVal theStream As Stream) ' Установить курсор на начало потока theStream.Position = О
' Обработать поток в цикле и показать его содержимое While theStream.Position о theStream.Length
Console.WriteLine("{0:x2}”, theStream.ReadByte()) End While
End Sub
// C#
static void DumpStream(Stream theStream) {
// Установить курсор на начало потока theStream.Position = 0;
// Обработать поток в цикле и показать его содержимое while (theStream.Position != theStream.Length) {
Console.WriteLine("{0:x2}”, theSt ream.ReadByte()); }
Этот код не зависит от типа потоков и способен обрабатывать любые типы потоков. Следующий пример иллюстрирует стандартный алгоритм добавления данных к потоку: ' VB
Shared Sub AppendToStream(ByVal theStream As Stream,
ByVai data() As Byte)
Установить курсор в конец потока
theStream.Position = theStream.Length
Добавить байты
theStream.Write(data, 0, data.Length)
End Sub
// C#
static void AppendToStream(Stream theStream, byte[] data)
{
// Установить курсор в конец потока theStream.Position = theStream.Length;
// Добавить байты
theStream.Write(data, 0, data.Length);
}
78
Программирование ввода-вывода
Глава 2
Классы, упрощающие чтение и запись данных
В чтении и записи файлов участвуют и некоторые другие классы. Большинство операций выполняется с использованием класса File. Этот класс поддерживает статические методы (в Visual Basic — общие), позволяющие открывать и создавать файлы. Класс File также способен выполнять:
1.	Атомарные операции чтения-записи файла целиком;
2.	Создание и открытие файлов для чтения;
3.	Создание и открытие файлов для записи;
4.	Простые операции с файлами {File.Exists, File.Delete и т. п.).
Когда файл создан или открыт, класс File возвращает объекты различных типов. Простейший из них — FileStream, представляющий файл.
Класс File также поддерживает методы, возвращающие объекты StreamReader и StreamWriter. Эти классы по сути являются оболочками класса FileStream, поддерживающими последовательное чтение-запись в поток.
Класс File поддерживает те же операции над файлами, что и Fileinfo (см. занятие 1).
Помимо перечисленных имеется класс Memory Stream, реализующий специальный поток для работы с данными в оперативной памяти. Этот класс часто используется при оптимизации ввода-вывода для создания потоковых объектов в памяти.
Класс File
Класс File предоставляет базовую функциональность по открытию файловых потоков для чтения и записи. В табл. 2-16 приводятся важнейшие статические (общие) методы класса File.
Табл. 2-16. Статические (Общие) методы класса File
Имя	Описание
AppendAllText	Добавляет заданную строку к существующему файлу, если файл не существует, создает его
AppendText	Открывает файл (если он не существует, создает его) и возвращает объект StreamWriter, настроенный для добавления текста в файл
Copy	Создает копию файла. Завершается успешно, только если заданный файл отсутствует
Create CreateText	Создает новый файл и возвращает объект FileStream Создает или открывает файл, возвращает объект StreamWriter, подготовленный для записи текста в файл
Move Open OpenRead	Перемещает файл в другой каталог Открывает существующий файл и возвращает объект FileStream. Открывает существующий файл и возвращает объект FileStream, настроенный только для чтения.
OpenText Open Write	Открывает существующий файл и возвращает объект StreamReader Открывает существующий файл для записи и возвращает объект StreamWriter
ReadAllBytes	Открывает файл, считывает его содержимое целиком в массив байтов и закрывает файл
Занятие 2
Чтение и запись файлов
79
Табл. 2-16.	(окончание)
Имя	Описание
ReadAllLines	Открывает файл, считывает его содержимое целиком в массив строк (одна строка массива соответствует одной строке файла) и закрывает файл
ReadAUText	Открывает файл, считывает его содержимое целиком в строку и закрывает файл
WriteAllBytes	Открывает файл, записывает в него содержимое массива байтов целиком (перезаписывая существующие данные) и закрывает файл
WriteAllLines	Открывает файл, записывает в него содержимое массива строк целиком (перезаписывая существующие данные) и закрывает файл
WriteAllText	Открывает файл, записывает в него содержимое строки целиком (перезаписывая существующие данные) и закрывает файл
Класс Directory
Помимо класса File, .NET Framework поддерживает класс Directory, предоставляющий статический (общий) интерфейс для создания каталогов файловой системы и управления ими. Класс Directory предоставляет базовую функциональность по открытию файловых потоков для чтения и записи. В табл. 2-17 приводятся важнейшие статические (общие) методы класса Directory.
Табл. 2-17. Статические (общие) методы класса Directory
Имя	Описание
CreateDirectory	Создает папку в каталоге, путь к которому передан в качестве параметра
Delete	Удаляет заданную папку
Exists	Определяет, существует ли в файловой системе папка
GetCreation Time	Возвращает дату и время создания папки
GetCurrentDirectory	Возвращает объект Directory Info, представляющий текущую рабочую папку приложения
GetDirectories GetDirectoryRoot	Возвращает имена папок, вложенных в заданный каталог Возвращает информацию о томе и/или корневом каталоге для указанной папки
GetFiles GetFileSystemEntries GetLastAccessTime GetLastWrite Time GetLogicalDrives GetParent	Возвращает имена файлов в указанном каталоге Возвращает имена вложенных папок и файлов в заданном каталоге Возвращает дату и время последнего обращения к указанной папке Возвращает дату и время последней записи в указанную папку Возвращает логические диски текущей системы в формате «С:\» Возвращает родительский каталог заданной папки
Move SetCreation Time	Перемещает файл или папку (с содержимым) в указанный каталог Устанавливает дату и время создания указанной папки
80
Программирование ввода-вывода
Глава 2
Табл. 2-17. (окончание)
Имя	Описание
SetCurrentDirectory	Устанавливает указанную папку в качестве текущей рабочей папки приложения
SetLastAccess Time SetLastWrite Time	Устанавливает дату и время последнего обращения к папке Устанавливает дату и время последней записи в папку
Перечислимое FileAccess
Перечислимое FileAccess предоставляет члены, определяющие права доступа к файлу.
В табл. 2-18 приводятся члены перечислимого FileAccess.
Табл. 2-18.	Члены перечислимого FileAccess
Имя	Описание
Read Write	Задает доступ к файлу только для чтения Задает доступ к файлу для записи. Файл становится недоступным для чтения — только для записи
ReadWrite	Задает полный доступ к файлу для чтения и записи (эквивалент комбинации Read и Write)
Перечислимое FileMode
Перечислимое FileMode предоставляет члены, определяющие режиме открытия или создания файла. В табл. 2-19 приводится большинство членов перечислимого FileMode.
Табл. 2-19.	Члены перечислимого Members FileMode
Имя	Описание
Append	Открывает файл и перемещает указатель FileStream в конец файла. Используется только с FileAccess. Write
Create CreateNew Open	Создает новый файл. Если файл существует, он перезаписывается Создает новый файл. Если файл существует, генерирует исключение Открывает существующий файл. Если файл не существует, генерирует исключение
OpenOrCreate	Открывает существующий файл. Если файл не существует, создается новый файл
Truncate	Открывает существующий файл и усекает его до нулевого размера
Класс FileStream
Класс FileStream предоставляет базовую функциональность, позволяя открывать файловые потоки для чтения и записи. В табл. 2-20 и 2-21, соответственно, приводятся наиболее важные свойства и методы класса FileStream.
Занятие 2
Чтение и запись файлов
81
Табл, 2-20.	Свойства FileStream
Имя	Описание
CanRead	Определяет, поддерживает ли поток чтение (наследуется от класса Stream)
CanSeek	Определяет, поддерживает ли поток поиск (наследуется от класса Stream)
CanTimeout	Определяет, можно ли для потока задать время ожидания (наследуется от класса Stream)
CanWrite	Определяет, поддерживает ли поток запись (наследуется от класса Stream)
Handle	Возвращает описатель файла, с которым работает поток
Length Имя	Возвращает длину потока (в байтах) (наследуется от класса Stream) Возвращает имя файла
Position	Возвращает или устанавливает виртуальный курсор, определяющий текущее положение в потоке Значение свойства Position не должно превышать длины потока (наследуется от класса Stream)
ReadTimeoul	Возвращает или устанавливает время ожидания потока при чтении (наследуется от класса Stream)
WriteTimeout	Возвращает или устанавливает время ожидания потока при записи (наследуется от класса Stream)
Табл. 2-21.	Методы FileStream
Имя	Описание
Close	Закрывает поток и освобождает все занятые с ним ресурсы (наследуется от класса Stream)
Flush	Очищает все буферы потока и сбрасывает модифицированный данные на накопитель, с которым работает поток (наследуется от класса Stream)
Lock	Предотвращает изменение файла целиком и по частям другими процессами
Read	Последовательно считывает указанное число байтов, начиная с текущего положения, перемещая курсор в конец прочитанной поледовательности (наследуется от класса Stream)
ReadByte	Считывает один байт и перемещает курсор на один байт Эквивалент вызова метода Read для чтения единственного байта (наследуется от класса Stream)
Seek	Устанавливает положение курсора в потоке (наследуется от класса Stream)
SetLength	Задает длину потока. Усекает поток, если новая длина меньше старой, и расширяет поток в противном случае (наследуется от класса Stream)
Unlock	Разрешает другим процессам изменять связанный с потоком файл целиком либо частями
82
Программирование ввода-вывода
Глава 2
Табл. 2-21. (окончание)
Имя	Описание
Write	Записывает в поток данные как серию байтов, перемещая курсор в конец записанной последовательности (наследуется от класса Stream)
WriteByte	Записывает один байт в поток и перемещает курсор. Равносилен вызову метода Write для записи единственного байта (наследуется от класса Stream)
Класс StreamReader
Класс StreamReader предоставляет базовую функциональность для чтения данных из экземпляра класса производного от Stream. В табл. 2-22 и 2-23, соответственно, приводится важнейшие свойства и методы класса StreamReader.
Табл. 2-22. Свойства StreamReader
Имя	Описание
BaseStream	Возвращает поток, связанный с объектом чтения
CurrentEncoding	Возвращает текущую кодировку потока, связанного с объектом чтения
EndOfStream	Определяет, достигнут ли при чтении конец потока
’Л
Табл. 2-23. Методы StreamReader
Имя	Описание
Close	Закрывает объект чтения и соответствующий поток
Peek	Возвращает следующий символ из потока, не изменяя положения
	курсора
Read	Считывает из потока следующий набор символов
ReadBlock	Считывает из потока следующий блок символов
Read Line	Считывает из потока следующую строку символов
ReadToEnd	Считывает все символы до конца потока
Как читать из файла
Открытие файла — рутинная операция в программировании. Проще всего выполнить ее, вызвав соответствующий метод класса File и указав путь к файлу в качестве параметра. Если файл открывается для чтения, член перечислимого FileMode.Open указывает на то, что файл существует, а член FileAccess.Read используется для получения доступа к файлу только для чтения, как и показано в следующем примере:
‘ VB
Dim theFile As FileStream =
File.Open("C:\boot.ini", FileMode.Open, FileAccess.Read)
11 C#
FileStream theFile =
File.Open(@"C:\boot.ini", FileMode.Open, FileAccess.Read);
Занятие 2
Чтение и запись файлов
83
Метод File.Open возвращает объект FileStream. Файловый поток является обычным потоком, поэтому просмотреть его содержимое можно при помощи методов Read или ReadByte класса Stream. Но классы StreamReader и StreamWriter поддерживают механизмы, упрощающие операции чтения и записи. Как показано в следующем примере, для чтения файла достаточно создать объект класса StreamReader — оболочку класса FileStream-.
’ VB
Dim rdr As StreamReader = New StreamReader(theFile)
Console. Write( rdr. ReadToEndO)
rdr.CloseO
theFile.Close()
// C#
StreamReader rdr = new StreamReader(theFile);
Console. Write( rdr. ReadToEndO);
rdr.Close();
theFile.Close();
Класс StreamReader предназначен для чтения потока как строки символов, а не последовательности байтов. Таким образом, все методы StreamReader для чтения возвращают строки или массивы строк.
Класс File поддерживает дополнительные методы, упрощающие открытие файла для чтения. В предыдущем примере сначала был создан объект FileStream, а затем Stream-Reader. Как показано в этом фрагменте, класс File может непосредственно создавать StreamReader вызовом метода OpenText.
’ VB
Dim rdr As StreamReader = File.OpenText("C:\boot.ini”)
Console.Write( rdr. ReadToEndO) rdr.Close()
// C#
StreamReader rdr = File.OpenText(@"C:\boot.ini");
Console.Write(rd r.ReadToEnd());
rdr.Close();
Когда требуется просто вывести весь файл целиком, можно обойтись единственным вызовом метода Read AllText класса File. Этот метод скрывает все детали потока и реализацию объекта чтения:
' VB
Console.WriteLine(File.ReadAllText("С:\boot.ini"))
// C#
Console.WriteLine(File.ReadAllText(@"C:\boot.ini"));
Но если все данные можно получить вызовом метода ReadAllText класса File, для чего тогда остальные методы? Дело в том, что обычно нет необходимости обрабатывать текстовый файл целиком. Такой же подход особенно удобен для поиска определенного текста. Например, как показано в этом примере, данные выбираются построчно, каждая строка проверяется на соответствие условию, это не требует загружать в память весь файл:
84
Программирование ввода-вывода
Глава 2
' VB
Dim rdr As StreamReader = File.OpenText("C:\boot.ini”)
Проверяем поток до конца
While Not rdr.EndOfStream
Dim line As String = rdr.ReadLineO If line.Contains("boot") Then Обнаружив слово «boot», уведомить пользователя и прекратить чтение файла.
Console.WriteLine("Found boot:")
Console.WriteLine(line)
Exit While
End If
End While
’ Clean Up rdr.Close()
// C#
StreamReader rdr = File.OpenText(@"C:\boot.ini");
// Обнаружив слово «boot», уведомить пользователя
while (!rdr.EndOfStream) {
string line = rdr.ReadLineO;
if (line.Contains("boot")) {
// Обнаружив слово «boot», уведомить пользователя
// и прекратить чтение файла.
Console.WriteLine("Found boot:");
Console.WriteLine(line); break;
}
11 Clean Up rdr.CloseO;
Такой прием особенно полезен при поиске в очень больших файлах.
Класс StreamWriter
Класс Stream Writer предоставляет базовую функциональность для записи данных в классы, производные от Stream. В табл. 2-24 и 2-25, соответственно, приводятся наиболее важные свойства и методы класса Stream Writer.	;
Занятие 2
Чтение и запись файлов
85
Табл. 2-24. Свойства Stream Writer	
Имя	Описание
AutoFlush	Возвращает или устанавливает свойство, которое определяет, будут ли модификации копироваться в соответствующий поток при каждом вызове метода Write
BaseStream	Возвращает поток, к которому привязан объект записи
Encoding	Возвращает текущую кодировку потока, привязанного к объекту записи
NewLine	Возвращает или устанавливает символ конца строки. Обычно используется, только когда требуется изменить стандартный символ конца строки
Табл. 2-25.	Методы StreamWriter
Имя	Описание
Close	Закрывает объект записи и соответствующий поток
Write	Выполняет запись в поток
WriteLine	Записывает в поток данные и символы конца строки
Как выполнять запись в файл
Чтобы иметь возможность записи в файл, его требуется предварительно открыть для записи. Эта операция аналогична открытию файла для чтения. Вот пример кода, открывающего файла для записи: ’ VB FileStream theFile = File.Create("c:\somefile.txt")
11 C#
FileStream theFile = File.Create(@"c:\somefile.txt");
В отличие от кода, открывающего файл для чтения, этот код создает новый файл, возвращая объект FileStream, готовый для записи. Получив объект FileStream, можно писать данные прямо в поток. Однако для записи данных в новый файл чаше используется объект StreamWriter, как показано в этом листинге:	ч
• VB
Dim writer As StreamWriter = New StreamWriter(theFile) writer.WriteLine("Hello") writer. CloseO theFile.Close()
// C#
StreamWriter writer = new StreamWriter(theFile);
writer.WriteLine("Hello");
writer. CloseO;
theFile. CloseO;
gg Программирование ввода-вывода
Глава 2
Объект StreamWriter позволяет записывать текст прямо в новый файл. Такая конструкция очень похожа на таковую для чтения файла. Класс File создает объект StreamWriter при вызове метода CreateText, как в случае StreamReader для чтения:
’ VB
Dim writer As Streamwriter =
File.CreateText("c:\somefile.txt")
writer.WriteLine("Hello") writer.Close()
// C#
Streamwriter writer = File.CreateText(@”c:\somefile.txt");
writer.WriteLine("Hello");
writer. CloseO;
Класс File также поддерживает метод Write AllText, записывающий строку в новый файл:
VB
File.WriteAllText("с:\somefile.txt", "Hello")
// C#
File.WriteAllText(@"c:\somefile.txt", "Hello");
Эта операция очень проста, но иногда требуется записать данные в существующий файл. Запись в существующий файл выполняется практически так же, но файл необходимо соответствующим образом открыть. Чтобы открыть файл для записи, можно воспользоваться методом Open класса File, указав в параметрах, что необходимо записать данные в возвращаемый методом поток:
* VB
Dim theFile As FileStream
theFile = File.Open("c:\somefile.txt",
FileMode.Open,
FileAccess.Write)
11 C#
FileStream theFile = null;
theFile = File.Open(@"c:\someflie.txt",
FileMode.Open,
FileAccess.Write);
Метод OpenWrite класса File позволяет обойтись меньшим количеством кода для той же операции. Вызов метода Open класса File с параметром, задающим доступ для записи, можно заменить таким кодом:
' VB
theFile = File.OpenWrite("c:\somefile.txt”)
// C#
theFile = File.OpenWrite(@"c:\somefile.txt");
Занятие 2
Чтение и запись файлов
87
Этот код работает, только если файл существует. Но часто требуется открыть существующий файл либо создать новый. К сожалению, метод Open Write только открывает существующие файлы. Конечно, можно написать код, проверяющий существование файла и создающий файл в случае его отсутствия, но, к счастью, метод Open класса File поддерживает параметр, который указывает, что нужно сделать: создать файл либо открыть его:
' VB
theFile = File.Open("c:\somefile.txt",
FileMode.OpenOrCreate.
FileAccess.Write)
// C#
theFile = File.Open(@"c:\somefile.txt”,
FileMode OpenOrCreate,
FileAccess.Write);
Значение перечислимого FileMode.OpenOrCreate позволяет обойтись и без этого.
Введение в объекты чтения и записи
Как показано в предыдущем разделе, классы StreamReader и StreamWriter упрощают запись в текстовые потоки и чтение из них. Класс StreamReader является производным от абстрактного класса TextReader (объявлен с Mustlnherit в Visual Basic), само собой, класс StreamWriter — потомок абстрактного класса TextWriter. Эти абстрактные классы предоставляют базовые интерфейсы для всех объектов чтения и записи, работающих с текстовыми данными.
Есть и другие объекты чтения и записи текстов — StringReader и StringWriter. Эти классы предназначены для чтения-записи строк, хранящихся в памяти. Следующий пример иллюстрирует чтение из строки при помощи класса StringReader.
’ VB
Dim s As String = "Hello all” &
Environment.NewLine &
"This is a multi-line" & _
Environment.NewLine &
"string"
Dim rdr As New StringReader(s)
’ Проверить, нет ли еще символов
While rdr.Peek() о -1
Dim line As String = rdr.ReadLineO Console.WriteLine(line)
End While
// C#
string s = @”Hello all This is a multi-line text string";
Qg Программирование ввода-вывода	Глава 2
StringReader rdr = new StringReader(s);
// Проверить, нет ли еще символов
while (rdr.Peek() != -1) {
string line = rdr. ReadLineO;
Console.WriteLine(line);
}
Соовтетственно, для записи строк имеет смысл применять StringWriter. StringWriter использует класс StringBuilder, и поэтому эффективен при создании длинных строк. С этим классом работают так:
’ VB
Dim writer As New StringWriter()
writer.Writeline("Hello all”)
writer.WriteLine("This is a multi-line")
writer.WriteLine("text string")
Console WriteLine(writer.ToString())
// C#
StringWriter writer = new StringWriterO;
writer.WriteLine("Hello all");
writer.WriteLine("This is a multi-line");
writer.WriteLine("text string”);
Console. WriteLine(writer. ToStringO);
Но записывать текстовые данные требуется не всегда, поэтому пространство имен .NET Framework содержит два класса для записи двоичных данных. Классы BinaryReader и BinaryWriter используются для чтения-записи двоичных данных в потоки. Например, создав файл для хранения двоичных данных, можно при помощи класса BinaryWriter записать в соответствующий поток различных типы данных, как показано здесь:
' VB
Dim NewFile As FileStream = File.CreateC"c:\somefile bin")
Dim writer As BinaryWriter = New BinaryWriter(NewFile)
Dim number As Long = 100
Dim bytesO As Byte = New Byte() {10, 20, 50, 100}
Dim s As String = "hunger"
writer.Write(number)
writer.Write(bytes)
writer.Write(s)
writer. CloseO
Занятие 2
Чтение и запись файлов
89
// с#
FileStream newFile = File.Create(@"c:\somefile.bin");
BinaryWriter writer = new BinaryWriter(newFile);
long number = 100;
byte[] bytes = new byte[] { 10, 20, 50, 100 };
string s = "hunger";
writer.Write(number);
writer.Write(bytes);
writer. Write(s);
writer.Close();
Данные, записанные через BinaryWriter, можно прочитать в той же последовательности при помощи Binary Reader, каждому вызову методов BinaryWriter. Write или WriteLine должен соответствовать вызов подходящего метода BinaryReader. Read. К примеру, следующий код читает данные, записанные выше:
' VB
Dim NewFile As FileStream = File.0pen("c:\somefile.bin", FileMode.Open)
Dim reader As New BinaryReader(NewFile)
Dim number As Long = reader.Readlnt64()
Dim bytesO As Byte = reader. ReadBytes(4)
Dim s As String = reader.ReadString()
reader Close()
Console.WriteLine(number)
Dim b As Byte
For Each b In bytes
Console.Write("[{0}]", b)
Next
Console.WriteLine()
Console WriteLine(s)
// C#
FileStream newFile = File.0pen(@"c:\somefile.bin", FileMode.Open);
BinaryReader reader = new BinaryReader(newFile);
long number = reader.Readlnt64();
byte[] bytes = reader.ReadBytes(4);
string s = reader.ReadString();
Глава 2
1ч Программирование ввода-вывода
reader.Close();
Console.WriteLine(number);
foreach (byte b in bytes)
{
Console.Write("[{0}]", b);
}
Console WriteLineO;
Console.WriteLine(s);
Класс Memorystream
Класс MemoryStream предоставляет базовую функциональность для создания потоков в оперативной памяти. В табл. 2-26 и 2-27, соответственно, приводятся важнейшие свойства и методы класса MemoryStream.
Табл. 2-26.	Свойства MemoryStream
Имя	Описание
CanRead	Определяет, поддерживает ли поток чтение (наследуется от класса Stream)
CanSeek	Определяет, поддерживает ли поток поиск (наследуется от класса Stream)
CanTimeout	Определяет, можно ли для потока задать время ожидания (наследуется от класса Stream)
Can Write	Определяет, поддерживает ли поток запись (наследуется от класса Stream)
Capacity Length Position	Возвращает или устанавливает число байтов, выделенных для потока Возвращает длину потока (в байтах) (наследуется от класса Stream) Возвращает или устанавливает виртуальный курсор, определяющий текущее положение в потоке Значение свойства Position не должно превышать длины потока (наследуется от класса Stream)
ReadTimeout	Возвращает или устанавливает время ожидания потока для операций чтения (наследуется от класса Stream)
WriteTimeout	Возвращает или устанавливает время ожидания потока для операций записи, (наследуется от класса Stream)
Табл. 2-27.	Методы MemoryStream
Имя	Описание
Close	Закрывает поток и освобождает все занятые с ним ресурсы (наследуется от класса Stream)
Flush	Очищает все буферы потока и сбрасывает модифицированный данные на накопитель, с которым работает поток (наследуется от класса Stream)
GetBuffer	Извлекает и возвращает массив использовавшихся при создании потока байтов без знака
Занятие 2
Чтение и запись файлов
91
Табл. 2-27.	(окончание)
Имя	Описание
Read	Последовательно считывает указанное число байтов, начиная с текущего положения, перемещая курсор в конец прочитанной поледовательности (наследуется от класса Stream)
ReadByte	Считывает один байт и перемещает курсор на один байт Эквивалент вызова метода Read для чтения единственного байта (наследуется от класса Stream)
Seek	Устанавливает положение курсора в потоке (наследуется от класса Stream)
SetLength	Задает длину потока. Этот метод усекает поток, если новая длина меньше старой, и расширяет поток в противном случае (наследуется от класса Stream)
ToArray Write	Записывает поток целиком в массив байтов Записывает в поток данные как серию байтов, перемещая курсор в конец записанной последовательности (наследуется от класса Stream)
WriteByte	Записывает один байт в поток и перемещает курсор. Равносильно вызову Write для записи единственного байта (наследуется от класса Stream)
WriteTo	Записывает MemoryStream в другой поток
Применение класса Memorystream
Как видно, умение работать с потоками данных важно для каждого разработчика. К сожалению, часто требуется создать поток раньше, чем появится необходимость сохранить его (например, в файле). Класс MemoryStream помогает создавать потоки в памяти. Чтобы создать поток в памяти, достаточно создать экземпляр класса MemoryStream' ' VB
Dim memStrm As New MemoryStream()
// C#
MemoryStream memStrm = new MemoryStream();
Чтобы записать данные в новый объект MemoryStream, можно воспользоваться StreamWriter, как ранее при записи в FileStream-.
' VB
Dim writer As New StreamWriter(memStrm)
writer.WriteLine("Hello")
writer.WriteLine("Goodbye")
// C#
StreamWriter writer = new StreamWriter(memStrm);
write r.WriteLine("Hello");
writer.WriteLine("Goodbye");
Как работать с потоком после записи данных в объект MemoryStream? Разработчики класса MemoryStream понимают, что существование потока в памяти — временное явле
92 Программирование ввода-вывода	Глава 2
ние. Поэтому этот класс поддерживает запись в другие потоки и копирование данных в другое хранилище. Часто класс MemoryStream используется, чтобы свести к минимуму время, на протяжении которого файл открыт для записи (так как на этот период файл блокируется). Поэтому, продолжая последний пример, лучше переписать MemoryStream в FileStream, как показано здесь:
' VB
Приказать объекту записи переписать данные в поток
writer. FlushO
' Создать файловый поток
Dim theFile As FileStream = File CreateC'c\inmemory.txt")
Переписать содержимое потока в памяти в файл целиком
memStrm.WriteTo(theFile)
Освободить ресурсы
writer. CloseO
theFile. CloseO
memSt rm. CloseO
// C#
// Приказать объекту записи переписать данные
// в поток
writer. FlushO;
// Создать файловый поток
FileStream theFile = File.Create(@”c:\inmemory txt");
// Переписать содержимое потока в памяти в файл целиком
memStrm.WriteTo(theFile);
// Освободить ресурсы
writer. CloseO;
theFile. CloseO;
memSt rm. CloseO;
Этот код выполняет следующие действия:
1.	Объект StreamWriter получает указание перенести модифицированные данные в соответствующий поток (в нашем случае этот поток в памяти MemoryStream).
2.	Создается новый файл.
3.	Объект MemoryStream получает указание записать свое содержимое в объект FileStream.
Такая процедура позволяет долго обрабатывать данные, хранящиеся в MemoryStream, затем открыть файл, быстро записать в него результаты и сразу закрыть.
Занятие 2
Чтение и запись файлов
93
Класс BufferedStream
Класс BufferedStream поддерживает оптимизацию производительности путем буферизации чтения-записи потоков, для которых он является оболочкой. В табл. 2-28 и 2-29, соответственно, приводятся наиболее важные свойства и методы класса BufferedStream.
Табл. 2-28.	Свойства BufferedStream
Имя	Описание
CanRead	Определяет, поддерживает ли поток чтение (наследуется от класса Stream)
CanSeek.	Определяет, поддерживает ли поток поиск (наследуется от класса Stream)
CanTimeout	Определяет, можно ли для потока задать время ожидания (наследуется от класса Stream)
Can Write	Определяет, поддерживает ли поток запись (наследуется от класса Stream)
Length Position	Возвращает длину потока (в байтах) (наследуется от класса Stream) Возвращает или устанавливает виртуальный курсор, определяющий текущее положение в потоке Значение свойства Position не должно превышать длины потока (наследуется от класса Stream)
ReadTimeout	Возвращает или устанавливает время ожидания потока при чтении (наследуется от класса Stream)
WriteTimeout	Возвращает или устанавливает время ожидания потока для операций записи (наследуется от класса Stream)
Табл. 2-29.	Методы BufferedStream
Имя	Описание
Close	Закрывает поток и освобождает все занятые с ним ресурсы (наследуется от класса Stream)
Flush	Очищает все буферы потока и сбрасывает модифицированный данные на накопитель, с которым работает поток (наследуется от класса Stream)
Read	Последовательно считывает указанное число байтов, начиная с текущего положения, перемещая курсор в конец прочитанной поледовательности (наследуется от класса Stream)
ReadByte	Считывает один байт, смещая курсор на один байт. Эквивалент вызова метода Read для чтения единственного байта (наследуется от класса Stream)
Seek	Устанавливает положение курсора в потоке (наследуется от класса Stream)
SetLength	Задает длину потока. Этот метод усекает поток, если новая длина меньше старой, и расширяет поток в противном случае (наследуется от класса Stream)
Write	Записывает в поток данные как серию байтов, перемещая курсор в конец записанной последовательности (наследуется от класса Stream)
WriteByte	Записывает один байт в поток и перемещает курсор. Равносильно вызову Write для записи единственного байта (наследуется от класса Stream)
94
Программирование ввода-вывода
Глава 2
Применение класса BufferedStream
Иногда запись данных прямо в поток не обеспечивает максимальную эффективность. В таких случаях имеет смысл использовать класс BufferedStream. BufferedStream — это оболочка для другого объекта-потока, поддерживающая запись в буфер, и только при сбросе буфера данные из него попадают в связанный поток. Ниже показано, как использовать класс BufferedStream'.
1.	Получите новый объект FileStream, создав файл при помощи класса File.
2.	Создайте новый буферизованный поток на основе файлового потока.
3.	Используя StreamWriter, запишите данные в буферизованный поток.
’ Делается это так:
’ VB
Dim NewFile As FileStream = File.Create("c:\test.txt")
Dim buffered As New BufferedStream(NewFile)
I
Dim writer As New StreamWriter(buffered)
writer.WriteLine("Some data")
writer. CloseO
// C#
FileStream newFile = File.Create(@"c:\test.txt");
BufferedStream buffered = new BufferedStream(newFile);
StreamWriter writer = new StreamWriter(buffered);
writer.WriteLine("Some data");
writer. CloseO;
Практикум. Чтение и запись файлов
Сейчас вы создадите файл, запишете в него данные и закроете файл. Затем вы снова откроете этот файл, прочитаете из него данные и выведете их на консоль. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение 1. Запись в новый файл
Вы должны создать новый файл и записать в него текст.
1.	Создайте новое консольное приложение с именем FileDemo.
2.	Добавьте директиву Import (в C# — using), чтобы импортировать в новый проект пространство имен System. 10.
3.	В методе Main создайте новый объект StreamWriter, используя метод Create класса File.
Занятие 2
Чтение и запись файлов
95
4.	Запишите несколько строк в объект записи потока посредством метода WriteLme.
5.	Закройте StreamWriter. Полученный код выглядит примерно так:
’ VB
Shared Sub Main(ByVal args() As String)
Dim writer As StreamWriter =
File.C reateText("c:\newfile.txt")
writer.WriteLine("This is my new file")
writer.WriteLine("Do you like its format?")
writer. CloseO
End Sub
// C#
static void Main(string[] args)
{
StreamWriter writer = File.CreateText(@"c:\newfile.txt");
writer.WriteLine("This is my new file");
writer.WriteLine("Do you like its format?");
writer. CloseO;
}
6.	Соберите проект и исправьте ошибки. Проверьте, появился ли файл.
Упражнение 2. Чтение из файла
В этом упражнении вы откроете ранее созданный файл и выведете его содержимое на консоль.
1.	Откройте проект FileDemo, созданный в упражнении 1.
2.	В методе Main, закрыв объект StreamWriter, вызовите метод OpenText класса File, чтобы открыть файл и создать новый объект StreamReader.
3.	Создайте строку с именем contents и вызовите метод ReadToEnd класса StreamReader, чтобы пол)чить содержимое файла целиком.
4.	Закройте объект StreamReader.
5.	Выведите строку на консоль. Полученный код выглядит примерно так:
' VB
Dim reader As StreamReader = _
File.OpenText("c:\newfile.txt”)
Dim contents As String = reader. ReadToEndO
reader. CloseO
Console.WriteLine(contents)
// C#
StreamReader reader = File.OpenText(@"c.\newfile.txt");
string contents = reader.ReadToEndO;
reader. CloseO;
Console.WriteLine(contents);
6.	Соберите проект, исправьте ошибки. Убедитесь, что консольное приложение правильно выводит содержимое файла в окне консоли.
96
Программирование ввода-вывода
Глава 2
Резюме
	Класс File позволяет открывать, создавать, читать и записывать файлы целиком либо по частям.
	Класс FileStream представляет файл и (в зависимости от способа создания объекта класса) позволяет выполнять чтение и запись.
	Чтобы упростить чтение-запись строк в потоки, используются классы StreamReader и StreamWriter.
	Класс MemoryStream — специализированный поток, поддерживающий создание в памяти буфера чтения-записи и запись данных буферизованного потока в другие потоки.
Закрепление материала
Ниже приводятся вопросы для самопроверки Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Какие методы класса FileStream изменяют свойство Position? (Укажите все верные ответы.)
A. Read.
В. Lock.
С. Write.
D. Seek.
2.	Как приказать объекту Stream Writer переписать модифицированные данные в другой поток? (Укажите все верные ответы.)
А. Закрыть the StreamWriter.
В. Вызвать метод Flush класса Stream Writer.
С. Установить значение свойства AutoFlush класса StreamWriter равным true.
D. Закрыть поток. ~
3.	Как создать FileStream для записи, открыв существующий файл, либо, в случае его отсутствия, создав новый файл? (Укажите все верные ответы.)
А. Создать новый экземпляр класса FileStream, указав член перечислимого File Mode. OpenOrCreate.
В. Для создания объекта FileStream вызвать метод File.Create.
С. Вызвать метод File.Open, указав член перечислимого FileMode.OpenOrCreate.
D. Вызвать метод File.Open, указав член перечислимого FileMode.Open.
Занятие 3. Сжатие потоков
После знакомства с основами работы с потоками пришло время узнать о новом, особом типе потоков. В реальных проектах часто имеет смысл сжимать данные для экономии памяти и ресурсов каналов связи. .NET Framework поддерживает два новых клаисд потоков, поддерживающих сжатие данных.
Занятие 3
Сжатие потоков
97
Изучив материал этого занятия, вы сможете:
J сжимать потоки при помощи классов GZipStream и DeflateStream-,
J производить декомпрессию потоков при помощи классов GZipStream
и DeflateStream.
Продолжительность занятия —10 минут.
Введение в сжатие потоков
В числе средств ввода-вывода .NET Framework реализованы два алгоритма сжатия данных: GZIP и DEFLATE. Оба алгоритма являются открытыми промышленными стандартами. Поэтому вы можете использовать любой из них в своих приложениях, не опасаясь нарушения авторских прав.
ПРИМЕЧАНИЕ Ограничения на размер сжимаемых данных
Оба алгоритма имеют ограничение по размеру исходных сжимаемых данных: он не должен превышать 4 Гб.
В .NET Framework алгоритмы сжатия представлены особыми типами потоков, поддерживающих сжатие и декомпрессию. Эти потоки реализованы в классах GZipStream и DeflateStream.
ПРИМЕЧАНИЕ Какой алгоритм выбрать?
Оба класса, и DeflateStream, и GZipStream, сжимают данные по идентичному алгоритму. Единственное отличие заключается в том, что спецификация GZIP1 позволяет хранить в заголовках дополнительную информацию для декомпрессии распространенным архиватором gzip. Если вы работаете со сжатыми данными только внутри собственной программы, DeflateStream будет компактнее благодаря отсутствию дополнительной информации в заголовках, но если вы предполагаете распространять сжатые файлы для декомпрессии посредством GZIP, используйте GZipStream.
Класс GZipStream
Класс GZipStream позволяет сжимать данные по алгоритму GZIP, используя дополнительный поток. В табл. 2-30 и 2-31, соответственно, приводятся наиболее важные свойства и методы класса GZipStream.
Табл. 2-30.	Свойства GZipStream
Имя	Описание
BaseStream	Возвращает базовый поток
CanRead	Определяет, поддерживает ли поток чтение при декомпрессии файла (наследуется от класса Stream)
CanSeek	Определяет, поддерживает ли поток поиск (наследуется от класса Stream)
CanTimeout	Определяет, можно ли для потока задать время ожидания (наследуется от класса Stream)
CanWrite	Определяет, поддерживает ли поток запись (наследуется от класса Stream)
1 http://www.ietf.org/rfc/rfcl952.txt?number=1952
98
Программирование ввода-вывода
Глава 2
Табл. 2-30.	(окончание)
Имя	Описание
Length	He используется. Генерирует исключение NotSupportedException (наследуется от класса Stream)
Position	Не используется. Генерирует исключение NotSupportedException (наследуется от класса Stream)
ReadTimeout	Возвращает или устанавливает время ожидания потока при чтении (наследуется от класса Stream)
WriteTimeout	Возвращает или устанавливает время ожидания потока для операций записи (наследуется от класса Stream)
Табл. 2-31.	Методы GZipStream
Имя	Описание
Close	Закрывает поток и освобождает все занятые с ним ресурсы (наследуется от класса Stream)
Flush	Очищает все буферы потока и сбрасывает модифицированный данные на накопитель, с которым работает поток (наследуется от класса Stream)
Read	Последовательно считывает указанное число байтов, начиная с текущего положения, перемещая курсор в конец прочитанной поледовательности (наследуется от класса Stream)
ReadByte	Считывает один байт и перемещает курсор на один байт Эквивалент вызова Read для чтения единственного байта (наследуется от класса Stream)
Seek.	Не используется. Генерирует исключение NotSupportedException (наследуется от класса Stream)
SetLength	Не используется. Генерирует исключение NotSupportedException (наследуется от класса Stream)
Write	Записывает в поток данные как серию байтов, перемещая курсор в конец записанной последовательности (наследуется от класса Stream)
WriteByte	Записывает один байт в поток и перемещает курсор. Равносильно вызову Write для записи единственного байта (наследуется от класса Stream)
Класс DeflateStream
Класс DeflateStream позволяет сжимать данные по алгоритму DEFLATE, используя дополнительный поток. В табл. 2-32 и 2-33, соответственно, приводятся наиболее важные свойства и методы класса DeflateStream.
Табл. 2-32.	Свойства DeflateStream
Имя	Описание
BaseStream CanRead	Возвращает базовый поток Определяет, поддерживает ли поток чтение (наследуется от класса Stream)
Занятие 3
Сжатие потоков
99
Табл. 2-32.	(окончание)
Имя	Описание
CanSeek	Определяет, поддерживает ли поток поиск (наследуется от класса Stream)
CanTimeout	Определяет, можно ли для потока задать время ожидания (наследуется от класса Stream)
Can Write	Определяет, поддерживает ли поток запись (наследуется от класса Stream)
Length	Не используется. Генерирует исключение NotSupportedException (наследуется от класса Stream)
Position	Не используется. Генерирует исключение NotSupportedException (наследуется от класса Stream)
ReadTimeout	Возвращает или устанавливает время ожидания потока при чтении (наследуется от класса Stream)
WriteTimeout	Возвращает или устанавливает время ожидания потока при записи (наследуется от класса Stream)
Табл. 2-33.	Методы DeflateStream
Имя	Описание
Close	Закрывает поток и освобождает все занятые с ним ресурсы (наследуется от класса Stream)
Hush	Очищает все буферы потока и сбрасывает модифицированный данные на накопитель, с которым работает поток (наследуется от класса Stream)
Read	Последовательно считывает указанное число байтов, начиная с текущего положения, перемещая курсор в конец прочитанной поледовательности (наследуется от класса Stream)
ReadByte	Считывает один байт и перемещает курсор на один байт Эквивалент вызова Read для чтения единственного байта (наследуется от класса Stream)
Seek	Не используется. Генерирует исключение NotSupportedException (наследуется от класса Stream)
SetLength	Не используется. Генерирует исключение NotSupportedException (наследуется от класса Stream)
Write	Записывает в поток данные как серию байтов, перемещая курсор в конец записанной последовательности (наследуется от класса Stream)
WriteByte	Записывает один байт в поток и перемещает курсор. Равносильно вызову Write для записи единственного байта (наследуется от класса Stream)
Как сжать данные
Сжатые потоки несколько отличаются от потоков, рассматривавшихся на предыдущих занятиях. Вместо записи данных в некий ресурс (в файл, как FileStream, или в память,
100 Программирование ввода-вывода
Глава 2
как MemoryStream), такой поток записывает данные в другой поток. Поток сжатия получает данные так же, как все остальные потоки, но записывает их в другой поток в сжатом (или несжатом) виде.
Ниже приводится типичный пример: сжатие и запись сжатых данных в файл. Сначала следует открыть файл, который требуется сжать, и файл, в который будет записан результат:
' VB
Dim sourceFile As FileStream = File.OpenRead(inFilename)
Dim destFile As FileStream = File Create(outFilename)
// C#
FileStream sourceFile = File.OpenRead(inFilename);
FileStream destFile = File.Create(outFilename);
Поток сжатия — оболочка для выходного потока. Здесь показано, как это работает решается эта задача при вызове конструктора:
’ VB
Dim compStream As
New GZipStream(destFile, CompressionMode.Compress)
// C#
GZipStream compStream =
new GZipStream(destFile, CompressionMode.Compress);
В этой строке кода поток сжатия получает указание сжать данные и поместить их в целевой поток. Значение перечислимого CompressionMode, определяющее выбор между сжатием и декомпрессией, передается конструктору в качестве параметра. Поскольку в этом примере поток требуется сжать, используется значение Compression Mode.Compress. После создания сжатого потока остается только считывать данные из исходного потока и передавать их сжатому потоку, как показано здесь:
’ VB
Dim theByte As Integer = sourceFile. ReadByteO
While theByte <> -1
compStream.WriteByte(CType(theByte, Byte))
theByte = sourceFile ReadByteO
End While
// C#
int theByte = sourceFile.ReadByteO;
while (theByte != -1)
{
compStream WriteByte((byte)theByte);
theByte = sourceFile. ReadByteO;
}
Этот код байт за байтом переносит данные из исходного файла (sourceFile) в поток сжатия (compStream), а не — обратите внимание! — в целевой файл (destFile). Поскольку запись ведется через поток сжатия, целевой поток заполняется уже сжатыми данными исходного файла.
Занятие 3
Сжатие потоков
101
Приведенный код подходит не только для алгоритма GZIP. Если заменить вызов конструктора потока на вызов конструктора DeflateStream, остальной будет работать как прежде, достаточно создать объект DeflateStream вместо GZipStream, как показано здесь (заметьте, что сигнатура не изменилась):
1 VB
Dim compStream As
New DeflateStream(destFile, CompressionMode.Compress)
// C#
DeflateStream compStream =
new DeflateStream(destFile, CompressionMode.Compress);
Декомпрессия данных
Подход к программированию декомпрессии аналогичен таковому для сжатия, за исключением небольших нюансов, касающихся работы с потоками. Как и раньше, требуется создать исходный и целевой файлы:
•	VB
Dim sourceFile As FileStream = File.OpenRead(inFilename)
Dim destFile As FileStream = File.Create(outFilename)
// C#
FileStream sourceFile = File.OpenRead(inFilename);
FileStream destFile = File.Create(outFilename);
В этом примере сжатый файл является исходным, а в целевой файл будут записаны разуплотненные данные. В создании потока сжатия — два отличия: поток сжатия служит оболочкой для исходного файла со сжатыми данными, а в качестве параметра передается значение перечислимого CompressionMode.Decompress, оно указывает на необходимость декомпрессии инкапсулированного потока:
' VB
Dim compStream As
New GZipStream(sourceFile, CompressionMode.Compress)
// C#
GZipStream compStream =
new GZipStream(sourceFile, CompressionMode.Compress);
К тому же меняется сама процедура обработки файла, поскольку чтение производится из потока сжатия, а не исходного файла, а запись — прямо в файл, а не в поток сжатия:
•	VB
Dim theByte As Integer = compStream. ReadByteO
While theByte <> -1
destFile.WriteByte(CType(theByte, Byte))
theByte = compStream. ReadByteO
End While
t.
102	Программирование ввода-вывода
Глава 2
// C#
int theByte = compStream. ReadByteO;
while (theByte != -1)
{
destFile.WriteByte((byte)theByte);
theByte = compStream ReadByteO;
}
И при сжатии, и при декомпрессии поток сжатия должен быть оболочкой потока, со сжатыми данными. В зависимости от того, производится ли сжатие или декомпрессия, выполняется чтение либо запись сжатых данных.
Практикум. Сжатие и декомпрессия существующего файла
В этом практикуме вы создадите простое консольное приложение, которое читает файл, сжимает его и записывает в новый файл. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение 1. Сжатие существующего файла
В этом упражнении вы сожмете существующий файл, получив новый, сжатый.
1.	Создайте новое консольное приложение с именем CompressionDemo.
2.	Добавьте директиву Import (в C# — using) чтобы импортировать в новый проект пространства имен System.IO и System.IO.Compression.
3.	Создайте статический (в Visual Basic — общий) метод с именем CompressFile, принимающий в качестве параметров две строки: inFilename и outFilename. Сигнатура метода должна выглядеть так:
' VB
Shared Sub CompressFile(ByVal inFilename As String. ByVai outFilename As String) End Sub
// C#
static void CompressFile(string inFilename, string outFilename)
{
}
4.	В теле этого метода откройте объект FileStream (с именем sourceFile), открывающий файл, заданный параметром inFilename.
5.	Создайте новый объект FileStream (с именем destFile), создающий файл, заданный параметром outFilename.
6.	Создайте новый объект GZipStream (с именем compStream), задав destFile в качестве потока для записи сжатых данных. Также укажите, что это поток для сжатия. Полученный код выглядит примерно так:
' VB	F
Dim compStream As
New GZipStream(destFile, CompressionMode.Compress)
Занятие 3
Сжатие потоков
103
/Л с#
GZipStream compStream =
new GZipStream(destFile, CompressionMode.Compress);
7.	Байт за байтом перенесите данные из исходного файла в поток сжатия:
’ VB
Dim theByte As Integer = sourceFile. ReadByteO
While theByte <> -1
compStream.WriteByte(CType(theByte, Byte))
theByte = sourceFile. ReadByteO '
End While
11 C#
int theByte = sourceFile.ReadByteO;
while (theByte != -1)
compStream.WriteByte((byte)theByte);
theByte = sourceFile. ReadByteO;
}
8.	Перед выходом из метода закройте все потоки.
9.	Из метода Main нового консольного проекта вызовите метод CompressFile, передав ему в качестве параметров имена существующего и нового файла (имя нового файла обычно получают прибавлением .gz к имени исходного). Получится примерно такой код:
’ VB
CompressFile("c:\boot.ini", "c:\boot.ini.gz”)
// C#
CompressFile(@"c:\boot.ini", @"c:\boot.ini.gz");
10.	Соберите проект и исправьте ошибки. Проверьте, появился ли сжатый файл. В текстовом редакторе, таком как Блокнот, содержимое файла должно выглядеть бессмысленным набором символов.
Упражнение 2. Декомпрессия файла
Сейчас вы откроете файл, созданный в упражнении 1, разуплотните его содержимое и запишете его в новый файл.
1.	Откройте проект CompressionDemo, созданный в упражнении 1.
2.	Создайте новый статический (в Visual Basic — общий) метод с именем UncompressFile, принимающий в качестве параметров две строки: inFileName и outFileName. Сигнатура метода должна выглядеть так:
’ VB
Shared Sub UncompressFile(ByVal inFilename As String,
ByVai outFilename As String)
End Sub
104 Программирование ввода-вывода	Глава 2
// C#
static void UncompressFile(string inFilename, string outFilename) { }
3.	В теле этого метода откройте объект FileStream (с именем sourceFile), открывающий файл, заданный параметром inFilename, — это должен быть сжатый файл, записанный в упражнении 1.
4.	Создайте новый объект FileStream (с именем destFile), создающий файл, заданный параметром outFilename.
5.	Создайте объект GZipStream (с именем compStream), задав sourceFile в качестве потока, из которого будут считываться сжатые данные. Также укажите, что это поток для декомпрессии. Полученный код выглядит примерно так:
VB
Dim compStream As
New GZipStream(sourceFile, CompressionMode.Decompress) End Sub
U c#
GZipStream compStream =
new GZipStream(sourceFile, CompressionMode.Decompress);
6.	Байт за байтом перенесите данные из сжатого файла в результирующий файл. Полученный код выглядит примерно так:
• VB
Dim theByte As Integer = compStream. ReadByteO
While theByte <> -1
destFile.WriteByte(CType(theByte, Byte)) theByte = compStream. ReadByteO End While
// C#
int theByte = compStream.ReadByteO;
while (theByte != -1) {
destFile.WriteByte((byte)theByte);
theByte = compStream. ReadByteO; }
7.	Перед выходом из метода закройте все потоки.
8	Из метода Main нового консольного проекта вызовите метод UncompressFile, передав ему в качестве параметров имя сжатого файла, созданного в упражнении, и имя файла для записи несжатых данных. Получится примерно такой код:
• VB
DecompressFile("c:\boot.ini.gz", c:\boot.ini.test)	$
// C#
DecompressFile(@"c:\boot.ini.gz", @”c:\boot.ini.test”);
Занятие 4
Работа с изолированным хранилищем Q§
9. Соберите проект и исправьте ошибки. Убедитесь, что консольное приложение правильно создало несжатый файл, открыв его в Блокноте. Сравните содержимое этого файла с исходным — файлы должны быть идентичными.
Резюме
	Классы потоков сжатия (GZipStream и DeflateStream) поддерживаю сжатие-декомпрессию данных объемом до 4 Гб.
	Классы потоков сжатия служат оболочками потоков, хранящих сжатые данные.
Закрепление материала
Ниже приводятся вопросы для самопроверки Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1. Как указать поток для записи сжатых данных при сжатии посредством класса DeflateStream class?
А. Занести в свойство BaseStream целевой поток, а свойству CompressionMode присвоить значение Compression.
В. Указать поток для записи при создании объекта DeflateStream (например, в конструкторе).
С. Использовать метод Write класса DeflateStream.
D. Добавить обработчик события BaseStream класса DeflateStream.
2. Какие данные можно сжимать при помощи класса GZipStream?. (Укажите все верные ответы.)
А. Любые файлы.
В.	Любые данные.
С.	Любые данные, объем которых не превышает 4 Гб.
D.	Любые файлы, размеры которых не превышают 4 Гб.
Занятие 4. Работа с изолированным хранилищем
Становится все очевиднее, что предоставлять программам неограниченный доступ к компьютеру не следует. Учитывая опасности, исходящие от вирусов, шпионских программ и прочего вредоносного кода, ясно, что для большинства пользователей оптимальный выбор — работа в изолированной программной среде («песочнице», sanbox) с ограниченными правами доступа. К сожалению, многим программам требуется хранить данные своего состояния, но состояние можно защитить так же надежно, как данные в кэше. Чтобы найти компромисс между потребностями приложений в сохранении данных и желанием администраторов и пользователей ограничить права доступа кода, в .NET Framework поддерживается концепция изолированного хранения (isolated storage).
106 Программирование ввода-вывода
Глава 2
Изучив материал этого занятия, вы сможете:
✓ При помощи класса IsolatedStorageFile получать доступ к изолированному хранилищу для сохранения данных программы.
J Создавать файлы и папки в изолированном хранилище, используя класс IsolatedStorageFileStream.
J Получать доступ к различным областям внутри изолированного хранилища, специфичным для пользователя и компьютера, используя класс IsolatedStorageFile.
Продолжительность занятия — 15 минут.
Введение в изолированное хранилище
Злоумышленники так и мечтают всучить пользователям вирусы и шпионские программы под видом «благонадежных» программ, поэтому выполнение кода с ограниченными правами имеет много преимуществ. В .NET Framework заложено несколько механизмов реализации принципа наименьших привилегий. Поскольку большинству приложений требуется хранить свое состояние в постоянной памяти (но не используя базы данных и аналогичные им средства), неплохо иметь безопасное хранилище, доступное приложениям независимо от наличия доступа к жесткому диску. Изолированное хранилище разработано именно для решения этой задачи.
Изолированное хранилище доступно и без наличия у пользователя разрешений для соответствующих файлов и папок файловой систем. Основное преимущество изолированного хранилища в том, что с ним приложение будет работать независимо от контекста безопасности, в котором оно работает (т.е. является ли оно частично, ограниченно или полностью доверяемым).
ПРИМЕЧАНИЕ NET 2.0
В .NET 2.0 появились приложения нового типа, предназначенные для развертывания и установки с Wsb-страниц и называемые «приложения, развертываемые одним щелчком» (Click-Once applications) или «умные клиенты» (smart clients). Приложения нового типа предназначены для развертывания ПО в сети компании или предприятия.
К СВЕДЕНИЮ Приложения Click-Once
Подробнее о приложениях этого типа см. по адресу http://msdn2.microsoft.com/en-us/ library/142dbbz4. aspx.
Класс IsolatedStorageFile
Класс IsolatedStorageFile предоставляет базовую функциональность для создания файлов и папок в изолированном хранилище. В табл. 2-34 приводятся наиболее важные статические (общие) методы класса IsolatedStorageFile.
Табл. 2-34. Статические (Общие) методы класса IsolatedStorageFile
Имя	Описание	?
GetMachineStoreForApplication	Возвращает системное хранилище для	Click-Once-
приложения
GetMachineStoreForAssembly	Возвращает системное хранилище для	сборки
Занятие 4
Работа с изолированным хранилищем	*| Q7
Табл. .2-34. (окончание)
ИмЯ	Описание
GetMachineStoreForDomain	Возвращает системное хранилище для AppDomain вызванной сборки
GetStore	Возвращает хранилище, соответствующее заданному значению перечислимого IsolatedStorageScope
GetUserStoreForApplication	Возвращает хранилище уровня пользователя для вызванного Click- Once—приложения
GetUserStoreForAssembly	Возвращает пользовательское хранилище для вызванной
	сборки
GetUserStoreForDomain	Возвращает пользовательское хранилище для AppDomain вызванной сборки
В табл. 2-35. приводятся наиболее важные свойства класса IsolatedStorageFile.
Табл. 2-35. Свойства IsolatedStorageFile	
Имя	Описание
AppUcationldentity	Удостоверения Click-Once—приложения, использующего изолированное хранилище
Assemblyldentity	Удостоверение сборки, использующей изолированное хранилище
CurrentSize	Текущий размер данных, находящихся в изолированном хранении
Domainidentity	Удостоверения AppDomain, использующего изолированное хранилище
MaximumSize	Максимальный размер хранилища для изолированного хранения
Scope	Значение перечислимого IsolatedStorageScope, определяющее область действия изолированного хранилища
В табл. 2-36 приводятся наиболее важные методы класса IsolatedStorageFile.	
Табл. 2-36. Методы IsolatedStorageFile	
Имя	Описание
Close	Закрывает экземпляр хранилища
CreateDirectory	Создает каталог в хранилище
DeleteDirectory	Удаляет каталог из хранилища
DeleteFile	Удаляет файл из хранилища
GetDirectoryNames	Возвращает массив имен каталогов в хранилище, соответствующих заданной маске
GetFileNames	Возвращает массив имен файлов в хранилище, соответствующих заданной маске
Remove	Удаляет хранилище целиком
108 Программирование ввода-вывода
Глава 2
Как создать хранилище
Перед записью данных в изолированное хранилище следует определить его область действия. В большинстве приложений используется один из следующих вариантов:
	Хранилище уровня компьютера (Assembly/ Machine)
В этом случае хранилище содержит информацию, специфичную для сборки и локального компьютера. Такой вариант подходит для данных уровня приложения.
	Хранилище уровня пользователя (Assembly/User)
В этом случае хранилище содержит информацию, специфичную для сборки и текущего пользователя. Подходит для данных уровня пользователя.
Для создания хранилища уровня компьютера (системного хранилища) следует вызывать метод GetMachineStoreForAssembly класса IsolatedStorageFile'.
• VB
Dim machinestorage as IsolatedStorageFile =
IsolatedStorageFile.GetMachineStoreForAssembly()
// C#
IsolatedStorageFile machinestorage =
IsolatedStorageFile. GetMachineStoreForAssemblyO;
Созданное изолированное хранилище хранения доступно вызывающей сборке, независимо от того, является ли она главным исполняемым файлом проекта Windows Forms или динамически подключаемой библиотекой (DLL) в составе проекта.
Аналогично создается и хранилище уровня пользователя (пользовательское хранилище), только для этого вызывается метод GetUserStoreForAssembly.
’ VB
Dim userStorage as IsolatedStorageFile =
IsolatedStorageFile.GetUserStoreForAssembly()
11 C#
IsolatedStorageFile userStorage =
IsolatedStorageFile.GetUserStoreForAssembly();
В этом случае хранилище доступно пользователю, запустившему сборку. Если требуется указать пользователя хранилища, следует использовать олицетворение (см. гл. 12).
К СВЕДЕНИЮ Хранилище уровня приложения
Для Click-Once-приложений поддерживается изолированное хранилище уровня приложения (в дополнение к вышеописанным уровням). Хранилища уровня приложения работают только с Click-Once-приложениями, поскольку идентификационные данные запущенной сборки могут оказаться несовместимыми с локальными приложениями.
Занятие 4
Работа с изолированным хранилищем *| gg
К СВЕДЕНИЮ Изолированное хранилище
Дополнительные сведения об изолированном хранилище см. в следующих источниках:
	Введение в изолированное хранение, разработанное Microsoft (доступно по адресу http://msdn.microsoft.com/library/rus/default.asp7url-/library/RUS/cpguide/html/cpconintro-ductiontoisolatedstorage.asp).
	Книга «Программирование Windows Forms на C#/VB.NET» (Windows Forms Programming in C#/VB.NET) Криса Селлза (Chris Sells) (Addison-Wesley, 2004); см. гл. 11, в которой рассматриваются особенности различных типов пользовательских хранилищ, в том числе для перемещаемых.
Класс IsolatedStorageFileStream
Класс IsolatedStorageFileStream инкапсулирует поток, используемый для создания файлов в изолированном хранилище. Этот класс — потомок класса FileStream, поэтому работа с его экземплярами практически не отличается от работы с объектами FileStream. В табл. 2-37 приводятся наиболее важные свойства класса IsolatedStorageFileStream.
Табл. 2-37. Свойства IsolatedStorageFileStream
Имя	Описание
CanRead	Определяет, поддерживает ли поток чтение (наследуется от класса Stream)
CanSeek	Определяет, поддерживает ли поток поиск (наследуется от класса Stream)
CanTimeout	Определяет, можно ли для потока задать время ожидания (наследуется от класса Stream)
CanWrite	Определяет, поддерживает ли поток запись (наследуется от класса Stream)
Handle	Возвращает описатель файла, связанного с потоком (наследуется от класса FileStream)
Length	Возвращает длину потока (в байтах) (наследуется от класса Stream)
Имя	Возвращает имя файла (наследуется от класса FileStream)
Position	Возвращает или устанавливает виртуальный курсор, определяющий текущее положение в потоке Значение свойства Position не должно превышать длину потока (наследуется от класса Stream)
ReadTimeout	Возвращает или устанавливает время ожидания потока при чтении (наследуется от класса Stream)
WriteTimeout	Возвращает или устанавливает время ожидания потока при записи (наследуется от класса Stream)
В табл. 2-38 приводятся наиболее важные методы класса IsolatedStorageFileStream.	
Табл. 2-38.	Методы IsolatedStorageFileStream
Имя	Описание
Close	Закрывает поток и освобождает все занятые с ним ресурсы (наследуется от класса Stream)
Flush	Очищает все буферы потока и сбрасывает модифицированный данные на накопитель, с которым работает поток (наследуется от класса Stream)
Lock	Предотвращает изменение файла целиком и по частям другими процессами (наследуется от класса FileStream)
*110 Программирование ввода-вывода
Глава 2
Табл. 2-38.	(окончание)
Имя	Описание
Read	Последовательно считывает указанное число байтов, начиная с текущего положения, перемещая курсор в конец прочитанной поледовательности (наследуется от класса Stream)
ReadByte	Считывает один байт и перемещает курсор на один байт Эквивалент вызова Read для чтения единственного байта (наследуется от класса Stream)
Seek	Устанавливает положение курсора в потоке (наследуется от класса Stream)
SetLength	Задает длину потока. Усекает поток, если новая длина меньше старой, и расширяет поток в противном случае (наследуется от класса Stream)
Unlock	Разрешает другим процессам изменять связанный с потоком файл целиком либо частями (наследуется от класса FileStream)
Write	Записывает в поток данные как серию байтов, перемещая курсор в конец записанной последовательности (наследуется от класса Stream)
WriteByte	Записывает один байт в поток и перемещает курсор. Равносильно вызову Write для записи единственного байта (наследуется от класса Stream)
Чтение-запись в изолированное хранилище
Запись данных в изолированное хранилище производится так же, как в файловую систему, только с помощью класса IsolatedStorageFileStream. Чтобы получить новый объект IsolatedStorageFileStream, достаточно создать новый экземпляр класса, передав конструктору относительное имя файла и объект хранилища, в которое будет помещен файл, например:
' VB
Dim userStore as IsolatedStorageFile =
IsolatedStorageFile.GetUserStoreForAssembly()
Dim userStream As IsolatedStorageFileStream = New
IsolatedStorageFileStream("UserSettings.set”,
FileMode.Create, userStore)
П C#
IsolatedStorageFile userStore =
IsolatedStorageFile. GetUserStoreForAssemblyO;
IsolatedStorageFileStream userStream =
new IsolatedStorageFileStreamCUserSettings.set”,
FileMode.Create, userStore);
После создания хранилища можно создать файловый поток, указав имя файла, значение перечислимого FileMode (чтобы создать новый или открыть существующий файл) и
Занятие 4
Работа с. изолированным хранилищем *111
сам объект хранилища. С созданным экземпляром класса IsolatedStorageFileStream работают как с любым файловым потоком (см. занятие 2), ведь класс IsolatedStorageFileStream является производным от FileStream. Это иллюстрирует следующий фрагмент кода:
’ VB
Dim userWriter As StreamWriter = New StreamWriter(userStream) userWriter.WriteL±ne("User Prefs") userWriter.Close()
// C#
StreamWriter userWriter = new StreamWriter(userStream)» userWriter.WriteLine("User Prefs");
userWriter.Close();
В приведенном примере стандартный объект StreamWriter записывает данные в поток. С полученным объектом userStream работают, как с любым файлом файловой системы.
Чтобы подготовиться к обратной операции — чтению данных — достаточно создать объект потока, открыв (а не создав) файл, как показано ниже:
' VB
Dim userStream As IsolatedStorageFileStream = New _
IsolatedStorageFileStream("UserSettings.set",
FileMode.Open,
userStore)
// C#
IsolatedStorageFileStream userStream =
new IsolatedStorageFileStream("UserSettings.set",
FileMode.Open, userStore);
Чтобы открыть файл, а не создавать его, замените значение перечислимого FileMode на Open. В отличие от API файлов файловой системы, API файлов в изолированном хранилище не поддерживает непосредственную проверку существования файла, скажем, методом File. Exists. Вместо этого требуется запросить у хранилища список файлов, соответствующих заданной маске. Если файл найден, его можно открыть:
• VB
Dim filesO As String = userStore.GetFileNames("UserSettings.set")
If files.Length = 0 Then
Console.WriteLine("No data saved for this user") Else
End If
// C#
stringf] files = userStore.GetFileNames("UserSettings.set");
if (files.Length == 0)
{
Console WriteLine("No data saved for this user");
j j 2 Программирование ввода-вывода
Глава 2
} else {
// ...
}
Для получения списка файлов по заданной маске (например, *.sef) можно воспользоваться методом GetFileNames класса IsolatedStorageFile. Этот способ подходит для проверки существования файла перед попыткой чтения, удаления или замены.
Работа с папками изолированного хранилища
Данные можно хранить просто как набор файлов в изолированном хранилище либо распределить эти файлы по папкам. Для этого необходимо вызвать метод Create Directory класса IsolatedStorageFile: ' VB
use rSto re. C reateDi recto ry("SomeDi г”)
Dim userStream As IsolatedStorageFileStream = New _
IsolatedStorageFileStream("SomeDir\UserSettings.set",
FileMode.Create, userStore)
// C#
userStore.CreateDirectory("SomeDir");
IsolatedStorageFileStream userStream = new
IsolatedStorageFileStream(@"SomeDir\UserSettings.set",
FileMode.Create, userStore);
В этом примере папка создается до попытки создания в ней нового файла. Если папка не существует, генерируется исключение при разборе пути.
Проверка существования папки аналогична таковой для файла и выполняется методом, возвращающим массив путей, соответствующих маске. Перед попыткой создания папки имеет смысл выяснить, существует ли она, посредством метода Get Directory Names класса IsolatedStorageFile: ’ VB Dim directoriesO As String =
userStore.GetDi recto ryNames("SomeDi r")
If directories Length = 0 Then
use rSto re.C reateDi recto ry("SomeDi r")
End If
// C#
string[] directories =	\
userStore.GetDirectoryNames("SomeDir");
Занятие 4
Работа с изолированным хранилищем 113
if (directories.Length == 0)
{
userStore.СreateDi rectory("SomeDir");
}
Можно проверить, существует ли папка (получив массив имен папок, соответствующих заданному критерию), и создать папку, если она еще не существует.
Класс IsolatedStorageFilePermission
Класс IsolatedStorageFilePermission инкапсулирует разрешения на доступ к изолированному хранилищу, предоставляемые коду. В табл. 2-39 приводится важнейшие свойства класса IsolatedStorageFilePermission.
Табл. 2-39.	Свойства IsolatedStorageFilePermission
Имя	Описание
UsageAllowed	Возвращает или устанавливает разрешенные операции
UserQuota	Возвращает или устанавливает место, доступное пользователю в хранилище
Разрешения для изолированного хранилища
Для обращения к изолированному хранилищу сборки (или приложения) должны получить соответствующие разрешения. Чтобы гарантировать наличие разрешений у кода, их следует предварительно запросить. Эту задачу можно решить, пометив класс или метод атрибутом IsolatedStorageFilePermission'.
' VB
<IsolatedStorageFilePermission(SecurityAction.Demand)>
Class Program
End Class
// C#
[IsolatedStorageFilePermission(SecurityAction.Demand)] class Program {
// ...
}
Атрибут IsolatedStorageFilePermission обеспечивает успешное выполнение любого размещенного в теле класса кода, обращающегося к изолированному хранилищу. Если у кода нет разрешения на доступ к изолированному хранилищу, этот атрибут подскажет администратору, какой сборке требуется разрешение, чтобы выдать его при необходимости.
Это разрешение также поддерживает несколько атрибутов, уточняющих допустимые операции над изолированным хранилищем:
' VB
<IsolatedStorageFilePermission(SecurityAction.Demand,
1 *14	Программирование ввода-вывода
Глава 2
UserQuota:=1024.
UsageAllowed:=IsolatedSto rageContainment.AssemblylsolationByUser)>
Class Program
End Class
// C# [IsolatedStorageFilePermission(SecurityAction.Demand,
UserQuota=1024,
UsageAllowed=IsolatedSto rageContainment.AssemblylsolationByUser)] class Program {
// ...
}
Значения UserQuota и UsageAllowed указывают системе безопасности, что именно собирается делать код в изолированном хранилище.
Практикум. Чтение-запись в изолированное хранилище
Сейчас вы создадите в изолированном хранилище новый файл и прочитаете его из хранилища. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение 1. Создание файла в изолированном хранилище
В этом упражнении вы создадите в изолированном хранилище новый файл.
1.	Создайте новое консольное приложение с именем IsolatedStorageDemo.
2.	Добавьте директиву Import (в C# — using), чтобы импортировать в проект пространства имен System. 10 и System.IO.IsolatedStorage.
3.	В методе Main нового проекта создайте экземпляр IsolatedStorageFile с именем userStore, связанный с текущим пользователем и сборкой. Полученный код выглядит примерно так:
’ VB
IsolatedStorageFile userStore = _
IsolatedStorageFile.GetUserStoreForAssembly()
H C#
IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForAssembly()
4.	Создайте новый экземпляр объекта IsolatedStorageFileStream, передав в качестве параметра имя UserSettings.set и новый объект хранилища:
• VB
IsolatedStorageFileStream userStream = new _
IsolatedSto rageFileSt ream("Use rSettings.set”,
FileMode.Create, userStore)
// C#
IsolatedStorageFileStream userStream = new
Занятие 4
Работа с изолированным хранилищем 115
IsolatedStorageFileStream("UserSettings set",
FileMode.Create, userStore);
5.	Запишите какие-нибудь данные в поток при помощи StreamWriter и закройте объект записи. Полученный код выглядит примерно так:
' VB
StreamWriter userwriter = new StreamWriter(userStream)
userWriter.WriteLine("User Prefs")
userWriter. CloseO
// C#
StreamWriter userWriter = new StreamWriter(userStream);
userWriter.WriteLine("User Prefs");
userWriter. CloseO;
6.	Соберите проект и исправьте ошибки. Проверьте, появился ли новый файл в каталоге C:\Documents and Settings\<u.ver>\Local Settings\ Application Data\IsolatedStorage. Это временный каталог, поэтому в нем размещены папки с программно сгенерированными именами. Если открывать вложенные папки, новый файл окажется в папке AssemFiles.
Упражнение 2. Чтение файла из изолированного хранилища
Сейчас вы прочитаете файл, созданный в упражнении 1.
1.	Откройте проект, созданный в упражнении 1 (IsolatedStorageDemo).
2.	После кода, созданного ранее, добавьте код, который проверяет существование файла в хранилище и в случае отсутствия файла выводит сообщение на консоль. Полученный код выглядит примерно так:
' VB
Dim files() As String = userStore.GetFileNames("UserSettings. set")
If files.Length = 0 Then
Console.WriteLine("No data saved for this user")
End If
// C#
string[] files = userStore.GetFileNames("UserSettings.set");
if (files Length == 0)
{
// ...
3.	Если файл существует, создайте новый объект IsolatedStorageFileStream, открывающий файл, созданный в упражнении 1. Создайте объект StreamReader, чтобы прочитать весь текст из файла в локальную строковую переменную. Полученный код выглядит примерно так:
' VB
userStream = New _
IsolatedStorageFileStream("UserSettings.set", FileMode.Open, userStore) Dim userReader As StreamReader = New StreamReader(userStream)
116 Программирование ввода-вывода
Глава 2
Dim contents As String = userReader.ReadToEndO
// C#
userStream = new
IsolatedStorageFileStream("UserSettings.set",
FileMode Open, userStore);
StreamReader userReader = new StreamReader(userStream);
string contents = userReader.ReadToEndO;
4.	Выведите на консоль строку, созданную при помощи StreamReader.
5.	Соберите проект и исправьте ошибки. Убедитесь, что консольное приложение правильно выводит содержимое файла в окне консоли.
Резюме
	Для хранения данных сборок и пользователей в защищенных областях используется класс IsolatedStorageFile.
	Класс IsolatedStorageFileStream позволяет обмениваться данными с такими безопасными хранилищами.
	Поскольку класс IsolatedStorageFileStream — потомок FileStream, с созданными им файлами можно работать, как с любыми другими файлами файловой системы.
	Класс IsolatedStorageFilePermission гарантирует наличие у кода разрешений, необходимых для взаимодействия с изолированным хранилищем.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1. Какие методы используются для создания объектов IsolatedStorageFile! (Укажите все верные ответы.)
A. IsolatedStorageFile.GetStore.
В. IsolatedStorageFile.GetMachineStoreForAssembly.
С. IsolatedStorageFile.GetUserStoreForAssembly.
D. конструктор класса IsolatedStorageFile.
2. Объект IsolatedStorageFileStream работает с любым объектом FileStream.
А. Да.
В. Нет.
Закрепление материала главы
Для закрепления знаний и навыков, полученных при изучении этой главы, выполните следующее:
	изучите резюме главы;
	повторите основные термины, использованные в этой главе;
Лабораторная работа -| -| 7
	выполните задания лабораторной работы по темам этой главы;
	выполните предложенные упражнения;
	пройдите пробный экзамен.
Резюме главы
	Классы FileSystemlnfo {Fileinfo и Directory Info) предназначены для навигации по файловой системе и получения различной информации о файлах и каталогах, включая размер, временные метки, имена, атрибуты и т. п.
	Класс File предоставляет базовые возможности для создания и открытия файлов файловой системы.
	Класс FileStream используется для обмена данными между потоками и файловой системой.
	Классы StreamReader и Stream Writer служат для записи данных в потоки, такие как FileStreams, MemoryStreams и IsolatedStorageFileStream^, и извлечения данных из потоков.
	.NET Framework поддерживает два класса для сжатия данных: класс GZipStream и класс DeflateStream.
	Изолированное хранилище — это защищенная область для хранения данных, специфичных для сборки, пользователя или приложения. Для работы с изолированным хранилищем не требуется высоких привилегий, поэтому приложения смогут хранить в нем свои данные, даже не обладая разрешениями на доступ к системе пользователя.
Основные термины
	алгоритм сжатия (deflate);
	файловая система (file system);
	изолированное хранилище (isolated storage);
	алгоритм сжатия gzip (gzip).
Лабораторная работа
Сейчас вы примените на практике знания и навыки, полученные при изучении этой главы. Ответы на вопросы см. в разделе «Ответы» в конце книги.
Занятие 1. Сохранение пользовательских настроек
Вас приняли на работу в крупную страховую компанию. Руководитель поручает вам дополнить существующее приложение Windows Forms диалогом для ввода пользовательских настроек. При этом возможность сохранения настроек требуется не только администраторам, но и обычным пользователям. Диалог планируется добавить в приложение для учета потенциальных клиентов, с которым часто будут работать на общедоступных компьютерах. При этом важно не допустить просмотра конфиденциальной информации посторонними.
| j g	Программирование ввода-вывода
Глава 2
Вопросы
Ответьте на следующие вопросы руководства:
1. Кратко опишите, как вы планируете хранить информацию пользователей.
2. Что придется изменить в проекте приложения, если потребуется сжимать индивидуальные данные пользователей, чтобы уменьшить объема хранимой информации?
Занятие 2. Отслеживание активности старых серверов
Вы — разработчик в крупной ИТ-компании. В следующем месяце предполагается отключить множество устаревших серверов. Однако, на эти серверы неизвестные пользователи до сих пор записывают данные, но определить их пока не удается. Вам предложено написать небольшое приложение для установки на серверы, которое будет отправлять сообщения по электронной почте руководству при создании или сохранении файла на сервере.
Результаты опроса
 ИТ-менеджер
«Я уверена, что некоторые пользователи до сих пор работают с устаревшими серверами, не подозревая об этом. Если нам удастся в течение недели отследить активность старых серверов, то я смогу перевести этих пользователей на новые серверы, а старые можно будет отключить, сэкономив немалые средства на их поддержке».
 Руководитель департамента разработки
«Один из наших программистов несколько недель назад пытался написать приложение для мониторинга файлов, но не справился с этим. Его идея состояла в том, чтобы вести список файлов и проверять его каждые пять минут, эта задача отнимала слишком много времени».
Вопросы
Ответьте на следующие вопросы руководства:
1.	Какое приложение позволит решить проблему ИТ-отдела?
2.	Как вы собираетесь отслеживать активность файловых серверов?
3.	Как справиться с проблемой быстродействия мониторинга файлов на серверах?
Рекомендуемые упражнения
Чтобы лучше проработать темы экзамена, рассмотренные в этой главе, выполните следующие упражнения.
Создание приложения для поиска файлов
Выполнить как минимум упражнения 1 и 2. Дополнительный опыт работы со сжатыми потоками позволит получить упражнение 3.
	Упражнение 1 Напишите приложение для поиска заданного файла на диске.
	Упражнение 2 Добавьте код, использующий класс FileStream и позволяющий просматривать файл в текстовом окне.
	Упражнение 3 В заключение добавьте возможность сжатия найденного файла.
Пробный экзамен 119
Создание простого хранилища конфигурационных данных
Выполнить как минимум упражнения 1 и 2. Выполнив упражнение 3, вы поймете различия между данными пользователя и сборки, расположенными в изолированном хранилище.
	Упражнение 1 Создайте приложение Windows Forms, позволяющее пользователям сохранять данные в изолированном хранилище.
	Упражнение 2 Для проверки запустите созданное приложение под учетными записями различных пользователей.
	Упражнение 3 Измените приложение так, чтобы оно сохраняло некоторые данные уровня — это поможет различать данные, одинаковые для всех пользователей.
Пробный экзамен
На прилагаемом компакт-диске, содержится пробный экзамен, обеспечивающий широкие возможности по самоподготовке. Например, можно проверить усвоение темам этой главы либо всех тем экзамена 70-536. Можно сымитировать настоящий экзамен либо выбрать режим обучения, когда после каждого вопроса приводятся правильные ответы с пояснениями.
Пробный экзамен
Подробнее о пробном экзамене рассказано во введении.
ГЛАВА 3
Поиск, изменение и перекодирование текста
Занятие 1. Построение регулярных выражений
Занятие 2. Кодирование-декодирование строк
121
142
Обработка текста — одна из наиболее распространенных задач в программировании. Обычно пользователи вводят информацию ч текстовом формате, но ее мало просто получить — она нуждается в проверке, исправлении и форматировании. Часто разработчикам приходится обрабатывать текстовые файлы, сгенерированные унаследованными системами, чтобы извлечь из них важные данные. Эти системы часто используют нестандартные методы кодирования текста. Кроме того, иногда требуется выводить текстовые файлы в специальном формате для загрузки в унаследованные системы.
В этой главе описано применение регулярных выражений для проверки ввода, переформатирования текста и извлечения данных. Также рассматриваются различные типы кодировки текста, применяемые в файлах.
Темы экзамена:
	Улучшение функций обработки текста в .NET-приложениях (см. пространство имен System.Text); поиск, изменение и проверка текста в .NET-приложениях с использованием регулярных выражений (см. пространство имен System.RegularExpressions)'. □ класс StringBuilder, □ класс Regex’,
□	классы Match и MatchCollection',
□	класс Group и класс GroupCollection',
□	кодирование текста с использованием классов Encoding',
□	декодирование текста с использованием классов Decoding classes;
□	класс Capture и класс CaptureCollection.
Занятие 1
Построение регулярных выражений 121
Прежде всего
Чтобы выполнить упражнения этой главы, необходимо знать Microsoft Visual Basic или C# и уметь:
	создавать консольные приложения в Microsoft Visual Studio, используя Visual Basic или С#;
	добавлять в проект ссылки на системные библиотеки классов;
 читать и записывать файлы и потоки.
Занятие 1. Построение регулярных выражений
Разработчикам часто приходится обрабатывать текст, например удалять или заменять во введенном пользователем тексте специальные символы. Порой требуется писать поддержку обработки текста, сгенерированного унаследованным приложением, чтобы интегрировать новое ПО в существующую систему. Десятилетиями UNIX- и Perl-разра-ботчики использовали сложную, но эффективную методику обработки текста, основанную на регулярных выражениях.
Регулярное выражение — это набор символов; сравнивая его со строкой, можно определить, соответствует ли строка заданному формату. Регулярные выражения также можно использовать для выборочного извлечения или замены фрагмента текста. Сравнивая с текстом регулярные выражения, состоящие из чисел, букв в определенном регистре или шестнадцатеричных строк, можно принимать решения, влияющие на работу программы. Также можно извлекать важные фрагменты текста, например адреса пользователей или ссылки на изображения из HTML-страниц. Наконец, регулярные выражения позволяют изменять формат текста и удалять недопустимые символы.
Изучив материал этого занятия, вы сможете:
J использовать регулярные выражения для определения соответствия строки заданному шаблону;
J использовать регулярные выражения для извлечения данных из текстового файла;
J использовать регулярные выражения для изменения формата текстовых данных.
Продолжительность занятия — 45 минут.
Применение регулярных выражений для поиска по шаблону
Для проверки написанного вами кода с регулярными выражениями создайте консольное приложение с именем TestRegExp, принимающее две строки. Это приложение определяет, встречается ли первая строка (это и есть регулярное выражение) во второй. Следующее консольное приложение, использующее пространство имен Sy stem.Text. Regular Expressions, проверяет регулярное выражение с помощью статичес
122 Поиск, изменение и перекодирование текста	Глава 3
кого метода System. Text.RegularExpressions.Regex.IsMatch и отображает результаты на консоли:
' VB
Imports System.Text.RegularExpressions
Namespace TestRegExp
Class Classi
<STAThread>
Shared Sub Main(ByVal args() As String)
If Regex.IsMatch(args(1),args(O)) Then
Console.WriteLine("Input matches regular expression.") Else
Console.WriteLine("Input DOES NOT match regular expression.") End If
End Sub
End Class
End Namespace
// C#
using System.Text.RegularExpressions;
namespace TestRegExp
{
class Classi
{
[STAThread]
static void Main(string[] args)
if (Regex IsMatch(args[1], args[0]))
Console.WriteLineC Ввод соответствует регулярному выражению."); else
Console.WriteLine("Input matches regular expression.”);
}
}
}
Затем выполните приложение, чтобы проверить, соответствует ли регулярное выражение «Л\б{5}$» строке *12345» или *1234». Если вы не можете прочитать регулярное выражение, не смущайтесь — к концу занятия вы сможете в нем разобраться. Вывод программы должен выглядеть так:
C\>TestRegExp "\d{5}$ 1234
С:\>TestRegExp ~\d{5}$ 12345 Input matches regular expression.
Занятие 1
Построение регулярных выражений -| 23
Код программы демонстрирует использование метода RegexJsMatch для сравнения регулярного выражения со строкой, он возвращает true, если строка соответствует регулярному выражению. В этом примере «A\d{5}$» означает, что строка должна содержать ровно пять цифр. Как показано на рис. 3-1, символ возведения в степень («Л») означает начало строки, «\d» — то, что дальше идут цифры, «{5}» — то, что цифр должно быть пять подряд, а *$» — конец строки.
Начало ввода
Соответствует только цифрам
Соответствует лишь строке из пяти символов
Конец ввода
Л\с1{5}$
Рис. 3-1. Анализ регулярного выражения
Если удалить первый символ из регулярного выражения, значение шаблона радикально изменится. Регулярное выражение «\d{5}$» все еще будет соответствовать пяти цифрам, например «12345», но не только. Оно также будет соответствовать строке «abcdl2345» или «drop table customers — 12345». То есть, такое регулярное выражение будет соответствовать любой строке, оканчивающейся пятью цифрами.
ВАЖНО! Первый символ «“»
При проверке вводимых данных пропуск первого символа «Л» может привести к возникновению бреши в защите. Чтобы не рисковать из-за опечаток в коде, попросите эксперта проверить его.
При проверке вводимых данных всегда начинайте регулярные выражения с символа «Л» и заканчивайте их символом «$». Это гарантирует проверку строки, точно соответствующей заданному шаблону, а не просто содержащей его.
Регулярные выражения также могут использоваться для поиска во входных данных по сложным шаблонам. Следующее регулярное выражение соответствует адресам электронной почты:
~([\w-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\ [0-9]{1,3}\. )|(([\w-]+\.)+)) ([a-zA-Z]{2,4}|[0-9]{1.3})(\]?)$
Регулярные выражения — чрезвычайно эффективный способ проверки вводимых пользователем данных, однако их применения имеет следующие ограничения.
	Если вы не знаете формат регулярных выражений как свои пять пальцев, то будете часто допускать ошибки при их создании.
Если у вас за плечами годы программирования на Perl, проблем с использованием регулярных выражений в С#-коде не будет. Но если ваш опыт ограничивается скриптами на Visual Basic, загадочный формат регулярных выражений на первых порах покажется вам абсолютно нелогичным.
	Создание регулярных выражений, возможно, кажется запутанным, а вот их чтение стопроцентно таковым является.
Другие программисты, вычитывая ваш код, почти наверняка пропустят ошибки в регулярных выражениях. Чем сложнее регулярное выражение, тем больше шансы, что ошибки в нем останутся незамеченным.
124 Поиск, изменение и перекодирование текста	Глава 3
В следующих разделах описываются эти и другие аспекты использования регулярных выражения для поиска по образцу более подробно. При чтении рекомендуем экспериментировать с различными типами регулярных выражений, используя приложение TestRegExp.
К СВЕДЕНИЮ Регулярные выражения
Регулярным выражениям посвящены целые книги, поэтому на этом занятии вы получите лишь вводную информацию, но ее достаточно для сдачи экзамена. Если вы желаете побольше узнать о возможностях регулярных выражений, прочтите книгу «Regular Expression Language Elements» в руководстве по .NET Framework по адресу http:// msdn.microsoft.com/library/en-us/cpgenref/ html/cpconRegularExpressionsLanguageElements.asp.
Поиск в простом тексте
Простейшее использование регулярных выражений состоит в определении соответствия строки заданному шаблону. Например, регулярному выражению «аЬс» соответствуют строки «аЬс», «abcde» и «yzabc», поскольку каждая из них содержит это регулярное выражение, символы подстановки здесь не обязательны.
Поиск соответствия в заданном месте
Если искомый соответствующий фрагмент должен быть в начале строки, регулярное выражение должно начинаться с «Л». Например, регулярному выражению «ЛаЬс» соответствуют строки «аЬс» и «abcde», но не «yzabc». Чтобы найти соответствующий текст в конце строки, поставьте символ «$» в конце регулярного выражения. Например, регулярному выражению «аЬс$» соответствуют строки «аЬс» и «yzabc», но не «abcde». Чтобы задать точное соответствие, используйте оба символа, «Л» и «$». Например, «лаЬс$» соответствует только «аЬс», но не «abcde» или «yzabc».
При поиске слов используйте «\Ь», чтобы задать границы слова. Например, «саг\Ь» соответствует строкам *-саг» и «tocar», но не «carburetor». Напротив, ключ «\В» указывает, что искомая строка находится в середине слова. Например, «саг\В» соответствует слову «carburetor», но не «tocar».
ПРИМЕЧАНИЕ Озадачены?
Если регулярные выражения показались вам запутанными, это не удивительно — они и в самом деле такие. Наверное, в .NET Framework не найдется ни одной сущности, столь зависимой от специальных символов, расшифровать которые весьма непросто. Причина этого проста: регулярные выражения возникли в мире UNIX в то время, когда емкость оперативной памяти и дисков была чрезвычайно ограничена, и разработчики стремились по максимуму использовать каждый символ. Поэтому регулярные выражения нуждаются в подробных комментариях. Без них этого вряд ли кто сможет прочитать написанные вами регулярные выражения, поскольку эта задача еще сложнее, чем писать их самому.
В табл. 3-1 перечислены символы, которые используются в регулярных выражениях и задают поиск в определенном месте строки. Из них наиболее важны символы «Л» и «$».
Занятие 1
Построение регулярных выражений -| 25
Табл. 3-1. Символы для поиска соответствия в сроках
Символ	Описание
А	Поиск в начале строки. При анализе многострочного ввода л соответствует началу любой строки
$	Определяет поиск в конце строки (либо последовательности, ограниченной ключом \п или символом конца строки). При анализе многострочного ввода $ соответствует концу любой строки
\А	Поиск в начале строки (многострочный ввод игнорируется)
\z	Определяет поиск в конце строки, (либо последовательности, ограниченной ключом \п), многострочный ввод игнорируется
\z	Определяет, что соответствие должно заканчиваться в последнем символе строки, многострочный ввод игнорируется
\G	Поиск соответствия после окончания найденного ранее фрагмента. При использовании с Match.NextMatch это гарантирует непрерывность найденных фрагментов
\b	Поиск на границе между алфавитно-цифровыми (\w) или не алфавитно-цифровыми (\W) символами. Искомая строка должна располагаться на границах слова, которыми являются первые или последние символы слов, разделенные любыми не алфавитно-цифровыми символами
\в	Поиск во всех местах, кроме границ, заданных ключом \Ъ.
Обратите внимание, что регулярные выражения чувствительны к регистру даже в Visual Basic. Часто символы в разных регистрах, имеют противоположное значение.
Многие коды регулярных выражений начинаются с обратной косой черты. В С#-коде регулярное выражение должно начинаться со знака @, чтобы символ обратной косой черты воспринимался буквально, а не как управляющий. Желательно использовать @, даже если регулярное выражение не содержит обратной косой черты — так меньше риск внести трудноуловимые ошибки при дальнейшем редактировании регулярного выражения, например: // C#
Regex.IsMatch("pattern", @"\Apattern\Z")
Подготовка к экзамену
Не старайтесь вызубрить все коды регулярных выражений. Это, несомненно, впечатлило бы юниксоидов в вашем офисе, но для экзамена достаточно знать наиболее востребованные, которые описаны в этой книге
Поиск соответствия специальных символов
Регулярные выражения позволяют искать специальные символы. Например, \t соответствует символу табуляции, а \п — символу начала новой строки. Спецсимволов, показанных в табл. 3-2, не должно быть в пользовательском вводе или обычном текстовом файле, однако они встречаются в выходных данных унаследованных систем и UNIX-компьютеров.
126 Поиск, изменение и перекодирование текста
Глава 3
Табл. 3-2.	Escape-символы в регулярных выражениях
Символ	Описание
\а \ъ	Звуковой сигнал \u0007 Граница слова (между символами \w и \W), при заключении в [] — символ Backspace. В шаблоне для замены \Ь всегда означает Backspace
\t \г \V \f \n \е \040	Символ горизонтальной табуляции \u0009 Символ возврата каретки \u000D Символ вертикальной табуляции \u000B Символ перевода строки \u000C Символ новой строки \u000A Символ Escape \u001B ASCII-символу в восьмеричном формате (до трех разрядов); если в начале нет нуля, считаются обратной ссылкой; числа из одного разряда соответствуют номеру группы. Например, символ \040 представляет пробел
\х20 \сС \u0020	ASCII—символ в шестнадцатеричном формате (только два разряда) Управляющий ASCII—символ; например, \сС —эквивалент Control + С Символ в кодировке Unicode в шестнадцатеричном представлении (четыре разряда)
\	Если далее идет символ, отличный от управляющего (escape-символа), интерпретируется как этот символ. Например, \* представляет звездочку (а не повтор); \\ — одну обратную косую черту
Поиск с использованием подстановочных знаков
Регулярные выражения также применяют для поиска повторяющихся символов. Символ «*» заменяет последовательность предыдущих символов произвольной длины. Например, «Ю*п» соответствует «ton», «tooon» и «tn». Аналогично работает символ «+», но повторов в этом случае должно быть не меньше одного. Так, «to+п» соответствует «ton», «tooon», но не «tn».
Для поиска последовательности заданной длины используются символы «{л}», где п — длина последовательности. Например, «to{3}n» соответствует «tooon*, но не «ton» или «tn». Диапазон длин последовательности повторяющихся символов задают как *{пип,тах}». Например, «to{l,3}n» соответствует «ton» или «tooon» но не «tn» или «toooon». Чтобы задать только минимальную длину, второе число в фигурных скобках опускают. Например, «to{3,}n» соответствует строке с тремя или более идущими подряд символами «о».
Необязательные символы кодируют знаком «?». Например, «ю?п» соответствует «ton» или «tn», но не «tooon». Символ «.» соответствует любому одиночному символу. Например, «to.n» соответствует «totn» или «tojn», но 'не «ton» или «tn».
Альтернативные символы заключают в квадратные скобки. Например, «to[ro]n» соответствует «toon» или «tom» но не «ton» или «toron». Также можно задать диапазоны символов. Так, <to[o-r]n* соответствует «toon», «topn», «toqn» или «tom», но не «toan* или «toyn».
Занятие 1
Построение регулярных выражений j 27
В табл. 3-3 дана сводка символов, которыми в регулярных выражениях кодируют группы символов.
Табл. 3-3. Коды диапазонов символов и подстановочные знаки в регулярных выражениях
Символ	Описание
*	Повтор предшествующего символа или подстроки произвольной длины. Например, «zo*» соответствует «г» и «zoo». Эквивалент «{0,}»
+	Повтор предшествующего символа или подстроки не менее одного раза. Например, «zo+» соответствует «го» и «zoo», но не «г». Эквивалент «{1,}»
?	Предшествующий символ или подстрока либо пустая строка. Например, «do(es)?» соответствует «do» в словах «do» и «does». Эквивалент «{0,1}»
{П}	Здесь п — неотрицательное целое, соответствует повтору предшествующего символа длиной п. Например, «о{2}» не соответствует «о» в «ВоЬ», но соответствует «оо» в слове «food»
{П,}	Здесь п — неотрицательное целое, соответствует повтору предшествующего символа с минимальной длиной п. Например, «о{2,}» не соответствует ни «о» в слове «ВоЬ», ни серии «о» в «foooood». Последовательность «о{1,}» эквивалентна «о+». «о{0,}» — эквивалент «о*»
{n,m[	Здесь /лил- неотрицательные целые, причем п <= т. Соответствует повтору длиной не менее л и не более т. Например, «о{1,3}» соответствует первым трем «о» в «fooooood», «о{0,1}» — эквивалент «о?». Внимание: между запятой и числами не должно быть пробела!
?	Если этот символ находится после любого из следующих: ♦, +, ?, {п}, {п,}, {n,m}, будут найдены минимальные фрагменты, соответствующие заданному шаблону, тогда как по умолчанию ищется соответствующая строка максимальной длины. Например, в строке «оооо», «о+?» найдет «о», а «о+» — все символы «о» Соответствует любому одиночному символу, кроме «я|Г Чтобы обойти это ограничение, используйте шаблоны типа «[\s\S]»
х|у	Соответствует х и у. Например, «z|food» соответствует «г» и «food», «(z|f)ood» соответствует «zood» и «food»
[xyz]	Набор символов, соответствует любому из символов набора. Например, «[аЬс]» соответствует «а» в «plain»
[a-z]	Диапазон символов. Соответствует любому символу из заданного диапазона. Например, «[a-z]» соответствует любой строчной букве из диапазона «а»—«г»
Регулярные выражения также поддерживают специальные символы, представляющие диапазоны обычных символов. Код «[0-9]» соответствует любой цифре, аналогично работает «\d». Напротив, «\D» соответствует любому символу, отличному от цифры. Код «\s» соответствует пробельным символам, a «\S» — любым символам, кроме них. Эти коды описаны в табл. 3-4.
128 Поиск, изменение и перекодирование текста
Глава 3
Табл. 3-4.	Коды регулярных выражений
Символ	Описание
\d \D \s	Соответствует цифрам. Эквивалент «[0-9]» Соответствует любому символу, кроме цифры. Эквивалент «[Л0-9]» Соответствует пробелу, символу табуляции и разрыва страницы. Эквивалент «[ \f\n\r\t\v]».
\s	Соответствует любым символам, кроме пробельных. Эквивалент «[Л \f\n\r\t\v]»
\w	Соответствует любому алфавитно-цифровому символу, включая символ подчеркивания. Эквивалент «[A-Za-zO-9_J»
\W	Соответствует любым символам, кроме алфавитно-цифровых. Эквивалент «[AA-Za-zO-9_]»
Чтобы задать поиск группы символов, заключите их в круглые скобки. Например, «foo(loo){l,3}hoo» будет соответствовать «fooloohoo» и «fooloolooloohoo» но не «foohoo» или «foololohoo». Аналогично, «foo(loo|roo|)hoo» будет соответствовать как «fooloohoo», так и «fooroohoo». Группировать можно любые символы, включая подстановочные и спецсимволы.
Можно присвоить группе имя и в дальнейшем ссылаться на группу по имени. Для этого используется формат *(?<имя> шаблон)». Например, регулярное выражение «foo(?<mid>loo|roo)hoo» будет соответствовать «fooloohoo». Далее можно использовать группу, ссылаясь на нее по имени «mid», для поиска подстроки «1оо». То же регулярное выражение позволит найти строку «fooroohoo», так как в «mid» имеется код «гоо».
Поиск с использованием обратных ссылок
Обратные ссылки основаны на именованных группах и позволяют искать строки, соответствующие подстановочным знакам. Обратные ссылки удобны для поиска повторяющихся групп символов. Они работают как макрокоды, позволяющие повторно задавать поиск данной строки в одном и том же регулярном выражении.
Например, регулярное выражение (?<char>\w)\k<char> использует именованные группы и обратные ссылки для поиска соседствующих повторов. В строке «1’11 have а small coffee» такое выражение найдет «1’11», «small» и «coffee». Метасимвол \w соответствует любым алфавитно-цифровым символам. Группа (?<char>) с метасимволами кодирует часть регулярного выражения, соответствующую любой букве или цифре, под именем «char». Обратная ссылка \k<char> сравнивает текущий символ с предыдущим, закодированным как группа «char». Таким образом, это регулярное выражение ищет пары алфавитно-цифровых символов.
Для поиска повторяющихся слов можно изменить группу так, чтобы она соответствовала любой серии символов (а не одиночному символу), стоящей перед пробелам. Вместо метасимвола \w можно использовать выражение \w+, соответствующее любой группе символов, либо метасимвол \s, соответствующий пробелу перед группой символов. Получится регулярное выражение (?<char>\s\w+)\k<char>, которое ищет пары повторяющихся целых слов (« the the») и их фрагментов («the theory)».
Чтобы вторая часть искомой пары располагалась на границе слова, добавьте метасимвол \Ь после второй части выражения. Полученное регулярное выражение (?<char>\s\w+)\k<char>\b будет искать только пары целых повторяющих слов, которым предшествуем пробел.
Занятие 1
Построение регулярных выражений -| 2g
Обратные ссылки соответствуют последнему определению группы (при поиске слева направо — первому слева). Точнее, при поиске повторяющихся строк с повторяющимися фрагментами обратные ссылки ссылаются на последнее определение группы. Например, <?<1>а)(?<1>\1Ь)* соответствует aababb с шаблоном (a)(ab)(abb). Видно, что циклические коды не прибавляют определениям групп простоты.
Если группа не кодирует строку, обратные ссылки на эту группу остаются неопределенными, при этом регулярное выражение ничего не найдет. Например, выражение \1() не соответствует ничему, а ()\1 — пустой строке.
В табл. 3-5 перечислены дополнительные параметры обратных ссылок
Табл. 3-5. Параметры обратных ссылок
Конструкция	Определение
\number \к<имя>	Обратная ссылка. Например, (\w)\l находит повторы букв и цифр Именованная обратная ссылка. Например, (?<char>\w)\k<char> находит повторы букв и цифр, как и (?<43>\w)\43. Угловые скобки можно заменить одинарными кавычками, например так: \k'char'
Определение параметров регулярных выражений
Шаблон регулярного выражения можно настроить при помощи параметров. Параметры регулярных выражений задают двумя способами: в параметре options конструкции Regexfpattem, options), где options — поразрядное логическое ИЛИ либо комбинации значений перечислимого RegexOptions’, они также поддерживают синтаксис для встраивания определений в шаблоны с помощью групп или конструкции (?imnsx-imnsx).
Во встроенных определениях знак минус (—) перед параметром или набором параметров отключает их. Например, встроенная конструкция (?ix—ms) включает параметры IgnoreCase и IgnorePattemWhitespace и выключает параметры Multiline и Singleline. Все параметры регулярных выражений по умолчанию отключены.
В табл. 3-6 перечислены члены RegexOptions и эквиваленты встроенных параметров. Обратите внимание, что параметры RightToLeft и Compiled применимы только к выражению в целом и не допускаются во встроенных параметрах (они могут быть определены только в дополнительном параметре для конструктора Regex). Параметры None и ECMAScript не могут быть встроенными.
Табл. 3-6. Параметры регулярных выражений
Член RegexOption	Символ во встроенном определении	Описание
None	N/A	Параметры не установлены
IgnoreCase	i	Поиск без учета регистра
Multiline -с *	m	Включает поиск многострочных фрагментов. При этом л и $ кодируют начало и конец любой серии символов, а не только строки
ExplicitCapture	n	Поиск только именованных серий, явно заданных в виде (?<имя>...). При этом круглые скобки не входят в определение искомой подстроки, что позволяет избежать неуклюжей конструкции (?:...)
130 Поиск, изменение и перекодирование текста
Глава 3
Табл. 3-6. (окончание)
Член RegexOption	Символ во встроенном определении	Описание
Compiled	N/A	Компиляция регулярного выражения в сборку, содержащую код на промежуточном языке Microsoft (Microsoft intermediate language, MSIL), в результате регулярные выражения быстрее обрабатываются за счет снижения издержек во время выполнения
Singleline	s	Включает однострочный режим. Изменяет смысл знака (.), в результате, он соответствует любому символу, включая \п
IgnorePattem Whitespace	X	Пробелы, не помеченные в шаблоне спецсимволом, игнорируются, разрешаются комментарии (предваренные знаком #). Внимание: пробельные символы никогда не игнорируются В символьном классе
RightToLeft	N/A	Задает поиск в направлении справа налево (вместо слева направо, заданного по умолчанию). Поэтому исходная позиция для поиска должна быть установлена на конец, а не на начало строки. Этот параметр нельзя определять в середине шаблона — это ограничение защищает от бесконечных циклов. Однако сходная по действию конструкция (?<) может быть использована в середине шаблона. RightToLeft просто изменяет направление поиска, не обращая искомую подстроку. Направление просмотра (вперед или назад) не изменяется: прямой просмотр — слева направо, обратный — справа налево
ECMAScript	N/A	Активизирует поддержку £СЛ£45спр/-совместимых функций для выражений. Может использоваться только с флагами IgnoreCase и Multiline. Использование ECMAScript с другими флагами приводит к возникновению исключения
Culturelnvariant	N/A	Игнорирует языковые различия, связанные с культурой
Рассмотрим следующий файл:
аЬс def ghi
Если скопировать его в строковую переменную s, следующий метод вернет false, так как «def» не является и началом, и концом строки:
Regex.IsMatch(s, ""def$”)
Занятие 1
Построение регулярных выражений *13 "I
Зато этот метод вернет true, потому что с заданным параметром RegexOptions.Multiline символ «Л» соответствует началу любой серии символов (а не только строки), а символ «$» — концу серии:
Regex.IsMatch(s, "~def$", RegexOptions.Multiline)
Извлечение найденных данных
Регулярные выражения определяют не только поиск, но и извлечение заданных строк. Например, при обработке текстового файла со строкой «Company TQame: Contoso, Inc.» регулярное выражение позволяет извлечь только название компании.
Чтобы получить найденную строку:
1.	Создайте регулярное выражение, заключив искомый шаблон в круглые скобки.
2.	Создайте экземпляр класса System.Text.RegularExpressions.Match, используя метод Regex. Match.
3.	Извлеките найденные данные из элементов массива Match.Groups.
Например, следующий код извлекает название компании из вводимой строки и отображает его на консоли:
' VB
Dim input As String = "Company name: Contoso, Inc."
Dim m As Match = Regex Match(input, " Company name (.*$)")
Console.WriteLine(m.Groups(1))
// C#
string input = "Company name: Contoso, Inc.”;
Match m = Regex.Match(input, ©"Company name: (.*$)");
Console.WriteLine(m Groups[1]);
При запуске это консольное приложение (оно использует пространство имен System. Text.RegularExpressions) отображает «Contoso, Inc.». Этот пример демонстрирует, как регулярные выражения позволяют, используя минимум кода, извлекать текст. Заметьте, что в примере применяются безымянные группы, которым во время выполнения автоматически присваиваются значения, начиная с 1.
Следующий пример ищет вводимую строку и выводит все значения href="..." и их позиции в строке. Это делается путем создания компилированного объекта Regex, затем для поиска заданной подстроки используется объект Match. В этом примере метасимвол \s соответствует любому пробельному символу, a \S — любому, кроме пробельного.
' VB
Sub DumpHrefs(inputString As String)
Dim r As Regex
Dim m As Match
r = New Regex("href\s*=\s*(?:""(?<1>[''""]*)""|(?<1>\S+))", RegexOptions.IgnoreCase Or RegexOptions.Compiled)
m = r.Match(inputString)
While m.Success
Console.WriteLine("Found href " & m.Groups(1).Value
& " at " & m.Groups(1).Index.ToStringO)
132 Поиск, изменение и перекодирование текста
Глава 3
m = m.NextMatchO
End While
End Sub
// C#
void DumpHrefs(String inputstring)
Regex r;
Match m;
r = new Regex("href\\s*=\\s*(?:\”(?<1>["\"]*)\"l(?<1>\\S+))"> RegexOptions.Igno reCase|RegexOptions.Compiled);
for (m = r.Match(inputString); m.Success; m = m.NextMatchO)
{
Console.WriteLine("Found href ” + m.Groups[1] + " at "
+ m.Groups[1].Index);
}
}
Вызовом метода Match.Result можно переформатировать извлеченную подстроку. Следующий пример кода Match.Result извлекает из URL имя протокола и номер порта. Например, «http://www.contoso.com:8080/letters/readme.html» возвратит «http:8080».
' VB
Function Extension^rl As String) As String
Dim r As New Regex(""(?<proto>\w+)://[''/]+? (?<port>: \d+)?/", RegexOptions.Compiled)
Retu rn r. Match(и rl). Result(”${p rotо}${po rt}")
End Function
// C#
String Extension(String url)
{
Regex r = new Regex(@"'*(?<proto>\w+)://["/]+?(?<port>:\d+)?/”, RegexOptions.Compiled);
return r.Match(url).Result("${proto}${port}");
Замена подстрок с помощью регулярных выражений
Регулярные выражения позволяют заменять намного более сложные выражения, чем метод String.Replace. В следующем примере статический метод Regex.Replace используется для замены даты в формате месяц/день/год на дату в формате день-месяц-год\
’ VB
Function MDYToDMY(input As String) As String
Return Regex.Replace(input, "\b(?<month>\d{1.2})/(?<day>\d{1,2})/(?<year>\d{2,4})\b", _ "${day}-${month}-${year}")
End Function
Занятие 1
Построение регулярных выражений -| 33
// C#
String MDYToDMY(String input) {
return Regex Replace(input,
"\\b(9<month>\\d{1,2})/(?<day>\\d{1,2})/(9<year>\\d{2,4})\\b", "${day}-${month}-${year}");
}
Это также пример использования в шаблонах именованных обратных ссылок как альтернативы Regex.Replace. Здесь вместо выражения ${day} подставляется строка, закодированная группой (?<day>...).
Следующий пример кода использует статический метод Regex.Replace для удаления недопустимых символов из строки. Такой метод Cleaninput позволяет отбрасывать потенциально опасные символы, введенные пользователем в текстовые поля на форме. Cleaninput возвращает строку, выбросив из нее символы, кроме алфавитно-цифровых, @, дефиса и точки.
' VB
Function Cleanlnput(strln As String) As String
Замена недопустимых символов пустыми строками.
Return Regex.Replace(strin, ”[''\w\.@-]"1 "") End Function
// C#
String Cleanlnput(string strin)
{
// Замена недопустимых символов пустыми строками.
return Regex.Replace(strln,	"");
>
Единственные специальные конструкции, которые распознаются в шаблоне, — это управляющие символы и символы замены. Все конструкции, описанные ниже, разрешаются только в регулярных выражениях и не распознаются в искомых шаблонах. Например, шаблон a*${txt}b вставляет строку «а*» плюс подстроку, закодированную группой txt, если та определена, плюс строку «Ь». Символ ♦ не распознается как метасимвол в шаблоне замены, а также в шаблонах регулярных выражений, где он кодирует конец строки.
В табл. 3-7 показано, как кодируются шаблоны для замены.
Табл. 3-7.	Управляющие символы, используемые при поиске с заменой
Символ	Описание
$number	Последнюю подстроку, соответствующую группе number (десятичное число)
${name} $$ $& $' S’ $+ $_	Последнюю подстроку, соответствующую группе (?<имя>) Литерал «$» Копию заданной шаблоном строки Часть строки, стоящую до найденной подстроки Часть строки, стоящую после найденной подстроки Строку, совпадающую с последней группой Всю вводимую строку
*| 34 Поиск, изменение и перекодирование текста
Глава 3
Использование регулярных выражений для ограничения ввода строк
При построении системы защиты приложения использование регулярных выражений является наиболее эффективным способом проверки пользовательского ввода. Если приложение принимает от пользователя пятизначное число, применение регулярных выражений гарантирует, что будет принята строка длиной ровно пять символов, каждый из которых будет цифрой от 0 до 9. Аналогично, вводимые имена и фамилии также можно проверять с помощью регулярных выражений и генерировать исключение, если ввод содержит числа, разделители и другие символы, отличные от букв.
К сожалению, далеко не каждую строку можно описать так же просто, как числа и электронные почтовые адреса. Проверить правильность имен и адресов улиц чрезвычайно сложно, так как они метут содержать символы иноязычных алфавитов. Например, O’Dell, Varkey Chudukatil, Skjonaa, Craciun и McAskill-White —обычные в своих странах фамилии. Программно отфильтровать из таких строк злонамеренный код, такой как «1’ DROP TABLE PRODUCTS --» (это атака инъекцией SQL-кода) весьма трудно.
Один из распространенных подходов — дать пользователям знать, как заменять экзотические символы в именах. Например, апострофы и дефисы в именах можно опустить. Символы с диакритическими знаками, также кириллицу можно заменить сходными символами латинского алфавита либо писать транслитом. Это позволят строже проверять ввод, но пользователям придется жертвовать точностью записи своих имен, что нравится далеко не всем.
Альтернатива — максимально полная фильтрация строк при вводе с последующей окончательной очисткой от потенциально вредоносного кода. Обычно при проверке вводимых данных рекомендуется рассчитывать на худшее и принимать только проверенные символы. Однако проверка имен при вводе должна быть более «оптимистичной» и генерировать ошибки только при обнаружении ряда явно запрещенных символов. Например, можно отклонить имя пользователя, если оно содержит !, @, #, $, %, Л, *> 0, <» >• Появление этих символов в имени вряд ли возможно, а вот использование их для атаки — весьма вероятно. Microsoft Visual Studio .NET поддерживает следующее регулярное выражение для проверки имен: «[a-zA-Z”- T,Br\s]{ 1,40}».
Пример из практики
Тони Нортроп (Топу Northrup)
Часто я не признаю ошибки. Много лет я просто игнорировал регулярные выражения, так как они — часть «мира UNIX», я же обитал в «мире Windows».,
Недавно я перечитал код, написанный мной в ту пору. В нем мне пришлось добавить несколько десятков строк для проверки допустимости вводимого текста, которые можно было заменить одним-единственным регулярным выражением. Вообще, я считаю, что чем подробнее код, тем он понятнее, но в этом случае проверка оказалась настолько сложной, что отловить присутствовавшие в ней баги стало просто нереально.
Я переписал все, используя регулярные выражения, в результате удалось не только исправить ошибки, но и упростить код. Так что не рекомендую вам игнорировать регулярные выражения только потому, что они кажутся слишком сложными. Право, стоит потратить несколько часов на то, чтобы поближе познакомиться с ними.
Занятие 1
Построение регулярных выражений | 35
Практикум. Создание объекта Regex дна проверки текста
Вы должны выбрать в массиве строк допустимые телефонные номера и почтовые индексы, затем переформатировать телефонные номера. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение 1. Выборка телефонных номеров и почтовых индексов
В этом упражнении вы напишете код, отличающий телефонные номера и почтовые индексы от недопустимых данных.
1. Скопируйте Visual Basic- или С#-версию папки Chapter03\Lessonl-Exercisel с прилагающегося компакт-диска на жесткий диск и откройте файл решения.
2. Переделайте метод IsPhone так, чтобы он возвращал true при вводе чисел в любом из следующих форматов (используйте не более одной строки кода): □ (555)555-1212 □ (555) 555-1212 □ 555-555-1212
□ 5555551212
Пример метода IsPhone может быть таким:
* VB
Function IsPhone(ByVal s As String) As Boolean
Return Regex.IsMatch(s, "~\(?\d{3}\)?[\s\-]?\d{3}\-?\d{4}$")
End Function
// C#
static bool IsPhone(string s)
{
return Regex.IsMatch(s, O?\d{3}\)?[\s\-]?\d{3}\-?\d{4}$");
}
Каждый компонент этого регулярного выражения соответствует обязательной или необязательной части телефонного номера:
□ А
— началу строки;
□ \(?
— открывающейся круглой скобке (необязательно). Круглой скобке предшествует обратная косая черта, потому что круглая скобка — спецсимвол в регулярных выражениях; вопросительный знак делает круглую скобку необязательной;
□ \d{3)
— трем цифрам;
□ \)?
—	закрывающейся круглой скобке (необязательно). Скобка помечена обратной косой чертой, так как круглая скобка является спецсимволом в регулярных выражениях; знак вопроса делает круглую скобку необязательной;
□ [\s\-J?
—	пробелу («\s») или дефису («\~*)> отделяющему код города. Знак вопроса дела ет пробел или дефис необязательным;
136 Поиск, изменение и перекодирование текста
Глава 3
□ \d{3}
—	трем цифрам;
□ \-?
—	дефису (необязательно);
□ \d{4}$
— требует завершения строки четырьмя цифрами.
3.	Строку кода завершает вызов IsZip, он возвращает true, если ввод имеет один из следующих форматов: □ 01111 □ 01111-1111
Код IsZip выглядит примерно так: ' VB
Function IsZip(ByVal s As String) As Boolean Return Regex.IsMatch(s, ""\d{5}(\-\d{4})?$") End Function
// C#
static bool IsZip(string s) {
return Regex.IsMatch(s, @""\d{5}(\-\d{4})?$");
}
Компоненты этого регулярного выражения соответствуют частям почтового индекса: □ Л
—	началу строки;
□ \d{5}
—	пяти цифрам;
□ (\-\d{4})?
дефису, следующему после цифр (необязательно, так как это выражение находится в круглых скобках и сопровождается знаком вопроса);
□ $
—	концу строки;
4.	Соберите и запустите проект. Вывод должен быть таким:
(555)555-1212 is a phone number
(555) 555-1212 is a phone number
555-555-1212 is a zip code
5555551212 is a zip code
01111 is a zip code
01111-1111 is a zip code
47 is unknown 111-11-1111 is unknown
Если вывод отличается, исправьте регулярное выражение.
Занятие 1
Построение регулярных выражений 37
Упражнение 2. Переформатирование строк
В этом упражнении необходимо привести телефонный номер к стандартному формату: (###) ###_####.
1.	Откройте проект, созданный в Упражнении 1.
2.	Добавьте метод Reformat Phone, возвращающий строку и принимает единственный строковый параметр. Используя регулярные выражения, он должен принимать телефонные номера в формате, используемом в упражнении 1, и приводить их к формату (###) ###-####. Метод IsZip выглядит примерно так:
' VB
Function ReformatPhone(ByVal s As String) As String
Dim m As Match = Regex.Match(s, "~\(?(\d{3})\)?[\s\-]?(\d{3>)\-?(\d{4})$”)
Return String.Format("({0}) {1}-{2}’, m Groups(1), m.Groups(2), m Groups(3)) End Function
// C#
static string ReformatPhone(string s)
{
Match m = Regex.Match(s, @""\(?(\d{3})\)?[\s\-]?(\d{3))\-?(\d{4})$");
return String.Format('({0}) {1}-{2}", m.Groups[1], m.Groups[2], m Groups[3]); }
Обратите внимание, что это регулярное выражение почти такое же, как в методе Is Phone, и отличается только выражениями \d{w}, заключенными в круглые скобки. Оно разбивает строку на группы, которые легко отформатировать с помощью String.Format.
3.	Измените метод Main так, чтобы в цикле foreach записывался результат ReformatPhone(s), а не просто s, например:
' VB
For Each s As String In input
If IsPhone(s) Then
Console.WriteLine(ReformatPhone(s) + " is a phone number")
Else
If IsZip(s) Then
Console.WriteLine(s + " is a zip code”)
Else
Console.WriteLine(s + " is unknown”)
End If
End If
Next
// C#
foreach (string s in input)
{
if (IsPhone(s)) Console WriteLine(ReformatPhone(s) + ” is a phone number"); else if (IsZip(s)) Console.WriteLine(s + " is a zip code");
else Console.WriteLine(s + " is unknown”);
}
138 Поиск, изменение и перекодирование текста
Глава 3
4.	Соберите и запустите проект. Вывод должен быть таким:
(555) 555-1212 is a phone number
(555) 555-1212 is a phone number
(555) 555-1212 is a phone number
(555) 555-1212 is a phone number
01111 is a zip code
01111-1111 is a zip code
47 is unknown
111-11-1111 is unknown
Обратите внимание, что телефонные номера переформатируются, независимо от их исходного формата. Если полученный вывод отличается, исправьте регулярное выражение
Резюме
	Регулярные выражения позволяют искать строки практически любого формата. Регулярные выражения поддерживают множество специальных символов и операторов. Наиболее часто используются «Л» (соответствует началу строки), «$» (соответствует концу строки), «?» (кодирует символ как необязательный), «.» (соответствует любому символу) и «♦» (соответствует серии повторяющихся символов).
	Для поиска данных с использованием регулярных выражений требуется создать шаблон (при необходимости определяя группы), и вызвать Regex.Match для создания объекта Match далее следует работать с элементами массива Match.Groups.
	Чтобы с помощью регулярных выражений переформатировать текстовые данные, вызывают статический метод Regex.Replace.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1. Вы разрабатываете приложение, изменяющее абсолютные гиперссылки в HTML-файлах. HTML файл загружается в переменную типа String с именем s. Какой код лучше подходит для замены «http://» на «https://»?
А.
’ VB
s = Regex.Replace(s, "http://", "https://")
П C#
s = Regex.Replace(s, "http://", "https://");
B.
• VB
s = Regex.Replace(s, "https://", "http://”)
Занятие 1
Построение регулярных выражений 3g
// C#
s = Regex.Replace(s, "https://", "http://");
C.
• VB
s = Regex.Replace(s, "http://", "https://", RegexOptions.IgnoreCase)
// C#
s = Regex.Replace(s, "http://", "https://", RegexOptions.IgnoreCase); D.
* VB
s = Regex.Replace(s, "https://", "http://", RegexOptions.IgnoreCase)
// C#
s = Regex.Replace(s, "https://", "http://", RegexOptions.IgnoreCase);
2.	Вы пишете приложение для обработки текстовых данных. Каждый файл содержит информацию об одном клиенте: First Name: Tom Last Name: Perham Address: 1 Pine St. City: Springfield State: MA Zip: 01332
Данные считываются в переменную s с типом String. Какой код корректно сохраняет данные формы в переменные fullName, address, city, state и zip?
A.
’ VB
Dim p As String = "First Name: (?<firstName>.*$)\n" + "Last Name: (?<lastName>.*$)\n" + _ "Address: (?<address>.*$)\n" + _ "City: (?<city>.*$)\n" + "State: (?<state>.*$)\n" + _ "Zip: (?<zip>.*$)"
Dim m As Match = Regex.Match(s, p, RegexOptions.Multiline)
Dim fullName As String = m.Groups("firstName").ToString + " " + m. Groups("lastName").ToString
Dim address As String = m.Groups("address").ToString
Dim city As String = m.Groups("city").ToString
Dim state As String ® m.Groups("state”).ToString
Dim zip As String = m.Groups("zip").ToString
// C#
string p = ©"First Name: (?<firstName>.*$)\n" +
©"Last Name: (?<lastName>.*$)\n" + ©"Address: (?<address>.*$)\n" + ©"City: (?<city>.*$)\n" +
140 Поиск, изменение и перекодирование текста
Глава 3
©"State: (?<state>.*$)\п" +
@"Zip: (?<zip>.*$)";
Match m = Regex.Match(s, p, RegexOptions.Multiline);
string fullName = m.Groups["firstName"] + ” ” + m.Groups["lastName"];
string address = m.Groups["address"].ToStringO;
string city = m Groups["city"].ToStringO;
string state = m.Groups["state"].ToStringO;
string zip = m.Groupsfzip"].ToStringO;
B.
Dim p As String = "First Name: (?<firstName>.*$)\n" + _
"Last Name: (?<lastName>.*$)\n" + _
"Address (?<address>.*$)\n" + _
"City. (?<city>.*$)\n" +
"State: (?<state>.*$)\n" +
"Zip: (?<zip>.*$)"
Dim m As Match = Regex.Match(s, p)
Dim fullName As String = m.Groups("firstName").ToString + " " +
m.Groups("lastName").ToString
Dim address As String = m.Groups("address").ToString
Dim city As String = m.Groups("city").ToString
Dim state As String = m.Groups("state").ToString
Dim zip As String = m.Groupsfzip") ToString
// C#
string p = ©"First Name: (?<firstName>.*$)\n" +
©"Last Name: (?<lastName>.*$)\n" +
©"Address: (?<address>.*$)\n" +
@"City: (?<city>.*$)\n" +
©"State: (?<state>.*$)\n" +
@"Zip (?<zip>.*$)";
Match m = Regex Match(s, p);
string fullName = m.Groups["firstName"] + " " + m.Groupsf"lastName"];
string address = m.Groups["address"].ToStringO;
string city = m.Groups["city"].ToStringO;
string state = m.Groups["state"].ToStringO;
string zip = m.Groupsfzip"].ToStringO;
C.
Dim p As String = "First Name: (9<firstName>.*$)\n" +
"Last Name: (?<lastName>.*$)\n" +
"Address: (9<address>.*$)\n" + _
"City: (?<city>.*$)\n" + _
"State: (?<state>.*$)\n" +
"Zip: (?<zip>.*$)"
Dim m As Match = Regex.Match(s, p, RegexOptions.Multiline)
Dim fullName As String = m.Groups("<firstName>").ToString + " " +
m.Groups("<lastName>").ToString
Занятие 1
Построение регулярных выражений 141
D?m address As String = m.Groups("<address>").ToString
Dim city As String = m.Groups("<city>”).ToString
Dim state As String = m.Groups("<state>").ToString
Dim zip As String = m.Groups("<zip>").ToString
// C#
string p = ©"First Name: (?<firstName>.*$)\n" +
©"Last Name: (?<lastName>.*$)\n" +
©"Address: (?<address>.*$)\n" +
@"City: (?<city>.*$)\n" +
©"State: (?<state>.*$)\n" +
@"Zip: (?<zip>.*$)";
Match m = Regex.Match(s, p, RegexOptions.Multiline);
string fullName = m.Groups["<firstName>"] + " " + m.Groups["<lastName>”];
string address = m.Groups["<address>"].ToStringO;
string city = m.Groups["<city>"].ToStringO;
string state = m.Groups["<state>"].ToStringO;
string zip = m.Groups["<zip>"].ToStringO;
D.
Dim p As String = "First Name: (?<firstName>.*$)\n” +
"Last Name: (?<lastName>.*$)\n" + _
"Address: (?<address>.*$)\n" + _
"City: (?<city>.*$)\n" +
"State: (?<state>.*$)\n" + _
"Zip: (?<zip>.*$)"
Dim m As Match = Regex.Match(s, p)
Dim fullName As String = m.Groups("<firstName>").ToString + " " +
m.Groups("clast Name>").ToString
Dim address As String = m.Groups("<address>").ToString
Dim city As String = m.Groups("<city>").ToString
Dim state As String = m.Groups("<state>").ToString
Dim zip As String = m.Groups("<zip>").ToString
// C#
string p = ©"First Name: (?<firstName>.*$)\n" +
©"Last Name: (?<lastName>.*$)\n” +
©"Address: (?<address>.*$)\n" +
@"City: (?<city>.*$)\n" +
©"State: (?<state>.*$)\n" +
@"Zip: (?<zip>.*$)";
Match m = Regex.Match(s, p);
string fullName = m.Groups["<firstName>"] + " " + m.Groups["<lastName>"];
sfring address = m.Groups["<address>"].ToStringO;
string city = m.Groups["<city>"].ToStringO;
string state = m.Groups["<state>"].ToStringO;
string zip = m.Groups["<zip>"].ToStringO;
142 Поиск, изменение и перекодирование текста
Глава 3
3.	Какое из следующих регулярных выражений соответствует строкам «zoot» и «zot»?
A.	z(oo)+t;
В.	zo*t$;
С.	$zo*t;
D.	A(zo)+t.
4.	Какая из следующих строк соответствует регулярному выражению ,,Aa(mo)+t.*z$"? (Укажите все верные ответы.)
A.	amotz;
В.	amomtrewz;
С.	amotmoz;
D.	atrewz;
Е.	amomomottothez. >
Занятие 2. Кодирование-декодирование строк
Все данные в текстовых строках и файлах кодируются с использованием одного из стандартов кодирования. Почти всегда .NET Framework обрабатывает данные в разных кодировках автоматически. Однако время от времени кодированием-декодированием приходится управлять вручную, например при’
	взаимодействии с унаследованными или UNIX-системами;
	чтении-записи файлов на других языках;
	создании HTML-страниц;
	генерации сообщений электронной почты.
Ниже описаны наиболее востребованные методы кодирования и освещено их применение в приложениях .NET Framework.
Изучив материал этого занятия вы сможете:
J рассказать о цели кодирования текста и перечислить основные кодировки;.
J использовать класс Encoding для определения кодировки и преобразования текста в разных кодировках;
J программно определять кодовые страницы, поддерживаемые .NET Framework;
J создавать файлы в заданной кодировке;
J читать файлы в необычных кодировках.
Продолжительность занятия — 30 минут.
Введение в кодировки
Американский стандартный код для обмена информацией (American Standard Code for Information Interchange, ASCII) не был первым стандартом кодирования текстовой информации, но лег в основу других стандартных кодировок. В ASCII символы кодируются 7-битными последовательностями (числами от 0 до 127). Этого достаточно для представления заглавных и строчных букв английского алфавита, цифр, знаков препинания и некоторых спецсимволов. Например, 0x21 кодирует «!», 0x31 — ♦!», 0x43 — «С>, 0x63 — «с», a 0x7D — «}».
Занятие 2
Кодирование-декодирование строк -|43
Возможностей ASCII было достаточно для большинства текстов на английском языке, и в ASCII не было кодов для символов из алфавитов других языков. Чтобы компьютерами смогли пользоваться жители разных стран, производители задействовали дополнительный бит (значения 128—255), в результате каждый символ стал кодироваться восемью битами или одним байтом. Постепенно в различных странах были приняты собственные кодировки, использующие для представления специфичных для их языка символов коды больше 127. К сожалению, кодов не хватало для представления символов всех алфавитов, поэтом неизбежное дублирование создавало проблемы при перекодировании документов, созданных на разных языках.
Для решения этой проблемы Американский национальный институт стандартов (American National Standards Institute, ANSI) определил стандартные кодовые страницы, состоящие из универсальной части — ASCII-кодов от 0 до 127, и части, специфичной для национальных языков, включавшей коды с 128 по 255. Кодовая страница — это упорядоченный список кодов для символов того или иного языка. Кодовые страницы обычно применяются для поддержки определенных языков или языковых групп на устройствах ввода-вывода. Кодовая страница Windows содержит 256 кодов символов, начиная с нуля.
Те, кому приходилось когда-нибудь получать сообщение или видеть Web-страницы с нечитаемыми символами вместо текста, проблема с кодировкой знакома. Поскольку Web-страницы и электронные письма создают на различных языках, в них непременно должна быть информация о кодировке. Например, в электронном письме должен быть такой заголовок:
Content-Type: text/plain; charset=IS0-8859-1
Content-Type: text/plain; charset="Windows-1251”
Строка «ISO-8859-1» соответствует западноевропейской кодовой странице 28591, «Western European (ISO)». Идентификатор «ISO-8859-7» соответствует греческой кодовой странице 28597, «Greek (ISO)». В Web-страницы на языке HTML обычно включают такие мета-теги:
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8”>
Кодировки ASCII и ISO 8859 постепенно вытесняются кодировкой Unicode. Unicode можно сравнить с большой кодовой страницей, поддерживающей десятки тысяч символов, охватывающей большинство языков и национальных алфавитов, включая латинский, греческий, кириллицу, иврит, арабский, китайский, японский и множество других).
Unicode сама по себе не определяет способ кодирования, для этого существует несколько стандартов кодировки Unicode. В .NET Framework для представления символов используется кодировка Unicode UTF-16 (16-битный Unicode Transformation Format). В некоторых случаев для внутреннего представления данных .NET Framework использует UTF-8. Пространство имен System. Text содержит классы, позволяющие кодировать и декодировать символы. System. Text поддерживает следующие кодировки:  Unicode UTF-32
— представляет символы Unicode как 32-разрядные целые числа. Класс UTF32Encoding преобразует символы в кодировку UTF-32 и обратно.
 Unicode UTF-16
— представляет символы Unicode как 16-разрядные целые числа. Класс UnicodeEncoding преобразует символы в кодировку UTF-16 и обратно.
144 Поиск, изменение и перекодирование текста	Глава 3
	Unicode UTF-8
— Unicode UTF-8 использует 8-, 16-, 24- и 48-битное представление символов. Значения от 0 до 127 используются при 8-битном кодировании и соответствуют ASCII-кодам, обеспечивая преемственную совместимость. Коды 128—2047 используются в 16-битном представлении и обеспечивают поддержку латинского, греческого, кириллического, еврейского и арабского алфавитов. Значения 2048—65535 используются в 24-битном кодировании многочисленных иероглифов китайского, японского, корейского и других языков с идеографической письменностью. Класс UTF8Encoding преобразует символы в кодировку UTF-8 и обратно.
	ASCII
— кодирует латинский алфавит с помощью 7-битных ASCII-кодов. Поддерживает только коды U+0000—U+007F, и потому обычно не подходит для интернациональных приложений. Класс ASCIIEncoding преобразует символы в кодировку ASCII и обратно.
 ANSI/ISO
Класс System.Text.Encoding поддерживает широкий спектр кодировок ANSI/ISO.
К СВЕДЕНИЮ Unicode
Подробнее о Unicode см. в тексте «The Unicode Standard» по адресу http://www.unicode.org.
Использование класса Encoding
Метод System. Text.Encoding.GetEncoding возвращает объект, представляющий текст в заданной кодировке. Метод Encoding.GetBytes преобразует строку Unicode в серию байтов с заданной кодировкой. Следующий пример использует метод Encoding.GetEncoding для перекодирования в корейскую кодовую страницу. В коде вызывается метод Encoding.GetBytes, преобразующий строку Unicode в байты в корейской кодировке. Затем строки отображаются в байтовом представлении в корейской кодовой странице.
• VB
Получить корейскую кодировку
Dim е As Encoding = Encoding.GetEncoding("Korean")
Преобразовать байты ASCII-кодов в корейскую кодировку
Dim encoded As Byte()
encoded = e.GetBytes("Hello, world!")
Отобразить кодированный байт
Dim i As Integer
For i = 0 To encoded.Length - 1
Console.WriteLine("Byte {0}: {1}", i, encoded(i))
Next i
// C#
// Получить корейскую кодировку
Encoding e = Encoding.GetEncoding("Korean");
// Преобразовать байты ASCII-кодов в корейскую кодировку
Занятие 2
Кодирование-декодирование строк 45
byte[] encoded;
encoded = e.GetBytes("Hello, world}");
// Отобразить байты в заданной кодировке for (int i = 0; i < encoded.Length; i++)
Console.WriteLine("Byte {0}: {1}", i, encoded[i]);
Этот пример демонстрирует преобразование текста в другую кодовую страницу, однако преобразовать англоязычный текст в другие кодовые страницы обычно не так просто. В большинстве кодовых страниц коды от 0 до 127 представляют одни и те же ASCII-символы. Это сделано для сохранения наследуемого кода. А рот коды 128—255 в разных кодовых страницах кодируют разные символы. Поэтому данный код преобразует фразу «Hello, world!» в ASCII-кодах (она содержит лишь ASCII-коды из диапазона 0-127) в идентичную фразу даже в корейской кодовой странице, так как данные коды представляют в ней те же символы, что и в ASCII.
К СВЕДЕНИЮ Кодовые страницы
Полный список поддерживаемых кодовых страниц см. в разделе «Encoding Class» (http:// msdn2.microsoft.com/en- us/library/system.text.encoding(VS.80).aspx).
Проверка поддерживаемых кодовых страниц
Для проверки поддерживаемых кодовых страниц в .NET Framework вызовите Encoding.GetEncodings — метод, возвращающий массив объектов Encodinginfo. Следующий пример кода отображает номер, официальное название и понятное имя базы кодов.NET Framework:
' VB
Dim ei As Encodinglnfo() = Encoding.GetEncodings
For Each e As Encodinginfo In ei
Console.WriteLine("{0}: {1}, {2}", e.CodePage, e.Name, e.DisplayName)
Next
// C#
Encodinglnfo[] ei = Encoding.GetEncodingsQ;
foreach (Encodinginfo e in ei)
Console.WriteLine(”{0}: {1}, {2}", e.CodePage, e.Name, e.DisplayName);
Определение кодировки при записи файла
Чтобы задать кодировку при записи файла используйте перегруженный конструктор Stream, принимающий объект Encoding. Следующий код создает несколько файлов в различных кодировках:
' VB
Dim swUtf7 As StreamWriter = New StreamWriterCutf7.txt", False, Encoding.UTF7) swUtf7.WriteLine("Hello, World!") swUtf7.Close
Dim swUtf8 As StreamWriter = New StreamWriter("utf8.txt", False, Encoding.IITF8)
146	Поиск, изменение и перекодирование текста
Глава 3
swUtf8.WriteLine("Hello, World!”)
swUtf8.Close
Dim swUtf16 As StreamWriter = New StreamWriterCutf16.txt", False, Encoding Unicode) swUtf16.WriteLine("Hello, World!") swUtf16.Close
Dim swUtf32 As StreamWriter = New StreamWriterCutf32.txt", False, Encoding.UTF32) swUtf32.WriteLine("Hello, World! ) swUtf32.Close
// C#
StreamWriter swUtf7 = new StreamWriterCutf7.txt", false, Encoding.UTF7); swUtf7.WriteLine("Hello, World!");
swUtf7.Close();
StreamWriter swUtf8 = new StreamWriterCutf8.txt", false, Encoding.UTF8); swUtf8.Writel_ine("Hello, World!");
swUtf8.Close();
StreamWriter swUtf16 = new StreamWriterCutf16.txt", false, Encoding.Unicode) swUtf16.WriteLine("Hello, World!");
swUtf16.Close();
StreamWriter swUtf32 = new StreamWriter("utf32.txt", false, Encoding.UTF32) swUtf32.WriteLine("Hello, World!");
swUtf32.Close();
Обратите внимание, что предыдущий пример генерирует четыре файла разного размера, в зависимости от кодировки: в UTF-7 — 19 байтов, в UTF-8 — 18 байтов, в UTF-16 — 32 и в UTF-32 — 64. Если открыть эти файлы в Блокноте, файлы в UTF-8 и UTF-16 отобразятся корректно, а файлы в UTF-7 и UTF-32 — нет. Дело не в ошибке кодировки, просто Блокнот не поддерживает чтение файлов в кодировках UTF-7 и UTF-32.
ПРИМЕЧАНИЕ Выбор кодировки
Если вы не знаете, какую кодировку выбрать при создании файла, оставьте кодировку по умолчанию, в .NET Framework это UTF-16.
Определение кодировки при чтении файла
Как правило, нет необходимости задавать кодировку при чтении файла .NET Framework автоматически декодирует наиболее распространенные кодировки. Однако при необходимости можно указать кодировку, используя перегруженный конструктор Stream'.
 VB
Dim fn As String = "file.txt"
Dim sw As StreamWriter = New StreamWriter(fn, False, Encoding.UTF7)
sw.WriteLine("Hello, World!”)
sw Close
Занятие 2
Кодирование-декодирование строк j 47
Dim sr As StreamReader = New StreamReader(fn, Encoding.UTF7)
Console.WriteLine(sr.ReadToEnd)
sr.Close
// C#
string fn = "file.txt";
StreamWriter sw = new StreamWriter(fn, false, Encoding.UTF7);
sw. WriteLineC'Hello, World!");
sw. CloseO;
StreamReader sr = new StreamReader(fn, Encoding.UTF7);
Console. WriteLine(sr. ReadToEndO);
sr.CloseO;
В отличие от большинства кодировок Unicode, UTF-7 в предыдущем примере требует явного объявления при чтении файла Если выполнить следующий код, не определяющий UTF-7, файл будет отображен неверно:
' VB
Dim fn As String = "file.txt"
Dim sw As StreamWriter = New StreamWriter(fn, False, Encoding.UTF7)
sw WriteLine( Hello, World!")
sw.Close
Dim sr As StreamReader = New StreamReader(fn)
Console.WriteLine(sr.ReadToEnd)
sr.Close
11 C#
string fn = "file.txt";
StreamWriter sw = new StreamWriter(fn, false, Encoding.UTF7);
sw WriteLineC'Hello, World’”);
sw CloseO;
StreamReader sr = new StreamReader(fn);
Console.WriteLine(s r.ReadToEnd());
sr CloseO;
Практикум. Чтение и запись файлов в разных кодировках
Вы должны преобразовать текстовый файл из одной кодировки в другую. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение. Преобразование текстового файла в другую кодировку
В этом упражнении вы преобразуете текстовый файл в кодировку UTF-7.
1.	Создайте новое консольное приложение в Visual Studio 2005.
Г --Ч
148 Поиск, изменение и перекодирование текста	Глава 3
2.	Напишите код для чтения файла C:\boot.ini, перекодирования его в UTF-7 и записй результата в файл boot-utf7.txt. Примером может быть следующий код (он использует пространство имен System.1О)‘.
’ VB
Dim sr As StreamReader = New StreamReader("C:\boot.ini")
Dim sw As StreamWriter = New StreamWriterCboot-utf7.txt", False, Encoding.UTF7) sw.WriteLine(sr.ReadToEnd) sw. CloseO sr. CloseO
// C#
StreamReader sr = new StreamReader(@"C:\boot.ini");
StreamWriter sw = new StreamWriter("boot-utf7.txt", false, Encoding.UTF7); sw.WriteLine(sr. ReadToEndO);
sw. CloseO;
sr. CloseO;
3.	Запустите приложение и откройте файл boot-utf7.txt в Блокноте. Если перекодирование файла прошло успешно, часть символов в Блокноте будет нечитабельной (Блокнот не поддерживает UTF-7).
Резюме
	Стандарты кодирования текстов определяют соответствие между значениями байтов и понятными символами. ASCII является одним из самых старых и распространенных форматов кодирования текстов, но обеспечивающим очень ограниченную поддержку языков, отличных от английского. На сегодняшний день многоязыковую поддержку обеспечивают различные стандартные кодировки Unicode.
	Класс System. Text. Encoding поддерживает статические методы для кодирования-деко-дирования текста.
	Метод Encoding.GetEncodings используется для получения списка поддерживаемых кодовых страниц.
	Чтобы задать кодировку при записи файла используют перегруженный конструктор Stream, принимающий объект Encoding.
	Обычно при чтении файла не требуется задавать кодировку, однако это можно сделать с помощью перегруженного конструктора Stream, принимающего объект Encoding.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	В какой из следующих кодировок размер файла будет максимальным?
A.	UTF-32;
В.	UTF-16;
С.	UTF-8;
D.	ASCII.
Резюме главы
149
2.	Какие кодировки поддерживают китайский язык? (Укажите все верные ответы.)
A.	UTF-32;
В.	UTF-16;
С.	UTF-8;
D.	ASCII.
3.	Вам необходимо декодировать файл в кодировке ASCII. Какая кодировка позволит сделать это? (Укажите все верные ответы.)
A.	Encoding.UTF32;
В.	Encoding.UTF16;
С.	Encoding.UTF8;
D.	Encoding.UTF7.
4.	Вы пишете приложение, генерирующее сводные отчеты. Эти отчеты предназначены для корейского отделения вашей компании и потому должны поддерживать корейский язык. Какую кодировку следует использовать в приложении?
А. iso-2022-kr;
В. x-EBCDIC-KoreanExtended;
С. x-mac-korean;
D. UTF-16.
Закрепление материала главы
Для закрепления знаний и навыков, полученных при изучении этой главы, выполните следующее:
	изучите резюме главы;
	повторите основные термины, использованные в этой главе;
	выполните задания лабораторной работы по темам этой главы;
	выполните предложенные упражнения;
	пройдите пробный экзамен.
Резюме главы
	Регулярные выражения берут свое начало в UNIX и Perl, и потому часто кажутся .NET-разработчикам сложными и чуждыми. Однако регулярные выражения чрезвычайно эффективны и удобны при проверке текстового ввода, извлечения и переформатирования тестовых данных.
	За последнее десятилетие на смену популярному стандарту кодирования текстовых файлов ASCII пришел Unicode. Unicode поддерживает несколько кодировок. В .NET Framework по умолчанию используется UTF-16, поддерживаются и другие кодировки, что обеспечивает совместимость с различными системами.
150 Поиск, изменение и перекодирование текста
Глава 3
Основные термины
	кодовая страница;
	регулярное выражение;
	Unicode.
Лабораторная работа
Сейчас вы примените полученные знания о регулярных выражениях для проверки правильности ввода и обработки текстовых файлов в разных кодировках. Ответы на вопросы см. в разделе «Ответы» в конце книги.
Занятие 1. Проверка ввода
Ваша компания, Northwind Traders, нуждается в Web-приложении, позволяющем клиентам вводить свою контактную информацию в базу данных. Вы должны решить простую задачу: создать пользовательский интерфейс и подготовить вводимые пользователем данные для сохранения в базе данных. Вы решили начать с опроса сотрудников компании и анализа технических требований.
Результаты опроса
	ИТ-менеджер
«Это ваше первое задание, поэтому оно будет простым. Напишите Web-страницу для сбора вводимых пользователями данных. Думаю, на это вам хватит и пяти минут».
	Разработчик базы данных
«Передавайте вводимые данные в строковые переменные «company Name», «соп-tactName» и «phoneNumber». Они будут записаны в базу данных SQL, я допишу этот код, когда вы закончите. Да, «companyName» должна быть не длиннее 40 символов, «contactName» — 30 символов, a «phoneNumber» — 24 символов».
	Директор по безопасности
«Это задание не такое простое, как кажется. Эта страница будет доступна всем пользователям Интернета, включая злоумышленников и хакеров. Недавно в прессе были негативные отклики о наших методах защиты международной торговли из-за успешных атак хакеров. Вы должны сделать все возможное для очистки вводимых данных от вредоносного кода».
Технические требования
Вы должны создать приложение ASP.NET, принимающее от пользователя и строго проверяющее следующие данные:
	название компании;
	контактное лицо;
	номер телефона.
Рекомендуемые упражнения j g-|
Вопросы
Ответьте на следующие вопросы руководства.
1. Как ограничить вводимые данные до программной обработки?
2. Как подвергнуть ввод дальнейшей проверке в программе?
Занятие 2. Обработка данных, введенных
на унаследованном ПК
Вы — разработчик приложений в компании Humongous Insurance. Недавно руководство начало переход с унаследованного сервера на собственные .NET-приложения. Вам необходимо ответить на следующие вопросы о планировании перехода.
Вопросы
Ответьте на следующие вопросы руководства.
1. На старом сервере данные хранятся в базе, но они недоступны, так как нет программистов для устаревших СУБД. Однако возможно вывести необходимые нам данные в текстовые файлы. Можно ли проанализировать их и извлечь только данные, без лишних меток и тэгов? Как это сделать, с помощью каких классов и методов?
2. Текстовые файлы выводятся в кодировке ASCII. Можно ли обрабатывать их? Если да, то как?
Рекомендуемые упражнения
Чтобы лучше проработать темы экзамена, рассмотренные в этой главе, выполните следующие упражнения.
Дополнение обработки текстов в .NET-приложениях поиском с заменой на основе регулярных выражений
Выполните как минимум упражнения 1—4. Дополнительный опыт работы с различными кодировками позволит получить упражнение 5.
	Упражнение 1 Напишите консольное приложение, читающее файл C:\boot.ini и отображает только тайм-аут выбора вариантов загрузки.
	Упражнение 2 Напишите консольное приложение, обрабатывающее файл %windir%\ WindowsUpdate.log и отображающее время, дату и коды завершения.
	Упражнение 3 Напишите приложение Windows Forms, принимающее имя, адрес и телефон пользователя. Добавьте кнопку Submit, код которой проверяет ввод с помощью регулярных выражений.
	Упражнение 4 Напишите консольное приложение, читающее файл %windir%\Wndows-Update.log, изменяющее формат даты на «месяц-день-год» и записывающее результаты в новый файл.
	Упражнение 5 Напишите консольное приложение, читающее файл %windir%\Windows-Update.log и записывающее его в другой файл с заданной кодировкой. Сравните размеры генерируемых файлов в разных кодировках.
152 Поиск, изменение и перекодирование текста
Глава 3
Пробный экзамен
На прилагаемом компакт-диске, содержится пробный экзамен, обеспечивающий широкие возможности по самоподготовке. Например, можно проверить усвоение темам этой главы либо всех тем экзамена 70-536. Можно сымитировать настоящий экзамен либо выбрать режим обучения, когда после каждого вопроса приводятся правильные ответы с пояснениями.
Пробный экзамен
Подробнее о пробном экзамене рассказано во введении.
ГЛАВА 4
Наборы и обобщения
Занятие 1.	Сбор элементов данных	154
Занятие 2.	Работа с последовательными	списками	166
Занятие 3.	Работа со словарями	173
Занятие 4.	Применение специализированных наборов	188
Занятие 5.	Обобщенные наборы	201
Наборы — классы, предназначенные для группировки связанных объектов, управления ими и обработки их в циклах, — являются одним из основных инструментов программиста. Объекты в наборах можно хранить, искать и итеративно обрабатывать. Образно говоря, наборы начинаются там, где заканчиваются массивы. Как ни полезны массивы, но без богатых возможностей наборов многие приложения никогда не бы не появились на свет.
Темы экзамена:
	Управление группами связанных данных .NET-приложений с использованием наборов (см. пространство имен Sy stem.Collections):
□	класс Array List,
□	интерфейсы наборов;
□	итераторы;
□	класс Hashtable\
□	классы CollectionBase и ReadOnlyCollectionBase,
□	классы Dictionary Base и DictionaryEntry,
□	класс Comparer,
□	класс Queue',
□	класс SortedLisf,
□	класс BitArray,
□	класс Stack.
	Управление данными .NET-приложений с использованием специализированных наборов, (см. пространство имен Sy stem.Collections. Specialized):
□	классы, специализированные для String',
□	специализированый набор Dictionary,
154 Наборы и обобщения
Глава 4
□	класс NameValueCollection;
□	Collections Util;
□	структуры BitVector32 и BitVector32.Section.
 Повышение производительности и эффективности контроля типов в .NET-при-ложениях с использованием обобщенных наборов (см. пространство имен System.Collections.Generic):
□	интерфейсы обобщенного набора;
□	обобщение Dictionary;
□	классы обобщений Comparer и EqualityComparer,
□	структура-обобщение KeyValuePair,
□	класс-обобщение List, структуры-обобщения List.Enumerator и класс-обобщение SortedList;
□	класс-обобщение Queue и структура-обобщение Queue.Enumerator;
□	класс-обобщение SortedDictionary;
□	обобщение LinkedList;
□	класс-обобщение Stack и структура-обобщение Stack.Enumerator.
Прежде всего
Чтобы освоить материал этой главы, необходимо владеть Microsoft Visual Basic или C# и уметь:
	создавать консольные приложения в Microsoft Visual Studio, используя Visual Basic или С#;
	добавлять в проект ссылки на системные библиотеки классов;
Пример из практики
Шон Уилдермьюс (Shawn Wildermuth)
Большинство современных приложений основано на применении наборов. Я использовал их почти в каждом из своих приложений. Например, несколько лет назад я работал над почтовой программой, в которой хранились списки сообщений, и с успехом применял в ней наборы.
Занятие 1. Сбор элементов данных
Компьютеры идеально устроены для обработки больших объемов данных. Поэтому к рутинным задачам программиста относится надлежащая организация хранения данных. .NET Framework включает широкий спектр наборов, пригодных для хранения и обработки данных.
Занятие 1
Сбор элементов данных *| 55
* Изучив материал этого занятия, вы сможете:
/ создавать наборы;
J добавлять элементы в набор и удалять их из него;
J перебирать элементы набора в цикле.
Продолжительность занятия — 15 минут.
Типы наборов
В .NET Framework пространство имен System.Collections поддерживает несколько типов наборов. Такие наборы представляют собой классы, позволяющие собирать и упорядочивать информацию. Задача программиста заключается в правильном выборе типа набора, соответствующего поставленной задаче. В табл. 4-1 приведены наиболее распространенные наборы из пространства имен System.Collections с описанием их предназначения.
Табл. 4-1. Типы наборов
Имя	Описание
ArrayList	Простой, поддерживающий индексиование и изменение размера набор объектов
SortedList	Упорядоченный набор пар объектов «имя-значение»
Queue	Набор объектов, организованный по принципу «первым вошел, первым вышел»
Stack	Набор объектов, организованный по принципу «последним вошел, первым вышел»
Hashtable	Набор пар объектов «имя-значение», предоставляющих доступ к элементам как по имени, так и по индексу в наборе
BitArray	Компактный набор значений типа Boolean
StringCollection	Простой, поддерживающий изменение размера, набор строк
StringDictionary	Набор пар строк «имя-значение», предоставляющих доступ к элементам как по имени, так и по индексу в наборе
ListDictionary	Набор, подходящий для хранения небольших списков объектов
HybridDictionary	Набор, в котором элементы хранятся в ListDictionary, если их мало, либо в Hashtable, если их много
NameValueCollection	Набор пар строк «имя-значение», предоставляющих доступ к элементам как по имени, так и по индексу в наборе
Все эти наборы используются в разных ситуациях. На первых четырех занятиях обсуждается как использование наборов, так и выбор подходящего набора. В завершением мы рассмотрим хранение и получение объектов при помощи набора Array List — наиболее общего представителя наборов.
Добавление и удаление элементов
Класс ArrayList — простой неупорядоченный контейнер для объектов любого типа. И добавление, и удаление элементов этого класса выполняется просто.
156 Наборы и обобщения
Глава 4
ArrayList поддерживает два метода, добавляющие элементы в набор. Add и Add Range. Метод Add позволяет добавить в набор одиночные объекты. При помощи метода Add можно поместить в набор любой объект .NET. Ниже приводится несколько примеров добавления в ArrayList объектов различных типов:
' VB
Dim coll As New ArrayListO
Добавление в набор одиночных элементов
Dim s As String = "Hello"
coll.Add(s)
coll. AddC'hi")
coll.Add(50)
coll. Add (New ObjectO)
// C#
ArrayList coll = new ArrayListO;
// Добавление в набор одиночных элементов
string s = "Hello”;
coll.Add(s);
coll. AddC'hi");
coll.Add(50);
coll.Add(new object());
Обратите внимание: можно добавлять как существующие объекты из переменных, так и объекты, созданные «на месте», при вызове Add. Можно даже добавлять значимые типы (такие, как число *50 в примере выше). Вообще, в наборах разрешается хранить значимые типы, но сначала их следует преобразовать к ссылочному типу, выполнив операцию, называемую упаковкой.
К СВЕДЕНИЮ Упаковка
Подробнее об упаковке см. в статье Эрика Гуннерсона (Eric Gunnerson) (“Nice Box. What’s in it?) с сайта MSDN Online: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/ dncscol/html/csharp02152001.asp
Помимо метода Add, ArrayList поддерживает метод AddRange, предназначенный для добавления групп элементов, обычно из массива или другого набора. Пример кода приведен ниже:
' VB
Dim anArrayO As String = {"more", "or", "less"} coll.AddRange(anArray)
Dim anotherArrayO As Object = {New ObjectO, New ArrayListO} coll.AddRange(anotherArray)
// C#
stringt] anArray =
new string[] { "more", "or", "less" };
Занятие 1
Сбор элементов данных j 57
coll.AddRange(anArray);
object[] anotherArray =
new object[] { new objectO, new ArrayListO };
coll.AddRange(anotherArray);
Метод AddRange позволяет добавлять диапазоны любых объектов, поддерживающих интерфейс ICollection (включая массивы, объекты ArrayList и большинство наборов, рассматриваемых в этой главе).
Методы Add и AddRange добавляют элементы в конец набора. Поскольку наборы ArrayList являются динамическими, они поддерживают вставку объектов в заданном положении. Для этого ArrayList предоставляет методы Insert and InsertRange. Пример их использования приведен ниже:
' VB
coll.Insert(3, "Неу All")
Dim merestrings() As String = {"goodnight", "see ya"}
coll.InsertRange(4, moreStrings)
// C#
coll.Insert(3, "Hey All”);
string[] moreStrings =
new string[] { "goodnight", "see ya" };
coll.InsertRange(4, moreStrings);
Можно не только вызывать методы Insert и Add, но и присваивать элементу набора заданный объект при помощи индексатора, как показано ниже:
’ VB
со11(3) = "Неу АН"
// C#
Со11[3] = "Неу АН";
Заметьте, что при использовании индексатора, в отличие от метода Insert, объект не вставляется в набор, а присваивается элементу набора с заданным индексом, перезаписывая прежний объект, бывший в этом элементе.
Наконец, ArrayList поддерживает удаление элементов из набора. Для удаления элементов предназначены три метода: Remove, RemoveAt и RemoveRange. Метод Remove удаляет из набора заданный объект. Этот метод не уведомляет о неудаче удаления. Другими словами, если заданного элемента в наборе не окажется, Remove завершится, не сгенерировав исключения. Ниже приведен пример вызова метода Remove'.
’ VB
coll.Add("Hello")
coll.Remove("Hello")
// C#
coll.Add("Hello");
coll.Remove("Hello");
j58 Наборы и обобщения
Глава 4
Метод RemoveAt удаляет из набора элемент с заданным индексом, а метод RemoveRange — сразу несколько объектов, заданных как диапазон индексов. Примеры использования обоих методов показаны ниже:
• VB
Удаление из ArrayList первого элемента
coll.RemoveAt(0)
' Удаление из ArrayList первых четырех элементов
coll.RemoveRange(0, 4)
// C#
// Удаление из ArrayList первого элемента
coll.RemoveAt(0)
// Удаление из ArrayList первых четырех элементов
coll.RemoveRange(0, 4);
Класс ArrayList поддерживает и другие методы, удобные для добавления и удаления объектов из набора:
	метод Clear удаляет все элементы из набора;
	метод IndexOf определяет индекс заданного элемента в наборе;
	метод Contains проверяет наличие заданного элемента в наборе.
Применение этих методов позволяет решать более сложные задачи по добавлению и удалению элементов, как показано в следующем примере:
' VB
Dim myString As String = "My String'
If coll.Contains(myString) Then
Dim index As Integer = coll.IndexOf(myString)
’ coll.RemoveAt(index)
Else
coll.Clear()
End If
// C#
string myString = "My String";
if (coll.Contains(myString))
int index = coll.IndexOf(myString);
coll.RemoveAt(index);
} 5 ' else
{
coll.Clear();
}
Итак, вы научились управлять объектами в наборах, но как их получить оттуда?
>
Занятие 1
Сбор элементов данных 15g
Перебор элементов в цикле
От наборов мало пользы, если их элементы нельзя перебирать в цикле. К счастью, ArrayList (подобно большинству наборов, рассматриваемых в этой главе) предлагает несколько способов перебора в цикле содержимого набора. ArrayList поддерживает числовой индексатор, позволяющий выводить элементы в порядке, в котором они представлены в наборе (это работает почти так же, как у массивов), что и показано в этом простом примере:
• VB
Dim х As Integer
For x = 0 To coll.Count - 1 Step + 1
Console.WriteLine(coll(x))
Next
// C#
for (int x = 0; x < coll.Count: ++x)
{
Console.WriteLine(coll[x]);
}
При помощи свойства Count набора ArrayList и индексатора можно без труда обойти все элементы набора. ArrayList также поддерживает интерфейс LEnumerable, обеспечивающий доступ к набору объект-перечислитель типа Enumerator. Наличие интерфейса LEnumerable указывает на то, что класс поддерживает метод GetEnumerator, возвращающий интерфейс LEnumerator. В свою очередь, LEnumerator — это простой интерфейс для прямого просмотра набора. Подробнее интерфейс LEnumerator представлен в табл. 4-2 (свойства) и 4-3 (методы).
Табл. 4-2. Свойства LEnumerator
Имя	Описание
Current	Возвращает текущий элемент набора при перечислении
Табл. 4-3.	Методы LEnumerator
Имя	Описание
MoveNext Reset	Перемещает к следующему элементу набора. Возвращаемое им значение определяет, достигнут ли конец набора Устанавливает перечислитель перед первым элементом набора, следующий вызов MoveNext переместит перечислитель на первый элемент набора
Интерфейс LEnumerator позволяет перебрать объекты в том порядке, в котором они расположены в списке, как показано в этом примере: ’ VB
Dim enumerator As lEnumerator = coll.GetEnumerator()
While enumerator.MoveNextO
Console. Writel_ine(enumerator. Current) End While
160 Наборы и обобщения
Глава 4
// C# lEnumerator enumerator = coll.GetEnumerator();
while (enumerator.MoveNextO)
Console.WriteLine(enumerator.Current);
Это простейший пример получения перечислителя и просмотра набора вызовом метода MoveNext. Свойство перечислителя Current возвращает текущий элемент списка.
Как Visual Basic, так и C# поддерживают конструкции, максимально упрощающие подобные перечисления: foreach. Как показано ниже, этот оператор позволяет перечислить все элементы списка:
' VB
For Each item As Object In coll
Console.WriteLine(item)
Next item
// C#
foreach (object item in coll)
{
Console.WriteLine(item);
}
Из синтаксиса оператора foreach видно, что он перебирает в цикле элементы со//, создавая объект item для каждого элемента набора. В основе этого оператора лежит интерфейс lEnumerable. Вообще, эту конструкцию можно использовать с любым набором, поддерживающий интерфейс lEnumerable. Одним из преимуществ такой разновидности цикла является возможность указать в операторе foreach тип объектов (если он известен), сэкономив время выполнения за счет приведения типов:
' VB
Dim newColl As New ArrayListO
newColl.Add("Hello")
newColl.Add("Goodbye")
For Each item as String In newColl
Console.WriteLine(item)
Next item
// C#
ArrayList newColl = new ArrayListO; .
newColl.Add("Hello");
newColl.Add("Goodbye");
foreach (string item in newColl)
Console.WriteLine(item);
Занятие 1
Сбор элементов данных 161
Поскольку известно, что все элементы набора — строки, можно указать string в качестве типа элементов. Если в наборе попадется элемент, не являющийся строкой, .NET Framework сгенерирует исключение из-за несоответствия типов.
Согласованные интерфейсы наборов
Как упоминалось в предыдущем разделе, интерфейс lEnumerable унифицирует перебор элементов набора в цикле. .NET Framework такде поддерживает общий интерфейс — образец API наборов. Этот интерфейс называется ICollection и происходит от интерфейса lEnumerable. Это означает, что любой набор, поддерживающий интерфейс ICollection, также поддерживает интерфейс lEnumerable.
Этот интерфейс унифицирует получение элементов набора и копирование наборов в объект Array. В табл. 4-4 и 4-5, соответственно, приведены наиболее важные свойства и методы интерфейса ICollection.
Табл. 4-4. Свойства ICollection
Имя	Описание
Count	Возвращает текущее число элементов набора
IsSynchronized SyncRoot	Возвращает признак безопасности набора в многопоточном окружении Возвращает объект, который можно использовать для синхронизации набора
Табл. 4-5. Методы ICollection
Имя	Описание
СоруТо	Копирует содержимое набора в Array
Для простых наборов, представляющих собой списки (как ArrayList), .NET Framework поддерживает еще один интерфейс для доступа к элементам списка. Этот интерфейс называется IList и происходит непосредственно от ICollection. Если класс поддерживает интерфейс IList, он также поддерживает интерфейсы ICollection и lEnumerable. Такая иерархия интерфейсов вносит концептуальную упорядоченность в работу с наборами.
Вам уже знакомо большинство членов интерфейса IList. Многие свойства и методы разбирались в этой главе ранее (о членах класса ArrayList см. в разделе «Добавление и удаление элементов»/ В табл. 4-6 и 4-7, соответственно, перечислены наиболее важные свойства и метода интерфейса IList.
Табл. 4-6. Свойства IList
Имя	Описание
IsFixedSize	Позволяет узнать, можно ли изменять размер набора
IsReadOnly	Позволяет узнать, можно ли изменять набор
Item	Возвращает или устанавливает элемент набора с заданным индексом
152	Наборы и обобщения
Глава 4
Табл. 4-7. Методы IList	
Имя	Описание
Add	Добавляет в набор элемент
Clear	Удаляет из набора все элементы
Contains	Проверяет, присутствует ли указанный элемент в наборе
IndexOf	Находит в наборе элемент и возвращает его индекс
Insert	Добавляет в набор элемент по заданному индексу
Remove.	Удаляет из набора первое вхождение указанного объекта
RemoveAt	Удаляет из набора элемент с заданным индексом
Упорядочение элементов
Набор ArrayList поддерживает метод для упорядочения элементов. Чтобы упорядочить элементы в наборе ArrayList, просто вызовите метод Sort класса ArrayList, как показано ниже:
' VB
coll.Sort()
// C#
coll. Sorto;
Метод Sort использует для сравнения элементов класс Comparer. Класс Comparer является стандартной реализацией интерфейса IComparer. Для реализации этого интерфейса требуется единственный метод с именем Compare, который принимает в качестве параметров два объекта (например, а и Ь) и возвращает целое значение — результат сравнения. В табл. 4-8 представлены возможные результаты сравнения с их описанием.
Табл. 4-8. Результаты вызова
Значение	Условие
Меньше нуля	Объект в левой части неравенства меньше объекта в правой части
Нуль	Объекты равны
Больше нуля	Объект в левой части неравенства больше объекта в правой части
Метод Sort поддерживает объект IComparer, отличный от стандартного. Например как показано ниже, вместо класса Comparer можно передать в качестве параметра CaselnsensitiveComparer, который производит сравнение без учета регистра:
1 VB
coll.Sort(new CaseInsensitiveComparer())
// C#	’
coll.Sort(new CaseInsensitiveComparer());
Довольно несложно разработать свой собственный класс для сравнения, для этого достаточно реализовать метод Compare интерфейса IComparer. Например, для обратного сравнения (с целью сортировки по убыванию) можно быстро написать такой класс:
Занятие 1
Сбор элементов данных ) gg
’ VB г
Public Class DescendingComparer
Implements IComparer
Private .comparer As New CaseInsensitiveComparer()
Public Function Compare(x As Object, у As Object) As Integer _
Implements IComparer
Для сортировки по убыванию
объекты, переданные для сравнения, меняются местами
Return .comparer.Compaге(у, x)
End Function
End Class
// C#
public class DescendingComparer : IComparer {
CaselnsensitiveComparer .comparer = new CaseInsensitiveComparer();
public int Compare(object x, object y) {
// Для сортировки по убыванию,
// объекты, переданные для сравнения, меняются местами return .comparer.Compare(y, x);
}
}
Этот класс реализует интерфейс IComparer. В методе Compare сравниваемые объекты, которые передаются как параметры, просто меняются местами, что дает обратный результат сравнения.
Теперь новый объект сравнения можно использовать для сортировки набора по убыванию:
' VB
coll.Sort(new DescendingComparer())
П C#
coll.Sort(new DescendingComparerO);
Практикум. Сортировка строк таблицы
На этом практикуме вы создадите набор строк и отсортируете его. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
164	Наборы и обобщения	Глава 4
Упражнение 1. Создание и упорядочение набора строк
В этом упражнении вы разработаете новое консольное приложение, которое создает простой набор, добавляет в него строки и выводит их на консоль. Затем вы отсортируете набор и выведете элементы набора на консоль в другом порядке.
1.	Создайте консольное приложение с именем BasicCollection.
2.	В основной файл с кодом импортируйте пространство имен System.Collections.
3.	В методе Main создайте новый экземпляр класса ArrayList.
4.	Добавьте в новый набор четыре строки: “First”, “Second”, “Third” и “Fourth”.
5.	Переберите элементы набора в цикле, выводя каждый элемент в новой строке окна консоли.
6.	Упорядочите набор при помощи метода Sort.
7.	Чтобы убедиться, что элементы набора расположены в другом порядке, повторно переберите их в цикле, выводя каждый элемент в консольное окно. Готовый код может быть таким:
' VB
Imports System.Collections
Class Program
Public Overloads Shared Sub Main(ByVal args() As String)
Dim myList As New ArrayListO myList.Add("Fi rst”) myList.Add("Second") myList.Add("Third") myList.Add("Fou rth")
For Each item as String In myList
Console.WriteLine("Unsorted: {0}", item) Next item
' Сортировка при помощи стандартного объекта сравнения myList.Sort()
For Each item as String In myList
Console.WriteLineC Sorted: {0}", item) Next item
End Sub
End Class
// C#
using System.Collections;
Занятие 1
Сбор элементов данных -| gg
class Program
{
static void Main(string[] args)
{
ArrayList myList = new ArrayListO;
myList.Add("First");
myList.Add("Second");
myList.Add("Thi rd");
myList,Add("Fourth");
foreach (string item in myList)
{
Console.WriteLine("Unsorted: {0}", item);
}
// Сортировка при помощи стандартного объекта сравнения
myList.Sort();
foreach (string item in myList)
{ Console.WriteLine(" Sorted: {0}”, item);
}
}
}
8.	Соберите проект, исправьте ошибки. Убедитесь, что консольное приложение правильно отображает как неупорядоченные, так и отсортированные элементы в окне консоли.
Резюме
	.NET Framework поддерживает различные классы наборов, которые можно использовать в зависимости от обстоятельств.
	ArrayList — простой набор неупорядоченных элементов.
	Методы Add и AddRange класса ArrayList применяются для добавления элементов в ArrayList.
	Методы Insert и InsertRange класса ArrayList применяются для вставки элементов в набор в заданном положении.
	Методы Remove, RemoveAt и RemoveRange класса ArrayList применяются для удаления элементов из набора.
	Индексатор ArrayList можно использовать для перебора элементов набора в цикле.
	Интерфейсы lEnumerable и lEnumerator также позволяют организовывать перечисление набора.
	Оператор For Each (foreach) в языках Visual Basic и C# использует интерфейс lEnumerable и позволяет быстро перебрать элементы набора в цикле.
166	Наборы и обобщения
Глава 4
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	При помощи каких из перечисленных методов класса ArrayList можно определить, присутствует ли элемент в наборе? (Укажите все верные ответы.) A. Remove.
В. Contains.
С. IndexOf.
D. Count.
2.	Для чего используется класс Comparer? (Укажите все верные ответы.)
А. Для сравнения двух объектов, обычно с целью сортировки.
В. Для проверки идентичности ссылок на объект
С. Для упорядочения набора ArrayList при помощи метода Array List. Sort.
D. Для того, чтобы предоставить стандартную реализацию интерфейса IComparer.
Занятие 2. Работа с последовательными списками
Не все наборы одинаковы. Иногда имеет смысл обрабатывать списки объектов последовательно, а не в произвольном порядке.
Изучив материал этого занятия, вы сможете:
•/ создавать наборы, организованные по принципу «первым вошел, первым вышел» (first-in, first-out; FIFO);
•J создавать наборы, организованные по принципу «последним вошел, первым вышел» (last-in, first-out; LIFO).
Продолжительность занятия — 10 минут.
Введение в последовательные списки
Часто набор просто представляет собой последовательность объектов, которые следует обрабатывать по порядку. Например, можно создать класс, который принимает объекты и выполняет над ними некоторые операции. В зависимости от задачи, может потребоваться последовательная обработка элементов. В этом случае произвольный доступ к элементам списка просто не нужен.
Вместо того ArrayList, можно воспользоваться одним из двух классов списков .NET Framework, предоставляющих последовательный доступ к объектам по мере необходимости. Это классы Queue и Stack.
Занятие 2
Работа с последовательными списками -| gy
Класс Queue
Класс Queue — набор объектов типа FIFO. Интерфейс класса Queue очень прост: он поддерживает добавление элементов в очередь и изъятие их оттуда.
Работа с классом Queue в корне отличается от работы с ArrayList (см. занятие 1). А именно, при работе с ArrayList обращение к элементу и удаление элемента из набора — различные операции, a Queue объединяет эти операции в комбинированном методе Dequeue. Эти операции исходно логически связаны в силу специфики класса Queue. Как следует из названия, Queue обращается с объектами, как кассир с клиентами в очереди к кассе. Его интересует лишь, кто следующий и за кем будут занимать очередь новые посетители.
В табл. 4-9 и 4-10, соответственно, приведены наиболее важные свойства и методы класса Queue.
Табл. 4-9. Свойства Queue
Имя	Описание
Count	Возвращает количество элементов в очереди
Табл. 4-10.	Методы Queue
Имя	Описание
Dequeue	Возвращает первый элемент очереди, одновременно удаляя его
Enqueue	Добавляет элемент в конец очереди
Peek	Возвращает первый элемент в очереди, не удаляя его
Работа с классом Queue очень проста. Создав экземпляр класса, можно вызывать метод Enqueue для добавления элементов в очередь и метод Dequeue для удаления элементов из списка:
’ VB
Dim q As New Queue() q. Enqueue("An item") Console. WriteLine(q. DequeueO)
// C#
Queue q = new QueueO;
q. Enqueue("An item");
Console.WriteLine(q.Dequeue());
Класс Queue позволяет добавлять в список дублирующиеся элементы и null-значения, поэтому методы Dequeue и Реек не помогут определить, пуста ли очередь Queue. Для этого можно проверить свойство Count. Например, если требуется добавить и затем удалить элементы в Queue, выводя их при этом на консоль, можно воспользоваться следующим кодом:
' VB
Dim q As New QueueO
q. Enqueue("First")
168	Наборы и обобщения
Глава 4
q.Enqueue("Second")
q.Enqueue("Third")
q. EnqueueC'Fourth")
While q.Count > 0
Console. WriteLine(q. DequeueO) End While
11 C#
Queue q = new QueueO;
q.Enqueue("First");
q.Enqueue("Second");
q.Enqueue("Third");
q. EnqueueC'Fourth");
while (q.Count > 0) {
Console.WriteLine(q.Dequeue());
}
Поскольку набор Queue организован по принципу FIFO, код предыдущего примера выводит на консоль строки в следующем порядке:
First
Second
Third
Fourth
Иногда требуется проверить следующий элемент, не удаляя его. Допустим, что ваш код работает с объектами определенных типов. Если вы воспользуетесь методом Dequeue и обнаружите, что полученный элемент следовало обработать иначе, вернуть элемент в очередь удастся, но только в другое положение. В такой ситуации поможет метод Реек' ' VB
If TypeOf q.Peek() Is String Then
Console. WriteLine(q. DequeueO) End If
П C#
if (q.Peek() is String) {
Console.WriteLine(q.Dequeue());
}
Иногда требуются последовательные наборы, организованные не по принципу LIFO. В этих случаях применяется класс Stack.
Занятие 2
Работа с последовательными списками j gg
Класс Stack
В отличие от Queue, класс Stack представляет собой набор, организованный по принципу «последним вошел, первым вышел» (LIFO). Интерфейс класса Stack также очень прост: он поддерживает «заталкивание» элементов в стек и «выталкивание» их оттуда.
Класс Stack можно сравнить с колодой карт, из которой можно брать карты и класть их только сверху. В табл. 4-11 и 4-12, соответственно, приведены самые важные свойства и методы класса Stack.
Табл. 4-11. Свойства Stack	
Имя	Описание
Count	Возвращает число элементов в стеке
Табл. 4-12.	Методы Stack
Имя	Описание
Pop	Возвращает элемент с вершины стека, одновременно удаляя его
Push	Добавляет элемент на вершину стека
Peek	Возвращает верхний элемент стека, не удаляя его
Работа с классом Stack похожа на работу с классом Queue, только вместо очереди используется стек. Создают экземпляр класса Stack, затем для добавления элементов в стек вызывают метод Push, а для удаления — метод Pop:
' VB
Dim s as new Stack()
s.Push("An item”)
Console.WriteLine(s.Pop())
// C#
Stack s = new Stack();
s.PushC'An item”);
Console WriteLine(s.PopO);
Как и в случае класса Queue, к стеку можно добавлять дублирующиеся элементы и null-значения, поэтому методы Pop и Реек не помогут определить, пуст ли стек. Чтобы добавить элементы в Stack, а затем удалить элементы и вывести их на консоль, можно воспользоваться следующим кодом:
' VB
Dim s As New Stack()
s. PushC'First")
s.Push("Second")
s.Push(”Third")
s.Push("Fourth")
While s.Count > 0
Console.WriteLine(s.Pop())
End While
Наборы и обобщения	Глава 4
// C#
Stack s = new Stack():
s.Push("First");
s.Push("Second”);
s.Push("Third");
s.Push("Fourth");
while (s.Count > 0)
{
Console. WriteLine(s. PopO);
}
Поскольку Stack организован по принципу LIFO, этот код выводит элементы в порядке, обратном таковому в показанном выше примере с Queue’.
Fourth
Third
Second
First
Подготовка к экзамену
Отличить Queue от Stack несложно, представив их аналогии из реальной жизни. Очередь Queue похожа на очередь в кинотеатр, а стек объекта Stack — на стопку бумаги на столе: вряд ли вы войдете в кинотеатр раньше тех, кто стоит перед вами, а из стопки бумагу обычно берут сверху.
Практикум. Построение FIFO- и LIFO-списков
На этом практикуме вы создадите очередь и стек, а затем выведете их элементы на консоль. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение 1. Создание и использование очереди
В этом упражнении вы создадите очередь, добавите в нее элементы и очистите очередь, выводя ее содержимое в окно консоли.
1.	Создайте консольное приложение с именем SequentialCollections. . г
2.	В основной файл с кодом импортируйте пространство имен System.Collections.
3.	В методе проекта Main создайте новый экземпляр класса Queue.
4.	Добавьте в новый набор четыре строки: “First”, “Second”, “Third” tn “Fourth”.
5.	Поочередно удалите все элементы из очереди, проверяя свойство Count, чтобы определить, не опустел ли набор. Получится примерно такой код:
’ VB
Imports System.Collections
Class Program
Public Shared Sub Main(ByVal args() As String)
Занятие 2
Работа с последовательными списка ли 171
г Dim queue As New QueueO
queue.Enqueue("First")
queue.Enqueue("Second")
queue.Enqueue("Third")
queue.Enqueue("Fourth")
While queue.Count > 0
Dim obj As Object = queue. DequeueO
Console.WriteLine("From Queue: {0}", obj)
End While
End Sub
End Class
// C#
using System.Collections;
class Program
{
static void Main(string[] args)
{
Queue queue = new QueueO;
queue.Enqueue("First");
queue.Enqueue("Second");
queue.Enqueue("Third");
queue.Enqueue("Fourth");
while (queue.Count > 0)
object obj = queue.DequeueO;
Console.WriteLine("From Queue: {0}", obj);
}
}
}
6.	Соберите проект и исправьте возможные ошибки. Убедитесь, что консольное приложение выполняется нормально, выводя элементы очереди по принципу «первым вошел, первым вышел».
Упражнение 2. Создание и использование стека
В этом упражнении вы создадите стек, добавите в него элементы и очистите стек, выводя его содержимое в окно консоли.
1.	Откройте консольное приложение с именем SequentialCollections, созданное в упражнении 1.
2.	После кода, относящегося к классу Queue, создайте новый экземпляр класса Stack.
*| 72 Наборы и обобщения
Глава 4
3.	Добавьте в стек четыре строки: “First”, “Second", “Third” и “Fourth".
4.	Поочередно удалите элементы стека, проверяя свойство Count, чтобы определить, не опустел ли стек. Получится примерно такой код:
' VB
Dim stack As New Stack()
stack.Push("First”)
stack Pusn("Second')
stack.Push("Third")
stack.Push("Fourth")
While stack.Count > 0
Dim obj As Object = stack PopO
Console.WriteLine("From Stack: {0}", obj)
End While
// C#
Stack stack = new Stack();
stack.Push("First”);
stack Push("Second"),
stack.Push("Third");
stack.Push("Fourth”);
while (stack.Count > 0)
{
object obj = stack.Pop();
Console.WriteLine("From Stack: {0}", obj);
}
5.	Соберите проект и исправьте возможные ошибки. Убедитесь, что консольное приложение выполняется нормально, выводя элементы стека в порядке, обратном по сравнению порядком элементов очереди.
Резюме
	.NET Framework поддерживает классы Queue и Stack для реализации наборов, представляющих последовательные списки элементов.
	Набор Queue организован по принципу «первым вошел, первым вышел» (FIFO).
	Класс Queue поддерживает методы Enqueue и Dequeue для добавления элементов в набор и удаления элементов из набора.
	Набор Stack организован по принципу «последним вошел, первым вышел» (LIFO).
	Класс Stack поддерживает методы Push и Pop, соответственно, для добавления и удаления элементов из набора.
	Оба класса последовательных списков поддерживают метод Реек для просмотра следующего элемента в наборе без его удаления.
Занятие 3
Работа со словарями -| 73
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Какие операции выполняет метод Dequeue класса Queued (Укажите все верные ответы.)
А.	возвращает первый элемент набора;
В.	добавляет элемент в набор;
С.	удаляет из набора первый элемент;
D.	удаляет из набора все элементы;
2.	В каком порядке возвращает элементы Stack при вызове метода Pop?
А.	в случайном порядке;
В.	«первым вошел, первым вышел»;
С.	«последним вошел, первым вышел»;
D.	«последним вошел, последним вышел».
Занятие 3. Работа со словарями
Альтернатива последовательных списков — словари. Это наборы, предназначенные для хранения пар «ключ — значение» (или «имя — значение»), поддерживающие поиск значения по ключу (имени).
Изучив материал этого занятия, вы сможете:
использовать класс Hashtable для создания простого списка уникальных элементов;
J использовать класс SortedList для упорядочения списка объектов;
S использовать объекты Dictionary Entry для хранения пар «имя-значение»;
J перечислять элементы словарей и применять объекты Dictionary Entry',
J генерировать уникальные ключи для словарей типа Hashtable при помощи интерфейса lEqualityComparison',
J использовать HybridDictionary для повышения эффективности хранения пар «имя-значение»;
J использовать OrderedDictionary для записи пар «имя-значение» в набор с сохранением их порядка;
Продолжительность занятия — 10 минут.
J *
Применение объекта Dictionary
Классы Dictionary, поддерживаемые .NET Framework, позволяют связывать ключи и значения в пары. По сути они предназначены для создания таблиц поиска, содержащих пары произвольных ключей и значений. В большинстве случаев класс Hashtable применяется для сопоставления компонентов пар «ключ-значение». Предположим, к приме
j 74 Наборы и обобщения
Глава 4
ру, что требуется сопоставить полные имена пользователей электронным адресам Для хранения таких данных можно использовать класс Hashtable, как показано в следующем фрагменте кода:
‘ VB
Dim emailLookup As New HashtableO
Метод Add принимает в качестве первого параметра ключ, а в качестве второго - значение
emailLookup.Add("sbishop@contoso.com", "Bishop, Scott")
Использование индексатора эквивалентно вызову Add emailLookup("sbishop@contoso.com") = "Bishop, Scott"
11 C#
Hashtable emailLookup = new HashtableO;
// Метод Add принимает в качестве первого параметра ключ,
// а в качестве второго - значение
emailLookup.Add("sbishop@contoso.com”, "Bishop, Scott”);
Ц Использование индексатора эквивалентно вызову Add
emailLookup["sbishop@contoso.com"] = "Bishop, Scott";
В отличие от рассмотренных выше типов наборов, элементы словаря всегда состоят из двух частей: ключ (имя) и значение. В приведенном выше примере показаны два способа добавления элементов в набор. В первом варианте вызывается метод Add, добавляющий элемент, заданный в виде пары «ключ — значением. К тому же элемент можно добавить при помощи индексатора, указав ключ в качестве индекса и присвоив индексатору нужное значение ключа.
Получение объектов из словаря также не представляет сложности. Чтобы получить доступ к данным, уже добавленным в словарь {Hashtable), достаточно вызвать индексатор с требуемым ключом:
' VB
Console WriteLine(emailLookup("sbishop@contoso.com"))
И C#
Console.WriteLine(emailLookup["sbishop@contoso.com"]);
Поскольку словари предназначены для поиска значений по ключу, не удивительно, что перебор элементов словаря в цикле сложнее, чем элементов обычного набора. Предположим, что объект Hashtable создан, и требуется перебрать его значения в цикле. Код для решения этой задачи выглядит приблизительно так:
' VB
Dim emailLookup As New HashtableO
emailLookup("sbishop@contoso.com”) = "Bishop, Scott"
emailLookup("chess@contoso.com") = "Hess, Christian"
Занятие 3
Работа со словарями	-| 75
emailLookup("djump@contoso.com") = "Jump, Dan"
For Each name as Object In emailLookup
Console.WriteLine(name)
Next name
11 C#
Hashtable emailLookup = new HashtableO:
emailLookup["sbishop@contoso.com"] = "Bishop, Scott"; emailLookup["chess@contoso.com"] = "Hess, Christian";
emailLookup["djump@contoso.com"] = "Jump, Dan";
foreach (object name in emailLookup)
{
Console.WriteLine(name);
Казалось бы, этот код отобразит имена всех субъектов, информация о которых хранится в переменной emailLookup. Но на самом деле на консоль выводится следующее:
System.Collections.DictionaryEntту
System.Collections.DictionaryEntry
System.Collections.DictionaryEntry
Почему так происходит? Потому, что на самом деле в цикле перебираются не ключи или значения, а элементы, «Словарные статьи», хранимые в объекте Dictionary. Если требуется вывести имена всех пользователей, следует указать итератору, что он работает с объектами типа Dictionary Entry’.
' VB
For Each entry as DictionaryEntry In emailLookup
Console.WriteLine(entry.Value)
Next entry
// C#
foreach (DietIonaryEntry entry in emailLookup)
Console.WriteLine(entry.Value);
}
Объект DictionaryEntry — это просто контейнер, содержащий ключи (Key) и значения (Value). Поэтому значения из него получают так же, перебирая в цикле элементы, но обращаясь к свойству Value или Key, в зависимости от задачи.
Все классы словарей (включая Hashtable) поддерживают интерфейс IDictionary. Интерфейс IDictionary — потомок интерфейса ICollection. В табл. 4-13 и 4-14, соответственно, приведены наиболее важные свойства и методы интерфейса IDictionary.
j 75	Наборы и обобщения
Глава 4
Табл. 4-13.	Свойства IDictionary
Имя	Описание
IsFixedSize	Позволяет узнать, можно ли изменять размер набора
IsReadOnly	Позволяет узнать, можно ли изменять набор
Item	Возвращает или устанавливает элемент набора по заданному ключу
Keys	Возвращает объект ICollection со списком ключей набора
Values	Возвращает объект ICollection со списком значений набора
Табл. 4-14.	Методы IDictionary
Имя	Описание
Add	Добавляет в набор пары «ключ-значение»
Clear	Удаляет из набора все элементы
Contains	Проверяет, присутствует ли указанный элемент в наборе
GetEnumerator	Возвращает перечислитель для набора — объект IDictionaryEnumerator. (отличается от метода интерфейса lEnumerable, возвращающего интерфейс lEnumerator)
Remove	Удаляет из набора элемент с заданным ключом
Интерфейс IDictionary похож на IList (см. занятие 1), но он предоставляет доступ к элементам не по индексу, а по ключу. Этот интерфейс предоставляет прямой доступ к спискам ключей и значений как к наборам объектов. Такой подход удобен, когда требуется перебрать в цикле эти списки по отдельности.
Выше в этом разделе показан код, перебирающий имена из списка электронных адресов перебирались в цикле при помощи объекта Dictionary Entry, полученного от итератора. Также можно перебрать в цикле эти значения как элементы свойства Values, что и показано в следующем примере:
' VB
For Each name as Object In emailLookup.Values
Console WriteLine(name)
Next name
//C#
foreach (object name in emailLookup.Values)
Console.WriteLine(name);
}
Помимо интерфейса IDictionary, класс Hashtable поддерживает два метода проверки существования ключей и значений. Эти методы приведены в табл. 4-15
Табл. 4-15. Методы Hashtable
Имя	Описание
ContainsKey	Определяет,	содержит	ли набор указанный ключ
ContainsValue	Определяет,	содержит	ли набор указанное значение
' Занятие 3
Работа со словарями j 77
Концепция равенства
Класс Hashtable — это особый класса словаря, использующий при хранении ключей целое значение (так называемый хэш-код). Хэш-код служит классу Hashtable для ускорения поиска заданного ключа в наборе. Каждый объект в .NET является потомком класса Object. Этот класс поддерживает метод GetHash, возвращающий уникальное целочисленное значение, идентифицирующее объект.
В чем польза разработчику от того, что класс Hashtable хранит хэш-коды? Дело в том, что класс Hashtable требует уникальности хэш-кодов, а не связанных с ними значений. Если же попытаться сохранить разные значения с одним и тем же ключом, то второе значение заменит первое:
’ VB
Dim duplicates As New HashtableO
duplicates("First") = "1st"
duplicates("First") = "the first"
Console.WriteLine(duplicates.Count) ’ 1
// C#
Hashtable duplicates = new HashtableO;
duplicates["First"] = "1st";
duplicates["First"] = "the first";
Console.WriteLine(duplicates.Count); // 1
В наборе duplicates, рассмотренном в этом примере, хранится только один элемент, поскольку хэш-коды “First” и “First” совпадают. Чтобы достичь этого, в классе String переопределяется метод GetHashCode класса Object. Предполагается, что строки с одинаковым текстом идентичны, даже если это разные экземпляры. Так класс Hashtable использует для проверки равенства хэш-коды объектов. В большинстве случаев данный подход устраивает разработчика.
Тем не менее, в .NET Framework смысл понятия «равенство» порой отличается от общепринятого. Представьте, к примеру, что вы создали простой класс с именем Fish, в котором хранятся названия рыб:
' VB
Public Class Fish
Private name As String
Public Sub New(theName As String)
name = theName
End Sub
End Class
// C#
| уд Наборы и обобщения
Глава 4
public class Fish { t*	•
string name;
public Fish(string theName)
{ « name = theName;
}
Если создать два экземпляра класса Fish с одинаковыми значениями, Hashtable будет считать их различными объектами, что видно из следующего фрагмента кода:
• VB
Dim duplicates As New HashtableO
Dim key1 As New Fish("Herring")
Dim key2 As New Fish("Herring")
duplicates(keyl) = "Hello"
duplicates(key2) = "Hello"
Console.WriteLine(duplicates.Count)
// C#
Hashtable duplicates = new HashtableO;
Fish key1 = new Fish("Herring");
Fish key2 = new Fish("Herring");
duplicates[key1] = "Hello";
duplicates[key2] = "Hello";
Console.WriteLine(duplicates.Count); fl 2
Почему в наборе оказалось два элемента, хотя значения у них одни и те же? В приведенном выше примере набор duplicates хранит два элемента, поскольку реализация метода GetHashCode класса Object создает уникальный хэш-код для каждого экземпляра класса. Можно попробовать переопределить метод GetHashCode в классе Fish так, чтобы класс Hashtable признавал экземпляры с одинаковыми значениями идентичными:
1 VB
Public Overrides Function GetHashCodeO As Integer
Return name.GetHashCodeO
End Function
II C#
public override int GetHashCodeO {
return name GetHashCodeO;
}
Занятие 3
Работа со словарями j уд
Если генерировать хэш-код по названию рыб, то у двух экземпляров класса, содержащих одно и то же название, будет одинаковый хэш-код. Достаточно ли этого, чтобы Hashtable признал эти экземпляры одним объектом? К сожалению, нет. Обнаружив два объекта с одинаковым хэш-кодом, Hashtable вызывает их метод Equals, чтобы убедиться, что эти объекты действительно идентичны. И в этом случае стандартная реализация метода Object.Equals возвращает false д ля разных экземпляров одного класса. Поэтому в классе Fish требуется переопределить также и метод Equals'.
" VB
Public Overrides Function Equals(ByVal obj As Object) As Boolean
Dim otherFish As Fish = obj as Fish
If otherFish Is Nothing Then
Return False
End If
Return otherFish.name = name
End Function
// C#
public override bool Equals(object obj)
{
Fish otherFish = obj as Fish;
if (otherFish == null) return false;
return otherFish.name == name;
}
Сначала мы проверяем, имеет ли другой объект тип Fish, и, если это так, сравниваем названия рыб, чтобы окончательно убедиться в идентичности объектов. Только после этого класс Hashtable признает эти два ключа одинаковыми.
Применение интерфейса lEqualityComparer
Да, можно добиться сравнения экземпляров класса по нужным нам правилам, модифицируя сами классы, но иногда удобно проверять равенство объектов средствами, внешними по отношению к классу. Представьте, к примеру, что вы предполагаете хранить в Hashtable строковые ключи, не учитывая их регистр. «Отбить» чувствительность к регистру у существующего класса String либо создать аналогичный собственный класс сложно. В этом случае имеет смысл воспользоваться средствами сравнения классов, предоставляемыми Hashtable.
Класс Hashtable поддерживает конструктор, принимающий в качестве параметра экземпляр класса lEqualityComparer. lEqualityComparer похож на класс IComparer, примеяе-мый для сортировки наборов (см. занятие 1) и поддерживает два метода: GetHashCode и Equals. Эти методы позоляют переложить проверку равенства объектов на класс сравнения, освободив от нее сами объекты. Например, следующий код создает простой объект сравнения, игнорирующий регистр строковых ключей:
• VB
Public Class InsensitiveComparer
Implements lEqualityComparer
Dim _comparer As CaselnsensitiveComparer = _
New CaseInsensitiveComparer()
180 Наборы и обобщения
Глава 4
Public Function GetHashCode(ByVal obj As Object) As Integer
Implements lEqualityComparer
Return obj.ToString().ToLowerInvariant().GetHashCode()
End Function
Public Shadows Function Equals(ByVal x As Object, ByVai у As Object)
As Boolean Implements lEqualityComparer
If -Comparer.Compare(x,у) = 0 Then
Return True
Else
Return False
End If
End Function
End Class
// C#
public class InsensitiveComparer : lEqualityComparer {
CaselnsensitiveComparer -Comparer = new CaselnsensitiveComparerO;
public int GetHashCode(object obj)
{
return obj .ToStringO.ToLowerInvariant().GetHashCode();
}
public new bool Equals(object x, object y)
{
if (-Comparer.Compare(x, y) == 0)
{
return true;
}
else 4
{
return false;
}
}
}
Показанный выше новый класс реализует интерфейс lEqualityComparer, получая в результате требуемые хэш-коды и поддержку сравнения. Заметьте, что этот класс использует встроенный класс CaselnsensitiveComparer для вызова метода Equals, выполняющего сравнение. К тому же метод GetHashCode преобразует переданный в качестве параметра объект в нижний регистр перед получением его хэш-кода. В этом случае созданный хэш-код оказывается нечувствительным к регистру. Теперь достаточно указать этот класс в качестве класса сравнения при создании Hashtable'.
' VB
Dim dehash As Hashtable = New Hashtable(New InsensitiveComparerO)
Занятие 3
Работа со словарями ] g-|
dehash("First") = "1st"
defiash ("Second") = "2nd"
dehash("Third") = "3rd"
dehash("Fourth") = "4th" dehash("fourth") = "4th"
Console.WriteLine(dehash.Count) ' 4
11 C#
Hashtable dehash = new Hashtable(new InsensitiveComparerO);
dehash["First"] = "1st";
dehash["Second"] = "2nd”;
dehash[”Third"] = "3rd";
dehash["Fourth"] = "4th";
dehash["fourth"] = "4th";
Console.WriteLine(dehash.Count); // 4
Поскольку при создании Hashtable был передан нечувствительный к регистру объект сравнения, в наборе окажется только четыре элемента. Объекты “Fourth”и “fourth”считаются одинаковыми.
Класс Hashtable предоставляет прекрасные возможности для создания поисковых таблиц, но иногда также требуется упорядочивать наборы элементов по значениям ключа. При переборе элементов класса Hashtable в цикле элементы возвращаются в порядке хранения их хэш-кодов. В большинстве случаев это неудобно. Класс SortedList — это класс словаря, поддерживающий сортировку.
Применение класса SortedList
Хотя класс SortedList по определению относится к объектам-словарям, в некоторых случаях он ведет себя аналогично простым спискам. Это означает, что вы можете (и, скорее всего, будете) получать элементы, упорядоченно хранящиеся в наборе SortedList. Ниже показан пример использования SortedList для упорядочения простого списка элементов:
’ VB
Dim sort As SortedList = New SortedListO
sort("First") = "1st"
sortC'Second") = "2nd"
sort("Third") = "3rd"
sort("Fourth") = "4th" sort("fourth") = "4th"
For Each entry as DictionaryEntry In sort
Console.WriteLine("{0} = {1}", entry.Key, entry.Value) Next
II C#
SortedList sort = new SortedListO;
182	Наборы и обобщения
Глава 4
sort["First"] = "1st";
sortf"Second"] = "2nd"; sort["Third"] = "3rd”;
sort["Fourth"] = "4th"; sort["fourth”] = "4th”;
foreach (DictionaryEntry entry in sort)
{
Console.WriteLine("{0} = {1}", entry.Key, entry.Value);
}
Этот код сортирует вывод следующим образом:
First = 1st
fourth = 4th
Fourth = 4th
Second = 2nd
Third = 3rd
Из синтаксиса итератора foreach в предыдущем фрагменте кода видно, что SortedList является классом словаря (о чем свидетельствует тип DictionaryEntry). Помимо интерфейса, который реализуют все классы словарей, класс SortedList поддерживает дополнительные свойства для доступа к ключам и значениям по числовому индексу. В табл. 4-16 и 4-17, соответственно, приведены свойства и методы класса SortedList (сюда не вошли члены интерфейса IDictionary).
Табл. 4-16. Свойства SortedList
Имя	Описание
Capacity	Возвращает или устанавливает возможное число элементов в наборе. Это общее число отведенных под элементы ячеек, а не текущее число элементов набора (число элементов набора возвращает свойство Count)
Табл. 4-17.	Методы SortedList
Имя	Описание
ContainsKey	Определяет, содержит ли набор указанный ключ
ContainsValue	Определяет, содержит ли набор указанное значение
GetBylndex	Возвращает значение по указанному индексу в наборе
GetKey	Возвращает ключ по указанному индексу в наборе
GetKeyList	Возвращает упорядоченный список ключей
GetValueList	Возвращает список значений
IndexOfKey	Возвращает индекс ключа в наборе
IndexOfValue	Возвращает индекс первого вхождения в набор заданного значения : 3
RemoveAt	Удаляет из набора элемент с заданным индексом
SetBylndex	Заменяет элемент с заданным индексом в наборе
TrimToSize	Используется для освобождения неиспользуемых ячеек набора
Занятие 3
Работа со словарями •( 33
Как видно из этих таблиц, в класс SortedList добавлено несколько методов для доступа к данным по числовому индексу. Этот класс поддерживает выборку по индексу как ключей, так и значений, также он поддерживает поиск индекса по ключу или значению. Поскольку содержимое этого класса сортируется, при добавлении или удалении элементов индекс элемента может измениться.
Чтобы упорядочить набор, представленный SortedList, можно применить процедуру, рассмотренную на занятии 1. Вызов метода Sort не требуется, SortedList сам упорядочит элементы в момент добавления. Учитывая это, вы можете задавать IComparer при создании объекта SortedList, чтобы контролировать способ сортировки. Если позаимствовать готовый класс Descending£omparer (см. занятие 1), достаточно модифицировать код следующим образом:
' VB
Dim sort As SortedList = New SortedList(New DescendingComparerO) sort("First ) = "1st" sort("Second”) = "2nd" sort("Third”) = "3rd” sort("Fourth") = "4th"
Dim enTry
For Each entry As DictionaryEnTry In sort
Console WriteLine("{0} = {1}", entry Key, entry.Value) Next
// C#
SortedList sort = new SortedList(new DescendingComparerO);
sort["First"] = "1st";
sort["Second"] = "2nd";
sort["Third"] = "3rd";
sortf'Fourth”] = ’4th";
foreach (DictionaryEntry entry in sort)
{
Console.WriteLine(”{0} = {1}", entry.Key, entry.Value);
Теперь элементы упорядочены по убыванию (алфавитная сортировка выполняется не по номеру, а по названию):
Third = 3rd
Second = 2nd
Fourth = 4th
First = 1st
Специализированные словари
Для решении некоторых задач возможностей стандартных словарей {SortedList и Hashtable) недостаточно: функционально или по производительности. Эту нишу в .NET Framework заполняют еще три класса-словаря: ListDictionary, HybridDictionary и OrderedDictionary.
184	Наборы и обобщения
Глава 4
ListDictionary
В большинстве случаев подойдет класс Hashtable. Единственный недостаток Hashtable — высокое потребление ресурсов, при использовании маленьких наборов (меньше десяти элементов) это снижает производительность. В этом случае испоьзуйте класс ListDictionary. Он оптимален для небольших наборов, поскольку устроен как простой массив. Класс ListDictionary имеет тот же интерфейс, что и Hashtable, и потому легко заменяет последний. Ниже показан пример, уже приводившийся для иллюстрации работы с Hashtable. Однако на этот раз мы используем класс ListDictionary. Заметьте, что изменился только вызов конструктора:
*
 VB
Dim emailLookup As New ListDictionaryO
emailLookup("sbishop@contoso com") = "Bishop, Scott" emailLookup("chess@contoso.com") = "Hess, Christian" emailLookup("djump@contoso.com") = "Jump, Dan"
For Each entry as DictionaryEntry In emailLookup
Console.WriteLine(entry.Value)
Next name
I/ C#
ListDictionary emailLookup = new ListDictionary ();
emailLookupt"sbishop@contoso.com"] = "Bishop, Scott"; emailLookupt"chess@contoso.com"] = "Hess, Christian"; emailLookup["djump@contoso.com"] = "Jump, Dan";
foreach (DictionaryEntry entry in emailLookup)
{
Console.WriteLine(ent ry.Value);
}
HybridDictionary
Из обсуждения класса ListDictionary ясно, что для маленьких наборов Hashtable не вполне подходит. Однако класс ListDictionary совсем не подходит для обработки больших списков. Вообще говоря, для маленьких наборов следует использовать ListDictionary, а для больших — Hashtable. Но что делать, если нельзя оценить размер набора заранее? В этих случаях применяется класс HybridDictionary. По умолчанию он реализован как ListDictionary, а когда список становится слишком большим, превращается в Hashtable. Класс HybridDictionary — лучший выбор для списков, размер которым сильно меняется.
Как и у класса ListDictionary, интерфейс HybridDictionary совпадает с интерфейсом Hashtable, поэтому этим классом также легко заменить Hashtable’.
' VB	я
Dim emailLookup As New HybridDictionaryO
Занятие 3
Работа со словарями j 35
emailLookup("sbishop@contoso.com") = "Bishop, Scott" emailLookup("chess@contoso.com") = "Hess, Christian" emailLookup("djump@contoso.com") = "Jump, Dan"
For Each entry as DictionaryEntry In emailLookup
Console.WriteLine(entry.Value)
Next name
// C#
HybridDictionary emailLookup = new HybridDictionary ();
emailLookupt"sbishop@contoso.com"] = "Bishop, Scott";
emailLookupt"chess@contoso.com"] = "Hess, Christian"; emailLookupfdjump@contoso.com"] = "Jump, Dan";
foreach (DictionaryEntry entry in emailLookup)
{
Console.WriteLine(entry.Value);
}
Ordered Dictionary
Иногда требуется функциональность Hashtable вместе с поддержкой сортировки элементов набора. При добавлении элементов в Hashtable действуют следующие ограничения: нельзя выбирать элементы по индексу, и при использовании перечислителя для обхода этого ограничения элементы будут упорядочены по хэш-кодам. Вы можете воспользоваться классом SortedList, но только если ключи отсортированы как требуется (а порядок сортировки обычно один).
Для случаев, когда требуется словарь с быстрым доступом и сортировкой, .NET Framework предоставляет OrderedDictionary. Класс OrderedDictionary очень похож на Hashtable, но имеет дополнительные свойства и методы (см. табл. 4-18 и 4-19), соответственно, для доступа к элементам по индексу.
Табл. 4-18. Дополнительные свойства OrderedDictionary
Имя	Описание
Item	Перегруженное свойство для поддержки доступа по индексу
Табл. 4-19.	Дополнительные методы OrderedDictionary
Имя	Описание
Insert	Вставляет пару «ключ-значение» в набор по указанному индексу
RemoveAt	Удаляет из набора пару «ключ-значение» с указанным индексом
Благодаря этим дополнениям, набор можно обрабатывать как гибрид ArrayList и Hashtable.
186	Наборы и обобщения
Глава 4
Практикум. Создание таблицы поиска
На этом практикуме вы создадите таблицу поиска, позволяющую заменять числа их строковыми представлениями. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение 1. Создание таблицы поиска
В этом упражнении вы создадите таблицу поиска с цифрами, разберете строку на цифры и выведете результат на консоль.
1.	Создайте консольное приложение с именем Dictionarycollection.
2.	В основной файл с кодом импортируйте пространство имен System.Collections.
3.	В методе проекта Main создайте новый экземпляр класса Hashtable.
4.	Добавьте в новый экземпляр класса Hashtable элементы, в которых ключи — это строки, содержащие числа от нуля до девяти, а значения — те же числа, но записанные прописью.
5.	Затем создайте строковую переменную с серией цифр.
6.	При помощи оператора foreach переберите символы в строке.
7.	Внутри цикла foreach сгенерируйте строку по символьной переменной.
8.	Проверьте, содержит ли набор Hashtable ключ, совпадающий со сгенерированной строкой длиной в один символ.
9.	Если это так, получите из набора Hashtable значение по этому ключу и выведите его на консоль. Полученный код выглядит примерно так:
’ VB
Imports System.Collections
Class Program
Shared Sub Main(ByVal args() As String) Dim lookup As Hashtable = New HashtableO
lookupC'O") = "Zero” lookup("1") = "One" lookup("2") = "Two" lookup("3") = "Three" lookup("4”) = "Four" lookup("5") = "Five" lookup("6") = "Six" lookup("7") = "Seven" lookup("8") = "Eight" lookup("9") = "Nine"
Dim ourNumber As String = "888-555-1212"
For Each c as Char In ourNumber
Dim digit As String = c.ToStringO If lookup.ContainsKey(digit) Then
Console.WriteLine(lookup(digit))
Занятие 3
Работа со словарями -| gy
*	End If
Next
End Sub
End Class
// C#
using System.Collections;
class Program
{
static void Main(string[] args)
{
Hashtable lookup = new HashtableO;
lookup["0"] = "Zero”;
lookup["1"] = "One";
lookup["2"] = "Two";
lookup["3”] = "Three";
lookup[”4"] = "FOur”;
lookup["5"] = Five";
lookup["6"] = "Six";
lookup["7"] = "Seven";
lookup["8"] = "Eight";
lookup["9”] = "Nine";
string ourNumber = "888-555-1212";
foreach (char c in ourNumber)
string digit = c.ToStringO;
if (lookup.ContainsKey(digit))
{
Console.WriteLine(lookup[digit]);
}
}
10.	Соберите проект, исправьте ошибки. Убедитесь, что консольное приложение правильно выводит цифры прописью.
Резюме
	Интерфейс IDictionary поставляет базовые правила вызова для всех наборов Dictionary.
	Класс Hashtable применяется для создания таблиц поиска.
	При помощи объекта DictionaryEntry можно получить ключ и значение объекта в наборе Dictionary.
188 Наборы и обобщения
Глава 4
	Класс SortedList позволяет создавать списки элементов, которые можно упорядочить по ключу.
	lEqualityComparer генерирует хэш-коды и позволяет сравнивать значения классов по произвольным правилам.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Какие методы позволяют определить, уникален ли ключ при добавлении элемента в набор Hashtable? (Укажите все верные ответы.)
A. GetType’,
В. GetHashCode,
С. ToString",
D. Equals.
2.	Какое из следующих утверждений истинно?
А.	при создании объекта Hashtable можно передать конструктору в качестве параметра экземпляр класса, поддерживающего интерфейс LEqualityComparer, чтобы изменить алгоритм сравнения ключей;
В.	можно назначить объект LEqualityComparer существующему экземпляру Hashtable’, С. объект LEqualityComparer нельзя использовать с Hashtable’, D. класс Hashtable реализует LEqualityComparer.
Занятие 4. Применение специализированных наборов
Выше мы рассмотрели классы наборов, которые можно использовать для хранения любых объектов .NET. Эти наборы очень удобны, но при получении элементов из них часто приходится выполнять приведение типов. В .NET Framework появилось новое пространство имен System.Collections.Specialized, которое включает специализированные наборы, хранящие элементы с определенным типом данных.
Изучив материал этого занятия, вы сможете:
J применять классы BitArray и BitVector32 для работы с наборами значений типа Boolean’,
'А применять классы StringCollection и StringDictionary для хранения наборов строк;
S использовать NameValueCollection, поддерживающий контроль типов при хранении пар «имя-значение».
Продолжительность занятия — 30 минут.
Занятие 4
Применение специализированных наборов	j gg
Работа с двоичными значениями (битами)
Программисты часто сталкиваются с обработкой булевых значений, чаще всего — списков двоичных значений, равных 1 либо 0. .NET Framework поддерживает два класса, упрощающих работу с наборами двоичных значений: BitArray и BitVector32.
Класс BitArray представляет набор для хранения значений типа Boolean. Этот класс поддерживает изменение размера и общие поразрядные операции, такие как «И» (And), «НЕ» (Not), «ИЛИ» (Or) и «исключающее ИЛИ» (Хог).
Структура BitVector32 несколько отличается от BitArray и предназначена для работы с битами 32-разрядных целочисленных значений. Размер набора BitVector32 фиксирован и всегда равен 32 битам, поскольку этот набор предназначен для обработки битов только 32-разрядных целых чисел.
Применение BitArray
Класс BitArray поддерживает нединамическое изменение размера. Размер набора необходимо указывать при создании экземпляра класса BitArray. После этого для изменения размера можно воспользоваться свойством Length. В отличие от других наборов, BitArray не поддерживает методы Add и Remove. Это связано с тем, что элементы набора BitArray принимают одно из двух значений: true или false, поэтому сама концепция добавления или удаления элементов к такому набору неприменима.
Как только экземпляр класса BitArray создан, он будет содержать набор значений типа Boolean, равных по умолчанию false. Чтобы задать значения для отдельных битов, можно использовать индексатор, как показано здесь:
' VB
Dim bits As BitArray = New BitArray(3)
bits(O) = False
bits(1) = True
bits(2) = False
// C#
BitArray bits = new BitArray(3);
bits[O] = false;
bits[1] = true;
bits[2] = false;
Истинная ценность класса BitArray заключается в том, что он позволяет выполнять булевы операции над двумя объектами BitArray (естественно, одинакового размера). Чтобы создать два объекта BitArray и произвести над ними операцию «исключающее ИЛИ», выполните следующее:
1.	Создайте экземпляр класса BitArray, указав требуемый размер.
2.	Задайте значения отдельных битов.
3.	Создайте второй экземпляр класса BitArray того же размера, что и первый, повторив шаги 1 и 2.
4.	Вызовите метод Хог первого объекта BitArray, указав в качестве параметра второй объект BitArray. Будет получен новый объект BitArray, хранящий результат операции Хог. Полученный код выглядит примерно так:
' VB
Dim bits As BitArray = New BitArray(3)
bits(O) = False
190 Наборы и обобщения
Глава 4
bits(1) = True
bits(2) = False
Dim moreBits As BitArray = New BitArray(3)
bits(O) = True
bits(1) = True
bits(2) = False
Dim xorBits As BitArray = bits.Xor(moreBits)
For Each bit as Boolean In xorBits
Console. Writel_ine( bit)
Next
// C#
BitArray bits = new BitArray(3);
bits[O] = false;
bits[l] = true;
bits[2] = false;
BitArray moreBits = new BitArray(3);
bits[O] = true;
bits[1] = true;
bits[2] = false;
BitArray xorBits = bits.Xor(moreBits);
foreach (bool bit in xorBits) {
Console.WriteLine(bit);
}
Применение BitVector32 для работы с битовыми масками
Структура BitVector32 очень удобна для управления отдельными битами больших чисел. BitVector32 хранит свои данные как 32-разрядное целое число. Все операции над объектами BitVector32 в действительности изменяют значение целого внутри структуры. Получить значение соответствующего целого числа можно в любой момент, вызвав свойство Data.
Структура BitVector32 позволяет последовательно создавать битовые маски, вызывая статический [в Visual Basic — общий (shared)] метод CreateMask. Вызов метода CreateMask без параметров создает маску для первого бита структуры. Последующий вызов, которому в качестве параметра передается маска, полученная на предыдущем шаге, создает маску для следующего бита, и тд. Эти маски можно использовать вместе с индексатором структуры BitVector32 для установки или получения значения отдельного бита, соответствующего маске.
Поясним вышеизложенное на примере. Предположим, что в 32-разрядном целом числе требуется установить значения первых четырех битов. Чтобы создать битовые маски, установить и получить значения для выбранных битов, выполните следующее: 1. Создайте экземпляр структуры BitVector32 и задайте в качестве начального значения нуль, чтобы сбросить все биты.
Занятие 4
Применение специализированных наборов 191
2.	Создайте маску для первого бита, вызвав метод BitVector32.CreateMask без параметров.
3.	Создайте маску для следующего бита, вызвав метод BitVector32.CreateMask с маской, полученной на предыдущем шаге, в качестве параметра.
4.	Повторяйте предыдущий шаг, пока не получите все нужные битовые маски. Готовый код выглядит примерно так:
* VB
Dim vector As BitVector32 = New BitVector32(0)
Dim firstBit As Integer = BitVector32.CreateMask()
Dim secondBit As Integer = BitVector32.CreateMask(firstBit)
Dim thirdBit As Integer = BitVector32.CreateMask(secondBit)
// C#
BitVector32 vector = new BitVector32(0);
int firstBit = BitVector32.CreateMask();
int secondBit = BitVector32.CreateMask(firstBit);
int thirdBit = BitVector32 CreateMask(secondBit);
5.	Теперь при помощи индексатора установите значения первого и второго бита равными true, как показано ниже:
' VB
Vector(firstBit) = True
vector(secondBit) = True
// C#
vector[firstBit] = true;
vector[secondBit] = true;
6.	Выведите значение свойства Data класса BitVector32 на консоль, чтобы убедиться, что оно равно 3: 1 (первый бит) + 2 (второй бит).
’ VB
Console.WriteLine(”{0} should be 3", vector.Data)
// C#
Console.WnteLine("{0} should be 3", vector Data);
7.	Если вывести на консоль структуру целиком (а не только свойство Data), то будут видны биты со значением true:
' VB
Console.WriteLine(vector)
’ BitVecto r32{00000000000000000000000000000011}
// C#
Console.WriteLine(vector);
. M // BitVector32{00000000000000000000000000000011}
8.	Затем создайте новый объект BitVector32 и установите его начальное значение рав-R г ным 4 (бит 3 равен 1, биты 1 и 2 — 0).
192 Наборы и обобщения
Глава 4
9.	Далее при помощи индексатора BitVector32 получите каждый из первых трех битов как значения типа Boolean. Можно воспользоваться масками, созданными для первых трех битов: они не привязаны к какому-либо экземпляру BitVector32, поскольку эта структура всегда хранит 32 бита. Получится примерно такой код:
' VB
Dim NewVector As BitVector32 = New BitVector32(4)
Dim bit1 As Boolean = NewVector(firstBit)
Dim bit2 As Boolean = NewVector(secondBit)
Dim bit3 As Boolean = NewVector(thirdBit)
' bit1 = false, bit2 = false, bit3 = true
11 C#
BitVector32 newVector = new BitVector32(4);
bool bit1 = newVector[firstBit];
bool bit2 = newVector[secondBit];
bool bit3 = newVector[thirdBit];
// bit1 = false, bit2 = false, bit3 = true
Введение в двоичные вычисления
Структура BitVector32 призвана упростить применение двоичной алгебры при работе р отдельными битами больших чисел. Тем не менее, полностью понять’работу структуры BitVector32 можно, только усвоив принципы двоичных вычислений.
Каждая область памяти компьютера, хранящая фрагмент данных, похожа на набор переключателей, каждый из которых либо включен, либо выключен. Чтобы сохранить двоичное число с помощью такого набора переключателей, следует выделить переключатели, по числу разрядов сохраняемого числа. Можно сказать, что эти гипотетические «переключатели» представляют собой биты. Например, байт без знака — это 8-разрядное число, он может хранить числа от нуля до 255. Так получается, поскольку значение каждого разряда числа равно 2 в степени, соответствующей позиции разряда (они нумеруются справа налево, начиная с нуля). Таким образом, если включен первый бит, это 2° или 1. Второй дает 2* или 2 и т.д. Ситуацию, когда все 8 битов в байте без знака «включены» (равны 1), можно представить как 1+2 + 4 + 8 + 16 + 32 + 64 + 128 = 255. К примеру, чтобы сохранить десятичное число 5, следует «включить» биты номер 0 и 2 (00000101): (2°) + (22) = 1 + 4 = 5.
Восьмиразрядные двоичные числа невелики и не создают проблем. Но чем больше разрядов, тем труднее интерпретировать число — этим и объясняется потребность в структуре BitVector32. Например, последний бит в 32-разрядном целом числе без знака соответствует довольно большому числу (2 147 483 648). Более того, BitVector32 на самом деле манипулирует целым без знака, а последний бит соответствует —(2Л31), представляя диапазон отрицательных 32-разряд-ных целых чисел со знаком. Таким образом, класс BitVector32 «маскирует» числа, представляя их поразрядно, в виде проиндексированных битов.
Занятие 4
Применение специализированных наборов [ (
Применение BitVector32 для упаковки битов
Помимо того, что структура BitVector32 очень удобна для работы с отдельными битами, она также под держивает упаковку битов. В данном контексте «упаковка» — сборка нескольких маленьких чисел в одно большое. Упаковка часто позволяет сэкономить память, необходимую для хранения маленьких чисел.
Предположим, что есть три маленьких числа. Максимальное значение первого числа равно 10, второго — 50 и третьего — 500. Можно хранить три числа в переменных типа Int 16, но при этом часть памяти будет потрачена впустую. Вместо этого можно при помощи BitVector32 поместить все три значения в одно 32-разрядное число.
В структуре BitVector32 можно создавать разделы для хранения чисел определенных размеров. Создать разделы следует перед упаковкой. Эта операция похожа на создание маски, только при создании раздела требуется указать максимальное число, помещающееся в раздел. Поэтому, продолжая наш пример, разделы будут созданы так:
’ VB
Dim firstSection As BitVector32.Section =
BitVector32.CreateSection(10)
Dim secondSection As BitVector32.Section =
BitVector32.CreateSection(50, firstSection)
Dim thirdSection As BitVector32.Section =
BitVector32.CreateSection(500, secondSection)
// C#
BitVector32.Section firstSection =
BitVecto r32.C reateSection(10);
BitVector32.Section secondSection =
BitVecto r32.CreateSection(50, fi rstSection);
BitVector32.Section thirdSection =
BitVecto r32.CreateSection(500, secondSection);
Как и CreateMask, метод CreateSection использует предыдущий раздел, чтобы определить место для упаковки нового числа.
Как только разделы созданы, можно назначать им значения и получать их при помощи индексатора и переменных, как показано ниже:
’ VB
Dim packedBits As BitVector32 = New BitVector32(0)
packedBits(firstSection) =10
packedBits(secondSection) = 1
packedBits(thirdSection) =192
Console.WriteLine(packedBits(firstSection))
Console.WriteLine(packedBits(secondSection))
Console.WriteLine(packedBits(thirdSection))
// C#
BitVector32 packedBits = new BitVector32(0);
i
packedBits[firstSection] = 10;
194	Наборы и обобщения
Глава 4
packedBits[secondSection] = 1;
packedBits[thirdSection] = 192;
Console.W гiteLi ne(packedBitsEfirstSection]);
Console.WriteLine(packedBits[secondSection]);
Console.WriteLine(packedBits[thirdSection]);
После манипуляций над разделами можно получить свойство Data структуры BitVector32, представляющее общее число, в которое упакованы три числа:
’ VB
Console.WriteLine(packedBits.Data) ' 98314
Console.WriteLine(packedBits)
' BitVecto r32{00000000000000011000000000001010}
11 C#
Console.WriteLine(packedBits.Data);
II 98314 '. - -
Console.WriteLine(packedBits);
11 BitVecto r32{00000000000000011000000000001010}
Чтобы получить из 98314 числа 10, 1 и 192, можно выполнить соответствующие расчеты вручную, но BitVector32 упрощает эту процедуру.
Хранение строк в наборах
Вероятно, чаще всего в наборах хранят строки. С этой целью .NET Framework поддерживает два специализированных набора с контролем типов: String? ollection и StringDictionaiy.
Класс StringCollectfon
Набор StringCollection под держивает динамическое изменение размера (как ArrayList), но хранит только строки. Но StringCollection — это просто набор, поэтому работа с ним, в сущности, аналогична работе с набором ArrayList, что видно из приведенного ниже примера:
1 VB
Dim coll As New StringCollectionO
coll.Add("First")
coll.Add("Second")
coll.Add("Third")
coll.Add("Fourth")
coll.Add("fourth")
’ coll.Add(50); <- Ошибка компиляции - это не строка
Dim theString As String = coll(3)
’ Теперь приведение типов не требуется
’ string theString = (string) coll[3];	\
Занятие 4	Применение специализированных наборов ( 95
// C#
Stringcollection coll = new StringCollectionO;
coll.Add("First");
coll.Add("Second");
coll.Add("Third");
coll. Add("Fourth");
coll.Add("fourth");
// coll.Add(50); <- Ошибка компиляции - это не строка
string theString = coll[3J;
// Теперь приведение типов не требуется
// string theString = (string) coll[3];
Код, добавляющий строки в набор, выглядит так же, как и в приведенном выше примере с использованием ArrayList. Единственное отличие в том, что попытка добавления объекта с типом, отличным от строкового, влечет ошибку компиляции (см. комментарии в коде). К тому же теперь, получая строки из набора, вы работаете не с объектами, а именно со строками, что устраняет необходимость приведения типов.
Класс StringDictionary
StringDictionary — это разновидность словаря (см. занятие 3) с контролем типов. Это означает, что он работает как Hashtable, но и ключ, и значение должны быть строками: ' VB
Dim diet As New StringDictionaryO
dict("First") = "1st"
dict("Second") = "2nd”
diet("Third") = "3rd"
dict("Fourth") = "4th"
dict(’fourth") = "fourth"
dict(50) = "fifty"; <- Ошибка компиляции - это не строка
Dim converted As String = dict("Second )
Приведение типов не требуется
// C#
StringDictionary diet = new StringDictionaryO;
dictf'First"] = "1st";
dict["Second"] = "2nd";
dict["Third"] = "3rd";
dict["Fourth"] = "4th";
dict["fourth"] = "fourth";
// dict[50] = "fifty ; <- Ошибка компиляции - это не строка
string converted = diet["Second"];
// Приведение типов не требуется
| gg Наборы и обобщения
Глава 4
Важно понимать, что по умолчанию ключи для объектов StringDictionary нечувствительны к регистру, поэтому ключи “Fourth” и “FOURTH” эквиваленты.
Наборы, нечувствительные к регистру
Как показано ранее при обсуждении наборов, интерфейсы IComparer и lEqualityComparer помогают при сравнении и проверке равенства. Чаще всего эти интерфейсы используются для создания словарей, игнорирующих регистр. Поскольку это задача распространена в программировании, .NET Framework включает класс Collection Util, поддерживающий создание объектов Hashtable и SortedList, нечувствительных к регистру. Для создания этих объектов достаточно вызвать метод CreateCaselnsensitiveHashtable или CreateCaselnsensitiveSortedList.
' VB
Dim inTable As Hashtable =
CollectionsUtil.CreateCaseInsensitiveHashtable()
inTable("hello") = "Hi"
inTable("HELLO") = "Heya"
Console.WriteLine(inTable.Count) ’ 1
Dim inList As SortedList = _
CollectionsUtil.CreateCaselnsensitiveSortedList()
inList("hello") = "Hi"
inList("HELLO") = "Heya"
Console.WriteLine(inList.Count) ' 1
11 C#
Hashtable inTable =
CollectionsUtil.CreateCaselnsensitiveHashtable();
inTable["hello"] = "Hi";
inTablet"HELLO"] = "Heya";
Console.WriteLine(inTable.Count); // 1
SortedList inList =
CollectionsUtil.CreateCaseInsensitiveSortedList();
inListt"hello"] = "Hi";
inListt"HELLO"] = "Heya";
Console.WriteLine(inList.Count): // 1
Наборы, нейтральные по отношению к культуре
По умолчанию наборы используют текущую культуру (здесь и далее «культура» — совокупность региональных параметров) потока. Это означает, что сравнение зависит от правил, принятых в текущей культуре. Объекты сравниваются как для проверки равенства, так и для сортировки элементов.
В зависимости от конкретной задачи, может потребоваться выполнить сравнение способом, не зависящим от культуры. Такая ситуация возникает при работе с We^-прип ложениями и глобализованными приложениями. Как работать со списком, содержащем ключи на английском, испанском, иврите и фарси? Как сортировать его элементы? В большинстве подобных случаев требуется создавать наборы, на которые не влияет те
Занятие 4
Применение специализированных наборов gy
кущая культура. Методы CollectionUtil не помогут создать такие наборы (хотя позволяют создавать наборы, нечувствительные к регистру). Вместо этого следует передать новому набору экземпляр объекта StringComparer, выполняющий сравнение строк с учетом регистра и по правилам сравнения инвариантной культуры (культуры по умолчанию). Также можно создавать наборы Hashtable и SortedList, нечувствительные к регистру и независимые от культуры. Получившийся код может выглядеть примерно так:
' VB
Dim hash As Hashtable = New Hashtable(
StringComparer.Invariantculture)
Dim list As SortedList = New SortedList(
St ringCompa re r.Inva riantCultu re)
// C#
Hashtable hash = new Hashtable(
StringComparer.Invariantculture);
SortedList list = new SortedList(
St ringComparer.InvariantCultu re);
Класс NameValueCollection
В заключение рассмотрим специализированный класс NameValueCollection. На первый взгляд он похож на класс StringDictionary, так как тоже позволяет добавлять элементы из строковых ключей и их значений. Тем не менее, между ними имеются некоторые различия: допускается несколько значений, соответствующих одному ключу, к тому же значения можно получать не только по ключу, но и по индексу.
Таким образом, при работе с классом NameValueCollection можно хранить несколько значений с одним ключом. Метод Add позволяет делать это. Чтобы получить все значения, соответствующие отдельному ключу, достаточно вызвать метод GetValues, как показано ниже:
' VB
Dim nv As NameValueCollection = New NameValueCollection()
nv.Add(”Key", "Some Text")
nv.Add("Key", "More Text")
Dim s As String
For Each s In nv.GetValues("Key")
Console.WriteLine(s)
Next
Текст
Продолжение текста
// C#
NameValueCollection nv = new NameValueCollectionO;
nv.Add("Key", "Some nv Add("Key", "More
Text");
Text’’);
j gg Наборы и обобщения
Глава 4
foreach (string s in nv.GetValues("Key"))
Console.WriteLine(s);
}
// Текст
// Продолжение текста
Для доступа к значениям с одинаковыми ключами, добавленным при помощи метода Add, можно вызвать метод GetValues с ключом в качестве параметра. При выводе значений в цикле будут показаны оба значения, добавленные методом Add.
Интересно, что поведение индексатора отличается от поведения метода Add. К примеру, если добавить два элемента с одним ключом при помощи индексатора, в наборе останется только последний добавленный ключ, как в случае других словарей, рассмотренных на занятии 3. Следующий пример иллюстрирует разницу в работе индексатора и метода Add при добавлении элемента:
’ VB
nv("First") = "1st" nv("First") = "FIRST"
nv.Add(Second", "2nd")
nv.Add("Second", "SECOND")
Console.WriteLine(nv.GetValues("First").Length)
' 1	.
Console WriteLine(nv.GetValues("Second") Length) • 2
// C#
nv["First"] = "1st"; nv["First"] = "FIRST";
nv.Add("Second", "2nd");
nv.Add("Second", "SECOND");
Console.WriteLine(nv.GetValues("Fi rst").Length);
// 1
Console WriteLine(nv GetValues("Second").Length);
11 2
Последнее отличие NameValueCollection от StringDictionary заключается в том, что можно получать элементы по индексу. То есть, если запросить у NameValueCollection значение по индексу, будет возвращено значение, связанное с ключом, соответствующим этому индексу. Если с данным ключом связано более одного значения, будет возвращен список этих значений, разделенных запятыми:
' VB
Dim nv As NameValueCollection = New NameValueCollectionO
Занятие 4
Применение специализированных наборов
nv.Add("First", "1st") nv.Add("Second", "2nd") nv.Add("Second", "Not First")
Dim x As Integer
For x = 0 To nv.Count- 1 Step + 1 Console.WriteLine(nv(x))
Next ’ 1-й ’ 2-й,не 1-й
// C#
NameValueCollection nv = new NameValueCollection();
nv.Add("First", "1st");
nv.Add("Second", "2nd”); nv.Add("Second", "Not First");
for (int x = 0; x < nv.Count; ++x) {
Console.WriteLine(nv[x]); } // 1-й // 2-й.не 1-й
Практикум. Нечувствительная к регистру таблица поиска, поддерживающая локализацию
На этом практикуме вы создадите таблицу поиска, поддерживающую локализацию. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение 1. Создание объекта-словаря
В этом упражнении вы создадите таблицу поиска с названиями стран. Ключи будут заданы на испанском языке.
1.	Создайте консольное приложение с именем Lookupcollection.
2.	В основной файл с кодом импортируйте пространства имен System.Collections, System.Collections.Specialized и System.Globalization.
3.	В методе проекта Main создайте новый экземпляр класса ListDictionary, нечувствительный к регистру и независимый от культуры.
4.	Добавьте в набор три пары для поиска, задав ключ «Estados Unidos» для «United States of America», ключ «Canada» для «Canada» и «Espana* для «Spain».
5.	Выведите на консоль значения для ключей на испанском языке, обозначающих Испанию и Канаду. Полученный код выглядит примерно так:
VB
Imports System.Globalization
2Q0 Наборы и обобщения
Глава 4
Imports System.Collections
Imports System.Collections.Specialized
Class Program
Shared Sub Main(ByVal args() As String)
' Создать словарь, нечувствительный к регистру
Dim list as New ListDictionary( _
New CaseInsensitiveComparer(CultureInfo.Invariantculture))
’ Добавить несколько элементов
list("Estados Unidos") = "United States of America"
list("Canada") = "Canada"
list("Espana") = "Spain"
' Показать результаты
Console. Writel_ine(list( "espana"))
Console.WriteLine(list("CANADA"))
Console.Read()
End Sub
End Class
11 C#
using System.Globalization;
using System.Collections;
using System.Collections.Specialized;
class Program
{
static void Main(string[] args) {
// Создать Словарь, нечувствительный к регистру ListDictionary list = new ListDictionary(
new CaseInsensitiveComparer(CultureInfo.Invariantculture));
// Добавить несколько элементов
listfEstados Unidos"] = "United States of America";
list["Canada"] = "Canada";
list["Espana"] = "Spain";
// Показать результаты	i
Console.WriteLine(list["espana"]);
Console.WriteLine(list["CANADA"]);
Занятие 5
Обобщенные наборы 201
Console.Read();
}
}
6.	Соберите проект,исправьте ошибки. Убедитесь, что консольное приложение правильно выводит значения Spain и Canada.
Резюме
	Класс BitArray и структура BitVector32 используются для поразрядных операций над группами значений типа Boolean.
	Классы StringCollection и StringDictionary обеспечивают контроль типов при хранении строк.
	При помощи класса CollectionUtil можно создать нечувствительные к регистру версии объектов Hashtable и SortedList.
	NameValueCollection удобен, когда требуется хранить несколько соответствующих одному ключу значений, в наборе пар «имя-значение».
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Какие типы наборов можно создать при помощи класса CollectionsUtiC (Укажите все верные ответы.)
А.	нечувствительный к регистру StringDictionary;
В.	независимый от культуры Hashtable;
С.	нечувствительный к регистру Hashtable;
D.	нечувствительный к регистру SortedList.
2.	Какие типы объектов можно хранить в качестве значения {Value) в StringDictionary*!
А.	строки;
В.	объекты;
С.	массивы строк;
D.	любые типы .NET.
Занятие 5. Обобщенные наборы
До занятия 4 обсуждались только наборы, поддерживающие работу с объектами. Если вам требовалось получить из набора определенный тип объекта, следовало привести полученный объект к его настоящему типу. На занятии 4 были рассмотрены распространенные специализированные наборы для работы с такими популярными типами, как String. Но добавление пары специализированных наборов не устраняет общую проблему обеспечения контроля типов при работе с наборами. Эта задача решается при помощи обобщенных наборов.
202 Наборы и обобщения
Глава 4
Изучив материал этого занятия, вы сможете:
J создавать и использовать списки, обеспечивающие контроль типов;
J создавать и использовать очереди, обеспечивающие контроль типов;
Z создавать и использовать стеки, обеспечивающие контроль типов;
J создавать и использовать словари, обеспечивающие контроль типов;
J создавать и использовать связные списки, обеспечивающие контроль типов.
Продолжительность занятия — 20 минут.
Работа с обобщениями
Программирование есть решение конкретных задач. Однако иногда встречаются задачи, общие для множества ситуаций. Например, широко распространена задача создания упорядоченного списка элементов. В инфраструктуре .NET Framework такую задачу можно решать при помощи класса ArrayList. Поскольку класс ArrayList «не знает», какие объекты намереваются хранить пользователи, он хранит «объекты вообще» (объекты типа Object). Известно, что в .NET любая сущность может быть представлена как объект, следовательно, ArrayList сможет хранить объекты любого типа. Можно ли считать это решением?
В принципе, наборы объектов типа Object решают эту задачу, такое решение порождает новые проблемы. Например, для хранения набора целых чисел можно написать следующий код:
’ VB
Dim mylnts As New ArrayListO
mylnts.Add(1)
mylnts.Add(2)
mylnts.Add(3)
For Each i as Object In mylnts
Dim number As Integer = CType(i, Integer) Next
// C#
ArrayList mylnts = new ArrayListO;
mylnts.Add(1);
mylnts.Add(2);
mylnts.Add(3);
foreach (Object i in mylnts)
{
int number = (int)i;
}
Все хорошо, вы создаете набор и добавляете в него целые числа. Далее вы можете получить из набора целые числа, приведя к ним Object, возвращаемый набором. Но что произойдет, если добавить следующую строку:
Занятие 5	Обобщенные наборы 203
' VB
mylnts.Add("4")
// C#
mylnts.Add("4");
Такой код прекрасно компилируется, но при выполнении цикла foreach будет сгенерировано исключение, так как в данном случае «4» — это строка, а не целое число. Обработка таких исключений доставляет лишние хлопоты. Гораздо удобнее работать с набором, в котором хранятся только целые числа. В следующем фрагменте кода показано, как создать такой класс:
' VB
Public Class IntList
Implements ICollection
Implements lEnumerable
Private _innerList As ArrayList = New ArrayListO
Public Sub Add(ByVal number As Integer)
_innerList.Add(number)
End Sub
Default Public Readonly Property Item(index As Integer) As Integer
Get
Return CType(_innerList(index), Integer)
End Get
End Property
«region ICollection Members
Примечание: Члены ICollection для краткости не приведены здесь. Вам следует реализовать интерфейс ICollection в ваших, собственных наборах
«End Region
«region lEnumerable Members
’ Примечание: Члены lEnumerable для краткости не приведены здесь.
Вам следует реализовать интерфейс lEnumerable в ваших собственных наборах
«End Region
End Class
11 Ctt
public class IntList : ICollection, lEnumerable {
private ArrayList _innerList = new ArrayListO;
public void Add(int number)
204 Наборы и обобщения
Глава 4
{
_inne гList.Add(numbeг);
}
public int this[int index] {
get {
return (int)_innerList[index]; }
}
«region ICollection Members
// Примечание: Члены ICollection для краткости опущены.
II	Вам следует реализовать интерфейс ICollection в собственных наборах
«endregion
«region lEnumerable Members
И Примечание: Члены lEnumerable для краткости опущены.
// Вам следует реализовать интерфейс lEnumerable в собственных наборах «endregion
}
Итак, вы создаете новый набор, который поддерживает базовые интерфейсы набора (ICollection и lEnumerable). Элементы хранятся в наборе ArrayList. В завершение вы пишете метод Add и индексатор с контролем типов, допускающий только целые числа. Работать с этим классом можно примерно так:
' VB
Dim mylntegers As New IntListQ
mylntegers.Add(l)
mylntegers.Add(2)
mylntegers.Add(3)
mylntegers.Add("4") ошибка компиляции!
For Each i As Object In mylntegers
Dim number As Integer = CType(i , Integer) ' Никогда не сгенерирует исключения Next
И C#
IntList mylntegers = new IntListQ;
mylntegers.Add(1);
mylntegers.Add(2);
mylntegers.Add(3);
// mylntegers.Add("4"); ошибка компиляции!
Занятие 5
Обобщенные наборы 205
foreach (Object i in mylntegers)
{
int number = (int)i; // Никогда не сгенерирует исключения
}
Этот код работает отлично. При попытке добавить в набор нечто, что не является целым числом, генерируется ошибка компиляции (в VB должен быть включен параметр Option Strict On). В цикле foreach никогда не будет сбоев, поскольку в набор не попадает ничего, кроме целых чисел. Может быть, это — решение?
Да, но очень трудоемкое. В идеале хотелось бы создавать классы наборов, просто указывая, какой тип должен в нем использоваться. К счастью, такое возможно при помощи обобщений.
Обобщения — это типы, которые принимают имена других типов в качестве параметров, и используют их. Вместо того, чтобы создавать строго типизированные наборы для каждого типа, достаточно создать набор, которые можно настроить на любой тип:
' VB
Public Class MyList(Of Т)
Implements ICollection
Implements lEnumerable
Private _innerList As ArrayList = New ArrayListO
Public Sub Add(ByVal val As T)
-innerList.Add(val)
End Sub
Default Public Readonly Property Item(index As Integer) As T
Get
Return CType(_innerList(index), T)
End Get
End Property
«region ICollection Members
«End Region
«region lEnumerable Members
«End Region
End Class
// C«
public class MyList<T> : ICollection, lEnumerable
{
private ArrayList _innerList = new ArrayListO;
206 Наборы и обобщения
Глава 4
public void Add(T val)
{
_innerList Add(val);
}
public T this[int index]
{
get
return (T)_innerl_ist[index];
}
}
#region ICollection Members
// ...
#endregion
«region lEnumerable Members
// ...
ftendregion
}
Этот класс аналогичен классу, созданному выше, но вместо реализации набора целых чисел используется обобщенный параметр Т — он повсюду заменяет тип Integer. Во время компиляции Т заменяется конкретным именем типа. Таким образом, этот класс можно использовать для создания строго типизированных наборов для любых типов .NET, как показано в следующем примере:
’ VB
Dim mylntList As new MyList(Of Integer)()
mylntList.Add(1)
mylntList. Add(’’4") ошибка компиляции!
Dim myStringList As new MyList(Of String)()
myStringList.Add("1 )
myStringList.Add(2) ошибка компиляции!
П C#
MyList<int> mylntList = new MyList<int>();
mylntList.Add(1);
// mylntList.Add(”4"); ошибка компиляции!
MyList<String> myStringList = new MyList<String>();
myStringList.Add("1”);
// myStringList.Add(2); // ошибка компиляции!
Чтобы использовать класс обобщения, достаточно включить обобщенный параметр (имя типа-параметра) в код класса, который вы пишете. В первом примере создается
Занятие 5
Обобщенные наборы 207
класс набора для Integer, но на основе того же класса-обобщения создает и набор строк, или набор объектов любого типа .NET, даже собственного.
Обобщения используются в различных областях .NET Framework, но чаще всего — для создания обобщенных наборов. Заметьте, что вам не потребуется создавать собственный обобщенный список — в инфраструктуре есть готовый, а также много других «заготовок».
Улучшение контроля типов и повышение производительности
В инфраструктуре .NET Framework есть обобщения для большинства классов, о которых шла речь в этой главе. К тому же есть несколько новых наборов, доступных только в качестве обобщений. В этом разделе приведены примеры использования всех таких типов. В табл. 4-20 перечислены типы наборов и соответствующие им обобщения.
Табл. 4-20. Эквивалентные обобщения
Имя	Описание
ArrayList	List< >
Queue	Queue<>
Stack	Stack< >
Hashtable	DictionaryO
SortedList	SortedListO
ListDictionary	DictionaryO
HybridDictionary	DictionaryO
OrderedDictionary	DictionaryO
SortedDictionary	SortedDictionary< >
Name ValueCollection	DictionaryO
DictionaryEntry	Name ValuePair< >
StringPollection	List<String>
StringDictionary	Dictionary<String>
N/A	LinkedListO
Как видно из табл. 4-20, большинство классов из этой главы имеют соответствующее обобщение. Позднее мы кратко рассмотрим единственный новый тип набора — LinkedList.
Класс-обобщение List
Класс-обобщение List используется для создания простых упорядоченных списков объектов, поддерживающих контроль типов. Например, если требуется список целых чисел, следует создать объект List, указав тип Integer в качестве параметра обобщения. Для нового экземпляра обобщения List доступны следующие операции
	Можно добавлять элементы в List при помощи метода Add, но тип элементов должен соответствовать типу, указанному в качестве параметра обобщения List.
	Можно получать элементы из List, используя синтаксис с индексатором.
208 Наборы и обобщения
Глава 4
	Воспользовавшись оператором foreach, можно перебрать элементы списка в цикле.
Ниже приведен список целых чисел, созданный на основе List:
' VB
Dim intList As new List(Of Integer)()
intList.Add(1)
intList.Add(2)
intList.Add(3)
Dim number as Integer = intList(O)
For Each i as Integer in intList
Console.WriteLine(i)
Next
// C#
List<int> intList = new List<int>();
intList.Add(1);
intList.Add(2);
intList.Add(3);
int number = intList[O];
foreach (int i in intList)
{
Console WriteLine(i);
}
Работать с классом обобщения List не сложнее, чем с ArrayList, но класс-обобщение дополнительно обеспечивает контроль типов за счет параметра обобщения. Чтобы упорядочить List, достаточно вызвать метод Sort (см. занятие 1). Для класса-обобщения List он вызывается точно так же, но один нюанс, связанный с перегрузкой, стоит упомянуть. Метод Sort класса обобщения List поддерживает обобщенный делегат.
Что такое обобщенные делегаты, спросите вы? Они аналогичны классам- или структурам-обобщениям, но используют обобщенные параметры обобщения, только чтобы определять соглашения по вызову делегата. Например, метод Sort класса-обобщения List принимает в качестве параметра обобщенный делегат Comparison. Определение обобщенного делегата Comparison выглядит так:
’ VB
public delegate int Comparison<T> (
T x,
T у
)
// C#
public delegate int Comparison<T> (
T x,
T у
)
Занятие 5
Обобщенные наборы 209
Предположим, что требуется содержимое List требуется отсортировать в обратном порядке. Для этого можно написать целый класс Comparer. А можно упростить задачу и свести ее к разрабо гке единственного метода, выполняющего сравнение в обобщенном виде:
’ VB
Shared Function ReverseIntComparison(ByVal х As Integer,
ByVai у As Integer) As Integer
Return у - x
End Function
// C#
static int ReverseIntComparison(int x, int y)
{
return у - x;
}
Заметьте: этот метод не является обобщенным сам по себе, но соответствует обобщенному делегату Comparison. (Поскольку List в этом примере состоит из целых чисел, то и данный метод Comparison должен принимать в качестве параметров целые числа.) Такое соответствие позволяет передать созданный метод функции сортировки в качестве параметра; этот метод и будет выполнять сравнение:
' VB
IntList.Sort(ReverselntComparison)
// C#
IntList.Sort(ReverselntComparison);
Такой подход намного проще, чем разработка объекта Comparison для не слишком часто используемых сравнений.
Классы-обобщения Queue и Stack
Эти классы-обобщения являются версиями классов Queue и Stack (см. занятие 2), обеспечивающими контроль типов. Для работы с ними просто создайте новый экземпляр соответствующего класса, указав в качестве параметра обобщения тип который предполагается хранить в Queue или Stack. Чтобы работать с классом-обобщением Queue, достаточно создать экземпляр класса Queue, после чего станут доступными следующие операции.
	Можно добавлять элементы в Queue при помощи метода Enqueue, но тип элементов должен соответствовать типу, указанному в качестве параметра обобщения Queue.
	Для получения элементов из Queue можно воспользоваться методом Dequeue. Ниже приведен пример очереди из строк в Queue:
' VB
Dim que as new Queue(Of String)()
que.Enqueue("Hello")
dim queued as String = que. DequeueO
// C#
Queue<String> que = new Queue<String>();
210 Наборы и обобщения
Глава 4
que.Enqueue('Hello");
String queued = que. DequeueO;
Так же несложно работать и с обобщением Stack. После создания экземпляра класса Stack следующие операции доступны:
	Можно добавлять элементы в Stack при помощи метода Push, но тип элементов должен соответствовать типу, указанному в качестве параметра обобщения Stack.
	Для получения элементов из Stack можно воспользоваться методом Pop. Вот пример стека Stack с целыми числами:
' VB
Dim serials As new Stack(Of Integer)()
serials.Push(1)
Dim serialNumber As Integer = serials.Pop()
// C#
Stack<int> serials = new Stack<int>();
serials.Push(1);
int serialNumber = serials.Pop();
Класс-обобщение Dictionary
Класс-обобщение Dictionary очень похож на Hashtable, ListDictionary и HybridDictionary. Dictionary отличается от классов обобщений List, Stack и Queue тем, что в словаре хранятся пары «ключ-значение». Поэтому при создании экземпляра Dictionary требуется указать два обобщенных параметра. Для работы с обобщением Dictionary выполните следующее:
1. Создайте экземпляр класса-обобщения Dictionary, указав типы ключей и значений, которые будут храниться в Dictionary.
2. Добавлять и получать элементы Dictionary можно при помощи индексатора, но типы элементов должны соответствовать типам, указанным в качестве параметра обобщения Dictionary. В этом примере в Dictionary заносятся целые числа в качестве ключей и строки в качестве значений:
' VB
Dictionary(Of Integer, String) diet = new Dictionary(Of Integer, String)() dict(3) = "Three" diet(4) = "Four" dict(1) = "One" dict(2) = "Two"
Dim str as String = dict(3)
// C#	'
Dictionarycint, string> diet = new Dictionary<int, string>(); dict[3] = "Three";
dict[4] = "Four";
dict[1] = "One";
dict[2] = "Two";
String str = dict[3];
Занятие 5
Обобщенные наборы 211
В примере показано, как работать с целыми числами в качестве ключей Dictionary и со строками в качестве значений. Существенным отличием обобщения Dictionary от его необобщенных аналогов является то, что класс-обобщение не применяет объект DictionaryEntry для хранения пар «ключ-значение». Поэтому для получения отдельных объектов или для перебора элементов набора в цикле следует использовать другое обобщение — KeyValuePair.
Класс-обобщение KeyValuePair, как и Dictionary, принимает два объекта в качестве параметров обобщения. Обычно экземпляры этого типа-параметра не создаются, а возвращаются при работе с классом обобщения Dictionary. Например, при обработке объекта Dictionary в цикле перечислитель возвращает KeyValuePair, связанный с теми же типами ключа и значения, что и Dictionary. Следуя инструкциям, приведенным ниже, можно перебрать в цикле элементы класса обобщения Dictionary.
1. Воспользуйтесь оператором foreach, указав класс-обобщение KeyValuePair в качестве типа объектов, возвращаемых на каждом шаге цикла. Типы, указанные для KeyValuePair, должны соответствовать типам, используемым в самом наборе Dictionary.
2. В теле цикла foreach для доступа к ключам и значениям можно использовать, соответственно, свойства Key and Value объекта KeyValuePair. Этот код является продолжением примера работы с Dictionary'.
 VB
For Each i as KeyValuePair(Of Integer, String) in diet
Console.WrlteLine("{0} = {1}", i.Key, i.Value)
Next
11 C#
foreach (KeyValuePalrcint, string> i in diet)
{
Console.WriteLine("{0} = {1}", i.Key, i.Value); .
}
Класс-обобщение Dictionary сохраняет порядок элементов списка.
Классы-обобщения SortedList и SortedDictionary
Классы-обобщения SortedList и SortedDictionary похожи на Dictionary, но хранят элементы, упорядоченные по ключу набора. Для работы с SortedList выполните следующее:
1.	Создайте экземпляр SortedList, передав как обобщенные параметры типы ключа и значения.
2.	Можно добавлять и получать элементы SortedList при помощи индексатора, но типы элементов должны соответствовать типам, указанным в качестве параметров SortedList.
3.	В операторе цикла foreach укажите обобщенный класс KeyValuePair как тип объекта, возвращаемого на каждом шаге цикла. Типы, указанные для KeyValuePair, должны соответствовать типам, первоначально заданным для SortedList.
4.	В теле цикла foreach получите ключи и значения при помощи, соответственно, свойств Key и Value класса KeyValuePair. В этом примере строки являются ключами, а целые числа — значениями набора SortedList'.
’ VB
Dim sortList As new SortedList(Of String, Integer)()
2*| 2 Наборы и обобщения
Глава 4
sortListC'One") = 1
sortList("Two") = 2
sortList("Three") = 3
For Each i as KeyValuePair(Of String, Integer) in sortList Console.WriteLine(i)
Next
// Cfl
SortedList<string, int> sortList = new SortedList<string, int>(); sortListf’One"] = 1;
sortList["Two”] = 2;
sortList["Three"] = 3,
foreach (KeyValuePair<string, int> i in sortList) {
Console.WriteLine(i);
}
Аналогично используется и SortedDictionary. Для работы с SortedDictionary выполните следующее:
1.	Создайте экземпляр SortedDictionary, передав как параметры обобщения типы ключа и значения.
2.	Можно добавлять и получать элементы SortedDictionary при помощи индексатора, но типы элементов должны соответствовать типам, указанным в качестве параметров SortedDictionary.
3.	В операторе цикла foreach укажите обобщенный класс KeyValuePair как тип объекта, возвращаемого на каждом шаге цикла. Типы, указанные для KeyValuePair, должны соответствовать типам, первоначально заданным для SortedDictionary.
4.	В теле цикла foreach получите ключи и Значения при помощи, соответственно, свойств Key и Value класса KeyValuePair. В этом примере строки являются ключами, а целые числа — значениями набора SortedDictionary’.
• VB
Dim sortedDict as new SortedDictionary(Of String, Integer)() sortedDict("One") = 1 sortedDict("Two") = 2 sortedDict("Three") = 3
For Each KeyValuePair(Of string, int) i in sortedDict Console.WriteLine(i)
Next
11 C#
SortedDictionary<string, int> sortedDict = new SortedDictionary<string, int>();
sortedDict["One"] = 1;
Занятие 5
Обобщенные наборы 213
sortedDict["Two"] = 2;
sOrtedDict["Three"] = 3;
foreach (KeyValuePair<string, int> i in sortedDict) {
Console.WriteLine(i);
}
Класс-обобщение LinkedList
Класс-обобщение LinkedList — новая для .NET разновидность набора, хотя его концепция не нова и уже проверена временем. Я помню, как программировал LinkedList еще в колледже. Идея в том, что элементы связного списка «соединены» друг с другом, и можно перейти к следующему или предыдущему элементу от любого элемента списка, не обращаясь к самому набору. Это полезно при манипуляциях над объектами, которым нужны данные о «соседях».
В табл. 4-21 и 4-22 приведен интерфейс класса обобщения LinkedList:
Табл. 4-21.	Свойства LinkedList
Имя	Описание
Count	Возвращает число узлов в LinkedList
First	Возвращает первый узел LinkedList
Last	Возвращает последний узел LinkedList
Табл. 4-22.	Методы LinkedList
Имя	Описание
AddAfter AddBefore AddFirst	Добавляет в LinkedList новый узел после существующего Добавляет в LinkedList новый узел перед существующим Добавляет новый узел в начало LinkedList
AddLast	Добавляет новый узел в конец LinkedList
Clear	Удаляет из LinkedList все узлы
Contains	Проверяет, присутствует ли значение в списке LinkedList
CopyTo Find	Копирует LinkedList целиком в Array Находит первый узел, содержащий указанное значение
FindLast	Находит последний узел, содержащий указанное значение
Remove	Удаляет из LinkedList первое вхождение значения или узла
RemoveFirst	Удаляет из LinkedList первый элемент
RemoveLast	Удаляет из LinkedList последний элемент
Список LinkedList содержит набор объектов LinkedListNode. При работе с LinkedList вы получаете узлы и переходите между ними. Свойства класса-обобщения LinkedListNode представлены в табл. 4-23.
2*| 4 Наборы и обобщения
Глава 4
Табл. 4-23. Свойства LinkedListNode
Имя	Описание
List	Возвращает LinkedList, которому принадлежит узел
Next	Возвращает следующий узел в списке LinkedList
Previous	Возвращает предыдущий узел в списке LinkedList
Value	Возвращает значение, хранящееся в узле
Класс-обобщение LinkedList имеет одну особенность — реализация перечислителя ILinkedListEnumerator позволяет перечислять значения списка без использования объектов LinkedListNode. Это поведение отлично от поведения обобщения Dictionary, для которого перечислитель возвращает объект обобщения NameValuePair. Это связано с тем, что объекты LinkedListNode можно использовать для прохода по элементам списка, несмотря на то, что в каждом узле присутствует только одна единица данных. Поэтому при перечислении возвращать узлы не требуется.
Для работы со связным списком LinkedList создайте экземпляр класса LinkedList, укажите типы узлов списка, после чего можно выполнять любые из следующих операций:
	Для добавления элементов в начало и конец списка можно воспользоваться, соответственно, методами AddFirst и AddLast. Если передать методам AddFirst и AddLast в качестве параметра просто значение, то они вернут LinkedListNode.
	Для добавления значений в середину списка можно воспользоваться методами AddBefore и AddAfter. Для работы с этими методами необходимо иметь доступ к узлу LinkedListNode, перед которым или после которого требуется добавлять значения.
	Чтобы перебрать в цикле значения списка LinkedList, можно использовать оператор foreach. Заметьте, что тип перебираемых объектов — это тип значений списка, а не узлов. В этом примере в списке LinkedList хранятся строки:
' VB
Dim links As new LinKedList(Of String)()
Dim first as LinkedListNode(Of String) = links.AddLast("First")
Dim last as LinkedListNode(Of String) = links.AddFirst("Last")
Dim second as LinkedListNode(Of String) = links AddBeforedast, "Second") links.AddAfter(second, "Third")
For Each s As String In links Console.WriteLine(s)
Next
// C#
LinkedList<String> links = new LinkedList<string>();
LinkedListNode<string> first = links.AddLast(”First”);
LinkedListNode<string> last = links.AddFirst("Last");
LinkedListNode<string> second = links.AddBefore(last, "Second"); links.AddAfter(second, "Third");
Занятие 5
Обобщенные наборы 215
foreach (string s in links)
Г .. •. .
Console.WriteLine(s);
}
Структура класса обобщенного набора
Как и у других наборов, при работе с обобщенными наборами часто встречаются общие конструкции. К числу таких конструкций относятся интерфейсы обобщенных наборов, обобщенные перечислители и обобщенные сравнения.
Интерфейсы обобщенных наборов
В необобщенных наборах группа интерфейсов определяет интерфейс, сходный у всех наборов. В число этих интерфейсов входят lEnumerable, ICollection, IList и т. п. Обобщенные наборы не только реализуют эти интерфейсы, но и поддерживают их обобщенные версии:
’ VB
Dim stringList As New List(Of String)
Dim theList As IList = CType(stringList, IList)
Dim firstltem As Object = theList(O)
// C#
List<String> stringList = new List<String>();
// ...
IList theList = (IList)stringList;
object firstltem = theList[0];
Обобщенный набор List поддерживает необобщенный интерфейс IList, а также одноименный обобщенный интерфейс, обеспечивающий контроль типов при получении данных, как показано ниже:
' VB
Dim typeSafeList As IList(Of String) = CType(stringList, IList(Of String))
Dim firstString As String = typeSafeList(O)
// C#
IList<String> typeSafeList = (IList<String>) stringList;
String firstString = typeSafeList[O];
To же верно для интерфейсов ICollection, IDictionary, lEnumerable и т.п. В общем случае, если при работе с обобщенными наборами требуется не заданный класс, а интерфейс, следует использовать обобщенную версию интерфейса, чтобы получить поддержку контроля типов.
2*| 6 Наборы и обобщения
Глава 4
Перечислители обобщенных наборов
Все обобщенные наборы, представленные на этом занятии, поддерживают перебор значений набора в цикле. Для упрощения этой операции все наборы поддерживают собственную вложенную обобщенную структуру Enumerator. Тип структуры перечислителя тот же, что и у родительского класса. Чтобы воспользоваться перечислителем вместо оператора foreach, достаточно получить его, вызвав метод Get Enumerator, как показано здесь: ' VB
Dim stringList As New List(Of String) ()
Dim e As List(Of String).Enumerator = stringList.GetEnumerator
While e.MoveNext
Обращение к текущему элементу с контролем типов
Dim s As String = е.Current
End While
// C#
List<string> stringList = new List<string>();
// 
List<string>.Enumerator e = stringList.GetEnumeratorO;
while (e.MoveNextO) {
// Обращение к текущему элементу, обеспечивающее контроль типов string s = е.Current;
}
При помощи структуры Enumerator вы можете получить текущий элемент обобщенного набора с применением контроля типов. Структуру Enumerator поддерживают все обобщенные наборы.
Обобщенные операции сравнения
На предыдущих занятиях показано, что интерфейсы IComparer и lEqualityComparer предоставляют операции сравнения для сравнения и сортировки элементов наборов. Существуют обобщенные версии этих интерфейсов для обобщенных наборов. Если требуется создать собственные реализации интерфейсов IComparer и lEqualityComparer, основную работу выполнят за вас базовые классы. Это классы-обобщения Comparer и EqualityComparer. Если требуется реализовать собственный алгоритм сравнения, воспользуйтесь этими базовыми классами, реализуйте все абстрактные методы и переопределите поведение классов согласно стоящей задаче:	,
’ VB
Class MyComparer(Of T)
Inherits Comparer(Of T)
Занятие 5
Обобщенные наборы 217
Public Overrides Function Compare(ByVai x As T, ByVai у As T) As Integer
Return (x.GetHashCode - y.GetHashCode)
End Function
End Class
// C#
class MyComparer<T> Comparer<T>
public override int Compare(T x, T y)
{
return x.GetHashCodeO - y.GetHashCodeO;
}
}
Разработка собственных наборов
Для реализации собственных наборов можно использовать интерфейсы наборов, упоминавшиеся в этой главе (например, I List и ICollection). Можно начать «с чистого листа» и разработать собственные наборы, реализующие эти интерфейсы, тогда ваши наборы станут поддерживаться всей инфраструктурой .NET Framework.
Программирование собственных наборов почти одинаково для многих типов наборов. .NET Framework предоставляет несколько базовых классов для этой цели:
	CollectionBase;
	ReadOnlyCollectionBase;
	Dictionary Base.
Эти базовые классы могут служить основой собственного набора. Класс CollectionBase поддерживает интерфейсы [List, lEnumerable и ICollection. От CollectionBase можно породить набор, поддерживающий эти интерфейсы. Если требуется получить простой набор с функциональностью, которой нет во встроенных наборах, можно воспользоваться классом CollectionBase.
Как и класс CollectionBase, ReadOnlyCollectionBase поддерживает интерфейсы IList, lenumerable и ICollection. Существенная особенность класса ReadOnlyCollectionBase заключается в том, что он поддерживает изменение набора только средствми самого класса. Этот класс идеально подходит для создания собственного набора, доступного только для чтения
В отличие от базовых классов CollectionBase и ReadOnlyCollection, класс Dictionary-Base реализует интерфейсы IDictionary, lEnumerable и ICollection. Класс DictionaryBase используется для реализации собственного набора с ключами.
До появления .NET 2.0 было принято создавать собственные наборы с реализацией этих интерфейсов в целях получения поддержки контроля типов. Поскольку в настоящее время доступны обобщения, то для создания наборов с контролем типов лучше использовать обобщенные наборы.
218 Наборы и обобщения	Глава 4
Практикум. Создание и использование обобщенных наборов
На этом практикуме вы создадите обобщенный набор Dictionary, в котором будут храниться международные телефонные коды и полные названия стран. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение 1. Создание обобщенного набора для хранения информации о государствах
В этом упражнении вы создадите обобщение Dictionary для хранения кодов государств вместе с их полными названиями.
1.	Создайте консольное приложение с именем GenericCollections.
2.	В методе проекта Main создайте новый экземпляр обобщенного класса Dictionary и укажите, что ключи будут целыми числами, а значения — строками.
3.	Добавьте в набор элементы, используя коды стран в качестве ключей и названия стран в качестве значений.
4.	Попробуйте добавить в качестве ключей коды стран в строковом представлении, чтобы убедиться, что Dictionary поддерживает контроль типов. Если эти строки кода вызовут ошибку компиляции, удалите или закомментируйте их.
5.	Выведите на консоль страну, соответствующую одному из кодов, используя синтаксис индексатора класса Dictionary.
6.	Переберите элементы набора в цикле и выведите код и название страны для каждого элемента KeyValuePair из Dictionary. Полученный код выглядит примерно так:
’ VB
Class Program
Public Overloads Shared Sub Main()
Dim countryLookup As New Dictionary(Of Integer, String)()
countryLookup(44) = "United Kingdom"
countryLookup(33) = "France"
countryLookup(31) = "Netherlands"
countryLookup(55) = "Brazil"
'countryLookup["64"] = "New Zealand';
Console.WriteLine("The 33 Code is for: {0}”, countryLookup(33))
For Each item As KeyValuePair(Of Integer, String) In countryLookup Dim code As Integer = item.Key Dim country As String = item.Value Console WriteLineC'Code {0} = {1}", code, country) - Next
Console.Read()
End Sub
End Class ,
Занятие 5
Обобщенные наборы 219
И с#
class Program
{
static void Main(string[] args)
Dictionary<int, Stnng> countryLookup =
new Dictionary<int, String>();
countryLookup[44] = "United Kingdom";
countryLookup[33] = "France";
countryLookup[31] = "Netherlands";
countryLookup[55] = "Brazil";
//countryLookup["64"] = "New Zealand";
Console.WriteLine("The 33 Code is for: {0}", countryLookup[33]);
foreach (KeyValuePair<int, String> item in countryLookup) {
int code = item.Key;
string country = item.Value;
Console.WriteLine("Code {0} = {1}", code, country);
}
Console.Read();
}
}
7.	Соберите проект и исправьте возможные ошибки. Убедитесь, что консольное приложение правильно показывает информацию обо всех добавленных в набор странах.
Резюме
	Обобщенные наборы позволяют создавать версии наборов, поддерживающие контроль типов и потенциально более быстрые, чем их необобщенные аналоги.
	Обобщенные классы List, Dictionary, Queue, Stack, SortedList и SortedDictionary — это обеспечивающие контроль типов версии наборов, рассмотренных на занятиях 1-3.
	Новый обобщенный класс LinkedList — это набор для хранения элементов, которые «знают» соседние элементы списка, что позволяет перебирать список без обращения к самому набору.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы .
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
220 Наборы и обобщения
Глава 4
1. Объекты какого рода возвращает перечислитель обобщения Dictionary?
A. Object.
В. Объекты-обобщения KeyValuePair.
С. Key.
D. Value.
2. В какое место связного списка LinkedList можно добавлять элементы? (Укажите все верные ответы.)
А. В начало LinkedList.
В. Перед любым заданным узлом.
С. После любого заданного узла.
D. В конец LinkedList.
Е. По любому числовому индексу в LinkedList.
Закрепление материала главы
Для закрепления знаний и навыков, полученных при изучении этой главы, выполните следующее:
	изучите резюме главы;
	повторите основные термины, использованные в этой главе;
	выполните задания лабораторной работы по темам этой главы;
	выполните предложенные упражнения;
	пройдите пробный экзамен.
Резюме главы
	Простейшим набором является ArrayList, позволяющий хранить упорядоченный список элементов.
	Наборы Queue и Stack позволяют хранить последовательные списки элементов.
	Словари используются для хранения пар элементов «ключ-значение», что позволяет выполнять быстрый поиск.
	Существуют специализированные наборы для хранения строк и значений типа Boolean, а также для создания таблиц поиска, содержащих только строки.
	Обобщенные наборы представляют собой механизм для создания строго типизированных наборов без разработки собственных классов наборов.
Основные термины
Понятны ли вам значения приведенных здесь основных терминов? Чтобы проверить свои ответы, загляните в глоссарий, приведенный в конце книги.
	набор;
	обобщение;	*
	шаг цикла, итерация.
Лабораторная работа 221
Лабораторная работа
Сейчас вы примените на практике все, что узнали о работе с наборами. Ответы на вопросы см. в разделе «Ответы» в конце книги.
Занятие 1. Использование ArrayList для хранения кодов состояний
Вы — разработчик из ИТ-отдела крупной компании. Вы пишете небольшие приложения, помогающие пользователям просматривать заказы. Руководитель поручил вам добавить в существующее приложение поле кода состояния. Коды состояния должны быть постоянны, их будет минимум пять, хотя впоследствии это число может измениться.
Вопросы
Ответьте на следующие вопросы руководства:
1. Как вы планируете хранить коды состояния для использования в форме?
2. Не возникнет ли проблем, если коды состояния будут отсортированы для разных пользователей по-разному?
Занятие 2. Выбор подходящего набора
Вы — разработчик в небольшом агентстве недвижимости. Вы написали приложение д ля учета договоров. Руководитель сообщает, что эта фирма вошла в состав более крупной компании, и поручает изменить существующее приложение так, чтобы в нем был список всех работающих в настоящее время агентов по продажам.
Результаты опроса
 Руководитель
«Мы не знаем точно, сколько у нас новых агентов по продажам, но нам нужен доступ к кодам продаж и полным именам агентов».
 Агент по продажам
«В настоящее время приложение не выводит список агентов, отвечающих за продажи. Но если после добавления такой возможности программа станет медленнее работать, лучше обойтись без этой функции».
Вопросы
Ответьте на следующие вопросы руководства.
1. Какой набор вы будете использовать при условии, что размер списка может как уменьшиться, так и увеличиться?
2. Как вывод списка агентов по продажам повлияет на производительность вашего приложения?
Занятие 3. Добавление наборов с поддержкой контроля типов
Вы — ведущий разработчик в крупной банковской компании. В вашем отделе много начинающих разработчиков. Один из младших программистов создал набор для хранения списка банковских операций за год. У других разработчиков при работе с этим на
222 Наборы и обобщения
Глава 4
бором возникают ошибки времени выполнения, поскольку все банковские операции хранятся в ArrayList.
Результаты опроса
	Руководитель
«Требуется заменить набор, чтобы программисты смогли ускорить разработку приложений и повысить их надежность».	-д *
	Младший программист
«При работе с набором я записываю в него сведения о банковских операциях, и при этом иногда случайно добавляю объект неверного типа. Компиляция проходит нормально, но у кассиров приложение дает сбои».
Вопросы
Ответьте на следующие вопросы руководства:
1. Как вы предполагаете изменить набор для решения перечисленных выше задач?
2. Будет ли новый набор работать медленнее, чем существующий?
Рекомендуемые упражнения
Чтобы лучше проработать темы экзамена, рассмотренные в этой главе, выполните следующие упражнения.
Применение обобщенных наборов
Выполните как минимум упражнения 1 и 2. Чтобы глубже понять обобщенные наборы, также выполните упражнение 3.
Упражнение 1
	Создайте новый объект ArrayList.
	Добавьте в него несколько объектов различных типов.
Упражнение 2
	Создайте для определенного типа обобщение List.
	Добавьте несколько объектов подходящего и неподходящего типов.
	Посмотрите, что будет в каждом из этих случаев.
Упражнение 3
	Создайте объект класса обобщения Dictionary.
	Добавьте в него несколько элементов.
	Переберите элементы в цикле, обращая внимание на особенности работы класса-обобщения KeyValuePair.
' J.-.4
Сравнение классов Dictionary
Выполните как минимум упражнение 1. Чтобы получить представление о работе с большими наборами, также выполните упражнения 2 и 3.
Пробный экзамен 223
Упражнение 1
	Создайте объекты Hashtable, ListDictionary и HybridDictionary.
	В каждом из словарей сохраните по пять объектов.
	Оцените скорость поиска в различных словарях и разберитесь в их особенностях, связанных с размером хранимого списка. а’
Упражнение 2
	Увеличьте до 100 количество объектов, хранящихся в словарях из упражнения 1, и проверьте, изменилась ли скорость поиска.
Упражнение 3
	Увеличьте до 10 000 количество объектов, хранящихся в словарях из упражнения 1, и проверьте, изменилась ли скорость поиска.
Пробный экзамен
На прилагаемом компакт-диске, содержится пробный экзамен, обеспечивающий широкие возможности по самоподготовке. Например можно проверить усвоение темам этой главы либо всех тем экзамена 70-536. Можно сымитировать настоящий экзамен либо выбрать режим обучения, когда после каждого вопроса приводятся правильные ответы с пояснениями.
Пробный экзамен
Подробнее о пробном экзамене рассказано во введении.
ГЛАВА 5
Сериализация
Занятие 1.	Сериализация объектов	225
Занятие 2.	Сериализация XML	241
Занятие 3.	Собственные методы сериализации	253
Многие приложения должны хранить или передавать данные, содержащиеся в объектах. Чтобы максимально упростить эти задачи, в .NET Framework включено несколько способов сериализации. Эти способы позволяют преобразовывать объекты в двоичные данные, документы Simple Object Access Protocol (SOAP) или файлы XML, которые можно хранить, передавать и получать. В этой главе обсуждается программирование сериализации при помощи встроенных инструментов .NET Framework в соответствии с определенными требованиями.
Темы экзамена:
	Сериализация и десериализация объекта или цепочки объектов с использованием сериализации во время выполнения (см. пространство имен System.Runtime. Serialization):
□	интерфейсы сериализации;
□	атрибуты сериализации;
□	структура SerializationEntry и класс Serializationinfo-,
□	класс ObjectManager,
□	классы Formatter, FormatterConverter и Formatterservices',
□	структура Streamingpontext.
	Управление сериализацией объекта в формат XML, используя пространство имен System.Xml. Serialization:
□	сериализация и десериализация объектов в формате XML, используя класс XmlSerializer,
□	управление сериализацией при помощи атрибутов сериализации;
□	реализация интерфейсов XML Serialization для особого форматирования данных для сериализации XML;
□	делегаты и обработчики событий, предоставленные пространством имен System.Xml.Serialization.
Занятие 1
Сериализация объектов 225
 Реализация собственного формата сериализации при помощи классов Serialization Formatter:
□	класс SoapFormatter (см. пространство имен System.Runtime.Serialization.Formatters.Soap);
□	класс BinaryFormatter (см. пространство имен System.Runtime.Serialization.Format-ters.Binary).
Прежде всего
Чтобы освоить материал этой главы, необходимо иметь навыки работы с Microsoft Visual Basic или C# и уметь:
	создавать консольные приложения в Microsoft Visual Studio, используя Visual Basic или С#;
	добавлять в проект ссылки на системные библиотеки классов;
 запись в файлы и потоковые объекты.
Занятие 1. Сериализация объектов
Создавая объекты в приложении .NET Framework, вы, возможно, никогда не задумывались о том, как данные хранятся в памяти. Да этого и не требуется — об этом позаботится .NET Framework. Но когда требуется сохранять содержимое объекта в файле, передавать объект другому процессу или по сети, возникает необходимость преобразования его в другой формат. Такое преобразование называется сериализацией.
Изучив материал этого занятия, вы сможете:
J выбирать тип сериализации: двоичная, SOAP, XML или другая;
J сериализовагь и десериализовать объекты, используя стандартные библиотеки;
Z создавать классы, которые можно сериализовать и десериализовать;
J изменять стандартную функциональность сериализации и десериализации;
J реализовать собственную сериализацию для управления процессом сериализации.
Продолжительность занятия — 45 минут.
Введение в сериализацию
Поддержка сериализации в пространстве имен System.Runtime.Serialization делится на поддержку сериализации и десериализации объектов с целью их сохранения, передачи и воссоздания. Сериализация — это процесс преобразования объекта в линейную последовательность байтов, которую можно хранить и передавать. Десериализация — это процесс обратного преобразования созданной последовательности байтов в объект.
226 Сериализация
Глава 5
Пример из практики
Тони Нортроп (Топу Northrup)
Сериализация позволяет сэкономить много времени при разработке. До того, как сериализация стала доступна, мне приходилось писать много кода просто для передачи или записи информации. Естественно, если я что-то изменял в приложении, этот код требовалось переписывать. Сегодня при использовании .NET Framework для решения этой задачи достаточно пары строк кода. В большинстве случаев даже не требуется изменять функции сериализации по умолчанию — она и так неплохо работает.
В сущности, когда требуется сохранить объект (или несколько объектов) в файле для дальнейшей обработки, записываемые в файл данные являются результатом сериализации. При чтении этих объектов вызываются методы десериализации, и объект воссоздается в исходном виде. Аналогично, если нужно передать объект приложению, работающему на другом компьютере, вы устанавливаете сетевое подключение, сериализуете объект в поток, а затем десериализуете этот объект в удаленном приложении. Телепортация из научной фантастики — хорошая аналогия сериализации (увы, настоящая телепортация в .NET Framework пока не поддерживается).
ПРИМЕЧАНИЕ Сериализация за кулисами
В Windows сериализация используется при решении многих важных задач, включая работу Web-служб, удаленный доступ и копирование элементов в буфер обмена.
Как сериализовать объект
Сериализация состоит из следующих основных этапов:
1.	Создается потоковый объект, который будет хранить выходные данные сериализации.
2.	Создается объект BinaryFormatter (находится в System.Runtime.Serializfltion.Formatters.Binary).
3.	Вызывается метод BinaryFormatter.Serialize, чтобы сериализовать объект и отправить результат в поток.
Для включения поддержки сериализации достаточно написать немного кода. Это показано в следующем консольном приложении, требующем пространств имен System.IO, System.Runtime.Serialization и System.Runtime.Serialization.Formatters.Binary'.
‘ VB
Dim data As String = "This must be stored in a file."
Создаем файл, в который будут сохраняться данные
Dim fs As FileStream = New FileStream("SerializedString.Data”, FileMode.Create)
Создаем объект BinaryFormatter для выполнения сериализации
Dim bf As BinaryFormatter = New BinaryFormatter
Используем объект BinaryFormatter для сериализации данных в файл bf.Serialize(fs, data)
Занятие 1	Сериализация объектов 227
' Закрываем файл fs.Close
И Сй
string data = "This must be stored in a file.";
// Создаем файл, в который будут сохраняться данные
FileStream fs = new FileStream("SerializedString.Data", FileMode.Create);
// Создаем объект BinaryFormatter для выполнения сериадизации BinaryFormatter bf = new BinaryFormatter();
// Используем объект BinaryFormatter для сериализации данных в файл bf.Serialize(fs, data);
// Закрываем файл fs. CloseO;
Если запустить приложение и открыть файд SerializedString.Data в текстовом редакторе, можно увидеть строки в окружении двоичных данных (которые в текстовом редакторе выглядят абракадаброй, см. рис. 5-1). В .NET Framework строки хранятся как текст в формате ASCII, и в его начало и конец добавляется еще несколько двоичных байтов с описанием метода десериализации.
Serialized.Data Notepad	_ X
UM?. Й	————
..»*----- . .-•Jt,- ---
□ УУУУП	00 OThis must be stored in a file.O'*
W	............... ,	Э ..
Рис. 5-1. Сериализованные объекты можно хранить как файлы (но это не текстовые файлы)
Если нужно просто сохранить в файле строку, нет нужды применять сериализацию, достаточно записать эту строку напрямую в текстовый файл. Сериализация становится полезной при сохранении более сложной информации, такой как дата и время. Как показано в следующем фрагменте кода, сериализация сложных объектов так же проста, как сериализация строк:
1	VB
Создаем файл, в который будут сохраняться данные
Dim fs As FileStream = New FileStream("SerializedDate.Data", FileMode.Create)
Создаем объект BinaryFormatter для выполнения сериализации
Dim bf As BinaryFormatter = New BinaryFormatter
Используем объект BinaryFormatter для сериализации данных в файл bf.Serialize(fs, System.DateTime.Now)
228 Сериализация
Глава 5
' Закрываем файл fs.CloseQ
// C#
// Создаем файл, в который будут сохраняться данные
FileStream fs = new FileStream("SerializedDate.Data", FileMode.Create);
// Создаем объект BinaryFormatter для выполнения сериализации
BinaryFormatter bf = new BinaryFormatter();
// Используем объект BinaryFormatter для сериализации данных в файл bf.Serialize(fs, System.DateTime.Now);
// Закрываем файл fs.Close();
Как десериализовать объект
Десериализация объект позволяет создать новый объект на основе сохраненных данных. В сущности, десериализация — это восстановление сохраненного объекта. Основные этапы десериализации таковы:
1.	Создается потоковый объект для считывания результатов выполненной ранее сериализации.
2.	Создается объект BinaryFormatter.
3.	Создается объект для хранения данных, полученных после десериализации.
4.	Вызывается метод Binary Formatter. Deserialize для десериализации объекта и приведения его к нужному типу.
Реализовать этапы десериализации объекта в коде очень просто. Следующее консольное приложение (требующее пространств имен System.IO, System.Runtime.Serialization и System.Runtime.Serialization.Formatters.Binary) показывает, как считать и отобразить сериализованную строку данных, сохраненную в предыдущем примере:
’ VB
Открываем файл, из которого будут считываться данные
Dim fs As FileStream = New FileStream("SerializedString.Data", FileMode.Open)
Создаем объект BinaryFormatter для выполнения десериализации
Dim bf As BinaryFormatter = New BinaryFormatter
Создаем объект для хранения десериализованных данных
Dim data As String = ""
Используем объект BinaryFormatter для десериализации данных из файла data = CType(bf.Deserialize(fs),String)
’ Закрываем файл
fs.Close
Занятие 1
Сериализация объектов 229
Отображаем десериализованную строку
Console.WriteLine(data)
// Cit
// Открываем файл, из которого будут считываться данные
FileStream fs = new FileStream("SerializedString.Data”, FileMode.Open);
// Создаем обиект BinaryFormatter для выполнения десериализации BinaryFormatter bf = new BinaryFormatter();
// Создаем объект для хранения десериализованных данных string data =
// Используем объект BinaryFormatter для десериализации данных из файла data = (string) bf.Deserialize(fs)
// Закрываем файл fs. CloseO;
// Отображаем десериализованную строку
Console.WriteLine(data);
Десериализация более сложного объекта, такого как DateTime, выполняется точно так же. В следующем читаются данные, сохраненные в предыдущем примере, и отображаются день недели и время:
' VB
Открываем файл, из которого будут считываться данные
Dim fs As FileStream = New FileStream("SerializedDate.Data”, FileMode.Open)
Создаем объект BinaryFormatter для выполнения десериализации
Dim bf As BinaryFormatter = New BinaryFormatter
Создаем объект для хранения десериализованных данных
Dim previousTime As DateTime = New DateTime
Используем объект BinaryFormatter для десериализации данных из файла previousTime = CType(bf.Deserialize(fs),DateTime)
Закрываем файл fs.Close
Отображаем десериализованное время
Console.WriteLine(("Day: "
+ (previousTime.DayOfWeek + (”, Time: "
+ previousTime TimeOfDay.ToString))))
// C#
230 Сериализация
Глава 5
// Открываем файл, из которого будут считываться данные
FileStream fs = new FileStreamC’SerializedDate.Data", FileMode.Open);
// Создаем объект BinaryFormatter для выполнения десериализации BinaryFormatter bf = new BinaryFormatterO;
// Создаем объект для хранения десериализованных данных DateTime previousTime = new DateTimeO;
Ц Используем объект BinaryFormatter для десериализации данных из файла previousTime = (DateTime) bf.Deserialize(fs);
// Закрываем файл fs.CloseO;
// Отображаем десериализованное время
Console.WriteLine("Day: " + previousTime.DayOfWeek + ", _
Time: " + previousTime.TimeOfDay.ToStringO);
Как видно из этих примеров, для сохранения и получения объектов достаточно лишь нескольких строк кода, независимо от сложности этих объектов.
ПРИМЕЧАНИЕ Механизм десериализации
«За кулисами» исполняющей среды десериализация является сложным процессом. Исполняющая среда осуществляет все этапы десериализации, с первого по последний. Если объект в сериализуемом потоке ссылается на другой объект, процесс становится еще сложнее.
В этом случае объект Formatter (см. занятие 3) запрашивает ObjectManager, чтобы определить, был ли десериализован объект, на который встретилась ссылка (если да, то ссылка называется обратной, если нет — прямой). Если ссылка обратная, Formatter тут же использует ее, в противном случае Formatter регистрирует требование обработки этого объекта (т.н. fixup) у ObjectManager. Fixup — это окончательная обработка ссылки на объект после его десериализации. ObjectManager обрабатывает ссылку на объект после десериализации этого объекта.
Создание сериализуемых классов
Собственные классы можно будет сериализовать и десериализовать, если добавить к ним атрибут Serializable. Важно сделать это, чтобы разработчики, которые будут использовать ваш класс, смогли без труда сохранять или передавать его экземпляры. Даже если острой необходимости в сериализации нет, рекомендуется добавлять ее поддержку на всякий случай.
Если вас устраивает работа сериализации по умолчанию, достаточно добавить атрибут Serializable. При сериализации класса исполняющая среда сериализует все его члены, включая закрытые.
Занятие 1
Сериализация объектов 231
ПРИМЕЧАНИЕ Безопасность при сериализации
Сериализация открывает другому коду возможность просмотра и модификации экземпляров защищенных объектов, недоступных в других случаях. Поэтому код, выполняющий сериализацию, требует атрибута SecurityPermission (из пространства имен System.Security.Permissions) с флагом SerializationFormatter. Политика по умолчанию не дает это разрешение коду, загруженному из Интернета или интрасети; его получает только код на локальном компьютере. Метод GetObjectData должен быть явно защищен либо требованием атрибута SecurityPermission с указанным флагом SerializationFormatter (см. примеры в занятии 3), либо требованием других разрешений на доступ к закрытым данным. Подробнее о защите кода см. в главе 12.
Также можно управлять сериализацией классов с целью повышения производительности класса или выполнение особых требований. В следующем разделе обсуждается настройка поведения классов во время сериализации.
Как отключить сериализацию отдельных членов
Некоторые члены класса, такие как временные или вычисляемые значения, сохранять не требуется. В качестве примера рассмотрим класс ShopplngCartltem:' VB
<Serializable()> Class ShoppingCartltem
Public productld As Integer
Public price As Decimal
Public quantity As Integer
Public total As Decimal
Public Sub New(ByVal .productID As Integer, ByVai _price As Decimal,
ByVai .quantity As Integer)
MyBase.New
productld = .productID price = .price quantity = .quantity total = (price * quantity) End Sub
End Class
// C#
[Serializable]
class ShoppingCartltem
{
public int pioductld;
public decimal price;
public int quantity;
public decimal total;
public ShoppingCartItem(int .productID. decimal price, int quantity)
{
productld = .productID;

232 Сериализация	Глава 5
price = _price;
quantity = .quantity;
total = price * quantity;
}
}
Класс ShoppingPartltem содержит три члена, значение которых должно определяться приложением при создании объекта. Четвертый член, total, динамически вычисляется умножением цены на количество. Если бы этот класс подвергался сериализации <как есть», член total сохранялся бы в сериализованном объекте, лишь зря занимая память. Чтобы уменьшить размер сериализованного объекта (а значит, и сэкономить место на диске и трафик, требуемые для хранения и передачи этого объекта), пометьте член total атрибутом NonSerialized’.
' VB
<NonSerialized()> Public total As Decimal
11	C#
[NonSerialized] public decimal total;
Теперь член total будет исключен из объекта при сериализации и не будет инициализирован при десериализации. Однако значение total нужно вычислить до того, как десериализованный объект будет использован.
Чтобы класс автоматически инициализировал несериализуемые члены, используйте интерфейс IDeserializationCallback и метод IDeserializationCallback.OnDeserialization. Исполняющая среда будет вызывать IDeserializationCallback.OnDeserialization каждый раз после завершения десериализации класса. В следующем примере класс ShoppingPartltem изменен так, что значение total не сериализуется, но автоматически вычисляется после десериализации:
' VB
<Serializable()> Class ShoppingCartltem
Implements IDeserializationCallback
Public productld As Integer
Public price As Decimal
Public quantity As Integer
<NonSerialized()> Public total As Decimal
Public Sub New(ByVal .productID As Integer, ByVai .price As Decimal,
ByVai quantity As Integer)
MyBase.New
productld = .productlD
price = .price
quantity = .quantity
total = (price * quantity)
End Sub
Sub IDeserializationCallback_OnDeserialization(ByVal sender As Object) Implements IDeserializationCallback.OnDeserialization
Занятие 1
Сериализация объектов 233
’ После десериализации подсчитываем общую стоимость total = (price * quantity)
End Sub
End Class
// C#
[Serializable]
class ShoppingCartltem : IDeserializationcallback {
public int productld;
public decimal price;
public int quantity;
[NonSerialized] public decimal total;
public ShopplngCartItem(int _product!D, decimal _price, int „quantity)
productld = _productID;
price = „price;
quantity = „quantity;
total = price * quantity;
}
void IDeserializationCallback.OnDeserialization(Object sender)
{
// После десериализации подсчитываем общую стоимость total = price * quantity;
}
С реализованным событием OnDeseriahzation член total определяется корректно и будет доступен приложениям после десериализации класса.
Как обеспечить совместимость версий
При попытке десериализовать объект, сериализованный предыдущей версией приложения, могут возникнуть проблемы с совместимостью. Например, если добавить член к собственному классу и попытаться десериализовать объект, в котором этого члена нет, исполняющая среда сгенерирует исключение. Другими словами, если вы добавили к классу некоторый член в версии 3.1 вашего приложения, это приложение не сможет десериализовать объекты этого класса, созданные приложением версии 3.0.
Обойти это ограничение можно двумя способами.
1.	Реализовать собственный метод сериализации, как описано в занятии 3, чтобы получить возможность импорта прежних версий сериализованных объектов.
2.	Применить атрибут OptionalField к новым членам, способным вызвать проблемы с совместимостью.
Атрибут OptionalField не влияет на процесс сериализации. Если член не был сериализован, во время десериализации исполняющая среда присвоит ему значение null, и не будет генерировать исключение. В следующем примере показано, как использовать атрибут OptionalField'.
’ VB
<Serializable()> Class ShoppingCartltem
Implements IDeserializationCallback
234 Сериализация
Глава 5
Public productld As Integer
Public price As Decimal
Public quantity As Integer
<NonSerialized()> Public total As Decimal <OptionalField()> Public taxable As Boolean
11 C#
[Serializable]
class ShoppingCartltem : IDeserializationCallback
{
public int productld;
public decimal price;
public int quantity;
[NonSerialized] public decimal total;
[OptionalField] public bool taxable;
Если требуется инициализировать дополнительные члены, реализуйте интерфейс IDeserializationCallback (см. выше) либо обработку события сериализации (см. занятие 3).
ПРИМЕЧАНИЕ .NET 2.0
В .NET Framework 2.0 можно десериализовать объекты, у которых имеются неиспользуемые члены; таким образом, если из объекта после сериализации будет удален какой-либо член, этот объект все равно можно десериализовать. В .NET Framework 1.0 и 1.1 такой возможности нет. Если в сериализованном объекте обнаруживается дополнительная информация, генерируется исключение.
Рекомендации по обеспечению совместимости версий
Чтобы обеспечить совместимость версий, придерживайтесь следующих правил при модификации классов:
	никогда не удаляйте сериализуемые поля;
	никогда не применяйте атрибут NonSerializedAttribute к полю, если в предыдущей версии класса его не было;
	никогда не изменяйте ни имя, ни тип сериализуемого поля;
	при добавлении нового сериализуемого поля применяйте атрибут OptionalFleldAttribute',
	при удалении атрибута NonSerializedAttribute из поля, которое не было сериализуемым в предыдущей версии, применяйте атрибут OptionalFieldAttribute\
	используя обратные вызовы при сериализации, установите для всех дополнительных полей значения по умолчанию, отличные от 0 или null (если только значения 0 или null не принимаются в качестве значений по умолчанию).
Выбор формата сериализации
В .NET Framework доступны два метода форматирования сериализованных данных (см. пространство имен System.Runtime.Serialization), реализующих интерфейс IRemntingFormatter.
1. BinaryRjrmatter Находится в пространстве имен System.Runtime.Serializfltion.Formatters.Binary, является наиболее эффективным способом сериализации объектов (только для .NET-приложений).
Занятие 1
Сериализация объектов 235
2. SoapFormatter Расположен в пространстве имен System.Runtime.Serializption.Fonnatters.Soap, основан на XML, представляет наиболее надежный способ сериализации объектов, которые будут передаваться по сети или считываться приложениями, не использующими .NET Framework. У SoapFormatter больше шансов преодолеть брандмауэры, чем у BinaryFormatter.
Короче говоря, BinaryFormatter следует использовать, только если заранее известно, что к сериализованным данным будут обращаться исключительно .NET-приложения. Следовательно, если приложение записывает на диск объекты, которые позже будут прочитаны тем же приложением, вам отлично подойдет BinaryFormatter. Если же сериализованные данные будут считываться другими приложениями либо передаваться по сети, используй re SoapFormatter. SoapFormatter надежно работает и в ситуациях, когда можно использовать BinaryFormatter, но сериализованный им объект может занимать в три-четыре раза больше места.
SoapFormatter основан на XML и в первую очередь предназначается для использования Web-службами SOAP. Если требуется хранить объекты в документе, совместимом с открытыми стандартами и кросс-платформными приложениями, наиболее гибкой будет сериализация, основанная на XML, подробнее о ней — на занятии 2 в этой главе.
Использование SoapFormatter
Чтобы использовать SoapFormatter, добавьте в проект ссылку на сборку System.Run-time.Serialization.Formatters.Soap.dll (в отличие от BinaryFormatter, она не добавляется по умолчанию). Затем напишите код, такой же, как при использовании BinaryFormatter, только замените класс BinaryFormatter на SoapFormatter.
Код для использования классов BinaryFormatter и SoapFormatter очень похож, но сгенерированные ими сериализованные данные сильно отличаются. Ниже приведен пример объекта с тремя членами, сериализованного при помощи SoapFormatter (текст немного изменен, чтобы его было легче читать):
<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<SOAP-ENV:Body>
<a1:ShoppingCartItem id="ref-1">
<productld>100</productld>
<price>10.25</price>
<quantity>2</quantity>
</a1:ShoppingCartItem>
</SOAP-ENV:Body>
C/SOAP-ENV:Envelope>
ПРИМЕЧАНИЕ .NET 2.0
SoapFormatter не поддерживает совместимость по сериализации между версиями .NET Framework. Использование объектов, сериализованных в .NET Framework 1.1, в версии 2.0 (и наоборот) часто заканчивается неудачей. Напротив, BinaryFormatter обеспечивает совместимость между версиями.
Управление сериализацией SOAP
Двоичная сериализация предназначена для использования только приложениями, основанными на .NET Framework. Следовательно, редко возникает необходимость изме-
236 Сериализация
Глава 5
нять стандартное форматирование. Напротив, сериализация SOAP совместима с различными платформами. Кроме того, к сериализации объектов могут предъявляться определенные требования, например, по наличию предопределенных атрибутов SOAP и имен элементов.
Управлять форматированием документа, сериализованного с использованием SOAP, можно при помощи атрибутов, перечисленных в табл. 5-1.
Табл. 5.1. Атрибуты сериализации XML
Атрибут	К чему применяется	Что указывает
SoapAttribute	Открытое поле, свойство, параметр или возвращаемое значение	Член класса будет сериализован как XML-атрибут
SoapElement	Открытое поле, свойство, параметр или возвращаемое значение	Класс будет сериализован как элемент XML
Soap Enum	Открытое поле, являющееся идентификатором перечислимого	Имя элемента члена перечислимого
Soaplgnore	Открытые свойства и поля	Свойство или поле должно игнорироваться, когда содержащий его класс сериализуется
Soaplnclude	Открытые объявления производного класса и открытые методы для WSDL-документов	Тип должен быть использован при создании схемы (для распознавания при сериализации)
Атрибуты сериализации SOAP функционируют аналогично атрибутом сериализации XML. Подробнее об атрибутах сериализации XML — на занятии 2 в этой главе.
Советы по сериализации
Используя сериализацию, учитывайте следующие рекомендации:
	если сомневаетесь, включайте атрибут Serializable. если сейчас сериализация не нужна, она может понадобиться позже (например, если другому разработчику нужно будет сериализовать класс, производный от вашего);
	для вычисляемых и временных членов устанавливайте атрибут NonSerialized. Например, если переменная отслеживает ID потока, учтите, что ID вряд ли останется действительным после десериализации, а значит, хранить такие данные не нужно;
	Используйте Soap Formatter, когда требуется переносимость, a BinaryFormatter — если требуется производительность.
Практикум. Сериализация и десериализация объектов
Сейчас вы измените код класса так, чтобы обеспечить эффективную сериализацию, и обновите приложение, чтобы оно выполняло сериализацию и десериализацию этого класса. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Занятие 1
Сериализация объектов 237
Упражнение 1. Включение возможности сериализации класса
Вы должны изменить класс, чтобы разработчики могли сохранять соответствующие объекты на диске для дальнейшего использования и передачи по сети другому .NET-приложению.
1.	Скопируйте на жесткий диск папку Chapter01\Lesson3-Person с компакт-диска, прилагаемого к книге, и откройте версию проекта CreateStruct для C# или Visual Basic.
2.	Просмотрите класс Person. Какие изменения нужно внести, чтобы этот класс можно было сериализовать?
Нужно добавить атрибут Serializable.
3.	Добавьте в класс пространство имен System.Runtime.Serialization.
4.	Добавьте к классу Person атрибут Serializable, а затем соберите проект, чтобы убедиться, что он успешно компилируется.
Упражнение 2. Сериализация объекта
Сейчас вы напишете код для сохранения объекта на диске, используя наиболее эффективный метод.
1.	Откройте проект Serialize-People, который вы изменяли в упражнении 1.
2.	Импортируйте пространства имен System.IO, System.Runtime.Serialization и System.Run-time. Serialization .Formatters.Binary в файл, содержащий функцию Main.
3.	Добавьте в метод Serialize код для сериализации объекта sp в файл Person.dat, расположенный в текущей папке. Код будет выглядеть примерно так:
’ VB
Private Sub Serialize(ByVal sp As Person)
Создаем файл для сохранения данных
Dim fs As FileStream = New FileStream("Person.Dat", FileMode.Create)
Создаем объект BinaryFormatter для выполнения сериализации
Dim bf As BinaryFormatter = New BinaryFormatter
Используем объект BinaryFormatter для сериализации данных в файл bf.Serialize(fs, sp)
’ Закрываем файл fs. CloseO
End Sub
11 Ctt
private static void Serialize(Person sp)
{
// Создаем файл для сохранения данных
FileStream fs = new FileStream("Person.Dat", FileMode.Create);
11 Создаем объект BinaryFormatter для выполнения сериализации BinaryFormatter bf = new BinaryFormatterO;
// Используем объект BinaryFormatter для сериализации данных в файл
238 Сериализация
Глава 5
bf.Serialize(fs, sp),
// Закрываем файл fs. CloseO;
}
4.	Соберите проект, исправьте ошибки.
5.	Откройте командную строку в папке приложения и протестируйте его, выполнив следующую команду:
Serialize-People Топу 1923 4 22
6.	Откройте файл и просмотрите сериализованные данные, чтобы убедиться, что введенное имя успешно перехвачено. Проверьте также дату и возраст; однако эти данные труднее обнаружить в текстовом редакторе.
Упражнение 3. Десериализация объекта
Вы прочитаете с диска сериализованный объект, используя BinaryFormatter.
1.	Откройте проект Serialize-People, который вы изменяли в упражнениях 1 и 2.
2.	Добавьте код в метод Deserialize основной программы, чтобы десериализовать объект dsp из файла Person.dat, расположенного в текущей папке. Этот код выглядит примерно так:
' VB
Private Function DeserializeO As Person
Dim dsp As Person = New Person
Открываем файл для чтения данных
Dim fs As FileStream = New FileStream("Person.Dat", FileMode.Open)
Создаем объект BinaryFormatter для выполнения десериализации
Dim bf As BinaryFormatter = New BinaryFormatter
Используем объект BinaryFormatter для десериализации данных из файла dsp = CType(bf.Deserialize(fs), Person)
Закрываем файл
fs. CloseO
Return dsp
End Function
// C#
private static Person DeserializeO
{
Person dsp = new Person();
// Открываем файл для чтения данных	*
FileStream fs = new FileStream("Person.Dat", FileMode.Open);
Занятие 1
Сериализация объектов 239
« // Создаем объект BinaryFormatter для выполнения десериализации BinaryFormatter bf = new BinaryFormatter();
11 Используем объект BinaryFormatter для десериализации данных из файла
dsp = (Person)bf.Deserialize(fs);
// Закрываем файл
fs.CloseO;
return dsp;
}
3.	Соберите проект, исправьте ошибки.
4.	Откройте командную строку в папке приложения и выполните следующую команду без параметров:
Serialize-People
Заметьте, что команда Serialize-People отображает имя, дату рождения и возраст, взятые из ранее сериализованного объекта Person.
Упражнение 4. Оптимизация класса для десериализации
Ваша задача — изменить класс, чтобы повысить эффективность сериализации.
1.	Откройте проект Serialize-People, с которым вы работали в упражнениях 1-3.
2.	Измените класс Person, чтобы предотвратить сериализацию члена с данными о возрасте. Для этого добавьте атрибут NonSerialized, как показано в следующем фрагменте кода:
’ ,VB
<NonSerialized()> Public age As Integer
// C#
[NonSerialized] public int age;
3.	С и запустите проект без параметров командной строки. Заметьте, что команда Serialize- People отображает имя и дату рождения, полученные из ранее сериализованного объекта Person. Однако вместо возраста отображается ноль.
4.	Измените класс Person, чтобы реализовать в нем интерфейс IDeserializationCallback, как показано в следующем фрагменте кода:
' VB
<Serializable()> Public Class Person Implements IDeserializationCallback
// C#
namespace Serialize-People
{
[Serializable]
class Person : IDeserializationCallback
240 Сериализация
Глава 5
5.	Добавьте в класс Person метод IDeserializationCallback.OnDeserialization. Код будет выглядеть примерно так:
’ VB
Sub IDeserializationCallback_OnDeserialization(ByVdl sender As Object) Implements IDeserializationCallback.OnDeserialization
После десериализации вычисляем возраст
CalculateAgeO
End Sub
11 C#
void IDeserializationCallback.OnDeserialization(Object sender)
{
// После десериализации вычисляем возраст
CalculateAgeO;
6 Постройте и запустите проект без параметров командной строки. Заметьте, что команда Serialize- People отображает имя, дату рождения и возраст, полученные из ранее сериализованного объекта Person. Теперь возраст отображается корректно, так как вычисляется сразу после десериализации.
Резюме
	Сериализация — это процесс преобразования информации в поток байтов, который можно сохранять или передавать.
	Чтобы сериализовать объект, сначала нужно создать потоковый объект. Затем создается объект BinaryFormatter и вызывается метод Binary Formatter. Serialize. Десериализация выполняется так же, но вызывается метод BinaryFormatter.Deserialize.
	Чтобы создать класс, который можно сериализовать, добавьте атрибут Serializable. Можно также использовать атрибуты для отключения сериализации отдельных членов.
	Класс SoapFormatter является менее эффективной, но имеющей более широкие возможности взаимодействия альтернативой классу BinaryFormatter.
	Чтобы использовать SoapFormatter, выполните ту же последовательность действий, что и для BinaryFormatter, но используйте класс System.Runtime.Serialization. Formatters.Soap.SoapFormatter.
	Можно управлять сериализацией SoapFormatter, используя атрибуты, чтобы указать имена сериализуемых элементов и способа сериализации члена — как элемента либо как атрибута.
	Рекомендуется делать все классы сериализуемыми, даже если в текущий момент сериализация не требуется. Сериализацию следует отключать для вычисляемых и временных членов.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
Занятье 2
Сериализация XML 241
1.	Что из следующего требуется для сериализации объекта? (Укажите все верные ответы.) А. экземпляр BinaryFormatter;
В. разрешения на создания временных файлов;
С. службы Internet Information Services;
D. потоковый объект.
2.	Какой из предложенных атрибутов следует добавить в класс, чтобы сделать его сериализуемым?
A. I Serializable',
В. Serializable',
С. Soaplnclude',
D. (^Deserialization.
3.	Какой из следующих атрибутов следует добавить к члену, чтобы исключить его из сериализации с использованием BinaryFormatter!
A. NonSerialized',
В. Serializable',
С. SerializationException',
D. Soap Ignore.
4.	Какой из перечисленных интерфейсов следует реализовать, чтобы можно было вызвать метод после сериализации экземпляра класса?
A. IFormatter,
В. [Serializable",
С. I Deserializationcallback',
D. lObjectReference.
Занятие 2. Сериализация XML
XML — это стандартный формат текстовых документов, подходящий для хранения информации, к которой будут обращаться приложения. Как HTML является стандартом форматирования документов, предназначенных для чтения людьми, так и XML является стандартом документов, обрабатываемых компьютерами. В формате XML можно хранить данные любого типа, включая документы (последняя версия Microsoft Office хранит документы в формате XML), рисунки, музыку, двоичные файлы и информацию баз данных.
В .NET Framework включено несколько библиотек для чтения и записи в файлы XML, включая пространство имен System.Xml.Serialization. Пространство имен System.Xml.Serializfltion предоставляет методы преобразования объектов, в том числе и собственных классов, в XML-файлы и обратно. При помощи сериализации XML можно записать в текстовый файл почти любой объект, а затем извлечь его оттуда, написав лишь несколько строк кода. Сериализацию XML можно использовать и для передачи объектов между компьютерами через Web-службы, даже если удаленный компьютер не работает с .NET Framework.
242 Сериализация
х Глава 5
Изучив материал этого занятия, вы сможете:
J сериализовать и десериализовать объекты, используя сериализацию XML;
J настраивать поведение сериализации собственных классов для удовлетворения определенных требований, например, схемы XML;
•J сериализовать набор данных.
Продолжительность занятия — 40 минут.
Применение сериализация XML
Сериализация XML используется для обмена объектами с приложениями, не использующими .NET Framework, а также, когда не требуется сериализовать закрытые члены. Сериализация XML имеет следующие преимущества перед обычной сериализацией: 1. Больше возможностей взаимодействия.
XML — это стандартный текстовый формат. Все современные среды разработки включают библиотеки для обработки файлов XML. Следовательно, объект, сериализованный в формат XML, можно будет обрабатывать в приложении, написанном для другой операционной системы, и в другой среде разработки.
2.	Удобнее работать администраторам.
Объекты, сериализованные в XML, можно просматривать и редактировать в любом текстовом редакторе, включая Блокнот. Если объекты хранятся в файлах, администратор получает возможность просматривать и редактировать XML-файл. Это удобно при оптимизации, отладке и разработке новых приложений, взаимодействующих с уже существующими.
3.	Улучшенная прямая совместимость.
Объекты, сериализованные в формат XML, содержат подробную информацию о себе и могут быть без труда обработаны. Когда придет время замены приложения новой версией, последней будет проще обработать объекты, если они были сериализованы в формат XML.
Кроме этого, следует использовать сериализацию XML, если нужно следовать определенной схеме XML или управлять способом кодирования объектов. Однако XML годится не для всех случаев. В частности, сериализация в XML Имеет следующие ограничения:
	в XML можно сериализовать только открытые данные; закрытые данные так сериализовать нельзя;
	нельзя сериализовать графы объектов; сериализация XML применима только к отдельным объектам.
Использование XML для сериализации объекта
Сериализация в XML включает следующие этапы;
1.	Создается поток, а также объект TextWriter или Xml Writer, который будет содержать выходные данные сериализации.
2.	Создается объект XmlSerializer (см. пространство имен System.Xml.Serialization), для этого ему передается тип объекта, который нужно сериализовать.
3.	Вызывается метод XmlSerializer.Serialize для сериализации объекта и вывода результатов в поток.
Занятие 2
Сериализация XML 243
Эти этапы программируются аналогично стандартной сериализации. Следующее консольное приложение (требует пространства имен System.10 и System.Xml.Serialization) демонстрирует, насколько это просто:
' VB
‘ Создаем файл для сохранения данных
Dim fs As FileStream = New FileStream("SerializedDate.XML", _ FileMode.Create)
’ Создаем объект XmlSerializer для выполнения сериализации Dim xs As XmlSerializer = New XmlSerializer(GetType(DateTime))
' Используем объект XmlSerializer для сериализации данных в файл xs. Senalize(fs, System. DateTime.Now)
’ Закрываем файл fs.Close
// C#
/I Создаем файл для сохранения данных
FileStream fs = new FileStream("SerializedDate.XML", FileMode.Create);
// Создаем объект XmlSerializer для выполнения сериализации XmlSerializer xs = new XmlSerializer(typeof(DateTime));
// Используем объект XmlSerializer для сериализации данных в файл xs.Serialize(fs, System.DateTime.Now);
// Закрываем файл fs. CloseO;
При запуске это приложение создаст текстовый файл следующего содержания: c’xml version="1.0" ?>
<dateTime>2005-12-05T16:28:11.0533408-05 00</dateT±me>
По сравнению с сериализованным объектом DateTime, созданным на занятии 1, этот метод генерирует намного более читабельный файл, который к тому же легко редактировать.
Использование XML для десериализации объекта
Чтобы десериализовать объект, выполните следующее:
1.	Создайте поток, объект TextReader или XmlReader, чтобы прочитать сериализованные данные.
2.	Создайте объект XmlSerializer (из пространства имен System.Xml.Serialization), передав ему тип объекта, который нужно десериализовать.
3.	Вызовите метод XmlSerializer.Deserialize для десериализации объекта и приведите результат к нужному типу.
В следующем примере десериализуется XML-файл, содержащий объект DateTime, и отображаются день недели и время, полученные из этого объекта.
244 Сериализация
Глава 5
VB
Открываем файл для чтения данных
Dim fs As FileStream = New FileStream("SerializedDate.XML", FileMode.Open)
Создаем объект XmlSerializer для выполнения десериализации
Dim xs As XmlSerializer = New XmlSerializer(GetType(DateTime))
Используем объект XmlSerializer для десериализации данных из файла
Dim previousTime As DateTime = CType(xs.Deserialize(fs),DateTime)
Закрываем файл
fs.Close
Отображаем десериализованное время
Console.WriteLine(("Day: "
+ (previousTime.DayOfWeek + (", Time "
+ p reviousTime.TimeOfDay.ToSt ring))))
// C#
// Открываем файл для чтения данных
FileStream fs = new FileStream(”SerializedDate.XML", FileMode.Open);
И Создаем объект XmlSerializer для выполнения десериализации XmlSerializer xs = new XmlSerializer(typeof(DateTime));
// Используем объект XmlSerializer для десериализации данных из файла DateTime previousTime = (DateTime)xs.Deserialize(fs);
11 Закрываем файл fs Close();
// Отображаем десериализованное время
Console.WriteLine("Day: " + previousTime.DayOfWeek + ",
Time: " + previousTime.TimeOfDay.ToStringO);
Создание классов, сериализуемых в формат XML
Чтобы создать класс, который можно сериализовать в формат XML, нужно выполнить следующее:
	объявить класс как открытый;
	объявить все члены, которые нужно сериализовать, как открытые (public);
	создать конструктор, не принимающий параметров.
В отличие от стандартной сериализации, для сериализации в XML классы не обязаны иметь атрибут Serializable. Закрытые и защищенные члены игнорируются при сериализации.
Занятие 2
Сериализация XML 245
Управление сериализацией XML
При сериализации класса, удовлетворяющего требованиям сериализации в XML, но не имеющего соответствующих атрибутов, исполняющая среда использует параметры по умолчанию, которые устраивают большинство разработчиков. Имена XML-элементов основаны на именах классов и членов; каждый член сериализуется как отдельный элемент XML. Например, рассмотрим следующий простой класс:
’ VB
Public Class ShoppingCartltem
Public productld As Int32
Public price As Decimal
Public quantity As Int32
Public total As Decimal
Public Sub New()
MyBase.New
End Sub
End Class
// C#
public class ShoppingCartltem
{
public Int32 productld;
public decimal price;
public Int32 quantity;
public decimal total;
public ShoppingCartltemO
{
}
При сериализации экземпляра этого класса будет создан следующий XML-файл (текст изменен для удобочитаемости):
<?xml version="1.0" ?>
<ShoppingCartItem>
<productld>100</productld>
<price>10.25</price>
<total>20.50</total>
</ShoppingCartItem>
Для определения схемы XML этого достаточно. Однако, если нужно создать документы XML, удовлетворяющие определенным требованиям, сериализацию следует контролировать. Это можно сделать при помощи атрибутов, перечисленных в табл. 5-2.
246 Сериализация
Глава 5
Табл. 5-2. Атрибуты сериализации в XML
Атрибут	К чему применяется	Что указывает
XmlAnyAttribute	Открытое поле, свойство, параметр или возвращаемое значение, которое возвращает массив объектов XmlAttribute	При десериализации массив будет заполнен объектами XmlAttribute, представляющими атрибуты XML, не определенные в схеме
XmlAnyElement	Открытое поле, свойство, параметр или возвращаемое значение, которое возвращает массив объектов XmlElement	При десериализации массив заполняется объектами XmlElement, представляющими атрибуты XML, не определенные в схеме
XmlArray	Открытое поле, свойство, параметр или возвращаемое значение, которое возвращает массив сложных объектов	Члены массива будут сгенерированы в виде членов массива XML
Xml Array Item	Открытое поле, свойство, параметр или возвращаемое значение, которое возвращает массив сложных объектов	Производные типы, которые могут быть включены в массив. Обычно применяется совместно с XmlArrayAttribute
XmlAttribute	Открытое поле, свойство, параметр или возвращаемое значение	Член будет сериализован как атрибут XML
XmlChoiceldentifier Открытое поле, параметры или возвращаемое значение		Неоднозначность члена можно устранить, используя перечислимые
XmlElement	Открытое поле, свойство, параметр или возвращаемое значение	Поле или свойство будет сериализовано как элемент XML
XmlEnum	Открытое поле, являющееся идентификатором перечислимого	Имя элемента члена перечислимого
Xmllgnore	Открытые свойства и поля	Свойство или поле должно игнорироваться при сериализации. Функционирует аналогично атрибуту NonSerialized при стандартной сериализации
Xmllnclude	Открытые объявления производного класса и возвращаемые значения открытых методов для WSDL-документов	Класс должен быть включен в схему (для распознавания при сериализации)
XmlRoot	Открытые объявления класса	Управляет XML-сериализацией атрибута как корневого элемента XML. Используйте этот атрибут, чтобы подробнее определить пространство имен и имя элемента
XmlText	Открытые свойства и поля	Свойство или поле должно быть сериализовано как текст в формате XMI,
XmlType	Открытые объявления класса	Имя и пространство имен типа XML
Занятие 2
Сериализация XML 247
Эти атрибуты можно использовать, чтобы обеспечить соответствие сериализованного класса определенным требованиям XML. Например, рассмотрим атрибуты, требуемые для внесения в сериализованный документ XML трех следующих изменений:
	изменение имени элемента ShoppingCartltem на Cartitem;
	назначение productld атрибутом элемента Cartitem, а не отдельным элементом;
ПРИМЕЧАНИЕ Атрибуты и элементы в XML
Элемент в XML может содержать вложенные элементы, подобно тому, как у объектов бывают члены. Элементы могут иметь атрибуты, которые описывают элемент, как свойства описывают объект .NET Framework. При просмотре документа XML атрибуты можно распознать по угловым скобкам (<>), в которые они заключены. Чтобы усвоить различия между атрибутами и элементами, сравните два примера, приведенных в этом разделе.
 исключение значения total из сериализованного документа.
Чтобы выполнить эти требования, измените класс, как показано ниже:
' VB
<XmlRoot("CartItem")> Public Class ShoppingCartltem
<XmlAttribute()> Public productld As Int32
Public price As Decimal
Public quantity As Int32
<XmlIgnore()> Public total As Decimal
J
Public Sub New()
MyBase.New
End Sub
End Class
// C#
[XmlRoot ("Cartitem")]
public class ShoppingCartltem
[XmlAttribute] public Int32 productld;
public decimal price;
public Int32 quantity;
[Xmllgnore] public decimal total;
public ShoppingCartltemO
}
}
В результате получится XML-файл, удовлетворяющий указанным требованиям:
<?xml version="1.0" ?>
<CartItem productld="100">
<price>10.25</price>
- <quantity>2</quantity>
</CartItem>
248 Сериализация
Глава 5
Хотя атрибуты позволяют реализовать практически любые требования к сериализации в XML, можно получить абсолютный контроль над ней, реализовав в классе интерфейс IXmlSerializable. Например, можно разделить данные на байты, а не помещать в буфер большие массивы данных, чтобы избежать переполнения, нередкого при использовании кодировки Base64. Для управления сериализацией следует реализовать методы ReadXml и WriteXmlt управляющие классы XmlReader и XmlWriter, используемые для чтения и записи данных в формате XML.
Реализация требований схемы XML
Обычно, когда требуется организовать обмен XML-файлами между приложениями, их разработчики объединяются для создания файла XML-схемы. Схема XML определяет структуру XML-документа. Существует много типов XML-схем, и по возможности следует применять существующие схемы.
К СВЕДЕНИЮ Схемы XML
Подробнее о схемах XML см. по адресу http://www.w3.org/XML/Schema.
Если имеется схема XML, можно запустить утилиту определения схемы XML (Xsd.exe), чтобы создать набор классов, определенных в строгом соответствии со схемой, со всеми сопутствующими атрибутами. XML-файлы, генерируемые при сериализации такого класса, строго соответствуют схеме. Это более простая альтернатива использования других классов .NET Framework, таких как XmlReader и XmlWriter, для анализа и записи данных в XML-поток.
Чтобы создать класс на основе схемы, выполните следующее:
Создайте или загрузите на компьютер файл XML-схемы с расширением .xsd.
1.	Откройте командную строку Visual Studio 2005.
2.	В командной строке Visual Studio 2005 введите Xsd.exe schema.xsd/classes/language:[CS | VB]. Например ,чтобы создать новый класс на основе файла схемы с именем C:\schema\library.xsd, нужно выполнить следующую команду:
' VB
xsd С \schema\library xsd /classes /language:VB
// C#
xsd C:\schema\library.xsd /classes /language:CS
3.	Откройте созданный файл (с именем Schema.CS или Schema.VB) и добавьте класс в приложение.
При сериализации сгенерированный класс автоматически согласуется со схемой XML. Это упрощает создание приложений, взаимодействующих со стандартными Web-службами.
К СВЕДЕНИЮ Согласование со схемой XML
Подробнее о согласовании со схемой XML см. в статье «XML Schema Part 0: Primer» по адресу http://www.w3.org/TR/2001/REC-xmlschema-0-20010502/ и «Using Schema and Serialization to Leverage Business Logic» по адресу http://msdn.microsoft.com/library/en-us/ dnexxml/html/ xml04162001.asp.
Занятие 2
Сериализация XML 249
Как сериализовать объект DataSet
Сериализовать можно не только экземпляр открытого класса, но и экземпляр объекта DataSet, как показано ниже:
’ VB
Private Sub SerializeDataSet(filename As String)
Dim ser As XmlSerializer = new XmlSerializer(GetType(DataSet)) ’ Создаем DataSet; добавляем таблицу, столбец и десять строк. Dim ds As DataSet = new DataSet("myDataSet") Dim t As DataTable = new DataTable("table1' ) Dim c As DataColumn = new DataColumn("thing") t.Columns.Add(c) ds.Tables.Add(t) Dim r As DataRow Dim i As Integer for i = 0 to 10 r = t.NewRow() r(0) = "Thing " & i t.Rows.Add(r)
Next
Dim writer As Textwriter = new StreamWriter(filename) ser.Serialize(writer, ds) writer. CloseO End Sub
// C#
private void SerializeDataSet(string filename)!
XmlSerializer ser = new XmlSerializer(typeof(DataSet));
// Создаем DataSet; добавляем таблицу, столбец и десять строк DataSet ds = new DataSet(’myDataSet”);
DataTable t = new DataTable(”table1");
DataColumn c = new DataColumn("thing");
t.Columns.Add(c);
ds.Tables.Add(t);
DataRow r;
for(int i = 0; i<10;i++){ r = t.NewRow();
r[0] = "Thing " + i;
t.Rows.Add(r);
}
TextWriter writer = new StreamWriter(filename);
ser.Serialize(writer, ds);
writer. CloseO;
}
Аналогичным образом можно сериализовать массивы, наборы и экземпляры классов XmlElement и XmlNode. Хотя это удобно, вы не получите той степени контроля, как
250 Сериализация	Глава 5
при хранении данных в собственных классах. В качестве альтернативы можно использовать методы DataSet. WriteXml, DataSet.ReadXML и DataSet.GetXml.
Практикум. Использование сериализации XML
Выполнив следующие упражнения, вы обновите приложение, которое пока использует сериализацию с помощью BinaryFormatter, чтобы добавить к нему поддержку сериализации в XML. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение. Замена двоичной сериализации на сериализацию XML
В этом упражнении вы обновите проект, чтобы добавить к нему поддержку данных в формате XML.
1.	Скопируйте на жесткий диск папку Chapter01\Lesson3-Person с компакт-диска, прилагаемого к книге, и откройте версию проекта CreateStruct для C# или Visual Basic.
2.	Импортируйте в основную программу пространство имен System.XmLSerialization.
3.	Перепишите метод Serialization так, чтобы вместо двоичной сериализации использовалась сериализация XML. Дайте временному файлу имя Person.xml. Код будет выглядеть примерно так:
' VB
Private Sub Serialize(ByVal sp As Person)
Создаем файл для сохранения данных
Dim fs As FileStream = New FileStream("Person.XML", FileMode.Create)
Создаем объект XmlSerializer для выполнения сериализации
Dim xs As XmlSerializer = New XmlSerializer(GetType(Person))
Используем объект XmlSerializer для сериализации данных в файл xs.Serialize(fs, sp)
Закрываем файл fs.Close
End Sub
// C#
private static void Serialize(Person sp)
// Создаем файл для сохранения данных
FileStream fs = new FileStream("Person.XML", FileMode.Create);
// Создаем объект XmlSerializer для выполнения сериализации XmlSerializer xs = new XmlSerializer(typeof(Person));
// Используем объект XmlSerializer для сериализации данных в файл xs.Serialize(fs, sp);
Ц Закрываем файл
Занятие 2
Сериализация XML	251
fs.Close();
}
4.	Перепишите метод Deserialization так, чтобы вместо двоичной десериализации использовалась десериализация XML. Готовый код выглядит примерно так:
* VB
Private Function DeserializeO As Person
Dim dsp As Person = New Person
Создаем файл для сохранения данных
Dim fs As FileStream = New FileStream("Person.XML", FileMode Open)
' Создаем объект XmlSerializer для выполнения десериализации
Dim xs As XmlSerializer = New XmlSerializer(GetType(Person))
' Используем объект XmlSerializer для десериализации данных в файл
dsp = CType(xs.Deserialize(fs), Person)
Закрываем файл
fs.CloseO
Return dsp
End Function
// C#
private static Person DeserializeO
(
Person dsp = new PersonO,
// Создаем файл для сохранения данных
FileStream fs = new FileStream("Person.XML”, FileMode.Open);
// Создаем объект XmlSerializer для выполнения десериализации XmlSerializer xs = new XmlSerializer(typeof(Person));
// Используем объект XmlSerializer для десериализации данных в файл
dsp = (Person)xs.Deserialize(fs);
/I Закрываем файл
fs.CloseO;
return dsp;
}
5.	Соберите проект, исправьте ошибки.
6.	Откройте командную строку в папке приложения и выполните следующую команду:
Serialize-People Топу 1923 4 22
252 Сериализация
Глава 5
Какое сообщение об исключении вы получите и почему?
Вы увидите сообщение «Invalid parameters. Serialize_People.Person is inaccessible due to its protection level. Only public types can be processed» (Недопустимые параметры. Нет доступа к Serialize People.Person. Обрабатывать можно только открытые типы). Эта ошибка возникла, поскольку класс Person не помечен как открытый.
7.	Измените класс Person, объявив его как открытый. Соберите проект заново и снова выполните следующую команду:
Serialize-People Топу 1923 4 22
8.	Проверьте сериализованные данные, чтобы убедиться, что введенная в командной строке информация успешно перехвачена. Почему в сериализованном файле появились данные о возрасте, хотя член, определяющий возраст, имеет атрибут NonSerializedl
Атрибут NonSerialized применяется в двоичной сериализации, а на сериализацию в XML он не оказывает влияния.
9.	Выполните ту же команду без параметров, чтобы проверить, выполняется ли десериализация.
Резюме
	Сериализация в XML дает возможность взаимодействия с другими платформами и реализовать требования схемы XML.
	Сериализацию XML нельзя использовать для закрытых данных или графов объектов.
	Чтобы сериализовать объект, сначала нужно создать поток, а также объект TextWriter или XmlWriter, затем создать объект XmlSerializer и вызвать метод XmlSerializer.Serialize. Чтобы десериализовать объект, нужно выполнить те же действия, но вызвать метод XmlSerializer. Deserialize.
	Чтобы создать класс, который можно сериализовать, нужно объявить класс и все его члены как открытые и создать конструктор, не принимающий параметров.
	Сериализацией XML можно управлять, используя атрибуты. При помощи атрибутов можно изменять имена элементов, сериализовать члены как атрибуты, а не как элементы, а также исключать члены из сериализации.
	Утилита Xsd.exe используется для создания классов, которые автоматически согласуются со схемой XML при сериализации.
	Объекты Dataset, массивы, наборы и экземпляры классов XmlElement и XmlNode можно сериализовать при помощи класса XmlSerializer.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
Занятие 3
Собственные методы сериализации 253
1.	Каким требованиям должен удовлетворять класс, чтобы можно было применить к нему сериализацию в XML? (Укажите все верные ответы.)
А. класс должен быть открытым;
В.	класс должен быть закрытым;
С.	класс должен иметь конструктор, не принимающий параметров;
D.	класс должен иметь конструктор, принимающий параметр Serializationinfo.
2.	Какой атрибут нужно использовать, чтобы член сериализовался как атрибут, а не как элемент?
A.	XmlAnyAttribute;
В.	XMLType;
С. XMLElement;
D. XMLAttribute.
3.	Какая утилита позволяет создать класс, который при сериализации создает документ XML, согласованный со схемой XML?
A.	Xsd.exe;
В.	Xdcmake.exe;
С.	XPadsi90.exe;
D. Xcacls.exe.
4.	Какой атрибут нужно добавить члену, чтобы он не участвовал в сериализации XML?
A.	XMLType;
В.	XMLIgnore;
С. XMLElement;
D. XMLAttribute.
Занятие 3. Собственные методы сериализации
Собственные методы сериализации позволяют контролировать сериализацию и десериализацию типов. Управляя сериализацией, можно обеспечить совместимость по сериализации, т.е. возможность сериализовать и десериализовать объекты разных версий типа, не изменяя его базовую функциональность. Например, в первой версии типа могло быть два поля, а в следующей версии появились дополнительные поля. При этом вторая версия приложения должна поддерживать сериализацию-десериализацию обеих версий типа. Ниже рассказывается, как управлять сериализацией с помощью собственных классов сериализации.
Изучив материал этого занятия, вы сможете:
J реализовать интерфейс ISerializable для управления сериализацией класса;
J обрабатывать события сериализации для исполнения кода на различных этапах процесса сериализации;
J писать код, изменяющий процессы сериализации и десериализации в зависимости от контекста;
J описать роль интерфейса IFormatter.
Продолжительность занятия — 30 минут.
254 Сериализация
Глава 5
Как реализовать собственную сериализацию
Сериализация в .NET Framework — очень гибкий процесс, который можно настраивать в соответствии с требованиями приложения. В некоторых случаях требуется полный контроль над этим процессом.
Можно переопределить сериализацию, встроенную в .NET Framework, реализовав интерфейс ISerializable и применив к классу атрибут Serializable. Это особенно удобно, когда значение переменной-члена становится недействительным после сериализации, и нужно присвоить этой переменной значение, чтобы полностью восстановить состояние объекта. Кроме того, не следует использовать сериализацию по умолчанию для классов, помеченных атрибутом Serializable и имеющих декларативные или обязательные требования к защите разрешений на уровне класса или в его конструкторах. Вместо этого в подобных классах всегда следует реализовывать интерфейс ISerializable.
Реализация интерфейса ISerializable включает реализацию метода GetObjectData и особого конструктора, который используется при десериализации объекта. Исполняющая среда при сериализации вызывает метод GetObjectData, а при десериализации — особый конструктор. Если вы забыли реализовать этот метод, компилятор сообщит об этом, а вот о забытом особом конструкторе вы не узнаете, пока во время выполнения не будет сгенерировано исключение сериализации.
Когда исполняющая среда во время сериализации вызывает метод GetObjectData, вы отвечаете за содержимое объекта Serializationinfo, переданного при вызове метода. Просто добавьте переменные, которые нужно сериализовать, как пары «имя-значение», используя метод AddValue, который для хранения информации создает структуры SerializationEntry. В качестве имени можно использовать любой текст. Вы вольны решать, какие переменные-члены добавить в объект Serializationinfo, чтобы передать достаточно данных для восстановления объекта после десериализации. Когда исполняющая среда вызывает конструктор сериализации, достаточно передать значения переменных из объекта Serializationinfo, используя имена, созданные при сериализации.
Приведем пример, показывающий, как реализовать интерфейс ISerializable, конструктор сериализации и метод GetObjectData (используются пространства имен System.Runtime.Serialization и System .Security.Permissions)'.
' VB
<Serializable()> Class ShoppingCartltem
Implements ISerializable
Public productld As Int32
Public price As Decimal
Public quantity As Int32
<NonSerialized()> Public total As Decimal
Стандартный конструктор, не использующий сериализацию
Public Sub New(ByVal _productID As Integer, ByVai _price As Decimal, ByVai .quantity As Integer)
MyBase.NewO
productld = .productID price = .price quantity = .quantity total = (price * quantity) End Sub
Занятие 3
Собственные методы сериализации 255
' Конструктор для десериализации
Protected Sub New(ByVal info As Serializationinfo, ByVai context As Streamingcontext) MyBase.New() productld = info.Getlnt32("Product ID") price = info.GetDecimal("Price") quantity = info.Getlnt32("Quantity") total = (price * quantity)
End Sub
' Метод, вызываемый во время сериализации <SecurityPermissionAttribute(SecurityAction.Demand, _ SerializationFormatter:=True)> Public Overridable Sub _
GetObjectData(ByVal info As Serializationinfo, ByVai context As Streamingcontext)
Implements System.Runtime.Serialization.ISerializable.GetObjectData info.AddValueC'Product ID", productld) info.AddValue("Price”, price) info.AddValue("Quantity", quantity)
End Sub
Public Overrides Function ToStringO As String Return (productld + (": " + (price + (" x ” + (quantity +("=”+ total))))))
End Function
End Class
// C#
[Serializable]
class ShoppingCartltem : ISerializable {
public Int32 productld;
public decimal price;
public Int32 quantity;
[NonSerialized] public decimal total;
// Стандартный конструктор, не использующий сериализацию
public ShoppingCartItem(int _productID, decimal _price, int .quantity) {
productld = _productID;
price = .price;
quantity = .quantity;
total = price * quantity;
}
256 Сериализация	Глава 5
// Конструктор для десериализации
protected ShoppingCartItem(SerializationInfo info,
Streamingcontext context)
{
productld = info.Getlnt32("Product ID");
price = info.GetDecimal("Price");
quantity = info.GetInt32("Quantity");
total = price * quantity;
}
// Метод, вызываемый во время сериализации
[Secu rityPe rmissionAtt ribute(Secu rityAction.Demand,
SerializationFormatter=true)]
public virtual void GetObjectData(SerializationInfo info,
Streamingcontext context)
{
info.AddValue("Product ID", productld);
info.AddValue("Price", price);
info AddValue("Quantity , quantity);
}
public override string ToStringO
{
return productld + ": " + price + " x " + quantity + " = " + total;
}
}
В этом примере объект Serializationinfo выполняет большую часть работы по сериализации и десериализации. Конструкция объекта Serializationlnfo требует объекта, в типе которого реализован интерфейс IFormatterConverter. BinaryFormatter и SoapFormatter всегда создают экземпляр типа System.Runtime.Serialization.FormatterConverter, не предоставляя возможности использовать другой тип IFormatterConverter. FormatterConverter включает методы для преобразования значений разных базовых типов, например Decimal в Double или целого числа со знаком в целое без знака.
ВАЖНО! Проверяйте данные
Следует выполнять проверку данных в конструкторе сериализации и генерировать исключение SerializationException, если конструктору предоставлены неверные данные. Есть опасность того, что злоумышленник воспользуется вашим классом и предоставит поддельный сериализованный объект, пытаясь использовать уязвимость. Следует исходить из того, что любой вызов с недопустимыми или подозрительными данными исходит от злоумышленника. Подробнее о защите кода см. в главе 12.
Обработка событий сериализации
.NET Framework 2.0 поддерживает события двоичной сериализации, если используется класс BinaryFormatter. Эти события вызывают методы вашего класса при сериализации и десериализации. Существует четыре события сериализации:
Занятие 3
Собственные методы сериализации 257
	Serializing
генерируется непосредственно перед началом сериализации. К методу, который должен выполняться при возникновении этого события, применяйте атрибут OnSe-rializing.
	Serialized
генерируется сразу после выполнения сериализации. Методы, которые должны выполняться при возникновении этого события, помечайте атрибутом OnSerialized.
	Deserializing
генерируется непосредственно перед десериализацией. Методы, которые должны выполняться при возникновении этого события, помечайте атрибутом OnDeserialized.
	Deserialized
Генерируется сразу после выполнения десериализации и после вызова метода IDeserializationCallback.OnDeserialization. Если может использоваться форматирующий объект, отличный от BinaryFormatter, следует применять метод IDeserializa-tionCallb ack.On Deserialization. Методы, которые должны выполняться при возникновении этого события, помечайте атрибутом On Deserializing.
Последовательность этих событий показана на рис. 5-2.
Рис. 5-2. События сериализации применяют для вызова методов на различных этапах сериализации и десериализации
Использование этих событий — лучший и самый простой метод управления сериализацией. Эти методы не обращаются к потоку сериализации, позволяя изменять объект до и после сериализации или десериализации. Атрибуты можно применять на всех уровнях иерархии типов, методы также можно вызывать как из базового типа, так и из производных типов. Этот механизм позволяет избежать сложностей реализации интерфей
258 Сериализация	Глава 5
са ISerializable, переложив ответственность за сериализацию и десериализацию на производные типы.
Метод—обработчик этих событий должен:
	принимать объект StreamingContext в качестве параметра;
	возвращать пустое значение;
	иметь атрибут, соответствующий обрабатываемому событию.
В следующем примере показано, как создать объект, реагирующий на события сериализации. Можно обрабатывать любое число событий. Кроме того, можно связать одно из событий сериализации с несколькими методами, а также несколько событий с одним методом.
’ VB
<Serializable()> Class ShoppingCartltem
Public productld As Int32
Public price As Decimal
Public quantity As Int32
Public total As Decimal
<OnSerializing()>
Private Sub CalculateTotal(ByVal sc As StreamingContext) total = (price * quantity)
End Sub
<OnDeserialized()>
Private Sub CheckTotal(ByVal sc As StreamingContext)
If (total = 0) Then
CalculateTotal(sc)
End If
End Sub
End Class
// C#
[Serializable]
class ShoppingCartltem
{
public Int32 productld;
public decimal price;
public Int32 quantity;
public decimal total;
[OnSerializing]
void CalculateTotal(StreamingContext sc)
{
total = price * quantity;
}
[OnDeserialized]
Занятие 3
Собственные методы сериализации 259
void CheckTotal(StreamingContext sc)
(
if (total == 0) { CalculateTotal(sc); }
}
}
События поддерживаются только для сериализации с помощью BinaryFormatter. Для сериализации с применением SoapFormatter или собственного метода сериализации можно использовать только интерфейс IDeserializationCallback, о котором рассказывалось на занятии 1 выше.
Изменение сериализации по контексту
Обычно при сериализации объекта место назначения не играет роли. Однако в некоторых случаях нужно сериализовать и десериализовать объект по-разному, в зависимости от получателя объекта. Например, обычно не сериализуют члены, содержащие информацию о процессе, поскольку весьма вероятно, что эта информация станет недействительной после десериали гации объекта. Цо эта информация может быть полезной, если объект десериализуется в том же процессе. Ьсли объект полезен, только если десериализация выполняется в том же процессе, можно генерировать исключение, если становится известно, что объею адресован другому процессу.
Структура Streamin^ontcx^. может предоставляет информацию о месте назначения сериализованного объекта классам, реализующим интерфейс ISerializable. Streamingcontext передается методу GetObjectData и конструктору сериализации. Структура Streamingcontext имеет два свойства:
	Context
Ссылка на объект, содержащий информацию, нужную пользователю.
	State
Набор битовых флагов, указывающих источник либо приемник сериализуемого или десериализуемого объекта. Поддерживаются следующие флаги:
□	CrossProcess
Источник или приемник — другой процесс на том же компьютере.
□	CrossMachine
Источник или приемник находится на другом компьютере.
□	File
Источник или приемник является файлом. Не предполагает десериализацию в том же процессе.
□	Persistence
Источник или приемник — база данных, файл и т.п. Не предполагает десериализацию в том же процессе.
□	Remoting
Источник или приемник — неизвестный процесс Remoting, работающий на том же или на другом компьютере.
□	Other
Источник или приемник неизвестен.
□	Close
Граф объектов клонируется. Код сериализации предполагает, что десериализация выполняется тем же процессом, что обеспечивает безопасность доступа к неуправляемым ресурсам.
260 Сериализация
Глава 5
□	CrossAppDomain
Источник или приемник находится в другом AppDomain.
□	АП
Источник или приемник может быть в любом из перечисленных контекстов. Этот контекст используется по умолчанию.
Чтобы указать контекст для сериализации и десериализации, нужно реализовать в классе интерфейс {Serialization. При сериализации используйте структуру StreamingContext, передаваемую методу GetObjectData объекта. При десериализации используйте структуру StreamingContext, передаваемую конструктору сериализации объекта.
Если нужно сериализовать или десериализовать объект и предоставить информацию о контексте, измените свойство IFormatter.Context StreamingContext перед вызовом методов Serialize и Deserialize форматирующего объекта. Это свойство реализуется как в классе BinaryFormatter, так и в классе SoapFormatter. При конструировании форматирующего объекта он автоматически присваивает свойству Context значение null, а свойству State — значение АН.
Как создать собственный форматирующий объект
Чтобы создать собственный форматирующий объект, реализуйте интерфейс {Formatter или {GenencFormatter. Интерфейс {Formatter реализуется как в классе BinaryFormatter, так и в классе SoapFormatter. Класс FormatterServices имеет статические методы (включая GetObjectData), удобные для реализации форматирующего объекта.
ПРИМЕЧАНИЕ .NET 2.0
Интерфейс {Formatter был доступен и в .NET 1.1, но {GenericFormatter появился в .NET 2.0.
К СВЕДЕНИЮ Собственные форматирующие объекты
Собственные форматирующие объекты требуются крайне редко, поэтому здесь они рассматриваются очень кратко. Подробнее о собственных форматирующих объектах см. в статье «Format Your Way to Success with the .NET Framework Versions 1.1 and 2.0» по адресу http://msdn.microsoft.com/msdnmag/issues/04/l0/AdvancedSerialization/default.aspx.
Практикум. Реализация собственной сериализации
Вы должны изменить класс, переопределив сериализацию по умолчанию и указав, какие члены будут участвовать в сериализации. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение. Добавление к классу собственного метода сериализации
В этом упражнении вы повысите эффективность сериализации и получите полный контроль над сохранением и извлечением данных.
1.	Скопируйте на жесткий диск папку Chapter01\Lesson3-Person с компакт-диска, прилагаемого к книге, и откройте версию проекта CreateStruct для C# или Visual Basic.
2.	Добавьте в класс Person пространство имен System.Runtime.Serialization.
3.	Добавьте в класс Person атрибут Serializable, а затем соберите проект, чтобы убедиться, что он компилируется корректно.
Занятие 3	Собственные методы сериализации 261
4.	Измените класс Person, реализовав интерфейс ISerializable.
5.	Добавьте метод GetObjectData, принимающий объекты Serializfltionlnfo и StreamingContext, добавьте в объект Serializationinfo элементы, которые нужно сериализовать. Добавьте в объект Serialization Info переменные пате и dateOfBirth, но не добавляйте переменную для сведений о возрасте. Код будет выглядеть так:
• VB
Public Overridable Sub GetObjectData(ByVal info As Serializationinfo,
ByVai context As StreamingContext)
Implements System.Runtime.Serialization.ISerializable.GetObjectData
info.AddValue("Name", name)
infо.AddValue("DOB”, dateOfBi rth)
End Sub
11 C#
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Name", name):
info.AddValue("DOB", dateOfBirth);
}
6.	Добавьте конструктор сериализации, принимающий объекты Serialization Info и StreamingContext, а затем инициализируйте переменные-члены, используя содержимое объекта Serializationinfo. Используйте те же имена элементов, что и на предыдущем шаге. После десериализации всех переменных, вызовите метод CalculateAge, чтобы инициализировать переменные, хранящие сведения о возрасте. Код будет выглядеть следующим образом:
' VB
Public Sub New(ByVal info As Serializationinfo,
ByVai context As StreamingContext)
name = info.GetString("Name")
dateOfBirth = info.GetDateTime("DOB")
CalculateAge
End Sub
// C#
public Person(SerializationInfo info, StreamingContext context) {
name = info.GetString("Name"):
dateOfBirth = info.GetDateTime("DOB");
CalculateAgeO;
}
7.	Соберите проект, исправьте ошибки.
262 Сериализация
Глава 5
8.	Откройте командную строку в папке приложения и выполните следующую команду: Serialize-People Топу 1923 4 22
9.	Запустите эту команду без параметров, чтобы убедиться, что десериализация выполняется корректно.
Резюме
	Чтобы создать собственный метод сериализации, нужно реализовать интерфейс ISerialization.
	Класс BinaryFormatter предоставляет четыре события, которые можно использовать для управления этапами сериализации: OnSerializing, OnSerialized, OnDeserializing и OnDeserialized.
	Класс StreamingContext, экземпляр которого передается методам, вызываемым в ответ на события сериализации, предоставляет информацию об источнике или предполагаемом приемнике сериализации, которую должен указывать метод, выполняющий сериализацию.
	Чтобы создать собственные форматирующие объекты, нужно реализовать интерфейс IFormatter или IGenericFormatter, хотя полный контроль над сериализацией требуется очень редко.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Какие параметры должен принимать конструктор, если класс реализует интерфейс ISerializablel (Укажите все верные ответы.)
A. Serializationinfo',
В. Formatter,
С. StreamingContext',
D. ObjectManager.
2.	Какое событие нужно использовать, чтобы запустить метод сразу после выполнения десериализации?
A. OnSerializing;
В. OnDeserializing',
С. OnSerialized',
D. OnDeserialized.
3.	Какое событие нужно использовать, чтобы запустить метод сразу после выполнения сериализации?
A. OnSerializing',
В. OnDeserializing',
С. OnSerialized',
D. OnDeserialized.
Основные термины 263
4.	Какие требования предъявляются к методу — обработчику события сериализации? (Укажите все верные ответы.)
А.	принимать объект StreamingContext в качестве параметра;
В.	принимать объект Serialization Info в качестве параметра;
С.	возвращать пустое значение;
D.	возвращать объект StreamingContext.
Закрепление материала главы
Для закрепления знаний и навыков, полученных при изучении этой главы, выполните следующее:
	изучите резюме главы;
	повторите основные термины, использованные в этой главе;
	выполните задания лабораторной работы по темам этой главы;
	выполните предложенные упражнения;
	пройдите пробный экзамен.
Резюме главы
	В процессе сериализации объект преобразуется в набор байтов. В процессе десериализации сериализованный объект считывается и восстанавливается с прежними значениями. Большинство собственных классов можно сериализовать, просто добавив атрибут Serializable. В некоторых случаях можно повысить эффективность и предоставить возможность изменения структуры классов, переопределив стандартные функции сериализации.
	Сериализация XML — это способ сохранения и передачи объектов с использованием открытых стандартов. Сериализацию XML можно настраивать согласно требованиям схемы XML, чтобы упростить преобразование объектов в документы XML, а документов XML — обратно в объекты.
	Собственный метод сериализации требуется, когда классы содержат сложную информацию, в новой версии приложения произошли значительные изменения в объектной модели, либо требуется полный контроль над сохраняемой информацией. Поддержку собственного метода сериализации можно добавить, реализовав интерфейс ISerializable или обработчики событий сериализации.
Основные термины
	класс BinaryFormatter,
	десериализация;
	сериализация;
	класс SoapFormatter,
	XML (extensible Markup Language).
264 Сериализация
Глава 5
Лабораторная работа
Сейчас вы примените все, что узнали о реализации и использовании сериализации, на практике. Ответы на вопросы см. в разделе «Ответы» в конце книги.
Выбор способа сериализации
Вы — разработчик в энергетической компании. Вы и ваша команда занимаетесь созданием распределенного .NET-приложения, которое должно заменить устаревшую систему для учета потребления электроэнергии и рассылки счетов потребителям. Вы создали компоненты для наблюдения за потреблением электроэнергии и теперь вам нужно разработать способ передачи полученной информации системе оплаты счетов. Руководитель поручил вам опросить ответственных служащих, чтобы найти ответы на ряд вопросов.
Результаты опроса
	Руководитель группы разработки билинга
«Мой сотрудник уже работает над этим, он написал методы, которые принимают ваши объекты Usage и добавляют информацию о счетах в базу данных. Так что вам остается лишь создать и передать эти объекты нам по внутренней сети».
	Сетевой администратор
«Все серверы в бухгалтерии и отделе билинга находятся в одной подсети, так что не нужно беспокоиться о передаче трафика через брандмауэры. Я бы хотел, чтобы вы снизили трафик, ведь у нас миллионы абонентов, а сеть почти перегружена».
Вопросы
Ответьте на следующие вопросы руководства:
1.	Какой способ сериализации вы будете использовать?
2.	Какие изменения нужно внести в класс, чтобы добавить поддержку сериализации?
3.	Сколько примерно строк кода понадобится для сериализации?
Сериализация в различных версиях приложения
Вы — разработчик в страховой компании. Недавно вы запустили версию 1.0 приложения Incident, построенного на основе .NET 1.1. Это приложение для учета и обработки страховых случаев.
Успешно внедрив версию 1.0, вы начали разработку версии 2.0 на основе .NET 2.0. На планерке руководитель поинтересовался, как при внедрении новой версии будет выполняться обновление.
Вопросы
1.	Я знаю, что в версии 1.0 пользовательские настройки, такие как положения окон, сохраняются в файл путем сериализации объекта Preferences с помощью класса BinaryFormatter. Можно ли будет в версии 2.0 напрямую сериализовать эти параметры, не изменяя класс Preferences9.
2.	К нам поступили просьбы о расширении возможностей настройки. Если добавить в класс Preferences дополнительные члены, удастся ли напрямую десериализовать эти объекты в версии 2.0? Какие изменения нужно будет внести для этого?
Рекомендуемые упражнения 265
3.	Из ИТ-отдела поступила просьба о переходе на использование XML-файлов — их проще редактировать. Можно ли десериализовать существующие двоичные объекты, сериализуя объект XML, и как это сделать?
Рекомендуемые упражнения
Чтобы лучше проработать темы экзамена, рассмотренные в этой главе, выполните следующие упражнения.
Сериализация и десериализация объектов и графов объектов
Выполните как минимум упражнения 1 и 2. Чтобы лучше понять, как сериализацию можно использовать на практике, выполните также упражнение 3.
	Упражнение 1 Добавьте в последний созданный вами класс поддержку сериализации. После этого создайте приложение для сериализации и десериализации класса при помощи класса BinaryFormatter. Проверьте сериализованные данные. Измените приложение так, чтобы оно использовало класс SoapFormatter. Также проверьте сериализованные данные.
	Упражнение 2 Найдите в классе, использованном в первом упражнении, член, который не нужно сериализовать. Измените класс так, чтобы этот член не сериализовался, но автоматически определялся после десериализации.
	Упражнение 3 Создайте клиент-серверное приложение для передачи объектов между двумя компьютерами по сети, используя сериализацию и десериализацию.
Управление сериализацией в XML с использованием пространства имен System.Xml.Serialization
Выполните все упражнения, чтобы на практике освоить программирование XML-сериализации и работу со схемами.
	Упражнение 1 Создайте приложение, использующее сериализацию и десериализацию XML для работы с классом, созданным вами ранее.
	Упражнение 2 Найдите в классе, который вы использовали в первом упражнении, член, который не нужно сериализовать. При помощи соответствующего атрибута измените класс, чтобы исключить этот член из сериализации.
	Упражнение 3 Найдите в Интернете любую схему XML и создайте класс, согласующийся с этой схемой при сериализации, двумя способами: вручную и при помощи утилиты Xsd.exe.
Реализация собственной сериализации при помощи классов форматирования
Выполните как минимум первые два упражнения. Если хотите подробно изучить процесс сериализации, выполните также упражнение 3.
	Упражнение 1 Измените последний класс, реализовав интерфейс [Serialization и добавив поддержку сериализации и десериализации. Проверьте члены класса, чтобы оптимизировать сериализацию, исключая из нее вычисляемые значения.
266 Сериализация	Глава 5
	Упражнение 2 Создайте класс с обработчиками всех четырех событий сериализации BinaryFormatter.
	Упражнение 3 Реализуйте интерфейс IFormatter в собственном форматирующем объекте. Задействуйте его в сериализации и десериализации.
Пробный экзамен
На прилагаемом компакт-диске содержится пробный экзамен, обеспечивающий широкие возможности по самоподготовке. Например, можно проверить усвоение темам этой главы либо всех тем экзамена 70-536. Можно сымитировать настоящий, экзамен либо выбрать режим обучения, когда после каждого вопроса приводятся правильные ответы с пояснениями.
Пробный экзамен
Подробнее о пробном экзамене рассказано во введении.
ГЛАВА 6
Программирование графических элементов
Занятие 1. Рисование графических элементов	268
Занятие 2. Работа с изображениями	287
Занятие 3. Форматирование текста	292
-V'’
Графические функции используются для улучшения пользовательского интерфейса приложений, создания диаграмм и отчетов, а также для создания и редактирования изображений. .NET Framework поддерживает средства для рисования линий, фигур, шаблонов и текста. В данной главе рассматривается создание графических объектов и изображений с помощью классов из пространства имен System.Drawing.
Темы экзамена:
	Дополнение пользовательского интерфейса .NET-приложений средствами пространства имен System.Drawing:
□	дополнение пользовательского интерфейса .NET-приложений с помощью объектов-кистей, перьев, цветов и шрифтов;
□	улучшение пользовательского интерфейса .NET-приложений с помощью графических объектов, изображений, в том числе растровых, и значков;
□	дополнение пользовательского интерфейса .NET-приложений с помощью фигур, управление размерами.
Прежде всего
Чтобы освоить материал этой главы, необходимо иметь навыки работы с Microsoft Visual Basic или C# и уметь:
	создавать приложения Windows Forms в Microsoft Visual Studio на Visual Basic или С#;
	производить запись в файлы и потоки.
-ж
268 Программирование графических элементов	Глава 6
Занятие 1. Рисование графических элементов
Возможности NET Framework позволяют дополнять пользовательский интерфейс линиями, окружностями и другими фигурами. Для отображения этих элементов на формах и других элементах управления Windows Forms достаточно лишь нескольких строк кода.
Изучив материал этого занятия, вы сможете:
J описать члены пространства имен System. Drawing’,
J управлять расположением, размером и цветом элементов управления;
J рисовать линии, пустые фигуры и заполненные фигуры;
J настраивать перья и кисти для улучшения вида графических элементов.
Продолжительность занятия — 60 минут.
Пространство имен System. Drawing
В состав .NET Framework входит пространство имен System.Drawing, которое дает возможность создавать новые а также изменять существующие графические объекты. Пространство имен System. Drawing позволяет:
	динамически добавлять окружности, линии и другие фигуры к пользовательскому интерфейсу;
	создавать диаграммы;
	редактировать рисунки и изменять их размеры;
	изменять коэффициент сжатия рисунков, сохраненных на диске;
	кадрировать и масштабировать рисунки.
	добавлять к рисункам знаки защиты авторских прав и текст.
Данное занятие посвящено работе с графикой, занятие 2 — работе с изображениями, а занятие 3 — форматированию текста.
В табл. 6-1 перечислены наиболее важные классы пространства имен System.Drawing, применяемые для создания объектов для создания или редактирования изображений.
Табл. 6-1. Классы пространства имен системное рисование
Класс	Описание	<
Bitmap	Инкапсулирует растровые изображения GDI +, состоящие из
данных, представляющее пикселы изображения, и их атрибутов. Объект Bitmap — это объект, применяемый для работы с изображениями, и определяемый в данных графического элемента. Данный класс вы будете использовать для загрузки и сохранения изображений
Brush	Классы, производные от данного абстрактного базового класса
Brush (см. раздел о заливке фигур), определяют объекты, которые служат для заливки графических элементов, таких как прямоугольники, эллипсы, сектора, многоугольники и контуры
Занятие 1
Рисование графических элементов 269
Табл, 6-1. (продолжение)
Класс	Описание
Brushes	Кисти для всех стандартных цветов. Наследование от данного класса запрещено он позволяет избежать создания экземпляров класса Brush
ColorConvef*er	Преобразует типы данных, представляющие цвет. К этому классу обращаются через объект TypeDescriptor
ColorTranslator	Преобразует данные, представляющие цвет, к структурам Color из GDI+, а также выполняет обратное преобразование. Наследование от данного класса запрещено
Font	Определяет формат текста (гарнитуру, кегль и начертание). Наследование от данного класса запрещено
FontConverter t	Преобразует тип данных объектов типа Font. К классу FontConverter обращаются через объект TypeDescriptor
FontFamily	Определяет группы сходных по дизайну гарнитур. Наследование от данного класса запрещено
Graphics	Инкапсулирует интерфейс «холста» GDI+. Наследование от данного класса запрещено Данный класс используется всякий раз, когда необходимо нарисовать линию, фигуру или добавить изображение текста на поверхность элемента управления или другого изображения
Icon	Объекты значков Microsoft Windows — небольших растровых изображений, представляющих файлы, папки и прочие объекты ОС. Фактически, значки — это прозрачные битовые карты с размером, фиксированным ОС
IconConverter	Преобразует тип данных объектов Icon. К этому классу обращаются через объект TypeDescriptor
Image	Абстрактный базовый класс, который обеспечивает функциональность производных классов Bitmap и Metafile.
ImageAnimator	Позволяет получить анимрованое изображение, состоящее из сменяющих друг друга кадров
ImageConverter	.Используется для преобразования типа объектов Image. К этому классу обращаются через объект TypeDescriptor
ImageFormatConverter	ImageFormatConverter — это класс, который используется для преобразования типа данных объектов, представляющих цвета. К этому классу обращаются через объект TypeDescriptor
Pen	Объект, используемый для рисования линий, кривых и стрелок. Наследование от данного класса запрещено
Pens	Перья стандартных цветов. Наследование от данного класса запрещено, он освобождает от создания экземпляров класса Реп
PointConverter	Преобразует тип данных объектов Point. К этому классу обращаются через объект TypeDescriptor
RectangleConverter	Преобразует тип данных прямоугольников. К этому классу обращаются через объект TypeDescriptor
270 Программирование графических элементов
Глава 6
Табл. 6-1. (окончание)
Класс	Описание
Region	Описывает внутренную часть графической фигуры, образованой прямоугольниками и контурами. Наследование от данного класса запрещено
SizeConverter	Используется для преобразования типов данных. К этому классу обращаются через объект TypeDescriptor
SolidBrush	Представляет одноцветные кисти, применяемые для заливки прямоугольников, эллипсов, секторов, многоугольников и контуров. Наследование от данного класса запрещено
StringFormat	Инкапсулирет информацию о параметрах оформления текста (выключку, интерлиньяж), манипуляциях при выводе (например, о подстановке многоточий и разделителей разрядов), а также объект ОрепТуре. Наследование от данного класса запрещено
SystemBrushes	Свойства класса SystemBrushes представляют объекты типа SolidBrush — цвета системных элементов Windows
SystemColors	Свойства класса SystemColors представляют структуры типа Color — цвета системных элеметов Windows
SystemFonts	Определяет шрифты для прорисовке текста на системных элементах управления Windows
Systemicons	Свойства класса Systemicons представляют объекты типа Icon д ля системных значков Windows. Наследование от данного класса запрещено
SystemPens	Свойства класса SystemPens представляют объекты Реп — перья цвета системных элементов управления Windows и толщиной 1 пиксел
Texture Brush	Свойства класса TextureBrush представляют объекты типа Brush, заливающие фигуры изображениями. Наследование от данного класса запрещено
ToolboxBitmapAttribute	Применяется к элементам управления, чтобы контейнеры, такие как дизайнер форм Visual Studio, могли получать значки, представляющие эти элементы управления Растровое изображение (битовая карта) значка может быть в отдельном файле либо в сборке, содержащей элемент управления Размер битовой карты, внедряемой в сборку с элементом управления (или сохраняемой в файле), должен равняться 16 х 16. Метод Getlmage объекта ToolboxBitmapAttribute может возвращать маленькое (16 х 16) или большое (32 х 32) представление значка, последнее создается масштабированием меньшего представления значка
Из данных классов наиболее востребован класс Graphics, т. к. он предоставляет методы для рисования на экране дисплея. Класс Реп используется для рисования линий и кривых, а классы, производные от абстрактного класса Brush, — для заливки фигур. Также следует знать класс PictureBox, который можно использовать в приложениях Windows Forms для вывода изображений как элементов пользовательского интерфейса. Пространство имен Sy stem. Drawing включает структуры, описанные в табл. 6-2.
Занятие 1
Рисование графических элементов 271
Табл. 6-2. Структуры System.Drawing
Класс	Описание
CharacterRange	Представляет позиции символов в строке
Color	Представляет цвет
Point	Представляет собой упорядоченную пару целочисленных координат х и у, определяющих точку на плоскости
PointF	Представляет упорядоченную пару координат с плавающей точкой х и у, определяющих точку на плоскости
Rectangle	Содержит множество из четырех целых чисел, определяющих положение и размер прямоугольника (дополнительные функции доступны у объекта типа Region)
RectangleF	Содержит множество из четырех чисел с плавающей точкой, определяющих положение и размер прямоугольника (дополнительные функции доступны у объекта типа Region)
Size	Хранит упорядоченную пару целых чисел, обычно представляющих ширину и высоту прямоугольника
SizeF	Содержит упорядоченную пару чисел с плавающей точкой, обычно представляющих ширину и высоту прямоугольника
Важнейшие из этих структур — Color, Point, Rectangle, и Size.
Определение положения и размера элементов управления
Пространство имен System.Drawing часто используют для того, чтобы задать положение элементов управления в приложенииях Windows Forms. Это удобно для создания динамических фигур, параметры которых зависят от того, что вводит пользователь.
Чтобы задать расположение элемента управления, создайте новую структуру Point, указав координаты относительно левого верхнего угла формы; структуру Point запишите в Location — обязательное свойство элемента управления. Сходная структура PointF оперирует координатами с плавающей точкой (а не целочисленными) и не годится для определения положения элементов управления. Например, чтобы переместить кнопку в левый верхний угол формы на расстоянии в 10 пикселов от верхней и левой границ, потребуется следующий код:
 VB
buttonl Location = New Point(10. 10)
// C#
buttonl.Location = new Point(10, 10);
ПРИМЕЧАНИЕ Примеры кода для работы с графикой требуют проекта Windows Forms
В этой книге большинство примеров кода написано в виде консольных приложений, но в главе 6 в качестве примеров используются приложения Windows Forms — они упрощают вывод графики.
Вместо структуры Point можно использовать свойства Left и Тор или Right и Bottom элемента управления. Но для этого потребуется две строки кода:
272 Программирование графических элементов
Глава 6
’ VB
buttonl.Left = 10
buttonl.Top = 10
H C#
buttonl.Left = 10;
buttonl.Top = 10;
Задать размер элемента управления не сложнее, чем его положение. Следующий фрагмент кода показывает, как сделать это с помощью класса Size'.
' VB
buttonl.Size = New Size(30, 30)
11 C#
buttonl Size = new Size(30, 30);
Определение цветов элементов управления
Цвет элемента управления задают при помощи структуры Color. Проще всего сделать это с применением предопределенных свойств System. Drawing.Color.
’ VB
Buttonl.ForeColor = Color.Red
Buttonl.BackColor = Color.Blue
// C#
buttonl.ForeColor = Color.Red;
buttonl.BackColor = Color.Blue;
Чтобы задать пользовательский цвет, используйте статический метод Color FromArgb. Существует несколько перегруженных версий данного метода, в том числе позволяющая задать цвет как единственный байт, определяющий значения цветовых компонентов (красного, зеленого и синего), либо другими способами. Ниже показано, как задать цвет в виде трех целочисленых значений красного, зеленого и синего компонентов:
' VB
Buttonl.ForeColor = Color.FromArgb(10, 200, 200)
Buttonl.BackColor = Color.FromArgb(200, 5, 5)
// C#
buttonl.ForeColor = Color.FromArgb(10, 200, 200);
buttonl.BackColor = Color.FromArgb(200, 5, 5);
Рисование линий и фигур
Чтобы нарисовать графический элемент на форме или элементе управления:
1. Создайте объект Graphics вызовом метода System. Windows.Forms.Control.CreateGraphics. 2. Создайте объект Pen.
3.	Вызовите член класса Graphics для рисования на элементе управления с помощью объекта Реп.
Занятие 1
Рисование графических элементов 273
Рисование начинается с класса System. Drawing.Graphics. Для создания его экземпляра обычно вызывают метод CreateGraphics элемента управления. Если же требуется сохранить рисунок в файле, можно создать объект Graphics на основе объекте Image (см. занятие 2). После создания объекта графического элемента становится доступно множество методов для рисования:  Clear
очищает поверхность для рисования («холст») и заливает ее заданным цветом;
	DrawEllipse
рисует эллипс или окружность, вписанную в заданный прямоугольник, заданный парой координат, высотой и шириной.
	Draw Icon and DrawIconUnstretched
рисует изображение, представленное заданным значком, по заданным координатам, с использованием масштабирования или без него;
	Drawlmage, DrawImageUnscaled, and DrawImageUnscaledAndClipped
рисует заданный объект Image по заданным координатам, с использованием масштабирования и кадрирования либо без них;
	DrawLine
рисует линию, соединяющую две точки с заданными координатами;
	DrawLines
рисует серию линий, соединяющих точки, заданные массивом структур Point",
	DrawPath
рисует серию соединенных линий и кривых;
	DrawPie
рисует сектор, определенный как эллипс (заданный парой координат, шириной и высотой), и двумя радиусами. Внимание: координаты, переданные DrawPie, задают левый верхний угол воображаемого прямоугольника, в который вписан сектор, а не центр сектора;
	DrawPolygon
рисует фигуру с тремя и более сторонами, заданными массивом структур Point’,
	DrawRectangle
рисует прямоугольник или квадрат, заданный координатами одного из углов, шириной и высотой;
	Draw Rectangles
рисует серию прямоугольников или квадратов, заданных структурами Rectangle’,
	DrawString
рисует изображение заданной текстовой строки по определенным координатами с использованием заданных объектов Brush и Font.
Для использования любого из этих методов необходимо создать экземпляр класса Реп. Обычно с помощью конструктора определяют цвет и ширину объекта Реп (в пикселах). Например, следующий фрагмент кода рисует красную линию толщиной 7 пикселов из левого верхнего угла (координаты 1,1) до точки в центре формы (100,100), как показано на рис. 6-1. Чтобы выполнить этот код, создайте приложение Windows Forms и добавьте код примера к обработчику события Paint формы:
” VB
' Создать графический объект из формы
274 Программирование графических элементов
Глава 6
Dim g As Graphics = Me.CreateGraphics
Создать объект pen для рисования Dim р As Pen = New Pen(Color.Red, 7)
Нарисовать линию
g.DrawLine(p, 1, 1, 100, 100)
11 Ctt
// Создать графический объект из формы Graphics g = this.CreateGraphics();
// Создать объект реп для рисования
Pen р = new Pen(Color.Red, 7);
// Нарисовать линию g.DrawLine(p, 1, 1, 100, 100);
Рис. 6-1. Применение Graphics.DrawLine для рисования прямых
Следующий код рисует синий сектор с дугой в 60° (рис. 6-2): ' VB
Dim g As Graphics = Me.CreateGraphics
Dim p As Pen = New Pen(Color.Blue, 3)
g.DrawPie(p, 1, 1, 100, 100, -30, 60)
11 C#
Graphics g = this.CreateGraphics();
Pen p = new Pen(Color.Blue, 3);
g.DrawPie(p, 1, 1, 100, 100, -30, 60);
Занятие 1
Рисование графических элементов 275
Рис. 6-2. Рисование секторов с помощью Graphics.DrawPie
Методы Graphics. Draw Lines, Graphics.DrawPolygon, и Graphics. Draw Rectangles могут принимать как аргументы массивы, что позволяет рисовать с их помощью более сложные фигуры. Например, следующий код рисует пурпурный пятиугольник (рис. 6-3): ' VB
Dim g As Graphics = Me.CreateGraphics
Dim p As Pen = New Pen(Color.MediumPurple, 2)
' Создать массив точек
Dim points As Point() = New PointO {New Point(10, 10),
New Point(10, 100),
New Point(50, 65),
New Point(100, 100),
New Point(85, 40)}
‘ Нарисовать фигуру, заданную массивом точек g.DrawPolygon(p, points)
// C#
Graphics g = this.CreateGraphics();
Pen p = new Pen(Color MediumPurple, 2);
// Создать массив точек
Point[] points = new Point[]
{new Point(10, 10), new Point(10, 100), new Point(50, 65), new Point(100, 100), new Point(85, 40)};
// Нарисовать фигуру, заданную массивом точек g.DrawPolygon(p, points);
276 Программирование графических элементов
Глава 6
Рис. 6-3. Рисование многоугольников с помощью Graphics.Draw Polygon
ПРИМЕЧАНИЕ Очередность координат
Любому методу .NET Framework передается сначала координата по горизонтали (X), а затем — по вертикали (Y). Так, у изображения размером 100 х 100 пикселов точка с координатами (0,0) находится в левом верхнем углу, точка (100,0) — в правом верхнем; (0, 100) — в левом нижнем; а (100,100) — в правом нижнем.
Настройка перьев
Возможно управлять не только цветом и размером пера (в конструкторе объекта Реп), но также его шаблоном и формой. Форма пера определяет вид концов линий, которые им будут нарисованы, а также служат для создания стрелок и ряда спецэффектов.
По умолчанию перо рисует сплошные линии. Для рисования пунктирной линии создайте экземпляр класса Реп и присвойте свойству Pen.DashStyle одно из следующих значений: DashStyle.Dash, DashStyle.DashDot, DashStyle.DashDotDot, DashStyle.Dot или DashStyle. Solid. Следующий пример кода использует пространство имен System.Drawing. Drawing2D и демонстрирует соответствующие линии (рис. 6-4): ' VB
Dim g As Graphics = Me.CreateGraphics
Dim p As Pen = New Pen(Color.Red, 7)
p.DashStyle = DashStyle.Dot
g.DrawLine(p,	50,	25,	400,	25)
p.DashStyle = DashStyle.Dash
g.DrawLine(p,	50,	50,	400,	50)
p.DashStyle = DashStyle.DashDot
g.DrawLine(p,	50,	75,	400,	75)
p.DashStyle = DashStyle.DashDotDot
g.DrawLine(p, 50, 100, 400, 100)
p.DashStyle = DashStyle.Solid
g.DrawLine(p, 50, 125, 400, 125)
// C#
Graphics g = this.CreateGraphicsO;
Занятие 1
Рисование графических элементов 277
Pen р = new Pen(Color.Red, 7);
р.DashStyle = DashStyle.Dot;
g.DrawLine(p, 50, 25, 400, 25);
p. DashStyle = DashStyle.Dash;
g.DrawLine(p, 50, 50, 400, 50);
p.DashStyle = DashStyle.DashDot;
g.DrawLine(p, 50, 75, 400, 75);
p.DashStyle = DashStyle.DashDotDot;
g.DrawLine(p, 50, 100, 400, 100);
p.DashStyle = DashStyle.Solid;
g. DrawLine(p, 50, 125, 400, 125);
Рис. 6-4. Класс Pen поддерживает рисование пунктирных линий разного вида
Чтобы задать пользовательский шаблон для пунктира, можно использовать свойства Pen.DashOffset и Pen. DashPattern.
Видом концов линий, стрелок и выносок управляют через свойства Pen.StartCap и Pen.EndCap с помощью перечислимого типа LineCap. Следующий фрагмент кода демонстрирует большинство стилей концов линий (рис. 6-5):
• VB
Dim g As Graphics = Me.CreateGraphics
Dim p As Pen = New Pen(Color.Red, 10)
p.StartCap = LineCap.ArrowAnchor p.EndCap = LineCap.DiamondAnchor g.DrawLine(p, 50, 25, 400, 25)
p.StartCap = LineCap.SquareAnchor p.EndCap = LineCap.Triangle
g.DrawLine(p, 50, 50, 400, 50)
p.StartCap = LineCap.Flat
p.EndCap = LineCap.Round
g.DrawLine(p, 50, 75, 400, 75)
278 Программирование графических элементов
Глава 6
р.StartCap = LineCap.RoundAnchor
р.EndCap = LineCap.Square
g.DrawLine(p, 50, 100, 400, 100)
// C#
Graphics g = this.CreateGraphicsO; Pen p = new Pen(Color.Red, 10);
p.StartCap = LineCap.ArrowAnchor;
p.EndCap = LineCap.DiamondAnchor;
g.DrawLine(p, 50, 25, 400, 25);
p.StartCap = LineCap.SquareAnchor;
p.EndCap = LineCap.Triangle;
g.DrawLine(p, 50, 50, 400, 50);
p.StartCap = LineCap.Flat;
p.EndCap = LineCap Round;
g.DrawLine(p, 50, 75, 400, 75);
p.StartCap = LineCap.RoundAnchor;
p.EndCap = LineCap.Square;
g DrawLine(p, 50, 100, 400, 100);
Рис. 6-5. Класс Pen поддерживает параметры, управляющие видом концов линий
Заливка фигур
У большинства методов Draw класса Graphics есть соответствующие методы Fill для рисования залитых фигур. Эти методы работают аналогично методам Draw, только требуют экземпляр класса Brush, а не Реп. Класс Brush — абстрактный, поэтому сначала необходимо создать экземпляр одного из его производных классов:
	System.Drawing. Drawing2D.HatchBrush
определяет прямоугольную кисть, которая заштриховывает объект цветами переднего и заднего плана;
	System.Drawing.Drawing2D.LinearGradientBrush
инкапсулирует кисть, заливающую фигуры линейным градиентом, придавая им более привлекательный и профессиональный вид;
	System.Drawing.Drawing2D.PathGradientBrush
аналогичен LinearGradientBrush, но поддерживает сложные шаблоны, выполняющие заливку фигурным градиентом, заданным набором точек;
Занятие 1
Рисование графических элементов 279
	System. Drawing.SolidBrush определяет кисть одного цвета;
	System. Drawing.Texture Brush
кисть, созданная на основе изображения, заполняющая им фигуру как мозаикой. Следующий фрагмент кода рисует пурпурный пятиугольник (рис. 6-6):
' VB
Dim g As Graphics = Me.CreateGraphics
Dim b As Brush = New SolidBrush(Color.Maroon)
Dim points As Point() = New Point() {New Point(10, 10), New Point(10, 100), New Point(50, 65), New Point(100, 100), New Point(85, 40)}
g FillPolygon(b, points)
// C#
Graphics g = this.CreateGraphics():
Brush b = new SolidBrush(Color.Maroon);
Point[] points = new Pointf]
{new Point(10, 10),
new Point(10, 100),
new Point(50, 65), new Point(100, 100), new Point(85. 40)};
g.FillPolygon(b, points);
Рис. 6-6. Рисование объектов с заливкой при помощи класса Brush и различных методов Graphics.Fill
Для рисования контурных объектов с заливкой нужно вызвать метод Fill класса Graphics, затем метод Draw класса Graphics. Например, следующий код рисует многоугольник, показанный на рис. 6-7: ' VB
Dim g As Graphics = Me.CreateGraphics
Dim p As Pen = New Pen(Color.Maroon, 2)
Dim b As Brush = New LinearGradientBrush(New Point(1, 1), New Point(100, 100), _
Color White, Color.Red)
Dim points As Point() = New PointO {New Point(10, 10),
New Point(10, 100),
New Point(50, 65),
280 Программирование графических элементов
Глава 6
New Point(100, 100), New Point(85, 40)}
g.FillPolygon(t), points) g.DrawPolygon(p, points)
// C#
Graphics g = this.CreateGraphics();
Pen p = new Pen(Color.Maroon, 2);
Brush b = new LinearGradientBrush(new Point(1,1), new Point(100,100), Color.White, Color.Red);
Point[] points = new Point[] {new Point(10, 10), new new new new
Point(10, Point(50, Point(100, Point(85,
100),
65),
100), 40)};
g.FillPolygon(b, points); g.DrawPolygon(p, points);
Рис. 6-7. Применение методов Graphics.Fill и Graphics.Draw для рисования контурных объектов с заливкой	ч
На элементах управления, таких как кнопки и объекты PictureBox, рисуют примерно так же. Чтобы залить объект Graphics одним цветом, используйте метод Graphics.Clear.
Практикум. Создание метода для рисования круговой диаграммы
Сейчас вы создадите метод для рисования круговой диаграммы, а затем доработаете его. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение 1. Рисование круговой диаграммы
В данном упражнении вы напишете метод для рисования круговой диаграммы, заданному массивом координат и структурой Size. Пока достаточно нарисовать контур из черных линий.
1.	Скопируйте папку Chapter06\Lessonl - Exercise 1-PieChart с компакт-диска, прилагаемого к книге, на жесткий диск и откройте С#- или Visual Basic-версию проекта Pie-Chart.
Занятие 1
Рисование графических элементов 281
2.	Изучите форму. На форме имеется простая диаграмма PictureBox, привязанная к сторонам формы. Обратите внимание: по событию Paint вызывается метод Draw.
3.	Изучите метод Draw. В коде метода вы найдете пример передачи параметров методу drawPieChart, который вам предстоит доработать. Заметьте, что метод drawPieChart возвращает объект Image, который используется для определения диаграммы PictureBox.
4.	Изучите класс PieChartElement. Этот простой класс содержит информацию, описывающую один из сегментов круговой диаграммы.
5.	Изучите метод drawPieChart. Он принимет как параметры объект ArrayList, содержащий только объекты PieChartElement, и структуру Size.
6.	Доработайте метод drawPieChart. Для начала определите возвращаемый объект Bitmap, создайте на его основе объект Graphics, после чего верните объект Bitmap'.
' VB
Dim bm As Bitmap = New Bitmap(s.Width, s.Height)
Dim g As Graphics = Graphics.Fromlmage(bm)
Доработка: нарисуйте круговую диаграмму в g
Return bm
// C#
Bitmap bm = new Bitmap(s.Width, s.Height);
Graphics g = Graphics.Fromlmage(bm);
// Доработка: нарисуйте круговую диаграмму в g return bm;
7.	Первый этап завершен, но круговая диаграмма пока не нарисована. Чтобы создать круговую диаграмму из объектов PieChartElement, хранящихся в списке ArrayList, нужно расчитать длины дуг секторов. Для этого необходимо подсчитать общее число объектов: PieChartElement.value.
’ VB
' Подсчитать общее число строк
Dim total As Single = О
For Each e As PieChartElement In elements
If e.value < 0 Then
Throw New ArgumentExceptionC'All elements must have positive values") End If total += e value
Next
// C#
// Подсчитать общее число строк
float total = 0;
foreach (PieChartElement e in elements)
282 Программирование графических элементов	Глава 6
{
if (е value < 0)
{
throw new ArgumentExceptionC'All elements must have positive values");
}
total += e.value;
}
8.	Теперь следует задать прямоугольник, в который будет вписана круговая диаграмма, по структуре Size (она передается этому методу в качестве параметра). Следующий фрагмент кода оставляет достаточно места по краям изображения:
’ VB
Определить прямоугольник, ограничивающий круговую диаграмму
Dim rect As Rectangle = New Rectangle(1, 1, s.Width - 2, s.Height - 2)
// C#
// Определить прямоугольник, ограничивающий круговую диаграмму Rectangle rect = new Rectangle(1, 1, s.Width - 2, s.Height - 2);
9.	Затем объявите объект Pen для рисования круговой диаграммы. Это может быть простое черное перо толщиной в один пиксел:
’ VB
Dim р As Pen = New Pen(Color.Black, 1)
// C#
Pen p = new Pen(Color.Black, 1);
10.	В завершение создайте цикл foreach для подсчета длин дуг секторов круговой диаграммы (в градусах) и рисования диаграммы. Это можно сделать множеством спосо- • бов, например, так:
' VB
Нарисовать первый сектор, начиная с 0 градусов
Dim startAngle As Single = 0
Нарисовать элементы круговой диаграммы
For Each е As PieChartElement In elements
Подсчитать длину дуги (в градусах) для данною сектора, ’ по его доле в окружности
Dim sweepAngle As Single = (e.value / total) * 360
Нарисовать круговую диаграмму
g.DrawPie(p, rect, startAngle, sweepAngle)
Подсчитать длину дуги следующего сектора путем прибавления текущего значения к текущей сумме.
startAngle += sweepAngle
Next
Занятие 1
Рисование графических элементов 283
// C#
// Нарисовать первый сектор (начиная с 0 градусов) float startAngle = 0;
// Нарисовать элементы круговой диаграммы foreach (PieChartElement е in elements) {
И Подсчитать длину дуги (в градусах) для данного сектора,
// по его доле в окружности
float sweepAngle = (е value / total) * 360;
// Нарисовать круговую диаграмму
g.DrawPie(p, rect, startAngle, sweepAngle);
// Подсчитать длину дуги следующего сектора путем прибавления
// текущего значения к текущей сумме. startAngle += sweepAngle;
}
11.	Запустите готовое приложение, исправьте ошибки. Измените размер формы. Заметьте, что размеры круговой диаграммы автоматически изменяются вслед за размерами формы: при этом обработчик события Paint вызывает метод Draw.
Упражнение 2. Улучшение вида круговой диаграммы
Ваша задача — доработать проект, созданный в упражнении 1, чтобы улучшить вид круговой диаграммы. Вы должны залить сектора разными цветами и активировать сглаживание линий.
1.	Скопируйте папку Chapter06\Lessonl-Exercise!-PieChart с компакт-диска, прилагаемого к книге, на жесткий диск и откройте версию C# или Visual Basic проекта Pie-Chart. Также можно продолжить работу с проектом, созданным в упражнении 1.
2.	Прежде всего, в методе drawPieChart создайте массив, представляющий цвета круговой диаграммы. Цвета задаются последовательно, поэтому не программируйте два похожих цвета подряд. Ради простоты мы проигнорируем случай, когда элементов диаграммы больше, чем цветов в массиве, например:
' VB
Dim colors As Color() = {Color.Red, Color.Orange, Color.Yellow, Color.Green, _ Color.Blue, Color.Inoigo, Color.Violet, Color DarkRed, Color.DarkOrange, _ Color.DarkSalmon, Color.DarkGreen, Color.DarkBlue, Color.Lavender, _ Color.LightBlue, Color.Coral}
If elements.Count > colors.Length Then
Throw New ArgumentException("Pie chart must have " + colors.Length.ToStringO + ” or fewer elements") End If
11 Ctt
Color[] colors = { Color Red, Color Orange, Color.Yellow, Color.Green
284 Программирование графических элементов	Глава 6
Color.Blue, Color.Indigo, Color.Violet, Color.DarkRed,
Color.DarkOrange, Color.DarkSalmon, Color DarkGreen,
Color.DarkBlue, Color.Lavender, Color.LightBlue, Color.Coral };
if (elements.Count > colors.Length)
throw new ArgumentException("Pie chart must have " + colors.Length.ToString() + ” or fewer elements");
}
ПРИМЕЧАНИЕ Упрощения в примере кода
Для достижения целей этого практкума некоторые аспекты этого упражнения искусственно упрощены. Например, здесь не реализована возможность выбора цвета сегментов, для чего нужно добавить объект Color к классу PieChartElement. Также исключены обработка исключений и проверка введенных данных.
3.	Необходимо отслеживать использование цветов. Перед циклом foreach следует инициализировать нулем целочисленную переменную и использовать ее в качестве счетчика:
' VB
Dim colorNum As Integer = О
// C#
int colorNum = 0;
4.	В теле цикла foreach добавьте две строки: одну для создания объекта Brush object, а вторую для вызова метода Graphics.FillPie. Вызовите Graphics.FillPie непосредственно перед вызовом Graphics. Draw Pie, чтобы нарисовать контур поверх залитого сектора. В следующем примере используется класс LinearGradientBrush, требующий импорта в проект пространства имен System.Drawing.Drawing2D:
' VB
Нарисовать первый сегмент от 0 градусов
Dim startAngle As Single = 0
Dim colorNum As Integer = 0
Нарисовать все элементы диаграммы
For Each e As PieChartElement In elements
Создать градиентную кисть
Dim b As Brush = New LinearGradientBrush(rect, colors(colorNum), Color.White, 45) colorNum += 1
Подсчитать длину дуги сектора
по его доле в окружности
Dim sweepAngle As Single = (e.value / total) * 360
Нарисовать сектора с заливкой
g.FillPie(b. rect, startAngle, sweepAngle)	**
Занятие 1
Рисование графических элементов 285
7 ' Нарисовать сектора
g.DrawPie(p, rect, startAngle, sweepAngle)
’ Подсчитать длину дуги следующего сектора прибавлением
' текущего значения к текущей сумме.
startAngle += sweepAngle
Next
И C#
И Нарисовать первый сегмент начиная с радиуса 0°
float startAngle = 0;
int colorNum = 0;
// Нарисовать все элементы диаграммы
foreach (PieChartElement е in elements)
{
Ц Создать градиентную кисть
Brush b = new Linear€radienlBrush(rect, colors[colorNumH-], Color.White, (float)45 );
// Подсчитать длину дуги сектора
// по его доле в окружности
float sweepAngle = (е.value / total) * 360;
Ц Нарисовать сектора с заливкой
g.FillPie(b, rect, startAngle, sweepAngle);
/I Нарисовать контуры секторов
g.DrawPie(p, rect, startAngle, sweepAngle);
// Подсчитать длину следующего сектора прибавлением
// текущего значения к текущей сумме.
startAngle += sweepAngle;
)
5.	Теперь, запустите полученное приложение. Поэкспериментируйте с кистями различных типов и выберите наиболее привлекательный для вас вариант. Заметьте, что нарисованные линии кажутся зубчатыми; это можно исправить, установив для свойства Graphics.SmoothingMode следующее значение:
’ VB
g.SmoothingMode = SmoothingMode.HighQuality
// C#
д.SmoothingMode = SmoothingMode.HighQuality;
Резюме
	Классы из пространства имен System.Drawing позволяют рисовать графические элементы и редактировать существующие изображения. Наиболее полезны классы Graphics, Image, и Bitmap.
286 Программирование графических элементов
Глава 6
 Классы Point и Size позволяют задавать положена и размере элементов управления.
 Структура System Drawing, Color поддерживает предопределенные свойства, представляющие системные цвета.
 Для рисования линий и фигур с помощью объекта Реп необходимо создать класс Graphics, затем объект Реп, после чего вызвать один из методов Graphics.
 Перья можно настраивать, выбирая нестандартные концы линий, а также шаблоны для пунктирных линий.
 Для рисования залитых фигур с помощью объекта Brush, создайте экземпляр класса Graphics, затем объект Brush, после чего вызовите один из методов Graphics.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Какой из перечисленных методов позволит нарисовать одноцветный квадрат?
A. Graphics.DrawLines;
В. Graphics. Dr aw Rectangle,
C. Graphics. Draw Polygon’,
D. Graphics. Draw Ellipse’,
E. Graphics.FillRectangle',
F. Graphics.FillPolygon’,
G. Graphics.FillEllipse.
2.	Какой из перечисленных ниже методов позволит нарисовать треугольник без заливки?
A. Graphics.DrawLines',
В. Graphics.DrawRectangle',
С. Graphics.DrawPolygon',
D. Graphics.DrawEllipse;
Е. Graphics.FillRectangle’,
Е Graphics.FillPolygon’,
G. Graphics.FillEllipse.
3.	Какие из перечисленных классов необходимы, чтобы нарисовть окружность без заливки? (Укажите все верные ответы.)
A.	System. Drawing.Graphics;
В.	System.Drawing.Реп;
С. System.Drawing.Brush;
D. System. Drawing.Bitmap.
4.	Какую из перечисленных кистей можно использовать для рисования прямоугольника с градиентной заливкой от красного до белого цвета?
A.	System.Drawing.Drawing2D. HatchBrush;
В.	System.Drawing.Drawing2D.LinearGradientBrush;	j &
С.	System.Drawing.Drawing2D.PathGradientBrush;
Занятие 2
Работа с изображениями 287
D.	System.Drawing.SolidBrush',
Е.	System.Drawing. TextureBrush.
5.	Линию какого типа рисует следующий код?
’ VB
Dim g As Graphics = Me.CreateGraphics
Dim p As Pen = New Pen(Color.Red, 10)
p.StartCap = LineCap Flat
p.EndCap = LineCap.ArrowAnchor g.DrawLine(p, 50, 50, 400, 50)
// C#
Graphics g = this.CreateGraphicsO;
Pen p = new Pen(Color.Red, 10);
p.StartCap = LineCap.Flat;
p.EndCap = LineCap.ArrowAnchor;
g.DrawLine(p, 50, 50, 400, 50);
А.	стрелка, направленная вверх;
В.	стрелка, направленная вниз;
С.	стрелка, направленная влево;
D.	стрелка, направленная вправо.
Занятие 2. Работа с изображениями
Довольно часто программистам приходится выводить на экран, создавать и редактировать изображения. Средства .NET Framework позволяют работать с изображениями различных форматов, в том числе выполнять большинство простых операций по их редактированию.
Изучив материал этого занятия, вы сможете:
J описать назначение классов Image и Bitmap',
J размещать рисунки на формах и объектах PictureBox’,
S создавать рисунки, добавлять к ним линии и фигуры, сохранять их в файл.
Продолжительность занятия — 30 минут.
Классы Image и Bitmap
Абстрактный класс System.Drawing.Image позволяет создавать, загружать, модифицировать и сохранять изображения форматов .BMP, .JPG, и .TIF. С помощью класса Image можно:
	создавать и сохранять рисунки и диаграммы;
	использовать текст (см. упражнение 3) для добавления к рисунку знаков защиты авторских прав и водяных знаков;
288 Программирование графических элементов
Глава 6
	изменять размер JPEG-изображений для экономии места и ускорения загрузки графических файлов.
Класс Image абстрактный, но можно создавать его экземпляры с помощью методов Image.FromFile (принимает путь к файлу изображения) и Image.FromStream (принимает объект System. IO. Stream). Существуют еще два класса, унаследованных от Image'. System. Drawing. Bitmap для статичных изображений и System. Drawing. Imaging. Metafile — для анимированных изображений.
Класс Bitmap чаще всего используют для работы с новыми и существующими изображениями. Различные конструкторы позволяют создавать Bitmap из существующего объекта Image, файла или потока, а также создавать пустые битовые карты заданной высоты и ширины. Bitmap, в отличие от Image, поддерживает два особо удобных метода:  GetPixel
возвращает объект Color для отдельного пиксела изображения. Пиксел — это точка изображения, окрашенная в цвет, который образован красным, зеленым и синим компонентами;
	SetPixel
задает цвет пиксела.
Для редактирования сложных изображений следует создать объект Graphics путем вызова Graphics.Fromlmage.
Вывод изображений на экран
Для вывода на экран изображения, сохраненного на диске в составе формы, загрузите его с помощью Image.FromFile и создайте элемент управления PictureBox, после чего используйте Image для определения PictureBox.Backgroundimage. Как это делается, показывает следующий пример (для него нужна форма с объектом PictureBox, названным pictureBoxl)'.
' VB
Dim I As Image = Image.FromFile( C:\windows\gone fishing bmp") PictureBoxl.Backgroundimage = I
// C#
Image i - Image.FromFile(@"C:\windows\gone fishing.bmp"); pictureBoxl.Backgroundimage = i;
Следующий код делает то же самое с помощью класса Bitmap'.
' VB
Dim В As Bitmap = Image.FromFile("C:\windows\gone fishing.bmp") PictureBoxl.Backgroundimage = В
// C#
Bitmap b = new Bitmap(@"C:\windows\gone fishing.bmp"); pictureBoxl.Backgroundimage = b;
Вместо этого можно с помощью метода Graphics. Draw Image сделать изображение фоном формы или элемента управления. У данного метода более 30 перегруженных методов, так что способов задать расположение и размер изображения — множество. В следующем коде данный метод используется, чтобы сделать изображение фоновым для формы произвольного размера:
Занятие 2
Работа с изображениями 289
' VB
Dim Вт As Bitmap = New Bitmap("С:\WINDOWS\Web\Wallpaper\Azul.jpg")
Dim G As Graphics = Me.CreateGraphics
G.Drawlmage(Вт, 1, 1, Me.Width, Me.Height)
// C#
Bitmap bm = new Bitmap(@"C.\WINDOWS\Web\Wallpaper\Azul.jpg")
Graphics g = this.CreateGraphics();
g.Drawimage(bm, 1, 1, this.Width, this.Height);
Создание и сохранение рисунков
Чтобы получить пустой рисунок, создайте экземпляр класса Bitmap, вызвав один из конструкторов, создающих изображение «с нуля». После этого изображение можно отредактировать с помощью метода Bitmap.SetPixel или Graphics.Fromlmage.
Для сохранения рисунка вызовите метод Bitmap.Save, у которого есть несколько простых перегруженных версий. Для двух из них параметром служит тип System. Draw-ing.Imaging.ImageFormat. Чтобы задать тип файла, определите одно из следующих свойств типа System. Drawing. Imaging, image Format: Bmp, Emf, Exit, Gif, Icon, Jpeg, MemoryBmp, Png, Tiff, или Wmf. Формат JPEG чаще всего используют для фотографий, a GIF — для диаграмм, экранных снимков и рисунков.
Следующий код создают пустой объект Bitmap размером 600 к 600, на его основе — объект Graphics; далее при помощи методов Graphics.FillPolygon и Graphics. Dr aw Poly gon на созданном объекте Bitmap рисуются фигуры, результат записывается в файл bm.jpg, расположенный в текущей папке. Данный код можно запускать как консольное приложение; он использует пространства имен System. Drawing. Drawing2D и System.Drawing.Imaging. ' VB
Dim Bm As Bitmap = New Bitmap(600, 600)
Dim G As Graphics = Graphics.Fromlmage(bm)
Dim В As Brush = New LinearGradientBrush(New Point(1, 1), New Point(600, 600), Color White, Color.Red)
Dim Points As Point() = New Point() {New Point(10, 10),
New Point(77, 500),
New Point(590, 100),
New Point(250, 590),
New Point(300, 410)}
G FillPolygon(B, Points)
Bm.Save("bm jpg", ImageFormat.Jpeg)
// C#
Bitmap bm = new Bitmap(600, 600);
Graphics g = Graphics.Fromlmage(bm);
Brush b - new LinearGradientBrush(new Point(1, 1), new Point(600, 600),
Rl Color White, Color.Red);
Point[] points = new Point[]
290 Программирование графических элементов
Глава 6 р
{new Point(10, 10), new Point(77, 500), new Point(590, 100), new Point(250, 590), new Point(300, 410)};
g.FillPolygon(b, points);
bm.Save("bm.j pg", ImageFo rmat.Jpeg);
Чтобы отредактировать существующее изображение, в предыдущем примере достаточно заменить вызов конструктора Bitmap на загрузку рисунка.
Работа со значками
Значки — это прозрачные битовые карты определенного размера, которые Windows применяет для отображения состояния системы. В .NET Framework параметром класса Systemicons являются стандартные системные значки размером 40 х 40.
Проще всего добавлять значок к форме или изображению вызовом метода Graphics.Drawicon или Graphics.DrawIconUnstretched. На рис. 6-8 показан результат выполнения следующего кода:
’ VB
Dim G As Graphics = Me.CreateGraphics
G.Drawicon(Systemicons.Question, 40, 40)
// Cfl
Graphics g = this.CreateGraphicsO;
g.Drawicon(Systemicons.Question, 40, 40);
. Рис. 6-8. Systemicons предоставляет доступ к системным значкам, отображающим состояние
Редактировать системные и загружать ранее сохраненные значки можно также с помощью конструкторов класса Icon. Создав экземпляр класса Icon, нужно вызвать метод Icon. ToBitmap, чтобы получить объект Bitmap для редактирования.
Практикум. Сохранение круговой диаграммы
В этом практикуме вы напишете код для сохранения объекта Bitmap в JPEG-файл на диске. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске,, прилагаемом к книге.
Упражнение. Сохранение круговой диаграммы
В этом упражнении вы добавите код для сохранения круговой диаграммы на диск.
1.	Скопируйте папку Chapter06\Lesson2-Exercisel-PieChart с компакт-диска, прилагаемого к книге, на жесткий диск и откройте версию для C# или Visual Basic проекта PieChart.
Занятие 2	Работа с изображениями 291
2.	Добавьте к методу saveButton_Click код, запрашивающий у пользователя имя файла, и запишите круговую диаграмму на диск. Ради простоты стоит сохранять рисунки в JPEG-файлы. Как это делается, показано в следующем примере (требует пространства имен System. Drawing. Imaging)'.
VB
Показать диалог сохранения файла
Dim saveDialog As SaveFileDialog = New SaveFileDialog
saveDialog.DefaultExt = ".jpg"
saveDialog.Filter = "JPEG files (* jpg)|*.jpg;*.jpeg|All files (*.*)|*.*”
If Not (saveDialog ShowDialog = DialogResult.Cancel) Then Сохранить изображение в указанном JPEG-файле
chart.Image.Save(saveDialog.FileName, ImageFormat.Jpeg)
End If
// C#
// Показать диалог сохранения файла
SaveFileDialog saveDialog = new SaveFileDialogO;
saveDialog.DefaultExt = ".jpg”;
saveDialog.Filter = "JPEG files (*.jpg)|*.jpg;*.jpeg|All files (*.*)!*.*";
if (saveDialog.ShowDialog() != DialogResult.Cancel)
{
// Сохранить изображение в указанном JPEG-файле chart.Image.Save(saveDialog.FileName, ImageFormat.Jpeg);
}
3.	Запустите и протестируйте полученное прилофение; убедитесь, что сохраненный файл открывается.
Резюме
Классы Image и Bitmap позволяют создавать, редактировать и сохранять рисунки.
	Чтобы вывести рисунок на поверхности формы Windows Forms, загрузите его в экземпляр класса Image или Bitmap, создайте экземпляр элемента управления PictureBox, после чего запишите объект Image или Bitmap в свойство PictureBox.Backgroundlmage.
	Для создания и сохранения рисунка объявите объект Bitmap, отредактируйте его с помощью объекта Graphics, и вызовите метод Bitmap.Save.
	Для отображения значка вызовите метод Graphics.DrawIcon или Graphics.DrawIconUnstretched с помощью одного из свойств класса Systemicons.
Закрепление материала
Ниже приводятся вопросы для самопроверки Их также можно найти на компакт-диске, прилагаемом к книге.
292 Программирование графических элементов
Глава 6
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Какие из перечисленных классов можно использовать для вывода на форму JPEG-изображения из существующего файла? (Укажите все верные ответы.)
A. System.Drawing.Image',
В. System.Drawing.Bitmap',
С. System.Drawing.Imaging.Metafile’,
D. System. Windows.Forms.PidureBox.
2.	Как нарисовать черую рамку для сохраненного ранее JPEG-изображения и обновить изображение на диске?
А. Создать объект Graphics, загрузив JPEG-изображение с диска. Нарисовать обрамление, вызвав Graphics.DrawRectangle. Сохранить обновленное изображение, вызвав Graphics.Save.
В. Создать объект Bitmap, загрузив JPEG-изображение с диска. Нарисовать обрамление, вызвав В it тар. Draw Red angle. Сохранить обновленное изображение, вызвав Bitmap.Save.
С. Создать объект Bitmap, загрузив JPEG-изображение с диска. Создать объект Graphics, вызвав Graphics.Fromlmage. Нарисовать обрамление, вызвав Graph-ics.DrawRectangle. Сохранить обновленное изображение, вызвав Bitmap.Save.
D. Создать объект Bitmap, загрузив JPEG-изображение с диска. Создать объект Graphics, вызвав Bitmap.CreateGraphics. Нарисовать обрамление, вызвав Graphics.DrawRectangle. Сохранить обновленное изображение, вызвав Bitmap.Save.
3.	Какой из форматов следует выбрать для сохранения фото, предназначенного для открытия в разных приложениях?
A. ImageFormat. Bmp',
В. ImageFormat.Gif’,
С. ImageFormat.Jpeg',
D. ImageFormat. Png.
4.	Какой из форматов следует выбрать для сохранения круговой диаграммы, которая будет открываться многими приложениями?
A. ImageFormat.Bmp',
В. ImageFormat.Gif,
С. ImageFormat.Jpeg’,
D. ImageFormat. Png.
Занятие 3. Форматирование текста
При аннотировании объектов и создании отчетов разработчики часто добавляют к изображениям текст. Это занятие посвящено добавлению форматированного текста на изображения.
Занятие 3
Форматирование текста 293
* Изучив материал этого занятия, вы сможете:
J описать создание объектов, необходимых для добавления текста на изображения;
J создавать объекты Font согласно требованиям к гранитуре, кеглю и начертанию шрифта;
J использовать Graphics.DrawString для добавления текстовых комментариев;
J форматировать текст.
Продолжительность занятия — 30 минут.
Добавление текста на графические элементы
Чтобы добавить текст на изображение необходимо, как и при добавлении фигур с заливкой, создать экземпляр класса Graphics. Выполните следующие действия:
1.	Создайте объект Graphics, как описано на предыдущих занятиях.
2.	Создайте объект Font.
3.	При необходимости создайте объект Brush.
4.	Вызовите Graphics. DrawString и задайте расположение текста.
Пример из практики
Тони Нортроп (Топу Northrup)
Когда я не пишу код, я фотографирую. Чтобы хоть частично окупить безумные деньги, вложенные мной в фотоборудование, я продаю свои фотографии через Web. К сожалению, для аудио- и видеофайлов есть системы защиты авторских прав (DRM), а вот для защиты фотографий ничего такого нет. А пока таких технологий нет, можно добавлять к изображениям, опубликованным в Web, заметные водяные знаки и сообщения о защите авторских прав. Это не остановит нарушителей авторских прав, но хотя бы затруднит использование ворованных изображений.
Создание объекта Font
Класс Font поддерживает 13 различных конструкторов. Простейший способ создать объект Font — передать название семейства шрифтов (как строку), кегль (в виде целого числа или числа с плавающей точкой) и начертание (свойство System.Drawing.FontStyle). Например, следующий конструктор создает шрифт Arial с полужирным начертанием и кеглем 12 пунктов:
' VB
Dim F As Font - New Font("Arial", 12, Fontstyle.Bold)
// C#
Font f ~ new Font("Arial", 12, Fontstyle.Bold);
294 Программирование графических элементов
Глава 6
Создать объект Font можно также, используя Font Family, как показано в следующем примере: ’ VB
Dim Ff As FontFamily = New FontFamily("Arial")
Dim F As Font = New Font(Ff, 12)
// C#
FontFamily ff = new FontFamily("Arial");
Font f = new Font(ff, 12);
Чтобы прочитать тип шрифта из строки, можно воспользоваться классом FontConverter. Однако это не рекомендуется, поскольку передача имени шрифта в строке ненадежно, т. к. не позволяет компилятору обнаружить опечатки. То есть, ошибка в названии шрифте не будет обнаружена, пока во время выполнения не будет сгенерировано исключение ArgumentException. Следующий пример создает объект шрифта Arial с кеглем 12:
' VB
Dim Converter As FontConverter = New FontConverter
Dim F As Font = CType(converter.ConvertFromString("Arial, 12pt"), Font)
11 C#
FontConverter converter = new FontConverter();
Font f = (Font)converter.ConvertFromString("Arial, 12pt'');
Прорисовка текста
Создав объект Font, необходимо получить объект Brush (см. занятие 1) для заливки текста. Чтобы избежать создания Brush, можно просто задать свойство System.Drawing.Brushes. Чтобы добавить текст к изображению, вызовите Graphics.DrawString. Следующий код рисует текст на заданной форме (рис. 6-9): ’ VB
Dim G As Graphics = Me.CreateGraphics
Dim F As Font = New Font("Arial", 40, Fontstyle.Bold)
G.DrawString("Hello, World!", F, Brushes.Blue, 10, 10)
// C#
Graphics g = this.CreateGraphics();
Font f = new Font("Arial", 40, Fontstyle.Bold);
g.DrawString("Hello, World!", f, Brushes.Blue, 10, 10);
Fun with рнпй	» П X
Hello, World!
Рис. 6-9. Вызов Graphics.DrawString для добавления текста на объект Graphics
Конечно, намного проще добавлять текст в форму с помощью объектов Label Впрочем, Graphics.DrawString также позволяет добавлять текст на объекты Image и Bitmap. Это
Занятие 3
Форматирование текста 295
удобно при добавлении инфоррмании о защите авторских прав, а также временных отметок и примечаний к рисункам и диаграммам.
Форматирование текста
Средства .NET Framework позволяют управлять выравниванием и направлением текста с помощью класса StringFormat. Создав и настроив объект StringFormat, можно использовать метод Graphics.DrawString для управления форматом текста. Наиболее важными членами класса StringFormat являются:
 Alignment — получает или устанавливает горизонтальное выравнивание текста. Возможные значения:
□	StringAlignment.Center — горизонтальное выравниватие по центру;
□	StringAlignment. Near — горизонтальное выравниватие по левому краю;
□	StringAlignment.Far — горизонтальное выравниватие по правому краю.
 FormatFlags — получает или устанавливает перечислимый тип StringFormatFlags, содержащий информацию о форматировании. Возможные значения StringFormatFlags'
□	DirectionRightToLeft — текст направлен справа налево;
□	Direction vertical — текст ориентирован вертикально;
□	DisplayFormatControl — вывод управляющих символов наряду с глифами;
□	FitBlackBox — часть символов может выходить за прямоугольник, ограничивающий строку (по умолчанию, символы перегруппируются так, чтобы символы не выходили за границы этого прямоугольника);
□	LineLimit — в ограничивающем прямоугольнике находятся только целые строки. По умолчанию, отображается весь текст либо часть текста, которая умещается в ограничивающем прямоугольнике. По умолчанию последняя строка может быть частично скрыта, если она не умещается в прямоугольнике, высота которого не кратна высоте строки. Чтобы строки отображались полностью, укажите это значение и задайте высоту прямоугольника разметки, равную как минимум высоте одной строки;
□	MeasureTrailingSpaces — добавляет пробел в конце каждой строки. По умолчанию ограничивющий прямоугольник, возвращаемый методом MeasureString, удаляет пробелы в конце строк. Если этот флаг установлен, концевые пробелы учитываются при измерениях;
□	NoClip — делает видимыми части глифов и текст, расположенный вне прямоугольника разметки. По умолчанию эти элементы отсекаются и не видны;
□	NoFontFallback — запрещает использование альтернативных шрифтов для вывода символов, не поддерживаемых заданным шрифтом. Отсутствующие символы обычно отображаются как пустые квадраты;
□	No Wrap — запрещает перенос текста внутри прямоугольника разметки. Предполагается, что данный флаг установлен, когда вместо прямоугольника передается точка либо если сторона прямоугольника нулевой длины.
	LineAlignment получает или устанавливает вертикальное выравнивание текста. Возможные значения:
□	StringAlignment.Center — по центру;
□	StringAlignment.Near — по верхней границе;
□	StringAlignment.Far — по нижней границе.
296 Программирование графических элементов
Глава 6
	Trimming — получает или устанавливает перечислимый тип StringTrimming для объекта StringFormat. Возможные значения:
□	Character — текст отсекается до ближайшего символа;
□	Ellipsischaracter — текст отсекается до ближайшего символа, отсеченный текст заменяется многоточием;
□	Ellipsis Path — центральная часть строк вырезается, заменяется многоточием и выравнивается. Алгоритм сохраняет максимально возможную часть вырезанного сегмента;
□	Ellipsis Word — текст отсекается до ближайшего целого слова, отсеченная часть заменяется многоточием;
□	None — текст не отсекается;
□	Word — текст отсекается до ближайшего целого слова.
Ниже демонстрируется применение класса StringFormat (рис. 6-10):
' VB
Dim G As Graphics = Me.CreateGraphics
' Создать прямоугольник
Dim R As Rectangle = New Rectangle(New Point(40, 40), New Size(80, 80))
' Создать два объекта StringFormat
Dim F1 As StringFormat = New StringFormat(StringFormatFlags.NoClip)
Dim F2 As StringFormat = New StringFormat(fl)
' Установить разные значения свойств LineAlignment и
' Alignment для обоих объектов StringFormat
F1.LineAlignment = StringAlignment.Near
f1.Alignment = StringAlignment.Center
f2.LineAlignment = StringAlignment.Center
f2.Alignment = StringAlignment.Far
f2.FormatFlags = StringFormatFlags.Directionvertical
’ Нарисовать граничный прямоугольник и строку для каждого
' объекта StringFormat
G.DrawRectangle(Pens.Black, R)
G.DrawStringC'FormatT’, Me.Font, Brushes.Red, CType(R, RectangleF), F1)
G.DrawString("Format2",* Me.Font, Brushes.Red, CType(R, RectangleF), F2)
11 C#
Graphics g = this.CreateGraphicsO;
// Создать прямоугольник.
Rectangle г = new Rectangle(new Point(40, 40), new Size(80, 80));
// Создать два объекта StringFormat
StringFormat f1 = new StringFormat(StrlngFormatFlags.NoClip);
StringFormat f2 = new StringFormat(fl);
Занятие 3
Форматирование текста 297
// Установить разные значения свойств LineAlignment и // Alignment для обоих объектов StringFormat.
f1 LineAlignment = StringAlignment.Near;
f1.Alignment = StringAlignment.Center;
f2.LineAlignment = StringAlignment.Center;
f2.Alignment = StringAlignment.Far;
f2.FormatFlags = StringFormatFlags.Directionvertical;
// Нарисовать граничный прямоугольник и строку для каждого
// объекта StringFormat.
g.DrawRectangle(Pens.Black, г);
g.DrawString("Format1", this.Font, Brushes.Red, (RectangleF)r, fl);
g.DrawString("Format2”, this.Font, Brushes.Red, (RectangleF)r, f2);
Рис. 6-10. Использование StringFormat для управления выравниванием и направлением текста
Практикум. Добавление текста на изображение
В данном практикуме вы добавите информацию о защите авторских прав на рисунок и сохраните его на диск, а затем добавите легенду к круговой диаграмме. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение 1. Добавление информации о защите авторских прав
1.	Скопируйте папку Chapter06\Lesson3 - Exercise 1 -PieChart с компакт-диска, прилагаемого к книге, на жесткий диск и откройте С#- или Visual Basic-версию проекта Pie-Chart. Можно также продолжить работу с проектом, созданным в упражнении 2.
2.	Не измененяя круговую диаграмму PictureBox, добавьте на сохраняемое изображение информацию о защите авторских прав. В левом верхнем углу должно быть следующее сообщение: «Copyright 2006, Contoso, Inc.». Замените содержимое блока if следующим коде:
' VB
If Not (saveDialog.ShowDialog = DialogResult.Cancel) Then
Определить объекты Bitmap, Graphics, Font, и Brush для информации о защите авторских прав
Dim bm As Bitmap = CType(chart.Image, Bitmap)
Dim g As Graphics = Graphics.Fromlmage(bm)
Dim f As Font = New Font("Arial", 12)
Dim b As Brush = New SolidBrush(Color.White)
298 Программирование графических элементов
Глава 6
' Добавить текст о защите авторских прав
g DrawString( Copyright 2006, Contoso, Inc.", f, b, 5, 5)
' Сохранить изображение в заданный файл формата JPEG chart.Image.Save(saveDialog.FileName, ImageFormat.Jpeg) End If
11 C#
if (saveDialog.ShowDialogO != DialogResult.Cancel)
// Определить объекты Bitmap, Graphics, Font, и Brush
// для информации о защите авторских прав
Bitmap bm = (Bitmap)chart.Image;
Graphics g = Graphics.Fromlmage(bm);
Font f = new Font("Arial”, 12);
Brush b = new SolidBrush(Color White);
// Добавить текст о защите авторских прав
g.DrawString("Copyright 2006, Contoso, Inc.", f, b, 5, 5);
// Сохранить изображение в заданный файл формата JPEG bm.Save(saveDialog.FileName, ImageFormat.Jpeg);
3.	Запустите полученное приложение и сохраните рисунок. Обраните внимание: сообщение о защите авторских прав трудно рассмотреть там, где оно перекрывается с рисунком. Это можно исправить, повторно прорисовав текст цветом, контрастным по сравнению с цветом заднего плана и сместив его на пиксел в любом направлении. Следующий пример добавляет белый текст о защите авторских прав на черном фоне:
*	•;	«г -
VB
Определить объекты Bitmap, Graphics, Font, and Brush для информации о защите авторских прав
Dim bm As Bitmap = CType(chart.Image, Bitmap)
Dim g As Graphics = Graphics.Fromlmage(bm)
Dim f As Font = New Font("Arial", 12)
' Создать кисть для’текста переднего плана
Dim b As Brush = New SolidBrush(Color.White)
' Создать кисть для текста заднего плана
Dim bb As Brush = New SolidBrush(Color.Black)
Добавить задний план
Dim ct As String = Copyright 2006, Contoso, Inc."
g.DrawString(ct, f, bb, 4, 4)
g.DrawString(ct, f, bb, 4, 6)
Занятие 3
Форматирование текста 299
g.DrawString(ct, f, bb, 6, 4)
д.DrawString(ct, f, bb, 6, 6)
’ Добавить текст о защите авторских прав д.DrawString(ct, f, b, 5, 5)
' Сохранить изображение в заданный файл формата JPEG chart.Image.Save(saveDialog.FileName, ImageFormat.Jpeg)
// C#
// Объявить объекты Bitmap, Graphics, Font, и Brush
// для информации о защите авторских прав
Bitmap bm = (Bitmap)chart.Image;
Graphics g = Graphics.Fromlmage(bm);
Font f = new Font("Arial", 12);
// Создать кисть для текста переднего плана
Brush b = new SolidBrush(Color.White);
// Создать кисть для текста заднего плана
Brush bb = new SolidBrush(Color.Black);
// Добавить фоновый текст
string ct = "Copyright 2006, Contoso, Inc.";
g.DrawString(ct, f, g.DrawString(ct, f, g.DrawString(ct, f, g.DrawString(ct, f,	bb, 4, 4); bb, 4, 6); bb, 6, 4); bb, 6, 6);
// Добавить текст о g.DrawString(ct, f,	защите авторских прав b, 5, 5);
// Сохранить изображение в заданный файл формата JPEG bm.Save(saveDialog.FileName, ImageFormat.Jpeg);
4.	Перезапустите приложение и снова сохраните изображение. Заметьте, что в месте перекрытия с диаграммой текста прорисован на черном фоне и потому вполне различим.
Упражнение 2. Добавление легенды к круговой диаграмме
Ваша задача — модифицировать метод drawPieChart, созданный в предыдущих упражнениях, чтобы разделить изображение на две части. На левой половине будет круговая диаграмма, а на правой — легенда, отображающая цвета, подписи и значения сегментов круговой диаграммы.
1.	Скопируйте папку Chapter06\Lesson3-Exercise2-PieChart с компакт-диска, прилагаемого к книге, на жесткий диск и откройте С#- или Visual Basic-версию проекта Pie-Chart. Также можно продолжить работу с проектом, созданным в упражнении 1.
300 Программирование графических элементов
Глава 6
2.	Перепишите метод drawPieChart так, чтобы круговая диаграмма занимала только левую половину изображения. Это можно сделать так:
’ VB
Определить поямоугольник для круговой диаграммы
' Использовать только левую половину, чтобы освободить место для легенды Dim rect As Rectangle = New Rectangle(1, 1, (s.Width/2) - 2, s.Height - 2)
11 C#
// Определить прямоугольник для круговой диаграммы
// Использовать только левую половину, чтобы освободить место для легенды Rectangle rect = new Rectangle(1, 1, (s.Width/2) - 2, s.Height - 2);
3.	Далее на правой половине изображения нарисуйте черный прямоугольник на белом фоне:
' VB
’ Определить прямоугольник для легенды
Dim IRectCorner As Point = New Point((s.Width / 2) + 2, 1)
Dim IRectSize As Size = New Size(s.Width - (s.Width / 2) - 4, s.Height -2)
Dim IRect As Rectangle = New Rectangle(lRectCorner, IRectSize)
' Нарисовать черный прямоугольник на белом фоне.
Dim lb As Brush = New SolidBrush(Color.White)
Dim Ip As Pen = New Pen(Color.Black, 1) g.FillRectangle(lb, IRect) g.DrawRectangle(lp, IRect)
// C#
// Определить прямоугольник для легенды
Point IRectCorner = new Point((s.Width / 2) + 2, 1);
Size IRectSize = new Size(s.Width - (s.Width / 2) - 4, s.Height - 2); Rectangle IRect = new Rectangle(lRectCorner, IRectSize);
// Нарисовать черный прямоугольник на белом фоне.
Brush lb = new SolidBrush(Color.White);
Pen Ip = new Pen(Color.Black, 1);
g.FillRectangle(lb, IRect);
g.DrawRectangle(lp, IRect);
4.	Вычислите значения, необходимые для рисования элементов легенды, включая:
□	число пикселов по вертикали для каждого элемента легенды;
□	ширину прямоугольника легенды;
а высоту прямоугольника легенды;
□	расстояние между элементами легенды;
□	отступ для текста легенды; а ширину текста легенды.
Занятие 3
Форматирование текста 301
Ниже приводится соответствующий пример кода:
' VB
Определить число пикселов по вертикали на каждый элемент легенды
Dim vert As Integer = (IRect.Height - 10) / elements.Count
Вычислить ширину прямоугольника легенды (20% общей ширины)
Dim legendWidth As Integer = IRect.Width / 5
’ Вычислить высоту прямоугольника легенды (75% общей высоты)
Dim legendHeight As Integer = CType((vert * 0.75), Integer)
Вычислить расстояние между элементами легенды
Dim buffer As Integer = CType((vert - legendHeight), Integer) / 2
' Вычислить отступ текста легенды
Dim textX As Integer = IRectCorner.X + legendWidth + buffer * 2
Вычислить ширину текста легенды
Dim textwidth As Integer = IRect.Width - (IRect.Width / 5) - (buffer * 2)
11 C#
// Определить число пикселов по вертикали на каждый элемент легенды int vert = (IRect.Height - 10) / elements.Count;
// Вычислитьть ширину прямоугольника легенды (20% от общей ширины) int legendWidth = IRect.Width / 5;
// Вычислить высоту прямоугольника легенды (75% от общей высоты) int legendHeight = (int) (vert * 0.75);
// Вычислить расстояние между элементами легенды
int buffer = (int)(vert - legendHeight) / 2;
// Вычислитть отступ слева текста легенды
int textX = IRectCorner.X + legendWidth + buffer * 2;
// Вычислить ширину текста легенды
int textwidth = IRect.Width - (IRect.Width / 5) - (buffer * 2);
5.	Обработав в цикле объекты PieChartElements, нарисуйте элементы легенды. В следующем примере ради простоты используются отдельные циклы, в реальном же коде для повышения быстродействия следует выполнять данные операции в существующем цикле foreach:
’ VB
' Рисуем легенду, отступив 5 пикселов от верхней границы прямоугольника
Dim currentVert As Integer = 5
Dim legendColor As Integer = 0
302 Программирование графических элементов
Глава 6
For Each е As PieChartElement In elements
Создать градиентную кисть
Dim thisRect As Rectangle = New Rectangle(lRectCorner.X + buffer, currentVert + buffer, legendWidth, legendHeight)
Dim b As Brush = New LinearGradientBrush(thisRect,
colors(System. Math.Min(System.Threading. Interlocked. Increment(legendColor), legendColor - 1)), Color.White, CType(45, Single))
Нарисовать прямоугольник легенды с заливкой и обрамлением g.FillRectangle(b, thisRect) g.DrawRectangle(lp, thisRect)
' Определить прямоугольник для текста
Dim textRect As RectangleF = New Rectangle(textX, currentVert + buffer, textwidth, legendHeight)
Определить шрифт для текста
Dim tf As Font = New Font("Arial", 12)
Создать кисть текста переднего плана
Dim tb As Brush = New SolidBrush(Color.Black)
Определить вертикальное и горизонтальное выравнивание текста
Dim sf As StringFormat = New StringFormat
sf.Alignment = StringAlignment.Near sf.LineAlignment = StringAlignment.Center
Нарисовать текст
g.DrawString(e.name +	" + e.value.ToStringO, tf, tb, textRect, sf)
Вычислить новую позицию по вертикали currentVert += vert
Next
11 C#
// Рисуем легенду, отступив 5 пикселов от верхней границы прямоугольника int currentVert = 5;
int legendColor = 0;
foreach (PieChartElement e in elements)
// Создать градиентную кисть
Rectangle thisRect = new Rectangle(lRectCorner.X + buffer, currentVert + buffer, legendWidth, legendHeight);
Brush b = new LinearGradientBrush(thisRect, colors[leQendColor++], Color.White, (float)45);
Занятие 3
Форматирование текста 3Q3
// Нарисовать прямоугольник легенды с заливкой и обрамлением
g.FillRectangle(b, thisRect);
g.DrawRectangledp, thisRect);
// Определить прямоугольник для текста
RectangleF textRect = new Rectangle(textX, currentVert + buffer, textwidth, legendHeight);
// Определить шрифт для текста
Font tf = new Font("Arial", 12);
// Создать кисть для текста переднего плана
Brush tb = new SolidBrush(Color.Black);
// Определить вертикальное и горизонтальное выравнивание текста StringFormat sf = new StringFormat();
sf.Alignment = StringAlignment.Near;
sf.LineAlignment = StringAlignment.Center;
I/ Нарисовать текст
g.DrawString(e.name +	" + e.value, tf, tb, textRect, sf);
// Вычислить новую позицию по вертикали currentVert += vert;
}
6.	Запустите полученное приложение и проверьте его работу. После добавления легенды большая часть текста о защите авторских прав накладывается на диаграмму, и его эффективно оттеняет черный фон.
Резюме
	Чтобы добавить текста к графическим элементам создайте объекты Graphics, Font, при необходимости Brush, после чего вызовите метод Graphics.DrawString.
	Чтобы создать объект Font, необходимо передать название семейства шрифтов, кегль и начертание.
	Для прорисовки текста необходимо вызвать метод Graphics. DrawString. Метод DrawString требует объекты Font и Brush для определения цвета и расположения текста.
	Класс StringFormat применяют для управления форматом текста. Данный класс можно использовать для изменения направления или выравнивания текста.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
304 Программирование графических элементов
Глава 6
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Как добавить текст на изображение?
А. создать объекты Graphics и string. После этого вызвать string. Draw.
В. создать объекты Graphics, Font и Brush. После этого вызвать Graphics .DrawString’, С. создать объекты Graphics, Font и Реп. После этого вызвать Graphics .DrawString’, D. создать объекты Bitmap, Font и Brush. После этого вызвать Bitmap . DrawString.
2.	Какой из следующих классов требуется для создания строки, выровненной по центру? A. StringFormat’,
В. StringAlignment,
С. FormatFlags',
D. LineAlignment.
3.	Какая из перечисленных ниже команд выравнивает текст по левому краю?
A. StringFormat. LineAlignment — Near,
В. StringFormat.LineAlignment — Far,
C. StringFormatAlignment = Near,
D. StringFormatAlignment — Far,
Закрепление материала главы
Для закрепления знаний и навыков, полученных при изучении этой главы, выполните следующее:
	изучите резюме главы;
	повторите основные термины, использованные в этой главе;
	выполните задания лабораторной работы по темам этой главы;
	выполните предложенные упражнения;
	пройдите пробный экзамен.
Резюме главы
	Для создания графических элементов необходимы два объекта: Реп и Graphics. Реп, определяющий цвет и ширину рисунка, а также Graphics, позволяющий работать с методами для рисования линий и фигур. Объекты Brush и Graphics предназначены для заливки фигур.
	Классы Image и Bitmap предназначены для работы с изображениями. Объекты Image или Bitmap, производные от Graphics, предназначены для редактирования изображений.
	Для добавления текста на поверхность изображения или графического элемента создают объекты Font и Brush, после чего вызывают Graphics.DrawString. Класс StringFormat предназначен для форматирования текста.
Лабораторная работа 3Q5
Основные термины
	растровое изображение (битовая карта);
	кисть;
	графический элемент;
	перо.
Лабораторная работа
Сейчас вы примените на практике знания и навыки, полученные при изучении этой главы. Ответы на вопросы см. в разделе «Ответы» в конце книги.
Занятие 1. Выбор метода работы с графикой
Вы — разработчик корпоративных приложений в компании Contoso, Inc. Вам и вашей команде поручили разработать приложение для учета заказов и инвентаризации. На днях, сотрудник отдела по связям с обществаенностью попросил вашего руководителя обсудить изменения, которые необходимо внести в интерфейс корпоративных приложений. Руководитель поручил вам продумать необходимые изменения.
Результаты опроса
 Сотрудник отдела по связям с общественностью
«Мы работаем над новым корпоративным стилем. Он включает множество элементов: рисунков, цветов, шрифтов и логотипов. Для унификации и продвижения нашего бренда как внутри, так вне компании необходима стандартизация. В частности, на заставке корпоративных приложений желательно поместить фотографию штаб-квартиры корпорации и логотип в левом верхнем углу, логотип должен отображаться при загрузке приложения. Кроме того, нужно использовать исключительно шрифт Arial кегля 12. Юридический отдел еще не утвердил логотип, поэтому он может измениться. Я могу предоставить текущий вариант логотипа и фотографию штаб-квартиры корпорации в формате .JPEG.»
 ИТ-менеджер
«У PR-отдела семь пятниц на неделе: они переделают логотип еще раз десять, так что советую хранить рисунки в файлах, расположенных в установочном каталоге, и загружать их динамически при запуске приложения. Я бы не рассчитывал, что даже кегль шрифтов и размер изображений останутся постоянными».
Вопросы
Ответьте на следующие вопросы руководства:
1.	В фото штаб-квартиры корпорации — 6 мегапикселов, а это многовато для заставки. Как изменить его размер?
2.	Как отобразить логотип поверх фото?
3.	Если размер логотипа изменится, как гарантировать, что он не закроет всю фотографию, а займет только ее верхний левый угол?
4.	Как изменить шрифты приложения?
306 Программирование графических элементов
Глава 6
Занятие 2. Создание простых диаграмм
Вы — разработчик в ИТ-отделе компании Fabrikam, Inc. Недавно, вы выпустили первую версию корпоративного приложения Sales, которое используется отделом сбыта для учета заказов. Приложение Sales заменило бумажный документооброт В отделе сбыта остались довольны новым приложением.
Теперь вице-президент по продажам пожелал обсудить требования к функциональности новой версии приложения.
Результаты опроса
 Вице-президент по продажам
«Ваше приложение для учета заказов великолепно, оно значительно увеличило эффективность организации сбыта. Теперь требуется доступ к информации в базе данных о деятельности разных подразделений продаж за определенные периоды времени. Мне нужна поквартальная информация о продажах в виде графиков или столбчатых диаграмм, у каждого отдела должен быть свой цвет на диаграмме. Также необходима возможность сохранения графиков для передачи руководству».
Вопросы	к!.
Ответьте на следующие вопросы руководства:
1.	Какой элемент управления предназначен для отображения диаграмм в приложениях Windows Forms?
2.	Какой метод предназначен для рисования графиков?
3.	Какой метод предназначен для рисования столбчатых диаграмм?
4.	Как сохранить диаграмму?
Рекомендуемые упражнения
Чтобы лучше проработать темы экзамена, выполните следующие упражнения.
Улучшение пользовательского интерфейса с помощью кистей, перьев, цветов и шрифтов
Выполните как минимум упражнения 1—3. Дополнительный опыт работы с кистями, перьями, цветами и шрифтами позволит получить упражнение 4.
	Упражнение 1 Напишите приложение Windows Forms для демонстрации различных приемов работы с Graphics.SmoothingMode. Нарисуйте окружность и отобразите значение SmoothingMode. Каждые пять секунд окружность и значения SmoothingMode должны обновляться. Проследите зависимость границ окружности от различных настроек SmoothingMode.
	Упражнение 2 Нарисуйте на форме окружность с заливкой, изменяя цвет и отображая его название в нижней части формы каждые две секунды.
	Упражнение 3 Нарисуйте на форме окружность с заливкой, изменяя кисть и отображая ее название в нижней части формы каждые пять секунд.
	Упражнение 4 Создайте приложение, в котором свойства Pen.DashOffset и Pen.DashPattem определяют пользовательский шаблон пунктирной линии.
Пробный экзамен 307
Улучшение пользовательского интерфейса с помощью графических элементов и значков
Выполните следующие упражнения.
	Упражнение 1 Напишите приложение Windows Forms для просмотра изображений, сохраненных на диске.
	Упражнение 2 Напишите приложение, генерирующее для всех изображений в заданной папке миниатюры размером 80 Ч 80.
	Упражнение 3 Напишите приложение, которое будет читать изображения из папки, добавлять на рисунки информацию о защите авторских прав и сохранять изображения в другой папке.
Улучшение пользовательского интерфейса с помощью фигур
Выполните как минимум упражнения 1 и 2. Дополнительный опыт работы с фигурами позволит получить упражнение 3.
	Упражнение 1 Напишите приложение, которое рисует на форме многоугольники с вершинами, заданными щелчками мыши.
	Упражнение 2 Добавьте в массив серию прямоугольников со случайными параметрами, и вызовите Graphics Draw Rectangles, чтобы отобразить их.
«	Упражнение 3 Создайте метод для рисования столбчатой диаграммы, аналогичный методу DrawPieChart.
Пробный экзамен
На прилагаемом компакт-диске, содержится пробный экзамен, обеспечивающий широкие возможности по самоподготовке. Например, можно проверить усвоение темам этой главы либо всех тем экзамена 70-536. Можно сымитировать настоящий экзамен либо выбрать режим обучения, когда после каждого вопроса приводятся правильные ответы с пояснениями.
Пробный экзамен
Подробнее о пробном экзамене рассказано во введении.
ГЛАВА 7
Потоки
Занятие 1. Создание потоков	309
Занятие 2. Общий доступ к данным	324
Занятие 3. Асинхронная модель программирования	347
Концепция потоков имеет большое значение для разработки программного обеспечения. Основной смысл потоков в том, что они позволяют выполнять сразу несколько операций. Эти операции можно представить как отдельные «потоки» в общей логике программы. Часто исполнение программ приостанавливается в ожидании какого-либо события (например, отклика Web-сервера или доступа к ресурсу). Используя потоки, можно заставить процессор (или процессоры) решать другие задачи, пока текущая операция простаивает в ожидании какого-либо события или ресурса.
В наши дни многопроцессорные системы получают все большее распространение. Если приложение не использует потоки, оно не сможет воспользоваться преимуществами дополнительных процессоров. Поддержка потоков в .NET Framework позволяет создавать надежные многопоточные приложения.
Темы экзамена:
 Разработка многопоточных .NET-приложений (см. пространство имен System.IO):
□	класс Thread',
а класс ThreadPool',
□	делегаты ThreadStart и ParameterizedThreadStart’,
□	класс Timeout, класс Timer, делегат TimerCallback, делегат WaitCallback, класс WaitHandle и делегат WaitOrTimerCallback’,
□	классы hreadExceptionEventArgs и ThreadExceptionEventHandler,
□	перечислимые ThreadState и ThreadPriority',
□	класс ReaderWriterLock',
□	классы AutoResetEvent и ManualResetEvent’,
□	интерфейс LAsyncResult (см. пространство имен System)’,
□	классы EventWaitHandle и Registered Wait Handle, делегаты SendOrPostCallback и lOCompletionCallback’,
□	класс Interlocked, структура NativeOverlapped и класс Overlapped’,
Занятие 1
Создание потоков
309
□	классы ExecutionContext, HostExecutionContext, HostExecutionContextManager, делегат ContextCallback\
□	структура LockCookie, классы Monitor, Mutex и Semaphore.
Пример из практики
Шон Уилдермъюс (Shawn Wildermuth)
Я часто использовал систему потоков .NET в работе над своими проектами. Так, класс ThreadPool я применял для организации очередей задач, ускоряющих выполнение длительных операций и работу приложений Windows Forms, запустив часть его кода в фоновом потоке. Словом, потоки — важный инструмент из моего арсенала.
Прежде всего
Чтобы освоить материал этой главы, необходимо иметь навыки работы с Microsoft Visual
Basic или C# и уметь:
	создавать консольные приложения в Microsoft Visual Studio, используя Visual Basic или С#;
	добавлять в проект ссылки на системные библиотеки классов;
	создавать текстовые файлы;
 добавлять события в журнал событий.
Занятие 1. Создание потоков
Потоки — основа любых приложений, претендующих на высокую производительность. В .NET Framework пространство имен System. Threading содержит типы, используемые для создания и управления потоками в приложениях.
Изучив материал этого занятия, вы сможете:
J создавать потоки для одновременного выполнения различных операций;
J запускать и объединять потоки;
J завершать потоки;
J использовать критические области.
Продолжительность занятия — 20 минут.
Введение в потоки
Начинать работу с потоками нужно со знакомства с классом Thread, представляющим одиночные потоки. Класс Thread используется для создания и запуска потоков. Наиболее важные свойства и методы этого класса перечислены в табл. 7-1 и 7-2.
310
Потоки
Глава 7
Табл. 7-1. Свойства класса Thread
Имя	Описание
IsAlive	Указывает, что данный поток выполняется в текущий момент
IsBackground	Определяет, должен ли поток выполняться как фоновый
IsThreadPoolThread	Показывает, находится ли поток в пуле потоков
ManagedThreadld	Получает идентификатор для текущего потока, отличный от ID потока в операционной системе
Имя	Имя, связанное с потоком
Priority	Приоритет потока
ThreadState	Определяет значение ThreadState для потока
Табл. 7-2. Методы класса Thread	
Имя	Описание
Abort	Генерирует для потока исключение ThreadAbortException, которое указывает, что поток должен быть завершен
Interrupt	Вызывает исключение Thread Interrupted Exception, если поток заблокирован (ThreadState.WaitJoinSleep). Если поток не блокируется, он никогда не будет прерван этим методом
Join	Блокирует вызывающий поток, пока поток не завершится
Resume	Не рекомендуется к использованию
Start	Планирует потока к выполнению
Suspend	Не рекомендуется к использованию
В дополнение к свойствам и методам экземпляра класс Thread также имеет статические свойства и методы (см. табл. 7-3 и 7-4).	
Табл. 7-3. Статические свойства класса Thread	
Имя	Описание
CurrentContext	Текущий объект ThreadContext, относящийся к текущему потоку
CurrentPrincipal	Пользователь, связанный с текущим потоком
CurrentThread	Исполняемый в данный момент поток
Табл. 7-4. Статические методы класса Thread	
Имя	Описание
BeginCriticalRegion	Оповещать хост о том, что код, который нужно выполнить, ' нельзя завершить безопасно. Завершение потока между вызовами BeginCriticalRegion и EndCriticalRegion может оставить Арр Domain в нестабильном состоянии
EndCriticalRegion	Оповещает хост о завершении критической области
GetDomain	Получает AppDomain, связанный с исполняемым в данное время
	потоком
Занятие 1
Создание потоков 31 “I
Табл. 7-4. (окончание)
Имя	Описание
GetDomainlD	Получает уникальный идентификатор Арр Domain, связанного с исполняемым в данный момент потоком
ResetAbort Sleep	Отменяет запрос Abort для исполняемого в данный момент потока Блокирует текущий поток на заданное время. Передает управление другим потокам для завершения их работы
SpinWait	Блокирует текущий поток на определенное число итераций, не передавая управление другим потокам
VolatileRead	Считывает последнюю версию значения поля независимо от того, какой из процессоров многопроцессорной системы записал его
VblatileWrite	Немедленно записывает значение в поле, делая его доступным всем процессорам
Помимо класса Thread в работе с потоками важно перечислимое ThreadState. Значения перечислимого ThreadState приведены в табл. 7-5.
Табл. 7-5. Перечислимое ThreadState
Имя	Описание
Aborted AbortRequested	Поток остановлен Получен запрос завершения потока, но он еще не завершен и исключение ThreadAbortException не получено
Background Running Stopped StopRequested	Поток работает в фоновом режиме Поток запущен Поток остановлен Получен запрос остановки процесса (только для внутреннего использования)
Suspended	Поток приостановлен. Поддерживается для обратной совместимости; поскольку методы Suspend/Resume применять не рекомендуется, это состояние также не должно использоваться
SuspendedRequested	Получен запрос приостановки потока. Поддерживается для обратной совместимости; поскольку методы Suspend/Resume применять не рекомендуется, это состояние также не должно использоваться
Unstarted WaitSleepJoin	Поток создан, но метод Thread.Start еще не вызван Поток блокирован вызовом Monitor. Wait, Thread.Sleep или ThreadJoin
Создание потока
Чтобы создать и запустить поток, выполните следующее:
1.	Создайте метод, не принимающий аргументов и не возвращающий никаких данных (для этого, например в С#, объявите возвращаемое значение как void). Такой метод выглядит примерно так:
’ VB
Shared Sub SimpleWorkO
312
Потоки
Глава 7
Console.WriteLine( Thread: {0}", Thread.СиrrentThread.ManagedThreadld) End Sub
11 C#
static void SimpleWork()
{
Console.WriteLine("Th read: {0}", Th read.Си r rentTh read.ManagedTh readld);
}
2.	Создайте класс делегата ThreadStart на основе метода, созданного на первом шаге.
3.	Создайте объект Thread, указав объект ThreadStart, созданный на шаге 2.
4.	Вызовите Thread.Start, чтобы запустить новый поток. Готовый код выглядит следующим образом:
’ VB
Dim operation As New ThreadStart(SimpleWork)
Создаем новый поток, но не запускаем его
Dim theThread As New Thread(operation)
Запускаем задачу в новом потоке
theThread.Start()
И C#
ThreadStart operation = new ThreadStart(SimpleWork);
/I Создаем новый поток, но не запускаем его
Thread theThread = new Thread(operation);
// Запускаем задачу в новом потоке
theThread. StartO;
При вызове метода Start, для нового потока вызывается метод SomeWork и поток выполняется, пока этот метод не завершится. В этом примере метод SimpleWork выводит на экран фразу «In Thread» и отображает свойство ManagedThreadld. Это свойство представляет номер, который присваивается каждому потоку. Позже мы воспользуемся этим потоком, чтобы узнать задачи, выполняемые разными потоками.
Использование нескольких потоков
Мы рассмотрели очень простой пример, но более типична ситуация, в которой для решения поставленной задачи требуется создать несколько потоков. Приведенный выше код можно переписать так, чтобы выполнять одну операцию в нескольких потоках:
’ VB
Dim operation As New ThreadStart(SimpleWork)
For x As Integer = 1 To 5
Создаем новый поток, но не запускаем его
Dim theThread As New Thread(operation)
Занятие 1
Создание потоков
313
’ Запускаем задачу в новом потоке
theThread.Start()
Next
// Cd
Threadstart operation - new ThreadStart(SimpleWork);
for (int x = 1; x <= 5; ++x)
{
// Создаем новый поток, но не запускаем его
Thread theThread = new Th read(operation);
// Запускаем задачу в новом потоке theThread.Start();
}
В этом примере заданная операция выполняется в пяти отдельных потоках. Синхронность работы этих потоков зависит от возможностей компьютера. После внесения этих изменений мы получим пять потоков, каждый из которых выводит в окне консоли собственный идентификатор:
Thread: 3
Thread: 4
Thread: 5
Thread: 6
Thread: 7
Номера потоков выводятся последовательно, так как выполняемая методом SimpleWork операция завершается очень быстро. Попробуем запрограммировать более сложную операцию, чтобы изучить синхронную работу потоков:
VB
Shared Sub SimpleWorkO
For x As Integer = 1 To 10
Console.WriteLine("Thread: {0}",
Th read.Си r rentTh read.ManagedTh readld)
' Замедляем работу потока, позволяя другим потокам продолжить выполнение Thread.Sleep(10)
Next
End Sub
// C#
static void SimpleWorkO
{
for (int x = 1; x <= 10; ++x)
{
Console.WriteLine("Thread: {0}",
Th read.Cu r rentTh read ManagedTh readld);
314
Потоки
Глава 7
// Замедляем работу потока, позволяя другим потокам продолжить выполнение Thread.Sleep(10);	-
}
}
В новой версии метода Simple Work мы выводим на экран идентификатор потока 10 раз. Кроме того, мы используем Thread.Sleep, чтобы замедлить работу кода. Метод Thread.Sleep позволяет приостановить выполнение потока на заданное время (в миллисекундах), тогда как остальные потоки будут продолжать работу. Работа каждого потока останавливается на 10 миллисекунд, и другие потоки в это время выводят свои данные на консоль. Чтобы посмотреть, как это работает, можно изменить метод SimpleWorkiax, чтобы он показывал номер текущей итерации:
Thread: 3
Thread: 4
Thread: 5
Thread: 6
Thread: 7
Thread: 3
Thread: 4
Thread: 5
Thread: 6
Thread: 7
Это позволит достичь максимально параллельного выполнения операций, возможного для компьютера с данной конфигурацией.
ПРИМЕЧАНИЕ Потоки и процессоры
Раньше созданием многопоточных приложений обычно занимались разработчики серверного ПО. Теперь это не так. С появлением технологии HyperThreading и двухъядерных процессоров для настольных систем и лэптопов многопоточность стала актуальной и для большинства приложений.
Сложности выполняемых операций и время работы потоков увеличилось, поэтому нужно сделать так, чтобы главнный поток (содержащий код для создания потоков) ожидал завершения остальных потоков. Для этого служит метод Thread.Join.
Использование метода Thread.Join
Часто приложение должно ожидать завершения выполнения потока. Для этого в классе Thread предусмотрен метод Join'.
' VB
theThread.Join()
П C#
theThread.Join();
Метод Join сообщает системе, что приложение должно ожидать завершения потока. Конечно, в таком простом примере второй поток на самом деле не нужен, так как сите-ма простаивает в ожидании его завершения. Для большей наглядности создадим пять потоков, выполняющих некоторые операции, и организуем ожидание их завершения. При работе с многопоточным кодом задача программиста несколько усложняется, так
Занятие 1
Создание потоков 315
как необходимо дождаться завершения всех потоков. Сделать это можно, сохранив ссылки на все потоки и вызвав для каждого из них метод Join, чтобы дождаться завершения очередного потока:
’ VB
Dim operation As ThreadStart = New ThreadStart(SomeWork)
Dim theThreads(S) as Thread
For x As Integer = 0 To 4
' Создаем поток, но не запускаем его theThreads(x) = New Thread(operation)
’ Запускаем задачу в новом потоке
theThreads(x).Start()
Next
' Ожидаем поочередного завершения работы потоков
For Each t As Thread In theThreads
t.Join()
Next
// C#
ThreadStart operation = new ThreadStart(SomeWork);
ThreadEJ theThreads = new Thread[5];
for (int x = 0; x < 5; ++x) --r
{
// Создаем поток, но не запускаем его theThreads[x] = new Thread(operation);
// Запускаем задачу в новом потоке theThreadsEx].Start();
}
// Ожидаем поочередного завершения работы потоков
foreach (Thread t in theThreads)
{
t.Join();
>
Сохранив потоки в массиве, можно дождаться поочередного завершения всех потоков. По завершении потока метод Join возвращает управление, после чего работа кода может продолжаться.
Приоритет потоков
Класс Thread поддерживает установку и получение приоритета потока. Для этого Используется перечислимое ThreadPriority. Его значения приведены в табл. 7-6.
316
Потоки
Глава 7
Табл. 7-6. Значения перечислимого ThreadPriority
Имя	Описание
Highest AboveNormal Normal BelowNormal Lowest	Наивысший приоритет Приоритет выше обычного {Normal) Приоритет по умолчанию Приоритет ниже обычного Самый низкий приоритет
Последовательность выполнения потоков зависит от этого перечислимого. В большинстве случаев следует использовать приоритет по умолчанию {Normal). Повышение или понижение приоритета может привести к тому, что операционной системе придется выделить некоторым потокам гораздо больше (или меньше) ресурсов, чем ожидалось. А если назначить некоторому потоку наивысший приоритет, остальные операции в системе могут просто остановиться. Порой повышать и понижать приоритет приходится, но делайте это с осторожностью. Надолго повысив приоритет одного потока, вы вряд ли сможете повысить производительность системы в целом, так как при этом возникнет нехватка ресурсов у других потоков, что может привести к непредсказуемым результатам.
Передача данных потокам
Во предыдущих примерах мы использовали делегат StartThread, не принимающий параметров. На практике же чаще приходится передавать потокам какую-либо информацию. Для этого нужно использовать другой делегат — ParameterizedStartThread. У этого делегата имеется сигнатура метода, который принимает единственный параметр типа Object и ничего не возвращает. Это иллюстрирует следующий фрагмент кода:
' VB
Shared Sub WorKWithParameter(ByVal о As Object)
Dim info As String = CType(o, String)
For x = 0 To 9
Console.WriteLine("{0}: {1}”, info,
Th read.Си r rentTh read.ManagedTh readld)
Замедляем работу потока и позволяем другим потокам продолжить выполнение Thread.Sleep(10)
Next
End Sub
11 Ctf
static void WorkWithParameter(object o)
string info = (string) o;
for (int x = 0; x < 10; ++x)
{
Console.WriteLine("{0): {1}", info,
Th read.Си r rentTh read.ManagedTh readld);
Занятие 1
Создание потоков
317
И Замедляем работу потока и позволяем другим потокам продолжить выполнение Thread.Sleep(1O);
}
}
Этот метод принимает единственный параметр Object (то есть, любой объект). Чтобы использовать этот метод для запуска потока, можно создать делегат ParameterizedThreadStart со ссылкой на новый метод, и использовать перегруженный метод Thread.Start, принимающий одиночный параметр — объект. Это иллюстрирует следующий фрагмент кода:
’ VB ParameterizedThreadStart operation = _
New Ра ramete rizedTh readSta rt(Wo rkWithParamete r)
Создаем поток, но не запускаем его
Dim theThread As Thread = New Thread(operation)
Запускаем задачу в новом потоке
theTh read.Sta rt("Hello")
Второй поток с другим параметром
Dim NewThread As New Thread(operation)
NewTh read.Sta rt("Goodbye")
// C#
ParameterizedThreadStart operation =
new Pa ramete rizedTh readStart(WorkWithPa ramete r);
// Создаем поток, но не запускаем его
Thread theThread = new Thread(operation);
// Запускаем задачу в новом потоке
theTh read.Start("Hello");
// Второй поток с другим параметром
Thread newThread = new Thread(operation);
newThread.Start("Goodbye");
Учтите, что поскольку метод WorkWithParameter принимает произвольный объект, Thread.Start может быть вызван с любым объектом, а не только с ожидаемой им строкой. Внимательно выбирайте метод, запускающий поток, предуматривайте обработку неизвестных типов — это залог надежной работы многопоточного кода. Чтобы не приводить наудачу тип объекта, который метод получил как параметр, к строковому, прежде проверьте тип объекта-параметра:
' VB
Dim info As String = о as String
If info Is Nothing Then
Throw InvalidProgramException("Parameter for thread must be a string")
End If
318 Потоки	Глава 7 /
// C#
string info = о as string;
if (info == null)
{
throw InvalidProgramException("Parameter for thread must be a string");
}
Остановка потоков
В задачи управления потоками в приложениях часто входит остановка потоков. Обычно это делается с помощью метода Thread.Abort. При вызове Thread.Abort система потоков готовится к генерации в потоке исключения ThreadAbortException. Независимо от того, обрабатывается ли это исключение, поток останавливается после его генерации. Это иллюстрирует следующий фрагмент кода:
’ VB
Dim NewThread As Thread = New Thread(New ThreadStart(AbortThisThread))
NewThread.StartO
NewThread.Abort()
Shared Sub AbortThisThread()
' Определяем данные
SomeClass.IsValid = True
SomeClass.IsComplete = True
Выводим объект на консоль
SomeClass.WriteToConsole()
End Sub
// C#
Thread newThread = new Thread(new ThreadStart(AbortThisThread));
newThread.Start();
newThread Abort();
static void AbortThisThread()
{
// Определяем данные
SomeClass.IsValid = true;
SomeClass.IsComplete = true,
// Выводим объект на консоль
SomeClass.WriteToConsole();
}
Так как метод AbortThisThread никогда не перехватывает исключение ThreadAbortException, этот поток остановится на строке, исполняемой в момент вызова Abort основным пото
Занятие 1
Создание потоков 319
ком. Это чревато проблемами, так как система потоков «не знает», когда можно безопасно завершить поток. Если завершить поток в неподходящий момент, в данных может возникнуть несогласованность. Например, если исключение ThreadAbortException сгенерировано между установкой свойств IsValid и IsComplete, объект SomeClass может остаться в несогласованном состоянии. Если ThreadAbortException сгенерировано после установки свойств, но до вызова метода Write ToConsole, объект также будет несогласованным и его содержимое никогда не попадет на консоль.
Дтя решения проблемы несогласованности объектов и нестабильности AppDomain класс Thread содержит два важных статических метода: BeginCriticalRegion и EndCriticalRegion. Вызовы этих методов запрещают завершать поток в пределах критической области. Это иллюстрирует следующий фрагмент кода:
' VB
Shared Sub AbortThisThread()
' Определение данных
Thread. BeglnCriticalRegionO
SomeClass.IsValid = True
SomeClass.IsComplete = True
Thread. EndCriticalRegionO
' Вывод объекта на консоль
SomeClass.WriteToConsole()
End Sub
// C#
static void AbortThisThread()
// Определение данных
Thread. BeglnCriticalRegionO;
SomeClass.IsValid = true;
SomeClass.IsComplete = true;
Th read. EndCriticalRegionO;
// Вывод объекта на консоль
SomeClass.WriteToConsole();
с
Идея критических областей состоит в том, что помещенный в них код выполняются как одна строка. Запросы завершения потока будут удовлетворены, только когда он пройдет критическую область. После выхода из критической области поток будет завершен с генерацией исключения ThreadAbortException. Разница между работой потоком в критической области и вне ее показана на рис. 7-1.
ПРИМЕЧАНИЕ .NET 2.0
В .NET Framework версии 2.0 появилось серьезное изменение: методы Thread.Suspend и Thread.Resume больше не используются (и помечены как устаревшие). Чтобы приостанавливать и возобновлять работу потоков, нужно использовать методы синхронизации потоков (см. занятие 2).
320
Потоки
Глава 7
Контекст выполнения
С каждым потоком связаны определенные данные, которые обычно передаются новым потокам. К таким данным относятся сведения о безопасности (интерфейс IPrincipal и идентификатор потока), параметры локализации (культура, с которой запущен поток) и сведения о транзакциях из System.Transactions. Для доступа к текущему контексту выполнения служат статические методы класс ExecutionContext.
По умолчанию контекст выполнения передается вспомогательным потокам, правда ценой некоторых издержек. Чтобы остановить передачу контекста (так удастся повысить производительность, отбросив сведения о безопасности, культуре и транзакциях, составляющих контекст), можно использовать класс ExecutionContext. Чтобы блокировать передачу контекста, вызовите метод ExecutionContext. SuppressFlow, а чтобы возобновить ее — метод ExecutionContext.Restore Flow.
' VB
Dim flow As AsyncFlowControl = ExecutionContext.SuppressFlowQ
Новый поток не получит контекст выполнения
Dim thread As Thread = New Thread(New ThreadStart(SomeWork)) thread.Start() thread.Join()
Занятие 1
Создание потоков Q21
Восстанавливаем передачу контекста
Executioncontext.RestoreFlow()
также можно использовать flow Undo()
// C#
AsyncFlowControl flow = ExecutionContext. SuppressFlowO;
// Новый поток не получит контекст выполнения
Thread thread = new Thread(new ThreadStart(SomeWork));
thread.Start();
thread.Join();
// Восстанавливаем передачу контекста
Executioncontext.Resto reFlow();
// также можно использовать flow.UndoO;
Также можно вызвать статический метод Run класса ExecutionContext, позволяющий выполнять в текущем потоке произвольный код с заданным контекстом. Чтобы воспользоваться методом Run, нужно получить копию контекста выполнения. Для этого применяют метод ExecutionContext.Capture. Затем вызывается метод Run с нужным контекстом и делегат ContextCallback с методом, который должен быть запущен в этом контексте. Это иллюстрирует следующий фрагмент кода:
’ VB
Получаем текущий контекст
Dim ctx As ExecutionContext = Executioncontext.CaptureO
’ Вызываем метод ContextCalled
ExecutionContext.Run(ctx, New ContextCallback(ContextCalled), Nothing)
// C#
// Получаем текущий контекст
ExecutionContext ctx = ExecutionContext.CaptureO;
// Вызываем метод ContextCalled
ExecutionContext.Run(ctx, new ContextCallback(ContextCalled), null);
ПРИМЕЧАНИЕ Контекст, специфичный для хоста
Если в свойстве HostExecutionContextManager класса AppDomainManager есть ссылка на действительный объект HostExecutionContextManager, исполняющая среда будет запускать каждый поток в отдельном контексте с именем HostExecutionContext. Как и в случае с ExecutionContext, можно захватывать (методом Capture) и освобождать (методом Revert) данные контекста HostExecutionContext, но только хост (код, создавший AppDomairi) может определить, нужно ли это делать.
322
Потоки
Глава 7
Практикум. Многопоточный код, использующий класс Thread
Сейчас вы создадите простое приложение, которое считает от 1 до 10 и выводит промежуточный счет в окне консоли. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение. Создание нескольких потоков
В этом упражнении вы создадите простое консольное приложение и запустите два потока одновременно.
1.	Создайте консольное приложение с именем SimpleThreadingDemo.
2.	Объявите статический метод с именем Counting.
3.	В новом классе импортируйте пространство имен System. Threading.
4.	В новом методе создайте цикл for, в котором выполняется отсчет от 1 до 10.
5.	В цикле/ог выведите на экран текущий счет и идентификатор ManagedThreadld текущего потока.	, г,
6.	После вывода данных на консоль приостановите текущий поток на 10 миллисекунд.
7.	Вернитесь в метод Main и создайте делегат StartThread со ссылкой на метод Counting.
8.	Создайте два потока со ссылками на метод Counting.
9.	Запустите оба потока.
10.	Объедините потоки, чтобы приложение не завершалось до завершения обоих потоков. Полученный код выглядит примерно так:
1 VB
Imports System.Threading
Class Program
Public Overloads Shared Sub Main()
Dim starter As Threadstart = New ThreadStart(AddressOf Counting) Dim first As Thread = New Thread(starter) Dim second As Thread = New Thread(starter)
first. StartO
second.Start()
first.Join()
second.Join()
Console. ReadO
End Sub
Shared Sub CountingO
Dim i As Integer
For i = 1 To 10 Step i + 1
Занятие 1
Создание потоков
323
Console.WriteLine("Count: {0} - Thread: {!}", i, Thread.CurrentThread.ManagedThreadld)
Thread.Sleep(10)
Next
End Sub
End Class
// C#
using System;
using System.Collections Generic;
? using System.Text;
using System.Th reading;
class Program
{
static void Main(string[] args) {
ThreadStart starter = new ThreadStart(Counting);
Thread first = new Thread(starter);
Thread second = new Thread(starter);
first. StartO;
second. StartO;
first.Join();
second.Join();
Console.Read();
}
static void CountingO
{
for (int 1=1; i <= 10; i++)
{
Console.WriteLine("Count: {0} - Thread: {1}", i, Th read.Си rrentTh read.ManagedTh readld);
Thread.Sleep(10);
} }
}
11.	Соберите проект, исправьте ошибки. Убедитесь, что консольное приложение показывает вывод потоков, работающих параллельно. Это видно по чередованию чисел, выводимых потоками, которое зависит быстродействия оборудования компьютера. В однопроцессорной системе счет будет идти по порядку, а в многопроцессорной (или на компьютере с многоядерным процессором) цифры будут «перемешаны» сильнее.	..	'	’
324
Потоки
Глава 7
Резюме
	Для организации параллельного выполнения операций используйте класс Thread.
	Чтобы запустить поток, используйте метод Start класса Thread.
	Для ожидания завершения потоков используйте метод Join класса Thread.
	Чтобы отменить выполнение потока, используйте метод Abort класса Thread.
	Для организации общего доступа к данным из разных потоков используйте класс ExecutionContext.
Закрепление материала
Ниже приводятся вопросы для самопроверки Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1. Какого типа объект нужен для запуска потока, требующего одиночный параметр?
А. Делегат ThreadStarf,
В. Делегат ParameterizedThreadStart',
С. Класс Synchronizationcontext',
D. Класс ExecutionContext.
2. Какой метод останавливает запущенный поток?
A. Thread.Suspend',
В. Thread.Resume',
С. Thread.Abort',
D. Thread.Join.-
Занятие 2. Общий доступ к данным
Наиболее сложная часть работы с потоками — это организация общего доступа к данным для нескольких потоков. Если в приложении запущено несколько потоков, появляется необходимость защиты данных, доступных разным потокам. К счастью, сделать это в .NET Framework совсем не сложно.
Изучив материал этого занятия, вы сможете:
J использовать класс Interlock для выполнения атомарных операций;
J использовать ключевое слово lock (С#) или SyncLock (Visual Basic) для блокировки данных;
J использовать класс Monitor для блокировки данных;
J использовать ReaderWriterLock для блокировки данных;
J использовать класс Mutex для синхронизации потоков;
J использовать класс Semaphore для управления работой потоков;
J использовать класс Event для подачи сигналов потокам.
Продолжительность занятия — 40 минут.
-
Занятие 2
Общий доступ к данным 325
Предотвращение коллизий
До появления многопоточности можно к объектам можно было обращаться только поочередно. Но наступила эпоха многопоточности, и теперь приходится учитывать ситуации, когда к одному объекту могут обращаться сразу несколько потоков. Иногда это приводит к весьма неочевидным проблемам. В качестве примера рассмотрим следующий класс:
' VB
Public Class Counter
Public Shared Count As Integer
End Class
// C#
public class Counter
{
public static int Count;
}
Класс Counter содержит статическое поле Count, предоставляющее прямой доступ к нескольким элементам из набора. Можно написать простой метод, увеличивающий значение Count в цикле:
• VB
Shared Sub UpdateCountO
Dim x As Integer
For x = 1 To 10000
Counter.Count = Counter.Count + 1
Next
End Sub
// C#
static void UpdateCountO
{
for (int x = 1; x"<= 10000; ++x)
{
Counter.Count = Counter.Count + 1;
}
}
Независимо от числа вызовов метода UpdateCount, к исходному числу всегда нужно добавлять 10 000. Логично предположить, что многопоточный код выдаст результат, равный произведению 10 000 на число потоков. Например, следующий код считает сумму в 10 потоках:
’ VB
Dim starter As New ThreadStart(UpdateCount)
Dim threadsQ As Thread = New Thread(10) {}
Dim x As Integer
For x = 0 To 9
threads(x) = New Thread(starter)
326
Потоки
Глава 7
threads(x). StartO
Next	.
 Ожидаем завершения
For х = 0 То 9
threads(x).Join()
Next
' Выводим на консоль итоговое значение
Должно быть: 10 * 10 000 =100 000
Console.WriteLine("Total: {0}", Counter.Count)
// C#
ThreadStart starter = new ThreadStart(UpdateCount);
Thread[] threads = new Thread[10J;
for (int x = 0; x < 10; ++x)
{
threads[x] = new Thread(starter);
threads[x]. StartO;
}
// Ожидаем завершения
for (int x = 0; x < 10; ++x)
threads[x].Join();
}
// Выводим на консоль итоговое значение
// Должно быть: 10 * 10 000 =100 000
Console.WriteLine("Total: {0}", Counter.Count);
В однопроцессорной (одноядерной) системе этот код так и будет работать. Но если несколько раз выполнить этот код в многопроцессорной системе (или системе с многоядерным процессором), результат иногда будет меньше ожидаемых 100 000. Почему, спросите вы?
Это происходит из-за механизма обновления поля Count процессором. Обычно этот код выполняется в три этапа:
1.	Значение помещается в регистр процессора.
2.	В регистре это значение увеличивается (или уменьшается).
3.	Значение копируется из регистра в память.
Проблема заключается в том, что при многопоточной обработке все три этапа выполняются как атомарные операции. Как показано на рис. 7-2, сразу два потока могут считывать и обновлять значения в памяти, поэтому в приведенном примере код «проскакивает» часть итераций.
Занятие 2
Общий доступ к данным 327
Рис. 7-2. Приращение значений несколькими потоками
А что, если изменить синтаксис? Можно использовать следующий код: ’ VB
Counter.Count = Counter.Count + 1 ' or 
Counter.Count += 1
// C#
Counter.Count++;
// or
Counter.Count += 1;
Эти изменения ни к чему не приведут, так как скомпилированный код все равно выполняется в три этапа. Как решить эту проблему? Для этого в приращении можно использовать класс Interlocked. Статические методы класса Interlocked перечислены в табл. 7-7.
Табл. 7-7. Статические методы класса Interlocked	
Имя	Описание
Add	Выполняет атомарную операцию сложения двух целых чисел. Вычитание выполняется прибавлением отрицательных чисел
Decrement	Выполняет атомарную операцию вычитания
Exchange	Выполняет атомарную операцию обмена пары значений
Increment	Выполняет атомарную операцию увеличения значения на единицу
Read	Выполняет атомарную операцию чтения 64-разрядного числа. Необходим в 32-разрядных системах, так как в них 64-разрядные числа представлены двумя 32-разрядными
Чтобы решить проблему с потоками в методе AddCount, мы можем воспользоваться классом Interlocked следующим образом:
’ VB
Shared Sub UpdateCountQ
Dim x As Integer
For x = 1 To 10000
Counter.Count = Counter.Count + 1
Interlocked. Inc reinent (Counter. Count)
Next
End Sub
328
Потоки
Глава 7
// C#
static void UpdateCountO
{
for (int x = 1; x <= 10000; ++x)
{
// Counter.Count = Counter.Count + 1;
Inte rlocked.Inc rement(ref Cou nte r.Count);
}
}
ПРИМЕЧАНИЕ Асинхронно изменяемые данные
Класс Thread (как и компилятор С#) поддерживает асинхронное чтение и запись полей. Суть таких операций в том, что они предотвражают возникновение несогласованности данных в многопоточных приложениях путем запрета кэширования данных процессором. Вместо методов VolatileRead и VolatileWrite класса Thread (или ключевого слова volatile в С#) используйте методы класса Interlocked или высокоуровневую синхронизацию (см. ниже). Это гарантирует корректную работу кода независимо от архитектуры процессора и памяти системы, в которой работает приложение.
К СВЕДЕНИЮ Синхронизация потоков
Синхронизация потоков — сложная тема с множеством нюансов, зависимых от окружения. Подробнее об этом см. в о втором издании книге Джеффри Рихтера (Jeffrey Richter) *CLR via C#» (Microsoft Press, 2006).
Метод Interlocked.Increment принимает ссылку на значение, которое нужно увеличить, и гарантирует исполнение этой операции как атомарной, в результате вышеописанная проблема не возникает. Основной недостаток класса Interlocked в том, что он работает с ограниченным набором типов .NET. А что если вам нужно синхронизировать доступ к собственным классам или исполнять большой фрагмент кода как атомарную операцию? Как защитить от одновременного доступа нескольких потоков другие типы кода? Решение — в синхронизации с помощью блокировок.
Синхронизация с помощью блокировок
Блокировки предназначены для синхронизации доступа к объектам .NET. Если изменить приведенный выше класс Counter так, чтобы он считал два значения (первое будет увеличиваться на единицу при каждой итерации, а второе — только когда переменная цикла имеет четное значение), код будет выглядеть следующим образом:
' VB
Public Class Counter
Dim _count As Integer = 0
Dim _evenCount As Integer = 0
Public Readonly Property CountO As Integer
Get
Return _count
End Get
End Property
Занятие 2
Общий доступ к данным 329
Public Readonly Property EvenCount() As Integer Get
Return _evenCount
End Get
End Property
Public Sub UpdateCountO
Interlocked.Increment(„count)
If Count X 2 = 0 Then ' Четное число Interlocked. Increment(„evenCount) End If End Sub
End Class
11 C# public class Counter {
int _count = 0; int _evenCount = 0;
public int Count
get { return „count; } }
public int EvenCount {
get { return „evenCount; }
public void UpdateCountO <
Interlocked.Increment(ref „count);
if (Count X 2 == 0) // Четное число {
Interlocked.Increment(ref „evenCount);
> }
Теперь мы можем использовать этот класс в измененной версии предыдущего кода: 1 VB
Dim count As New CounterQ
ParameterizedThreadStart starter = New _ Pa ramete rizedTh readSta rt(UpdateCount) Dim threads() As New Thread(10) {}
330
Потоки
Глава 7
Dim х As Integer
For x = 0 To 9
threads(x) = New Thread(starter) threads(x).Start(count)
Next
' Ожидаем завершения
Dim x As Integer
For x = 0 To 9 threads(x).Join()
Next
' Выводим на консоль итоговые значения
Console.WriteLine("Total: {0} - EvenCount: {1}", count.Count, count.EvenCount)
Shared Sub UpdateCount(ByVal param As Object) Dim count As Counter = CType(param, Counter)
Dim x As Integer For x = 1 To 10000
Увеличиваем значение счетчика на два count.UpdateCount()
Next
End Sub
// C#
Counter count = new CounterO;
ParametenzedThreadStart starter = new
Pa ramete rizedTh readSta rt(UpdateCount), Thread[] threads = new Thread[10];
for (int x = 0; x < 10; ++x)
threads[x] = new Thread(starter); threadsfx].Start(count);
}
/I Ожидаем завершения
for (int x = 0; x < 10; ++x)
{
threads[x].Join();
}
// Выводим на консоль итоговые значения
Console.WriteLine("Total: {0} - EvenCount: {1}", count.Count, count.EvenCount);
Занятие 2
Общий доступ к данным 331
static-void UpdateCount(object param) {•
Counter count = (Counter)param;
for (int x = 1; x <= 10000; ++x)
{
// Увеличиваем значение счетчика на два count. UpdateCountO;
}
}
Как и раньше, мы создаем 10 потоков для ведения счета, но в этот раз потоки сообща используют единственный экземпляр класса Counter. При запуске этого кода мы увидим, что первое значение (простой счетчик) всегда верно, а в результатах четных итераций цикла встречаются ошибки. Дело в том, что два потока иногда быстро обновляют первый счетчик друг за другом, а проверка четности не успевает завершиться за это время. И хотя по отдельности эти операции безопасны в многопоточной среде, в целом они оказываются небезопасными.
Чтобы решить эту проблему, можно воспользоваться синхронизирующими блокировками. В C# для этого служит ключевое слово lock, а в Visual Basic — SyncLock. Чтобы применить синхронизирующую блокировку в классе Counter, нужно переписать его следующим образом:
• VB
Public Sub UpdateCountO
SyncLock Me
_count = _count + 1
If Count % 2 = 0 Then ' An even number
_evenCount = _evenCount + 1
End If
End SyncLock
End Sub
// C#
public void UpdateCountO
{
lock (this)
{
_count = _count + 1;
if (Count % 2 == 0) // An even number
{
.evenCount = _evenCount + 1;
}
}
}
Включение в код класса Interlocked синхронизирующей блокировки гарантирует, что в заблокированную секцию кода сможет войти лишь один поток одновременно. Для блокировки нужно указать объект, который станет идентификатором блокировки.
332
Потоки
Глава 7
В коде, работающем с несколькими элементами данных класса, можно использовать текущий экземпляр класса (ключевые слова this в C# и Me в Visual Basic).
Если какой-либо поток уже обратился к коду, синхронизирующая блокировка запретит другим потокам доступ к этому коду. Так, обращение к коду любого потока приведет к его блокировке на данном экземпляре класса. Второй поток сможет обратиться к заблокированному фрагменту, только когда первый поток освободит его, то же будет и с остальными потоками.
Хотя часто синхронизирующую блокировку в Visual Basic и C# проще всего объявить при помощи соответствующих ключевых слов, иногда требуется больше контроля над блокировками. В действительности, синхронизирующие блокировки основаны на классе Monitor. В табл. 7-8 перечислены важнейшие статические методы этого класса.
Табл. 7-8. Статические методы класса Monitor
Имя	Описание
Enter Exit TryEnter	Создает монопольную блокировку на указанном объекте Снимает монопольную блокировку с указанного объекта Пытается создать монопольную блокировку на указанном объекте, позволяет указать тайм-аут ожидания освобождения блокировки
Wait	Снимает монопольную блокировку и блокирует текущий поток, пока он не сможет снова установить блокировку
Идентичные функции можно получить и при помощи класса Monitor. 1 VB
Public Sub UpdateCountO
Monitor.Enter(Me)
Try
_count = _count + 1
If Count % 2 = 0 Then ’An even number _evenCount = _evenCount + 1
End If
Finally
Monitor.Exit(Me)
End Try
End Sub
// C#
public void UpdateCountO {
Monitor.Enter(this);
try {
_count = _count + 1;
if (Count % 2 == 0) // An even numoer {
_evenCount = _evenCount + 1;
}
Занятие 2
Общий доступ к данным	333
}
finally
{
Monitor.Exit(this);
}
}
Блокировки решают много проблем с синхронизацией потоков, но могут привести и к взаимной блокировке потоков (т. н. «смертельному захвату»).
Взаимные блокировки
Взаимная блокировка — это ситуация, в которой два фрагмента кода пытаются получить доступ к одним и тем же объектам, блокируя друг друга. Это иллюстрирует следующий листинг:
’ VB
Class Deadlocker
Dim ResourceA As Object = New ObjectO
Dim ResourceB As Object = New ObjectO
Public Sub First()
SyncLock ResourceA
SyncLock ResourceB
•	Console.WriteLine("First")
End SyncLock
End SyncLock
End Sub
Public Sub SecondO
SyncLock ResourceB
SyncLock ResourceA
Console.WriteLine("Second")
End SyncLock
End SyncLock
End Sub
End Class
11 C# class Deadlocker {
object ResourceA = new ObjectO;
object ResourceB = new ObjectO;
public void FirstO
lock (ResourceA)
{
lock (ResourceB)
334
Потоки
Глава 7
{
Console.WriteLine("Fi rst");
}
}
}
public void SecondO
{
lock (ResourceB)
{
lock (ResourceA)
{
Console.WriteLine("Second");
}
}
}
}
Если мы вызовем этот класс так:
’ VB
Dim deadlock As New Deadlocked)
Dim firstStart As New ThieadStart(deadlock.First)
Dim secondStart As New ThreadStart(deadlock.Second)
Dim first As New Thread(firstStart)
Dim second As New Thread(secondStart)
first. StartO
second.Start()
first.Join()
second.Join()
II C#
Deadlocker deadlock = new Deadlocked);
ThreadStart firstStart = new ThreadStart(deadlock.First);
Threadstart secondStart = new ThreadStart(deadlock Second);
Thread first = new Thread(firstStart);
Thread second = new Thread(secondStart);
first StartO;
second. StartO;
first.Join();
second.Join();
Занятие 2
Общий доступ к данным 335
Возникает взаимная блокировка:
1.	Первый поток запускается и блокирует ресурс А.
2.	Второй поток запускается и блокирует ресурс В.
3.	Первый поток блокируется, ожидая освобождения ресурса В.
4.	Второй поток блокируется, ожидая освобождения ресурса А.
5.	Приложение «зависает».
Сейчас был бы как нельзя кстать некий «волшебный класс», который помогает избежать взаимных блокировок. Увы, такого класса нет. Лучшее лекарство от взаимных блокировок — внимательность при разработке. Кроме отслеживания всех блокировок при помощи класса Monitor, можно порекомендовать следующее:,
	используйте метод Monitor. Try Enter с тайм-аутом, чтобы взаимные блокировки освобождались автоматически;
	стремитесь уменьшить размер блокированного кода, чтобы уменьшить время блокирования ресурса.
СОВЕТ «Вечное» ожидание
Чтобы реализовать неопределенно долгое ожидание, следует использовать статическое свойство Infinite класса Timeout, а не задавать гигантские сроки в миллисекундах.
Другие методы синхронизации
Хотя класс Monitor очень удобен и прост в применении при разработке многопоточных приложений, есть и другие, не менее полезные механизмы синхронизации. В этом разделе будет рассказано о следующих способах синхронизации:
	класса ReaderWriterLock',
	синхронизация при помощи объектов ядра Windows;
□	класс Mutex',
□	класс Semaphore’,
□	класса AutoResetEvent’,
□	класса ManualResetEvent.
Класс ReaderWriterLock
Класс ReaderWriterLock позволяет различать классы, использующие определенные ресурсы. При помощи этого класса можно избирательно блокировать доступ объектам, которые читают или записывающих данные. ReaderWriterLock позволяет сразу нескольким объектам одновременно получать доступ для чтения к данным, блокируя доступ для записи (разрешая его только одному объекту одновременно). Прежде чем записывающий объект сможет заблокировать данные, все читающие объекты должны снять с этих данных свои блокировки. Свойства и методы класса ReaderWriterLock перечислены в табл. 7-9 и 7-10.
Табл. 7-9. Свойства класса ReaderWriterLock
Имя	Описание
IsReaderLockHeld	Показывает, установил ли блокировку читающий объект
IsWriterLockHeld	Показывает, установил ли блокировку записывающий объект
336
Потоки
Глава 7
Табл. 7-10. Методы класса ReaderWriterLock
Имя	Описание
Acquire Reader Lock	В течение указанного времени пытается установить блокировку. Если за это время блокировку установить не удалось, генерирует исключение
Acquire Writer Lock	В течение указанного времени пытается установить блокировку записи. Если за это время блокировку установить не удалось, генерирует исключение
DowngradeFrom Writer Lock ReleaseReaderLock Release Writer Lock Upgrade To Writer Lock	Преобразует блокировку записи в блокировку чтения Освобождает блокировку чтения Освобождает блокировку записи Заменяет блокировку чтения на блокировку записи
Чтобы установить блокировку чтения, выполните следующее:
1.	Создайте экземпляр класса ReaderWriterLock для совместного использования любыми потоками.
2.	Создайте блок try/catch (для отслеживания ApplicationExceptiori). Этот блок перехватит исключение, сгенерированное по тайм-ауту попытки установить блокировку.
3.	В блоке try/catch запросите блокировку чтения, вызвав ReaderWriterLock.AcquireReaderLock.
4.	После установки блокировки чтения создайте блок try/finally, в котором будет содержаться код для чтения данных.
5.	Выполните все нужные операции, но читать данные, чтение которых должно быть безопасным в многопоточной среде, следует в секции try блока try/finally.
6.	В секции finally блока try/finally освободите блокировку чтения, вызвав ReaderWriterLock. Release Reader Lock.
Так, блокировку чтения можно использовать, если нужно считать значение с консоли:
' VB
Dim rwLock As New ReaderWriterLock()
Dim counter As Integer = 0
Try
rwLock.AcquireReaderLock(IOO)
Try
Console WriteLine(counter)
Finally
rwLock.ReleaseReaderLock()
End Try
Catch
Console.WriteLine("Failed to get a Reader Lock")
End Try
// C#
ReaderWriterLock rwLock = new ReaderWriterLock();	j
int counter = 0;
Занятие 2
Общий доступ к данным	237
try {
rwLock.AcquireReader Lock(100);
try {
Console.WriteLine(counter);
finally {
rwLock.ReleaseReaderLock();
}
}
catch (ApplicationException) {
Console.WriteLine( Failed to get a Reader Lock");
}
Для модификации данных нужно установить блокировку записи. Для этого выполните следующее:
1.	Создайте экземпляр класса ReaderWriterLock для совместного использования любыми потоками.
2.	Создайте блок try/catch (для отслеживания ApplicationException). Этот блок перехватит исключение, сгенерированное по тайм-ауту попытки установить блокировку.
3.	В блоке try/catch запросите блокировку записи, вызвав ReaderWriterLock.Acquire WriterLock.
4.	После запроса блокировки создайте блок try/finally, в котором будет содержаться код для записи данных.
5.	В секции try блока try/finally выполните нужные операции.
6.	В секции finally блока try/finally освободите блокировку записи, вызвав ReaderWriterLock. Release Writer Lock.
Так, блокировку записи можно использовать для безопасной модификации значения:
1 VB
Dim rwLock As New ReaderWriterLock()
Dim counter As Integer = 0
Try
rwLock.AcquireWriterLock(IOOO)
Try
Interlocked.Increment(counter)
Finally
rwLock.ReleaseWriterLock()
End Try
Catch
Console.WriteLine("Failed to get a Writer Lock")
End Try
338
Потоки
Глава 7
// C#
ReaderWriterLock rwLock = new ReaderWriterLockQ; int counter = 0;
try {
rwLock.Acqui reWriterLock(1000);
try
{
Interlocked. Increment ref counter);
}
finally
{
rwLock. ReleaseWriterLockO;
}
}
catch (ApplicationException)
{
Console.WriteLine("Failed to get a Writer Lock");
}
Класс ReaderWriterLock работает с блокировками чтения и записи. Еще удобнее то, что класс ReaderWriterLock поддерживает преобразование блокировки чтения в блокировку записи и наоборот. Эти операции выполняются методами UpgradeToWriterLock и DowngradeFromWriterLock. Эти методы нужно использовать совместно, как Monitor.Enter и Monitor.Exit. Метод UpgradeToWriterLock возвращает объект LockCookie. LockCookie — это структура, которую ReaderWriterLock использует для обратного преобразования блокировки после записи заблокированных данных. Это иллюстрирует следующий фрагмент кода:
VB
Try
Dim cookie As LockCookie = rwLock.UpgradeToWriterLock(1000)
counter = counter + 1
rwLock.DowngradeFromWriterLock( cookie)
Catch
Установить блокировку не удалось, игнорируем запись
End Try
// C# try {
LockCookie cookie = rwLock.UpgradeToWriterLock(IOOO);
counter++;
rwLock.DowngradeFromWriterLock(ref cookie);
}
catch (ApplicationException)
Занятие 2
Общий доступ к данным	339
{
// Установить блокировку не удалось, игнорируем запись
}
Заметьте, что вызов UpgradeToWriterLock требует значения тайм-аута и может не успеть установить блокировку записи за указанное время (это верно для любой попытки установить блокировку записи). Напротив, метод DowngradeFromWriterLock не требует тайм-аута, так как освобождает блокировку записи и восстанавливает блокировку чтения, полученную до установки блокировки записи.
Синхронизация при помощи объектов ядра Windows
На уровне операционной системы существует три объекта ядра — Mutex, Semaphore и Event — отвечающих за синхронизацию. Эти объекты дают широкие возможности по синхронизации, но довольно «тяжеловесны». Например, при использовании объекта Mutex синхронизация выполняется примерно в 33 раза медленнее, чем при помощи класса Monitor (об этом говорит Джеффри Рихтер в своей книге *CLR via С#»). Да, эти объекты не так быстры, но зато они позволяют решать задачи синхронизации, недоступные для классов Monitor и ReaderWriterLock’.
	объект Mutex позволяет (подобно блокировке) выполнять синхронизацию в пределах процесса и AppDomain-,
	объект Semaphore используется для ограничения одновременного доступа потоков к ресурсу;
	объект Event позволяет уведомлять потоки (расположенные в других процессах и AppDomain) о некоторых событиях.
Этих синхронизирующие объекты представлены классами .NET Framework — Mutex, Semaphore, AutoResetEvent и ManualResetEvent. Все эти классы происходят от общего класса WaitHandle. Наиболее важные свойства и методы класса WaitHandle перечислены в табл. 7-11 и 7-12.
Табл. 7-11. Свойства WaitHandle
Имя	Описание
Handle	Получает и устанавливает «родной» (системный) описатель объекта ядра
Табл. 7-12.	Методы класса WaitHandle
Имя	Описание
Close	Освобождает все ресурсы, занятые текущим объектом ядра
WaitOne	Блокирует текущий поток до освобождения объекта ядра
Класс Mutex
Класс Mutex предоставляет механизм блокировки и работает во многом сходно с классом Monitor. Основное различие в том, что класс Mutex может блокировать в других процессах и AppDomain. Ниже показано, как использовать класс BufferedStreanv.
1.	Создайте экземпляр класса Mutex, который смогут сообща использовать потоки:
' VB
Dim memStrm As New MemoryStream()
340 Потоки	Глава 7
// C#
MemoryStream memStrm = new MemoryStreamO;
2.	В новом потоке создайте оператор if, вызвав метод WditOne класса Mutex', это необходимо для ожидания освобождения блокировки:
' VB
If m.WaitOne(1000, False) Then End If
// C#
if (m.Wait0ne(1000, false)) // wait 1 second for lock { }
3.	Внутри оператора if создайте блок try/finally.
4.	В секции try блока try/finally выполните операции, требующие монопольного доступа к объекту Mutex.
5.	В ccKWAVifinally блока try/finally освободите объект Mutex, вызвав метод Mutex. Release Mutex. ' VB
Try
Выполняем операции
Finally
m.ReleaseMutex()
End Try
// C# try {
// Выполняем операции
}
finally
{
m. ReleaseMutexQ;
}
6.	После блока if можно создать блок else на тот случай, если блокировку установить не удастся:
’ VB
Else
Реагируем на невозможность установить блокировку (например, пробуем еще раз или уведомляем пользователя).
End If
И C#
} else {
// Реагируем на невозможность установить блокировку (например,
Занятие 2
Общий доступ к данным 34*1
// пробуем еще раз или уведомляем пользователя) }
В большинстве случаев нужно создавать именованный объект Mutex, чтобы можно было обращаться к нему из других процессов и/или AppDomain. Если создать такой объект, последний будет доступен при помощи статического метода OpenExisting класса Mutex. Это иллюстрирует следующий фрагмент кода:
' VB
Dim theMutex As Mutex = Nothing
Try
theMutex = Mutex.OpenExistingC'MYMUTEX")
Catch ex as WaitHandleCannotBeOpenedException
' He удается открыть мьютекс, так как он не существует End Try
' Создаем его, если он не существует
If theMutex Is Nothing Then
theMutex = New Mutex(False, "MYMUTEX”) End If
// C#
Mutex theMutex = null;
try // Пробуем открыть объект Mutex {
theMutex = Mutex.OpenExistingC’MYMUTEX");
}
catch (WaitHandleCannotBeOpenedException) {
// He удается открыть мьютекс, так как он не существует }
// Создаем его, если он не существует if (theMutex == null)
{
theMutex = new Mutex(false, "MYMUTEX");
При попытке открыть существующий объект Mutex нужно использовать блок try/catch, так как вместо возврата null при неудаче генерируется исключение WaitHandleCannotBeOpenedException. Использование именованного объекта Mutex — обычный способ синхронизации потоков разных процессов. Первый процесс, пытающийся открыть Mutex, создаст его, а остальные процессы просто будут получать этот объект.	<
Класс Semaphore
Класс Semaphore используется для ограничения доступа к некоторым ресурсам. Он создает объект ядра, поддерживающий определенное число действительных слотов. Если
342
Потоки
Глава 7
все слоты заняты, другие попытки обратиться к ресурсу блокируются до тех пор, пока какой-либо поток не освободит слот.
ПРИМЕЧАНИЕ .NET 2.0
Класс Semaphore впервые появился в .NET Framework 2.0.
При создании экземпляра класса Semaphore можно указать текущее число занятых слотов и максимальное общее число слотов. Это иллюстрирует следующий фрагмент кода:
’ VB
Dim theSemaphore As New Semaphored, 10)
// C#
Semaphore theSemaphore = new Semaphored, 10);
С объектом Semaphore работают почти так же, как и с Mutex (см. выше).Основное различие в том, что при освобождении объекта Semaphore можно указать, сколько слотов нужно освободить:
' VB
theSemaphore.Release(5)
// C#
theSemaphore.Release(5);
Кроме того, как и для класса Mutex, можно указать имя, которое будет использоваться для создания и открытия общих семафоров в других процессах и AppDomain. Это иллюстрирует следующий фрагмент кода:
' VB
Dim theSemaphore As Semaphore = Nothing
Try ' Пытаемся открыть Semaphore
theSemaphore = Semaphore OpenExistingC'THESEMAPHORE")
Catch ex as WaitHandleCannotBeOpenedException
He можем открыть Semaphore, так как он не существует
End Try
Создаем его, если он не существует
If theSemaphore Is Nothing Then
theSemaphore = New Semaphore(0, 10, "THESEMAPHORE")
End If
11 C#
Semaphore theSemaphore = null;
try // Пытаемся открыть Semaphore {
theSemaphore = Semaphore.OpenExistingC'THESEMAPHORE”);
}
Занятие 2
Общий доступ к данным	343
catch (WaitHandleCannotBeOpenedException) {
// Не можем открыть Semaphore, так как он не существует
}
// Создаем его, если он не существует
if (theSemaphore == null)
{
theSemaphore = new Semaphored, 10, "THESEMAPHORE");
}
Класс Event
События — это объекты ядра, способные находится в одном из двух состояний: «включено» и «выключено». Эти состояния позволяют потокам приложения ожидать от события сигнала о том, что требуется выполнить какое-то действие. Есть два типа событий: сбрасываемые автоматически и сбрасываемые вручную. При освобождении автоматически сбрасываемого события первый же объект, ожидающий это событие, возвращает его в состояние «занято». Такое поведение аналогично поведению объектов Mutex. И наоборот, событие с ручным сбросом освобождает от блокировки все ожидающие его потоки, пока событие не будет вручную переведено в состояние «занято».
В .NET Framework эти события представлены классами AutoResetEvent и ManualResetEvent. Оба класса происходят от общего класса EventWaitHandle (а тот — от класса WaitHandle}.
ПРИМЕЧАНИЕ .NET 2.0
Классы EventWaitHandle, AutoResetEvent и ManualResetEvent впервые появились в .NET Framework 2.0.
При создании экземпляра класса события можно указать его состояние:
’ VB
Dim autoEvent As New AutoResetEvent(true)
Dim manualEvent As New ManualResetEvent(false)
// C#
AutoResetEvent autoEvent = new AutoResetEvent(true);
ManualResetEvent manualEvent = new ManualResetEvent(false);
Класс EventWaitHandle поддерживает новые методы для работы с событиями: Set и Reset. Эти методы используются для переключения состояний события, как показано в следующем примере:
' VB
autoEvent.Set() manualEvent Reset()
// C#
autoEvent.Set();
manualEvent. ResetO;
Как и другие объекты ядра, события позволяют задать имя, обращаясь по которому, их можно создавать и открывать в других процессах и AppDomain. Именованные собы
344
Потоки
Глава 7
тия поддерживаются на уровне класса EventWaitHandle. При создании или открытии именованного события приходится иметь дело с классом EventWaitHandles, а не AutoResetEvent или ManualResetEvent. При создании объекта EventWaitHandle нужно указать не только состояние, но и тип требуемого события. Приведем пример кода, который создает и открывает именованное событие:
' VB
Dim theEvent As EventWaitHandle = Nothing
Try
theEvent = EventWaitHandle.OpenExisting("THEEVENT") Catch ex as WaitHandleCannotBeOpenedException
He можем открыть AutoResetEvent, так как он не существует
End Try
Создаем его, если он не существует
If theEvent Is Nothing Then
theEvent = New EventWaitHandle(False, EventResetMode.AutoReset, "THEEVENT") End If
// C#
EventWaitHandle theEvent = null;
try // Пробуем открыть Event
{
theEvent = EventWaitHandle.OpenExisting("THEEVENT”);
}
catch (WaitHandleCannotBeOpenedException)
// He можем открыть AutoResetEvent, так как он не существует }
// Создаем его, если он не существует
if (theEvent == null)
{
theEvent = new EventWaitHandle(false, EventResetMode.AutoReset, "THEEVENT”);
}
Практикум. Использование объекта Mutex дпя создания приложения, исполняемого в единственном экземпляре
Сейчас вы создадите простое консольное приложение, в котором объект Mutex будет применяться для того, чтобы разрешить запуск не более одного экземпляра приложения. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
1.	Создайте консольное приложение с именем Singleinstance.
2.	В основном файле импортируйте пространство имен System. Threading.
Занятие 2
Общий доступ к данным 345
3.	В методе main консольного приложения создайте локальную переменную Mutex и присвойте ей значение null (или Nothing в Visual Basic).
4.	Создайте строку-константу с именем общего объекта Mutex. Присвойте ей значение «RUNMEONCE».
5.	Создайте блок try/catch.
6.	В секции try блока try/catch вызовите метод Mutex.OpenExisting, используя строку, назначенную на шаге 4 именем объекта Mutex. Затем присвойте результат переменной Mutex, созданной на шаге 2.
7.	В секции catch блока try/catch block перехватите исключение WditHandleCannotBeOpene-dException, чтобы определить, существует ли объект Mutex с таким именем.
8.	Проверьте равенство переменной Mutex, созданной на шаге 2, null (или Nothing в Visual Basic), чтобы узнать, можно ли найти объект Mutex.
9.	Если найти объект Mutex не удалось, создайте его и назовите именем, представленным строкой из шага 4.
10.	Если объект Mutex найден, закройте переменную Mutex и выйдите из приложения. Полученный код выглядит примерно так:
• VB
Imports System.Threading
Class Program
Public Overloads Shared Sub Main()
Dim oneMutex As Mutex = Nothing
Const MutexName As String = "RUNMEONLYONCE”
Try ' Пытаемся открыть Mutex
oneMutex = Mutex.OpenExisting(MutexName)
Catch ex as WaitHandleCannotBeOpenedException
' He можем открыть Mutex, потому что он не существует
End Try
’ Создаем его, если он не существует
If oneMutex Is Nothing Then
oneMutex = New Mutex(True, MutexName)
Else
' Закрываем Mutex и выходим из приложения,
’ так как разобщается запуск только одного его экземпляра oneMutex. CloseO
Return
End If
Console.WriteLine("Our Application")
Console.Read()
End Sub
End Class
346
Потоки
Глава 7
И C# using System.Threading;
*
class Program {
static void Main(string[] args) {
Mutex oneMutex = null;
const string MutexName = "RUNMEONLYONCE";
try // Пытаемся открыть Mutex {
oneMutex = Mutex.OpenExisting(MutexName);
}
catch (WaitHandleCannotBeOpenedException) {
// He можем открыть Mutex, потому что он не существует }
// Создаем его, если он не существует if (oneMutex == null) {
oneMutex = new Mutex(true. MutexName);
} else {
// Закрываем Mutex и выходим из приложения,
// так как разрешается запуск только одного его экземпляра oneMutex. CloseO;
return; }
Console WriteLine("Our Application");
Console.Read(); }
}
11.	Соберите проект, исправьте ошибки. Убедитесь, что запускается только один экземпляр приложения одновременно.
Резюме
	Чтобы выполнить атомарную математическую операцию, используйте класс Interlock.
	Чтобы заблокировать данные, используйте ключевое слово lock (С#) или SyncLock (Visual Basic).
	Чтобы заблокировать данные с помощью синхронизирующего объекта, используйте класс Monitor.
Занятие 3
Асинхронная модель программирования
347
	Чтобы заблокировать данные так, что доступ для чтения был разрешен сразу нескольким объектами, а для записи — только одному, используйте класс ReaderWriterLock.
	Для синхронизации потоков в разных процессах и AppDomain используйте объект Mutex.
	Для ограничения доступа потоков при помощи синхронизирующего объекта, основанного на ресурсах, используйте объект Semaphore.
	Для уведомления потоков в других процессах и AppDomain используйте объект Event.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Блокировка записи не установлена. Сколько объектов может обращаться для чтения к данным при помощи класса ReaderWriterLock'!
А.	0;
В.	1;
С.	10;
D.	сколько угодно.
2.	Что из перечисленного ниже можно использовать для синхронизации потоков в других процессах и AppDomain! (Укажите все верные ответы.)
А. Класс Monitor,
В. класс Mutex’,
С. класс Semaphore’,
D. ключевое слово lock (С#) или SyncLock (Visual Basic).
Занятие 3. Асинхронная модель
программирования
В большинстве случаев .NET Framework позволяет выполнять операции нелинейно. Используя модель асинхронного программирования (Asynchronous Programming Model, АРМ), определенную в .NET Framework, можно улучшить производительность и отзывчивость приложений, а также максимально полно использовать системные ресурсы.
Изучив материал этого занятия, вы сможете:
J понять модель асинхронного программирования (АРМ);
J использовать класс ThreadPool’,
'А использовать класс Timer,
J использовать интерфейс LAsyncResult для выполнения асинхронных вызовов;
Z понять, как в АРМ работают исключения.
Продолжительность занятия — 40 минут.
348
Потоки
Глава 7
Суть асинхронного программирования
Асинхронное программирование позволяет вынести исполнение некоторых частей кода в отдельные потоки. Такой подход называется асинхронной моделью программирования (Asynchronous Programming Model, АРМ). В .NET Framework многие классы поддерживают АРМ посредством различных версий методов BeginXXX и EndXXX. Например, у класса FileStream есть метод Read, читающий данные из потока. Для поддержки АРМ этот класс также имеет методы Begin Read и EndRead. Методы BeginXXX и EndXXX позволяют асинхронно вызывать другие методы, как показано в следующем примере: ’ VB
Dim buffer() As Byte = New Byte(100) {}
Dim filename as String =
String.Concat(Environment.SystemDirectory, "\\mfc71.pdb")
FileStream strm = New FileStream(filename,
FileMode.Open, FileAccess.Read, FileShare.Read, 1024, FileOptions.Asynch ronous)
Производим асинхронный вызов Dim result As lAsyncResult = strm.BeginRead(buffer, 0, buffer.Length, Nothing, Nothing)
Здесь выполняются нужные операции «в фоновом режиме»
Вызов EndRead блокируется, пока не завершится выполнение Async
Dim numBytes As Integer = strm.EndRead(result)
He забудьте закрыть поток strm. CloseO
Console.WriteLine("Read {0} Bytes", numBytes)
Console.WriteLine(BitConverter.ToString(buffer))
// c#
byte[] buffer = new byte[100];
string filename =
string.Concat(Environment.SystemDirectory, "\\mfc71.pdb");
FileStream strm = new FileStream(filename,
FileMode.Open, FileAccess.Read, FileShare. Read, 1024, FileOptions Asynchronous);
// Производим асинхронный вызов
lAsyncResult result = strm.BeginRead(buffer, 0, buffer.Length, null, null);
Занятие 3
Асинхронная модель программирования 34g
// Здесь выполняются нужные операции «в фоновом режиме»
// Вызов EndRead блокируется, пока не завершится выполнение Async int numBytes = strm.EndRead(result);
// He забудьте закрыть поток st rm. CloseO;
Console.WriteLine("Read {0} Bytes", numBytes);
Console.WriteLine(BitConverter.ToString(buffer));
Чтобы понять, как это работает, рассмотрим сигнатуру метода FileStream.Read:
' VB
Function Read(ByVal arrayO As Byte,
ByVai offset As Integer,
ByVai count As Integer) As Integer
// C#
int Read(byte[] array, int offset, int count);
Метод BeginRead весьма похож на метод Read:
’ VB
Function BeginRead(ByVai arrayO As Byte,
ByVai offset As Integer,
ByVai numBytes As Integer,
ByVai userCallback As AsyncCallback, ByVai stateObject As Object) As lAsyncResult
// C#
lAsyncResult BeginRead(byte[] array, int offset, int numBytes,
AsyncCallback userCallback, object stateObject);
Отличие в том, что этот метод возвращает lAsyncResult, а не набор записанных байтов; для поддержки АРМ в сигнатуру метода добавлены два параметра. Их назначение будет объяснено далее, в разделе, посвященном обратным вызовам.
Вызов метода EndRead означает конец асинхронной операции:
’ VB
Function EndRead(ByVal asyncResult As lAsyncResult) As Integer
// C#
int EndRead(lAsyncResult asyncResult):
В конце операции нужно вызвать EndRead с объектом lAsyncResult, чтобы получить записанные байты. Таким образом, метод BeginRead передает входные данные для асинхронной обработки, a EndRead — возвращает ее результаты.
Естественно, при асинхронных операциях нужно знать, когда и где нужно вызвать метод EndXXX. Для этого используются различные способы уведомления о завершении асинхронных операций.
350
Потоки
Глава 7
Уведомление о завершении асинхронной операции
Есть три стиля обработки завершения асинхронных вызовов в АРМ-программировании: ожидание завершения, опрос и обратный вызов. Рассмотрим каждый из них.
Ожидание завершения
Эта модель позволяет сделать асинхронный вызов, а затем выполнять другую работу, по завершении которой можно попытаться завершить асинхронный вызов; при этом попытка завершения блокируется до завершения асинхронного вызова. Это иллюстрирует следующий фрагмент кода:
VB
Dim buffer() As Byte = New Byte(100) {}
Dim filename as String =
String.Concat(Environment.SystemDirectory, "\\mfc71.pdb”)
FileSrream strm = New FileStream(filename,
FileMode.Open, FileAccess.Read, FileShare.Read, 1024,
FileOptions.Asynchronous)
Производим асинхронный вызов
strm.Read(buffer, 0, buffer.Length)
Dim result As lAsyncResult =
strm.BeginRead(buffer, 0, buffer.Length, Nothing, Nothing)
Здесь нужные операции выполняются «в фоновом режиме»
Вызов EndRead блокируется, пока не завершится выполнение Async
Dim numBytes As Integer = strm.EndRead(result)
' He забудьте закрыть поток
strm. CloseO
Console WriteLine("Read {0} Bytes", numBytes)
Console.WriteLine(BitConverter.ToString(buffer))
II C#
byte[] buffer = new byte[100];
string filename =
string.Concat(Environment.SystemDirectory, "\\mfc71 pdb");
FileStream strm = new FileStream(filename
FileMode.Open, FileAccess.Read, FileShare.Read, 1024,
FileOptions.Asynchronous);
Занятие 3
Асинхронная модель программирования 35 j
// Производим асинхронный вызов
strm. Read (buffer,. О, buffer.Length);
lAsyncResult result = strm.BeginRead(buffer, 0, buffer.Length, null, null);
// Здесь нужные операции выполняются «в фоновом режиме»
// Вызов EndRead блокируется, пока не завершится выполнение Async
Int numBytes = strm.EndRead(result);
// He забудьте закрыть поток
strm.CloseO;
Console, WriteLine("Read {0} Bytes", numBytes);
Console.WriteLine(BitConverter.ToString(buffer));
После вызова BeginRead объекты обратного вызова и состояния устанавливаются в null (или Nothing в Visual Basic): мы не собираемся использовать обратный вызов, и потому они не нужны. После выполнения нужных операций код вызывает метод EndRead, и этот вызов блокирует поток до завершения асинхронного вызова.
Опрос
Этот способ аналогичен предыдущему, за тем исключением, что код опрашивает lAsyncResult, чтобы выяснить, не завершен ли асинхронный вызов. Это иллюстрирует следующий фрагмент кода:
’ VB
Dim buffer() As Byte = New Byte(100) {}
Dim filename as String =
String.Concat(Environment.SystemDirectory, "\\mfc71.pdb")
FileStream strm = New FileStream(filename,
FileMode.Open, FileAccess.Read, FileShare.Read, 1024,
FileOptions.Asynchronous)
' Производим асинхронный вызов
Dim result As lAsyncResult = _
strm.BeginRead(buffer, 0, buffer.Length, Nothing, Nothing)
* Выполняем опрос, чтобы проверить завершение
While Not result.IsCompleted
’ Здесь нужные операции выполняются «в фоновом режиме»
Thread.Sleep(100)
End While
Вызов завершен, так что можно вызвать EndRead без блокировки
Dim numBytes As Integer = strm.EndRead(result)
' He забудьте закрыть поток
352
Потоки
Глава 7
strm. CloseO
Console.WriteLine("Read {0} Bytes”, numBytes)
Console.WriteLine(BitConverter.ToString(buffer))
// C#
byte[] buffer = new byte[100];
string filename =
string.Concat(Environment.SystemDirectory, ”\\mfc71.pdb”);
FileStream strm = new FileStream(filename,
FileMode.Open, FileAccess.Read, FileShare.Read, 1024, FileOptions.Asynch ronous)
// Производим асинхронный вызов
lAsyncResult result = strm.BeginRead(buffer, 0. buffer.Length, null, null);
/I Выполняем опрос, чтобы проверить завершение while (I result.IsCompleted)
{
// Выполняем другие операции, если вызов еще не завершен Thread.Sleep(100);
>
// Вызов завершен, так что можно вызвать EndRead без блокировки int numBytes = strm.EndRead(result);
11 He забудьте закрыть поток strm. CloseO;
Console.WriteLine("Read {0} Bytes", numBytes);
Console.WriteLine(BitConverte r.ToSt ring(buffer));
Вызвав свойство IsCompleted объекта 1AsyncResult, возвращенного методом Begin Read, можно узнать, завершен ли асинхронный вызов, и, если нет, продолжить другую работу.
Обратный вызов
Применение обратныйх вызовов требует указывать метод для обратного вызова и необходимые ему сведения о состоянии. Модель с обратным вызовом можно проиллюстрировать следующим примером:
' VB
Shared buffer() As Byte = New Byte(100) {}
Shared Sub TestCallbackAPMO
Dim filename as String =
String.Concat(Environment.SystemDirectory, "\\mfc71.pdb")
Занятие 3
Асинхронная модель программирования 353
FileStream strm = New FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read, 1024, L FileOptions.Asynchronous)
Производим асинхронный вызов
lAsyncResult result = strm.BeginRead(buffer, 0, buffer.Length, New AsyncCallback(CompleteRead), strm)
End Sub
// C#
static byte[] buffer = new byte[100];
static void TestCallbackAPM()
{
string filename =
string.Concat(Environment.SystemDirectory, "\\mfc71.pdb");
FileStream strm = new FileStream(filename,
FileMode.Open, FileAccess.Read, FileShare.Read, 1024, FileOptions.Asynch ronous);
// Производим асинхронный вызов
lAsyncResult result = strm.BeginRead(buffer, 0, buffer.Length, new AsyncCallback(CompleteRead), strm);
}
Здесь создается делегат AsyncCallback co ссылкой на метод, который нужно вызвать (в другом потоке) по завершении асинхронной операции. Также нужно указать объект, представляющий состояние для обратного вызова. В этом примере мы передаем потоковый объект, так как нужно вызвать EndRead и закрыть поток.
Метод для вызова в конце асинхронной операции может выглядеть так:
' VB
Shared Sub CompleteRead(ByVal result As lAsyncResult)
Console.WriteLi ne("Read Completed")
Dim strm As FileStream = CType(result.AsyncState, FileStream)
Вызов завершен, так что можно вызвать EndRead без блокировки
Dim numBytes As Integer = strm.EndRead(result)
He забудьте закрыть поток
strm.CloseO
Console.WriteLine("Read {0} Bytes", numBytes)
Console.WriteLine(BitConverter.ToString(buffer)) End Sub
354
Потоки
Глава 7
// C#
static void CompleteRead(lAsyncResult result)
{
Console.WriteLine("Read Completed");
FileStream strm = (FileStream) result.AsyncState;
// Вызов завершен, так что можно вызвать EndRead без блокировки int numBytes = strm.EndRead(result);
// He забудьте закрыть поток
strm. CloseO;
Console.WriteLineC'Read {0} Bytes", numBytes);
Console.WriteLine(BitConverter.ToString(buffer));
}
Заметьте, что код не блокируется на lAsyncResult, а передает его как параметр методу обратного вызова. После этого можно получить объект FileStream, который и будет передан в качестве состояния. Все остальное работает, как описано выше.
Исключения и АРМ
При использовании АРМ некоторые операции могут генерировать исключения во время асинхронной обработки. Чтобы не прерывать асинхронную обработку, все исключения генерируются во время вызова EndXXX, а не в момент, когда складывается исключительная ситуация. Обрабатывать исключения следует при вызове EndXXX. Например, можно дополнить вызов EndRead из предыдущего примера кодом для перехвата и обработки исключений lOExceptions'.
• VB
Dim numBytes As Integer = 0
Try
numBytes = strm.EndRead(result)
Catch
Console.WriteLine("An 10 Exception occurred")
End Try
// C#
int numBytes = 0;
try
{
numBytes = strm.EndRead(result);
}
catch (lOException)
{
Console.WriteLine("An 10 Exception occurred");
}
Занятие 3
Асинхронная модель программирования 355
Обработка исключений в приложениях Windows Forms
При возникновении исключения в любом месте приложения Windows Forms (в основном потоке либо в асинхронных вызовах) пользователь получит стандартное сообщение о возникновении исключения и процесс будет прерван. У разработчика есть возможность выбора действий в этой ситуации путем регистрации события ThreadException для события Application следующим образом:
' VB
AddHandler Application,ThreadException,
AddressOf Me.Application_ThreadException
Application.Run(New Forml)
Private Shared Sub Application_ThreadException(ByVai sender As Object,
ByVai e As ThreadExceptionEventArgs)
MessageBox.Show(String.Format("{0}", e.Exception)) End Sub
11 C#
Application.ThreadException += new
ThreadExceptionEventHandler(Appiication_ThreadException);
Application.Run(new Form1());
static void Application_ThreadException(object sender,
ThreadExceptionEventArgs e) {
MessageBox.Show(string.Format(’’{0}", e.Exception));
}
Делегат ThreadExceptionEventHandler определяет правила вызова обработчика события, а класс ThreadExceptionEventArgs содержит объект исключения.
ПРИМЕЧАНИЕ АРМ
Подробнее об АРМ см. в книге Джеффри Рихтера «CLR via С#» (второе издание, Microsoft Press, 2006).
Использование ThreadPool
Выше в этой главе рассказано о создании собственных потоков с использованием модели асинхронного программирования. Но во многих случаях создание собственных потоков не нужно либо нежелательно. .NET поддерживает встроенный пул потоков, которые во многих случаях успешно заменяют собственные потоки. Например, возьмем код из занятия 1 и поместим некоторые операции в поток:
1	VB
Shared Sub WorkWithParameter(ByVal о As Object)
Dim info As String = CType(o, String)
356
Потоки
Глава 7
For х = 0 То 9
Console.WriteLine("{0}: {1}", info,
* Th read.Cu r rentTh read.ManagedTh readld)
' Замедляем работу потока и позволяем другим потокам продолжить выполнение Thread.Sleep(10)
Next
End Sub
11	C#
static void WorkWithParameter(object o)
{
string info = (string) o;
for (int x = 0; x < 10; ++x)
{
Console.WriteLine("{0}: {1}", info,
Th read.Cu r rentTh read.ManagedTh readld);
// Замедляем работу потока и позволяем другим потокам продолжить выполнение Thread.Sleep(10);
}
Вместо создания и управления собственного потока можно получить поток из ThreadPool и делегировать ему эту работу с использованием метода QueueWorkltem:
' VB
Dim workitem As New WaitCallback(WorkWithParameter))
If Not ThreadPool.QueuellserWorkItem(workItem, "ThreadPooled") Then
Console.WriteLine("Could not queue item”)
End If
П C#
WaitCallback workitem = new WaitCallback(WorkWithParameter));
if (!ThreadPool.QueueUserWorkItem(workItem, "ThreadPooled”))
{
Console.WriteLine("Could not queue item");
}
Это не упрощенный способ создания своих потоков, а возможность задействовать готовые потоки из пула .NET, доступные для многократного использования в приложениях. Этот пул потоков работает быстрее, поскольку его потоки доступны для повторного использования, что позволяет экономить ресурсы на их создании. Пул также помогает ограничить число потоков, которое может одновременно запустить один процесс, организовав очередь задач, ожидающих выполнения. По мере освобождения потоки пула принимают новые задачи из очереди.
Класс ThreadPool поддерживает методы для создания очередей задач и управления ими. Важнейшие методы класса ThreadPool перечислены в табл. 7-13.
Занятие 3
Асинхронная модель программирования 357
Табл. 7-13. Статические методы класса ThreadPool
Имя	Описание
GetAvailable Threads	Возвращает число доступных потоков в пуле
GetMaxThreads	Возвращает максимальное число потоков, поддерживаемое классом ThreadPool для этого процесса
GetMinThreads	Возвращает минимальное число создаваемых потоков (число готовых потоков, доступных в пуле)
Queue UserWorkltem	Добавляет в пул потоков задачи, которые будут выполнены доступным потоком
RegisterWaitForSingleObject	Позволяет производить обратный вызов на определенном WaitHandle после его освобождения
Set Max Threads	Определяет максимальное число потоков в пуле этого процесса
SetMinThreads	Определяет минимальное число потоков, созданных в пуле
UnsafeQueueNativeOverlapped	Используется для создания очереди асинхронных портов завершения файлового ввода-вывода, используя структуры Overlapped и NativeOverlapped Подробнее об этом см. в гл. 13
UnsafeQueue UserWorkltem	Созадет очередь рабочих элементов потока для повышения производительности. Не передает новому потоку стек вызовов или контекст выполнения
UnsafeRegisterWaitForSingleObject	Позволяет произвести обратный вызов на WaitHandle после освобождения этого объекта. Используется для повышения производительности Не передает новому потоку стек вызовов или контекст выполнения
Ограничение числа потоков в ThreadPool
Класс ThreadPool поддерживает статические методы, позволяющие задать минимальное и максимальное число потоков в пуле. В большинстве случаев автоматически настроенное число потоков в пуле является оптимальным. Если у приложения возникает нехватка потоков из пула, можно задать предельные значения самостоятельно. Изменение этих значений повлияет только на текущий процесс.
Изменять предельные значения пула потоков следует в двух случаях: при нехватке потоков, и когда запуск новых потоков в пуле нежелателен.
В первом случае работа приложения, использующего пул потоков, тормозится чрезмерным числом рабочих элементов и полной загруженности доступных потоков в пуле. Чтобы увеличить максимальное число потоков, нужно просто воспользоваться методом ThreadPool.SetMaxThreads, как показано ниже:
' VB
Dim threads As Integer
Dim completionPorts As Integer
' Получаем число потоков из ThreadPool
Th readpool.Ge tMaxTh reads(th reads, completionPo rts)
358
Потоки
Глава 7
Увеличиваем это число
ThreadPool.SetMaxlhreads(threads + 10, completionports + 100)
*	4
// C#
int threads;
int completionPorts;
// Получаем число потоков из ThreadPool
ThreadPool.GetMaxThreads(out threads, out completionPorts);
// Увеличиваем это число
ThreadPool.SetMaxThreads(threads + 10, completionPorts + 100);
Сначала этот код получает число потоков и портов завершения из ThreadPool. Порты завершения — это особые объекты потоков уровня ядра, применяемые для асинхронного файлового ввода-вывода. Обычно портов завершения намного больше, чем управляемых потоков. Затем можно изменить число потоков, просто указав новые предельные значения.
Если «цена» запуска потоков из пула слишком высока, имеет смысл увеличить минимальное число потоков. Минимальное число потоков определяет, сколько потоков сразу создается в пуле при запуске процесса. Обычно ThreadPool позволяет процессу создавать не больше двух потоков в секунду. Если приложению требуется больше потоков либо нужно создавать их быстрее, это число можно увеличить. Минимальное число потоков изменяют так же, как максимальное:
’ VB
Dim threads As Integer
Dim completionPorts As Integer
Получаем число потоков из ThreadPool
Th readPool.GetMinTh reads(th reads, completionPorts)
' Увеличиваем минимальное число потоков
ThreadPool.SetMinThreads(th reads + 10, completionPorts + 100)
// C#
int threads;
int completionPorts;
// Получаем число потоков из ThreadPool
ThreadPool.GetMinThreads(out threads, out completionPorts);
// Увеличиваем минимальное число потоков
ThreadPool.SetMinThreads(threads + 10, completionPorts + 100);
Этот код идентичен предыдущему, только в нем используются методы GetMinThreads и SetMinThreads.
Занятие 3
Асинхронная модель программирования 35g
ThreadPool и WaitHandle
На занятии 2 сказано, что все синхронизирующие объекты уровня ядра (Mutex, Semaphore и Event) происходят от класса WaitHandle. Пул потоков также поддерживает ожидание потоками и обратные вызовы при освобождении объекта WaitHandle. Это выполняется вызовом ThreadPool.RegisterWaitHandle, как показано ниже: ’ VB
Создаем Mutex
Dim mutex As New Mutex(True)
/
' Регистрируем объект для уведомления
Th readPool.Registe rWaitFo rSingleObj ect(mutex,
New WaitOrTimerCallback(MutexHasFired), Nothing, Time.Infinite, True)
Сигнализируем мьютексу, что нужно обратиться к потоку mutex.ReleaseMutex()
И C#
// Создаем Mutex
Mutex mutex = new Mutex(true);
// Регистрируем объект для уведомления
ThreadPool.RegisterWaitForSingleObject(mutex, new WaitOrTimerCallback(MutexHasFired), null, Timeout.Infinite, true);
// Сигнализируем мьютексу, что нужно обратиться к потоку mutex.ReleaseMutex();
Метод RegisterWaitForSingleObject принимает объект WaitHandle, а также делегат со сылкой на метод, принимающий объект (представляющий состояние потока, указанное при вызове метода), и значение типа Boolean, указывающее, не истек ли тайм-аут освобождения объекта WaitHandle. Метод обратного вызова MutexHasFired ыожея выглядеть так:
' VB
Shared Sub MutexHasFired(ByVai state As Object, ByVai timedOut As Boolean)
If timedOut = True Then
Console.WriteLine("Mutex Timed out")
Else
Console.WriteLine("Mutex got signaled")
End If
End Sub
// C#
static void MutexHasFired(object state, bool timedOut) {
if (timedOut)
360
Потоки
Глава 7
{
Console.WriteLine("Mutex Timed out");
} “ else {
Console.WriteLineC’Mdtex got signaled”);
}
}
Класс Synchronizationcontext
При создании асинхронного кода возможны проблемы, зависимые от окружения. Модель потоков в Windows Forms отличаются от таковой в ASP.NET. В модели потоков Windows Forms код пользовательского интерфейса желательно исполнять в главном потоке, в котором работает интерфейс. В ASP.NET это не так: здесь основная работа выполняется потоками из пула, а асинхронные вызовы могут выполняться только потоками пула. Для работы с этими моделями .NET Framework поддерживает класс Synchronizationcontext, позволяющий писать код, не зная, какая именно модель потоков будет использована в приложении.
Чтобы использовать класс Synchronizationcontext, сначала нужно получить экземпляр этого класса, вызвав его статическое свойство Current. После этого можно выполнить любое из следующих действий:
	Вызвать метод Send класса Synchronizationcontext для выполнения какого-либо кода. Вызвав Send, можно выполнить код (возможно, в отдельном потоке), но вызов будет блокирован до завершения этого кода.
’ VB
Dim ctx As Synchronizationcontext = Synchronizationcontext.Current
Отдаем команду на синхронное выполнение кода ctx.Send(AddressOf RunMe, "Hi”)
// C#
Synchronizationcontext ctx = Synchronizationcontext.Current;
// Отдаем команду синхронного выполнения кода ctx Send(RunMe, "Hi"),
	Вызвать метод Post класса Synchronizationcontext, чтобы выполнить какой-либо код. Метод Post работает по принципу «вызвал и забыл», помещая запрос в очередь и немедленно возвращая управление.
’ VB
Dim ctx As Synchronizationcontext = Synchronizationcontext.Current
Отправляем команду на асинхронное выполнение кода ctx.Post(AddressOf RunMe, Hi")
// C#
Synchronizationcontext ctx = Synchronizationcontext.Current;
Занятие 3
Асинхронная модель программирования 261
// Отправляем команду на асинхронное выполнение кода ctx.Post(RunMe, "Hi");
В зависимости от конкретной модели потоков, оба метода могут возвращать управление не сразу. Если асинхронное выполнение кода не поддерживается (как в модели потоков Windows Forms), оба метода вернут управление только после исполнения кода.
Методы Send и Post не возвращают объекты, которые можно ожидать или использовать для получения каких-либо значений (пример такого объекта — интерфейс lAsyncResult из предыдущих примеров). Класс Synchronizationcontext удобен для выполнения произвольного кода, результаты которого не влияют на дальнейшую работу.
Применение объектов Timer
Timer — это базовый объект, асинхронно вызывающий методы через заданный промежуток времени. Такие объекты создаются на основе класса Timer из пространства имен System. Threading. При создании объекта Timer нужно указать делегат TimerCallback, ссылающийся на метод, который следует запустить по команде Timer. Также нужно указать задержку запуска таймера (ноль означает немедленный запуск таймера) и его интервал. Например, можно создать объект Timer, который сразу начнет ежесекундно запускать метод TimerTick'.
' VB
Dim tm As Timer = New Timer(New TimerCallback(TimerTick), Nothing, 0, 1000)
Shared Sub TimerTick(ByVal state As Object)
Console.WriteLine("Tick")
End Sub
11 C#
Timer tm = new Timer(new TimerCallback(TimerTick), null, 0, 1000);
static void TimerTick(object state)
{
Console.WriteLine("Tick");
}
Класс Timer также поддерживает метод Change, позволяющий указать задержку и интервал срабатывания Timer.
' VB
Dim tm As Timer = New Timer(New TimerCallback(TimerTick), Nothing, 0, 1000)
Если нужно временно остановить таймер, используйте параметр Infinite tm.Change(Time.Infinite, 1000)
// C#
Timer tm = new Timer(new TimerCallback(TimerTick), null, 0, 1000);
// Если нужно временно остановить таймер, используйте параметр Infinite
tm.Change(Timeout.Infinite, 1000);
362
Потоки
Глава 7
ПРИМЕЧАНИЕ Классы Timer
Всего классов Timer три:
	System.Threading.Timeг -
обсуждался на этом занятии.
	System.Windows.Forms.Timer -
генерирует события в потоке, где работает форма, используя оконное сообщение WMTIMER. Таймер System.Windows.Forms.Timer не связан с таймером System. Threading. Timer.
	System.Timers.Timer -
оболочка System. Threading. Timer для использования последнего в дизайнерах Visual Studio.
Практикум. Создание очереди рабочих элементов при помощи ThreadPool
Сейчас вы создадите приложение, использующее пул потоков для создания очереди методов, которые должны быть вызваны в отдельных потоках. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
1.	Создайте пустое консольное приложение с именем ThreadPoolDemo.
2.	Включите (а в Visual Basic импортируйте) пространство имен System. Threading.
3.	Создайте метод, просто отображающий текст. Назовите его Show Му Text. Определите один параметр типа object и назовите его state.
4.	В методе Show Му Text создайте переменную строку и приведите параметр state к String, сохранив его в текстовой переменной.
5.	В методе Show Му Text выведите на экран идентификатор ManagedThreadld текущего потока, а новую строку — на консоль.
6.	В методе Main консольного приложения создайте новый экземпляр делегата WaitCallback, который ссылается на метод ShowMyText.
7.	Используйте ThreadPool, чтобы поместить в очередь несколько вызовов делегата WaitCallback, передавая как состояние различные строки. Полученный код выглядит примерно так:
' VB
Imports System Threading
Class Program
Shared Sub Main(ByVal args() As String)
Dim callback As WaitCallback = _
New WaitCallback(AddressOf ShowMyText)
ThreadPool.QueueUserWorkItem(callback, "Hello") ThreadPool.QueueUserWorkItem(callback, "Hi") ThreadPool.QueueUserWorkItem(callback, "Heya") ThreadPool.QueueUserWorkItem(callback, "Goodbye")
Занятие 3
Асинхронная модель программирования 333
Console.Read()
End Sub
Shared Sub ShowMyText(ByVal state As Object)
Dim myText As String = CType(state, String)
Console.WriteLine("Thread: {0} - {1}", Th read.Cu r rentTh read.ManagedTh readld, myText)
End Sub
End Class
// C#
using System.Threading;
class Program
{
static void Main(string[] args)
{
WaitCallback callback = new WaitCallback(ShowMyText);
ThreadPool.QueueUserWorkItem(callback, "Hello");
ThreadPool.QueueUserWorkItem(callback, "Hi");
ThreadPool.QueueUserWorkItem(callback, "Heya");
ThreadPool.QueuellserWorkItem(callback, "Goodbye");
Console.Read();
}
static void ShowMyText(object state)
{
string myText = (string)state;
Console.WriteLine("Thread: {0} - {1}", Th read.Cu rrentTh read.ManagedTh readld, myText);
}
}
8.	Соберите проект, исправьте ошибки. Убедитесь, что консольное приложение успешно отображает все вызовы методы ShowMyText на консоли. Обратите внимание, что некоторые рабочие элементы выполняются в разных потоках.
Резюме
 Асинхронная модель программирования (АРМ) позволяет повысить эффективность взаимодействия пользователей с приложением и уменьшить его время отклика за счет одновременного выполнения нескольких операций.
364
Потоки
Глава 7
	Чтобы выполнять асинхронные операции без издержек на создание объектов Thread, используйте класс ThreadPool.
	Для периодического вызова методов используйте класс Timer.
	Чтобы получить результаты асинхронной операции, используйте интерфейс lAsyncResult.
	Будьте готовы к перехвату исключений при выполнении асинхронных операций (обычно они генерируются в методе EndXXX).
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Какие методы класса ThreadPool служат для выполнения произвольного кода с помощью потоков из пула ThreadPool? (Укажите все верные ответы.)
A.	ThreadPool.RegisterWaitForSingleObject;
В.	ThreadPool.QueueUserWorkltem;
С.	ThreadPool. UnsafeRegisterWditForSingleObject;
D.	ThreadPool. UnsafeQueueUserWorkltem.
2.	Как приостановить таймер?
А.	Вызывать Dispose для объекта Timer;
В.	вызвать Timer.Change с Timeout.Infinite;
С.	позволить объекту Timer выйти за пределы области действия;
D.	вызвать Timer.Change и указать нулевое время.
Закрепление материала главы
Для закрепления знаний и навыков, полученных при изучении этой главы, выполните следующее:  изучите резюме главы;
	повторите основные термины, использованные в этой главе;
	выполните задания лабораторной работы по темам этой главы;
	выполните предложенные упражнения;
	пройдите пробный экзамен.
Резюме главы
	Класс Thread позволяет разделить операции в приложении и выполнять их одновременно.
	Ключевое слово lock {SyncLock в Visual Basic) обеспечивает безопасность программной модификации данных.
Лабораторная работа 355
	Написание кода, безопасного в многопоточной среде, требует пристального внимания, иначе не избежать взаимных блокировок.
	Класс ReaderWriterLock обеспечивает безопасность модификации объектов, разрешая запись только одному потоку одновременно.
	Классы, производные от WaitHandle (Mutex, Semaphore и Event), позволяют создавать синхронизирующие объекты уровня операционной системы.
	Большинство элементов .NET Framework поддерживает модель асинхронного программирования (АРМ), позволяющую асинхронно выполнять код, не используя напрямую классы ThreadPool и Threads.
	Класс ThreadPool удобен для быстрого создания потоков д дя организации очередей и ожидания объектов, производных от класса WaitHandle.
	Объекты Timer можно использовать для исполнения кода в отдельных потоках через заданные промежутки времени.
Основные термины
	асинхронная модель программирования;
	поток;
	объекты ядра Windows.
Лабораторная работа
Сейчас вы примените на практике знания и навыки, полученные при изучении этой главы. Ответы на вопросы см. в разделе «Ответы» в конце книги.
Занятие 1. Повышение эффективности обработки данных на сервере
Вы — программист в небольшой Интернет-компании. Вы создали простое приложение, которое раз в неделю считывает данные из базы и рассылает их по электронной почте зарегистрированным пользователям Web-сайта компании. Дела у компании идут хорошо и база пользователей уже насчитывает более 100,000 записей. Поскольку число пользователей значительно увеличилось, отправка данных по электронной почте требует слишком много времени. Руководство требует ускорить этот процесс.
Результаты опроса
 Руководитель ИТ-отдела
«Мы заметили, что приложение слишком сильно загружает сервер. Один процессор занят почти полностью, но три других совершенно не используются».
Вопросы
Ответьте на следующие вопросы руководства:
1.	Почему приложение использует не все процессоры?
2.	Как вы планируете решить эту проблему?
366
Потоки
Глава 7
3.	Как сделать, чтобы приложение не использовало слишком много потоков во избежание «зависания»?
Занятие 2. Работа с несколькими приложениями
Вы — разработчик в небольшой компании, специализирующейся на ПО для мониторинга работы различных приборов. Компания создала несколько таких приложений для разных приборов. К сожалению, большинство приборов взаимодействует с системой через интерфейсы, которые могут быть доступны только одному процессу одновременно. Вам поручено разработать план управления доступом к интерфейсу приборов, предотвращающий одновременное обращение потоков.
Вопросы
Ответьте на следующие вопросы руководства:
1. Как синхронизировать приложения так, чтобы они получали доступ к интерфейсу поочередно?
2. Как это повлияет на производительность приложения?
Рекомендуемые упражнения
Чтобы лучше проработать темы экзамена, рассмотренные в этой главе, выполните следующие упражнения.
Создание приложения с ThreadPool
Выполните как минимум первые два упражнения. Если хотите подробно изучить работу класса ThreadPool, выполните также упражнение 3.
Упражнение 1
 Создайте тестовое приложение, которое выводит на консоль данные, включая сведения о потоке, используемые кодом.
	При помощи ThreadPool поместите в очередь 20 экземпляров объекта, записывающего данные.
	Обратите внимание на число потоков и частоту их получения из пула (это можно сделать, наблюдая за идентификатором потока (ManagedThreadld), который используют разные экземпляры кода).
Упражнение 2
	Отобразите размер пула ThreadPool, вызвав методы ThreadPool.GetMinThreads и ThreadPool. GetMaxThreads.
	Измените число потоков в пуле при помощи методов ThreadPool.SetMinThreads и ThreadPool. SetMaxThreads.
	Запустите приложение с различными параметрами и изучите работу пула потоков при этом.
Пробный экзамен 357
Упражнение 3
 Перенесите приложение в многопроцессорные и/или системы с многоядерным процессором. Пронаблюдайте, как изменяется работа пула потоков, а также за максимальным и минимальным числом потоков.
Пробный экзамен
На прилагаемом компакт-диске содержится пробный экзамен, обеспечивающий широкие возможности по самоподготовке. Например, можно проверить усвоение темам этой главы либо всех тем экзамена 70-536. Можно сымитировать настоящий экзамен либо выбрать режим обучения, когда после каждого вопроса приводятся правильные ответы с пояснениями.
Пробный экзамен
Подробнее о пробном экзамене рассказано во введении.
ГЛАВА 8
Домены приложений и службы
Занятие 1. Создание доменов приложений	369
Занятие 2. Конфигурирование доменов приложений	377
Занятие 3. Создание служб Windows	384
В этой главе освещаются две темы: домены приложений и службы. Домены приложений позволяют максимально эффективно и безопасно вызывать внешние сборки. Службы — это отдельная разновидность сборок, выполняющихся в фоновом режиме, не имеющих пользовательского интерфейса и управляемых при помощи специальных инструментов. В этой главе обсуждается создание и конфигурирование доменов приложений, а также разработка и установка служб.
Темы экзамена:
1.	Создание блоков изоляции для общеязыковой исполняющей среды в .NET-приложениях при помощи доменов приложений (см. пространство имен System):
□	создание домена приложения;
□	выгрузка домена приложения;
□	конфигурирование домена приложения;
□	извлечение сведений о настройке из домена приложения;
□	загрузка сборок в домен приложения.
2.	Реализация и установка служб, управление службами (см. пространство имен System. ServiceProcess):
□	наследование от класса ServiceBase',
□	классы ServiceController и ServiceControllerPermissiorr,
□	классы Servicelnstaller и Service Processinstaller,
□	структура SessionChangeDescription и перечислимое SessionChangeReason.
Занятие 1
Создание доменов приложений 35g
Прежде всего
Чтобы освоить материал этой главы, необходимо иметь навыки работы с Microsoft Visual Basic или C# и уметь:
	создавать консольные приложения в Microsoft Visual Studio, используя Visual Basic или С#;
	добавлять в проект ссылки на системные библиотеки классов;
	создавать текстовые файлы;
	добавлять события в журнал событий.
Занятие 1. Создание доменов приложений
Разработчикам часто приходится запускать внешние сборки. Однако запуск внешней сборки может привести к неэффективному расходованию ресурсов и возникновению уязвимостей в защите. Лучший способ держать эти риски под контролем — создать домен приложения и запустить сборку внутри него, под надежной защитой.
Изучив материал этого занятия, вы сможете:
J формулировать назначение домена приложения;
J разрабатывать код, работающий с классом AppDomain',
J создавать домены приложений;
J запускать сборку внутри домена приложения;
J выгружать домен приложения.
Продолжительность занятия — 20 минут.
Введение в домены приложения
Домен приложения (application domain) — логический контейнер, позволяющий выполнять внутри одного процесса несколько сборок, не предоставляя им прямого доступа к памяти других сборок. Домены поддерживают многие из функций, стандартных для процессов, включая разграничение областей памяти и доступа к ресурсам. Однако домены приложений эффективнее процессов, поскольку в одном домене приложений могут работать несколько сборок, но без издержек на запуск отдельных процессов. На рис. 8-1 показан отдельный процесс, содержащий несколько доменов приложений.
ВАЖНО! Ключевое различие между доменами приложений и процессами
Доменами приложения управляет исполняющая среда .NET Framework, а процессами — операционная система.
Наилучшей иллюстрацией применения доменов приложений может служить рабочий процесс ASP.NET в службах IIS 5.0 (Aspnet_wp.exe). Если Web-сайт на основе ASP.NET одновременно посетят 10 человек, для каждого из них ASP.NET создаст отдельный домен приложения, по сути, запустив 10 экземпляров соответствующей сборки.
370 Домены приложений и службы
Глава 8
Операционная система
Процесс
Исполняющая среда .NET Framework
Домен приложения
Рис. 8-1. Домены приложений изолируют сборки внутри процесса
У каждого экземпляра сборки может быть свойство userName, и оно останется недоступным другим сборкам, которые не смогут перезаписать его. Аналогичного эффекта можно достичь, запустив десять копий сборки в десяти отдельных процессах, но переключение между процессами загружает процессор и снижает производительность.
Обычно домены приложений для ваших сборок автоматически создает хост исполняющей среды. Примерами встроенных хостов Microsoft Window могут быть ASP.NET, Internet Explorer (который создает домен приложения для всех сборок открытого Web-сайта) и сама операционная система. Для настройки доменов приложений служат удобные инструменты, такие как Internet Information Services Manager и .NET Framework Configuration.
Подобно процессу Aspnet_wp.exe, программист может создавать собственные домены приложений, чтобы вызывать сборки, не опасаясь некорректных действий со стороны сборки или доступа к ресурсам, запрещенным для нее. На рис. 8-2 показаны сборки с доменами приложений.
Рис. 8-2. Сборки также могут быть хостами для доменов приложений
Сборки изолируют не только в целях защиты, но и для повышения надежности и производительности.
Занятие 1
Создание доменов приложений	37 -|
	Повышение надежности
Используйте домены приложений для изоляции задач, которые могут вызвать аварийное завершение процесса. Если домен приложения становится нестабильным, его можно выгрузить, не затрагивая процесс в целом. Это особенно важно, когда процесс должен длительное время работать без перезапуска. С помощью доменов приложений также изолируют операции, манипулирующие конфиденциальными данными.
	Повышение производительности
Если сборка загружена в домен приложения по умолчанию, ее нельзя выгрузить из памяти, пока выполняется процесс. Но если открыть второй домен приложения, загрузить и запустить в нем сборку, эта сборка может быть выгружена вместе с доменом приложения. Эта методика позволяет сократить рабочий набор процессов, работающих длительное время и использующих громоздкие динамически подключаемые библиотеки (DLL).
Класс AppDomain
В .NET Framework домены приложений реализуют при помощи класса System.AppDomain. Чтобы получить домен приложения, достаточно создать экземпляр класса AppDomain и запустить в нем сборку. В табл. 8-1 приводятся свойства класса AppDomain.
Табл. 8-1. Свойства AppDomain
Имя	Описание
ActivationContext Applicationldentity	Возвращает контекст активации для текущего домена приложения Возвращает идентификационные данные приложения в домене приложения
ApplicationTrust	Возвращает информацию о наличии у приложения разрешения и уровня доверия, достаточного для его запуска
BaseDirectory	Возвращает базовый каталог, в котором распознаватель сборок ищет сборки
CurrentDomain	Возвращает текущий домен приложения для текущего объекта Thread. Позволяет определять контекст текущего домена и проверять его разрешения
DomainManager	Возвращает диспетчер доменов, предоставленный хостом при инициализации домена приложения
DynamicDlrectory	Возвращает каталог, в котором распознаватель сборок производит поиск динамически созданных сборок
Evidence	Возвращает объект Evidence, связанный с доменом приложения и являющийся аргументом политики безопасности. Подробнее про объекты Evidence рассказывается в гл. 11
FriendlyName	Возвращает понятное имя домена приложения. Понятное имя доменов приложений, созданных .NET Framework, имеет вид «ИмяПроекта.УьксМ.ехе.» При программном создании доменов приложений понятное имя указывает разработчик
Jd	Возвращает целочисленный уникальный идентификатор домена приложения внутри процесса
372 Домены приложений и службы
Глава 8
Табл. 8-1. (окончание)
Имя	*	Описание
RelativeSearchPath Setuplnformation ShadowCopyFiles	Возвращает путь относительно базового каталога, где распознаватель сборок должен искать закрытые сборки Возвращает сведения о конфигурации домена приложения для данного экземпляра Позволяет узнать, все ли сборки, загруженные в домен приложения, имеют теневые копии
В табл. 8-2 приводятся наиболее важные методы класса AppDomain.	
Табл. 8-2. Методы AppDomain	
Имя	Описание
ApplyPolicy	Возвращает отображаемое имя сборки после применения
	политики
CreateComlnstanceFrom	Создает новый экземпляр заданного СОМ-типа
CreateDomain	Создает новый домен приложения. Применяется вместо вызова конструктора класса AppDomain
Createlnstance	Создает новый экземпляр заданного типа, объявленного в указанной сборке
CreatelnstanceAnd Unwrap	Создает новый экземпляр заданного типа
Create InstanceFrom	Создает новый экземпляр заданного типа, определенного в указанном файле сборки
CreateInstanceFromAndWrap	Создает новый экземпляр заданного типа, объявленного в указанном файле сборки
DefineDynamicAssembly	Определяет динамическую сборку в текущем домене приложения
DoCallBack	Выполняет код в другом домене приложения, заданном указанным делегатом
ExecuteAssembly	Выполняет сборку из заданного файла
ExecuteAssemblyByName	Выполняет сборку
GetAssemblies	Возвращает сборки, загруженные в контексте выполнения этого домена приложения
GetCurrentThreadld	Возвращает идентификатор текущего потока
GetData	Возвращает значение, хранящееся в текущем домене приложения, по заданному имени
Initialize Lifetime Service	Переопределяемый метод, позволяет AppDomain существовать неопределенно долго без освобождения
IsDefaultAppDomain	Позволяет узнать, является ли домен приложения для процесса доменом приложения по умолчанию
IsFinalizingForUnload	Позволяет узнать, выгружен ли домен приложения и подготовлены ли содержащиеся в нем объекты к уничтожению общеязыковой исполняющей средой
Load	Загружает сборку в домен приложения
Занятие 1
Создание доменов приложений 273
Табл. 8-2. (окончание)
Имя	Описание
ReflectionOnlyGetAssemblies	Возвращает сборки, загруженные в домен приложения в контексте «только для отражения» (reflection-only)
SetAppDomainPolicy	Устанавливает для домена приложения уровень политики безопасности.
SetData	Присваивает значение некоторому свойству домена приложения
SetDynamicBase	Устанавливает заданный каталог как путь для хранения и доступа к динамически созданным файлам
SetPrincipalPolicy	Указывает, как участники безопасности и объекты идентификационных данных должны прикрепляться к потоку при попытке его привязки к участнику безопасности во время выполнения в домене приложения
SetShadowCopyFiles	Включает теневое копирование
SetShadowCopy Path	Устанавливает заданный каталог как путь для теневых копий сборок
SetThreadPrincipal	Устанавливает объект, представляющий участника безопасности по умолчанию, который следует привязать к потокам при попытке связаться с другим участником безопасности во время выполнения в домене приложения
Unload	Выгружает заданный домен приложения
Как создать домен приложения
Для создания домена приложения вызывается один из перегруженных методов AppDomain.CreateDomain. При вызове следует задать хотя бы понятное имя нового домена приложения: ’ VB
Dim d As AppDomain = AppDomain.CreateDomain("NewDomain")
Console.WriteLine("Host domain: " + AppDomain.CurrentDomain.FriendlyName) Console.WriteLine("Child domain: ” + d.FriendlyName)
П C#
AppDomain d = AppDomain.CreateDomain( NewDomain")
Console.WriteLine("Host domain: ’’ + AppDomain.CurrentDomain.FriendlyName);
Console.WriteLine("Child domain: " + d.FriendlyName);
Как видно из приведенного выше кода, при помощи свойства Арр Domain.Current Domain можно получить домен приложения, где выполняется сборка в настоящий момент (возможно, этот домен приложения создан автоматически .NET Framework).	*
374 Домены приложений и службы
Глава 8
Загрузка сборок в домен приложения
Чтобы создать новый домен приложения и запустить в нем сборку, достаточно создать экземпляр класса System. Арр Domain с заданным именем, а затем вызвать метод ExecuteAssembly: ’ VB
Dim d As AppDomain = AppDomain.CreateDomain("NewDomain") d.ExecuteAssembly("Assembly.exe")
// C#
AppDomain d = AppDomain.CreateDomain("NewDomain");
d.ExecuteAssembly("Assembly.exe");
У метода AppDomain.ExecuteAssembly также есть перегруженная версия, позволяющая передавать параметры через командную строку. Вместо указания полного пути к сборке можно добавить ссылку на нее и вызывать сборку по имени методом Арр Domain. Execute Assembly By Name:
' VB
Dim d As AppDomain = AppDomain.CreateDomain("NewDomain") d.ExecuteAssemblyByName("Assembly")
// C#
AppDomain d = AppDomain.CreateDomain("NewDomain");
d.ExecuteAssemblyByName("Assembly");
Такой способ вызова изолирует сборку, но не позволяет в полной мере воспользоваться преимуществами доменов приложений. Конфигурирование доменов приложений обсуждается на занятии 2.
Выгрузка доменов приложений
Одним из преимуществ загрузки сборок в новые домены приложений является возможность в любой момент освободить ресурсы, выгрузив домен приложения. Чтобы выгрузить домен приложения со всеми его сборками, достаточно вызвать статический (в Visual Basic — общий) метод AppDomain. Unload: ' VB
Dim d As AppDomain = AppDomain.CreateDomain("NewDomain")
AppDomain.Unload(d)
// C#
AppDomain d = AppDomain.CreateDomain("NewDomain");
AppDomain.Unload(d);
Отдельные сборки и типы выгрузить нельзя.
Практикум. Создание доменов и загрузка сборок
На этом практикуме вы создадите домен приложения и загрузите в него сборку двумя способами: по имени файла и по ссылке. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Занятие 1
Создание доменов приложений 375
Упражнение 1. Загрузка сборки по имени ее файла
В этом упражнении вы создадите домен приложения и запустите в нем сборку, которая отображает содержимое файла boot.ini.
1.	Скопируйте на жесткий диск папку Chapter08\Lessonl-ShowBootIni с компакт-диска, прилагаемого к книге, и откройте версию проекта ShowBootlni для C# или Visual Basic.
2.	Соберите консольное приложение ShowBootlni и запустите его, чтобы убедиться в его корректной работе. Вместо boot.ini приложение может отображать любой другой текстовый файл.
3.	Создайте консольное приложение с именем ShowFilesDemo.
4.	Добавьте в приложение код, создающий объект AppDomain. Ниже приводится соответствующий пример кода:
VB
Dim d As AppDomain = AppDomain.CreateDomain("NewDomain")	*
// C#
AppDomain d = AppDomain.CreateDomain("New Domain");
5.	Затем напишите код для запуска сборки ShowBootlni внутри созданного домена AppDomain, явно указав полный путь к файлу. Следующий код будет работать, если указать путь к ее исполняемому файлу:
' VB
d.ExecuteAssemblyC"ShowBootlni.exe")
// C#
d.ExecuteAssemblyC"ShowBootlni.exe");
6.	Соберите проект, исправьте ошибки. Убедитесь, что консольное приложение успешно вызывает сборку ShowBootIni.exe, а так — корректно отображает текстовый файл.
Упражнение 2. Загрузка сборки по имени сборки
В этом упражнении вы измените созданное в упражнении 1 консольное приложение так, чтобы вызывать сборку по ее имени, а не по имени файла.
1.	Откройте проект, созданный в упражнении 1.
2.	Добавьте ссылку на сборку ShowBootlni.
3.	Замените вызов AppDomain.ExecuteAssembly вызовом метода AppDomain .ExecuteAs-semblyByName, например, так:
’ VB
Dim d As AppDomain = AppDomain.CreateDomain("NewDomain")
d.ExecuteAssemblyByName("ShowBootlni")
// C#
AppDomain d = AppDomain.CreateDomain("New Domain");
d ExecuteAssemblyByName("ShowBootlni");
4.	Соберите проект, исправьте ошибки. Убедитесь, что консольное приложение успешно вызывает сборку ShowBootIni.exe, а так — корректно выводит текстовый файл в
окне консоли.
376 Домены приложений и службы
Глава 8
Резюме
	Домен приложения — это логический контейнер, позволяющий выполнять несколько сборок внутри одного процесса, не предоставляя им прямого доступа к памяти других сборок. Следует всегда создавать домен приложения, когда требуется запустить сборку.
	Класс AppDomain поддерживает методы для определения привилегий, рабочих папок и других параметров нового домена приложения, а также для запуска сборок и выгрузки доменов приложений.
	Для создания объекта класса AppDomain достаточно вызвать статический (в Visual Basic — общий) метод AppDomain.CreateDomain. Класс AppDomain не имеет традиционных конструкторов.
	Для загрузки сборки в домен приложения создается экземпляр класса AppDomain, а затем вызывается метод Арр Domain. ExecuteAssembly.
	Для выгрузки домена приложения вызывается статический (в Visual Basic — общий) метод AppDomain. Unload.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Выберите веские причины для создания домена приложения. (Укажите все верные ответы.)
А.	это единственный способ запуска отдельного процесса;
В.	можно освободить ресурсы, удалив домен приложения;
С.	домены приложений повышают производительность;
D.	домены приложений обеспечивают изоляцию и защиту.
2.	Как запустить сборку внутри домена приложения? (Укажите все верные ответы.)
A.	AppDomain.CreateDomain;
В.	Арр Domain. Execute Assembly;
С.	Арр Domain. ExecuteAssembly By Name;
D.	AppDomain.Applicationldentity.
3.	Как закрыть домен приложения из приведенного ниже примера?
' VB
Dim d As AppDomain = AppDomain.CreateDomain("New Domain")
d ExecuteAssemblyByName("MyAssembly")
// C#
AppDomain d = AppDomain.CreateDomain("New Domain");
d.ExecuteAssemblyByName("MyAssembly");
A.	d.DomainUnload();
B.	d = null;
C.	d.Unload();
D.	AppDomain.Unload(d);
Занятие 2
Конфигурирование доменов приложений 377
Занятие 2. Конфигурирование доменов приложений
Домены приложений настраивают, чтобы создать подходящую для сборок среду. Это делается в основном для того, чтобы ограничить доступ и устранить уязвимости в системе безопасности. Идеально сконфигурированный домен не только изолирует сборки, но и ограничивает ущерб, который может быть нанесен злоумышленником, взломавшим защиту сборки.
Изучив материал этого занятия, вы сможете:
J запускать сборки в домене приложения с ограниченными привилегиями;
J конфигурировать домены приложения, управлять расположением рабочих папок и другими настройками.
Продолжительность занятия — 25 минут.
Запуск сборок с ограниченными привилегиями
Ограничение разрешений домена приложения значительно снижает риск выполнения злонамеренных действий вызванной сборкой. Рассмотрим следующий сценарий: вы приобретаете сборку у стороннего разработчика и используете ее для работы с базой данных. Злоумышленник обнаруживает в этой сборке уязвимость и использует ее для автоматического запуска шпионской программы. Отвечать за последствия придется вам, поскольку именно ваше приложение доверило чужой сборке и запускало ее с привилегиями, достаточными для установки программ.
Теперь предположим, что используется домен приложения с ограниченными привилегиями. Злоумышленник обнаруживает уязвимость защиты чужой сборки. Но, когда он попытается воспользоваться ей для записи файлов на локальный жесткий диск, запрос ввода-вывода файлов будет отклонен из-за недостаточных привилегий. Уязвимость остается, но воспользоваться ей не удастся из-за ограниченных привилегий домена приложения.
Этот пример так называемой эшелонированной защиты. Такую защиту обеспечивает участник системы безопасности, поддерживающий несколько уровней защиты, срабатывающей даже при возникновении уязвимости на одном из уровней. Эшелонированная защита особенно важна при использовании внешнего кода от сторонних разработчиков, поскольку он может содержать скрытые уязвимости, о которых вы не знаете и потому не можете устранить их.
В следующих разделах описывается использование идентификационных данных (evidence) для конфигурирования доменов приложений. Существует еще несколько способов управления разрешениями сборок. Подробнее о безопасности доступа см. в гл. 11.
Идентификация сборки
После создания домена приложения и запуска сборки разработчик получает полный контроль над идентификационными данными (evidence) сборки. Идентификационные данные (удостоверения) — это информация, о сборке, которую исполняющая среда полу
378 Домены приложений и службы
Глава 8
чает о сборке, чтобы определить, к какой группе кода относится последняя. В свою очередь, принадлежность к той или иной группе кода определяет привилегии сборки. В общем случае удостоверения содержит информацию о папке или Web-сайте, откуда получена сборка, а также ее цифровые подписи.
Назначение сборке удостоверений позволяет управлять предоставлением ей разрешений. Чтобы назначить сборке удостоверения, следует создать объект System.Security.Policy.Evidence, а затем передать его как параметр перегруженному методу ExecuteAssembly объекта AppDomain.
Если объект Evidence создается при помощи конструктора, принимающего в качестве параметров два массива объектов, в первом массиве передают удостоверения хоста, а во втором — сборки. Вместо любого из массивов можно передать null', и если объект удостоверений сборки не создан явно, можно назначить только удостоверения хоста. Может показаться странным, что Evidence принимает в качестве параметров не только Evidence, а любые объекты. Действительно, в качестве удостоверений можно назначать объекты произвольного типа, будь то строка, целочисленное значение или собственный класс. Поэтому даже при использовании встроенных в .NET Framework типов удостоверений их все равно следует передавать в массиве Object.
К СВЕДЕНИЮ Удостоверения
Подробнее об удостоверениях см. в гл. 11.
Проще всего управлять разрешениями, назначаемыми сборке в домене приложения, передав удостоверения зоны безопасности (Zone) при помощи объекта System.Security.Policy.Zone и перечислимого System.Security.SecurityZone. В следующем примере вызывается конструктор Evidence, принимающий два массива объектов: сначала создается объект Zone, затем он добавляется в массив объектов hostEvidence, и уже этот массив передается как параметр конструктору объекта Evidence для создания объекта удостоверений internet Evidence. В завершение объект Evidence передается как параметр методу домена приложения ExecuteAssembly (вместе с именем файла сборки). Следующий пример кода использует пространства имен System.Security и System. Security. Policy.
’ VB
Dim hostEvidence As ObjectO = {New Zone (SecurityZone.Internet)}
Dim internetEvidence As Evidence = New Evidence (hostEvidence, Nothing) Dim myDomain As AppDomain = AppDomain.CreateDomainC'MyDomain") myDomain. ExecuteAssemblyCSecondAssembly.exe", internetEvidence)
// C#
object [] hostEvidence = {new Zone(SecurityZone.Internet)};
Evidence internetEvidence = new Evidence(hostEvidence, null);
AppDomain myDomain = AppDomain.CreateDomain("MyDomain");
myDomain.ExecuteAssembly("SecondAssembly.exe", internetEvidence);
В результате сборка выполняется в изолированном домене приложения с разрешениями группы кода Intemet_Zone. Когда домен приложения запускает сборку, исполняющая среда анализирует ее удостоверения. Поскольку они соответствуют коду из Интернета, исполняющая среда отнесет сборку к группе кода Intemet_Zone, обладающей крайне ограниченным набором разрешений, соответствующим зоне «Интернет».
Занятие 2
Конфигурирование доменов приложений 37g
ВНИМАНИЕ! Управление удостоверениями
Запуск сборки как принадлежащей к группе кода Intemet_Zone удобен для обеспечения максимальной защиты, поскольку в этом случае сборка получает ограниченные разрешения для сборок, полученных из Интернета. Но далеко не факт, что данная сборка получена из Интернета — возможно, она хранится в одной папке с вызывающей сборкой. В данном случае исполняющая среда получает «фальшивые» удостоверения. Технически, можно «предъявить; исполняющей среде такие удостоверения, с которыми она получит больше привилегий, чем положено, существенно расширив круг доступных ей действий. Помешать этому можно, ограничив назначение разрешения SecurityPermission.ControlEvidence (см. гл. 11).
Назначение удостоверений доменам приложений
Удостоверения можно назначать и доменам приложений целиком. Соответствующая процедура похожа на назначение удостоверений сборке — вызывается перегруженный метод AppDomain.CreateDomain, которому передается как параметр объект Evidence (см. код ниже; этот пример требует пространства имен System.Security и System.Security.Policy)-.
• VB
Dim hostEvidence As ObjectO = {New Zone (SecurityZone.Internet)}
Dim appDomainEvidence As Evidence = New Evidence (hostEvidence, Nothing)
Dim d As AppDomain = AppDomain.CreateDomain("MyDomain", appDomainEvidence) d.ExecuteAssembly("SecondAssembly.exe")
// C#
object [] hostEvidence = {new Zone(SecurityZone.Internet)};
Evidence appDomainEvidence = new Evidence(hostEvidence, null);
AppDomain d = AppDomain.CreateDomain("MyDomain", appDomainEvidence);
d.ExecuteAssembly("SecondAssembly.exe");
Настройка свойств домена приложения
Класс AppDomainSetup позволяет передать исполняющей среде информацию о конфигурации нового домена приложения. Важнейшее свойство доменов приложений, созданных пользователем, — Application Base. Другие свойства AppDomainSetup используются в основном хостами исполняющей среды для настройки отдельных доменов приложений Изменение свойств экземпляра AppDomainSetup не отражается на существующих AppDomain. Объект AppDomainSetup влияет только на создаваемые AppDomain, когда передается методу CreateDomain в качестве параметра.
В табл. 8-3 приводятся наиболее полезные свойства AppDomainSetup.
Табл. 8-3. Свойства AppDomainSetup
Имя	Описание
ActivationArguments	Возвращает	или	устанавливает	данные об активизации^домена
приложения
ApplicationBase	Возвращает или устанавливает имя корневого каталога
приложения. При разрешении запросов типов исполняющая среда ищет сборку с нужным типом в папке, заданной свойством ApplicationBase
380 Домены приложений и службы
Глава 8
Табл. 8-3. (окончание)
Имя *	Описание
ApplicationName Application Trust	Возвращает или устанавливает имя приложения Возвращает или устанавливает объект с информацией о защите и уровне доверия
ConfigurationFile	Возвращает или устанавливает имя конфигурационного файла домена приложения
DisallowApplicationBaseProbing	Определяет, следует ли искать загружаемые сборки в корневом каталоге приложения (заданном свойством ApplicationBase) и каталоге закрытых двоичных сборок (свойство PrivateBinaryPath)
DisallowBindingRedirects	Возвращает или устанавливает значение, определяющее, допускает ли домен приложения переадресацию привязки сборок
DisallowCodeDownload	Возвращает или устанавливает значение, определяющее, разрешена ли загрузка сборок по HTTP для этого домена приложения. По умолчанию это свойств равно false, что таит угрозу для служб (см. занятие 3). Для предотвращения загрузки ненадежного кода служб установите это свойство в true
Disallow Publisher Policy	Возвращает или устанавливает значение, определяющее, применяется ли секция политики издателя из конфигурационного файла к домену приложения
DynamicBase	Возвращает или задает базовый каталог для каталога с динамически генерируемыми файлами
LicenseFile	Возвращает или устанавливает путь к файлу с лицензией для этого домена
LoaderOptimization	Определяет политику оптимизации, используемую при загрузке исполняемого файла
PrivateBinPath	Возвращает или устанавливает список каталогов в корневом каталоге приложения (свойство ApplicationBase), в которых ведется поиск закрытых сборок
Чтобы применить перечисленные выше свойства к домену приложения, достаточно создать и настроить объект Арр Domain Setup, а затем передать его (вместе с объектом Evidence) методу AppDomain.CreateDomain, как показано ниже:
' VB
Создание и инициализация объекта настроек для второго AppDomain
Dim ads As AppDomainSetup = New AppDomainSetup
ads.ApplicationBase = "file://" + System.Environment.CurrentDirectory
ads.DisallowBinoingRedirects = False
ads.DisallowCodeDownload = True
ads.ConfigurationFile =
AppDomain.Cu r rentDomain.Setupinformation.ConfigurationFile
Создание второго домена приложения AppDomain
Dim d As AppDomain = AppDomain.CreateDomain("New Domain", Nothing, ads)
Занятие 2
Конфигурирование доменов приложений
// C#
// Создание и инициализация объекта настроек для второго AppDomain.
AppDomainSetup ads = new AppDomainSetupO;
ads.ApplicationBase = "file://" + System.Environment.CurrentDirectory;
ads.DisallowBindingRedirects = false;
ads.DisallowCodeDownload = true;
ads.ConfigurationFile =
AppDomain.CurrentDomain Setupinformation.ConfigurationFile;
// Создание второго домена приложения AppDomain
AppDomain d = AppDomain.CreateDomain("New Domain", null, ads);
Получить и проверить значения свойств текущего домена приложения можно при помощи свойства AppDomain.CurrentDomain.Setupinformation'.
' VB
Dim ads As AppDomainSetup = AppDomain.CurrentDomain.Setupinformation
Console.WriteLine(ads.ApplicationBase)
Console.WriteLine(ads.ApplicationName)
Console.WriteLine(ads.DisallowCodeDownload)
Console.WriteLine(ads.DisallowBindingRedirects)
// C#
AppDomainSetup ads = AppDomain.CurrentDomain.Setupinformation;
Console WriteLirie(ads.ApplicationBase);
Console.WriteLineC ads.ApplicationName);
Console.WriteLine(adsDisallowCodeDownload);
Console.WriteLine(ads.DisallowBindingRedirects);
Практикум. Управление привилегиями домена приложения
На этом практикуме вы создадите домен приложения с ограниченными привилегиями с целью снижения угроз безопасности при запуске внешних сборок. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение. Загрузка сборки с ограниченными привилегиями
В этом упражнении вы загрузите сборку без права чтения системных файлов.
1.	Скопируйте на жесткий диск папку Chapter08\Lesson2-Exercisel-AppDomainDemo с компакт-диска, прилагаемого к книге, и откройте версию проекта AppDomainDemo для C# или Visual Basic.
2.	Импортируйте в проект пространства имен System.Security и System.Security.Policy.
3.	Перед созданием AppDomain создайте объект Evidence для зоны безопасности «Интернет» следующим образом:
' VB
’ Создание объекта Evidence для зоны безопасности «Интернет»
Dim safeZone As Zone = New Zone(SecurityZone Internet)
382 Домены приложений и службы	Глава 3
Dim hostEvidence As ObjectO = {New Zone(SecurityZone.Internet)}
Dim e As Evidence = New Evidence(hostEvidence, Nothing)
7/ C#
// Создание объекта «Evidence» для зоны безопасности «Интернет»
Zone safeZone = new Zone(SecurityZone.Internet);
object[] hostEvidence = { new Zone(SecurityZone.Internet) };
Evidence e = new Evidence(hostEvidence, null);
4.	При вызове метода AppDomain.CreateDomain передайте созданный объект удостоверений, например:
' VB
' Создание домена приложения
Dim d As AppDomain = AppDomain.CreateDomain("NewDomain", e)
11 C#
// Создание домена приложения.
AppDomain d = AppDomain.CreateDomain("New Domain", e);
5.	Соберите и запустите консольное приложение AppDomainDemo. На этот раз при попытке сборки запустить ShowBootlni исполняющая среда сгенерирует исключение SecurityException. Дело в том, что домен приложения создан в зоне безопасности «Интернет», у которой отсутствуют привилегии для чтения файла Boot.ini. Если сборка содержит уязвимость или в нее сознательно добавлен вредоносный код, то удостоверения домена приложения, дающие лишь ограниченные привилегии, помогут предотвратить заражение вирусом или атаку шпионской программы.
Резюме
	Запустить в домене приложений сборку с ограниченными привилегиями проще всего, указав в ее удостоверениях зону безопасности с ограниченными разрешениями, такую как «Интернет».
	Чтобы сконфигурировать домен приложения, следует создать экземпляр класса AppDomainSetup и воспользоваться им для создания домена приложения.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Для чего указывают удостоверения домена приложения при его создании?
А.	Для определения приоритета процесса;
В.	для идентификации автора сборки;
С.	для определения привилегий, которые следует предоставить сборке;
D.	для отслеживания действий сборки.
Занятие 2
Конфигурирование доменов приложений 333
2.	Какой из приведенных ниже фрагментов кода запускает сборку в зоне «Интернет»? (Укажите все верные ответы.) А.
1 VB
Dim hostEvidence As ObjectO = {New Zone (SecurityZone.Internet)}
Dim e As Evidence = New Evidence (hostEvidence, Nothing) Dim d As AppDomain = AppDomain.CreateDomain("MyDomain", e)
d. ExecuteAssembly("Assembly.exe")
11 C#
object [] hostEvidence = {new Zone(SecurityZone Internet)};
Evidence e = new Evidence(hostEvidence, null);
AppDomain d = AppDomain.CreateDomain("MyDomain", e);
d. ExecuteAssembly("Assembly.exe");
B.
 VB
Dim hostEvidence As ObjectO = {New Zone (SecurityZone.Internet)}
Dim d As AppDomain = AppDomain.CreateDomain("MyDomain")
Dim e As Evidence = New Evidence (hostEvidence, Nothing) d.Evidence = e
d.ExecuteAssembly("Assembly.exe")
// C#
object [] hostEvidence = {new Zone(SecurityZone.Internet)};
AppDomain d = AppDomain.CreateDomain("MyDomain");
Evidence e - new Evidence(hostEvidence, null);
d.Evidence = e;
d.ExecuteAssembly("Assembly exe");
C.
• VB
Dim myDomain As AppDomain = AppDomain.CreateDomain("MyDomain") myDomain.ExecuteAssembly("Assembly.exe", New Zone (SecurityZone.Internet))
// C#
AppDomain myDomain = AppDomain.CreateDomain("MyDomain");
myDomain.ExecuteAssembly("Assembly.exe", new Zone(SecurityZone.Internet));
D.
' VB
Dim e As Evidence = New Evidence
e.AddHost(New Zone (SecuntyZcne.Internet))
Dim myDomain As AppDomain = AppDomain.CreateDomain("MyDomain") myDomain ExecuteAssemblyCAssembly.exe", e)
384 Домены приложений и службы
Глава 8
И с#
Evidence е = new EvidenceO;
е.AddHost(new Zone(SecurityZone.Internet));
AppDomain myDomain = AppDomain.CreateDomain("MyDomain");
myDomain.ExecuteAssembly("Assembly.exe", e);
3. Как задать корневой каталог приложения в домене приложения?
А. создать экземпляр класса AppDomain, затем установить значение свойства DynamicDirectory’,
В. создать экземпляр класса AppDomain, затем установить значение свойства BaseDirectory’, С. создать экземпляр класса AppDomainSetup, затем установить значение свойства DynamicBase. Передать объект AppDomainSetup в качестве параметра конструктору AppDomain’,
D. создать экземпляр класса AppDomainSetup, затем установить свойство ApplicationBase. Передать объект AppDomainSetup в качестве параметра конструктору AppDomain’,
4. Вам требуется уведомить пользователя, если сборке запрещено загружать другие сборки по HTTP. Как проверить наличие такого разрешения?
А. проверить AppDomain.CurrentDomain.Setupinformation.DisallowCodeDownload\
В. проверить AppDomain.CurrentDomain.DisallowCodeDownload',
С. проверить AppDomain.CurrentDomain.Setupinformation.DisallowPublisherPohcy, D. проверить AppDomain.CurrentDomain.Disallow Publisher Policy.
Занятие 3. Создание служб Windows
Создание сборки как службы позволяет запускать ее в фоновом режиме, без взаимодействия с пользователем. Службы идеально подходят для непрерывного мониторинга каких-либо событий, прослушивания сетевых подключений и запуска сборок до входа пользователя в систему. Эти особенности служб — причина нестандартных требований к их защите и установке.
Изучив материал этого занятия, вы сможете:
J формулировать назначение служб;
J создавать проекты служб в Visual Studio;
J задавать свойства службы;
J устанавливать службу вручную;
J создавать проект установочной программы для службы;
J запускать службу и управлять ей, используя встроенные инструменты
Windows.
Продолжительность занятия — 45 минут.
Введение в службы Windows
Службы Windows — это процессы, выполняющиеся в фоновом режиме, не имеющие пользовательского интерфейса и работающие в отдельном сеансе. Службы могут запускаться автоматически при загрузке компьютера, даже если пользователь не вошел в си-
Занятие 3
Создание служб Windows 335
стему. Таким образом, службы идеально подходят для реализации приложений, которые должны непрерывно работать без взаимодействия с пользователем. В Windows имеется множество встроенных служб, включая Server (для создания в сети общих папок), Workstation (для подключения к общим папкам) и World Wide Web Publishing (для обслуживания Web-страниц).
ПРИМЕЧАНИЕ Создание служб Windows в различных версиях Visual Studio
Шаблон Служба Windows и связанные с ним функции не доступны в Visual Studio Standard Edition.
Существует несколько различий в работе служб и других приложений*
	Для корректной работы проекта службы следует предварительно установить исполняемый файла, сгенерированный проектом. Службы невозможно отлаживать и запускать нажатием клавиш F5 или F11, как нельзя запускать их напрямую и вести пошаговую отладку кода служб. Вместо этого следует установить и запустить службу, а затем подключить к ее процессу отладчик.
ПРИМЕЧАНИЕ Отладка служб
Подробнее об отладке служб см. в статье по адресу http://msdn.microsoft.com/library/rus/ default.asp?url=/libraiy/RUS/vbcon/html/vbtskdebuggingserviceapplications.asp.
	В отличие от других типов проектов, для служб нужны установочные компоненты. Они устанавливают и регистрируют службу на сервере, а также создают запись о службе при помощи Диспетчера управления службами Windows (Windows Services Control Manager).
	Метод Main службы должен послать команду Run службам в составе проекта. Метод Run загружает службы в Диспетчер управления службами (Services Control Manager) сервера. При использовании шаблона Служба Windows (Windows Services) данный метод создается автоматически.
	Службы Windows работают в отдельной оконной станции, отличной от той, с которой взаимодействует пользователь, вошедший в систему. Оконная станция — это защищенный объект, содержащий буфер обмена (Clipboard), набор глобальных атомов и группу объектов рабочего стола. Поскольку станция службы Windows не является интерактивной и не поддерживает диалоги, диалоговые окна, вызванные кодом службы, не будут отображаться и могут привести к зависанию программы. Аналогично, сообщения об ошибках служб должны записываться в журнал событий Windows, а не выводиться на экран.
	Службы Windows выполняются в собственном контексте безопасности и запускаются до входа пользователя в систему. Необходимо с осторожностью выбирать учетную запись, от имени которой будет запускаться служба. Службы, запущенные от имени системной учетной записи имеют больше разрешений и привилегий, чем службы, запущенные от имени учетной записи пользователя. Чем больше привилегий у службы, тем больше ущерб, который могут причинить взломавшие ее злоумышленники. Поэтому следует запускать службу с минимальными привилегиями, чтобы минимизировать возможный ущерб.	<
4
386 Домены приложений и службы
Глава 8
Пример из практики
Тони Нортроп (Топу Northrup)
Я начал работать с .NET Framework с первой бета-версии. Однако в ранних версиях .NET Framework создание служб не поддерживалось. Мне не хотелось возвращаться к прежней среде разработки, поэтому пришлось использовать недокументированные возможности для запуска сборок в фоновом режиме. Обычно я создавал консольное приложение и при помощи Планировщика заданий настраивал его для автоматического запуска от имени учетной записи пользователя. Так удавалось добиться непрерывной работы процесса в фоновом режиме, но управлять им было неудобно из-за невозможности воспользоваться оснасткой Службы (Services).
Создание проекта службы
Чтобы создать проект службы, выполните следующие действия:
1.	Создайте проект на основе шаблона Windows Service, как показано на рис. 8-3. Шаблон автоматически создаст класс, производный от класса ServiceBase и генерирует основную часть кода, например код для запуска службы.
Рис. 8-3. В состав Visual Studio входит шаблон приложения Windows Service
2.	Напишите код для методов OnStart и OnStop, и переопределите остальные требуемые методы.
3.	Добавьте компоненты, необходимые для установки службы. По умолчанию класс с установочными компонентами добавляется к приложению по щелчку ссылки Add Installer. Первый компонент-установщик устанавливает процесс, а другой — все службы проекта.
4.	Соберите проект.
Занятие 3
Создание служб Windows 337
5.	Создайте проект установочной программы для службы и установите ее.
6.	Запустите службу, воспользовавшись оснасткой Службы (Services).
Ниже рассказывается, как написать код для реализации этих действий.
Реализация службы
После создания проекта службы в Visual Studio выполните следующее:
1.	Средствами дизайнера измените свойство ServiceBase.ServiceName. У служб должны быть уникальные имена, так что это очень важный момент. ServiceName — это не то имя, которое отображается оснасткой Службы (Services). По ServiceName операционная система распознает службы, это имя также применяется для программной идентификации служб. Например, можно запустить службу из командной строки, выполнив команду Net Start ServiceName.
2.	Добавьте в метод OnStart код, отвечающий за опрос и мониторинг системы. Обратите внимание: метод OnStart на самом деле ничего не отслеживает. После запуска службы метод OnStart должен вернуть управление операционной системе. В этом методе не должно быть бесконечных циклов и блокировок. Чтобы реализовать простой механизм опроса, воспользуйтесь компонентом System. Timers. Timer. Следует задать параметры этого компонента в методе OnStart, а затем установить его свойство Enabled в true. Таймер будет периодически генерировать события, в момент которых служба будет вести мониторинг (см. пример в практикуме упражнения 1).
3.	Добавьте в метод OnStop код, выполняющий все действия, которые требуются при остановке службы.
4.	По желанию можно переопределить методы On Pause, OnContinue и OnShutdown. OnPause вызывается, когда пользователь приостанавливает службу через оснастку Службы (Services), что случается довольно редко. OnContinue вызывается, когда приостановленная служба возобновляет работу. OnShutdown вызывается, когда операционная система завершает работу. Если эти методы переопределяются, следует установить свойства ServiceBase.CanPauseAndContinue или ServiceBase.CanShutdown в true.
Создание установочной программы для службы
В отличие от других приложений, службу нельзя просто запустить, как обычный исполняемый файл. Это ограничение не дает запускать и отлаживать службу прямо в среде разработки Visual Studio. Прежде чем запустить службу, ее необходимо установить. Для этого .NET Framework предоставляет классы ServiceInstaller и ServiceProcessInstaller. Servicelnstaller позволяет задать описание службы, ее отображаемое имя, имя службы и тип запуска. При помощи ServiceProcessInstaller можно настроить учетную запись службы.
На практике писать код, работающий с классами Servicelnstaller и ServiceProcessInstaller, не требуется, поскольку Visual Studio генерирует его автоматически, Чтобы создать программу установки службы при помощи Visual Studio, выполните следующее: 1. Перейдите в режим дизайнера в Visual Studio. Щелкните правой кнопкой мыши в окне дизайнера и в контекстном меню выберите пункт Add Installer — Visual Studio создаст компонент Projectinstaller.
2.	Задайте свойство StartType для компонента Projectinstaller Servicelnstaller, выбрав одно из следующих значений:
□	Automatic
Служба будет запускаться австоматически при загрузке компьютера, независимо от того, вошел ли в систему пользователь.
388 Домены приложений и службы
Глава 8
□	Manual (по умолчанию)
Пользователь будет запускать службу вручную.
□	Disabled (отключено)
Служба не будет запускаться автоматически, и пользователи не смогут ее запустить, пока не изменят тип запуска
3.	Задайте для компонента Service Installer свойства Description и Display Name.
4.	Задайте контекст безопасности службы, выбрав для свойства Account компонента Projectinstaller ServiceProcessInstaller одно из следующих значений:
□	LocalService
Служба выполняется в контексте учетной записи непривилегированного пользователя текущего компьютера, предоставляя удаленным серверам удостоверения анонимного пользователя. LocalService сводит к минимуму угрозы системе безопасности.
□	NetworkService
Позволяет службе проходить аутентификацию на других компьютерах сети. Такая аутентификация не требуется для анонимных подключений, которые допускает большинство Web-серверов.
□	LocalSystem
Служба выполняется с практически неограниченными привилегиями, удаленным серверам при этом передаются учетные данные текущего компьютера. Применение этой учетной записи представляет серьезную угрозу безопасности, так как уязвимость вашего приложения позволит получить полного контроля над компьютером пользователя.
□	User (по умолчанию)
При установке службы запрашиваются имя и пароль пользователя (если они не заданы в свойствах Username и Password объекта ServiceProcessInstaller).
5.	Определите начальный объект (точку входа) в проекте службы. Щелкните правой кнопкой мыши проект в дереве Solution Explorer, затем щелкните Properties. В Project Designer на вкладке Application выберите в списке Startup Object класс службы.
6.	Соберите проект.
Теперь можно установить службу вручную при помощи инструмента InstallUtil или создать проект с мастерами установки либо пакет Windows Installer (MSI-файл). Обе эти возможности будут рассмотрены в следующих разделах.
Установка служб вручную
Когда код службы готов и проект собран, можно установить службу вручную. Для этого достаточно запустить в командной строке инструмент InstallUtil.exe с именем сборки службы в качестве параметра. Команда InstallUtil yourservice. ехе установит службу (сборка которой называется yourservice.exe), а команда InstallUtil /и yourservice ехе — удалит службу.
Как создать проект установочной программы для службы
1.	Добавьте к текущему решению проект Setup Project, как показано на рис. 8-4.
2.	Добавьте проект службы как выходной элемент к проекту установочной программы: А. Щелкните правой кнопкой мыши проект установки в Solution Explorer и выберите Add | Project Output.
Занятие 3
Создание служб Windows 389
lanplate»^^
Eroject types:
Visual Studto Instated templates
My Templates
^Search Onine Templates...
Add Nrv/ Project
Srhjplreject
J* "H Module Project CAB Project
Цате-
C:\Documents and SetdngsjW»dowsServtcel
Location;

Рис. 8-4. Добавление проекта установки, упрощающего развертывание служб
S Visual Basic Windows ; tf Smart Device Database i Stater Kits
S Visual C# И Visual J# & Visual C++
S Other Project Types Setup and Deployment Database ExtensfeMy
SWeb Setup Project
Sehp Wizard jjBSmart Device CAB Project
В. В открывшемся окне Add Project Output Group выберите из списка Project ваш проект службы, щелкните Primary Output и ОК.
3.	Добавьте пользовательское действие для установки исполняемого файла службы:
А.	Щелкните правой кнопкой мыши проект установочной программы в Solution Explorer, выберите View | Custom Actions.
В.	В редакторе действий щелкните правой кнопкой Custom Actions и выберите Add Custom Action.
С.	В открывшемся окне Select Item In Project выберите Application Folder | Primary Output From имя_проекта (рис. 8-5). Щелкните OK — основной вывод будет добавлен ко всем узлам пользовательских действий: Install, Commit, Rollback и Uninstall
Рис. 8-5. Создание проекта установки для службы — отдельная задача
390 Домены приложений и службы
Глава 8
D.	В Solution Explorer щелкните правой кнопкой проект установки и выберите команду Build. Теперь в папке проекта находится файл Setup.exe — программа интерактивной установки службы и MSI-файл для автоматического развертывания службы.
Установленную службу можно удалить одним из стандартных способов: вручную, при помощи инструмента Установка и удаление программ (Add or Remove Programs) или автоматически, при помощи инструмента Установщик Windows (Windows Installer, MSI).
Управление службами и их настройка
После установки службы ее следует запустить. Если для нее задан тип запуска Авто (Automatic), служба сама запустится после перезагрузки компьютера. Если же задан тип запуска Вручную (Manual) либо требуется запустить службу, не перезагружая компьютер, следует воспользоваться оснасткой Службы (Services)'.
1.	Войдите в систему под учетной записью администратора или пользователя с правом управления службами, щелкните кнопку Пуск (Start), правой кнопкой мыши щелкните Мой компьютер (Му Computer), выберите Управление (Manage).
2.	Раскройте ветвь Службы и приложения (Services And Applications), затем щелкните Службы (Services).
3.	На правой панели щелкните правой кнопкой вашу службу, затем щелкните Пуск (Start), как показано на рис. 8-6.
Рис. 8-6. Запуск служб из оснастки Службы (Services)
Также можно останавливать [Стоп (Stop)], приостанавливать [Пауза (Pause)], возобновлять [Продолжить (Resume)] или перезапускать [Перезапустить (Restart)] службы. Чтобы изменить тип запуска службы или учетную запись пользователя, под которой она работает, щелкните службу правой кнопкой и выберите Свойства (Properties), как показано на рис. 8-7.
Управлять службами можно также из командной строки посредством команды Net: Net Start имя_службы или Net Stop имя службы.
Занятие 3
Создание служб Windows 3g )
Рис. 8-7. Настройка типа запуска и учетной записи службы
Для программного управления службами применяют класс System.ServiceProcess.Ser-viceController. Он позволяет подключаться к службам на локальном или удаленном компьютере, получать сведения о функциях службы, а также запускать, останавливать, приостанавливать и возобновлять работу служб. В следующем примере, требующем пространства имен System.ServiceProcess (необходимо импортировать вручную) и System.Threading, показано, как это делается:
• VB
’ Подключение к службе Server
Dim sc As Servicecontroller = New ServiceController("Server")
Остановка службы sc.StopO
Пауза в две секунды перед новым запуском службы Thread.Sleep(2000)
Запуск службы
sc StartO
// C#
// Подключение к службе Server
Servicecontroller sc = new ServiceController("Server");
// Остановка службы sc.StopO;
392 Домены приложений и службы
Глава 8
// Пауза на две секунды перед новым запуском службы
Thread Sleep(2000);
// Запуск службы sc. StartO;
Практикум. Создание, установка и запуск службы мониторинга Web-сайта
На этом практикуме вы создадите в Visual Studio проект службы и напишете ее код. Эта служба будет каждые 10 секунд записывать в журнал состояние Web- страницы. Затем вы создадите проект установочной программы для службы. В завершение вы установите и запустите службу.
Упражнение 1. Создание службы для отслеживания состояния Web-сайта
В этом упражнении вы создадите и скомпилируете службу Windows, проверяющую состояние Web-сайта каждые 10 секунд и записывающую в журнал сообщение об успешном или неудачном запросе Web-страницы.
1.	Создайте в Visual Studio проект на основе шаблона Windows Service. Назовите этот проект MonitorWebSite.
2.	В режиме дизайнера измените свойства Name и ServiceName, указав для них значение MonitorWebSite. Установите свойства CanPauseAndContinue и CanShutdown в True.
3.	Импортируйте в проект пространства имен Sy stem.Timers, System.IO и System.Net.
4.	В теле класса MonitorWebSite создайте объект Timer. Ниже приводится соответствующий пример кода:
VB
Private t As Timer = Nothing
// C#
private Timer t = null;
5.	В теле конструктора MonitorWebSite (в Visual Basic метод New располагается в файле Service 1.Designer.VB) настройте таймер так, чтобы он вызвал метод каждые 10 секунд:
VB
t = New Timer(IOOOO)
AddHandler t.Elapsed. New System.Timers.ElapsedEventHandler(AddressOf Me.t_Elapsed)
// C#
t = new Timer(10000);
t.Elapsed += new ElapsedEventHandler(t_Elapsed);
6.	Добавьте следующий код в метод OnStart, чтобы активизировать и запустить таймер:
V3
t StartO
Занятие 3
Создание служб Windows 393
// C# t.Start();
7.	Добавьте следующий код в метод OnStop, чтобы остановить таймер:
VB
t Stop()
// C#
t. StopO;
8.	Переопределите методы On Pause, OnContinue и OnShutdown и добавьте в них код, запускающий и останавливающий таймер:
' VB
Protected Overrides Sub OnContinueO
t.StartO
End Sub
Protected Overrides Sub OnPauseO
t.	StopO
End Sub
Protected Overrides Sub OnShutdown()
t.	StopO
End Sub
// C#
protected override void OnContinueO
{
t.StartO;
}
protected override void OnPauseO
{
t.	StopO;
protected override void OnShutdown()
t.	StopO;
}
9. В метод-обработчик события Elapsed (ElapsedEventHandler) добавьте код для проверки Web-сайта и записи текущего времени и кода состояния в текстовый файл. Если получено исключение, добавьте соответствующую запись в журнал событий, так как у службы нет пользовательского интерфейса, с помощью которого она могла бы сообщить пользователю об исключении. Это делается так:
' VB
Protected Sub t_Elapsed(ByVal sender As System.Object,
394 Домены приложений и службы	Глава 8
ByVai е As System.Timers.ElapsedEventArgs)
Try
' Отправка HTTP-запроса
Dim url As String = "http://www microsoft.com"
Dim g As HttpWebRequest = CType(WebRequest.Create(url), _ HttpWebRequest)
Dim r As HttpWebResponse = CType(g.GetResponse, HttpWebResponse)
’ Запись отклика в журнал, представленный текстовым файлом
Dim path As String =
AppDomain.CurrentDomain.Setupinformation.ApplicationBase + "log.txt”
Dim tw As Textwriter = New StreamWriter(path, True)
tw WriteLine(DateTime.Now.ToString + " for " + url + ":	+ _
r.StatusCode.ToSt ring)
tw. CloseO
' Закрытие объекта HTTP-отклика
r. CloseO
Catch ex As Exception
System Diagnostics.Eventlog.WriteEntry("Application", _ Exception: " + ex.Message.ToString)
End Try
End Sub
// C#
void t_Elapsed(object sender, ElapsedEventArgs e)
{
try
// Отправка НТТР-запроса
string url = "http://www microsoft.com";
HttpWebRequest g = (HttpWebRequest)WebRequest.Create(url);
HttpWebResponse r = (HttpWebResponse)g. GetResponseO;
// Запись отклика в журнал, представленный текстовым файлом string path =
AppDomain.Си гrentDomain.Setupinformation.ApplicationBase + "log.txt”;
Textwriter tw = new StreamWriter(path, true);
tw.WriteLine(DateTime.Now.ToStringO + ” for " + url +
": " + r.StatusCode.ToStringO);
tw. CloseO;
// Закрытие объекта HTTP-отклика
r. CloseO;
Занятие 3
Создание служб Windows 395
}
catch (Exception ex)
{
System.Diagnostics.EventLog.WriteEntry("Application", "Exception: " + ex.Message.ToStringO);
}
}
10. Соберите проект и исправьте ошибки. Заметьте: пока нет программы-установщика, службу запустить невозможно.
Упражнение 2. Создание установщика службы
В этом упражнении вы разработаете установочную программу для проекта, созданного в упражнении 1.
1.	Добавьте компонент-установщик к проекту службы.
2.	Задайте для свойств установщика следующие значения:
□	Тип запуска StartType Automatic.
□	Description
«Записывает в журнал отклики от Web-сайта Microsoft.com».
□	DisplayName
«Web Site Monitor».
□	Учетная запись Account
LocalSystem. Заметьте, что обычно использовать учетную запись LocalSystem не рекомендуется, но этому проекту требуется доступ для записи к текстовому файлу, возможный при использовании LocalSystem. А лучше создать учетную запись пользователя и наделить ее только необходимыми привилегиями, но создание учетных записей не входит в задачи этого практикума.
3.	Если это еще не сделано, назначьте проект службы точкой входа.
4.	Добавьте к решению проект Setup Project, затем добавьте выходной элемент на основе проекта службы к проекту программы установки.
5.	Добавьте пользовательское действие для установки исполняемого файла службы в папку приложения.
6.	Соберите проект установочной программы.
Упражнение 3. Установка, запуск и управление службой
В этом упражнении вы установите проект, созданный в упражнениях 1 и 2, и выполните над ним некоторые действия.
1.	Запустите файл Setup.exe, созданный в упражнении 2, и установите службу со настройками по умолчанию.
2.	Откройте консоль Управление компьютером (Computer Management) и выберите узел Службы (Services).
3.	Правой кнопкой мыши щелкните службу Web Site Monitor и выберите коцднду Пуск (Start). Обратите внимание на то, что оснастка Службы (Services) показывает значения свойств Name и Description, заданные в упражнении 2.
396 Домены приложений и службы
Глава 8
4.	Подождите 30 секунд и откройте текстовый файл, в который ваша служба записывает отклики. Убедитесь, что опрос Web-сервера прошел успешно, и результат попал в файл журнала.
5.	Приостановите службу, подождите 30 секунд, и убедитесь, что за это время информация не добавилась в журнал.
6.	Возобновите работу службы, подождите 30 секунд: служба вновь станет заносить информацию в файл журнала.
7.	Остановите службу, открыв интерфейс командной строки и выполнив команду net stop monitorwebsite.
8.	Удалите службу, запустив Setup.exe еще раз.
Резюме
	Служба Windows — это процесс, выполняющийся в фоновом режиме, в отдельном сеансе и не имеющий пользовательского интерфейса.
	Чтобы создать службу Windows в Visual Studio, создайте проект на основе шаблона Windows Service. Затем напишите код методов OnStart, OnStop и переопределите остальные методы, необходимые для работы службы. Добавьте к службе компоненты-установщики. В завершение создайте проект установочной программы для службы.
	Для службы следует указать имя, описание и тип запуска, затем переопределите методы OnStart, OnStop, OnPause, OnContinue и On Shutdown.
	Для создания проекта установочной программы службы следует определить свойства объекта Servicelnstaller, задав описание службы, отображаемое имя, имя службы и тип запуска. Затем определите свойства объекта ServiceProcessInstaller, укажите учетную запись для службы. После этого можно установить службу как вручную, так и создать для нее проект установочной программы.
	Управлять службой вручную можно через командную строку (командой Net), оснастку Службы (Services) или программно — при помощи класса System. ServicePro-cess.ServiceController.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги, -------------------------а......... ..... ...............-............—— _  
1.	Какая учетная запись несет минимальную угрозу безопасности?
A. LocalService',
В. NetworkService',
С. LocalSystem', D. User.
2.	Какая учетная запись позволит свести к минимуму проблемы из-за недостаточных привилегий на локальном компьютере?
A. LocalService',
В. NetworkService',
С. LocalSystem',
D. User.
Основные термины 397
3.	Как установить службу на компьютер? (Укажите все верные ответы.)
А.	добавить ярлык сборки в группу Автозагрузка (Startup) в профиле пользователя;
В.	применить InstallUtil;
С.	настроить Планировщик задач (Scheduled Tasks) для запуска сборки при загрузке;
D.	создать в Visual Studio установочную программу для службы.
4.	Какие средства позволят изменить учетную запись, под которой работает установленная служба?
А. Мой комьютер (Му Computer);
В.	Управление компьютером (Computer Management);
С.	команда net;
D.	Microsoft .NET Framework 2.0 Configuration.
Закрепление материала главы
Для закрепления знаний и навыков, полученных при изучении этой главы, выполните следующее:  изучите резюме главы;
	повторите основные термины, использованные в этой главе;
	выполните задания лабораторной работы по темам этой главы;
	выполните предложенные упражнения;
	пройдите пробный экзамен.
Резюме главы
	Домены приложений — это логические контейнеры, благодаря которым несколько сборок могут работать внутри одного процесса, не получая прямого доступа к памяти других сборок. Домены приложений обеспечивают разграничение областей памяти и доступа к ресурсам без издержек на создание отдельных процессов.
	При создании домена приложения можно настроить многие его параметры. Пгавное, можно ограничить привилегии сборок, выполняющихся внутри домена приложения, назначив им удостоверения при создании домена приложения либо при запуске процесса.
	Службы выполняются в фоновом режиме и не имеют пользовательского интерфейса. Разработка служб отличается от создания приложений других типов, поскольку запускать исполняемый файл службы напрямую нельзя. Вместо этого следует установить службу вручную или создать для нее установочный пакет. Специфическими параметрами служб являются тип запуска, учетная запись для работы и инструментарий для управления.
Основные термины
	домен приложения;
	удостоверения сборки;
398 Домены приложений и службы
Глава 8
	эшелонированная защита;
	объект удостоверений (Evidence)",
	LocalService;
	LocalSystem;
	NetworkService;
	служба.
Лабораторная работа
Сейчас вы примените на практике знания и навыки, полученные при изучении этой главы. Ответы на вопросы см. в разделе «Ответы» в конце книги.
Занятие 1. Создание инструмента для тестирования
Вы — разработчик в Музее Науки Болдуина (Baldwin Museum of Science), Удаленные пользователи запускают созданное вами приложение. Поскольку исполняющая среда NET Framework назначает сборке различные права в зависимости от ее размещения, сборки этого приложения часто будут запускается в частично доверяемом окружении. Такая ситуация чревата проблемами для пользователей приложения. Руководство поручило вам выяснить мнение ведущих сотрудников компании по этому вопросу и ответить на вопросы, возникающие у них. Вам необходимо разработать программу, создающую домен приложения и запускающую сборку в новом домене приложения с разрешениями зоны «Интернет», чтобы проверить работу приложения в окружении с ограниченными правами.
Результаты опроса
	Руководитель службы поддержки
«Мы получаем множество просьб от клиентов, желающих установить наше приложение с Wfeb-сервера. Однако это у них не получается. При этом часто возникают разные ошибки. По словам пользователей, характер сбоев зависит от того, запускают ли они приложение из Интернета либо из интрасети. Сейчас мы советуем им загрузить приложение на локальный компьютер и запускать его с жесткого диска, что решает проблему. Но этот обходной путь не нравится специалистам из ИТ-отдела, которые хотят разобраться, почему не работать установка с Web-сервера».
	Руководитель департамента разработки
«Из разговора с руководителем службы поддержки я понял, что у пользователей возникают проблемы из-за прав доступа кода, ограниченных в целях безопасности. Нам нужно проверить работу приложения в различных зонах безопасности, чтобы понять суть возникающих проблем. Пожалуйста, напишите приложение, которое позволит нашим тестировщикам запускать приложение в различных зонах безопасности».
Вопросы
Ответьте на следующие вопросы:
1. Опишите общую схему приложения.
2. Напишите программу, которая создает домен приложения и запускает сборку CASDemands в новом домене приложения с разрешениями зоны безопасности «Интернет».
Рекомендуемые упражнения
Занятие 2. Мониторинг файлов
Вы — разработчик в ИТ-отделе страховой компании «Humongous Insurance». Толька что вы сдали проект, над которым работали несколько месяцев. В ожидании следующего проекта руководитель ИТ-отдела поручил вам разработку инструмента для системных администраторов, который позволит им поддерживать целостность файлов на рабочих станциях организации.
Результаты опроса
	ИТ-менеджер
«Благодаря вашим новым разработками все приложения поддерживают конфигурационные файлы формата XML. Это отличное решение, поскольку позволяет опытным пользователей самостоятельно корректировать настройки приложений. Однако один из пользователей при этом случайно отключил защиту приложения. Я не против самостоятельной настройки приложений пользователями, но хорошо бы при этом получать уведомления об изменениях, влияющих на безопасность приложений. Система мониторинга файлов не оптимизирована для этого, поскольку сообщает о любых изменениях, внесенных пользователями в конфигурационные файлы. Нам также нужна возможность установки службы средствами Systems Management Server, поэтому установочный пакет должен быть в формате MSI».
	Руководитель департамента разработки
«Полностью запрещать пользователям вносить изменения в настройки нельзя, но не знаю, как защитить их от нежелательных изменений, не блокируя доступ к конфигурационным файлам. Возможно, достаточно заносить в журнал запись об изменении пользователем параметров защиты в конфигурационном файле. После этого программа мониторинга ИТ-отдела уведомит об этом администратора, а тот примет соответствующие меры. Записи должна вноситься в журнал сразу же после сохранения пользователем измененного файла, то есть служба мониторинга должна работать постоянно, а не только ночью».
Вопросы
Ответьте на следующие вопросы руководства:
1.	Какого типа приложение позволит решить проблему ИТ-отдела?
2.	Как организовать установку приложения с использованием MSI-файла?
3.	Какой тип запуска подойдет?
4.	Какой тип учетной записи следует использовать?
Рекомендуемые упражнения
Чтобы лучше проработать темы экзамена, рассмотренные в этой главе, выполните следующие упражнения.
400 Домены приложений и службы
Глава 8
Создание блоков изоляции для CLR при помощи доменов приложений
Выполните оба упражнения.
	Упражнение 1 Создайте сборку, которая будет имитировать вредоносное поведение, читая файл из папки Мои документы текущего пользователя, а затем подключаясь к Web-серверу. Затем напишите сборку, которая задаст при создании домена приложения удостоверения, дающие ограниченные права. В результате запущенная в этом домене приложения «вредоносная» сборка не сможет прочитать конфиденциальную информацию пользователя.
	Упражнение 2 Создайте сборку, выделяющую большой объем памяти. Запустите сборку и при помощи оснастки Performance понаблюдайте за утилизацией памяти. Затем создайте вторую сборку, которая запустит первую в другом домене приложения, и выгрузите этот домен приложения Проверив объем занятой сборкой памяти, убедитесь, что часть ресурсов освобождена.
Создание, установка и управление службой
Выполните как минимум упражнение 1. Чтобы освоить работу со службами на практике, также выполните упражнения 2 и 3.
	Упражнение 1 Создайте службу, прослушивающую входящие сетевые подключения, и установите ее при помощи инструмента InstallUtil. Убедившись, что служба работает, удалите ее при помощи InstallUtil.
	Упражнение 2 Создайте службу для решения задач, поставленных в упражнении 2 лабораторной работы.
	Упражнение 3 Внесите изменения в службу, созданную в упражнениях 1 и 2 практикума занятия 3, чтобы она работала под учетной записью LocalService. Определите привилегии, которые должны быть у LocalService, чтобы служба работала правильно. Создайте новую учетную запись, назначьте ей только необходимые для работы привилегии и настройте службу для работы под этой учетной записью.
м;
Пробный экзамен
На прилагаемом компакт-диске, содержится пробный экзамен, обеспечивающий широкие возможности по самоподготовке. Например, можно проверить усвоение темам этой главы либо всех тем экзамена 70-536. Можно сымитировать настоящий экзамен либо выбрать режим обучения, когда после каждого вопроса приводятся правильные ответы с пояснениями.
Пробный экзамен
Подробнее о пробном экзамене рассказано во введении.
ГЛАВА 9
Установка и конфигурирование приложений
Занятие 1.	Настройка конфигурации	402
Занятие 2.	Создание установщика	433
Занятие 3.	Использование утилиты .NET Framework 2.0 Configuration	441
Занятие 4.	Управление конфигурацией	450
В этой главе освещается установка и конфигурирование приложений. Поскольку невозможно угадать, какие именно параметры при установке приложения придется настраивать, имеет смысл разрабатывать конфигурируемые приложения, позволяющие без труда вносить необходимые изменения. К тому же создание эффективных процедур установки, интегрирующих разработанное вами ПО в систему и поддерживающее его удаление, сегодня является не столько предметом обсуждения, сколько требованием рынка. Более того, процесс установки существенно влияет на отношение пользователей к приложению. Таким образом, легкая, интуитивно понятная и законченная процедура установки является необходимой составляющей профессионально разработанных приложений.
Темы экзамена:
	Встроенные средства конфигурирования .NET-приложений (см. пространство имен System. Configuration).
□	классы Configuration и ConfigurationManager,
□	классы ConfigurationElement, ConfigurationElementCollection и Configuration Element Property,
□	классы Configurationsection, ConfiguratioriSectionCollection, ConfigurationSeciionGroup и ConfigurationSectionGroupCollection’,
□	реализация интерфейса 1 Settings Provider Service',
□	реализация интерфейса lApplicationSettingsProvider,
□	класс ConfigurationValidatorBase.
402 Установка и конфигурирование приложений
Глава 9
	Реализация, установка и управление службой (см. пространство имен System. Service Process).
□	наследование от класса ServiceBase;
□	классы ServiceController и ServiceControllerPermisston',
□	классы Servicelnstaller и ServiceProcessinstaller,
□	структура SessionChangeDescription и перечислимое SessionChangeReason.
Прежде всего
Чтобы освоить материал этой главы, необходимо иметь навыки работы с Microsoft Visual Basic или Microsoft C# и уметь:
	создавать консольные приложения в Visual Studio при помощи Visual Basic или С#;
	направлять вывод в окно отладки или в окно консоли;
	устанавливать в отладчике точки прерывания;
	в режиме пошагового выполнения заходить в сегмент кода;
	в режиме пошагового выполнения выходить из сегмента кода;
	в режиме пошагового выполнения проходить через сегмент кода;
	добавлять в проект ссылки на системные библиотеки классов;
	иметь представление о структуре XML-документов.
Пример из практики
Уильям Райн (William Ryan)
Методы конфигурирования ПО существенно изменились за последнее время. Еще совсем недавно не было ни пространства имен Sy stem.Configuration, ни реестра Windows, ни изолированного хранения. Даже с появлением .NET Framework создание и конфигурирование параметров приложения остается достаточно трудоемким занятием. Когда я разрабатывал свое первое коммерческое .NET-при-ложение, у меня ушло больше шести часов на создание файлов конфигурации, написание классов для их обработки и тестирование кода. Для сравнения я переписал все элементы, связанные с настройкой этого приложения, воспользовавшись новыми инструментами из Visual Studio. Задачу, которая в прошлый раз отняла у меня свыше шести часов, на этот раз я решил минут за 15!
Занятие 1. Настройка конфигурации
При разработке ПО очень редко используются универсальные «рецепты». Разработчики не руководствуются догмами, а ищут компромиссные подходы. Некоторые решения проще других, однако они приводят к потерям, например, производительности. Чтобы производить доступный по цене и качественный продукт, укладываясь при этом в срок, необходимо тщательно взвешивать плюсы и минусы тех или иных решений.
Однако есть практически универсальное правило — стоит избегать жесткой прошивки в коде элементов (параметров, значений и пр.), которые могут (и должны) изменяться. Такое негибкое программирование ведет к возникновению ряда серьезных проблем. Иногда оно целесообразно, но в общем случае подобной практики лучше избегать.
Занятие 1
Настройка конфигурации 4Q3
Инфраструктура .NET Framework предоставляет широкий спектр инструментов, помогающих избежать «жесткого» программирования и тем самым повысить гибкость приложений. Чтобы в полной мере оценить инструменты .NET Framework 2.0, имеет смысл вспомнить, как производилась настройка .NET-приложений в предыдущих версиях инфраструктуры .NET.
До появления .NET Framework 2.0 разработчикам были доступны два способа работы с параметрами настройки. При первом подходе все параметры просто заносились в раздел аррSettings файла конфигурации. Преимуществом этой стратегии была простота, но платой за простоту являлась невозможность работать с такими параметрами объектно-ориентированными средствами. При другом подходе в файле конфигурации определялись разделы собственных параметров, для работы с которыми разрабатывались соответствующие классы. В этом случае программисты могли работать с параметрами конфигурации полностью объектно-ориентированными средствами, но для этого им приходилось писать тонны однообразного кода. При разработке приложений в предыдущих версиях .NET Framework потратить около двух часов на создание и тестирование собственных разделов конфигурации было обычным делом. Инструменты же .NET Framework 2.0 позволяют создавать средства настройки даже сложных приложений буквально за несколько минут, поскольку стандартный код для этого уже есть.
Соответственно, можно отметить следующие преимущества настройки приложений при помощи .NET:
	можно устанавливать и получать параметры настройки, не зная, как они изменятся в будущем;
	можно поддерживать параметры конфигурации в интуитивно понятном объектно-ориентированном стиле;
	можно считывать и записывать параметры настройки независимо от реестра Windows. Таким образом, приложения становятся менее зависимыми от системы безопасности и сетевых администраторов (поскольку не требуется изменять или получать разрешения на доступ к реестру Windows). Это также способствует и межплатформенной совместимости. (Другие операционные системы, например Linux или Mac OS, не имеют реестра.)
Изучив материал этого занятия, вы сможете:
J управлять общими параметрами настройки;
J управлять настройкой приложения;
J получать отдельные параметры настройки;
J регистрировать удаленные компоненты.
Продолжительность занятия - 20 минут.
Конфигурирование в .NET Framework 2.0
Пространство имен System.Configuration — это библиотека, служащая репозитарием всех классов, используемых для управления конфигурацией.
404 Установка и конфигурирование приложений
Глава 9
ПРИМЕЧАНИЕ В примерах кода, приведенных ниже, требуется ссылка на пространство имен System. Configuration.
Во всех примерах кода, приведенных в этой главе, подразумевается, что пространство имен Sy stem.Configuration добавлено в программу. Для этого следует в начале класса или модуля добавить Imports System.Configuration (в Visual Basic) или using Sy stem.Configuration (в С#). Возможно, потребуется также установить ссылку на System.Configuration, в меню проекта Visual Studio выбрав Project, затем Add Reference и на вкладке .NET выбрав System.Configuration.
Это пространство имен предоставляет как объекты общего назначения, так и специализированные объекты для всех возможных сценариев конфигурирования.
ПРИМЕЧАНИЕ .NET 2.0
Вначале класс ConfigurationManager появился в составе Enterprise Library, разработанной Microsoft. С тех пор он стал полноценным членом .NET Framework, позволяющим отказаться от многих подходов к управлению данными конфигурации.
Во главе иерархии пространства имен System.Configuration располагаются классы Configuration и ConfigurationManager.
При работе с любым объектом в этой главе (или с любым членом пространства имен System.Configuration) следует указывать полные имена объектов или задавать псевдоним для пространства имен System.Configuration. Чтобы импортировать пространство имен System.Configuration, достаточно добавить строку Imports System. Conf iguration в начало модуля или класса Visual Basic или строку using System. Configuration в начало класса С#.
При работе с этими классами вы несомненно заметите их логическую связь. Также следует отметить, что ни один из них не имеет конструктора. В табл. 9-1 и 9-2 приведены определения классов Configuration и ConfigurationManager. Внимательно изучите определения обоих классов, это пригодится в дальнейшем.
Табл. 9-1. Свойства и методы класса Configuration
Имя	Описание
Свойство AppSettings	Возращает объект AppSettingsSection раздела конфигурации, относящегося к объекту Configuration
Свойство Connectionstrings	Возвращает объект Connectionstrings раздела конфигурации, относящегося к объекту Configuration
Свойство Contextinformation	Возвращает объект Contextinformation раздела конфигурации, относящегося к объекту Configuration
Свойство FilePath	Возвращает физический путь к файлу конфигурации, представленному объектом Configuration
Метод GetSection Метод GetSectionGroup Метод HasFile	Возвращает указанный объект Configurationsection Возвращает указанный объект ConfigurationSectionGroup Определяет наличие файла конфигурации для ресурса, представленного объектом конфигурации
Свойство NamespaceDeclared	Возвращает или устанавливает значение, определяющее наличие в файле конфигурации пространства имен XML
Занятие 1
Настройка конфигурации / Q§
Табл. 9-1. (окончание)
Имя	Описание
Свойство RootSectionGroup	Возвращает объект ConfigurationSectionGroup для объекта Configuration
Метод Save	Записывает параметры конфигурации, содержащиеся внутри объекта Configuration в текущий XML-файл конфигурации
Метод SaveAs	Записывает параметры конфигурации, содержащиеся внутри объекта Configuration в заданный XML-файл конфигурации
Табл. 9-2. Свойства и методы класса ConfigurationManager
Имя	Описание
Свойство AppSettings	Возвращает данные раздела AppSettingsSection для стандартной конфигурации текущего приложения
Свойство Connectionstrings	Возвращает данные раздела ConnectionStringsSection для стандартной конфигурации текущего приложения
Метод GetSection	Возвращает указанный раздел конфигурации для стандартной конфигурации текущего приложения
Метод OpenExeConfiguration	Открывает заданную клиентскую конфигурацию как объект System.Configuration.Configuration
Метод OpenMachineConfiguration	Открывает файл конфигурации уровня компьютера (machine configuration file) на текущем компьютере как объект Configuration
Метод OpenMappedExeConfiguration	Открывает указанную клиентскую конфигурацию как объект System.Configuration.Configuration с использованием заданого сопоставленного файла и уровня пользователя
Метод OpenMappedMachineConfiguration	Открывает указанную клиентскую конфигурацию как объект System.Configuration.Configuration с использованием в заданного сопоставленного файла
К этому моменту два положения должны представляться очевидными. Во-первых, оба класса имеют два аналогичных свойства (AppSettings и Connection Strings). Во-вторых, все свойства класса ConfigurationManager возвращают объекты Configuration. Теперь нетрудно догадаться, как именно два класса взаимодействуют между собой. Чтобы получить параметры конфигурации, выполните следующее:
1. Объявите объект Configuration.	*
2. При помощи различных методов класса ConfigurationManager, имя которых начинается с префикса Open, откройте файл конфигурации уровня компьютера или приложения. Пример кода:
406 Установка и конфигурирование приложений
Глава 9
’ VB
Dim cs As Configuration = _
ConfigurationManager.OpenExeConfiguration(ConfigurationllserLevel.None)
// C#
Configuration cs =
ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
’ VB
Dim cs As Configuration = ConfigurationManager.OpenMachineConfigurationO // C#
Configuration cs = ConfigurationManager.OpenMachineConfiguration();
' VB
Dim MyMap As New ExeConfigurationFileMap
MyMap.ExeConfigFilename = "DBConnectionStringDemo.exe.config”
Dim cs As Configuration =
Configu rationManager.OpenMappedExeConfigu ration(MyMap,
ConfigurationllserLevel. None)
// C#
ExeConfigurationFileMap MyMap = new ExeConfigurationFileMapO;
MyMap.ExeConfigFilename = ©"DBConnectionStringDemo.exe.config";
Configuration cs =
ConfigurationManager.OpenMappedExeConfiguration(MyMap,
ConfigurationllserLevel. None);
' VB
Dim MyMap As New ExeConfigurationFileMap
MyMap.ExeConfigFilename = "DBConnectionStringDemo.exe.config"
Dim cs As Configuration = _
ConfigurationManager.OpenMappedMachineConfiguration(MyMap)
11 C#
ExeConfigurationFileMap MyMap = new ExeConfigurationFileMapO;
MyMap.ExeConfigFilename = ©"DBConnectionStringDemo.exe.config";
Configuration cs =
ConfigurationManager.OpenMappedMachineConfiguration(MyMap);
Все эти методы схожи и выполняют одну и ту же задачу — открывают файл конфигурации и возвращают содержащиеся в нем значения объекту Configuration. Первый и третий методы используются для открытия файлов конфигурации уровня приложений, а второй и четвертый — уровня компьютера. Как видно из примера, операция открытия файлов и считывания соответствующей информации о конфигурации при помощи класса ConfigurationManager понятна на интуитивном уровне, но несколько моментов все-таки необходимо прояснить.
Занятие 1	Настройка конфигурации 4Q7
Во-первых, следует обсудить использование перечислимого ConflgurationUserLevel.
Члены перечислимого ConflgurationUserLevel представлены в табл. 9-3.
Табл. 9-3. Перечислимое ConflgurationUserLevel
Имя	Описание
None	Возвращает объект System.Configuration.Configuration,
относящийся ко всем пользователям
PerUserRoaming	Возвращает перемещаемый объект
4	System.Configuration.Configuration, относящийся	к текущему
**	пользователю
PerUserRoamingAndLocal < Возвращает локальный объект System.Configuration.Configuration, относящийся к текущему пользователю
Основной проблемой является то, что по умолчанию используется значение None, что не вполне отвечает интуитивным представлениям.
Следующий момент, который требует пояснений, связан с применением объекта ExeConfigurationFileMap. Естественно, если требуется использовать сопоставленный (связанный) файл, исполняющая среда должна иметь соответствующие механизмы, чтобы узнавать о факте использования этого файла и о его местоположении. Для этого можно воспользоваться свойством ExeConfigFilename,
При вызове любого метода OpenMappedExeConfiguration или OpenMappedMachineConfiguraton исполняющая среда получает информацию о том, что будет использоваться связанный файл. Это требование конструктора, как и требование указать местоположение файла. Приложение, использующее файл, должно иметь соответствующие разрешения на доступ к файлу, следовательно, необходимо убедиться, что файл существует и право на доступ к нему имеется.
К СВЕДЕНИЮ Разрешения и декларативная защита
Важнейшей частью создания безопасных .NET-приложений является управление разрешениями. Разрешения, а также декларативная и императивная защита подробно рассматриваются в главе 11. Подробнее — см. на странице http://msdn.microsoft.com/library/ rus/default.asp?url=/library/rus/cpguide/html/cpconpermissions.asp.
Следует не только убедиться, что пользователь имеет права на доступ к файлу, но и проверить местоположение файла. Если в качестве значения свойства ExeConfigFilename указать пустую строку или пробел, исполняющая среда сгенерирует исключение ArgumentException. На рис. 9-1 показан результат, полученный при попытке присвоить свойству ExeConfigFilename значение String. Empty.
К сожалению, если для свойства ExeConfigFilename указать несуществующий файл, исполняющая среда не отреагирует (по крайней мере до тех пор, пока вместо ожидаемых содержательных данных не будут получены значения null). Подобной ошибки можно избежать, если перед присвоением свойству значения убедиться, что нужный файл существует. Вы можете реализовать любую схему проверки, но чтобы пример был проще и понятней, в приведенном ниже коде существование файла проверяется при помощи метода Debug.Assert:
408 Установка и конфигурирование приложений
Глава 9
Рис. 9-1. Попытка установить свойство ExeConfigFilename равным пустой строке
’ VB
Dim cs As Configuration = ConfigurationManager.OpenMachineConfiguration() Debug.Assert(File.Exists(ExeFileName),
"The mapped file or path is missing or incorrect!”);
MyMap.ExeConfigFilename = ExeFileName
// C#
ExeConfigurationFileMap MyMap = new ExeConfigurationFileMapO;
String ExeFileName = @"DBConnectjonString exe config”;
Debug.Assert(File.Exists(ExeFileName),
’’The mapped file or path is missing or incorrect!");
MyMap.ExeConfigFilename = ExeFileName;
Общие параметры
Термин общие параметры {common settings) относится к нескольким категориям параметров настройки выполнения приложений. В качестве примера можно настроить приложение для выполнения в определенной версии .NET Framework. Например, можно собрать приложение в имеющейся версии.NET Framework, но для его запуска выбрать другую. В этом случае следует указать версию supportedRuntime в разделе запуска. Если вы хотите запускать свое приложение под управлением версии .Net Framework 1.1, введите в соответствующий раздел файла конфигурации приложения или Web-приложения следующие строки:
<?хш1 version ="1.0"?>
<configuration>
<startup>
<supportedRuntime version="v1.1.4322" />
</startup>
</configuration>
Однако порой нужной версии на компьютере нет. В таких случаях действуют строгие правила:
 Если присутствует версия, под управлением которой приложение собрано, то приложением используется эта версия.
Занятие 1
Настройка конфигурации 499
	Если версия .NET Framework, использовавшаяся при сборке приложения, отсутствует и в элементе версии supported Runtime ничего не указано, приложение запускается под управлением самой поздней доступной на компьютере версии .NET Framework. Таким образом, если приложение собрано в версии 1.x, а на машине установлена только .NET Framework 2.0, приложение будет запущено под ее управлением.
	Если версия .NET Framework, использовавшаяся при сборке приложения, отсутствует, но в файле конфигурации задан элемент supportedRuntime, используется указанная версия .NET Framework, либо запуск окончится неудачей.
Эти правила понятны без дополнительных комментариев. Если нужная для запуска версия исполняющей среды отсутствует, а альтернативная версия не указана, исполняющая среда делает все возможное, чтобы запустить приложение. Если же исполняющая среда не может запустить сборку в доступной версии, система констатирует ошибку.
Другая распространенная ситуация связана с использованием общей сборки и проверки ее работы с несколькими приложениями. Установка такой сборки в кэш глобальных сборок (global assembly cache, GAC) и удаление ее из GAC — трудоемкая задача. Чтобы упростить ее, можно воспользоваться специальной переменной DEVPATH, предварительно настроив ее. Для этого следует выполнить две операции:
1. Добавить переменную окружения с именем DEVPATH, указывающую местоположение сборки.
2. Установить значение элемента developmentMode равным true. Пример кода:
<configuration>
<runtime>
<developmentMode developerInstallation="true'/>
</runtime>
</configuration>
Еще одна часто встречающаяся задача связана с заданием размещения определенной версии сборки. В этом случае можно воспользоваться инструментом.NET Framework 2.0 Configuration (который подробно рассматривается в занятии 3) или вручную отредактировать файл конфигурации компьютера. Там присутствует специальный элемент с именем CodeBase, позволяющий задать как расположение, так и версию сборки, в результате исполняющая среда будет использовать эти параметры. Вот решающий эту задачу код: <configuration>
<runtime>
<assemblyBinding xmlns=”schemaname">
<dependentAssembly>
<assemblyldentity name="myprogram" publicKeyToken="xxxxxxxxx'' culture="en-us" /> <codeBase version="x.0.0.0”
href="http://www.adatum.com/myprogram.dll’7>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
Другие общие параметры представляют собой значения конфигурации, уже определенные для разработчика инфраструктурой .NET Framework. Сюда входят два основных раздела: connectionstrings и appSettings. Во многих отношениях эти значения конфигурации аналогичны любым другим составляющим конфигурации, но имеются определен
410 Установка и конфигурирование приложений
Глава 9
ные нюансы, дающие заметное преимущество этим элементам по сравнению с прочими. Поскольку работа с этими параметрами ведется особым образом, для них в файле конфигурации заранее отведено место. В следующем коде представлен пример файла конфигурации, содержащий оба раздела — appSettings и connectionstrings-.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
Odd key= "Foo" value=”Hello World!”/>
</appSettings> .
cconnectionSt rings>
<clear/>
Odd name=”AdventureWorksString"
p r ovide rName="System.Data.SqlClient"
connectionString="Data Source=localhost;
Initial Catalog=AdventureWorks; Integrated Security=true"/>
</connectionStrings>
</configuration>
В приведенном коде создается по одному значению appSettings и connectionstrings. Значение параметра appSettings имеет имя Foo и содержит литерал «Hello World». Значение параметра connectionstring имеет имя AdventureWorksString и содержит строку подключения, которую можно использовать для соединения с базой данных SQL Server.
Работать с разделом appSettings очень просто. Задается ключ с уникальным именем, идентифицирующий параметр и позволяющий тем самым получать этот параметр и его значение по имени. Единственное назначение ключа заключается в предоставлении понятного имени, по которому можно получить заданное значение. В данном примере смысл слова Foo не очевиден, поэтому лучшим выбором являлся бы ключ CompanyName. При чтении файла конфигурации несложно догадаться, на что ссылается ключ CompanyName. Хотя класс Configurationsettings устарел, для обратной совместимости в нем присутствует свойство AppSettings. Однако существуют и другие способы обратиться к этому параметру при работе в .NET Framework 2.0. В следующем фрагменте кода показано, как получить значение из AppSettings.
’ VB
Dim HelloWorldVariable = Configurationsettings.AppSettingsC’Foo")
11 C#
String HelloWorldVariable = Configurationsettings.AppSettings["Foo"];
ПРИМЕЧАНИЕ Что значит «устаревший»
Хотя слово «устаревший» имеет и общеразговорный смысл, в инфраструктуре .NET Framework его значение вполне конкретно. Многие методы, свойства и объекты, утратившие свое значение и поэтому считающиеся устаревшими, сохраняются в инфраструктуре Framework для обратной совместимости. Формально говоря, их можно использование не нарушает работу приложений. Но дальнейшая поддержка устаревших элементов не гарантируется, и хороший стиль программирования требует избегать подобных конструкций, насколько это возможно. К тому же при использовании устаревших элементов компилятор выдает предупреждения, и, в зависимости от параметров сборки, компиляция может быть прервана.
Занятие 1
Настройка конфигурации 411
Как бы то ни было, свойство AppSettings считается устаревшим, и на его использование компилятор реагирует предупреждением. Корректное решение заключается в том, чтобы воспользоваться свойством AppSettings объекта ConfigurationManager, а не объекта Configurationsettings. Вот код, вызывающий для получения AppSettings метод .NET Framework 2.0:
’ VB
Dim AllAppSettings As NameValueCollection = ConfigurationManager AppSettings Console.WriteLine(AllAppSettings("Foo"))
Console.WriteLine(AllAppSettings(O))
// C#
NameValueCollection AllAppSettings = ConfigurationManager.AppSettings;
Console.WriteLine(AllAppSettings["Foo"]);
Console.WriteLine(AllAppSettings[O]);
Для работы c AppSettings следует объявить экземпляр объекта NameValueCollection и присвоить ему свойство AppSettings объекта CorfigurationManager. Тогда можно получать значения как по индексу, так и по строке. В рассматриваемом примере определена только одна переменная AppSettings. В ней содержится значение «Hello World» и ключ «Foo». Поскольку в наборе содержится только один элемент, ему соответствует первый индекс набора, равный 0. К тому же, поскольку AppSettings содержит значения только типа System.String, приведение типов не требуется.
Необходимо отметить еше один нюанс. В некоторых случаях требуется перебрать элементы AppSettings в цикле вместо того чтобы непосредственно обратиться к определенному элементу. Класс AppSettings реализует интерфейс lEnumerable (из пространства имен System.Collections), поэтому можно перечислять этот набор так же, как и любой объект, поддерживающий Enumerator. Достаточно просто объявить объект lEnumerator и присвоить ему результат, возвращаемый методом GetEnumerator свойства Keys экземпляра AppSettings. Теперь можно просмотреть набор вызовом метода MoveNext. В приведенном коде перечисляются значения раздела AppSettings:
' VB
Dim AllAppSettings As NameValueCollection =
ConfigurationManager.AppSettings
Dim SettingsEnumerator As lEnumerator = AllAppSettings.Keys GetEnumerator
Dim Counter As Int32 = 0
While SettingsEnumerator.MoveNext
Console.WriteLine("Item: {0} Value: {1}",
AllAppSettings.Keys(Counter), AllAppSettings(Counter))
End While
// C#
NameValueCollection AllAppSettings =
ConfigurationManager.AppSettings,
Int32 Counter = 0:
lEnumerator SettingsEnumerator = AllAppSettings.Keys.GetEnumerator();
while (SettingsEnumerator.MoveNext())
{
Console.WriteLine("Item: {0} Value: {1}”, AllAppSettings.Keys[Counter],
412 Установка и конфигурирование приложений
Глава 9
AllAppSettings[Counter]);
}
Использование элемента Connectionstrings требует дополнительных усилий, но также интуитивно понятно и несложно. Поскольку многие современные приложения тем или иным образом работают с базами данных, разработчики инфраструктуры Framework решили, что необходимо предоставить изящный и надежный способ для хранения строк подключения к базам данных. Жесткая прошивка строки подключения в коде значительно уменьшает гибкость приложения, так как изменение имени сервера или любое другое изменение параметров подключения ведет к необходимости внесения изменений в код и повторного развертывания приложения. Хранение строки подключения в файле конфигурации обеспечивает гораздо большую гибкость, поскольку при смене имени сервера достаточно отредактировать этот файл, изменив параметры настройки.
ВНИМАНИЕ! Защита строки подключения
Строки подключения (и любая другая конфиденциальная информация) должны быть зашифрованы всегда, когда это возможно. Хранение строк подключения к базам данных в виде- открытого текста (который можно просто прочитать) представляет серьезную уязвимость системы безопасности.
Существует распространенное заблуждение, что при использовании доверительной аутентификации на основе встроенной системы безопасности Windows шифровать строку подключения не обязательно. Порой это справедливо, но далеко не всегда. Квалифицированный злоумышленник может взломать защиту, хотя аутентификация Windows сводит к минимуму возможность получения несанкционированного доступа. Когда аутентификация Windows не используется, хранить строку подключения в виде открытого текста очень опасно, поскольку злоумышленник легко может получить как имя пользователя для доступа к базе данных, так и его пароль. Настоятельно рекомендуется всегда шифровать такую информацию.
ПРИМЕЧАНИЕ Шифрование и хэширование
Шифрование и хэширование подробно рассматриваются в главе 12„ их следует применять всегда, кроме случаев, когда безопасность не важна.
В предыдущих версиях Framework не делалось различий между строками подключений и другими строками. Важнейшее новшество .NET Framework 2.0, относящееся к конфигурированию, — появление поддержки контроля типов (хотя для него были и другие причины). Улучшенный вариант конфигурирования предоставляет возможность согласования с типом провайдера (см. пространство имен System.Data).
По умолчанию доступ к свойству ConnectionStrings предоставляется аналогично доступу к Арр Settings. Основное отличие заключается в том, что вместо объекта NameValueCollection используется ConnectionStringSettingpollections в сочетании с ConnectionStringSettings. Сначала рассмотрим подходящий пример файла конфигурации. Для полноты картины этот файл содержит следующие библиотеки-провайдеры данных: SqlClient, OracleClient, OleDb и Odbc\
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<clear/>
Odd name="Adventu reWorksString”
Занятие 1
Настройка конфигурации 413
providerName="System.Data.SqlClient"
connections!ring="Data Source=localhost;Initial
Catalog=AdventureWorks, Integrated Security=true"/>
Odd name="MarsEnabledSqlServer2005String"
p rovide rName="System.Data.SqlClient"
connectionString=
"Server=Aron1;Database=
pubs;Trusted_Connection=True;MultipleActiveResultSets=true" />
Odd name="OdbcConnectionString"
providerName="System.Data.Odbc"
connections!ring=
Driver={Microsoft Access Driver (*.mdb)};Dbq=C:\adatabase mdb, Uid=Admin; Pwd=R3m3emberToUseStrongPasswords; "/>
Odd name="AccessConnectionString"
providerName="System.Data.OleDb”
connections!ring="Provider=Microsoft.Jet.OLEDB.4.0;
Data SourceAPathOrShare\mydb.mdb;
User Id=admin;Password=Rememb3rStr0ngP4sswords;" />
Odd name="0racleConnectionString”
providerName="System Data.OracleClient"
connectionString="Data Source=MyOracleDB;Integrated Security=yes;" / >
</connectionSt rings>
</configuration>
Чтобы сконфигурировать вышеупомянутые библиотеки провайдеров данных (OleDb, SqlClient, OracleClient, Odbc), сделайте следующее:
1.	Задайте элемент Clear, чтобы исключить существующие строки подключения
2.	Для каждой строки подключения (например, SqlClient, OracleClient и т. п.) добавьте элемент Name. Этот элемент позволяет получать значение по имени, освобождая разработчика от необходимости запоминать индексы.
3.	Задайте атрибут providerName для каждого из элементов Name (например, System.Da-ta.OracleClient).
4.	Задайте атрибут connectionstring, содержащий строку подключения для соединения с нужным источником данных.
В приведенном выше примере для каждого типа провайдера я добавил подходящую строку подключения. Для SqlClient я добавил две различных строки подключения, чтобы показать возможность такого варианта. Вторая строка подключения SqlClient отличается от первой главным образом тем, что позволяет использовать несколько активных результирующих наборов [технология Multiple Active Result Sets (MARS)].
Рассмотрев представление набора значений в файле конфигурации, обсудим, как эти значения получать:
1	VB
Dim MySettings As Connections!ringSettingsCollection = _
ConfigurationManager.Connectionstrings
If Not MySettings Is Nothing Then
Dim sb As New StringBuilder
414 Установка и конфигурирование приложений
Глава 9
Dim individualsettings As ConnectionStringSettings
For Each individualsettings In MySettings
sb. AppendC’Full Connection String: " & _ individualsettings.ConnectionSt ring)
sb.Append("Provider Name: " & individualsettings.ProviderName)
sb.Append("Section Name: " & individualsettings.Name)
Next
Console.WriteLine(sb.ToSt ring)
End If
// C#
ConnectionStringSettingsCollection MySettings =
Configu rationManage r.ConnectionSt rings;
if (MySettings != null)
{
StringBuilder sb = new StringBuilder();
foreach (ConnectionStringSettings individualsettings in MySettings)
sb.AppendC’Full Connection String: " + individualsettings.Connectionstring + "\r\n"); sb.Append("Provider Name: " + individualsettings.ProviderName + "\r\n"); sb.Append("Section Name: " + individualsettings.Name + "\r\n");
}
Console.WriteLine(sb.ToString());
}
Кратко можно описать приведенный выше код таким образом: создается экземпляр объекта ConnectionStringSettingsCollection, которому присваивается свойство Connectionstrings объекта ConfigurationManager. Три свойства особенно важны: Name, ProviderName и Connectionstring. Из них, по-видимому, важнее всего Connectionstring, поскольку именно оно требуется для создания экземпляра объекта Connection. Для создания соответствующих подключений в тело цикла foreach из приведенного выше фрагмента кода достаточно включить следующие строки (требующие ссылок на пространства имен System.bata.SqlClient, System. Data. OracleClient, Sy stem. Data.Ole D В и System. Data.Odbc):
’ VB
Dim MyConnection As IDbConnection
Select Case individualsettings.ProviderName
Case "System.Data.SqlClient"
MyConnection = New SqlConnection(individualSettings.Connectionstring)
Case "System.Data.OracleClient"
MyConnection = New OracleConnection(individualsettings.Connectionstring) Case "System.Data.OleDb"
MyConnection = New 01eDbConnection(individualSettings.Connectionstring) Case "System.Data.Odbc"
Занятие 1
Настройка конфигурации 415
MyConnection = New OdbcConnection(individualSettings.Connectionstring) End Select
// C#
IDbConnection MyConnection = null;
switch (individualsettings.ProviderName)
{
case "System.Data.SqlClient":
MyConnection = new SqlConnection(individualsettings.Connectionstring); break;
case "System Data.OracleClient":
MyConnection = new racleConnection(individualsettings.Connectionstring); break;
case "System.Data.OleDb":
MyConnection = new 01eDbConnection(individualsettings.Connectionstring); break;
case "System.Data. Odbc":
MyConnection = new OdbcConnection(individualSettings.Connectionstring); break;
}
Хотя в приведенном выше коде показано, как получать из файла конфигурации разные строки подключения, большинство приложений использует один провайдер баз данных. Обычно используется одна или несколько однотипных баз данных — например, несколько баз SQL Server. В сущности, если известно, какое подключение требуется, перебор в цикле является, возможно, лишней операцией. Следовательно, можно получить строку подключения для определенного провайдера данных напрямую, если она уже существует. Для этого предлагается два способа. В первом варианте используется имя раздела, соответствующее типу провайдера:
’ VB
Dim MySettings As ConnectionStringSettings = _
ConfigurationManager. ConnectionSt rings(‘‘AdventureWorksSt ring")
If Not MySettings Is Nothing Then
Dim MyConnection As New SqlConnection(MySettings.Connectionstring)
Console WriteLine(MySettings Connectionstring)
End If
11	C#
ConnectionStringSettings MySettings =
ConfigurationManager.ConnectionStrings["AdventureWorksString"];
if (MySettings != null)
{
SqlConnection cn = new SqlConnection(MySettings Connectionstring);*
Console,WriteLine(MySettings.ConnectionSt ring);
}
416 Установка и конфигурирование приложений
Глава 9
При втором подходе используется индекс, отвечающий позиции требуемого элемента в наборе Connectionstrings:
• VB
Dim MySettings As ConnectionStringSettings =
ConfigurationManager.ConnectionStrings(O)
If Not MySettings Is Nothing Then
Dim MyConnection As New SqlConnection(MySettings.Connectionstring)
Console.WriteLine(MySettings.ConnectionSt ring)
End If
// C#
ConnectionStringSettings MySettings =
ConfigurationManager.ConnectionStrings[0];
if (MySettings != null)
{
SqlConnection cn = new SqlConnection(MySettings.Connectionstring);
Console.WriteLine(MySettings.ConnectionSt ring);
}
Аналогично, при наличии нескольких баз данных одного типа (снова в качестве примера возьмем SQL Server), можно перебрать значения в цикле, проверяя их тип и затем создавая подходящие соединения. В следующем примере файл конфигурации предоставляет две строки подключения для баз данных SQL Server: AdventureWorksString и MarsEnabledSqlServer2005String. Производится поиск определенного типа провайдера (System.Data.SqlClient) и создаются соответствующие подключения.
’ VB
Dim MyTypeSettings As ConnectionStringSettingsCollection = _
ConfigurationManager.Connectionstrings
If Not MyTypeSettings Is Nothing Then
For Each typesettings As ConnectionStringSettings In MyTypeSettings
If typesettings.ProviderName = typeOrName Then
Dim MyConnection As New _
SqlConnection(MyTypeSettings.Connectionstring)
Console.WriteLine("Connection String: " & _ typesettings.ConnectionSt ring) End If
Next
End If
// C#
ConnectionStringSettingsCollection MyTypeSettings =
ConfigurationManager.Connectionstrings;
if (MyTypeSettings != null)
{
foreach (ConnectionStringSettings typesettings in MyTypeSettings)
{
Занятие 1
Настройка конфигурации 417
if (typesettings ProviderNarne == typeOrName) {
SqlConnection MyConnection = new
SqlConnection(typeSettings.Connectionstring);
Console.WriteLine("Connection String ” + typesettings.Connectionstring);
}
}
}
Предыдущий пример демонстрирует работу с классом ConfigurationManager, представляющим собой основной механизм хранения и получения информации о конфигурации приложений Windows Forms и консольных приложений (вообще всех приложений, за исключением Web-приложений). Хотя этот способ будет работать в приложениях ASP.NET, для получения значений конфигурации в Web-приложениях можно и нужно применять другую технику. Основное отличие управления информацией о конфигурации в Web-приложениях от подобной функции в приложениях Windows Forms заключается в том, что Web-приложения должны использовать класс WebConfigurationManager вместо ConfigurationManager. Хотя последнее утверждение чрезмерно упрощает картину, можно с уверенностью утверждать, что класс WebConfigurationManager можно применять, как и ConfigurationManager, для решения тех же задач, за тремя исключениями:
	Класс WebConfigurationManager содержит метод GetWebApplicationSection, отсутствующий в классе ConfigurationManager. Этот метод извлекает раздел Configurationsection из файла web.config целиком и выполняет ту же функцию, что и метод GetSection класса ConfigurationManager.
	Метод OpenMappedExeConfiguration класса ConfigurationManager заменен методом OpenMappedWebConfiguration класса WebConfigurationManager.
	Метод OpenExeConfiguration класса ConfigurationManager заменен методом Open WebConfiguration класса WebConfigurationManager.
Разобранные ранее в этом занятии примеры, вызывающие методы класса ConfigurationManager, имена которых начинаются префиксом Open, будут работать и с классом WebConfigurationManager (если учесть перечисленные выше исключения). Кроме того, применение этих классов практически ничем не различается. В следующем примере показано, как при помоши класса WebConfigurationManager перебрать в цикле элементы, представляющие строки подключения, и получить эти строки. Приведенный ниже файл конфигурации можно использовать как для примера на Visual Basic, так и на С#.
ПРИМЕЧАНИЕ Класс WebConfigurationManager относится к пространству имен
System. Web.Configuration
Класс WebConfigurationManager не является членом пространства имен System.Configuration, как можно было бы ожидать. Он относится к пространству имен System. Web.Configuration. Чтобы выполнить код из приведенных ниже примеров, установите ссылки на пространства имен System. Web.Configuration и System.Collections.
Configuration File: <?xml version="1 . 0"?> <configuration>
418 Установка и конфигурирование приложений
Глава 9
<appSettings/>
<connectionStrings>
<clear/>
<add name="AdventureWorksString"
providerName="System.Data.SqlClient"
connectionString="Data Source=localhost;Initial
Catalog=AdventureWorks; Integrated Security=true"/>
<add name=”MarsEnabledSqlServer2005String"
providerName="System.Data.SqlClient"
ConnectionString="Server=Aron1;Database=pubs;Trusted_Connection=True;
MultipleActiveResultSets=true" />
Odd name= "OdbcConnectionSt ring"
providerName="System.Data.Odbc"
connectionString="Driver={Microsoft Access Driver
(*.mdb)};Dbq=C:\mydatabase.mdb;Uid=Admin;Pwd=;"/>
Odd name="AccessConnectionString"
providerName="System.Data.OleDb"
ConnectionString="Provider=Microsoft.Jet.OLEDB.4.0;
Data Source=\somepath\mydb.mdb;User Id=admin;Password3;" />
Odd name="OracleConnectionString"
providerName="System.Data.OracleClient"
connectionString="Data Source=MyOracleDB;Integrated Security=yes;" / >
</connectionSt rings>
Oystem. web>
<i---->
«compilation debug=”true"/>
<!  -->
Outhentication mode="Windows"/>
«I---->
«/system.web>
</configuration>
’ VB
Private Const CONNECTIQNSTRING As String = "connectionstrings"
Protected Sub Page_Load(ByVal sender As Object, ByVai e As System.EventArgs)
Handles Me.Load
Dim ConnectionStringsSection As ConnectionStringsSection = _ CType(WebConfigurationManager.GetSection(CONNECTIONSTRING), _ ConnectionStringsSection)
Dim Connectionstrings As ConnectionStringSettingsCollection 3 _ ConnectionSt ringsSection.Connectionstrings
Dim ConnectionStringsEnumerator As System.Collections.lEnumerator = _ Connectionstrings.GetEnumerator
Занятие 1
Настройка конфигурации д| g
Dim i As Int32 = 0
Response.Write("[Display the connectionstrings]:<BR>")
While ConnectionStringsEnumerator.MoveNext
Dim ConnectionStringName As String = ConnectionStrings(i).Name
Response.Write(String.FormatC'Name: {0} Value: {1} <BR>", ConnectionStringName, ConnectionStrings(ConnectionStringName))) i += 1
End While
End Sub
// C#
private const String CONNECTIONSTRINGS = "ConnectionStrings";
protected void Page_Load(object sender, EventArgs e)
{
ConnectionStringsSection ConnectionStringsSection =
WebConf i gu rationManage r.GetSection(CONNECTIONSTRINGS) as ConnectionStringsSection;
ConnectionStringSettingsCollection Connectionstrings = ConnectionStringsSection.ConnectionStrings;
//Удостоверьтесь, что используется lEnumerator из пространства имен //System.Collections, а не из пространства имен System.Collections.Generic, //поскольку для работы с последним требуются дополнительные усилия System.Collections.lEnumerator ConnectionStringsEnumerator =
Connectionstrings.GetEnumerator();
// Перебор набора в цикле
// и вывод пар ключей и значений строк подключения.
Int32 i = 0;
Response.Write("[Display the connectionstrings]:<BR>");
while (ConnectionStringsEnumerator.MoveNext())
{
String ConnectionStringName = ConnectionStrings[i].Name;
Response.Write(String.FormatC'Name: {0} Value: {1}", ConnectionStringName, ConnectionStrings[ConnectionStringName]));
}
}
Параметры настройки приложения
Параметры настройки приложения относятся к самому приложению и не связаны с отдельным пользователем. Обычно в их число входят строки подключения к базам данных (подробнее — в практикуме в конце занятия), URL Web-сервисов, параметры настрой-
420 Установка и конфигурирование приложений
Глава 9
ки удаленного взаимодействия и т.п К тому же одним из основных преимуществ использования параметров настройки приложения является возможность хранить и получать их в объектно-ориентированном стиле, с поддержкой контроля типов. Чтобы понять, как работает эта конструкция, разберем пример реального кода. Здесь в проект добавлен параметр WebServiceUrl для последующего использования в приложении. Он создается в классе Sample Settings. Важные разделы кода выделены жирным шрифтом: <?xml version="1.О" encoding="utf-8" ?>	,
<configuration>
<configSections>
<sectionGroup name="userSettIngs” type="System.Configuration.UserSettingsGroup, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name=”WindowsApplication2.SampleSettings" type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
allowExeDefinition="MachineToLocalUser" />
</sectionGroup>
<sectionGroup name=”applicationSettings”
type=”System.Configuration.ApplicationSettingsGroup, System,
Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="WindowsApplication2.SampleSettings" type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</sectionGroup>
</configSections>
<userSettings>
<WindowsApplication2.SampleSettings />
</userSettings>
<applicationSettings>
<WindowsApplication2.SampleSettings>
<setting name=”WebServiceUrl” serializeAs=”String">
<value>http://www.adatum.com/myservice.asmx</value>
</setting>
</WindowsApplication2.SampleSettings>
</applicationSettings>
</configuration>
Существует два способа получить такой результат, но один из них значительно проще. Первый способ заключается в том, чтобы написать весь код вручную. Этот подход способен серьезно смутить разработчика, поскольку файлы конфигурации не терпят ошибок. Такие ошибки, как замена строчной буквы на прописную и наоборот или лишние пробелы, могут привести к неправильной работе приложения. Другой способ заключается в применении класса Settings и использовании инструмента-дизайнера. Этот подход намного лучше во всех отношениях. Чтобы воспользоваться дизайнером, достаточно добавить к проекту новый элемент проекта типа Settings File, как показано на рис. 9-2.
Занятие 1
Настройка конфигурации 42
Рис. 9-2. Добавление класса Settings в интегрированной среде разработки (IDE) Visual Studio 2005
После того как для файла будет задано понятное имя, станет доступным дизайнер IDE, позволяющий создавать параметры настройки и управлять ими. Диалоговое окно Settings позволяет создавать и переименовывать параметры, определять область действия каждого из них, выбирать тип и указывать значение по умолчанию, как показано на рис. 9-3.
setbngsl^ecungs арр.config Start Page	* X
Synchronize View Code
Application settings allow you to store and retrieve property settings and other information for your application dynamicafy. For example, the application can save a user's color preferences, then retrieve them the next time it runs, team вд shout epcfcatroo settings. ..
Scope Appkabon User
User Appkatio-i Appkabon
Trpe
stung	jji
string	jdj
strl,-3
System.Dra...
System.Dra... *] ’Jl
Рис. 9-3. Application Settings Designer в Visual Studio 2005
Application Settings Designer существенно экономит время разработчика. Еще одно преимущество заключается в том, что этот инструмент освобождает разработчика от необходимости быть в курсе нюансов каждой спецификации (в конце концов даже самый умелый разработчик не держит в голове все значения PublicKeyToken для данного класса). Поддержка контроля типов и использования дизайнеров также приносят разработчику существенную пользу. При помощи дизайнера можно выбрать любой из доступных в .NET Framework типов (например, System. Drawing.Color), не заучивая и не набирая на клавиатуре их имена. Application Settings Designer также предоставляет специализированные дизайнеры для любого типа. Намного проще выбрать цвет по схеме, чем пытаться вспомнить его ЯбЯ-значения.
422 Установка и конфигурирование приложений	Глава 9
Соответствующее свойство класса (здесь стоит отметить, что в предыдущих версиях этот код приходилось набирать вручную) теперь готово:
' VB
<Global.System.Configuration.Applicat ionScopedSettingAtt ribute(),
Global. System. Diagnostics. DebuggerNonllserCodeAttributeO,
Global.System.Configuration.DefaultSettingValueAttribute
("http://www.adatum.com/myse rvice.asmx")>
Public Readonly Property WebServiceUrl() As String
Get
Return CType(Me("WebServiceUrl"),String)
End Get
End Property
//C#
[global::System.Configuration.ApplicationScopedSettingAtt ribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()J
[global::System.Configuration.DefaultSettingValueAttribute
("http://www.adatum.com/myservice.asmx")]
public string WebServiceUrl	{.f
get
{
return ((st ring)(t his[ "WebSe rvicell rl" ]));
}
}
Ниже показано, как просто теперь работать с созданным параметром:
’ VB
Dim mySettings as new SampleSettingsO
Debug.WriteLine(mySettings WebServiceUrl)
11 Ctt
SampleSettings mySettings = new SampleSettingsO;
Debug.WriteLine(mySettings WebServiceUrl)-
Последним инструментом для хранения и получения собственных параметров настройки является ApplicationSettingsBase. Обратите внимание на то, что в предыдущем примере дизайнером пользовалась и сама среда Visual Studio. Аналогично предыдущему примеру, ApplicationSettingsBase содержит внутренний набор пар Key/Vdlue, позволяющий ссылаться на элементы по ключу. Требуется только породить класс от класса ApplicationSettingsBase и снабдить каждое свойство, соответствующее значению параметра конфигурации, атрибутом User Scoped Settings, либо атрибутом ApplicationScopedSettings. ’ VB
Public Class AppSettingsHelper
Inherits ApplicationSettingsBase
<UserScopedSetting()> _
Public Property Key() As String
Занятие 1
Настройка конфигурации 42З
Get
Return СТуре(Ме("Кеу"), String)
End Get
Set(ByVal value As String)
MeC'Key”) = value
End Set 
End Property
<ApplicationScopedSetting()> _
Public Property SettingValuesO As String
Get
Return CType(Me("SettingValue”), String)
End Get
Set(ByVal value As String)
Me("SettingValue") = value
End Set
End Property
End Class
11 C#
class AppSettingsHelper : ApplicationSettingsBase
{
[UserScopedSetting() ]
public String Key
{
get { return (this["Key”] as String); }
set { thisfKey"] = value;}
}
[ApplicationScopedSettingO]
public String SettingValue
{
get { return (this["SettingValue"] as String); }
set { thisf’SettingValue"] = value; } }
}
Нам осталось обсудить еще один аспект настройки — конфигурирование удаленных компонентов. Мало в каких областях гибкие возможности настройки так полезны, как при программировании удаленного взаимодействия. Грамотно реализованная настройка удаленного взаимодействия дает целый ряд преимуществ. Во-первых, в этом случае можно добавлять сборки без повторной компиляции и повторного развертывания приложения. В мире, где нельзя терять ни минуты, одного этого аргумента достаточно, чтобы выбрать подобный подход. К тому же есть и другие преимущества. Можно изменять размещение сборок, также без повторной компиляции и развертывания. Причина, по которой это важно, упомянута выше. Приведенный ниже пример позволит наглядно оценить все преимущества.
Допустим, имеется обычное трехуровневое приложение, включающее презентационный уровень, уровень бизнес-логики и уровень доступа к данным. Далее предполо
424 Установка и конфигурирование приложений
Глава 9
жим, что все три уровня работают на одном и том же компьютере. Приложение работает хорошо, но поскольку пользователей становится все больше, производительность снижается и работа замедляется. Удивляться этому не приходится, так как все части приложения выполняются на одном и том же компьютере. Если не воспользоваться удаленным взаимодействием, придется приобрести более мощный компьютер. Это дорогостоящее решение, к тому же оно непрактично, поскольку пользователям вынужденный простой может не понравиться.
При использовании удаленного взаимодействия можно переместить сборки на другие компьютеры, чтобы распределить нагрузку на них. Однако, если регистрировать компоненты непосредственно в коде, придется перекомпилировать сборки с новыми ссылками на каталоги, а затем заново развертывать приложение. В среде, не допускающей простоев, такой вариант исключается, и все преимущества удаленного взаимодействия будут сведены на нет. Но если поддерживается конфигурирование вместе с удаленным взаимодействием достаточно просто скопировать сборки на новый компьютер или компьютеры и отредактировать файл конфигурации. Даже без перезапуска IIS (Internet Information Services) приложение найдет и загрузит сборки на новых компьютерах.
Также предположим, что требуется изменить один из компонентов и добавить несколько новых сборок. При использовании только удаленного взаимодействия или только конфигурирования, разработчик снова окажется в ситуации, которую мы рассматривали. Однако сочетание удаленного взаимодействия и конфигурирования позволяет максимально упростить решение — достаточно добавить файлы на выбранный компьютер и создать подходящие элементы в файле конфигурации. Хотя и на словах все это звучит довольно привлекательно, по-настоящему оценить красоту такого подхода можно только на практике. Чтобы зарегистрировать компонент-сервер, можно воспользоваться таким кодом:
<system.runtime.remoting>
Application name ="MyApplication">
<service>
<wellknown type="FullyQualifiedName,AssemblyName” mode=”Singleton" objectUri= MyClass.rem"
</service>
</application>
</system.runtime.remoting>
По сути требуется только указать полное имя объекта и имя сборки, режим объекта {Singleton, SingleCall) и универсальный идентификатор ресурса (Uniform Resource Identifier, URI). Если сборка должна быть размещена на сервере IIS, следует использовать расширение .гет.
Так же просто сконфигурировать клиентское приложение, чтобы оно нашло и загрузило сборку. Единственное различие заключается в том, что не требуется указывать режим объекта, a URI должен ссылаться на виртуальный каталог и порт компьютера, на котором расположен объект. Если предположить, что приложение работает на локальном компьютере и привязано к порту 5000, соответствующая конфигурация клиента должна выглядеть приблизительно так:
<system.runtime.remoting>
Application name ="MyClientApplication">
<service>
Занятие 1
Настройка конфигурации 425
<wellknown type="FullyQualifiedName, AssemblyName" url="http://localhost:5000/MyClass.rem"
</service>
</application>
</system.runtime.remoting>
Практикум. Получение строки подключения к базе данных
На этом практикуме вы создадите файл конфигурации и сохраните в нем строку подключения к базе данных. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение. Получение строки подключения из файла конфигурации
В этом упражнении вы создадите в файле конфигурации элемент для хранения строки подключения к базе данных SQL Server и напишете клиентский код, извлекающий эту строку.
1.	Откройте Visual Studio 2005. В меню File выберите New, затем Project и укажите подходящий язык программирования. Выберите Windows project и Console Application. Откроется диалоговое окно, аналогичное показанному на рис. 9-4.
Рис. 9-4. Создание нового консольного приложения
2.	Назовите проект ConnectionStringDemo.
3.	Теперь выберите Project\Add New Item и Application Configuration File (рис. 9-5).
4.	В полученный XML-файл app.config добавьте раздел <ConnectionStrings>. В Тнего добавьте разделы AdventureWorksString и MarsEnabledSqlServer2005String. Для каждого раздела задайте значение ProviderName равным System.Data.SqlClient.
426 Установка и конфигурирование приложений
Глава 9
Рис. 9-5. Добавление файла конфигурации приложения в Visual Studio 2005
<connectionSt rings>
<clear/>
Odd name="AdventureWQrksString"
providerName="System.Data.SqlClient"
connectionString="Data Soiirce=localhost;Initial Catalog=AdventureWorks;
Integrated Security=true"/>
Odd name="MarsEnabledSqlServer2005String"
providerName="System.Data.SqlClient
connectionString="Server=Aron1;Database=pubs;
Trusted_Connection=True,MultipleActiveResultSets=true" /> </connectionSt rings>
5.	Добавьте строки подключения, точно совпадающие со строками, показанными в примере выше.
6.	Создайте класс DemoConnectionStringHandler, в меню Project\Add New Item выбрав Добавить класс (Add Class). Назовите его DemoConnectionStringHandler.cs или DemoCon-nectionStringHandler.vb в зависимости от выбранного языка программирования.
7.	По мере необходимости добавьте к проекту ссылки на библиотеки System. Data.SqlClient, OleDb, Odbc и OracleClient. Для этого щелкните проект правой кнопкой и выберите Add Reference. На вкладке .NET добавьте каждую из упомянутых библиотек. И добавьте все методы, приведенные в следующем коде:
' VB
Imports System.Diagnostics
Imports System.Configuration
Imports System.Data
Imports System.Data.SqlClient
Imports System.Data.OleDb
Imports System.Data.Odbc
Imports System.Data.OracleClient
Imports System.Text
Занятие 1
Настройка конфигурации 427
Public Enum RetrievalType
ByName = О
ByProviderType = 1
End Enum
Public Class DemoConnectionStringHandler
Public Shared Sub GetSpecificConnectionString(ByVal type As RetrievalType, ByVai typeOrName As String)
If typeOrName Is Nothing Or typeOrName = String.Empty Then
Throw New ArgumentException("Name cannot be empty", "TypeOrName”) End If
Select Case type
Case RetrievalType.ByName
Dim MySettings As ConnectionStringSettings =
Configu rationManage r.ConnectionSt rings(typeO rName)
If Not MySettings Is Nothing Then
Console.WriteLine(MySettings.ConnectionSt ring)
End If
Case RetrievalType.ByProviderType
Dim MyTypeSettings As ConnectionStringSettingsCollection = ConfigurationManager.Connectionstrings
If Not MyTypeSettings Is Nothing Then
For Each typesettings As ConnectionStringSettings In MyTypeSettings
If typesettings.ProviderName = typeOrName Then Console.WriteLine("Connection String: " & _ typesettings.ConnectionSt ring)
End If
Next
End If
End Select
End Sub
Public Shared Sub GetAllConnectionStringsO
Dim MySettings As ConnectionStringSettingsCollection = ConfigurationManager.Connectionstrings
If Not MySettings Is Nothing Then
Dim sb As New StringBuilder
Dim individualsettings As ConnectionStringSettings
For Each individualsettings In MySettings
sb.AppendC'Full Connection String: ” & _ individualsettings.ConnectionSt ring)
sb.Append("Provider Name: " & _ individualsettings.ProviderName)
sb.Append("Section Name: " & individualsettings.Name)
428 Установка и конфигурирование приложений
Глава 9
Dim MyConnection As IDbConnection Л
Select Case individualsettings.ProviderName
Case "System.Data.SqlClient"
MyConnection = New
SqlConnection(individualsettings.Connectionstring)
Case "System.Data.OracleClient"
MyConnection = New _
OracleConnection(individualsettings.Connectionstring)
Case "System.Data.OleDb"
MyConnection = New
OleDbConnection(individualsettings.ConnectionSt ring)
Case "System.Data.Odbc"
MyConnection = New _
OdbcConnection(individualsettings.ConnectionSt ring) End Select
Next
Console.WriteLine(sb.ToString)
End If
End Sub
End Class
// C#
using System;
using System.Data;
using System.Collections.Generic;
using System.Text;
using System.Configuration;
using System.Diagnostics;
using System.Data.SqlClient;
using System.Data.OleDb;
using System.Data.OracleClient;
using System.Data.Odbc;
namespace ConnectionscringDemo
{
public enum RetrievalType
{
ByName = 0,
ByProviderType = 1
}
class DemoConnectionStringHandler
{
private String _MyValue;
public String MyValue
{
Занятие 1
Настройка конфигурации 429
get { return this._MyValue; } set { this._MyValue = value; } }
public static void GetSpecificConnectionStrings(RetrievalType type, String typeOrName) {
if (typeOrName == string.Empty || typeOrName == null) { throw new ArgumentExceptionC'Name cannot be empty", "typeOrname"); }
switch (type)
{
case Ret rievalType.ByName: ConnectionStringSettings MySettings = ConfigurationManager.ConnectionStrings[typeOrName]; Debug.Assert(MySettings != null,
"The name does not appear to exist, verify it in the configuration file"); if (MySettings != null)
Console.WriteLine(MySettings.Connectionstring); } break;
case RetrievalType.ByProviderType: ConnectionStringSettingsCollection MyTypeSettings = ConfigurationManager.Connectionstrings;
Debug.Assert(MyTypeSettings != null, "Type does not appear to be present."); if (MyTypeSettings != null) { foreach (ConnectionStringSettings typesettings in MyTypeSettings) { if (typesettings.ProviderName == typeOrName) { SqlConnection MyConnection = new
SqlConnection(typeSettings.Connectionstring);
Console.WriteLine("Connection String " + typesettings.Connectionstring);
} } break; } }
public static void GetAllConnectionStringsO
{
ConnectionStringSettingsCollection MySettings =
430 Установка и конфигурирование приложений
Глава 9
ConfigurationManager.Connectionstrings;
Debug.Assert(MySettings != null);
// Если значений нет, в отдадочной версии
// выполнение прекращается
if (Му Settings != null)
{
StringBuilder sb = new StringBuilderO;
foreach (Connections!ringSettings individualsettings in MySettings)
{
sb.Append("Full Connection String: " + individualsettings Connectionstring + ”\r\n");
sb.Append("Provider Name: " +
individualsettings ProviderName + "\r\n");
sb.Append("Section Name: " +
individualsettings.Name + "\r\n");
}
Console. WriteLine(sb.ToStringO);
}
}
}
8.	Если вы используете С#, выберите файл Program, cs, если Visual Basic — файл Modulel.vb. Добавьте в тело метода Main следующий код:
’ VB
DemoConnectionStringHandler.GetAllConnectionStringsO
// C#
DemoConnectionStringHandler.GetAllConnectionStrings()
9.	Нажмите клавишу F5 или в меню Build выберите Build Solution и запустите приложение. Окно консоли должно выглядеть примерно так, как показано на рис. 9-6.
Рис. 9-6. Данные, выведенные на консоль методом GetAllConnectionStrings
Занятие 1
Настройка конфигурации	43-|
10.	Если вы используете С#, выберите файл Program.cs, если Visual Basic — файл
Module l.vb. Добавьте в метод Main следующий код:
' VB
DemoConnectionSt ringHandler._
GetSpecificConnectionSt ring(Ret rievalType.ByName, "Adventu reWorksSt ring")
DemoConnectionStringHandler._
GetSpecificConnectionString(RetrievalType ByName,_
"MarsEnabledSqlServer2005String")
DemoConnectionStringHandler._
GetSpecificConnectionSt ring(Ret rievalType.ByProvide rType,_
"System.Data SqlClient")
Console. ReadLineO
// C#
DemoConnectionSt ringHandle r.GetSpecificConnectionSt ring(Ret rievalType.ByName, "Adventи reWo rksSt ring");
DemoConnectionStringHandler Ge tSpecificConnectionString(RetrievalType.ByName, ' MarsEnabledSqlServer2005String");
DemoConnectionSt ringHandle r.
GetSpecificConnectionString(RetrievalType.ByProviderType,
"System.Data.SqlClient");
Console. ReadLineO;
11.	Нажмите клавишу F5 или в меню Build выберите Build Solution и запустите приложение. Окно консоли должно выглядеть примерно так, как показано на рис. 9-7.
File C./Projects DotNet DBf.   "n	. ,1 - . ’«-'i in  rt«v< Wi< ч«*‘.. Г
Data Source localhost;In itial Catalog AdventureWorks; Integrated Security true pl Connection String Data Source localhost; Init ial Catalog=AdventureWorks; Integr* .•ted Security true
 ounection String : Data Source localhost;Initial Catalog AdventureWorks; Integra ated Security-true
Connection String - Data Source localhost;Initial Catalog-AdventureWorks; IntegrM ated Security true	/
Рис. 9-7. Данные, которые выведет на консоль метод GetAUConnectionStrings
Резюме
 Пространство имен Sy stem.Configuration играет центральную роль при конфигурировании .NET-приложений.
 Элемент Supported Runtime позволяет указать исполняющей среде .NET, какую версию .NET Framework использовать.
432 Установка и конфигурирование приложений
Глава 9
 Класс ConfigurationManager предоставляет два стандартных свойства для хранения информации о конфигурации — AppSettings и Connectionstrings.
 В разделе AppSettings файла конфигурации можно хранить собственные параметры.
 Получить строки подключения к базам данных можно при помощи свойства Connectionstrings класса ConfigurationManager.
 Хранение строк подключения в виде открытого текста приводит к появлению серьезной уязвимости в системе безопасности. Из соображений безопасности все строки подключения должны быть зашифрованы.
 Удаленное взаимодействие .NET может быть настроено посредством файлов конфигурации. Для этого требуется добавить в файл конфигурации раздел <System.Runtime.Remoting>.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1. Какой из фрагментов кода является оптимальным для получения раздела «Foo» приведенного ниже файла конфигурации?
<?xml version="1.О" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="Foo" value="Hello World!"/>
</appSettings>
</configuration>
A.
NameValueCollection AllAppSettings = ConfigurationManager.AppSettings; Console.WriteLine(AllAppSettings["Hello World! "]);
B.
Console.WriteLine(ConfigurationSettings. AppSettingsf'Foo"]),
C.
NameValueCollection AllAppSettings = ConfigurationManager.AppSettings;
Console WriteLine(AllAppSettings[5]);
D.
NameValueCollection AllAppSettings = ConfigurationManager.AppSettings; Console.WriteLine(AllAppSettings["Foo"]);
2. Что произойдет, если воспользоваться следующим разделом кода для хранения строки подключения?
<?xml version="1.О" encoding="utf-8” ?>
<configuration>
<SqlConnectionStrings>
<clear/>
Odd name="Adventи reWo rksString"
Занятие 2
Создание установщика 433
providerName="System.Data.SqlClient" connectionString="Data Source=localhost;Initial Catalog=AdventureWorks;
Integrated Security=true"/>
</ SqlConnectionStrings >
</configuration>
А. Никаких проблем с этим файлом нет, можно без затруднений получить из него строку подключения.
В.	Не обязательно использовать элемент <clear/>, поскольку имеется только одна строка подключения, а этот элемент может помешать при синтаксическом разборе файла.
С.	Если воспользоваться свойством SqlConnectionStrings объекта ConfigurationManager, будет получено правильное значение.
D.	Для SqlConnectionStrings нет предопределенного свойства, поэтому файл не будет обработан корректно. Если заменить SqlConnectionStrings на connectionstrings все отработает правильно.
3. Какие из перечисленных объектов или интерфейсов предоставляют оптимальное решение для управления собственной конфигурацией? (Укажите все верные ответы.) A. IConfigurationSectionHandler.
В. ConfigurationValidatorBase.
С. IConfigurationSystem.
D. Applicationsettings Base.
Занятие 2. Создание установщика
Чтобы адаптировать среду для сборок, можно сконфигурировать домены приложения. Главным образом, изменения стандартных параметров домена приложения должны приводить к ограничению прав в целях снижения риска возникновения уязвимостей системы безопасности. Идеально сконфигурированный домен приложения не только предоставляет единицу изоляции, но ограничивает ущерб, который могут нанести злоумышленники, если им удастся воспользоваться сборкой.
Изучив материал этого занятия, вы сможете:
J использовать базовый установщик;
J фиксировать установку;
J откатывать установку.
Продолжительность занятия — 25 минут.
Использование базового установщика
Класс Installer является основным инструментом, предоставленным разработчикам инфраструктурой .NET Framework для установки приложений. Вот краткий перечень причин применения программ-установщиков:
434 Установка и конфигурирование приложений
Глава 9
	Благодаря им приложение выглядит профессионально. Хоть этот довод не всегда — решающий, все приложения, выполненные на профессиональном уровне, включают программы-установщики.
	Они упрощают процесс подготовки приложения к работе. Чем сложнее пользователям установить новый продукт и начать с ним работать, тем меньше шансов, что им будут пользоваться.
	Они позволяют задавать параметры, которые требуются для работы приложения [например, универсальные указатели на ресурс (URL) Web-сервисов или расположение баз данных], позволяют создавать разделы в реестре, ярлыки на рабочем столе, а также предлагают множество других функций, существенно расширяющих возможности пользователя.
	Они предоставляют пользователям механизм для удаления приложения без «следов», таких как файлы, рисунки или элементы реестра.
Базовым классом для создания собственных установщиков является класс Installer. В инфраструктуре .NET Framework имеются также два предопределенных установщика: Assembly Installer и Componentinstaller. К ним мы обратимся чуть позже. Чтобы при разработке приложения воспользоваться классом Installer, необходимо понимать принципы его работы. Согласно документации MSDN, чтобы применить класс, производный от класса Installer, нужно выполнить следующее:
1.	Породить класс от класса Installer.
2.	Переопределить методы Install, Commit, Rollback и Uninstall.
3.	Добавить к производному классу атрибут RunlnstallerAttribute и установить его равным true.
4.	Поместить порожденный класс в сборку с устанавливаемым приложением.
5.	Вызвать установщик, например, с помощью InstallUtil.exe.
К СВЕДЕНИЮ Класс Installer
Подробнее о классе Installer — на странице http://msdn.microsoft.com/library/rus/ default.asp?url=/library/rus/cpref/html/frlrfsystemconfigurationinstallinstallerclasstopic.asp.
Хотя приведенные выше инструкции просты, последний шаг требует пояснений. Запустить установщик можно несколькими способами — к примеру, с помощью утилиты InstallUtil.exe. В классе Installer имеется свойство Installers, являющееся на самом деле экземпляром InstallerCollection. Польза от этого свойства в том, что оно позволяет добавлять к сборке несколько установщиков для различных задач.
Далее обсудим, что происходит после запуска установки. Сначала вызывается метод Install, начинающий установку. Если во время установки не происходит никаких ошибок, по завершении установки вызывается метод Commit, чтобы зафиксировать установку. Слово «commit» имеет особый смысл для тех, кто знаком с базами данных и транзакциями, в данном случае его смысл тот же самый. Либо все операции, составляющие установку, завершаются успешно, либо все они отменяются. Процесс установки в этом контексте можно рассматривать как транзакцию, гарантирующую отсутствие проблем из-за незавершенной установки приложений. Этот подход дает существенное преимущество. Возьмем в качестве примера большого, объемного приложения пакет Microsoft Office. Предположим, установка была выполнена на 99%, а затем произошел сбой. Если бы процесс установки не выполнялся по принципу транзакции, на компьютере осталось бы множество бесполезных файлов и элементов реестра. Этого было бы достаточно для возникновения серьезных неполадок в работе компьютера. Но Office содержит кор-
Занятие 2
Создание установщика 435
рекгную программу-установщик, которая в случае неудачной установки восстанавливает состояние компьютера, убирая за собой весь мусор. Классы, порожденные от класса Installer, ведут себя так же. Однако нужен механизм, объявляющий установку успешной и указывающий, что уборка не требуется. Эту задачу решает метод Commit.
Также, когда в приложении больше нет необходимости, можно воспользоваться методом Uninstall. Этот метод дает пользователям возможность просто удалить приложение и автоматически вернуть компьютер в состояние, в котором он был до установки.
Для упрощения вызов метода Commit в классе имеется делегат InstallEventHandler. Рассмотрим особенности его реализации. Для использования установщика добавьте, если нужно, ссылки на пространства имен System, System.Collections, System.ComponentModel и System.Conjlguration.Install и выполните следующее:
1. Создайте класс, производный от класса Installer.
' VB
Public Class Customlnstaller
Inherits Installer
End Class
11 CH
namespace InstallerDemo
{
class Customlnstaller : Installer
{ }
2. Переопределите каждый основные методы (Install, Commit, Rollback и Uninstall)'.
' VB
Public Sub New()
MyBase.NewO
' Подключение обработчика события ’Committed'.
AddHandler Me.Committed, AddrcssOf Customlnstaller_Committed
' Подключение обработчика события 'Committing'.
AddHandler Me.Committing, AddressOf CustomInstaller_Committing End Sub 'New
' Обработчик для события 'Committing'.
Private Sub CustomInstaller_Committing(ByVal sender As Object, _
ByVai e As InstallEventArgs)
'Comitting finished
End Sub
' Обработчик для события 'Committed'.
Private Sub CustomInstaller_Committed(ByVal sender As Object, .
ByVai e As InstallEventArgs)
' CommittedO
End Sub
436 Установка и конфигурирование приложений
Глава 9
Переопределение метода 'Install'.
Public Overrides Sub Install(ByVal savedState As IDictionary)
MyBase.Install(savedState) End Sub 'Install
Переопределение метода 'Commit'.
Public Overrides Sub Commit(ByVal savedState As IDictionary)
MyBase.Commit(savedState) End Sub 'Commit
Переопределение метода 'Rollback'.
Public Overrides Sub Rollback(ByVal savedState As IDictionary)
MyBase.Rollback(savedState)
End Sub
11 C# public CustomlnstallerO
: base()
// Подключение обработчика события 'Committed'.
this.Committed += new InstallEventHandler(CustomInstaller_Committed);
// Подключение обработчика события 'Committing'.
this.Committing += pew InstallEventHandler(CustomInstaller_Committing);
}
private void CustomInstaller_Committing(object sender, InstallEventArgs e)
{
//Произошло событие 'Committing'
}
private void CustomInstaller_Committed(object sender, InstallEventArgs e)
{
// Произошло событие 'Committed'
}
// Переопределение метода 'Install'.
public override void Install(IDictionary savedState)
{
base.Install(savedState);
}
// Переопределение метода 'Commit'.
public override void Commit(IDictionary savedState)
{
base.Commit(savedState);
}
Занятие 2
Создание установщика 437
// Переопределение метода 'Rollback'.
public override void Rollback(IDictionary savedState)
{
base.Rollback(savedState);
}
Фиксация установки
В каждом блоке кода мы переопределяем реализацию базового класса и заменяем ее своей собственной. Единственно, следует упомянуть отдельно, что в тело конструктора класса добавляется делегат InstallEventHandler, который подключается к обработчикам событий Committed и Committing. Они отличаются от методов тем, что из них не требуется вызывать реализацию базового класса: базовому классу «нет дела» до событий и до того, произойдут ли они вообще.
Ранее упоминался набор Installers, вернемся к нему ненадолго. Можно иметь несколько установщиков, выполняющих различные задачи. Чем сложнее процедура установки, тем лучше подойдет такое решение. Для простой установки в наборе может быть только один элемент, для альтернативных вариантов — несколько. Рассмотрим работу такой конструкции.
Когда происходит обращение к классу, перебираются все элементы набора и вызываются соответствующие методы:
	Если установка прошла успешно, вызывается метод Commit.
	Если произошел сбой и установка потерпела неудачу, вызвается метод Rollback.
	Если приложение удаляется, вызывается метод Uninstall.
Для программного запуска установщика используется класс Assembly Installer или Componentinstaller. Чтобы воспользоваться одним из этих классов:
1. Создайте экземпляр любого из этих объектов.
2. Вызовите метод или методы, соответствующие действиям, которые должен выполнить установщик.
Пример, приведенный ниже, делает эту операцию понятнее:
' VB
Sub Main(ByVal args As StringO)
Dim Actions = New HashtableO
Try
Dim CustomAssemblylnstaller As _
New AssemblyInstaller(”ExampleAssembly.exe”,args)
CustomAssemblylnstaller.UseNewContext = True
CustomAssemblylnstaller.Install(Actions)
CustomAssemblylnstaller.Commit(Actions)
Catch e As ApplicationException
Console.WriteLine(e.ToString)
End Try
End Sub
438 Установка и конфигурирование приложений
Глава 9
// C#
IDictionary Actions = new HashtableO;
try {
// Создание экземпляра класса 'Assemblylnstaller'.
Assemblylnstaller CustomAssemblylnstaller = new
Assemblylnstaller("CustomAssembly.exe", args);
CustomAssemblylnstaller.UseNewContext = true;
// Установка сборки 'MyAssembly'.
CustomAssemblylnstaller.Install(Actions);
// Фиксация сборки 'MyAssembly'.
CustomAssemblylnstaller.Commit(Actions);
}
catch (ApplicationException e)
Console.WriteLine(e.ToString());
}
Откат установки
Мы рассмотрели установку приложения, но его можно с такой же легкостью и удалить, изменив код так, чтобы вызывался метод Uninstall.
' VB
Sub Main(ByVal args As StringO)
Dim Actions = New HashtableO
Try
Dim CustomAssemblylnstaller As New _
Configuration.Install.Assemblylnstaller("ExampleAssembly.exe", args)
CustomAssemblylnstaller.UseNewContext = True
CustomAssemblylnstaller.Uninstall(Actions)
Catch e As ApplicationException Console.WriteLine(e.ToString)
End Try
End Sub
// C#
IDictionary Actions = new HashtableO;
try { Assemblylnstaller CustomAssemblylnstaller = new
Занятие 2
Создание установщика
429
Assemblylnstaller("CustomAssembly.exe", args);
Customssemblylnstaller.UseNewContext = true;
CustomAssemblylnstaller.Uninstall(Actions);
}
catch (ApplicationException e)
{
Console.WriteLine(e.ToString());
}
Для удаления приложения:
1.	Создайте новый объект Assembly Installer или Component Installer.
2.	Передайте в качестве параметра имя сборки или приложения
3.	Вызовите метод Uninstall.
Аналогичный подход используется не только для удаления приложения, но и для отката установки. В простейшем случае достаточно вызвать метод Rollback:
' VB
CustomAssemblylnstaller.Rollback(Actions)
// C#
CustomAssemblylnstaller.Rollback(Actions);
Практикум. Создание и удаление разделов реестра
На этом практикуме вы создадите пакет установщика, добавляющий разделы в реестр Windows, и затем удалите эти разделы вместе с программой. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение. Создание и удаление разделов реестра
В этом упражнении вы создадите в файле конфигурации элемент для хранения строки подключения к базе данных SQL Server и напишете клиентский код, извлекающий эту строку.
1.	Откройте Visual Studio 2005. В меню File выберите New\Project\Other Project Types\Setup And Deployment. Выберите шаблон Setup Project. Откроется диалоговое окно, показанное на рис. 9-8.
2.	Назовите приложение CustomlnstallerDemo.
3.	В Solution Explorer щелкните правой кнопкой верхнюю часть дерева проекта CustomlnstallerDemo и выберите View, а затем Registry. Вы окажетесь в редакторе реестра.
4.	Щелкните User/Machine Hive правой кнопкой и выберите New Key
5.	Назовите новый раздел Custom Key.
6.	Щелкните правой кнопкой созданный раздел CustomKey и выберите New, а затем String Value (строковый параметр). Есть и другие типы параметров, которые можно выбрать, но на этом практикуме мы будем использовать только простой строковый параметр. Задайте значение параметра равным Demo Value.	*
7.	Снова щелкните раздел реестра CustomKey правой кнопкой и выберите Properties Window. Окно свойств станет видимым.
8.	Установите свойство AlwaysCreate равным True. В этом случае параметр будет создаваться при каждой установке.
Глава 9

440 Установка и конфигурирование приложений
New Project
Му Templates
J^Seardi Onlne Templates..,
i^Sama:
Vocation
Solution Nafflei
_ 'SetupProject f" ,j Module Project
^]CAB Pro;id
Project types:
Я P Create directory for solution Г Add to Source Control
Jempletes.
Visual Studn krstaBed terr<>late«
Рис. 9-8. Шаблон Setup Project в Visual Studio 2005

S Smart Device Database Starter Kits Й Other Languages i В Visual Basic : Windows Office I 9 Smart Device i- Database - Starter Kits E Other Project Types
Setup aft1 Deployment Database Extcnsjuty
Visual Studio Solutions
If Web Setup Project J Seti p Wizard (jo Smart Device CAB Project

Create a Windows Instaier project to which files can be added
| CustomlnstalerDemo
J C:\Projects\DotNet
] CustomlnstalerDemo
Э fr0*5*- 1
9.	Выберите свойство DeleteAtUninstall и установите его равным True.
Теперь при удалении программы параметр реестра будет автоматически удален, к тому же он будет автоматически создаваться при каждой установке.
Резюме
	Класс Installer предоставляет средства создания собственных пакетов установки для .NET-приложений.
	При неудачном выполнении установки Installer автоматически откатит все произведенные изменения. Это относится к изменениям в файловой системе, в меню Пуск (Start), в реестре Windows и на Рабочем столе.
	Метод Commit класса Installer дает знать, что установка прошла успешно и изменения можно зафиксировать.
	Метод Rollback класса Installer оповещает об ошибке и приказывает откатить внесенные изменения.
	Метод Uninstall класса Installer предоставляет основной механизм для полной отмены успешной и зафиксированной установки приложения.
	Режим Registry дизайнера класса Installer предоставляет интерфейс для получения, установки и управления параметрами настройки в реестре.
	При помощи условий запуска можно настроить установку приложения.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
Занятие 3
Использование утилиты .NET Framework 2.0 Configuration	44-|
1.	От какого базового класса нужно порождать классы-установщики при создании собственных приложений?
А. Класс InstallContext.
В. Класс InstallerCollection.
С. Класс Managed!nstallerClass.
D. Класс Installer.
2.	Допустим, вы хотите создать установку, которая откатит все внесенные изменения в случае неудачного выполнения. При помощи какого механизма решается подобная задача?
А. Метод Rollback класса Installer.
В. Метод Undo класса Installer.
С. Методы Clear и Rollback класса Installer.
D. Метод Uninstall класса Installer.
3.	Какой инструмент предоставляет разработчику среда Visual Studio 2005 для редактирования и управления разделами реестра?
А. Режим Custom Actions.
В. Режим File System.
С. Режим Registry Editor.
D. Режим Registry.
Занятие 3. Использование утилиты
.NET Framework 2.0 Configuration
Утилита .NET Framework 2.0 Configuration является мощным визуальным инструментом, позволяющим пользователю управлять практически всеми аспектами конфигурирования сборок. Это оснастка Консоли управления Microsoft (Microsoft Management Console, ММС) и доступна во всех последних версиях Microsoft Windows, включая Windows 2000\ХР\2003. Хотя, конечно, рассматриваемый инструмент имеет и другие назначения, но в основном помогает разработчикам:
	Конфигурировать и управлять сборками из глобального кэша сборок (global assembly cache, GAC);
	Настраивать политику безопасности доступа кода;
	Настраивать службы удаленного взаимодействия.
Изучив материал этого занятия, вы сможете:
J запускать утилиту .NET Framework 2.0 Configuration для любой установленной на компьютере версии .NET Framework;
S добавлять приложение, которое требуется настроить;
S изменять конфигурацию приложения;
S восстанавливать конфигурацию приложения.
Продолжительность занятия — 15 минут.
442 Установка и конфигурирование приложений
Глава 9
Просмотр конфигураций
Чтобы воспользоваться инструментом, следует сначала его открыть. Получить доступ к этому инструменту можно из группы Администрирование (Administrative Tools), .NET Framework X.X Configuration) или из командной строки:
System root%\Microsoft. NET\Framework\version/Vi/fflt>er\Mscorcfg. msc
На рис. 9-9 показан вид окна этой утилиты.
Рис. 9-9. Утилита .NET Framework 2.0 Configuration
Здесь доступно:
	Управление кэшем сборок (Manage The Assembly Cache).
	Управление сконфигурированными сборками (Manage Configured Assemblies).
	Настройка политики безопасности доступа кода (Configure Code Access Security Policy).
	Настройка служб удаленного взаимодействия (Adjust Remoting Services).
	Управление отдельными приложениями (Manage Individual Applications).
Рассмотрим каждый вариант и его функции по отдельности. Первым в списке числится пункт Manage The Assembly Cache. Выбрав его, можно выполнить следующие операции:
	Просмотр списка сборок в кэше сборок (View List Of Assemblies In The Assembly Cache).
	Добавление сборки в кэш сборок (Add An Assembly To The Assembly Cache).
При выборе пункта View List Of Assemblies In The Assembly Cache откроется список сборок из GAC.
Добавить сборку в кэш также очень просто. Достаточно щелкнуть Add An Assembly То The Assembly Cache и выбрать в Проводнике Windows добавляемую сборку.
Для пункта Manage Configured Assemblies) возможны следующие варианты:
	Просмотр списка сконфигурированных сборок (View List of Configured Assemblies).
	Конфигурирование сборки (Configure An Assembly).
Занятие 3
Использование утилиты .NET Framework 2.0 Configuration 443
Последний пункт позволяет управлять отдельными приложениями. Единственная операция в этом случае — добавление приложения для конфигурирования. Следует отметить, что если сборки посредством этого инструмента не добавлялись, то будут представлены только сборки, перечисленные в GAC.
Изменение конфигурации
Процедура изменения конфигурации столь же проста:
1.	В списке операций, относящихся к узлу дерева Configured Assemblies, выберите пункт Configure An Assembly.
2.	Откроется окно мастера, в котором нужно указать местоположение сборки.
3.	Выберите сборку, которую хотите настроить.
4.	Откроется диалоговое окно, в котором можно внести требуемые изменения (рис. 9-10).
Рис. 9-10. Диалоговое окно свойств сборки
На вкладке Binding Policy можно изменить перенаправление привязки. Обычно это делается для замены конечной версии на более новую или наоборот.
Аналогичное назначение имеет и вкладка Codebases. Здесь вы можете удалить любую базу кода или вернутся к более ранней, выбрав ее и щелкнув кнопку Apply.
Также можно изменить параметры безопасности. Для работы с ними:
1.	Откройте утилиту. ЛЕТ2.0 Configuration, разверните узел Му Computer и выберите Runtime Security Policy. Здесь присутствует несколько элементов, которые можно изменять, — узлы Enterprise, Machine и User.
2.	Каждый из перечисленных узлов имеет разделы Code Groups, Permission Sets и Policy Assemblies. Выберите раздел, который собираетесь изменить.
3.	Для узлов Enterprise, Machine и User можно либо изменить свойства группы кода, либо добавить дочернюю группу кода В основном манипуляции производятся с условиями членства и наборами разрешений. Например, можно изменить зону (Zone) со значения Local Intranet на Му Computer. Окно при этом должно выглядеть так, как показано на рис. 9-11.
444 Установка и конфигурирование приложений
Глава 9
Рис. 9-11. Диалоговое окно Security Settings, условия членства
Имеется особый параметр группы кода с именем All_Code. Если выбрать этот узел, откроется диалоговое окно с текстом «Code group grants all code full trust and forms the root of the code group tree» (Группа кода предоставляет любому коду полное доверие и является корневой для дерева групп кода) Поэтому можно без труда при помощи этого элемента связать идентификационные данные All Code со специальным набором разрешений. Редактирование наборов разрешений выполняется аналогично. Вкладка Permission Set показана на рис. 9-12.
[All Code Properties
General | Membersh^ Cond^' Permawrn Set |” " ‘
Atsembfcs that meet ttws code groups rrwnbetthip condition w# | receive the релпвясп». in the	speeded betaw.
ййИИИИИМИИ.-
;	I ’
The selected permission set has the fpfc**»ng permisaorut J"W"......—	- .............  ।
g] Environment Variables g] File Dialog g] Isolated Storage File g] Reflection fvj Security fv] User Interface g|DNS gj Printing
;5 '
 a
У



£ar»jl fippfy

Рис. 9-12. Свойства параметра All_Code — вкладка Permission Set
Таким образом, можно просмотреть наборы разрешений любой зоны, изменить их или выполнить и то и другое. Разберем две распространенные задачи — создание набора разрешений и создание группы кода. Чтобы создать набор разрешений:
Занятие 3
Использование утилиты .NET Framework 2.0 Configuration 445
1.	Откройте инструмент конфигурирования и последовательно разверните узлы Му Computer, Runtime Security Policy и Machine.
2.	Разверните узел Machine, щелкните узел Permission Sets) правой кнопкой и выберите New. Откроется диалоговое окно Create Permission Set (рис. 9-13).
( reale Permission Set
! Identify the new Permission Set
The new permission set should have a name and description to help others understand its use.
Рис. 9-13. Диалоговое окно Create Permission Set
3.	Выберите Create A New Permission Set и в поле Name введите MyDemoPeemission.
4.	Щелкните Next.
5.	В списке Available Permissions выберите Registry. Соответствующее диалоговое окно показано на рис. 9-14.
I
Create Permission Set
1
»,иЯ.иж>»1»|<>-й.а..гм1М.Дмии.и11ИИ.
Рис. 9-14. Список Available Permissions в диалоговом окне Create Permission Set
Each permission set is a collection of many different permissions to various resources on the computer. Select the permissions that you would like to have in this permission set.
Cirectory Sjrrices DNS
Event Log Environment Variables File 10
, File Dialog
>* IsolatedStorageFile Message Queue Performance Counter
A a^ned Perrtt^SwA
Add».. I
Lr g
«Rt-novs I Propene.
Reflection Security Service Controller Socket Access SQL Client Web Access User Interface XS09 Store
446 Установка и конфигурирование приложений
Глава 9
6.	Щелкните кнопку Add. Откроется диалоговое окно Permission Setting (рис. 9-15).
Рис. 9-15. Диаговое окно Permission Settings
7.	Выберите разрешения Read, Write и Create, введите текст MyKeyValue в раздел Key и щелкните ОК. Элемент MyDemoPermission должен появиться в разделе Available Permissions. Щелкните Finish.
Чтобы создать новую группу кода:
1.	Откройте инструмент конфигурирования и разверните узлы Му Computer, Machine и Code Groups.
2.	Щелкните узел All Code правой кнопкой и выберите New. Откроется диалоговое окно Create Code Group (рис. 9-16).
и, в,, 1	....
Рис. 9-16. Диалоговое окно Create Code Group
Занятие 3
Использование утилиты .NET Framework 2.0 Configuration 447
3.	В поле Name введите MyCodeGroup и щелкните Next.
4.	Когда будет предложено выбрать тип условия, выберите Application Directory и щелкните Next.
5.	Выберите MyDemoPermissionSet и щелкните Next и Finish.
К настоящему моменту вы успешно создали как набор разрешений, так и группу кода. Также можно повысить уровень доверия сборки. Для этого выполните следующее: 1. Откройте инструмент конфигурирования и разверните узел Му Computer. Выберите узел Runtime Security Policy.
2. Выберите в правой части окна пункт Increase Assembly Ihist.
Далее вам будет предложено задать сборку, а затем вы сможете внести необходимые изменения. Если вы все выполните правильно, то увидите диалоговое окно Trust Ап Assembly (рис. 9-17).
Рис. 9-17. Диалоговое окно Trust An Assembly
Сброс конфигурации
В конфигурации имеются элементы, значения которых порой требуется восстановить, чаще всего сбрасываются настройки защиты. Проще всего решить эту задачу, указав узел Runtime Security Policy и выбрав команду Reset All Policy Levels.
ВНИМАНИЕ! Сброс конфигурации
Сбрасывайте конфигурацию, только если уверены, что это не отразится на работе компьютера. При сбросе конфигурации все элементы политики безопасности, которые были изменены на вашем компьютере, будут приведены к исходному, стандартному состоянию. Если вы не уверены, использовать эту возможность следует крайне осторожно.
448 Установка и конфигурирование приложений
Глава 9
При попытке выполнить эту операцию откроется окно для подтверждения, поскольку отменить впоследствии сброс конфигурации нельзя. Когда вы выберете пункт Reset All Policy Levels, откроется диалоговое окно, показанное на рис. 9-18, — щелкайте Yes, только при полной уверенности, что восстановить все параметры действительно нужно.
Рис. 9-18. Запрос подтверждения
Еще одна возможность предполагает работу с отдельной сборкой. Как только вы добавили сборку, укажите ее узел и выберите Fix This Assembly. Единственным препятствием для применения этого инструмента является то обстоятельство, что для его правильной работы требуется хотя бы один успешный запуск приложения. Сразу после запуска этот инструмент запрашивает предыдущую версию, и будет использовать ее для отмены любых внесенных изменений. И эту функциональность инструмента следует использовать с осторожностью: если восстановление не даст желаемого результата, вам придется потратить много времени, чтобы вернуть подходящие параметры
Практикум. Изменение и восстановление параметров настройки приложения
На этом практикуме вы создадите файл конфигурации и прочитаете из него значения. Далее вы узнаете, как записывать значения в файл конфигурации.
Упражнение. Изменение и восстановление параметров настройки приложения
1.	В Панели управления выберите Администрирование (Administrative Tools, а затем .NET Framework 2.0 Configuration).
2.	Выберите узел Runtime Security Policy.
3.	Выберите Make Changes To This Computer.
4.	В Проводнике Windows выберите приложение, созданное на занятии 1.
5.	Мастер предложит выбрать подходящий уровень безопасности. Выберите Full Thist и щелкните Finish.
6.	Перейдите на главную вкладку-NET 2.0 Configuration).
7.	Выберите вкладку Application. Если там нет приложений, выберите Add Application То Configure и укажите созданную вами сборку.
8.	После добавления сборки укажите ее узел и выберите Fix This Application, как в окне утилиты .NET Framework 2.0 Configuration (рис. 9-19).
9.	Если имеются уже существующие конфигурации, все они будут вам предложены, и вы сможете выбрать конфи! урацию, к которой предпочитаете вернуться. Это позволит эффективно отменить все внесенные с тех пор изменения.
Занятие 3	Использование утилиты .NET Framework 2.0 Configuration 449
1 Tir -NET Framework 24) Configuration
Ejte fiction Vtow fcjdp
Configired Assembles Remoting Services Ё Runtime Security Poky % Enterprise ' i g-СЙ Code Groups i —AljCode i S Permission Sets
SFulTrust SkpVerification Execution Nothhg Loceilntranet Internet Everything ’ii^Poicy Assemblies
H В Machine . tpQjl code Groups
Permission Sets  Poky Assembles Ё -С User Щ Cp Code Groups S -Qil Permission Sets ' Poky Assemblies ‘ЧЙ Trusted Appkations В Applications
CanfigSettingsSample
This application can have its own configured assemblies and remot affect this one application. Use the tasks below to get started.
Location:
C:\Projects\DotNet\ConfigSettingsSample\ConfigSettingsSample\bii
Tasks
Use the application's Properties dialog box to make changes to garbage ci private path used to find additional assemblies.
View .the Assembly Dependencies	:
Assembly dependencies are the assemblies that make up the application, create configured assemblies.
Managed configured Assemblies
Configured assemblies are the set of assemblies from the assembly cadr These rules can determine which version of the assembly gets loaded эс.
SI П DBComectionStringDemo
1 i assembly.
' adm Remoting Sfiryitfi?
Use the Remoting Services dialog box to adjust communication channels г
Fix this Application

I
Рис. 9-19. Утилита .NETFramework 2.0 Configuration
Резюме
	Утилита .NET Framework 2.0 Configuration предоставляет визуальный интерфейс для управления стандартными конфигурациями приложений, создания собственных групп кода, наборов разрешений и экспорта политик.
	Чтобы просмотреть или добавить элементы в GAC, достаточно в .NET Framework Configuration раскрыть узел Му Computer и выбрать узел Assembly Cache.
	Названия групп кода в .NET отражают предоставляемые ими идентификационные данные.
	Группы кода должны иметь уникальные имена. Утилита не допустит дублирования имен.
	Политики Enterprise и User содержат единственную группу кода All_Code.
	Для предоставления приложению неограниченных привилегий можно использовать группу кода All_Code в одном из узлов Enterprise или User.
	Для политики Machine, в отличие от Enterprise и User, набором разрешений по умолчанию является Nothing.
	Чтобы найти набор разрешений для политики Machine, следует использовать группы кода, например My_Computer_Zone или Intemet_Zone.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
450 Установка и конфигурирование приложений
Глава 9
К СВЕДЕНИЮ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы* в конце книги.
1. Какие задачи можно решать при помощи утилиты .NETFramework 2.0 Configuration? (Укажите все верные ответы.)
А. Просматривать все запущенные процессы и управлять их наборами разрешений.
В. Просматривать все запущенные службы и управлять их работой.
С. Просматривать сконфигурированные приложения.
D. Просматривать все сборки в глобальном кэше сборок.
2. Какие из перечисленных элементов можно создавать при помощи утилиты .NET Framework 2.0 Configuration?
А. Наборы разрешений.
В. Наборы кода.
С. Наборы сборок.
D. Наборы приложений.
Занятие 4. Управление конфигурацией
Как следует из названия, управление конфигурацией — это манипуляции над конфигурацией приложения. К управлению конфигурацией относятся такие задачи, как указание версии исполняющей среды, для которой предназначено приложение, или перечисление баз данных, к которым приложение будет подключаться. Следует поддерживать управление конфигурацией на высоком уровне, поскольку от этого во многом зависит степень удовлетворения пользователя при работе с приложением.
Изучив материал этого занятия, вы сможете:
J получать и сохранять параметры конфигурации;
J реализовывать интерфейсы в пространстве имен System.Configuration.
Продолжительность занятия — 25 минут.
Получение и хранение параметров настройки
Главную роль в получении параметров конфигурации играет объект ConfigurationManager.
Если вы еще не знаете, как открыть файл конфигурации, вернитесь к занятию 1. Если значение конфигурации прочитается успешно, получить параметр настройки будет просто. Вот код, лежащий в основе получения параметров настройки:
’ VB
Dim AllAppSettlngs As NameValueCollection = ConfigurationManager.AppSettings Console.WrlteLine(AllAppSettlngs("MyFlrstSettlng"))
Console.WrlteLlne(AllAppSettings(O))
11 C#
NameValueCollection AllAppSettlngs = ConfigurationManager.AppSettings;
Занятие 4
Управление конфигурацией 45 j
Console.WriteLine(AllAppSettings["MyFirstSetting"]);
Console WriteLine(AllAppSettings[O]);
При помощи свойства AppSettings объекта ConfigurationManager можно получить доступ к каждому значению раздела appSettings файла конфигурации. Допустим, имеется следующий файл конфигурации:
<appSettings>
Odd key="ApplicationName" value="Demo Application"/>
Odd key="ApplicationVersion" value="2.0.0.1"/>
Odd key="UserFirstName" value="John"/>
Odd key="llserLastName” value="Public"/>
</appSettings>
Свойство AppSettings возвращает набор из четырех объектов. Ключами для этих объектов будут служить ApplicationName, Application version, UserFirstName и UserLastName. Можно также ссылаться на элементы по их индексам, в этом случае ApplicationName соответствует индексу 0, ApplicationFirstName — индексу 1 и т. д.
К тому же получать значения можно при помощи свойства Connectionstrings. Подробно об этом — в занятии 1.
Наконец, можно получить значения, воспользовавшись методом GetSection объекта ConfigurationManager. Допустим, имеется следующий элемент файла конфигурации: <configSections>
<sectionGroup name="MyFirstSection"
type="DBConnectionStringDemo MyFirstSectionHandler,
DBConnectionSt ringDemo"/>
</configSections>
<MyFirstSection>
<DemoValues>
<Value>
<Identifier>111</Identifier>
<SettingValue>System.Data.SqlClient</SettingValue>
</Value>
<Value>
<Identifier>112</Identifier>
<SettingVa Lue>System.Data.01eDb</SettingValue>
</Value>
<Value>
<Identifier>113</Identifier>
<SettingValue>System.Data.Odbc</SettingValue>
</Value>
</DemoValues>
</MyFirstSection>
Прежде всего следует в секции ConfigSections объявить раздел MyFirstSection. Теоретически можно использовать неограниченное число разделов, и имеет смысл создавать разделы по количеству блоков изоляции. Для больших приложений уровня предприятия здесь часто объявляется несколько разделов. Теперь можно получать значения следующим образом:
452 Установка и конфигурирование приложений
Глава 9
' VB
ConfigurationManager.GetSectionC'MyFirstSection/DemoValues”) as ValuesHandler
// C#
ConfigurationManager.GetSection("MyFirstSection/DemoValues") as ValuesHandler;
Возвращаемое значение имеет тип Object, поэтому для дальнейшей работы необходимо выполнить приведение типов к конкретному типу объекта. Процедура приведения типов обсуждается в следующем разделе.
Сохранение параметров выполняется еще проще. В простейшем случае для сохранения параметров достаточно вызвать метод Save или SaveAs текущего экземпляра объекта Configuration. Метод Save добавит все разделы, которые были добавлены, и удалит все разделы, которые были удалены, если задать значение свойства ForceSave, являющегося членом свойства Sectioninformation объекта Configurationsection, равным true, и сохранить файл в формате Application Name.еке.config. Если не установить ForceSave равным true, то не модифицированные значения будут проигнорированы как методом Save, так и методом SaveAs. Метод SaveAs работает аналогично функции SaveAs в любых приложениях Windows — сохраняет параметры в указанный файл.
Реализация интерфейсов конфигурации
Как и большинство составляющих инфраструктуры Framework, пространство имен System.Configuration поддерживает несколько интерфейсов, реализовав которые, разработчики смогут добиться подходящей функциональности. Некоторые из этих интерфейсов, например IConfigurationSectionHandler, предназначены для использования в библиотеках общего назначения. Другие, такие как I Settings ProviderService и lApplicationSettings-Provider, предлагают более узкую функциональность. Например, интерфейс ISettingsPro-viderService применяется практически только для поддержки построения компонентов во время проектирования или для расширения возможностей отладчиков. А это довольно редкие задачи в разработке программ.
Иногда требуется довольно высокая степень модульности собственных классов, и в этом случае лучше воспользоваться интерфейсом System.Configuration.IConfigurationSectionHandler. В .NET Framework 2.0 этот элемент помечен как не рекомендуемый к использованию, но его потом можно будет заменить классом Configurationsection. Хотя на данный момент использовать интерфейс IConfigurationSectionHandler по запрещено, предпочтительнее работать с новым классом Configurationsection и поддерживающими его классами.
В примерах занятия 1 мы работали с двумя специальными разделами файла конфигурации — AppSettings и Connectionstrings. Помимо этого, можно добавлять элементы в раздел файла конфигурации ConfigSections. Подобные изменения можно производить на уровне конфигурации приложения, конфигурации машины и конфигурации Web, или сразу на всех трех уровнях. Благодаря этой гибкости разработчик может добавлять несколько изолированных разделов (в отличие от раздела общего назначения AppSettings) и читать значения при помощи предназначенных для этого классов. По своей логике такая операция будет эквивалентна примеру из предыдущего раздела.
Чтобы реализовать интерфейс IConfigurationSectionHandler, следует предварительно создать в разделе ConfigSections файла конфигурации объект SectionGroup. Объекты Configurationsection хранятся в свойстве ConfigurationSectionCollection объекта Configuration. Стандартное объявление выглядит приблизительно так:
<configSections>
<sectionGroup name="MyFirstSection"
Занятие 4
Управление конфигурацией 453
type="DBConnectionStringDemo.MyFirstSectionHandler, DBConnectionStringDemo"/>
</configSections>
Аналогичное группирование производится с классом ConfigurationSectionGroup и соответствующим классом ConfigurationSectionGroupCollection (который является контейнером для объектов ConfigurationSectionGroup). Эта задача решается при помощи класса Configuration'.
' VB
Dim config As Configuration =
ConfigurationManager.OpenExeConfiguration(ConfigurationllserLevel.None)
Dim DemoGroups As ConfigurationSectionGroupCollection = config.SectionGroups
For Each groupName As String In DemoGroups.Keys
Console.WriteLine(g roupName)
Next
// C#
Configuration config =
ConfigurationManager.OpenExeConfiguration(
ConfigurationllserLevel. None);
ConfigurationSectionGroupCollection
DemoGroups = config SectionGroups;
foreach (String groupName in DemoGroups.Keys)
{
Console.WriteLine(groupName);
}
Как говорилось ранее, редактирование и поддержка файлов конфигурации собственными средствами — неблагодарная работа. В нескольких словах это можно обрисовать следующим образом.
Обратите внимание, что первоначально в файле конфигурации была создана группа ConfigSections. Затем был объявлен раздел с именем MyFirstSection. После указания имени следует задать тип. Для задания типа требуется сослаться на полное имя объекта, за которым следует имя сборки. Выполнять эту операцию необходимо крайне осторожно, поскольку малейшая ошибка в наборе текста может повлечь серьезные проблемы. В этом примере заведен только один раздел конфигурации, однако теоретически не существует ограничения на число разделов, которые можно добавить. В приложениях уровня предприятия встречается от 5 до 15 собственных разделов.
После объявления можно создавать сам раздел. Он должен быть размещен вне других элементов конфигурации, примерно так:
<configSections>
<sectionGroup name=”MyFirstSection"
type="DBConnectionStringDemo.MyFirstSectionHandler,
DBConnectionSt ringDemo'7>
</configSections>
<MyFirstSection>
<DemoValues>
<Value>
454 Установка и конфигурирование приложений
Глава 9
cldentifier>111</Identifier>
<SettingValue>System.Data.SqlClient</SettingValue>
</Value>
<Value>
<Identifier>112</Identifier>
<SettingValue>System.Data.01eDb</SettingValue>
</Value>
<Value>
<Identifier>113</Identifier>
<SettingValue>System.Data.Odbc</SettingValue>
</Value>
</DemoValues>
</MyFirstSection>
Теперь можно добиться высокой степени гибкости в управлении параметрами, но для этого требуется как минимум реализовать интерфейс IConfigurationSectionHandler. Говоря точнее, можно получать значения по атрибуту, элементу или любой их комбинации. К счастью, этот интерфейс реализовать очень просто, достаточно реализовать метод Create. Что касается реализации, то существует бесчисленное количество способов для ее осуществления. Воспользуемся приведенным выше примером, в котором сохраняются Identifier и SettingValue. Это иллюстрация простейшей пары Key I Value. Теперь создадим класс, реализующий интерфейс IConfigurationSectionHandler, и назовем его MyFirstSectionHandler.
' VB
Dim ConfigValue As New Hashtable
Dim Root As XmlElement = CType(section, XmlElement)
Dim TempValue As String
For Each ParentNode As XmlNode In Root.ChildNodes
For Each ChildNode As XmlNode In ParentNode.ChildNodes
If ChildNode.Name = "Identifier" Then
TempValue = ChildNode.InnerText
End If
If ChildNode.Name = "SettingValue" Then TempValue = ChildNode.InnerText
End If
Next
Next
Dim MyHandler As New ValuesHandler(ConfigValues)
Return MyHandler
11 C#
Hashtable ConfigValues = new HashtableO;
XmlElement Root = section as XmlElement;
String TempValue = string.Empty;
foreach (XmlNode ParentNode in Root.ChildNodes)
Занятие 4
Управление конфигурацией д^
{ foreach (XmlNode ChildNode in ParentNode.ChildNodes)
{
if (ChildNode.Name == "Identifier") {
TempValue = ChildNode.InnerText;
}
if (ChildNode.Name == "SettingValue")
{
ConfigValues[TempValue] = ChildNode.InnerText;-} }
}
ValuesHandler MyHandler = new ValuesHandler(ConfigValues);
return MyHandler;
Для удобства создадим еще один вспомогательный класс, который будет хранить объект HashTable и поддерживать механизм получения значений из таблицы: ’ VB Public Class ValuesHandler
Private customValue As Hashtable
Public Sub ValuesHandler(ByVal configValues As Hashtable) Me.customValue = configValues
End Sub
Public Function GetValueFromKey(ByVal key As String) As String Return Me.customValue(key)
End Function
End Class
11 C#
class ValuesHandler
{
private Hashtable customValue;
public ValuesHandler(Hashtable configValues)
{
this.customValue = configValues;
}
public String GetValueFromKey(String key)
{
return this.customValue[key] as String;
}
}
456 Установка и конфигурирование приложений
Глава 9
Предположим, что мы воспользовались бы абсолютно другой методикой работы с файлом конфигурации. Например, вместо значений на основе элементов мы бы использовали значения на основе атрибутов. Наша процедура синтаксического разбора XML была бы обязана учитывать это, заполняя значения. В этом случае алгоритм метода Create зависел бы от XML, содержащегося в файле конфигурации, и от соответствующего объекта или объектов для хранения значений. Следует учитывать, что такая реализация представляет собой только пример, и фактически любая реализация будет работать, пока XML будет корректным.
Пока все в порядке, и приложение готово получить значения. Для решения этой задачи есть два основных метода. Первый, ConfigSettings.GetConfig, является устаревшим, и его следует применять с той же долей осторожности, что и любой другой устаревший метод. Второй — это метод класса ConfigurationManager с именем GetSection:
' VB
Dim vals As ValuesHandler =
ConfigurationManager.GetSection("MyFirstSection/DemoValues")
11 C#
ValuesHandler vals =
ConfigurationManager.GetSection("MyFirstSection/DemoValues")
as ValuesHandler;
При получении значений крайне важно убедиться, что указан правильный узел или узлы. Хотя обсуждение специфики синтаксического разбора XML выходит за рамки этого занятия, необходимо указать корневую область, с которой должен начаться синтаксический разбор. Всякий раз при инициализации класса вызывается метод Create и производится синтаксический разбор XML. Фрагмент файла конфигурации без конкретных значений приводится только для сравнения:
<MyFirstSection>
<DemoValues>
</DemoValues>
</MyFirstSection>
MyFirstSection — имя раздела, и именно отсюда начинается процесс. Посредством вызова метода GetSection код указывает синтаксическому анализатору, что DemoValues следует взять в качестве корневого узла и начать с него анализ.
В версии .NET Framework 2.0 появились новые методы, позволяющие достичь тех же целей, и, к счастью, новые методы намного проще и мощнее. Допустим, нам нужно создать Configurationsection с именем Chapter9Section. Для объявления можно воспользоваться следующим кодом:
<configSections>
<section name="Chapter9Section"
type="Chapter9.Configuration.ConfigHandler, App_Code" />
</configSections>
По сути достаточно добавить собственный объект Configurationsection к разделу configSections в файле конфигурации, а затем добавить имя и тип. Первая часть атрибута type включает полное имя объекта {namespace.objectname) и после запятой — имя сборки. Этот пример относится к Web-приложению ASP.NET 2.0, которое хранит класс в папке App_Code, и Арр_Code используется в качестве имени сборки. До настоящего момента процедура аналогична процедуре реализации объекта IConfigurationSectionHandler.
Занятие 4
Управление конфигурацией 57
В предыдущем абзаце рассматривалось объявление собственного объекта Configurationsection. Теперь настало время реализовать его. Для простоты добавим несложный Configurationsection с двумя элементами. В этом примере фигурирует только один объект Configurationsection, хотя допускается и несколько. При работе с Configurationsection он добавляется в набор ConfigurationSectionCollection, откуда его можно получить по имени или по индексу. Обычно, как и для объектов IConfigurationSectionHandler, имеет смысл обращаться к разделам по имени. (Дело в том, что намного легче запомнить текст, например «WebServiceSettings», чем хранить в памяти тот факт, что это девятый объект Configurationsection.)
В зависимости от потребности разработчика можно создать более простой или более сложный объект Configurationsection. Вместо элементов можно воспользоваться атрибутами или добавить несколько элементов. Короче говоря, допустима практически любая комбинация, необходимая для достижения цели. Приведенный ниже фрагмент XML создан для обсуждаемого примера (к тому же он просто состоит из раздела Соп-figurationSection с именем Chapter9Section, который, в свою очередь, состоит из двух элементов ConfigurationElement с именами LastName и FirstName).
<configSections>
<Chapter9Section LastName="Ryan" FirstName="William" />
</ConfigSections>
Выше было показано, как подготавливать объекты Configurationsection и добавлять их । набор ConfigurationSectionCollection, но работу с ними необходимо обсудить подробнее. Класс Configurationsection можно использовать в качестве базового для классов, получающих значения. Ниже приводится пример класса с именем ConfigHandler, порожденного от класса Configurationsection. В нем определено два свойства — FirstName и LastName, что соответствует показанному выше объекту ConfigurationSection.
' VB
Namespace Chapter9.Configuration
Public Class ConfigHandler
Inherits Configurationsection
<ConfigurationProperty("FirstName")>
Public Property FirstNameO As String
Get
Return CType(Me("FirstName"), String)
End Get
Set(ByVal value As String)
Me("FirstName") = value
End Set
End Property
<ConfigurationProperty("LastName")>
Public Property LastNameO As String
Get
Return CType(Me("LastName"), String)
End Get
Set(ByVal value As String)
Установка и конфигурирование приложений
Глава 9
Ме("LastName") = value
End Set
End Property
End Class
End Namespace
11 C#
namespace Chapters.Configuration
{
public class ConfigHandler: Configurationsection
[ConfigurationPropertyCLastName",IsRequired=false, DefaultValue=”NotGiven") ]
public String LastName
{
get { return (String)base["LastName"]; }
set { base["LastName"] = value; }
}
[ConfigurationProperty("FirstName", IsRequired = false, Defaultvalue = "NotGiven”)]
public String FirstName
{
get { return (String)base["FirstName”]; }
set { base["FirstName”] = value; }
}
public ConfigHandlerO
{}
}
}
Теперь можно манипулировать переменными, задающими соответствие класса и файла конфигурации:
' VB
Dim Chapter9Config As Configuration = _
WebConfigu rationManager.OpenWebConfigu ration(Request.ApplicationPath)
Dim Chapter9Section As ConfigHandler = New ConfigHandlerO
Response.Write(Chapte rSSection.Fi rstName)
Response.Write(Chapter9Section.LastName)
Chapter9Config.Sections.Clear()
Chapter9Config.Sections.Add("Chapter9Section", Chapter9Section)
Chapter9Config.Save()
11 C#
Configuration Chapter9Config =
WebConfigurationManager.OpenWebConfiguration(Request.ApplicationPath);
ConfigHandler Chapter9Section = new ConfigHandlerO;
Занятие 4
Управление конфигурацией 45g
Response.Write(Chapter9Section.FirstName);
Response.Write(Chapter9Section.LastName);
Chapter9Config.Sections.Clear();
Chapter9Config.Sections.Add("Chapter9Section", Chapter9Section);
Chapter9Config.Save();
В приведенном выше коде значения читаются непосредственно из собственного раздела ConfigurationSection (предполагается, что такой раздел существует), значения записываются в собственный раздел ConfigurationSection (предполагается, что такой раздел существует), и создается собственный раздел ConfigurationSection, если его до сих пор не было.
Еще один удобный интерфейс, применяющийся повсюду в пространстве имен System.Configuration, — это интерфейс LApplicationSettingsProvider. В занятии 1 рассматривался к пасс ApplicationSettingsBase. Класс ApplicationSettingsBase в числе других интерфейсов реализует интерфейс lApplicationSettingsProvider. (Заметим, что класс FocalFileSettings Provider также реализует интерфейс lApplicationSettingsProvider.) Этот интерфейс имеет три метода, перечисленные в табл. 9-4.
Табл. 9-4. Интерфейс lApplicationSettingsProvider
Имя	Описание
Метод GetPreviousVersion	Возвращает значение указанного свойства параметров для предыдущей версии того же приложения
Метод Reset	Восстанавливает значения по умолчанию параметров, относящихся к указанному приложению
Метод Upgrade	Указывает провайдеру, что приложение было обновлено. Таким образом провайдеру предлагается соответствующим образом обновить сохраненные параметры
Этот интерфейс предоставляет определенные преимущества, которые, следовательно, получают все классы, реализующие интерфейс, например, ApplicationSettingsBase. Согласно документации с сайта MSDN Online, к этим преимуществам относятся:  Возможность выполнения различных версий приложения на одном компьютере.  Сохранение параметров приложения при обновлении.
	Восстановление значений по умолчанию для параметров текущей версии приложения.
Хотя перечисленные полезные качества могут как подходить, так и не подходить для начального развертывания приложения, класс ApplicationSettingsBase, к примеру, реализует эти качества интерфейса, и разработчик при необходимости может воспользоваться соответствующей функциональностью. Есть еще несколько заслуживающих упоминания особенностей, отличающих обсуждаемый класс от класса, реализующего интерфейс IConfigurationSectionHandler.
	Каждое свойство может использоваться как на уровне приложения, так и на уровне пользователя. С объектами IConfigurationSectionHandler невозможно достичь такой степени модульности.
	Значения, соответствующие уровню пользователя, хранятся отдельно от значений, соответствующих уровню приложения.
	Если используется атрибут Application, то создается дополнительный файл конфигурации. Имя этого файла стандартно, но с одной поправкой — для входа в систему
460 Установка и конфигурирование приложений
Глава 9
используется имя пользователя вида User Name, а в конце добавляется .config. То есть, если входить в систему под именем John Public, будет создан отдельный файл конфигурации с именем JohnPubhc.config.
	Этот файл хранится в специальной папке Windows, и доступ к нему можно получить при помощи свойства LocalUserAppDataPath класса Application.
	Воспользовавшись классом LocalFileSettingsProvider, можно упростить доступ к файлу.
Применяя контроль типов, можно решить важную задачу по проверке корректности значений в файле конфигурации. Например, используя объект IConfigurationSectionHandler для заполнения объекта, состоящего из ключа типа Int32 и значения типа String, можно определить, что вместо числа 1 прочитана строка «Bill» и сгенерировать исключение. Контроль типов — одно из основных преимуществ файлов конфигурации по сравнению с прежними способами. Часто проверка типов необходима, но сама по себе она еще не обеспечивает того уровня контроля, который требуется приложению. Например, предположим, что требуется хранить пары Key/Value, но хочется убедиться, что значения Value представляют собой корректные телефонные номера на территории США. Где и когда проверять, являются ли строки телефонными номерами, в большой степени зависит от выбранной методики. Если работать с объектом IConfigurationSectionHandler, то можно добавить процедуру проверки в метод Create и при получении узла, предположительно содержащего телефонный номер, запускать ее.
Последний интерфейс, который необходимо упомянуть, — это IConfigurationSystem. В документации MSDN указано, что этот класс не предназначен для разработчиков. Он чаще используется проектировщиками инструментов, поддерживающих IDE.
Практикум. Чтение и запись параметров конфигурации
На этом практикуме вы создадите файл конфигурации и прочитаете из него значения. Прочитав значения из файла конфигурации, вы освоите процедуру записи значений в этот файл конфигурации. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение. Чтение и запись параметров конфигурации
В этом упражнении вы создадите элемент файла конфигурации для хранения строки подключения к базе данных SQL Server и напишете код клиента для ее получения.
1.	Откройте Visual Studio 2005. В меню File выберите New\Project и укажите подходящий язык программирования. Выберите Windows project и Console Application.
2.	В меню Visual Studio 2005 выберите Project\Add New Item\Application Configuration File и щелкните кнопку Add.
3.	В Solution Explorer щелкните относящийся к вашему проекту узел References правой кнопкой и выберите Add Reference. В списке на вкладке .NET выделите сборку System.Configuration и щелкните ОК. Если не добавить эту ссылку явно, обращаться к классу ConfigurationManager напрямую будет невозможно.
4.	Добавьте в файл конфигурации следующий фрагмент кода:
<appSettings>
<add key="Foo" value-'Hello World!"/>
</appSettings>
Занятие 4
Управление конфигурацией 45
5.	Выберите в приложении файл Program.cs или Module l.vb и добавьте в метод Main следующий код:
' VB
Imports System.Collections.Specialized
Imports System.Collections
Imports System.Configuration
Dim AllAppSettings As NameValueCollection = ConfigurationManager.AppSettings Console.WriteLine(AllAppSettings("Foo"))
Console. WriteLine( AHAppSettings(O))
// C#
using System.Collections.Specialized;
using System.Collections;
using System.Configuration;
NameValueCollection AllAppSettings = ConfigurationManager.AppSettings;
Console.WriteLine(AllAppSettings["Foo"]);
Console.WriteLine(AllAppSett i ngs[O]);
6.	Нажмите F5, чтобы запустить приложение. Появится окно консоли, в которое будет выведено значение «Hello World».
7.	Выберите файл Program.cs или Modulel.vb и откройте его. Создайте новый статичес-кий/общий метод, не возвращающий значения, и назовите его WriteSettings, как показано ниже:
' VB
Private Shared Sub WriteSettingsO
End Sub
// C#
private static void WriteSettingsO {};
8.	Добавьте в тело метода приведенный ниже код:
' VB
Private Shared Sub WriteSettingsO
Try
Dim LabSection As Configurationsection
Dim config As
System.Configuration.Configuration =
ConfigurationManager.OpenExeConfiguration(
ConfigurationUserLevel.None)	<
*If config.Sections("LabSection”) Is Nothing Then
LabSection = New ConfigurationSectionO
462 Установка и конфигурирование приложений
Глава 9
config.Sections Add(’LabSection", Configurationsection) customsection.Sectioninformation ForceSave = True
* config.Save(ConfigurationSaveMode.Full) End If
Catch ex As ApplicationException Console. WriteLine(ex. ToStringO) End Try
End Sub
// C# try { Configurationsection LabSection;
// Открытие текущего файла конфигурации System.Configuration.Configuration config = ConfigurationManager.
OpenExeConfiguration(ConfigurationUserLevel.None);
if (config.Sections["LabSection"] == null) { customSection = new ConfigurationSection(); config Sections.Add("LabSection", Configurationsection), customSection.Sectioninformation.ForceSave = true;
config.Save(Configu rationSaveMode.Full); }
}
catch (ApplicationException ex)
{
Console WriteLine(ex.ToStringO); }
9.	Удалите весь код из метода Main и вставьте следующую строку:
’ VB
WriteSettingsO
И C#
WriteSettingsO;
10.	Откройте файл конфигурации и убедитесь, что в нем появился раздел LabSection.
Резюме
	Интерфейс IConfigurationSectionHandler предоставляет возможность доступа к собственным разделам файла конфигурации.
	Имеется единственный метод Create, который необходимо реализовать для удовлетворения согласно контракту интерфейса IConfigurationSectionHandler.
Закрепление материала главы 433
	Для надежности кода классы, реализующие интерфейс IConfigurationSectionHandler, должны быть безопасны в многопоточном окружении и не должны хранить состояния
	Узел ConfigSections файла конфигурации предоставляет основу для реализации собственных разделов.
	Чтобы добавлять разделы конфигурации программным путем, а не вводить их вручную в файл конфигурации, можно использовать объекты ConfigurationSection.
	Метод Save сохраняет любые изменения, произведенные в объекте Configuration.
	Метод SaveAs упрощает сохранение изменений объекта Configuration в другой файл.
	Класс ApplicationSettingsBase является оболочкой для конфигурируемых параметров настройки.NET-приложений.
	Все параметры, собранные в классе ApplicationSettingsBase, для хранения используют класс LocalFileSettingsProvider.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Какие методы класса Configuration подходят для открытия файла конфигурации? (Укажите все верные ответы.)
A.	OpenExeConfiguration.
В.	OpenMachineConfiguration.
С. OpenMappedExeConfiguration.
D. OpenMappedMachineConfiguration.
2.	Какой из методов позволяет прочитать параметры из объекта IConfigurationSectionHandler^ A. Create.
В. ReadSection.
С. GetConfig.
D. GetAppSettings.
Закрепление материала главы
Для закрепления знаний и навыков, полученных при изучении этой главы, выполните следующее:
	изучите резюме главы;
	повторите основные термины, использованные в этой главе;
	выполните задания лабораторной работы по темам этой главы;
	выполните предложенные упражнения;
	пройдите пробный экзамен.
464 Установка и конфигурирование приложений
Глава 9
Резюме главы
	Пространство имен System.Configuration поддерживает множество инструментов для конфигурирования приложений.
	Конфигурирование имеет две основные области применения: собственные и встроенные параметры. В число собственных параметров могут войти, например, имя клиента или заданное пользователем расположение окна приложения на экране. Встроенные параметры могут, в частности, хранить версию исполняющей среды, подходящей для выполнения приложения, или предоставлять средства для получения специальных разделов, таких как строки подключения.
	Строки подключения играют особую роль во многих приложениях, поэтому для доступа к ним существуют специальные инструменты. В этой главе рассматривались строки подключения в виде открытого текста, но в реальных приложениях никогда не следует оставлять их незашифрованными.
	Программы, выполненные на профессиональном уровне, должны иметь в своем составе установщик. При помощи метода Commit можно сообщить приложению, что установка прошла успешно. Если установка потерпела неудачу, ее можно отменить, воспользовавшись методом Rollback. Полностью удалить уже установленное приложение можно при помощи метода Uninstall.
	При помощи встроенных в Installer инструментов можно отредактировать различные аспекты программного окружения. В нем есть редактор реестра для создания и редактирования разделов реестра, меню Desktop для редактирования ярлыков, добавляемых на Рабочий стол пользователя, пункт Start Menu для добавления элементов в меню Пуск (Start) и многое другое.
	Утилита .NET Framework 2.0 Configuration позволяет визуально контролировать настройку приложения. Этот инструмент позволяет настраивать элементы, относящиеся к политике безопасности, а также добавлять сборки в глобальный кэш сборок (GAC) и управлять сборками в нем.
Основные термины
	настройка приложения;
	управление конфигурацией;
	строка подключения;
	утилита .NETFramework 2.0 Configuration',
	откат;
	удаление приложения.
Лабораторная работа. Установка и настройка приложения
Сейчас вы примените на практике все, что узнали об установке настройке и настройке Ответы на вопросы см. в разделе «Ответы» в конце книги.
Рекомендуемые упражнения 455
Вы разрабатываете для своей компании крупное корпоративное приложение. К приложению предъявляется довольно много технических требований. Во-первых, требуется сохранять пользовательские параметры настройки, например положение окон. Также все параметры, кроме относящихся к безопасности, должны храниться как открытый текст и быть доступными для редактирования. Если в программе нет редактора, его следует добавить. Приложение должно быть снабжено установщиком, который в случае неудачной установки выполнит откат.
Результаты опроса
	Руководитель ИТ-отдела
«Разработчик, которой заканчивал программу, считал, что реестр на моем компьютере полностью в его распоряжении. Вся информация, относящаяся к приложению, Читалась из реестра и записывалась в него. Это абсолютно неприемлемо. После развертывания приложения я применяю групповую политику, ограничивающую права, и если приложение не сможет работать без доступа для записи в реестр, мы не будем им пользоваться. Также хотелось бы иметь хороший установщик, который выполнит откат в случае неудачной установки».
	Руководитель отдела разработки
«Хотелось бы, чтобы все параметры можно было настроить. Мне не нравится, когда параметры жестко «зашиты» в коде. Если имеется параметр, пусть он будет конфигурируемым, чтобы избежать повторного развертывания приложений».
Вопросы
Ответьте на следующие вопросы руководства.
1. Если нельзя использовать реестр, чем следует его заменить?
2. Какие дополнительные функции должна поддерживать программа установки?
Рекомендуемые упражнения
Чтобы лучше проработать темы экзамена, рассмотренные в этой главе, выполните следующие упражнения.
Создание строк подключения в файле конфигурации
Выполните как минимум упражнения 1 и 2. Для лучшего понимания работы пространства имен Sy stem.Configuration выполните также упражнения 3 и 4. Если вы разбираетесь в .NET Remooting, выполните упражнение 5.
	Упражнение 1 Добавьте в файл конфигурации строки подключения для нескольких типов провайдеров, включая SqlClient, OleDb, Odbc и Oracle Client.
	Упражнение 2 Создайте несколько параметров настройки приложения и получите их при помощи класса ConfigurationManager.
	Упражнение 3 Создайте собственный раздел в файле конфигурации и примените либо базовый класс, либо интерфейс, например IConfigurationSectionHandler, для получения параметров.
466 Установка и конфигурирование приложений
Глава 9
	Упражнение 4 Создайте установщик приложения, заносящий значения в реестр или добавляющий на Рабочий стол ярлык. Удалите приложение и убедитесь, что удаление выполнено корректно.
	Упражнение 5 Создайте сервер удаленного взаимодействия и при помощи файлов конфигурации зарегистрируйте компоненты как для клиента, так и для сервера.
Пробный экзамен
На прилагаемом компакт-диске содержится пробный экзамен, обеспечивающий широкие возможности по самоподготовке. Например, можно проверить усвоение тем этой главы либо всех тем экзамена 70-536. Можно сымитировать настоящий экзамен либо выбрать режим обучения, когда после каждого вопроса приводятся правильные ответы с пояснениями.
Пробный экзамен
Подробнее о пробном экзамене рассказано во введении.
ГЛАВА 10
Инструментарий для мониторинга
Занятие 1.	Регистрация событий	468
Занятие 2.	Отладка и трассировка	478
Занятие 3.	Мониторинг производительности	501
Занятие 4.	Обнаружение управляющих событий	521
Эта глава посвящена инструментальным средствам .NET Framework 2.0, представленным в основном средствами для мониторинга. Инструментарий содержит средства для решения самых разных задач, связанных с ведением журналов и мониторингом различных событий в приложениях.
Темы экзамена:
	Управление журналом событий средствами пространства имен System.Diagnostics:
□	запись в журнал событий;
□	чтение из журнала событий;
о создание журнала событий.
 Управление системными процессами и мониторинг производительности .NET-приложений с помощью диагностических функций .NET Framework 2.0 (см. пространство имен System.Diagnostics)'.
□	получение списка процессов;
□	извлечение информации о текущем процессе;
□	получение списка загруженных процессом модулей;
□	классы PerformanceCounter, PerformanceCounterCategory и CounterCreationData',
□	запуск процессов с использованием аргументов командной строки и без них;
а класс StackTrace\	?
а класс StackFrame.
 Отладка и трассировка .NET-приложений средствами пространства имен System.Diagnostics: □ классы Debug и Debugger,
468 Инструментарий для мониторинга
Глава 10
□	классы Trace, CorrelationManager, Trace Listener, TraceSource, Trace Switch, XmlWriter-TraceListener, DelimitedListTraceListener и EventlogTraceListener,
□	атрибуты Debugger.
	Добавление управляющей информации и обработчика события B.NET-приложения (см. пространство имен System. Management)’.
□	извлечение набора объектов Management средствами класса Management-ObjectSearcher и его производных классов;
□	класс ManagementQuery;
□	подписка на уведомление об управляющих событиях средствами класса Management Event Watcher.
Прежде всего
Чтобы освоить материал этой главы, необходимо иметь навыки работы с Microsoft Visual Basic или C# и уметь:
	создавать консольные приложения в Microsoft Visual Studio, используя Visual Basic или С#;
	добавлять в проект ссылки на системные библиотеки классов;
	просматривать журналы событий Windows.
Занятие 1. Регистрация событий
Невозможно предугадать все ситуации, с которыми вы столкнетесь после развертывания приложения. Например, нельзя определить, когда следует выгружать приложение из памяти, поскольку нельзя сказать заранее, каков объем ОЗУ пользовательского компьютера.
Изучив материал этого занятия, вы сможете:
J создавать журналы событий;
J просматривать и заносить записи в журнал событий.
Продолжительность занятия — 20 минут.
Работа с событиями Microsoft Windows и их регистрация
•	'Я
Независимо от того, насколько хорошо написано и протестировано приложение, вероятность появления «багов» и иных непредвиденных ситуаций достаточно велика. Кроме того, большинство пользователей не являются разработчиками и не знакомы с привычной разработчикам терминологией. С другой стороны, пользователям зачастую сложно объяснить суть проблемы на языке, понятном разработчикам. Отсутствие взаимопонимания сильно усложняет процесс отлова багов. Выяснить причины возникновения обнаруженных багов удается далеко не всегда. Все это значительно усложняет диагностику и сильно замедляет устранение ошибок.
Последние версии Windows, такие как Windows 2000/ХР и Windows Server 2003, предоставляют механизм, позволяющий приложениям регистрировать возникающие события. С помощью журнала событий разработчики могут регистрировать определенные аспекты состояния приложений, включая возникновение серьезных ошибок. По суще
Занятие 1
Регистрация событий 4gg
ству, после развертывания приложения разработчик получает возможность регистрировать практически любые параметры. Возможность анализа важных событий упрощает диагностику проблем специалистам службы поддержки. Преимущества средств обработки и регистрации событий Windows в том, что они предоставляют простые механизмы для:
	регистрации заданных параметров состояния приложений;
	ситуаций, которые разработчики считают необычными;
	проверки состояния запускаемых приложений.
На рис. 10-1 показана консоль Просмотр событий (Event Viewer) в Windows ХР.
Рис. 10-1. Консоль Просмотр событий в Windows ХР
Создатели .NET Framework предусмотрели средства для создания и управления записями журналов событий. Хотя механизм регистрации событий изящен и прост, его работа далеко не «бесплатна» в плане расходования системных ресурсов. Прежде чем давать рекомендации по использованию журналов событий, следует сделать ряд важных замечаний. Документация на сайте MSDN (см. http://msdn2.microsoft.com/en-US/library/ system.diagnostics.eventlog(VS.80).aspx) предостерегает о следующем:
	создание объекта Event Log, запись в него и передача его коду, не пользующемуся полным доверием, может привести к серьезной уязвимости защиты. Объекты EventLog, в том числе EventLogEntry и EventLog Entry Collect ion, нельзя передавать любому менее доверенному коду. В силу этих причин важно знать контекст защиты, в котором работают эти объекты;
	разрешение EventLogPermission необходимо для многих манипуляций с EventLog. Получение его частично доверенным кодом может привести к возникновению значительной уязвимости. Например, получив это разрешение и запустившись в частично доверенной среде, злонамеренный код запросто сможет «обхитрить» другие приложения. Он сможет, например, незаметно отключить антивирус или антишпионскую программу, а затем нанести сколь угодно большой вред.
470 Инструментарий для мониторинга
Глава 10
Операции чтения и регистрации событий требуют значительного места на диске, а также много процессорного времени и других ресурсов. После заполнения объектов EventLog дальнейшие попытки записи в них приведут к генерации исключений. Так что объекты EventLog следует использовать благоразумно и только при необходимости
ВНИМАНИЕ! Избегайте применения объектов EventLog в частично доверяемых средах
В этих ситуациях использование объектов EventLog может повлечь появление серьезных брешей в защите.
А теперь, приняв к сведению эти предупреждения, рассмотрим преимущества использования объектов EventLog.
Создание и удаление журнала событий
Класс EventLog служит для создания журнала событий в .NET Framework. Для работы с ним необходимо определить свойство Source и записать текст сообщения, как показано в следующем примере, требующем пространство имен System. Diagnostics'.
’ VB
Public Shared Sub CreateEventLogO
Dim DemoLog As New EventLog("ChaplODemo")
DemoLog.Source = "ChaplODemo"
DemoLog.WriteEntryC'CreateEventLog called", EventLogEntryType.Information) End Sub
// C#
public static void CreateEventLogO
{
EventLog DemoLog = new EventLog("ChaplODemo");
DemoLog.Source = "ChaplODemo";
DemoLog.WriteEntryC'CreateEventLog called", EventLogEntryType.Information);
}
После создания объекта EventLog и определения его источника (это можно сделать с помощью одного из перегруженных конструкторов данного объекта), результаты его работы станут видимы в консоли Просмотр событий Windows. На рис. 10-2 показано содержимое EventLog после успешного выполнения данного кода.
Удалить журнал событий столь же просто. Рассмотрим процедуру удаления на примере журнала, созданного в ходе данного упражнения. Для его удаления используйте метод Delete объекта EventLog, как показано в следующем коде:
• VB
Public Shared Sub DeleteEventLogO
EventLog.Delete ("ChaplODemo")
End Sub
// C#
Public static void DeleteEventLogO
EventLog.Delete("ChaplODemo');
}
Занятие 1
Регистрация событий 47-|
Рис. 10-2. Консоль Просмотр событий Windows после запуска приложения ChaplODemo
Использовать данный метод следует с осторожностью, чтобы случайно не удалить журнал с ценной информацией!
Запись в журнал событий
Теперь нам пригодится написанный ранее код для создания журнала. Чтобы получить поддержку записи в журнал, достаточно слегка изменить показанный выше пример кода: ’ VB
Public Shared Sub CreateEventLogO
Dim DemoLog As New EventLog("ChaplODemo”)
DemoLog Source = "ChaplODemo"
DemoLog.WriteEntry("CreateEventLog called",
EventLogEntryType.Information)
End Sub
// C#
public static void CreateEventLogO
{
EventLog DemoLog = new EventLog("ChaplODemo");
DemoLog.Source = "ChaplODemo";
DemoLog.WriteEntryC'CreateEventLog called”, EventLogEntryType.Information);
}
В данном примере метод Write Entry кажется простым, но у него целых 10 перегруженных версий. Как обычно, в этом случае можно использовать конструктор с минимальным числом параметров, после чего задать все необходимые свойства объекта. Альтернатива — задать все параметры в конструкторе. Хотя настройка максимума парамет
472 Инструментарий для мониторинга
Глава 10
ров в перегруженном конструкторе считается более элегантным и простым подходом, существуют ситуации, в которых он не годится, например в случаях, когда известны не все значения, подлежащие регистрации.
Чтобы разобраться, как это работает, посмотрим на перегруженные конструкторы в действии:
' VB
Public Shared Sub CreateEventLogO
Dim DemoLog As New EventLog("Chap10Demo")
DemoLog.Source = "ChaplODemo"
DemoLog.WriteEntry("CreateEventLog called", EventLogEntryType.Information) End Sub
// C#
public static void CreateEventLogO
{
EventLog DemoLog = new EventLog("ChaplODemo');
DemoLog.Source = "ChaplODemo";
DemoLog WriteEntry( CreateEventLog called", EventLogEntryType.Information);, }
Следующий пример иллюстрирует использование перегруженного конструктора для добавления идентификатора события:
' VB
Public Shared Sub CreateEventLogO
Dim DemoLog As New EventLog("Security")
DemoLog.Source = "ChaplODemo"
DemoLog.WriteEntry("CreateEventLog called", EventLogEntryType Information, 100)
End Sub
// C#
public static void CreateEventLogO
{
EventLog DemoLog = new EventLog("Security");
DemoLog.Source = "ChaplODemo";
DemoLog.WriteEntry("CreateEventLog called", EventLogEntryType.Information, 100);
}
Помимо чтения пользовательских журналов, объект EventLog позволяет читать и записывать системные журналы событий, а именно журналы Приложение (Application), Безопасность (Security) и Система (System). Можно создать отдельный журнал для своего приложения либо воспользоваться системными журналами. Рассмотрим второй случай на примере Web-приложения ASP.NET, применяемого для аутентификации пользователей. Допустим далее, что имеется код для распознавания атак «впрыскиванием» SQL-кода. У разработчиков мало шансов противостоять таким атакам (они могут лишь уповать на то, что код их предотвратит), для администраторов же важно знать о попытках
Занятие 1
Регистрация событий 473
атак такого рода, даже успешно отраженных. Так что имеет смысл регистрировать связанные события в системном журнале Безопасность для полследующего анализа.
ВНИМАНИЕ! Атаки впрыскиванием SQL-кода
Такие атаки являются одной из разновидностей взлома через приложения, доверяющие вводимым пользователями данным. Атакующий при помощи спецсимволов вставляет во вводимые строки команды SQL. В зависимости от имеющихся прав, злоумышленник может получить полный контроль над базой данных либо вовсе уничтожить ее.
Ниже приведены примеры записи в системные журналы. Следует помнить, что для записи в эти журналы необходимы соответствующие разрешения.
	Показанный ниже код предназначен для записи в журнал Приложение'.
' VB
Public Shared Sub WriteToApplicationEventLogO
Dim DemoLog As New EventLog("Application")
DemoLog.Source = "DemoApp"
DemoLog.WriteEntry("Written to Application Log",
EventLogEntryType.Information)
End Sub
// C#
public static void WriteToApplicationEventLogO
{
EventLog DemoLog = new EventLog("Application");
DemoLog.Source = "DemoApp";
DemoLog.WriteEntry("Written to Application Log", EventLogEntryType.Information);
}
	А этот код осуществляет записи в журнал Безопасность'.
' VB
Public Shared Sub WriteToSecurityEventLogO
Dim DemoLog As New EventLog("Security")
DemoLog.Source = "DemoApp"
DemoLog.WriteEntry("A Sql Injection Attack just occurred from
IP Address 100.000.00.0", EventLogEntryType.Information) End Sub
// C#
public static void WriteToSecurityEventLogO
{
EventLog DemoLog = new EventLog("Security");
DemoLog.Source = "DemoApp";
DemoLog WriteEntryC'A Sql Injection Attack just occurred from
IP Address 100.000.00.0", EventLogEntryType.Warning);
}
474 Инструментарий для мониторинга
Глава 10
	Следующий код предназначен для записи в журнал Система:
' VB
Public Shared Sub WriteToSystemEventLogO
Dim DemoLog As New EventLog("System")
DemoLog.Source = "DemoApp
DemoLog.WriteEntry("A DemoService Restarted due to reboot",
EventLogEntryType.Information)
End Sub
// C#
public static void WriteToSystemEventLogO
EventLog DemoLog = new EventLog("System");
DemoLog.Source = "DemoApp";
DemoLog.WriteEntry("A DemoService Restarted due to reboot", EventLogEntryType.Information);
}
Чтение из журнала событий
Итак, вы создали журнал событий и записали в него некоторую информацию. У объекта EventLog есть свойство Entries — экземпляр типа EventLogEntryCollection, хранящего объекты EventLogEntry. Создав экземпляр класса EventLog можно без труда перебирать записи журнала в цикле:
' VB
Public Shared Sub ReadEventLogO
Dim DemoLog As New EventLogO
DemoLog.Log = 'ChaplODemo"
For Each DemoEntry As EventLogEntry In DemoLog.Entries
Console WriteLine(DemoEntry.Source + ": " + DemoEntry,Message)
Next
End Sub
// C#
public static void ReadEventLogO
EventLog DemoLog = new EventLogO;
DemoLog.Log = "ChaplODemo";
foreach (EventLogEntry DemoEntry in DemoLog Entries)
Console.Write!ine(DemoEntry.Source +	+ DemoEntry.Message);
}
}
Результат выполнения предыдущего кода показан на рис. 10-3.
Занятие 1
Регистрация событий 475
Рис. 10-3. Вывод метода ReadEventLog
А теперь расскажем об очистке журнала простейшим способом. Чтобы очистить журнал, достаточно вызвать метод Clear экземпляра EventLog'.
' VB
Public Shared Sub ClearEventLog()
Dim LogDemo As New EventLog("ChaplODemo")
LogDemo.Source = "DemoApp"
LogDemo.Clear()
End Sub .•*
// C#
public static void ClearEventLog()
{
EventLog LogDemo = new EventLog("ChaplODemo");
LogDemo.Source = ’DemoApp";
ogDemo.Clear();
}
Если воспользоваться методом ReadEventLog (см. предыдущий раздел) после вызова ClearEventLog, можно убедиться, что в журнале не останется ни одной записи. Если же записи сохранились (кроме тех, что были добавлены позже другим кодом), что-то пошло не так.
Практикум. Создание и использование журнала событий в приложении
Ваша задача — создать пользовательский журнал событий и записать в него данные. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
1.	Откройте Visual Studio 2005.
2.	Создайте консольное приложение в Microsoft Visual C# 2005 или Microsoft Visual Basic 2005 (в Visual Basic 2005 будет создано модуль с пустой функцией Main, в Visual C# — класс Program.cs, также с пустой функцией Main).
3.	Убедитесь, что в проекте есть ссылка хотя бы на пространство имен System.
476 Инструментарий для мониторинга
Глава 10
4.	Импортируйте пространство имен System.Diagnostics (оператором using в C# или Imports в Visual Basic), чтобы в дальнейшем не указывать его при объявлении объектов.
5.	Обратите внимание, что в следующем примере вместо «CuckooLaptop» необходимо подставить хост-имя своего компьютера.
' VB
If Not EventLog.SourceExists("ChaplODemo", "CuckooLaptop") Then EventLog.CreateEventSource("ChaplODemo", "Application", "CuckooLaptop")
End If
Dim LogDemo As New EventLog("Application", "CuckooLaptop", "ChaplODemo") LogDemo.WriteEntry("Event Written to Application Log", EventLogEntryType.Information, 234, CType(3, Int16))
// C#
if (!EventLog.SourceExists("ChaplODemo", "CuckooLaptop"))
{
EventLog.CreateEventSource("ChaplODemo", "Application", "CuckooLaptop");
}
EventLog LogDemo = new EventLogC Application","CuckooLaptop", "ChaplODemo");
LogDemo.WriteEntry("Event Written to Application Log", EventLogEntryType.Information, 234, Convert,ToInt16(3));
6.	Сохраните и запустите полученное приложение. Убедитесь, что приложение добавляет записи журнал ChaplODemo.
7.	Для записи в журнал событий на удаленной машине просто подставьте ее имя вместо имени локального компьютера (потребуются привилегии для записи в журнал на удаленном компьютере).
Резюме
	Механизм журналов событий Windows — удобное средство для регистрации сведений, потенциально полезных для системных администраторов и пользователей.
	Существует множество способов регистрации информации, из них журналы событий позволяют сделать это максимально просто, с использованием объектно-ориентированного кода.
	Свойство Source объекта EventLog задает источника информации.
	Свойство EventLogEntryType задает тип добавляемой записи.
	Ключевую роль во взаимодействии с системными журналами событий является класс EventLog из пространства имен System.Diagnostics.
	Хотя работать с классом EventLog довольно просто, его использование заметно увеличивает расход системных ресурсов, поэтому следует применять его лишь при необходимости.
	При использовании объектов EventLog вероятность появления уязвимостей защиты возрастает. Поэтому не применяйте EventLog в частично доверяемых средах и не передавайте им эти объекты.
	Метод Clear служит для удаления всех записей из журнала событий.
	Свойство Message объекта EventLogEntry предназначено для считывания информации, записанной в объект EventLog.
Занятие 1
Регистрация событий 477
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Необходимо записать информацию о состоянии приложения. Это приложение запущено в полностью доверенной среде (Full Trust), но будет вызывать частично доверяемые сборки. Как лучше всего решить эту задачу?
А. По необходимости использовать класс EventLog.
В. Использовать класс EventLog в сборках, которые будут взаимодействовать с частично доверяемыми сборками.
С. Избегать использования класса EventLog, поскольку риск взлома защиты слишком велик.
D. Использовать объекты EventLog, если они необходимы данному приложению; если они используются частично доверяемыми объектами, создать отдельный журнал из соображений безопасности.
2.	Что нужно учесть при работе с объектами EventLog*! (Укажите все верные ответы.) А. Они могут переполниться, поэтому записывать в них нужно с осторожностью. В. Их следует избегать в частично доверяемых средах.
С. Они — потенциальные «пожиратели ресурсов», поэтому их следует использовать благоразумно.
D. Они представляют собой один из наиболее безопасных механизмов ввода-вывода, поэтому рекомендуется пользоваться или при каждой возможности.
3.	Какой из методов объекта EventLog следует использовать для очистки EventLog*!
A. Clear (для отдельных записей журнала).
В. Remove Entries вместе с Clear.
С. Clear.
D. ClearAll.
4.	Какой метод класса EventLog следует использовать для удаления объекта EventLog*!
A. ClearLog.
В. Remove Log.
С. Delete.
D. DeleteLog.
5.	Какого типа сообщения могут быть записаны в журнал событий? (Укажите все верные ответы.)
A. Error.
В. Warning.
С. Information.
D. SuccessAudit.
6.	Какие журналы по умолчанию доступны в Windows? (Укажите все верные ответы.) А. Приложение (Application).
В. Безопасность (Security).
С. Системса (System).
D. Аудит (Audit).
478 Инструментарий для мониторинга
Глава 10
Занятие 2. Отладка и трассировка
Наблюдая за знакомыми разработчиками, я заметил интересную вещь: довольно сложно застать их за написанием кода, так как большую часть времени они тратят на его отладку. Отладка, безусловно, является залогом качества кода, а совершенные навыки отладки повышают не только качество кода, но и производительность труда разработчика.
Изучив материал этого занятия, вы сможете:
J записывать вывод, генерируемый кодом;
J производить отладку атрибутов;
J создавать объекты-слушатели трассировки;
J конфигурировать слушатели.
Продолжительность занятия — 20 минут.
Запись вывода, генерируемого кодом
Как сказано выше, важный аспект труда разработчика — трассировка и поиск ♦багов». Умение эффективно использовать соответствующие инструменты крайне необходимо для решения следующих задач:
	проверка корректности работы кода;	(
	использование классов Debug и Debugger для генерации сведений о корректной (или некорректной) работе кода; при трассировке (пошаговом исполнении) кода эти классы экономят много времени;
	выявление ♦багов» после развертывания приложения.
Оба базовых класса {Debugger и Debug), предназначенных для отладки, входят в пространство имен System.Diagnostics.
Класс Debugger
Класс Debugger, обеспечивающий связь с отладчиком, довольно прост. Хотя у него множество членов, лишь некоторые из них важны для разработчиков (см. табл. 10-1).
Табл. 10-1. Члены класса Debugger
Имя	Описание
Break	Сигнализирует отладчику о прерывании. Идеален для остановки
	приложения по условию
IsAttached	Показывает подключен ли отладчик к процессу
IsLogging	Показывает, ведет ли Debugger журнал в данный момент
Launch	Запускает отладчик и подключает его к процессу
Log	Асинхронно отправляет сообщение текущему отладчику
Метод Break — программный эквивалент точки прерывания, установленной вручную. Предположим, имеется метод RetumMessage, возвращающий значение типа String. Далее, возврат значения null или nothing означает возникновение ошибки, и в этом случае нужно остановить выполнение и вызвать отладчик. Доступные варианты — установ
Занятие 2
Отладка и трассировка 47g
ка точки прерывания вручную или с помощью метода Break. По функциональности оба метода одинаковы. Следующий код показывает, как работать с методом Break (предполагается, что метод ReturnMessageO уже объявлен): ' VB
Dim MyMessage As String = ReturnMessageO
If MyMessage Is Nothing Then Debugger.Break() End If
// C#
String MyMessage = ReturnMessageO; if (MyMessage == null) {
Debugger.Break();
}
При этом результат использования метода Break будет сходен с С#-реализацией, показанной на рис. 10-4.
Также важен метод Log класса Debugger. Проще говоря, метод Log асинхронно отправляет информацию подключенному объекту Debugger, если таковой существует. Если же объекта Debugger нет, ничего не происходит.
using System;
using System.Collections.Generic; using System.Text; using System.Diagnostics; namespace Debugging < class Program < static void Haln(string[] args) < String MyMessage “ ReturnMessageO; it (MyMessage ““ null) < feebugg-'r. Break () ; ) )
Рис. 10-4. Вызов отладчика CLR в Visual Studio .NET 2005 с помощью метода . Debugger.Break
ПРИМЕЧАНИЕ Особенности окончательных версий сборок
Чтобы классы Debugger и Debug работали, сборку следует компилировать в отладочном режиме (Debug). Если же сборка скомпилирована как окончательная версия (Release), исполняющая среда .NET будет игнорировать операторы Debug or Debugger.
Вывод метода Log направляется в любой из объектов Listener, подключенных к отладчику. Объекты-слушатели рассматриваются ниже, в пока важно, что метод Log передает вывод слушателю. Для вызова метода Log достаточно задать уровень, категорию и сообщение. Для начала рассмотрим простой объект-слушатель — DefaultTraceListener. Данный слушатель получает при подключении данные от методов Log и записывает их как текст в заданный каталог. Ради простоты сделаем приемником данных окно Output. Добавим объект-слушатель и вызовем метод Log следующим образом:
480 Инструментарий для мониторинга
Глава 10
’ VB
Trace.Listeners.Clear()
Dim MyListener As New DefaultTraceListener
Trace.Listeners.Add(MyListener)
Debugger.Log(1, "Test", "This is a test")
Console. ReadLineO
// C#
Trace.Listeners.Clear();
DefaultTraceListener MyListener = new DefaultTraceListener();
Trace.Listeners.Add(MyListener);
Debugger.Log(2, "Test", "This is a test");
Console. ReadLineO;
Здесь выполняется очистка существующих объектов Listener из набора Listeners, добавляется объект DefaultTraceListener, который затем получает вывод трассировки. У DefaultTraceListener есть несколько перегруженных версий, позволяющих столь же легко направлять вывод, например, в текстовый файл. Если код написан без ошибок, в окне Output выводится текст «This is a test».
ПРИМЕЧАНИЕ Длина параметра Category
Длина параметра Category ограничена 256 символами, более длинные сообщения усекаются.
Класс Debug
Хотя класс Debugger очень удобен, у него всего два метода: Break и Log. Не стоит их недооценивать, но в некоторых ситуациях желателен более тонкий контроль над отладкой. Для таких случаев и предназначен класс Debug. В табл. 10-2 приведены наиболее часто используемые методы класса Debug.
Табл. 10-2.	Методы класса Debug
Имя	Описание
Assert Close	Проверяет условия и выводит сообщение, если проверка дала False Сбрасывает буфер вывода и вызывает метод Close для каждого из подключенных слушателей
Fail Flush	Выводит сообщение об ошибке Сбрасывает буфер вывода и записывает содержимое буфера в набор Listeners
Indent	Увеличивает отступ текста сообщения в журнале, особенно полезен для форматирования журналов
Print	Записывает сообщение плюс символ конца строки в слушатели из набора Listeners
Unindent	Метод, противоположный Indent. Уменьшает отступ сообщения в журнале на единицу
Write	Записывает информацию о связанных с классом Debug и Trace объектах-слушателях в набор Listeners
Занятие 2
Отладка и трассировка	4g j
Табл. 10-2. (окончание)
Имя	Описание
Writelf	Если заданное условие выполняется, производит те же действия, что и метод Write
WriteLine	Записывает информацию о связанных с классом Debug или Trace объектах-слушателях в набор Listeners
WriteLinelf	Если заданное условие выполняется, делает то же, что и WriteLine
Методы Indent и Unindent важны, но упоминаются здесь очень кратко, так как они предназначены исключительно для форматирования текста в журнале.
Начнем с метода Assert, наверное, одного из важнейших методов класса Debug.
Пример из практики
Уильям Райан (William Ryan)
Выступая на .NET User Groups и других мероприятиях, проводимых Microsoft, я отмечаю важные для профессиональной карьеры средства, среди которых далеко не последнее место занимает метод Assert. Проще говоря, проверка методом Assert просто необходима для написания хорошего кода. Даже «хороший» код можно значительно улучшить, интенсивно используя Assert. С этим утверждением согласны многие авторы, включая наиболее выдающихся мировых экспертов по отладке, Джона Роббинса (John Robbins) и Пола Киммела (Paul Kimmel).
К СВЕДЕНИЮ Метод Assert
Детальную информацию об этих двух экспертах можно прочитать в Debugging Applications for Microsoft .NET and Microsoft Windows Джона Роббинса (John Robbins) (Microsoft Press, 2001) и Visual Basic .NET Power Coding Пола Киммела (Paul Kimmel) (Addison-Wesley Professional, 2003).
Метод Assert
Как сказано выше, одно из преимуществ отладочных классов в автоматизации пошагового исполнения кода и проверки вывода. Конечно, порой ручная трассировка неизбежна, но для громоздких корпоративных приложений выполнить ее просто нереально. Как, скажите, вручную отладить 500 000 строк кода за ограниченное время? Одно из лучших средств экономии времени — это метод Assert. Всегда добавляйте к коду методы Debug.Assert, чтобы определить корректность работы кода путем проверки некоторых условий. Если проверка дает False, автоматически вызывается отладчик.
Главное достоинство данного способа — в возможности определить, действительно ли успешная проверка условия говорит о корректности работы кода. Часто разработчики вводят код в системы управления исходными текстами преждевременно, нарушая работу другого кода. При правильном использовании Assert сообщения об ошибке можно увидеть сразу же после запуска приложения, и устранить их «по горячим следам».
Допустим, вы работаете с объектом IConfigurationSectionHandler, и кто-то удалил соответствующую запись из конфигурационного файла. Без проверки с помощью Debug можно потратить полчаса на поиски этого объекта, прежде чем понять, что он удален. Вызов же Assert сразу укажет на суть проблемы. Кроме того, сообщения метода доволь-
482 Инструментарий для мониторинга	Глава 10
но подробны, что ускоряет поиск ошибок. К тому же при сборке окончаэельных версий приложений вызовы Assert, как и методов Debug, игнорируются, поэтому производительность приложения в итоге не снижается. Так что данный подход обеспечивает эффективную отладку без снижения производительности. Предположим, имеется следующий файл конфигурации, из которого непонятным образом удалены нужные настройки: <appSettings>
<add key="Foo" value="Hello World!"/>
Odd key="ApplicationName" value="Demo Application"/>
Odd key="ApplicationVersion” value="2.0.0.1'7>
Odd key=”llser FirstName" value="John"/>
Odd key="llserLastName" value="Public"/>
</appSettings>
С помощью следующего кода (требующего вызова пространств имен System.Configuration и System.Diagnostics) можно моментально определить, не удален ли случайно параметр Foo:
’ VB
Dim MySettings = ConfigurationManager.AppSettings
Debug.Assert(MySettings <> nothing AndAlso MySettings[”Foo”] <> nothing ,
"There was a problem with either the configuration file or the Foo Setting") Console.WriteLine(MySettings("Foo”))
// C#
NameValueCollection MySettings = ConfigurationManager.AppSettings;
Debug.Assert(MySettings != null && MySettings["Foo"] != null,
"There was a problem with either the configuration file or the Foo Setting");
Console.WriteLine(MySettings["Foo"]);
ПРИМЕЧАНИЕ .NET 2.0
Класс ConfigurationManager — новинка .NET 2.0, призванная заменить усгаревший класс Configurationsettings.
Если значение Foo определено, исполнение продолжится обычным порядком, еслу же параметр удален либо не определен, появится сообщение (см. рис. 10-5).
Assertion Failed Abort Quit, Retry :Debug, Ignore Continue
There was a problem ftn either th conf J -otlon fie or the Foo Setting
at Program.ShowAssertionO C ®cts\DotNet\D<'bugg:7ig\Oebugging\Frogram.cs(29)
at Program.Main(StringO argsj GV>r< it ct: tD itNettPebwBbuggm<iiProgrcan.cs(,5)
at AppDomain.nExecuteAssembiy(Assembty assembly 5tn igt] args)
□t AppDomain.ExecuteAssembly(String issemblyFt.’ rldence ussembl, Security, String^ ergs)
at HTSfroc.RunUsersAssemulyC
at Th tauHolper. ThreadstartJlortextfObject state)
at ExecutionContext “ x'_—-utijnConte' executioncontext, Context Callback cafcadg Object state) at Tnreadrielper.ThreadStartO
ghort j Retry 11; jgriore
Рис. 10-5. Сообщение о неудачной проверке
Занятие 2
Отладка и трассировка Дц^
Метод Fail похож на Assert. Он просто прерывает Debugger на строке кода, в которой есть ошибка, и выводит соответствующее сообщение. Метод Fail также не проверяет условия. Ниже показано, как работать с методом Fail'.
' VB
Dim MySettings = ConfigurationManager.AppSettings
If MySettingsC’Foo") Is Nothing Then
Debug.Fail(”Configuration Setting 'Foo' is Missing") End If
// C#
NameValueCollection MySettings
=Configu rationManager.AppSettings;
if(MySettings[”Foo"] == null)
Debug Fail("Configuration Setting 'Foo' is Missing");
Если секция Foo или значение этого параметра не определено, С#-версия примера выводит окно, показанное на рис. 10-6.
Рис. 10-6. Вывод метода Debug.Fail
В любом случае это окно можно закрыть кнопкой:
 Abort
— прерывает исполнение программы;
 Retry
— попытка повторного выполнения блока кода (если состояние программы не изменилось за это время, результат останется прежним);
 Ignore
— игнорирует ошибку и пытается продолжить исполнение программы.
Далее рассмотрим методы Write, Writelf, WriteLine и WriteLinelf. Данные методы очень похожи, и поэтому рассматриваются вместе.
Метод Write
Для использования метода Write достаточно вызвать его после отправки окну Output любого сообщения:
’ VB
Debug.Write("WriteStatements() was reached")
484 Инструментарий для мониторинга
Глава 10
// C#
Debug. Write( "WriteStatementsO was reached");
Данный код отправляет окну Output сообщение «WriteStatementsO was reached».
Метод Writelf
Метод Writelf работает аналогично методу Write, но записывает вывод только при выполнении некого условия.
’ VB
Dim s As String = Nothing
Debug.Writelf(s Is Nothing, "Variable [s] is null")
// C#
String s = null;
Debug.Writelf(s == null, "Variable [s] is null");
Данный код отправляет в окно Output сообщение «Variable [s] is null», если переменная действительно содержит null.
Методы WriteLine и WriteLinelf аналогичны предыдущим, только дописывают к Сообщению разделитель строк. Соответствующие методы отличаются только форматом.
Метод WriteLine
Для использования метода WriteLine достаточно вызвать его после отправки в окно Output любого сообщения: ’ VB
Debug.WriteLine("WriteStatementsO was reached")
// C#
Debug. WriteLine( "WriteStatementsO was reached");
Данный код отправляет в окно Output сообщение «WriteStatementsO was reached».
Метод WriteLinelf
Метод WriteLinelf работает аналогично методу WriteLine, но записывает вывод только при выполнении некого условия: ‘ VB
Dim s As String = Nothing
Debug.WriteLinelf(s Is Nothing, "Variable [s] is null")
// C#
String s = null;
Debug.WriteLineIf(s == null, "Variable [s] is null");
Данный код отправляет в окно Output сообщение «Variable [s] is null», если переменная действительно равна null.
Метод Print
Метод Print отличается тем, что записывает вывод только в подключенные объекты-слушатели.	.
Занятие 2
Отладка и трассировка 435
• VB
Dim s As String = Nothing
Debug.Print(s Is Nothing, "Variable [s] is null")
// Ot
String s = null;
Debug.Print(s == null, "Variable [s] is null");
Данный код отправляет сообщение «Variable [s] is null» всем подключенным объектам-слушателям.
Метод Flush
Метод Flush сбрасывает выводимые данные в подключенные объекты Listener. Он не имеет параметров:
VB
Debug.Flush()
// at
Debug.Flush();
После вызова Flush содержимое буфера сбрасывается во все подключенные объекты Listener.
Отладочные атрибуты
Отладочные атрибуты .NET Framework позволяют декларативно настраивать работу приложений. Атрибуты часто используются в .NET Framework, но особенно полезны они при отладке. Обычно, чтобы задать состояние объекта, его нужно создать и определить его свойства. Но как быть, если точно известно, что данный метод корректно работает, и заходить в него при отладке бессмысленно? Работая с традиционными (императивными) методами пришлось бы явно приказать отладчику пропустить этот метод. Нерационально, не так ли? Это все равно что обзвонить всех знакомых, лишь чтобы попросить их не звонить сегодня, так как вы заняты... Не проще ли отключить телефон на время? Атрибут позволяет описать известные особенности работы компонента и указать другим компонентам, как реагировать на те или иные действия объекта, которому этот атрибут принадлежит. Благодаря этому атрибуты особенно удобны для отладки сценариев.
В данном разделе рассматриваются следующие атрибуты:
	DebuggerBrow sable Attribute;
	Debugger Display Attribute;
	Debugger Hidden Attribute;
	DebuggerNonUserCodeAttribute;
	DebuggerStepperBoundaryAttribute;
	DebuggerStepThroughAttribute;
	DebuggerTypeProxyAttribute;
	DebuggerVisualizerAttribute.	•
Начнем с базового класса, на который можно ссылаться до и после добавления вышеупомянутых атрибутов. Создадим простой класс SoftwareCompany с тремя открытыми свойствами (CompanyName, CompanyCity и CompanyState) и тремя закрытыми переменными (—CompanyName, companyCity и -CompanyState). Воспользуемся перегруженным конструктором, позволяющим передавать любое из этих трех значений:
Инструментарий для мониторинга
Глава 10
' VB
Public Class Softwarecompany
Private _companyName As String
Private _companyCity As String
Private _companyState As String
Public Property CompanyNameO As String Get
Return Me._companyName
End Get
Set(ByVal value As String)
Me..companyName = value
End Set
End Property
Public Property CompanyCity() As String
Get
Return Me._companyCity
End Get
Set(ByVal value As String)
Me._companyCity = value	t
End Set
End Property
't-
Public Property CompanyStateO As String Get
Return Me._companyState
End Get
Set(ByVal value As String)
Me..companystate = value
End Set
End Property
Public Sub New(ByVal companyName As String, ByVai companyCity As String, ByVai companystate As String)
End Sub
End Class
// C#
class Softwarecompany {
private String _companyName;
private String .companystate;	м
Занятие 2
Отладка и трассировка 437
private String „companyCity;
public String CompanyName {
get { return _companyName; }
set { .companyName = value; } }
public String Companystate {
get { return _companyState; }
set { _companyState = value; } }
public String CompanyCity {
get { return „companyCity; } set { „companyCity = value; }
public SoftwareCompany(String companyName, String companystate, String companyCity) {
this.„companyCity = companyCity;
this.„companyName = companyName, this.„companystate = companystate;
}
Теперь перейдем к методу main и вставим в него следующий код:
• VB	*
Dim Company As New SoftwareCompany("A Datum", "Miami”, "Florida")
// C#
Softwarecompany Company
= new SoftwareCompanyC’A Datum", "Florida", "Miami");
Установите точку прерывания на этой строке, запустите программу и после ее завершения изучите окно Locals. Результат должен выглядеть примерно так, как показано на рис. 10-7.
* args {атегнюгв:[0]}-
| F— _companyOty.	TMami"
| ► -	", "A Datum"
I Ь— Л jcompanyState	. ] "Florida"
I J CompanyCity __	. ^em(.
If— ** CompanyName	, "A Datum"
j *— J CompanyState __	["Florida"
Рис. 10-7. Окно Locals и класс SoftwareCompany
488 Инструментарий для мониторинга
Глава 10
DebuggerBrowsableAttribute
Обратите внимание: отображаются как закрытые члены (^companyName, _companyCity и companyState) так и открытые {CompanyName, CompanyCity и Company State). Вернемся к закрытым членам класса, добавим DebuggerBrowsableAttribute и установим DebuggerBrowsableState в never. Код со ссылкой на пространство имен Sytems.Diagnostics должен выглядеть примерно так, как показано ниже.
ВНИМАНИЕ! Visual Basic не поддерживает атрибут DebuggerBrowsable
Следующий пример на Visual Basic представлен только для единообразия. Дело в том, что Visual Basic не поддерживает данный атрибут, но не реагирует на его использование исключением или сбоями. Visual Basic поддерживает атрибут DebuggerDisplayAttribute, сходный с DebuggerBrowsable.
’ VB
<Debugge rB rowsable(Debugge rB rowsableState.Neve r)>
Private _companyName As String
<Debugge rB rowsable(Debugge rB rowsableState.Neve r)>
Private _companyCity As String
<DebuggerBrowsable(DebuggerBrowsableState.Never)>
Private _companyState As String
// C#
[Debugge гВ rowsable(Debugge rB rowsableState.Neve r)] private String _companyName;
[Debugge rB rowsable(Debugge rB rowsableState.Neve r)] private String _companyState;
[DebuggerBrowsable(DebuggersrowsableState.Neve г)]
private String _companyCity;
Снова запустите программу и изучите вывод в окне Locals. Должно отображаться нечто похожее на рис. 10-8.
Рис. 10-8. Окно Locals класса SoftwareCompany с заданными атрибутами DebuggerBrowsable
Если все сделано правильно, закрытые переменные (_companyName, _сотрапуState, and _companyCity) не отображаются. В данном примере используется параметр Never перечислимого типа DebuggerBrowsableState, но есть еще два параметра (см. табл. 10-3).
Табл. 10-3. Перечислимый тип DebuggerBrowsableState
Имя	Описание
Never	Указывает, что элемент всегда невидим
Collapsed RootHidden	Отображает элемент в свернутом состоянии Скрывает корневой элемент, но отображает дочерние элементы, если они входят в набор или массив
Занятие 2
Отладка и трассировка 4gg
DebuggerDisplayAttribute
Чтобы вернуться на исходную позицию, запустите класс SoftwareCompany, установив точку прерывания сразу после конструктора. Кода предыдущего примера сгенерирует вывод, подобный показанному на рис. 10-9 (приводится пример для С#-кода, вывод в Visual Basic немного отличается).
' Mine	Value
’ф args	{Dimensiora:[01}
i. Щ 9 Company	{Debugging..SoftwareCompany^
Рис. 10-9. Окно Locals и класс SoftwareCompany
В узле Company должен быть объект Debugging.SoftwareCompany. Доработайте код, добавив DebuggerDisplayAttribute до определения Class, а также следующие параметры: ’ VB
<DebuggerDisplay("CompanyName = {..CompanyName}, Companystate = {_companyState}, CompanyCity{_companyCity}")>
Public Class SoftwareCompany
End Class
// C#
[DebuggerDisplay("CompanyName = {_companyName}, Companystate = {_companyState}, CompanyCity{_companyCity}")]
class SoftwareCompany
Здесь определяется атрибут с литералом «CompanyName» и закрытая переменная _companyName, далее выводится литерал «CompanyCity» и значение _companyCity. Результат выполнения программы показан на рис. 10-10 (приводится пример для С#-кода, вывод в Visual Basic немного отличается).
args
S 9 Company
Value
.Dime-islons^O]}
^CompanyName - "A Datum", CompanyState - ТНогИа", СотрапуСкуХатГ
Рис. 10-10. Окно Locals для класса SoftwareCompany, помеченного атрибутами
Debugger Display
Итак, мы лишь расставили ссылки на свойства класса SoftwareCompany. Так можно работать с любыми другими свойствами. Например, вместо companyState можно ссылаться на свойство Length члена -CompanyState. Допустим, что имеется свойство Collection класса SoftwareCompany с именем PhoneNumbers. Доступ к нулевому элементу можно получить так:
' VB
<DebuggerDisplay("PhoneNumber = {PhoneNumbers(O)}, CompanyState = {_companyState}, CompanyCity{_companyCity}")>
Public Class SoftwareCompany End Class
// C#
[DebuggerDisplay("PhoneNumber = {PhoneNumbers[0]>. CompanyState = {..companyState},
490 Инструментарий для мониторинга
Глава 10
CompanyCity{_companyCity}")] class Softwarecompany
{.. Г }
DebuggerHiddenAttribute
Класс DebuggerHiddenAttribute запрещает вставку точек прерывания в код объекта, на который он действует. При вызове класса, метода или свойства с данным атрибутом имеющиеся в них точки прерывания просто игнорируются. Все попытки установить точку прерывания в объекте с таким атрибутом также игнорируются.
DebuggerNonUserCodeAttribute
Данный атрибут похож на DebuggerHiddenAttribute. В основном он применяется в коде, сгенерированном дизайнером, а сами разработчики пользуются им редко. Любой код, помеченный этим атрибутом, и связанные с ним типы не отображаются и игнорируются отладчиком.
DebuggerStepperBoundaryAttribute
Данный атрибут используется в коде, расположенном в секции DebuggerNonUserCodeAttribute. Отладчик остановится, впервые встретив данный атрибут, а, начиная со следующей секции пользовательского кода, продолжит следовать инструкциям, заданным в атрибуте DebuggerNonUserCodeAttribute. Этот атрибут потребуется тем, кто будет работать со свойствами и методами, генерируемыми дизайнером. Он позволяет видеть лишь эти секции кода.
DebuggerStepThroughAttribute
Данный атрибут, как и два предыдущих, приказывает отладчику {Debugger) игнорировать и не отображать в своих окнах код, помеченный этим атрибутом. Он удобен для пропуска уже отлаженных блоков кода, где не осталось ни одной ошибки. Предположим, вы уверены, что класс, у которого инициализированы или установлены по умолчанию некоторые свойства, ни при каких обстоятельствах не будет генерировать исключения, и заданные значения не изменятся — такой класс можно пропустить при отладке. В предыдущих версиях .NET Framework данный атрибут добавлялся к методу InitializeComponent при создании форм. Это было удобно, так как пошаговое выполнение кода, инициализирующего многочисленные параметры, не имеет особого смысла, но отнимает много времени.
Три последних атрибута сходны по назначению. В отличие от DebuggerHiddenAttribute, этот метод приказывает отладчику игнорировать, а не скрывать код.
DebuggerTypeProxyAttribute
Этот атрибут позволяет переопределять способ отображения типов. Обычно данный атрибут используется для управления отображением внутренних типов. В следующем примере (использующем пространства имен System.Diagnostics и System.Collections) изменяется заданный по умолчанию способ отображения объекта ArrayList в отладчике:
' VB
<DebuggerTypeProxy("MyDemoObject")>
Public Class ArrayListDemo
Занятие 2
Отладка и трассировка 491
Inherits ArrayList
Private Shouldlgnore As String = "The Debugger won't see this"
Friend Class MyDemoObject
Dim jnyList As ArrayList
Private Shouldlgnore As String = "The Debugger WILL see this" Public Sub New(ByVal myList As ArrayList)
. Me. jnyList = myList End Sub
<DebuggerBrowsable(DebuggerBrowsableState.RootHidden)> Public Readonly Property AllValuesO As Object Get
Dim o() As Object = New ObjectO
For x As Int32 = 0 To Me. jnyList.Count o(x) = Me. jnyList(x)
Next Return о End Get End Property End Class End Class
// C#
[Debugge rTypeP roxy(typeof(MyDemoObj ect))] class ArrayListDemo : ArrayList {
public String Shouldlgnore = "The Debugger won't see this"; internal class MyDemoObject {
public String Shouldlgnore = "The Debugger WILL see this"; private ArrayList jnyList;
public MyDemoObject(ArrayList myList) {
this. jnyList = myList;
[Debugge rB rowsable(Debugge rB rowsableState.RootHidden)] public Object[] AllValues { get {
Object[] о = new Object[this. jnyList.Count];
for (Int32 x = 0;\x < this „myList.Count; x++) {
o[x] - this jnyList[x];

Глава 10
492 Инструментарий для мониторинга
} return о;
* }
}
}
}
}
Так удается указать, что вместо данных объекта ArrayList заданной по умолчанию требуется тип Proxy, в данном случае — MyDemoObject. Основная выгода применения данного атрибута — в возможности переопределить способы отображения типов, особенно внутренних.
DebuggerVisualizerAttribute
Визуализаторы (visualizers) — это новые инструменты Visual Studio 2005, позволяющие просматривать все свойства объектов, почти как с использованием IntelliSense (см. тему на странице http://msdn2.microsoft.com/en-us/library/e2zc529c.aspx.) Этот атрибут позволяет указать отладчику, что данному классу сопоставлен визуализатор.
ПРИМЕЧАНИЕ .NET 2.0
Класс DebuggerVisualizerAttribute — новинка .NET 2.0.
У данного атрибута есть 12 перегруженных конструкторов, для простейшего достаточно определить тип. Визуализатор, скажем MyDemoVisualizer, можно сопоставить с любым классом:
’ VB
<DebuggerVisualizer("MyDemoVisualizer")>
Public Class SoftwareCompany
End Class
// C#
[DebuggerVisualizer("MyDemoVisualizer")]
class SoftwareCompany
Создание слушателей трассировки
Пока основное внимание при рассмотрении отладки мы уделяли выводу. Но все показанные выше приемы бесполезны в отсутствие объектов-слушателей. По умолчанию доступны два основных класса инструментов мониторинга: Trace и Debug. Используются они практически одинаково, главное отличие заключается в их реализации. Trace реализован в сборках Release и Debug, тогда как Debug — только в сборке Debug. О классе Debug сказано уже достаточно, поэтому обратимся к классу Trace.
Класс Trace
Как и Debug, класс Trace применяют главным образом в связке с объектом-слушателем. В арсенале Visual Studio 2005 есть множество объектов-слушателей, включая XmlTraceListener, DefaultTraceListener, DelimmedListTraceListener и EventLogTraceListener. В табл. 10-4 перечислены наиболее важные методы данного класса.
Занятие 2
Отладка и трассировка 493
Табл. 10-4. Методы класса Trace
Имя	Описание
AutoFlush	Определяет или указывает, нужен ли автоматический сброс вывода после каждой операции записи
Close	Сбрасывает содержимое буфера вывода и закрывает все подключенные объекты-слушатели
Fail	Порождает сообщение об ошибке с заданной информацией
Flush	Сбрасывает содержимое буфера вывода и записывает его во все подключенные объекты-слушатели
Indent	Инкрементирует значение IndentLevel для вывода трассировки
IndentLevel	Получает или задает IndentLevel
IndentSize	Получает или задает число пробелов в Indent
Listeners	Получает набор слушателей, осуществляющих мониторинг вывода трассировки
Refresh	Обновляет трассировочную информацию
Traceinformation	Записывает уведомление во все подключенные к Trace объекты-слушатели
TraceWaming	Записывает предупреждение во все подключенные к Trace объекты-слушатели
Unindent	Декрементирует IndentLevel
UseGlobalLock	Позволяет узлать, следует ли выводить GlobalLock, при необходимости . задает его
Write	Записывает информацию о подключенных к Debug или Trace объектах Listener в набор Listeners
Writelf	Если заданное условие выполняется, делает то же, что и метод Write
WriteLine	Записывает информацию о связанных с классом Debug или Trace объектах Listener в набор Listeners
WriteLinelf	Если заданное условие выполняется, делает то же, что и метод WriteLine
Эти методы не требуют дополнительных пояснений либо идентичны рассмотренным ранее в разделе о классе Debug, поэтому мы перейдем к рассмотрению других классов.
Класс TraceSource
Класс TraceSource предоставляет механизм для трассировки исполнения кода и передачи трассировочных сообщений от источника. Для работы с TraceSource выполните следующие действия:
1.	Объявите экземпляр класса TraceSource.
2.	Задайте имя TraceSource.
3.	Вызовите соответствующие методы этого класса.
Следующий код иллюстрирует работу с TraceSource:
' VB
Dim DemoTrace As New TraceSource("DemoApp")
DemoTrace.Traceinformation("This is a Test")
494 Инструментарий для мониторинга
Глава 10
// C#
TraceSource DemoTrace = new TraceSource("DemoApp");
DemoTrace Traceinformation("This is a test");
Класс TraceSwitch
Класс TraceSwitch предназначен для динамического изменения поведения класса Trace. Например, можно «на лету» изменить способ регистрации классом Trace свойств TraceError, Trace Warning и Traceinfo. Также можно включить регистрацию полной информации, в котором регистрируются все параметры. Данный класс обеспечивает тонкую настройку трассировки. К примеру, он позволяет включать и отключать трассировку, а также изменять ее уровни. Объектами-переключателями, как и многими другими объектами из пространства имен System.Diagnostics, можно управлять программно либо с помощью конфигурационных файлов:
<configuration>
<system.diagnostics>
<switches>
Odd name="DemoApp" value="2" />
</switches>
</system.diagnostics>
</configuration>
’ VB
Dim DemoTrace As New TraceSource("DemoApp")
DemoTrace.Switch.ShouldTrace(TraceEventType.Information)
DemoTrace.Switch.Level = SourceLevels.Information
DemoTrace.Tracelnformation("This is a Test")
// C#
TraceSource DemoTrace = new TraceSource("DemoApp");
DemoT race.Switch.ShouldT race(T raceEventType.Information);
DemoTrace.Switch.Level = SourceLevels.Information;
DemoTrace.Tracelnformation("This is a test");
Объекты Listener
Классы Debug и Trace бесполезны без объектов-слушателей хотя бы потому, что выводимая ими информация ценна, только если ее можно анализировать. По умолчанию Visual Studio подключает объект-слушатель, который направляет вывод в окно Output. Каждый из классов Trace и Debug содержит набор Listeners, хранящий все подключаемые объекты-слушатели. С любым из этих объектов можно связать один или несколько объектов-слушателей (обычно так и поступают).
Класс DefaultTraceListener
Слушатели можно добавлять программно либо с помощью конфигурационного файла. Рассмотрим, как это делается. Для начала добавим DefaultTraceListener программно. DefaultTraceListener подключается к окну Output:
Занятие 2
Отладка и трассировка 495
1 VB
Trace.Listeners.С1еаг()
Trace. Listeners. Add (New DefaultTraceListenerQ)
Trace.WriteLine("This is a test”)
Console. ReadLineO
// C#
t race.Listeners.Clear();
DefaultTraceListener MyListener = new DefaultTraceListener();
Trace.Listeners.Add(MyListener);	5,
Trace.WriteLine("This is a test");
Тот же результат можно получить с помощью такой записи в файле конфигурации:
<configuration>
<system.diagnostics>
<trace autoflush="true" indentsize="5" />
</system.diagnostics>
</configuration>
Класс TextWriterTraceListener
Класс TextWriterTraceListener — один из простейших объектов-слушателей, направляющий вывод в текстовый файл или поток. Активизировать класс TextWriterTraceListener можно как программно, так и при помощи файла конфигурации. Следующий пример иллюстрирует программную активизацию данного класса:
’ VB	_•>
Trace.Listeners.С1еаг()
Тrace.Listeners.Add(New TextWrit^rTraceListener("C:\output.txt"))
Trace.AutoFlush = True
Trace.WriteLine("This is a test”)
// C#
Trace.Listeners.Clear();
TextWriterTraceListener MyListener = new TextWriterTraceListener(@"C:\output.txt");
T race.Listene rs.Add(MyListene r);
Trace.AutoFlush = true;
Trace.WriteLine("This is a test");
Того же можно добиться, добавив в конфигурационный файл такую запись:
<configuration>
<system.diagnostics>
ctrace autoflush="true" indentsize="5">
<listeners>
Odd name=”DemoListener" type="System. Diagnostics.TextWriterTraceListener" initializeData="output.txt" />
cremove name="Default" />
</listeners>
</trace>
496 Инструментарий для мониторинга
Глава 10
</system.diagnostics>
</configuration>
Хотя разные слушатели создаются вызовами разных конструкторов, принципы управления ими одинаковы.
Класс XmlWriterTraceListener
Класс XmlWriterTraceListener пересылает информацию из Debug или Trace объекту Text Writer или Stream.
ПРИМЕЧАНИЕ Именование выходных файлов класса XmlWriterTraceListener
Если имя файла для вывода не задано, то исполняющая среда .NET использует в качестве имени глобально уникальный идентификатор (GUID).
XmlWriterTraceListener очень похож на TextWriterTraceListener, и многие спросят, почему используют именно этот класс, а не TextWriterTraceListener. Дело в различных наборах атрибутов у этих классов (см. табл. 10-5).
ПРИМЕЧАНИЕ .NET 2.0
Класс ХМLWriterTraceListener — еще одна новинка в .NET 2.0
Табл. 10-5. Атрибуты и элементы класса XmlWriterTraceListener
Имя	Описание
CallStack	Отображает стек вызовов исполняющей среды, если установлена переменная CallStack в TraceOutputOptions
Computer	Имя компьютера, на котором запущено приложение
Correlation	Объект Correlation, который содержится в объекте CorrelationManager
Dataltem	Значение параметра data метода TraceData
Eventld	Идентификатор события, если он определен
Execution	Тип исполнения
Level	Любое целое от 0 до 255. Значения больше 255 ведут к исключению и присвоению Level значения 255
Message	Заданное текстовое сообщение
Source	Инициатор сообщения
TimeCreated	Текущее время по умолчанию
TimeStamp	Системное время машины, на которой запущено приложение (если установлена переменная Timestamp в TraceOutputOptions)
Tun	Тип объекта
За счет этих параметров (большинство из которых определено по умолчанию) функциональность этого класса несколько шире по сравнению с TextWriterTraceListener. Рассмотрим включение XmlTraceListener.
’ VB
Trace.Listeners.Clear()
Trace.Listeners.Add(New XmlWriterTraceListener("C:\output.xml"))
Trace.AutoFlush = True
Trace.WriteLine("This is a test")
Занятие 2
Отладка и трассировка 497
// C#
Trace.Listeners.С1еаг();
XmlWriterTraceListener MyListener = new XmlWriterTraceListener (@"C:\output.xml");
T race.Listene rs.Add(MyListene r);
Trace.AutoFlush = true;
Trace.WriteLine("This is a test");
To же самое можно сделать и так:
<?xml version="1.0" encoding="utf-8” ?>
<configuration>
<system.diagnostics>
<trace autoflush="true" indentsize="5">
<listeners>
<add name="DemoListener" type="System.Diagnostics.XmlWriterTraceListener" initializeData="output.xml" />
<remove name="Default" />
</listeners>
</trace>
</system.diagnostics>
</configuration>
Результат исполнения этого кода должен выглядеть примерно так, как показано на рис. 10-11.
Рис. 10-11. Вывод в output.xml
498 Инструментарий для мониторинга
Глава 10
Класс EventLogTraceListener
Предположим, что необходимо направить вывод в журнал событий, созданный в предыдущем занятии. Активизировать EventLogTraceListener можно программно либо через файла конфигурации. Следующий пример иллюстрирует первый способ: ' VB
Trace.Listeners.С1еаг()
Trace.Listeners.Add(New EventLogTraceListener("Chapter10"))
Trace.AutoFlush = True
Trace WriteLine("Something happened")
// C#
Trace.Listeners.Clear();
EventLogTraceListener MyListener = new EventLogTraceListener ("ChapterlO");
Trace.Listeners.Add(MyListener);
Trace.AutoFlush = true;
Trace.WriteLine("Something happened");
Класс DelimitedListTraceListener
Теперь допустим, что нужно активизировать слушатель другого типа, например DelimitedListTraceListener (он работает так же, как TextWriterTraceListener, но принимает маркер-разделитель записей вывода). Эта задача также решается программно или через конфигурационный файл. Следующий пример демонстрирует программную активизацию данного класса:
’ VB
Trace Listeners.Clear()
Тrace.Listeners.Add(New DelimitedListTraceListener ("C:\output.txt")) Trace AutoFlush = True
Trace.WriteLine("This is a test")
// C#
Trace.Listeners.Clear();
DelimitedListTraceListener MyListener = new DelimitedListTraceListener (@"C-\output.txt");
Trace Listeners.Add(MyListener);
Trace.AutoFlush = true;
Trace.WriteLine("This is a test");
А вот альтернативный способ:
<configuration>
<system diagnostics>
<trace autoflush="true” indentsize="5”>
<listeners>
Odd name="DemoListener"
Занятие 2
Отладка и трассировка 499
type="System.Diagnostics DelimitedListTraceListener"
delimiter="Г
initializeData="output.txt"/>
cremove name="Default" />
</listeners>
</trace>
</system.diagnostics>
</configuration>
Класс CorrelationManager
Иногда одно приложение управляет множеством процессов. Классический пример — исполняющая среда ASP.NET, постоянно обрабатывающая множество запросов. Класс CorrelationManager обеспечивает простой в применении механизм изоляции обработки уникальных запросов.
Практикум. Создание журнала событий приложения и работа с ним
В этом практикуме вы направите вывод Trace в объект EventLog. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
1.	Откройте Visual Studio 2005.
2.	Создайте консольное приложение на Visual C# 2005 или Visual Basic 2005. Visual Basic 2005 создает модуль с пустой функцией Main, a Visual C# — класс Program.cs с пустой функцией Main.
3.	Импортируйте пространства имен System и System.Diagnostics с помощью ключевого слова using в C# или Imports в Visual Basic, чтобы не указывать их в объявлениях объектов.
4.	Добавьте следующий код в Main.cs или Modulel.vb:
' VB
Trace.Listeners.Clear()
Dim MyLog as New EventLog("Chapter10", "localhost", "ChapterlODemo")
Trace.AutoFlush = True
Dim MyListener as EventLogTraceListener = new EventLogTraceListener(MyLog) Trace.WriteLine("This is a test")
11 C#
EventLog MyLog = new EventLog("Chapter10", "localhost", "ChapterlODemo");
Trace.AutoFlush = true;
EventLogTraceListener MyListener = new EventLogTraceListener(MyLog) Trace.WriteLine("This is a test");
5.	Скомпилируйте полученное приложение, нажав клавишу F5.
6.	Откройте журнал событий и проверьте, записан ли в него вывод. Если все сделано правильно, то вывод должен выглядеть примерно так, как показано на рис. 10-12.
500 Инструментарий для мониторинга
Глава 10
Рис. 10-12. Запись, созданная классом EventLogTraceListener в журнале событий
Резюме
	Классы Debug и Trace — основные инструменты проверки корректности работы приложений во время выполнения.
	Методы Write, WriteLine, Writelf и WriteLinelf можно использовать для синхронной передачи вывода трассировки в подключенные объекты-слушатели.
	Метод Assert классов Debug и Trace является одним из основных механизмов проверки работы кода и получения сведений о ней на основе проверки условий.
	Объекты-слушатели предназначены для хранения информации, генерируемой классами Debug и Trace.
	DefaultTraceListener используется, если не определен объект-слушатель.
	XmlTraceListener можно использовать для вывода детальной информации в формате XML.
	Класс TraceSource позволяет задавать источник трассировочных данных.
	TraceSwitch позволяет управлять почти любыми параметрами класса TraceSource.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Требуется изолировать трассировку разных логических операций. Как это осуществить?
А.	Использовать объект CorrelationManager.
В.	Использовать TraceSwitch для изменения трассировочной информации процесса.
Занятие 3
Мониторинг производительности §Q-|
С.	Использовать разные значения TraceLevel для процессов.
D.	Определить объекты Correlation и использовать CorrelationManager для управления ими.
2.	Какой из атрибутов задает объекг-визуализатор для класса?
A.	DebuggerDisplayAttribute.
В.	DebuggerVisualizer.Attribute.
С. DebuggerStepThroughAitnbute.
D. DebuggableAttribute.
Занятие 3. Мониторинг производительности
Независимо от числа полезных функций у приложения, если оно работает медленно, пользователи будут всячески избегать его. В действительности, низкая производительность порой хуже обычных «багов», поскольку увеличить ее намного сложнее, чем «выловить» отдельные ошибки в коде. Вот наиболее распространенные проблемы с производительностью:
	низкая отзывчивость пользовательского интерфейса (UI), «застывшие» окна и «зависания»;
	медленный доступ к сети,
	медленное обращение и длительное время обработки запросов к базам данных;
	замедленный файловый ввод-вывод.
Понимание причин этих проблем и знание принципов работы соответствующих механизмов играет ключевую роль в написании быстрых приложений. Но проблемы с быстродействием могут появиться и после развертывания приложения. Предположим, что вы написали Web-приложение, интенсивно использующее базу данных, и в службу поддержки компании посыпались жалобы на низкую производительность этой программы. Как быть что сказать клиентам и руководству? Если вы не в состоянии диагностировать проблему, то и устранить ее не сможете. Все это неизбежно введет вас в заблуждение и, следовательно, потере драгоценного времени. К счастью, пространство имен System.Diagnostics предоставляет все необходимые средства, чтобы избежать потери эффективности при решении проблем с производительностью.
Пример из практики
Уильям Райан (William Ryan)
Несколько лет назад я работал в компании, в которой менеджер по разработке крайне небрежно относился к большинству тонкостей, связанных с программированием: он ненавидел нормализацию базы данных, ООП. В общем он ненавидел отвечать что-либо кроме «Да сэр, мы выдержим сроки». Мы тестировали наши разработки по минимуму (так как тестирование он тоже ненавидел), поэтому тесты показывали хорошую производительность приложений. Зато после развертывания их на клиентских машинах всплывали «баги», да и производительность была далеко не блестящей. В ответ на жалобы клиентов менеджер лишь советовал «установить больше оперативной памяти».
502 Инструментарий для мониторинга
Глава 10
Однажды мы даже нарастили ОЗУ сервера с 2 до 12 Гб. Это обошлось весьма недешево. К сожалению, текущая версия Microsoft SQL Server поддерживала только 2 Гб, так что дополнительные 10 Гб попросту не использовались. В итоге менеджер настоял на обновлении SQL Server. Новая версия SQL Server задействовала все 12 Гб, но производительность возросла лишь незначительно и по-прежнему осталась неприемлемо низкой для клиента.
Теперь менеджер пенял на оборудование сервера клиента, сравнивая его с «Ferrari на велосипедных колесах», и порекомендовал клиенту модернизировать сервер. Однако клиент уже превысил смету больше чем на 20 000 долларов, и от модернизации отказался. Естественно, эти расходы пришлось принять на себя нашей компании. После модернизации повышение производительности вновь оказалось смехотворным, а расходы уже превысили 40 000 долларов.
Теперь в немилость попала сеть: менеджер потребовал заменить 100-мегабит-ные каналы на гигабитные. Вложив еще $4000, мы расширили полосу пропускания сети до 1 Гбит, но и это не подняло производительность на приемлемый уровень. Теперь менеджеру ничего не оставалось, кроме как обвинить Microsoft в «неграмотном проектировании» Windows 2000 Server и SQL Server 2000: по его заявлениям, Microsoft виновата в том, что старые консольные приложения «летают», а созданные его командой — «тормозят».
Настоящая же проблема была в следующем:
	неграмотная структура базы данных (в большинстве таблиц было 255 столбцов);
	чрезмерное использование неудачно написанных курсоров;
	весь код работает в одном потоке.
Если бы менеджер обнаружил проблему заранее, удалось бы сэкономить много времени и денег. Побеспокойся он о мониторинге производительности, прежде чем обвинять все и вся, удалось бы избежать краха успешной дот того компании Мораль сей басни такова: перед тем как тратить деньги и время на решение проблем, выясните их истинные причины.
Изучив материал этого занятия, вы сможете:
J перечислять процессы;
J использовать счетчики производительности; запускать обработку;
J работать с объектами StackTrace и StackFrame.
Продолжительность занятия — 20 минут.
Введение в процессы
Прежде чем создавать процессы и управлять ими, разберемся, что такое процесс. Определение из глоссария TechnoGeek гласит: «процесс — это область памяти, хранящая ресурсы». Более классическое определение звучит так: «процесс — это отдельная задача (или программа), выполняемая операционной системой». Процесс владеет одним или несколькими потоками операционной системы. Он также может создавать закрытое виртуальное адресное пространство, которым могут управлять только его потоки.
Занятие 3
Мониторинг производительности 5Q3
Большинству пользователей знакомы не процессы, а приложения. Например, Word из пакета Microsoft Office— это приложение, Outlook — тоже приложение. В зависимости от выполняемых действий, любое из этих приложений может управлять множеством процессов.
Технически (на стандартном 32-разрядном процессоре фирмы Intel) процесс проще всего представить как непрерывную область памяти размером 4 Гб. Она охватывает память с адресами от 0x00000000 до OxFFFFFFF. Эта память защищена, то есть закрыта от доступа других процессов. Это значительно повышает стабильность и производительность Windows-приложений. Следует отметить два момента, касающихся управления процессами:
	Windows выделяет на процесс ровно столько памяти, сколько необходимо. Максимальный размер ОЗУ традиционных 32-разрядных машин составляет 4 Гб.
	Диспетчер виртуальной памяти записывает фрагменты содержимого ОЗУ на диск в виде страниц. По умолчанию размер этих страниц составляет 4 Кб.
Класс Process
Экземпляр объекта Process представляет процесс операционной системы и может ссылаться на:
	один или несколько процессов, работающих на локальной машине;
	один или несколько процессов на удаленной машине.
Перечисление процессов
Существует четыре способа узнать, какие процессы работают в данный момент времени, или найти заданный процесс: GetCurrentProcess, GetProcessByld, GetProcessesByName и GetProcesses.
Простейший из них — использование метода GetCurrentProcess. Этот метод связывает объект Process с текущим активным процессом и возвращает его. Поскольку у данного метода нет параметров, его вызов тривиален (см. следующий пример, использующий пространство имен System.Diagnosticsy.
' VB
Dim ThisProcess as Process = Process. GetCurrentProcessO
Console.Writel_ine("Current Process: ")
Console.WriteLine(ThisProcess)
// C#
Process ThisProcess = Process.GetCurrentProcessO;
Console.WriteLine("Current Process: " + ThisProcess);
Хотя данный метод прост в использовании, его функциональность ограничена. Если нужно узнать что-то кроме того, какой процесс работает в данный момент, этот метод не подходит.
Следующий метод, перечисляющий процессы, GetProcessByld. Его назначение ясно из названия: он принимает идентификатор процесса и, если есть процесс с таким идентификатором, возвращает сведения о нем. Вторая перегруженная версия прини-мате идентификатор процесса и имя компьютера, а возвращает информацию о процессе. Внимание: если заданный процесс не найден, генерируется исключение ArgumentException. Поэтому, если вы не уверены, что искомый процесс существует (а в случае перегруженной версии, что существует и искомый компьютер), нужно предус
504 Инструментарий для мониторинга
Глава 10
мотреть обработку исключений. Следующий фрагмент кода принимает идентификатор процесса и возвращает информацию о нем (подставьте вместо machinename имя своего компьютера).
’ VB
'Цифра 2 взята для наглядности
Dim SpecificProcess As Process = Nothing
Try
SpecificProcess = Process GetProcessById(2)
Console.WriteLine(SpecificProcess.ProcessName)
Catch Problem As ArgumentException
Console. Writel_ine(Problem. Message)
End Try
T ry
SpecificProcess = Process.GetProcessById(2, "machinename")
Console.Writeline(SpecificProcess.ProcessName)
Catch Problem As ArgumentException
Console.WriteLine(Problem.Message)
End Try
// C#
// Цифра 2 взята для наглядности
Process SpecificProcess = null;
try
{
SpecificProcess = Process.GetProcessById(2);
Console.WriteLine(SpecificProcess.ProcessName);
}
catch (ArgumentException Problem)
{
Console.WriteLine(Problem.Message);
} try
{
SpecificProcess = Process.GetProcessById(2, "machinename");
Console.WriteLine(SpecificProcess.ProcessName);
}
catch (ArgumentException Problem)
Console.WriteLine(Problem.Message);
}
Эти методы возвращают информацию о заданных процессах (в данном примере — о процессе с идентификатором 2); следующие — о модулях, загруженных в процесс. О том, что делает первый из этих методов, GetProcessesByName, можно догадаться по его названию. В отличие от Get Process By Id, у данного метода нет перегруженных версий. Он принимает имя процесса и компьютера. Следующий код извлекает информацию о процессе с заданным именем (вместо числового идентификатора):
Занятие 3
Мониторинг производительности 5Q5
' VB
Dim SpecificProcesses() As Process = Nothing
SpecificProcesses = Process GetProcessesByName("explorer”, "machinename")
For Each ThisProcess As Process In SpecificProcesses
Console.WriteLine(ThisProcess.ProcessName)
Next
// C#
Processf] SpecificProcesses = null;
SpecificProcesses = Process.GetProcessesByName("explorer", "machinename");
foreach (Process ThisProcess in SpecificProcesses)
{
Console.WriteLine(ThisProcess.ProcessName);
}
Следующий метод — GetProcesses. Данный метод принимает имя компьютера и возвращает все запущенные на нем процессы. GetProcesses может сгенерировать следующие исключения:
	PlatformNotSupportedException;
	ArgumentNullException;
	InvalidOperationException;
	Win32Exception;
	ArgumentException.
Для обработки любого из них достаточно перехватывать исключение типа ArgumentException. Приемлемо ли это, зависит от стратегии обработки исключений. Следующий код перечисляет все процессы на компьютере.
' VB
Private Sub GetAllProcessWithoutMachineName()
Dim AllProcessesO As Process = Nothing
Try
AllProcesses = Process.GetProcesses
For Each Current As Process In AllProcesses
Console.WriteLine(Current.ProcessName)
Next
Catch Problem As ArgumentException
Console.WriteLine(Problem.Message)
End Try
End Sub
Private Sub GetAllProcessWithMachineName()
Dim AllProcessesO As Process = Nothing
Try
AllProcesses = Process.GetProcesses("machinename")
For Each Current As Process In AllProcesses
Console.WriteLine(Current.ProcessName)
Next
Catch Problem As ArgumentException
506 Инструментарий для мониторинга
Глава 10
Console WriteLine(Problem.Message)
End Try
End Sub «•
// C#
private static void GetAllProcessesWithoutMachineNameO
{
Process[] AllProcesses = null;
try
{
AllProcesses = Process. GetProcessesO;
foreach (Process Current in AllProcesses)
Console.WriteLine(Current.ProcessName);
}
}
catch (ArgumentException Problem)
{
Console.WriteLine(Problem.Message);
}
}
private static void GetAHProcessesWithMachineNameO
{
Process[] AllProcesses = null;
try
AllProcesses = Process. GetProcesses(’’machinename");
foreach (Process Current in AllProcesses)
{
Console.WriteLine(Current.ProcessName);
}
}
catch (ArgumentException Problem)
{
Console.WriteLine(Problem.Message);
}
}
Применение счетчиков производительности
Возможно, что производительность станет основным критерием, по которому пользователи будут оценивать приложение, поэтому измерение производительность имеет ключевое значение. Если в службу поддержки постоянно жалуются на медленную работу приложения, разумно организовать диагностику и устранение этой проблемы. Для этого необходимо:
1.	Убедится, что проблема с производительностью действительно существует.
2.	Записать эталонный уровень производительности приложения и сравнить его с фактическим уровнем.
Занятие 3
Мониторинг производительности 5Q7
3.	Зарегистрировать все функции, в которых наблюдается расхождение между фактической и эталонной производительностью.
4.	Проанализировать код и выявить причину этих расхождений.
5.	Устранить причины проблем
Хотя конкретные меры индивидуальны для компании и разработчика, в целом показанный алгоритм довольно типичен. К тому же большинство мер включает измерение реальной производительности.
Для этого в .NET Framework имеется класс PerformanceCounter.
ВНИМАНИЕ! Защита PerformanceCounter в .NET 2.0
В предыдущих версиях объектам PerformanceCounter требовались разрешения Full Trust (полное доверие). В .NET 2.0 PerformanceCounterPermission должно быть предоставлено явно. Разрешение PerformanceCounterPermission не предоставляется сборкам, работающим в частично доверяемом контексте, без веской причины. Обладая этим разрешением, сборка сможет запускать новые процессы, а также управлять другими системными процессами, что дает широкие возможности для атак. Рекомендации по защите класса PerformanceCounter те же, что и для EventLog.
Объекты PerformanceCounter позволяют собирать и получать с использованием чисто объектно-ориентированных методов. Значения, записанные в объект PerformanceCounter, хранятся в реестре Windows и зависят от платформы.
Данный класс можно использовать как программно, так и с применением компонента PerformanceCounter, представляющего графический интерфейс для управления объектами PerformanceCounter во время разработки. Для работы с этим компонентом просто откройте представление Components панели инструментов Visual Studio 2005, выберите счетчик производительности и перетащите его на форму (см. рис. 10-13).
LUHirliiiHiliWIIlfiBitli Nil 1 > 1 II li hl I	—
HL ЕЛ froject Bdd Bcbm Data Tools Window Community (jelp
Common Controls
” Cont ainers

Menus & foot ars Data
I omponents Pointer Backgroundworker
* I Ti DirectoryEntry DirectorySearcher
J ErrorProvtder EventLog
1^, FileSystemWatcher 5i HelpProvider
- ImageList MessageQueue
I Г7 PerformarKeCourter Process SeriaPort
' * ‘J ServiceControlt r Timer
П!
5
PerformanceCounter
J Version 2.0.0.0 from Hcrosoft Corporation
; .№T Component
'trig
а>
ч
Рис. 10-13. Компонент PerformanceCounter в Visual Studio
508 Инструментарий для мониторинга
Глава 10
Объекты PetformanceCounter чем-то сходны с путями к файлам, содержащим имена, папки и множество других атрибутов. Вот свойства объекта PetformanceCounter
	Имя компьютера (Computer Name);
	Имя категории (Category Name);
	Экземпляр категории (Category Instance);
	Имя счетчика (Counter Name).
Свойства компонента PetformanceCounter показаны на рис. 10-14.
Рис. 10-14. Свойства компонента PerformanceCounter
В Windows есть множество встроенных счетчиков производительности почти для любых ресурсов, интересующих разработчика. Набор счетчиков также зависит от приложений, установленных на машине. Важно знать, какие счетчики уже установлены, чтобы не «изобретать велосипед».
Класс CounterCreationData
В основном класс CounterCreationData используется для хранения свойств, необходимых для создания объекта PetformanceCounter. Класс CounterCreationData имеет три свойства, описанные в табл. 10-6.
Табл. 10-6.	Свойства класса CounterCreationData
Имя	Описание
CounterHelp CounterName CounterType	Получает или задает понятное описание счетчика Получает или задает имя счетчика Получает или задает значение PeiformanceCounterType для счетчика
Для создания экземпляра класса CounterCreationData выполните одно из следующих действий:
	объявите новый экземпляр объекта и задайте его свойства вручную;
	передайте значения трех параметров перегруженному конструктору.
Занятие 3
Мониторинг производительности ggg
Следующий фрагмент кода иллюстрирует первый способ:
1 VB
Dim DemoCounterName As String = ”DemoCounter"
Dim DemoData As New CounterCreationDataO
DemoData.CounterName = DemoCounterName
DemoData.CounterHelp = "Training Demo Counter"
DemoData.CounterType = PerformanceCounterType.SampleCounter
// C#
String DemoCounterName = "DemoCounter";
CounterCreationData DemoData = new CounterCreationDataO;
DemoData.CounterName = DemoCounterName;
DemoData.CounterHelp = "Training Demo Counter";
DemoData.CounterType = PerformanceCounterType.SampleCounter;
А этот фрагмент — второй:
' VB
Dim DemoCounterName As String = "DemoCounter"
Dim DemoData As New CounterCreationData(DemoCounterName, "Training Demo Counter", PerformanceCounterType.SampleCounter)
// C#
String DemoCounterName = "DemoCounter";
CounterCreationData DemoData = new CounterCreationData(DemoCounterName, "Training Demo Counter", PerformanceCounterType.SampleCounter );
Класс PerformanceCounterCategory
Класс PerformanceCounterCategory используется для управления и манипулирования объектами PerformanceCounter и их категориями. Он поддерживает пять статических методов, которые позволяют управлять огромным числом категорий:  Create
— создает класс PerfomanceCounterCategory;.
 Delete
— удаляет класс PerformanceCounterCategory;.
 Exists
— проверяет, существует ли экземпляр PerformanceCounterCategory;.
 GetCategories
— перечисляет все PerformanceCounterCategories на данной машине;.
 ReadCategory
— читает всю информацию о счетчике и сведения о производительности для заданного PeformanceCategory..
Для работы с методом Create вызовите статический метод Create класса PerformanceCounterCategory, передав значения CategoryName, Category Help, CategoryType, CounterName и CounterHelp. Перед попыткой вызова метода Create можно вызвать метод Exists, чтобы
51Q Инструментарий для мониторинга
Глава 10
проверить существование данной категории (ясно, что если она существует, то создавать ее не нужно). Следующий код проверяет, существует ли заданный счетчик производительности, и, если нет, создает его.
’ VB
Dim DemoCategory As New PerformanceCounterCategory("DemoCategory")
If Not PerformanceCounterCategory.Exists("DemoCategory”) Then
PerformanceCounterCategory.Create("DemoCategory", "Training Demo Category", PerformanceCoiinterCategoryType.Singlelnstance, "DemoCounter", "Training Counter Demo") End If
// C#
PerformanceCounterCategory DemoCategory = new PerformanceCounterCategory("DemoCategory"); if (!PerformanceCounterCategory.Exists("DemoCategory")) {
PerformanceCounterCategory.Create("DemoCategory", "Training Demo Category", PerformanceCounterCategoryType.Singlelnstance, "DemoCounter", "Training Counter Demo");
}
Пользуясь этим методом, следует знать, что, если Category уже существует, генерируется исключение InvalidOperationException.
Для удаления категории достаточно вызвать метод Delete класса Performance-CounterCategory и передать ему имя категории:
’ VB
If PerformanceCounterCategory.Exists("DemoCounter") Then
Pe rfо rmanceCounte rCatego ry.Delete("DemoCounter") End If
// C#
if (PerformanceCounterCategory.ExistsC'DemoCounter")) {
PerformanceCounterCategory.Delete("DemoCounter");
}
Следующий из перечисленных методов, GetCategories, поддерживает поиск объектов PerformanceCounterCategory на заданной машине. По умолчанию он обращается к PerformanceCounterCategory на локальном компьютере, но есть перегруженная версия конструктора, поддерживающая поиск объектов PerformanceCounterCategory на других машинах. Следующий пример демонстрирует работу с методом GetCategories: ' VB
Dim DemoCategoriesO As PerformanceCounterCategory = PerformanceCounterCategory.GetCategories("machinename") For Each ThisCategory As PerformanceCounterCategory In DemoCategoriesO
Console.WriteLine(ThisCategory.CategoryName) Next
Занятие 3
Мониторинг производительности 511
// C#
if (РеrfоrmanceCounterCategory.Exists("DemoCounter"))
{
PerformanceCounterCategory[] DemoCategories =
Pe rformanceCounterCategory.GetCatego ries("m achi nename");
foreach (PerformanceCounterCategory ThisCategory in DemoCategories)
{
Console.WriteLine(ThisCategory.CategoryName);
}
}
Последний метод в списке — ReadCategory. ReadCategory считывает всю информацию о счетчике и данные о производительности из экземпляра объекта, связанного с данной категорией. Этот метод возвращает объект InstanceDataCollectionCollection. Данный набор можно перебирать в цикле, чтобы получить все доступные сведения о данной категории. Следующий фрагмент кода иллюстрирует работу с методом ReadCategory.
' VB
Dim DemoCounterCategory As New PerformanceCounterCategoryC’MSMQ Service")
Dim DemoCollection As InstanceDataCollectionCollection =
DemoCounte rCatego ry.ReadCatego ry
For Each ThisCollection As InstanceDataCollection In DemoCollection
Debug.WriteLine(ThisCollection.CounterName)
Next
// C#
PerformanceCounterCategory DemoCounterCategory = new PerformanceCounterCategoryO; InstanceDataCollectionCollection DemoCollection = DemoCounterCategory. ReadCategory(); foreach (InstanceDataCollection ThisCollection in DemoCollection) {
Console. Writel_ine(ThisCollection. CounterName);
}
Класс Performancecounter
Все вышеописанные классы были частями механизма, предоставляющего несметное количество информации об объектах PerformanceCounter. Для демонстрации возможностей PerformanceCounter проверим число приложений в процессе ASP.NET. Существует предопределенная категория «ASP.NET» и счетчик «Applications Running». Предположим, что у счетчика два свойства:
' VB
Dim Crlf As String = ControlChars.CrLf
Dim Builder = New StringBuilderO
Builder.Append("Counter Name: " + DemoCounter.CounterName + Crlf)
Builder. Append( "Counter	Type: " + DemoCounter. CounterType. ToStringO, + Crlf)
Builder.Append("Category Name: " + DemoCounter.CategoryName + Crlf)
Builder.AppendC'Instance Name: " + DemoCounter.InstanceName + Crlf)
Builder.Append("Machine Name: ” + DemoCounter.MachineName + Crlf")
512 Инструментарий для мониторинга
Глава 10
MessageBox.Show(Builder.ToString())
// C# *
StringBuilder Builder = new StringBuilder();
Builder.Append("Counter Name:" + DemoCounter.CounterName + "\r\n”);
Builder.Append("Counter Type:" + DemoCounter.CounterType.ToStringO + "\r\n");
Builder.Append("Category Name:" + DemoCounter.CategoryName + "\r\n");
Builder.Append("Instance Name:" + DemoCounter.InstanceName+ "\r\n");
Builder.Append("Machine Name:" + DemoCounter.MachineName + "\r\n");
MessageBox.Show(Builder.ToString());
При запуске данный код выводит информацию о заданном PetformanceCounter.
Запуск процессов
Довольно часто приходится запускать внешние приложения или процессы из своих приложений. Например, чтобы отобразить файл справки для пользователя, необходимо запустить Internet Explorer, передав ему URL справочного файла. Это можно осуществить двумя способами:
	без использования аргументов командной строки;
	передав аргументы через командную строку.
Запуск процессов без передачи аргументов через командную строку
Чтобы запустить процесс, в .NET достаточно вызвать метод Start класса Process. У конструктора класса Process имеется пять перегруженных версий, три из которых не поддерживают командную строку.
ПРИМЕЧАНИЕ Определение параметров командной строки для запуска программы
У метода Main .NET-приложений имеется два перегруженных конструктора. Первый — без параметров, второй принимает массив объектов String, разделенных пробелами. Каждый из этих объектов связан с отдельным параметром. К примеру, литерал «This is а Test», переданный методу Main, расцениваться как четыре значения: ‘This’, ‘is’, ‘а’ и ‘Test’.
У первого из перегруженных методов — единственный параметр типа ProcessStartlnfo. У класса ProcessStartlnfo есть несколько свойств, обеспечивающих значительные возможности по контролю запуска процессов. Поскольку можно запускать процессы разных типов (например, консольные приложения, приложения Windows Forms и т. д.), свойства процессов могут существенно различаться. По этой причине мы уделяем основное внимание свойствам, которые определяют перегруженные конструкторам (см. пример ProcessStartDemo в папке Chapter 11 на прилагаемом компакт-диске).
ПРИМЕЧАНИЕ Работа с классом ProcessStartlnfo
Часто требуется задать гораздо больше свойств, чем позволяет конструктор. Для этого предназначен класс ProcessStartlnfo.
’ VB
Dim Info as New ProcessStartlnfoO
Занятие 3
Мониторинг производительности 513
Info.FileName = this.tbProcessName.Text
Process Start(Info)
// C#
ProcessStartlnfo Info = new ProcessStartlnfoO;
Info.FileName = this.tbProcessName.Text;
Process Start(Info),
Данный код создает экземпляр класса ProcessStartlnfo для запуска определенного процесса.
Второй из перегруженных методов принимает один параметр типа String, соответствующий имени приложения. Если приложение установлено в системе, достаточно указать имя приложения, в противном случае необходим полный путь и имя исполняемого файла ' VB
Dim Info as New ProcessStartlnfoO
String FileName = "C:\ProcessStartDemo.exe"
Process Start(FileName)
// C#
ProcessStartlnfo Info = new ProcessStartlnfoO;
String FileName = ©"C:\ProcessStartDemo.exe";
Process Start(FileName);
Третий перегруженный метод принимает имя файла, как и предыдущий; значение String, соответствующее имени входа пользователя; а также объект System.Security.SecureString, соответствующее паролю доменной учетной записи пользователя.
’ VB
Dim SecurePassword As New SecureSt ring
For i As Int32 = 0 To Me.tbPassword Text.Length - 1
SecurePassword.AppendChar(Convert.ToChar(Me.tbPassword.Text(i))) Next
Process.Start(Me.tbUserName.Text, Me.tbllserName.Text, SecurePassword, Me.tbDomain.Text)
// C#
SecureString SecurePassword = new SecureStringO;
for (Int32 i = 0; i < this.tbPassword.Text.Length; i++) {
SecurePassword.AppendChar(Convert.ToChar(this.tbPassword.Text[i]))-}
Process.Start(this.tbllserName.Text, this.tbPassword.Text, SecurePassword , this.tbDomain.Text);
ВНИМАНИЕ! UseShellExecute
Если заданы имя и пароль пользователя, свойство UseShellExecute должно быть установлено в false.
514 Инструментарий для мониторинга
Глава 10
Запуск процессов с помощью аргументов командной строки
Запуск процесса с использованием аргументов командной строки мало отличается от альтернативного способа. В первом примере предыдущего раздела показано использование ProcessStartlnfo для запуска процесса. У класса ProcessStartlnfo есть свойство Arguments, позволяющее задавать многие необходимые аргументы. Для этого достаточно указать их значения, разделенные пробелами. В предыдущем разделе свойство Arguments не задано, что позволяло запускать процесс без аргументов командной строки. Как определять аргументы командной строки, демонстрирует следующий код, требующий пространство имен System.Diagnostics'.
' VB
Dim Info as New ProcessStartlnfoO
Info.FileName = this.tbProcessName.Text
Info.Arguments = "EACH OF THESE WORDS IS AN ARGUMENT”
Process.Start(Info)
// C#
ProcessStartlnfo Info = new ProcessStartlnfoO;
Info.FileName = this.tbProcessName.Text;
Info.Arguments = "EACH OF THESE WORDS IS AN ARGUMENT " ;
Process.Start(Info);
При работе с классом ProcessStartlnfo достаточно определить свойство Arguments'.
' VB
Dim Info As New ProcessStartlnfoO
Info.FileName = Me.tbUserName.Text
If Me.tbArguments.Text <> String.Empty Then
Info.Arguments = Me.tbArguments.Text
End If
If Me.tbUserName.Text <> String.Empty Then
Dim SecurePassword As New System.Security.SecureStringO
For i As Int32 = 0 To Me.tbPassword.Text.Length - 1
Secu rePasswo rd.AppendCha г(Conve rt.ToCha r(Me.t bPasswo rd.Text(1)))
Info.UseShellExecute = False
Info.UserName = Environment.UserName
Info.Password = SecurePassword
Return
Next i
Elself tbPassword.Text <> String.Empty Then
MessageBox.Show("If a UserName is provided, a Password must be provided as well")
End If
If Me.tbDomain.Text <> String.Empty Then
Info.Domain = Me.tbDomain.Text
End If
If Me.tbProcessName.Text <> String.Empty Then
Info.FileName = Me.tbProcessName.Text
Занятие 3
Мониторинг производительности g-|g
Process.Start(Info)
End If
// C#
ProcessStartlnfo Info = new ProcessStartlnfoO;
Info.FileName = this.tbProcessName.Text;
if (this.tbArguments.Text != string.Empty)
{
Info.Arguments = this.tbArguments.Text;
}
if (this.tbUsername.Text != String.Empty)
{
Securest ring Secu rePassword = new SecureStringO;
for (Int32 i = 0; i < this.tbPassword.Text.Length; i++)
{
SecurePassword.AppendChar(Convert.ToChar(this.tbPassword.Text[i]));
// Если задано имя пользователя, установить UseShellExecute в false
Info.UseShellExecute = false.
Info.UserName = Environment.UserName;
Info.Password = SecurePassword;
}
else
{
MessageBox.Show("If a UserName is provided, a Password must be provided as well");
return;
if (this.tbDomain.Text != String.Empty){
Info.Domain = this.tbDomain.Text;
}
if (this.tbProcessName.Text != string.Empty)
{
Process.Start(Info);
Классы StackTrace и StackFrame
Класс StackTrace позволяет просматривать состояние стека вызовов исполняющей среды .NET в заданный момент времени. Детальное рассмотрение стека вызовов не входит в тему данного раздела, поэтому мы обсудим его лишь кратко.
При каждом вызове метода в стек помещается объект StackFrame. Все следующие вызовы записываются в стек по принципу «последним вошел — первым вышел» (last in, first out, LIFO). Если вызовов больше нет, имеющиеся вызовы исполняются и «выталкиваются» из стека. Таким образом, изучая StackTrace и его объекты StackFrame, можно получить огромное количество информации о состоянии приложения. Класс StackFrame поддерживает методы .NET Framework, и не доступен напрямую. Краткий обзор конструкторов класса StackFrame представлен в табл. 10-7.
516 Инструментарий для мониторинга
Глава 10
Табл. 10-7. Конструкторы класса StackFrame
Конструктор	Доступные параметры
StackTrace Constructor ()	—
StackTrace Constructor (Boolean)	fNeedFilelnfo
StackTrace Constructor (Exception)	e
StackTrace Constructor (Int32)	skipFrames
StackTrace Constructor (StackFrame)	frame
StackTrace Constructor (Exception, Boolean)	e
	fNeedFilelnfo
StackTrace Constructor (Exception, Int32)	e skipFrames
StackTrace Constructor (Int32, Boolean)	skipFrames fNeedFilelnfo
StackTrace Constructor (Thread, Boolean)	targetThread fNeedFilelnfo
StackTrace Constructor (Exception, Int32, Boolean)	e skipFrames fNeedFilelnfo
Каждый раз, когда исполняющая среда .NET генерирует исключение, в текущее свойство StackTrace объекта Exception записывается текущий объект StackTrace. Если в сборке, сгенерировавшей исключение, загружены отладочные символы, эту информацию можно просмотреть. Изучите следующий фрагмент кода:
' VB
Try
Dim х-as Int32 = 1
x = x - 1
Dim i as Int32 = 10 \ x
Catch Problem as DivideByZeroException
Debug.Assert(False, Problem.StackTrace)
End Try
// C#
try
{
Int32 x = 1;
x--;
Int32 i = 10 / x;
}
catch (DivideByZeroException Problem)
{
Debug.Assert(false, Problem.StackTrace);
}
Занятие 3
Мониторинг производительности 517
После перехвата объекта Exception код умышленно генерирует исключение с помощью Debug, чтобы изучить содержимое StackTrace (рис. 10-15).
Рис. 10-15. Содержимое объекта StackTrace
Практикум. Мониторинг производительности приложений
Ваша задача — создать приложение, добавить в него два объекта PerformanceCounter и отследить их значения. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге. 1. Откройте Visual Studio 2005.
2.	Выберите File\New, Project\Visual С#/ Visual Basic .NET\Windows\Console Application.
3.	Введите PerformanceCounterDemo в текстовое поле Name и щелкните OK.
4.	Выберите файл Program.cs в Visual C# или Modulel.vb в Visual Basic.
5.	Добавьте следующий код в начало файла Program.cs или Modulel.vb:
' VB
Imports System.Diagnostics
Imports System.Timers
// C#
using System.Diagnostics;
using System.Timers;
6.	Объявите три простейших закрытых члена: объект PerformanceCounter с именем HeapCounter, объект PerformanceCounter с именем ExceptionCounter и объект Timer с именем DemoTimer. Это показано ниже, в примере кода:
' VB
Private HeapCounter As PerformanceCounter
Private ExceptionCounter As PerformanceCounter
Private DemoTimer As Timer
// C#
private static PerformanceCounter HeapCounter = null;
private static PerformanceCounter ExceptionCounter = null;
private static Timer DemoTimer;
7.	В конструкторе создайте экземпляр DemoTimer и передайте перегруженному конструктору значение 3000. Это установит свойство Interval в 3000 миллисекунд или 3 секунды.
5 "| 8 Инструментарий для мониторинга
Глава 10
' VB
DemoTimer = New Timeг(3000)
И C#
DemoTimer = new Timer(3000);
8.	Создайте EventHandler для события Elapsed объекта DemoTimer и назовите его OnTick’.
' VB
Private Sub 0nTick(ByVal source As Object, ByVai e As ElapsedEventArgs)
End Sub
// C# private static void OnTick(object source, ElapsedEventArgs e)
}
9.	Вернитесь к методу Main и подключите обработчик события Elapsed’.
’ VB
AddHandler DemoTimer.Elapsed, AddressOf OnTick
11 C#
DemoTimer.Elapsed += new ElapsedEventHandler(OnTick);
10.	Сразу после предыдущей строки кода установите свойство Enabled объекта DemoTimer в значение true’.
' VB
DemoTimer.Enabled = True
// C#
DemoTimer.Enabled = true;
И. Создайте экземпляр HeapCounter PerformanceCounter и установите его свойство CategoryName в «.NET CLR Memory», a CounterName — в «# Bytes in all Heaps», как показано в следующем фрагменте кода. Затем установите свойство InstanceName в «_Global_»:
' VB
HeapCounter = New PerformanceCounter(".NET CLR Memory", "# Bytes in all Heaps")
HeapCounter.InstanceName = "_Global_"
// C#
HeapCounter = new PerformanceCounter(".NET CLR Memory”, ”# Bytes in all Heaps”);
HeapCounter InstanceName = ”_Global_";
12.	Таким же образом, за исключением установки свойства CounterName, создайте экземпляр ExceptionCounter PerformanceCounter:
Занятие 3
Мониторинг производительности 519
VB
Exceptioncounter = New PerformanceCounter(".NET CLR Exceptions”, "# of Exceps Thrown")
Exceptioncounter.InstanceName = "_Global_"
// C#
Exceptioncounter = new PerformanceCounter('.NET CLR Exceptions", "# of Exceps Thrown");
Exceptioncounter.InstanceName = "_Global_";
13.	После этого добавьте следующие строки кода. Первая строка служит только для отображения, а вторая позволяет продолжать выполнение приложения:
’ VB
Console.WriteLineC"Press [Enter] to Quit Program")
Console. ReadLineO
// C#
Console.WriteLine("Press [Enter] to Quit Program”);
Console. ReadLineO;
14.	Добавьте код для вывода свойства NextValue каждого из объектов PetformanceCounter в консольном окне EventHandler.
' VB
Console. WriteLineC"# of Bytes in all Heaps : " + HeapCounter.NextValueC). ToStringO) Console.WriteLineC"# of Framework Exceptions Thrown : ” + Exceptioncounter.NextValue() .ToString())
// C#
Console.WriteLineC# of Bytes in all Heaps : " + HeapCounter.NextValue().ToStringO) Console.WriteLineC’# of Framework Exceptions Thrown : ” + Exceptioncounter.NextValue() .ToString());
15.	Скомпилируйте полученное приложение, выбрав Build\Build Solution или нажав клавишу F5.
Резюме
	Процесс — это работающее приложение с уникальным идентификатором. Процесс — это механизм для изоляции работающих приложений в целях безопасности.
	Debug и Trace — главные классы для управления выводом приложений. В приложениях .NET объекты-слушатели класса Trace активны по умолчанию. Объекты-слушатели позволяют обрабатывать вывод Debug и Trace. XmlTextWriterListener позволяет записывать вывод Debug и Trace с размещением детальной информации в определенных XML-атрибутах. Метод Assert классов Debug и Trace основан на проверке условий, по результатам которой он продолжает выполнять приложение либо прерывает его и вызывает отладчик. Метод Break класса Debugger останавливает выполнение приложения в заданной точке прерывания.
520 Инструментарий для мониторинга
Глава 10
	Метод Start класса Process позволяет программно запускать приложения. Метод GetProcesses класса Process возвращает информацию о всех процессах, работающих на компьютере. Методы GetProcessByName и GetProcessByld предназначены для получения информации о заданном процессе (не о всех процессах).
	Параметры конструктора Main могут быть переданы как аргументы командной строки.
	Объект StackTrace предоставляет детальную информацию о состоянии приложения. Объекты StackFrame дополняют StackTrace, но предназначены для внутренних целей и не должны использоваться напрямую.
	Поскольку объекты String — ссылочного типа, их использование чревато уязвимостями в защите, если в них хранятся пароли. В текущей версии .NET Framework для управления конфиденциальными данными служит класс SecureString.
	Объекты PerformanceCounter позволяют измерять параметры утилизации ресурсов приложением.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Какой из методов запускает экземпляр Internet Explorer с URL «http:// www.microsoft.com»?
A. Process.Start(“iexplore.exe”).
В. Process.Start(“IntemetExplorer, http://www.microsoft.com”).
C. Process.Start(“iexplore.exe”, “http://www.microsoft.com”).
D. Process.Run(“iexplore.exe”, "http://www.microsoft.com”).
2.	Предположим, что данный код только что исполнен. Как вывести текущий StackTrace в окне консоли?
' VB
Try
Dim х as Int32 = 1
4	-
X = X - 1
Dim i as Int32 = 10 / x
Catch Problem as DivideByZeroException
'Здесь должен быть прочий код
End Try
И C#
try	t
{
Int32 x = 1;
x—;
Int32 i = 10 / x;
Занятие 4
Обнаружение управляющих событий	521
catch (DivideByZeroException Problem)
{
// Здесь должен быть прочий код
A.	Debug.Assert(Problem. Message).
В.	Debug.Assert(false, Problem.ToStringO-
C.	Console. WriteLine(Problem. ToStringO).
D.	Console. WriteLine(Problem.StackTrace. ToStringO).
3.	Как передать пароль при запуске процесса?
А.	Через переменную String.
В.	Через объект Array типа Char.
С.	Через объект Array типа String со ссылкой на индекс пароля в массиве.
D.	Через объект SecureString.
Занятие 4. Обнаружение управляющих событий
Пространство имен System.Management предоставляет обширный набор средств для мониторинга и управления системой, устройствами и приложениями посредством инструментария управления Windows (Windows Management Instrumentation, WMI).
Изучив материал этого занятия, вы сможете
J перечислять управляющие объекты;
J создавать очереди;
J обрабатывать события.
Продолжительность занятия — 20 минут.
Перечисление управляющих объектов
В пространство имен Sy stem. Management входит объект DirectoryObjectSearcher, способный программно обращаться к ресурсам через WMI. Данный механизм очень прост в применении, поскольку имитирует SQL-запросы, знакомые большинству разработчиков. Для реализации запроса с помощью DirectoryObjectSearcher нужно:
1.	Объявить экземпляр класса ConnectionOptions и задать его свойства UserName и Password.
2.	Объявить экземпляр класса DirectoryObjectSearcher.
3.	Объявить объект ManagementScope и задать свойства Path Name и ConnectionOptions.
4.	Создать объект ObjectQuery и определить запрос для запуска.
5.	Создать ManagementObjectCollection и задать значение, возвращаемое методом Direc-toryObjectSearcher's Get.
§22 Инструментарий для мониторинга
Глава 10
ПРИМЕЧАНИЕ Работа с ConnectionOptions
При использовании данного объекта следует побеспокоиться о безопасности, поскольку любое обращение к системным ресурсам — потенциальная лазейка для злоупотреблений. Среди уязвимых объектов — класс ConnectionOptions. Поскольку он содержит имя и пароль пользователя, нужно быть очень осторожным, чтобы сохранить их в секрете. К сожалению, класс ConnectionOptions пока не поддерживает объект SecureString для хранения пароля.
Приведенные ниже примеры воспроизводят этот алгоритм, различаясь лишь по запрашиваемому объекту.
В пространстве имен System.Management — множество членов, но основная часть его функциональности сосредоточена в нескольких классах, перечисленных в табл. 10-8.
Табл. 10-8. Управляющие объекты из пространства имен System.Management	
Имя	Описание
ManagementQuery	Базовый класс для запросов WMI
EventQuery	Объект, применяемый для запроса объектов WMI. Обеспечивает связь с мониторами событий
ObjectQuery	Объект для запроса экземпляров и классов
ManagementObjectSearcher	Класс для запроса набора Managementobjects, основанный на запросе WMI
Перечисление логических дисков
Для перечисления логических дисков средствами объектов System.Management сначала нужно определить цель запроса. В следующем примере запрашивается объект Win32 LogicalDisk. Атрибуты Size и Name возвращаются через ManagementObjectCollection.
•	VB
Dim DemoOptions As New ConnectionOptions
DemoOptions.Username = "\\Bill"
DemoOptions.Password = "mp99!!swaO"
Dim DemoScope As Managementscope = New ManagementScope( \\machinename', DemoOptions) Dim DemoQuery As New ObjectQueryC"SELECT Size, Name FROM Win32_LogicalDisk where DriveType=3")
Dim DemoSearcher As New ManagementObjectSearcher(DemoScope, DemoQuery)
Dim AllObjects As ManagementObjectCollection = DemoSearcher.Get
For Each DemoObject As Managementobject In AllObjects
Console WriteLineC"Resource Name: " & DemoObject("Name").ToStringO) Console WriteLineC"Resource Size: ” & DemoObjectC"Size").ToStringO)
Next
// C#
ConnectionOptions DemoOptions = new ConnectionOptionsO;
Занятие 4
Обнаружение управляющих событий 523
DemoOptions.Username = "\\ВШ";
DemoOptions.Password = "mp99!!swaO";
Managementscope DemoScope = new ManagementScope("\\machinename", DemoOptions); ObjectQuery DemoQuery = new ObjectQuery("SELECT Size, Name FROM Win32_LogicalDisk where DriveType=3");
ManagementObjectSearcher DemoSearcher = new ManagementObjectSearcher(DemoScope, DemoQuery); ManagementObjectCollection AllObjects = DemoSearcher.Get();
foreach (ManagementObject DemoObject in AllObjects) {
Console.WriteLine("Resource Name: " + DemoObject["Name"].ToStringO);
Console.WriteLine("Resource Size: " + DemoObject["Size"].ToString());
}
Перечисление сетевых адаптеров
Сетевые адаптеры заданного компьютера перечисляют практически также, как диски — достаточно немного изменить запрос. Главное отличие от предыдущего примера состоит в запросе объекта Win32_NetworkConfiguration вместо Win32_LogicalDisk. У сетевых адаптеров и дисковых разные свойства, поэтому код для отображения различных свойств несколько отличается:
•	VB
Public Const IPJEnabled As String = "IPEnabled"
Public Const IP_Address As String = "IPAddress"
Public Const IP_Subnet As String = "IPSubnet"
Public Const DNS_Hostname As String = "DNSHostName"
Public Const DNS_Domain As String = "DNSDomain"
Public Sub EnumerateAllNetworkAdapters()
Dim DemoQuery As ManagementObjectSearcher =
New ManagementObjectSearcher("SELECT * FROM Win32_NetworkAdapterConfiguration")
Dim DemoQueryCollection As ManagementObjectCollection = DemoQuery.Get()
For Each DemoManager As Managementobject In DemoQueryCollection
Console.WriteLine("Description : " & DemoManager("Description"))
Console.WriteLine("MacAddress : " & DemoManager("MacAddress"))
If (CType(DemoManager(IPJEnabled), Boolean) = True) Then
Dim IPAddressesO As String = CType(DemoManager(IP_Address), StringO) Dim IPSubnetsO As String = CType(DemoManager(IP_Subnet), StringO)
Console.WriteLine(DNS_Hostname & " : " & DemoManager(DNS_Hostname)) Console.WriteLine(DNS_Domain & " : " & DemoManager(DNS_Domain))
For Each IPAddress As String In IPAddresses
Console.WriteLine(IP_Address & ” : " & IPAddress)
524 Инструментарий для мониторинга
Глава 10
Next
For Each IPSubnet As String In IPSubnets
Console.WriteLine(IP_Subnet & " : " & IPSubnet)
Next
End If
Next
End Sub
// C#
public	const	String	IP_Enabled = "IPEnabled";
public	const	String	IP_Address = "IPAddress";
public	const	String	IP. Subnet = "IPSubnet";
public	const	String	DNS_HostName = "DNSHostName"
public	const	String	DNS_Domain = "DNSDomain";
public void EnumerateAllNetworkAdaptersO {
ManagementObjectSearcher DemoQuery =
new ManagementObjectSearcher("SELECT * FROM Win32_NetworkAdapterConfiguration");
ManagementObjectCollection DemoQueryCollection = DemoQuery.Get();
foreach (Managementobject DemoManager in DemoQueryCollection) {
Console.WriteLine("Description : " + DemoManager["Description"]);
Console.WriteLine("MacAddress : " + DemoManagerfMacAddress"]);
if (Convert.ToBoolean(DemoManager[IP_Address]) == true)
String[] IPAddresses = DemoManager[IP_Address] as String[];
StringC] IPSubnets= DemoManager[IP_Subnet] as String[];
Console.WriteLine(DNS_HostName + " : ” + DemoManager[DNS_HostName]);
Console.WriteLine(DNS_Domain + " : " + DemoManager[DNS_Domain]);
foreach (String IPAddress in IPAddresses) {
Console.WriteLine(IP_Address + " : " + IPAddress);
}
foreach (String IPSubnet in IPSubnets)
Console.WriteLine(IP_Subnet + " : ” + IPSubnet); } } } }
Занятие 4
Обнаружение управляющих событий 525
Поиск информации о приостановленных сервисах
Получение информации о службах Windows мало отличается от опроса других ресурсов Выполните следующие действия:
1.	Создайте объект ManagementObjectSearcher.
2.	Укажите для него запрос.
3.	Вызовите метод Get объекта ManagementObjectSearcher (из пространства имен System.Management), например так:
’ VB
Private Sub ListPausedServicesO
Dim DemoSearcher As New ManagementObjectSearcher("SELECT * FROM Win32_Service
WHERE Started = FALSE")
Dim AllObjects As ManagementObjectCollection = DemoSearcher.Get
For Each PausedService As Managementobject In AllObjects
Console.WriteLine("Service = " + PausedService("Caption"))
Next
End Sub
’ // C#
private static void ListPausedServicesO
ManagementObjectSearcher DemoSearcher =
new ManagementObjectSearcher("SELECT * FROM Win32_Service WHERE Started =
FALSE");
ManagementObjectCollection AllObjects = DemoSearcher.Get();
foreach (Managementobject PausedService in AllObjects)
{
Console.WriteLine("Service = " + PausedService["Caption"]);
}
}
Подписка на уведомление об управляющих событиях
с помощью ManagementEventWatcher
Класс ManagementEventWatcher позволяет получать информацию о событиях, которые происходят в контексте WMI. Для работы с ним выполните следующие действия:
1.	Создайте объект ManagementEventWatcher.
2.	Свяжите с ним объект EventQuery.
3.	Вызовите метод WaitForNextEvent.
4.	Остановите отправку уведомлений.
Вот соответствующий пример кода:
' VB
Public Sub QueryServices()
Dim DemoQuery As New EventQuery
DemoQuery.Querystring = "SELECT * FROM _InstanceCreationEvent WITHIN 1 WHERE
Targetinstance isa "”Win32_Service"" AND Targetinstance.State = 'Paused'"
526 Инструментарий для мониторинга
Глава 10
Dim DemoWatcher As New ManagementEventWatcher(DemoQuery)
DemoWatcher.Options.Timeout = New TimeSpan(O, 0, 20)
Console.WriteLine(
"Open an application to trigger WaitForNextEvent”)
Dim e As ManagementBaseObject = DemoWatcher.WaitForNextEventO
DemoWatcher StopO
End Sub
// C#
public static void QueryServicesO
{
EventQuery DemoQuery = new EventQueryO
DemoQuery.QueryString = "SELECT * FROM __InstanceCreationEvent WITHIN 2
WHERE Targetinstance isa \"Win32_Service\" AND TargetInstance.State = 'Paused'";
ManagementEventWatcher DemoWatcher = new ManagementEventWatcher(DemoQuery);
DemoWatcher.Options.Timeout = new TimeSpan(0, 0, 10);
Console.WriteLine("Open an application to trigger WaitForNextEvent");
ManagementBaseObject Event = DemoWatcher.WaitForNextEventO;
DemoWatcher StopO;
Практикум. Регистрация управляющих событий в журнале
Сейчас вы разработаете приложение для регистрации управляющих событий в журнале. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
1.	Откройте Visual Studio 2005.
2.	Выберите File\New\Project\Visual С#/ Visual Basic .N ET\Windows\Console Application.
3.	Введите PerformanceCounterDemo в поле Name и щелкните OK.
4.	Выберите файл Program.cs в Visual C# или Modulel.vb в Visual Basic. c
5.	Добавьте следующий код в начало файла Program.cs или Modulel.vb:
'VB
Imports System.Diagnostics
Imports System.Management
//C#
Using System.Diagnostics;
Imports System.Management;
Также необходимо добавить ссылку на пространство имен Sy stem. Management.
6.	В Modulel.vb или Program.es объявите метод WriteToEventLog и вставьте следующий код:
Занятие 4
Обнаружение управляющих событий 527
'VB
Public Sub WriteToEventLogO
Dim DemoQuery As New WqlEventQuery("__InstanceCreationEvent", New
TimeSpan(O, 0, 1), "Targetinstance isa ”"Win32_Process....)
Dim DemoWatcher As New ManagementEventWatcher() DemoWatcher.Query = DemoQuery
DemoWatcher.Options.Timeout = New TimeSpan(0, 0, 30)
Console.WriteLine("Open an application to trigger an event.") Dim e As ManagementBaseObject = DemoWatcher.WaitForNextEventO
Dim DemoLog As New EventLog("ChaplODemo")
DemoLog.Source = "ChaplODemo"
Dim EventName As String = CType(e("Targetinstance"),
ManagementBaseObj ect)("Name")
Console.WriteLine(EventName)
DemoLog.WriteEntry(EventName, EventLogEntryType.Information)
DemoWatcher. StopO
End Sub
//C#
public static void WriteToEventLogO
WqlEventQuery DemoQuery = new WqlEventQueryC________InstanceCreationEvent",
new T±meSpan(0, 0, 1), "Targetinstance isa \"Win32_Process\"”);
ManagementEventWatcher DemoWatcher = new ManagementEventWatcherO;
DemoWatcher.Query = DemoQuery;
DemoWatcher.Options.Timeout = new TimeSpan(0, 0, 30);
Console.WriteLine(
"Open an application to trigger an event.");
ManagementBaseObject e = DemoWatcher.WaitForNextEventO;
EventLog DemoLog = new EventLog("ChaplODemo");
DemoLog.Source = "ChaplODemo";
String EventName = ((ManagementBaseObject)e["TargetInstance"]) ["Name"]. ToStringO;	»
Console.WriteLine(EventName);
DemoLog.WriteEntry(EventName, EventLogEntryType.Information);
528 Инструментарий для мониторинга
Глава 10
DemoWatcher Stop();
7.	Вызовите WnteToEventLog из Маш следующим образом: ’VB
Sub Main()
WriteToEventLogO
Console. ReadLineO
End Sub
//C#
static void Main(string[] args)
{
WriteToEventLogO;
Console.ReadLine();
8.	Скомпилируйте и запустите полученное приложение. Оно предложит выбрать программу (скажем, notepad.exe), имя которой должно быть считано из окна консоли и записано в журнал событий с именем «ChapterlODemo».
Резюме
	WMI — это компонент операционной системы Windows, поддерживающий мониторинг практически любых ресурсов компьютера.
	Класс EventQuery представляет запросы WMI в .NET Framework.
	Объект Win32_Service применяют для запроса информации о службах Windows.
	От базового класса ManagementQuery порождают объекты-запросы.
	ManagementObjectSearcher предназначен для опроса системных ресурсов через WMI.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1. Какие из перечисленных элементов нельзя запросить с помощью WMI?
А. Сетевые адаптеры.
В.	Логические диски машины.
С.	Список OleDb-совместимых баз данных в сети.
D.	Список служб Windows и их состояние.
Лабораторная работа §29
Закрепление материала главы
Для закрепления знаний и навыков, полученных при изучении этой главы, выполните следующее:
	изучите резюме главы;
	повторите основные термины, использованные в этой главе;
	выполните задания лабораторной работы по темам этой главы;
	выполните предложенные упражнения;
	пройдите пробный экзамен.
Резюме главы
	Механизм регистрации событий Windows идеален для сбора информации об установленном приложении. В журналы событий Windows записывают уведомления, предупреждения, пользовательские сообщения и сообщения системы безопасности.
	Классы Debug и Trace позволяют отслеживать информацию о приложении во время разработки и после нее.
	Объекты-слушатели принимают данные от объектов Debug и Trace.
	Visual Studio 2005 поддерживает множество новых атрибутов, которые дают полный контроль над отладчиком. Примеры — DebuggerVisualizerAttribute и Debugger Proxy Ту ре.
	Визуализаторы — это новое мощное средство Visual Studio 2005, позволяющее просматривать практически любые параметры объектов во время отладки.
	Windows Management Instrumentation (WMI) и пространство имен System. Management обеспечивают инфраструктуру для запроса информации практически о любом из ресурсов компьютера.
Основные термины
	атрибут; Debug;
	Debugger;
	журнал событий;
	ManagementQuery;
	счетчик производительности;
	процесс;
	StackTrace;
	инстурментарий управления Windows.
Лабораторная работа
Сейчас вы примените на практике знания и навыки, полученные при изучении этой главы. Ответы на вопросы см. в разделе «Ответы» в конце книги.
530 Инструментарий для мониторинга
Глава 10
Лабораторная работа. Перенаправление вывода
Вы работаете в компании A Datum Corporation разработчиком приложений. Приложение, в разработке которого вы принимали участие, уже месяц как передано в эксплуатацию, и в последнее время пользователи стали жаловаться на его низкую производительность. Администратор баз данных (DBA) компании предупредил, что корпоративная база данных на основе SQL Server, по прогнозам, не сможет обслуживать больше приложений-клиентов, чем обслуживает сейчас. Несмотря предположения о дефиците аппаратных ресурсов, DBA уверен, что основная причина задержек — в «утечках» соединений. Кроме того, администратор сети сообщил, что Web-серверы не выдержат роста нагрузки на них.
Вице-президент компании недавно потребовал от отдела разработки предоставить список мер, необходимых, чтобы устранить проблему «раз и навсегда». Он сказал, что готов купить любое оборудование, но все, что потребуется сверх того, будет приобретаться за счет премиального фонда вашего отдела. Он также указал, что устранить нужно все проблемы, включая «баги», снижающие производительность, дефекты Web-сервера и базы данных, так что их тоже нужно указать в докладной. Вам поручено написать эту докладную. Как вы будете ее готовить?
Рекомендуемые упражнения
Чтобы лучше проработать темы экзамена, рассмотренные в этой главе, выполните следующие упражнения.
Добавление функций настройки, управления и установки в .NET-приложения
Выполните как минимум упражнения 1 и 2. Дополнительный опыт работы с пространствами имен System.Diagnostics и System Management позволят получить упражнения 3 и 4.  Упражнение 1 Создайте отдельный журнал событий для вашего приложения. Запишите в него значения, прочитайте их и очистите журнал.
	Упражнение 2 Преобразуйте строку с паролем в объект SecureString. С помощью этого SecureString запустите процесс, например Notepad или Excel.
	Упражнение 3 Запустите процесс без передачи параметров в командной строке. Теперь выполните ту же операцию, но с передачей параметров.
	Упражнение 4 С помощью кода из занятия 4 выполните несколько запросов, чтобы изучить системные ресурсы вашего компьютера. Аналогично запросите объекты ресурсов, не рассматривавшихся в этом занятии.
Пробный экзамен
На прилагаемом компакт-диске содержится пробный экзамен, обеспечивающий широкие возможности по самоподготовке. Например, можно проверить усвоение тем этой главы либо всех тем экзамена 70-536. Можно сымитировать настоящий экзамен либо выбрать режим обучения, когда после каждого вопроса приводятся правильные ответы с пояснениями.
Пробный экзамен
Подробнее о пробном экзамене рассказано во введении.
ГЛАВА 11
Безопасность приложений
Занятие 1. Защита доступа по правам кода	533
Занятие 2. Декларативная защита сборок	554
Занятие 3. Декларативная и императивная защита методов	567
Все слышали, что не следует постоянно работать под учетной записью администратора. Причина этого не в том, что вы не можете доверять себе, а в том, что вы не можете доверять запускаемым приложениям. При запуске неуправляемого приложения в Windows Server 2003, Windows ХР и ранних версиях Windows оно получает все привилегии, которые имеет учетная запись пользователя, запустившего его. Если вы случайно запустите вирус или троян, вредоносный код сможет сделать все, на что есть разрешение у вашей учетной записи. Поэтому следует входить в систему с минимумом привилегий, необходимых для работы.
Защита доступа по правам кода (Code access security, CAS) — концепция .NET Framework, позволяющая управлять разрешениями отдельных приложений. Если друг отправил вам новый текстовый редактор для .NET Framework, вы можете ограничить возможности этого приложения, разрешив ему только открывать окно и предлагать открыть или сохранить файл. Текстовый редактор не сможет отправлять сообщения по электронной почте, загружать данные на Web-сайты или создавать файлы, даже если вы зашли в систему с правами администратора.
CAS позволяет пользователям очень подробно определять, что может делать управляемый код. Разработчик должен понимать, как создавать приложения, которые будут работать, даже если им не хватает некоторых разрешений. При помощи CAS можно также повышать уровень безопасности приложения, указывая, какой вызывающий код может его использовать, и принудительно ограничивать его разрешения.
Темы экзамена:
	Реализация защиты доступа по правам кода для повышения уровня безопасности приложений .NET Framework (см. пространство имен System.Security):
□	класс SecurityManager,
□	класс CodeAccessPermission',
532 Безопасность приложений
Глава 11
□	изменение политики защиты доступа по правам кода на уровне компьютера, пользователя и предприятия при помощи утилиты Caspol.exe (Code Access Security Policy);
□	классы PermissionSet и NamedPermissionSets
□	стандартные интерфейсы защиты.
	Управление разрешениями на доступ к ресурсам при помощи классов System.Security. Permission (см. пространство имен System.Security.Permission):
□	класс Security Permissions
□	класс PrincipalPermissions
□	класс FilelOPermissions
□	класс StrongNameIdentity Permissions
□	класс UlPermissions
u класс UrlldentityPermissions
□	класс PublisherldentityPermissions
□	класс Gac Identity Permissions
□	класс FileDialogPermissions
□	класс DataProtectionPermissions
□	класс Environmentpermissions
□	интерфейс lUnrestrictedPermissions
□	класс RegistryPermissions
□	класс IsolatedStorageFilePermissions
□	класс KeyContainerPermissions
□	класс Reflectionpermissions
□	класс Store Permissions
□	класс SiteldentityPermissions
□	класс ZoneldentityPermission.
	Управление привилегиями кода при помощи классов System.Security.Policy (см. пространство имен System.Security.Policy):
□	классы ApplicationSecuritylnfo и Applicationsecurity Manager,
□	классы Application Trust и Application TrustCollection;
□	классы Evidence и PermissionRequestEvidences
□	классы CodeGroup, FileCodeGroup, FirstMatchCodeGroup, NetCodeGroup и UnionCodeGroups
□	классы Conditions
□	классы Policy Level и Policy Statement;
□	интерфейсы lApplication TrustManager, I Membershipcondition и HdentityPermissionFactory.
Прежде всего
Чтобы освоить материал этой главы, необходимо иметь навыки работы с Microsoft Visual Basic или C# и уметь:
	создавать консольные приложения в Microsoft Visual Studio на языках Visual Basic или С#;
	добавлять к проекту ссылки на системные библиотеки классов;
	выполнять запись в файлы и потоки.
Занятие 1
Защита доступа по правам кода 533
Занятие 1. Защита доступа по правам кода
Те, кто работал с предыдущими версиями .NET Framework, должны быть знакомы с концепциями защиты доступа по правам кода (Code Access Security, CAS). Но разработчикам, ранее не использовавшим .NET Framework, для применения CAS придется изучить совершенно новые концепции защиты. Это занятие посвящено концепциям, на которых основана модель CAS, и компонентам .NET Framework, используемым для реализации CAS.
Изучив материал этого занятия, вы сможете:
J объяснить предназначение CAS;
перечислить важнейшие составляющие CAS и обосновать их необходимость;
J описать, как политика безопасности определяет разрешения, выдаваемые сборке;
J объяснить принципы работы CAS в системе безопасности операционной системы;
J применять утилиту .NET Framework Configuration для настройки CAS;
J настраивать CAS при помощи утилиты Caspol.
Продолжительность занятия — 60 минут.
Что такое защита доступа по правам кода?
Защита доступа по правам кода (CAS) — это система, позволяющая администраторам и разработчикам контролировать авторизацию приложений аналогично тому, как они управляют авторизацией пользователей. Используя CAS, можно разрешить отдельному приложению чтение и запись в реестр, не предоставляя такого доступа другим приложениям. Можно управлять доступом к большинству из тех, защита которых в операционной системе основана на ролях (role-based security, RBS). Сюда можно отнести следующее:
	файловая система;
	реестр;
	принтеры;
	журналы событий.
Кроме этого, можно ограничивать доступ к ресурсам, которыми нельзя управлять при помощи RBS. Например, можно разрешить или запретить отдельному приложению отправлять данные в Интернет или посылать DNS-запросы. Именно такие действия наиболее типичны для вредоносного ПО, поэтому имеет смысл ограничить подобную активность при помощи CAS.
К сожалению, С AS можно применять только для управляемых приложений, которые работают в исполняющей среде .NET Framework. На неуправляемые приложения ограничения CAS не влияют, такие приложения можно ограничивать только при помощи RBS операционной системы. Если разрешения сборки ограничены с использованием CAS, говорят о частичном доверии. Если сборка с частичным доверием обращается к защищенному ресурсу, она должна пройти проверку разрешений CAS, прежде чем получит доступ. Некоторые сборки освобождены от проверок CAS. В этом случае говорят о полном доверии. Сборки с полным доверием, как и неуправляемый код, имеют доступ ко всем ресурсам, для которых у пользователя есть разрешения.
534 Безопасность приложений
Глава 11
Элементы CAS
Любая система защиты, включая CAS, должна иметь способ идентификации пользователей и определения их полномочий. Однако CAS управляет разрешениями, назначенными приложениям, а не людям, поэтому не может использовать привычную модель с именами пользователей, паролями и списками управления доступом (ACL).
Вместо этого CAS идентифицирует сборки при помощи доказательств (evidence). Доказательством, идентифицирующим сборку, может быть место хранения сборки, хэш ее кода или цифровая подпись. По предоставленному доказательству определяется, к какой группе кода принадлежит сборка. Разрешения на доступ к ресурсам назначаются группам кода. В следующем разделе мы рассмотрим каждое из упомянутых доказательств.
Доказательства
Доказательство — это сведения о сборке, при помощи которых исполняющая среда определяет, в какие группы кода входит сборка. Обычно в качестве доказательства используется папка или Web-сайт, откуда была запущена сборка, а также цифровая подпись.
ПРИМЕЧАНИЕ Терминология
Идентификационные данные или удостоверения — более подходящий термин, чем доказательство. Услышав слово «доказательство», можно подумать, что оно обозначает набор сведений-улик, позволяющих отследить того, кто хочет сохранить инкогнито. В CAS же доказательства используются как паспорт, пароль или PIN-код, т. е. как информация, подтверждающая личность и определяющая допустимый уровень доверия.
В табл. 11-1 перечислены общие типы доказательств, которые хост может предоставить исполняющей среде. Каждая строка таблицы соответствует члену класса из пространства имен System.Security.Policy.
Табл. 11-1. Типы доказательств
Доказательство	Описание
Каталог приложения	Каталог, в котором находится сборка
Хэш	Криптографический хэш сборки, уникально идентифицирующий ее версию. Любые изменения в сборке делают хэш недействительным
Издатель	Цифровая подпись издателя сборки, уникально идентифицирующая разработчика ПО. Сборка должна иметь цифровую подпись
Сайт	Сайт, с которого была загружена сборка (например, www.microsoft.com)
Строгое имя	Криптографическое строгое имя сборки, уникально идентифицирующее пространство имен сборки. Сборка должна быть подписана
URL	Адрес URL, с которого сборка была загружена (например, www.microsoft.com/assembly.exe)
Зона	Зона, в которой запущена сборка (например, зона Internet или Locallntranet)
Занятие 1
Защита доступа по правам кода 535
Существует два типа доказательств: доказательства хоста и доказательства сборки. Доказательство хоста описывает происхождение сборки, например, каталог приложения, адрес URL или сайт. Доказательство хоста также может описывать издателя сборки, ее хэш или строгое имя. Доказательство сборки — это особое доказательство, предоставляемое пользователем или разработчиком.
Разрешения
Разрешение — это запись в списке управления доступом CAS. Например, разрешение File Dialog (рис. 11-1) определяет, может ли сборка открыть диалоговое окно Open (Открыть), диалоговое окно Save (Сохранить), оба этих окна или ни одно из них. На рис. 11-1 показана конфигурация разрешения
По умолчанию утилита .NET Framework Configuration позволяет настроить 19 разрешений, каждое из которых соответствует двум членам пространства имен System.Security.Permissions', один для императивного использования и один для декларативного. Эти разрешения перечислены в табл. 11-2. Кроме того, можно добавлять собственные разрешения.
Рис. 11-1. Разрешения определяют, какие действия сборка может выполнять, а какие нет
Табл. 11-2. Разрешения, доступные по умолчанию
Разрешения
Описание
Directory Services (Службы каталогов)
DNS
Environment Variables
(Переменные окружения)
Предоставляет сборке доступ к Active Directoiy. Можно указать пути, по которым разрешен доступ на просмотр (Browse) или запись (Write) Разрешает или запрещает подавать запросы DNS Предоставляет сборкам доступ к переменным окружения (таким как Path, Username и Number_Of_Processors). Можно предоставить доступ сразу ко всем переменным окружения или только к некоторым. Чтобы просмотреть список переменных окружения^ из командной строки выполните команду Set
536 Безопасность приложений
Глава 11
Табл. 11-2. (продолжение)
Разрешения	Описание
Event Log (Журнал событий)	Предоставляет сборке доступ к журналам событий. Можно предоставить неограниченный доступ или разрешить только просмотр или аудит
File Dialog (Файловый диалог)	Определяет, может ли сборка показывать пользователю диалоговое окно Open (Открыть) и/или Save (Сохранить)
File 10 (Файловый ввод-вывод)	Ограничивает доступ к файлам и папкам. Можно предоставить сборке неограниченный доступ или указать список путей и соответствующих разрешений — Read, Write, Append или Path Discovery
Isolated Storage File (Изолированное хранилище файлов)	Предоставляет сборкам доступ к изолированному файловому хранилищу. Можно настроить уровень изоляции и размер дисковой квоты
Message Queue (Очередь сообщений)	Предоставляет сборкам доступ к очередям сообщений. Можно устанавливать ограничения по типу доступа или пути
Performance Counter (Счетчик производительности) Printing (Печать)	Определяет, может ли сборка считывать или записывать счетчики производительности Ограничивает возможности печати, доступные сборке
Reflection (Отражение)	Определяет, может ли сборка обнаружить в других сборках информацию о членах и типах
Registry (Реестр)	Ограничивает доступ к разделам реестра. Можно предоставить сборке неограниченный доступ или указать список разделов и соответствующие разрешения — Read, Write или Delete
Security (Защита)	Позволяет подробно определять уровень доступа сборки к различным функциям С AS. Для всех сборок должен быть установлен как минимум параметр Enable Assembly Execution (Разрешить выполнение сборки). Также определяет, может ли сборка вызывать неуправляемый код, назначать разрешения, управлять потоками, а также выполнять некоторые другие действия
Service Controller (Контроллер службы) Socket Access (Доступ к сокетам)	Определяет, какие службы могут просматривать сборку или управлять ей Определяет, может ли сборка устанавливать TCP/IP-соединения. Можно настраивать пункт назначения, номер порта и протокол
SQL Client (Клиент SQL)	Определяет, может ли сборка обращаться к серверу SQL и разрешены ли пустые пароли
User Interface (Пользовательский интерфейс)	Определяет, может ли сборка создавать новые окна и получать доступ к буферу обмена
Занятие 1
Защита доступа по правам кода
537
Табл. 11-2. (окончание)
Разрешения	Описание
Wfeb Access (Доступ к Vveb)	Определяет, может ли сборка получать доступ к Web-сайтам, и если может, то к каким
Х509 Store (Хранилище Х509)	Предоставляет сборкам доступ к хранилищу сертификатов Х509 и определяет, могут ли сборки добавлять, удалять и открывать хранилища сертификатов
Наборы разрешений
Набор разрешений — это список управления доступом (ACL) для CAS. Например, набор разрешений Internet, доступный по умолчанию, включает следующие разрешения:
	File Dialog;
	Isolated Storage File;
	Security;
	User Interface;
	Printing.
Зона Locallntranet включает больше разрешений, так как предполагается, что код, из локальной сети, заслуживает больше доверия, чем код из Интернета:
	Environment Variables;
	File Dialog;
	Isolated Storage File;
	Reflection;
	Security;
	User Interface;
	DNS:
	Printing.
В .NET Framework по умолчанию включено несколько наборов разрешений (см. табл. 11-3).
Табл. 11-3. Наборы разрешений, доступные по умолчанию
Наборы разрешений	Описание
FullTrust	Освобождает сборку от проверок разрешений CAS
SkipVeriflcation	Позволяет сборке пропустить проверку разрешений, что может повысить производительность, но в ущерб безопасности
Execution Nothing	Разрешает запуск сборки и не дает других разрешений Не предоставляет сборке никаких разрешений. Не разрешен даже запуск
Locallntranet	Предоставляет сборке широкий набор разрешений, включая доступ к журналу событий и возможность печати. Важно заметить, что сборка не получает доступа к файловой системе, за исключением диалоговых окон Open (Открыть) и Save (Сохранить)
538 Безопасность приложений
Глава 11
Табл. 11-3. (окончание)
Наборы разрешений	Описание
Л Internet	Предоставляет сборке ограниченный набор разрешений. В большинстве случаев для большей безопасности рекомендуется запускать сборку с этим набором разрешений. Даже если будет запущена сборка с вредоносным кодом, она не сможет причинить серьезного вреда
Everything	Предоставляет сборке все разрешения. Отличается от набора FullTrust, с которым сборка пропускает все проверки CAS. Сборки, имеющие набор разрешений Everything, продолжают проходить проверки
Группы кода
Группы кода — это устройство авторизации, связывающее сборки с наборами разрешений. Группы кода в CAS выполняют ту же функцию, что и группы пользователей в RBS. Например, если администратор хочет предоставить пользователям доступ к папке, он создает группу пользователей, добавляет в нее пользователей, а затем назначает этой группе нужные разрешения. Группы кода работают аналогично, за тем исключением, что не нужно вручную добавлять в группу отдельные сборки. Вместо этого членство в группе определяется доказательством, которое указано, как условие членства.
Например, любой код, запущенный из зоны Internet, должен входить в группу кода Intemet_Zone. Как показано на рис. 11-2, условием членства в группе кода Intemet_Zone является предоставление сборкой доказательства Zone, которое говорит о том, что сборка запущена из зоны Internet.
Рис. 11-2. Условием членства в группе кода Internet_Zone является доказательство Zone
Занятие 1
Защита доступа по правам кода 539
Тогда как труппы пользователей управляют авторизацией на основе распределенных ACL, связанных с каждым ресурсом, группы кода используют централизованные наборы разрешений. Например, на рис. 11-3 показано, что группа кода Intemet_Zone назначает набор разрешений Internet. Для удобства в этом диалоговом окне перечислены все входящие в набор разрешения. Однако отдельные разрешения назначать группам кода нельзя. Группа кода должна быть связана с набором разрешений.
Рис. 11-3. Группа кода Internet_Zone назначает набор разрешений Internet
СОВЕТ Работа с файлами
Приложения, запущенные в зонах Internet и Locallntranet, не получают разрешения FilelOPermission, а значит, не могут получать доступ к файлам напрямую. Однако они получают разрешение FileDialogPermission. Следовательно, в зоне Internet сборки могут открывать файлы, предлагая пользователю выбрать файл, используя объект OpenFileDia-log. Сборки в зоне Locallntranet могут также сохранять файлы, используя объект SaveFileDialog. Чтобы получить доступ к файлам, если нет разрешения FilelOPermission, вызовите метод ShowDialog класса OpenFileDialog или SaveFileDialog. Чтобы получить доступ к выбранному пользователем файлу, нужно использовать описатель файла, возвращенный методом OpenFile.
Возможность указать для группы кода только один тип доказательства и один набор разрешений может показаться ограничением. Однако как учетная запись пользователя может входить сразу в несколько групп, так и сборка может входить в несколько групп кода. Сборка получит все разрешения, назначенные группам кода, в которые она входит. Кроме того, группы кода можно вкладывать друг в друга и назначать разрешения только в том случае, если сборка удовлетворяет всем требованиям родительской и дочерней групп кода. Вкладывая группы кода, можно назначать разрешения на основании нескольких типов доказательств. На рис. 11-4 показана группа кода Microsoft_Strong_Name, вложенная в группу My_Computer_Zone, которая, в свою очередь, вложена в группу All_Code.
540 Безопасность приложений
Глава 11
Рис. 11-4. Вложение групп можно использовать для предъявления нескольких требований к доказательствам
В табл. 11-4 перечислены группы кода, размещенные в группе All_Code и доступные по умолчанию. Некоторые из этих групп кода также содержат вложенные группы.
Табл. 11-4. Группы кода, доступные по умолчанию
Группа кода	Доказательство	Набор разрешений
My_Computer Zone	Zone: My Computer	FuIlTrust
LocalIntranet_Zone	Zone: Local Intranet	Locallntranet
IntemetZone	Zone: Internet	Internet
Restncted_Zone	Zone: Untrusted sites	Nothing
Trusted_Zone	Zone: Trusted sites	Internet
Политика безопасности
Политика безопасности — логическая совокупность групп кода и наборов разрешений. Политика безопасности также может содержать особые сборки, определяющие другие типы политик. Политики безопасности предоставляют администраторам гибкий инструмент для настройки параметров CAS на многих уровнях. По умолчанию есть четыре уровня настройки политики: уровень предприятия, компьютера, пользователя и домена приложения (о доменах приложения см. в главе 8).
Самый высокий уровень политики безопасности — уровень предприятия, описывающий политику всего предприятия. Политику безопасности предприятия можно настраивать при помощи службы каталогов Active Directory. Политика компьютера, второй уровень политики безопасности, применяется ко всему коду, который выполняется на определенном компьютере. Третий уровень — политика, определяющая разрешения отдельных пользователей. Исполняющая среда отдельно проверяет политики на уровнях предприятия, компьютера и пользователя и предоставляет сборке минимальный набор разрешений, назначенный на каком-то из этих уровней (в этом случае говорят о
Занятие 1
Защита доступа по правам кода 54-|
пересечении наборов разрешений). По умолчанию политики безопасности уровня предприятия и пользователя считают весь код кодом с полным доверием, и таким образом ограничения CAS устанавливаются только на уровне компьютера.
Преимущества многоуровневой политики безопасности
Чтобы понять, как применяются политики безопасности, рассмотрим ситуацию с разработчиком приложений, который загрузил из Интернета сборку и хочет с ней поэкспериментировать. Разработчик скачал сборку на локальный компьютер, так что она будет запускаться в зоне Му Computer. Компьютер разработчика входит в домен Active Directory. Администратор этого домена создал в политике безопасности предприятия группу кода, которая предоставляет сборкам локального компьютера набор разрешений Everything. Этот набор накладывает больше ограничений, чем набор FullTrust, установленный на уровне компьютера, и поэтому имеет приоритет.
Разработчик не уверен, безопасно ли запускать эту сборку, и хочет применить набор разрешений Internet, чтобы сборка не могла записывать данные на диск или передавать по сети. Он не вошел в систему под учетной записью администратора, но может запустить утилиту .NET Framework Configuration и изменить политику безопасности на уровне пользователя (обычные пользователи не могут изменять политику безопасности на уровне компьютера). Изменив политику безопасности пользователя, разработчик может назначить сборкам в зоне Му Computer набор разрешений Internet. Сборки, запускаемые этим разработчиком, будут ограничены, и это никак не повлияет на сборки других пользователей того же компьютера.
Сборка входит в три группы кода: управляемую политикой безопасности предприятия, политикой безопасности компьютера и политикой безопасности пользователя. Исполняющая среда определяет разрешения сборки, сравнивая наборы разрешений каждой группы и применяя наиболее ограничивающий из них (пересечение). Так как наборы FullTrust и Everything содержат все разрешения из набора Internet (плюс некоторые другие), наиболее ограничивающим является набор Internet.
Взаимодействие CAS и системы безопасности операционной системы
CAS полностью независима от системы безопасности операционной системы. Фактически для администрирования CAS используются совершенно другие инструменты. Разрешения пользователя или группы пользователей можно настраивать при помощи Проводника Windows, но для ограничения разрешений сборки нужно воспользоваться утилитой .NET Framework Configuration.
CAS работает на основе встроенной системы безопасности операционной системы. При определении действий, которые может выполнять сборка, используется и CAS, и система безопасности операционной системы. Действует наиболее ограничивающий набор разрешений. Например, если CAS дает сборке разрешение на запись в папку C:\Windows\, а пользователь, запустивший сборку, не имеет таких разрешений, сборка не сможет произвести запись в эту папку. На рис. 11-5 показано, как CAS соотносится с системой безопасности операционной системы.
542
Безопасность приложений
Глава 11
Рис. 11-5. CAS дополняет, но не заменяет защиту на основе ролей
Подготовка к экзамену
Сборка не может получить больше разрешений, чем имеет запустивший ее пользователь, независимо от того, как она использует CAS.
Настройка CAS при помощи утилиты .NET Framework Configuration
Утилита .NET Framework Configuration предоставляет графический интерфейс для управления политикой безопасности .NET Framework и приложениями, которые используют службы удаленного доступа. Можно выполнять многие задачи, связанные с CASr включая следующие:
	проверка сборки для определения ее принадлежности к группам кода;
	проверка сборки для определения разрешений, которые будут ей назначены;
	добавление наборов разрешений;
	добавление групп кода;
	повышения уровня доверия к сборке,
	настройка уровня безопасности зоны;
	изменение уровня политики безопасности.
К СВЕДЕНИЮ Утилита .NET Framework Configuration
В этой главе рассматривается применение.NET Framework Configuration только для управления политикой CAS. Подробнее об этом инструменте — в главе 9.
В следующем разделе описывается, как выполнять эти задачи.
Занятие 1
Защита доступа по правам кода 543
Определение групп кода, назначающих разрешения сборке
При решении проблем с разрешениями CAS бывает нужно определить, какие группы кода назначают разрешения сборке. Чтобы сделать это, запустите утилиту .NET Framework Configuration и выполните следующее:
1.	Щелкните Runtime Security Policy.
2.	Щелкните Evaluate Assembly. Появится окно мастера Evaluate An Assembly.
3.	На странице What Would You Like To Evaluate щелкните Browse. Выберите сборку и щелкните Open.
4.	Щелкните View Code Groups That Grant Permissions To The Assembly. Затем щелкните Next.
5.	Раскройте каждый узел, представляющий уровень политики, чтобы определить, какие группы кода назначают разрешения сборки. На рис. 11-6 показана сборка, получающая разрешения от группы кода My_Computer_Zone.
Рис. 11-6. Мастер Evaluate An Assembly используется для определения групп кода, назначающих сборке разрешения
6.	Щелкните Finish.
Определение результирующих разрешений CAS, предоставленных сборке
При решении проблем с разрешениями CAS может понадобиться определить, какие разрешения будут предоставлены сборке. Чтобы сделать это, запустите утилиту .NET Framework Configuration и выполните следующее:
1.	Щелкните Runtime Security Policy.
2.	Щелкните Evaluate Assembly. Запустится мастер Evaluate An Assembly.
3.	На странице What Would You Like To Evaluate щелкните Browse. Выберите сборку и щелкните Open.
4.	Щелкните View Permissions Granted To The Assembly. Затем щелкните Next.
544 Безопасность приложений
Глава 11
5.	Мастер отобразит каждое разрешение, выданное сборке. Чтобы просмотреть подробные сведения о разрешении, выберите нужное и щелкните кнопку Mew Permission.
6.	Щелкните Finish.
Добавление набора разрешений
Чтобы создать новый набор разрешений, запустите утилиту .NET Framework Configuration и выполните следующее:
1.	Раскройте узел Runtime Security Policy.
2.	Раскройте узел Enterprise, Machine или User, в зависимости от того, для политики какого уровня нужно назначить набор разрешений.
3.	Щелкните Permission Sets. В правой части окна щелкните Create New Permission Set.
4.	На странице Identify The New Permission Set введите имя и описание набора. Щелкните Next.
5.	На странице Assign Individual Permissions То Permission Set выполните следующее: А. Выберите разрешение, которое нужно добавить в набор, и щелкните Add.
В.	Для каждого разрешения выберите уникальные параметры и щелкните ОК.
С.	Повторите эти действия для каждого разрешения, включенного в набор.
6.	Щелкните Finish.
Добавление группы кода
Чтобы добавить группу кода, запустите утилиту .NET Framework Configuration и выполните следующее:
1.	Раскройте узел Runtime Security Policy.
2.	Раскройте узел Enterprise, Machine или User, в зависимости от того, для политики какого уровня нужно определить группу кода.
3.	Раскройте узел Code Groups, раскройте All_Code и просмотрите дочерние группы кода. Если группа кода, которую нужно создать, будет определять подмножество разрешений для существующей группы кода, выберите ее. В противном случае выберите группу All_Code.
4.	Щелкните Add A Child Code Group.
5.	На странице Identify The New Code Group введите имя и описание группы и щелкните Next.
6.	На странице Choose A Condition Туре выберите тип условия, при помощи которого исполняющая среда будет идентифицировать код. Щелкните Next.
7.	Если один из существующих наборов соответствует вашим требованиям, на странице Assign A Permission Set То The Code Group выберите параметр Use Existing Permission Set. В противном случае щелкните Create A New Permission Set. Щелкните Next.
8.	Если выбран параметр Create A New Permission Set, сделайте следующее:
А.	На странице Identify The New Permission Set введите имя и описание набора. Щелкните Next.
В.	На странице Assign Individual Permissions То Permission Set выберите разрешения, которые хотите добавить в набор, и щелкните Add. Для каждого разрешения укажите уникальные параметры и щелкните ОК. Щелкните Next
9.	На странице Completing The Wizard щелкните Finish.
Занятие 1
Защита доступа по правам кода 545
Повышение уровня доверия к сборке
Если на компьютере ограничены разрешения CAS по умолчанию, может понадобиться повысить уровень доверия к некоторым сборкам, чтобы предоставить им дополнительные разрешения. Чтобы сделать это, запустите утилиту .NET Framework Configuration и выполните следующее:
1.	Щелкните Runtime Security Policy.
2.	Щелкните Increase Assembly Trust. Будет запущен мастер Trust An Assembly.
3.	На странице What Would You Like To Modify щелкните Make Changes To This Computer, чтобы изменить политику на уровне компьютера, или Make Changes For The Current User Only, чтобы изменить политику только на уровне пользователя. Щелкните Next. Только администраторы имеют право изменять политику на уровне компьютера.
4.	На странице What Assembly Do You Whnt To Thist щелкните Browse. Выберите сборку, которой хотите доверять, и щелкните Open. Доверять можно только сборкам, имеющим строгое имя. Щелкните Next.
5.	На странице Choose The Minimum Level Of Trust For The Assembly выберите минимальный уровень доверия к сборке. Щелкните Next.
6.	На странице Completing The Wizard проверьте выбранные параметры и щелкните Finish.
Настройка безопасности зоны
По умолчанию .NET Framework включает пять зон, каждая из которых имеет уникальный набор разрешений CAS. Рекомендуется использовать эти зоны всегда, когда возможно, но можно изменить набор разрешений, определенных для зоны. Чтобы сделать это, запустите утилиту .NET Framework Configuration и выполните следующее:
1.	Раскройте узел Runtime Security Policy, Machine, Code Groups, All_Code.
2.	Выберите зону, которую нужно настроить. В правой части окна щелкните Edit Code Group Properties.
3.	Перейдите на вкладку Permission Set (рис. 11-7) и в списке Permission Set выберите нужный набор разрешений. Щелкните ОК.
Рис. 11-7. Настройка разрешений зоны
§46 Безопасность приложений
Глава 11
Разработчик в первую очередь должен настроить набор разрешений, назначенный группе кода My_Computer_Zone. По умолчанию назначен набор Full Trust, то есть любые требования CAS, установленные в приложениях, будут игнорироваться. Нужно назначить набор Everything, который содержит похожие разрешения, но учитывает некоторые требования CAS. Если нужно применить к сборкам другие ограничения, можно выбрать другой набор разрешений.
Сброс уровней политики
Иногда после внесения изменений нужно восстановить уровни политики, которые были установлены по умолчанию. Чтобы сделать это, запустите утилиту .NETFramework Configuration и выполните следующее:
1. Щелкните Runtime Security Policy. В правой части окна щелкните Reset All Policy Levels.
2. Щелкните Yes, а затем OK.
Утилита .NET Framework Configuration восстановит исходные параметры уровня политики, в том числе удалит созданные наборы разрешений.
Утилита Caspol
При помощи утилиты Caspol.exe (Code Access Security Policy) можно просматривать и изменять политики безопасности доступа к коду на уровнях компьютера, пользователя и предприятия. Хотя утилита .NET Framework Configuration лучше всего подходит для ручной настройки политики, Caspol позволяет выполнять то же из командной строки.
СОВЕТ Caspol
Caspol позволяет настраивать огромное количество параметров, но мы рассмотрим только несколько наиболее часто используемых. Чтобы получить подробные инструкции, выполните в командной строке следующую команду:
Caspol -?.
Параметры Caspol
Caspol предоставляет очень широкий набор параметров. В табл. 11-5 перечислены наиболее часто используемые из них. Параметры —addgroup и —chggroup принимают дополнительные параметры в виде условий членства и флагов. Условия членства, описанные в табл. 11-6, являются доказательствами, которые используются .NET Framework для определения группы кода, к которой относится сборка. Флаги определяют имя, описание и другие параметры (табл. 11-7).
Табл. 11-5. Параметры Caspol
Параметр	Описание
-addfulltrust файл_сборки	Добавляет сборку, которая реализует объект системы
безопасности (например, разрешение или условие членства), в список сборок с полным доверием, находящихся на определенном уровне политики. Аргумент файл_сборки указывает сборку, которую нужно добавить. Этот файл должен быть подписан при помощи строгого имени
Занятие 1
Защита доступа по правам кода 547
Табл. 11-5. (продолжение)
Параметр	Описание
-addgroup имя_родительской_группы условие членства имя_набора^разрешений [флаги]	Добавляет новую группу кода. Аргумент имя_родительской_группы указывает имя группы кода, которая будет родительской для добавляемой группы. Аргумент условие_членства указывает условие членства в новой группе кода (табл. 11-6). Аргумент имя_набора-разрешений определяет имя набора разрешений, который будет связан с новой группой кода. Для новой группы можно также указать один или несколько флагов (табл. 11-7)
-all Г*	Указывает, что все параметры, следующие за этим, должны применяться к политикам на уровне предприятия, компьютера и текущего пользователя
-chggroup имя {условие_членства | имя_набора^разрешений ]флаги]	Изменяет условие членства в группе кода, набор разрешений или параметры флагов exclusive, levelfinal, пате и description. Аргумент имя указывает имя группы кода, которую нужно изменить. Аргумент имя набора-разрешений указывает имя набора разрешений, которое нужно связать с группой кода. Описание аргументов условие членства и флаги см. в табл. 11-6 и 11-7
-enterprise	Указывает, что все параметры применяются к политике уровня предприятия. Пользователи, не являющиеся администраторами предприятия, не имеют прав изменять политику предприятия, хотя могут просматривать ее
-execution {on | off}	Включает и отключает механизм проверки разрешений перед запуском кода
-help -list	Отображает синтаксис команд и параметры Caspol Отображает иерархию групп кода и наборы разрешений, связанных с политикой указанного уровня или всех уровней
-listdescription	Отображает описания всех групп кода указанного уровня политики
-listfulltrust	Отображает содержимое списка сборок с полным доверием на определенном уровне политики
-listgroups	Отображает группы кода в политике указанного уровня или в политиках всех уровней. Сначала выводится идентификатор группы кода, а затем имя, если оно есть
-listpset	Отображает наборы разрешений в политике указанного уровня или в политиках всех уровней
548
Безопасность приложений
Глава 11
Табл. 11-5. (окончание)
Параметр	Описание
-machine	Указывает, что все параметры применяются к политике уровня компьютера. Пользователи, не являющиеся администраторами, не имеют прав изменять политику компьютера, хотя могут просматривать ее. Для администраторов параметр -machine является параметром по умолчанию
-quiet	Временно отключает приглашение, отображающееся при применении параметра, приводящего к изменениям
-recover	Восстанавливает политику из файла резервной копии. При каждом изменении политики Caspol сохраняет копию старой политики в резервном файле
-remgroup имя	Удаляет указанную группу кода. Если эта группа имеет дочерние группы, Caspol также удаляет и их
-rempset имя_набораразрешений	Удаляет из политики указанные разрешения. Аргумент имя_набораразрешений указывает, какой набор нужно удалить. Caspol удаляет набор только в том случае, если он не связан с группой кода. Нельзя удалить встроенные наборы разрешений
-reset	Возвращает политику в состояние по умолчанию
-resolvegroup файлрборки	Показывает группы кода, которым принадлежит указанная сборка (файлрборки)
-resolveperm файлрборки	Отображает все разрешения, которые политика безопасности предоставит сборке (файлрборки), если разрешить запуск этой сборки
-security {on | off}	Включает или отключает защиту доступа к коду. Когда защита отключена, удовлетворяются все запросы на доступ к коду
-user	Указывает, что все параметры применяются к политике на уровне пользователя, на правах которого выполняется Caspol. Для пользователей, не являющихся администраторами, этот параметр устанавливается по умолчанию
_?	Отображает синтаксис команд и параметров Caspol
Табл. 11-6. Условия членства Caspol	
Условие членства	Что указывает
-all	Весь код
-appdir	Каталог приложения. Если указано это условие членства, URL, предоставленный в качестве доказательства кода, сравнивается с каталогом приложения, также предоставленным в качестве доказательства. Условие членства выполняется, если оба доказательства одинаковы
Занятие 1
Защита доступа по правам кода 54g
Табл. 11-6. (окончание)
Условие членства	Что указывает
-hash алгоритм хэширования	Код с указанным хэшем сборки Чтобы использовать
{-hex значение_хэша |	хэш в качестве условия членства, нужно указать либо
-file файл_сборки }	значение хэша, либо файл сборки
-pub	Код, чей издатель указан файлом сертификата,
{ -cert имяфайла сертификата	подписью файла или шестнадцатеричным
-file имя подписанного_файла | -hex шестнадцатеричная_строка }	представлением сертификата Х509
-site веб-сайт	Код, полученный с указанного сайта. Например: -site www.microsoft.com
-strong -file имя файла {name	Код, имеющий строгое имя файла, имя сборки,
| -noname} {version | - noversion}	указанное в виде строки, и версию сборки в формате major.minor.build.revision. Например: -strong -file myAssembly.exe myAssembly 1.2.3.4
-url URL	Код, полученный из источника с указанным адресом URL. В адрес URL должен входить протокол (например, http:// или ftp://). Чтобы указать несколько сборок, полученных из источника с определенным URL, можно использовать символ групповой подстановки (*). Если нужно указать имя файлового ресурса в сети, используется следующий синтаксис: - url \\имя_сервера\имя_ресурса\/. Символ * необходим для корректной идентификации ресурса
-zone имязоны	Код, полученный из указанной зоны. Аргумент имязоны может принимать следующие значения: MyComputer, Intranet, Trusted, Internet и Untrusted
Табл. 11-7. Флаги Caspol	
Флаг	Описание
-description "описание"	Если используется с параметром -addgroup, указывает описание группы кода, которую нужно добавить. Если используется с параметром -chggroup, указывает описание группы кода, которую нужно изменить. Описание должно заключаться в кавычки, даже если в нем нет пробелов
-exclusive {on|off}	Если этот параметр включен (on), он указывает, что при проверке условия членства в добавляемой или изменяемой группе кода проверяется только набор разрешений, связанный с этой группой. Если параметр отключен (off), Caspol проверяет наборы разрешений,
	связанные со всеми соответствующими группами кода на данном уровне политики
rjcjQ Безопасность приложений
Глава 11
Табл. 11-7. (окончание)
Флаг	Описание
-levelfinal {on|off}	Если этот параметр включен (on), указывает, что не рассматривается никакая политика, уровень которой ниже, чем уровень политики, на котором добавляется или изменяется группа кода. Обычно это параметр используется на уровне политики компьютера. Например, если этот флаг установлен для группы кода на уровне компьютера и какой-то код соответствует условию членства в этой группе, Caspol не применяет для этого кода политику, определенную на уровне пользователя
-name "имя”	Если используется с параметром -addgroup, указывает имя группы кода, которую нужно добавить. Если используется с параметром -chggroup, указывает имя группы кода, которую нужно изменить. Аргумент имя ррлжЕВ. заключаться в кавычки, даже если не содержит пробелов
Применение Caspol для решения типичных задач
Ниже приведен список примеров использования Caspol для выполнения типичных задач.
	Предоставление сборке полного доверия:
Caspol — addfulltrust assemblyname.exe
Например, чтобы предоставить сборке C:\Program Files\Mine\Mine.exe полное доверие, нужно выполнить следующую команду:
Caspol -addfulltrust "C:\Program Files\Mine\Mine.exe"
	Добавление группы кода в политику на уровне компьютера:
Caspol -machine -addgroup условиячленствав_родителъской_группе наборразрешений -name "имягруппы"
Например, чтобы добавить группу кода с именем My_Code_Group в труппу All_Code из политики уровня компьютера, назначающую разрешения Locallntranet, используя в качестве доказательства URL \\devserver\devshare, нужно выполнить следующую команду (требуются административные привилегии):
Caspol -machine -addgroup All_Code -url \\devserver\devshare\* Locallntranet
-name "My_Code_Group"
	Добавление группы кода в политику пользователя:
Caspol -user -addgroup условие_членства_в_группе_кода наборразрешений -name "имягруппы"
Чтобы добавить группу кода с именем User_Code_Group в группу All_Code из политики уровня пользователя, назначающую разрешения FullTrust, используя в качестве доказательства сайт www.contoso.com, нужно выполнить следующую команду:
Caspol -user -addgroup All_Code -site www.contoso.com FullTrust -name "User_Code_Group"
Занятие 1
Защита доступа по правам кода	55 -|
ПРИМЕЧАНИЕ Обновление утилиты .NET Framework Configuration
Чтобы увидеть изменения, внесенные при помощи Caspol, нужно закрыть утилиту .NET Framework Configuration и открыть ее снова. Но если открыта утилита .NET Framework Configuration, зачем использовать CaspoC
	Настройка безопасности зоны для политики компьютера:
Caspol -chggroup группа_кода набор^разрешений
Например, чтобы изменить политику безопасности My_Computer_Zone на уровне компьютера так, чтобы она использовала набор разрешений Intranet, выполните следующую команду (требуются административные привилегии):
Caspol -chggroup My_Computer_Zone Locallntranet
	Сброс политики на уровне компьютера:
Caspol -recover
Практикум. Настройка CAS
Сейчас вы настроите CAS при помощи графической утилиты .NET Framework Configuration и утилиты командной строки Caspol. Выполните упражнения 1-3. Чтобы внесенные изменения не повлияли на дальнейшую работу, на последнем шаге упражнения 3 будут восстановлены исходные параметры.
Упражнение 1. Компиляция сборки и тестирование ее разрешений
В этом упражнении вы скомпилируете сборку и протестируете разрешения сборки в ограниченной зоне My_Computer.
1.	Войдите в систему под учетной записью администратора.
ПРИМЕЧАНИЕ Вход в систему под учетной записью администратора
Другие упражнения этой главы, как и большинство задач, следует выполнять под учетной записью обычного пользователя. Это упражнение является исключением, так как в нем используется ресурс С$, к которому по умолчанию имеют доступ только администраторы. Если вы создали новый ресурс, к которому имеют доступ обычные пользователи, можете войти в систему под обнчной учетной записью.
2.	При помощи Проводника Windows скопируйте папку Chapter 1 l\ListPermissions с прилагаемого к книге диску в папку Му Documents\Visual Studio 2005\Projects\. Можете выбрать версию для C# или Visual Basic.
3.	В Проводнике Windows выберите папку Му Documents\Visual Studio 2005\Projects\ и дважды щелкните файл ListPermissions.csproj или ListPermissions.vbproj. Проект ListPermissions откроется в Visual Studio .NET 2005.
4.	В меню Build выберите Build Solution. Приложение скомпилируется.
5.	Скопируйте файл ListPermissions.exe в корень диска С:.
6.	Откройте командную строку и выполните команду С:\ListPermissions. ехе. Проект ListPermissions запустится и покажет список разрешений, а также сообщит, имеет ли сборка эти разрешения. Вы имеете все перечисленные разрешения. Нажмите Enter. Ответьте на следующие вопросы.
□ Почему сборка имеет все эти разрешения?
Сборка работает в зоне My_Computcr_Zone, так как запущена с диска С:. По умолчанию эта зона использует набор разрешений FullTrust.
552 Безопасность приложений
Глава 11
7.	Выполните команду \\127.0.0.1 \c$\ListРеrmissions. exe. Обратите внимание, теперь не хватает нескольких разрешений, в частности, разрешения IsolatedStorageFilePermission. Нажмите Enter. Ответьте на следующие вопросы.
□ Почему сборка не имеет некоторых разрешений? Какая группа кода предоставляла эти разрешения?
Теперь сборка запущена из общей папки, поэтому работает в зоне Internet. Так как используемый IP-адрес — это специальный адрес замыкания на себя, он входит в группу кода Internet—Same_Site_Access.
Упражнение 2. Создание группы кода и набора разрешений при помощи утилиты .NET Framework Configuration
В этом упражнении при помощи утилиты .NET Framework Configuration вы создадите группу кода с новым набором разрешений.
1.	Запустите утилиту .NET Framework 2.0 Configuration. Раскройте узел Runtime Security Policy, Machine, Code Groups, All_Code.
2.	Щелкните All_Code, а затем в правой части окна щелкните Add A Child Code Group.
3.	В поле Name введите Local_Shared_Folder. В поле Description введите Code Run From A Network Drive Mapped To The Local Shared C: Drive (Код, запущенный с сетевого диска, присоединенного к локальному диску С:). Щелкните Next.
4.	На странице Choose A Condition Туре выберите URL. В поле URL введите f ile: // 127.0.0.1/с$/* (рис. 11-8) и щелкните Next.
Рис. 11-8. Используйте адрес URL в качестве условия, чтобы указать группы кода для сборок, запускаемых из общих папок *
5.	На странице Assign A Permission Set То The Code Group выберите параметр Create A New Permission Set. Щелкните Next.
6.	На странице Identify The New Permission Set в поле Name введите Gene rousPermissions. В поле Description введите Permissions For The ListPermissions Assembly (Разрешения сборки ListPermissions). Щелкните Next.
Занятие 1
Защита доступа по правам кода 553
7.	На странице Assign Individual Permissions То Permission Set дважды щелкните Isolated Storage File. В диалоговом окне Permission Settings выберите Grant Assemblies Unrestricted Access To File-Based Storage. Щелкните OK, а затем Next.
8.	На странице Completing The Wizard щелкните Finish.
9.	Откройте командную строку и выполните команду \\127.0 0 1\c$\LislPe rmissions.exe. Заметьте, что теперь сборка ListPermission имеет разрешение IsolatedStorageFilePermission. Нажмите Enter. Ответьте на следующие вопросы.
□ Почему теперь сборка имеет разрешение IsolatedStorageFilePermission?
Сборка запущена из двух групп кода: Local_Shared_Folder и Intemet_Same_Site_Access. К уже существующим разрешениям добавились разрешения из набора Generous Permissions.
Упражнение 3. Изменение группы кода при помощи утилиты Caspol
и восстановление параметров по умолчанию
В этом упражнении вы измените новую группу кода при помощи утилиты Caspol, протестируете изменения, а затем восстановите параметры по умолчанию.
1.	Откройте командную строку Visual Studio 2005 и выполните следующую команду, чтобы изменить разрешения группы кода Local_Shared_Folder, связав с ней набор Everything'.
Caspol -chggroup Local_Shared_Folder Everything
2.	Когда будет предложено, нажмите Y и Enter.
3.	Выполните команду \\ 127.0.0 1\c$\Listpe г missions. Заметьте, что сборка теперь имеет все разрешения, а это говорит о том, что в группу кода включен набор разрешений Everything.
4.	Восстановите исходные параметры CAS, выполнив команду Caspol -recover.
Резюме
	CAS — это система безопасности, авторизующая управляемые сборки на доступ к системным ресурсам.
	CAS реализуется следующими четырьмя компонентами:
□	доказательство, идентифицирующее сборку;
□	разрешения, описывающие, к каким ресурсам сборка может получить доступ;
□	наборы разрешений, объединяющие различные разрешения;
□	группы кода, назначающие сборке разрешения, исходя из предоставленных доказательств.
	Политика безопасности — логическая совокупность групп кода и наборов разрешений. Можно использовать многоуровневую систему политик безопасности, чтобы упростить администрирование CAS. Сборки получают наиболее ограничивающий из всех наборов на всех уровнях политик.
	Разрешения CAS не могут перекрывать разрешения операционной системы, назначенные пользователю. Действующими разрешениями сборки является общее подмножество разрешений, предоставленных сборке в CAS и пользователю в операционной системе.
554 Безопасность приложений
Глава 11
 Утилита .NET Framework Configuration — графическая утилита, позволяющая настраивать любые параметры CAS. Чтобы использовать эту утилиту, запустите Microsoft .NET Framework Configuration из группы Администрирование (Administrative Tools).
 Caspol (Code Access Security Policy) — утилита командной строки, позволяющая настраивать множество аспектов поведения CAS. Чтобы использовать утилиту Caspol, запустите ее из папки, в которую установлена .NET Framework.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Какие из следующих доказательств требуют, чтоб сборка была подписана? (Укажите все верные ответы.) А. Зона.
В. Строгое имя.
С. Хэш.
D. Издатель.
2.	Какие разрешения должна иметь сборка, чтобы подключиться к веб-серверу?
A. SocketPermission.
В. Web Permission.
С. DnsPermission.
D. ServiceControllerPermission.
3.	Какая из следующих групп кода накладывает наиболее строгие ограничения?
A. My_Computer_Zone.
В. LocalIntranet_Zone.
С. Internet Zone.
D. Restricted_Zone.
4.	Ваша учетная запись имеет доступ на чтение файла text.txt. Вы запустили сборку в группе кода MyComputerZone, которая предоставляет набор разрешений FullTrust. Какие из следующих действий сборка сможет выполнять с этим файлом?
Читать.В. Выполнять запись.
С. Изменять разрешения.
D. Удалять.
Занятие 2. Декларативная защита сборок
Из занятия 1 вы узнали, что при помощи CAS можно ограничивать разрешения приложений. Так что можно запускать приложения в частично доверенном контексте безопасности. Иногда защита CAS налагает столько ограничений, что приложение не имеет разрешений, чтобы выполнять даже базовые функции, так что исполняющая среда должна определять это и не разрешать запуск сборки. Иногда приложение получает больше
Занятие 2
Декларативная защита сборок 555
разрешений, чем необходимо, нарушая тем самым принцип наименьших привилегий, и подвергается ненужному риску.
Чтобы обеспечить сборку всеми необходимыми разрешениями, но не предоставлять ненужных, можно применить декларативные требования CAS. Дополнительным преимуществом является то, что администраторы, развертывающие приложение, могут проверять декларативные требования CAS, предъявляемые сборкой, чтобы определить минимальные разрешения, необходимые для нормальной работы приложения.
Изучив материал этого занятия, вы сможете:
J описать, почему следует использовать объявления сборок CAS;
J перечислить встроенные в .NET Framework классы для реализации разрешений CAS;
J перечислить три типа объявлений сборок CAS;
J создавать объявления сборок С AS;
J привести рекомендации по эффективной реализации объявлений сборок CAS.
Продолжительность занятия — 45 минут.
Причины использования объявлений сборок CAS
Для использования объявлений сборок CAS есть три основных причины:
	Уверенность в том, что сборка не станет запускать приложение, если не имеет доступа к необходимым ресурсам.
Если приложение не имеет функций проверки на случай, если сборке не хватает разрешений CAS, используйте SecurityAction.RequestMinimum для объявления всех разрешений CAS, необходимых приложению для работы. Если пользователь пытается запустить приложение, но политика безопасности CAS не дает необходимого разрешения, исполняющая среда генерирует исключение. Получив исключение, пользователи могут не понять, в чем заключается проблема, но администраторы должны это понимать. В любом случае лучше использовать SecurityAction.RequestMinimum, чем получать во время работы приложения неожиданные исключения.
	Создание для приложения небольшой «песочницы», которая не позволит злоумышленникам манипулировать приложением, чтобы получить доступ к ресурсам, не предназначенным для использования.
' Применение принципа наименьших привилегий снижает вероятность того, что злоумышленник воспользуется сборкой, чтобы выполнить несанкционированные действия, такие как просмотр частных файлов, уничтожение данных или внедрение вирусов и червей. Используя объявления сборок CAS для ограничения разрешений до необходимого минимума, можно снизить вероятность того, что кто-то воспользуется приложением для доступа к закрытым ресурсам. Это снижает вероятность типичных атак, таких как атаки, основанные на предоставлении приложению поддельного файла.
	Проверка работоспособности приложения с ограниченными разрешениями, то есть возможности его запуска в зонах с частичным доверием.
Пока нет простого способа определить разрешения, требуемые приложением. Однако, если приложение разработано и протестировано при помощи объявлений CAS, созданных с использованием SecurityAction.RequestOptional, исполняющая среда пре-
556 Безопасность приложений
Глава 11
доставит сборке только указанные разрешения. Если добавить код, требующий дополнительных разрешений, исполняющая среда сгенерирует исключение System.Security.Policy.PolicyException и укажет требуемое разрешение. После этого можно добавить еще одно объявление CAS, предоставив разрешение, требуемое новым кодом.
Классы для реализации разрешений CAS
При помощи CAS можно ограничивать доступ к ресурсам различного типа — от файлов и папок до принтеров и сетевых ресурсов. Каждый тип ресурсов, которые можно защитить, в .NET Framework представлен отдельным классом. В табл. 11-8 перечислены все классы, используемые для создания объявлений сборок CAS, и права, которые эти классы представляют.
ПРИМЕЧАНИЕ Использование атрибутов
В .NET Framework также имеются классы атрибутов для каждого из классов, перечисленных в табл. 11-8. Классы атрибутов в именах имеют слово Attribute. Однако при написании кода об этом волноваться не нужно, так как .NET Framework автоматически использует атрибуты классов, если на этот класс имеется декларативная ссылка.
ПРИМЕЧАНИЕ .NET 2.0
Из перечисленных в табл. 11-8 классов следующие впервые появились в .NET Framework
2 0: Data Protection Permission, GacldentityPermission, KeyContainerPermission и StorePermission.
Табл. 11-8. Классы, используемые для объявлений сборок CAS
Класс	Представляемые права
AspNetHostingPermission	Доступ к ресурсам на хостах ASP.NET
DataProtectionPermission	Доступ к шифрованным данным и памяти. Этот класс появился в .NET Framework 2.0
DirectoryServicesPermission	Доступ к классам System.DirectoryServices
DnsPermission	Доступ к DNS
Environment Permission	Чтение и запись переменных окружения
EventLogPermission	Чтение и запись служб журналов событий
FileDialogPermission	Доступ к файлам, выбранным пользователем
	в диалоговом окне Open
FilelOPermission	Чтение, добавление или запись файлов или папок
GacIdentityPermission	Определяет разрешения идентификатора для файлов из кэша глобальных сборок. Этот класс появился в .NET Framework 2.0
IsolatedStorageFilePermission	Доступ к изолированному хранилищу, которое связано с определенным пользователем и каким-то аспектом идентификатора кода, таким как Wfeb-сайт, издатель или подпись	у
lUnrestnctedPermission	Интерфейс, позволяющий назначать разрешения, не накладывающие ограничений
Занятие 2
Декларативная защита сборок 557
Табл. 11-8. (окончание)
Класс	Представляемые права
KeyContainerPermission	Доступ к контейнерам открытого ключа. Этот класс появился в NET Framework 2.0
MessageQueuePermission	Доступ к очередям сообщений через управляемые интерфейсы Microsoft Message Queuing (MSMQ)
OdbcPermission OleDbPermission	Доступ к источникам данных ODBC Доступ к базам данных при помощи OLE DB.
Oracle Permission	Доступ к базам данных Oracle
PerformanceCounterPermission PrincipalPermission	Доступ к счетчикам производительности Управляет доступом на основе имен пользователей и членства в группах. Подробнее — в главе 12
PrintingPermission ReflectionPermission RegistryPermission	Доступ к принтерам Получает информацию о типе во время выполнения Позволяет читать, записывать, создавать и удалять ключи и параметры реестра
SecurityPermission	Выполняет и добавляет разрешения, вызывает неуправляемый код, пропускает проверку и т. д.
ServiceControllerPermission	Доступ к запущенным или остановленным службам
SiteldentityPermission	Определяет разрешения идентификатора для Wfeb-сайта, являющегося источником кода
SocketPermission	Устанавливает и принимает запросы на подключение, используя транспортный адрес
SqlClientPermission StorePermission	Доступ к базам данных SQL Доступ к хранилищам, содержащим сертификаты Х.509. Этот класс появился в .NET Framework 2.0
StrongName Identity Permission	Определяет разрешения идентификатора для строгих имен
UlPermission	Доступ к функциям пользовательского интерфейса. Требуется для отладки сборки
UrlldentityPermission	Определяет разрешения идентификатора для адреса URL, являющегося источником кода
WebPermission	Устанавливает и принимает запросы на подключение, используя адрес Vfeb-узла
ZoneldentityPermission	Определяет разрешения идентификатора для зоны, являющейся источником кода
Члены каждого класса уникальны, и их можно использовать для дальнейшего управления разрешениями. Например, при помощи свойства OleDbPermissionAttributeAllowBlankPassword можно определить, может ли сборка использовать пустой пароль. Аналогично, при помощи свойства DirectoryServicesPermissionAttribute, Path можно ограничить сборке доступ к отдельным ветвям структуры Active Directory. (Из-за их огромного количества мы не можем рассмотреть все классы и свойства.)
558 Безопасность приложений
Глава 11
Так как разрешения классов атрибутов наследуются от класса CodeAccessSecurityAt-tribute, они имеют некоторые свойства и методы. Однако обычно достаточно всего двух стандартных свойств:
	Action
Указывает действия, которые нужно выполнить для защиты. Определятся при помощи перечислимого SecurityAction;
	Unrestricted
Значение типа Boolean, предоставляющее доступ ко всем разрешениям класса. Если оно равно true, это эквивалентно установке параметра Grant Assemblies Unrestricted Access To The File System при определении параметров разрешений при помощи утилиты .NET Framework Configuration.
Типы объявлений разрешений сборки
Все классы атрибутов определяют свойство Action, которое указывает, как исполняющая среда будет обрабатывать разрешения. При создании объявлений сборок CAS всегда нужно задавать свойству Action значение, равное одному из трех членов перечислимого SecurityAction. Ниже приведено описание этих трех членов.
	SecurityAction. RequestMinimum
Требует разрешений на запуск сборки Если сборка не имеет указанных разрешений CAS, исполняющая среда генерирует исключение System.Security.Policy.PolicyException.
	SecurityAction. RequestOptional
Отзывает все разрешения, не перечисленные в объявлении Security Action. RequestOptional или SecurityAction.RequestMinimum. Определение разрешений при помощи этого действия гарантирует, что приложение не получит разрешений сверх объявленных. Если сборка не имеет запрошенных разрешений CAS, исполняющая среда не будет генерировать исключение, в отличие от случая с SecurityAction.RequestMinimum. Следовательно, если приложение не может нормально функционировать, когда ему не хватает разрешений, используйте объявления SecurityAction.RequestMinimum и SecurityAction.RequestOptional совместно.
	SecurityAction.RequestRefuse
Ограничивает разрешения, предоставленные приложению. Используйте этот тип объявления, чтобы запретить приложению доступ к критическим ресурсам, которые можно использовать для атаки. В отличие от SecurityAction.RequestMinimum, это объявление никогда не приводит к тому, что исполняющая среда генерирует исключение во время загрузки.
ПРИМЕЧАНИЕ Путаница с названиями
Названия этих объявлений могут привести вас в замешательство. Почему объявление называется RequestMinimum, если на самом деле оно является требованием? Имя RequestMinimum звучит так, будто код вежливо запрашивает нужные разрешения. Исходя из поведения этого объявления, его следовало бы назвать RequireMinimum, так как исполняющая среда не «любезничает», получив запрос на разрешения, которые не может предоставить, она просто генерирует исключение и отказывается запускать сборку. Кроме того, исполняющая среда никогда не предоставит коду разрешений, которых он не должен получить в любом случае. Объявление RequestOptional следовало бы назвать RefuseAUExcept, так как его основным предназначением является явное перечисление только тех разрешений CAS, которые приложение должно получить.
Занятие 2
Декларативная защита сборок 55g
Создание объявлений сборок
В следующем примере кода показана сборка, требующая доступа на чтение файла C:\boot.ini. Если политика безопасности не предоставляет сборке этого разрешения, исполняющая среда сгенерирует исключение, прежде чем запускать сборку.
' VB
Imports System.Security.Permissions
<Assembly: FileIOPermissionAttribute(SecuntyAction. RequestMinimum, Read := "C:\boot.ini")>
Module Modulel
Sub Main()
Console.WriteLine("Hello, World!”)
End Sub
End Module
11 C#
using System.Security.Permissions;
[assembly:FileIOPermissionAttribute(SecurityAction.RequestMinimum,
Read=@"C:\boot.ini")]
namespace DeclarativeExample
{
class Program
{
static void Main(string[] args)
{
Console.WriteLineC"Hello, World!");
}
}
}
ПРИМЕЧАНИЕ Заявленные требования и требования реальные
Сборка в этом примере на самом деле не обращается к файлу С: \boot. ini. Объявления CAS предъявляют совершенно произвольные требования. Сделать так, чтобы объявленные требования соответствовали требованиям приложения, — задача разработчика.
В предыдущем примере объявление SecurityAction.RequestMinimum позволяет исполняющей среде .NET Framework сгенерировать исключение, если сборка не имеет разрешений CAS на чтение файла С:\boot.ini. Это гарантирует, что сборка не будет запущена, если исполняющая среда не предоставит ей требуемых разрешений, предотвратив тем самым проблемы, которые могут возникнуть при работе приложения. Однако генерирование исключения не повышает уровень защиты сборки, так как при этом не выполняется ничего, что ограничивало бы разрешения сборки.
Подготовка к экзамену
На экзамене помните, что CAS имеет значение только для сборок с частичным доверием. Для сборок с полным доверием исполняющая среда полностью игнорирует объявления CAS.
560 Безопасность приложений
Глава 11
Для повышения уровня защиты сборки укажите перечислимые SecurityAction.RequestOptional или SecurityAction.RequestRefuse в качестве значения свойства Action нужного разрешения. Для одной сборки можно комбинировать несколько объявлений. Например, если нужно, чтобы исполняющая среда генерировала исключение, когда нет доступа к ключу реестра HKEY_ LOCAL_MACHINE\Software и не нужно предоставлять других разрешений CAS (конечно, за исключением разрешения Enable Assembly Execution), следует использовать следующие объявления:
’ VB
<Assembly: RegistryPeemission(SecurityAction.RequestMinimum,
Read:="HKEY_LOCAL_MACHINE\Software")>
<Assembly: UIPermission(SecurityAction.RequestOptional,
Un rest ricted:=True)>
<Assembly: RegistryPermission(SecurityAction.RequestOptional,
Read:="HKEY_LOCAL_MACHINE\Software")>
// C#
[assembly:RegistryPermission(SecurityAction.RequestMinimum,
Read=@"HKEY_LOCAL_MACHINE\Software")]
[assembly: UIPermission(SecurityAction.RequestMinimum, Unrestricted = true)] [assembly: RegistryPemission(SecurityAction.RequestOptional,
Read=@"HKEY_LOCAL_MACHINE\Software")]
Если используются какие-либо объявления SecurityAction.RequestOptional и нужно отладить сборку, необходимо объявить также атрибут UlPermission и задать параметру Unrestricted значение True, как показано во второй строке предыдущего фрагмента кода. В качестве значения атрибута UlPermission можно указать SecurityAction.RequestOptional или Security Action. Request Minimal. В противном случае сборка не будет иметь разрешений на взаимодействие с отладчиком. Однако можно будет запустить сборку без отладчика.
Можно комбинировать RequestMinimum, RequestOptional и Request Refuse, но комбинация RequestOptional и RequestRefuse не приведет ни к чему. Объявление RequestOptional отказывает во всех разрешениях, кроме явно указанных. Единственный случай, в котором следует комбинировать RequestOptional и RequestRefuse — если нужно отклонить набор разрешений, указанный в объявлении RequestOptional.
Например, следующие объявления приведут к генерации исключения в том случае, если сборка не имеет разрешений CAS на печать. Исполняющая среда будет отклонять все разрешения CAS, за исключением печати, открытия Windows и доступа к файловой системе диска С:. Доступ к каталогу C:\Windows также будет отклонен.
' VB
<Assembly: PrintingPermission(SecurityAction.RequestMinimum)>
<Assembly: UIPermission(SecurityAction.RequestOptional,
Un rest ricted:=True)>
<Assembly: FileIOPermissionAttribute(SecurityAction.RequestOptional, Read:="C:\")>
<Assembly: FileIOPermissionAttribute(SecurityAction.RequestRefuse,
Read:=”C:\Windows\")>
Занятие 2
Декларативная защита сборок 55 -|
// C#
[assembly PrintingPermission(SecurityAction.RequestMinimum)]
[assembly: UIPermission(SecurityAction.RequestMinimum,
Unrestricted = true)]
[assembly: FileIOPermissionAttribute(SecurityAction.RequestOptional, Read = @”C:\")]
[assembly: FileIOPermissionAttribute(SecurityAction.RequestRefuse,
Read = @"C:\Windows\")]
Если используются какие-то объявления Security Action. RequestOptional, нужно также объявить атрибут UlPermission и задать параметру Unrestricted значение True, чтобы можно было запускать сборку в отладчике, как показано во второй строке предыдущего примера. В противном случае сборка не будет иметь разрешений на взаимодействие с отладчиком.
СОВЕТ Использование объявлений сборок SecurityAction. RequestOptional
Есть много книг о том, как писать защищенный код, в которых можно найти много рекомендаций по его написанию. Но мы ведь просто люди. Иногда разработчик может забыть проверить входные данные или строго определить тип, а иногда просто торопится закончить проект и проявляет небрежность.
Использование объявлений SecurityAction.RequestOptional — одна из рекомендаций, которую всегда следует выполнять. Чтобы написать такое объявление, времени нужно немного, ведь это всего пара строк. Их отладку не трудно выполнять, поскольку исполняющая среда генерирует исключение, причину которого легко определить. Кроме того, они защищают каждую строку кода в сборке. Когда сборка увеличивается в размере, эта защита ослабевает, поэтому следует использовать объявления методов, как рассказывается на занятии 3.
Рекомендации по использованию объявлений сборок
Эти рекомендации помогут определить, какие объявления сборок CAS нужно использовать:
	используйте объявления SecurityAction.RequestMinimum, чтобы определить требования для каждого необходимого сборке разрешения, наличие которого сборка не проверяет;
	используйте объявления SecurityAction.RequestOptional, чтобы перечислить все разрешения, используемые сборкой Опишите разрешения как можно подробнее, включая сведения о файлах и ключах реестра, к которым требуется доступ;
	используйте объявления Security Action. Request Refuse для более подробного определения разрешений, перечисленных в объявлениях SecurityAction.RequestOptional.
Практикум. Использование запросов на разрешения сборок
В следующих упражнениях вы создадите объявления CAS, обеспечивающие сборку необходимыми привилегиями и позволяющие снизить риск, которому подвергается сборка при запуске.
Упражнение. Объявление требований к защите
В этом упражнении вы измените существующую сборку, добавив объявления CAS.
562 Безопасность приложений
Глава 11
1.	Скопируйте из папки Chapterl l\Lesson2-Exercise 1 компакт-диска версию проекта для C# или Visual Basic и откройте ее.
2.	Просмотрите код, чтобы определить, какие разрешения необходимы. Добавьте в сборку пространство имен Sy stem. Security. Permissions, чтобы можно было объявить требуемые разрешения С AS.
3.	Добавьте объявления CAS, перечисляющие минимальные требуемые разрешения, чтобы исполняющая среда генерировала исключение, если сборке не хватает необходимых разрешений. Для этого подойдет следующий код:
’ VB
<Assembly: UlPemission(SecurityAction.RequestMinimum, Unrestrioted:=True)>
<Assembly: FileIOPermission(SecurityAction.RequestMinimum,
ViewAndModify:="C:\Hello.txt")>
11 C#
[assembly: UIPermission(SecurityAction.RequestMinimum,
Unrestricted = true)]
[assembly: FileIOPermission(SecurityAction.RequestMinimum,
ViewAndModify = C:\Hello.txt")]
ПРИМЕЧАНИЕ .NET 2.0
Атрибут All класса FilelOPermission заменен на атрибут ViewAndModify.
4.	Запустите сборку и проверьте, работает ли она.
5.	Измените объявления CAS, чтобы отозвать все разрешения, кроме требуемых, и злоумышленники не смогли бы воспользоваться сборкой для выполнения несанкционированных действий. Код может быть следующим:
’ VB
<Assembly: UIPermission(SecurityAction.RequestOptional,
Un rest ricted:=True)>
<Assembly: FileIOPermission(SecurityAction.RequestOptional,
ViewAndModify:="C:\Hello.txt")>
// C#
[assembly: UIPermission(SecurityAction.RequestOptional,
Unrestricted = true)]
[assembly: FileIOPermission(SecurityAction.RequestOptional,
ViewAndModify = ©"C:\Hello.txt")]
6.	Запустите сборку и проверьте, корректно ли она работает.
Обратите внимание, можно использовать SecurityAction.RequestOptional, чтобы проверить, что объявлены все разрешения, требуемые сборкой, даже если планируете объявить требования к разрешениям CAS с использованием SecurityAction.RequestMinimum. Например, если на шаге 3 этого упражнения определить атрибут FilelOPermission, используя Read вместо ViewAndModify, исполняющая среда не будет генерировать исключение, даже если сборка будет записывать в файл. Однако при изменении объявления SecurityAction.RequestOptional исполняющая среда генерирует исключение, показывая, что объявлены не все требуемые разрешения.
Занятие 2
Декларативная защита сборок 553
Резюме
	Используйте объявления сборок CAS, так как они позволяют администраторам просматривать разрешения, требуемые приложением, запрещают запуск приложения, если оно не имеет достаточных разрешений, ограничивают разрешения, предоставленные приложению, и позволяют изолировать приложение, чтобы проверить возможность его запуска в зонах с частичным доверием.
	В .NET Framework включено более десятка классов для реализации разрешений CAS, описывающих такие ресурсы, как файловая система, реестр и принтеры.
	Объявления сборок CAS могут быть трех типов: RequestMinimum, RequestOptional и RequestRefuse.
	Чтобы создать объявление сборки, добавьте атрибуты сборки, используя классы разрешений.
	Используйте объявления RequestMinimum, если приложение не имеет функций проверки наличия разрешений. Используйте объявления RequestOptional для перечисления разрешений, требуемых приложением. Объявления RequestRefuse используйте для дальнейшего огранчения разрешений, перечисленных в объявлениях RequestOptional.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Администратор запускает следующее приложение с набором разрешений Everything. Каковы будут выходные данные этого приложения?
' VB
<Assembly: UIPermission(SecurityAction.RequestOptional, _
Un rest rioted:=True)>
<Assembly: FileIOPermissionAttribute(SecurityAction.RequestOptional, _ Read:="C:\")>
<Assembly: FileIOPermissionAttribute(SecurityAction.RequestRefuse, _
Read:="C:\Windows\")>
Module Modulel
Sub Main()
Console.WriteLine("Reading one line of the boot.ini file:")
Dim sr As StreamReader = New StreamReader("C:\boot.ini")
Console.WriteLine("First line of boot.ini: " + sr.ReadLine)
End Sub
End Module
// C#
[assembly: UIPermission(SecurityAction.RequestOptional,
Unrestricted = true)]
[assembly: FileIOPermissionAttribute(SecurityAction.RequestOptional,
564 Безопасность приложений
Глава 11
Read = @"С:\")] [assembly: FileIOPermissionAttribute(SecurityAction.RequestRefuse,
Read = @"C:\Windows\")]
namespace console_cs2
{
class Program
{
static void Main(string[] args)
Console.WriteLine("Reading one line of the boot.ini file:’’);
StreamReader sr = new StreamReader(@"C:\boot.ini");
Console.WriteLineC’First line of buot.ini: " + sr.ReadLineO); }
}
}
A.
Unhandled Exception: System.Security.SecurityException: Request for the permission of type ’System.Security.Permissions.
FilelOPemission, mscorlib, Version=2.0 0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089’ failed.
B.
Reading one line of the boot.ini file:
Unhandled Exception: System.Security.SecurityException: Request for the permission of type ’System.Security.Permissions.
FilelOPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' failed.
C.
Reading one line of the boot.ini file:
First line of boot.ini: [boot loader]
D. Перед началом выполнения приложения возникнет необработанное исключение Security Exception.
2.	Администратор запускает консольное приложение с набором разрешений Everything. Каковы будут выходные данные этого приложения?
’ VB
<Assembly: UIPermission(SecurityAction.RequestOptional,
Unrestricted:=True)>
<Assembly: FilelOPermissionAttribute(SecurityAction.RequestOptional, Read:="C:\Temp")>
<Assembly: FileIOPermissionAttribute(SecurityAction.RequestRefuse,
Read: =’’C: \Windows\" )>
Module Mooulel
Sub Main()
Console.WriteLine("Reading one line of the boot.ini file:") Dim sr As StreamReader = New StreamReader("C:\boot.ini")
Занятие 2
Декларативная защита сборок 555
Console.WriteLine("First line of boot.ini: " + sr.Headline) End Sub End Module
// C#
[assembly: UIPermission(SecurityAction.RequestOptional, Unrestricted = true)]
[assembly: FileIOPermissionAttribute(SecurityAction.RequestOptional,
Read = @"C:\Temp")]
[assembly: FileIOPermissionAttribute(SecurityAction.RequestRefuse,
Read = @"C:\Windows\")]
namespace console_cs2
{
class Program {
static void Main(string[] args) {
Console.WriteLineC"Reading one line of the boot.ini file:");
StreamReader sr = new StreamReader(@"C:\boot.ini");
Console. WriteLine("First line of boot.ini: " + sr. ReadLineO);
} }
A.
Unhandled Exception: System.Security.SecurityException; Request for the permission of type 'System.Security.Permissions.
FilelOPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' failed.
B.
Reading one line of the boot.ini file;
Unhandled Exception: System.Security.SecurityException: Request for the permission of type 'System.Security.Permissions.
FilelOPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' failed.
C.
Reading one line of the boot.ini file:
First line of boot.ini: [boot loader]
D.
Перед началом работы приложения возникнет необработанное исключение SecurityException.
3.	Администратор запускает следующее консольное приложение с набором разрешений Everything. Каковы будут выходные данные этого приложения?
' VB
<Assembly: UIPermission(SecurityAction.RequestMinimum,
566 Безопасность приложений
Глава 11
Unrestricted:=Т rue)>
<Assembly: FileIOPermissionAttribute(SecurityAction.RequestMinimum, Read:="C:\Temp")>
<Assembly: FileIOPermissionAttribute(SecurityAction,RequestRefuse,
Read: ="C:\Windows\")>
Module Modulel
Sub Main()
Console.WriteLine("Reading one line of the boot.ini file:")
Dim sr As StreamReader = New StreamReader("C:\boot.ini")
Console.WriteLine("First line of boot.ini: " + sr.ReadLine) End Sub
End Module
// C#
[assembly: UIPermission(SecurityAction.RequestMinimum, Unrestricted = true)]
[assembly: FileIOPermissionAttribute(SecurityAction.RequestMinimum, Read = @"C \Temp")]
[assembly: FilelOPermissionAttribute(SecurityAction.RequestRefuse, Read = @"C:\Windows\")]
namespace console_cs2 {
class Program
{
static void Main(string[] args)
{
Console.WriteLineC"Reading one line of the boot.ini file:");
StreamReader sr = new StreamReader(@"C \boot ini”);
Console.WriteLineC'First line of boot.ini: " + sr.ReadLineO);
} }
A.
Unhandled Exception: System.Security.SecurityException: Request for the permission of type 'System.Security.Permissions.
FilelOPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' failed.
B.
Reading one line of the boot.ini file:
Unhandled Exception: System.Security.SecurityException: Request for the permission of type 'System.Security.Permissions.
FilelOPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089’ failed.
Занятие 3
Декларативная и императивная защита методов 567
С.
Reading one line of the boot.ini file:
First line of boot.ini: [boot loader]
D.
Перед началом работы приложения возникнет необработанное исключение Security Exception.
4.	Какие из следующих разрешений требуются для запуска любых консольных приложений в отладчике?
A.	SocketPermission.
В.	WebPermission.
С. UlPermission.
D. FilelOPermission.
Занятие 3. Декларативная и императивная защита методов
Модель CAS можно использовать декларативно — в этом случае компилятор выполняет проверку защиты до запуска кода, либо императивно — в этом случае проверки защиты выполняет сам код и определяет, что должно происходить, если проверка не пройдена. На занятии 2 вы узнали, как использовать объявления CAS для защиты сборок. Можно также использовать CAS для декларативной защиты отдельных методов в сборке или секций кода внутри метода. На этом занятии вы узнаете, как и зачем использовать императивные и декларативные требования CAS для защиты кода внутри сборки.
Изучив материал этого занятия, вы сможете:
J перечислить типы запросов на разрешения методов;
J описать, как следует использовать запросы на разрешения методов для усиления защиты приложения;
J использовать CAS для запроса определенных разрешений для отдельных методов;
J ограничивать разрешения методов для уменьшения вероятности того, что метод будет использован для атаки;
J использовать метод Assert для снижения строгости разрешений и улучшения производительности;
J использовать наборы разрешений для запроса, ограничения и установки назначения нескольких разрешений одновременно,
Продолжительность занятия — 45 минут.
Типы запросов на разрешения методов
Хотя есть только три типа объявлений сборок CAS {RequestOptional, RequestMinimum и RequestRefuse), доступно шесть параметров для определения императивных и декларативных разрешений внутри метода. Ниже приводится описание этих параметров:
568 Безопасность приложений
Глава 11
	Assert
Приказывает исполняющей среде игнорировать тот факт, что вызывающий код может не иметь указанных разрешений. Для сборок должен быть установлен параметр безопасности разрешений Assemblies must have the Assert Any Permission That Has Been Granted.
	Demand
Приказывает исполняющей среде генерировать исключение, если вызывающий код и код, расположенный выше в стеке, не имеет указанных разрешений.
	Deny
Приказывает исполняющей среде ограничить доступ для метода путем удаления указанных разрешений.
	Inheritance Demand
Приказывает исполняющей среде генерировать исключение, если сборка, наследующая от класса, не имеет указанных разрешений.
	LinkDemand
Приказывает исполняющей среде генерировать исключение, если непосредственно вызывающий код, но не код, расположенный выше в стеке, не имеет указанных разрешений.
	PermitOnly
Приказывает исполняющую среду ограничить методу доступ, удалив все разрешения, кроме указанных.
Чтобы понять, как эти методы работают, можно привести аналогию с закрытой вечеринкой, на которую хотят попасть четыре гостя. Хозяин (метод) нанял вышибалу (исполняющая среда .NET Framework), который должен следить за тем, чтобы войти (вызывать метод) могли только гости (вызывающие сборки), имеющие приглашение (разрешение CAS).
Если хозяин вызвал метод InvitedGuests. LinkDemand, охранник проверит приглашение первого гостя, а затем пропустит всех остальных. Это быстро, но на вечеринку могут попасть люди без приглашения. Если вызван метод InvitedGuests.Demand, охранник проверит приглашение каждого гостя в отдельности. Это займет больше времени, но зато посторонние пройти не смогут.
Чтобы ускорить проверку приглашений, первый из приглашенных гостей должен использовать InvitedGuests.Assert, чтобы охранник был уверен, что все гости из группы были приглашены — предполагается, что охранник может положиться на приглашение первого гостя. При этом первый гость может привести с собой людей, не имеющих приглашения, что может быть хорошо, если хозяин хочет, чтобы на вечеринке было много народа, но не хочет раздавать много приглашений (которые могут попасть в плохие руки). Однако будет плохо, если какой-нибудь вор узнает, что он может попасть на вечеринку.
Если хозяин хочет, чтобы гости на вечеринке танцевали, а не делали что-то другое, он должен использовать Dancing.PermitOnly, чтобы проинструктировать охранника следить за тем, чтобы люди оставались на танцполе. Если хозяин хочет, чтобы люди делали все что угодно, но не танцевали, нужно использовать Dancing.Deny.
Занятие 3
Декларативная и императивная защита методов 5gg
Инструкции по применению запросов на разрешения методов
Разработчику доступно много вариантов реализации CAS в приложениях. Иногда бывает сложно выбрать вариант для конкретной ситуации. Следующие инструкции помогут в выборе метода:
	используйте объявления SecurityAction.PermitOnly, чтобы ограничить разрешения, доступные каждому методу. Перечислите все разрешения, требуемые методом;
	используйте объявления SecurityAction. Deny, чтобы подробнее определить разрешения, доступные каждому методу;
	используйте CodeAccessPermission.PermitOnly, чтобы императивно ограничить разрешения, если секция метода требует меньше разрешений, чем оставшаяся его часть. Это особенно важно, когда вызывающие объекты созданы сторонними производителями. Для восстановления разрешений используйте CodeAccessPermission.RevertPermitOnly,
	используйте CodeAccessPermission.Assert, если нужно разрешить коду с частичным доверием вызывать метод, требующий разрешений, которых может не иметь вызывающий код. Внимательно проверьте код на наличие потенциальных уязвимостей; параметр Assert может использоваться злоумышленниками для получения повышенных привилегий. После выполнения функций, требующих повышенных привилегий, исходные разрешения можно восстановить при помощи метода CodeAccessPermission.RevertAssert;
	используйте Code Access Permission. Demand только в том случае, если сборка реализует особые функции, не основанные на встроенной функциональности .NET Framework, например, вызов неуправляемого кода.
ПРИМЕЧАНИЕ Опасность декларативных требований
Существует мнение, что декларативные запросы менее безопасны, чем императивные, поскольку первые могут предоставить злоумышленникам слишком много сведений о коде и потенциальных уязвимостях. Действительно, декларативные запросы проще анализировать, но опытный взломщик может анализировать и императивные запросы, используя средства анализа IL-кода сборки. Анализировать IL-код сложнее, чем декларативные запросы, но для взломщика, достаточно опытного для того, чтобы воспользоваться сведениями о запросах, эта разница не имеет значения. Следует также учитывать, что декларативные запросы быстрее, чем императивные.
Способы запроса разрешений
Два перечислимых SecurityAction и два метода CodeAccessPermission позволяют исполняющей среде генерировать исключение, если отсутствуют разрешения CAS Demand и LinkDemand. Разница между двумя перечислимыми и методами в том, что Demand выполняет проверку разрешений, чтобы подтвердить доступ всего вызывающего кода, тогда как LinkDemand проверяет доступ только непосредственно вызывающего кода.
Чтобы понять разницу, сравните процедуру использования Demand, изображенную на рис. 11-9, с процедурой использования LinkDemand, изображенной на рис. 11-10. Как
570 Безопасность приложений
Глава 11
видите, Demand определяет, имеет ли весь вызывающий код все запрошенные разрешения и наборы разрешений, и генерирует исключение при их отсутствии. Это более безопасно, чем использование запроса LinkDemand, который проверяет только непосредственно вызывающий код Однако, как и в случае почти с любым механизмом защиты, есть свои недостатки. Demand требует, чтобы исполняющая среда выполняла больше проверок, а это требует больше ресурсов процессора и ухудшает производительность. Использование LinkDemand повышает производительность, но повышает риск того, что взломщик обойдет проверки защиты.
ВАЖНО! Проверка вызывающего кода при помощи Demand и LinkDemand
Demand и LinkDemand не проверяют разрешения текущего метода — проверяется вызывающий код. Однако, если сборка вызывает закрытый метод, который использует Demand или LinkDemand, исполняющая среда проверит разрешения сборки, так как в этом случае вызывающим кодом является сборка.
Исполняющая среда проверяет разрешения Assemblyl Methodi
Исполняющая среда проверяет разрешения Class2.Method2
©
Class3 Methods проверяет SecurityAction. Demand
Рис. 11-9. Demand проверяет разрешения всего вызывающего кода
Занятие 3
Декларативная и императивная защита методов 57,
Class3.Methods проверяет
SecurityAction. LinkDemand
Рис. 11-10. LinkDemand проверяет разрешения только у непосредственно вызывающего кода
Декларативные запросы разрешений CAS
Создание объявлений методов CAS очень похоже на создание объявлений сборок CAS. Однако нужно создавать объявления как атрибуты метода, а не сборки, и использовать различные перечислимые SecurityAction. Чтобы создать декларативный запрос, используйте один из классов, обсуждаемых в этой главе на занятии 2, с перечислимыми Security Action. Demand и SecurityAction. LinkDemand. В следующем примере показаны два метода, использующие классы FilelOPermissionAttribute (из пространства имен System.Security.Permissions) и Web Permission Attribute (из пространства имен System. Net), чтобы декларативно проверить, имеет ли код, вызывающий указанные методы, доступ к определенным файлам и веб-сайту www.microsoft.com.
' VB
<FileIOPermissionAttribute(SecurityAction.Demand, _
Write :s” СДР год ram Files\")> _
572 Безопасность приложений
Глава 11
Public Sub createProgramFolderO Код метода
End Sub *
<WebPe mission(Secu rityAction.Demand,
ConnectPatte rn:="http://www\.mic rosoft\.com/.*")>
Public Sub requestWebPageO Код метода
End Sub
// C#
[FileIOPermission(SecurityAction.Demand, Write = @"C:\Program Files\")] public static void createProgramFolderO
{
// Код метода
}
[WebPermission(SecurityAction.Demand,
ConnectPattern = @"http://www\.microsoft\.com/. *")] public static void requestWebPageO {
// Код метода
}
Если создать классы, от которых другие разработчики могут наследовать свои классы, можно ограничить список сборок, которые могут наследовать от этих классов, используя перечислимое SecurityAction.InheritanceDemand. Например, только сборки, подписанные сертификатом C:\Certificates\MyCertificate.cer, могут наследовать от следующего класса:
1 VB
<PublisherIdentityPermission(SecurityAction.InheritanceDemand,
CertFile:="C:\Certificates\MyCertificate.cer")> Public Class Protectedlnheritance
' Код класса End Class
// C#
[PublisherIdentityPermission(SecurityAction.InheritanceDemand.
CertFile = @”C:\Certificates\MyCertificate.cer")] public class Protectedlnheritance {
I/ Код класса
}
Тот же декларативный синтаксис можно использовать для защиты отдельных членов класса от переопределения в производном классе. Такой способ необходим, только если нужно обеспечить защиту отдельных членов на уровне более высоком, чем базовый класс.
Занятие 3
Декларативная и императивная защита методов 573
Императивные запросы разрешений CAS
Для каждого из перечислимых SecurityAction, используемых для определения объявлений CAS, имеется метод CodeAccessPermission с тем же именем и функциональностью, используемый для императивного определения разрешений. Для декларативного определения защиты используйте перечислимые SecurityAction, для императивного — методы CodeAccessPermission. В следующем примере выполняются те же проверки, что и в примере с использованием декларативных требований CAS, но в этот раз проверки выполняются императивно: ' VB
Public Class CASImperativeClass
Public Shared Sub createProgramFolderO
Try
Dim filePermissions As FilelOPermission =
New FileIOPermission(FileIOPermissionAccess.Write, "C:\Program Files\")
filePermissions.Demand()
' Код метода
Catch
' Код обработки ошибок
End Try
End Sub
Public Shared Sub requestWebPage()
Try
Dim connectPattern As Regex =
New Regex("http://www\.mic rosoft\.com/.*")
Dim webPermissions As WebPermission =
New WebPermission(NetworkAccess.Connect, connectPattern) webPermissions. DemandO
Код метода
Catch
' Код обработки ошибок
End Try
End Sub
End Class
// C#
public static void createProgramFolder() {
try
{
FilelOPermission filePermissions =
new FilelOPermission(FilelOPermissionAccess.Write,
т	@"C:\Program Files\");
filePermissions. DemandO;
// Код метода
}
574 Безопасность приложений	Глава 11
catch
{
// Код обработки ошибок
}
}
public static void requestWebPageO
{
try
{
Regex connectPattern = new Regex(@"http.//www\.microsoft\ com/.*");
WebPermission webPermissions =
new WebPermission(NetworkAccess.Connect, connectPattern);
webPe emissions.Demand();
// Код метода
} catch
11 Код обработки ошибок
}
}
Помните, преимущество использования императивных требований в том, что можно перехватывать и обрабатывать исключения, связанные с защитой, возникающие в методе. Если нужно просто сгенерировать исключение в вызывающем коде, используйте декларативные запросы.
Анализ предоставленных разрешений
Если нужно определить, имеет ли сборка определенное разрешение CAS, не следует использовать Demand. Demand используется для проверки разрешений кода, вызывающего сборку, а не самой сборки. Вместо этого, используйте метод System.Security.Se-curityManager.IsGranted, как показано в следующем примере:
’ VB
Dim filePermissions As FilelOPermission = New
FilelOPermission(FilelOPermissionAccess.Read, "C:\Windows\")
If SecurityManager.IsGranted(filePermissions) = True Then
Сборка может читать каталог C:\Windows Else
Сборка не может читать каталог C:\Windows End If
// C#
FilelOPermission filePermissions = new
FilelOPermission(FilelOPermissionAccess.Read, @"C:\Windows\");
if ( SecurityManager.IsGranted(filePermissions) == true )
// Сборка может читать каталог C:\Windows else
Ц Сборка не может читать каталог C:\Windows
Занятие 3
Декларативная и императивная защита методов 575
Этот метод использует приложение ListPermissions, рассмотренное на занятии 1; исходный код можно найти на прилагаемом к книге компакт-диске.
СОВЕТ Избегайте избыточных запросов
Большинство классов в .NET Framework используют запросы, чтобы удостовериться в наличии у вызывающего кода необходимых разрешений, так что вызов Demand будет избыточным. Например, при чтении строки из текстового файла при помощи объекта StreamWriter сам объект будет запрашивать FilelOPermission. Обычно запросы нужны для защиты особых ресурсов, требующих особых разрешений.
Способы ограничения разрешений
Всегда используйте объявления CAS, чтобы ограничить предоставленные сборке разрешения CAS до минимума, необходимого для ее функционирования. Разрешения можно настраивать более детально, ограничивая разрешения отдельных методов при помощи объявлений методов или при помощи императивных выражений внутри методов.
Два перечислимых SecurityAction и методов разрешений инструктируют исполняющую среду ограничить следующие разрешения CAS: Deny и PermitOnly. Различие между двумя перечислимыми состоит в том, что Deny удаляет одно разрешение или набор разрешений, тогда как PermitOnly удаляет все разрешения или наборы разрешений, кроме указанных Как вы можете помнить из занятия 2, Deny выполняет функции, аналогичные RequestRefuse. тогда как функции PermitOnly аналогичны RequestOptional.
Подготовка к экзамену
На экзамене помните, что RequestRefuse и RequestOptional используются для объявлений сборок, a Deny и PermitOnly — для объявлений методов.
Декларативное ограничение разрешений метода
Следующие два объявления показывают, как запретить методу доступ к каталогу C:\Windows\ и ограничить исходящие Web-запросы только сайтом wwwmicrosoft.com'.
' VB
<FileIOPermissionAttribute(SecurityAction. Deny,
ViewAndModify := "C:\Windows\')> _
<WebPermission(SecurityAction.PermitOnly, _
ConnectPattern:="http://www\ microsoft\.com/.*")> _
// C#
[FileIOPermission(SecurityAction.Deny, ViewAndModify = @"C:\Windows\")] [WebPermission(SecurityAction.PermitOnly,
ConnectPattern = @"http://www\.microsoft\.com/.*")]
ПРИМЕЧАНИЕ Ограничения декларативной защиты
Критерии декларативной защиты должны быть статичными. Если нужно динамически создавать пути к файлам, Web-адреса или любые другие элементы системы безопасности, нужно применять ограничения императивно.
576
Безопасность приложений
Глава 11
Императивное ограничение разрешений
В следующем примере применяются те же ограничения, что и в примере с использованием декларативных требований CAS, но в этот раз они применяются императивно:
’ VB
’ Запрет доступа к каталогу Windows
Dim filePermissions As FilelOPermission = New _
FilelOPermission(FilelOPermissionAccess.AllAccess, "C:\Windows\") filePermissions.Deny() ’ Код метода
' Разрешаем только доступ к сайту www.microsoft.com
Dim connectPattern As Regex = New Regex("http://www\.microsoft\.com/.*") Dim webPermissions As WebPermission =
New WebPermission(NetworkAccess.Connect, connectPattern) webPermissions.PermitOnly() ’ Код метода
// Ctf
// Запрет доступа к каталогу Windows
FilelOPermission filePermissions =
new FileIOPermission(FileIOPermissionAccess.AllAccess, @"C:\Windows\”); filePermissions.Deny();
// Код метода
// Разрешаем только доступ к сайту www.microsoft.com
Regex connectPattern = new Regex(@"http://www\.microsoft\.com/.
WebPermission webPermissions = new WebPermission(NetworkAccess.Connect, connectPattern);
webPermissions. PermitOnlyO;
// Код метода
Если часть кода требует разрешения, которое ранее было заблокировано при помощи Deny или PermitOnly, используйте статические методы System.Security.CodeAccessPermission.RevertDeny или System.Security .CodeAccessPermission.RevertPermitOnly, чтобы повторно включить это разрешение.
Рекомендации по обработке ошибок
Для ограничения разрешений во время обработки ошибок используйте PermitOnly. Злоумышленники часто создают в приложении ошибочное условие и пользуются возникшей ситуацией для выполнения действий, которые невозможно выполнить в обычных условиях. Использование PermitOnly для ограничения разрешений CAS до минимума, необходимого для записи события в журнал и сообщения пользователю об ошибке, значительно снижает риск того, что функции обработки ошибок будут использованы для атаки. Если приложение будет продолжать работу после ошибки, нужно вернуть исходные разрешения, иначе приложение не сможет нормально функционировать.
Занятие 3
Декларативная и императивная защита методов 577
Например, приведенный ниже код перехватывает исключение, ограничивает разрешения CAS до минимума, необходимого для добавления событий, а затем возвращает прежний набор разрешений:
’ VB
Try
Код сборки
Catch
Dim errorPerms As EventLogPermission =
New EventLogPermission (PermissionState.Unrestricted)
errorPerms PermitOnly
Запись события
CodeAccessPermission.RevertPermitOnly
End Try
// C# try {
// Код сборки
}
catch
{
EventLogPermission errorPerms = new
EventLogPe rmission(Permissionstate.Un rest ricted);
errorPerms.PermitOnly();
// Запись события
CodeAccessPermission. RevertPermitOnlyO;
}
Ограничение разрешений до требуемых конкретным блоком кода — отличный пример следования принципу наименьших привилегий. Хотя это особенно важно для функций обработки ошибок, такой прием можно использовать для любого блока кода.
Ослабление строгости разрешений для повышения производительности
Использование запросов CAS усиливает защиту, но может снижать производительность. В частности, «недешев» вызов метода Demand, так как он вынуждает исполняющую среду систематически проверять разрешения каждого вызывающего объекта Метод LinkDemand, рассмотренный выше, является одним из способов повысить производительность по сравнению с методом Demand, но за счет некоторого снижения уровня защиты. Другой способ — применение метода Assert, который инструктирует исполняющую среду пропустить все проверки защиты.
ВАЖНО! Сравнение с функцией assert в C++
Метод CodeAccessPermission.Assert не имеет ничего общего с функцией assert в С или C++.
Объекты разрешений включают метод Assert, который позволяет доверять вызывающим объектам. На рис. 11-11 показано, как вызов метода Assert запрещает исполняющей среде проверять разрешения CAS сборок, стоящих выше в стеке. Это имеет два эффек
578 Безопасность приложений
Глава 11
та: повышение производительности уменьшением числа проверок разрешений и разрешение непривилегированному коду вызывать методы с более высокими требованиями разрешений CAS.
Class3.Method3 проверяет
SecurltyActlon.Demand
ф
Рис. 11-11. Assert блокирует проверки, повышая производительность и позволяя непривилегированному коду вызывать методы с более строгими требованиями разрешений CAS
Например, если создать объект Registrypermission и вызвать метод Assert, сборке должно быть предоставлено разрешение RegistryPermisston, но любой код, вызывающий сборку, не требует этого разрешения. Если вызывать другой метод, использующий для запроса разрешения RegistryPermisston метод Demand, метод Demand будет успешно выполнен, независимо от того, имеет вызывающий код разрешение RegistryPermisston или нет.
Assert можно использовать как декларативно, так и императивно, синтаксис идентичен другим типам объявлений CAS. В следующем примере разрешения объявляются декларативно:
’ VB
<FilelOPermissionAtt ribute(Secu rityAction.Asse rt, _
Занятие 3
Декларативная и императивная защита методов 57g
ViewAndModify := "C:\Program Files\")>
<WebPermission(Secu rityAction.Assert,
ConnectPatte rn:="http://www\.mic rosoft\.com/.*")>
// C#
[FilelOPermission(SecurityAction.Assert, ViewAndModify = @"C:\Windows\")] [WebPermission(SecurityAction Assert,
ConnectPattern = @"http://www\.microsoft\.com/.*”)]
В следующем примере разрешения объявляются императивно:
' VB
Блокирует все проверки разрешений CAS при обращении к файлу из каталога Windows Dim filePermissions As FilelOPermission =
New FileIOPermission(FileIOPermissionAccess.AllAccess, "C:\Windows\") filePermissions.Assert()
Код метода
Блокирует все проверки разрешений CAS при доступе к сайту www.microsoft.com Dim ConnectPattern As Regex = New Regex("http://www\.microsoft\.com/.*") Dim webPermissions As WebPermission = _
New WebPermission(NetworKAccess.Connect, ConnectPattern) webPe rmissions Asse rt() ' Код метода
// C#
// Блокирует все проверки разрешений CAS при обращении к файлу из каталога Windows
FilelOPermission filePermissions =
new FileIOPermission(FilelOPermissionAccess.AllAccess, @"C:\Windows\"); filePermissions AssertO;
// Код метода
// Блокирует все проверки разрешений CAS при доступе к сайту www.microsoft.com Regex ConnectPattern = new Regex(@"http://www\.microsoft\.com/.*");
WebPermission webPermissions = new WebPermission(NetworkAccess.Connect, ConnectPattern). webPermissions.Assert();
// Код метода
Чтобы успешно использовать Assert, сборка должна иметь привилегию SecurityPermissionFlag.Assertion, как и объявленную привилегию. В утилите .NET Framework Configuration привилегия SecurityPermissionFlag.Assertion представлена элементом Assert Any Permission That Has Been Granted в окне свойств разрешения. Это разрешение входит в наборы FuIlTrust, Locallntranet и Everything.
Использование Assert позволяет сборке поручиться за защиту менее привилегированных сборок. Это отличный способ предоставления дополнительной функциональности сборкам, которые в обычном случае не имеют необходимых разрешений CAS. Например, при помощи Assert можно позволить сборке из зоны Internet сохранять файлы на диск. Просто создайте сборку с атрибутом AllowPartiallyTrustedCallersAttribute. Затем создайте открытый метод, записывающий файл, создайте объект FilelOPermission и
580 Безопасность приложений
Глава 11
вызовите метод Assert перед записью файла. Сборка в зоне Internet может сохранять файд на диск, не требуя от администраторов присвоения зоне Internet разрешений на доступ к файлам
Чтобы снизить вероятность того, что объявленные разрешения будут использованы для атаки, используйте статический метод CodeAccessPermission.RevertAssert. Как можно понять из имени, этот метод удаляет добавленные разрешения и возвращает обычное состояние. Используйте блок try/finally, чтобы можно было вызвать метод Revert Assert для каждого Assert, даже если произойдет сбой. Следующий метод демонстрирует этот прием, а также прекрасно показывает, как выполнять переход к более строгому набору разрешений:
’ VB
Dim filePermissions As FilelOPermission =
New FilelOPermission (FilelOPermissionAccess.Write,
"C:\Inetpuo\NewFile.txt") filePermissions.Assert Try
Dim newFile As StreamWriter = New StreamWriter
("0:\Inetpub\NewFile.txt")
newFile.WriteLine("Lesser privileged applications can save a file.") newFile.Close
Finally
CodeAccessPe emission.Reve rtAsse rt
End Try
// C#
FilelOPermission filePermissions =
new FileIOPermission(FilelOPermissionAccess.Write, @"C:\Inetpub\"); filePermissions.Assert();
try
{
StreamWriter newFile = new StreamWriter(@"C:\Inetpub\NewFile.txt");
newFile.WriteLineC"Lesser privileged applications can save a file."); newFile. CloseO;
}
finally
{
CodeAccessPe rmission.Reve rtAsse rt();
}
Assert имеет несколько ограничений. Использовать Assert в методе можно только один раз. Если требуется добавить несколько разрешений, нужно создать собственный набор разрешений (см. ниже). Второе ограничение — Assert не переопределяет защиту операционной системы, основанную на ролях, независимо от разрешений CAS, предоставленных сборке. Если пользователь не имеет разрешений для записи на диск D: и запускает сборку с полным доверием, которая добавляет это разрешение, метод Assert будет успешно выполнен, но сборка все равно не сможет вести запись на диск D:. Сборка не сможет преодолеть ограничения, наложенные на уровне пользователя.
Занятие 3
Декларативная и императивная защита методов 581
Вызов доверенного кода из кода с частичным доверием
Чтобы код с частичным доверием не смог избежать проверок защиты, при настройках по умолчанию такой код не может вызывать сборки со строго определенным именем. Этот параметр можно настраивать для отдельных сборок, добавляя на уровне сборки атрибут AllowPartiallyTrustedCallersAttribute'.
[assembly AllowPartiallyTrustedCallers]
Если сборка не имеет строгого имени, код с частичным доверием сможет получить доступ к открытым методам, даже если этот атрибут не добавлен.
Использование наборов разрешений
Набор разрешений — это группа разрешений, которую можно использовать императивно, так же, как и отдельные разрешения. Чтобы создать набор разрешений, используйте класс System Security.Permissions .PermissionSet, после чего используйте метод AddPermission, чтобы указать разрешения, входящие в набор. Затем можно вызывать любые стандартные методы разрешений, включая Assert, Demand, Deny и PermitOnly.
Например, в приведенном ниже коде создается набор разрешений, состоящий из доступа для чтения к папке C:\Windows, доступа на запись к папке C:\Inetpub\ и доступа для чтения к разделу реестра HKEY_LOCAL_MACHINE\Software. Затем запрашивается доступ ко всем ресурсам, чтобы исполняющая среда генерировала исключение, если какое-то разрешение не доступно
VB
Dim myPerms As PermissionSet = New PermissionSet(Permissionstate.None) myPerms.AddPermission(New FilelOPermission
(FilelOPermissionAccess Read, "C:\Windows"))
myPerms.AddPermission(New FilelOPermission
(FilelOPermissionAccess.Write, "C:\Inetpub"))
myPerms.AddPermission(New
Registrypermission (RegistryPermissionAccess.Write, "HKEY_LOCAL_MACHINE")) myPerms.Demand
II ctt
PermissionSet myPerms = new PermissionSet(Permissionstate.None);
myPerms.AddPermission(new FilelOPermission(
FilelOPermissionAccess.Read, @"C:\Windows”));
myPerms.AddPermission(new FileIOPermission(
FilelOPermissionAccess.Write, @"C:\Inetpub"));
myPerms.AddPermission(new
RegistryPermission(RegistryPermissionAccess.Write, @"HKEY_LOCAL_MACHINE\Software"));
myPerms.Demand();
’v В одном методе Assert можно вызывать только один раз, так что для добавления нескольких разрешений, необходимо использовать набор разрешений.
582 Безопасность приложений	Глава 11
Практикум. Защита методов при помощи запросов CAS
Сейчас вы будете работать с методами Deny и Assert, чтобы закрепить знания о декларативных и императивных разрешениях CAS. Выполните упражнения 1-4. В конце упражнения 4 все изменения будут отменены.
Упражнение 1. Экспериментируем с набором разрешений по умолчанию
В этом упражнении вы поэкспериментируйте с декларативными и императивными запросами CAS и определите, как будет изменяться их поведение, если ограничения CAS не установлены. Для выполнения этого упражнения требуется папка C:\Documents and Settings\Admimstrator\. Если такой папки не существует, создайте ее, прежде чем приступить к выполнению.
1.	Скопируйте из папки Chapterl l\CASDemands с компакт-диска, прилагаемого к книге, версию проекта для C# или Visual Basic и откройте ее. Постройте проект и скопируйте полученный исполняемый файл в папку вашего компьютера, к которой имеют доступ обычные пользователи (не являющиеся администраторами).
2.	Войдите в систему под учетной записью обычного пользователя и запустите файл CASDemands.exe.
3.	В приложении Code Access Security Demands щелкните кнопку Create File With No Demand, а затем ответьте на следующие вопросы:
А. В какой зоне работает сборка?
Сборка работает в зоне My_Computer_Zone, так как запущена с локальной файловой системы.
В Какой набор разрешений исполняющая среда предоставила сборке?
По умолчанию сборки в зоне My_Computer_Zone получают набор разрешений FullTrust.
С. Исключение какого типа было сгенерировано и почему сгенерировано исключение именно этого типа?
Сгенерировано исключение System. Unauthorized Access Except ion, так как обычный пользователь не имеет доступа к папке C:\Documents and Settings\Administrator\.
4.	В приложении Code Access Security Demands щелкните кнопку Create File With Declarative Demand, после чего ответьте на следующие вопросы.
А. Исключение какого типа было сгенерировано и почему сгенерировано исключение именно этого типа?
Сгенерировано исключение System. UnauthorizedAccessException, так как обычный пользователь не имеет доступа к папке C:\Documents and Settings\Administrator\.
В. Обратите внимание, появилось сообщение об ошибке Failed When Attempting То Create File, а не Failed Before Attempting To Create File. При помощи Visual Studio .NET просмотрите код метода declarativeDemandButton_Click. Этот метод вызывает метод declarativeCreateFile, который требует доступа на запись в файл, на создание которого у пользователя нет разрешений. Почему исключение было сгенерировано в методе createFile, а не при обработке декларативного запроса перед запуском метода declarativeCreateFile'!
Декларативные запросы CAS приведут к тому, что исключение будет сгенерировано, когда сам код не имеет требуемого разрешения. В нашем случае пользователь не имеет необходимого разрешения, а код имеет разрешения CAS на создание файла. Следовательно, декларативные запросы удовлетворяются, но более
Занятие 3
Декларативная и императивная защита методов 533
строгое требование защиты операционной системы приводит к тому, что исполняющая среда генерирует исключение, когда приложение пытается создать файл в методе createFile.
5.	В приложении Code Access Security Demands щелкните кнопку Create File With Imperative Demand, после чего ответьте на следующий вопрос:
□ Исключение какого типа было сгенерировано и почему сгенерировано исключение именно этою типа?
Исполняющая среда, как и при нажатии других кнопок, сгенерирует исключение System. UnauthorizedAccessException, так как разрешения CAS не учитываются, поскольку сборка запущена с набором разрешений FullTrust.
6.	Закройте приложение Code Access Security Demands.
Упражнение 2. Ограничение разрешений зоны My_Computer_Zone
В этом упражнении вы ограничите разрешения зоны My_Computer_Zone.
1.	Войдите в систему под учетной записью администратора.
2.	Запустите утилиту Microsoft .NET Framework 2.0 Configuration. Раскройте узел Runtime Security Policy, Machine, Code Groups, All_Code. Щелкните правой кнопкой мыши узел My_Computer_ Zone и выберите команду Properties.
3.	Перейдите на вкладку Permission Set и в раскрывающемся списке Permission Set выберите Internet. Щелкните ОК.
4.	Войдите в систему под учетной записью обычного пользователя.
5.	Запустите файл CASDemands.exe. Как показано на рис. 11-12, сборка больше не имеет неограниченных разрешений, и .NET Framework отображает предупреждение безопасности. Закройте окно предупреждения.
Рис. 11-12. .NET Framework показывает предупреждение, поскольку приложение запущено в контексте с частичным доверием
6.	В приложении Code Access Security Demands щелкните кнопку Create File With No Demand, после чего ответьте на следующие вопросы.
А. В какой зоне работает сборка?
Сборка использует зону My_Computer_ Zone, так как запущена с локальной файловой системы.	*
В. Какой набор разрешений исполняющая среда предоставит сборке?
Так как для изменения конфигурации использовалась учетная запись администратора, зона My_Computer_Zone предоставит сборке разрешение Internet.
584 Безопасность приложений
Глава 11
С. Исключение какого типа было сгенерировано и почему сгенерировано исключение именно этого типа?
Исполняющая среда сгенерирует исключение System. Security.Security Exception, так как приложение, работающее в зоне Internet, не имеет разрешений CAS на использование файловой системы.
7.	В приложении Code Access Security Demands щелкните кнопку Create File With Declarative Demand, после чего ответьте на следующие вопросы:
А. Исключение какого типа было сгенерировано и почему сгенерировано исключение именно этого типа?
Исполняющая среда сгенерирует исключение System. Security. Security Exception по тем же причинам, что и при щелчке кнопки Create File With No Demand.
В. В каком методе было перехвачено исключение?
Сообщение об ошибке The Failed Before Attempting To Create File указывает на то, что исключение было перехвачено в методе declarativeDemandButtonClick, так как было сгенерировано при обработке декларативного запроса, связанного с методом declarativeCreateFile. Исполняющая среда не запускала метод create File, так как это запрещено в запросе.
8.	В приложении Code Access Security Demands щелкните кнопку Create File With Imperative Demand и ответьте на следующий вопрос:
□ Исключение какого типа было сгенерировано и какой метод перехватил это исключение?
Как и в случае с декларативным запросом, исключение System.Security.SecurityExcep-tion было перехвачено до того, как достигло метода createFile. В этом случае исключение перехвачено в методе imperativeDemandButton_Click.
9.	Наконец, используйте утилиту .NET Framework Configuration с административными привилегиями, чтобы отменить все изменения, как описано на занятии 1 этой главы.
Упражнение 3. Предоставление доступа коду с частичным доверием
В этом упражнении вы измените класс, чтобы позволить коду, не имеющему разрешения FilelOPermission, выполнять запись на диск.
1.	Войдите в систему под учетной записью администратора. При помощи Проводника Windows скопируйте папки Chapterll\TrustedClass и Chapterll\PartiallyTrustedAssembly с компакт-диска, прилагаемого к книге, выбрав версию для Visual Basic или С#.
2.	Откройте файл TrustedClass.sln и постройте решение. Пока решение открыто, просмотрите код класса Distrust. Заметьте, что этот класс имеет один член — WriteToFile, который использует класс StreamWriter, автоматически запрашивающий разрешение FilelOPermission.
3.	Скопируйте файл TrustedClass.dll в папку C:\TrustedClass\.
4.	В командной строке выполните следующую команду, чтобы открыть общий доступ к этой папке:
net share trusted="C:\TrustedClass"
5.	Откройте файл PartiallyTrustedAssembly.sln.
6.	Щелкните Project, а затем щелкните Add Reference. В диалоговом окне Add Reference перейдите на вкладку Browse. В поле File Name введите \\127.0.0.1\Trusted\TrustedClass.dll. Щелкните ОК.
Занятие 3
Декларативная и императивная защита методов *
7.	Постройте решение, а затем скопируйте файлы PartiallyTrustedAssembly ехе и TrustedClass.dll в папку C:\PartiallyTrustedAssembly\.
8.	В командной строке выполните следующую команду, чтобы открыть общий доступ к папке:
net share untrusted="C:\PartiallyTrustedAssembly"
9.	В командной строке выполните следующую команду:
\\127.0.0.1\untrusted\PartiallyTrustedAssembly
Ответьте на следующие вопросы:
А. Сборка PartiallyTrustedAssembly пыталась выполнить запись в файл. Ей это удалось?
Нет, поскольку было сгенерировано исключение SecurityException, так как запрошенное разрешение FilelOPermission не получено.
В. Просмотрите исходный код и объясните его поведение.
Сборка PartiallyTrustedAssembly вызывает метод TrustedClass.Distrust. WriteToFile. Этот метод использует объект .NET Framework Stream Writer, который содержит запрос разрешения FilelOPermission. После того, как объект StreamWriter выполнит запрос, исполняющая среда проверит разрешение CAS у каждого вызывающего объекта. Так как сборка PartiallyTrustedAssembly не имеет этого разрешения, исполняющая среда сгенерирует исключение SecurityException.
10.	Добавьте в TrustedClass.Distrust метод WriteToFileWrapper, блокирующий при помощи метода Assert запрос разрешения FilelOPermission, после чего перестройте сборку и скопируйте ее в папку C:\TrustedClass.
11.	В решении PartiallyTrustedAssembly удалите и снова добавьте ссылку на класс TrustedClass. Затем измените исходный код сборки PartiallyTrustedAssembly, чтобы вызывать метод WriteToFileWrapper вместо WriteToFile. Ответьте на следующие вопросы: □ Какой код нужно написать, чтобы создать сборку?
Детали могут различаться, но код будет выглядеть примерно так:
' VB
Public Shared Sub WriteToFileWrapper(ByVal fileName As String,
ByVai contents As String)
Добавляем разрешение, чтобы вызывающий код пропустил проверку защиты Dim newFilePermission As FilelOPermission =
New FileIOPermission(FilelOPermissionAccess.Write, fileName) newFilePermission.Assert()
Try
WriteToFile(fileName, contents)
Finally
Удаляем добавленные разрешения
CodeAccessPermission.RevertAssert()
End Try
End Sub
// C#
public static void WriteToFileWrapper(string fileName, string contents)
586 Безопасность приложений	Глава 11
{
// Добавляем разрешение, чтобы вызывающий код пропустил проверку защиты FilelOPermission newFilePermission =
new FileIOPermission(FileIOPermissionAccess.Write, fileName): newFilePermission.Assert();
try
{
WriteToFile(fileName, contents);
}
finally
{
// Удаляем добавленные разрешения CodeAccessPe rmission.Reve rtAsse rt();
}
12.	Соберите решение заново и скопируйте файлы PartiallyTrustedAssembly.exe и TrustedClass.dll в папку C:\PartiallyTnxstedAssembly\.
13.	В командной строке выполните следующую команду:
W127.0.0.1\Untrusted\PartiallyTrustedAssembly
Ответьте на следующий вопрос:
□ Сборка PartiallyTrustedAssembly пыталась выполнить запись в файл. Ей это удалось? Обоснуйте ответ.
Нет, поскольку было сгенерировано исключение Security Exception, так как запрос SecurityPermission не удовлетворен. Новый метод пытается использовать метод Assert, так как не имеет некоторых разрешений из-за того, что назначен набор i разрешений TrustedClass.
14.	Теперь нужно повысить уровень доверия к сборке TrustedClass, чтобы эта сборка могла использовать метод Assert. Однако, чтобы можно было повысить уровень доверия сборки, эта сборка должна быть подписана. Поэтому в решении TrustedClass измените свойства проекта, чтобы подписать сборку. Подробные инструкции можно найти по адресу http://msdn2.microsoft.com/en-us/library/msl80781.aspx. Скомпилируйте сборку заново и скопируйте ее в папку C:\TrustedClass\.
15.	Запустите утилиту .NET Framework 2.0 Configuration. Раскройте узел Runtime Security Policy и щелкните Increase Assembly Trust в правой части окна.
16.	На странице What Would You Like To Modify установите переключатель Make Changes To This Computer и щелкните Next.
17.	На странице Which Assembly Do You Want To Trust введите \\127.0.0.1\trusted\ trustedclass.dll.H щелкните Next.
18.	На странице Thist This Assembly Or All Assemblies From This Publisher установите переключатель This One Assembly. Щелкните Next.
19.	На странице Choose The Minimum Level Of TYust For The Assembly переместите ползунок в положение Full Ihist. Щелкните Next, а затем щелкните Finish.
20.	В решении PartiallyTrustedAssembly удалите и добавьте снова ссылку на класс TrustedClass. Перекомпилируйте решение и скопируйте файлы PartiallyTrustedAssembly.exe и TrustedClass.dll в папку C:\PaitiallyTnistedAssembly\.
Занятие 3
Декларативная и императивная защита методов ggy
21.	В командной строке выполните следующую команду:
\\127.0.0.1\Untrusted\PartiallyTrustedAssembly
Ответьте на следующий вопрос:
□ Сборка пыталась выполнить запись в файл. Ей это удалось? Почему?
Да, так как метод WriteTo FileWrapper включает метод Assert, запрещающий методу Demand объекта StreamWriter проверку разрешений сборки. Кроме того, TrustedClass имеет достаточный уровень доверия, чтобы использовать Assert.
22.	Закройте общий доступ к папкам, открытый при выполнении этого упражнения, выполнив следующие команды с привилегиями администратора: net share trusted /delete net share untrusted /delete
23.	Наконец, используйте утилиту .NET Framework Configuration с правами администратора, чтобы отменить все изменения, как описано на занятии 1 этой главы.
Резюме
 Управлять разрешениями в сборке можно при помощи шести методов: Assert, Demand, Deny, InheritanceDemand, LinkDemand и PermitOnly.
 Методы PermitOnly и Deny следует использовать для уменьшения вероятности использования сборки для атаки. Методы Demand и LinkDemand следует использовать только для организации доступа к особым ресурсам или неуправляемому коду.
 При помощи методов Demand и LinkDemand можно защищать методы декларативно или императивно. Метод InheritanceDemand можно использовать декларативно для ограничения списка сборок, которые могут производить новые классы от указанного класса.
 Методы PermitOnly и Deny можно использовать как для декларативного, так и для императивного ограничения назначенных методу разрешений.
 Чтобы пропустить проверку требований CAS и позволить непривилегированным сборкам вызывать привилегированные методы, используйте метод Assert.
 Наборы разрешений предоставляют те же возможности, что и отдельные разрешения, но позволяют применить к одному объекту несколько разрешений одновременно. Чтобы создать набор разрешений, используйте класс System.Security.Permissions.PermissionSet, а затем при помощи метода AddPermission укажите разрешения, определяющие этот набор. После этого можно вызывать любой стандартный метод разрешений, включая Assert, Demand, Deny и PermitOnly.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Какой из следующих методов нужно использовать для генерации исключения, если сборка не имеет необходимых привилегий?
A. SecurityAction.Demand.
В. Security Action. Deny.
588 Безопасность приложений
Глава 11
С. SecurityAction.Asseri.
D. SecurityAction.RequestMinimum.
2.	Какой из следующих методов нужно использовать для генерации исключения перед тем, как начнется выполнения метода, если вызывающий код не имеет необходимых привилегий?
A. SecurityAction. Demand.
В. SecurityAction.Deny.
С. SecurityAction.Assert.
D. SecurityAction.RequestMinimum.
3.	Вы создали объект FilelOPermission с именем fp. Какой метод следует использовать, чтобы определить, имеет ли текущая сборка указанные разрешения, не генерируя исключения?
A. fp.Deny.
В. fp.IsGranted.
С. Security Manager. Deny (fp).
D. SecurityManager.IsGranted(fp).
4.	Какие из следующих строк кода отменят наложенные ограничения защиты? (Укажите все верные ответы.)
' VB
Dim е As EventLogPermission =
New EventLogPermission (Permissionstate.Unrestricted)
e.PermitOnly
// C#
EventLogPermission e =
new EventLogPermission(PermissionState.Unrest rioted);
e.PermitOnlyO;
A.	e.RevertPermitOnly.
B.	CodeAccessPermission.RevertPermitOnly.
C.	e. Revert All.
D.	CodeAccessPermission. Revert All.
E.	e.RevertDeny.
E CodeAccessPermission.RevertDeny.
Закрепление материала главы
Для закрепления знаний и навыков, полученных при изучении этой главы, выполните следующее:  изучите резюме главы;
	повторите основные термины, использованные в этой главе;
	выполните задания лабораторной работы по темам этой главы;
	выполните предложенные упражнения;
	пройдите пробный экзамен.
Лабораторная работа
Резюме главы
	CAS контролирует доступ управляемого кода, аналогично тому, как система безопасности операционной системы ограничивает доступ пользователя к ресурсам системы. Разрешения CAS можно настроить при помощи утилиты NET Framework Configuration или при помощи утилиты командной строки Caspol.
	Запросы на разрешения сборок позволяют администраторам просматривать требуемые сборкой разрешения. Запросы разрешений также могут значительно снизить вероятность того, что сборка будет использована для атаки, не предоставляя сборке доступа к ресурсам, которые ей не требуются.
	Разрешениями CAS в сборке можно управлять императивно или декларативно, что предоставляет большую степень контроля, чем объявления сборок. Это еще больше усиливает защиту приложения.
Основные термины "
	доказательство (удостоверение) сборки;
	Code Access Security (CAS);
	группы кода;
	доказательство;
	полное доверие;
	доказательство (удостоверение) хоста;
	код с частичным доверием;
	разрешение;
	набор разрешений;
	политика безопасности.
Лабораторная работа
Сейчас вы примените все, что узнали о реализации и использовании защиты доступа по правам кода. Ответы на вопросы см. в разделе «Ответы» в конце книги.
Упражнение 1. Объяснение модели CAS
Вы разработчик авиакомпании Blue Yonder Airlines. Глава компании прочитал статью, в которой говорится о том, что модель защиты CAS в .NET Framework можно использовать для защиты от распространения вирусов. Встретив вас, он сказал: «Я только что прочитал статью о .NET Framework. Там действительно есть новая модель защиты, при помощи которой можно сделать так, чтобы программы не делали, чего не положено? Может, вы установите ее на мой компьютер, чтобы я не беспокоился о вирусах или о том, что какие-нибудь программы будут отправлять мои файлы в Интернет?» Затем он задал вам несколько вопросов.
590 Безопасность приложений
Глава 11
Вопросы
Ответы на следующие вопросы:
1.	Может ли модель CAS в .NET Framework предотвратить распространение вирусов? Почему?
2.	Повысит ли установка на моем компьютере .NET Framework уровень защиты? Если нет, что изменится после установки?
3.	Может ли вирус на основе платформы .NET Framework, запущенный в зоне Internet с разрешениями CAS по умолчанию, распространять себя по сети? Почему?
4.	Может ли вредоносная сборка на основе .NET Framework, запущенная в зоне Intranet с разрешениями CAS по умолчанию, удалять файлы на жестком диске? Почему?
Упражнение 2. Настройка защиты CAS
Вы разработчик в компании Contoso, Inc. Вы с коллегами создавали приложения на основе .NET Framework 1.0. Однако, вы никогда не использовали CAS. Сейчас менеджеры просят, чтобы внешние партнеры компании установи /и разработанные ва..:и клиентские приложения. Однако они беспокоятся о том. что приложение может иметь уязвимости в системе защиты, которые подвергнут партнеров риску, что может повредить репутации компании Contoso. Модель CAS может снизить уровень риска.
На собрании руководитель сказал вам: «Люди из PR-отдела просят, чтобы ваше приложение установили сторонние специалисты. Я беспокоюсь о безопасности, особенно общедоступных компьютеров, ведь их постоянно кто-нибудь пытается взломать. Я бы хотел иметь гарантию того, что если кто-то установит наше приложение, оно не позволит взломщикам получить повышенные привилегии, записать вирус на локальный компьютер или сделать что-нибудь еще в том же духе».
Вопросы
Ответьте на следующие вопросы руководства:
1.	Как сделать так, чтобы партнеры и потребители могли установить ваше приложение, но не смогли через него читать или записывать данные локальной файловой системы?
2.	Какой класс вы будете использовать для ограничения доступа приложения к файловой системе?	л □ /
3.	Как реализация модели защиты CAS повлияет на конечных пользователей?
I	* . М	>> .
-3^.	?$» «	‘	‘pV’
Рекомендуемые упражнения
? 4
Чтобы лучше проработать темы экзамена, рассмотренные в этой главе, выполните следующие упражнения.
Реализация защиты доступа по правам кода
Выполните как минимум упражнение 1. Если хотите подробно изучить анализ разрешений, выполните также упражнения 2 и 3.
	Упражнение 1 При помощи утилиты Caspol добавьте группу кода в политику уровня компьютера. После этого удалите эту группу при помощи утилиты .NET Framework 2.0 Configuration.
Пробный экзамен 591
	Упражнение 2 Создайте сборку, получающую веб-страницы и сохраняющую их в файл на локальном жестком диске. Императивно подтвердите, что пользователь имеет достаточно привилегий, используя SecurityManager.IsGranted для максимально подробной проверки разрешений. Отобразите сообщение об ошибке, если пользователь не имеет требуемых разрешений.
	Упражнение 3 Создайте сборку, получающую веб-страницы и сохраняющую их в файл на локальном жестком диске. Убедитесь, что пользователь имеет достаточно привилегий, создав экземпляр класса PermissionSet с максимально подробным определением разрешений и вызвав метод PermissionSet.Demand.
Управление разрешениями на доступ к ресурсам
с применением классов System.Security. Permission
Выполните все три упражнения, чтобы получить опыт использования классов Permission.
	Упражнение 1 Откройте последнюю созданную сборку и добавьте максимально подробные запросы на разрешения сборки. Указывайте требования очень подробно — если сборке нужен доступ только к одному файлу, ограничьте доступ к файловой системе только этим файлом. Если вы используете Wfeb-службы, ограничьте объект WebPermission, чтобы предоставить сборке доступ только к серверу и го тько к каталогам, которые использует Wfeb-служба. Если сборку явно не проверяет разрешения, настройте запросы на разрешения так, чтобы исполняющая среда генерировала исключение, если отсутствуют необходимые разрешения, прежде чем сборка будет запущена.
	Упражнение 2 Используя ту же сборку, что и в упражнении 1, добавьте объявления разрешений для каждого метода.
	Упражнение 3 Используя ту же сборку. добавьте императивные объявления туда, где они необходимы.
Управление привилегиями кода при помощи классов
System. Security. Policy
Выполните как минимум упражнение 1. Если хотите подробно изучить анализ доказательств, выполните также упражнение 2.
я Упражнение 1 Создайте группу кода, которая требует, чтобы сборка была подписана сертификатом вашего издателя. Затем проверьте, что .NET Framework корректно анализирует доказательство и помещает сборку в нужную группу кода.
	Упражнение 2 Создайте консольное приложение, отображающее доказательство текущей сборки.
Пробный экзамен
На прилагаемом компакт-диске, содержится пробный экзамен, обеспечивающий широкие возможности по самоподготовке. Например, можно проверить усвоение темам этой главы либо всех тем экзамена 70-536. Можно сымитировать настоящий экзамен либо выбрать режим обучения, когда после каждого вопроса приводятся правильные ответы с пояснениями.
Пробный экзамен
Подробнее о пробном экзамене рассказано во введении.

ГЛАВА 12
Безопасность пользователя и данных
Занятие 1.	Аутентификация и авторизация пользователей	594
Занятие 2.	Работа со списками управления доступом	620
Занятие 3.	Шифрование и расшифровка данных	629
Компаниям и программам приходится регулировать доступ к конфиденциальной информации. Например, приложения для мониторинга финансовой информации должны показывать пользователям только их личные данные, сведения о других пользователях должны быть доступны только владельцам и уполномоченным менеджерам. Неавтори-зированные пользователи не должны получать доступ к такого рода информации, даже если им удалось обойти встроенную защиту приложения.
Защита информации базируется на координированном использовании различных технологий. Защита, основанная на ролях (role-based security, RBS), позволяет контролировать пользовательский доступ с помощью имени пользователя, и его членства в группах. В .NET Framework решения RBS принимаются на основе сведений из базы локальных пользователей, домена службы каталогов Active Directory или пользовательской базе данных. .NET Framework также позволяет настраивать списки управления доступом (access control lists, ACL). ACL — это механизм операционной системы, отслеживающий доступ и определяющий действия, которые следует регистрировать в журнале событий. Последняя линия обороны, с которой встретится атакующий, обошедший защиту ОС, включает средства .NET Framework для криптозащиты, шифрования, проверки подлинности и цифровой подписи.
Темы экзамена:
	Доступ к идентификационным данным и их изменение с помощью классов System.Security.Principal (см. пространство имен System.Security.Principal):
□	классы Generic Identity и Generic Principal',
□	классы Windows Identity и WindowsPrincipal\
□	классы NTAccount и Securityidentifier,
Занятие 1
Аутентификация и авторизация пользователей 593
□	интерфейсы Ildentity и IPrincipal;
□	класс WindowsImpersonationContext;
□	классы Identity Reference и Identity Referencecollection.
 Реализация пользовательской схемы аутентификации с помощью классов System.Security Authentication (см. пространство имен System.Security.Authentication)'.
□	независимое управление доступом с помощью классов System.Security.AccessControl;
□	классы Directory Security, FileSecurity, FileSystemSecurity и RegistrySecurity;
□	класс AccessRule;
□	классы AuthorizationRule и AuthorizationRuleCollection;
□	классы Common Ace, CommonAcl, CompoundAce, GeneralAce и GeneralAcl;
□	класс AuditRule;
□	классы MutexSecurity, ObjectSecurity и SemaphoreSecurity.
 Шифрование, расшифровка и хэширование данных с помощью классов System.Security.Cryptography (см. пространство имен System.Security.Cryptography):
□	классы DES и DESCryptoServiceProvider,
□	класс HashAlgorithm;
□	классы DSA и DSACryptoServiceProvider,
□	классы SHA1 и SHAlCryptoServiceProvider;
□	классы TripleDES и TripleDESCryptoServiceProvider,
□	классы MD5 и MD5CryptoServiceProvider;
□	классы RSA и RSACryptoServiceProvider;
□	класс RandomNumberGenerator;
□	класс CryptoStream;
□	класс CryptoConfig;
□	классы RC2 и RC2Crypto Service Provider;
□	класс AsymetricAlgorithm;
□	классы ProtectedData и ProtectedMemory;
□	классы RijndaelManaged и RijndaelManagedTransform;
□	класс CspParameters;
□	класс CryptoAPITransform;
□	Hash-based Message Authentication Code (HMAC).
Прежде всего
Чтобы освоить материал этой главы, необходимо иметь навыки работы с Microsoft Visual Basic или C# и уметь:
	создавать консольные приложения в Microsoft Visual Studio, используя Visual Basic или С#;
	производить запись в файлы и потоки;
Ч, управлять учетными записями пользователей с помощью встроенных графических средств Microsoft Windows;
 управлять файловыми разрешениями с помощью встроенных графических средств Microsoft Windows.
594
Безопасность пользователя и данных
Глава 12
Занятие 1. Аутентификация и авторизация пользователей
В данном занятии рассматриваются понятия аутентификации и авторизации, а также их различия. Также описываются реализация аутентификации и проверка удостоверений (идентификационных данных) пользователя, и также авторизация (проверка прав пользователя на доступ к ресурсам). Рассматриваемые технические приемы можно использовать для защиты фрагментов кода, что разрешает запускать определенные сборки, методы или сегменты кода только заданным пользователям или членам групп.
При использовании службы каталогов Active Directory или базы данных локальных пользователей можно интегрировать элементы управления, выполняющие авторизацию для приложения, с существующими службами каталогов при помощи классов И7л-dowsldentity и Windows Principal. Также можно создать собственную систему аутентификации или интегрировать службы каталогов сторонних производителей. Для создания баз сведений о пользователях предназначены классы Genericidentity и GenericPrincipal. Полный контроль над пользователями и ролями возможен после реализации интерфейсов lidentity и IPrincipal.
Изучив материал этого занятия, вы сможете:
S объяснить связь авторизации и аутентификации;
J использовать класс Windowsldentity для проверки имени пользователя и типа аутентификации;
J объяснить назначение класса Windows Principal',
J описать назначение класса Principalpermission;
J использовать декларативные RBS-требования для ограничения прав доступа к методам;
J использовать императивные RBS- требования для ограничения доступа к коду приложений;
J создавать специальные классы удостоверений с помощью интерфейсов lidentity и Iprincipal а также классов Genericidentity и GenericPrincipal;
J описать различные обстоятельства, в которых исполняющая среда использует AuthenticationException и InvalidCredentialException. 
Продолжительность занятия — 90 минут.
Введение в аутентификацию и авторизацию
Аутентификация — это процесс подлинности пользователя, одно из важнейших и фундаментальных понятий обеспечения безопасности. С аутентификацией мы сталкиваемся в повседневной жизни, когда предъявляем водительские права, вводим PIN-коды, имена пользователя и пароли. Без аутентификации невозможно ограничить доступ к ресурсам по персоналиям.
Авторизация — это проверка наличия у пользователя прав на доступ к ресурсу .Авторизация, как правило, происходит после аутентификации. Да и как узнать, разрешен ли доступ, если неизвестно, кто пытается получить доступ? На рис. 12-1 проиллюстрировано взаимодействие аутентификации и авторизации.
Занятие 1
Аутентификация и авторизация пользователей 595
Ресурс
Рис. 12-1. Аутентификация и авторизация пользователя при обращении к ресурсу
г.
Для получения денег в банке, входа в режимное здание, посадки в самолет, словом, доступа к любому ограниченному ресурсу требуется и аутентификация, и авторизация. Эти процессы тесно связаны, и их часто путают. Чтобы разобраться в их различиях, обратимся к примеру из реальной жизни — посадке в самолет. Перед посадкой нужно предъявить удостоверение личности и билет. Обычно в роли первого выступает паспорт. Он позволяет персоналу аэропорта узнать, кто вы. Проверка личности — это «аутентификационная» часть посадки. Далее персонал аэропорта проверит ваш билет, чтобы убедиться, что вы пытаетесь сесть на свой рейс. Проверка наличия прав на посадку в самолет — это уже авторизация.
В сетях аутентификация обычно производится путем ввода имени пользователя и пароля. Имя пользователя идентифицирует вас, а пароль подтверждает, что это действительно вы. После аутентификации компьютер вас «узнает», но ему еще неизвестно, есть ли у вас доступ к запрашиваемым ресурсам. Например, у персонала службы поддержки должны быть права на смену пароля любых пользователей; в то же время работники бухгалтерии должны иметь возможность изменять только собственные пароли. Обычно для авторизации система компьютера проверяет ACL, содержащий список пользователей и групп, имеющих доступ к данному ресурсу.
Класс Windowsldentity
Класс System.Security.Principal. Windowsldentity представляет собой учетную запись пользователя. Данный класс обеспечивает доступ к имени текущего пользователя, его типу аутентификации и маркеру учетной записи. С его помощью нельзя авторизовать пользователя — Windows берет авторизацию на себя. Windowsldentity только хранит результат аутентификации, в том числе имя пользователя и маркер аутентификации.
Как правило, для создания объекта вызывается один из методов Windowsldentity.
	GetAnonymous
Возвращает объект Windowsldentity, который является анонимным, не аутентифицированным пользователем Windows. Этот метод позволяет сымитировать анонимного пользователя, чтобы убедиться, что код не работает без ввода учетных данных.
596
Безопасность пользователя и данных
Глава 12
	GetCurrent
Возвращает объект Windowsldentity, который представляет собой текущего пользователя Windows. Данный метод можно использовать для проверки имени текущего пользователя и его членства в группах.
	Impersonate
Возвращает объект WindowsImpersonationContext, который представляет собой заданного пользователя системы. Данный метод можно использовать для проверки получения доступа к ресурсам под заданной учетной записью пользователя
Например, следующий код (требует пространства имен System.Security.Principal) создает объект Windowsldentity с именем currentidentity, который представляет текущего пользователя:
VB
Dim currentidentity As Windowsldentity = Windowsldentity. GetCurrentO
11 C#
Windowsldentity currentidentity = Windowsldentity. GetCurrentO;
После присвоения переменной значения можно обращаться к нескольким полезным свойствам, предоставляющим информацию о пользователе:
	AuthenticationType
Строка с названием метода аутентификации, обычно «NTLM».
	IsAnonymous
Булево значение; равно true, если пользователь — анонимный.IsAuthenticatedbyneBO значение; равно true, если пользователь аутентифицирован.
	IsGuest
Булево значение; равно true, если пользователь является гостем.
	IsSystem
Булево значение; равно true, если пользователь — системная учетная запись.
	Name
Строка с доменом и именем пользователя, разделенными обратной косой чертой в формате “ДОМЕН\Имя_пользователя”. Если учетная запись пользователя находится в БД локальных пользователей, доменом является имя машины. В противном случае домен — это домен Active Directory.
	Token
Целое число — маркер аутентификации пользователя, присваивается компьютером, производившим аутентификацию.
Класс Windowsldentity предназначен для проверки имени и типа аутентификации те -кущего пользователя, а также авторизации пользователя для запуска привилегированных секций кода. Проверка объектов данного класса полезна, например, перед запуском кода, который выводит конфиденциальную информацию.
Следующее простое консольное приложение (требует пространства имен System.Security.Principal) показывает вывод информации о текущем пользователе с помощью класса Windowsldentity.
' VB
Получить объект текущего пользователя
Dim currentidentity As Windowsldentity = Windowsldentity.GetCurrentO
Занятие 1
Аутентификация и авторизация пользователей 597
’ Вывести имя, маркер и тип аутентификации текущего пользователя
Console.WriteLine("Name: " + currentidentity.Name)
Console. WriteLine( "Token.: " + currentidentity.Token.ToStringO)
Console.WriteLine("Authentication Type: "
+ currentidentity.AuthenticationType)
Вывести информацию, основанную на булевых свойствах текущего пользователя
If currentidentity.IsAnonymous = True Then
Console.WriteLineC'Is an anonymous user")
End If
If currentidentity.IsAuthenticated = True Then
Console.WriteLineC’Is an authenticated user")
End If
If currentidentity.IsSystem = True Then
Console.WriteLineC’Is part of the system")
End If
If currentidentity.IsGuest = True Then
Console.WriteLineC’Is a guest”)
End If
// C#
// Получить объект текущего пользователя
Windowsldentity currentidentity = Windowsldentity. GetCurrentO;
// Вывести имя, маркер и тип аутентификации текущего пользователя
// текущего пользователя
Console.WriteLine("Name: " + currentidentity.Name);
Console.WriteLine("Token: " + currentidentity.Token.ToStringO);
Console.WriteLine("Authentication Type: "
+ currentidentity.AuthenticationType);
// Вывести информацию, основанную на булевых
// свойствах текущего пользователя
if (currentidentity.IsAnonymous)
Console.WriteLineC’Is an anonymous user");
if (currentidentity.IsAuthenticated)
Console.WriteLineC’Is an authenticated user");
if (currentidentity.IsGuest)
Console.WriteLineC’Is a guest");
if (currentidentity.IsSystem)
Console.WriteLineC’Is part of the system");
Класс WindowsPrincipal
Класс System.Security.Principal. Windows Principal обеспечивает доступ к сведениям о членстве в группах для пользователя. Объекты этого класса создают с помощью экземпляра класса Windowsldentity. Например, следующий код создает объект Windowsldentity с име
598 Безопасность пользователя и данных
Глава 12
нем currentidentity, представляющий текущего пользователя; далее на основе этого объекта Windows Identity создается объект Windows Principal с именем current Principal, также отображающий текущего пользователя.
' VB
Dim currentidentity As Windowsldentity = WindowsIdentity.GetCurrentO
Dim currentprincipal As WindowsPrincipal =
New WindowsPrincipal(currentidentity)
11 C#
Windowsldentity currentidentity = WindowsIdentity.GetCurrentO;
WindowsPrincipal currentprincipal = new WindowsPrincipal(currentldentity);
Альтернативный вариант — с помощью метода Windowsldentity.GetCurrent получить текущий объект WindowsPrincipal у текущего потока. Для этого задайте для текущего principal policy использование системы безопасности Windows, затем создайте объект WindowsPrincipal путем приведения System.Threading.Thread.CurrentPnncipal к типу WindowsPrincipal Данный способ показан в следующем примере (требует пространства имен System.Security.Principal и Sy stem.Threading)'.
' VB
’ Указать, что следует использовать WindowsPrincipal
AppDomain. CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal)
Привести текущий объект участника системы безопасности к типу WindowsPrincipal Dim currentprincipal As WindowsPrincipal =
CType(Thread.Currentprincipal, WindowsPrincipal)
// C#
// Указать, что следует использовать WindowsPrincipal
AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
// Привести текущий объект участника системы безопасности к типу WindowsPrincipal WindowsPrincipal currentprincipal = (WindowsPrincipal)Thread.CurrentPrincipal;
Класс WindowsPrincipal можно использовать для определения членства пользователя в группах. Для запроса членства в стандартных (встроенных) группах передайте методу WindowsPrincipal. IsInRole член класса System.Security.Principal.WindowsBuiltlnRole. Каждый член класса WindowsBuiltlnRole представляет встроенную группу локального компьютера или домена Active Directory. К примеру, следующий фрагмент консольного приложения проверяет три члена класса WindowsBuiltlnRole и показывает, входит ли текущий пользователь в соответствующие группы:
' VB
Создать объект Windowsldentity, представляющий текущего пользователя
Dim currentidentity As Windowsldentity - WindowsIdentity.GetCurrentO
Создать объект WindowsPrincipal, представляющий текущего пользователя Dim currentPrincipal As WindowsPrincipal =
New WindowsPrincipal(currentidentity)
Занятие 1
Аутентификация и авторизация пользователей ggg
Console.WriteLine("The current user is a member of the following roles: ")
‘ Проверить членство в трех пользовательских группах
If currentprincipal.IsInRole(WindowsBuiltInRole.Administrator) Then
Console. WriteLine(WindowsBuiltInRole. Administrator. ToStringO)
End If
If currentprincipal.IsInRole(WindowsBuiltInRole.PowerUser) Then
Console. WriteLine(WindowsBuiltInRole. PowerUser. ToStringO)
End If
It currentprincipal.IsInRole(WindowsBuiltInRole.User) Then
Console.WriteLine(WindowsBuiltInRole.User.ToString())
End If
// C#
// Создать объект Windowsldentity, представляющий текущего пользователя Windowsldentity currentidentity = Windowsldentity.GetCurrentO;
// Создать объект WindowsPrincipal, представляющий текущего пользователя WindowsPrincipal currentprincipal = new WindowsPrincipal(currentldentity);
Console.WriteLine("The current user is a member of the following roles: ’’);
// Проверить членство в трех пользовательских группах
if (currentprincipal.IsInRole(WindowsBuiltInRole.Administrator))
Console.WriteLine(WindowsBuiltInRole Administrator.ToString());
if (currentprincipal.IsInRole(WindowsBuilt.InRole. PowerUser))
Console WriteLine(WindowsBuiltInRole.PowerUser.ToStringO);
if (currentprincipal.IsInRole(WindowsBuiltInRole.User))
Console.WriteLine(WindowsBuiltInRole.User.ToString());
Встроенные группы зависят от операционной системы и роли компьютера (является ли компьютер контроллером домена), поэтому при проверке ролей пользователя необходимо подготовиться к перехвату исключений.
Для запроса пользовательских групп или групп домена передайте перегруженному методу IsInRole строку в формате «ИМЯ_ДОМЕНА\имя группы^. Для запроса локальной БД пользователей этот метод не подходит. Например, чтобы выполнить код только в том случае, если пользователь является членом группы CONTOSO\Accountants, можно использовать следующую конструкцию if'.
' VB
If currentprincipal.IsInRole("CONTOSO\Accounting") Then
Console WriteLine("User is in Accounting")
End If
// C#
if (currentprincipal.IsInRole(@ C0NT0S0\Accounting"))
Console WriteLine( User is in Accounting");
600 Безопасность пользователя и данных
Глава 12
Но довольно часто имя компьютера или домена неизвестно. В этом случае используются строковые свойства System.Environment.MachineName или System.Environment. UserDomainName. System.Environment.MachineName служит для определения имен групп локального компьютера. System.Environment.UserDomainName — для определения имен групп локального компьютера или домена службы каталогов Active Directory в зависимости от способа входа пользователя в систему.
Класс Principalpermission
Класс System.Security.Permissions.PrincipalPermission и сходный с ним PrincipalPer-missionAttribute позволяют проверять активных участников системы безопасности как при декларативной, так и при императивной защите. Все PrincipalPermission обычно используются при декларативном требовании аутентификации пользователей, запускающих код, либо для проверки их членства в определенной группе. Передав конструктору имя пользователя и/или роли, можно использовать PrincipalPermission для проверки соответствия этой информации активному участнику системы безопасности.
Можно устанавливать любую из трех комбинаций свойств PrincipalPermission:
	Authenticated
Булево значение. Равно true, если требуется аутентификация пользователя.
	Name
Строка для сравнения с именем пользователя.
	Role
Строка для сравнения с одной из ролей участника системы безопасности.
Подготовка к экзамену
Выучите эти свойства.Запомните: PrincipalPermission не работает с другими свойствами — ни с именем пользователя, ни с номером телефона, паролем или любым другим атрибутом.
У PrincipalPermission несколько методов, но в этой главе описывается только PrincipalPermission. Demand. Метод Demand проверяет соответствие активного участника системы безопасности заданным свойствам Authenticated, Name и Role. При несовпадении хотя бы одного свойства генерируется исключение.
Декларативное ограничение доступа к методам по ролям
Декларативные RBS-требования приказывают исполняющей среде выполнять проверку RBS перед запуском метода. Это наиболее безопасный способ ограничения доступа к коду с помощью RBS, поскольку исполняющая среда выполняет проверку разрешений до запуска кода. Существует два основных недостатка декларативных RBS-требований:
	они ограничивают доступ только к методу целиком;
	могут генерировать исключения. Если метод вызывается по событию Windows, операционная система перехватывает исключения и выполнение приложения прекращается.
Для работы с декларативными RBS-требованиями в коде должны присутствовать три элемента:
	метод System.AppDomain.CurrentDomain.SetPrincipalPolicy — для определения политики участников системы безопасности;
Занятие 1
Аутентификация и авторизация пользователей	gQ j
	блок Try/Catch для перехвата исключений при отказе в доступе из-за недостаточных прав и предоставления сведений об исключениях;
	атрибут Principalpermission для объявления требований доступа метода.
Для начала определим политику участников системы безопасности для потока, в котором используется метод System.AppDomain.CurrentDomain.SetPrincipalPolicy, уже описанным в этом занятии способом. Затем создадим блок Try/Catch для перехвата исключений System.Security.SecurityException, генерируемых исполняющей средой при обращении к методам, доступ к которым запрещен. Важно перехватить исключения данного типа и показать пользователю соответствующе сообщение, т. к. без него пользователь не поймет, в чем дело, потратит много времени на устранение сбоя и в конце концов откажется от использования этого приложения. Кроме того, неудачные попытки доступа следует регистрировать в журнале, чтобы администраторы смогли проанализировать их и устранить причину.
Следующий код вызывает метод AdministratorsOnlyMethod (скрытый), защищенный декларативным RBS-требованием, и выводит сообщения, если у пользователя нет необходимого разрешения:
' VB
Try
AdministratorsOnlyMethod()
Catch ex As System.Security.SecurityException
MessageBox.Show("Your account lacks permission to that function.") End Try
// C# try
{ AdministratorsOnlyMethodO; }
catch (System.Security.SecurityException ex)
{ MessageBox.Show("Your account lacks permission to that function."); }
Теперь добавьте декларативные разрешения с помощью класса PrincipalPermission перед каждым из методов, доступ к которым следует ограничить. Для PrincipalPermission нужно объявить:
	операцию PrincipalPermission; работает с перечислимым System.Security.Permissions. SecurityAction. Обычно Security Action. Demand используется для декларативных RBS;
	одно или несколько свойств PrincipalPermission. Authenticated служит для ограничения доступа аутентифицированных пользователей, Role — членов групп, a User — определенных имен пользователя.
Следующий код (требует пространства имен System.Security.Permissions) генерирует исключение System.Security.SecurityException, если пользователь не входит в группу локальных администраторов:
' VB
<PrincipalPermission(SecurityAction.Demand, Role:="BUILTIN\Administrators")>
Private Sub AdministratorsOnlyMethod ()
’ Код, доступный только администраторам
End Sub
602 Безопасность пользователя и данных
Глава 12
// C#
[PrincipalPermission(SecurityAction.Demand, Role = @"BUILTIN\Administrators")] static void AdministratorsOnlyMethodO
{ // Код, доступный только администраторам }
Также можно определить несколько декларативных требований, которые разрешат выполнение кода при выполнении любого из них. Следующий код разрешает выполнение метода:
	членам группы локальных администраторов;
	пользователю с именем CONTOSO\Userl, который также входит в группу CONTOSO \Managers’,
	всем аутентифицированным пользователям.
' VB
<PrincipalPermission(SecurityAction.Demand,
Name:=”CONTOSO\Administ rater")>
<PrincipalPermission(SecurityAction.Demand,
Name:="C0NT0S0\User1", Role:="CONTOSO\Managers")> _
<PrincipalPermission(SecurityAction.Demand, Authenticated:=True)> Private Sub AdministratorsOnlyMethod ()
Код, доступный только CONTOSO\Administrator
End Sub
// C#
[PrincipalPe mission(Secu rityAction.Demand,
Name = @"CONTOSO\Administrator' )] [PrincipalPermission(SecurityAction Demand, Name = @"C0NT0S0\User1", Role = @"CONTOSO\Managers")] [PrincipalPermission(SecurityAction.Demand, Authenticated = true)] static void AdministratorsOnlyMethodO
{ // Код, доступный только CONTOSO\Administrator }
Императивная RBS для фрагментов кода
Императивные RBS-требования объявляются непосредственно в коде и могут ограничивать доступ к его частям более гибко, чем декларативные. Иными словами, императивные RBS-требования позволяют защищать части метода, в то время как декларативные — только методы целиком. Для работы с императивными RBS-требованиями в коде должны присутствовать:
	метод System.AppDomain.CurrentDomain.SetPrincipalPolicy для определения политики участников системы безопасности;
	блок Try/Catch для перехвата исключений из-за попыток доступа при недостаточных правах и вывода соответствующих сообщений об ошибках;
	объект PrincipalPermission со свойствами, установленными согласно необходимым ограничениям;
	вызов метода PrincipalPermission.Demand для объявления требований для доступа к методу.
Занятие 1	Аутентификация и авторизация пользователей ggg
Первые два элемента аналогичны таковым для декларативных RBS-требований и реализуются так же. А вот работа с классом PrincipalPermission отличается. Сначала создается объект PrincipalPermission. У PrincipalPermission три перегруженных конструктора:
	PrincipalPermission(PermissionState)
Разрешает определять свойства объекта PrincipalPermisson с помощью объекта System.Security.Permissions. PermissionState.
	PrincipalPermission(Name, Role)
Задает значения свойств Name и Role нового объекта. Если нужно определить только имя пользователя или роль, во втором параметре можно передать null.
 PrincipalPermission(Name, Role, Authenticated)
Задает значения свойств Name, Role и Authenticated нового объекта. Если какое-либо из свойств не требуется, присвойте ему ««//-значение.
Следующие две строки генерируют исключение, если пользователь не является членом группы локальных администраторов. Обратите внимание, что первый аргумент конструктора PrincipalPermission — null', это говорит о том, что указывать имя пользователя не обязательно. Последний аргумент равен true, что свидетельствует о необходимости аутентификации пользователя (впрочем, это лишнее — анонимный пользователь не может входить в группу администраторов!).
’ VB
Dim р As PrincipalPermission= New PrincipalPermission (Nothing, "BUILTIN\Administrators", True)
p Demand
// C# •
PrincipalPermission p = new PrincipalPermission(null,' @"BUILTIN\Administrators", true);
p. DemandO;
Для императивного использования объекта PrincipalPermission в более крупном приложении обратимся к следующему консольному примеру. Оно выводит "Access allowed.", если текущий пользователь является членом группы локальной группы VS Developers. В противном случае оно перехватывает исключение, сгенерированное методом PrincipalPermission.Demand и выводит "Access denied".
’ VB
’ Определить политику безопасности Windows
System.AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal)
’ Получить имя группы в формате "MachineName\VS Developers"
Dim r As String = System.Environment MachineName + "\VS Developers"
Перехватить и зарегистрировать любые исключения из-за отказа в доступе Try
’ Создать и затребовать объект PrincipalPermission
Dim р As PrincipalPermission = New PrincipalPermission(Nothing, r, True) p. DemandO
Console.WriteLine("Access allowed.”)
* Здесь должен быть основной код
604 Безопасность пользователя и данных
Глава 12
Catch ex As System.Security.SecurityException
Console.Writel_ine("Access denied: " + ex Message) Здесь должен быть код для регистрации ошибок End Try
// C#
// Определить политику безопасности Windows
System.AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
// Получить имя группы в формате "MachineName\VS Developers" string г = System.Environment.MachineName + @"\VS Developers";
// Перехватить и зарегистрировать любые исключения из-за отказа в доступе try
// Создать и затребовать объект Principalpermission
Principalpermission р = new PrincipalPermission(null, г, true);
р. DemandO;
Console.WriteLine("Access allowed.");
// Здесь должен быть основной код приложения
}
catch(System.Security.SecurityException ex)
{
Console.WriteLine("Access denied: " + ex.Message);
// Здесь должен быть код для регистрации ошибок
Реализация нестандартных пользователей и ролей
При аутентификации пользователей в собственной БД учетных данных можно использовать интерфейсы System .Security. Principal.lidentity и System.Security.Principal.IPrincipal. Данные интерфейсы можно дополнять собственными классами, свойствами и функциями. Например, можно создать класс на основе Ildentity, поддерживающий нестандартные атрибуты пользователя, такие как имя или адрес, или класс на основе Iprincipal, реализовывающий иерархию ролей.
Создание нестандартного класса удостоверений
Интерфейс Ildentity является шаблоном для классов, представляющих удостоверения. Класс Windowsldentity — это реализация Ildentity, большинство его свойств и методов унаследовано непосредственно от Ildentity. Аналогично, Formsldentity и Passportidentity — это реализации Ildentity для работы с Web-аутентификацией; а класс Generic Identity обеспечивает более гибкую реализацию Ildentity.
Если ни одна из существующих реализаций Ildentity не удовлетворяет ваши потребности, можно расширить функциональность lidentity, создав на его основе собственный класс. В него можно добавлять любые подходящие свойства. Такие классы используют так же, как Windowsldentity.
Занятие 1
Аутентификация и авторизация пользователей gQ5
Для реализации lidentity нужно реализовать следующие свойства:
	Authentication Туре
Строка с описанием механизма аутентификации пользователей. С его помощью приложения определяют, следует ли доверять механизму аутентификации. Например, некоторое приложение требуют аутентификации по паспорту (а не встроенную) аутентификацию. При создании нестандартного механизма аутентификации необходимо задать для него уникальный AuthenticationType.
	IsAuthenticated
Булево значение; равно true, если пользователь аутентифицирован. При создании собственного механизма аутентификации устанавливайте это значение, когда пользователь пройдет аутентификацию.
	Name
Строка с именем пользователя. Данное свойство должно присутствовать, даже если механизм аутентификации не использует имя пользователя. Имя пользователя должно быть уникально среди учетных записей.
Кроме того, нужно реализовать конструктор для определения каждого из свойств объекта.
Следующий класс реализует lidentity и добавляет свойства для имени, фамилии, адреса, города, страны и почтового индекса. У данного класса два конструктора: первый без параметров, инициализирует все свойства ««//-значениями; второй инициализирует все свойства реальными значениями.
' VB
Public Class Customidentity
Implements Ildentity
Private _isAuthenticated As Boolean
Private _name As String
Private .authenticationType As String
Private _firstName As String
Private .lastName As String
Private _address As String
Private .city As String
Private .state As String
Private _zip As String
Public Sub New()
Me._name = String.Empty
Me._isAuthenticated = False
Me._authenticationType = "None"
Me..firstName = String.Empty
Me..lastName = String.Empty
Me._adoress = String.Empty
Me..city = String.Empty
Me..state = String.Empty	*
Me..zip = String.Empty
End Sub
606 Безопасность пользователя и данных
Глава 12
Public Sub New(ByVal isLogin As Boolean,
ByVai newAuthenticationType As String,
-ByVai newFirstName As String, ByVai newLastName As String,
ByVai newAddress As String,
ByVai newCity As String, ByVai newState As String,
ByVai newZip As String)
Me..name = newFirstName + newLastName
Me._isAuthenticated = isLogin
Me._authenticationType = newAuthenticationType
Me._firstName = newFirstName
Me._lastName = newLastName
Me.„address = newAddress
Me._city = newCity
Me..state = newState
Me._zip = newZip
End Sub
Public Readonly Property IsAuthenticatedQ As Boolean
Implements Ildentity.IsAuthenticated
Get
Return Me.-IsAuthenticated
End Get
End Property
Public Readonly Property NameO As String Implements Ildentity.Name Get
Return Me._name
End Get
End Property
Public Readonly Property AuthenticationTypeO As String Implements Ildentity.AuthenticationType
Get
Retu rn Me._authenticationType
End Get
End Property
Public Readonly Property FirstNameO As String,
Get
Return Me.-firstName
End Get
End Property
Public Readonly Property LastNameO As String
Занятие 1
Аутентификация и авторизация пользователей QQ7
Get
Return Me. JLastName
End Get
End Property
Public Readonly Property AddressO As String
Get
Return Me..address
End Get
End Property
Public Readonly Property City() As String
Get
Return Me..city
End Get
End Property
Public Readonly Property StateO As String
Get
Return Me..state
End Get
End Property
Public Readonly Property Zip() As String
Get
Return Me..zip
End Get
End Property
End Class
11 C#
class Customidentity : lidentity {
// Реализовать закрытые переменные для стандартных свойств private bool isAuthenticated;
private string name, authenticationType;
// Реализовать закрытые переменные для нестандартных свойств private string firstName, lastName, address, city, state, zip;
// Разрешить создание пустого объекта public CustomldentityO {
this.name = String.Empty;
this.isAuthenticated = false;
this.authenticationType = "None";
608 Безопасность пользователя и данных	Глава 12
this.firstName = String Empty;
this.lastName = String Empty;
this.address = String.Empty;
this.city = String.Empty;
this.state = String.Empty;
this.zip = String.Empty;
}
// Разрешить вызывающему создать объект и опередить все свойства
public Customidentity(bool isLogin, string newAuthenticationType,
string newFirstName,
string newLastName, string newAddress, string newCity,
string newState, string newZip)
{
// Создать уникальное имя пользователя конкатенацией имени и фамилии
this.name = newFirstName + newLastName;
this.isAuthenticated = isLogin;
this.authenticationType = newAuthenticationType;
this.firstName = newFirstName;
this.lastName = newLastName;
this.address = newAddress;
this.city = newCity;
this.state = newState;
this.zip = newZip;
}
// Реализовать открытые только для чтения интерфейсы для стандартных свойств public bool IsAuthenticated
{ get { return this.isAuthenticated; } }
public string Name
{ get { return this.name; } }
public string AuthenticationType
{ get { return this.authenticationType; } }
// Реализовать открытые только для чтения интерфейсы для нестандартных свойств
public string FirstName
{ get { return this firstName; } }
public string LastName
{ get { return this.lastName; } }
public string Address
{ get { return this.address; } }
Занятие 1
Аутентификация и авторизация пользователей gQQ
public string City
{ get { return this.city; } }
public string State
{ get { return this.state; } }
public string Zip
{ get { return this.zip; } }
}
К СВЕДЕНИЮ Когда не стоит реализовывать lldentity
Данный код показывает нестандартную реализацию lldentity. Однако при расширении аутентификации Windows новыми свойствами, маркерами защиты или другими свойствами системы безопасности Windows, нестандартная идентификация должна быть основана на классе Windowsldentity. То же касается IPrincipal и WindowsPrincipal.
Создание класса нестандартного участника системы безопасности
Windowsldentity основан на lldentity, а классы WindowsPrincipal и GenericPrincipal — на интерфейсе IPrincipal. Объекты, основанные на интерфейсе IPrincipal, представляют контекст безопасности пользователя, в том числе идентификатор, роли и группы пользователя.
Для реализации IPrincipal нужно реализовать минимум один конструктор, одно свойство и один метод. Конструктор должен получать объект lldentity и массив строк, содержащий идентификаторы ролей; также можно добавлять перегруженные конструкторы. Свойство IPrincipal.Identity обязательно для реализации; оно должно возвращать объект удостоверений участника системы безопасности (который должен определяться при конструировании объекта). Реализуемый метод IPrincipal.IsInRole получает простую строку и запрашиваемую роль, возвращает true, если идентификатор участника системы безопасности является членом этой роли. В противном случае он возвращает false.
С помощью перегрузки IPrincipal можно добавлять довольно интересные функции:
	свойство Roles, которое возвращает массив строк, содержащий роли, членом которых является пользователь;
	методы IsInAllRoles и IsInAnyRole, определяющие, является ли пользователь членом нескольких ролей;
	методы IsHigherThanRole и IsLowerThanRole, разрешающие членство во вложенных группах. Например, для члена роли Presidents метод IPrincipal. IsHigherThanRole(“Vice-Presidents”) вернет true.
Класс нестандартного участника системы безопасности следует реализовать на основе IPrincipal', и, как минимум, переопределить конструктор, свойство Identity и метод IsInRole. К примеру, следующий класс реализует интерфейс IPrincipal без расширения функциональности:
’ VB
Public Class CustomPrincipal
Implements IPrincipal
’ Реализовать открытые переменные для стандартных свойств
610 Безопасность пользователя и данных
Глава 12
Private -identity As Ildentity
Private _roles As StringO ,v
Разрешить вызывающему создать объект и определить все его свойства
Public Sub New(ByVal identity As Ildentity, ByVai roles As StringO)
-identity = identity
roles.CopyTo(_roles, 0)
Array.Sort(_roles)
End Sub
Реализовать открытые только для чтения интерфейсы для стандартных свойств Public Function IsInRole(ByVal role As String) As'Boolean
Implements IPrincipal.IsInRole
If Array.BinarySearch(_roles, role) >= 0 Then
Return True
Else
Return False
End If
End Function
Public Readonly Property IdentityO As Ildentity _ Implements IPrincipal.Identity
, sm
Get
Return -identity
End Get
End Property
End Class
class CustomPrincipal : IPrincipal
private Ildentity -identity;
private string[] _roles;
// Разрешить вызывающему создать объект и определить все его свойства public CustomPrincipal(IIdentity identity, string!!] roles)
-identity = identity;
_roles = new string[roles.Length];
roles.CopyTo(_roles, 0);
Array.Sort(_roles);
}
public Ildentity Identity
Занятие. 1
Аутентификация и авторизация пользователей 61 *1
{ get { return .identity; } }
public bool IsInRole(string role)
{ return Array.BinarySearch(_roles, role) >= 0 ? true : false; } }
Создание объектов простых привилегированных пользовательских моделей
Если не нужно использовать классы, основанные на Ildentity и IPrincipal, встроенных в исполняющую среду, а необходима лишь базовая функциональность интерфейсов Ildentity и IPrincipal, используйте System. Security. Principal.Generic Identity и System.Security.Principal.GenericPrincipal. Данные классы реализуют только свойства и методы, требуемые интерфейсами. Каждый из них предоставляет конструкторы, с помощью которых приложения могут определить свойства каждого класса.
У Genericidentity два перегруженных конструктора. Для создания объекта Genericidentity достаточно указать имя пользователя, либо имя пользователя и тип аутентификации Эти значения впоследствии не могут быть изменены; их определяют при создании объекта. Следующий пример показывает оба способа:
' VB
Dim myllserl As Genericidentity = New GenericIdentityC'AHankin")
Dim myllser2 As Genericidentity = New GenericIdentityC’TAdams", "Smartcard")
// C#
Genericidentity myllserl = new GenericIdentityC’AHankin");
Genericidentity myUser2 = new GenericIdentityC’TAdams", "SmartCard");
У GenericPrincipal только один конструктор, для которого требуются объект Genericidentity и массив строк с именами ролей. Следующий пример дополняет предыдущий код объектом GenericPrincipal, где myllserl — предварительно созданный объект Genericidentity.
’ VB
Dim myllser1Roles() As String =
New StringO {"IT", "Users", "Administrators"}
Dim myPrincipall As GenericPrincipal =
New GenericPrincipal(myUser1, myUserlRoles)
// C#
String[] myUserIRoles = new String[]{"IT", "Users", "Administrators"}; GenericPrincipal myPrincipall =
new GenericPrincipal(myUser1, myUserIRoles);
После создания объекта участника системы безопасности метод myPrincipall.Isln-Role(“Users”) возвращает true.
RBS для нестандартных удостоверений и участников системы безопасности
При определении нестандартных интерфейсов Ildentity и IPrincipal или использовании Genericidentity и GenericPrincipal, можно воспользоваться декларативной и императивной защитой Windowsldentity и WindowsPrincipal. Jljin. этого выполните следующие действия:
612 Безопасность пользователя и данных
Глава 12
1.	Создайте объект lldentity или Genericidentity, представляющий текущего пользователя.
2.	Создайте объект IPrincipal или Generic Principal на основе объекта lldentity.
3.	Задайте свойство Thread.CurrentPrincipal объекта IPrincipal.
4.	При необходимости добавьте любые декларативные или императивные RBS-требования.
Следующее консольное приложение (требует импорта пространств имен System.Security.Permissions, System.Security.Principal и Sy stem.Threading) демонстрирует декларативные RBS-требования с классами Genericidentity и Generic Principal. В данном примере запустить метод TestSecurity могут только члены роли IT. Приложение создает два идентификатора и участника системы безопасности. Объект myUserl с именем пользователя AHankin является членом роли IT должен запускать этот метод. Объект myUser2 с именем пользователя TAdams не является членом данной роли.
' VB
Sub Main()
Dim myUserl As Genericidentity = New Genericldentity("AHankin")
Dim myllserlRoles As StringO = New StringO {"II", "Users", "Administrators"}
Dim myPrincipall As GenericPrincipal = _
New GenericPrincipal(myUser1, myUserIRoles)
Dim myUser2 As Genericidentity = New Genericldentity("TAdams")
Dim myUser2Roles As StringO = New StringO {"Users"}
Dim myPrincipa!2 As GenericPrincipal =
New GenericPrincipal(myUser2, myUser2Roles)
Try
Thread.CurrentPrincipal = myPrincipall
TestSecurity()
Thread.CurrentPrincipal = myPrincipal2
TestSecurity()
Catch ex As Exception
Console.WriteLine(ex.GetType.ToString + " caused by " + _ Thread.CurrentPrincipal.Identity.Name)
End Try
End Sub
<PrincipalPermissionAttribute(SecurityAction.Demand, Role:="IT")>
Private Sub TestSecurityO
Console.WriteLine(Thread.Currentprincipal.Identity.Name + " is in IT.") End Sub
// C#
static void Main(string[] args)
Занятие 1
Аутентификация и авторизация пользователей 613
{
Genericidentity myUserl = new GenericIdentity("AHankin");
String[] myUserlRoles = new String[]{"IT", "Users”, "Administrators"};
GenericPrincipal myPrincipall =
new GenericPrincipal(myUser1, myUserlRoles);
Genericidentity myUser2 = new GenericIdentityC’TAdams");
String[] myUser2Roles = new String[]{"Users"};
GenericPrincipal myPrincipal2 =
new GenericPrincipal(myUser2, myUser2Roles);
try
{
Thread.Currentprincipal = myPrincipall;
TestSecurityO;
Thread.Currentprincipal = myPrincipal2;
TestSecurityO;
}
catch(Exception ex)
{ Console.WriteLine(ex.GetType().ToStringO + " caused by " + Th read.Currentprincipal.Identity.Name); }
}
[PrincipalPermission(SecurityAction.Demand, Role = "IT")] private static void TestSecurityO
{ Console.WriteLine(Thread.Currentprincipal.Identity.Name + " is in IT."); }
Данное приложение генерирует следующий вывод, подтверждающий, что декларативное RBS-требование защищает метод TestSecurity от пользователей, не входящих в роль IT:
AHankin is in IT.
System.Security.SecurityException caused by TAdams
Обработка исключений аутентификации в потоках
При аутентификации удаленных компьютеров с помощью классов System.Net.Secu-rity.NegotiateStream или System.Net.Security.SslStream, .NET Framework сгенерирует исключение, если клиент или сервер не пройдет аутентификацию. Поэтому при работе с NegotiateStream или SslStream всегда следует принимать меры для перехвата следующих исключений:
	System.Security.Authentication.AuthenticationException
Исключения данного типа свидетельствуют о том, что следует запросить у пользователя другие удостоверения и повторить аутентификацию.
	System.Security.Authentication.InvalidCredentialException
Исключения данного типа свидетельствуют о том, что базовый поток находится в недопустимом состоянии и повторная аутентификация невозможна.
6 "| 4	Безопасность пользователя и данных
Глава 12
Практикум. Добавление в приложение защиты, основанной на ролях
Ваша задача — добавить в приложение защиту, огранивающую доступ пользователей по имени пользователя и членству в группах. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Вы должны доработать Калькулятор, добавив в него RBS. Следует использовать наиболее надежные методы; также должны быть выполнены следующие требования:
	метод, связанный с кнопкой Add, могут запускать только члены группы Users;
	метод multiply могут запускать только члены группы Administrators',
	обработчик кнопки Divide, может запустить только пользователь CPhilp',
	кнопки, к которым у пользователей нет доступа, нужно скрыть.
1.	С помощью Windows Explorer скопируйте версию C# или Visual Basic папки Chapter 12\Lessonl-Exercise 1 с прилагаемого компакт-диска в папку Му Docu-ments\Visual Studio Projects'^. Откройте решение.
2.	Добавьте в код пространства имен System.Security.Permissions и System.Security.Principal.
3.	Чтобы разрешить проверку членства в группах Windows задайте политику участников системы безопасности Windows Policy. Это нужно выполнить в методе, запускаемом при открытии формы, например в конструкторе формы. Ниже приводится соответствующий пример кода:
' VB
Public Sub New()
MyBase.NewO
InitializeComponent()
’ Задать контекст политики безопасности Windows
System.AppDomain.CurrentDomain.SetPrincipalPolicy(
PrincipalPolicy.WindowsPrincipal)
End Sub
// C#
public Form1()
{
InitializeComponent();
/I Задать контекст политики безопасности Windows
System.AppDomain.CurrentDomain.SetPrincipalPolicy( PrincipalPolicy.WindowsPrincipal);
}
4.	Выполним первое требование. Метод, связанный с кнопкой Add, могут запускать только члены группы Users. Вот пример кода addButton_Click'.
' VB
Try
’ Членство пользователя во встроенной группе Users необходимо
’ для его защиты с помощью императивного RBS-требования, т. к.
Занятие 1
Аутентификация и авторизация пользователей 615
данный метод вызывается событием Windows
Dim userPermission As PrincipalPermission =
New PrincipalPermission(Nothing, "BUILTIN\Users")
userPermission.Demand()
' Выполнить вычисления
Dim answer As Integer = (Integer.Parse(integer1.Text) +
Integer.Parse(integer2.Text))
answer Label. Text = answer. ToStringO
Catch ex As System.Security.SecurityException
' Вывести сообщение об отказе в доступе
MessageBox.Show("You have been denied access: " + ex.Message)
’ Здесь должен быть код регистрации ошибок
End Try
// C#
try
{
// Членство пользователя во встроенной группе Users необходимо
// для его защиты с помощью императивного RBS-требования, т. к.
// данный метод вызывается событием Windows
PrincipalPermission userPermission =
new PrincipalPermission(null, @"BUILTIN\Users");
userPermission.Demand();
// Выполнить вычисления
int answer = (int.Parse(integer1.Text) + int.Parse(integer2.Text));
answerLabel.Text = answer. ToStringO;
}
catch (System.Security.SecurityException ex)
// Вывести сообщение об отказе в доступе
MessageBox.Show("You have been denied access: " + ex.Message);
// Здесь должен быть код регистрации ошибок
}
5.	Теперь перейдем ко второму требованию. Метод multiply могут запускать только члены группы Administrators. Поскольку метод multiply не вызывается событием Windows, можно использовать декларативную защиту. Следующий код защищает метод multiply.
’ VB
<PrincipalPermission(SecurityAction.Demand,
Role:="BUILTIN\Administ rators")>
11 C#
[PrincipalPermission(SecurityAction.Demand,
Role = @"BUILTIN\Administrators")]
616 Безопасность пользователя и данных
Глава 12
6.	Теперь — к третьему требованию. Метод-обработчик кнопки Divide может запустить только пользователь CPhilp. Вот пример кода метода divideButton_Click'.
’ VB*
Объединить имена пользователя и компьютера
Dim allowUser As String = System.Environment.MachineName + "\cphilp" Try
Имя пользователя "cphilp" на локальном компьютере необходимо для его защиты с помощью императивного RBS-требования, т. к. данный метод вызывается событием Windows
Dim р As Principalpermission =
New PrincipalPermission(allowUser, Nothing) p.Demand()
Сверхсекретные математические расчеты...
Dim answer As Decimal = (Decimal.Parse(integer1.Text)
/ Decimal.Parse(integer2.Text))
answerLabel.Text = Decimal. Round (answer, 2).ToStringO
Catch ex As System.Security.SecurityException
' Вывести сообщение об отказе в доступе
MessageBox.Show("You have been denied access: " + ex.Message) Здесь должен быть код для регистрации ошибок
End Try
// C#
// Объединить имена пользователя и компьютера
string allowUser = System.Environment.MachineName + @"\cphilp"; try {
// Имя пользователя "cphilp" на локальном компьютере необходимо
// для его защиты с помощью императивного RBS-требования, т. к.
Ц данный метод вызывается событием Windows
Principalpermission р = new PrincipalPermission(allowUser, null).;
p. DemandO;
// Сверхсекретные расчеты...
Decimal answer = (Decimal.Parse(integer1.Text)
/ Decimal.Parse(integer2.Text));
answerLabel.Text = Decimal. Round (answer, 2). ToStringO;
catch (System.Security.SecurityException ex) {
// Вывести сообщение об отказе в доступе
MessageBox.Show("You have been denied access: " + ex.Message);
Il Здесь должен быть код для регистрации ошибок
}
Занятие 1
Аутентификация и авторизация пользователей 617
7.	Теперь выполним четвертое требование. Кнопки, к которым у пользователей нет доступа, нужно скрыть. Это нужно выполнить в методе, запускаемом при открытии формы, например в конструкторе формы. Ниже приводится соответствующий пример кода:
’ VB
Public Sub New()
MyBase.NewO
InitializeComponent()
Создать объект Windowsldentity, представляющий текущего пользователя Dim currentidentity As Windowsldentity = Windowsldentity.GetCurrentO
Создать объект WindowsPrincipal, представляющий текущего пользователя Dim currentprincipal As WindowsPrincipal =
New WindowsPrincipal(currentidentity)
Задать контекст политики безопасности Windows
System.AppDomain.CurrentDomain.SetPrincipalPolicy(
PrincipalPolicy.WindowsPrincipal)
Скрыть кнопки subtract и multiply, если пользователь
не является администратором
If Not currentprincipal.IsInRole(WindowsBuiltInRole.Administrator) Then subtractButton.Visible = False multiplyButton.Visible = False
End If
Скрыть кнопку Add, если пользователь не входит в группу Users
If Not currentPrincipal.IsInRole(WindowsBuiltInRole.User) Then addButton.Visible = False
End If
' Скрыть кнопку Divide, если имя пользователя не CPhilp
If Not (currentidentity.Name.ToLowerO =
System.Environment.MachineName.ToLower() + "\cphilp”) Then divideButton.Visible = False End If
End Sub
// C#
public Form1()
InitializeComponent();
// Создать объект Windowsldentity, представляющий текущего пользователя Windowsldentity currentidentity = Windowsldentity.GetCurrentO;
618 Безопасность пользователя и данных
Глава 12
// Создать объект WindowsPrincipal, представляющий текущего пользователя WindowsPrincipal currentprincipal =
* new WindowsPrincipal(currentldentity);
// Задать контекст политики безопасности Windows
System.AppDomain.CurrentDomain.SetPrincipalPolicy(
PrincipalPolicy.WindowsPrincipal);
// Скрыть кнопки subtract и multiply, если пользователь
// не является администратором
if (!currentprincipal.IsInRole(WindowsBuiltInRole.Administrator))
{
subtractButton.Visible = false;
multiplyButton.Visible = false;
}
// Скрыть кнопку Add, если пользователь не входит в группу Users
if (I currentprincipal.IsInRole(WindowsBuiltInRole.User)) addButton.Visible = false;
// Скрыть кнопку Divide, если имя пользователя не CPhilp
if (!(currentidentity.Name.ToLower() ==
System.Environment.MachineName.ToLowerO + @"\cphilp")) divideButton.Visible = false;
}
8.	Откомпилируйте и запустите проект. Проверьте его работу с различными учетными записями, в том числе Cphilp, членов групп Administrators и Users.
Резюме
	Аутентификация — это проверка подлинности субъекта, она требует уникальных удостоверений, которые непросто подделать. Авторизация — проверка наличия разрешений на выполнение запрошенных действий. Аутентификация определяет кто вы, и потому должна выполняться перед авторизацией, которая определяет наличие разрешений на доступ к ресурсам.
	Класс Windowsldentity обеспечивает приложениям .NET Framework доступ к свойствам учетных записей пользователей. Проверить имя текущего пользователя и тип аутентификации можно, создав объект Windowsldentity с помощью метода Windowsldentity. GetCurrent.
	Класс WindowsPrincipal позволяет сборкам запрашивать из БД безопасности Windows сведения о членстве пользователя в группах. Для проверки членства в группах текущего пользователя нужно создать объект WindowsPrincipal с помощью удостоверений текущего пользователя, после чего вызвать метод WindowsPrincipal.IsInRole.
	Класс PrincipalPermission предназначен для определения имени пользователя, роли и требований аутентификации.
	Декларативные RBS-требования ограничивают доступ к методу в целом, генерируя исключения при несоответствии текущего участника системы безопасности заданным требованиям доступа. Для работы с декларативными RBS-требованиями нужно
Занятие 1
Аутентификация и авторизация пользователей g-| g
определить политику участников системы безопасности, создать блок try/catch для обработки исключений из-за отказа в доступе и определить требования для доступа к методу.
 Для работы с императивными RBS-требованиями необходимо определить политику участников системы безопасности, создать блок try/catch для перехвата исключений из-за отказа в доступе, создать объект Principalpermission для определения требований доступа метода, после чего вызвать метод PrincipalPermission.Demand. Для принятия решений на основании членства в группах пользуйтесь методом WindowsPrincipal. Is In Role. Декларативные RBS-требования идеально подходят, когда нужно ограничить доступ к методу, который приложение вызывает непосредственно. Императивные RBS-требования удобны, когда необходимо защитить только часть метода или метод-обработчик события Windows.
 Для создания нестандартных классов удостоверений и участников системы безопасности нужно дополнить интерфейсы Ildentity и IPrincipal с помощью перегрузки существующих свойств и добавления собственных методов и свойств. Для создания простых нестандартных классов пользователей пользуйтесь классами Genericidentity и GenericPrincipal вместо интерфейсов Ildentity и IPrincipal. Чтобы создать декларативные и императивные RBS-требования для нестандартных удостоверений и участников системы безопасности задайте свойство Thread.CurrentPrincipal для данного участника системы безопасности.
 Если нужно установить соединение с помощью SslStream, следует перехватывать исключения различных типов. При перехвате AuthenticationException необходимо запросить у пользователя другие удостоверения. При перехвате InvalidCredentialException некоторые данные потока повреждаются и повторная аутентификация с прежними удостоверениями невозможна.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Необходимо ограничить доступ к методу на основании членства в локальных группах. При этом нужно использовать наиболее безопасный способ. Каким механизмом следует воспользоваться?
A. WindowsPrincipal. Is In Role.
В. Windowsldentity.IsInRole.
С. Императивные RBS-требования.
D. Декларативные RBS-требования.
2.	Необходимо ограничить доступ к методу-обработчику событий Windows, на основании членства в локальной группе. Если у пользователя недостаточно прав доступа, нужно зарегистрировать событие и вывести пользователю сообщение. При этом нужно наиболее безопасное решение. Каким механизмом следует воспользоваться?
A. WindowsPrincipal.IsInRole.
В. Windows Identity. IsInRole.
С. Императивные RBS-требования.
D. Декларативные RBS-требования.
<¥
620
Безопасность пользователя и данных
Глава 12
3.	Вы разрабатываете метод для консольного приложения, которое выводит список доступных пользователю параметров на основе его членства в группах. Каким механизмом следует воспользоваться?
A.	WindowsPrincipal. Is InRole.
В.	Windowsldentity. Is In Role.
С. Императивные RBS-требования.
D. Декларативные RBS-требования.
4.	Вы создаете интерфейс для серверной БД, содержащей имена пользователей и группы. БД довольно проста, содержит только имена пользователей и сведения об их членстве в группах. В приложении нужно использовать императивные и декларативные RBS-требования на основе этой БД. Какие из перечисленных классов соответствуют данным требованиям и при этом наиболее эффективны? (Укажите все верные ответы.)
A. Genericidentity.
В. GenericPrincipal.
С. Ildentity.
D. IPrincipal.
Занятие 2. Работа со списками
управления доступом
В занятии 1 рассматривалось ограничение доступа определенных пользователей к фрагментам кода. Сходную функциональность операционные системы предоставляют при помощи списков управления доступом (ACL). ACL — это наиболее распространенный механизм ограничения доступа к файлам, папкам, принтерам, сервисам, параметрам реестра, словом, почти к любым системным ресурсам. Разработчик должен знать, как работает ACL, по двум причинам:
	они позволяют ограничить доступ к конфиденциальным файлам, папкам и другим объектам приложения;
	при необходимости ACL позволяют разрешить пользователям доступ к файлам и другим объектам, обычно им недоступным.	,х /з©
Данное занятие посвящено основам ACL и анализу их конфигурирования в приложении.
• .->« >
Изучив материал этого занятия, вы сможете:
J объяснить назначение ACL и описать принципы определения действующих разрешений в Windows;
J объяснить назначение защиты с помощью ACL;
J просмотреть и сконфигурировать ACL с помощью классов из пространства имен System.Security.AccessControl.
Продолжительность занятия — 30 минут.
Занятие 2
Работа со списками управления доступом	521
Введение в DACL
Список управления избирательным доступом (discretionary access control list, DACL) — это механизм ограничения доступа к объекту, основанный на списках пользователей и групп, которым доступ разрешен либо запрещен. Windows ХР и Windows Server 2003, как и все ОС семейства Microsoft Windows, отслеживают привилегии пользователей для доступа к ресурсам посредством DACL. Если DACL не идентифицирует однозначно пользователя или его членство в группах, этот пользователь не получает доступ к объекту. По умолчанию DACL управляет владелец или создатель объекта, DACL содержит элементы списка управления доступом (АСЕ), управляющие доступом пользователей к объекту. АСЕ — это запись в DACL объекта, предоставляющая разрешения пользовате- • лю или группе.
Явно назначенные и унаследованные разрешения
Если DACL присваивается объекту непосредственно, он считается явно назначенным. Явное присвоение разрешений всем папкам, файлам, параметрам реестра и объектам службы каталогов Active Directory — непосильная задача. Для этого требуется обрабатывать множество ACL, что снижает производительность Windows.
Для повышения эффективности управления разрешениями Windows поддерживает наследование разрешений. После установки Windows у большинства объектов имеются только разрешения, унаследованные от родительских объектов. Этот принцип используется и в файловой системе. Следовательно, каждая папка, созданная в корневом каталоге С:\, наследует все разрешения от этого каталога. Аналогично, все подразделы, создаваемые в разделе HKEY_LOCAL_MACHINE\Software\, унаследуют все его разрешения.
Благодаря наследованию не приходится явно задавать разрешения при создании файла, папки, раздела реестра или другого объекта. Новый объект просто наследует разрешения объекта-родителя. Системные администраторы часто тратят много времени и сил на настройку наследования, обычно разработчикам следует полагаться на решения системного администратора. Также важно соблюдать осторожность при размещении создаваемых объектов. Например, временные файлы нужно создавать во временной папке, а файлы пользователя — в его личной папке.
Определение действующих разрешений в Windows
Для определений действующих разрешений Windows недостаточно просто найти имя пользователя в ACL. АСЕ присваивают права доступа пользователям или группам. Кроме того, пользователи могут являться членами множества групп, в том числе вложенных. Таким образом, у пользователя может быть несколько АСЕ в одном ACL. Чтобы выяснить действующие разрешения пользователя, нужно понимать, как определяются разрешения для пользователей с множеством АСЕ.
Разрешения, предоставляемые пользователю или группе, в которую он входит, являются кумулятивными. Например, если Мэри входит в группы Accounting и Managers, и ACL для некоторого файла предоставляет ее учетной записи право на чтение, группе Accounting — право на изменение, а группе Managers — полный доступ, у Мэри будет полный доступ. Здесь есть одно «но». АСЕ, запрещающие доступ, всегда имеют приоритет перед АСЕ, разрешающими доступ. Поэтому, если группе Accounting явно отказано ь доступе к файлу, Мэри не сможет его открыть. Несмотря на то, что Магу является членом группы Managers, обладающей полным доступом, АСЕ с запретом приведет к отказу в доступе к файлу всем членам группы Managers.
622
Безопасность пользователя и данных
Глава 12
Если в ACL нет АСЕ, связанных с пользователем, ему будет отказано в доступе к объекту. Другими словами, отсутствие явных разрешений на доступ к объекту означает явный запрет доступа.
АСЕ в .NET Framework
Разные ресурсы имеют специфичные для них разрешения в АСЕ. Хотя у файлов и реестра есть разрешение на полный доступ и удаление, разрешения на чтение и исполнение уникально для файлов и папок, а разрешение на запрос параметров — для реестра. Поэтому в .NET Framework каждому ресурсу соответствует свой набор классов. К счастью, разрешения для разных ресурсов работают сходным образом, и соответствующие классы происходят от стандартных базовых классов.
В .NET Framework перечислимое FileSystemRights определяет разрешения файлов и папок. В нем содержится 24 метода, отвечающих за стандартные (см. табл. 12-1) и специальные разрешения, которые можно просматривать и редактировать с помощью Windows Explorer.
Табл. 12-1. Ставдартные разрешения файлов и папок
Член FileSystemRights	Стандартное разрешение	Описание
FullControl	Full Control	Пользователи могут выполнять над файлом или папкой любые операции: создавать, удалять, а также изменять разрешения
Modify	Modify	Пользователи могут читать, редактировать и удалять файлы и папки
ReadAndExecute	Read & Execute	Пользователи могут просматривать файлы и запускать приложения
ListDirectory	List Folder Contents	Пользователи могут просматривать папку
Read	Чтение r	Пользователи могут просматривать файл или содержимое папки, но не могу запускать приложение [
Write	Write	Пользователи могут создавать файлы -в папке, но не могут их читать. Удобно для создания папки, в которую пользователи могут копировать файлы, не имея доступа и даже не подозревая о существовании в ней файлов других пользователей
Other members	Special Permissions	Особые разрешения более гибкие, они дополняют стандартные разрешения
Введение в SACL
Список управления доступом системы безопасности (security access control list, SACL) — это механизм регистрации событий, определяющий аудит доступа к файдам и папкам. В отличие от DACL, SACL не может ограничить доступ к файлу или папке. SACL зано
Занятие 2
Работа do списками управления доступом g23
сит записи в журнал событий при каждом обращении к файлу или папке. Это можно использовать при поиске проблем с доступом и вторжений. .
Для специалиста по информационной безопасности SACL является важным средством обнаружения вторжений Системные администраторы же склонны использовать SACL для определения разрешений, необходимых для корректной работы приложения. Разработчики используют SACL для отслеживания ресурсов, недоступных приложениям, при устранении проблем из-за недостатка привилегий.
ПРИМЕЧАНИЕ Различия SACL и DACL
Для сдачи экзамена важно понимать разницу между SACL и DACL. Этот вопрос также часто задают на разного рода собеседованиях. К счастью, тут все просто: DACL ограничивают доступ, a SACL ведет учет доступа. На практике при написании приложений никто всерьез не задумывается о SACL, но многие тратят часы и часы на диагностику проблем, связанных с DACL. Поэтому в данной книге под ACL подразумевается именно DACL.
По умолчанию Windows не регистрирует события аудита, даже если вы добавили SACL. Для начала нужно активизировать на компьютере политику безопасности Аудит доступа к объектам (Audit Object Access):
1.	Откройте консоль Локальная политика безопасности (Local Security Policy) из группы Администрирование (Administrative Tools).
2.	Разверните узел Локальные политики (Local Policies) и выберите Политика аудита (Audit Policy).
3.	В правой панели дважды щелкните политику Аудит доступа к объектам (Audit Object Access). Установите флажок Отказ (Failure) для разрешения учета неудач и Успех (Success) для разрешения учета успехов обращения к ресурсам.
В домене Active Directory администраторы домена могут разрешить учет объектов для всех компьютеров с помощью параметров групповой политики (Group Policy).
Программный просмотр и конфигурирование ACL
Пространство имен System.Security.AccessControl поддерживает множество классов для просмотра и конфигурирования ACL объектов различных типов. В следующих разделах предоставлен обзор этого пространства имен, а также описаны процедуры анализа и изменения ACL.
Пространство имен System.Security.AccessControl
Классы из пространства имен System.Security.AccessControl предоставляют программный доступ к DACL, SACL и АСЕ для файлов, папок, разделов реестра, криптографических ключей, описателей Event Wait, мьютексов и семафоров.
ПРИМЕЧАНИЕ .NET 2.0
Пространство имен System.Security.AccessControl — новинка .NET 2.0. Раньше разработчикам приходилось вызывать неуправляемый код, чтобы проанализировать или изменить ACL.
624
Безопасность пользователя и данных
Глава 12
Для каждого типа ресурсов пространство имен System.Security.AccessControl предоставляет три класса ACL:
	<Type>Security
Наиболее востребованный класс, предоставляет методы для поиска DACL (GetAccess Rules) и SACL (GetAudit Rules), добавления или удаления ACL (AddAccess Rule, Remove Access Rule, AddAuditRule и RemoveAuditRule). Данные классы — производные от NativeObjectSecurity.
	<Type>AccessRule
Представляет набор разрешений пользователя или группы. Данные классы происходят от AccessRule, а тот — от AuthorizationRule.
	< Type >AuditRule
Представляет набор правил для аудита активности пользователя или группы. Данные классы происходят от AuditRule, а тот — от AuthorizationRule.
Кроме того, вызвав <Type>Security.GetAccessRules, можно извлечь экземпляр класса AuthorizationRuleCollection. Данный класс содержит набор экземпляров <Type>AccessRule или <Type>AuditRule, которые можно перебрать в цикле, анализируя ACL объекта.
Анализ ACL
Для анализа ACL выполните следующее:
1.	Создайте экземпляр класса, производного от NativeObjectSecurity, например Direc-torySecurity, FileSecurity, RegistrySecurity или MutexSecurity. Для создания таких объектов некоторые классы из пространства имен Microsoft.Win32 поддерживают метод GetAccessControl.
2.	Вызовите метод GetAccessRules для извлечения экземпляра AuthorizationRuleCollection.
3.	Обработайте элементы AuthorizationRuleCollection, чтобы извлечь и проанализировать нужные ACL.
Следующий пример кода (требуется пространство имен System.Security.AccessControl и System.Security.Principal) иллюстрирует отображение разрешений на доступ (DACL) к папке (аналогично работают с разрешениями файлов, параметров реестра и других объектов):
' VB
В следующей строке также можно вызвать Directory.GetAccessControl
Dim ds As Directorysecurity = New DirectorySecurity("C:\Program Files”, AccessControlSections.Access)
Dim arc As AuthorizationRuleCollection = ds.GetAccessRules(True, True, GetType(NTAccount))
For Each ar As FileSystemAccessRule In arc
Console.WriteLine(ar.IdentityReference.ToString + ”: ” +
ar.AccessControlType.ToString + ” " + ar.FileSystemRights.ToString) Next
// C#
// В следующей строке также можно вызвать Directory.GetAccessControl Directorysecurity ds = new DirectorySecurity(@”C:\Program Files",
AccessCont rolSections.Access);
AuthorizationRuleCollection arc = ds.GetAccessRules(true, true, typeof(NTAccount));
Занятие 2
Работа со списками управления доступом g25
foreach (FileSystemAccessRule ar in arc)
Console.WriteLine(ar.IdentityReference +	" + ar.AccessControlType +
" " + ar.FileSystemRights);
Подобная процедура годится и для других типов (естественно, класс для извлечения объекта будет другим). Следующий пример (требует пространства имен System.Security.AccessControl, System.Security.Principal и Microsoft. Win32), выводит разрешения на доступ к разделу реестра HKEY_LOCAL_MACHINE:
' VB
Dim rs As Registrysecurity = Registry.LocalMachine.GetAccessControl
Dim arc As AuthorizationRuleCollection = rs.GetAccessRules(True,
True, GetType(NTAccount))
For Eacn ar As RegistryAccessRule In arc
Console.WriteLine(ar.IdentityReference.ToString + ": ”
+ ar.AccessControlType.ToString + " ” + ar.RegistryRights.ToString) Next
// C#
Registrysecurity rs = Registry. LocalMachine.GetAccessControlO;
AuthorizationRuleCollection arc = rs.GetAccessRules(true, true, typeof(NTAccount));
foreach (RegistryAccessRule ar in arc)
Console.WriteLine(ar.IdentityReference + ": "
+ ar.AccessControlType + " " + ar.RegistryRights);
Для анализа SACL выполните те же действия, но вместо GetAccessRules вызовите GetAuditRules, используйте также необходимые классы аудита.
Конфигурирование ACL
Для конфигурирования ACL выполните следующее:
1.	Вызовите метод GetAccessControl для получения экземпляра класса, производного от NativeObjectSecurity, такого как Directory Security, FileSecurity, RegistrySecurity или MutexSecurity.
2.	Добавьте или удалите ACL. Обычно для этого указывают имя и группу пользователя, перечислимое со списком привилегий (например, FileSystemRights или RegistryRights), а также перечислимое AccessControlType, определяющее предоставленные и отозванные разрешения.
3.	Вызовите метод SetAccessControl для внесения изменений.
Следующий пример (требует пространства имен System.Security.AccessControl и System.IO) показывает добавление правила доступа к папке разрешением чтения (Read) папки C:\test. Подобным образом можно добавить ACL к файлу, параметру реестра или другому объекту:
’ VB
Dim dir As String = "C:\test"	♦
Dim ds As Directorysecurity = Directory.GetAccessControl(dir) ds.AddAccessRule(New FileSystemAccessRule("Guest”,
FileSystemRights.Read, AccessControlType.Allow))
Directory.SetAccessControl(dir, ds)
626
Безопасность пользователя и данных
Глава 12
И с#
string dir = @"C:\test";
Directorysecurity ds = Directory.GetAccessControl(dir);
ds.AddAccessRule(new FileSystemAccessRule("Guest”, FileSystemRights.Read, AccessControlType.Allow));
Di recto ry.SetAccessCont rol(dir, ds);
Для удаления правила доступа замените AddAccessRule на RemoveAccessRule.
Практикум. Работа с DACL
В данном практикуме вы научитесь работать с DACL файлов и папок, а также «спасать» папки, доступ к которым явно запрещен хозяевами. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение. Создание папки с явно заданными разрешениями
Ваша задача — разработать приложение, которое создает папку C:\Guest и предоставляет пользователю Guest разрешение на ее чтение. Затем необходимо создать в этой папке файл и вывести разрешения на доступ к этим файлу и папке, чтобы проверить работу приложения.
1.	Создайте консольное приложение на Visual Basic или С#.
2.	Добавьте в проект ссылки на пространства имен System.Security.AccessControl, System.Security.Policy, System.Security.Principal и System.IO.
3.	В методе Main напишите код для создания объекта Directory Security, который разрешает пользователю Guest чтение папки. Создайте папку путем определения объекта Directory Security. Ниже приводится соответствующий пример кода:
• VB
Dim ds As Directorysecurity = New DirectprySecurity
ds.AddAccessRule(New FileSystemAccessRule("Guest", FileSystemRights.Read, AccessControlType.Allow))
Di recto ry.C reateDi recto ry("C:\Guest", ds)
// C#
Directorysecurity ds = new Di recto rySecurityO;
ds.AddAccessRule(new FileSystemAccessRule("Guest", FileSystemRights.Read, AccessControlType.Allow));
Di recto ry.C reateDi recto ry(@"C:\Guest”, ds);
4.	Теперь создайте в этой папке файл Data.dat, как показано в следующем примере:
' VB
File.Create("С:\Guest\Data.Data")
// C#
File.Create(@"C:\Guest\Data.Data");
5	Откомпилируйте и запустите полученное приложение При попытке создания файла исполняющая среда должна сгенерировать исключение, поскольку вы не предоставили себе разрешений на изменение папки. Папка не унаследовала разрешений, так
Занятие 2
Работа со списками управления доступом §27
как они были явно заданы при создании папки. Если же создать папку, не указывая разрешения на доступ, а затем изменить их, папка унаследует разрешения от объекта-родителя.
6.	С помощью Windows Explorer просмотрите разрешения папки C:\Guest. Если приложение работает правильно, разрешение на чтение должно быть только у учетной записи Guest, у других пользователей доступа быть не должно.
К СВЕДЕНИЮ Разрешения на доступ к файлам в Windows ХР
О просмотре и редактировании разрешений на доступ к файлам в Windows ХР — в статье Microsoft Knowledge Base #308418 по адресу http://support.microsoft.com/kb/308418.
7.	Перед удалением папки C:\Guest нужно получить права ее владельца. Войдите в систему в качестве члена группы Administrators, откройте окно свойств C:\Guest. На вкладке Безопасность (Security) диалогового окна Свойства: Guest (Guest Properties), щелкните кнопку Дополнительно (Advanced). После этого щелкните Владелец (Owner), установите флажок Заменить владельца подконтейнеров и объектов (Replace Owner On Subcontainers And Objects) и щелкните OK. Щелкните Да (Yes) и снова OK.
8.	Теперь в Проводнике Windows удалите папку C:\Guest.
Резюме
	DACL предназначены для ограничения доступа к файлам, папкам и другим объектам операционной системы. По умолчанию дочерние объекты (такие как папки нижележащего уровня) наследуют ACL от родительских объектов (таких как корневой каталог).
	SACL определяют условия аудита доступа к объекту.
Члены пространства имен System.Security.AccessControl можно просматривать и конфигурировать ACL разных объектов, в том числе файлов, папок, разделов реестра, криптографических ключей, описателей Event Wait, семафоров и мьютексов. У этих объектов имеется по три класса, производные от NativeObjectSecurity, AccessRule и AuditRule.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Перечислите ресурсы, доступом к которым можно управлять посредством .NET Framework.
А. Файлы.
В. Разделы реестра.
С. Принтеры.
D. Сетевые ресурсы.
2.	Какой из приведенных ниже примеров кода корректно завершает изменения ACL?
• VB
Dim dir As String = "C:\MyApp
Dim ds As DirectorySecurity = Directory.GetAccessControl(dir)
628 Безопасность пользователя и данных
Глава 12
ds.AddAccessRule(New FileSystemAccessRuleC’Administrator", FileSystemRights.FullControl, AccessControlType.Allow))
Di recto ry.SetAccessCont rol(di r, ds)
// C#
string dir = @"C:\myApp";
Directorysecurity ds = Directory.GetAccessControl(dir);
ds.AddAccessRule(new FileSystemAccessRule("Guest”, FileSystemRights.FullControl, AccessControlType.Allow));
Directory.SetAccessControl(dir, ds);
A.
' VB
Di rectory.SetAccessCont rol(di r, ds)
// C#
Directory.SetAccessControl(dir, ds);
B.
 VB
Di recto ry.CreateDi recto ry(di r, ds)
// Cit
Directory.CreateDirectory(dir, ds);
C.
 VB
Di recto ry.SetAccessCont rol(ds)
// C#
Di recto ry.SetAccessCont rol(ds);
D.
" VB
Directory.CreateDirectory(ds)
// C#
Directory.CreateDirectory(ds);
3.	Какой из следующих классов описывает SACL раздела реестра?
A.	Registry Access Rule.
В.	Registry Audit Rule.
C.	AccessRule.
D.	Audit Rule.
4.	Что возвращает метод DirectorySecurity.GetAccessRules?
А. Базовый объект Collection, содержащий объекты AccessRule.
В. Базовый объект Collection, содержащий объект ы File Sy stem Access Rule.
С. Экземпляр AuthorizationRuleCollection, содержащий объекты FileSystemAccessRule.
D. Экземпляр AuthorizationRuleCollection, содержащий объекты AuthorizationRule.
Занятие 3
Шифрование и расшифровка данных §29
Занятие 3. Шифрование и расшифровка данных
Данные наиболее уязвимы при хранении в постоянной памяти (на дисках) и при передаче по сети. Хотя с помощью требований разрешений и ACL можно управлять доступом к приложениям и данным, атакующий, получив доступ к жесткому диску или сетевой инфраструктуре, способен, обойдя программную защиту, получать и изменять секретную информацию. Впрочем, защита от подобных атак возможна. Так, конфиденциальность и целостность данных, хранимых или передаваемых приложением, можно защитить посредством шифрования. .NET Framework предоставляет классы для поддержки разных типов шифрования, в том числе симметричного и асимметричного, а также хэширования и цифровой подписи. В этом занятии описаны все эти типы криптографических средств.
Изучив материал этого занятия, вы сможете:
J шифровать и расшифровывать данные с помощью секретного ключа (симметричное шифрование);
J шифровать и расшифровывать данные с помощью открытого ключа (асимметричное шифрование);
J использовать хэш-функции для проверки целостности данных;
J снабжать файлы цифровой подписью для верификации их подлинности и неизменности его данных.
Продолжительность занятия — 90 минут.
Шифрование и расшифровка с симметричным ключом
С шифрованием мы сталкиваемся уже в раннем возрасте. Детский шифр, придуманный для защиты от воображаемых шпионов, представляет собой секретный ключ в виде колец, позволяющих перевести шифр-текст в открытый текст. Поворот кольца позволяет соотнести символы шифра с символами открытого текста, и чтобы прочитать шифрованную депешу, нужно знать, как повернуть кольца-ключи относительно друг друга. Естественно, для обмена шифрованными сообщениями юные «агенты» сначала должны договориться о взаимном расположении колец. После этого они могут свободно передавать зашифрованные сообщения, будучи уверенными, что никто их не расшифрует. Для расшифровки мало захватить шифр-кольца, нужно знать их взаимное расположение.
Шифр-кольца — типичный пример симметричного шифрования, поскольку и отправитель, и получатель для расшифровки сообщения должны знать секретный ключ (взаимное расположение колец). Сегодня симметричные ключи — это не только игрушка, но и распространенный механизм криптозащиты связи. Многие с детства считают шифрование весьма занимательным вопросом. Многие программисты и в зрелом возрасте обожают встраивать его в свои приложения, значительно снижая шансы компрометации секретных данных.
630
Безопасность пользователя и данных
Глава 12
Шифрование с симметричным ключом
Шифрование с симметричным или секретным ключом — метод криптозащиты, где для шифрования и расшифровки данных используется один и тот же секретный ключ. Симметричные криптоалгоритмы (шифры) обрабатывают открытый текст с помощью секретного ключа и генерируют зашифрованные данные, называемые шифр-текстом. Без секретного ключа шифр-текст очень сложно преобразовать в открытый текст. На рис. 12-2 показана схема симметричного шифрования.
Рис. 12-2. В симметричном шифровании для шифрования и расшифровки используется один и тот же ключ
Симметричные алгоритмы работают очень быстро и идеально подходят для шифрования больших объемов данных. Хотя симметричное шифрование довольно безопасно, атакующий при наличии шифр-текста и достаточного количества времени все же может вывести открытый текст. Для получения открытого текста атакующему достаточно применить атаку подбором ключа, то есть по очереди сгенерировать все возможные симметричные ключи, пока один из них не подойдет. Время перебора всех ключей составляет сотни лет, если не больше, поскольку приходится перепробовать минимум 2s6 ключей. Более стойкие симметричные алгоритмы используют ключи большей длины, что экспоненциально увеличивает время, необходимое для их взлома.
Недостаток шифрования секретным ключом заключается в необходимости предварительного согласования ключа. Согласование симметричного ключа является сложной
Занятие 3
Шифрование и расшифровка данных	531
задачей, т. к. сам ключ зашифровать нельзя. Вообще, те, кто использует шифрование, не доверяют системе и хотят лишить атакующих доступа к данным. Следовательно, у пользователей должен быть безопасный способ передачи секретных ключей. После обмена секретными ключами стороны могут свободно обмениваться зашифрованными данными. Впрочем, ключи должны регулярно меняться по той же причине, по которой необходима периодическая смена паролей. Каждый раз при смене ключа пользователям необходим безопасный механизм связи.
На рис. 12-3 показан способ передачи зашифрованного сообщения и ключа с помощью разных механизмов связи, разрешающих дешифровать текст получателю и блокирующий чтение сообщения атакующим, перехватившим информацию из сети. Ключи часто передают устно, по телефону, пересылают по почте и доставляют получателю лично. Согласовав общий секретный ключ, оба участника взаимодействия могут использовать его для шифрования и расшифровки данных сколько угодно раз.
Рис. 12-3. Шифрование с симметричным ключом требует передачи ключа и зашифрованного документа по разным каналам
Необходимость согласования общего секретного ключа не позволяет считать симметричное шифрование надежным средством защиты данных при передаче по случайным сетевым подключениям. Например, симметричный ключ не подходит для защиты связи между клиентом и Web-сервером, потому что Интернет-пользователи не станут ждать сутками, пока владельцы Web-сайта пришлют им секретные ключи в конверте. Вместо этого для защиты сеансов связи через Интернет используются асимметричные ключи.
Классы симметричных криптоалгоритмов в .NET Framework
*
Большинство криптографических функций .NET Framework, включая четыре алгоритма симметричного шифрования, встроено в пространство имен System.Security.Cryptography. В табл. 12-2 приводятся классы, реализующие алгоритмы симметричного шифрования.
632 Безопасность пользователя и данных
Глава 12
Табл. 12-2. Классы симметричного шифрования
Класс *	Длина ключа	Описание
RijndaelManaged	128-256 бит с инкрементом по 32 бита	.NET-реализация алгоритма симметричного шифрования Rijndael. Известен также как государственный стандарт США — Advanced Encryption Standard, AES. RijndaelManaged — единственный управляемый класс симметричного шифрования, остальные классы шифрования вызывают неуправляемый код. Поэтому RijndaelManaged предпочтителен для приложений, работающих в частично доверяемой среде
RC2	Непостоянна	Стандарт шифрования, разработанный для замены DES', использует ключи переменной длины
DES	56 бит	Data Encryption Standard (DES) — алгоритм симметричного шифрования со сравнительно коротким ключом; уязвим для атак подбором, поэтому не рекомендуется к использованию. Тем не менее, его продолжают широко применять, поскольку он совместим со многими унаследованными платформами
TripleDES	156 бит, из которых для эффективного шифрования задействованы только 112	.NET-реализация алгоритма симметричного шифрования Triple DES (3DES), по существу представляет собой троекратное DES-шифрование
Все классы симметричных алгоритмов происходят от базового класса System.Secu-rity.Cryptography.SymmetricAlgorithm и поддерживают рад общих свойств:
	BlockSize
Получает или задает размер блока для криптоопераций (в битах). Размер блока — это число битов, одновременно обрабатываемых алгоритмом. Обычно не имеет значения для разработки приложений.
	FeedbackSize
Получает или задает размер значения, возвращаемого криптографической операцией (в битах). Это один из параметров криптоалгоритма; разработчик может смело игнорировать данное свойство.
	IV
Получает или задает вектор инициализации симметричного алгоритма. Как и в случае свойства Key, у шифрующей и дешифрующей сторон должны быть одинаковые значения этого свойства. Во избежание издержек на безопасную передачу IV между шифратором и дешифратором следует использовать статическое определение IV в приложении или получать его из свойства Key.
Занятие 3
Шифрование и расшифровка данных 533
ПРИМЕЧАНИЕ Вектор инициализации (IV)
Вектор инициализации — это значение, используемое симметричным криптоалгоритмом для шифрования первого блока информации; усложняет взлом шифра. Разработчику не обязательно знать роль IV в шифровании, но за синхронизацию значений IV у шифрующей и дешифрующей сторон отвечает он.
	Key
Получает или задает секретный ключ симметричного алгоритма. Если ключи не определены явно, они генерируются автоматически. После шифрования нужно сохранить значение этого свойства и передать его дешифрующей стороне.
	KeySize
Получает или задает размер секретного ключа симметричного алгоритма (в битах). При создании объекта симметричного криптоалгоритма исполняющая среда выбирает наибольший ключ из поддерживаемых платформой, поэтому данное свойство обычно можно игнорировать. Если же получатель сообщения не поддерживает такой же размер ключа, что и отправитель, с помощью этого свойства следует задать максимальный размер ключа, поддерживаемый обоими участниками взаимодействия.
	LegalBlockSizes
Массив KeySize, получает размеры блока, поддерживаемые симметричным алгоритмом. У каждого элемента массива есть свойства MinSize и MaxSize, определяющие диапазон допустимых размеров ключа (в битах), и свойство Skip Size, определяющее шаг допустимых размеров ключа (в битах).
	LegalKeySizes
Массив KeySize, получает размеры ключа, поддерживаемые симметричным алгоритмом. У каждого элемента массива есть свойства MinSize и MaxSize, определяющие диапазон допустимых размеров ключа (в битах), и свойство Skip Size, определяющее шаг допустимых размеров ключа (в битах).
	Mode
Содержит одно из значений перечислимого CipherMode, по умолчанию — Cipher Block Chaining (СВС). Значение по умолчанию подходит для большинства случаев, изменять это свойство следует у обоих участников взаимодействия.
	Padding
Перечислимое PaddingMode, определяет способ, которым алгоритм шифрования заполняет «зазор» между размером блока, определенным алгоритмом, и длиной шифруемых данных. В большинстве случаев подходит значение по умолчанию.
Кроме того, у всех классов симметричных алгоритмов есть следующие методы (стандартные методы объектов опущены):
	Create Decryptor
Для расшифровки сообщений необходимо создать объект симметричного алгоритма и вызвать данный метод, чтобы получить объект ICryptoTransform, который может использовать CryptoStream для расшифровки потока.
	CreateEncryptor
Создает объект симметричного шифрования, который используется CryptoStream для расшифровки потока.
634 Безопасность пользователя и данных
Глава 12
	GeneratelV
Генерирует случайный IV. Как правило, вызывать этот метод не требуется, поскольку IV генерируются автоматически, если он не задан явно. Этот метод вызывается, только когда нужно сменить IV.
	GenerateKey
Генерирует случайный ключ. Как и GeneratelV, необходим только для смены ключа.
	ValidKeySize
Определяет, подходит ли заданный ключ для текущего алгоритма и возвращает булево значение. Пользуйтесь данным методом при работе с неизвестным классом симметричного алгоритма для проверки пригодности ключа.
СОВЕТ Выбор симметричного криптоалгоритма
Используйте алгоритм AES, если отправитель и получатель данных работают под Windows ХР или выше, в остальных случаях пользуйтесь Triple DES. AES признан правительством США наиболее безопасным из всех симметричных криптоалгоритмов, поддерживаемых .NET Framework. Данный алгоритм поддерживает 128-, 192- и 256-битные ключи. Второй аргумент в пользу AES в том, что это единственный алгоритм, встроенный в .NET Framework, остальные алгоритмы вызывают неуправляемый код.
Создание симметричного ключа
Перед шифрованием и расшифровкой сообщений с помощью симметричного алгоритма у автора и получателя сообщения должны быть идентичные ключи. В роли ключа может выступать любой блок данных. Длина ключей алгоритмов симметричного шифрования должна быть одинаковой, поэтому заносить в свойство Key вводимый пользователем пароль сложно. Для генерации случайного ключа пользуются объектом симметричного алгоритма. Если свойство Key задано, но нужен новый случайный ключ, вызовите метод GenerateKey.	*
Можно также сгенерировать подходящий ключ на основе пароля пользователя, если вы доверяете пользователям передачу пароля между отправителем и получателем. Пароли нельзя использовать о качестве шифровальных ключей, прежде их следует преобразовать с помощью класса System.Security .Cryptography. Rfc2898Derive Bytes. Это удобно, когда стороны уже согласовали общий секретный ключ. Например, если при создании собственного механизма аутентификации приложению известны имя и пароль пользователя, можно, объединив их, получить общий ключ для шифрования и расшифровки.
ПРИМЕЧАНИЕ .NET 2.0
Rfc2898DeriveBytes — новинка .NET Framework. Он выполняет те же действия, что и PasswordDeriveBytes, но лучше, т.к. поддерживает стандарт. PasswordDeriveBytes также позволяет при необходимости задать алгоритм хэширования.
Кроме пароля пользователя, Rfc2898DeriveBytes требует три значения: значение salt, IV и число итераций для создания ключа. В идеале все эти значения генерируются случайным образом. Изменение любого из них порождает другой ключ, который должен быть и у шифрующей, и у дешифрующей сторон. Поэтому сгенерированные случайные значения этих параметров следует передавать, как и пароли. Поэтому защищенный обмен такими значениями обычно невозможен. Вместо этого можно прописать их в коде приложения, но безопаснее генерировать их на основе общей секретной информации, такой как пароль.
Занятие 3
Шифрование и расшифровка данных ggg
Создание симметричных ключей на основе пароля требует синхронизации следующих значений у шифрующей и дешифрующей сторон:
	пароль;
	значение salt;
	число итераций, используемых для создания ключа (можно принять значение по умолчанию).
Проще всего определить данные значения, передав их конструктору. После инициализации можно извлечь ключ, вызвав метод Rfc2898DeriveBytes .GetBytes. GetBytes принимает число байт, которое он должен вернуть в виде целого числа. После получения ключа определите длину, основываясь на длине значений свойств JKeySize и BlockSize объектов алгоритмов. Обратите внимание, что KeySize и BlockSize определяются количеством бит, тогда как метод Rfc2898DeriveBytes.GetBytes требует число байт. Чтобы определить число байтов, нужно поделить длину ключа в битах на 8.
Помимо общего ключа алгоритм шифрования требует идентичности IV у шифрующей и дешифрующей сторон. Если эти стороны имеют лишь общий пароль, для оптимальной безопасности следует сгенерировать IV на основе этого пароля. Несмотря на то, что длина ключа определяется свойством KeySize, длина IV задается свойством BlockSize объекта криптоалгоритма.
Следующий пример кода генерирует ключ и IV для объекта myAlg SymmetricAlgorithm с помощью статического пароля (в реальном приложении его вводит пользователь):
VB .
' Пароль (в реальном приложении вводится пользователем)
Dim password As String = "P@S5w0r]>"
‘ Создать объект алгоритма
Dim myAlg As RijndaelManaged = New RijndaelManagedO
‘ Получить ключ и использовать его для определения алгоритма
Dim salt As Byte() = System.Text.Encoding.ASCII.GetBytes("This is my salt") Dim key As Rfc2898DeriveBytes = New Rfc2898DeriveBytes(password, salt) myAlg.Key = key.GetBytes(myAlg.KeySize / 8) myAlg.IV = key.GetBytes(myAlg.BlockSize / 8)
11 C#
// Пароль (в реальном приложении вводится пользователем) string password = "P@S5w0r]>";
// Создать объект алгоритма
RijndaelManaged myAlg = new RijndaelManagedO;
// Получить ключ и использовать его для определения алгоритма byte[] salt = Encoding.ASCII.GetBytes("This is my salt");
Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(password, salt);
myAlg.Key = key.GetBytes(myAlg.KeySize / 8);
myAlg.IV = key.GetBytes(myAlg.BlockSize /8);
636 Безопасность пользователя и данных
Глава 12
Шифрование и расшифровка сообщений с помощью симметричных ключей
Согласовав общий ключ, шифрующая и дешифрующая стороны могут обмениваться зашифрованными сообщениями. .NET Framework упрощает данный процесс. Работа с шифрованием аналогично обычным чтению-записи в стандартные файлы и потоки, для поддержки шифрования требуется добавить всего несколько строк кода:
1.	Создайте объект Stream для чтения-записи в память или в файл.
2.	Создайте объект Symmetric Algorithm.
3.	Задайте ключ и/или IV алгоритма.
4.	Вызовите SymmetricAlgorithm.CreateEncryptor() или SymmetricAlgorithm.CreateDecryptor() для создания объекта ICryptoTransform.
5.	Создайте объект CryptoStream с помощью объектов Stream и ICryptoTransform.
6.	Выполните чтение или запись в объект CryptoStream, как в любой другой объект Stream.
Следующее консольное приложение иллюстрирует чтение незашифрованного файла C:\Boot.ini, шифрование его по алгоритму AES и сохранение результатов в новом файле (требуются пространства имен System.IO и System.Security.Cryptography).
' VB
Dim inFileName As String = "C:\Boot.ini"
Dim outFileName As String = "C:\Boot.ini.enc"
Шаг 1: Создать объекты Stream
Dim inFile As FileStream = New FileStream(inFileName, FileMode.Open, FileAccess.Read) Dim outFile As FileStream = New FileStream(outFileName.
FileMode.OpenOrCreate, FileAccess.Write)
Шаг 2: Создать объект SymmetricAlgorithm
Dim myAlg As SymmetricAlgorithm = New RijndaelManaged
Шаг 3: Задать ключ (необязательно)
myAlg.GenerateKey()
Прочитать незашифрованный файл в fileData
Dim fileData(inFile.Length - 1) As Byte
inFile.Read(fileData, 0, CType(inFile.Length, Integer))
Шаг 4: Создать объект ICryptoTransform
Dim encryptor As ICryptoTransform = myAlg.CreateEncryptor
Шаг 5: Создать объект CryptoStream
Dim encryptStream As CryptoStream = New CryptoStream(outFile, encryptor, CryptoStreamMode.Write)
Шаг 6: Записать содержимое в CryptoStream
encryptStream.Write(fileData, 0, fileData.Length)	»
Занятие 3
Шифрование и расшифровка данных 537
’ Закрыть описатели файла
encryptStream.Close()
inFile. CloseO outFile CloseO '
// C#
string inFileName = ©"C:\Boot.ini";
string outFileName = @"C:\Boot.ini.enc";
// Шаг 1. Создать объекты Stream
FileStream inFile = new FileStream(inFileName, FileMode.Open, FileAccess Read); FileStream outFile = new FileStream(outFileName, FileMode.OpenOrCreate, FileAccess.Write);
// Шаг 2: Создать объект SymmetricAlgorithm
SymmetncAlgorithm myAlg = new RijndaelManagedO
// Шаг 3: Задать ключ (необязательно) myAlg.GenerateKey();
// Прочитать незашифрованный файл в fileData byte[] fileData = new byte[inFile.Length];
inFile.Read(fileData, 0, (int)inFile.Length);
// Шаг 4. Создать объект ICryptoTransform
ICryptoTransform encryptor = myAlg CreateEncryptor();
// Шаг 5: Создать объект CryptoStream
CryptoStream encryptStream = new CryptoSt ream(outFile, encryptor, CryptoStreamMode.Write);
// Шаг 6: Записать содержимое в CryptoStream encryptStream.Write(fileData, 0, fileData.Length);
// Закрыть описатели файла
enc ryptSt ream.Close();
inFile. CloseO;
outFile. CloseO;
Поскольку ключ генерируется случайным образом, он будет меняться при каждом запуске приложения. Поскольку ключ не сохраняется, расшифровать файл не удастся. Ключ представляет собой массив байтов и может быть сохранен с помощью объекта BinaryWriter или передан по сети.
Код для расшифровки почти тот же, что и для шифрования, но он читает, а не генерирует ключ, и вызывает методы расшифровки, а не шифрования. Для расшифровки файла внесите в предыдущий пример следующие изменения:
	шаг 3 — добавьте код для чтения ключа и IV, использованных для шифрования;
	шаг 4 — замените метод CreateEncryptor на CreateDecryptor,
638 Безопасность пользователя и данных
Глава 12
	шаг 5 — замените перечислимое CryptoStreamMode. Write на CryptoStreamMode.Read','
	шаг 6 — добавьте код для чтения из объекта CryptoStream.
Асимметричное шифрование и расшифровка данных
Асимметричное шифрование, также известное как шифрование с открытым ключом, позволяет преодолеть важнейший недостаток симметричного шифрования — необходимость наличия общего секретного ключа у шифрующей и расшифровывающей сторон. В асимметричном шифровании используются пары ключей. Один из ключей пары открытый, а второй — закрытый. Открытый ключ можно свободно распространять, поскольку использовать его во зло практически невозможно. Сообщения, зашифрованные открытым ключом, можно расшифровать только закрытым ключом. Это позволяет передавать зашифрованные сообщения, которые может прочитать только владелец закрытого ключа.
Процесс асимметричного шифрования начинается с обмена открытыми ключами. Как правило, ими обмениваются клиент и сервер. Если шифровать данные требуется только одной из взаимодействующих сторон, предоставить открытый ключ должен только получатель данных. После обмена открытыми ключами отправитель шифрует данные открытым ключом получателя. Полученный шифр-текст может быть расшифрована только получателем — владельцем закрытого ключа, соответствующего открытому ключу, которым были зашифрованы данные. На рис. 12-4 показана схема асимметричного шифрования, в которой открытый ключ предоставляет только одна сторона.
Передача открытого ключа
Документ, зашифрованный открытым ключом, может быть расшифрован только закрытым ключом
Рис. 12-4. Асимметричная криптография использует разные ключи для шифрования и расшифровки
Асимметричные алгоритмы медленнее симметричных, но устойчивее к взлому. Асимметричные алгоритмы плохо подходят для шифрования больших объемов информации из-за снижения производительности. Асимметричные алгоритмы часто применяют для
Занятие 3	Шифрование и расшифровка данных ggg
защиты симметричных ключей и IV при передаче. После защищенного обмена этими значениями для пересылки данных используется симметричное шифрование. Так работают протоколы HTTP и SSL при защите шифрованием Web-соединений: асимметричное шифрование используется только в начале сеанса. Эта комбинация асимметричного и симметричного шифрования показана на рис. 12-5.
Другой важной сферой применения асимметричного шифрования является управление ключами. Обычно для управления ключами в организациях реализуют инфраструктуру открытых ключей (public key infrastructure, PKI), например службы сертификации из Windows Server 2003. PKI отвечает за распространение, управление и отзыва сертификатов в организации. Обычно разработчик не отвечает за создание и конфигурирование PKI.
• i
(J Передача асимметричного открытого ключа
Рис. 12-5. Комбинация асимметричного и симметричного шифрования — компромисс между безопасностью и производительностью
Классы асимметричных алгоритмов в .NET Framework
.NET Framework предоставляет два класса для работы с асимметричным шифрованием, производных qt System.Security.Cryptography.AsymmetricAlgorithm. Этот базовый класс поддерживает следующие свойства, некоторые из которых идентичны одноименным свойствам SymmetricAlgorithm’.
	KeyExchangeAlgorithm
Получает имя алгоритма обмена ключей. Как правило, задавать это свойство явно не требуется.
	KeySize
Получает или задает размер секретного ключа симметричного алгоритма (в битах).
Асимметричные ключи намного длиннее симметричных. Например, стандартный м> размер симметричного ключа —182 бит, а реализация алгоритма RSA в .NET
Framework поддерживает ключи длиной от 384 до 16384 бит.
640
Безопасность пользователя и данных
Глава 12
	LegalKeySizes	&
Массив KeySizes, который получает размеры ключей, поддерживаемые симметричным алгоритмом. Каждый элемент массива содержит свойства MinSize и MaxSize, определяющие диапазон допустимых значений ключа (в битах), и свойство SkipSize, определяющее шаг допустимых размеров ключа (в битах).
	SignatureAlgorithm
Получает URL документа XML, описывающего алгоритм расчета сигнатуры. Как правило, это свойство не требуется задавать явно.
В отличие от базового класса Symmetric Algorithm, у AsymmetricAlgorithm нет полезных для разработчиков методов. Функции шифрования встроены в объекты, производные от класса AsymmetricAlgorithm. .NET Framework предоставляет две реализации данного класса:
	RSACryptoServiceProvider
Предназначен для всех видов асимметричного шифрования и расшифровки, RSACryptoServiceProvider является реализацией алгоритма RSA .NET Framework. RSA назван по инициалами создателей — Роналда Райвеста (Ronald Rivest), Ади Шамира (Adi Shamir) и Леонарда Аллмана (Leonard Adleman) — разработавших этот алгоритм в 1977 г. Класс RSACryptoServiceProvider — это управляемая оболочка неуправляемой реализации RSA, доступной через Cryptography API.
	DSACryptoServiceProvider
Предназначен для цифровой подписи сообщений, также является управляемой оболочкой для неуправляемого кода.
В дополнение к AsymmetricAlgorithm класс RSACryptoServiceProvider предоставляет следующие свойства:
	PersistKeylnCsp
Получает или задает значение, определяющее, нужно ли хранить ключ в CSP. Для повторного использования ключа без его экспорта установите это свойство в true.
	UseMachineKeyStore
Получает или задает значение, определяющее, нужно ли поместить ключ в хранилище ключей на компьютере или в профиле пользователя.
Конструкторы, заданные по умолчанию, всегда присваивают параметрам алгоритма значения, обеспечивающие максимально надежную защиту. Кроме того, класс RSACryptoServiceProvider содержит методы для шифрования, расшифровки, импорта и экспорта ключей:
	Decrypt
Расшифровывает данные, зашифрованные по алгоритму RSA.
	Encrypt
Шифрует данные по алгоритму RSA.
	ExportParameters
Экспортирует структуру RSAParameters, определяющую пару ключей. Для экспорта открытого и закрытого ключей данному методу передают true, для экспорта только открытого ключа — false.
	FromXmlString
Импортирует пару ключей из XML-строки.
	Import Parameters
Импортирует пару ключей из заданного объекта RSAParameters.
Занятие 3
Шифрование и расшифровка данных 541
	Sign Data
Вычисляет хэш указанных данных и сохраняет его в массиве байтов.
	SignHash
Вычисляет сигнатуру заданного хэша, шифруя его открытым ключом, и сохраняет сигнатуру в массиве байтов.
	VerifyData
Проверяет заданную сигнатуру, сравнивая ее с рассчитанной сигнатурой данных.
 Verify Hash
Проверяет заданную сигнатуру, сравнивая ее с сигнатурой, рассчитанной по заданному хэшу.
Экспорт и импорт асимметричных ключей
RSA-ключи намного сложнее симметричных шифровальных ключей. Ключи RSA называются параметрами, их представляет структура RSAParameters. В табл. 12-3 перечислены важные члены этой структуры. Данная структура также включает ряд параметров, не описанных в таблице и не требующих прямого доступа: Z)P, PQ, InverseQ, PmQ.
Табл. 12-3. Члены структуры RSAParameters	
Parameter	Описание
D	Открытый ключ
Exponent	e, короткая часть открытого ключа
Modulus	л, длинная часть открытого ключа
Вам почти наверняка придется экспортировать открытый ключ, т. к. без него невозможно обмениваться зашифрованными сообщениями. Для этого в RSAParamaters передайте методу RSACryptoServiceProvider.ExportParameters булев параметр, равный false. При этом метод экспортирует только открытый ключ, а при значении true — и открытый, и закрытый ключи.
ВАЖНО! Экспорт закрытого ключа
Экспортировать закрытый ключ следует, только если его нужно перенести или импортировать в другое место. При этом необходимо позаботиться о сохранении секретности закрытого ключа.
Следующий пример создает объект RSA и экспортирует его автоматически сгенерированный открытый ключ в объект RSAParameters с именем publicKey.
' VB
' Создать экземпляр объекта RSA
Dim myRSA As RSACryptoServiceProvider = New RSACryptoServiceProvider
’ Создать параметры объекта RSAParameters с одним открытым ключом
Dim publicKey As RSAParameters = myRSA.ExportParameters(False)
11 C#
// Создать экземпляр объекта RSA
RSACryptoServiceProvider myRSA = new RSACryptoServiceProviderO;
// Создать параметры объекта RSAParameters с одним открытым ключом RSAParameters publicKey = myRSA.ExportParameters(false);
642
Безопасность пользователя и данных
Глава 12
После создания объекта RSAParameters можно свободно обращаться к любому байту массива параметров, перечисленных в табл. 12-3. Если необходимо сохранить или передать экспортируемый ключ, нужно использовать метод RSACryptoServiceProvider.ToXmlString. Как и Export Parameters, этот метод получает булево значение, определяющее, нужно ли экспортировать закрытый ключ. Но ToXmlString сохраняет данные в формате XML, который легко хранить, передавать и импортировать методом FromXmlString. Следующий пример иллюстрирует сокращенную версию экспорта пары ключей RSA, созданной вызовом RSACryptoServiceProvider.ToXmlString(true):
<RSAKeyValue>
<Modulus>vilaR5C3XtmH5...IGZNTs=</Modulus>
<Exponent>AQAB</Exponent>
<P>699j 5bpT04JlVkj z...66sYYxl G6VQ==</P>
<Q>zmNovTJlGUamU1Vk...EMtEJqhZgzhTw==</Q>
<DP>0WBf5p7qB6JzB7xek...tkQGoiMBK+Q==</DP>
<DQ>NLbZUrGjduA/99K...scf2p0zQTvKw==</DQ>
<InverseQ>BYZ3vVwb/N+...HjPcGz7Yg==</InverseQ>
<D>Jz81qMuPbP4MdEaF/...hYZ5WmrzeRRE=</D>
</RSAKeyValue>
Сохранение пар ключей для повторного использования
Также можно экспортировать ключи в CSP с помощью хранилища ключей CryptoAPI. Для сохранения закрытых ключей включите в код:
1.	Создание объекта CspParameters.
2.	Определение свойства CspParameters.KeyContainerName.
3.	Создание объекта RSACryptoServiceProvider с помощью перегруженного конструктора, поддерживаемого объектом CspParameters.
4.	Установку свойства RSACryptoServiceProvider.PersistKeylnCsp в true.
.NET Framework сама управляет созданием и поиском ключей. При первом определении объекта CspParameters и установке свойства PersistKeylnCsp в true .NET Framework создаст контейнер для ключа и сохранит в нем ключ. При повторном запуске этого приложения .NET Framework обнаружит, что соответствующий контейнер уже существует, и извлечет из него закрытый ключ. Например, при повторном запуске данного консольного приложения будет выводиться один и тот же закрытый ключ:
• VB
Создать объект CspParameters
Dim persistantCsp As CspParameters = New CspParameters persistantCsp.KeyContainerName = "AsymmetricExample"
Создать экземпляр объекта RSA
Dim myRSA As RSACryptoServiceProvider = _
New RSACryptoServiceProvider (persistantCsp)
Указать, что закрытый ключ нужно сохранить в CSP myRSA.PersistKeylnCsp = True
Создать объект RSAParameters с закрытым ключом
Dim privateKey As RSAParameters = myRSA.ExportParameters(True)
Занятие 3
Шифрование и расшифровка данных $43
, Вывести закрытый ключ
For Each thisByte As Byte In privateKey.D
Console.Write(thisByte.ToString("X2") + ’’ ")
Next
// C#
// Создать объект CspParameters
CspParameters persistantCsp = new CspParametersO;
persistantCsp.KeyContainerName = "AsymmetricExample";
// Создать экземпляр объекта RSA
RSACryptoServiceProvider myRSA = new RSACryptoServiceProvider(persistantCsp);
// Указать, что закрытый ключ нужно сохранить в CSP myRSA.PersistKeylnCsp = true;
// Создать объект RSAParameters с закрытым ключом
RSAParameters privateKey = myRSA.ExportParameters(true);
// Вывести закрытый ключ
foreach (byte thisByte in privateKey.D)
Console.Write(thisByte.ToString("X2") + " ”);
Но если изменить значение KeyContainerName и перезапустить приложение, выводится новый закрытый ключ, поскольку .NET Framework не найдет существующий контейнер ключа.
Асимметричное шифрование и расшифровка
Для шифрования и расшифровки по асимметричному алгоритму вызывают методы RSACryptoServiceProvider.Encrypt и RSACryptoServiceProvider.Decrypt, соответственно. У каждого из них два параметра:
	byte[] rgb
Массив байтов, содержащий шифруемое сообщение или шифр-текст.
	boolfOAEP
Булево значение. Если оно равно true, шифрование выполняется с заполнением по алгоритму ОДЕР (поддерживается только Windows ХР и более поздними операционными системами). Если же оно равно false, используется заполнение по алгоритму PKCS#1 vl.5. Для операций шифрования и расшифровки должны быть заданы идентичные типы заполнения.
Наиболее сложным аспектом шифрования является преобразование данных в массив байтов. Для преобразования строк в этот формат предназначены методы System.Text.Encoding.Unicode.GetBytes и System.Text.Encoding.Unicode.GetString. Например, следующее консольное приложение шифрует строку с заполнением PKCS#1 vl.5, после чего сразу дешифрует и выводит ее:
’ VB
Dim messagestring As String = "Hello, World!"
Dim myRsa As RSACryptoServiceProvider = New RSACryptoServiceProvider
644
Безопасность пользователя и данных
Глава 12
Dim messageBytes As ByteO = Encoding.Unicode.GetBytes(messagestring)
Dim encryptedMessage As ByteO = myRsa.Encrypt(messageBytes, False)
Dim decryptedBytes As ByteO = myRsa.Decrypt(encryptedMessage, False)
Console.WriteLine(Encoding.Unicode.GetString(decryptedBytes))
11 Ctt
string messagestring = "Hello, World!";
RSACryptoServiceProvider myRsa = new RSACryptoServiceProvider();
byte[] messageBytes = Encoding.Unicode.GetBytes(messageString);
byte[] encryptedMessage = myRsa.Encrypt(messageBytes, false);
byte[] decryptedBytes = myRsa.Decrypt(encryptedMessage, false);
Console WriteLine(Encoding.Unicode.GetString(decryptedBytes));
Главное, чтобы при расшифровке использовался тот же метод преобразования данных в массив, что применялся при шифровании.
Проверка целостности данных с помощью хэш-функций
Кроме того, важной областью применения криптографии является зашита целостности данных с помощью хэш-функций. Хэш — это контрольная сумма, уникальная для каждого файла или блока данных. С помощью хэша можно проверить, изменялся ли файл с момента его создания.
В отличие от шифра, из хэша нельзя получить исходные данные независимо от их размера. Другими словами, вычисление хэша — необратимая операция. Хэши часто применяются для проверки паролей без хранения самих паролей. После сохранения хэша пароля приложение может проверить введенный пароль, вычислив его хэш и сравнив его с хранимым хэшем. Если пользователь ввел верный пароль, хэши совпадут. При этом атакующий не сможет узнать пароль, даже если узнает его хэш.
Хэш-алгоритмы в .NET Framework
.NET Framework поддерживает шесть хэш-алгоритмов без ключа и два алгоритма с ключом. В табл. 12-4 перечислены хэша-алгоритмы без ключа. Каждый из них является членом класса System.Security.Cryptography и происходит от System.Security.Cryptography.HashAlgorithm.
Табл. 12-4. Алгоритмы хэша без ключа
Абстрактный класс	Класс с реализацией	Описание
MD5	MD5CryptoServiceProvider	Алгоритм MD5. Размер хэша — 128 бит
RIPEMD160	RIPEMD160Managed	Алгоритм MD160. Размер хэша — 160 бит
SHA1	SHA ICryptoServiceProvider	Алгоритм безопасного хэша 1. Размер хэша SHA1 - 160 бит
SHA256	SHA256Managed	Алгоритм SHA 256, размер хэша — 256 бит
SHA384	SHA384Managed	Алгоритм SHA 384. Размер хэша — 384 бит
SHA512	SHA512Managed	Алгоритм SHA 512. Размер хэша — 512 бит
Занятие 3
Шифрование и расшифровка данных 545
ПРИМЕЧАНИЕ .NET 2.0
RIPEMD160 — новинка .NET 2.0, предназначенная для замены MD5.
Следует также позаботиться о предотвращении модификации хэша атакующими. Если им удастся изменить хэш, его использование лишается смысла. Хэш-алгоритмы с ключом защищены от несанкционированной модификации симметричным шифрованием. В табл. 12-5 приводятся оба хэш-алгоритма с ключом, поддерживаемые .NET Framework; соответствующие объекты происходят от System.Security.Cryptography.KeyedHashAlgorithm.
Табл. 12-5. Хэш-алгоритмы с ключом
Класс	Описание
HMACSHA1	Служит для обнаружения несанкционированных модификаций
сообщений при передаче по небезопасному каналу, требует наличия у отправителя и получателя общего секретного ключа. HMACSHA1 поддерживает ключи любого размера и создает хэш длиной 20 байт
MACTripleDES Как и HMACSHA1, MACTripleDES используется для защиты от несанкционированного изменения сообщения, при передаче по небезопасному каналу, требует наличия у отправителя и получателя общего секретного ключа. MACTripleDES поддерживает ключи длиной 8, 16 и 24 байт, создает хэш длиной 8 байтов
Пример из практики. Хэш-значения не всегда уникальны
Тони Нортроп (Топу Northrup)
Много лет назад я разработал БД, которая индексировала тысячи файлов одного из крупных Интернет-сервисов по загрузке файлов. Часто один и тот же файл называется по-разному, поэтому во избежание дублирования файлов мало проверять существование файлов с одинаковыми именами. Сначала я сортировал файлы, чтобы найти дубли по размеру и содержимому. Но данный процесс выполнялся слишком медленно.
Я решил индексировать все файлы с помощью хэша MD5. Это позволит приложению проверить существование файла, просто проверив хэш MD5. Я удивился, когда приложение тут же нашло дубликаты. Оказалось, что хэши уникальных фалов совпадают! Казалось бы, это против законов математически, но хэш MD5 довольно мал — 128 бит, а размер файлов намного больше, так что есть вероятность получить идентичный хэш для множества файлов, и я столкнулся с таким редким событием. Хэша большей длины, например SHA512, позволит значительно снизить вероятность таких событий.
Хэш-алгоритмы без ключа
Чтобы вычислить хэш по алгоритму без ключа:
1.	Создайте объект алгоритма хэша.
2.	Запишите данные для хэширования в массив байтов.
3.	- Вызовите метод HashAlgorithm.ComputeHash.
4.	Прочитайте массив байт Hash Algorithm. Hash, содержащий значение хэша.
646
Безопасность пользователя и данных
Глава 12
Следующее консольное приложение создает хэш файла, заданного в args[O]. и выводит хэш в кодировке Base64:
’ VB
Sub Main(ByVal args As StringO)
' Шаг 1: Создать объект алгоритма хэша
Dim myHash As MD5 = New MD5CryptoServiceProvider
’ Шаг 2: Сохранить данные, подлежащие хэшированию, в массив байтов
Dim file As FileStream = New FileStream (args(O),
FileMode.Open, FileAccess.Read)
Dim reader As BinaryReader = New BinaryReader (file)
' Шаг 3: Вызвать метод HashAlgorithm.ComputeHash
myHash.ComputeHash(reader.ReadBytes(CType(file.Length, Integer)))
Шаг 4: Считать массив байтов HashAlgorithm.Hash
Console.WriteLine(Conve rt.ToBase64St ring(myHash.Hash))
End Sub
// C#
// Шаг 1: Создать объект алгоритма хэша
MD5 myHash = new MD5CryptoServiceProvider();
&
// Шаг 2: Сохранить данные, подлежащие хэшированию, в массив байтов , FileStream file = new FileStream(args[O], FileMode.Open, FileAccess.Read); BinaryReader reader = new BinaryReader(file);
// Шаг 3: Вызвать метод HashAlgorithm.ComputeHash myHash.ComputeHash(reade r.ReadBytes((int)file.Length));
// Шаг 4: Считать массив байтов HashAlgorithm.Hash
Console.WriteLine(Convert.ToBase64String(myHash.Hash));
При каждом запуске данное приложение будет выводить один и тот же хэш, пока файл не изменится. При изменении файла изменится и хэш. Рассмотрим вывод консольного приложения, которое создает текстовый файл, вычисляет хэш и изменяет файл.
C:\>echo Hello, World! > HashThis.txt
C:\>HashExample HashThis.txt
h7GTmgvuZdN0SGR0A6qdBA==
C:\>HashExample HashThis.txt
h7GTmgvuZdN0SGR0A6qdBA==
C:\>echo Hello, again. » HashThis.txt
C:\>HashExample HashThis.txt
F1QQW0eK/Yc2EwNR2BxCuw==
Занятие 3
Шифрование и расшифровка данных g^y
, Поскольку все объекты хэш-алгоритмов без ключа происходят от одного класса, можно изменить алгоритм хэша, изменив объявление соответствующего объекта. Чтобы проверить, не были ли модифицированы данные, пересчитайте хэш по тому же алгоритме и сравните прежний хэш с новым.
Вычисление хэша по алгоритму с ключом
Чтобы вычислить хэш по алгоритму с ключом:
1.	Создайте секретный ключ, который будет общим для сторон, вычисляющих или проверяющих хэш.
2.	С помощью секретного ключа создайте объект алгоритма хэща. Если секретный ключ не задан, он генерируется автоматически.
3.	Сохраните подлежащие хэшированию данные в массив байтов.
4.	Вызовите метод KeyedHashAlgorithm.ComputeHash.
5.	Прочитайте массив байтов KeyedHashAlgorithm.Hash, содержащий значение хэша.
Следующее консольное приложение создает по алгоритму HMACSHA1 хэш файла, заданного в args[l] (с использованием секретного ключа, сгенерированного по паролю, заданного параметром args[O]y.
' VB
Sub Main(ByVal args As StringO)
Шаг 1; Создать секретный ключ
Dim saltValueBytes As ByteO = System.Text.Encoding.ASCII.GetBytes("This is my salt")
Dim key As Rfc2898DeriveBytes = _
New Rfc2898DeriveBytes(args(0), saltValueBytes)
Dim secretKey As ByteO = key.GetBytes(16)
Шаг 2: Создать объект алгоритма хэша
Dim myHash As HMACSHA1 = New HMACSHA1(secretKey)
Шаг 3: Сохранить данные, подлежащие хэшированию, в массив байтдб
Dim file As FileStream =
New FileStream(args(1), FileMode.Open, FileAccess.Read)
Dim reader As BinaryReader = New BinaryReader(file)
Шаг 4: Вызвать метод HashAlgorithm.ComputeHash
myHash.ComputeHash(reade r.ReadBytes(CType(file.Length, Integer)))
Шаг 5: Считать массив 6aftTjd HashAlgorithm.Hash
Console.WriteLine(System.Conve rt.ToBase64St ring(myHash.Hash))
End Sub
// C#
byte[] saltValueBytes = Encoding.ASCII.GetBytes("This is my salt"): Rfc2898DeriveBytes passwordKey =
new Rfc2898DeriveBytes(args[0], saltValueBytes);
byte[] secretKey = passwordKey.GetBytes(16);
648
Безопасность пользователя и данных
Глава 12
// Шаг 2: Создать объект алгоритма хэша
HMACSHA1 myHash = new HMACSHAI(secretKey);
// Шаг 3: Сохранить данные, подлежащие хэшированию, в массив байтов
FileStream file = new FileStream(args[1], FileMode.Open, FileAccess.Read); BinaryReader reader = new BinaryReader(file);
// Шаг 4: Вызвать метод HashAlgorithm.ComputeHash myHash.ComputeHash(reader.ReadBytes((int)file.Length));
// Шаг 5: Считать массив байтов HashAlgorithm.Hash
Console.WriteLine(Convert.ToBase64String(myHash.Hash));
При изменении содержимого файла или пароля хэш также изменяется. Это гарантирует, что отправитель и получатель используют один пароль для генерации хэша, что предотвращает несанкционированное изменение хэша. Рассмотрим вывод консольного приложения, которое создает текстовый файл, вычисляет хэш и изменяет файл. После изменения файла, пароля (и ключа) хэш также изменяется:
C:\>echo Hello, World! > HashThis.txt
С:\>KeyedHashExample SomePassword HashThis txt t04kYA9Z2ki+JbzUqe711E6Ej N4=
C:\>KeyedHashExample SomePassword HashThis.txt t04kYA9Z2ki+JbzUqe711E6Ej N4=
C:\>KeyedHashExample NotSomePassword HashThis.txt TFNPh9TspBob0vixy1yJ0fX/+vo=
C:\>echo Hello, again. » HashThis.txt
C:\>KeyedHashExample SomePassword HashThis.txt
yW6K6G7diJEV3bV2nNttgtcCM0o=
Для предыдущего примера также подойдет HMACSHA1 или MACTripleDES. Но HMACSHA1 поддерживает секретные ключи любой длины, a MACTripleDES — только ключи длиной 8, 16 или 24 байт.
Подписывание файлов
Цифровая подпись — это значение, которое добавляют к электронным данным, связывающее автора данных с принадлежащим ему закрытым ключом. Для создания цифровой подписи применяют алгоритмы с открытым ключом. Цифровые подписи удостоверяют подлинность идентификатора отправителя (естественно, если вы доверяете открытому ключу отправителя) и защищают целостность данных. Подпись может проверить каждый, т.к. открытый ключ отправителя общедоступен и входит в цифровую подпись.
ВАЖНО! Различия между цифровой подписью и шифрованием
Цифровая подпись не шифрует содержимое подписанных файлов.
Занятие 3
Шифрование и расшифровка данных здд
Классы цифровой подписи в .NET Framework
.NET Framework поддерживает два класса для создания и проверки цифровых подписей: DSACryptoServiceProvider и RSACryptoServiceProvider. Эти классы используют разные алгоритмы, но выполняют одинаковые функции и поддерживают по четыре метода:  SignHash
Создает цифровую подпись на основе хэша файла.
	SignData
Сначала создает хэш файла, а затем — цифровую подпись на его основе.
	Verify Hash
Проверяет цифровую подпись по хэшу фала.
	VerifyData
Проверяет цифровую подпись по содержимому файлов.
Цифровые подписи поддерживают отдельные методы для подписи и проверки данных, а хэш-алгоритмы — нет. Дело в том, что хэш-алгоритмы не требуют подобных методов, проще пересчитать хэш и сравнить его с значением, присланным отправителем. Цифровая подпись использует асимметричные алгоритмы. Поэтому получатель не может создать подпись — у него нет закрытого ключа отправителя, но может проверить ее с помощью открытого ключа отправителя. Методы VerifyData и VerifyHash работают с открытым ключом отправителя, a SignData и SignHash — с закрытым.
Создание и проверка цифровой подписи файла
Для создания цифровой подписи выполните следующие действия:
1.	Создайте объект алгоритма цифровой подписи.
2.	Сохраните подлежащие подписанию данные в массив байтов.
3.	Вызовите метод SignData и сохраните подпись.
4.	Экспортируйте открытый ключ.
Для проверки цифровой подписи:
1.	Создайте объект алгоритма цифровой подписи.
2.	Импортируйте подпись и открытый ключ.
3.	Сохраните подлежащие проверке данные в массив байтов.
4.	Вызовите метод VerifyData.
Следующий пример получает в качестве параметра командной строки имя файла и выводит его цифровую подпись файла в кодировке Base64y основанную на динамически сгенерированной паре ключей. Открытый ключ и цифровая подпись сохраняются в переменных. После этого приложение проверяет подпись с открытым ключом путем создания нужных объектов.
’ VB
Sub Main(ByVal args As StringO)
Шаг 1 подписи: Создать объект алгоритма цифровой подписи
Dim signer As DSACryptoServiceProvider = New DSACryptoServiceProvider
ЧГ.
' Шаг 2 подписи: Сохранить данные, подлежащие подписанию, в массив байтов.
Dim file As FileStream =
New FileStream(args(O), FileMode.Open, FileAccess.Read)
Dim reader As BinaryReader = New BinaryReader(file)
650
Безопасность пользователя и данных
Глава 12
Dim data As ByteO = reader.ReadBytes(CType(file.Length, Integer))
Шаг 3 подписи: Вызвать метод SignData и сохранить подпись Dim signature As ByteO = signer.SignData(data)
’ Шаг 4 подписи: Экспортировать открытый ключ Dim publicKey As String = signer.ToXmlString(False)
Console.WriteLine("Signature: ” + Convert.ToBase64String(signature)) reader.Close() file.CloseO
Шаг 1 проверки: Создать объект алгоритма цифровой подписи
Dim verifier As DSACryptoServiceProvider = New DSACryptoServiceProvider
' Шаг 2 проверки : Импортировать подпись и открытый ключ, ve rifieг.F romXmlSt ring(publicKey)
Шаг проверки 3: Сохранить данные, подлежащие проверке, в массиве байтов Dim file2 As FileStream =
New FileStream(args(O), FileMode.Open, FileAccess.Read)
Dim reader2 As BinaryReader = New BinaryReader(file2)
Dim data2 As ByteO = reader2.ReadBytes(CType(file2.Length, Integer))
Шаг верификации 4: Вызвать метод VerifyData
If verifier.VerifyData(data2, signature) Then
Console.WriteLine("Signature verified") Else
Console.WriteLine("Signature NOT verified") End If reader2.Close() file2.Close() End Sub
‘si’s
11C#
// Шаг 1 подписи: Создать объект алгоритма цифровой подписи DSACryptoServiceProvider signer = new DSACryptoServiceProviderO;
11 Шаг 2 подписи: Сохранить данные, подлежащие подписанию, в массив байтов. FileStream file = new FileStream(args[O], FileMode.Open, FileAccess.Read); BinaryReader reader = new BinaryReader(file);
byte[] data = reader.ReadBytes((int)file.Length);
// Шаг 3 подписи: Вызвать метод SignData и сохранить подпись byte[] signature = signer.SignData(data);
// Шаг 4 подписи: Экспортировать открытый ключ string publicKey = signer.ToXmlString(false);
Занятие 3
Шифрование и расшифровка данных 651
Console.WriteLine("Signature: " + Convert.ToBase64String(signature)); reader. CloseO;
file. CloseO;
// Шаг 1 проверки : Создать объект алгоритма цифровой подписи DSACryptoServiceProvider verifier = new DSACryptoServiceProviderO;
Ц Шаг 2 проверки: Импортировать подпись и открытый ключ verifier.FromXmlString(publicKey);
И Шаг 3 проверки: Сохранить данные, подлежащие проверке, в массиве байтов FileStream file2 = new FileStream(args[O], FileMode.Openr FileAccess.Read); BinaryReader reader2 = new BinaryReader(file2);
bytef] data2 = reader2.ReadBytes((int)file2.Length);
// Шаг 4 проверки: Вызвать метод VerifyData
if (verifier.VerifyData(data2, signature))
Console.WriteLine("Signature verified");
else
Console.WriteLine("Signature NOT verified");
reader2. CloseO;
file2. CloseO;
Данный пример использует класс DSACryptoServiceProvider, но для цифровой подписи подходит и RSACryptoServiceProvider. Работать с RSACryptoServiceProvider проще, но методы SignData и VerifyData требуют объект хэш-алгоритма. Следующий пример содержит строки, необходимые для использования в предыдущем примере RSACryptoServiceProvider с хэш-алгоритмом SHAlCryptoServiceProvider.
' VB
' Шаг 1 подписи: Создать объект алгоритма цифровой подписи
Dim signer As RSACryptoServiceProvider = New RSACryptoServiceProvider
' Шаг 3 подписи: Вызвать метод SignData и сохранить подпись
Dim signature As Byte() =
signer.SignData(data, New SHAlCryptoServiceProvider)
' Шаг 1 проверки: Создать объект алгоритма цифровой подписи
Dim verifier As RSACryptoServiceProvider = New RSACryptoServiceProvider
' Шаг 4 проверки: Вызвать метод VerifyData
If verifier.VerifyData(data2, New SHAlCryptoServiceProvider, signature) Then
11 C#
// Шаг 1 подписи: Создать объект алгоритма цифровой подписи RSACryptoServiceProvider signer = new RSACryptoServiceProvider();
// Шаг 3 подписи: Вызвать метод SignData и сохранить подпись
byte[] signature = signer.SignData(data, new SHA1CryptoServiceProvider());
652
Безопасность пользователя и данных
Глава 12
// Шаг 1 проверки: Создать объект алгоритма цифровой подписи RSACryptoServiceProvider verifier = new RSACryptoServiceProviderO;
// Шаг 4 проверки: Вызвать метод VerifyData
if (verifier.VerifyData(data2, new SHA1CryptoServiceProvider(), signature))
Это упрощенный пример создания и проверки цифровой подписи в приложении, вообще же открытый ключ и цифровая подпись обычно передаются по сети. Наиболее удобный способ передачи цифровой подписи — создать двоичный файл с открытым ключом, цифровой подписью и данными, которые подписываются. Впрочем, данные компоненты можно передавать по отдельности и по разным сетям.
Практикум. Шифрование и расшифровкам файлов
Ваша задача — разработать консольное приложение для шифрования и расшифровки файлов с парольной защитой. Консольное приложение должно получать следующие три параметра: имя незашифрованного файла, имя зашифрованного файла и пароль. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение 1. Разработка консольного приложения для шифрования файлов
В данном упражнении вы создадите консольное приложение, шифрующее файлы с паролем.
1.	Создайте консольное приложение на Visual Basic или С#.
2.	Добавьте к проекту ссылки на пространства имен System.Security.Cryptography и System. IO.
3.	Добавьте код для считывания параметров командной строки. В Visual Basic следует заменить объявление метода Main так, чтобы он принимал массив строк, например «Sub Main(ByVal args As String())». Ниже приводится соответствующий пример кода, вам нужно добавить обработку ошибок выводом сообщения при передаче неверных параметров:
’ VB
Dim inFileName As String = args(O)
Dim outFileName As String = args(1)
Dim password As String = args(2)
// C#
string InFileName = args[0];
string outFileName = args[1];
string password = args[2];
4.	Напишите код для создания объекта шифрования, определения ключа и IV на основании введенного пароля. Ниже приводится соответствующий пример кода:
' VB
' Создать ключ из пароля
Dim saltValueBytes As Byte() =
System.Text.Encoding ASCII.GetBytes("This is my salt")
Занятие 3
Шифрование и расшифровка данных ggg
Dim passwordKey As Rfc2898DeriveBytes =
New Rfc2898DeriveBytes(password, saltValueBytes)
' Создать алгоритм и определить ключ и IV
Dim alg As RijndaelManaged = New RijndaelManaged
alg.Key = passwordKey.GetBytes(alg.KeySize / 8)
alg.IV = passwordKey.GetBytes(alg.BlockSize / 8)
// C#
// Создать ключ из пароля
byte[] saltValueBytes = Encoding.ASCII.GetBytes("This is my salt"); Rfc2898DeriveBytes passwordKey =
new Rfc2898DeriveBytes(password, saltValueBytes);
// Создайте алгоритм и определите ключ и IV
RijndaelManaged alg = new RijndaelManagedO;
alg.Key = passwordKey.GetBytes(alg.KeySize/8);
alg.IV = passwordKey.GetBytes(alg.BlockSize/8);
5.	Прочитайте незашифрованный файл в массив байтов, как показано в следующем примере:
' VB
’ Считать незашифрованный файл в fileData
Dim inFile As FileStream = New FileStream(inFileName,
FileMode.Open, FileAccess.Read)
Dim fileData(inFile.Length) As Byte
inFile.Read(fileData, 0, CType(inFile Length, Integer))
П C#
// Считать незашифрованный файл в fileData
FileStream inFile = new FileStream(inFileName,
FileMode.Open, FileAccess.Read);
byte[] fileData = new byte[inFile Length];
inFile.Read(fileData, 0, (int)inFile.Length);
6.	Создайте объект ICryptoTransform для нужного криптографического алгоритма. Затем создайте объект FileStream для записи зашифрованного файла. Создайте объект CryptoStream на основе объектов ICryptoTransform и FileStream, после чего запишите содержимое незашифрованного файла в CryptoStream,
' VB
' Создать объекты ICryptoTransform и CryptoStream
Dim encryptor As ICryptoTransform = alg.CreateEncryptor
Dim outFile As FileStream = New FileStream(outFileName,
FileMode.OpenOrCreate,
FileAccess.Write)
Dim encryptStream As CryptoStream =
New CryptoStream(outFile, encryptor, CryptoStreamMode.Write)
654 Безопасность пользователя и данных	Глава 12
' Записать содержимое в CryptoStream
encryptStream.Write(fileData, 0, fileData.Length)
// C#
11 Создать объекты ICryptoTransform и CryptoStream
ICryptoTransform encryptor = alg.CreateEncryptor();
FileStream outFile =
new FileStream(outFileName, FileMode.OpenOrCreate, FileAccess.Write);
CryptoStream encryptStream = new CryptoStream(outFile, encryptor, C ryptoSt reamMode.Write);
// Записать содержимое в CryptoStream
encryptStream.Write(fileData, 0, fileData.Length);
7.	Наконец, закройте файлы.
’ VB
Закрыть описатели файлов
enc ryptStream.Close()
inFile. CloseO
outFile. CloseO
// C#
// Закройте описатели файлов
enc ryptSt ream.Close();
inFile. CloseO;
outFile. CloseO;
Теперь откройте командную строку и с помощью написанного ранее приложения зашифруйте текстовый и графический файлы. Убедитесь, что размеры зашифрованных и незашифрованных файлов близки (зашифрованный файл немного больше за счет заполнения). Попробуйте открыть зашифрованные файлы и убедитесь, что они нечитаемы.
Упражнение 2. Разработка консольного приложения для расшифровки файлов
В данном упражнении вы создадите консольное приложение для расшифровки файлов, защищенных паролем.
1.	Создайте консольное приложение на Visual Basic или С#.
2.	Добавьте к проекту ссылки на пространства имен System. Security.Cryptography и System.IO.
3.	Как и в упражнении 1, напишите консольное приложение для создания объекта алгоритма шифрования с паролем, считывания зашифрованного и записи расшифрованного файлов. Ниже приводится соответствующий пример кода:
' VB
Sub Main(ByVal args As StringO)
Прочитать параметры командной строки
Dim inFileName As String = args(O)
Занятие 3
Шифрование и расшифровка данных 555
Dim outFileName As String = args(1)
Dim password As String = args(2)
' Создать ключ из пароля
Dim saltValueBytes As Byte() =
System.Text.Encoding.ASCII.GetBytes("This is my salt")
Dim passwordKey As Rfc2898DeriveBytes = New
Rfc2898De riveBytes(password, saltValueBytes)
' Создать алгоритм и определить ключ и IV
Dim alg As RijndaelManaged = New RijndaelManaged alg.Key = passwordKey GetBytes(alg.KeySize / 8) alg.IV = passwordKey.GetBytes(alg.BlockSize I 8)
' Считать зашифрованный файл в fileData
Dim decryptor As ICryptoTransform = alg.CreateDecryptor
Dim inFile As FileStream =
New FileStream(inFileName, FileMode.Open, FileAccess.Read)
Dim decryptStream As CryptoStream =
New CryptoStream(inFile, decryptor, CryptoStreamMode.Read)
Dim fileData(inFile.Length) As Byte
decryptStream.Read(fileData, 0, CType(inFile.Length, Integer))
' Записать содержимое незашифрованного файла
Dim outFile As FileStream =
New FileStream(outFileName, FileMode.OpenOrCreate, FileAccess.Write)
outFile.Write(fileData, 0, fileData.Length)
' Закрыть описатели файлов dec ryptSt ream.Close() inFile. CloseO outFile. CloseO
End Sub
// C#
// Считать параметры командной строки
string inFileName = args[OJ;
string outFileName = args[1];
string password = args[2];
// Создать ключ пароля
byte[] saltValueBytes = Encoding.ASCII.GetBytes( This is my salt ); Rfc2898DeriveBytes passwordKey = new Rfc2898DeriveBytes(password, saltValueBytes);
656 Безопасность пользователя и данных
Глава 12
// Создать алгоритм, определить ключ и IV
RijndaelManaged alg = new RijndaelManagedO;
alg.Key = passwordKey.GetBytes(alg.KeySize /8);
alg.IV = passwordKey.GetBytes(alg.BlockSize /8);
// Прочитать зашифрованный файл в fileData
ICryptoTransform decryptor = alg.CreateDecryptor();
FileStream inFile =
new FileStream(inFileName, FileMode.Open, FileAccess.Read), CryptoStream decryptStream =
new CryptoStream(inFile, decryptor, CryptoStreamMode.Read);
byte[] fileData = new byte[inFile.Length];
decryptStream.Read(fileData, 0, (int)inFile.Length);
// Записать содержимое незашифрованного файла
FileStream outFile = new FileStream(outFileName, FileMode.OpenOrCreate, FileAccess.Write);
outFile.Write(fileData, 0, fileData.Length);
// Закрыть описатели файлов
decryptStream.Close();
inFile. CloseO;
outFile. CloseO;
4.	Добавьте код для чтения параметров командной строки. В Visual Basic следует изменить объявление параметров Main так, чтобы метод принимал массив строк. Ниже приводится соответствующий пример кода; вам нужно добавить обработку ошибок при вводе параметров:
• VB
Dim inFileName As String = args(O)
Dim outFileName As String = args(1)
Dim password As String = args(2)
// C#
string inFileName = args[0];
string outFileName = args[1];
string password = args[2];
Теперь откройте командную строку и с помощью полученного приложения расшифруйте зашифрованные ранее файлы. Попытайтесь открыть эти файлы и убедитесь, что они читаемы. Попробуйте ввести неправильный пароль — приложение генерирует Sy stem. Security.Cryptography.Cryptographic Except ion. Данное исключение нужно перехватить и вывести пользователю понятное сообщение об ошибке при вводе пароля.
Резюме
	Симметричное шифрование — это криптографический метод защиты конфиденциальности данных с использованием единого секретного ключа для шифрования и расшифровки. .NET Framework поддерживает четыре симметричных алгоритма:
Занятие 3
Шифрование и расшифровка данных 557
RijndaelManaged, DES, TripleDES и RC2. Главным недостатком симметричного шифрования является необходимость обмена ключами между получателем и отправителем, а также ограничение на размеры ключа. Ключ можно генерировать из пароля с помощью класса Rfс2898Derive Bytes.
 Асимметричное шифрование — это криптографический метод, использующий шифрование с помощью пары ключей, в которой один ключ предназначен для шифрования, а второй — для расшифровки. .NET Framework поддерживает два класса асимметричных алгоритмов: RSACryptoServiceProvider и DSACryptoServiceProvider (предназначен только для создания цифровых подписей).
 Хэширование — создание на основе файла уникального значения, пригодного для проверки целостности этого файла. При любом изменении файла изменяется и его хэш. Таким образом, хэш удобен для защиты файла от несанкционированного изменения.
 Цифровые подписи позволяют с помощью открытого ключа поверять подпись файла, созданную с закрытым ключом. В среде с PKI цифровые подписи применяют для проверки подлинности создателя файла.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Какой из классов требует наличия единого ключа для шифрования и расшифровки? (Укажите все верные ответы.)
A. RSACryptoServiceProvider.
В.'< RijndaelManaged. <
С. TripleDES.
D. DSACryptoServiceProvider.
E. DES.
F. RC2.
2.	Какие из перечисленных объектов необходимо синхронизировать между шифрующей и расшифровывающей сторонами при симметричном шифровании? (Укажите все верные ответы.)
A. SymmetricAlgorithm.Key.
В. Symmetric Algorithm. Salt.
С. SymmetricAlgorithm.TV.
D. SymmetricAlgorithm.Mode.
3.	Когда требуется экспортировать закрытый ключ?
А. При передаче данных по сети в рамках одного сеанса.
В. Если удаленный компьютер должен переслать зашифрованные данные, которые вы должны расшифровать.
С. При шифровании файла, который нужно прочитать позднее
D. При передаче закрытого файла, который должен расшифровать удаленный компьютер.
658 Безопасность пользователя и данных
Глава 12
4.	Какие из перечисленных объектов содержат хэш-алгоритмы с ключом? (Укажите все верные ответы.)	к-
А. RIPEMD160. В. HMACSHA1.	* V
С. SHA512. D. MACTripleDES. Е. MD5.
Закрепление материала главы
Для закрепления знаний и навыков, полученных при изучении этой главы, выполните следующее:
	изучите резюме главы;
	повторите основные термины, использованные в этой главе;
	выполните задания лабораторной работы по темам этой главы;
	выполните предложенные упражнения;
	пройдите пробный экзамен.
Резюме главы
СИ	л
	Защита, основанная на ролях, предназначена для контроля доступа пользователей к отдельным аспектам приложения. RBS можно реализовать на основе локальной БД пользователей Windows, доменах Active Directory или нестандартной БД пользователей.
	Списки управления избирательным доступом (DACL) определяют, к каким объектам разрешен доступ пользователям, а списки управления доступом системы безопасности (SACL) управляют аудитом обращения к ресурсам. Классы пространства имен System.Security.AccessControl предназначены для просмотра и управления ACL обоих типов.
	Криптографические классы пространства имен System.Security.Cryptography обеспечивают шифрование-расшифровку данных (по симметричному или асимметричному алгоритму), проверку целостности (хэширование) и создание цифровой подписи для данных.
Основные термины
	список управления доступом (ACL);
	AES;
	асимметричное шифрование;
	аутентификация;
	авторизация;
	шифр-текст;
Лабораторная работа 55g
	DES;
	декларативные RBS-требования;
	цифровая подпись;
	список управления избирательным доступом (DACL);
	шифровальный ключ;
	хэш;
	императивные RBS-требования; 4
	унаследованное разрешение;
	вектор инициализации (IV);
	хэш-алгоритмы с ключом;
	MD5;
	политика участника системы безопасности;
	RC2;
	безопасность, основанная на ролях (RBS);
	список управления доступом системы безопасности (SACL);
	SHA1;
	общий секретный ключ;
	симметричное шифрование;
	Triple DES.
Лабораторная работа
Сейчас вы примените на практике знания и навыки, полученные при изучении этой главы. Ответы на вопросы см. в разделе «Ответы» в конце книги.
Упражнение 1. Создание нестандартных методов аутентификации
Вы — разработчик отдела информационных технологий компании Litware, Inc., которая управляет распространением программных продуктов. Начальник поручил вам разработать приложение Windows Forms для бухгалтерии. Приложение должно работать с существующей БД, позволяя работникам бухгалтерии отслеживать счета, выставленные авторизованным пользователями. Вы опросили ведущих специалистов и ознакомились с техническими требованиями.
Результаты опроса
	Руководитель ИТ-отдела
«В бухгалтерии хотят улучшить пакет для бухгалтерского учета, но, по-моему, это слишком дорого. Да мы за половину стоимости напишем нужную программу! Им всего-навсего нужно ввести сведения о платеже при поступлении счета и щелкнуть кнопку для перечисления денег. Естественно, их волнует безопасность, поскольку доверяют не всем сотрудникам. Не знаю, как они хотят контролировать их, но знаю, что учетные записи пользователей и групп для доступа к приложению хранятся в БД, а не в Active Directory».
660 Безопасность пользователя и данных
Глава 12
 Бухгалтер
«Мы не просим ничего особого. У нас три типа служащих: секретари, бухгалтеры и менеджеры. Секретари вводят данные и принимают счета к оплате. Бухгалтеры оплачивают счета, но не более, чем на 1500 долларов. Счета на более крупные суммы оплачивают только менеджеры».
 Администратор базы данных
«Эта бухгалтерская БД — настоящий кошмар, но мы не можем без нее обойтись. Имеется одна большая таблица Users, в которой для каждого пользователя выделена строка, содержащая его имя и пароль, хранящийся открытым текстом} Конечно, это никуда не годится, но зато не нужно возиться с шифрованием. Имеется также таблица Groups, содержащая по строке о членстве в каждой группе. Скажем, у секретаря по имени Джон в таблице Users есть строка с его именем и паролем, а в таблице Groups указано, что он является членом группы Temps. Лори, менеджер ИТ-отдела, входит в группы Accountants и Managers, поэтому для нее есть одна строка в таблице Users и две в Groups*.
Технические требования
Вам следует создать приложение Windows Forms с нестандартной аутентификацией на основе запроса бухгалтерской БД. Предполагается создать два метода: AddBill и PayBill. Права на вызов методов определяются на основе членства в группах.
Вопросы
1.	Какие классы и методы вы примените для реализации нестандартного механизма аутентификации?
2.	Как ограничить доступ к методу AddBill?
3.	Как ограничить доступ к методу РауВНП
Упражнение 2. Криптографическая защита данных
Вы разработчик в Blue Yonder Airlines — национальной авиакомпании, которая последние два года быстро развивалась: число рейсов за этот период увеличилось более чем в четыре раза. В Blue Yonder Airlines возникла нехватка разработчиков. Недавно начальник направил вас на курсы по информационной безопасности и теперь собирается использовать ваши навыки.
Вопросы
1.	Записи о всех рейсах и пассажирах сохраняются в центральной БД. Затем приложение, разработанное сотрудниками компании, передает эти данные в филиалы. Как лучше зашифровать эту информацию?
2.	Приложение сохраняет имена пользователей и пароли в БД. Как защитить пароли?
3.	Иногда мы даем сотрудникам и клиентам бонусы в виде бесплатных авиабилетов и повышения класса. Эти премии распределяются в центральном офисе. Приложение, работающее в офисе филиалов, должно иметь возможность проверки подлинности присланных решений. Как это осуществить?
Рекомендуемые упражнения ggj
Рекомендуемые упражнения
I
Чтобы лучше проработать темы экзамена, рассмотренные в этой главе, выполните следующие упражнения.
Реализация нестандартной аутентификации с помощью классов System.Security.Authentication
Выполните упражнения 1 и 2
	Упражнение 1 Разработайте клиент-серверное приложение для установки связи между хостами через SslStream.
	Упражнение 2 Добавьте в приложение, разработанное при выполнении упражнения 1, обработку ошибок аутентификации с помощью перехвата исключений из пространства имен System. Security.Authentication.
Модификация удостоверений с помощью классов
System. Security. Principal
Выполните как минимум упражнения 1 и 2. Дополнительный опыт реализации нестандартной аутентификации позволит получить упражнение 3. Упражнение 4 подразумевает работу с другими методами аутентификации.
	Упражнение 1 Выведите список всех пользователей, прошедших аутентификацию за последнюю неделю.
	Упражнение 2 Разработайте приложение для реализации нестандартной аутентификации с помощью классов Genericidentity и GenericPrincipal.
	Упражнение 3 Разработайте приложение для реализации нестандартной аутентификации с помощью интерфейсов Ildentity и IPrincipal.
	Упражнение 4 Доработайте разработанное ранее приложение для аутентификации пользователей с помощью БД, поддержкой классов System.Security.Authentication.
Реализация управления доступом с помощью классов
System.Security'AccessControl
Выполните упражнения 1—3.
	Упражнение 1 Разработайте приложение для анализа всех папок жесткого диска и вывода папок, к которым есть доступ у группы Все (Everyone).
	Упражнение 2 Разработайте консольное приложение Copy ACLs, по функциям аналогичное утилите Сору, но копирующее файлы вместе с ACL.
	Упражнение 3 Разработайте приложение для учета успешного добавления параметров в разделы реестра Run и RunOnce, которые находятся в HKEY_LOCAL_ MACHI-NE\Software\Microsoft\Windows\CurrentVersion\ и HKEY_ CURRENT_USER\Soft-ware\Microsoft\Windows\CurrentVersion\.
Шифрование, расшифровка и хэширование с помощью
классов System.Security.Cryptography
Выполните как минимум упражнения 1 и 2. Дополнительный опыт работы с асимметричным шифрованием позволит получить упражнение 3.
662
Безопасность пользователя и данных
Глава 12
	Упражнение 1 Разработайте приложение для подписи файлов и ее верификации.
	Упражнение 2 Напишите приложение для сохранения хэш-значений для всех DLL-и EXE-файлов на компьютере с последующим их анализом и выводом измененных файлов.
	Упражнение 3 Разработайте клиент-серверное приложение для шифрования файлов клиента открытым ключом сервера, передачи их по сети и расшифровки закрытым ключом на сервере.
Пробный экзамен
На прилагаемом компакт-диске содержится пробный экзамен, обеспечивающий широкие возможности по самоподготовке. Например, можно проверить усвоение темам этой главы либо всех тем экзамена 70-536. Можно сымитировать настоящий экзамен либо выбрать режим обучения, когда после каждого вопроса приводятся правильные ответы с пояснениями.
Пробный экзамен
Подробнее о пробном экзамене рассказано во введении.
ГЛАВА 13
Технологии Interop
Занятие 1.
Занятие 2.
Занятие 3.
Применение СОМ-объектов
Предоставление COM-компоненту доступа к .NET-компонентам
Работа с неуправляемым кодом
664
674
681
Название этих технологий происходит от слова взаимодействие (interoperation, отсюда — Interop) под которым здесь понимают любые операции, производимые управляемым кодом над неуправляемым. .NET Framework 2.0 предоставляет широкий спектр функций, но взаимодействие с неуправляемым кодом необходимо, и вот каким причинам.
	Многие компании потратили на разработку и тестирование имеющегося неуправляемого кода значительные средства.
	Не все функции интерфейса прикладного программирования (Application Programming Interface, API) Microsoft Windows имеют соответствующие оболочки в .NET Framework 2.0, поэтому некоторые задачи можно решить только при помощи взаимодействия.
Например, предположим, что .NET-программе требуется управлять приложением Excel из Microsoft Office. Поскольку в .NET не существует библиотеки Excel-функций, придется решать задачу посредством обращения к модели компонентных объектов (Component Object Model, COM). COM и .NET — разные исполняющие среды, поэтому Interop —единственный способ вызова общих неуправляемых библиотек.
Темы экзамена:
	Предоставление COM-компонентам доступа к .NET Framework и .NET Framework — к СОМ (см. пространство имен System.Runtime.InteropServices)'.
□	импорт библиотеки типов в виде сборки;
□	создание в управляемом коде СОМ-типов;
□	компиляция проекта, использующего Interop;
□	развертывание Interop-приложения;
□	выбор типов .NET Framework для реализации Interop;
□	применение атрибутов Interop, например ComVisibleAttribute',
□	упаковка сборки для СОМ;
□	развертывание приложения для доступа к СОМ.
664 Технологии Interop
Глава 13
	.NET-приложения: вызов неуправляемых функций из DLL и управление маршалингом данных (см. пространство имен System. Runtime. Interop Services)'.
□	вызов Platform Invoke (P/invoke);
□	создание класса, содержащего функции DLL;
□	создание прототипов в управляемом коде;
□	вызов DLL-функций;
□	вызов DLL-функции в особых случаях, например при передаче структур и при реализации функций обратного вызова;
□	создание нового класса исключений и привязка его к значению HRESULT;
□	характеристики маршалинга по умолчанию;
□	маршалинг данных с P/invoke;
□	маршалинг данных при помощи COM Interop;
□	классы MarshalAsAttribute и Marshal.
Пример из практики
Уильям Райан (William Ryan)
Сразу после выхода-NET компания, в которой я работал, решила принять ее на вооружение. Однако у нас было более 200 000 строк унаследованного кода. Если бы нам пришлось переписывать и заново тестировать этот код, прежде чем выпустить хоть какое-нибудь .NET- приложение, переход на новую технологию не осуществился бы никогда. Фирмам, вложившим средства в разработку неуправляемого кода, при переходе на .NET не следует руководствоваться принципом «все или ничего», чтобы не потерять деньги и время. Воспользовавшись преимуществами взаимодействия, мы смогли постепенно перевести на .NET существующие приложения почти незаметно для клиентов. Если бы не взаимодействие, то для этой и многих других компаний переход на .NET был бы невозможен.
Прежде всего
Чтобы освоить материал этой главы, необходимо иметь навыки работы с Microsoft Visual Basic или C# и уметь:
	создавать консольные приложения в Visual Studio при помощи Visual Basic или С#;
	добавлять к проекту ссылки на системные библиотеки классов;
	создавать текстовые файлы.
Занятие 1. Применение СОМ-объектов
До появления .NET Framework основной инфраструктурой, используемой Windows-разработчиками для взаимодействия с операционной системой, была инфраструктура СОМ. При работе как с СОМ, так и с «чистокровными» .NET-компонентами, перед использованием внешних библиотек последние нужно импортировать. После этого можно объявлять и создавать новые объекты так же, как любые другие. Для импорта типов можно воспользоваться командной строкой, а можно предоставить решение этой задачи Microsoft Visual Studio 2005.
Занятие 1
Применение СОМ-объектов 665
Изучив материал этого занятия, вы сможете:
J применять инструменты Visual Studio 2005, предназначенные для работы с СОМ-компонентами;
•J использовать COM-компоненты в .NET-приложениях;
J обрабатывать исключения СОМ-компонентов.
Продолжительность занятия — 20 минут.
i
Импорт библиотек типов
Инфраструктура .NET предоставляет всю необходимую поддержку СОМ-взаимодействия, в том числе возможность импорта библиотек типов. Чтобы исполняющая среда .NET смогла «общаться» с COM-компонентом, в качестве прокси используется механизм, известный как вызываемая оболочка исполняющей среды (Runtime Callable Wrapper, RCW). Ha RCW возложена бо?льшая часть нагрузки по обеспечению взаимодействия .NET и СОМ, в том числе маршалинг типов данных, обработка событий и работа с интерфейсами.
В отличие от «чистокровных» .NET-компонентов, COM-компоненты перед использованием требуется зарегистрировать. После того как компоненты зарегистрированы, следует импортировать их при помощи Visual Studio 2005 или программы импорта библиотеки типов Type Library Importer {Tlblmp.exe). При желании вы можете использовать в примерах этой главы любую COM DLL, здесь же предполагается, что в библиотеке Person.dll содержится следующая структура Person'.
' VB 6.0
Private mFirstName As String
Private mLastName As String
Property Get FirstnameO As String mFirstName = Firstname
End Property
Property Let Firstname(Value As String) mFirstName = Value
End Property
Property Get LastnameO As String
’ mLastName = Firstname
End Property
Property Let Lastname(Value As String) mLastName = Value
End Property
Чтобы убедиться, что Person.dll (или другая выбранная COM DLL) зарегистрирована, выполните следующую команду Regsvr32:
1. Откройте новое окно командной строки или Выполнить (Run) командой Пуск | Выполнить (Start | Run).
2. Выполните команду Regsvr32 Person.dll.
666 Технологии Interop
Глава 13
После того как DLL зарегистрирована, можно импортировать ее при помощи одного из следующих инструментов:
	Visual Studio 2005;
	Tlblmp.exe.
Строго говоря, можно воспользоваться классом из пространства имен System.Run-time.InteropServices, но этот подход чересчур трудоемок и чреват ошибками. На этом занятии мы сосредоточимся на двух указанных вариантах.
Импорт типа при помощи Visual Studio 2005
За одним незначительным исключением импорт библиотеки СОМ практически ничем не отличается от импорта любого другого типа. Исключение в том, что большинство DLL, на которые устанавливается ссылка, перечислены на вкладке СОМ диалогового окна Add Reference, как показано на рис. 13-1.
Рис. 13-1. Вкладка СОМ диалогового окна Add Reference
Здесь присутствуют все зарегистрированные COM-компоненты, поэтому для импорта типов выполните следующее:
1.	Создайте пустое решение ComDemos.
2.	Создайте проект Visual Basic 2005 или C# 2005 и назовите его TypeDemoVB или TypeDemoCS сооответственно языку.
3.	Разверните список Project в Solution Explorer и щелкните правой кнопкой узел References.
4.	Выберите Add Reference и перейдите на вкладку СОМ. (В большинстве конфигураций эта вкладка, следующая за вкладкой .NET.)
5.	Найдите компонент, который требуется зарегистрировать, и щелкните ОК.
Теперь компонент зарегистрирован. Если компонент зарегистрирован правильно, он появится на вкладке СОМ.
Применение Tlblmp.exe для импорта типа
Использование Tlblmp.exe сложнее, но не сильно. Чтобы импортировать библиотеку при помощи Tlblmp.exe, выполните следующее:
1.	Откройте командную строку Visual Studio 2005.
Занятие 1
Применение COM-объектов ggy
2.	Перейдите в каталог с DLL, предназначенной для импорта.
3.	Выполните команду tlbimp <имя_й11 >. dll
Эта команда импортирует DLL и создаст одноименную .NET-сборку. Например, Person.dll будет импортирована как Person.dll, MyObject — как MyObject.dll и т.п.
4.	Чтобы имя сборки отличалось от исходного имени DLL, выполните команду tlbimp <имя_(1Ц>. dll /out: <Требуемое_Имя>. dll.
На получившуюся сборку можно ссылаться, как на любую другую .NET-сборку. Важно понимать, что программа Tlblmp.exe создала из COM-библиотеки новую сборку, которая теперь появится только на вкладке .NET диалогового окна Add Reference.
Можно было бы этим ограничиться, но это было бы не совсем честно по отношению к читателю. Некоторые моменты спосоны доставить массу проблем, главным образом из-за «врожденных» различий между Visual Basic 2005 и С#. В отличие от Visual Basic 2005, C# не поддерживает необязательные параметры. И в чем же проблема, спросите вы? COM-компоненты не поддерживают перегруженные методы, поэтому для каждого параметра при вызове метода необходимо передать значение, даже если этот параметр не требуется. К тому же COM-параметры всегда передаются по ссылке, следовательно, передать в них ««//-значение невозможно.
При работе с Visual Basic 2005 это не проблема, так как большинство таких параметров являются необязательными, и их можно просто опустить по мере необходимости. С#, однако, не предоставляет такой возможности, поэтому требуется создать переменные типа object (учтите, что ««//-значение передавать нельзя) и передавать их при вызове. Такой подход неудобен по следующим причинам:
	он требует лишнего кода, который только запутывает логику приложения;
	часто это приводит к тому, что код становится нечитаемым (понятен ли будет список из 15 параметров?).
Специально для устранения этой проблемы в класс Туре было добавлено поле Type.Missing. Рассмотрим следующие примеры:
• VB
Imports Microsoft.Office.Core
Imports Microsoft.Office.Interop.Excel ’ Для этого примера Office должен быть установлен
Dim NewExcelApp As New Microsoft.Office.Interop.Excel.Application 'Это работает прекрасно NewExcelApp Worksheets.Add()
// C#
using Microsoft.Office.Core
using Microsoft.Office.Interop.Excel; // Для этого примера Office должен быть установлен
Application NewExcelApp = new ApplicationO;
// к этот код не компилируется.
NewExcelApp Worksheets.Add();
Вместо создания «фиктивных* переменных можно воспользоваться полем Type.Missing. Достаточно передать его при вызове в С#, и приложение будет работать корректно. В Visual Basic 2005 такой прием можно не использовать, поскольку там под
668 Технологии Interop
Глава 13
держиваются необязательные параметры, но многие считают применение необязательных параметров нежелательным приемом и советуют его избегать.
СОВЕТ Избегать ли применения необязательных параметров
Имеется много причин, по которым следует избегать применения необязательных параметров в Visual Basic, хотя дискуссии по этому поводу ведутся довольно часто. На мой взгляд, основная причина в том, что они поддерживаются не всеми .NET языками. Разработчики, не знающие Visual Basic, при попытке откомпилировать такой код столкнутся с трудностями — им будет сложно понять, почему конструкция с необязательными параметрами работает в одном контексте и не работает в другом. К тому же многие утверждают, что необязательные параметры — пережиток прошлого, и от них следует отказаться хотя бы поэтому.
Ниже приводится пример работы с полем Type.Missing:
’ VB
Module Modulel
Private OptionalParamHandler As Object = Type Missing
Sub Main()
Dim NewExcelApp As New Microsoft.Office.Interop.Excel.Application NewExcelApp.Wo rksheets.Add(OptionalPa ramHandle r,
OptionalParamHandler, OptionalParamHandler,
OptionalPa ramHandle r)
End Sub
End Module
// C#
class Program
{
private static Object OptionalParamHandler = Type.Missing;
static void Main(string[] args) {
Application NewExcelApp = new Application();
NewExcelApp.Worksheets.Add(ref OptionalParamHandler, ref OptionalParamHandler, ref OptionalParamHandler, ref OptionalParamHandler);
}
}
Такой подход решает сразу две задачи: позволяет использовать C# и делает код более понятным (не приходится гадать, зачем были созданы объекты, которые так и не используются).
Инструменты для COM Interop
.NET Framework 2.0 и Visual Studio 2005 предоставляют несколько инструментов для COM Interop. Инструменты .NET и их применение рассматриваются в табл. 13-1.
Занятие 1	Применение COM-объектов 669
Табл. 13-1. Инструменты .NET для COM Interop
Инструмент	Описание	Имя приложения
Type Libraiy Importer	Импортирует COM-компонент в новую .NET-сборку	Tlblmp.exe
Type Library Exporter	Создает библиотеку СОМ-типов, которую можно использовать в .NET-приложении	TlbExp.exe
Registry Editor	Для всех COM-компонентов в реестре Windows должны присутствовать соответствующие записи. Редактор реестра предназначен не только для работы с COM Interop, он позволяет находить существующие элементы реестра и манипулировать ими	Regedit.exe
Intermediate Language Disassembler	Дизассемблер, предназначенный для решения разных задач. Позволяет IL-код	Hdasm.exe
Assembly Registration Tool	Позволяет добавлять и удалять .NET-сборки из системной базы	Regasm.exe
ПРИМЕЧАНИЕ Переносимость кода
Платформы (кроме Windows), которые поддерживает .NET Framework (например, Linux и Macintosh), не имеют реестра. Инструменты для работы с реестром доступны только на платформе Windows.
Использование в коде СОМ-объекгов
После импорта библиотеки типов работа с объектом из такой библиотеки практически не отличается от работы с «родным» объектом .NET. В следующем примере для открытия и чтения PDF-файла будет использован COM-компонент « Adobe Acrobat Reader 7.0 Browser Document». Если у вас не установлен Adobe Acrobat Reader 7.0, вы можете скачать его бесплатно с сайта http://www.adobe.com/products/acrobat/readstep2_allversions.html. Для простоты добавьте компонент Adobe Acrobat 7.0 Browser Document на панель инструментов и перетащите его на форму (подробнее — в практикуме).
’ VB
AxAcroPDFI. LoadFileCSamplePDFDocument. pdf")
AxAcroPDF1.Print()
// C#
axAcroPDFI.LoadFile(@"SamplePDFDocument.pdf');
axAcroPDFI.Print();
Этот код кажется на удивление простым. Я подумал, впервые столкнувшись с такой задачей, что достаточно импортировать тип, и все будет готово. На первый взгляд причины такой простоты не вполне ясны. Но что происходит при импорте DLL? На ее основе создается .NET-сборка. Ваш код «не знает», откуда взялись функции, которые он вызывает, и работает с ними как обычно.
670 Технологии Interop	Глава 13
Обработка исключений в COM Interop
При использовании COM-объектов в среде .NET обработка исключений резко отличается (но в большинстве случаев эти отличия незаметны). В предыдущих версиях .NET Framework на вершине иерархии объектов Exception располагался класс System.Exception. Это означало, что любую ошибку в приложении можно было отловить, организовав перехват объекта System.Exception. Так думают многие, но они ошибаются. System.Exception действительно предоставляет все исключения, совместимые с общеязыковой спецификацией (Common Language Specification, CLS). Но «ошибки» СОМ не совместимы с CLS, и потому перехвачены не будут. Из-за неверных представлений о работе System.Exception написано много ненадежного и небезопасного кода.
В .NET Framework 2.0 пространство имен System.Runtime.CompilerServices содержит класс RuntimeWrappedException. В табл. 13-2 перечислены свойства класса RuntimeWrappedException согласно документации MSDN.
Табл. 13-2. Свойства RuntimeWrappedException
Имя	Описание
Data	Возвращает набор пар «ключ — значение», предоставляющих пользовательскую информацию об исключении
HelpLink	Возвращает или устанавливает ссылку на файл справки по исключению
InnerException	Возвращает экземпляр Exception, вызвавший данное исключение
Message	Возвращает сообщение, описывающее данное исключение
Source	Возвращает или устанавливает имя приложения или объекта — источника данного исключения
StackTrace	Возвращает строковое представление фреймов стеков вызова на момент генерации данного исключения
TargetSite	Возвращает метод, сгенерировавший данное исключение
WrappedException	Возвращает объект, для которого объект RuntimeWrappedException является оболочкой
Все эти свойства (кроме WrappedException) унаследованы от класса System.Exception. Поэтому главное отличие составляет свойство типа Object, имеющееся у объекта WrappedException. Почему это так важно? Когда .NET Framework 2.0 генерирует не совместимое с CLS исключение, CLR создает экземпляр RuntimeWrappedException и заносит в его свойство WrappedException объект исключения. Все это происходит незаметно для разработчика, но важно понимать суть этих процессов. Следующий пример иллюстрирует сказанное.
' VB
Private Sub IllustrateExceptionsO
Try
Код, генерирующий исключение
Catch ex As Exception
’ В предыдущих версиях перехватывались только CLS-совместимые исключения В текущей версии в этом блоке перехватываются как CLS-совместимые, так и несовместимые исключения.
Занятие 1
Применение СОМ-объекгов gy-|
End Try ' Эквивалента для Catch без исключения нет, ' так как исполнение до этого кода не дойдет. End Sub
// C#
private static void IllustrateExceptions() {
try
{ »' // Какой-то код, генерирующий исключение
}
... catch (Exception ex)
// В предыдущих версиях перехватывались только CLS-совместимые исключения Ц В текущей версии в этом блоке перехватываются как CLS-совместимые, // так и несовместимые исключения,
}
, catch
{
// Перехватываются все исключения, как CLS-совместимые, так и несовместимые >
}
Чтобы изменить это, можно воспользоваться атрибутом Runtimecompatibility'.
' VB	1-
Imports System.Runtime.Compilerservices
<Assembly: RuntimeCompatibility(WrapNonExceptionThrows:=False)>
11 C#
using System.Runtime.Compilerservices;
[assembly: RuntimeCompatibility(WrapNonExceptionThrows=false)]
К СВЕДЕНИЮ Управление исключениями
Управление исключениями в COM Interop обсуждается на занятии 3.
Ограничения COM Interop
С рождения в .NET присутствовали ограничения на использование COM Interop, тесно связанные с изменениями (по-моему, в лучшую сторону), отличающими .NET от прежних платформ. Ниже приведен список этих ограничений.
	Статические (общие) члены
COM-объекты существенно отличаются от типов .NET. Отсутствует поддержка статических (общих) членов.
	Параметризованные конструкторы
Типы СОМ не позволяют передавать конструктору параметры. Это сужает возможности управления инициализацией и не дает использовать перегруженные конструкторы.
672 Технологии Interop
Глава 13
	Наследование	С
Одна из самых существенных проблем — в размещении СОМ-объектов в иерархии наследования. Члены, замещающие члены базового класса, не поддерживаются, то есть недоступны.
	Переносимость
Системный реестр есть только в Windows, в других операционных системах его нет. Привязка к реестру ограничивает выбор среды для переноса .NET-приложений.
Практикум. Работа с COM-приложением в .NET
На этом практикуме вы создадите приложение, включающее COM-компонент Adobe Acrobat 7.0 Browser Document (или любой другой выбранный вами компонент). Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
1.	Откройте Visual Studio 2005 и создайте новое приложение Winfdows Forms на C# или Visual Basic 2005.
2.	Назовите проект TypeDemoCS или TypeDemoVB соответственно.
3.	В главном меню выберите Tools\Choose Toolbox Items. Откроется диалоговое окно с двумя вкладками: .NET Framework Components и COM Components. На вкладке COM Components выберите элемент Adobe Acrobat 7.0 Browser Document и щелкните OK.
4.	Выберите Forml.cs или Forml.vb соответственно и перетащите новый компонент Adobe Acrobat 7.0 Browser Document на форму. По умолчанию этот компонент получит имя axAcroPDFl.
5.	Дважды щелкните Forml.cs или Forml.vb, чтобы автоматически зарегистрировать событие Load, и перейти в окно кода. Добавьте в обработчик события Form_Load следующий код:
1 VB
AxAcroPDFl.LoadFile("SamplePDFDocument.pdf")
AxAcroPDFl,Print()
// C#
axAcroPDFl.LoadFile(@”SamplePDFDocnment.pdf');
axAcroPDFl.Print();
6.	Соберите проект, исправьте ошибки. Если все выполнено правильно, откроется окно (рис. 13-2), и вы сможете распечатать документ PDF.
Рис. 13-2. Окно TypeDemo
Занятие 1	Применение СОМ-объектое 573
Резюме
	СОМ-компоненты без труда встраиваются в .NET-приложения, но перед их использованием следует убедиться, что они действительно необходимы.
	Добавлять ссылки на зарегистрированные СОМ-компоненты можно при помощи вкладки СОМ диалогового окна Add Reference.
	Программа Tlblmp.exe позволяет импортировать COM-компонент в командной строке.
	Параметром out утилиты Tlblmp.exe позволяет присвоить импортируемой сборке любое имя.
	Теперь по умолчанию при перехвате объекта исключения System. Exception перехватываются как совместимые, так и не совместимые с CLS исключения.
	Атрибут RuntimeCompatibilty управляет перехватом исключений, не совместимых с CLS.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Как задействовать СОМ-компоненты в .NET-приложениях? (Укажите все верные ответы.)
А.	Добавить ссылку на компонент при помощи Microsoft Visual Studio 2005.
В.	Воспользоваться программой импорта библиотеки типов Tlblmport.exe.
С.	Воспользоваться командой Regsvr32.
D.	Убедиться, что приложение зарегистрировано, при необходимости воспользовавшись командой RegSvr32. Затем либо добавить ссылку при помощи вкладки СОМ диалогового окна Add Reference, либо применить Tbllmp.exe.
2.	Как перехватывать исключения, не совместимые с CLS в .NET Framework 2.0? (Укажите все верные ответы.)
А. Перехватывать экземпляр ApplicationException.
В. Перехватывать экземпляр System.Exception и устанавливать атрибут RuntimeCompatibilty равным false.
С. Просто перехватывать экземпляр объекта System.Exception.
D. Перехватывать экземпляр System.Exception и устанавливать атрибут RuntimeCompatibilty равным true.
3.	Каковы ограничения COM Interop? (Укажите все верные ответы.)
А. Отсутствие поддержки общих членов в Visual Basic и статических в С#.
В. Невозможность использования параметризованных конструкторов.
С. Ограниченная переносимость между ОС.
D. Ограниченные возможности наследования.
4.	Какие инструменты служат для управления COM Interop? (Укажите все верные ответы.)
А. Дизассемблер промежуточного языка Ildasm.exe.
В. .NET Framework 2.0 Configuration.
С. Программа импорта библиотек типов Tlblmp.exe.
D. Редактор реестра (Registry Editor).
674 Технологии Interop
Глава 13
Занятие 2. Предоставление СОМ-компоненту доступа к .NET-компонентам
До сих пор мы обсуждали получение доступа к COM-компонентам из инфраструктуры .NET. Теперь рассмотрим, как решить обратную задачу — предоставить доступ к .NET-компонентам для СОМ-компонентов.
Изучив материал этого занятия, вы сможете:
J создавать .NET-компоненты, которые можно использовать в СОМ;
J скрывать открытые данные СОМ-компонентов;
•S развертывать сборки для СОМ.
Продолжительность занятия — 20 минут.
Создание .NET-компонентов для использования в СОМ
Как COM-компоненты можно использовать в .NET-приложениях, и наоборот — .NET-сборки в приложениях, основанных на СОМ. Когда .NET-компоненты используются кодом СОМ, специальный объект-прокси (вызываемая оболочка COM, COM Callable Wrapper или CCW) организует маршалинг данных между .NET и СОМ.
ПРИМЕЧАНИЕ Работа CCW
Независимо от числа клиентов СОМ, обращающихся к одному и тому же управляемому объекту, исполняющая среда .NET создаст для последнего только одну CCW.
Чтобы компонент можно было использовать в СОМ, требуется выполнить одну дополнительную операцию.
1.	Стандартным образом создайте библиотеку классов .NET.
2.	Щелкните проект правой кнопкой и выберите Properties — откроется окно Project Properties.	v
3.	Щелкните вкладку Build, расположенную в праврй части окна.
4.	Выберите Register For COM Interop в секции Output (рис. 13-3).
5.	Соберите приложение.	ч*
При сборке проекта Visual Studio 2005 сгенерирует всю необходимою информацию о библиотеке типов, а также зарегистрирует все ее объекты как COM-объекты. Ниже приведен пример кода'^ЕТ-компонента для СОМ.
’ VB
Public Class ComVisiblePerson
Private _firstName As String
Private .lastName As String
Public Property FirstNameO As String
Get
Return Me..firstName
End Get
Set(ByVal value As String)
Занятие 2 Предоставление COM-компоненту доступа к .NET-компонентам gyg
ComVislWePerson.vb* Program.es 1 Start Page 7 NetForComDemoCS*	ComWsiWePerson.cs* Oassl.vb Classl.cs
Рис. 13-3. Управление свойствами проекта
Me._firstName = value End Set
End Property
Public Property LastNameO As String Get
Return Me._lastName
End Get
Set(ByVal value As String)
Me _lastName = value
End Set
End Property
End Class
// C#
using System;
using System.Collections.Generic;
using System.Text;
namespace NetForComDemoCS {
class ComVisiblePerson {
private String firstName;
private String lastName;
public String FirstName
676 Технологии Interop
Глава 13
{
get { return firstName; } set { firstName = value; } }
public String LastName {
get { return lastName; } set { lastName = value; } }
}
Если откомпилировать этот код, получится сборка, видимая для СОМ.
Сокрытие открытых классов .NET от СОМ
Среди прочего следует определить, какие члены следует сделать видимыми, а какие — невидимыми (делать все члены невидимыми не имеет смысла), а также понять, на каком уровне следует скрывать или открывать члены класса.
Видимостью по умолчанию для компонентов СОМ управляет атрибут ComVisible, определяемый на уровне сборки {Assembly), и принимающий значения true или false. ’ VB <Assembly: ComVisible(False)> ' По умолчанию видимость отключена
// C#
[assembly: ComVisible(false)]
Теперь для каждого класса и каждого члена, которые требуется сделать видимыми (или невидимыми), достаточно задать соответствубщее значение атрибута ComVisible’.
' VB
Imports System.Runtime.Compilerservices
Imports System.Runtime.InteropServices
<ComVisible(False)>
Public Class ComVisiblePerson
Private .firstName As String
Private .lastName As String
Private .salary As String
<ComVisible(True)>
Public Property FirstNameO As String Get
Return Me..firstName End Get Set(ByVal value As String)
Me. .firstName = value
Занятие 2
Предоставление СОМ*компоненту доступа к .NET-компонентам р -
End Set
End Property
<ComVisible(True)>
Public Property LastNameO As String Get
Return Me._lastName End Get
Set(ByVal value As String) Me._lastName = value End Set
End Property
<ComVisible(False)>
Public Property SalaryO As Int32 Get
Return Me..salary End Get
Set(ByVal value As Int32) Me._salary = value End Set
End Property
End Class
// C#
using System.Runtime.Compilerservices; using System.Runtime.InteropServices;
namespace NetForComDemoCS
[ComVisible(false)] class ComVisiblePerson {
private String firstName;
private String lastName;
private Int32 salary;
[ComVisible(true)] public String FirstName {
get { return firstName; } set { firstName = value: } }
678 Технологии Interop	Глава 13
[ComVisible(true)]	1
public String LastName { get { return lastName; } set { lastName = value; }
}
[ComVisible(false)] public Int32 Salary { get { return salary; } set{salary = value}
}
}
}
Развертывание сборок для COM
При создании сборки, видимой для СОМ, документация MSDN рекомендует придерживаться следующих правил:
	все классы должны использовать конструктор по умолчанию, не принимающий параметров;
	все типы должны быть открытыми;
	все члены должны быть открытыми;
	абстрактные классы не поддерживаются СОМ.
Сборка, удовлетворяющая всем этим критериям, считается готовой к экспорту. Экспортировать ее можно с помощью Visual Studio 2005 либо утилиты командной строки TlbExp.exe. Сначала требуется скомпилировать тип в Visual Studio или командной строке. Ниже приводится пример команды: ’ VB vbc /t:library ComVisiblePerson.vb
// C#
esc /t:library ComVisiblePerson.es
Затем необходимо воспользоваться TlbExp.exe. Запускать ее следует из командной строки Visual Studio 2005: tlbexp ComVisiblePerson.dll /out:ComVisiblePersonlib.tlb
Далее требуется создать сценарий ресурса ComVisiblePerson.res со следующим определением на языке определения интерфейсов (Interface Definition Language, IDL):
IDR_TYPELIB1 typelib "ComVisiblePersonlib.tlb"
Затем можно повторно откомпилировать приложение, добавив новый файл ресурсов: ’ VB
vbc /t:library ComVisiblePerson.vb /win32res:ComVisiblePerson.res
// C#
esc /t:library ComVisiblePerson.es /win32res:ComVisiblePerson.res
Занятие 2 Предоставление COM-компоненту доступа к .NET-компонентам gyg
Практикум. Создание сборки, пригодной для использования кодом СОМ
Сейчас вы создадите несложную .NET-сборку и класс, к которому можно обращаться из COM-компонента. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Дополнительный код на компакт-диске
Там же вы найдете дополнительный код, который здесь не приводится.
1.	Откройте Visual Studio 2005 и создайте приложение на C# или Visual Basic 2005.
2.	Создайте класс ComVlsiblePersonCS.cs или ComVisiblePersonVB.vb, соответственно.
3.	Добавьте следующее определение или определения для того класса или классов, которые планируете разработать:
' VB
Imports System.Runtime Compilerservices
Imports System.Runtime.InteropServices
<ComVisible(False)>
Public Class ComVisiblePerson
Private .firstName As String
Private _lastName As String
Private _salary As String
<ComVisible(True)>
Public Property FirstNameO As String
Get
Return Me._firstName
End Get
Set(ByVal value As String)
Me._firstName = value
End Set
End Property
<ComVisible(True)> _
Public Property LastNameO As String Get
Return Me..lastName
End Get
Set(ByVal value As String)
Me..lastName = value
End Set
End Property
<ComVisible(False)>
Public Property SalaryO As Int32
*	Get
Return Me..salary
680 Технологии Interop
Глава 13
End Get
Set(ByVal value As Int32) Me..salary = value End Set
End Property
End Class
// C#
using System.Runtime.Compilerservices; using System.Runtime.InteropServices;
namespace NetForComDemoCS
{
[ComVisible(false)] class ComVisiblePerson {
private String firstName;
private String lastName;
private Int32 salary;
[ComVisible(true)] public String FirstName {
get { return firstName; } set { firstName = value; }
[ComVisible(true)] public String LastName {
get { return lastName; } set { lastName = value; }
[ComVisible(false)] public Int32 Salary
get { return salary; } set{salary = value}
4.	Щелкните правой кнопкой проект и выберите Properties.
5.	На вкладке Build в области Output выберите Register For COM Interop.
6.	На вкладке Debug выберите Release.
7.	Соберите проект, исправьте ошибки.
К получившейся DLL можно обращаться из СОМ-компонента.
Занятие 3
Работа с неуправляемым кодом 68 *1
Резюме
 Установка флажка Register For COM Interop в окне свойств компиляции приложения автоматизирует операцию предоставления .NET-сборок СОМ-компонентам.
	Предоставлением доступа СОМ-компонентам к .NET-сборкам управляет атрибут ComVisible.
	Атрибут ComVisible можно применить как на уровне сборки, так и на уровне классов и отдельных членов.
	Чем ниже уровень, на котором используется атрибут ComVisible, тем выше его приоритет. Можно, например, пометить класс атрибутом ComVisible со значением false, а отдельный его член — тем же атрибутом, но с значением true. В результате этот член будет видимым для СОМ-компонентов.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Как сделать .NET-сборку видимой для компонента СОМ? (Укажите все верные ответы.) А. Установить флажок Register For СОМ в свойствах компиляции приложения.
В. Установить значение атрибута ComVisible равным true для всех классов, к которым требуется предоставить доступ.
С. Установить атрибут ComVisible равным false для всех членов класса, которые требуется скрыть.
D. Установить атрибут ComVisible равным true для всех членов класса, которые требуется сделать видимыми.
2.	Какой инструмент позволит создать тип, подходящий для использования в СОМ?
A. Tlblmp.exe.
В. TlbExp.exe.
С. Regedit.exe.
D. csc.exe.
3.	Требуется создать тип, доступный СОМ-компонентам. Какое из перечисленных ниже утверждений истинно?
А. Следует использовать стандартный конструктор без параметров.
В. Определение класса должно быть помечено ключевым словом public или private.
С. Члены должны быть помечены ключевым словом public или private.
D. Можно использовать как конкретные, так и абстрактные классы.
Занятие 3. Работа с неуправляемым кодом
Как уже говорилось, в .NET Framework есть оболочки для многих, но пока не для всех функций. Естественно, нет их и для унаследованного кода, созданного собственными разработчиками компаний либо написанного на заказ, та же ситуация возникает и при работе с Windows API. В каждой новой версии .NET Framework все больше функций Windows
682 Технологии Interop
Глава 13
API получают управляемые оболочки, но охватить все функции пока не удается. На этом занятии рассказывается, как работают с кодом, не имеющим .NET-оболочки.
Изучив материал этого занятия, вы сможете:
J применять Platform Invoke (P/Invoke) для доступа к неуправляемому коду;
J инкапсулировать функции DLL;
J преобразовывать типы данных при передаче между управляемым и неуправляемым кодом.
Продолжительность занятия — 20 минут.
Platform Invoke
Механизм Platform Invoke (P/Invoke) часто оказывается незаменимым, когда требуется вызвать неуправляемую функцию Windows API. Если есть неуправляемый код, решающий определенную задачу, но для него нет оболочки b.NET Framework, то использование P/Invoke, возможно, будет единственным эффективным решением.
ПРИМЕЧАНИЕ Когда приходится обращаться к функциям Windows API
1.	Для использования специфичной для Windows функциональности (переключения контекста, файлового ввода-вывода и т. п.).
2.	Дополнительные возможности управления окнами, меню, диалоговыми окнами и значками. Воспользоваться оконными функциями, не поддерживаемыми .NET Framework, например настройкой MessageBox, возможно только через Windows API.
3.	Усложненные графические функции.
За управление P/Invoke отвечает пространство имен System. Runtime. InteropServices, как и за другие аспекты взаимодействия с неуправляемым кодом. Для применения P/Invoke выполните следующее:
1.	Создайте статический/общий внешний метод с именем функции, которую нужно вызвать.
2.	Пометьте его атрибутом Dlllmport с указанием библиотеки, которую нужнр вызвать.
3.	Вызовите созданный метод.
В следующем примере вызывается функция GetWindowText из Windows API. Для этого необходимо корректно получить описатель активного окна операционной системы (оно может принадлежать совсем другому приложению, а не вашему).
' VB
Imports System.Text
Imports System.Runtime.InteropServices
Public Class WindowExample
Private Const BufferSize As Int32 = 256
<DllImport("user32.dH")> _
Private Shared Function GetForegroundWindowO As IntPtr End Function
Занятие 3
Работа с неуправляемым кодом ggg
<DllImport("user32.dir')>
Private Shared Function GetWindowText(ByVal hWnd As IntPtr,
ByVai textvalue As StringBuilder, ByVai counter As Int32) As Int32 End Function
Public Shared Sub GetScreenDemoO
Dim DemoBuilder As New StringBuilder(BufferSize)
Dim DemoHandle = GetForegroundWindowQ
If GetWindowTextCDemoHandle, DemoBuilder, BufferSize) > 0 Then Console .WriteLineC DemoBuilder. ToStringO)
End If
End Sub
End Class
11 C#
using System.Runtime.InteropServices;
namespace OptionalCS
{
class WindowExample
{
private const Int32 BufferSize = 256;
[DlllmportC"user32.dll")]
private static extern IntPtr GetForegroundWindow();
[Dlllmport("user32 dll")]
private static extern Int32 GetWindowText(IntPtr hWnd, StringBuilder textvalue, Int32 counter);
public static void GetScreenDemoO
{
StringBuilder DemoBuilder = new StringBuilder(BufferSize);
IntPtr DemoHandle = GetForegroundWindowO;
if (GetWindowText(DemoHandle, DemoBuilder, BufferSize) > 0) {
Console.WriteLineC DemoBuilder.ToSt ring());
}
}
}
}
СОВЕТ String и StringBuilder в P/lnvoke
При работе с Р/Invoke вместо String применяйте объекты StringBuilder. StringBuilder -обычный ссылочный тип без аномальных особенностей, он лучше подходит для P/Invoke в силу своего устройства.
684 Технологии Interop
Глава 13
Инкапсулирование функций DLL
Поскольку вызовы P/Invoke — менее чем изящный прием, по крайней мере по сравнению со стилем, к которому привыкли .NET-разработчики, имеет смысл создать для них класс-оболочку. В конце концов, ббльшая часть инфраструктуры .NET Framework организована именно так. Ниже перечислены преимущества такого подхода.
1.	Разработчики, которые будут использовать такой класс, не заметят разницы между ним и «нормальным» .NET-кодом, с которым они привыкли работать.
2.	Описанный подход освобождает разработчиков от необходимости запоминать имена и параметры API-функций. Достаточно один раз создать оболочку, и можно вызывать DLL-функции так же, как обычные методы .NET-классов.
3.	При таком подходе меньше вероятность ошибок. Малейшие опечатки приводят к сбоям P/Invoke, и даже самые внимательные программисты, а тем более их коллеги от них не застрахованы.
Предположим, что вы работаете с классом Windows Exam pie, показанным в предыдущем разделе. Его можно использовать как «нормальный» .NET-класс, так как его оболочка фактически готова. Вместо того, чтобы каждый раз заполнять своим кодом метод GetScreenDemo, можно сделать так:
’ VB
WindowExample.GetScreenDemo()
// C#
WindowExample.GetScreenDemo();
Преобразование типов данных
Вполне вероятно, что даже в простейшем .NET-приложении придется преобразовывать данные из одного типа к другому. Самый распространенный тип преобразования — посредством метода ToStringO. Каждый вызов ToStringO преобразует указанный объект в String. В полностью управляемых приложениях для преобразований можно использовать класс TypeConverter. Однако при работе с неуправляемым кодом чаще всего требуется другой подход.
Во-первых, для преобразования типов данных можно воспользоваться атрибутом MarshalAs. Атрибут MarshalAs можно применить к свойству либо к параметру. Как показано ниже, достаточно создать свойство, пометить его атрибутом MarshalAs и указать тип, к которому его следует преобразовать:
' VB
Imports System.Runtime.Compilerservices
Imports System.Runtime.InteropServices
Public Class MarshalAsDemo
<MarshalAs(UnmanagedType.LPStr)>
Public FirstName As String
Public LastName As String
<MarshalAs(UnmanagedType.Bool)>
Public IsCurrentlyWorking As Boolean
End Class
Занятие 3
Работа с неуправляемым кодом ggg
// C#
using System.Runtime.Compilerservices;
using System.Runtime.InteropServices;
namespace NetForComDemoCS
{
class MarshalAsDemo
{
[Ma rshalAs(UnmanagedType.LPSt r)]
public String FirstName;
public String LastName;
[MarshalAs(UnmanagedType.Bool)] public Boolean IsCurrentlyWorking;
}
}
Очень удобно то, что этот атрибут полностью поддерживается технологией Microsoft IntelliSense, поэтому найти подходящий тип данных очень просто.
Этот атрибут применяется как к методам и свойствам, так и к параметрам. Как показано ниже, в предыдущем примере можно заменить свойство LastName на функцию, принимающую Лпл^-параметр lastName’.
' VB
Imports System.Runtime.Compilerservices
Imports System.Runtime.InteropServices
Public Class MarshalAsDemo
<MarshalAs(UnmanagedType.LPStr)>
Public FirstName As String
Public Sub LastName(<MarshalAs(UnmanagedType.LPStr)>
ByVai lastName As String)
End Sub
<MarshalAs(UnmanagedType.Bool)>
Public IsCurrentlyWorking As Boolean
End Class
// C#
using System.Runtime.Compilerservices;
using System.Runtime.InteropServices;
namespace NetForComDemoCS
{
class MarshalAsDemo
686 Технологии Interop
Глава 13
[Ma rshalAs(UnmanagedType.LPSt r)]
public String FirstName;
public String LastName(
[MarshalAs(llnmanagedType. LPStr)] String lastName);
[MarshalAs(UnmanagedType.Bool)] public Boolean IsCurrentlyWorking;
}
Маршалинг структур
Структуры обычно используются в функциях Windows API и методах, работающих через P/Invoke. Чтобы лучше понять, как работает маршалинг неуправляемых структур, имеет смысл кратко обсудить события при работе с управляемыми типами.
Для CLR приоритетом является производительность, это легко показать на примере типов. При создании класса разработчик может объявлять его члены по собственному усмотрению. Обычно члены класса упорядочивают по функциональному и эстетическому принципам. Однако эти принципы субъективны. То, что кажется логичном для одного разработчика, другому покажется полной противоположностью. Microsoft опубликовала стандарты для разработчиков библиотек, и многие компании рекомендуют своим программистам следовать им. Но окончательное решение по приведению кода в соответствие со стандартами часто остается за разработчиком.
К СВЕДЕНИЮ Советы для разработчиков библиотек
Существуют инструменты (например, Microsoft FX Сор, см. http://www.gotdotnet.com/team/ fxcop), которые помогают привести код в соответствие со стандартами
Набор рекомендаций Microsoft для разработчиков библиотек доступен по адресу http:// www.gotdotnet.com/team/libraries.
При создании объекта за размещение его членов в памяти отвечает CLR. Чтобы явно указать CLR, как надо (или не надо) расположить члены типа в памяти, применяют атрибут System. Runtime. Interop Services. Struct Layout Attribute. В табл. 13-3 приведено определение класса StructLayoutAttribute из документации MSDN.
Табл. 13-3. Определение StructLayoutAttribute
Имя	Область действия/bin	Описание
Конструктор StructLayoutAttribute		
StructLayoutAttribute	Открытый конструктор	Перегружен. Инициализирует новый экземпляр класса StructLayoutAttribute
Открытые поля StructLayoutAttribute		
CharSet	Открытое поле	Определяет, следует ли выполнять маршалинг строковых данных класса как данных типа LPWSTR или LPSTR. (По умолчанию — LPSTR)
Pack	Открытое поле	Управляет выравниванием в памяти полей данных класса или структуры
Size	Открытое поле	Указывает абсолютный размер класса или структуры
Занятие 3
Работа с неуправляемым кодом 687
Табл. 13-3. (окончание)
Имя	Область действия/ Гил	Описание
Открытые свойства StructLayoutAttribute		
Typeld	Открытое свойство	При реализации в производном классе возвращает уникальный идентификатор этого атрибута (наследуется от класса Attribute)
Value	Открытое свойство Открытые методы StructLayoutAttribute		Возвращает значение LayoutKind, определяющее способ упорядочения класса или структуры
Equals	Открытый метод	Перегружен (наследуется от класса Attribute)
GetCustomAttribute	Открытый метод	Перегружен. Возвращает пользовательский атрибут заданного типа, примененный к сборке, модулю, члену типа или параметру метода (наследуется от класса Attribute)
GetCustomAttributes	Открытый метод	Перегружен. Возвращает массив пользовательских атрибутов, примененных к сборке, модулю, члену типа или параметру метода (наследуется от класса Attribute)
GetHashCode	Открытый метод	Возвращает хэш текущего экземпляра (наследуется от класса Attribute)
GetType	Открытый метод	Возвращает тип текущего экземпляра (наследуется от класса Object)
IsDefaultAttribute	Открытый метод	При переопределении в производном классе указывает, является ли значение текущего экземпляра значением по умолчанию для производного класса (наследуется от класса Attribute)
IsDefined	Открытый метод	Перегружен. Определяет, применены ли к сборке, модулю, члену типа или параметру метода какие-либо пользовательские атрибуты заданного типа (наследуется от класса Attribute)
Match	Открытый метод	При переопределении в производном классе возвращает значение, указывающее идентичен ли текущий экземпляр заданному (наследуется от класса Attribute)
ReferenceEquals	Открытый метод	Определяет, совпадают ли указанные экземпляры объектов (наследуется от класса Object)
ToString	Открытый метод	Возвращает строку, представляющую текущий объект (наследуется от класса Object)
688 Технологии Interop
Глава 13
Скорее всего, при использовании атрибута StructLayoutAttnbute главную роль играет конструктор, принимающий в качестве параметра одно из следующих значений:
	LayoutKind.Auto
разработчик передает управление порядком членов типа CLR;
	Layout Kind. Sequential
CLR сохраняет порядок, заданный разработчиком;
	Layout Kind. Explicit
CLR использует порядок, явно заданный разработчиком при помощи смещений в памяти.
ПРИМЕЧАНИЕ Следует убедиться, что структуры размещены в правильном порядке
По умолчанию CLR использует LayoutKind.Auto ддя ссылочных типов и LayoutKind.Sequential для типов значения. Поскольку этот раздел посвящен структурам, по умолчанию используется значение LayoutKind.Sequential.
Для примера возьмем структуру OSVersionlnfo, содержащую информацию о версии операционной системы OperatingSystem Version.
Layout.Sequential
В первом варианте используется параметр Layout.Sequential. В этом случае значения структуры должны располагаться в том же порядке, что и в вызываемой библиотеке:
VB
Imports System.Runtime Compilerservices
Imports System.Runtime.InteropServices
<	StructLayout(LayoutKind.Sequential)>
Public Class OSVersionlnfo
Public dwOSVersionlnfoSize As Int32
Public dwMajorVersion As Int32
Public dwMinorVersion As Int32
Public dwBuildNumber As Int32
Public dwPlatformld As Int32
<MarshalAs(l)nmanagedType. ByValTStr, SizeConst:=128)>
Public szCSDVersion As String
End Class
// C#
using System.Runtime.Compilerservices;
using System.Runtime.InteropServices;
namespace NetForComDemoCS
{
[StructLayout(LayoutKind.Sequential)] class OSVersionlnfo
{
public Int32 dwOSVersionlnfoSize;
public Int32 dwMajorVersion;
public Int32 dwMinorVersion;
Занятие 3
Работа с неуправляемым кодом ggg
public Int32 dwBuildNumber;
public Int32 dwPlatformld;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public String szCSDVersion;
}
}
Layout. Explicit
Можно также явно задать порядок значений (вариант с Explicit).
1. Для перечислимого Layout Kind выберите член Explicit.
2. Для каждого поля задайте смещение в байтах.
В остальном этот подход практически не отличается от варианта с Sequential.
• VB
Imports System.Runtime.Compilerservices
Imports System.Runtime.InteropServices
<StructLayout(LayoutKind.Explicit)>
Public Class OSVersionlnfo
<FieldOffset(O)> Public dwOSVersionlnfoSize As Int32
<Field0ffset(4)> Public dwMajorVersion As Int32
<Field0ffset(8)> Public dwMinorVersion As Int32
<Field0ffset(12)> Public dwBuildNumber As Int32
<Field0ffset(16)> Public dwPlatformld As Int32
<MarshalAs(UnmanagedType ByValTStr, SizeConst =128)>
<Field0ffset(20)> Public szCSDVersion As String
End Class
// C#
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace NetForComDemoCS
{
[St r uct Layout(LayoutKind.Explicit)]
class OSVersionlnfo
{
[FieldOffset(O)]
public Int32 dwOSVersionlnfoSize;
[Field0ffset(4)]
public Int32 dwMajorVersion;
[FieldOffset(8)]
public Int32 dwMinorVersion;
[Field0ffset(12)]
public Int32 dwBuildNumber;
[FieldOffset(16)]
public Int32 dwPlatformld;
690 Технологии Interop
Глава 13
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] [Field0ffset(20)]
public String szCSDVersion;
Использование обратных вызовов с неуправляемым кодом
Функции обратного вызова — важный инструмент в арсенале разработчика. Функции обратного вызова широко используются в .NET Framework и в ее библиотеке классов, как и в большинстве серьезных приложений. В неуправляемой среде роль функций обратного вызова не менее важна.
Традиционно функции обратного вызова реализуют при помоши указателей. Подобный подход давал программисту огромные возможности, правда, ценой некоторых сложностей, например отсутствия контроля типов. Для решения этой проблемы в .NET Framework предусмотрены делегаты (объекты Delegate), обеспечивающие контроль типов при работе с функциями обратного вызова.
ПРИМЕЧАНИЕ Контроль типов
Значение термина контроль типов {type safety) зависит от контекста, но обычно под ним понимают проверку соответствия заданных типов. Подробнее — на сайте Wikipedia по адресу http://en.wikipedia.org/wiki/Type_safety.
Правильная работа с объектами Delegate требует следующего:
1. Создайте объект Delegate с той же сигнатурой, что и у метода обратного вызова.
2. Замените Delegate на имя метода и выполните вызов. Ниже приведен пример использования неуправляемой функции обратного вызова:
VB
Imports System.Text
Imports System.Runtime.InteropServices
Imports System.Runtime.Compilerservices
Public Class UnmanagedCallbackDemoVB
Public Delegate Function DemoCallback(ByVal hWnd As IntPtr,
ByVai IParam As Int32) As Boolean
Private Const UserReference As String = "user32.dll"
Private Const BufferSize As Int32 = 100
<DllImport(UserReference)>
Public Shared Function EnumWindows(ByVal callback As DemoCallback,
ByVai param As Int32) As Int32 End Function
<DllImport(UserReference)>
Public Shared Function GetWindowText(ByRef hWnd As IntPtr.
ByRef IpString As StringBuilder, ByVai nMaxCount As Int32) As Int32 End Function
Занятие 3
Работа с неуправляемым кодом	gg -|
Public Shared Function DisplayWindowInfo(ByVal hWnd As IntPtr, _
ByVai IParam As Int32) As Boolean
Dim DemoBuilder As New StringBuilder(BufferSize)
If GetWindowText(hWnd, DemoBuilder, BufferSize) <> 0 Then Console.WriteLine("Demo Output: " + DemoBuilder.ToStringO)
End If
Return True
End Function
Public Shared Sub RunDemoO
EnumWindows(AddressOf DisplayWindowInfo, 0)
Console.WriteLine("Beginning process...")
Console. ReadLineO
End Sub
End Class
// C#
using System;
using System.Runtime.Compilerservices;
using System.Runtime.InteropServices;
namespace NetForComDemoCS {
public class UnmanagedCallbackDemo
{
public delegate Boolean DemoCallback(IntPtr hWnd, Int32 IParam);
private const String UserReference = "user32.dll;
private const Int32 BufferSize = 100;
[DllImport(UserReference)]
public static extern Int32 EnumWindows(DemoCallback callback, Int32 param);
[DllImport(UserReference)]
public static extern Int32 GetWindowText(IntPtr hWnd, StringBuilder
IpString, Int32 nMaxCount);
public static Boolean DisplayWindowInfo(IntPtr hWnd, Int32 IParam) {
StringBuilder DemoBuilder = new StringBuilder(BufferSize);
if (GetWindowText(hWnd, DemoBuilder, BufferSize) != 0) {
Console,WriteLine("Demo Output: "
+ DemoBuilder. ToStringO);
}
692 Технологии Interop
Глава 13
return true, }
public static void RunDemoO
EnumWindows(DisplayWindowInfo, 0);
Console.WriteLine('Beginning process...
Console. ReadLineO;
}
}
}
На рис. 13-4 показан вид окна консоли при выполнении такого приложения.
Рис. 13-4. Вывод неуправляемой функции обратного вызова в окне консоли
Обработка исключений
Исключения неуправляемого кода заметно отличаются от исключений, генерируемых управляемым кодом. В СОМ получить последнюю ошибку можно было при помощи функции GetLastError. Такой подход в управляемых приложениях не работает, поскольку значение, возвращаемое функцией GetLastError, может оказаться неверным. Почему? Дело в том, что GetLastError может получить значение как от объекта .NET Framework, так и от CLR.
Поскольку метод GetLastError использовать нельзя, требуется чем-то его заменить, ведь хуже отсутствия обработки исключений только неправильная обработка исключений. Ниже приводится пример обработки ошибки в управляемом коде:
Занятие 3
Работа с неуправляемым кодом 593
’ VB
Imports System.Runtime.Compilerservices
Imports System.Runtime.InteropServices
Public Class UnmanagedErrorDemo
Private Const KernelReference As String = "kernel32.dll"
Private Const UserReference As String = "user32.dll"
Private Const MessageSize As Int32 = 255
<DHImport(KernelReference)>
Private Shared Function FormatMessage(dwFlags as Int32,
IpSource as Int32, dwMessageld as Int32. dwLanguageld as Int32
IpBuffer ByRef, nSize as Int32, Arguments as Int32 ) As Int32 End Function
<DllImport(UserReference)>
Private Shared Function MessageBox(ByVal hWnd As Int32, ByVai pText As String, ByVai pCaption As String, ByVai uType As Int32) As Int32
End Function
Public Shared Sub ThrowMessageBoxExceptionO
Dim ProblemCauser As IntPtr = CType(-100, IntPtr) MessageBox(ProblemCauser, "This won't work", "Caption - This won't work", 0)
Dim ErrorCode As Int32 = Marshal.GetLastWin32Error()
Console.WriteLineC'Error Code: " & ErrorCode.ToStringO)
Console.WriteLine("Real Error Code: " & _ Get Last E r ro rMessage(E r ro rCode))
End Sub
Public Shared Function GetLastErrorMessage(
ByVai errorValue As Int32) As String
Порядок не играет роли, но его следует соблюдать 'для логической связанности
Dim FORMAT_MESSAGE_ALLOCATE_BUFFER As Int32 = &H100 '0x00000100
Dim FORMAT_MESSAGE_IGNORE_INSERTS As Int32 = &H200 ' 0x00000200
Dim FORMAT_MESSAGE_FROM_SYSTEM As Int32 = &H1000 '	0x00001000
Dim IpMsgBuf As String = String.Empty
Dim dwFlags As Int32 = FORMAT_MESSAGE_ALLOCATE_BUFFER And _
FORMAT_MESSAGE_FROM_SYSTEM And FORMAT_MESSAGE_IGNORE_INSERTS Dim ReturnValue As Int32 = FormatMessage(dwFlags, 0, errorValue,
0, IpMsgBuf, MessageSize, 0) If ReturnValue = 0 Then
Return Nothing
694 Технологии Interop	Глава 13
Else
Return IpMsgBuf End If End Function
11 C#
using System.Runtime.Compilerservices;
using System.Runtime.InteropServices;
namespace NetForComDemoCS {
public class UnmanagedErrorDemo {
private const String KernelReference = "kernel32.dll";
private const String UserReference = "user32 dll";
private const Int32 MessageSize = 255;
[DllImport(KernelReference)]
private static extern Int32 FormatMessage(Int32 dwFlags,
Int32 IpSource, Int32 intdwMessageld, Int32 dwLanguageld, ref String IpBuffer, Int32 nSize, Int32 Arguments);
[Dlllmport(UserReference, SetLastError=true)] private static extern Int32 MessageBox(IntPtr hWnd.
String pText, String pCaption, Int32 uType);
public static void ThrowMessageBoxException() {
IntPtr ProblemCauser = (IntPtr)(-100);
MessageBox(ProblemCauser, "This won't work", "Caption - This won’t work", 0);
Int32 ErrorCode = Marshal.GetLastWin32Error();
Console.WriteLine("Error Code: " + ErrorCode.ToStringO);
Console.WriteLine("Real Error Code: "
+ Get Last E r ro rMessage(E r ro rCode));
}
public static String GetLastErrorMessage(Int32 errorValue)
11 Порядок не играет роли, но его следует соблюдать // для логической связанности
Int32	FORMAT_MESSAGE_ALLOCATE_BUFFER	=	0x00000100;
Int32	FORMAT_MESSAGE_IGNORE_INSERTS	=	0x00000200;
Int32	FORMAT_MESSAGE_FROM_SYSTEM	=	0x00001000;
String IpMsgBuf = String.Empty;
Занятие 3
Работа с неуправляемым кодом 595
Int32 dwFlags = FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT-MESSAGE.FROM.SYSTEM | FORMAT.MESSAGE.IGNORE.INSERTS;
Int32 ReturnValue = FormatMessage(dwFlags, 0, errorValue,
0, ref IpMsgBuf, MessageSize, 0);
if (ReturnValue == 0) { return null; } else{return IpMsgBuf;}
}
}
}
При правильном выполнении код должен вернуть значение, говорящее о том, что вместо указателя было передано просто числовое значение.
Ограничения при работе с неуправляемым кодом
Исходно возможности работы с неуправляемым кодом в .NET были ограничены. В значительной степени эти ограничения были связаны с «врожденными» различиями между .NET и прежними платформами. Ниже приводится список таких ограничений.
	Производительность
Я подхожу к этому пункту с осторожностью. Дело в том, что существует распространенное заблуждение, будто производительность кода, использующего Interop, намного ниже, чем обычного кода .NET. Возможно, это и так, но производительность зависит от конкретных обстоятельств. Код, не управляемый исполняющей средой, обычно работает быстрее, чем эквивалентный управляемый код, в основном из-за издержек, связанных с маршалингом данных между неуправляемым кодом и исполняющей средой .NET 2.0. Важно также помнить, что неуправляемый код часто создает проблемы, такие как утечка памяти.
	Контроль типов
Неуправляемый код не всегда поддерживает контроль типов. Этот дефект может повлечь за собой другие неприятности, включая запутанный и ненадежный код. Улучшенная поддержка контроля типов является одним из ключевых преимуществ Visual Basic 2005 по сравнению с предыдущими версиями. Кроме того, сложно гарантировать правильность определений библиотек типов, так как они основаны на метаданных, и всегда есть вероятность ошибки.
	Безопасность
Модели защиты, реализованной в .NET Framework, раньше не было. Поэтому ранее разработанный код не может воспользоваться преимуществами этой модели защиты. Дополнительные возможности, такие как декларативная защиты, недоступны для неуправляемого кода, поэтому новый .NET-код придется приспосабливать к этой ситуации.
	Управление версиями
Как и защита, механизм управления версиями (с которым, кстати, и в прежних средах разработки было немало хлопот) существенно изменился. Следовательно, одновременное использование нескольких версий одного и того же неуправляемого кода или DLL невозможно.
696 Технологии Interop
Глава 13
Практикум. Вызов Windows-функций из DLL
На этом практикуме вы создадите простой метод, вызывающий Windows-функцию из DLL. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение 1. Вызов функции Windows API
В этом упражнении вы вызовете функции MessageBox.
1.	Откройте Visual Studio 2005 и создайте консольное приложение на C# или Visual Basic 2005.
2.	Откройте Program.cs или Modulel.vb, соответственно.
3.	Добавьте приведенное ниже определение к каждому своему классу:
VB
Public Const UserReference As String = "user32.dll"
<DllImport(UserReference)>
Private Function MessageBox(ByVal hWnd As Int32,
ByVai pText As String, ByVai pCaption As String,
ByVai uType As Int32) As Int32
End Function
C#
public const String UserReference = '‘user32.dll";
ГDlllmpo rt(Use rRefe rence, Set LastEr ro r=true)]
private static extern Int32 MessageBox(IntPtr hWnd, String pText, String pCaption, Int32 uType);
4.	Соберите проект, исправьте ошибки.
Резюме
1.	Инфраструктура .NET Framework предоставляет механизм для вызова функций Windows API и неуправляемого кода — Platform Invoke.
2.	Для работы с P/Invoke необходимо задать атрибут Dlllmport и имя вызываемой DLL.
3.	При работе с P/Invoke следует использовать атрибуты private и static/ shared.
4.	Оставить порядок членов по умолчанию в структуре, заданный разработчиком, для которой выполняется маршалинг, можно при помощи атрибута Layout.Sequential.
5.	Чтоб явно задать порядок членов структуры, можно воспользоваться атрибутом Layout .Explicit.
6.	Исключения, сгенерированные неуправляемым кодом, отличаются от управляемых. Корректно церехватывать исключения можно при помощи функций Windows API.
Закрепление материал#
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ЛРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
Резюме главы
697
1.	Какое из утверждений истинно для P/Invoke?
A. P/Invoke требует задавать атрибут Dlllmport.
В. Вместо P/Invoke, который служит оболочкой для всех необходимых функций библиотеки, можно использовать RCW.
С. Если значение параметра не играет роли, при вызове такой параметр лучше опустить.
D. Вместо String, лучше использовать объекты StringBuilder, поскольку String является ссылочным типом.
2.	Предположим, что для некоторого вызова P/Invoke требуется использовать структуру. Как это сделать? (Укажите все верные ответы.)
А. Определить структуру в самом начале кода .NET.
В. Если требуется указать размер структуры, воспользоваться методом SizeOf класса System.Runtime.Marshal.
С. Если порядок членов в структуре играет роль, можно воспользоваться атрибутом StructLayout.
D. Создать новую структуру, затем при помощи Type Library Exporter создать библиотеку типов. Добавить в сборки ссылку на полученную библиотеку типов.
3.	Для чего нужен атрибут MarshalAs? (Укажите все верные ответы.)
А. Этот атрибут связывает ранее существовавшие типы и типы .NET.
В. Этот атрибут необходим для правильного маршалинга данных при любых вызовах P/Invoke.
С. Благодаря этому атрибуту исполняющая среда игнорирует отсутствующие типы.
D. Этот атрибут позволяет явно указать нужные типы.
Закрепление материала главы
Для закрепления знаний и навыков, полученных при изучении этой главы, выполните следующее:
	изучите резюме главы;
	повторите основные термины, использованные в этой главе;
	выполните задания лабораторной работы по темам этой главы;
	выполните предложенные упражнения;
	пройдите пробный экзамен.
Резюме главы
	Зависимость от реестра Windows не позволяет работать с COM Interop на всех платформах.
	Программа ПЫтр.ехе работает в командной строке и импортирует СОМ-компоненты.
	По умолчанию при работе с объектами System.Exception перехватываются как CLS-совместимые, так и несовместимые исключения.
	Атрибут ComVisible можно применять к сборке, классу или отдельным его членам.
698 Технологии Interop
Глава 13
	Для работы с P/Invoke необходимо задать атрибут Dlllmport и имя вызываемой DLL.
	При работе с P/Invoke следует использовать атрибуты private и static/shared.
Основные термины
	CLS-совместимое исключение;
	модель компонентных объектов (Component Object Model, COM);
	вызываемая оболочка COM (Callable Wrapper, CCW);
	взаимодействие;
	управляемый код;
	маршалинг;
	утечки памяти;
	Platform Invoke;
	вызываемая оболочка исполняющей среды (Runtime Callable Wrapper, RCW);
	Type Library Exporter;
	Type Library Importer;
	контроль типов.
Лабораторная работа. Встраивание унаследованного кода в проект .NET
Сейчас вы примените на практике все, что узнали о работе с объектами пространства имен System.Runtime.InteropServices. Ответы на вопросы см. в разделе «Ответы» в конце книги.
Необходимость переписывать большой объем унаследованного кода при переходе на .NET Framework — распространенная проблема. Некоторая компания пытается перенести в .NET Framework готовое неуправляемое приложение.
Результаты опроса
	Птава ИТ-отдела
«С тех пор как мы перешли на .NET, производительность труда наших разработчиков резко повысилась. Принимая это во внимание, мы скорректировали свои приоритеты таким образом, что решили полностью перенести на .NET два основных продукта — WinformsProduct и WebProduct. Поскольку в настоящее время эти проекты используют общие библиотеки бизнес-уровня и уровня доступа к данным, их перенос послужит хорошей основой для создания новых продуктов. Мы вложили очень большие средства в COM-библиотеки, и пока не перепишем их полностью, будем переносить лишь тот код, который можно перенести оперативно. Поэтому мы переписываем библиотеки бизнес-уровня и уровня доступа к данным по очереди. Кроме того, у нас есть заявки на расширение функциональности существующих приложений, но это — не проблема, поскольку в новых .NET-библиотеках можно использовать существующие СОМ-компоненты».
Рекомендуемые упражнения ggg
	Ведущий аналитик
«Подобно многим компаниям, нам приходится бороться с текущим положением вещей. Обе линейки продуктов хорошо работают и имеют спрос у пользователей. Мы знаем, что технология NET поможет сократить цикл разработки и сделать многое из того, что ранее было слишком сложно, но есть пункт, по которому мы не можем пойти на компромисс. Если в приложении была та или иная функция, она должна в нем остаться. Мои знакомые из другой компании говорили, что, перейдя на Java, они лишились некоторых возможностей, поскольку Java не такой мощный язык, как C++. Подобная ситуация для нас просто немыслима, и если окажется, что .NET не поддерживает какую-либо из нужных нам функций, мы тут же откажемся от перехода на эту технологию.
В настоящее время в наших продуктах есть много функций, которые действительно нравятся пользователям, но я слышал, что они изначально не поддерживаются .NET. Например, наша довольно сложная справочная система тесно связана с функцией MessageBox из Windows API, и новые продукты обязательно должны сопровождаться подобной справочной системой. Другие возможности, такие как автоматическая отправка снимков экрана в службу технической поддержки по электронной почте, также жизненно важны. Благодаря им персонал службы поддержки точно видит, какие операции выполнял пользователь в момент сбоя независимо от того, умеет ли пользователь делать экранные снимки и пересылать их по электронной почте. Эти возможности обязательно должны присутствовать в новых продуктах».
	Директор по ИТ
«Мы уже вложили почти 1 000 000 долларов в свои библиотеки, и хотя я поддерживаю переход на технологию .NET, не считаю надежным безоглядно следовать трендам. Наши компоненты готовы, протестированы и очень хорошо работают, и я настаиваю на максимальной отдаче от вложений. Я понимаю, что проблемы неизбежны при любых инновациях, но пока мы не убедимся, что новые библиотеки полностью готовы, будут использоваться прежние».
Вопросы
1.	Какой подход следует применять по отношению' к неуправляемым функциям Windows API?
2.	Каким критерием следует руководствоваться, устанавливая очередность переноса библиотек на .NET?
3.	Как будет производиться тестирование новых компонентов?
4.	Действительно ли необходимы вызовы Windows API и другого неуправляемого кода?
Рекомендуемые упражнения
Чтобы лучше проработать темы экзамена, рассмотренные в этой главе, выполните следующие упражнения.
Выполните как минимум упражнения 1 и 2. Чтобы глубже понять Interop, также выполните упражнение 3.
 Упражнение 1 Создайте ссылку на библиотеку Excel Object и обратитесь к ней из .NET. Например, создайте в Excel книгу, добавьте новый лист, удалите существующий и переименуйте один из листов.
700 Технологии Interop	Глава 13
	Упражнение 2 При помощи Windows API получите заголовок текущего активного окна на компьютере пользователя.
	Упражнение 3 Воспользуйтесь Type Library Exporter, чтобы создать .NET-компонент, подходящий для обращения из СОМ.
Пробный экзамен
На прилагаемом компакт-диске содержится пробный экзамен, обеспечивающий широкие возможности по самоподготовке. Например, можно проверить усвоение тем этой главы либо всех тем экзамена 70-536. Можно сымитировать настоящий экзамен либо выбрать режим обучения, когда после каждого вопроса приводятся правильные ответы с пояснениями.
Пробный экзамен
Подробнее о пробном экзамене рассказано во введении.
ГЛАВА 14
Отражение
Занятие 1.	Введение в отражение	702
Занятие 2.	Атрибуты сборки	710
Занятие 3.	Отражение и типы	718
Занятие 4.	Динамическая генерация кода	730
Занятие 5.	Генерация кода во время выполнения	737
Одним из главных достоинств .NET Framework вообще и общеязыковой исполняющей среды (CLR) в частности является обилие доступной информации о типах. Система отражения (reflection) позволяет запрашивать информацию о типах и управлять ей. Это позволяет создавать довольно динамичные системы и упрощает создание архитектуры подключаемых модулей.
Темы экзамена:
	Реализация функций отражения в .NET-приложениях (см. пространство имен System.Reflection), создание метаданных, кода на языке MSIL и PE-файла с помощью классов из пространства имен System. Reflection. Emit:
□	класс сборки;
□	атрибуты сборки;
□	информационные классы;
□	классы Binder и BindingFlags',
□	классы MethodBase и MethodBody’,
□	класс Builder.
Пример из практики
Шон Уилдермьюс (Shawn Wildermuth)
Отражение — важный инструмент из арсенала современного разработчика. Несколько лет назад я создал приложение, в котором с помощью отражения определяется, есть ли в системе объекты с написанными нами пользовательскими атрибутами. Эти атрибуты позволяли определить способ сериализации
702 Отражение
Глава 14
объектов для их ввода и вывода из базы данных. Так, по атрибутам можно было узнать, является ли объект атомарным фрагментом данных и требуется ли для каждого элемента такого объекта отдельное поле. Такой подход позволил сохранять данные, основываясь на потребностях разработчика, без изменения схемы базы данных.
Прежде всего
Чтобы освоить материал этой главы, необходимо иметь навыки работы с Microsoft Visual Basic или C# и уметь:
	создавать консольные приложения в Microsoft Visual Studio на языках Visual Basic или С#;
	добавлять к проекту ссылки на системные библиотеки классов;
	создавать текстовые файлы.
Занятие 1. Введение в отражение
Отражение в NET предназначена для запроса сведений о типе и генерации кода «на лету». По существу отражение служит для представления кода в виде объектов. Отражение позволяет запрашивать и генерировать код, от целых сборок и модулей до отдельных выражений.
Изучив материал этого занятия, вы сможете:
J работать с классом Assembly,
J понять связь между сборками и модулями; динамически загружать сборку;
J перечислить модули сборки;
Продолжительность занятия — 15 минут.
Введение в сборки и модули
Код в CLR упаковывается в сборки. Метаданные сборки используются CLR для исполнения кода и содержат информацию о типах классов, структур, делегатов и интерфейсов, содержащихся в сборке. Информация о типе включает описание его методов, свойств, событий, делегатов и перечислимых. Кроме того, метаданные кода включают его размер и локальные переменные.
Обычно сборку отождествляют с файлом, но фактически это логический контейнер для следующих данных, необходимых CLR для исполнения кода:
	метаданные сборки;
	метаданные типа;
	код (на промежуточном языке);
	ресурсы.
Занятие 1
Введение в отражение 7Q3
Метаданные сборки состоят из ее описания, включая имя, версию, строгое имя и информацию о культуре. Метаданные сборки также называют манифестом. Метаданные типа — это информация, описывающая тип, включая его пространство имен и имя типа (класса), члены типа (такие как методы, свойства и конструкторы) и их параметры, если таковые имеются. Код на промежуточном языке (IL-код), составляющий сборку, компилируется в машинные коды при запуске сборки. Наконец, в сборках содержатся ресурсы — это объекты (такие как строки, изображения и другие файлы), которые используются кодом.
Обычно все эти компоненты находятся в одном файле, как показано на рис. 14-1.
Однофайловая сборка
Рис. 14-1. Однофайловая сборка
Помещать сборку в один файл не всегда удобно. Действительно, порой желательно разделить сборку на несколько файлов (например, у ClickOnce-приложений части сборки загружаются по мере необходимости). Метаданные сборки должны храниться в главном файле сборки. В этом файле также можно хранить и метаданные типов, код и ресурсы, обращаясь к другим файлам за дополнительными метаданными типов и кодом, а также хранить некоторые ресурсы вне файла сборки. Данный подход показан на рис. 14-2.
Многофайловая сборка
AnAssembly.dll	Foo.netmodule
Рис. 14-2. Многофайловая сборка
704 Отражение
Глава 14
Модули — это контейнеры типов, расположенные внутри сборки. Модуль может быть контейнером в простой, или, что более вероятно, многофайловой сборке. В общем, несколько модулей в одной сборке применяются в редких случаях, когда нужно добавить код на разных языках в одну сборку или обеспечить поддержку выборочной загрузки модулей. Visual Studio не поддерживает многомодульные сборки, поэтому многомодульные сборки создают в командной строке или другими средствами (например, с помощью утилиты MSBuild).
Анализ сборки
Перед изучением сборки нужно получить экземпляр класса Assembly, который поддерживает несколько статических методов для создания экземпляров данного класса. В табл. 14-1 эти методы представлены с описанием.
Табл. 14-1. Статические методы класса Assembly
Имя	Описание
GetAssembly	Возвращает объект Assembly
GetCallingAssembly	Возвращает Assembly с кодом, вызываемым текущим методом
GetEntryAssembly	Возвращает Assembly с кодом, запустившим текущий процесс
GetExecutingAssembly	Возвращает Assembly с кодом, исполняемым в данный момент
Load	Загружает Assembly в текущий AppDomain
LoadFile	Загружает Assembly из заданного каталога
LoadFrom	Загружает Assembly в текущий AppDomain из заданного каталога
ReflectionOnlyLoad	Загружает Assembly только для анализа, запуск сборки невозможен
ReflectionOnlyLoadFrom	Загружает Assembly из заданного каталога только для анализа
	сборки, ее запуск невозможен
Статические методы, перечисленные в табл. 14-1, возвращают экземпляр Assembly, представляющий сборку. GetAssembly и другие методы Load позволяет загружать Assembly.
Более интересны методы GetCallingAssembly, GetEntryAssembly и GetExecutingAssembly. Данные методы позволяют получить экземпляр класса Assembly, представляющий сборку из текущего стека вызовов. GetEntryAssembly возвращает экземпляр сборки, содержащей метод — точку входа (обычно это исполняемый файл приложения):
' VB
Dim theAssembly As Assembly = Assembly.GetExecutingAssembly
// C#
Assemoly theAssembly = Assembly.GetExecutingAssemblyO;
Определить сборку, содержащую работающую в данный момент код, можно вызовом GetExecutingAssembly. GetCallingAssembly извлекает экземпляр класса Assembly для метода, расположенного уровнем выше в стеке вызовов. Другими словами, он извлекает сборку, содержащую метод, вызвавший код, исполняемый в данный момент.
После получения экземпляра класса Assembly можно запросить свойства самой сборки. Свойства и методы класса Assembly описаны в табл. 14-2 и 14-3, соответственно.
Занятие 1
Введение в отражение 7Q5
Табл. 14-2. Свойства класса Assembly
Имя	Описание
EntryPoint FullName GlobalAssemblyCache Location ReflectionOnly	Получает метод, представляющий код, с которого начинается исполнение сборки Получает полное имя сборки Получает значение, показывающее, была ли сборка загружена из GAC Получает путь к сборке Получает значение, показывающее, была ли сборка загружена только для анализа
Табл. 14-3. Методы класса Assembly	
Имя	Описание
Createlnstance GetCustomAttributes GetExportedTypes GetFile GetFiles GetLoadedModules GetModule GetModules GetName GetSatelliteAssembly GetTypes IsDefined	Создает экземпляр определенного типа, входящего в сборку Возвращает массив атрибутов сборки Возвращает набор открытых, типов доступных извне сборки Возвращает объект FileStream для файла, входящего в ресурсы сборки Возвращает массив объектов FileStream, представляющих все файлы, входящие в ресурсы сборки Возвращает массив загруженных в настоящий момент модулей сборки Возвращает заданный модуль сборки Возвращает все модули сборки Возвращает объект AssemblyName, представляющий полное имя сборки Возвращает сопутствующую сборку для заданной культуры, если таковая существует Возвращает массив всех типов, определенных в модулях сборки Возвращает значение, показывающее определен ли особый атрибут сборки
После получения экземпляра класса Assembly можно получить информацию о самой сборке:
’ VB
Dim a As Assembly = Assembly.GetExecutingAssemblyO
Console.WriteLine("Full Name: {0}", a.FullName)
Console.WriteLine("Location: {0}", a.Location)
Console.WriteLine("Only Reflection?: {0}", a.ReflectionOnly)
706 Отражение
Глава 14
И C#
Assembly а = Assembly.GetExecutingAssemblyO;
Console.WriteLine("Full Name: {0}", a.FullName);
Console.WriteLine("Location: {0}", a.Location);
Console.WriteLine("Only Reflection?: {0}", a.ReflectionOnly);
Класс Assembly также поддерживает загрузку сборки только для получения информации о ней. Смысл ь том, что при загрузке сборки нужно прочитать ее содержимое и загрузить его в AppDomain. Иногда нужно загрузить сборку только для чтения информации о типах, тогда как исполнение кода и создание объектов не требуется. Для повышения эффективности данного процесса у класса Assembly имеются методы ReflectionOnlyLoad и ReflectionOnlyLoadFrom. Как и остальные статические методы, они возвращают экземпляры класса Assembly, но не поддерживают определенные вызовы (например, Createlnstance). Следующий пример показывает загрузку сборки только для отражения:
1 VB
Dim fullName As String = "System.Transactions, Version=2.0.0.0, " + _ "Culture=neutral, PublicKeyToken=b77a5c561934e089"
Dim theAssembly As Assembly = Assembly ReflectionOnlyLoad(fullName)
Console.Write("Location; {0}", theAssembly.Location)
' Генерирует исключение, т. к. сборка загружена только для отражения Dim о As Object  _
theAssembly.Createlnstance("System.Т ransactions. T ransactionScope")
П C#
string fullName = "System.Transactions, Version=2.0,0.0, " +
"Culture=neutral, PublicKeyToken=b77a5c561934e089";
Assembly theAssembly = Assembly.ReflectionOnlyLoad(fullName);
Console.Write("Location {0}", theAssembly.Location),
// Генерирует исключение, т. к. к. сборка загружена только для отражения object о 
theAssembly.Createlnstance("System.Transactions.Transactionscope");
В каждую сборку входит не менее одного модуля — контейнера информации о типах. С помощью класса Assembly можно получить модули сборки, вызвав метод GetModules'.
1 VB
Dim a As Assembly = Assembly.GetExecutingAssemblyO
Dim mods() As [Module] = a. GetModulesQ
For Each m As [Module] In mods
Console.WriteLine("Module Name: {0}", m.Name)
Next
Занятие 1
Введение в отражение 7Q7
// C#
Assembly а = Assembly GetExecutingAssemblyO;
Module[] mods = a.GetModulesO;
foreach (Module m in mods)
{
Console.WriteLineC"Module Name: {0}”, m.Name);
}
Свойства и методы класса Module описаны в табл. 14-4 и 14-5 соответственно.
Табл. 14-4. Свойства Module
Имя	Описание
Assembly	Получает сборку, в которую входит данный модуль
FuUyQualtfiedName	Получает полное имя данного модуля, в том числе путь к нему
Имя	Получает имя модуля (без пути)
Табл. 14-5. Методы Module	
Имя	Описание
FindTypes	Производит поиск модуля, содержащего заданные типы
GetCustomAttributes	Получает атрибуты, связанные с данным модулем
GetField	Возвращает заданное поле модуля
GetFields	Возвращает все поля модуля
GetMethod	Возвращает заданный метод модуля
GetMethods	Возвращает все методы модуля
GetTypes	Возвращает все типы модуля
IsResource	Определяет, являются ли заданные объекты ресурсами модуля
Класс Module можно использовать для извлечения или поиска типов в заданном модуле. Для сборок, изначально написанных на языке с поддержкой модулей (например, Visual Basic), данный класс также поддерживает методы Get Field, Get Fields, GetMethod и GetMethods. К модулям данного типа поля и методы можно подключать непосредственно. Об отражении полей и методов — в занятии 3.
Практикум. Изучение сборки средствами .NET
Сейчас вы разработаете консольное приложение для создания двух экземпляров класса Assembly и вывода некоторых их свойств. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
1.	Создайте консольное приложение Assembly Demo.
2.	Добавьте в главный файл ссылку на пространство имен System.Reflection.
3.	Создайте статический метод Show Assembly, параметром которого является класс Assembly.
4.	В теле нового метода ShowAssembly выведите свойства FullName, GlobalAssemblyCache, Location и ImageRuntimeVersion на консоль.
708 Отражение
Глава 14
5.	Переберите все Modules в Assembly и выведите имя каждого на консоль.
6.	В главном методе проекта загрузите сборку System.dll из каталога инфраструктуры в каталоге Windows с помощью метода Assembly.Load.
7.	Вызовите метод ShowAssembly с помощью нового экземпляра сборки.
8.	Создайте экземпляр класса Assembly путем получения исполняемой в данный момент сборки.
9.	Снова вызовите метод ShowAssembly с экземпляром Assembly исполняемой сборки Полученный код выглядит примерно так:
' VB
Imports System.Reflection
Class Program
Public Shared Sub Main(ByVal args() As String)
Dim path As String =
"C:\WINDOWS\Mic rosoft.NET\F ramewo rk\v2.0.50727\System.dll"
' Загрузить заданную сборку
Dim a As Assembly = Assembly.LoadFile(path)
ShowAssemblylnfo(a)
’ Получить сборку
Dim ourAssembly As Assembly = Assembly.GetExecutingAssembly
ShowAssemblylnfo(ourAssembly)
Console.Read()
End Sub
Shared Sub ShowAssemblylnfofByVal a As Assembly)
Console.WriteLine(a.FullName)
Console.WriteLine("From GAC? {0}”, a.GlobalAssemblyCache)
Console.WriteLine("Path:	{0}",	a.Location)
Console.WriteLine("Version: {0}", a.ImageRuntimeVersion)
*
’ Вывести модули
For Each m As [Module] In a.GetModules
Console.WriteLine(" Mod: {0}", m.Name)
Next
Console.WriteLine()
End Sub
End Class
Занятие 1
Введение в отражение 709
// C#
using System.Reflection;
class Program
{
static void Main(string[] args)
{
string path =
@"C:\WINDOWS\Mi crosoft.NET\Framework\v2.0.50727\System.dll";
// Загрузить заданную сборку
Assembly a = Assembly.LoadFile(path);
ShowAssemblylnfo(a);
// Получить сборку
Assembly ourAssembly = Assembly.GetExecutingAssemblyO, ShowAssemblyInfo(our Assembly);
Console.Read();
}
static void ShowAssemblyInfo(Assembly a)
{
Console.WriteLine(a FullName);
Console.WriteLine("From GAC? {0}", a.GlobalAssemblyCache);
Console.WriteLine("Path:	{0}",	a.Location);
Console.WriteLine("Version: {0}", a.ImageRuntimeVersion);
// Вывести модули
foreach (Module m in a GetModulesQ) {
Console.Writeline(" Mod: {0}", m.Name);
}
Console.WriteLine();
}
10.	Соберите проект, исправьте ошибки. Убедитесь, что консольное приложение выводит обе сборки и их модули.
Резюме
	Сборки — это контейнеры для модулей, а модули — контейнеры для типов.
	Для обращения к сборкам, связанным с текущим запущенным кодом, загрузите соответствующие сборки и перечислите модули сборки с помощью класса Assembly.
710 Отражение	Глава 14
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1. Какой статический метод класса Assembly возвращает экземпляр, представляющий сборкус точкой входа в приложение?
A. Assembly.GetCallingAssembly.
В. Assembly.GetExecutingAssembly.
С. Assembly.GetEntryAssembly.
D. Assembly.Load.
2. Что из сказанного о сборках и модулях верно? (Укажите все верные ответы.)
А. Сборка может содержать другие сборки.
В. Сборка может содержать один или несколько модулей.
С. Внутри сборки могут быть типы, вложенные непосредственно в нее.
D. Модуль может содержать типы, вложенные непосредственно в него.
Занятие 2. Атрибуты сборки
При рассмотрении класса Assembly вы могли заметить, что значительная часть информации о сборке не доступна данному классу. Ее получают из атрибутов сборки. К ней относятся сведения о защите авторских прав, свдения периода выполнения, в том числе о культуре и файлах ключей.
Изучив материал этого занятия, вы сможете:
J работать с атрибутами сборки;
J запрашивать атрибуты сборки.
Продолжительность занятия — 20 минут.
Системные атрибуты
Атрибуты сборок предоставляют дополнительную информацию о них. Существует множество встроенных атрибутов, с которыми разработчик сталкивается каждый день.
Обычно атрибуты сборки добавляются в файл Assemblyinfo {Assemblylnfo.cs в C# и Assemblylnfo.vb в Visual Basic). Их добавляют в файл Assemblyinfo путем объявления с префиксом {assembly: в C# и Assembly: в Visual Basic). Ниже приведен соответствующий пример кода:
1 VB
<Assembly: AssemblyCompany("Proseware, Inc.")>
Занятие 2
Атрибуты сборки 711
// C#
[assembly: AssemblyCompany("Proseware, Inc.")]
Атрибугы представлены классами, имеющими те же имена, но без суффикса Attribute. Например, в предыдущем примере кода классу AssemblyCompanyAitribute соответствует атрибут AssemblyCompany.
В следующих разделах подробно рассматриваются наиболее востребованные атрибуты и работа с ними.
Класс AssemblyAlgorithmldAttribute
Атрибут AssemblyAlgorithmld предназначен для определения хэш-алгоритма для чтения хэш-значений из манифеста сборки:
’ VB
<Assembly: AssemblyAlgorithmId(AssemblyHashAlgorithm. MD5)>
// C#
[assembly: AssemblyAlgorithmId(AssemblyHashAlgorithm.MD5)]
Класс AssemblyCompanyAitribute
Атрибут AssemblyCompany определяет фирму-разработчика сборки. С его помощью компилятор помещает название компании в заголовок DLL:
' VB
<Assembly: AssemblyCompany("Proseware, Inc.")>
// C#
[assembly: AssemblyCompany("Proseware, Inc.")]
Класс AssemblyConfigurationAttribute
Атрибут Assemblyconfiguration задает тип сборки, например «DEBUG» (отладочная версия) или «RELEASE» (окончательный выпуск). В Visual Studio данный тип обычно назначает пользователь, при необходимости его можно переопределить:
’ VB
<Assembly: AssemblyConfiguration("DEBUG’{)>
// C#
[assembly: AssemblyConfiguration("DEBUG")]
Класс AssemblyCopyrightAttribute
Атрибут AssemblyCopyright задает сведения об авторских правах:
' VB
<Assembly: AssemblyCopyright("Copyright © Proseware, Inc. 2006")>
// C#
[assembly: AssemblyCopyright("Copyright © Proseware, Inc. 2006")]
712 Отражение
Глава 14
Класс AssemblyCultureAttribute
Атрибут AssemblyCulture задает культуру сборки. Обычно сборкам назначают нейтральную культуру, но в случае сопутствующих сборок для определения культуры применяется данный атрибут. Ниже приведен соответствующий пример кода:
• VB
<Assembly: AssemblyCulture("de")> ’ Немецкий
// C#
[assembly: AssemblyCultureC’de")] // Немецкий
Класс AssemblyDefaultAliasAttribute
Атрибут AssemblyDefaultAlias упрощает имя сборки, если ее имя слишком витиевато. Например, если имя сборки — Proseware.Framework.Foundation.DataLayer, его следует заменить псевдонимом, например «Data Layer*. Ниже приведен соответствующий пример кода:
' VB
<Assembly: AssemblyDefaultAlias("DataLaye г")>
П C#
[assembly: AssemblyDefaultAlias("DataLayer")]
Класс AssemblyDelaySignAttribute
Атрибут AssemblyDelay Sign определяет, получит ли сборка после компиляции строгое имя (цифровую подпись). При работе с данным атрибутом нужно предоставить атрибут AssemblyKeyFile, определяющий временный ключ:
’ VB
<Assembly: AssemblyDelaySign(Тrue)>
// C#
[assembly: AssemblyDelaySign(true)]
Класс AssemblyDescriptionAttribute
Атрибут Assembly Description позволяет снабдить сборку комментариями:
' VB
<Assembly: AssemblyDescription("This the Data Access Layer")>
П C#
[assembly: AssemblyDescription("This the Data Access Layer")]
Класс AssemblyFileVersionAttribute
Атрибут AssemblyFileVersion предназначен для определения версии файла сборки. Версию сборки можно прочитать из файла версий. Если этот файл недоступен, используется значение версии из атрибута:
•П *
Занятие 2
Атрибуты сборки 713
• VB
<Assembly: AssemblyFileVersion("1.О.О.О”)>
// C#
[assembly: AssemblyFileVersion(”1.0.0.О”)]
Класс AssembfyFlagsAttribute
Атрибут Assembly Flags определяет значения AssemblyNameFlags для сборки. Перечислимое AssemblyNameFlags подробно описано в табл. 14-6.
Табл. 14-6. Перечисление AssemblyNameFlags
Имя	Описание
EnableJITcompileOptimizer	Активизирует оптимизацию JIT-компилятора
EnableJITcompileTracking	Активизирует мониторинг ЛТ-компилятора
None	Отменяет действия флагов
PublicKey	Генерирует новый открытый ключ сборки на основе заданного
	ключа, а не его маркера
Retargetable	Позволяет связать сборку с другим издателем
Для работы с атрибутом AssemblyFlags нужно определить одно или несколько значений AssemblyNameFlags'.
' VB
<assembly:
AssemblyFlags(AssemblyNameFlags.EnableJITcompileOptimizer Or _ AssemblyNameFlags.EnabledITcompileTracking)>
// C#
[assembly: AssemblyFlags(AssemblyNameFlags.EnableJITcompileOptimizer | AssemblyNameFlags.EnableJITcompileT racking)]
Класс AssemblylnformationalVersionAttribute
Атрибут AssemblylnformationalVersion определяет версию только для ознакомительных целей. Исполняющая среда никогда не использует это значение для управления версиями сборки во время выполнения или при развертывании. Ниже приведен соответствующий пример кода:
' VB
<Assembly: AssemblylnformationalVersion(”1.0.0.1”)>
// C#
[assembly: AssemblyInformationalVersion("1.0.0.1")]
Класс AssembfyKeyFHeAttribute
Атрибут AssemblyKeyFile задает путь к файлу ключа для цифровой подписи (создания строгого имени) для данной сборки. С помощью утилиты sn.exe из .NET Framework SDK
714 Отражение	Глава 14
можно создать файл ключа. Для работы с данным атрибутом нужно определить путь к файлу ключа:
' VB
<Assembly: AssemblyKeyFile("../key.snk")>
// Cit
[assembly: AssemblyKeyFile("../key.snk")]
Класс AssemblyTitleAttribute
Атрибут AssemblyTitle определяет заголовок сборки в манифесте:
’ VB
<Assembly: AssemblyTitle("Proseware.Framework.DataLayer")>
11 C#
[assembly: AssemblyTitle("Proseware.Framework.DataLayer")]
Класс AssemblyTrademarkAttribute
Атрибут AssemblyTrademark задает сведения о торговой марке для данной сборки:
’ VB
<Assembly: Assembly!radema rk(”Р rosewa re®")>
// C#
[assembly: Assembly!radema rk("P rosewa re®")]
Класс AssemblyVerstonAttribute
Атрибут AssemblyVersion задает версию сборки. Версия сборки состоит из четырех элементов, разделенных точками:
<старший номер версии>.<младший номер версии>.<номер сборки>.<ревизия> например,
1.2.3.4
Атрибут AssemblyVersion позволяет заменить номер сборки и ревизии звездочкой. Обнаружив звездочку, компилятор подставит вместо нее автоматически сгенерированные номера сборки и ревизии. Номера обновляются раз в сутки. Автоматически генерируемый номер ревизии представляет собой случайное число. Если задан конкретный номер ревизии, нельзя использовать звездочку вместо номера сборки. Задать AssemblyVersion с помощью атрибута можно так:
' VB
<Assembly: AssemblyVersion("1.2.*.*")>
// C#
[assembly: AssemblyVe rsion("1.2.*.*")]
Занятие 2	Атрибуты сборки 7^5
Получение атрибутов сборки
Определение атрибутов сборки — это только полдела. Также необходимо иметь возможность получения этих атрибутов. Чаше всего для этого вызывают метод GetCustomAttributes класса Assembly. Данный метод является частью интерфейса ICustomAttributeProvider. Интерфейс ICustomAttributeProvider предназначен для поиска типов объектов, поддерживающих нестандартные атрибуты. Это позволяет указать методу GetCustomAttribute с помощью булева значения, нужно ли получать унаследованные атрибуты. Как вы поймете после изучения занятия 3, это довольно полезно. Но поскольку GetCustomAttributes входит в класс Assembly, .NET Framework полностью игнорирует булевы значения (у сборок отсутствует иерархия наследования).	-,	• V
Получить все атрибуты сборки вызовом метода GetCustomAttributes можно так:
' VB
Dim a As Assembly = Assembly.GetExecutingAssemblyO
Dim attrs() As Object = a.GetCustomAttributes(false)
For Each attr As Attribute In attrs
Console.WriteLine("Attribute: {0}”, attr.GetType.Name)
Next
// C#
Assembly a = Assembly.GetExecutingAssemblyO;
object[] attrs = a.GetCustomAttributes(false);
foreach (Attribute attr in attrs) {
Console.WriteLine("Attribute: {0}", attr.GetType().Name);
}
Получить конкретный атрибут можно с помощью метода GetCustomAttributes, указав тип искомого атрибута. В следующем примере показано получение атрибута AssemblyDescription:
1 VB
Dim a As Assembly = Assembly.GetExecutingAssemblyO
Dim attrType As Type = GetType(AssemblyDescriptionAttribute)
Dim versionAttrsO As Object = a.GetCustomAttributes(attrType, false)
If (versionAttrs.Length > 0) Then
Dim desc As AssemblyDescriptionAttribute = _
CType(ve rsionAtt rs(0), AssemblyDescriptionAttribute)
Console.WriteLine("Found Description!")
Console.WriteLine("Desc: {0}", desc.Description)
End If
// C#
Assembly a = Assembly.GetExecutingAssemblyO;
Type attrType = typeof(AssemblyDescriptionAttribute);
716 Отражение
Глава 14
object[] versionAttrs = a.GetCustomAttributesCattrType, false);
if (versionAttrs.Length > 0)
{
AssemblyDescriptionAttribute desc =
(AssemblyDescrlptlonAttribute)versionAttrs[0];
Console.WriteLineC"Found Description!");
Console.WriteLineC"Desc: {0}", desc.Description);
}
Практикум. Определение и вывод атрибутов сборки
во время выполнения
Ваша задача — определить атрибуты сборки и вывести их на консоль. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
1.	Создайте консольное приложение AssemblyAttrDemo.
2.	Откройте файл assemblyinfo {AssemblyInf о.cs в C# и Assembly Info.vb в Visual Basic). В C# он находится в папке Properties. В Visual Basic щелкните кнопку Show All Files и поищите его в папке Му Project.
3.	Присвойте атрибуту AssemblyDescription значение « This is a demo of the AssemblyDescription attribute*.
4.	Откройте главный файл кода проекта и добавьте ссылку на пространство имен System.Reflection.
5.	В методе Main проекта создайте экземпляр класса Assembly и инициализируйте его сборкой кода, исполняемого в данный момент
6.	Создайте локальную переменную для хранения экземпляра класса Туре и занесите в нее тип класса AssemblyDescriptionAttribute.
7.	Вызовите метод GetCustomAttributes экземпляра Assembly для определения экземпляра Туре, будет собран на шаге 6, для наследования укажите false. Сохраните результат в новой локальной переменной, содержащей массив объектов.
8.	Если длина массива больше нуля, создайте переменную для сохранения объекта AssemblyDescriptionAttribute и присвойте первому элементу массива ее значение.
9.	Выведите на консоль содержимое переменной AssemblyDescriptionAttribute. Полученный код выглядит примерно так:
’ VB
Imports System.Reflection
Class Program
Public Shared Sub Main(ByVal args() As String)
Dim a As Assembly = Assembly GetExecutingAssembly
Dim attrType As Type = GetType(AssemblyDescriptionAttribute)
Dim attrs() As Object = a.GetCustomAttributes(attrType, False)
Занятие 2
Атрибуты сборки 717
If (attrs.Length > 0) Then
Dim desc As AssemblyDescriptionAttribute = CType(attrs(O), AssemblyDescriptionAttribute)
Console.WriteLine("Description is: {0}", desc.Description)
End If
End Sub
End Class
// C#
using System.Reflection;
class Program
static void Main(string[] args)
{
Assembly a = Assembly. GetExecutingAssemblyO;
Type attrType = typeof(AssemblyDescriptionAttribute);
object[] attrs = a.GetCustomAttributes(attrType, false);
if (attrs.Length > 0)
AssemblyDescriptionAttribute desc = (AssemblyDescriptionAttribute)attrs[0];
Console.WriteLine("Description is: {0}", desc.Description);
}
}
}
10.	Соберите проект, исправьте ошибки. Убедитесь, что консольное приложение выводит описание в консольном окне.
Резюме
	Системные атрибуты сборки служат для определения ее метаданных.
	Для получения конкретных атрибутов нужно запросить сборку.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
718 Отражение
Глава 14
1. Как связан вызов AssemblyCultureAttribute с точно определенной культурой?
А. Точка входа сборки работает только на компьютере, поддерживающих определенные региональные параметры.
В. Текущая сборка является сопутствующей.
С. Никак, он служит просто для ознакомления.
D. Позволяет преобразовывать ресурсы сборки в локализованные ресурсы.
2. Что указывает компилятору звездочка в поле ревизии номера версии AssemblyVersionAttribute (например, 1.0.0.*)?
А. Ничего.
В. Данная сборка совместима со сборками, версии которых начинаются с 1.0.0.
С. Вместо звездочки нужно подставить автоматический номер.
D. Звездочку нужно заменить случайным числом.
Занятие 3. Отражение и типы
До сих пор мы изучали особенности контейнеров кода, в частности сборок и модулей. Это же занятие посвящено основному вопросу этой темы — отражению членов типа.
Изучив материал этого занятия, вы сможете:
J обращаться к информации о типе без запуска сборки;
J запрашивать типы атрибутов;	 *. ] г
J перечислить члены типа;
J перечислить типы сборки;
J с помощью перечислимого BindingFlags определять члены, извлекаемые из
Туре.
Продолжительность занятия — 25 минут.
Получение типов
Прежде, чем обращаться за информацией о типе нужно научиться получать объекты Туре. Существуют следующие способы получения объектов Туре'.
.	,,	7 о dWr/fjii - Ч 'V	s	- -у &.
	из класса Assembly;
	из класса Module;
	из экземпляров объекта;
	с помощью ключевого слова typeof в C# или GetType в Visual Basic.
При работе с классом Assembly можно запросить все объекты Types из всех модулей сборки, вызвав GetTypes'.
• VB
Dim a As Assembly = Assembly.GetExecutingAssemblyO
' Получить все типы сборки
Dim assemblytypes() As Type = a.GetTypesO
Занятие 3
Отражение и типы 719
// C#
Assembly а = Assembly.GetExecutingAssembly();-
// Получить все типы сборки
Туре[] assemblytypes = a.GetTypes();
При работе с объектом Module также можно та] эосить все связанные с ним Types путем вызова метода GetTypes класса Module’.
' VB
Dim mods() As [Module] = a.GetModulesO
Dim m As [Module] = mods(O)
Получить все типы из модуля
Dim moduleTypesO As Type = m.GetTypesO
// 0#	<:	;
Module[] mods = a.GetModulesO;
Module m = mods[0];
// Получит.1? все типы из модуля
Туре[] moduleTypes = m.GetTypesO;
Тип также можно получить из экземпляра об'г *\та путем вызова его метода GetType:
• VB
Dim о As New ObjectO	4.
’ Получить тип экземпляра
Dim objType As Type = o.GetTypeO
// C# object о = new object();
// Получить тип экземпляра
Type objType = o.GetTypeO;
Наконец, объект Type можно создать с помощью ключевого слова typeof в C# или GetType в Visual Basic GetType’.
’ VB
Dim specif±cType As Type = GetType(Int32)
// C#
Type specificType = typeof(Int32);
Теперь рассмотрим, для чего применяется объект Туре. Класс Туре представляет тип, что позволяет анализировать его методы, свойства, события, интерфейсы и дерево наследования. Перед изучением элементов Туре с помощью отражения рассмотрим сам •класс Туре. Наиболее важные свойства и методы класса Туре представлены в табл. 14-7 и 14-8, соответственно.
720 Отражение
Глава 14
Табл. 14-7. Свойства класса Туре
Имя	Описание
Assembly	Получает Assembly сборки, с которой связан тип
AssembtyQualifiedName	Получает полное имя типа, включая имя сборки
Attributes	Получает атрибуты данного объекта Туре
BaseType	Получает Туре прямого предка данного типа
FullName	Получает полное имя типа без информации о сборке
HasElementType	Позволяет узнать содержит ли данный Туре другой тип (является ли он массивом объектов Туре)
IsAbstract	Позволяет узнать, является ли данный Туре абстрактным классом
IsByRef	Позволяет узнать, передается ли данный Туре по ссылке
IsClass	Позволяет узнать, является ли данный Туре классом
Is Enum	Позволяет узнать, является ли данный Туре перечислимым
IsGenericType	Позволяет узнать, содержит ли данный Туре обобщенные параметры
Islnteiface	Позволяет узнать, является ли данный Туре интерфейсом
IsMarshalByRef	Позволяет узнать, возможен ли маршалинг данного Туре по ссылке
IsNotPublic	Позволяет узнать, является ли данный Туре открытым типом
IsPrimitive	Позволяет узнать, является ли данный тип примитивом (например, Boolean, Byte, SByte, Intl6, UIntl6, Int32, UInt32, Int64, UInt64, Char, Double или Single)
IsPublic	Получает значение, отображающее, является ли данный Туре открытым
IsSealed	Позволяет узнать, является ли данный Туре запечатанным (не запрещено ли наследование от него)
IsValueType	Позволяет узнать, является ли данный Туре типом значения
IsVisible	Позволяет узнать, доступен ли данный Туре извне сборки
Module	Получает Module для данного Туре
Namespace	Получает пространство имен для данного Туре
Табл. 14-8. Методы класса Туре
Имя	Описание
GetConstructor	Извлекает объект Constructor Info,	связанный с данным Type
GetConstructors	Извлекает все связанные с данным Туре объекты Constructorinfo
GetElementType	Извлекает Туре, входящий в данный (обычно допустим, только если данный Туре является массивом)
GetEvent	Извлекает определенный связанный с данным Туре объект Eventinfo
GetEvents	Извлекает все связанные с данным Туре объекты Eventinfo
GetField	Извлекает определенный связанный с данным Туре объект Fieldinfo *
GetFields	Извлекает все связанные с данным Туре объекты Fieldinfo
Занятие 3
Отражение и типы 721
Табл. 14-8. (окончание)
Имя	Описание
Getlnterface	Извлекает определенный связанный с данным Туре объект Interfaceinfo
Getlnterfaces GetMember	Извлекает все связанные с данным Туре объекты Interfaceinfo Извлекает определенный связанный с данным Туре объект MemberItrfo +
GetMembers GetMethod	Извлекает все связанные с данным Туре объекты Memberinfo Извлекает определенный связанный с данным Туре объект Methodinfo
GetMethods GetNestedType GetNestedTypes GetProperty	Извлекает все связанные с данным Туре объекты Methodinfo Извлекает определенный вложенный в данный Туре объект Туре Извлекает рее вложенные в данный Туре объекты Туре Извлекает определенный связанный с данным Туре объект — Propertyinfo
GetProperties IsInstanceOfType IsSubclassOf	Извлекает все связанные с данным Туре объекты Propertyinfo Проверяет, является ли объект экземпляром данного Туре Проверяет, является ли данный Туре производным определенного Туре '*
Получив экземпляр объекта Туре, с его помощью можно получить информацию о типе:
1 VB
Dim t as Type = GetType(String)
Console.WriteLineC"Type: {0}", t.Name)
Console.WriteLine( Console.WriteLineC Console.WriteLineC Console.WriteLineC Console.WriteLineC Console.WriteLine( Console.WriteLine(
Namespace : {0}",
FullName : {0}”,
Is ValueType?: {0}",
Is Sealed? : {0}",
Is Abstract? : {0}",
Is Public? : {0}",
Is Class? : {0}",
t.Namespace) t.FullName) t.IsValueType) t.IsSealed) t.IsAbstract) t.IsPublic) t.IsClass)
// C#
Type t = typeof(String):
Console.WriteLine("Type: {0}", t.Name):
Console.WriteLineC" Console.WriteLineC" Console.WriteLineC" Console.WriteLine(" Console.WriteLineC" Console. WriteLineC Console.WriteLine("
Namespace FullName Is ValueType? Is Sealed? Is Abstract? Is Public? Is Class?
{0}", t.Namespace):
{0}", t.FullName):
{0}”, t.IsValueType):
{0}”, t.IsSealed):
{0}", t.IsAbstract):
{0}", t.IsPublic);
{0}", t.IsClass);
722 Отражение
Глава 14
Кроме того, можно проверить атрибуты класса так же, как атрибуты сборки: >
' VB
For Each attr As Attribute In t.GetCustomAttributes(false)
Console. WriteLineC {0}', attr.GetType.Name)
Next
// C#
foreach (Attribute attr in t.GetCustomAttributes(lalse)) {
Console.WriteLineC {0}", attr.GetType().Name);
}
Важное отличие атрибутов типов от атрибутов сборок — булев параметр inherit, который игнорируется классом Assembly. Поскольку атрибуты можно добавлять к любому из классов в иерархии, параметр inherit указывает системе отражения получать атрибуты определенного типа или всех типов иерархии наследования.
’	Я	'Д •	f 
Определение типа объекта
Разумеется, каждый объект является производным класса Object, который находится на вершине иерархии. Класс Object поддерживает метод GetType. Данный , метод возвращает объект Туре, который ссылается на тип объекта (представленный объектом Туре). В следующем примере создается класс Animal: ~
 VB
Public Class Animal
End Class
Ц C#
public class Animal
{
}
Теперь создадим два производных от него класса:
1 VB
Public Class Dog
Inherits Animal
End Class
Public Class Cat
Inherits Animal
End Class
II C#
public class Dog : Animal
{
}
Занятие 3
Отражение и типы 723
public class Cat : Animal
{ }
Затем можно создать три экземпляра, по одному для каждого из полученных классов. Сохраним их все в качестве Animals и выведем в консольном окне имя Туре:
' VB	<
Dim anAnimal As Animal = New Animal
Dim dogAnimal As Animal = New Dog
Dim catAnimal As Animal = New Cat
Console.WriteLine("Typename for anAnimal is {0}',
anAnimal.GetType.Name)
Console.WriteLineC"Typename for dogAnimal is {0}",
dogAnimal.GetType.Name)
Console.WriteLineC"Typename for catAnimal is {0}",
catAnimal.GetType.Name)
// C#
Animal anAnimal = new AnimalO;
Animal dogAnimal = new Dog();
Animal catAnimal = new Cat();
Console.WriteLineC"Typename for anAnimal is {0}",
anAnimal.GetTypeC).Name);
Console.WriteLineC"Typename for dogAnimal is {0}",
dogAnimal.GetTypeC).Name);
Console.WriteLineC"Typename for catAnimal is {0}",
catAnimal.GetTypeC).Name);
При выводе имен типов получаются имена созданных типов, а не локальных переменных:
Typename for anAnimal is Animal
Typename for dogAnimal is Dog
Typename for catAnimal is Cat
Почему так происходит? У объекта всегда один тип. Он может принадлежать к одному из своих базовых типов или интерфейсов, но только к одному из них.
Члены типа-перечислителя
В класс Туре входят методы для получения частей объекта Туре для методов, свойств, полей и событий. Эти части Туре представлены классами отражения, имена которых заканчиваются словом Info. Таким образом, класс, представляющий поле, называется Fieldinfo:, событие — Eventinfo, и т. д. Все /л/о-классы — производные от системного абстрактного класса Memberinfo. В действительности класс Туре также происходит от Memberinfo. Класс Memberinfo поддерживает многие часто используемые функции. Большая часть сведений о конкретных членах типа содержится в производных от него классах.
724 Отражение
Глава 14
Как говорилось ранее, методы класса Туре позволяет получить любую часть сведений о типе. Это верно и для info-классов. Например, методы GetEvent и GetEvents возвращают объекты Event Info, а методы Get Field и Get Fields — объекты Fieldinfo. Таким образом, перечислить любые данные в составе Туре несложно. Следующий пример перечисляет все свойства класса:
' VB
For Each prop As Propertyinfo In t.GetPropertiesO
Console.WriteLineC {0}", prop. Name)
Next
// C#
foreach (Propertyinfo prop in t.GetPropertiesO) {
Console.WriteLineC {0}", prop.Name);
}
Кроме частей типа, класс Туре поддерживает получение вложенных типов. Вложенными могут быть классы, интерфейсы, перечислимые и т. д. Получить их можно вызовом метода GetNestedType или GetNestedTypes. Данные методы возвращают объекты Туре, а не ш/Ь-классы. Поскольку класс Туре происходит от Memberinfo, он в любом случае является /л/о-классом. Вложенные типы можно получить следующим образом:
’ VB
For Each nestedType As Type In t.GetNestedTypes()
Console.WriteLineC {0}", nestedType.Name)
Next
// C#
foreach (Type nestedType in t.GetNestedTypesO)
Console.WriteLineC {0}", nestedType.Name);
Кроме того, анализировать части Туре можно с помощью GetMembers. Данный метод возвращает массив объектов Memberlnfo\
’ VB
For Each member As Memberinfo In t.GetMembersO
Console.WriteLineC. {0}: {1}", member.MemberType, member.Name) Next
// C#
foreach (Memberinfo member in t.GetMembersO)
Console.WriteLineC’ {0}: {1}”, member.MemoerType, member.Name);
}
Каждый из данных /л/b-классов предназначен для работы с определенной частью объекта Туре. В табл. 14-9 представлены все производные классы Memberinfo.
Занятие 3
Отражение и типы 725
Табл. 14-9. Производные классы Memberinfo
Имя	Что представляет
Constructorinfo	Конструктор
Eventinfo	Событие
Fieldinfo	Поле
LocalVariablelnfo	Локальную переменную в теле метода
MethodBase	Любой из членов, который может содержать код. Ими могут быть методы и конструкторы. Данный класс является базовым для Constructorinfo и Methodinfo
Methodinfo	Метод
Propertyinfo	Свойство. У свойств есть методы для чтения и установки значения свойства
Type	Один из типов, вложенный либо нет
С типами членов всех перечисленных выше объектов Memberinfo можно ознакомиться путем просмотра перечислимого MemberType класса Member Inf о. Данное перечислимое содержит типы членов, представленные объектом Memberinfo. Объекты Memberinfo также можно привести к производному типу:
1	VB
If (member.MemberType = MemberTypes.Property) Then
Dim prop As Propertyinfo = CType(member,Propertyinfo)
Console.WriteLineC Property Type {0}", prop.PropertyType.Name) End If
11	Cit
if (member.MemberType == MemberTypes.Property) {
Propertyinfo prop = (Propertylnfo)member;
Console.WriteLineC Property Type. {0}”, prop.Propertylype.Name);
}
Перечисление членов типа позволяет определить только открытые члены, как статические, так и динамические. Поддерживается получение всех членов класса, в том числе унаследованных. Для более гибкого контроля над получением членов отражение поддерживает перечислимое, которое позволяет задать, какие члены следует вернуть, — BindingFlags.
Перечислимое MethodBody
Просмотр информации о типе позволяет узнать его структуру, но не код. Для этого предназначен объект MethodBody. MethodBody — это контейнер особого вида, содержащий локальные переменные и текущие команды промежуточного языка (IL), которые CLR компилирует в машинные коды. Получить MethodBody можно вызовом метода GetMethodBody экземпляра MethodBase (класса Constructorinfo или Methodinfo)'.
’ VB
Dim body As MethodBody = meth.GetMethodBodyO
726 Отражение
Глава 14
// C#
MethodBody body = meth.GetMethodBody();
После создания экземпляра класса MethodBody можно получить локальные переменные и размер стека: • VB
Console.WriteLineC MaxStack: {0}", body.MaxStackSize)
For Each local As LocalVariablelnfo In body.Localvariables
Console.WriteLine("Local Var ({0}): {1} , local.LocalType, local,Locallndex) Next
11 Ctt
Console.WriteLineC MaxStack: {0}", body.MaxStackSize); foreach (LocalVariablelnfo local in body.LocalVariables) {
Console.WriteLine("Local Var ({0}): {1}", local.LocalType, local.Locallndex); }
Локальные переменные возвращаются в виде массива объектов LocalVariablelnfo, содержащего информацию о типе каждой переменной. Имя переменной при этом недоступно, поскольку метаданные типа содержат только его порядок, а не имя.
Наконец, можно получить массив байтов IL-кода, который используется CLR для компиляции и запуска машинного кода. Ниже приведен соответствующий пример: • VB For Each b As Byte In body.GetILAsByteArrayO
Console.Wnte('{0:x2} ”, b) Next
11 Ctt foreach (Byte b in body.GetILAsByteArrayO) {
Console.Write("{0:x2} ", b);
К СВЕДЕНИЮ Промежуточный язык
Синтаксис IL не рассматриваются в данной книге. Подробно об IL — на Web-сайте MSDN.
Работа с Binding Flags
Перечислимое BindingFlags управляет извлечением членов типа с помощью методов GetMembers и других методов, специфичных для членов типа. Перечислимое BindingFlags поддерживает флаги, то есть, принимает несколько значений. В табл. 14-10 приводятся члены перечисления BindingFlags и их описание.
Занятие 3
Отражение и типы 727
Табл. 14-10. ; Перечислимое BindingFlags **
Имя	Описание
DeclaredOnly	Содержит члены, объявленные непосредственно в заданном типе Унаследованные члены игнорируются j
Default	Флаги связывания не используются
FlattenHierarchy	Требуются объявленные, унаследованные и защищенные члены
IgnoreCase	Включает сравнение имен членов, нечувствительное к регистру
Instance	Содержит члены, которые входят в экземпляры типа
NonPublic	Содержит закрытые И другие члены, не являющиеся открытыми: защищенные, внутренние или, для! Visual Basic, friend-члены
Public	Содержит открытые члены w
Static	Содержит члены, объявленные как статические
При работе с BindingFlags комбинации флагов задают нужные члены. Например, для получения всех членов (открытых и закрытых) экземпляра типа необходимо выполнить следующее:
r :	₽ s j ь. и  .
Dim t As Type = GetType(String) f£K<i
Определить экземпляры открытых и закрытых членов
Dim flags As BindingFlags = _
BindingFlags.Public Or BindingFlags.NonPublic Or BindingFlags.Instance
С помощью флагов получить члены
Dim members() As Memberinfo = t.GetMembers(flags)
For Each member as Memberinfo In members
Console.WriteLine('Member: {0}", member.Name)
Next
11 Ctt
Type t = typeof(String);
11 Определить экземпляры открытых и закрытых членов
BindingFlags flags =
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
// С помощью флагов получить
Memberlnfo[] members = t.GetMembers(flags);
foreach (Memberinfo member in members) {
Console.WriteLlneC’Member: {0}", member.Name);
728 Отражение
Глава 14
Практикум. Загрузка сборки и вывод ее информации о типе
Ваша задача — создать консольное приложение для загрузки сборки и вывода информации о ее типах. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
1.	Создайте консольное приложение AssemblyDemo.
2.	Добавьте в главный файл кода ссылку на пространство имен System.Reflection.
3.	В теле метода main создайте локальную строку, содержащую путь к известной сборке. Сборка System.ServiceProcess в .NET Framework невелика и подойдет для данного проекта.
4.	Создайте локальную переменную BindingFlag и сохраните флаги для получения экземпляров определенных открытых членов.
5.	Создайте экземпляр класса Assembly путем загрузки сборки, на которую ссылается локальная строка, созданная на шаге 3.
6.	Выведите на консоль полное имя Assembly.
7.	Получите все типы объекта сборки, созданного на шаге 5.
8.	В цикле обойдите все возвращаемые типы.’ f*'
9.	В теле цикла добавьте вывод имени типа на консоль.
10.	С помощью переменной BindingFlag из шага 4 в цикле получите члены всех типов.
11.	В цикле обойдите все возвращаемые члены и выведите в консольное окно MemberType и имя члена. Полученный код выглядит примерно так:
• VB
Imports System.Reflection
Class Program
Public Overloads Shared Sub Main()
Dim path As String = "C:\WIND0WS\Microsoft.NET\Framework\v2.0.50727\" + _ "System.Se rviceP rocess.dll"
Получить экземпляры и объявленные члены с помощью BindingFlags Dim flags As BindingFlags =
BindingFlags.DeclaredOnly Or _
BindingFlags.Public Or
BindingFlags.Instance
Загрузить сборку из заданного каталога
Dim theAssembly As Assembly = Assembly.LoadFrom(path)
Console.WriteLine(theAssembly.FullName)
Dim typesO As Type = theAssembly. GetTypes
For Each t As Type In types
Console.WriteLineC" Type: {0}", t.Name)
Dim membersO As Memberinfo = t GetMembers(flags)
Занятие 3	Отражение и типы 729
For Each member As Memberinfo In members
Console.WriteLineC	{0}: {1}", member.MemberType, _
member.Name)
Next	л*
Next
Console.Read()
End Sub
End Class
	Й
11 C#
using System.Reflection;
class Program
{
static void Main(string[] args)
। * *
string path = ©"C:\WIND0WS\Microsoft.NET\Framework\v2.0.50727\" + "System.ServiceProcess.dll";
// Получить экземпляры и объявленные члены с помощью BindingFlags BindingFlags flags =
BindingFlags.DeclaredOnly |
BindingFlags.Public |
BindingFlags.Instance;
// Загрузить сборку из заданного каталога
Assembly theAssembly = Assembly.LoadFrom(path);
Console.WriteLineCtheAssembly.FullName);
Type[] types = theAssembly. GetTypesO;
foreach (Type t in types) {
Console.WriteLineC Type: {0}", t.Name);
Memberlnfo[] members = t.GetMembers(flags);
foreach (Memberinfo member in members) {
Console.WriteLineC {0} {1}", member.MemberType, member.Name);	> -
730 Отражение
Глава 14
Console.Read();
} .
12. Соберите проект, исправьте ошибки. Убедитесь, что приложение выводит все типы заданной сборки.
Резюме
	Метод GetType класса Object предназначен для получения информации о типе непосредственно из объекта.
	Классы Memberinfo и MemberlnfoCollection предназначены для перечисления членов типа, таких как методы, свойства, поля, события и перечислимые.
	Метод GetTypes класса Assembly предназначен для перечисления всех типов конкретной сборки.
	Перечислимое BindingFlags позволяет удобно управлять извлечением сведений о членах из Туре.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1. Какие типы являются производными от класса Memberinfo? (Укажите все верные ответы.)
А. Класс Fieldlrtfo.
В. Класс Methodinfo.
С. Класс Assembly.
D. Класс Туре.
2. Какое из значений BindingFlags необходимо для поиска открытых не унаследованных членов? (Укажите все верные ответы.) A. BindingFlags. Public.
В. BindingFlags.Static.
С. BindingFlags.DeclaredOnly.
D. BindingFlags.Instance.
Занятие 4. Динамическая генерация кода
До сих пор мы просматривали информацию о типах сборки и ее вывод. Но порой код этих сборок нужно не только анализировать, но и запускать. Также бывает, что во время компиляции код недоступен. Динамическая генерация кода предназначен для динамической загрузки и исполнения сборки без предварительного обращения к ней.
Занятие 4	Динамическая генерация кода 731
Изучив материал этого занятия, вы сможете:
J создавать экземпляры типа с помощью класса Constructorinfo;
J исполнять произвольный код с помощью ш/о-классов;
J вызывать статические методы и свойства.
Продолжительность занятия -*15 минут.
Работа с динамическим кодом
Система отражения позволяет динамически создавать объекты даже из сборок, ссылок на которые до сих пор не было. Это сложнее написания кода, обеспечивающего контроль типов при компиляции.
Как создать объект программно? Естественно, с помощью конструктора. Следующий пример кода создает Hashtable, добавляет элемент и возвращает номер.
ВАЖНО! О следующем примере
В данном примере используется класс Hashtable, на который ссылаются почти все .NET-приложения, так как все они используют сборку mscorlib. Динамический же код создается, только если загружается код, на который в приложение раньше не ссылалось.
• VB
Dim tbl As New HashtableO
tbl.Add("Hi", "Hello")
Console.WriteLineC"Hash count {0}", tbl.Count)
// C#
Hashtable tbl = new HashtableO;
tbl.AddC'Hi", "Hello”);
Console.WriteLineC"Hash count: {0}", tbl.Count);
Создание объектов
Другой способ — динамическая генерация кода на основе информации о типах, собранной посредством отражения. Для начала нужно получить информацию о типах, как рассказано в занятии 3:
’ VB
Dim path As String = "C:\WINDOWS\Microsoft NET\Framework\v2.0.50727\" + _ "mscorlib.dll"
’ Получить сборку
Dim theAssembly As Assembly = Assembly.LoadFile(path)
’ Получить тип Hashtable
Dim hashType As Type = theAssembly GetType("System.Collections.Hashtable")
// C#
string path = ©"C:\WIND0WS\Microsoft.NET\Framework\v2.0.50727\" + "mscorlib.dll";
732 Отражение
Глава 14
// Получить сборку
Assembly theAssembly = Assembly.LoadFile(path);
// Получить тип Hashtable
Type hashType = theAssembly.GetType("System.Collections.Hashtable");
Теперь можно запросить объект Constructorinfo для конструирования нового типа:
' VB
Dim argumentTypes() As Type = Type.EmptyTypes ' Empty Constructor Dim ctor As Constructorinfo = hashType.GetConstructor(argumentTypes)
// C#
Type[] argumentTypes = Type.EmptyTypes; // Empty Constructor Constructorinfo ctor = hashType.GetConstructor(argumentTypes);
Constructorinfo — специализированный объект MethodBase — действует как метод, но возвращает экземпляр заданного типа. В данном примере у класса Туре запрашивается пустой конструктор (для этого нужно передать пустой массив Array объектов Туре.) Также можно запросить конструктор с заданными аргументами, предоставив массива с типами аргументов:
’ VB
Dim argumentTypes() As Type = _
New Type() {GetType(System.Int32)} ’ One argument of type Int32 Dim ctor As Constructorinfo = hashType.GetConstructor(argumentTypes)
// C#
Type[] argumentTypes =
new Type[] { typeoffint) }; 11 Один аргумент типа int Constructorinfo ctor = hashType.GetConstructor(argumentTypes);
После получения Constructorinfo для создания требуемого объекта достаточно вызвать конструктор. Ниже приведен соответствующий пример кода:
’ VB
Dim newHash as Object = ctor. Invoke (New ObjectO {})
11 C#
object newHash = ctor.Invoke(new object[] {}); «
Вызов членов
Создав экземпляр объекта с помощью отражения, довольно просто получить щ/о-класс, вызов которого позволяет исполнять код. Следующий пример вызывает метод Add для нового экземпляра Hashtable-.
' VB
Dim meth As Methodinfo = hashType.GetMethod("Add") meth.Invoke(newHash, New ObjectO {"Hi", "Hello"})
// C#
Methodinfo meth = hashType.GetMethod("Add");
meth.Invoke(newHash, new object[] { "Hi", "Hello" });
Занятие 4
Динамическая генераций кода 733
Вызов Invoke класса Methodinfo требует объекта для запуска метода и массива параметров, соответствующих одной из перегруженных версий метода. В данном случае Invoke вызывается для динамически созданного Hashtable с двумя аргументами, которые позволяют добавить в Hashtable ключ и значение. Теперь для проверки работоспособности Add можно с помощью класса Property Info получить номер элементов Hashtable'.
' VB ,.н к.
Dim prop As Propertyinfo = hashType.GetProperty("Count")
Dim count As Integer = CType(prop.GetValue(newHash, Nothing),Integer)
// C#
Propertyinfo prop = hashType.GetProperty("Count");
int count = (int) prop.GetValue(newHash, null);
Класс Propertyinfo поддерживает получение и установку отдельных свойств. В данном случае метод GetValue класса Propertyinfo вызывается для получения значения свойства Count. Метод GetValue работает аналогично рассмотренному ранее Invoke. Он требует определения объекта-аргумента для вызова Get, и всех параметров. Параметры задаются только для свойств-индексаторов.
К СВЕДЕНИЮ Другие Znfb-классы
Данный способ вызова кода объекта используется в различных /л/о-классы {Fieldinfo, Eventinfo и т. д.). Подробнее о вызове всех типов данных /л/о-классов — в документации MSDN.
Запуск статических членов
Динамическая генерация кода не всегда требует экземпляра класса. Так, для динамического создания статических методов можно обойтись без создания объекта. Следующий пример получает тип класса System.Console и Methodinfo для статического метода Write Line'.
' VB
Dim consoleType As Type = GetType(Console)
Dim writeLineMethod As System.Reflection.Methodinfo = consoleType.GetMethod("WriteLine", New Type() {GetType(String)})
// C#
Type consoleType = typeof(Console);
Methodinfo writeLineMethod =
consoleType.GetMethodC'WriteLine", new Type[] { typeof(string) });
Данный код получает Methodinfo для одной из перегруженных версий перегрузки класса WriteLine, который получает простую строку String и выводит ее на консоль. Единственное отличие от примера, рассмотренного выше, в том, что метод — статический:
' VB writeLineMethod.Invoke(Nothing, New ObjectO {count.ToString})
// C#
writeLineMethod.Invoke(null, new object[] { count.ToStringO });
734 Отражение	Глава 14
Поскольку данный метод статический, вместо объекта-параметра для вызова метода, можно передать null. Эти параметры определяются так .же, как в предыдущем примере.
Класс Binder
Работая с динамическим кодом, можно сконструировать собственный класс Binder, определяющий способ преобразования типов и расположение динамического кода. Экземпляр класса Binder является необязательным аргументом методов класса Туре, возвращающих члены типа. Если не задан собственный экземпляр класса Binder, система работает с экземпляром, заданным по умолчанию. Нестандартный класс Binder удобен, когда требуется преобразование типов или подбор других параметров для подходящего метода, свойства или события при поиске zn/o-класса с нужной реализацией. .
К СВЕДЕНИЮ Класс Binder
Исчерпывающий пример создания класса Binder приводится в документации MSDN.
Практикум. Вызов членов посредством отражения
В данном практикуме вы создадите простое консольное приложение для загрузки класса HttpUtility из сборки System. Web и перекодирования текста в HTML. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
1.	Создайте консольное приложение DynamicCodeDemo.
2.	В главном файле кода добавьте ссылку на пространство имен System.Reflection.
3.	В методе main создайте строку пути к сборке System. Web в каталоге Framework.
4.	Создайте объект Assembly и загрузите сборку System. Web с помощью строки из шага 3.
5.	Создайте в объекте Assembly (см. Шаг 4) экземпляр класса Туре для получения System. Web.HttpUtility.
6.	Получите объекты Methodinfo для методов HtmlEncode и HtmlDecode путем вызова GetMethod нового объекта Туре.
7.	Создайте строку, содержащую необходимые элементы HTML-тэгов (например, '<’, *>’ или Выведите эту строку на консоль.
8.	С помощью объекта Methodinfo из HttpEncode перекодируйте данную строку. Выведите эту строку на консоль.
9.	С помощью объекта Methodinfo из Http Decode декодируйте строку. Выведите ее на консоль. Ниже приведен соответствующий пример кода:
VB
Imports System.Reflection
Class Program
Public Overloads Shared Sub Main()
Dim path As String = "C:\WINDOWS\Microsoft.NET\Framework\" + _
" v2.0.50727\System.Web.dll"
Захятие 4 к
Динамическая генерация кода 735
' Получить сборку из файла
Dim webAssembly As Assembly = Assembly.LoadFile(path)
' Получить тип класса Httputility
Dim utilType As Type =
webAssembly.GetType("System.Web.HttpUtility")
’ Получить статические методы HtmlEncode и HtmlDecode
Dim encode As Methodinfo = utilType.GetMethod("HtmlEncode", New Type() {GetType(System.String)})
Dim decode As Methodinfo = utilType.GetMethod( HtmlDecode", New Type() {GetType(System.String)})
’ Создать строку для кодирования
Dim originalString As String =
"This is Sally & Jack's Anniversary <sic>"
Console.WriteLine(originalString)
закодировать и вывести ее значение
Dim encoded As String =
CType(encode.Invoke(Nothing, New ObjectO {originalString}), String)
Console.WriteLine(encoded)
' декодировать строку и проверить ее правильность
Dim decoded As String = _
CType(decode.Invoke(Nothing, New ObjectO {encoded}), String) Console.WriteLine(decoded)
End Sub
Л -
End Class
// C#
using System.Reflection;
class Program
{
static void Main(string[] args)
{
string path = ©"C:\WINDOWS\Microsoft.NET\Framework\" + v2.0.50727\System. Web. dll ;
// Получить сборку из файла
Assembly webAssembly = Assembly.LoadFile(path);
736 Отражение
Глава 14
// Получить тип класса HttpUtility
Type utilType = webAssembly.GetType("System.Web.HttpUtility”);
// Получить статические методы HtmlEncode и HtmlDecode Methodinfo encode = utilType GetMethod("HtmlEncode”, new Type[] { typeof(string) });
Methodinfo decode - utilType.GetMethod("HtmlDecode", new Type[] { typeof(string) });
s'
// Создать строку для кодирования
string originalstring =
"This is Sally & Jack’s Anniversary <sic>";
Console.WriteLine(originalString);
// закодировать и вывести ее значение
string encoded =
(string) encode.Invoke(null, new object[] { originalstring });
Console.WriteLine(encoded);
// декодировать строку и проверить ее правильность string decoded =
(string) decode.Invoke(null, new object[] { encoded });
Console WriteLine(decoded);
}
}
10.	Соберите проект, исправьте ошибки. Убедитесь в правильности кодирования и декодирования строки.
Резюме
	Классы Туре и Constructorinfo предназначены для создания экземпляра типа.
	Классы Memberinfo предназначены для исполнения произвольного кода над заданным экземпляром типа.
	Классы Memberinfo предназначены для исполнения статического кода, связанного с определенным типом.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1. Какой из методов класса MethodBase позволит синхронно исполнить метод, принимающий два параметра и возвращающий булево значение?
Занятие 5
Генерация кода во время выполнения 737
А.
’ VB
method.Invoke(returnvalue, paraml, param2)
П C#
method.Invoke(ref returnvalue, paraml, param2);
B.
 VB
Dim result As Boolean = CType(method.Invoke(param1, param2),Boolean)
// C#
bool result = (bool)method.Invoke(paraml, param2);
C.
' VB
Dim result As Boolean =
CType(method.Invoke(thelnstance, paraml, param2),Boolean)
// C#
bool result = (bool)method.Invoke(thelnstance, paraml, param2);
D.
’ VB
Dim result As Boolean = CType(method.Invoke(thelnstance,
new Object() { paraml, param2 }),Boolean)
// C#
bool result = (bool)method.Invoke(thelnstance,
new object[] {paraml, param2});
2. Можно ли с помощью класса MethodBody вызвать часть тела метода?
А. Да.
В. Нет.
Занятие 5. Генерация кеда во время
выполнения
Возможности отражения не ограничиваются определением информации о сборках и типах. Определить данную информацию и даже создать собственную сборку (хранимую в памяти или на диске) можно во время выполнения.
Изучив материал этого занятия, вы сможете:
J динамически создавать сборки;
J динамически создавать модули в новой сборке;
J создавать новые типы с помощью классов генерации;
J сериализовать созданные сборки на диск.
Продолжительность занятия — 10 минут.
Глава 14
738 Отражение
Сборка с собственным кодом
Отражение включает подмножество пространства имен Emit {System. Reflection.Emit), которое содержит набор классов, генерирующих сборки, типы, методы и т. д. Чтобы сгенерировать код во время выполнения, его, как и любой другой код, нужно инкапсулировать: (создать сборку, в ней — модуль, а нем — типы). Каждый из этих классов является производным соответствующего zn/o-класса. Например, Assembly Builder происходит от Assembly, MethodBuilder — от Methodinfo, a TypeBuilder — от Туре. В табл. 14-11 представлены все классы генерации кода во время выполнения.
Табл. 14-11. Классы Builder
Имя	Что генерируют
AssemblyBuilder	Сборки
ConstructorBuilder	Конструкторы
EnumBuilder	Перечислимые
EventBulder	События
FieldBuilder	Поля
LocalBuilder	Локальные переменные для методов и конструкторов
MethodBuilder	Методы	1-
ModuleBuilder	Модули
ParameterBuilder	Параметры методов
PropertyBuilder	Свойства
TypeBuilder	Типы
Создание сборки и модуля
Перед определением типа нужно создать сборку и модуль. Для этого необходимо запросить у AppDomain создание динамической сборки. Метод DefineDynamicAssembly класса AppDomain получает параметры AssemblyName и AssemblyBuilderAccess. Объект
AssemblyName содержит все части полного имени сборки. В данном случае требуется только простое имя сборки. Создать объект AssemblyName можно следующим образом?
' VB
Dim tempName As New AssemblyNameO

tempName.Name = "MyTempAs^embly"

ft»f ’>>
// C#
AssemblyName tempName = new AssemblyNameO;
tempName.Name = "MyTempAssembly”f
AssemblyBuilderAccess действия, которые можно выполнять над новой динамической сборкой. В зависимости от задач, можно выбрать ReflectionOnly, Run, Save или RunAndSave Для создания собственной динамической сборки можно вызвать метод DefineDynamicAssembly текущего AppDomain'.
’ VB
4 Г’	''Г*
Dim assemBldr As AssemblyBuilder = _
AppDomain.CurrentDomain.DefineDynamicAssembly(tempName,
AssemblyBuilderAccess RunAndSave)
Занятие 5
Генерация кода во время выполнения 73g
// C#
AssemblyBuilder assemBldr =
AppDomain.CurrentDomain.DefineDynamicAssembly(tempName,
AssemblyBuilderAccess.RunAndSave);
Получив объект AssemblyBuilder, можно создать ModuleBuilder, т. e. сгенерировать информацию о типах. Для этого достаточно при вызове ml года AssemblyBuilder. DefineDynamicModule задать имя сборки и ее файла: ~ ’ VB
Dim modBldr As ModuleBuilder = assemBldr.DefineDynamicModule("MainMod”,
"MyTempAssembly.dll")	w
// C#
ModuleBuilder modBldr = assemBldr.DefineDynamicModule("MainMod", "MyTempAssembly.dll");
Если сборка собрана в одном файле, сериализирована и записана на диске, следует указывать то же имя файла, что в методе Save (см. ниже). Во время выполнения ModuleBuilder можно создать динамический тип. 
Определение типа
Создание динамического типа начинается с вызова метода DefineType класса ModuleBuilder. Данный метод принимает имя типа и перечислимое TypeAttributes. Type Attributes определяет параметры типа, в данном случае — является ли тип открытым, а также является ли он классом, структурой или интерфейсом: ’ VB
Dim typeBldr As TypeBuilder = modBldr.DefineType("MyNewType",
TypeAttributes.Public Or TypeAttributes.Class)
// C#
TypeBuilder typeBldr = modBldr.DefineType("MyNewType",
TypeAttributes.Public | TypeAttributes.Class);
,*^дкже можно определить базовый класс и интерфейсы. Это делается с помощью перс определенного метода pefineType, который получает как третий и четвертый параметры, соответственно, базовый класс и массив типов реализованных интерфейсов:
’ VB
Dim typeBldr2 As TypeBuilder = modBldr.DefineType("MyNewType",
(TypeAttributes.Public Or TypeAttributes.Class),
GetType(Hashtable),
New Type() {GetType(IDisposable)})
// C#
TypeBuilder typeBldr2 = modBldr.DefineType("MyNewType",
TypeAttributes.Public | TypeAttributes.Class,
typeof(Hashtable),
new Type[] { typeof(IDisposable) });
740 Отражение
Глава 14
Создание членов
Класс TypeBuilder, возвращаемый методом DefineType, — важнейший класс для динамической генерации кода. TypeBuilder позволяет определять любые элементы типов. В табл. 14-12 представлены различные методы TypeBuilder.
Табл. 14-12. Методы определения TypeBuilder
Имя	Что определяет
DefineConstructor	Конструкторы
DefineDefaultConstructor	Конструкторы без параметров
DefineEvent	События
DefineField	Поля
DefineGenericParameters	Обобщенные параметры обобщенных типов
DefineMethod	Методы
DefineMethodOverride	Перегруженные методы
DefineNestedType	Вложенные типы
DefinePInvokeMethod	Вызовы внешнего кода с помощью PInvoke
DefineProperty	Свойства
Сначала нужно создать конструктор. В данном случае это конструктор, заданный по умолчанию (т. е. не имеющий параметров), поэтому используется метод DefineDefaultConstructor.
' VB
Dim ctorBldr As ConstructorBuilder =
typeBldr.DefineDefaultConstructor(MethodAttributes.Public)
// C#
ConstructorBuilder ctorBldr =
typeBldr.DefineDefaultConstructor(MethodAttributes.Public);
Метод DefineDefaultConstructor получает перечислимое MethodAttributes, определяющее тип конструктора. В данном случае конструктор, заданный по умолчанию, является открытым.
По-настоящему возможности этого метода раскрываются при написании собственного кода. Теперь с помощью конструктора можно создать объект ILGenerator. Класс ILGenerator генерирует локальные переменные и IL-код. В следующем примере конструктор возвращает управление, не выполняя никаких других действий:
’ VB
Dim codeGen As ILGenerator = ctorBldr.GetILGenerator()
codeGen.. Emit (Opcodes. Ret)
// C#
ILGenerator codeGen = ctorBldr.GetILGeneratorO;
codeGen.Emit(Opcodes.Ret);
Вызов ILGenerator.Emit вставляет новую строку IL-кода в конструктор (или метод), которым создан ILGenerator. Класс OpCodes обрабатывает каждый из IL-кодов, представленный OpCodes, как отдельный статический член. OpCodes.Ret определяет возвращаемый IL-код.
Занятие 5
Генерация кода во время выполнения	74-|
К СВЕДЕНИЮ Промежуточный язык Microsoft (IL)
Чтобы добавить код в тело конструктора или метода, нужно знать принцип работы IL. Это отдельная тема, которая не входит в программу экзамена 70-536 и потому не рассматривается в данной книге. Реализация IL, созданная Microsoft, называется MSIL. Подробнее о MSIL — в документации MSDN по адресу http://msdn2.microsoft.com/en~US/ library/8ffc3x75(VS.80).aspx.
Создание конструктора является только начальным этапом, необходимо создать и другие члены типа. Создадим теперь метод. Для этого требуется задать типы параметров и возвращаемого значения. Ниже приведен соответствующий пример кода:
' VB
Dim methBldr As MethodBuilder = typeBldr. DefineMethodC’Add", MethodAttributes.Public, Nothing,
New Type() {GetType(System.String)})
// C#
MethodBuilder methBldr = typeBldr.DefineMethodC’Add",
MethodAttributes.Public,
null,
new Type[] { typeof(string) });
Как и другие методы, данный метод содержит перечень атрибутов, определяющих метод. Третьим и четвертым параметром метода DefineMethod являются типы возвращаемого значения и параметров, соответственно.
Добавив атрибут MethodAttributes.Static, mo^hq определить статический (в Visual Basic — общий) метод:
’ VB
Dim methBldr As MethodBuilder = typeBldr.DefineMethodC’Add",
MethodAttributes.Public Or MethodAttributes.Static,
Nothing,
New Type() {GetType(System.String)})
// C#
MethodBuilder methBldr = typeBldr.DefineMethodC’Add",
MethodAttributes.Public | MethodAttributes.Static, null, new Type[] { typeof(string) });
Далее определим новое закрытое поле _count, содержащее текущий счетчик нового типа:
’ VB
Dim fieldBldr As FieldBuilder = typeBldr.DefineField("_count", GetType(System.Int32), FieldAttributes.Private)
// C#
FieldBuilder fieldBldr = typeBldr.DefineField("_count",
742 Отражение
Глава 14
typeof(int),
FieldAttгibates.Private);
Как и остальные классы генерации кода, FieldBuilder содержит перечислимое FieldAttributes для определения атрибутов поля. В данном случае поле объявляется как открытое.
Наконец, можно добавить к типу свойство, хранящее новое поле. Свойство можно определить методом DefineProperty.
' VB
Dim propBldr As PropertyBuilder = typeBldr.DefineProperty("Count",
PropsrtyAttributes.None,
GetType(System.Int32),
Type.EmptyTypes)
// C#
PropertyBuilder propBldr = typeBldr.DeflneProperty("Count",
PropertyAttributes.None,
typeof(int),
Type.EmptyTypes);
Процедура определения свойства очень похоже на таковую для метода, перечислимое PropertyAttribute позволяет задавать далеко не все аспекты свойств. Например, так нельзя задать область видимости свойства. Вместо этого следует создать методы для чтения и установки свойства. Ниже приведен соо гветствующий пример кода:
' VB
Dim getAttributes As MethodAttributes = MethodAttributes.Pi bile _
Or MethodAttributes.SpecialName Or _
MethodAttributes.HideBySig
Dim propGetBldr As MethodBuilder = typeBldr.DefineMethod('get_Count", getAttributes,
GetType(System.Int32),
Type.EmptyTypes)
// C#
MethodAttributes getAttributes = MethodAttributes.Public |
MethodAtt ributes.SpecialName |
MethodAttributes.HideBySig;
MethodBuilder propGetBldr = typeBldr. DefineMethod(*'get_Count",
getAttributes,
typeof(int),
Type.EmptyTypes);
Данные методы можно создавать посредством HideBySig и MethodAttributes из SpecialName. Данные атрибуты объявляют этот метод специальным закрытым методом. После присвоения имени метода его следует вызвать так: get_XXXX — для получения, и setXXXX — для установки значения поля,, (здесь ХХХХ — имя свойства, в данном случае — get_Count). Полученный метод можно связать со свойством вызовом SetGetMethod (или SetSetMethod для метода, устанавливающего свойство):
Занятие 5
Генерация кода во время выполнения 743
' VB
propBld г.SetGetMethod(р ropGetBld г)
И C#
р ropBld г.SetGetMethod(р ropGetBld г);
Сохранение кода на диск
Динамически сгенерированные типы и исполняемый код можно запустит в любое время (если данный объект Assembly допускает исполнение кода). При этом желательно сохранить сгенерированную сборку на диск. После этого любой процесс сможет загружать данную сборку как обычно. Сборка записывается на диск просто вызовом метода Save:
’ VB
assemBldr.Save("MyTempAssembly.dll")
// C#
assemBldr.Save("MyTempAssembly.dll");
Практикум. Создание динамической сборки
Ваша задача — создать консольное приложение, генерирующее новую сборку. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
1.	Создайте консольное приложение DemoDynamic.
2.	В главный файл кода добавьте ссылку на пространства имен System.Reflection и System.Reflection.Emit.
3.	В теле метода Main создайте объект Assembly Name и задайте его имя и версию.
4.	Создайте переменную AppDomain и сохраните в ней текущий AppDomain.
5.	Создайте объект AssemblyBuilder посредством вызова метода DefineDynamicAssembly класса AppDomain, передав значение AssemblyName, взятое из шага 3, и указав, что данная сборка предназначена только для работы с отражением.
6.	Определите объект ModuleBuilder в сборке AssemblyBuilder, созданной на шаге 5.
7.	Затем определите TypeBuilder с помощью объекта ModuleBuilder, созданного на шаге 6.
8.	В завершение с помощью TypeBuilder создайте объект Туре для созданного типа и обработайте в цикле его методы, свойства, события и поля. Полученный код выглядит примерно так:
’ VB
Imports System.Reflection
Imports System.Reflection.Emit
* t
Class Program
Public Shared Sub Main(ByVal args() As String)
' Создать имя сборки
Dim theName As AssemblyName = New AssemblyName
theName.Name "DemoAssembly"
744 Отражение
Глава 14
theName.Version = New Version("1.0.0.0")
' Получить AppDomain для добавления сборки
Dim domain As AppDomain = AppDomain CurrentDomain
' Создать сборку
Dim assemBldr As AssemblyBuilder = domain.DefineDynamicAssembly(theName, AssemblyBuilderAccess.ReflectionOnly)
' Определить модуль для хранения типа
Dim modBldr As ModuleBuilder = _ assemBldr.DefineDynamicModuleC'CodeModule", "DemoAssembly.dll")
Создать тип
Dim animalBldr As TypeBuilder = modBldr.DefineType("Animal", TypeAttributes.Public)
' Вывести новый тип
Dim animal As Type = animalBldr.CreateType
Console.WriteLine(animal.FullName)
For Each m As Memberinfo In animal.GetMembers
Console.WriteLineC Member ({0}): {1}", m.MemberType, m.Name) Next
Console.Read()
End Sub
End Class
// C#
using System.Reflection;
using System.Reflection.Emit;
class Program {
static void Main(string[] args) {
// Создать имя сборки
AssemblyName theName = new AssemblyNameO;
theName.Name = "DemoAssembly";
theName.Version = new Version("1.0.0.0");
// Получить AppDomain для добавления сборки AppDomain domain = AppDomain.CurrentDomain;
// Создать сборку
Занятие 5	Генерация кода во время выполнения 745
AssemblyBuilder assemBldr = domain.DefineDynamicAssembiy(theName, AssemblyBuilderAccess.ReflectionOnly);
11 Определить модуль для хранения типа ModuleBuilder modBldr =
assemBldr.DefineDynamicModule("CodeModule", "DemoAssembly.dll');
// Создать тип
TypeBuilder animalBldr = modBldr.DefineType("Animal", TypeAttributes.Public);
// Вывести новый тип
Type animal = animalBldr.CreateTypeO;
Console.WriteLine(animal.FullName);‘ foreach (Memberinfo m in animal.GetMembersQ) {
Console.WriteLineC Member ({0}): {1}", m. MemberType, m.Name);
}
Console.Read();
}
9.	Соберите проект, исправьте ошибки. Убедитесь, что полученное приложение создает сборку, модуль, тип и выводит члены производного класса Object.
Резюме
	Метод Define Dynamic Assembly класса AppDomain предназначен для динамического создания сборок во время выполнения.
	Для создания модуля в динамической сборке нужно вызвать DefineDynamicModule с помощью класса AssemblyBuilder.
	Метод DefineType класса ModuleBuilder предназначен для генерации типов во время выполнения.
	Методы DefineConstructor, Define Method, DefineProperty и DefineField класса TypeBuilder предназначены для генерации членов типов.
	Метод Save класса AssemblyBuilder предназначен для сохранения динамической сборки на диск.
Закрепление материала
Ниже приводятся вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы На эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
УЧ
*	V
746 Отражение	Глава 14
1.	Какой класс позволяет динамически создавать сборки (экземпляры класса AssemblyBuilder)?
A.	AssemblyBuilder.
В.	ModuleBuilder.
С. AppDomain.
D. Application.
2.	Какие значения перечислимого AssemblyBuilderAccess разрешают исполнение кода сборки?
A.	AssemblyBuilderAccess.Run.
В.	AssemblyBuilderAccess.ReflectionOnly.
С. AssemblyBuilderAccess.RunAndSave.
D. AssemblyBuilderAccess.Save.
Закрепление материала главы
Для закрепления знаний и навыков, полученных при изучении этой главы, выполните следующее:
	изучите резюме главы;
	повторите основные термины, использованные в этой главе;
	выполните задания лабораторной работы по темам этой главы;
	выполните предложенные упражнения;
	пройдите пробный экзамен.
Резюме главы
	Пространство имен Reflection содержит классы для запроса метаданных сборок, модулей и типов у CLR.
	Определить информацию о сборке (сведения об авторских правах, версию и товарный знак разработчика) можно с помощью множества атрибутов.
	Большинство классов для хранения метаданных происходит от класса Memberinfo. В их число входят Type, Methodinfo и Fieldinfo.
	Перечисление BindingFlags предназначено для определения различных опций поиска и извлечения членов разнообразных типов.
	Классы Memberinfo позволяют запускать код, даже если во время компиляции в проекте не было ссылок на этот код.
	Классы генерации кода предназначены для создания сборок, модулей и типов во время выполнения, а также для сохранения сгенерированных сборок на диск.
• •<>,
Основные термины
.«г
	модуль;
	многофайловые сборки;
	сопутствующие сборки.
Рекомендуемые упражнения 747
Лабораторная работа. Конструирование архитектуры подключаемых модулей
Сейчас вы примените на практике знания и навыки, полученные при изучении этой главы. Ответы на вопросы см. в разделе «Ответы» в конце книги.
Вы работаете в небольшой фирме, занятой разработкой программного обеспечения Ваш основной продукт — приложение для рисования схем сетей. Необходимо предоставить другим компаниям возможность дополнять вашу программу собственными функциями. Руководству требуется знать, как вы планируете решить поставленную задачу.
Результаты опроса
	Руководитель департамента разработки
«У сторонних фирм должна быть возможность добавлять собственный код к нашим приложениям, при этом их код должен работать. Мы не знаем, будет ли установлен .NET SDK на всех клиентских машинах, так что необходимо обойтись без перекомпиляции приложения после добавления компонентов сторонних компаний.»
Вопросы
Ответьте на следующие вопросы руководства:
1. Как вы планируете решать поставленную задачу?
2. Как вывести на экран товарный знак и другую информацию о компании-партнере?
Рекомендуемые упражнения
Чтобы лучше проработать темы экзамена, рассмотренные в этой главе, выполните следующие упражнения.
Приложение Assembly Explorer
Выполните как минимум упражнения 1 и 2. Дополнительный опыт работы с отражением позволит получить упражнение 3.
Упражнение 1
	Создайте приложение Windows Forms, которое загружает сборки и выводит в древовидном меню данные, полученные с помощью отражения.
	Дайте пользователям возможность просмотра любых атрибутов с помощью ICustomAttributeProvider и вызова GetCustomAttributes.
Упражнение 2
	Добавьте к приложению возможность создания экземпляров любого типа.
	Разрешите пользователям задавать свойства и запускать методы созданных объектов.  Дайте пользователям возможность вызова статических методов.
748 Отражение	Глава 14
Упражнение 3
	Разрешите пользователям копировать типы в динамические сборки и сохранять последние.
Пробный экзамен
На прилагаемом компакт-диске содержится пробный экзамен, обеспечивающий широкие возможности по самоподготовке. Например, можно проверить усвоение темам этой главы либо всех тем экзамена 70-536. Можно сымитировать настоящий экзамен либо выбрать режим обучения, когда после каждого вопроса приводятся правильные ответы с пояснениями.
Пробный экзамен
Подробнее о пробном экзамене рассказано во введении.
ГЛАВА 15
Электронная почта
Занятие 1. Создание почтового сообщения
Занятие 2. Отправка почты
750
760
Электронная почта — очень популярное средство связи. В отличие от телефона и большинства других способов связи, электронную почту можно использовать программно. Для разработчика электронная почта — эффективный инструмент, позволяющий отправлять из приложений различные файлы и отчеты, а также уведомлять пользователя о произошедших событиях или возникших проблемах.
В .NET Framework 2.0 добавлено пространство имен System.Net.Mail, включающее классы, которые позволяют без труда создавать и передавать сообщения электронной почты. Сообщения могут включать простой текст, текст в формате HTML и вложенные файлы. В общем, отправка почты состоит из двух этапов: создание сообщения и его отправка на сервер SMTP (Simple Mail Transfer Protocol). В занятии 1 рассказывается, как создавать сообщения, а в занятии 2 — как их отправлять.
ПРИМЕЧАНИЕ .NET 2.0
Пространство имен System.Net.Mail впервые появилось в .NET Framework 2.0.
Темы экзамена:
	Отправка сообщений электронной почты на SMTP-сервер из .NET-приложений (см. пространство имен System.Net.Mail):
□	класс MailMessage',
□	классы Mail Address и MailAddressCollection',
□	классы SmtpClient, SmtpPermission и SmtpPermissionAttribute’,
□	классы Attachment, AttachmentBase и Attachmentcollection',
□	классы SmtpException, SmtpFailedRecipientException и SmtpFailedRecipientsException',
□	делегат SendCompletedEventHandler,
□	классы LinkedResource и LinkedResourceCollection',
□	классы AlternateView и AlternateViewCollection.
750 Электронная почта
Глава 15
Прежде всего
Чтобы освоить материал этой главы, необходимо иметь навыки работы с Microsoft Visual Basic или C# и уметь:
	создавать консольные приложения в Visual Studio при помощи Visual Basic или С#;
	добавлять в проект ссылки на системные библиотеки классов.
Занятие 1. Создание почтового сообщения
Процедура создания сообщений электронной почты может быть как простой, так и сложной. В простейшем варианте в сообщение включены сведения об отправителе и получателе, тема и текст (тело) сообщения. Используя .NET Framework, простые сообщения можно создавать с помощью одной строки кода. Сложные сообщения включают особые типы кодировки, простой текст и текст в формате HTML, вложенные файлы и изображения, внедренные средствами HTML.
Изучив материал этого занятия, вы сможете:
J описать процедуру создания и отправки почтовых сообщений;
J создавать объекты MailMessage',
J прикреплять файлы к почтовому сообщению;
J создавать сообщения в формате HTML, с изображениями или без них;
J перехватывать и обрабатывать различные исключения, которые могут быть сгенерированы при создании сообщений.
Продолжительность занятия — 30 минут.
Процедура создания и отправки сообщений электронной почты
Для создания и отправки почтового сообщения выполните следующее:
1.	Создайте объект MailMessage. MailMessage и другие классы, относящиеся к почтовым сообщениям, находятся в пространстве имен System.Net.Mail.
2.	Если вы не указали получателей в конструкторе MailMessage, добавьте их в объект MailMessage.
3.	Если нужно предоставить несколько видов отображения текста (простой текст и HTML), создайте объекты AltemateView и добавьте их в объект MailMessage.
4.	Если требуется, создайте один или несколько объектов Attachment и добавьте их в объект MailMessage.
5.	Создайте объект SmtpClient и укажите SMTP-сервер.
6.	Если SMTP-сервер требует аутентификации клиентов, добавьте учетные данные в объект SmtpClient.
7.	Передайте объект MailMessage методу SmtpClient.Send. В качестве альтернативы можно использовать SmtpClient.SendAsync для асинхронной передачи сообщения.
Шаги 5—7 подробно описаны в занятии 2.
Занятие 1
Создание почтового сообщения 75 -|
Создание объекта MailMessage
Объект MailMessage имеет четыре конструктора, позволяющих создать пустой объект MailMessage} указать отправителя и получателя или отправителя, получателя, тему и тело сообщения. Если создается простое сообщение с одним получателем, большую часть работы можно выполнить в конструкторе MailMessage’.
’ VB
Dim m As MailMessage = New MailMessage
("jane@contoso.com",
"ben@contoso.com",
"Quarterly data report.",
"See the attached spreadsheet.")
// C#
MailMessage m = new MailMessage
("j ane@contoso.com",
"ben@contoso.com",
"Quarterly data report.",
"See the attached spreadsheet.");
ПРИМЕЧАНИЕ Быстрая отправка сообщений
Можно также использовать перегруженную версию метода SmtpClient.Send для отправки сообщения электронной почты без создания объекта MailMessage. Класс SmtpClient описывается в занятии 2.
Отправителя и получателя можно указать как строку или как объект MailAddress. Объект MailAddress позволяет указать адрес электронной почты, отображающееся имя и тип кодировки, как показано в следующем фрагменте кода:
' VB
Dim m As MailMessage = New MailMessage
(New MailAddress("lance@contoso.com", "Lance Tucker"),
New MailAddress("ben@contoso.com", "Ben Miller"))
11 C#
MailMessage m = new MailMessage
(new MailAddress("lance@contoso.com", "Lance Tucker"), new MailAddress("ben@contoso.com", "Ben Miller”));
ПРИМЕЧАНИЕ Типы кодировки
Необходимость указывать тип кодировки для адресов электронной почты возникает не так часто.
Если нужно указать несколько получателей, используйте пустой конструктор MailMessage. Затем добавьте объекты MailAddress в свойство MailMessage. То (имеющее тип MailAddressCollection), а затем укажите значения свойств Mail Message. From, MailMessage.Subject и MailMessage.Body.
1 VB
Dim m As MailMessage = New MailMessageO
752 Электронная почта
Глава 15
m.From = New MailAddressClance@contoso.com", "Lance Tucker”) m.To.Add(New MailAddressCjames@contoso.com", "James van Eaion")) m.To.Add(New MailAddress("ben@contoso.com", "Ben Miller")) m.To.Add(New MailAddress("burke@contoso.com", "Burke Fewel")) m.Subject = "Quarterly data report." m.Body = "See the attached spreadsheet."
// C#
MailMessage m = new MailMessageO;
m.From = new MailAddress("lance@contoso.com", "Lance Tucker");
m.To.Add(new MailAddress("james@contoso.com", "James van Eaton”)); m.To.Add(new MailAddress("ben@contoso.com", "Ben Miller”));
m.To Add(new MailAddress("burke@contoso.com", "Burke Fewel'));
m.Subject = "Quarterly data report.";
m.Body = "See the attached spreadsheet.";
Кроме этого, можно добавить получателей в свойства MailMessage.Сс и MailMessage.Вес аналогично тому, как они добавляются в MailMessage.From. Получатели, указанные в свойстве MailMessage.Сс, получат сообщение, а их имена будут видны в строке СС почтового сообщения, которая видна всем получателям. Получатели, указанные в свойстве MailMessage.Все, получат сообщение, но их имена не будут видны другим получателям. ВСС означает «blind carbon сору» (слепая копия) — термин, произошедший в те времена, когда документы размножали, печатая их под копирку.
ПРИМЕЧАНИЕ Риск при использовании ВСС
Вместо использования ВСС следует отправлять отдельную копию сообщения каждому получателю, который должен получить «слепую» копию. Проблема с ВСС в том, что спам-фильтры часто блокируют сообщения, в заголовке From которых не указан адрес получателя. Поэтому при использовании ВСС высока вероятность того, что сообщение будет задержано фильтрами.
Следующие свойства класса MailMessage используются не так часто:
	DeliveryNotificationOptions
Указывает SMTP-серверу отправить сообщение по адресу, указанному свойством MailMessage.From, если сообщение задержано, не доставлено, успешно доставлено или перенаправлено другому серверу. Это перечислимое типа DeliveryNotificationOptions, имеющее значения OnSuccess, OnFailure, Delay, None и Never.
	Reply To
Адрес электронной почты, на который будет отправлено ответное сообщение. Так как в NET Framework нет почтового клиента, а значит, типичное приложение не сможет получать сообщения, в большинстве случаев просто следует указать в свойстве MailMessage.From адрес, на который будут приходить ответы, а не использовать ReplyTo.
	Priority
Приоритет сообщения. Никак не влияет на обработку сообщения исполняющей средой или почтовым сервером, однако отображается почтовым клиентом получателя Можно фильтровать автоматически генерируемые сообщения по приоритету события, инициировавшего отправку. Это перечислимое типа MailPriority, принимающее значения Normal, High и Low.
Занятие 1
Создание почтового сообщения 753
Прикрепление файлов
Чтобы прикрепить файл, добавьте его в MailMessage.Attachments Attachmentcollection, вызвав метод MailMessage.Attachments.Add. Самый простой способ добавить файл — указать его имя:
’ VB
Dim m As MailMessage = New MailMessageO
m.Attachments.Add(New Attachment("C:\boot.ini"))
// C#
MailMessage m = new MailMessageO;
m.Attachments.Add(new Attachment(@"C:\boot.ini"));
Можно также указать MIME-тип (Multipurpose Internet Mail Extensions, многоцелевые расширения электронной почты Интернета) содержимого, используя перечислимое System.Net.Mime.MediaTypeNames. Существуют специальные MIME-типы для текста и изображений, но обычно нужно указывать MediaTypeNames.Application.Octet. В следующем фрагменте кода (в котором кроме System.Net.MaibNh\ch требуются пространства имен System.IO и System.Net.Mime) показано, как использовать объект Stream в качестве прикрепленного файла и как указать М1МЕ-тип:
' VB
Dim m As MailMessage = New MailMessageO
Dim sr As Stream = New FileStream("C:\B00t.ini", FileMode.Open, FileAccess.Read) m.Attachments.Add(New Attachment(sr, "myfile.txt", MediaTypeNames.Application.Octet))
11 C#
MailMessage m = new MailMessageO;
Stream sr = new FileStream(@"C:\Boot.ini", FileMode.Open, FileAccess.Read);
m.Attachments.Add(new Attachment(sr, "myfile.txt", MediaTypeNames.Application.Octet));
Как показано в этом примере, при создании вложения из объекта Stream следует указать имя файла. В противном случае вложение будет названо общим именем, таким как «application_octect-stream.dat». Поскольку расширение файла будет неверным, пользователи не смогут открыть прикрепленный файл в соответствующем приложении.
Создание сообщений в формате HTML
Чтобы создать сообщение в формате HTML, укажите текст с разметкой HTML в свойстве MailMessage.Body и задайте атрибуту MailMessage.IsBodyHtml значение True, как показано в следующем примере:
' VB
Dim m As MailMessage = New MailMessage
m.From = New MailAddress("lance@contoso.com", "Lance Tucker") m.To.Add(New MailAddress("burke@contoso.com", "Burke Fewel")) m.Subject = "Testing HTML"
Указываем тело сообщения
m.Body = "<htmlxbody><h1>My Message</hlxbr>This is an HTML message. </bodyx/html>" m.IsBodyHtml = True
754 Электронная почта
Глава 15
Отправляем сообщение
Dim client As SmtpClient = New SmtpClient("smtp.contoso.com") client.Send(m)
// C#
MailMessage m = new MailMessageO;
m.From = new MailAddress("lance@contoso.com", "Lance Tucker"); m.To.Add(new MailAddress("burke@contoso.com", "Burke Fewel")); m.Subject = "Testing HTML";
// Указываем тело сообщения
m.Body = "<html><bodyxh1>My Message</h1><br>This is an HTML message. </bodyx/html>"; m.IsBodyHtml = true;
// Отправляем сообщение
SmtpClient client = new SmtpClient("smtp.contoso.com’) client.Send(m);
MailMessage.Subject всегда является простым текстом. MailMessage.Body можно определить как любую HTML-страницу. Однако большинство почтовых клиентов игнорирует секцию <head> и все клиентские сценарии, а также не скачивает изображения с Web-сайтов автоматически.
Встроить изображения в HTML-сообщение, чтобы они появлялись по щелчку пользователя (не требуя явной загрузки изображений) можно при помощи классов AltemateView и LinkedResource. Сначала создайте сообщение HTML, используя Alter-nateView, а затем добавьте изображения, используя LinkedResource, как показано в следующем фрагменте кода: ’ VB
Создаем тело HTML-сообщения
Ссылаемся на встроенные изображения, используя ID контента
Dim htmlBody As String = <html><body><h1>Picture</h1><br>” +
"<img src=""cid:Pic1""></body></html>"
Dim avHtml As AltemateView = AlternateView.CreateAlternateViewFromString (htmlBody, Nothing, MediaTypeNames.Text.Html)
Создаем объект LinkedResource для каждого встроенного изображения
Dim pici As LinkedResource = New LinkedResourceC'pic.jpg", MediaTypeNames.Image.Jpeg) pid.Contentld = "Pid"
avHtml. LinkedResources. Add(pid)
Создаем альтернативный вариант отображения для клиентов, не поддерживающих HTML
Dim textBody As String = "You must use an e-mail client that supports HTML messages"
Dim avText As AltemateView = AltemateView.CreateAlternateViewFromString
(textBody, Nothing, MediaTypeNames.Text.Plain)
Занятие 1
Создание почтового сообщения 755
' Добавляем альтернативные форматы отображения вместо использования MailMessage Body Dim m As MailMessage = New MailMessage
m.AlternateViews.Add(avHtm])
m.AlternateViews.Add(avText)
' Указываем адрес и отправляем сообщение
m.From = New MailAddress("lance@contoso.com”, "Lance Tucker")
m.To.Add(New MailAddressCjames@contoso.com", "James van Eaton"))
m.Subject = "A picture using alternate views"
Dim client As SmtpClient = New SmtpClient("smtp contoso.com") client Send(m)
// C#
// Создаем тело HTML-сообщения
// Ссылаемся на встроенные изображения, используя ID контента
string htmlBody = "<htmlxbodyXh1>Picture</h1><brximg src=\*‘cid: Pic1\"></ bodyx/html>";
AlternateView avHtml = AlternateView.CreateAlternateViewFromString
(htmlBody, null, MediaTypeNames.Text.Html);
// Создаем объект LinkedResource для каждого встроенного изображения LinkedResource pic1 = new LinkedResourceCpic.jpg", MediaTypeNames.Image.Jpeg); pic1.Contentld = "Pic1";
avHtml.LinkedResources.Add(pic1);
// Создаем альтернативный формат отображения для клиентов, не поддерживающих HTML
string textBody = "You must use an e-mail client that supports HTML messages";
AlternateView avText = AlternateView.CreateAlternateViewFromString (textBody, null, MediaTypeNames.Text.Plain);
II Добавляем альтернативные форматы отображения вместо использования MailMessage.Body MailMessage m = new MailMessageO;
m.Alte rnateViews.Add(avHtml);
m.Alte rnateViews.Add(avText);
// Указываем адрес и отправляем сообщение
m.From = new MailAddress("lance@contoso.com", "Lance Tucker");
m.To.Add(new MailAddress("james@contoso.com", "James van Eaton"));
m.Subject = "A picture using alternate views";
SmtpClient client = new SmtpClient("smtp.contoso.com");
client.Send(m);
Этот код создает сообщение HTML, показанное на рис. 15-1 (предполагается, что pic.jpg — это изображение, хранящееся в той же папке, что и сборка).
756 Электронная почта
Глава 15
Рис. 15-1. Встраивание изображений в почтовые сообщения при помощи
Alternate View и LinkedResource
Ссылки из тела HTML-сообщений на изображения, прикрепленные как связанные ресурсы, имеют вид cidzcontentlD и реализуются в тэге <img>. Затем укажите то же имя для свойства LinkedResource.ContentID. В предыдущем примере тело HTML-сообщения содержит тэг <img src=\"cid:Picl\">, а свойству LinkedResource.ContentID задано значение "Piel". Каждый объект LinkedResource должен иметь уникальный ContentID.
Практикум. Создание объекта MailMessage
Сейчас вы создадите объект MailMessage, основанный на данных, введенных пользователем в приложении Windows Forms. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
1.	При помощи Проводника Windows скопируйте из папки Chapter 15\Lessonl-Exercise 1 компакт-диска версию проекта для C# или Visual Basic в папку Му Documents\Visual Studio Projects\. Откройте этот проект.
2.	Добавьте в код ссылку на пространство имен System.Net.Mail.
3.	По щелчку кнопки Send на форме исполняющая среда вызывает метод sendButton Click. Теперь нужно добавить код для создания объекта MailMessage с использованием введенных пользователем данных:
VB
' Создаем объект MailMessage
Dim mm As MailMessage = New MailMessage
Занятие 1
Создание почтового сообщения
// C#
// Создаем объект MailMessage MailMessage mm = new MailMessageO;
4.	После этого укажите отправителя и получателя. Заметьте, что пользователь может не вводить отображаемые имена для адресов получателя и отправителя, так как конструктор MailAddress допускает значение Null для второй строки. В следующем фрагменте кода показано, как это сделать:
' VB
' Определяем отправителя и получателя
mm.From = New MailAddress(fromEmailAddress.Text, fromDisplayName.Text)
mm.To.Add(New MailAddress(toEmailAddress.Text, toDisplayName.Text))
// C#
// Определяем отправителя и получателя
mm.From = new MailAddress(fromEmailAddress.Text, fromDisplayName.Text);
mm.To.Add(new MailAddress(toEmailAddress.Text, toDisplayName.Text));
5.	Определите тему и тело сообщения, как показано в следующем фрагменте кода:
' VB
' Определяем тему и тело сообщения
mm.Subject = subjectTextBox.Text
mm. Body = bodyTextBox.Text
mm.IsBodyHtml = htmlRadioButton.Checked
// C#
// Определяем тему и тело сообщения
mm.Subject = subjectTextBox.Text;
mm.Body = bodyTextBox.Text;
mm.IsBodyHtml = htmlRadioButton.Checked;
6.	Соберите и запустите проект в режиме отладки. Ничего не вводя, щелкните кнопку Send. Исполняющая среда сгенерирует исключение ArgumentException. Заключите код в блок Try/Catch, чтобы перехватывать это исключение и выводить сообщение, как показано в следующем фрагменте кода:
’ VB
Try
Код метода опущен
Catch ex As ArgumentException
MessageBox.Show("You must type from and to e-mail addresses", "Invalid input", MessageBoxButtons.OK, MessageBoxIcon.Error) End Try
// C#
try
{
// Код метода опущен
}
758 Электронная почта
Глава 15
catch (ArgumentException)
{
MessageBox Show(”You must type from and to e-mail addresses",
"Invalid input", MessageBoxButtons.OK, MessageBoxIcon.Error); }
ПРИМЕЧАНИЕ Почему бы не проверить введенные данные?
В главе 3 рассказывалось о создании регулярных выражений для проверки введенных пользователем данных, там же приводились примеры с проверкой адресов электронной почты. Хотя в приведенном примере можно использовать регулярные выражения, это было бы избыточным. Объект MailMessage включает логику для проверки введенных данных, как показано на следующем шаге. В некоторых случаях в целях безопасности имеет смысл создать два уровня проверки. В этом же простом примере сложная проверка не требуется.
7.	Снова соберите и запустите проект в режиме отладки. Введите в поля адресов отправителя и получателя текст hello и щелкните кнопку Send. Если пользователь введет адрес в неверном формате, исполняющая среда сгенерирует исключение Format Exception. Добавьте блок Catch для перехвата исключения Format Exception и вывода сообщения об ошибке:
' VB
Catch ex As FormatException
MessageBox.Show("You must provide valid from and to e-mail addresses”,
"Invalid input", MessageBoxButtons.OK, MessageBoxIcon.Error)
// C#
catch (FormatException)
{
MessageBox.Show("You must provide valid from and to e-mail addresses", "Invalid input”, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
8.	Соберите и запустите проект. Заполните поля формы допустимыми данными и щелкните кнопку Send. Ничего не произойдет, так как вы еще не добавили код отправки сообщения. На занятии 2 вы завершите создание этого приложения.
Резюме
	Для отправки сообщения электронной почты создайте объект MailMessage, укажите отправителя и тело сообщения, затем добавьте получателей, альтернативные форматы отображения и прикрепите файлы. После этого создайте экземпляр класса SmtpClient и вызовите метод SmtpClient.Send или SmtpClient.SendAsync (см. занятие 2).
	Объект MailMessage включает конструкторы, позволяющие создавать простые сообщения, написав всего одну строку кода. Чтобы создать более сложные сообщения или сообщения с несколькими получателями, требуется дополнительный код.
	Чтобы прикрепить файл, создайте экземпляр класса Attachment и добавьте его в набор Mail Message. Attachments.
Занятие 1
Создание почтового сообщения 75g
 Чтобы создать сообщение HTML без изображений, нужно просто ввести теги HTML в тело сообщения и задать свойству MailMessage.IsBodyHtml значение True. Если нужно включить изображения, создайте объект AlternateView и добавьте для каждого изображения объект LinkedResource.
Закрепление материала
Ниже приведены вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Из каких источников можно прикреплять файл к сообщениям электронной почты? (Укажите все верные ответы.)
А. Локальная файловая система.
В. Объект Stream.
С. Web-сайт.
D. Входящее сообщение электронной почты.
2.	Что нужно сделать для отправки сообщения в формате HTML? (Укажите все верные ответы.)
А. В качестве значения свойства MailMessage. Body ввести текст HTML-сообщения.
В. В качестве значения свойства MailMessage.Head ввести заголовок HTML.
С. Задать свойству MailMessage.IsBodyHtml значение True.
D. В качестве значения свойства MailMessage.Subject ввести текст HTML-сообщения.
3.	Какие из тэгов HTML ссылаются на следующий связанный ресурс как на изображение?
VB
' Создаем объект LinkedResource для каждого встроенного изображения
Dim Ir As LinkedResource = New LinkedResou rceCmyPic.jpg", MediaTypeNames.Image.Jpeg) Ir.Contentld = "myPic"
II Ctt
// Создаем объект LinkedResource для каждого встроенного изображения LinkedResource lr = new LinkedResourceC’myPic.jpg", MediaTypeNames.Image.Jpeg); Ir.Contentld = "myPic";
A. <img src="cid:myPic">.
B. <img src="cid:myPic.jpg">.
C. <img src="myPic">.
D. <img src="myPic.jpg">.
4.	Вы хотите отправить HTML-сообщение, которое можно просматривать также в клиентах, не поддерживающих HTML. Какой класс следует использовать?
A. LinkedResource.
В. Attachment.
С. AltemateView.
D. SmtpClient.
760 Электронная почта
Глава 15
Занятие 2. Отправка почты
Часто отправка почтового сообщения оказывается простой задачей, требующей одной-двух строк кода. Однако, как и любые сетевые коммуникации, в некоторых случаях эта задача значительно усложняется. Если сервер не отвечает, нужно предоставить пользователям возможность продолжить ожидание или отменить передачу сообщения. Некоторые серверы требуют действительных учетных данных пользователя, так что может потребоваться предоставить имя пользователя и пароль. По возможности, следует использовать протокол SSL (Secure Sockets Layer) для защиты сообщения шифрованием.
Изучив материал этого занятия, вы сможете:
J настраивать SMTP-сервер и отправлять сообщение электронной почты;
при необходимости предоставлять SMTP-серверу имя пользователя и пароль для аутентификации;
J включать SSL для защиты шифрованием канала связи с SMTP-сервером;
J асинхронно отправлять сообщения, чтобы во время отправки приложение могло взаимодействовать с пользователем и выполнять другие задачи;
J перехватывать и обрабатывать различные исключения, которые могут быть сгенерированы при отправке сообщения.
Продолжительность занятия — 30 минут.
Отправка сообщений
Созданное сообщение, нужно отправить на сервер SMTP (Simple Message Transfer Protocol), который перешлет его получателю. В .NET Framework SMTP-сервер представлен классом SmtpClient. В большинстве случаев отправить сообщение довольно просто, как показано в следующем фрагменте кода (где smtp.contoso com — имя локального сервера SMTP):
• VB
Dim m As MailMessage = New MailMessage
("jane@contoso.com",
"ben@contoso.com",
Quarterly data report.",
"See the attached spreadsheet.")
Dim client As SmtpClient = New SmtpClient("smtp contoso.com") client.Send(m)
11 C#
MailMessage m = new MailMessage
("jane@northrup.org",
"ben@northrup. org",
"Quarterly data report.",
"See the attached spreadsheet.");
SmtpClient client = new SmtpClient("smtp.contoso.com");
client.Send(m);
Чтобы отправить сообщение, вызовите метод SmtpClient.Send.
Занятие 2
Отправка почты 75 -|
ПРИМЕЧАНИЕ Свойство SmtpCIient.PickupDirectoryLocation
Свойство SmtpClient.PickupDirectoryLocation предназначено для Web-приложений. Чтобы отправить сообщение через SMTP-сервер на основе IIS, укажите имя хоста сервера IIS, как это делается для любого другого SMTP-сервера. Чтобы указать локальный компьютер, введите local host или 127 0 0 1.
Пример из практики
Тони Нортроп (Топу Northrup)
Несколько лет назад я рекомендовал всем устанавливать IIS, включать службу SMTP и отправлять сообщения через локальный SMTP-сервер. Это эффективнее использования удаленного SMTP-сервера, так как локальный сервер для отправки сообщения может напрямую связываться с SMTP-серверами получателями. Кроме того, не нужно обрабатывать сетевые подключения.
Однако с тех пор, как организации стали бороться со спамом, такой подход стал ненадежным. Теперь многие компании блокируют IP-адреса, с которых приходят непрошенные сообщения. Даже если вы не спамер, сообщения с вашего локального SMTP-сервера могут блокироваться, если ваш IP-адрес почему-то попал в «черные списки». Ситуация осложняется тем, что в одни домены сообщения доходят, а в других — блокируются.
Кроме того, службы SMTP стали слишком сложными, чтобы рекомендовать всем создавать собственные почтовые серверы. Поэтому теперь для отправки сообщений я рекомендую использовать корпоративные почтовые серверы организаций или серверы провайдеров.
Обработка исключений, возникающих при отправке почты
Отправке почты может многое помешать. Например, почтовый сервер может быть недоступен, сервер может не принять учетные данные или определить, что адрес получателя недействителен. В каждом из этих случаев исполняющая среда сгенерирует исключение, и приложение должно быть готово к его перехвату.
При отправке сообщения приложение всегда должно быть готово к перехвату исключения SmtpException. Часто сообщения отклоняются из-за того, что не найден SMTP-сервер, сервер посчитал сообщение спамом или требует указать имя пользователя и пароль.
Также может возникнуть исключение Smtp Failed Recipient Exception, генерируемое в том случае, если SMTP-сервер отклоняет адрес получателя. Серверы SMTP сразу отклоняют письма для локальных получателей, адреса которых не существуют. Другими словами, если вы отправляете сообщение по адресу tony@contoso.com через SMTP-сервер Contoso, сервер отклонит сообщение, если адреса tony@contoso.com не существует (исполняющая среда сгенерирует исключение SmtpFailedRecipientExceptiori). Однако, если отправить сообщение на тот же адрес через другой SMTP-сервер, например Fabrikam, сервер не сможет сразу определить, что этот адрес недействителен. Следовательно, исполняющая среда не сгенерирует исключения.
В табл. 15-1 приведены сведения об этих и других исключениях, которые может сгенерировать исполняющая среда.
762 Электронная почта
Глава 15
Табл. 15-1. Исключения, возникающие при отправке почты
Ситуация	Исключение	Синхронная или асинхронная передача
Не определено имя хоста сервера	InvalidOperationException	Оба вида
Имя хоста сервера не найдено	SmtpException с встроенным WebException	Оба вида
Сообщение отправлено получателю на локальном почтовом сервере, но получатель не имеет почтового ящика	SmtpFailedRecipientException	Синхронная
Ваша учетная запись пользователя недействительна или возникли другие проблемы с передачей	SmtpException	Оба вида
При использовании SmtpClient.SendAsync недействительные получатели и некоторые другие события не вызывают генерацию исключения. Вместо этого исполняющая среда генерирует событие SmtpClient.SendCompleted. Если AsyncCompletedEventArgs.Error имеет значение, отличное от Null, возникла ошибка.
Настройка учетных данных
Чтобы уменьшить поток спама, все SMTP-серверы должны отклонять сообщения от неавторизованных пользователей, если получатели сообщения обслуживаются другим SMTP-сервером. Серверы SMTP, предоставленные провайдерами, обычно определяют, авторизован ли пользователь, проверив его IP-адрес. Если он входит в сеть провайдера, то может использовать SMTP-сервер.
Другие SMTP-серверы проверяют имя и пароль пользователя. Чтобы использовать сетевые учетные данные, задайте свойству SmtpClient. Use Defaultcredentials значение True. В качестве альтернативы можно задать свойству SmtpClient.Credentials значение CredentialCache.DefaultNetworkCredentials (из пространства имен System.Net), как показано в следующем фрагменте кода:
’ VB
Dim client As SmtpClient = New SmtpClient("smtp.contoso.com") client.Credentials = CredentialCache.DefaultNetworkCredentials
// C#
SmtpClient client = new SmtpClient("smtp.contoso.com");
client.Credentials = CredentialCache.DefaultNetworkCredentials;
Чтобы указать имя пользователя и пароль, создайте экземпляр класса Sy stem. Net. NetworkCredential и используйте его для определения SmtpClient.Credentials. В следующем примере показаны жестко запрограммированные учетные данные; в реальности же всегда следует предлагать пользователю ввести их.
Занятие 2
Отправка почты 763
’ VB
Dim client As SmtpClient = New SmtpClient("smtp.contoso.com”) client.Credentials = New NetworkCredentialC'user", "password”)
// Ctt
SmtpClient client = new SmtpClientCsmtp.contoso.com");
client.Credentials = new NetworkCredentialC'user", "password");
Настройка SSL
Еще одно важное для защиты свойство — SmtpClient. EnableSsl. Если оно имеет значение True, исполняющая среда будет шифровать канал связи с SMTP, используя SSL. Не все SMTP-серверы поддерживают SSL, но это свойство всегда следует включать, если поддержка доступна.
ПРИМЕЧАНИЕ .NET 2.0
Свойство SmtpClient.EnableSsl впервые появилось в .NET 2.0.
Асинхронная отправка сообщений
Отправка почтового сообщения часто занимает меньше секунды. Но иногда SMTP-сервер может медленно работать или вовсе не отвечать на запросы, заставляя приложение ожидать в течение времени, указанного свойством SmtpClient. Timeout. Пока приложение ожидает ответа то сервера (по умолчанию до 100 секунд), оно не будет реагировать на действия пользователя, а курсор примет вид песочных часов. Обычно в этих случаях пользователи принудительно завершают работу приложений.
К счастью, можно отправлять почтовые сообщения асинхронно, позволяя пользователю вот время отправки выполнять другие действия. Пользователю можно даже предоставить возможность отмены передачи сообщения. Для асинхронной передачи сообщения выполните следующее:
1.	Создайте метод, отвечающий на событие SmtpClient.SendCompleted. Этот метод позволяет определить, была ли передача завершена успешно, не была завершена успешно или была отменена.
2.	Добавьте обработчик события SmtpClient.SendCompleted.
3.	Вызовите метод SmtpClient.SendAsync.
4.	Можно также предоставить пользователю возможность отменить передачу, вызвав метод SmtpClient.SendAsyncCancel.
Например, следующий метод отвечает на событие SmtpClient.SendCompleted (требует наличия пространства имен System.ComponentModely.
VB
Sub sc_SendCompleted(ByVal sender As Object, ByVai e As AsyncCompletedEventArgs) If e.Cancelled Then
Console.WriteLine("Message cancelled")
Else
If Not (e.Error Is Nothing) Then
Console WriteLineC'Error; " + e.Error.ToString)
Else
764 Электронная почта
Глава 15
Console.WriteLine("Message sent") End If End If End Sub
11 C#
void sc_SendCompleted(object sender, AsyncCompletedEventArgs e) {
if (e.Cancelled)
Console.WriteLine("Message cancelled );
else if (e.Error != null)
Console.WriteLine(”Error: " + e. Error.ToStringO); else
Console.WriteLineC"Message sent");
}
Следующий фрагмент кода создает объект SmtpClient, добавляет обработчик события, вызывает асинхронную отправку, а затем сразу же отменяет ее. В реальном приложении инициировать отмену должен пользователь. В этом примере считается, что объект MailMessage с именем тт уже существует.
' VB
SmtpClient sc = new SmtpClient("server_name");
Добавляем обработчик события
AddHandler sc.SendCompleted, AddressOf sc_SendCompleted
Асинхронно отправляем сообщение sc.SendAsync(mm, Nothing)
Отменяем отправку sc.SendAsyncCancel()
// C# sc = new SmtpClient("server_name");
// Добавляем обработчик события
sc.SendCompleted += new SendCompletedEventHandler(sc_SendCompleted);
г // Асинхронно отправляем сообщение sc.SendAsync(mm, null);
// Отменяем отправку sc.SendAsyncCancel();
SmtpClient. SendAsync принимает два параметра: объект MailMessage, который нужно отправить, и универсальный объект Object. В качестве второго параметра можно указать значение Null или любой другой объект; этот параметр нужен только вам. .NET
Занятие 2
Отправка почты 755
Framework просто передает его обработчику события. Если асинхронно передается сразу несколько сообщений, при помощи второго параметра можно определить сообщение, сгенерировавшее событие.
Практикум. Отправка почтового сообщения
Сейчас вы завершите разработку приложения, созданного на занятии 1, отправив почтовое сообщение. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
Упражнение 1. Создание объекта SmtpClient
для отправки объекта MailMessage
В этом упражнении вы расширите возможности существующего приложения, создав объект SmtpClient и добавив код для отправки сообщений.
1.	Скопируйте в папку Му Documents\Visual Studio Projects\ папку Chapter 15\Lesson2-Exercisel с прилагаемого компакт-диска и откройте версию проекта для C# или Visual Basic. Можно также продолжить работу с приложением, созданным на занятии 1, добавив новый код сразу после определения почтовых сообщений.
2.	Добавьте в код пространство имен System.Net (потребуется класс System.Net.Network-Credential).
3.	Напишите код, создающий экземпляр класса SmtpClient, и при необходимости включите SSL и настройте учетные данные, как показано ниже.
' VB
’ Настраиваем почтовый сервер
Dim sc As SmtpClient = New SmtpClient(serverTextBox.Text)
sc.EnableSsl = sslCheckBox.Checked
If Not String.IsNullOrEmpty(usernameTextBox.Text) Then
sc.Credentials = New NetworkCredential(usernameTextBox.Text, passwordTextBox.Text) End If
// C#
// Настраиваем почтовый сервер
SmtpClient sc = new SmtpClient(serverTextBox.Text);
sc.EnableSsl = sslCheckBox.Checked;
if (IString.IsNullOrEmpty(usernameTextBox.Text))
sc.Credentials = new NetworkCredential(usernameTextBox.Text, passwordTextBox.Text);
4.	Добавьте код для отправки почтового сообщения и уведомите пользователя о том, что сообщение успешно отправлено, как показано в следующем примере:
’ VB
Отправляем сообщение и уведомляем пользователя об успешной отправке
sc.Send(mm)
MessageBox.Show("Message sent successfully. ",
"Success", MessageBoxButtons.OK, MessagePoxIcon.Information)
t:
766 Электронная почта
Глава 15
// C#
// Отправляем сообщение и уведомляем пользователя об успешной отправке
sc.Send(mm);
MessageBox.Show("Message sent successfully.",
'Success", MessageBoxButtons.OK, MessageBoxIcon.Information);
5.	Соберите и запустите проект в режиме отладки. Введите адреса отправителя и получателя в поля From и То, а затем щелкните кнопку Send, не вводя имя сервера. Исполняющая среда сгенерирует исключение InvalidOperationException. Создайте блок Catch для перехвата исключения InvalidOperationException и вывода сообщения, как показано в следующем примере:
VB
Catch ex As InvalidOperationException
MessageBox.Show("Please provide a server name.", "No SMTP server provided", MessageBoxButtons.OK, MessageBoxIcon.Error)
// C#
catch (InvalidOperationException)
{
MessageBox.Show("Please provide a server name.",
"No SMTP server provided’, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
6.	Снова запустите проект. Введите недействительное имя сервера и щелкните кнопку Send. Исполняющая среда сгенерирует исключение SmtpException. Просмотрите исключение при помощи отладчика. Объект SmtpException не предоставляет понятного описания проблемы. Однако внутренний объект исключения является экземпляром класса WebException и в точности описывает возникшую проблему — недопустимое DNS-имя. Добавьте код для перехвата исключения SmtpException и вывода сообщения из базового типа исключений, как показано в следующем фрагменте кода:
' VB
Catch ex As SmtpException
Недопустимые имена хостов приводят к генерации исключения WebException InnerException,
предоставляющего более подробную информацию об ошибке
Dim inner As Exception = ex.GetBaseException
MessageBox.Show("Could nat send message " + inner Message,
"Problem sending message", MessageBoxButtons.OK, MessageBoxIcon Error)
// C#
catch (SmtpException ex)
{
// Недействительные имена хостов приводят к генерации исключения WebException InnerException,
/I предоставляющего более подробную информацию об ошибке
Exception inner = ex.GetBaseException();
MessageBox.Show("Could not send message: " + inner Message,
"Problem sending message , MessageBoxButtons.OK. MessageBoxIcon Error);
}
Занятие 2
Отправка почты 767
7.	Снова запустите проект. Введите имя SMTP-сервера. В поле То введите недействительный адрес электронной почты из локального домена. Затем щелкните Send. Исполняющая среда должна сгенерировать исключение SmtpFailedRecipientException (хотя поведение SMTP-сервера может меняться). Добавьте код для перехвата исключения SmtpFailedRecipientException и вывода сообщения:
' VB
Catch ex As SmtpFailedRecipientException
MessageBox.Show("The mail server says that there is no mailbox for " +
toEmailAddress.Text + "Invalid recipient", MessageBoxButtons.OK, MessageBoxIcon.Error)
// C#
catch (SmtpFailedRecipientException)
{
MessageBox.Show("The mail server says that there is no mailbox for " + toEmailAddress.Text + "Invalid recipient", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
8.	Снова запустите проект и отправьте почтовое сообщение, чтобы проверить работу приложения.
Упражнение 2. Асинхронная отправка почтовых сообщений
В этом упражнении вы измените созданное ранее приложение, чтобы позволить пользователям отменять отправку сообщения. Для этого вы замените SmtpClient.Send на SmtpClient.SendAsync, измените метку кнопки Send на Cancel и добавите код, выполняемый по щелчку кнопки Cancel.
1.	Скопируйте в папку Му Documents\Visual Studio Projects\ версию проекта для C# или Visual Basic из папки Chapter 15\Lesson2-Exercise2 с прилагаемого компакт-диска. Откройте проект. Можно также продолжить работу с приложением, созданным при выполнении предыдущего упражнения.
2.	Закомментируйте строку кода с методом SmtpClient.Send.
3.	Ответить нужно после отправки сообщения, поэтому добавьте обработчик события SmtpClient.SendCompleted. Затем вызовите метод SmtpClient.SendAsync и передайте объект MailMessage. Удалите код вывода сообщения об успешной передаче сообщения, так как исполняющая среда немедленно продолжит работу, не ожидая доставки сообщения. Это делается так:
' VB
' Отправьте сообщение и сообщите пользователю об этом
' sc.Send(mm)
AddHandler sc.SendCompleted, AddressOf sc_SendCompleted sc.SendAsync(mm, Nothing)
// C#
11 Отправьте сообщение и сообщите пользователю об этом
Ц	sc.Send(mm);
768 Электронная почта
Глава 15
sc.SendCompleted += new SendCompletedEventHandler(sc_SendCompleted); sc.SendAsync(mm, null);
4.	Чтобы позволить нескольким методам обращаться к переменной SmtpClient, перенесите объявление переменной на уровень класса. Однако все равно нужно определить переменную, задав ей значение serverTextBox. Text в методе sendButton_Click.
5.	После начала отправки сообщения нужно дать пользователю возможность отменить передачу. Можно добавить в приложение вторую кнопку Cancel, а можно просто изменить подпись кнопки Send на Cancel. В любом случае, если пользователь щелкает кнопку Cancel, нужно вызвать метод SmtpClient. SendAsyncCancel В следующем примере показано, как это можно выполнить, добавив оператор If в метод sendButton_Click, определяющий состояние (Send или Cancel) кнопки в момент ее щелчка:
' VB
If sendButton.Text = "Send" Then
... код для ясности опущен...
Отправляем сообщение и сообщаем пользователю об этом
' sc.Send(mm)
AddHandler sc.SendCompleted, AddressOf sc_SendCompleted
sc.SendAsync(mm, Nothing)
sendButton.Text = "Cancel”
Else
sc.SendAsyncCancel()
End If
// C#
if (sendButton.Text == "Send")
{
// .. код для ясности опущен...
// Отправляем сообщение и сообщаем пользователю об этом
//	sc.Send(mm);
sc.SendCompleted += new SendCompletedEventHandler(sc_SendCompleted);
sc.SendAsync(mm, null);
sendButton.Text = "Cancel";
}
else
{
sc.SendAsyncCancel();
}
6.	Создайте метод, отвечающий на событие SmtpClient.SendCompleted. На основе переменной с именем sc Visual Studio создает метод с именем scSendCompleted. В методе нужно выполнить следующие задачи:
□	если SendAsync был отменен, вывести сообщение, подтверждающее отмену;
□	если произошла ошибка, вывести сообщение об ошибке;
□	если передача успешно завершена, сообщить об этом пользователю.
Занятие 2
Отправка почты ygg
Как это сделать, показано в следующем фрагменте кода. Если вы используете Visual Basic, добавьте ссылку на пространство имен System.ComponentModel.
' VB
Sub sc_SendCompleted(ByVal sender As Object, ByVai e As AsyncCompletedEventArgs) If e.Cancelled Then
MessageBox.Show( "Message cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Error) Else
If Not (e.Error Is Nothing) Then
MessageBox.Show("Error: ” + e.Error.ToString, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
Else
MessageBox.Show("Message sent successfully.", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information) End If End If sendButton.Text = "Send" End Sub
// C# void sc_SendCompleted(object sender, AsyncCompletedEventArgs e) {
if (e.Cancelled)
MessageBox.Show("Message cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Error); else if(e.Error != null)
MessageBox.Show("Error: " + e. Error.ToStringO,
"Error", MessageBoxButtons.OK, MessageBoxIcon.Error); else
MessageBox.Show("Message sent successfully.", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); sendButton.Text = "Send";
}
7.	Соберите и запустите приложение. Убедитесь в успешности отправки сообщения.
8.	Затем отправьте сообщение, но сразу же щелкните кнопку Cancel. Если SMTP-сервер отвечает очень быстро, кнопка Cancel также может исчезнуть очень быстро. Убедитесь, что приложение отменяет отправку и сообщает об этом пользователю.
9.	Убедитесь, что приложение корректно отвечает на ввод неверного имени сервера, недействительных имен и паролей. Обратите внимание на то, как обрабатывается неверное имя сервера: событием SmtpClient.SendCompleted или методом sendButton_Click.
Резюме
	Чтобы отправить сообщение, создайте экземпляр класса SmtpClient. Настройте имя SMTP-сервера, а затем вызовите метод SmtpClient.Send.
Если вызвать SmtpClient.Send, не определив имя сервера, исполняющая среда сгенерирует исключение InvalidOperationException. Если имя определено, но сервер не най
TIQ Электронная почта
Глава 15
ден, исполняющая среда сгенерирует исключение SmtpException с внутренним исклю чением WebException. Если SMTP-сервер сообщает, что адрес получателя недействителен, исполняющая среда сгенерирует исключение SmtpFailedRecipientException. Все остальные проблемы с отправкой почты приводят к генерации исключения SmtpEx-сept ion.п Чтобы использовать сетевые учетные данные по умолчанию, задайте свойству SmtpClient. UseDefaultCredentials значение True. Чтобы указать имя пользователя и пароль, создайте экземпляр класса System. Net. NetworkCredential и используйте его для определения SmtpClient. Credentials.
	Чтобы использовать SSL-шифрование для защиты канала связи с SMTP-сервером, задайте свойству SmtpClient.EnableSsl значение True. Не все SMTP-серверы поддерживают SSL.
	Для асинхронной отправки сообщения сначала создайте метод-обработчик события SmtpClient .SendCompleted. Затем добавьте обработчик события SmtpClient.SendCompleted и вызовите метод SmtpClient.SendAsync. Чтобы отменить асинхронную передачу сообщения до ее завершения, можно вызвать метод SmtpClient.SendAsyncCancel.
Закрепление материала
Ниже приведены вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Какой метод нужно вызвать, чтобы отправить сообщение электронной почты и продолжить работу не раньше, чем отправка завершится?
A. MailMessage.Send.
В. SmtpClient.Send.
С. SmtpClient.SendAsync.
D. MailMessage.SendAsync.
2.	Вам нужно программно отправить почтовые сообщения. Компьютер, на котором работает сборка, также выполняет функции SMTP-сервера. Какое из следующих значений может иметь свойство SmtpClient.Hosfl (Укажите все верные ответы.) A. self.
В. 10.1.1.1.
С. localhost.
D. 127.0.0.1.
3.	Исключения какого типа будет генерировать исполняющая среда, если SMTP-сервер отклоняет адрес получателя?
A. SmtpFailedRecipientException.
В. SmtpFailedRecipientsException.
С. SmtpException.
D. SmtpClientException.
4.	Вам нужно отправить почтовое сообщение на SMTP-сервер, шифруя данные при передаче по каналу связи. Какое свойство нужно изменить для этого?
A. SmtpClient.Credentials.
В. SmtpClient.DeliveryMethod.
С. SmtpClient.Port.
D. SmtpClient.EnableSsl.
Лабораторная работа 771
Закрепление материала главы
Для закрепления знаний и навыков, полученных при изучении этой главы, выполните следующее:
	изучите резюме главы;
	повторите основные термины, использованные в этой главе;
	выполните задания лабораторной работы по темам этой главы;
	выполните предложенные упражнения;
	пройдите пробный экзамен.
Резюме главы
	Для создания почтового сообщения используйте класс System.Net.MailMessage. Этот класс поддерживает простой текст, HTML, сообщения с несколькими форматами отображения, различные типы кодировки и прикрепленные файлы.
	Чтобы отправить сообщение, используйте класс System.Net.SmtpClient. Этот класс поддерживает SMTP-серверы, принимающие анонимные подключения, серверы, требующие аутентификации, и серверы, поддерживающие SSL-шифрование. Кроме того, можно отправлять сообщения асинхронно, чтобы пользователи могли отменить передачу сообщения до ее завершения.
Основные термины
	Multipurpose Internet Mail Extensions (MIME);
	Secure Sockets Layer (SSL);
	Simple Message Transfer Protocol (SMTP).
Лабораторная работа. Добавление функций отправки почты
Сейчас вы примените на практике знания и навыки, полученные при изучении этой главы. Ответы на вопросы см. в разделе «Ответы» в конце книги.
Вы — разработчик приложений в компании Contoso, Inc. Последние два года вы разрабатывали и поддерживали внутреннюю систему управления связями с клиентами. Служащие из отдела продаж попросили добавить функцию отправки почтовых сообщений клиентам перед изменением сведений об их учетных записях. Руководитель поручил вам опросить ответственных работников, а затем ответить на его вопросы по поводу проекта приложения.
	Менеджер по продажам
«Для успешной работы БД клиентов должна содержаться в порядке. Порой кто-то неверно вводил адрес или телефонный номер, в результате заказы доставлялись не туда, клиенты были недовольны, а мы терпели убытки. Мы хотим, чтобы клиентам автоматически отправлялись сообщения, когда мы вносим изменения в их контакт
772 Электронная почта
Глава 15
ную информацию. Если внесена неправильная информация, клиент сможет сооб щить об этом. Я думаю, нужно предоставить клиентам возможность ответить по электронной почте, перейти на наш сайт по ссылке или позвонить нам. Предполагается, что после этого ошибки будут возникать реже».
 Директор по безопасности
«Я понимаю необходимость подтверждения изменений в контактной информации, но мы должны учитывать возможный риск. Отправка конфиденциальной информации по электронной почте — не очень хорошая идея. Конечно, вы можете шифровать данные, которые передаются SMTP-серверу, но после доставки данных вы теряете контроль над ними. Сообщения часто передаются от сервера к серверу, и большинство подключений не защищено. Учитывая, что передаваться будет только контактная информация, я не против, но, пожалуйста, уделимте максимум внимания безопасности».
Ответьте на следующие вопросы руководства.
1.	Какие классы .NET Framework используются для передачи почтовых сообщений?
2.	Как можно обрабатывать ответы от клиентов?
3.	Как можно защитить данные в почтовых сообщениях?
Рекомендуемые упражнения
Чтобы лучше проработать темы экзамена, рассмотренные в этой главе, выполните следующие упражнения.
Отправка почтового сообщения на SMTP-сервер из приложения .NET Framework
Выполните упражнения 1 и 2. Если хотите лучше изучить работу с прикрепленными файлами, выполните упражнения 3-5. Чтобы изучить применение SSL, выполните также упражнение 6.
	Упражнение 1 Используя созданное на занятиях этой главы приложение, попробуйте отправить сообщения через разные SMTP-серверы. Посмотрите, как отвечают различные серверы. Чтобы найти SMTP-сервер домена, из командной строки выполните nslookup -type=mx domain (например, nslookup -type=mx contoso.com).
	Упражнение 2 Измените созданное приложение так, чтобы позволить пользователям прикреплять файлы.
	Упражнение 3 Попробуйте отправлять прикрепленные файлы, увеличивая их размер, пока SMTP-сервер не перестанет их принимать. Проверьте несколько SMTP-серверов. Обратите внимание на максимальный размер файлов.
	Упражнение 4 Прикрепив к сообщению текстовый файл, изменяйте его расширение на .txt, .jpg, .bat, .cmd, .dll и .exe. Проверьте, какие файлы почтовые серверы пропускают и какие можно просматривать в почтовых клиентах (например, в Microsoft Outlook).
	Упражнение 5 При выполнении упражнений главы 6 вы написали приложение, создающее диаграмму. Добавьте в приложение кнопку, по щелчку которой эта диаграмма будет прикрепляться к почтовому сообщению.
Пробный экзамен 773
	Упражнение 6 При помощи анализатора протокола (его также называют сниффером), например Network Monitor, захватите трафик, создаваемый при использовании стандартного протокола SMTP, стандартного протокола SMTP с учетными данными пользователя и протокола SMTP с шифрованием SSL. Просмотрите сетевые пакеты и определите, сможет ли взломщик получить доступ к сети, если перехватит сообщение и учетные данные пользователя.
Пробный экзамен
На прилагаемом компакт-диске содержится пробный экзамен, обеспечивающий широкие возможности по самоподготовке. Например, можно проверить усвоение тем этой главы либо всех тем экзамена 70-536. Можно сымитировать настоящий экзамен либо выбрать режим обучения, когда после каждого вопроса приводятся правильные ответы с пояснениями.
Пробный экзамен
Подробнее о пробном экзамене рассказано во введении.
ГЛАВА 16
Глобализация
>
Занятие 1. Использование информации о культуре	775
Занятие 2. Создание собственной культуры	789
В .NET Framework 2.0 включен невероятно полезный набор инструментов, позволяющий создавать приложения, приспособленные для работы в географически удаленных друг от друга областях с разными региональными стандартами. Так, можно создать приложения, работающие одинаково в Японии и в Великобритании без внесения изменений в код. Для этого используется пространство имен System.Globalization.
Темы экзамена:
	Изменение формата данных на основе информации о культуре (см. пространство имен System.Globalization):
□	доступ к сведениям о культуре и регионе из приложений .NET Framework;
□	изменение формата даты и времени в соответствии с культурой;
□	изменение формата чисел в соответствии с культурой;
□	выполнение операций сравнения строк, зависящих от культуры;
□	создание собственной культуры на основе существующих классов культуры и региона.
Прежде всего
Чтобы освоить материал этой главы, необходимо иметь навыки работы с Microsoft Visual Basic или C# и уметь:
	создавать консольные приложения в Visual Studio при помощи Visual Basic или С#;
	добавлять в проект ссылки на системные библиотеки классов.
Занятие 1
Использование информации о культуре 775
Занятие 1. Использование информации о культуре
Географическая область распространения приложений постоянно расширяется. Вместе с этим увеличивается и цена незнания того, кто пользуется приложением. Решать проблемы задним числом всегда дороже, чем включать функции для их предотвращения, и создание приложений для различных географических областей в этом отношении не является исключением. В .NET Framework включено пространство имен Sy stem.Globalization, позволяющее разработчикам решать подобные вопросы.
Изучив материал этого занятия, вы сможете:
J определять культуру пользователя;
J выполнять действия, зависящие от текущей культуры;
J изменять текущую культуру.
Продолжительность занятия — 20 минут.
Класс Cultureinfo
Одним из ключевых инструментов для получения и обработки информации о контексте культуры, в котором запущено приложение, является класс Culture Info. Этот класс предоставляет доступ к информации, специфичной для культуры, такой как формат чисел и дат. В более широком плане он представляет имя культуры, систему записи, календарь, язык и его варианты, страну и регион, а также предоставляет методы для обработки всех этих сведений. Основным применением класса Cultureinfo является управление следующими аспектами:
	сравнение строк;
	сравнение чисел;
	сравнение дат и форматов;
	использование и получение ресурсов.
ВАЖНО! Все культуры представлены в формате «нейтральная культура — точно определенная культура»
Культуры «zh-CHS» (упрощенный китайский) и «zh-СНТ» (традиционный китайский) — нейтральные, а следовательно, являются исключениями из стандартного формата представления культуры, в котором за именем нейтральной культуры стоит дефис и имя точно определенной культуры.
Как правило, культуры разделяются по трем категориям: инвариантные, нейтральные и точно определенные. В следующем списке приведены различия этих категорий:
	Инвариантная культура
Эта культура не зависит от текущей культуры пользователя. Она предназначена для использования в качестве культуры по умолчанию, а также для унификации параметров. В качестве примера, в котором желательно использовать эту категорию, можно привести приложения с жестко определенной датой окончания испытательного
776
Глобализация
Глава 16
периода. Инвариантная культура позволит проверять наступление указанной даты независимо от формата культуры, что значительно упрощает задачу сравнения дат Инвариантная культура как таковая не основана на английском языке, но связана с ним теснее, чем с любой другой культурой. Хотя можно подумать, что инвариантную культуру следует использовать для любого сравнения, а точно определенные сравнения культур можно игнорировать, это было бы большой ошибкой. Злоупотребляя инвариантной культурой, можно получить множество синтаксических и смысловых ошибок.
	Нейтральная культура
Примерами нейтральных культур являются английская (еп), французская (fr) и испанская (sp). Нейтральная культура связана с языком, но не со странами и регионами. Например, английский язык, на котором говорят в Англии, отличается от английского в США. То же самое с испанским языком в Мексике и в Испании. Как упоминалось ранее, нейтральная культура обозначается в классе Culture Info двумя первыми символами. Если указаны только две буквы, они будут относиться к классу Neutral. Как и в случае с инвариантной культурой, может возникнуть желание использовать везде нейтральную культуру, но по возможности этого следует избегать по тем же причинам, что и для инвариантной культуры. Язык, на котором говорят в различных странах, с которыми можно связать нейтральную культуру, почти наверняка будет заметно отличаться. В действительности, таких различий можно найти довольно много. Следовательно, неоправданное использование нейтральных культур может привести к применению некорректного или несоответствующего языка.
	Точно определенная культура
Это наиболее точная категория культур. Обозначается она нейтральной культурой и аббревиатурой точно определенной культуры, отделенной дефисом. Например, в обозначениях «fr-FR» и «еп-US» первые две буквы fr и еп представляют нейтральную культуру (французскую и английскую), a FR и US представляют точно определенную культуру (Франция и США). Если возможно, всегда следует использовать точно определенные культуры.
Для определения текущей культуры пользователя используйте свойство CurrentCulture свойства CurrentThread выполняющегося потока, как показано в следующем примере:
• VB
Dim UsersCulture As Cultureinfo = Thread.CurrentThread.CurrentCulture
Console.WriteLineC"The current culture of this application is : "
+ UsersCulture.Name)
Console.WriteLine("The Display Name of this application is : "
+ UsersCulture.DisplayName)
Console.Writel_ine("The Native Name of this application is : "
+ UsersCulture.NativeName)
Console.WriteLineC"The ISO Abbreviation of this application is : "
+ UsersCulture.TwoLetterlSOLanguageName)
// C#
Cultureinfo UsersCulture = Thread.CurrentThread.CurrentCulture;
Console.WriteLineC"The current culture of this application is : "
+ UsersCulture.Name);
Console.WriteLineC'The Display Name of this application is : "
Занятие 1
Использование информации о культуре 777
».	+ UsersCulture. DisplayName);
Console.WriteLineC"The Native Name of this application is : "
1	+ UsersCulture.NativeName);
Console WriteLineC"The ISO Abbreviation of this application is : "
+ UsersCulture.TwoLetterlSOLanguageName);
Если скомпилировать и запустить это приложение, можно получить результаты, аналогичные показанным на рис. 16-1 (могут изменяться в зависимости от конфигурации компьютера).
Рис. 16-1. Вывод информации о текущей культуре
Здесь одним из важных аспектов является способ обработки строк. Если в приложении жестко определить многие значения, а не использовать форматирующие объекты, различия в культурах не будут видны. Рассмотрим следующие примеры:
' VB
tbSalary.Text = "$100,000.00"
// C#
tbSalary.Text = "$100,000.00";
Если изменить текущую культуру (CurrentCulture) на, допустим, японскую, после чего перезапустить приложение, можно ожидать появления символа иены, японской денежной единицы. Однако этого не произойдет. Будет отображаться «$100,000.00». Это может показаться противоречащим тому, о чем говорилось выше, — изменение текущей культуры предполагает изменение форматов. Именно это и происходит — форматы изменяются. Однако, если используются не форматирование, а жестко определенные значения, исполняющая среда никаким образом не может узнать, что эти значения нужно изменить.
Если в этом примере использовать форматирование, изменение текущей культуры приведет к изменению всех связанных с ней значений, например tbSalary:
' VB
tbSalary.Text = Format(100000, "Currency”)
// C#
tbSalary.Text = (100000).ToStringC’C”);
778
Глобализация
Глава 16
Важно также обратить внимание на свойство CurrentUICulture класса Cultureinfo. Хотя часто это свойство имеет то же значение, что и свойство CurrentCulture класса Cultureinfo, они могут и различаться. Таким образом, для вычислений и внутренних операций может использоваться одна культура, а для отображения информации — другая. К свойству CurrentUICulture приложения можно обратиться через свойство CurrentUICulture класса CurrentThread.
• VB
Dim UsersCulture As Cultureinfo = Thread.CurrentThread.CurrentUICulture Console.WriteLineC"The current UI culture of this application is :
+ UsersCulture.Name)
11 C#
Cultureinfo UsersCulture = Thread.CurrentThread.CurrentUICulture;
Console.WriteLineC"The current UI culture of this application is :
+ UsersCulture.Name);
ПРИМЕЧАНИЕ Свойство CurrentUICulture должно определяться при запуске приложения
Свойство CurrentCulture можно изменять в любой момент во время выполнения. Но не свойство CurrentUICulture. Для корректного использования свойство CurrentUICulture должно быть определено в начале работы приложения (лучше всего в методе Main) или в конструкторе формы. Если поступить иначе, последствия будут непредсказуемыми.
Изменение текущей культуры аналогично получению сведений о ней. CurrentCulture — это свойство свойства CurrentThread класса Thread. Следовательно, для изменения или установки текущей культуры нужно просто указать другое значение свойства CurrentCulture, как показано в следующем фрагменте кода:
' VB
Dim UsersCulture As Cultureinfo = Thread.CurrentThread.CurrentCulture
Console.WriteLineC"The current culture of this application is :
& UsersCulture.Name)
'Изменить на «испанский для Венесуэлы»
Thread.CurrentThread.CurrentCulture = New Culturelnfo("es-VE")
Console.WriteLineC"The current culture of this application is : " _
& Th read.Cu r rentTh read.Cu r rentCultu re.Name)
11 C#
Cultureinfo UsersCulture = Thread.CurrentThread.CurrentCulture;
Console.WriteLineC"The current culture of this application is :
+ UsersCulture.Name);
// Изменить на «испанский для Венесуэлы»
Thread.CurrentThread.CurrentCulture = new Culturelnfo("es-VE”);
Console.WriteLineC"The current culture of this application is :
+ Th read.Cu r rentTh read.Cu r rentCultи re);
Занятие 1
Использование информации о культуре 77g
ПРИМЕЧАНИЕ Неожиданное поведение метода ClearCacheData
Метод ClearCacheData класса Cultureinfo может вести себя немного странно. Он реализован как метод экземпляра и не имеет статического эквивалента. Однако этот метод применим к классу, совместно используемому всеми экземплярами класса Cultureinfo. Можно подумать, что «совместное использование всеми экземплярами класса» работает как Static или Shared. В действительности этот метод так себя и ведет. Он не влияет на отдельные экземпляры, что наводит на мысли о том, что этот метод должен быть реализован как свойство Static или Shared.
Если запустить предыдущий фрагмент кода, можно увидеть, что свойство CurrentCulture сначала имеет значение en-US, которое затем изменяется на es-VE. Выходные данные этого кода должны быть аналогичны показанным на рис. 16-2.
Рис. 16-2. Изменение текущей культуры с en-US (английский, США) на es-VE (испанский, Венесуэла)
Класс Cultureinfo также предоставляет механизм получения сведений о культуре пользователя — метод GetCultures. Этот метод принимает один параметры типа CultureTypes.
Перечислимое CultureTypes
CultureTypes — перечислимое, помеченное атрибутом FlagsAttribute, что позволяет пользователям указывать несколько значений.
Табл. 16-1. Члены перечислимого CultureTypes
Имя	Описание
AllCultures	Как можно понять из имени, относится ко всем доступным культурам
FrameworkCultures	Нейтральные и точно определенные культуры, включенные в .NET Framework (новинка .NET Framework 2.0)
InstalledWin32Cultures NeutralCultures	Включает все культуры, установленные в операционной системе Включает культуры, связанные с языком, но не с конкретной страной/регионом
780 Глобализация
Глава 16
Табл. 16-1. (окончание)
Имя	Описание
Replacementcultures	Включает созданные пользователем культуры, заменяющие культуры, поставляемые с .NET Framework (впервые появился в .NET Framework 2.0)
SpecificCultures	Включает культуры, специфичные для страны/региона
UserCustomCulture	Включает культуры, созданные пользователем (впервые появился в NET Framework 2.0)
WindowsOnlyCultures	Включает культуры, установленные в операционной системе, но не в .NET Framework. Значения WindowsOnlyCultures и FrameworkCultures взаимоисключающие (впервые появился в .NET Framework 2.0)
Метод GetCultures класса Cultureinfo можно вызывать, передавая значения, перечисленные в табл. 16-1. Приведем пример использования метода GetCultures с указанием значения SpecificCultures перечислимого CultureTypes'.
‘ VB
For Each UsersCulture As Cultureinfo In
Cultureinfo.GetCultures(CultureTypes.SpecificCultures)
Console.WriteLine("Culture: " & UsersCulture.Name)
Next
//c#	’	'	*
foreach (Cultureinfo UsersCulture in
Cultureinfo.GetCultures(CultureTypes.SpecificCultures))
{
Console.WriteLine("Culture: ” + UsersCulture.Name),
}
Класс Regioninfo
Хотя класс Cultureinfo предоставляет весьма подробную информацию о культуре, иногда требуются еще более подробные сведения. Одним из средств получения более подробных сведений является класс Regioninfo. Короче, класс Regioninfo предоставляет информацию, специфичную для определенной страны или региона.
Класс Cultureinfo имеет два типа свойств, которые можно использовать совместно с классом Regioninfo', номинальные идентификаторы (такие как свойство Name) и числовые идентификаторы (такие как LCID). Выполнение любого из следующих вариантов кода приведет к одинаковым результатам:
’ VB
Dim UsersCulture As Cultureinfo = Thread.CurrentThread.CurrentCulture
Dim DemoRegion As Regioninfo = New RegionInfo(UsersCulture.LCID)
или:
Dim DemoRegion As Regioninfo = New RegionInfo(UsersCulture.Name)
Занятие 1
Использование информации о культуре 781
// C#
Cultureinfo UsersCulture = Thread.CurrentThread.CurrentCulture;
Regioninfo DemoRegion = new RegionInfo(UsersCulture.LCID);
ИЛИ'
Regioninfo DemoRegion = new RegionInfo(UsersCulture.Name);
После этого любое из нужных свойств можно получить напрямую, как и в случае с любым другим классом:
1 VB
Dim UsersCulture As Cultureinfo = Thread.CurrentThread.CurrentCulture
Dim DemoRegion As Regioninfo = New RegionInfo(UsersCulture.LCID)
Console.WriteLineC"English Name: ” & DemoRegion.EnglishName)
Console.WriteLineC"Display Name: " & DemoRegion.DisplayName)
Console WriteLineC"Currency Symbol: " & DemoRegion CurrencySymbol)
// C#
Cultureinfo UsersCulture = Thread.CurrentThread.CurrentCulture;
Regioninfo DemoRegion = new RegionInfo(UsersCulture.LCID);
Console WriteLineC"English Name: " + DemoRegion.EnglishName);
Console.WriteLineC"Display Name: " + DemoRegion.DisplayName );
Console.WriteLineC"Currency Symbol: " + DemoRegion.CurrencySymbol);
Если скомпилировать и запустить это приложение, можно получить результаты, аналогичные показанным на рис. 16-3 Смогут изменяться в зависимости от конфигурации компьютера).
Engli h Name United States Display Name: United States Currency Symbol: $
Рис. 16-3. Получение информации при помощи класса Regioninfo
Классы DateTimeFormatlnfo и NumberFormatlnfo
При разработке приложений, пересекающих границы различных культур, неопытные разработчики часто (и напрасно) предполагают, что в разных культурах одинаковы:
	даты и их форматы;
	числа и их форматы;
	денежные единицы.
782
Глобализация
Глава 16
Например, если сравнить американский вариант английского языка с японским, вряд ли кого-то удивит, что слово, обозначающее первый рабочий день недели будет в этих языках разным. Большинство слов в разных языках не похожи друг на друга. Но по какой-то причине люди часто предполагают обратное для дат, денежных единиц и чисел. В США денежная единица — доллар, а в Японии — иена, и не удивительно, что в этих двух странах формат и представление денежных единиц различаются.
То же с датами. Так, формат даты в Венесуэле заметно отличается от формата, принятого в США. В Венесуэле используется формат день/месяц/год, а в США — месяц/ день/год.
Классы DateTimeFormatlnfo и NumberFormatlnfo позволяют управлять тем, как обрабатываются эти различия. Класс Cultureinfo имеет свойства, содержащие информацию о конкретной культуре.
Класс DateTimeFormatlnfo имеет обширный набор методов и свойств для обработки дат в различных культурах. Рассмотрим следующий пример:
' VB
Dim UsersCulture As Cultureinfo = New Culturelnfo("es-VE")
Dim Days() As String = UsersCulture.DateTimeFormat.DayNames
For Each Day As String In Days
Console.WriteLineC"Day Name for Venezuelan Spanish : " & Day)
Next
11 C#
Cultureinfo UsersCulture = new Culturelnfo("es-VE");
String[] Days = UsersCulture.DateTimeFormat.DayNames;
foreach (String Day in Days) {
Console.WriteLineC"Day Name for Venezuelan Spanish : " + Day); }
Соответствующие выходные данные показаны на рис. 16-4.
Рис. 16-4. Дни недели в венесуэльском варианте испанского язь!ка
Если тот же код запустить в культуре «английский-США», выходными данными будут английские названия дней недели: Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday. Различия существуют и в месяцах, как показано в следующем примере:
Занятие 1
Использование информации о культуре 733
' VB
Dim UsersCulture As Cultureinfo = New Culturelnfo("es-VE")
Dim MonthsO As String = UsersCulture.DateTimeFormat.MonthNames
For Each Month As String In Months
Console.WriteLineC"Month Name for Venezuelan Spanish : " & Month) Next
// C#
Cultureinfo UsersCulture = new Culturelnfo("es-VE");
String[] Months = UsersCulture.DateTimeFormat.MonthNames;
foreach (String Month in Months)
{
Console.WriteLineC"Month Name for Venezuelan Spanish : " + Month);
}
Этот код выводит на экран названия месяцев на венесуэльском испанском. Есть также много других свойств, соответствующих всевозможным аспектам обработки дат.
Это очень важно, когда заходит речь о проверке и сравнении. Если в одной культуре используется формат даты mm/dd/yyyy, а в другой dd/mm/ уууу, что произойдет при попытке сравнения дат? Скорее всего результат сравнения будет неверным. Однако это можно исправить, указав, что эти даты из разных культур.
При сравнении чисел и денежных единиц возникает та же проблема, но и решение схожее: использовать класс NumberFormatlnfo вместо класса DateFormatlnfo. Яркой иллюстрацией этого может быть приведенный выше фрагмент кода с небольшими изменениями:
’ VB
Dim UsersCulture As Cultureinfo = New Culturelnfo("es-VE")
Console.WriteLineC"Venezuelan Currency Symbol: "
+ UsersCulture.NumberFormat.CurrencySymbol)
Console.WriteLineC"Number Decimal Symbol: "
+ UsersCulture.NumberFormat.NumberDecimalSeparator)
// C#
Cultureinfo UsersCulture = new Culturelnfo("es-VE");
Console.WriteLineC"Venezuelan Currency Symbol: "
+ UsersCultu re.NumberFo rmat.Cu r rencySymbol);
Console.WriteLineC"Number Decimal Symbol: "
+ UsersCulture.NumberFormat.NumberDecimalSeparator);
Как видно из выходных данных, аббревиатура венесуэльской денежной единицы — «Bs», а для разделения целой и дробной части чисел используется запятая.
Использование класса Compareinfo и перечислимого CompareOptions для сравнения с учетом культуры
Одна из важных причин для использования класса Cultureinfo — возможность выполнения операций сравнения с учетом культуры. Для этого класс Cultureinfo имеет свойство Compareinfo, которое является экземпляром класса Compareinfo. Можно создать экземпляр класса Compareinfo или, что более практично, присвоить его в качестве значение
784 Глобализация
Глава 16
свойства Compareinfo свойства CurrentCulture. В следующем примере выводятся значения свойств текущего объекта Compareinfo, связанного со свойством CurrentCulture:
' VB
Dim DemoInfo As Compareinfo =
Th read.Cu r rentTh read.Cu r rentCultu re.Compa reInfo
Console.WriteLine(DemoInfo.Name)
Console.WriteLine(DemoInfo.LCID)
// C#
Compareinfo DemoInfo = Thread.CurrentThread.CurrentCulture.Compareinfo;
Console.WriteLine(DemoInfo.Name);
Console.WriteLine(DemoInfo.LCID);
Операции сравнения обычно основаны на одной точно определенной культуре. (Например, если приложение написано в США, все операции сравнения почти наверняка будут основаны на культуре «en-US».) Поэтому обычно наилучшим решением является создание объекта Compareinfo, основанного на текущей культуре. Но при необходимости можно создать объект Cultureinfo на основе любой культуры, как показано в следующем примере:
' VB
Dim DemoInfo As Compareinfo = New Culturelnfo("en-US").Compareinfo
Console.WriteLine(DemoInfo.Name)
Console.WriteLine(DemoInfo.LCID)
// C#
Compareinfo DemoInfo = Thread.CurrentThread.CurrentCulture.CompareInfo;
Console.WriteLine(DemoInfo.Name);
Console.WriteLine(DemoInfo.LCID);
Самый простой способ использования класса Compareinfo — вызвать метод Compare, передав ему сравниваемые значения. Следующий пример демонстрирует сравнение двух слов при помощи метода Compare класса Compareinfo:
' VB
Dim FirstString = "Cote"
Dim SecondString = "cote"
Dim DemoInfo As Compareinfo = New Culturelnfo("fr-FFT).Compareinfo
DemoInfo.Compare(FirstString, SecondSt ring)
// C#
String FirstString = "Cote";
String SecondString = "cote";
Compareinfo DemoInfo = new Culturelnfo("fr-FR").Compareinfo;
DemoInfo.Compare(FirstString, SecondString);
Выполнив этот код, можно увидеть, что строки не считаются равными. Хотя сведения о культуре одинаковы, регистр символов разный. Для обработки подобных элементов можно использовать члены перечислимого CompareOptions, позволяющие выбирать способ сравнения. В соответствии с документацией MSDN, члены этого перечислимого определены так, как показано в табл. 16-2.
Занятие 1
Использование информации о культуре 735
Табл. 16-2. Члены перечислимого CompareOptions
Имя	Описание
IgnoreCase IgnoreKanaType	Указывает, что сравнение строк выполняется без учета регистра Указывает, что сравнение строк выполняется без учета типа каны. Тип каны (катакана или хирагана) определяет графическую форму японской слоговой азбуки. Хирагана используется в исконно японских выражениях и словах, а катакана — в словах, заимствованных из других языков, например «компьютер» и «Интернет». Один звук может быть представлен и в хирагане,
	и в катакане. Если выбран этот член, символы, представляющие один звук в хирагане и катакане, считаются равными
IgnoreNonSpace	Указывает, что при сравнении строк игнорируются непробельные комбинированные символы, такие как диакритические знаки. В стандарте Unicode под комбинированными понимаются символы, которые комбинируют с основными с целью создания новых символов. Комбинируемые символы, не являющиеся пробельными, не занимают отдельного места в строке. Подробнее — по адресу http://www.unicode.org
IgnoreSymbols	Указывает, что при сравнении не учитываются такие символы, как пробелы, знаки пунктуации, символы денежных единиц, знаки процента, математические символы, амперсанды и т. д.
IgnoreWidth	Указывает, что сравнение строк выполняется без учета ширины символа. Например, символы японской катаканы могут записываться в полную ширину или в половину ширины. Если выбран этот член, символы катаканы, записанные в полную ширину, считаются равными записанным в половину ширины
None	Указывает, что сравнение строк выполняется с параметрами по умолчанию
Ordinal	Указывает, что сравнение строк выполняется с использованием значений Unicode для каждого символа. Такое сравнение выполняется быстро, но не поддерживает концепцию культуры. Считается, что строка, начинающаяся с «U+хххх», стоит перед строкой, начинающейся с «U+уууу», если хххх меньше уууу. Этот флаг используется отдельно, его нельзя комбинировать с остальными
OrdinallgnoreCase	Указывает, что сравнение строк выполняется без учета регистра, а затем выполняет порядковое (Ordinal) сравнение. Это эквивалентно преобразованию строк в верхний регистр с использованием инвариантной культуры и выполнению порядкового сравнения полученных результатов
StringSort	Указывает, что в операции сравнения должен использоваться алгоритм сортировки строк, в котором дефис, апостроф и другие символы, не являющиеся цифрами и буквами, идут перед цифрами и буквами
Если в код из предыдущего примера внести одно изменение (использовать перечислимого Сотри/ eOptions с параметром IgnoreCase) он будет работать так, как ожидается. Это продемонстрировано в следующем фрагменте кода:
786 Глобализация
Глава 16
' VB
Dim FirstString = "Cote"
Dim SecondString = "cote"
Dim DemoInfo As Compareinfo = New Culturelnfo("fr-FR").Compareinfo
DemoInfo.Compare(FirstString, SecondString, CompareOptions.IgnoreCase)
// C#
String FirstString = "Cotti";
String SecondString = "cotw";
Compareinfo DemoInfo = new Culturelnfo("fr-FR").Compareinfo;
DemoInfo.Compare(FirstString, SecondString, CompareOptions.IgnoreCase);
Практикум. Создание кода, учитывающего культуру
Сейчас вы напишите код, распознающий изменения в культуре. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
1.	Откройте Visual Studio 2005.
2.	Создайте консольное приложение при помощи C# или Visual Basic. В Visual Basic автоматически создается модуль с пустой процедурой Main. В Visual C# автоматически создается класс Program.cs с пустой процедурой Main.
3.	Убедитесь, что в проекте присутствуют ссылки на пространства имен System, System. Threading и Sy stem.Globalization.
4.	Добавьте в процедуру Main следующий код:
’ VB
Console.WriteLineC"Please select a Culture format [i.e. en-US, es-ES]")
Dim Original As String = Console.ReadLineC)
Dim UsersCulture As Cultureinfo = New Culturelnfo(Original)
Dim Days As StringO = UsersCulture.DateTimeFormat.DayNames
For Each Day As String In Days
Console.WriteLineC"Day Name for " + _ UsersCulture.DisplayName + "	" + Day)
Next
Console.WriteLineC"Please select a NEW Culture format [i.e. en-US, es-ES]")
Dim Modified As String = Console.ReadLineC)
Dim ModifiedUsersCulture As Cultureinfo = New Culturelnfo(Modified)
Dim ModifiedDays As StringO =
ModifiedUsersCulture.DateTimeFormat.DayNames
For Each day As String In ModifiedDays
Console.WriteLineC"Day Name for " + _
ModifiedUsersCulture. DisplayName + "	” + day)
Next
Console.ReadLineC)
Занятие 1
Использование информации о культуре 7Q7
// C#
Console.WriteLineC"Please select a Culture format [i.e. en-US, es-ES]");
String Original = Console.ReadLineO;
Cultureinfo UsersCulture = new Culturelnfo(Original);
StringE] Days = UsersCulture.DateTimeFormat.DayNames; foreach (String Day in Days) {
Console.WriteLine("Day Name for "
+ UsersCulture.DisplayName + "	" + Day);
} /
Console.WriteLineC"Please select a NEW Culture format [i.e. en-US, es-ES]");
String Modified = Console.ReadLineO;
Cultureinfo ModifiedUsersCulture = new Culturelnfo(Modified);
StringE] ModifiedDays = ModifiedUsersCulture.DateTimeFormat.DayNames; foreach (String Day in ModifiedDays) {
Console.WriteLineC"Day Name for "
+ ModifiedUsersCulture.DisplayName + "	" + Day);
Console. ReadLineO;
5.	Соберите проект, исправьте ошибки и запустите приложение.
6.	Когда появится первый запрос, введите точно определенную культуру. Если не знаете, что ввести, введите es-ES.
7.	Когда появится второй запрос, введите другую культуру. Если не знаете, что ввести, введите en-US.
8.	Сравните выходные данные. Если вы использовали предложенные значения, в выходных данных будут серьезные различия.
Резюме
	Культуры представлены классом Cultureinfo.
	Текущую культуру приложения можно узнать, проверив значение свойства CurrentCul-ture класса Cultureinfo. Чтобы изменить текущую культуру приложения, просто измените значение свойства CurrentCulture.
	Чтобы эффективно реализовать глобализацию, данные должны форматироваться, а не жестко «прошиваться» в коде. В следующем фрагменте кода показаны методы, первый из которых использовать не рекомендуется:
’ VB
Dim DollarValue As String = "$20.00"
// C#
String UsersCulture = "$20.00";
* VB
Dim DollarValue As String = Format(20.00, "Currency")
788
Глобализация
Глава 16
И C#
String DollarValue = (20).ToString("C");
 Класс Cultureinfo является основным средством управления сведениями о культуре.
 Свойство CurrentCulture класса CurrentThread используется для получения и изменения сведений о культуре текущего приложения.
 Свойство CurrentUICulture класса CurrentThread используется для получения и изменения сведений о культуре визуальных элементов приложения. В отличие от CurrentCulture это свойство определяется только при запуске приложения или в конструкторе объекта.
 Класс NumberFormatlnfo позволяет получать сведения о представлении и отображении чисел.
 Класс DateFormatlnfo позволяет получать сведения о представлении и отображении дат.
Закрепление материала
Ниже приведены вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1.	Как можно получить сведения о текущей культуре приложения? (Укажите все верные ответы.)
А. Создать экземпляр класса Cultureinfo, не указывая в конструкторе никаких значений.
В. Использовать свойство CurrentCulture свойства CurrentThread экземпляра класса Thread.
С. Создать экземпляр класса Cultureinfo и использовать инвариантную культуру.
D. Создать экземпляр класса Culture Info, используя точно определенную культуру, и проверить свойство CurrentCulture.
2.	Какое из следующих выражений описывает нейтральную и точно определенную культуру «en-US»?
А. Точно определенная культура — еп, нейтральная культура — US.
В. Точно определенная культура — US, нейтральная культура — еп.
С. Комбинация двух культур описывает нейтральную культуру.
D. Нейтральная культура — en-US, точно определенная культура не указана.
3.	Как выполнить сравнение строк без учета регистра и с поддержкой концепции культур?
А. Указать культуру каждой строки. Создать экземпляр класса Compareinfo и вызвать его метод Compare, передав обе строки.
В. Указать культуру каждой строки. Создать экземпляр класса Compareinfo, а затем вызвать его метод Compare, передав обе строки и указав параметр CompareOptions.IgnoreCase.
С. Использовать метод String.CompareTo и указать другую строку.
D. Использовать метод ToUpper или То Lower для каждой строки, и просто убедиться, что они находятся в одинаковом регистре.
Занятие 2
Создание собственной культуры ygg
Занятие 2. Создание собственной культуры
Иногда встроенные в .NET Framework культуры не удовлетворяют возникшие потребности. На этот случай имеется специальное средство. Класс CultureRegionAndlnfoBuilder позволяет создавать и применять собственные культуры. Более того, он позволяет устанавливать эти культуры на другие компьютеры.
ПРИМЕЧАНИЕ .NET 2.0
Класс CultureRegionAndlnfoBuilder впервые появился в .NET 2.0.
ПРИМЕЧАНИЕ Установка собственной культуры
При установке созданной культуры необходимо учесть следующее. В соответствии с документацией MSDN, «собственная культура может быть зарегистрирована на компьютере только пользователем, имеющим права администратора. Следовательно обычные приложения не смогут создать собственную культуру. Вместо этого следует использовать класс CultureAndRegionlnfoBuilder для создания утилиты, при помощи которой администратор может создать и зарегистрировать культуру. После создания и регистрации культуры на компьютере можно использовать класс Cultureinfo для создания экземпляра этой культуры, как в случае предопределенной культуры».
При помощи класса CultureAndRegionlnfoBuilder можно создать культуру с нуля, создать ее на основе существующей или полностью скопировать существующую культуру. В следующем примере показано, как использовать существующую культуру.
ПРИМЕЧАНИЕ Для использования объекта CultureAndRegionlnfoBuilder необходимо добавить ссылку на библиотеку sysglobl.dll
В примерах предыдущих занятий достаточно было установить ссылку и включить пространство имен System.Globalization при помощи операторов using или Imports. Однако сейчас такой способ работать не будет. Если попытаться создать экземпляр класса CultureAndRegionlnfoBuilder, возникнет ошибка «тип не определен». Нужно добавить отдельную ссылку на библиотеку sysglobl.dll, которая обычно находится в папке C:\Win-dows\.NET Framework\[VERSION]\.
• VB
Dim DemoBuilder As new CultureAndRegionInfoBuilder(”en-US",
Cultu reAndRegionModifiers.Neut ral)
// C#
CultureAndRegionlnfoBuilder DemoBuilder =
new CultureAndRegionInfoBuilder("en-US”,
Cultu reAndRegionModifie rs.Neut ral);
Все, что надо сделать, — создать экземпляр класса CultureAndRegionlnfoBuilder, использовать существующую культуру и указать значение CultureAndRegionModifiers, о котором мы вскоре расскажем. Для заполнения объекта CultureAndRegionlnfoBuilder можно также использовать существующие объекты Regioninfo, объекты Cultureinfo или и те и другие. Это продемонстрировано в следующем примере:
790 Глобализация
Глава 16
' VB
Dim USCulture As New CultureInfo("en-US")
Dim USRegion As New Regionlnfo("en-US")
Dim DemoBuilder As New CultureAndRegionInfoBuilder("en-US",
Cultu reAndRegionModifiers.Neut ral)
DemoBuilder.LoadDataFromCulturelnfo(USCulture)
DemoBuilder.LoadDataFromRegionlnfo(USRegion)
// C#
Cultureinfo USCulture = new Culturelnfo("en-US");
Regioninfo USRegion = new RegionInfo("US");
CultureAndRegionlnfoBuilder DemoBuilder =
new CultureAndRegionlnfoBuilder("en-US",
CultureAndRegionModifiers.Neutral);
DemoBuilder.LoadDataFromCulturelnfo(USCulture);
DemoBuilder.LoadDataFromRegionlnfo(USRegion);
Создание собственной культуры с измененными параметрами немного отличается от только что показанного способа. В первую очередь необходимо выяснить, что должно называться культурой, и определить, будет ли новая культура основана на одной из существующих. Допустим, нужно создать культуру на основе культуры «еп-US» специально для компании Microsoft. Это можно сделать так:
' VB
Dim USCulture As New Culturelnfo("en-US")
Dim USRegion As New RegionlnfoC en-US")
Dim DemoBuilder As
New CultureAndRegionInfoBuilder("en-US-Microsoft",
CultureAndRegionModifiers.None)
DemoBuilder.LoadDataFromCulturelnfo(USCulture)
DemoBuilder.LoadDataFromRegionlnfo(USRegion)
// C#
Cultureinfo USCulture = new Culturelnfo("en-US");
Regioninfo USRegion = new RegionlnfoC"US");
CultureAndRegionlnfoBuilder DemoBuilder =
new CultureAndRegionInfoBuilder("en-US-Microsoft", CultureAndRegionModifiers.None);
DemoBuilde r.LoadDataFromCultu reInfo(USCulture);
DemoBuilde г.LoadDataF romRegionlnfо(USRegion);
Здесь просто создается новая культура (en-US-Microsoft), основанная на определениях культур еп и US. После этого можно определить любые доступные свойства, такие как CurrencyName, CultureName, NumberFormat и т. д.
Для использования класса CultureAndRegionlnfoBuilder конструктор принимает аргумент из перечислимого CultureAndRegionModifiers. Это очень простое перечислимое, имеющее всего три члена. В соответствии с документацией MSDN, члены этого перечислимого определены так, как показано в табл. 16-3.
Занятие 2
Создание собственной культуры yg-f
Табл. 16-3.	Члены перечислимого CultureAndRegionModifiers
Имя	Описание
Neutral	Особая нейтральная культура
None	Особая дополнительная культура
Replacement	Особая культура, замещающая культуру, существующую в .NET Framework или Windows
Если нужно использовать существующую культуру, но не регион, следует указать параметр Neutral. Если нужно указать для объекта дополнительную информацию, следует указать параметр None (чтобы объем наследуемой информации был минимальным). Если нужно заменить существующую культуру, следует использовать параметр Replacement, который указывает, что все свойства будут определены разработчиком.
Практикум. Создание собственной культуры
Сейчас вы создадите собственную культуру. Если при выполнении упражнения у вас возникнут затруднения, обратитесь к готовым проектам в папке Code на компакт-диске, прилагаемом к книге.
1.	Откройте Visual Studio 2005.
2.	Создайте консольное приложение при помощи C# или Visual Basic. В Visual Basic автоматически создается модуль с пустой процедурой Main. В Visual C# автоматически создается класс Program.cs с пустой процедурой Main.
3.	Убедитесь, что в проекте присутствуют ссылки на пространства имен System, Sy stem.Threading и System.Globalization.
4.	Добавьте ссылку на библиотеку sysglobl.dll, как было описано ранее.
5.	Добавьте в процедуру Main следующий код:
VB
Dim USCulture As New Culturelnfo("en-US")
Dim USRegion As New Regionlnfo("en-US")
Dim DemoBuilder As New CultureAndRegionInfoBuilder(''x-en-US-sample", _
CultureAndRegionModifiers.None)
DemoBuilder.LoadDataFromCulturelnfo(USCulture)
DemoBuilder.LoadDataFromRegionlnfo(USRegion)
11 C#
Cultureinfo USCulture = new Culturelnfo("en-US");
Regioninfo USRegicn = new RegionInfo("US );
CultureAndRegionlnfoBuilder DemoBuilder =
new CultureAndRegionInfoBuilder("x-en-US-sample", CultureAndRegionModifiers.None);
DemoBuilder.LoadDataFromCulturelnfo(USCulture);
DemoBuilder.LoadDataFromRegionlnfo(USRegion);
6.	Добавьте объект NumberFormatlnfo и определите свойства, как показано в следующем фрагменте кода:
‘ VB
Dim Numberinfo As new NumberFormatlnfoO
792 Глобализация
Глава 16
Numberinfo.CurrencySymbol =
Numberinfo.CurrencyDecimaJDigits = 4
DemoBuilder.NumberFormat = Numberinfo
// C#
NumberFormatlnfo Numberinfo = new NumberFormatlnfoO;
NumberInfo.CurrencySymbol =
Numberinfo.CurrencyDecimalDigits = 4;
DemoBuilder NumberFormat = Numberinfo;
7.	Добавьте объект DateTime Format Info и определите свойства, как показано в следующем фрагменте кода:
• VB
Dim Dateinfo As DateTimeFormatlnfo =
New DateTimeFormatlnfoO
Dateinfo.DateSeparator =
Dateinfo.DayNames = New StringO {"FirstDay", "SecondDay",
"ThirdDay", "FourthDay", "FifthDay", "SixthDay", "SeventhDay"}
// C#
DateTimeFormatlnfo Dateinfo = new DateTimeFormatlnfoO;
Dateinfo.DateSeparator =
Dateinfo.DayNames = new String[] { "FirstDay", "SecondDay", "ThirdDay", "FourthDay", "FifthDay", "SixthDay", "SeventhDay" };
8.	Постройте проект, исправьте ошибки и запустите приложение.
9.	При помощи класса Culture Info получите сведения о свойствах созданной культуры.
Резюме
	Собственные культуры можно создавать при помощи класса CultureAndlnfoRegionBuilder.
	Культуры класса CultureAndRegionlnfoBuilder могут наследовать параметры от существующих культур.
	Параметр класса CultureAndRegionlnfoBuilder, определяющий регион, также можно наследовать от родительской культуры.
	Формат чисел в собственных классах можно изменять при помощи свойства Number-Format класса CultureAndRegionlnfoBuilder.
Закрепление материала
Ниже приведены вопросы для самопроверки. Их также можно найти на компакт-диске, прилагаемом к книге.
ПРИМЕЧАНИЕ Ответы
Ответы на эти вопросы и пояснения к ним находятся в разделе «Ответы» в конце книги.
1. Какая из причин наиболее весома для использования в приложении класса CultureAndRegionlnfoBuilder^
N. Текущая культура работает корректно, но такие элементы, как символ денежной еди-
ницы, Moiyr измениться по политическим, экономическим или иным причинам.
Резюме главы
793
В.	Текущая культура работает корректно, но такие элементы, как формат даты, могут измениться по политическим, экономическим или иным причинам.
С.	Существует потребность в особой культуре, которой не существует по умолчанию.
D.	Пользователям не нравится, как представлена культура, и они хотят полностью изменить ее.
2. Какое значение нужно задать параметру CultureAndRegionModifiers при создании собственной культуры с использованием класса CultureAndRegionlnfoBuilder^
A.	None.
В.	Neutral.
С.	Replacement.
Закрепление материала главы
Для закрепления знаний и навыков, полученных при изучении этой главы, выполните следующее:
	изучите резюме главы;
	повторите основные термины, использованные в этой главе;
	выполните задания лабораторной работы по темам этой главы;
	выполните предложенные упражнения;
	пройдите пробный экзамен.
Резюме главы
	В .NET Framework включено пространство имен System.Globalization, позволяющее включить в приложения возможность корректно работать в различных географических областях.
	Класс NumberFormatlnfo позволяет получать сведения о представлении и отображении чисел и денежных единиц.
	Класс DateFormatlnfo позволяет получать сведения о представлении и отображении даты и времени.
	Чтобы эффективно реализовать глобализацию, данные должны форматироваться, а не жестко прописываться в коде. Первый из следующих примеров иллюстрирует нежелательный подход в программировании:
' VB
Dim DollarValue As String = ”$20.00"
П C#
String UsersCulture = "$20.00";
' VB
Dim DollarValue As String = Format(20.00, "Currency")
794 Глобализация
Глава 16
И с#
String DollarValue = (20).ToString(”C");
	Собственные культуры можно создавать при помощи класса CultureAndlnfoRegionBuilder.
	Формат чисел в собственных классах можно изменять при помощи свойства NumberFormat класса CultureAndRegionlnfoBuilder.
	Формат даты и времени в собственных классах можно изменять при помощи свойства GregorianDateTimeFormat класса CultureAndRegionlnfoBuilder.
Основные термины
	текущая культура;
	текущая культура пользовательского интерфейса;
	глобализация;
	локализация.
Лабораторная работа
Сейчас вы примените на практике знания и навыки, полученные при изучении этой главы. Ответы на вопросы см. в разделе «Ответы» в конце книги.
Установка и настройка нового приложения
Вы разработчик ПО в компании A Datum Corporation. Вы работаете над приложением под названием A-Datum 2005. Это приложение Windows Forms, написанное на С#, хотя при желании вы можете использовать в нем сборки Visual Basic. Предыдущая версия приложения, A-Datum 2004, продавалась исключительно в США (где и разрабатывалась) и не учитывала различия культур. Компания заключила много контрактов по всему миру, поэтому требуется включить в приложение поддержку форматов данных, принятых в географических областях, где говорят на французском и испанском языках. В ближайшем будущем возможно расширение области распространения приложения, так что следует предусмотреть возможность поддержки других культур.
Результаты опроса
	ИТ-менеджер
«Самые серьезные проблемы возникают, когда треубется различать денежные единицы и даты. В приложении множество жестко прописанных дат и денежных сумм, которые совершенно не подходят для работы за пределами США».
	Руководитель отдела разработки
«Мы часто отступали от правил и экономили время, потому что надо было быстрее закончить продукт. Это помогало в прошлом, но теперь, когда мы выходим на мировой рынок, такой подход не работает. Теперь мы можем потратить время на то, чтобы сделать все правильно, и совершенно необходимо учесть различия в датах и денежных единицах. Даже несмотря на то, что продукт будет продаваться, я не сочту его завершенным, пока он не станет корректно работать в любом регионе мира».
Пробный экзамен 795
Вопросы
Ответьте на следующие вопросы руководителя:
1.	Как можно исправить проблемы с жестко определенными значениями?
2.	Как выполнять сравнения дат и денежных единиц без учета культуры?
3.	Как учесть особенности культуры в пользовательском интерфейсе?
Рекомендуемые упражнения
Чтобы лучше проработать темы экзамена, рассмотренные в этой главе, выполните следующие упражнения.
Использование информации о культуре
	Упражнение 1 Создайте простое приложение, часто использующее денежные единицы и даты. Сначала игнорируйте любые параметры культуры. Затем выберите три культуры, с которыми вы не знакомы (если вы знакомы со всеми культурами, выберите три случайных), и по очереди изменяйте текущую культуру на каждую из выбранных. Проверьте даты и денежные единицы и посмотрите, к какому эффекту приводят изменения.
	Упражнение 2 Создайте приложение Win forms, использующее культуру en-US по умолчанию. Создайте файлы ресурсов и параметры культуры, чтобы пользователь мог выбирать культуру в диалоговом окне. Включите в приложение возможность применять выбранную пользователем культуру.
Создание собственной культуры
	Упражнение 1 Выберите регион, в котором вы жили, живете сейчас или когда-нибудь там были. Создайте собственную культуру, представляющую его. Выберите любой символ, который будет представлять денежную единицу, и формат даты, который вы никогда ранее не встречали. Создайте файлы ресурсов и используйте выдуманную культуру так же, как и любую существующую, чтобы приложение могло корректно в ней работать.
Пробный экзамен
На прилагаемом компакт-диске, содержится пробный экзамен, обеспечивающий широкие возможности по самоподготовке. Например, можно проверить усвоение тем этой главы либо всех тем экзамена 70-536. Можно сымитировать настоящий экзамен либо выбрать режим обучения, когда после каждого вопроса приводятся правильные ответы с пояснениями.
Пробный экзамен
Подробнее о пробном экзамене рассказано во введении.
Ответы
Глава 1. Закрепление материала
Занятие 1
1.	Верные ответы: А, С и D
А.	Верно: Decimal — тип значения.
В.	Неверно: String — ссылочный тип.
С.	Верно: System.Drawing.Point — тип значения.
D.	Верно: Integer — тип значения.
2.	Верный ответ: В
А. Неверно: во-первых, типы значения необходимо инициализировать до передачи методам, если эти типы не объявлены как nullable. Во-вторых, передача null-значения не влияет на работу типа значения.
В.	Верно: методы работают с отдельными копиями типов значений, а модификации, вносимые в копии таких типов не влияют на исходное значение.
С.	Неверно: возможно, переменную объявили повторно, но это не повлияет на ее значение.
D.	Неверно: если бы переменная была ссылочного типа, ее исходное значение изменилось бы.
3.	Верный ответ: В
А.	Неверно: в коде на Visual Basic используются угловые, а не круглые скобки, а в C# — наоборот.
В.	Верно: это правильное объявление и присваивания целочисленной переменной, принимающей null-значения. В C# также допустим следующий синтаксис: int? i = null;
С.	Неверно: для объявления целочисленной переменной, принимающей null-значения, необходимо использовать обобщение Nullable (по умолчанию целочисленные переменные таковыми не являются).
D.	Неверно: это неверный синтаксис для использования обобщения Nullable.
Ответы
797
4.	Верный ответ: D
А.	Неверно: создать ссылочный класс можно, но он может быть модифицирован при передаче методу.
В.	Неверно: создать ссылочную структуру невозможно.
С.	Неверно: можно создать тип значения, но структуры обычно более эффективны D. Верно: структуры на основе типов значения обычно более эффективны.
Занятие 2
1.	Верные ответы: В и С
А.	Неверно: Nullable-типы могут быть только типами значения.
В.	Верно: строки — типы значения.
С.	Верно: исключения — ссылочные типы.
D.	Неверно: типы значения происходят от System.Object, поэтому не все производные типы — ссылочные.
2.	Верный ответ: С
А.	Неверно: сначала располагают самую конкретную конструкцию Catch, а затем самую общую.
В.	Неверно: перехватывается первый подходящий тип, следующие конструкции Catch игнорируются.
С.	Верно: перехватывается первый подходящий тип, следующие конструкции Catch игнорируются. Поэтому сначала располагают самую конкретную конструкцию Catch, а затем самую общую — это позволяет перехватывать сначала ошибки, для которых есть обработчики, а потом — общие исключения.
D.	Неверно: перехватывается первый подходящий тип, следующие конструкции Catch игнорируются.
3.	Верный ответ: А
А.	Верно: при использовании типа String для создания динамических строк в памяти может оставаться множество временных строк, поскольку этот тип — неизменяемый. В силу этих причин класс StringBuilder предпочтительнее.
В.	Неверно: максимальная длина строки 32 767, а не 256 байтов.
С.	Неверно: стандартный класс String поддерживает поиск с заменой.
D.	Неверно: строки — ссылочные типы, а не типы значения.
4.	Верный ответ: В
А.	Неверно: это правильно, но преимущество блока Finally в том, что расположенный в нем код будет исполнен, даже если исполняющая среда не сгенерирует исключение, тогда как код в блоке Catch исполняется только в случае возникновения исключения.
В.	Верно: в блок Finally помещают код, который должен быть исполнен независимо от того, будет ли сгенерировано исключение.
С.	Неверно: компилятор не сгенерирует исключение, если нет блока Finally — этот блок не обязателен.
D.	Неверно: в блоке Catch можно освобождать ресурсы, но этот код будет выполнен, только если возникнет исключение, а ресурсы требуется освобождать независимо от этого.
798
Ответы
Занятие 3
1.	Верные ответы: В и С
А.	Неверно: интерфейс определяет своего рода «соглашение» между типами, а наследование позволяет объявлять типы на основе других типов.
В.	Верно: интерфейс определяет своего рода «соглашение» между типами, по которому в типе должны быть реализованы определенные члены.
С.	Верно: наследование позволяет объявлять типы на основе других типов, производные типы автоматически получают все члены, реализованные в базовом классе, которые можно дополнить новыми либо переопределить.
D.	Неверно: интерфейс определяет своего рода «соглашение» между типами, а наследование позволяет объявлять типы на основе других типов.
2.	Верные ответы: А и С
А.	Верно: Nullable — обобщенный тип.
В.	Неверно: Boolean — обычный тип значения.
С.	Верно: EventHandler — обобщенный тип.
D.	Неверно: System.Drawing.Point — обычная структура.
3.	Верный ответ: D
А.	Неверно: у класса Object нет метода Dispose. Кроме того, чтобы вызывать метод Dispose, необходимо ограничение, требующее от ваших типов реализации интерфейса IDisposable.
В.	Неверно: реализация интерфейса не позволит обобщенным типам вызывать методы, объявленные в интерфейсе.
С.	Неверно: обобщенный класс, производный от интерфейса, не сможет использовать методы этого интерфейса.
D.	Верно: при наличии ограничения, требующего от типа реализации некоторого интерфейса, можно вызывать любой из методов этого интерфейса.
4.	Верный ответ: А
А.	Верно: делегат определяет сигнатуру (аргументы и тип возвращаемого значения) метода-точки входа.
В.	Неверно: обработчики могут иметь тип Shared/static и быть членами экземпляров.
С.	Неверно: при опечатке в имени обработчика компилятор сгенерирует другую ошибку.
D.	Неверно: обработчики одинаково эффективны независимо от языка программирования.
Занятие 4
1.	Верный ответ: А
А.	Верно: основная причина избегать частой упаковки — затраты ресурсов на эту операцию.
В.	Неверно: упаковка не требует особых привилегий.
С.	Неверно: упаковка не снижает читабельность кода.
2.	Верные ответы: А и В
А.	Верно: типы значения упаковываются при вызове абстрактного метода, унаследованного от System.Object. Переопределив этот метод, можно избежать упаковки.
Ответы
799
В.	Верно: по умолчанию, метод ToString просто возвращает имя типа, что не очень полезно.
С.	Неверно: компилятор не требует переопределения метода ToString в структурах.
D.	Неверно: метод ToString никогда не вызывает ошибок времени выполнения, он просто возвращает имя типа (если он не был переопределен).
3.	Верный ответ: В
А.	Неверно: нельзя опустить член интерфейса, не нарушив его.
В.	Верно: InvalidCastException — рекомендуемый тип исключения.
С.	Неверно: генерировать собственное исключение можно, но применение стандартных исключений упрощает использование ваших типов другими разработчиками с целью перехвата определенных исключений.
D.	Неверно: необходимо возвращать значения для каждого члена.
4.	Верные ответы: А и С
А.	Верно: неявное преобразование Intl6 в Int32 допустимо, так как это расширяющее преобразование: тип Int32 способен хранить любое из возможных значений Intl6.
В.	Неверно: неявное преобразование Int32 в Intl6 запрещено, поскольку это преобразование — сужающее: тип Intl6 неспособен хранить весь диапазон значений Int32.
С.	Верно: неявное преобразование Intl6 в Double разрешено, так как это расширяющее преобразование: тип Double способен хранить любое из возможных значений Intl6.
D.	Неверно: неявное преобразование Double в Int 16 разрешено, так как это расширяющее преобразование: тип Intl6 неспособен хранить весь диапазон значений.
Глава 1. Практикум
1.	В учетных записях и подписчиков и врачей много общей информации, например имена, номера телефонов и адреса, но есть и уникальные данные. Например, для подписчиков необходимо отслеживать сведения план подписки и сведения о платежах. Для врачей необходимо отслеживать сведения о контрактах, профессиональной сертификации и другие детали. Поэтому необходимо создать отдельные классы, представляющие подписчиков и врачей, но их нужно породить от общего класса, содержащего общие для обоих производных классов члены. Для этого можно создать отдельный класс Person, а от него породить классы Subscriber и Doctor.
2.	Для хранения группы объектов подписчиков можно использовать массив.
3.	Да, обобщения можно использовать при написании метода, принимающего объекты подписчиков и врачей. Чтобы получить доступ к членам базового класса, общим для обоих производных классов, необходимо использовать ограничение для обобщенного метода.
4.	Ошибка защит обычно приводит к генерации исключения. Для его обработки необходимо заключить код в блок Try/Catch. В блоке Catch следует поместить код, отображающий пользователю сообщение о том, что ему следует связаться с руководством.
800
Ответы
Глава 2. Закрепление материала
Занятие 1
1.	Верные ответы: А, В и D
А.	Верно: если значение FileAccess для File.Open не определено, предоставляется доступ для чтения и записи.
В.	Верно: явно указав FileAccess. Write, вы получите доступ для записи к файлу.
С.	Неверно: явно указав FileAccess.Read, вы не получите доступ для записи к файлу.
D.	Верно: если создать объект Fileinfo и открыть его методом FileMode.Create, по умолчанию вы получите доступ для чтения и записи к файлу.
2.	Верные ответы: А, В, С и D
А.	Верно: FileSystem Watcher оповещает о новых файлах.
В.	Верно: FileSystemWatcher оповещает о новых каталогах.
С.	Верно: FileSystemWatcher оповещает о модифицированных файлах.
D.	Верно: FileSystemWatcher оповещает о переименованных файлах.
Е.	Неверно: обнаружить все эти изменения невозможно.
3.	Верный ответ: В
А.	Неверно: класс Path обрабатывает только путь и не вносит изменений в файловую систему.
В.	Верно: класс Path обрабатывает только путь и не вносит изменений в файловую систему.
Занятие 2
1.	Верные ответы: А, С и D
А.	Верно: этот метод читает файл и перемещает указатель вперед.
В.	Неверно: Lock не влияет на положение указателя
С.	Верно: этот метод записывает в файл и перемещает указатель вперед.
D.	Верно: этот метод перемещает указатель в файле.
2.	Верные ответы: А, В и С
А.	Верно: если закрыть StreamWriter, данные будут сброшены, а поток, с которым работает этот объект, — закрыт.
В.	Верно: метод Flush вызывает сброс данных.
С.	Верно: параметр AutoFlush вызывает принудительный сброс данных в поток при каждом вызове StreamWriter.
D.	Неверно: если закрыть поток, сброс данных не происходит, и последующие попытки использования StreamWriter (в том числе вызова StreamWriter.Close) приведут к генерации исключения.
3.	Верные ответы: А и С
А.	Верно: если создать новый экземпляр FileStream методом File Mode.OpenOrCreate, файл будет открыт или создан.
В.	Неверно: File.Create перезаписывает существующие файлы.
С.	Верно: вызов File.Open с FileMode.OpenOrCreate открывает либо создает новый файл.
D.	Неверно: вызов File.Open с FileMode.Open только открывает существующий файл, создать новый файл он не может.
Ответы
801
Занятие 3
1.	Верный ответ: В
А.	Неверно: свойство BaseStream доступно только для чтения, свойства CompressionMode нет.
В.	Верно: поток и режим указывают в конструкторе.
С.	Неверно: метод Write не указывает поток.
D.	Неверно: у класса DeflateStream нет события BaseStream.
2.	Верные ответы: С и D
А.	Неверно: сжать можно любой файл размером не более 4 Гб.
В.	Неверно: сжать можно любой файл размером не более 4 Гб.
С. Верно.
D. Верно.
Занятие 4
1.	Верные ответы: А, В и С
А.	Верно: метод GetStore позволяет получать дополнительные типы хранилищ.
В.	Верно: метод GetMachineStoreForAssembly позволяет получать хранилище уровня компьютера для вызывающей сборки.
С.	Верно: метод GetUserStoreForAssembly позволяет получать хранилище уровня пользователя для вызывающей сборки.
D.	Неверно: этот конструктор не документирован.
2.	Верный ответ: А
А.	Верно: класс IsolatedStorageFileStream происходит от FileStream и потому может применяться как любой объект типа FileStream.
В.	Неверно: класс IsolatedStorageFileStream происходит от класса FileStream и потому может применяться как любой объект типа FileStream.
Глава 2. Практикум
Упражнение 1
1. Для хранения информации о пользователях компания должна использовать изолированное хранилище. Назначив ему уровень пользователя или сборки, можно разграничить доступ к личным данным пользователей одного компьютера, а также исполнять приложения с минимальным набором разрешений.
2. Чтобы сэкономить место на диске, можно порекомендовать сжимать файл параметров с помощью GZipStream (или DeflateStream).
Упражнение 2
1.	Следует создать простую службу, которая будет непрерывно работать.
2.	Следует использовать FileSystem Watcher для мониторинга новых, модифицированных и удаленных файлов. При модификации файла можно с помощью объекта Fileinfo получить ACL и узнать, кто создал файл, и отправить эти сведения по электронной
> почте в ИТ-отдел.
802
Ответы
3.	Применяя класс FileSystemWatcher, можно устранить риск снижения производительности, которого опасается главный разработчик, поскольку вместо всеобщей регистрации и проверки каждого файла будут передаваться сведения только об изменениях файловой системы.
Глава 3. Закрепление материала
Занятие 1
1.	Верный ответ: С
А.	Неверно: этот код работает, но выполняет замену с учетом регистра. Поэтому он не сможет заменить строку «HTTP:/ /».
В.	Неверно: в этом примере обращен порядок параметров, поэтому он заменит «https://» на «http://».
С.	Верно: этот код заменит «http://» на «https://» независимо от регистра.
D.	Неверно: в этом примере обращен порядок параметров, поэтому он заменит «https://» на «http://».
2.	Верный ответ: А
А.	Верно: ISerializable — не атрибут, а интерфейс, который реализуют с целью поддержки нестандартного метода сериализации.
В.	Неверно: для обработки многострочного ввода необходимо указать параметр RegexOptions. Multiline.
С.	Неверно: в имени группы не должно быть угловых скобок.
D.	Неверно: в имени группы не должно быть угловых скобок. Кроме того, для обработки многострочного свода необходимо указать параметр RegexOptions. Multiline.
3.	Верный ответ: В
А.	Неверно: это регулярное выражение соответствует «zoot», но не «zot».
В.	Верно: это регулярное выражение соответствует обеим строкам.
С.	Неверно: это регулярное выражение не соответствует ни одной из строк, поскольку оно начинается с символа «$», соответствующего концу строки.
D.	Неверно: это регулярное выражение соответствует «zot», но не «zoot».
4.	Верные ответы: А, С и Е
А.	Верно: эта строка не соответствует данному регулярному выражению.
В.	Неверно: четвертый символ этой строки не соответствует данному регулярному выражению.
С.	Верно: эта строка соответствует данному регулярному выражению.
D.	Неверно: эта строка не соответствует данному регулярному выражению: третий и четвертый символы должны быть «то».
Е.	Верно: эта строка соответствует данному регулярному выражению.
Занятие 2
1.	Верный ответ: А
А.	Верно: коды UTF-32 имеют максимальный размер, соответственно, файлы в этой кодировке имеют наибольший размер.
В.	Неверно: размер кодов UTF-16 меньше, чем кодов UTF-3.
Ответы
803
С.	Неверно: размер кодов UTF-8 меньше, чем кодов UTF-3.
D.	Неверно: размер кодов ASCII меньше, чем кодов UTF-3.
2.	Верные ответы: А, В и С
А.	Верно: размер кодов UTF-32 достаточно велик для поддержки символов Chinese Unicode.
В.	Верно: размер кодов UTF-16 достаточно велик д ля поддержки символов Chinese Unicode.
С.	Верно: размер кодов UTF-8 достаточно велик для поддержки символов Chinese Unicode.
D.	Неверно: ASCII поддерживает только символы латинского алфавита.
3.	Верные ответы: С и D
А.	Неверно: ASCII использует 8-битные коды, в UTF-32 коды длиннее.
В.	Неверно: ASCII использует 8-битные коды, в UTF-16 коды длиннее.
С.	Верно: в UTF-8 используются 8-битные коды, включая диапазон ASCII, поэтому кодировка UTF-8 обратно совместима с ASCII.
D.	Верно: UTF-7 позволит корректно декодировать ASCII-файлы.
4.	Верный ответ: D
А.	Неверно: кодовая страница iso-2022-kr поддерживает символы корейского алфавита. Однако Unicode, помимо корейского, поддерживает все остальные языки и обеспечивает максимальную совместимость.
В.	Неверно: x-EBCDIC-KoreanExtended поддерживает символы корейского алфавита. Однако Unicode, помимо корейского, поддерживает все остальные языки и обеспечивает максимальную совместимость.
С.	Неверно: x-mac-korean поддерживает символы корейского алфавита. Однако Unicode, помимо корейского, поддерживает все остальные языки и обеспечивает максимальную совместимость.
D.	Верно: хотя существует несколько кодовых страниц, поддерживающих корейский алфавит, Unicode поддерживает все остальные языки и обеспечивает максимальную совместимость.
Глава 3. Практикум
Упражнение 1
1. Для ограничения ввода в поля можно использовать отдельные элементы управления ASP.NET RegularExpressionValidator. Свойству ValidationExpression валидатора, проверяющего имя компании, присвойте значение «[a-zA-Z’-A,A\s]{l,40}», для валидатора, проверяющего имя подрядчика, используйте «[a-zA-Z*- A,A\s]{l,30}», а для валидатора телефонного номера можно использовать встроенное регулярное выражение ASP.NET, «((\(\d{3}\) ?)|(\d{3}-))?\d{3}-\d{4}».
2. Можно написать код с дополнительными ограничениями и проверкой Например, разработчики БД отсеивают апострофы и знаки процентов с помощью метода String.Replace.
Упражнение 2
1. Да, можно извлекать данные из текстовых отчетов с помощью регулярных выраже-i:i ний методами Regex.Match и набора Match.Groups.
2. Да, можно читать ASCII-файлы, не указывая кодировку, т.к. подобные файлы корректно читаются со стандартными параметрами потоков.
804
Ответы
Глава 4. Закрепление материала
Занятие 1
1.	Верные ответы: В и С
А.	Неверно: метод Remove возвращает управление, не генерируя исключение даже при отсутствии в наборе нужного элемента.
В.	Верно: Contains применяют для проверки существования элемента
С.	Верно: IndexOf вернет <;$М1>1, если элемент отсутствует в наборе.
D.	Неверно: число элементов набора не позволит узнать, есть ли в нем заданный элемент.
2.	Верные ответы: А, С и D
А.	Верно: объект Comparer сравнивает два объекта и возвращает целое число, которое показывает, что объект, расположенный в левой части равенства больше, меньше или равен объекту, расположенному в правой части выражения.
В.	Неверно: этот класс сравнивает объекты относительно друг друга, а не проверяет идентичность ссылок.
С.	Верно: он используется методом ArrayList.Sort для сортировки по умолчанию.
D.	Верно: это реализация по умолчанию интерфейса IComparer.
Занятие 2
1.	Верные ответы: А и С
А.	Верно: так можно получить первый элемент набора.
В.	Неверно: так добавить элемент в набор нельзя.
С.	Верно: так элемент удаляется из набора при получении.
D.	Неверно: так очистить набор нельзя.
2.	Верный ответ: С
А.	Неверно: стек работает по принципу «последний вошел, первый вышел».
В.	Неверно: стек работает по принципу «последний вошел, первый вышел».
С.	Верно: да, этот набор работает по принципу «последний вошел, первый вышел».
D.	Неверно: стек работает по принципу «последний вошел, первый вышел >.
Занятие 3
1.	Верные ответы: В и D
А.	Неверно: GetType не позволяет определить уникальность объекта.
В.	Верно: да, можно проверить наличие в наборе объекта с данным хэшем.
С.	Неверно: строковое представление объекта не позволяет проверить уникальность его ключа. Класс Hashtable использует для проверки уникальности только значения хэша.
D.	Верно: если два хэш-значения идентичны, можно вызвать Equals, чтобы проверить равенство объектов перед присваиванием значения ключу.
2.	Верный ответ: А
А.	Верно: чтобы изменить процедуру проверки уникальности, необходимо передать объект, поддерживающий этот интерфейс.
Ответы
805
В.	Неверно: после создания Hashtable присваивание lEqualityComparer невозможно.
С.	Неверно: lEqualityComparer можно применять с Hashtable.
D.	Неверно: Hashtable поддерживает конструктор, принимающий объект типа lEqualityComparer, но сам интерфейс в нем не реализован.
Занятие 4
1.	Верные ответы: С и D
А.	Неверно: на основе класса CollectionUtil можно создавать только нечувствительные к регистру объекты Hashtable и SortedList.
В.	Неверно: на основе класса CollectionUtil можно создавать только нечувствительные к регистру объекты Hashtable и SortedList (а не объекты с инвариантной культурой).
С.	Верно: на основе класса CollectionUtil можно создавать нечувствительные к регистру объекты Hashtable и SortedList.
D.	Верно: на основе класса CollectionUtil можно создавать нечувствительные к регистру объекты Hashtable и SortedList.
2.	Верный ответ: А
А.	Верно: только строки можно хранить в свойстве Value объекта StringDictionary.
В.	Неверно: только строки можно хранить в свойстве Value объекта StringDictionary.
С.	Неверно; только строки можно хранить в свойстве Value объекта StringDictionary.
D.	Неверно: только строки можно хранить в свойстве Value объекта StringDictionary.
Занятие 5
1.	Верный ответ: В
А.	Неверно: перечислитесь обеспечивает контроль типов при получении KeyValuePair.
В.	Верно: перечислитесь обеспечивает контроль типов при получении KeyValuePair.
С.	Неверно: перечислитесь обеспечивает контроль типов при получении KeyValuePair.
D.	Неверно: перечислитесь обеспечивает контроль типов при получении KeyValuePair.
2.	Верные ответы: А, В, С и D
А.	Верно: добавление элементов в начало LinkedList разрешено.
В.	Верно: добавление элементов перед заданным узлом LinkedList разрешено
С.	Верно: добавление элементов после заданного узла LinkedList разрешено.
D.	Верно: добавление элементов в конец LinkedList разрешено.
Е.	Неверно: вставка по индексу не поддерживается.
Глава 4. Практикум
Упражнение 1
1. Используйте ArrayList для хранения кодов, поскольку их, скорее всего, будет немного; этот подход позволит легко добавить коды при необходимости.
2. Для сортировки кодов пользователя используйте функцию Sort объекта ArrayList.
Упражнение 2
1. Используйте HybridDictionary, поскольку он поддерживает поиск по коду продавца и эффективно обрабатывает как длинные, так и короткие списки.
806
Ответы
2. Если хранить имена агентов в наборе, расположенном в оперативной памяти, их не придется постоянно загружать с диска, и значительного снижения производительности удастся избежать.
Упражнение 3
1. Используйте обобщенный набор List, добавьте поддержку контроля типов при регистрации банковских операций. Этот набор должен принимать только объекты заданного типа, попытка передать другие объекты должна вызывать ошибку компиляции, чтобы разработчик смог устранить ошибку до сдачи приложения в эксплуатацию.
2. Быстродействие набора останется прежним или увеличится. Проверка типов, замедляющая исполнение кода, осуществляется во время компиляции, поэтому итоговый код станет проще, и, возможно, быстрее.
Глава 5. Закрепление материала
Занятие 1
1.	Верные ответы: А и D
А.	Верно: следует вызвать метод BinaryFormatter. Serialize.
В.	Неверно: для сериализации объекта разрешения на доступ к файлу не требуются, также возможна сериализация объекта в сетевой поток.
С.	Неверно: службы IIS не требуются для сериализации, но сериализованные объекты часто передаются Web-сервисам.
D.	Верно: метод BinaryFormatter.Serialize требует указать объект потока в качестве приемника сериализации.
2.	Верный ответ: В
А.	Неверно: ISerializable — не атрибут, а интерфейс, который реализуют с целью поддержки нестандартного метода сериализации.
В.	Верно: у сериализуемого класса должен быть атрибут Serializable.
С.	Неверно: Soaplnclude используется при генерации схем для SOAP-сериализации, для поддержки собственно сериализации он не требуется.
D.	Неверно: OnDeserialization — не атрибут, а метод интерфейса ISerializable, реализация этого метода позволяет управлять сериализацией.
3.	Верный ответ: А
А.	Верно: атрибут NonSerialized запрещает сериализацию члена.
В.	Неверно: Serializflble — атрибут класса, а не члена.
С.	Неверно: SerializationException — не атрибут, а класс, представляющий исключения при сериализации.
D.	Неверно: атрибут Soaplgnore запрещает сериализацию члена только при помощи SoapFormatter, но не BinaryFormatter.
4.	Верный ответ: С
А.	Неверно: IFormatter обеспечивает форматирование сериализованных объектов.
В.	Неверно: ISerializable позволяет объекту управлять собственной сериализацией-десериализацией.
Ответы
807
С.	Верно: для вызова кода после сериализации объекта необходимо реализовать интерфейсы IDeserializationCallback и IDeserializationCallback.OnDeserialization.
D.	Неверно: lObjectReferences указывает, что объект, в котором реализован данный интерфейс, является ссылкой на другой объект.
Занятие 2
1.	Верные ответы: А и С
А.	Верно: классы, поддерживающие XML-сериализацию, должны быть открытыми.
В.	Неверно: XML-сериализация не поддерживается для закрытых классов.
С.	Верно: для XML-сериализации у класса должен быть конструктор с параметрами.
D.	Неверно: XML-сериализация не требует параметра Serializationinfo.
2.	Верный ответ: D
А.	Неверно: атрибут XmlAnyAttribute вызывает заполнение массива объектами XmlAttribute, представляющими XML-атрибуты, не объявленные в схеме. Используется при десериализации, а не при сериализации.
В.	Неверно: атрибут XMLType позволяет указать имя и пространство имен XML-типа.
С.	Неверно: XMLElement вызывает сериализацию поля или свойства в XML-элемент.
D.	Верно: по умолчанию члены сериализуются в элементы. Добавление атрибута XMLAttnbute вызывает сериализацию члена в атрибут.
3.	Верный ответ: А
А.	Верно: утилиту Xsd.exe* применяют для создания классов на основе XML-схемы.
В.	Неверно: Xdcmake.exe не используется для создания классов на основе XML-схемы.
С.	Неверно: XPadsi90.exe служит для регистрации БД SQL Server в Active Directory и не используется для создания классов на основе XML -схемы.
D.	Неверно: Xcacls.exe служит для настройки ACL файлов и не используется для создания классов на основе XML-схемы.
4.	Верный ответ: В
А.	Неверно: атрибут XMLType позволяет указать имя и пространство имен XML-типа.
В.	Верно: атрибут XMLIgnore запрещает XML-сериализацию члена.
С.	Неверно: XMLElement вызывает сериализацию поля или свойства в XML-элемент.
D.	Неверно: XMLAttribute вызывает сериализацию поля или свойства в XML-атрибут.
Занятие 3
1.	Верные ответы: А и С
А.	Верно: десериализующий конструктор должен принимать два объекта, Serializationinfo и Streamingcontext.
В.	Неверно: класс Formatter используется при сериализации, но не передается десериализующему конструктору.
С.	Верно: десериализующий конструктор должен принимать объекты Serializationinfo и StreamingContext.
D.	Неверно: класс ObjectManager используется при сериализации, но не передается десериализующему конструктору.
808
Ответы
2.	Верный ответ: В
А.	Неверно: событие OnSerializing происходит до сериализации.
В.	Верно: событие OnDeserializing происходит непосредственно перед десериализацией.
С.	Неверно: событие OnSerialized происходит после сериализации.
D.	Неверно: событие OnDeserialized происходит после десериализации.
3.	Верный ответ: С
А.	Неверно: событие OnSerializing происходит до сериализации.
В.	Неверно: событие OnDeserializing происходит до десериализации.
С.	Верно: событие OnSerialized происходит сразу после сериализации.
D.	Неверно: событие OnDeserialized происходит после десериализации.
4.	Верные ответы: А и С
А.	Верно: обработчики события сериализации должны принимать объект StreamingContext в качестве параметра.
В.	Неверно: обработчики события сериализации должны принимать в качестве параметра объект StreamingContext, а не Serializationinfo.
С.	Верно: обработчики события сериализации должны возвращать void.
D.	Неверно: обработчики события сериализации должны возвращать void, а не объект StreamingContext.
Глава 5. Практикум
Упражнение 1
1.	Используйте BinaryFormatter — это обеспечит взаимодействие только с .NET-приложениями и позволит сэкономить трафик.
2.	Скорее всего, достаточно будет атрибута Serializable.
3.	Ответ зависит от способа подключения по сети, для поддержки сериализации достаточно пары строк кода.
Упражнение 2
1.	Да, Binary Formatter способен десериализовать объекты, сериализованные .NET прежних версий.
2.	Да, класс Preferences удастся десериализовать даже при отсутствии некоторых его членов, но для этого каждый новый член необходимо пометить атрибутом OptionalField, иначе исполняющая среда сгенерирует исключение сериализации. Далее, после десериализации потребуется инициализировать новые члены значениями по умолчанию. Для этого нужно реализовать IDeserializfltionCallback или создать обработчик события OnDeserialized.
3.	Ничто не мешает сериализации этого класса с помощью BinaryFormatter или XmlSerializer. Сначала следует проверить, наличие XML-файла с сериализованным объектом, и, если такого нет, попытаться десериализовать двоичный файл.
Ответы
809
Глава 6. Закрепление материала
Занятие 1
1.	Верный ответ: Е
А.	Неверно: метод Graphics. Draw Lines рисует ломаные, поэтому он способен нарисовать контуры квадрата, но не может залить его.
В.	Неверно: проще всего нарисовать контур квадрата с помощью Graphics.DrawRectangle, но залить его это класс не сможет.
С.	Неверно: метод Graphics. Draw Poly gon способен нарисовать контур квадрата, но сможет залить его.
D.	Неверно: метод Graphics.DrawEllipse рисует только овалы и непригоден для рисования залитых квадратов.
Е.	Верно: метод Graphics. FillRectangle рисует залитые квадраты и прямоугольники.
Е Неверно: методом Graphics.FillPolygon можно нарисовать залитый квадрат, но это нерациональный способ.
G.	Неверно: метод Graphics.FillEllipse служит для рисования овалов и непригоден для рисования залитых квадратов.
2.	Верный ответ: С
А.	Неверно: метод Graphics.DrawLines метод Graphics.DrawLines рисует ломаные, поэтому он способен нарисовать контуры квадрата, но это не рациональный способ.
В.	Неверно: Graphics.DrawRectangle рисует контуры квадраты и прямоугольники, он непригоден для рисования треугольников.
С.	Верно: вызов Graphics.DrawPolygon — оптимальный способ рисования контуров треугольника.
D.	Неверно: метод Graphics.DrawEllipse рисует овалы и непригоден для рисования треугольников.
Е.	Неверно: Graphics.FillRectangle рисует залитые квадраты и прямоугольники, он непригоден для рисования контура треугольника.
Е Неверно: Graphics.FillPolygon рисует залитые треугольники, но не может нарисовать треугольный контур.
G.	Неверно: Graphics.FillEllipse рисует овалы и непригоден для рисования треугольных контуров.
3.	Верные ответы: А и В
А.	Верно: чтобы нарисовать окружность, вызовите метод Graphics.DrawEllipse, передав ему экземпляр класса Graphics.
В.	Верно: вызов метода Graphics.DrawEllipse требует экземпляра класса Реп.
С.	Неверно: System.Drawing.Brush используется для рисования залитых фигур, а не контуров.
D.	Неверно: объект Graphics можно создать из System.Drawing.Bitmap, но есть множество лучших способов.
4.	Верный ответ: В
А.	Неверно: HatchBrush задает прямоугольную узорную кисть, а также цвета фона и переднего плана.
810
Ответы
В.	Верно: LinearGradientBrush служит для заливки фигуры плавным переходом от одного цвета к другому.
С.	Неверно: PathGradientBrush поддерживает градиентную заливку объекта, но лучше использовать LinearGradientBrush.
D.	Неверно: SolidBrush заливает объекты только одним цветом.
Е.	Неверно: TextureBrush заливает объекты заданным изображением.
5.	Верный ответ: D
А.	Неверно: стрелка указывает вправо.
В.	Неверно: стрелка указывает вправо.
С.	Неверно: стрелка указывает вправо.
D.	Верно: стрелка указывает вправо.
Занятие 2
1.	Верные ответы: А и В
А.	Верно: изображение можно загрузить из файла с помощью конструктора Image, а затем вызвать Graphics. Draw Image для вывода изображения на форме.
В.	Верно: класс Bitmap происходит от класса Image и в большинстве случаев может заменить его.
С.	Неверно: класс MetaFile непригоден для загрузки изображений в формате JPEG.
D.	Неверно: класс PictureBox используется для вывода изображений на формах, но не поддерживает загрузку изображений из файлов.
2.	Верный ответ: С
А.	Неверно: создать объект Graphics непосредственно из сохраненного на диске изображения нельзя.
В.	Неверно: класс Bitmap не поддерживает рисование.
С.	Верно: перед сохранение необходимо создать объекта Bitmap, а из него — Graphics.
D.	Неверно: метода Bitmap.CreateGraphics нет, вместо него для создания объекта Graphics следует вызывать Graphics. Fromlmage.
3.	Верный ответ: С
А.	Неверно: фото можно хранить в формате BMP, но в формате JPEG они занимают намного меньше места.
В.	Неверно: формат GIF не очень подходит для хранения фотографий.
С.	Верно: формат JPEG обеспечивает высокое качество фотографий, занимает немного места на диске и поддерживается большинством приложений.
D.	Неверно: формат PNG весьма эффективен, но не столь совместим по сравнению с GIF и JPEG.
4.	Верный ответ: В
А.	Неверно: диаграммы можно хранить в формате BMP, но в формате GIF они занимают намного меньше места.
В.	Верно: формат GIF идеален для хранения диаграмм.
С' Неверно: диаграммы можно хранить в формате JPEG, но при этом они будут менее четкими по сравнению с GIF.
D. Неверно: формат PNG весьма эффективен, но не столь совместим по сравнению с GIF и JPEG.
Ответы
811
Занятие 3
1.	Верный ответ: В
А- Неверно: класс string не под держивает метода Draw.
В.	Верно: чтобы добавить текст, следует вызвать метод Graphics.DrawString, требующий объекты Graphics, Font и Brush.
С.	Неверно: Graphics.DrawString требует объект Brush, а не Реп.
D.	Неверно: класс Bitmap не поддерживает метод DrawString.
2.	Верный ответ: А
А.	Верно: чтобы задать выравнивание строки, создайте экземпляр StringFormat и передайте его методу Graphics.DrawString.
В.	Неверно: StringAlignment позволяет задать форматирование строки, но это — перечислимое, и создать его экземпляр нельзя.
С.	Неверно: FormatFlags позволяет задать форматирование строки, но это свойство класса StringFormat и создать его экземпляр нельзя.
D.	Неверно: LineAlignment позволяет задать форматирование строки, но это свойство класса StringFormat и создать его экземпляр нельзя.
3.	Верный ответ: С
А.	Неверно: так линия рисуется наверху ограничивающего прямоугольника.
В.	Неверно: так линия рисуется внизу ограничивающего прямоугольника.
С.	Верно: так линия рисуется слева ограничивающего прямоугольника.
D.	Неверно: так линия рисуется справа ограничивающего прямоугольника.
Глава 6. Практикум
Упражнение 1
1.	Размер изображения задают в конструкторе Bitmap, и .NET Framework автоматически масштабирует его. Чтобы сохранить пропорции изображения, необходимо учесть исходные пропорции при расчете новых размеров изображения.
2.	Загрузите файл изображения в объект Bitmap, создайте из него объект Graphics и вызовите Graphics.Drawlmage для отрисовки логотипа (фон логотипа должен быть прозрачным).
3.	Определите максимальные размеры логотипа по горизонтали и вертикали (в процентах), рассчитайте число пикселов по отношению к текущему размеру заставки и измените соответствующим образом размеры логотипа.
4.	Для этого графика не требуется, достаточно манипулирования размерами надписей и полей в свойствах элементов управления.
Упражнение 2
1.	Используйте элемент управления Picture Box.
2.	Graphics.DrawLines.
3.	Graphics. Draw Rectangles.
4.	PictureBox.Image.Save.
812
Ответы
Глава 7. Закрепление материала
Занятие 1
1.	Верный ответ: В
А.	Неверно: делегат ThreadStart не передает параметры.
В.	Верно: делегат ParameterizedThreadStart принимает один параметр.
С.	Неверно: класс Synchronizationcontact не запускает потоки.
D.	Неверно: класс ExecutionContext не нужен для запуска потока с одним параметром.
2.	Верный ответ: С
А.	Неверно: метод Thread.Suspend больше не поддерживается, он приостанавливает, а не останавливает потоки.
В.	Неверно: метод Thread.Resume больше не поддерживается, он не останавливает потоки.
С.	Верно: метод Thread.Abort останавливает поток, генерируя исключение ThreadAbortException .
D.	Неверно: Thread.Join ожидает завершения потока, а не останавливает его
Занятие 2
1.	Верный ответ: D
А.	Неверно: ReaderWriterLock не ограничивает число читающих объектов.
В.	Неверно: ReaderWriterLock не ограничивает число читающих объектов.
С.	Неверно: ReaderWriterLock не ограничивает число читающих объектов.
D.	Верно: ReaderWriterLock не ограничивает число читающих объектов.
2.	Верные ответы: В и С
А.	Неверно: класс Monitor недоступен через границы AppDomain и процессов.
В.	Верно: объекты Mutex имеют имена и потому доступны через границы AppDomain и процессов.
С.	Верно: объекты Semaphore имеют имена и потому доступны через границы AppDomain и процессов.
D.	Неверно: ключевые слова lock (С#) и SyncLock (Visual Basic) используют для блокировки объекты Monitor, недоступные через границы AppDomain и процессов.
Занятие 3
1.	Верные ответы: В и D
А.	Неверно: ожидать освобождения WaitHandle позволяет RegisterWaitForSingleObject.
В.	Верно: для исполнения кода в потоке из пула используется QueueUserWorkltem.
С.	Неверно: для ожидания освобождения WaitHandle используется UnsafeRegister-WaitForSingleObject.
D.	Верно: для исполнения кода в потоке из пула используется UnsafeQueueUserWorkltem.
2.	Верный ответ: В
А.	Неверно: уничтожение объекта Timer остановит таймер, и перезапустить его не удастся.
В.	Верно: используйте Timer.Change и Timeout.Infinite.
Ответы
813
С.	Неверно: даже если объект Timer выйдет из области видимости, он продолжит генерировать события до сбора мусора, время которого не определено.
D.	Неверно: если задать нулевой тайм-аут, таймер будет срабатывать с максимально возможной скоростью, тогда как требуется диаметрально противоположный результат.
Глава 7. Практикум
Упражнение 1
1.	Это приложение — однопоточное, поэтому его код работает только в главном потоке.
2.	Можно сделать приложение многопоточным, чтобы оно оптимально использовало ресурсы доступных процессоров.
3.	Используйте класс ThreadPool, который изменяет число доступных потоков согласно числу установленных на компьютере процессоров.
Упражнение 2
1. Используйте именованный Mutex, чтобы разрешить доступ к интерфейсу только одному приложению в данный момент времени.
2. Mutex — объект уровня (ядра) операционной системы, поэтому снижение производительности имеет место, но довольно небольшое.
Глава 8. Закрепление материала
Занятие 1
1.	Верные ответы: В и D
А.	Неверно: есть и другие способы запуска отдельных процессов.
В.	Верно: чтобы закрыть домен приложения и освободить ресурсы, вызовите AppDomain. Unload.
С.	Неверно: создание отдельного домена приложения не увеличивает производительность.
D.	Верно: домены приложений обеспечивают уровень изоляции. Также можно ограничить привилегии домена приложения, чтобы снизить риск использования злонамеренным кодом уязвимостей в системе безопасности.
2.	Верные ответы: В и С
А.	Неверно: AppDomain.CreateDomain создает новые домен приложения, но не запускает сборку.
В.	Верно: AppDomain.ExecuteAssembly позволяет запустить сборку, если указан путь к ней.
С.	Верно: AppDomain.ExecuteAssemblyByName позволяет запустить сборку, если указан путь и ссылку на нее.
D.	Неверно: AppDomain.ApplicationIdentity — свойство, оно не позволяет запустить сборку.
814
Ответы
3.	Верный ответ: D
А.	Неверно: AppDomain.DomainUnload — событие, которое происходит при выгрузке домена приложения.
В.	Неверно: присвоение домену приложения значения null не выгружает его из памяти.
С.	Неверно: у экземпляров класс AppDomain нет метода Unload.
D.	Верно: чтобы выгрузить AppDomain, передайте его статическому методу AppDomain. Unload.
Занятие 2
1.	Верный ответ: С
Неверно: доказательства не влияют на приоритет процессов.
А.	Неверно: доказательства определяют автора сборку, но исполняющая среда использует их, только если для заданного автора соответствующим образом настроены параметры безопасности.
В.	Верно: основное назначение доказательств домена приложения — назначение ему особых привилегий времени исполняющей средой.
С.	Неверно: доказательства не связаны с аудитом.
2.	Верные ответы: А и D
А.	Верно: передав доказательства методу AppDomain.CreateDomain, можно применить их к любой сборке, исполняемой в домене приложения.
В.	Неверно: AppDomain.Evidence можно читать, но нельзя изменять. Чтобы задать доказательства для доменов приложения, необходимо передать Evidence конструктору AppDomain.
С.	Неверно: AppDomain.ExecuteAssembly не принимает название зоны как параметр. Его нужно указать в объекте Evidence и передать его методу ExecuteAssembly.
D.	Верно: чтобы связать доказательства с заданной сборкой, передайте их методу AppDomain.ExecuteAssembly.
3.	Верный ответ: D
А.	Неверно: свойство DynamicDirectory доступно только для чтения. Оно указывает каталог для размещения динамически генерируемых файлов, а не базовый каталог приложения.
В.	Неверно: свойство BaseDirectory доступно только для чтения.
С.	Неверно: свойство DynamicBase указывает каталог для размещения динамически генерируемых файлов, а не базовый каталог приложения.
D.	Верно: Экземпляр AppDomainSetup позволяет настроить домен приложения, а свойство AppDomainSetup.ApplicationBase — задать имя каталога приложения.
4.	Верный ответ: А
А.	Верно: это булево свойство указывает, разрешено ли текущему домену приложения загружать сборки.
В.	Неверно: свойство DisallowCodeDownload находится в AppDomain.CurrentDomain.Setup-Information.
С.	Неверно: свойство DisallowPublisherPollcy определяет, применена ли к домену приложения секция «политика издателя» конфигурационного файла. Вместо него проверьте свойство DisallowCodeDownload.
Ответы
815
D.	Неверно: во-первых, свойство Disallow Publisher Policy находится в AppDomain.Cur-rentDomain.SetupInformation. Во-вторых, вместо него следует использовать Dis-allowCode Download.
Занятие 3
1.	Верный ответ: А
А. Верно: под учетной записью LocalService служба работает в контексте непривилегированного пользователя на локальном компьютере и предоставляет удаленным серверам удостоверения анонимного пользователя. Применение LocalService оптимально с точки зрения безопасности, т.к. оно сводит к минимуму потенциальный ущерб, который может нанести служба, если злоумышленнику удастся захватить контроль над ней.
В.	Неверно: NetworkService проходит аутентификацию на удаленных компьютерах, что несет некоторый риск для безопасности.
С.	Неверно: учетная запись LocalSystem обладает практически неограниченными привилегиями на локальном компьютере, поэтому под ней злоумышленник, захвативший контроль над службой, может выполнять на компьютере почти любые действия.
D.	Неверно: учетная запись User заставляет систему запрашивать действительное имя пользователя и пароль при установке службы. В зависимости от привилегий, которыми владеет указанный пользователь, риск для безопасности может быть существенным.
2.	Верный ответ: С
А.	Неверно: под учетной записью LocalService служба работает в контексте непривилегированного пользователя на локальном компьютере. Применение LocalService оптимально с точки зрения безопасности, но может привести к ошибкам при выполнении обычных действий, например записи в файл.
В.	Неверно: учетную запись NetworkService применяют для служб, которым необходимо проходить аутентификацию на удаленных компьютерах, если служба использует только ресурсы локального компьютера, ее лучше не использовать.
С.	Верно: учетная запись LocalSystem обладает практически неограниченными привилегиями на локальном компьютере, поэтому используйте ее, только если безопасность не так важна.
D.	Неверно: учетная запись User заставляет систему запрашивать действительное имя пользователя и пароль при установке службы. Назначив указанному пользователю достаточные привилегии, можно снизить риск для безопасности.
3.	Верные ответы: В и D
А.	Неверно: сборку можно автоматически запустить, добавив ее ярлык в группу Автозагрузка (Startup), запустить так службу нельзя.
В.	Верно: утилита InstallUtil позволяет устанавливать службы вручную через командную строку.
С.	Неверно: сборку можно автоматически запустить с помощью Планировщика Windows, запустить так службу нельзя.
D.	Верно: пользователям проще всего устанавливать службу с помощью установщи-5: ’ ка, созданного в Visual Studio.
816
Ответы
4.	Верный ответ: В
А.	Неверно: в папке Мой компьютер (Му Computer) нет средств для настройки учетных записей служб.
В.	Верно: консоль Управление компьютером (Computer Management) содержит оснастку Службы (Services), которая позволяет настраивать учетные записи для служб.
С.	Неверно: утилита командной строки net позволяет запускать, останавливать, приостанавливать и возобновлять работу служб, но не позволяет настраивать для них учетные записи.
D.	Неверно: в Microsoft .NET Framework Configuration нет средств для настройки учетных записей служб.
Глава 8. Практикум
Упражнение 1
I. Создайте приложение, которое запрашивает у пользователя зону и сборку. Получив эту информацию, приложение должно запустить сборку в домене приложения с доказательствами, по которым она будет отнесена к группе кода, соответствующей выбранной зоне.
2. Проще всего сделать это, назначив сборке доказательства для группы кода в зоне Интернет (Internet)'.
' VB
Dim hostEvidence As ObjectO = {New Zone (SecurityZone.Internet)}
Dim internetEvidence As Evidence = New Evidence (hostEvidence, Nothing)
Dim myDomain As AppDomain = AppDomain.CreateDomain("QADomain") myDomain.ExecuteAssembly("C:\path\CASDemands.exe", internetEvidence)
// C#
object [] hostEvidence = {new Zone(SecurityZone.Internet)};
Evidence internetEvidence = new Evidence(hostEvidence, null);
AppDomain myDomain = AppDomain.CreateDomain("QADomain");
myDomain.ExecuteAssembly(@"C:\pat7)\CASDemands.exe", internetEvidence),
При запуске приложения CAS Demand исполняющая среда должна предупредить, что приложение запускается в частично доверяемом контексте. Если вы не получили этого предупреждения, ограничить разрешения сборки не удалось.
Упражнение 2
1.	Следует создать службу Windows.
2.	Необходимо создать проект установочной программы для службы, который сгенерирует MSI-файл, который позволит сотрудникам ИТ-отдела распространить его по компьютерам организации с помощью Systems Management Server (SMS).
3.	Для службы следует установить тип запуска Автоматически (Automatic).
4.	Следует указать использование учетной записи пользователя (User) и попросить администраторов создать учетную запись с правами только для чтения конфигурационных файлов и добавления событий. Привилегий учетной записи LocalService достаточно для этого, а учетная запись LocalSystem обладает слишком большими для этой задачи привилегиями.
Ответы
817
Глава 9. Закрепление материала
Занятие 1
1.	Верный ответ: D
А.	Неверно: подход верный, но он позволяет получить значение («Hello World»), а не ключ. При использовании показанного конфигурационного файла этот код вернет null-объект, поскольку значение Key для «Hello World» отсутствует.
В.	Неверно: хотя использование AppSettings сработает, этот подход устарел и его поддержка в будущих выпусках не гарантируется. При его использовании компилятор сгенерирует предупреждение, либо, в зависимости от параметров компиляции, его работа прервется.
С.	Неверно: подход верный, но в конфигурационном файле имеется единственное значение, а индекс равен 5. В наборе нет 5 объектов, так что использование такого кода приведет к возникновению null-ссылок.
D.	Верно: свойство AppSettings объекта ConfigurationManager вернет набор NameValueCollection, на который может ссылаться Key. Для значения «Foo» существует значение Key, и этот подход позволяет получить значение ключа —«Hello World».
2.	Верный ответ: D
А.	Неверно: секции SqlConnectionStrings нет, поэтому этот код завершится с ошибкой.
В.	Неверно: тэг <clear/> — единственное безопасное решение в этом случае. Если в файле определены другие строки подключения, либо файл будет прочитан еще раз, проблемы с дублирующимися ключами не возникнет.
С.	Неверно: у объекта ConfigurationManager нет свойства SqlConnectionString.
D.	Верно: у объекта ConfigurationManager нет свойства SqlConnectionString, вместо него следует использовать свойство connectionstrings.
3.	Верный ответ: D
А.	Неверно: интерфейс IConfigurationSectionHandler пригоден для реализации собственного механизма управления конфигурацией. Однако лучше всего использовать ApplicationSettingsBase.
В.	Неверно: ConfigurationValidatorBase используется для проверки значений конфигурационного файла, а не для реализации.
С.	Неверно: интерфейс IConfigurationSystem предназначен для внутреннего использования .NET Framework и не должен использоваться непосредственно.
D.	Верно: о&ьекг ApplicationSettingsBase оптимально подходит для управления нестандартной конфигурацией.
Занятие 2
1.	Верный ответ: D
А.	Неверно: InstallContext — свойство, а не класс, поэтому от него нельзя порождать другие классы.
В.	Неверно: InstallerCollection — набор объектов Installer, потому он не подходит на роль базового класса установщика.
818
Ответы
С.	Неверно: класс ManagedlnstallerClass используется внутренними механизмами .NET Framework и не предназначен для разработчиков.
D.	Верно: все объекты Installer следует порождать от одноименного класса.
2.	Верный ответ: А
А.	Верно: Rollback — метод класса Installer, вызываемый в случае неудачной установки.
В.	Неверно: у класса Installer нет метода Undo.
С.	Неверно: у класса Installer нет метода Clear. Вместо него отмена установки выполняется методом Rollback.
D.	Неверно: метод Uninstall используется для удаления уже установленных программ, а не для отмены незавершенной установки.
3.	Верный ответ: D
А.	Неверно: представление Custom Actions позволяет использовать нестандартные действия во время установки и не влияет на реестр.
В.	Неверно: представление File System позволяет вносить изменения только в файловую систему и не влияет на реестр.
С.	Неверно: представления Registry Editor не существует.
D.	Верно: для просмотра изменений, вносимых в реестр, служит представление Registry.
Занятие 3
1.	Верные ответы: С и D
А.	Неверно: для просмотра процессов служит класс Process, а не .NET Framework Configuration.
В.	Неверно: .NET Framework Configuration не позволяет просматривать службы.
С.	Верно: настроенными приложениями управляют средствами узла Applications.
D.	Верно: сборками в GAC управляют средствами узла Assembly Cache.
2.	Верный ответ: А
А.	Верно: наборы разрешений можно добавлять, а также управлять ими можно при помощи утилиты для конфигурации.
В.	Неверно: эта утилита управляет группами кода (Code Groups), а не наборами кода.
С.	Неверно: наборов сборок не существует.
D.	Неверно: наборов приложений не существует.
Занятие 4
1.	Верные ответы: А, В, С и D
А.	Верно: метод OpenExeConfiguration подходит для чтения конфигурационного файла.
В.	Верно: метод OpenMachineConfiguration подходит для чтения файла конфигурации компьютера.
С.	Верно: метод OpenMappedExeConfiguration подходит для чтения конфигурационного файла, если задан соответствующий .ехе-файл.
D.	Верно: метод OpenMappedMachineConfiguration подходит для чтения конфигурационного файла компьютера.
Ответы
819
2.	Верный ответ: А
А.	Верно: Create — единственный метод из интерфейса IConfigurationSectionHandler, который требуется реализовать.
В.	Неверно: метода ReadSection нет в интерфейсе IConfigurationSectionHandler.
С.	Неверно: GetConfig для этого параметра не подходит.
D.	Неверно: GetAppSettings для этого параметра не подходит.
Глава 9. Практикум
1. Эту задачу можно решить, только используя формат, понятный человеку. Пользователям требуется возможность настройки параметров, поэтому лучше всего использовать формат, подобный XML.
2. Желательно создать ярлык для приложения на рабочем столе и группу в меню Пуск (Start). У файлов, которые создает приложение, также должен быть связанный тип.
Глава 10. Закрепление материала
Занятие 1
1.	Верный ответ: В
А.	Неверно: поскольку этот код может использоваться частично доверяемыми объектами, и, возможно, вредоносным кодом, следует использовать объекты EventLog с осторожностью.
В.	Верно: объекты EventLog безопасны, если они не передаются частично доверяемым сборкам.
С.	Неверно: объекты EventLog безопасны при наличии подходящих границ безопасности.
D.	Неверно: реализовать такое решение невозможно.
2.	Верный ответ: А, В и С
А.	Верно: чрезмерное использование EventLog чревато проблемами из-за нехватки места на диске.
В.	Верно: использование объектов EventLog в частично доверяемой среде несет множество угроз для безопасности.
С.	Верно: объекты EventLog используют множество системных ресурсов, поэтому их бесконтрольное использование излишне увеличивает нагрузку на систему.
D.	Неверно: они безопасны при правильном использовании, но требуют особых разрешений, их также следует осторожно использовать в частично доверяемых средах.
3.	Верный ответ: С
А.	Неверно: метод Clear обрабатывает все записи, поэтому вызывать его для каждой записи нет необходимости.
В.	Неверно: метод Clear один в состоянии решить эту задачу.	<
С.	Верно: есть только один метод, способный решить эту задачу.
i D. Неверно: метода ClearAll нет, задача решается методом Clear.
820
Ответы
4.	Верный ответ: С
А.	Неверно: метода ClearLog у класса EventLog нет.
В.	Неверно: метода RemoveLog у класса EventLog нет.
С.	Верно: Delete — единственный метод для удаления объектов EventLog.
D.	Неверно: метода RemoveLog у класса EventLog нет.
5.	Верные ответы: А, В, С и D
А.	Верно: записи типа «ошибка» допустимы.
В.	Верно: записи типа «предупреждение» допустимы.
С.	Верно: записи типа «уведомление» допустимы.
D.	Верно: записи типа «аудит успехов» допустимы.
6.	Верные ответы: А, В и С
А.	Верно: журнал Приложение (Application) ведется по умолчанию.
В.	Верно: журнал Безопасность (Security) ведется по умолчанию.
С.	Верно: журнал Система (System) ведется по умолчанию.
D.	Неверно: журнал Аудит (Audit) по умолчанию не ведется.
Занятие 2
1.	Верный ответ: А
А.	Верно: основная функция CorrelationManager — обеспечение уникальности идентификационных данных.
В.	Неверно: трассировочные данные можно изменить с помощью TraceSwitch, данную задачу он не решит, а только внесет путаницу.
С.	Неверно: указав другое значение TracelLevel для каждого процесса, можно изменить трассировочные данные, но логически разделить их так не удастся.
D.	Неверно: хотя CorrelationManager позволит достичь желаемого результата, он не управляет объектами Correlation.
2.	Верный ответ: В
А.	Неверно: атрибут DebuggerDisplayAttribute не указывает объект визуализации.
В.	Верно: DebuggerVisualizerAttribute — единственный атрибут, который может указывать объект визуализации.
С.	Неверно: атрибут DebuggerStepThroughAttribute служит для отладки кода без захода и не влияет на применяемые объекты визуализации.
D.	Неверно: DebuggableAttribute не подключает объекты визуализации.
Занятие 3
1.	Верный ответ: С
А.	Неверно: хотя этот метод запускает экземпляр Internet Explorer, он не позволяет открыть заданную страницу.
В.	Неверно: при этом все вводимые данные обрабатываются как одно значение, что введет к ошибке.
С.	Верно: этот метод принимает имя приложения как первый параметр и верный URL как единственный аргумент командной строки.
D.	Неверно: у класса Process нет метода Process.Run.
Ответы
821
2.	Верный ответ: D
А.	Неверно: Debug.Assert принимает вычисляемое выражение, а не String.
В.	Неверно: этот метод вызывает неудачу проверки, но вместо StackTrace выводит исключение целиком и не выводит его в окне консоли.
С.	Неверно: этот метод выводит StackTrace в окне консоли, при этом он так же выводит исключение целиком.
D.	Верно: этот метод выводит в окне консоли только StackTrace.
3.	Верный ответ: D
А.	Неверно: по соображениям безопасности пароли не следует хранить в объектах String.
В.	Неверно: массив Char отличается от String, но так же не обеспечивает необходимой безопасности.
С.	Неверно: элемент массива String не отличается от отдельного объекта этого типа.
D.	Верно: тип System.Security.SecureString подходит для хранения в свойстве Password объекта ProcessStartlnfo, так как он самый защищенный из перечисленных типов.
Занятие 4
1.	Верный ответ: С
А.	Неверно: WMI определяет сетевые адаптеры.
В.	Неверно: WMI определяет и опрашивает логические диски.
С.	Верно: WMI позволяет получать некоторые сведения из БД, получить полный список OleDb-совместимых БД с помощью запросов WMI нельзя.
D.	Неверно: WMI предоставляет список работающих служб с указанием их состояния.
Глава 10. Практикум
Чтобы решить поставленную задачу, выполните следующие действия:
1.	Добавьте на уровне доступа к данным счетчики производительности (используйте .NET Data Provider для SQL Server). Отслеживайте все параметры объектов Connection, так как есть подозрение на утечку, в частности, число подключений в пуле и число освобожденных мониторов подключений.
2.	Для каждого из Web-компонентов добавьте счетчики для ASP.NET. Web-сервер — возможная причина снижения производительности, но он не так сильно влияет на производительность, как БД, поэтому много счетчиков для него не потребуется.
3.	Для мониторинга утилизации сети приложением добавьте счетчик производительности .NET CLR Networking performance.
4.	Выполните аналогичные действия для клиентского кода, чтобы вести мониторинг утилизации памяти, сетевого трафика и файлового ввода-вывода.
5.	Регистрируйте в журнале событий Windows все условия, в которых приложение загружает процессор сильнее заданного уровня.
6.	Добавьте код, проверяющий ваши допущения о работе приложения.
7.	Включите трассировку и добавьте побольше кода для мониторинга различных ис-ключений и вызывающих их обстоятельств.
822
Ответы
Глава 11. Закрепление материала
Занятие 1
1.	Верные ответы: В и D
А.	Неверно: доказательство Zone определяется тем, откуда запущена сборка, а не строгим именем.
В.	Верно: доказательство Strong Name имеется только у подписанных сборок.
С.	Неверно: доказательство Hash основано на уникальном значении, рассчитанном по двоичному файлу сборки, строгое имя ему не требуется.
D.	Верно: доказательство Publisher имеется только у подписанных сборок.
2.	Верный ответ: В
А.	Неверно: разрешение SocketPermission связано с работой сети, но требуется для создания необработанных TCP/IP-подключений, а не соединения с Web по протоколу HTTP.
В.	Верно: разрешение WebPermission необходимо для отправки НТТР-запросов Web-серверу.
С.	Неверно: разрешение DnsPermission необходимо для работы DNS и часто (но не обязательно) входит в Web-запрос.
D.	Неверно: разрешение ServiceControllerPermission управляет возможностью запуска, остановки и приостановки служб.
3.	Верный ответ: D
А.	Неверно: зона My_Computer_Zone использует набор разрешений FullTrust и предоставляет наивысший уровень привилегий.
В.	Неверно: зона LocalIntranet_Zone использует набор разрешений Localintranet и предоставляет сравнительно высокий уровень привилегий.
С.	Неверно: зона Intemet_Zone использует набор разрешений Internet permission и предоставляет очень ограниченный набор привилегий.
D.	Верно: зона Restricted Zone использует набор разрешений Nothing, не предоставляющий вообще никаких привилегий.
4.	Верный ответ: А
А.	Верно: прочитать файл возможно, поскольку это позволяют разрешения учетной записи пользователя и CAS.
В.	Неверно: CAS-разрешения сборки разрешают запись в файл, но разрешения пользователя дают право только на чтение.
С.	Неверно: CAS-разрешения сборки позволяют изменять разрешения файла, но разрешения пользователя дают право только на чтение.
D.	Неверно: CAS-разрешения сборки позволяют удалять файл, но разрешения пользователя дают право только на чтение.
Занятие 2
1.	Верный ответ: С
А.	Неверно: набора разрешений Everything достаточно для работы приложений.
В.	Неверно: набора разрешений Everything достаточно для работы приложений.
С.	Верно: декларативные разрешения не помешают сборке прочитать первую строку файла C:\boot.ini.
Ответы
823
D.	Неверно: исключение из-за нарушения безопасности возникнет, только если во время запуска сборки с отладчиком администратором будет удален запрос разрешения UlPermission.
2.	Верный ответ: В
А.	Неверно: разрешений достаточно для вывода первой строки, но исполняющая среда сгенерирует исключение при обращении к файлу в корне диска С:\.
В.	Верно: исполняющая среда сгенерирует исключение при обращении к файлу в корне диска С:\, поскольку объявление SecurityAction.RequestOptional FilelOPermissionAttribute дает доступ только к папке C:\Temp.
С.	Неверно: декларативное разрешение SecurityAction.RequestOptional запрещает доступ к корневой папке диска С:\.
D.	Неверно: исключение из-за нарушения безопасности возникнет, только если во время запуска сборки с отладчиком администратором будет удален запрос разрешения UlPermission.
3.	Верный ответ: С
А.	Неверно: запрос SecurityAction.RequestOptional отсутствует, поэтому у сборки отозваны только разрешения, перечисленные в объявлении SecurityAction.RequestRefuse.
В.	Неверно: запрос SecurityAction.RequestOptional отсутствует, поэтому у сборки отозваны только разрешения, перечисленные в объявлении SecurityAction.RequestRefuse.
С.	Верно: у сборки есть разрешение на чтение файлов в корне диска С:\, поскольку она обладает разрешением Everything, которое не было отозвано явно.
D.	Неверно: исключение из-за нарушения безопасности возникнет, только если во время запуска сборки с отладчиком администратором будет удален запрос разрешения UlPermission.
4.	Верный ответ: С
А.	Неверно: разрешение SocketPermission управляет доступом к сети и не требуется для консольных приложений.
В.	Неверно: разрешение WebPermission управляет доступом к HTTP- запросам и не требуется для консольных приложений.
С.	Верно: разрешение UlPermission необходимо консольным приложениям для взаимодействия с отладчиком во время отладки.
D.	Неверно: разрешение FilelOPermission управляет доступом к файловой системе и не требуется для консольных приложений.
Занятие 3
1.	Верный ответ: А
А.	Верно: SecurityAction.Demand приказывает исполняющей среде генерировать исключение, если у данного вызова и всех вызовов, расположенных в стеке выше, отсутствует заданное разрешение.
В.	Неверно: SecurityAction.Deny приказывает исполняющей среде ограничить права метода, отозвав у него заданное разрешение.
С.	Неверно: SecurityAction.Assert приказывает исполняющей среде игнорировать отсутствие у вызывающего заданного разрешения (у сборки должен быть установлен параметр Assert Any Permission That Has Been Granted).
D.	Неверно: SecurityAction.RequestMinimum используется для декларативной проверки разрешений.
824
Ответы
2.	Верный ответ: D
А.	Неверно: SecurityAction приказывает исполняющей среде генерировать исключение, если у данного вызова и всех вызовов, расположенных в стеке выше, отсутствует заданное разрешение. Однако SecurityAction.Demand используется императивно, а по условию требуется декларативная защита.
В.	Неверно: SecurityAction.Deny приказывает исполняющей среде ограничить права метода, отозвав у него заданное разрешение.
С.	Неверно: SecurityAction.Assert приказывает исполняющей среде игнорировать отсутствие у вызывающего заданного разрешения.
D.	Верно: SecurityAction. Request Minimum используется для декларативной проверки разрешений; если у вызывающего нет нужных привилегий, исполняющая среда генерирует исключение.
3.	Верный ответ: D
А.	Неверно: вызов IPermission.Deny генерирует исключение, если отсутствует разрешение.
В.	Неверно: метод IsGranted является членом класса Security Manager, а не IPermission.
С.	Неверно: метод Deny является членом интерфейса IPermission, а не класса SecurityManager.
D.	Верно: метод Boolean Security Manager. IsGranted позволяет узнать, есть ли у сборки заданное разрешение.
4.	Верные ответы: В и D
А.	Неверно: метода EventLogPermission.RevertPermitOnly нет.
В.	Верно: вызов CodeAccessPermission.RevertPermitOnly отменяет действие вызова EventLogPermission.PermitOnly.
С.	Неверно: метод RevertAll является членом класса CodeAccessPermission, а не EventLogPermission.
D.	Верно: вызов CodeAccessPermission.RevertAll отменяет вызов EventLogPermission.PermitOnly и EventLogPermission. Deny.
Е.	Неверно: метод Revert Deny является членом класса CodeAccessPermission, а не EventLogPermission.
Е Неверно: вызов CodeAccessPermission.RevertDeny отменяет вызов EventLogPermission.Deny, но не вызов EventLogPermission. PermitOnly.
Глава 11. Практикум
Упражнение 1
1.	Нет. Коль скоро операционная система допускает исполнение неуправляемого кода, вирусы смогут обойти CAS, просто отказываясь от использования .NET Framework. Для неуправляемого кода CAS-разрешения не проверяются.
2.	Нет. Впрочем, будет возможен запуск приложений, использующих .NET Framework.
3.	Нет, вирус, написанный с использованием .NET Framework, получит набор разрешений Internet, который разрешает сборкам обращаться через сеть только к сайту, с которого они были получены.
4.	Нет. Зона Intranet запрещает сборкам обращаться к файловой системе напрямую, максимум — запрашивать у пользователя согласие на то, чтобы открыть или записать файлы.
Ответы
825
Упражнение 2
1.	Встроив поддержку CAS в приложение, вы сможете ограничивать доступные ему разрешения, в частности, доступ к файловой системе. Используйте объявления сборки, чтобы ограничить ее разрешения до необходимого минимума. Защищайте методы при помощи соответствующих объявлений. Наконец, управлять разрешениями метода можно при помощи императивной защиты CAS. Если методу не требуется записывать данные в файлы, следует отозвать у него соответствующее право.
2.	Следует использовать разрешение FilelOPermission.
3.	Никакого негативного эффекта не ожидается.
Глава 12. Закрепление материала
Занятие 1
1.	Верный ответ: D
А.	Неверно: Windows Principal. Is In Role используется для императивной проверки членства в группах, однако декларативная RBS обеспечивает более надежную защиту, проверяя разрешения до запуска метода.
В.	Неверно: метод IsInRole является членом WindowsPrincipal, а не Windowsldentity.
С.	Неверно: для проверки членства в группах можно использовать императивную RBS, но декларативная RBS обеспечивает более надежную защиту, проверяя разрешения до запуска метода.
D.	Верно: декларативная RBS ограничивает доступ к методу в целом, обеспечивая максимально надежную защиту от уязвимостей системы безопасности.
2.	Верный ответ: С
А.	Неверно: WindowsPrincipal.IslnRole используется для императивной проверки членства в группах, однако декларативная RBS обеспечивает более надежную защиту, проверяя разрешения до запуска метода.
В.	Неверно: метод IsInRole является членом WindowsPrincipal, а не Windowsldentity.
С.	Верно: императивная RBS ограничивает доступ кода путем генерации исключений, которые можно без труда перехватить и вывести пользователю соответствующие сообщения об ошибках.
D.	Неверно: хотя декларативная RBS обеспечивает более надежную защиту, чем императивная, требования первой определяются как атрибуты метода. Поэтому труднее перехватывать исключения, сгенерированные декларативной RBS при вызове обработчика события в Windows.
3.	Верный ответ: А
А.	Верно: WindowsPrincipal.IslnRole идеален для ветвления алгоритма в зависимости от членства в группах, если не требуется надежная защита.
В.	Неверно: метод IsInRole является членом класса WindowsPrincipal, а не Windowsldentity.
С.	Неверно: императивная RBS ограничивает доступ кода, генерируя исключения. Исключения прерывают обработку, поэтому императивную RBS следует использовать, только когда нужно полностью запретить пользователю исполнение кода.
826
Ответы
D.	Неверно: декларативная RBS полностью запрещает пользователю исполнение метода. По условию же методу требуется принимать решения в зависимости от членства в группах; WindowsPrincipal. Is In Role позволяет сделать это, не блокируя доступ к методу целиком.
4.	Верные ответы: А и В
А.	Верно: Genericidentity представляет отдельных пользователей, поддерживает императивную и декларативную RBS, а значит, соответствует этим требованиям.
В.	Верно: GenericPrincipal представляет группы пользователей, поддерживает императивную и декларативную RBS, а значит, соответствует этим требованиям.
С.	Неверно: Ildentity представляет пользователей, но Genericidentity лучше соответствует условию.
D.	Неверно: Ildentity представляет пользователей, но Genericidentity лучше соответствует условию.
Занятие 2
1.	Верные ответы: А и В
А.	Верно: класс FileSecurity управляет доступом к файлам.
В.	Верно: класс RegistrySecurity управляет доступом к разделам реестра.
С.	Неверно: в .NET Framework нет библиотек для настройки разрешений на доступ к принтерам.
D.	Неверно: в .NET Framework нет библиотек для настройки разрешений на доступ к общим ресурсам.
2.	Верный ответ: А
А.	Верно: чтобы применить ACL к папке, вызывают метод Directory.SetAccessControl, принимающий путь к каталогу и объект Directory Security, содержащий ACL.
В.	Неверно: можно создать каталог, указав объект Directory Security, но для нормальной работы этого кода каталог должен быть создан заранее.
С.	Неверно: Directory.SetAccessControl требует объект DirectorySecurity и путь к каталогу.
D.	Неверно: можно создать каталог, указав объект DirectorySecurity, но для нормальной работы этого кода каталог должен быть создан заранее. Кроме того, нельзя создать каталог, указав лишь объект DirectorySecurity.
3.	Верный ответ: В
А.	Неверно: RegistryAccessRule описывает DACL раздела реестра.
В.	Верно: RegistryAuditRule описывает SACL раздела реестра.
С.	Неверно: AccessRule — базовый класс для всех DACL.
D.	Неверно: AuditRule — базовый класс для всех объектов SACL, а не только объектов SACL разделов реестра. Класс RegistryAuditRule, производный от AuditRule, представляет SACL разделов реестра.
4.	Верный ответ: С
А.	Неверно: DirectorySecurity.GetAccessRules возвращает экземпляр AuthorizationRuleCollection с объектами FileSystemAccessRule.
В.	Неверно: DirectorySecurity.GetAccessRules возвращает экземпляр AuthorizationRuleCollection с объектами FileSystemAccessRule.
С.	Верно: DirectorySecurity.GetAccessRules возвращает экземпляр AuthorizationRuleCollection с объектами FileSystemAccessRule.
Ответы
827
D.	Неверно: несмотря на возможность обработки объектов AuthorizationRule в наборе AuthorizationRuleCollection, у вас не будет доступа к важным элементам ACL файловой системы. Вместо этого следует использовать класс FileSystemAccessRule, производный от AuthorizationRule.
Занятие 3
1.	Верные ответы: В, С, Е и F
А.	Неверно: класс RSACryptoServiceProvider поддерживает асимметричное шифрование, требующие разных (но связанных) ключей для шифрования и расшифровки.
В.	Верно: класс RijndaelManaged поддерживает симметричное шифрование, использующее идентичные ключи для шифрования и расшифровки.
С,	Верно: класс TripleDES поддерживает симметричное шифрование, использующее идентичные ключи для шифрования и расшифровки.
D.	Неверно: класс DSACryptoServiceProvider поддерживает цифровые подписи и асимметричное шифрование, требующие разных (но связанных) ключей для шифрования и расшифровки.
Е.	Верно: класс DES поддерживает симметричное шифрование, использующее идентичные ключи для шифрования и расшифровки.
Е Верно: класс RC2 поддерживает симметричное шифрование, использующее идентичные ключи для шифрования и расшифровки.
2.	Верные ответы: А, С и D
А.	Верно: значения SymmetricAlgorithm.Key у шифрующей и расшифровывающей сторон должны быть идентичными.
В.	Неверно: в симметричном шифровании не используются «заготовки» ключей, наподобие тех, что применяются при создании ключей на основе паролей при помощи класса Rfc2898DeriveBytes.
С.	Верно: значения SymmetricAlgorithm.IV у шифрующей и расшифровывающей сторон должны быть идентичными.
D.	Верно: значения SymmetricAlgorithm.Mode у шифрующей и расшифровывающей сторон должны быть идентичными.
3.	Верный ответ: С
А.	Неверно: при передаче данных по сети требуется экспортировать только открытый ключ. Поскольку для последующих сеансов будут созданы другие ключи, нет необходимости сохранять закрытый ключ.
В.	Неверно: чтобы расшифровывать файл, зашифрованный удаленным компьютером, прежде нужно передать этому компьютеру свой открытый ключ. Экспортировать закрытый ключ не требуется.
С.	Верно: для расшифровки файла, зашифрованного открытым ключом, необходим закрытый ключ.
D.	Неверно: файл, предназначенный для отправки удаленному компьютеру, следует зашифровать его открытым ключом, генерировать и экспортировать ключи не требуется.
4.	Верные ответы: В н D
А.	Неверно: RIPEMD160 — хэш-алгоритм, не использующий ключи.
В.	Верно: HMACSHA1 — хэш-алгоритм, использующий ключ.
828
Ответы
С.	Неверно: SHA512 — хэш-алгоритм, не использующий ключи.
D.	Верно: MACTripleDES — хэш-алгоритм, использующий ключ.
Е.	Неверно: MD5 — хэш-алгоритм, не использующий ключи.
Глава 12. Практикум
Упражнение 1
1.	Используйте Genericidentity и GenericPrincipal, поскольку простые соотношения между пользователями и группами, которые используются бухгалтерским приложением, не требуют создания нестандартных классов, основанных на Ildentity и IPrincipal.
2.	Используйте декларативную RBS для ограничения доступа к AddBill. Пользователи, прошедшие аутентификацию и являющиеся членами групп Temps, Accountants или Managers, могут использовать этот метод.
3.	Используйте декларативную RBS для ограничения прав аутентифицированных членов групп Accountants и Managers. В этом методе используйте метод GenericPrincipal.IsInRole для проверки членства пользователей в группе Managers, а также максимальной суммы, которую пользователь может санкционировать к оплате (1500 долл, и больше).
Упражнение 2
1.	По условию подходит и симметричное, и асимметричное шифрование, но лучше использовать симметричное из-за меньших издержек на администрирование. Этому способствует и тот факт, что данные предполагается передавать между стационарными компьютерами, расположенными в удаленных офисах, на которых сравнительно легко настроить общий секретный ключ. Альтернативный вариант — использование асимметричного шифрования. При этом пары ключей хранятся в центральной БД, а открытые ключи распространяются по удаленным офисам. Далее с использованием асимметричных ключей приложения генерируют симметричные сеансовые ключи для защиты передачи данных.
2.	Как минимум, необходимо хранить хэш-паролей. Хэш-алгоритмы с ключом более стойкие по сравнению с алгоритмами, не использующими ключей. Основная цель хэширования паролей — предотвратить доступ атакующих к паролям.
3.	Следует использовать цифровые подписи; хранить пары ключей в центральном офисе и рассылать открытые ключи в удаленные офисы. Все конфиденциальные данные при передаче через каналы связи необходимо подписывать закрытым ключом, а в удаленных офисах — проверять цифровую подпись.
Глава 13. Закрепление материала
Занятие 1
1.	Верные ответы: А, В и D
А.	Верно: при добавлении ссылки в Visual Studio 2005 все действия, необходимые для импорта библиотеки СОМ выполняются автоматически.
В.	Верно: Tlblmport.exe позволяет импортировать типы, хотя эта утилита командной строки считается менее удобной, чем добавление ссылки.
Ответы
829
С.	Неверно: утилита Regsrv регистрирует компоненты СОМ, она используется независимо от .NET-приложений.
D.	Верно: прежде всего, при помощи утилиты Regsvr следует убедиться, что компонент СОМ надлежащим образом зарегистрирован, а затем импортировать его любым из методов.
2.	Верные ответы: С и D
А.	Неверно: при перехвате ApplicationException будут перехвачены только CLS-совместимые исключения, специфичные для данного приложения.
В.	Неверно: установка атрибута RuntimeCompatibilty в false отключает включенный по умолчанию перехват System.Exceptions только в случае их совместимости с CLS.
С.	Верно: System.Exception перехватывает как CLS-совместимые, так и несовместимые исключения.
D.	Верно: это не требуется, так как данная функция активна по умолчанию; при этом System.Exception будет перехватывать как CLS-совместимые, так и несовместимые исключения.
3.	Верные ответы: А, В. С и D
А.	Верно: любые члены объектов СОМ должны быть реализованы как члены экземпляров, а не статические (общие) члены. В .NET Framework последние используются очень часто, поэтому объекты, созданные в этих средах, сильно отличаются.
В.	Верно: СОМ не поддерживает конструкторы с параметрами, a .NET Framework — поддерживает.
С.	Верно: объекты СОМ необходимо регистрировать в реестре Windows, а у других ОС реестра нет.
D.	Верно: в СОМ иерархия наследования существенно более «плоская», а возможности наследования сильно ограничены.
4.	Верные ответы: А, С и D
А.	Верно: это позволит просматривать IL-код, сгенерированный после импорта компонента СОМ.
В.	Неверно: эта утилита не поддерживает COM Interop.
С.	Верно: эта утилита позволяет импортировать компоненты СОМ и управлять именами, под которыми они импортированы.
D.	Верно: все компоненты СОМ должны быть зарегистрированы в системном реестре Windows, этот инструмент позволяет просматривать изменения, внесенные в реестр Windows.
Занятие 2
1.	Верные ответы: А, В, С и D
А.	Верно: этого параметра достаточно, чтобы компонент СОМ смог использовать сборку.
В.	Верно: этот параметр принимает значения true или false. Если установить его в true, класс станет доступным компонентам СОМ, если его члены не были переопределены.
С.	Верно: для членов, которые следует сделать недоступными для СОМ, следует ус-щ. тановить атрибут ComVisible в false.
830
Ответы
D.	Верно: для членов, которые следует сделать доступными для СОМ, следует установить атрибут ComVisible в true либо оставить его неопределенным (при условии, что класс или сборка помечены как Visible). Установка значения true не вызовет проблем, даже если оно уже установлено на более высоких уровнях иерархии.
2.	Верный ответ: В
А.	Неверно: Tlblmp.exe импортирует тип, а не экспортирует его.
В.	Верно: TlbExp.exe — единственный инструмент для экспорта типов.
С.	Неверно: Regedit.exe не экспортирует типы, но позволяет просматривать записи реестра для типов.
D.	Неверно: инструмент csc.exe компилирует приложения, но сам не открывает доступ к типам для компонентов СОМ.
3.	Верный ответ: А
А.	Верно: СОМ поддерживает только конструкторы без параметров.
В.	Неверно: закрытые классы недоступны для компонентов СОМ.
С.	Неверно: закрытые члены недоступны для компонентов СОМ.
D.	Неверно: абстрактные классы не поддерживаются компонентами СОМ.
Занятие 3
1.	Верный ответ: А
А.	Верно: атрибут Dlllmport следует использовать с именем вызываемой DLL.
В.	Неверно: RCW облегчает использование внешних библиотек, но для решения данной задачи не нужна.
С.	Неверно: сигнатуры параметров метода и DLL должны максимально соответствовать. В некоторых случаях удается вызвать DLL и без этого, но последствия могут быть нежелательными.
D.	Неверно: объекты String — ссылочные типы, как и StringBuilder. Но, в отличие от String, StringBuilder работают как настоящие ссылочные типы, и лучше подходят для данной задачи.
2.	Верные ответы: А, В и С
А.	Верно: перед использованием структуру следует объявить. В некоторых случаях проблему можно решить с помощью метода MarshalAs.
В.	Верно: метод SizeOf— единственный механизм для определения размера структуры.
С.	Верно: атрибут StructLayout позволяет указать положение и формат структуры, если он отличается от ожидаемого библиотекой.
D.	Неверно: хотя инструмент TlbExport создает необходимые библиотеки типов, передавать структуры он не позволяет.
3.	Верные ответы: А и D
А.	Верно: управление типами — основное назначение этого атрибута.
В.	Неверно: это необходимо для некоторых (но не для всех) вызовов P/Invoke. Несо» ответствие типов возникает не при каждом вызове.
С.	Неверно: несоответствие типов игнорировать нельзя.
D.	Верно: явно определять связи не обязательно, однако это не повредит и облегчит другим разработчикам чтение вашего кода.
Ответы
831
Глава 13. Практикум
1.	Сначала необходимо выяснить множество вопросов. Так, необходимо определить и задокументировать все критически важные функции. Для каждой из этих функций нужно узнать, есть ли подходящий для нее .NET-объект. если да, следует использовать встроенный объект, если нет — следует воспользоваться библиотекой-оболочкой.
2.	Основной критерий — наличие готовой управляемой библиотеки. Если таковой нет, нужно решить, достаточно ли управляемых классов, чтобы вынести их в отдельную библиотеку.
3.	Новые функции .NET Framework позволяют перехватывать как управляемые, так и неуправляемые исключения. Перехват System.Exception работает чуть иначе, но эти отличия мало влияют на тестирование новых компонентов.
4.	Потребность в вызове неуправляемого кода полностью определяется задачами кода, и обычно она существует. Так, для некоторых функций Windows API еще нет .NET-оболочек. Так что при решении некоторых задач без использования неуправляемого кода не обойтись.
Глава 14. Закрепление материала
Занятие 1
1.	Верный ответ: С
А.	Неверно: GetCallingAssembly возвращает сборку, содержащую код, который вызвал текущий метод. Возможно, эта не та сборка, что содержит вызывающий объект или точку входа.
В.	Неверно: GetExecutingAssembly возвращает сборку, содержащую код, исполняемый в данное время. Возможно, эта не та сборка, что содержит вызывающий объект или точку входа.
С.	Верно: GetEntryAssembly возвращает сборку, содержащую точку входа.
D.	Неверно: Assembly.Load возвращает сборку, которая не загружена в данный момент, и потому не может точку входа.
2.	Верные ответы: В и D
А.	Неверно: сборки не могут содержать другие сборки.
В.	Верно: в сборках может быть несколько модулей.
С.	Неверно: типы не могут располагаться непосредственно в сборках. Типы находятся внутри модулей, а те — внутри сборки.
D.	Верно: только модули могут быть непосредственными контейнерами типов.
Занятие 2
1.	Верный ответ: В
А.	Неверно: этот атрибут приказывает компилятору сгенерировать сопутствующую сборку с локализованными для заданной культуры ресурсами
В.	Верно: этот атрибут приказывает компилятору сгенерировать сопутствующую сборку с локализованными для заданной культуры ресурсами, которая не может быть главной сборкой.
832
Ответы
С.	Неверно: этот атрибут приказывает компилятору сгенерировать сопутствующую сборку с локализованными для заданной культуры ресурсами, которая не может быть главной сборкой.
D.	Неверно: этот атрибут приказывает компилятору сгенерировать сопутствующую сборку с локализованными для заданной культуры ресурсами, которая не может быть главной сборкой.
2.	Верный ответ: D
А.	Неверно: звездочка не может быть в номере версии. Автоматически увеличиваются номер сборки, но не номер ревизии.
В.	Неверно: звездочка не влияет на совместимость. Автоматически увеличиваются номер сборки, но не номер ревизии.
С.	Неверно: при этом компилятор генерирует случайное значение для номера ревизии. Автоматически увеличиваются номер сборки, но не номер ревизии.
D.	Верно: при этом компилятор генерирует случайное значение для номера ревизии.
Занятие 3
1.	Верные ответы: А, В и D
А.	Верно: класс Fieldinfo происходит от класса Memberinfo.
В.	Верно: класс Methodinfo происходит от класса Memberinfo.
С.	Неверно: класс Assembly не является производным от класса Memberinfo.
D.	Верно: класс Туре происходит от класса Memberinfo.
2.	Верные ответы: А, С и D
А.	Верно: BindingFlags. Public позволяет получать открытые члены.
В.	Неверно: BindingFlags.Static позволяет получать статические члены; BindingFlags.Instance позволяет получать члены, не являющиеся статическими.
С.	Верно: BindingFlags. DeclaredOnly позволяет получать члены, объявленные в данном классе, а не унаследованные от базового класса.
D.	Верно: BindingFlags.Instance позволяет получать члены экземпляров, а не статические члены.
Занятие 4
1.	Верный ответ: D
А.	Неверно: возвращаемое значение передает метод Invoke, поэтому объекта-аргумента для вызова метода нет; параметры передаются как массив объектов.
В.	Неверно: объекта-аргумента для вызова метода нет; параметры передаются как массив объектов.
С.	Неверно: параметры передаются как массив объектов.
D.	Верно: возвращаемое значение передает метод Invoke, поэтому объект-аргумент для вызова метода имеется; параметры передаются как массив объектов.
2.	Верный ответ: В
А.	Неверно: нет, метод MethodBody класс позволяет опрашивать IL-код и другие свойства метода, но не позволяет исполнять его код по частям.
В.	Верно: класс MethodBody позволяет опрашивать IL-код и другие свойства метода, но не позволяет исполнять его код по частям.
Ответы
833
Занятие 5
1.	Верный ответ: С
А.	Неверно: у класса AssemblyBuilder нет методов (ни статических, ни конструкторов) для создания экземпляров AssemblyBuilder.
В.	Неверно: у класса ModuleBuilder нет методов для создания экземпляров AssemblyBuilder.
С.	Верно: у класса AppDomain есть метод DefineDynamicAssembly, который создает экземпляры AssemblyBuilder.
D.	Неверно: у класса Application нет методов для создания экземпляров AssemblyBuilder.
2.	Верные ответы: А и С
А.	Верно: AssemblyBuilderAccess.Run допускает исполнение динамически сборок.
В.	Неверно: AssemblyBuilderAccess RefiectionOnly позволяет получать информацию о типе, но не позволяет исполнять код.
С.	Верно: AssemblyBuilderAccess.RunAndSave допускает исполнение динамически сборок.
D.	Неверно: AssemblyBuilderAccess.Save не позволяет исполнять код, а только сериализовать сборки.
Глава 14. Практикум
1. Если клиенты разрабатывают свои компоненты с использованием .NET, можно будет загружать их сборки в исполняющую среду (из заданного каталога) и добавлять их компоненты к приложению, не переписывая код приложения (достаточно перезапустить приложение после добавления компонента).
2. Название компании и товарного знака можно прочитать из атрибутов сборки во время загрузки компонентов и вывести их для пользователя (на заставке или в окне About).
Глава 15. Закрепление материала
Занятие 1
1.	Верные ответы: А и В
А.	Верно: да, можно приложить локальный файл к письму, указав его имя.
В.	Верно: да, можно приложить файл при помощи объекта Stream, созданного на основе различных источников.
С.	Неверно: нет, приложить файл, взяв его напрямую с веб-сайта, невозможно; файл прежде следует загрузить и сохранить.
D.	Неверно: .NET Framework не поддерживает получение входящих сообщений электронной почты.
2.	Верные ответы: А и С
А.	Верно: чтобы отправить сообщение в формате HTML, следует присвоить свойству MailMessage. Body значение HTML.
В.	Неверно: свойства MailMessage.Head нет, заголовки следует включать в тело сообщения (хотя почтовые клиенты обычно не обрабатывают заголовки).
834
Ответы
С.	Верно: чтобы отправить сообщение в формате HTML, следует присвоить свойству MailMessage.IsBodyHtml значение True.
D.	Неверно: MailMessage.Subject всегда имеет текстовый формат, а не HTML.
3.	Верный ответ: А
А.	Верно: ссылка на связанное изображение имеет формат
<img src=”cid:ContentID”>
В.	Неверно: для ссылки на изображение имя файла не требуется.
С.	Неверно: идентификатор содержимого необходимо предварять префиксом cid:
D.	Неверно: для ссылки на изображение имя файла не требуется.
4.	Верный ответ: С
А.	Неверно: для внедрения ресурсов используется класс LinkedResource.
В.	Неверно: для вложения файлов используется класс Attachment.
С.	Верно: класс AltemateView позволяет создать несколько версий почтового сообщения, из которых почтовый клиент сможет выбрать подходящую.
D.	Неверно: для отправки сообщений используется класс SmtpClient.
Занятие 2
1.	Верный ответ: В
А.	Неверно: у класса MailMessage нет метода Send.
В.	Верно: для отправки сообщений следует создать экземпляр SmtpClient и вызвать метод Send.
С.	Неверно: класса SmtpServer нет.
D.	Неверно: класса MailCUent нет.
2.	Верные ответы: С и D
А.	Неверно: «self* не является ключевым словом, поэтому исполняющая среда попытается разрешить его как DNS-имя.
В.	Неверно: 10.0.0.1 — частный IP-адрес, но не адрес локального компьютера, поэтому исполняющая среда попытается доставить сообщение на компьютер с этим адресом.
С.	Верно: «localhost» — ключевое слово, представляющее локальный компьютер.
D.	Верно: 127.0.0.1 — особый IP-адрес, представляющий локальный компьютер.
3.	Верный ответ: А
А.	Верно: исполняющая среда генерирует исключение SmtpFailedReceipientException, если SMTP-сервер отклоняет сообщение.
В.	Неверно: SmtpFailedReceipientsException — класс, предназначенный для внутреннего использования исполняющей средой, приложения не смогут перехватывать исключения этого типа.
С.	Неверно: исполняющая среда генерирует исключение SmtpException, если не удается связаться с SMTP-сервером.
D.	Неверно: класса SmtpClientException не существует.
4.	Верный ответ: D
А.	Неверно: для использования SSL не требуется указывать учетные данные.
В.	Неверно: метод доставки изменять не нужно, подойдет значение по умолчанию (Smtp Delivery Method. Network).
Ответы
835
С.	Неверно: порт изменять не нужно, SMTP использует один и тот же порт (TCP 25) для доставки и обычных, и зашифрованных сообщений.
D.	Верно: потребуется установить в True единственное свойство, EnableSsl.
Глава 15. Практикум
1.	Для отправки электронной почты используются классы System.Net.MailMessage и System.Net.SmtpClient.
2.	.NET Framework не поддерживает классы для обработки входящей почты, создать их самостоятельно также непросто. Вместо этого можно занести в поле From адрес кого-нибудь из сотрудников отдела продаж, который сможет вручную исправлять ошибки. Альтернативный вариант — создать Web-сайт ASP.NET, на котором клиенты смогут самостоятельно исправлять свою контактную информацию.
3.	При отправке сообщений SMTP-серверу можно использовать SSL, правда, после получения сообщений такая защита перестанет действовать.
Глава 16. Закрепление материала
Занятие 1
1.	Верные ответы: В и D
А.	Неверно: этот метод сработает, но он не надежен. Если перед проверкой свойства изменить культуру, то получить текущую культуру не удастся.
В.	Верно: Thread.CurrentThread.CurrentCulture вернет значение по умолчанию, если оно не было изменено.
С.	Неверно: при использовании инвариантной культуры будет возвращена именно эта культура.
D.	Верно: так удастся получить текущую культуру, если не установлена другая культура.
2.	Верный ответ: В
А.	Неверно: US — точно определенная культура, а еп нейтральная культура.
В.	Верно: US — точно определенная культура, а еп нейтральная культура..
С.	Неверно: нейтральная культура обозначается как «еп», а не «en-US».
D.	Неверно: US — заданная точно определенная культура.
3.	Верный ответ: В
А.	Неверно: этот метод выполняет сравнение с учетом регистра.
В.	Верно: присвоив перечислимому CompareOptions значение IgnoreCase, удастся выполнять сравнение без учета регистра.
С.	Неверно: метод СотрагеТо не позволит решить эту задачу.
D.	Неверно: этот подход сработает, но только за счет изменения регистра всех строк, а сравнение будет по-прежнему чувствительно к регистру.
Занятие 2
1.	Верный ответ: С
А.	Неверно: формат денежных единиц можно изменить, просто воспользовавшись классом NumberFormatlnfo.
836
Ответы
В.	Неверно: формат денежных единиц можно изменить, просто воспользовавшись классом DateTimeFormatlnfo.
С.	Верно.
D.	Неверно: если вам не нравится представление информации, заданное стандартной культурой, можно изменить ее отдельные свойства. Полностью изменять культуру не следует, иначе те, кто не знает о внесенных изменениях, столкнуться с существенными проблемами.
2.	Верный ответ: С
А.	Неверно: ни одно значение не подойдет, если культура создается с нуля.
В.	Неверно: нейтральная культура не подойдет из-за текущих настроек.
С.	Верно: при создании собственной культуры с нуля и переопределении всех имеющихся значений подходит только замена культуры.
Глава 16. Практикум
1.	Форматы даты и денежных единиц теперь не следует «зашивать» в коде, вместо этого нужно пользоваться объектами форматирования. Не стоит строить предположения о формате дат и денежных единицы, чтобы избежать зависимости от культуры.
2.	Использование объектов форматирования позволяет решить множество задач, главная из которых — использование инвариантной культуры в операциях сравнения, а также изменение свойства CurrentCulture для приложения.
3.	Сначала следует создать локализованные ресурсы для каждой из культур, с которыми предполагается использовать приложение. Далее установите соответствующие значения CurrentUICulture и напишите код, корректно определяющий и устанавливающий соответствующую культуру.
Словарь терминов
Advanced Encryption Standard (AES) — он же алгоритм Рейндела (Rijndael), алгоритм симметричного шифрования, использующий ключи длиной 128-256 битов.
BinaryFormatter — класс из пространства имен System.Runtime.Serialization.Formatters.Binary, обеспечивает максимальную эффективность сериализации объектов (но только для .NET-приложений).
Bitmap — класс из пространства имен System.Drawing, применяемый для загрузки и сохранения сообщений, а также для редактирования отдельных точек сообщений.
Brush — класс из пространства имен System.Drawing, классы, производные от него, необходимы для прорисовки текста и залитых фигур.
CAS — см. защита доступа по правам кода.
CCW — см. COM Callable Wrapper.
CLS-совместимое исключение ~ CLS-compliant exception — любой объект исключения, управляемый .NET Framework. Все CLS-совместимые исключения происходят из иерархии System.Exception. CLS, Common Language Specification — общеязыковая спецификация.
COM Callable Wrapper (CCW) — Класс прокси, расположенный между .NET-сборкой и компонентом СОМ, позволяет последнему использовать .NET-сборку.
DACL — см. Спискок управления избирательным доступом.
Data Encryption Standard (DES) — алгоритм симметричного шифрования, использующий сравнительно короткие ключи; уязвим к атакам взломщиков.
Debug — константа, определенная в приложении; позволяет подключать к коду объекты Debugger.
Debug — класс, обеспечивающий доступ к отладчику по умолчанию, подключенному к приложению.
DES (Data Encryption Standard) — алгоритм симметричного шифрования, использующий сравнительно короткие ключи, уязвимый к атакам взломщиков.
evidence — см. доказательство сборки. Исключение ~ exception — непредвиденное событие, прерывающее нормальное исполнение сборки.
Graph — класс из пространства имен System.Drawing, обеспечивающий рисование линий, фигур и текста.
Interop — Сокращение от Interoperation, механизм, обеспечивающий взаимодействие управляемого и неуправляемого кода.
ГУ — см. вектор инициализации.
Local System — служебная учетная запись, обладающая практически безграничными привилегиями.
838 Словарь терминов
MD5 — хэш-алгоритм Message Digest, генерирует хэш размером 128 бит.
Multipurpose Internet Mail Extensions (MIME) — стандарт, обеспечивающий чтение и публикацию двоичных данных в Интернете. MIME-тип данных хранится в заголовке файла с двоичными данными. Эти сведения указывают клиентским программам (Web-браузерам и почтовым клиентам), как обрабатывать такие данные.
.NET Framework 2.0 Configuration — утилита с графическим интерфейсом из состава .NET Framework, применяется для настройки приложений и сборок и управления ими.
Network — служебная учетная запись, применяемая для аутентификации удаленных компьютеров.
Реп — класс из пространства имен System.Drawing, позволяет задавать цвет и толщину линий при рисовании.
Platform Invoke — механизм вызова неуправляемого кода из управляемого.
RC2 — стандартный алгоритм симметричного шифрования, замена DES; использует ключи переменной длины.
RCW (Runtime Callable Wrapper) — класс прокси, расположенный между компонентом СОМ и .NET-сборкой; позволяет последней использовать этот компонент.
Rijndael — см. AES.
Runtime Callable Wrapper (RCW) — см. RCW.
Secure Sockets Layer (SSL) — стандарт защиты сетевого взаимодействия при помощи шифрования с открытым ключом.
SHA1 — Secure Hash Algorithm 1, алгоритм, генерирующий хэш длиной 160 бит.
Simple Message Transfer Protocol (SMTP) — стандартный протокол, который клиенты используют для передачи почтоыйх сообщений на сервер, а серверы — для обмена сообщениями друг с другом.
SoapFormatter — класс из пространства имен System.Runtime.Serialization.Formatters.Soap, служит для форматирования, основанного на XML; наиболее надежный способ сериализации объектов для передачи их по сети приложениям, не использующим .NET Framework. У объектов, сериализованных таким образом, больше шансов преодолеть брандмауэры, чем у объектов, сериализованных с помощью BinaryFormatter.
Stack — упорядоченный набор объектов StackFrame.
Triple DES — стандартный алгоритм симметричного шифрования, использующий 156-битные ключи. В сущности, представляет собой троекратное шифрование по алгоритму DES.
Type Library Exporter — утилита для экспорта .NET-типов в компоненты СОМ.
Type Library Importer — утилита для импорта СОМ-типов в .NET-типы.
Unicode — большая кодовая страница из десятков тысяч знаков, поддерживающая большинство языков и алфавитов, включая латинский, греческий, кириллицу, иврит, арабский, китайский и японский.
Windows Management Instrumentation — технология, обеспечивающая доступ к сведениям об объектах в управляемой среде.
XML (extensible Markup Language) — станартный текстовый формат для хранения информации, предназначенной для чтения программами.Стек ~ stack — область памяти, в которой хранятся типы значения.
Авторизация ~ authorization — проверка наличия у пользователя прав на доступ к запрошенному ресурсу.
Словарь терминов ggg
Алгоритм сжатия deflate — бесплатный распространенный и эффективный алгоритм сжатия данных, принят как промышленный стандарт.
Алгоритм сжатия gzip — расширение алгоритма deflate, поддерживает хранение дополнительной информации в заголовке архива; является промышленным стандартом.
Асимметричное шифрование ~ asymmetric encryption — криптографический алгоритм, использующий разные (закрытый и открытый) ключи для шифрования и расшифровки данных. Также называется «шифрованием с открытым ключом».
Асинхронная модель программирования (Asynchronous Programming Model) — способ написания кода с использованием некоторых типов .NET и пары методов Begin/End с целью асинхронного исполнения некоторые методов.
Атрибут ~ attribute — особый вид классов .NET Framework, обеспечивающий декларативное связывание кода.
Аутентификация ~ authentication — проверка подлинности пользователя.
Вектор инициализации ~ initialization vector (IV) — значение, которе в симетричных алгоритмах используется для более стойкого шифрования первого блока данных, чтобы дополнительно затруднить взлом шифра.
Глобализация ~ globalization — добавление к приложений поддержки форматов чисел и дат, принятых в различных культурах.
Цгуппа кода ~ code group — механизм авторизации, связывающий сборки с наборами разрешений.
Декларативные требования RBS ~ declarative RBS demands — ограничения доступа, объявленные в виде атрибутов метода; приказывают исполняющей среде выполнить проверку доступа до исполнения этого метода.
Десериалцзация ~ deserialization — преобразование ранее сериализованной последовательности байтов в объект.
Доказательства хоста ~ host evidence — предоставляемые хостом сведения, включая происхождение сборки, например каталог приложения, URL или адрес сайта.
Доказательство сборки ~ assembly evidence — идентификационные данные (удостоверения) сборки: хэШг название издателя, строгое имя.
Домен приложения ~ application domain — логический контейнер, позволяющий исполнять несколько сборок в одном процессе, не дает сборкам напрямую обращаться к памяти других сборок.
Журнал событий ~ event log — механизм, позволяющий приложениям регистрировать и сохранять сведения об их состоянии.
Защита доступа по правам кода ~ CAS, (code access security) — система безопасности, которая позволяет администраторам и разработчикам автроризовать приложения, подобно тому, как они обычно авторизуют пользователя.
Защита, основанная на ролях ~ role-based security (RBS) — модель защиты, основанная на аутентификации пользователей с последующей авторизацией согласно разрешениям, назначенным их учетным записям и группам.
Изолированное хранилище ~ isolated storage — защищенная область системы, доступной пользователю, сборке или приложению, в которой можно сохранять данные, не имея высоких привилегий.
Интерфейс ~ interface — тип, в котором объявлен общий набор членов, которые должны быть реализованы во всех классах, реализующих данный интерфейс.
Итерация ~ iteration — перебор элементов набора (в цикле).
g40 Словарь терминов
Кодовая страница ~ code page — упорядоченный список кодов символов. Кодовые стра  ницы обычно применяются для поддержки определенных языков или языковых групп на устройствах ввода-вывода. Кодовая страница Windows содержит 256 кодов символов, начиная с нуля.
Контракт (интерфейс) ~ contract — см. интерфейс.
Контроль типов ~ type safety — проверка типов, исключающая ошибки из-за их несоответствия.
Куча ~ heap — область памяти, в которой хранятся ссылочные типы.
Локализация ~ localization — добавление к приложению поддержки форматирования чисел и дат согласно правилам, принятым в определенной культуре.
Маршалинг ~ marshaling — передача типов через границы областей исполнения.
Многофайловые сборки ~ multifile assemblies — сборки, модули которых хранятся в отдельных файлах, которые можно независимо загружать по мере необходимости.
Модель компонентных объектов ~ COM (Component Object Model) — до появления .NET была фундаментальной инфраструктурой Microsoft для разработки.
Модуль ~ module — контейнер типов, расположенный в сборке. В сборке может быть несколько модулей.
Набор ~ collection — любой класс, позволяющий объединять элеименты в списки и перебирать их в циклах.
Набор разрешений ~ permission set — CAS ACL, содержащий несколько разрешений.
Строка подключения ~ connection string — набор параметров для подключения к некоторой базе данных. Для подключения к любым ODBC- и OleDb-совместимым базам данным (таковыми являются базы данных всех крупных производителей) необходима строка подключения. По соображениям безопасности строки подключений вегда следует шифровать.
Обобщение ~ generic [type] — элемент кода, способный адаптироваться для выполнения сходных действий над различными типами данных.
Общий секретный ключ ~ shared secret — ключ для симметричного шифрования.
Объекты ядра Windows ~ Windows kernel objects — встроенные средства ОС для синхронизации процессов. Вкючают мьютексы, семафоры и события.
Ограничение ~ constraint — условие параметра, ограничивающее тип аргумента, который можно передавать в этом параметре. Ограничения могут требовать, чтобы тип аргумента содержал некоторый реализованный интерфейс, был производным от определенного класса, имел конструктор без параметров, а также был ссылочным типом либо типом значения. Ограничения можно комбинировать, но указывать с ними можно не более одного класса.
Откат ~ roll back — действие, выполняемое при неудачном завершении установки. Включает отмену всех изменений, внесенных до возникновения ошибки, и возврат системы в состояние, в котором она была на момент запуска установки.
Параметр приложения ~ application setting — пользовательский параметр, который приложения читает и/или записывает.
Политика безопасности ~ security policy — логическая группа, включающая правила для групп кода, наборы разрешений и пользовательские правила.
Политика участника [системы безопасности] ~ principal policy — схема, применяемая в .NET Framework; определяет участника системы безопасности, возвращаемого приложению, запросившему текущего участника системы безопасности.
Словарь терминов S41
Полностью доверяемый [код] ~ fully trusted — сборка, освобожденная от проверки разрешений С AS.
Поток ~ thread — синхронно исполняемый код.
Приведение типов ~ cast — преобразование одних типов в другие.
Процесс ~ process — исполняемое в данный момент приложение. Процессы служат для изоляции ресурсов.
Разрешение ~ permission — элемент списка управления доступом (ACL) в системе безопасности CAS.
Распаковка ~ unboxing — преобразование ссылочного типа, полученного путем упаковки, обратно в тип значения.
Расширяющее преобразование ~ widening — такое преобразование ссылочных типов, при котором диапазон значений целевого типа шире или равен по ширине диапазону значений исходного типа. Такие преобразования могут быть неявными.
Регулярное выражение - regular expression — набор символов, который можно сравнивать со строкой, чтобы определить, соответствует ли ее формат заданному; позволяет извлекать либо заменять заданные фрагменты текста.
Сбор мусора ~ garbage collection — механизм освобождения памяти в куче посредством удаления элементов, на которые больше не ссылается ни один объект.
Сериализация ~ serialization — процесс преобразования объектов последовательность байтов для хранения, передачи и воссоздания в дальнейшем.
Сигнатура ~ signature — возвращаемый тип, число и тип параметров данного члена.
Симметричное шифрование ~ symmetric encryption — криптографический метод, использующий единый секретный ключ для шифрования и расшифровки данных. Также называется шифрованием с секретным ключом.
Служба ~ service — процесс, работающий в фоновом режиме в собственном сеансе, не имеющий пользовательского интерфейса.
Сопутствующие сборки ~ satellite assemblies — сборки с локализованными для различных культур ресурсами, хранимые отдельно от главной сборки.
Список управления доступом ~ access control list (ACL) — другое название списка управления избирательным доступом (discretionary access control list, DACL) — механизма ог-ранйчения прав, основанного на идентификации пользователей и групп, которым предоставляются (либо не предоставляются) разрешения на доступ к объектам.
Список управления доступом системы безопасности ~ security access control list (SACL) — механизм регистрации использования ресурса, определяет способ аудита доступа к файлам и папкам.
Структура ~ structure — Пользовательский тип значения, созданный из других типов.
Сужающее преобразование ~ narrowing — такое преобразование типов значений, когда диапазон значений целевого типа меньше, чем у исходного типа. В C# и Visual Basic (при включенном параметре Option Strict) такие преобразования должны быть -к явными.
Счетчик производительности ~ performance counter — механизм измерения быстродействия исполняемого кода.
Текущая культура ~ current culture — культура, для работы с которой приложение настроено в данный момент.
842 Словарь терминов
Текущая культура пользовательского интерфейса ~ current UI culture — культура, которая используется для отображения элементов пользовательского интерфейса. Может отличаться от текущей культуры.
Тип, принимающий nu/Z-значения ~ nullable type — тип значения, которому можно присвоить значение Nothing/null.
Требования императивной защиты на основе ролей ~ imperative role-based security (RBS) demands — ограничения доступа, объявленные в коде; обеспечивают тонкую настройку прав доступа к коду.
Удаление - uninstall — удаление остатков приложения с возвратом компьютера в состояние, вкотором он был до установки приложения.
Упаковка ~ boxing — неявное преобразование типа значения в ссылочный тип.
Управление конфигурацией ~ configuration management — приемы и механизмы конфин-гурирования приложений.
Унаследованное разрешение ~ inherited permission — разрешение, унаследованное объектом от его родительского объекта.
Управляемый код ~ managed code — код, который работает под управлением исполняющей среды .NET Framework.
Утечка памяти ~ memory leak — проблема, возникающая из-за того, что занятая ненужными объектами память не освобождается.
Файловая система ~ file system — механизм операционной системы, обеспечивающий хранение данных на дисках в виде файлов и папок.
Фильтрация исключений ~ filtering exceptions — упорядоченная система блоков Catch, обеспечивающая перехват сначала специфичных, и только потом общих типов исключений.
Хэш ~ hash — значение, вычисленное на основе большего объема данных; позволяет проверить, не были ли данные модифицированы после генерации хэша.
Хэш-алгоритмы с ключом ~ keyed hash algorithms — алгоритмы для защиты данных от модификации при помощи шифрования с секретным ключом, который должен быть известен и отправителю, и получателю данных.
Цифровая подпись ~ digital signature — значение, которое добавляется к электронным данным для подтверждения факта их создания владельцем соответствующего закрытого ключа.
Частично доверяемый код ~ partially trusted code — сборка, для которой при каждом обращении которой к защищенному ресурсу должны проверяться разрешения CAS.
Шифровальный ключ ~ encryption key — число или строка, позволяющая зашифровать и расшифровать данные. В симметричном шифровании также называется «общим секретным ключом».
Шифрованный текст - cipher text — текст, сгенерированный криптоалгоритмом; не может быть преобразован в исходный открытый текст без знания секретного ключа.
Эшелонированная защита ~ defense-in-depth — принцип безопасности, требующий организации нескольких уровней защиты, чтобы при успешной атаке одного из них остальные обеспечили безопасность.
Основы разработки приложений на платформе
MicrobtHt*
.NET Framework
Учебный курс Microsoft
Экзамен 70-536
Официальное пособие для самоподготовки
Освоив материалы этого курса, вы научитесь создавать Windows-приложения, использующие инфраструктуру .NET Framework, на языках Visual Basic .NET и С#, а также подготовитесь к сдаче экзамена № 70-536 — базового курса для сертификации Microsoft MCTS.
MCTS
В этой книге:
управление данными с использованием системных типов, наборов и обобщений;
применение регулярных выражений для проверки ввода, форматирования текста и извлечения данных;
-	разработка служб, создание доменов приложений и многопоточных приложений;
улучшение интерфейса приложений путем добавления изображений и графических элементов;
	реализация моделей защиты, основанных на правах кода и ролях, а также шифрование данных;
	применение сериализации и отражения;
инструментальные средства для трассировки приложений
и ведения журналов;
	взаимодействие с унаследованным кодом средствами СОМ Interop и Plnvoke.
На прилагаемом компакт-диске:
	демонстрационная версия экзаменационных тестов с автоматическим подсчетом результатов и разъяснением верных и неверных ответов;
	упражнения, которые позволят вам на практике закрепить полученные знания:
	электронные книги на английском языке;
	словарь терминов.
Издательский дом
Питер
Санкт-Петербург
Б. Сампсониевский пр., 29а
Е-таИ: sales@piter.com
Internet: www.piter.com
Тел./факс: (812) 703-7383
Издательство
Русская Редакция
Москва
Шелепихинская наб., 32
E-mail: info@rusedit.com Internet: www.rusedit.com
Тел./факс: (095) 256-7145
Mk-osoft
96 Visual Stud io 2005
Microsoft