/
Author: Троелсен Э.
Tags: компьютерные технологии компьютерные науки языки программирования язык программирования c# платформа .net 4
ISBN: 978-5-8459-1682-2
Year: 2010
Text
ЯЗЫК ПРОГРАММИРОВАНИЯ
С#2010
И ПЛАТФОРМА .NET 4
5-е издание
PRO C# 2010
AND THE
.NET 4 PLATFORM
Fifth Edition
Andrew Troelsen
Apress*
ЯЗЫК ПРОГРАММИРОВАНИЯ
С#2010
И ПЛАТФОРМА .NET 4
5-е издание
Эндрю Троелсен
Москва • Санкт-Петербург • Киев
2011
ББК 32.973.26-018.2.75
Т70
УДК 681.3.07
Издательский дом "Вильяме"
Зав. редакцией С.Н. Тригуб
Перевод с английского Я.П. Волковой, А.А. Моргунова, Н.А. Мухина
Под редакцией Ю.Н. Артпеменко
По общим вопросам обращайтесь в Издательский дом "Вильяме" по адресу:
info@williamspublishing.com, http://www.williamspublishing.com
Трое л сен, Эндрю.
Т70 Язык программирования С# 2010 и платформа .NET 4.0, 5-е изд. : Пер. с англ. —
М. : ООО "И.Д. Вильяме", 2011. — 1392 с. : ил. — Парал. тит. англ.
ISBN 978-5-8459-1682-2 (рус.)
ББК 32.973.26-018.2.75
Все названия программных продуктов являются зарегистрированными торговыми марками
соответствующих фирм.
Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой
бы то ни было форме и какими бы то ни было средствами, будь то электронные или
механические, включая фотокопирование и запись на магнитный носитель, если на это нет письменного
разрешения издательства APress, Berkeley, CA.
Authorized translation from the English language edition published by APress, Inc., Copyright © 2010.
All rights reserved. No part of this work may be reproduced or transmitted In any form or by any
means, electronic or mechanical, Including photocopying, recording, or by any Information storage or
retrieval system, without the prior written permission of the copyright owner and the publisher.
Trademarked names may appear in this book. Rather than use a trademark symbol with every
occurrence of a trademarked name, we use the names only in an editorial fashion and to the benefit
of the trademark owner, with no intention of infringement of the trademark.
Russian language edition is published by Williams Publishing House according to the Agreement
with R&I Enterprises International, Copyright © 2011.
Научно-популярное издание
Эндрю Троелсен
Язык программирования С# 2010
и платформа .NET 4.0
5-е издание
Верстка Т.Н. Артпеменко
Художественный редактор С.А. Чернокозинский
Подписано в печать 28.10.2010. Формат 70x100/16.
Гарнитура Times. Печать офсетная.
Усл. печ. л. 112,23. Уч.-изд. л. 87,1.
Тираж 2000 экз. Заказ № 24430.
Отпечатано по технологии CtP
в ОАО "Печатный двор" им. А. М. Горького
197110, Санкт-Петербург, Чкаловский пр., 15.
ООО "И. Д. Вильяме", 127055, г. Москва, ул. Лесная, д. 43, стр. 1
ISBN 978-5-8459-1682-2 (рус.) © Издательский дом "Вильяме", 2011
ISBN 978-1-43-022549-2 (англ.) © by Andrew TYoelsen, 2010
Оглавление
Часть I. Общие сведения о языке С# и платформе .NET 43
Глава 1. Философия .NET 44
Глава 2. Создание приложений на языке С# 80
Часть II. Главные конструкции программирования на С# 105
Глава 3. Главные конструкции программирования на С#: часть Г 106
Глава 4. Главные конструкции программирования на С#: часть ГГ 150
Глава 5. Определение инкапсулированных типов классов 185
Глава 6. Понятия наследования и полиморфизма 231
Глава 7. Структурированная обработка исключений 265
Глава 8. Время жизни объектов 292
Часть III. Дополнительные конструкции программирования на С# 319
Глава 9. Работа с интерфейсами 320
Глава 10. Обобщения 356
Глава 11. Делегаты, события и лямбда-выражения 386
Глава 12. Расширенные средства языка С# 421
Глава 13. L1NQ to Objects 463
Часть IV. Программирование с использованием сборок .NET 493
Глава 14. Конфигурирование сборок .NET 494
Глава 15. Рефлексия типов, позднее связывание и программирование
с использованием атрибутов 542
Глава 16. Процессы, домены приложений и контексты объектов 582
Глава 17. Язык C1L и роль динамических сборок 607
Глава 18. Динамические типы и исполняющая среда динамического языка 648
Часть V. Введение в библиотеки базовых классов .NET 669
Глава 19. Многопоточность и параллельное программирование 670
Глава 20. Файловый ввод-вывод и сериализация объектов 711
Глава 21. ADO.NET, часть Г: подключенный уровень 754
Глава 22. ADO.NET, часть ГГ: автономный уровень 804
Глава 23. ADO.NET, часть ПГ: Entity Framework 857
Глава 24. Введение в UNQ to XML 891
Глава 25. Введение в Windows Communication Foundation 906
Глава 26. Введение в Windows Workflow Foundation 4.0 961
Часть VI. Построение настольных пользовательских
приложений с помощью WPF 995
Глава 27. Введение в Windows Presentation Fbundation и XAML 996
Глава 28. Программирование с использованием элементов управления WPF 1048
Глава 29. Службы визуализации графики WPF 1103
Глава 30. Ресурсы, анимация и стили WPF 1137
Глава 31. Шаблоны элементов управления WPF и пользовательские
элементы управления 1170
Часть VII. Построение веб-приложений с использованием ASP.NET 1213
Глава 32. Построение веб-страниц ASP.NET 1214
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1257
Глава 34. Управление состоянием в ASP.NET 1294
Часть VIII. Приложения 1327
Приложение А. Программирование с помощью Windows Forms 1328
Приложение Б. Независимая от платформы разработка .NET-приложений
с помощью Mono 1369
Предметный указатель 1386
Содержание
Об авторе 31
О техническом редакторе 31
Благодарности 31
Введение 32
Автор и читатели — одна команда 33
Краткий обзор содержания 33
Исходный код примеров 42
От издательства 42
Часть I. Общие сведения о языке С# и платформе .NET 43
Глава 1. Философия .NET 44
Предыдущее состояние дел 44
Подход с применением языка С и API-интерфейса Windows 45
Подход с применением языка C++ и платформы MFC 45
Подход с применением Visual Basic 6.0 45
Подход с применением Java 46
Подход с применением СОМ 46
Сложность представления типов данных СОМ 47
Решение .NET 48
Главные компоненты платформы .NET (CLR, CTS и CLS) 49
Роль библиотек базовых классов 50
Что привносит язык С# 50
Другие языки программирования с поддержкой .NET 52
Жизнь в многоязычном окружении 54
Что собой представляют сборки в .NET 54
Однофайловые и многофайловые сборки 56
Роль CIL 56
Преимущества CIL 58
Компиляция CIL-кода в инструкции, ориентированные на конкретную платформу 58
Роль метаданных типов в .NET 59
Роль манифеста сборки 60
Что собой представляет общая система типов (CTS) 60
Типы классов 61
Типы интерфейсов 61
Типы структур 62
Типы перечислений 62
Типы делегатов 63
Члены типов 63
Встроенные типы данных 63
Что собой представляет общеязыковая спецификация (CLS) 64
Забота о соответствии правилам CLS 66
Что собой представляет общеязыковая исполняющая среда (CLR) 66
Различия между сборками, пространствами имен и типами 67
Роль корневого пространства Microsoft 71
Получение доступа к пространствам имен программным образом 71
Добавление ссылок на внешние сборки 73
Изучение сборки с помощью утилиты ildasm. exe 74
Просмотр CIL-кода 74
Просмотр метаданных типов 74
Просмотр метаданных сборки (манифеста) 74
Изучение сборки с помощью утилиты Reflector 75
Содержание 7
Развертывание исполняющей среды .NET 76
Клиентский профиль исполняющей среды .NET 77
Не зависящая от платформы природа .NET 77
Резюме 79
Глава 2. Создание приложений на языке С# 80
Роль комплекта . NET Framework 4.0 SDK 80
Окно командной строки в Visual Studio 2010 81
Создание приложений на С# с использованием csc.exe 81
Указание целевых входных и выходных параметров 82
Добавление ссылок на внешние сборки 84
Добавление ссылок на несколько внешних сборок 84
Компиляция нескольких файлов исходного кода 85
Работа с ответными файлами в С# 85
Создание приложений .NET с использованием Notepad++ 87
Создание приложений.NET с помощью SharpDevelop 88
Создание простого тестового проекта 89
Создание приложений .NET с использованием Visual С# 2010 Express 90
Некоторые уникальные функциональные возможности Visual С# 2010 Express 91
Создание приложений .NET с использованием Visual Studio 2010 92
Некоторые уникальные функциональные возможности Visual Studio 2010 92
Ориентирование на .NET Framework в диалоговом окне New Project 93
Использование утилиты Solution Explorer 93
Утилита Class View 95
Утилита Obj ect Browser 9 5
Встроенная поддержка рефакторинга программного кода 96
Возможности для расширения и окружения кода 98
Утилита Class Designer 100
Интегрируемая система документации . NET Framework 4.0 102
Резюме 104
Часть II. Главные конструкции программирования на С# Ю5
Глава 3. Главные конструкции программирования на С#: часть I 106
Разбор простой программы на С# 106
Варианты метода Ma i n () 108
Спецификация кода ошибки в приложении 109
Обработка аргументов командной строки 110
Указание аргументов командной строки в Visual Studio 2010 111
Интересное отклонение от темы: некоторые дополнительные члены
класса System.Environment 112
Класс System.Console 113
Базовый ввод-вывод с помощью класса Console 114
Форматирование вывода, отображаемого в окне консоли 115
Форматирование числовых данных г 116
Форматирование числовых данных в приложениях, отличных от консольных 117
Системные типы данных и их сокращенное обозначение в С# 117
Объявление и инициализация переменных 119
Внутренние типы данных и операция new 120
Иерархия классов типов данных 121
Члены числовых типов данных 123
Члены System.Boolean 123
Члены System.Char 124
Синтаксический разбор значений из строковых данных 125
8 Содержание
Типы System.DateTimeи System.TimeSpan 125
Пространство имен System.Numerics в .NET4.0 126
Работа со строковыми данными 127
Базовые операции манипулирования строками 128
Конкатенация строк 129
Управляющие последовательности символов 130
Определение дословных строк 131
Строки и равенство 131
Неизменная природа строк 132
Тип System.Text.StringBuilder 133
Сужающие и расширяющие преобразования типов данных 135
Перехват сужающих преобразований данных 137
Настройка проверки на предмет возникновения условий
переполнения в масштабах проекта 139
Ключевое слово unchecked 139
Роль класса System. Convert 140
Неявно типизированные локальные переменные 140
Ограничения, связанные с неявно типизированными переменными 142
Неявно типизированные данные являются строго типизированными 143
Польза от неявно типизированных локальных переменных 144
Итерационные конструкции в С# 144
Цикл for 145
Цикл f oreach 145
Использование var в конструкциях foreach 146
Конструкции whilendo/while 146
Конструкции принятия решений и операции сравнения 147
Оператор if /else 147
Оператор switch 148
Резюме 149
Глава 4. Главные конструкции программирования на С#: часть II 150
Методы и модификаторы параметров 150
Стандартное поведение при передаче параметров 151
Модификатор out 152
Модификатор г е f 153
Модификатор par ams 154
Определение необязательных параметров 156
Вызов методов с использованием именованных параметров 157
Перегрузка методов 158
Массивы в С# 160
Синтаксис инициализации массивов в С # 161
Неявно типизированные локальные массивы 162
Определение массива объектов 163
Работа с многомерными массивами 163
Использование массивов в качестве аргументов и возвращаемых значений 165
Базовый класс System. Array 165
Тип enum 167
Управление базовым типом, используемым для хранения
значений перечисления 168
Объявление переменных типа перечислений 168
Тип System.Enum 169
Динамическое обнаружение пар "имя/значение" перечисления 170
Типы структур 172
Создание переменных типа структур 173
Содержание 9
Типы значения и ссылочные типы 174
Типы значения, ссылочные типы и операция присваивания 175
Типы значения, содержащие ссылочные типы 177
Передача ссылочных типов по значению 178
Передача ссылочных типов по ссылке 179
Заключительные детали относительно типов значения и ссылочных типов 180
Нулевые типы в С # 181
Работа с нулевыми типами 183
Операция?? 184
Резюме 184
Глава 5. Определение инкапсулированных типов классов 185
Знакомство с типом класса С# 185
Размещение объектов с помощью ключевого слова new 187
Понятие конструктора 188
Роль конструктора по умолчанию 188
Определение специальных конструкторов 189
Еще раз о конструкторе по умолчанию 190
Роль ключевого слова this 191
Построение цепочки вызовов конструкторов с использованием this 193
Обзор потока конструктора 195
Еще раз об необязательных аргументах 196
Понятие ключевого слова static 197
Определение статических методов 198
Определение статических полей данных 198
Определение статических конструкторов 201
Определение статических классов 202
Основы объектно-ориентированного программирования 203
Роль инкапсуляции 204
Роль наследования 204
Роль полиморфизма 206
Модификаторы доступа С# 207
Модификаторы доступа по умолчанию 208
Модификаторы доступа и вложенные типы 208
Первый принцип: службы инкапсуляции С# 209
Инкапсуляция с использованием традиционных методов доступа и изменения 210
Инкапсуляция с использованием свойств .NET 212
Использование свойств внутри определения класса 214
Внутреннее представление свойств 215
Управление уровнями видимости операторов get/set свойств 217
Свойства, доступные только для чтения и только для записи 218
Статические свойства 218
Понятие автоматических свойств 219
Взаимодействие с автоматическими свойствами 221
Замечания относительно автоматических свойств и значений по умолчанию 221
Понятие синтаксиса инициализации объектов 223
Вызов специальных конструкторов с помощью синтаксиса инициализации 224
Инициализация вложенных типов 225
Работа с данными константных полей 226
Понятие полей только для чтения 228
Статические поля только для чтения 228
Понятие частичных типов 229
Резюме 230
10 Содержание
Глава 6. Понятия наследования и полиморфизма 231
Базовый механизм наследования 231
Указание родительского класса для существующего класса 232
О множественном наследовании 234
Ключевое слово sealed 234
Изменение диаграмм классов Visual Studio 235
Второй принцип ООП: подробности о наследовании 236
Управление созданием базового класса с помощью ключевого слова base 238
Хранение фамильных тайн: ключевое слово protected 240
Добавление запечатанного класса 241
Реализация модели включения/делегации 242
Определения вложенных типов 243
Третий принцип ООП: поддержка полиморфизма в С# 245
Ключевые слова virtual и override 245
Переопределение виртуальных членов в Visual Studio 2010 247
Запечатывание виртуальных членов 248
Абстрактные классы 249
Полиморфный интерфейс 250
Сокрытие членов 253
Правила приведения к базовому и производному классу 255
Ключевое слово as 257
Ключевое слово is 257
Родительский главный класс System.Object 258
Переопределение System.Object.ToStringO 261
Переопределение System.Object.Equals() 261
Переопределение System.Object.GetHashCodeO 262
Тестирование модифицированного класса Ре г s on 263
Статические члены System.Object 264
Резюме 264
Глава 7. Структурированная обработка исключений 265
Ода ошибкам и исключениям 265
Роль обработки исключений в .NET 266
Составляющие процесса обработки исключений в .NET 267
Базовый класс System. Exception 268
Простейший пример 269
Генерация общего исключения 271
Перехват исключений 272
Конфигурирование состояния исключения 273
Свойство TargetSite 273
Свойство StackTrace 274
Свойство НеlpLink 275
Свойство Data 275
Исключения уровня системы (System. SystemException) 277
Исключения уровня приложения (System. ApplicationException) 278
Создание специальных исключений, способ первый 278
Создание специальных исключений, способ второй 280
Создание специальных исключений, способ третий 281
Обработка многочисленных исключений 282
Общие операторы catch 284
Передача исключений 285
Внутренние исключения 286
Блок finally 287
Какие исключения могут выдавать методы 287
Содержание 11
К чему приводят необрабатываемые исключения 288
Отладка необработанных исключений с помощью Visual Studio 289
Несколько слов об исключениях, связанных с поврежденным состоянием 290
Резюме 291
Глава 8. Время жизни объектов 292
Классы, объекты и ссылки 292
Базовые сведения о времени жизни объектов 293
CIL-код, генерируемый для ключевого слова new 294
Установка объектных ссылок в пи 11 296
Роль корневых элементов приложения 297
Поколения объектов 298
Параллельная сборка мусора в версиях .NET 1.0 — .NET 3.5 299
Фоновая сборка мусора в версии . NET 4.0 300
Тип System. GC 300
Принудительная активизация сборки мусора 302
Создание финализируемых объектов 304
Переопределение System.Object .Finalize () 305
Описание процесса финализации 307
Создание высвобождаемых объектов 307
Повторное использование ключевого слова using в С# 310
Создание финализируемых и высвобождаемых типов 311
Формализованный шаблон очистки 312
Отложенная инициализация объектов 314
Настройка процесса создания данных L a z у о 317
Резюме 318
Часть III. Дополнительные конструкции программирования на С# зш
Глава 9. Работа с интерфейсами 320
Что собой представляют типы интерфейсов 320
Сравнение интерфейсов и абстрактных базовых классов 321
Определение специальных интерфейсов 324
Реализация интерфейса 326
Вызов членов интерфейса на уровне объектов 328
Получение ссылок на интерфейсы с помощью ключевого слова a s 329
Получение ссылок на интерфейсы с помощью ключевого слова i s 329
Использование интерфейсов в качестве параметров 330
Использование интерфейсов в качестве возвращаемых значений 332
Массивы типов интерфейсов 332
Реализация интерфейсов с помощью Visual Studio 2010 333
Устранение конфликтов на уровне имен за счет реализации интерфейсов явным образом 334
Проектирование иерархий интерфейсов 337
Множественное наследование в случае типов интерфейсов 338
Создание перечислимых типов (IE numerable и I Enumerator) 340
Создание методов итератора с помощью ключевого слова yield 343
Создание именованного итератора 344
Внутреннее представление метода итератора 345
Создание клонируемых объектов (ICloneable) 346
Более сложный пример клонирования 348
Создание сравнимых объектов (IComparable) 350.
Указание множества критериев для сортировки (I Compare г) 353
Использование специальных свойств и специальных типов для сортировки 354
Резюме 355
12 Содержание
Глава 10. Обобщения 356
Проблемы, связанные с необобщенными коллекциями 356
Проблема производительности 358
Проблемы с безопасностью типов 362
Роль параметров обобщенных типов 365
Указание параметров типа для обобщенных классов и структур 366
Указание параметров типа для обобщенных членов 367
Указание параметров типов для обобщенных интерфейсов 367
Пространство имен System.Collections.Generic 369
Синтаксис инициализации коллекций 370
Работа с классом List<T> 371
Работа с классом Stack<T> 373
Работа с классом Queue<T> 374
Работа с классом SortedSet<T> 375
Создание специальных обобщенных методов 376
Выведение параметра типа 378
Создание специальных обобщенных структур и классов 379
Ключевое слово default в обобщенном коде 380
Обобщенные базовые классы 381
Ограничение параметров типа 382
Примеры использования ключевого слова where 383
Недостаток ограничений операций 384
Резюме 385
Глава 11. Делегаты, события и лямбда-выражения 386
Понятие типа делегата .NET 386
Определение типа делегата в С# 387
Базовые классы System.MulticastDelegate и System.Delegate 389
Простейший пример делегата 391
Исследование объекта делегата 392
Отправка уведомлений о состоянии объекта с использованием делегатов 393
Включение группового вызова 396
Удаление целей из списка вызовов делегата 397
Синтаксис групповых преобразований методов 398
Понятие ковариантности делегатов 400
Понятие обобщенных делегатов 402
Эмуляция обобщенных делегатов без обобщений 403
Понятие событий С# 404
Ключевое слово event 405
"За кулисами" событий 406
Прослушивание входящих событий 407
Упрощенная регистрация событий с использованием Visual Studio 2010 408
Создание специальных аргументов событий 409
Обобщенный делегат Event Handle r<T> 410
Понятие анонимных методов С# 411
Доступ к локальным переменным 413
Понятие лямбда-выражений 413
Анализ лямбда-выражения 416
Обработка аргументов внутри множества операторов 417
Лямбда-выражения с несколькими параметрами и без параметров 418
Усовершенствование примера PrimAndProperCarEvents за счет
использования лямбда-выражений 419
Резюме 419
Содержание
13
Глава 12. Расширенные средства языка С# 421
Понятие методов-индексаторов 421
Индексация данных с использованием строковых значений 423
Перегрузка методов-индексаторов 424
Многомерные индексаторы 425
Определения индексаторов в интерфейсных типах 425
Понятие перегрузки операций 426
Перегрузка бинарных операций 427
А как насчет операций += и -=? 429
Перегрузка унарных операций 429
Перегрузка операций эквивалентности 430
Перегрузка операций сравнения 431
.внутреннее представление перегруженных операций 432
Финальные соображения относительно перегрузки операций 433
Понятие преобразований пользовательских типов 434
Числовые преобразования 434
Преобразования между связанными типами классов 434
Создание специальных процедур преобразования 435
Дополнительные явные преобразования типа Square 437
Определение процедур неявного преобразования 438
Внутреннее представление процедур пользовательских преобразований 439
Понятие расширяющих методов 440
Понятие частичных методов 448
Понятие анонимных типов 450
Анонимные типы, содержащие другие анонимные типы 454
Работа с типами указателей 455
Ключевое слово un s a f e 456
Работа с операциями * и & 458
Небезопасная и безопасная функция обмена значений 459
Доступ к полям через указатели (операция ->) 459
Ключевое слово stackalloc 460
Закрепление типа ключевым словом fixed 460
Ключевое слово sizeof 461
Резюме 462
Глава 13. LINQ to Objects 463
Программные конструкции, специфичные для LINQ 463
Неявная типизация локальных переменных 464
Синтаксис инициализации объектов и коллекций 464
Лямбда-выражения 465
Расширяющие методы 466
Анонимные типы 466
Роль LINQ 467
Выражения LINQ строго типизированы 468
Основные сборки LINQ 468
Применение запросов LINQ к элементарным массивам 469
Решение без использования LINQ 470
Рефлексия результирующего набора LINQ 471
LINQ и неявно типизированные локальные переменные 472
LINQ и расширяющие методы 473
Роль отложенного выполнения 474
Роль немедленного выполнения 475
Возврат результата запроса LINQ 475
Возврат результатов LINQ через немедленное выполнение 476
14 Содержание
Применение запросов LINQ к объектам коллекций 477
Доступ к содержащимся в контейнере подобъектам 478
Применение запросов LINQ к необобщенным коллекциям 478
Фильтрация данных с использованием OfType<T>() 479
Исследование операций запросов LINQ 480
Базовый синтаксис выборки 481
Получение подмножества данных 482
Проекция новых типов данных 482
Получение счетчиков посредством Enumerable • 484
Обращение результирующих наборов 484
Выражения сортировки 484
LINQ как лучшее средство построения диаграмм 485
Исключение дубликатов 486
Агрегатные операции LINQ 486
Внутреннее представление операторов запросов LINQ 487
Построение выражений запросов с использованием операций запросов 488
Построение выражений запросов с использованием типа Enumerable
и лямбда-выражений 488
Построение выражений запросов с использованием типа Enumerable
и анонимных методов 490
Построение выражений запросов с использованием типа Enumerable
и низкоуровневых делегатов 490
Резюме 491
Часть IV. Программирование с использованием сборок .NET 493
Глава 14. Конфигурирование сборок .NET 494
Определение специальных пространств имен 494
Устранение конфликтов на уровне имен за счет использования полностью
уточненных имен 496
Устранение конфликтов на уровне имен за счет использования псевдонимов 497
Создание вложенных пространств имен 498
Пространство имен, используемое по умолчанию в Visual Studio 2010 499
Роль сборок .NET 500
Сборки повышают возможность повторного использования кода 500
Сборки определяют границы типов 501
Сборки являются единицами, поддерживающими версии 501
Сборки являются самоописываемыми 501
Сборки поддаются конфигурированию 501
Формат сборки .NET 502
Заголовок файла Windows 502
Заголовок файла CLR 504
CIL-код, метаданные типов и манифест сборки 505
Необязательные ресурсы сборки 505
Однофайловые и многофайловые сборки 505
Создание и использование однофайловой сборки 506
Исследование манифеста 510
Исследование CIL-кода 512
Исследование метаданных типов 512
Создание клиентского приложения на С# 513
Создание клиентского приложения на Visual Basic 514
Межъязыковое наследование в действии 515
Создание и использование многофайловой сборки 516
Исследование файла uf о .netmodule 517
Содержание 15
Исследование файла airvehiсles .dll 518
Использование многофайловой сборки 518
Приватные сборки 519
Идентификационные данные приватной сборки 520
Процесс зондирования 520
Конфигурирование приватных сборок 521
Конфигурационные файлы и Visual Studio 2010 522
Разделяемые сборки 524
Строгие имена 524
Генерирование строгих имен в командной строке 526
Генерирование строгих имен с помощью Visual Studio 2010 528
Установка сборок со строгими именами в GAC 529
Просмотр содержимого GAC с помощью проводника Windows 530
Использование разделяемой сборки 531
Исследование манифеста SharedCarLibClient 532
Конфигурирование разделяемых сборок 533
Фиксация текущей версии разделяемой сборки 533
Создание разделяемой сборки версии 2.0.0.0 533
Динамическое перенаправление на конкретную версию разделяемой сборки 536
Сборки политик издателя 537
Отключение политик издателя 538
Элемент <codeBase> 538
Пространство имен System. Configuration 540
Резюме 541
Глава 15. Рефлексия типов, позднее связывание и программирование
с использованием атрибутов 542
Необходимость в метаданных типов 542
Просмотр (части) метаданных перечисления EngineState 543
Просмотр (части) метаданных типа Саг 544
Изучение блока TypeRef 546
Просмотр метаданных самой сборки 546
Просмотр метаданных внешних сборок, на которые имеются ссылки в текущей сборке 546
Просмотр метаданных строковых литералов 547
Рефлексия 547
Класс System.Type 548
Получение информации о типе с помощью System. Ob j ect. GetType () 549
Получение информации о типе с помощью typeof () 549
Получение информации о типе с помощью System. Туре . GetType () 549
Создание специальной программы для просмотра метаданных 550
Рефлексия методов 550
Рефлексия полей и свойств 551
Рефлексия реализуемых интерфейсов 552
Отображение различных дополнительных деталей 552
Реализация метода Ma in () 552
Рефлексия обобщенных типов 554
Рефлексия параметров и возвращаемых значений методов 554
Динамически загружаемые сборки 556
Рефлексия разделяемых сборок 558
Позднее связывание 560
Класс System.Activator 560
Вызов методов без параметров 562
Вызов методов с параметрами 563
Роль атрибутов .NET 564
16 Содержание
Потребители атрибутов 565
Применение предопределенных атрибутов в С# 565
Сокращенное обозначение атрибутов в С# 567
Указание параметров конструктора для атрибутов 567
Атрибут [Obsolete] в действии 567
Создание специальных атрибутов 568
Применение специальных атрибутов 569
Синтаксис именованных свойств 569
Ограничение использования атрибутов 570
Атрибуты уровня сборки и модуля 571
Файл Ass emb lylnfo.cs, генерируемый Visual Studio 2010 572
Рефлексия атрибутов с использованием раннего связывания 572
Рефлексия атрибутов с использованием позднего связывания 573
Возможное применение на практике рефлексии, позднего связывания
и специальных атрибутов 575
Создание расширяемого приложения 576
Создание сборки CommonSnappableTypes . dll 576
Создание оснастки на С# 577
Создание оснастки на Visual Basic 577
Создание расширяемого приложения Windows Fbrms 578
Резюме 581
Глава 16. Процессы, домены приложений и контексты объектов 582
Роль процесса Windows 582
Роль потоков 583
Взаимодействие с процессами в рамках платформы .NET 585
Перечисление выполняющихся процессов 587
Изучение конкретного процесса 588
Изучение набора потоков процесса 588
Изучение набора модулей процесса 590
Запуск и останов процессов программным образом 592
Управление запуском процесса с использованием класса ProcessStartlnfo 592
Домены приложений .NET 594
Класс System.AppDomain 594
Взаимодействие с используемым по умолчанию доменом приложения 596
Перечисление загружаемых сборок 597
Получение уведомлений о загрузке сборок 598
Создание новых доменов приложений 599
Загрузка сборок в специальные домены приложений 600
Выгрузка доменов приложений программным образом 601
Границы контекстов объектов 602
Контекстно-свободные и контекстно-зависимые типы 603
Определение контекстно-зависимого объекта 604
Инспектирование контекста объекта 604
Итоговые сведения о процессах, доменах приложений и контекстах 606
Резюме 606
Глава 17. Язык CIL и роль динамических сборок 607
Причины для изучения грамматики языка CIL 607
Директивы, атрибуты и коды операций в CIL 608
Роль директив CIL 609
Роль атрибутов CIL ; 609
Роль кодов операций CIL 609
Разница между кодами операций и их мнемоническими эквивалентами в CIL 609
Содержание 17
Помещение и извлечение данных из стека в CIL 610
Двунаправленное проектирование 612
Роль меток в коде CIL 615
Взаимодействие с CIL: модификация файла * . i 1 616
Компиляция CIL-кодас помощью ilasm.exe 617
Создание CIL-кода с помощью SharpDevelop 618
Рольpeverify.exe 619
Использование директив и атрибутов в CIL 619
Добавление ссылок на внешние сборки в CIL 619
Определение текущей сборки в CIL 620
Определение пространств имен в CIL 620
Определение типов классов в CIL 621
Определение и реализация интерфейсов в CIL 622
Определение структур в CIL 623
Определение перечислений в CIL 623
Определение обобщений в CIL 623
Компиляция файла CILTypes . il 624
Соответствия между типами данных в библиотеке базовых классов .NET, C# и CIL 625
Определение членов типов в CIL 626
Определение полей данных в CIL 626
Определение конструкторов для типов в CIL 627
Определение свойств в CIL 627
Определение параметров членов 628
Изучение кодов операций в CIL 628
Директива .maxstack 631
Объявление локальных переменных в CIL 631
Отображение параметров на локальные переменные в CIL 632
Скрытая ссылка this 633
Представление итерационных конструкций в CIL 633
Создание сборки . NET на CIL 634
Создание С ILCars.dll 634
СозданиеCILCarClient.exe 637
Динамические сборки 638
Пространство имен System. Re f lection. Emit 639
Роль типа System. Reflection. Emit. ILGenerator 640
Создание динамической сборки 641
Генерация сборки и набора модулей 642
Роль типа Module Builder 643
Генерация типа HelloClassn принадлежащей ему строковой переменной 644
Генерация конструкторов 645
Генерация метода S а у Н е 11 о () 646
Использование динамически сгенерированной сборки 646
Резюме 647
Глава 18. Динамические типы и исполняющая среда динамического языка 648
Роль ключевого слова С# dynamic 648
Вызов членов на динамически объявленных данных 650
Роль сборки Microsoft.CSharp.dll 651
Область применения ключевого слова dynamic 652
Ограничения ключевого слова dynamic 653
Практическое применение ключевого слова dynamic 653
Роль исполняющей среды динамического языка (DLR) 654
Роль деревьев выражений 654
Роль пространства имен System. Dynamic 655
18 Содержание
Динамический поиск в деревьях выражений во время выполнения 655
Упрощение вызовов позднего связывания с использованием динамических типов 656
Использование ключевого слова dynamic для передачи аргументов 657
Упрощение взаимодействия с СОМ посредством динамических данных 659
Роль первичных сборок взаимодействия 660
Встраивание метаданных взаимодействия 661
Общие сложности взаимодействия с СОМ 661
Взаимодействие с СОМ с использованием средств языка С# 4.0 662
Взаимодействие с СОМ без использования средств языка С# 4.0 666
Резюме 667
Часть V. Введение в библиотеки базовых классов .NET 669
Глава 19. Многопоточность и параллельное программирование 670
Отношения между процессом, доменом приложения, контекстом и потоком 670
Проблема параллелизма 671
Роль синхронизации потоков 672
Краткий обзор делегатов .NET 672
Асинхронная природа делегатов 674
Методы BeginlnvokeO и EndlnvokeO ч 675
Интерфейс System.IAsyncResult 675
Асинхронный вызов метода 676
Синхронизация вызывающего потока 676
Роль делегата AsyncCallback 678
Роль класса AsyncResult 679
Передача и прием специальных данных состояния 680
Пространство имен System.Threading 681
Класс System.Threading.Thread 682
Получение статистики о текущем потоке 683
Свойство Name 684
Свойство Priority 684
Программное создание вторичных потоков 685
Работа с делегатом ThreadStart 685
Работа с делегатом ParametrizedThreadStart 687
Класс AutoResetEvent 688
Потоки переднего плана и фоновые потоки 689
Пример проблемы, связанной с параллелизмом 690
Синхронизация с использованием ключевого слова С# 1о с к 692
Синхронизация с использованием типа System.Threading.Monitor 694
Синхронизация с использованием типа System.Threading. Interlocked 695
Синхронизация с использованием атрибута [Synchronization] 696
Программирование с использованием обратных вызовов Timer 697
Пул потоков CLR 698
Параллельное программирование на платформе .NET 700
Интерфейс Tksk Parallel Library API 700
Роль класса Parallel 701
Понятие параллелизма данных 702
Класс Task 703
Обработка запроса на отмену 704
Понятие параллелизма задач 705
Запросы параллельного LINQ (PLINQ) 708
Выполнение запроса PLINQ 709
Отмена запроса PLINQ 709
Резюме 710
Содержание
19
Глава 20. Файловый ввод-вывод и сериализация объектов 711
Исследование пространства имен System.10 711
Классы Directory (Directorylnfo) и File (FileInfo) 712
Абстрактный базовый класс FileSystemlnfo 713
Работа с типом Directorylnfo 714
Перечисление файлов с помощью типа Directorylnfo 715
Создание подкаталогов с помощью типа Directorylnfo 716
Работа с типом Directory 717
Работа с типом Drive Info < 717
Работа с классом File Info 719
Метод Filelnfo.Create () 719
Метод FileJnfo.OpenO 720
Методы FileOpen.OpenRead() и Filelnfo.OpenWrite() 721
Метод Filelnfo.OpenText() 722
Методы Filelnfо.CreateTextO и Filelnfo.AppendTextO 722
Работа с типом File 722
Дополнительные члены File 723
Абстрактный класс Stream 724
Работа с классом FileStream 725
Работа с классами StreamWriter и StreamReader 726
Запись в текстовый файл 727
Чтение из текстового файла 728
Прямое создание экземпляров классов StreamWriter/StreamReader 729
Работа с классами StringWriter и StringReader 730
Работа с классами BinaryWriter и BinaryReader 731
Программное отслеживание файлов 732
Понятие сериализации объектов 734
Роль графов объектов 736
Конфигурирование объектов для сериализации 737
Определение сериализуемых типов 737
Общедоступные поля, приватные поля и общедоступные свойства 738
Выбор форматера сериализации 738
Интерфейсы IFormatter и IRemotingFormatter 739
Точность типов среди форматеров 740
Сериализация объектов с использованием BinaryFormatter 741
Десериализация объектов с использованием BinaryFormatter 742
Сериализация объектов с использованием SoapFormatter 743
Сериализация объектов с использованием XmlSerializer 743
Управление генерацией данных XML 744
Сериализация коллекций объектов 746
Настройка процессов сериализации SOAP и двоичной сериализации 747
Углубленный взгляд на сериализацию объектов 748
Настройка сериализации с использованием интерфейса ISerializable 749
Настройка сериализации с использованием атрибутов 751
Резюме 752
Глава 21. AD0.NET, часть I: подключенный уровень 754
Высокоуровневое определение ADO. NET 754
Три стороны ADO.NET 755
Поставщики данных ADO.NET 756
Поставщики данных ADO.NET от Microsoft 757
О сборке System.Data.OracleClient.dll 759
Получение сторонних поставщиков данных ADO.NET 759
Дополнительные пространства имен ADO.NET 759
20 Содержание
Типы из пространства имен System.Data 760
Роль интерфейса IDbConпесtion 761
Роль интерфейса I DbTrans act ion 762
Роль интерфейса IDbCommand 762
Роль интерфейсов IDbDataParameter и IDataParameter 762
Роль интерфейсов IDbDataAdapter и IDataAdapter 763
Роль интерфейсов IDataReader и IDataRecord 763
Абстрагирование поставщиков данных с помощью интерфейсов 764
Повышение гибкости с помощью конфигурационных файлов приложения 766
Создание базы данных AutoLot 767
Создание таблицы Inventory 767
Создание хранимой процедуры GetPetName () 769
Создание таблиц Customers и Orders 770
Визуальное создание отношений между таблицами 772
Модель генератора поставщиков данных ADO. NET 111
Полный пример генератора поставщиков данных 774
Возможные трудности с моделью генератора поставщиков 776
Элемент<connectionStrings> 777
Подключенный уровень ADO.NET 778
Работа с объектами подключения 779
Работа с объектами ConnectionStringBuilder 781
Работа с объектами команд 782
Работа с объектами чтения данных 783
Получение множественных результатов с помощью объекта чтения данных 784
Создание повторно используемой библиотеки доступа к данным 785
Добавление логики подключения 786
Добавление логики вставки 787
Добавление логики удаления 788
Добавление логики изменения 788
Добавление логики выборки 789
Работа с параметризованными объектами команд 790
Выполнение хранимой процедуры 792
Создание консольного пользовательского интерфейса 793
Реализация метода М a i n () 794
Реализация метода Showlnstructions () 795
Реализация метода List Inventory () 795
Реализация метода DeleteCarO 796
Реализация метода InsertNewCar () 797
Реализация метода UpdateCarPetName () 797
Реализация метода LookUpPetName () 798
Транзакции баз данных 799
Основные члены объекта транзакции ADO.NET 800
Добавление таблицы CreditRisksB базу данных AutoLot 800
Добавление метода транзакции в InventoryDAL 801
Тестирование транзакции в нашей базе данных 802
Резюме 803
Глава 22. AD0.NET, часть II: автономный уровень 804
Знакомство с автономным уровнем ADO. NET 804
Роль объектов Data Set 805
Основные свойства класса Data Set 806
Основные методы класса Data Set 807
Создание DataSet 807
Работа с объектами DataColumn 808
Содержание
21
Создание объекта DataColumn 809
Включение автоинкрементных полей 810
Добавление объектов DataColumn в DataTable 810
Работа с объектами DataRow 810
Свойство RowState 812
Свойство DataRowVersion 813
Работа с объектами DataTable 814
Вставка объектов DataTable в DataSet 815
Получение данных из объекта DataSet 815
Обработка данных из DataTable с помощью объектов DataTableReader 816
Сериализация объектов DataTable и DataSet в формате XML 817
Сериализация объектов DataTable и DataSet в двоичном формате 818
Привязка объектов DataTable к графическим интерфейсам Windows Forms 819
Заполнение DataTable из обобщенного List<T> 820
Удаление строк из DataTable 822
Выборка строк с помощью фильтра 823
Изменение строк в DataTable 826
Работа с типом DataView 826
Работа с адаптерами данных 828
Простой пример адаптера данных 829
Замена имен из базы данных более понятными названиями 829
Добавление в AutoLotDAL. dl 1 возможности отключения 830
Определение начального класса 831
Настройка адаптера данных с помощью SqlCommandBuilder 831
Реализация метода GetAlllnventory () 833
Реализация метода Updatelnventory () 833
Установка номера версии 833
Тестирование автономной функциональности 833
Объекты DataSet для нескольких таблиц и взаимосвязь данных 834
Подготовка адаптеров данных 835
Создание отношений между таблицами 836
Изменение таблиц базы данных 837
Переходы между взаимосвязанными таблицами 837
Средства конструктора баз данных в Windows Forms 839
Визуальное проектирование элементов DataGridView 840
Сгенерированный файл арр. conf ig 843
Анализ строго типизированного DataSet 843
Анализ строго типизированного DataTable 845
Анализ строго типизированного DataRow 845
Анализ строго типизированного адаптера данных 845
Завершение приложения Windows Forms 846
Выделение строго типизированного кода работы с базами данных
в библиотеку классов 847
Просмотр сгенерированного кода 848
Выборка данных с помощью сгенерированного кода 849
Вставка данных с помощью сгенерированного кода 850
Удаление данных с помощью сгенерированного кода 850
Вызов хранимой процедуры с помощью сгенерированного кода 851
Программирование с помощью LINQ to DataSet 851
Библиотека расширений DataSet 853
Получение DataTable, совместимого с LINQ 853
Метод расширения DataRowExtensions.Field<T>() 855
Заполнение новых объектов DataTable с помощью LINQ-запросов 855
Резюме 856
22 Содержание
Глава 23. ADO.NET, часть III: Entity Framework 857
Роль Entity Framework 857
Роль сущностей 859
Строительные блоки Entity Framework 860
Роль служб объектов 861
Роль клиента сущности 861
Роль файла *.edmx 863
Роль классов ObjectContext и ObjectSet<T> 863
Собираем все вместе 865
Построение и анализ первой модели EDM 866
Генерация файла *. е dmx 866
Изменение формы сущностных данных 869
Просмотр отображений 871
Просмотр данных сгенерированного файла *. еdmx 871
Просмотр сгенерированного исходного кода 873
Улучшение сгенерированного исходного кода 875
Программирование с использованием концептуальной модели 875
Удаление записи 876
Обновление записи 877
Запросы с помощью LINQ to Entities 878
Запросы с помощью Entity SQL 879
Работа с объектом EntityDataReader 880
Проект AutoLotDAL версии 4.0, теперь с сущностями 881
Отображение хранимой процедуры 881
Роль навигационных свойств 882
Использование навигационных свойств внутри запросов LINQ to Entity 884
Вызов хранимой процедуры 885
Привязка данных сущностей к графическим пользовательским
интерфейсам Windows Fbrms 886
Добавление кода привязки данных 888
Резюме 890
Глава 24. Введение в LINQ to XML 891
История о двух API-интерфейсах XML 891
Интерфейс LINQ to XML как лучшая модель DOM 893
Синтаксис литералов Visual Basic как наилучший интерфейс LINQ to XML 893
Члены пространства имен System.Xml.Linq 895
Осевые методы LINQ to XML 895
Избыточность XName (и XNamespace) 897
Работа с XElement HXDocument 898
Генерация документов из массивов и контейнеров 899
Загрузка и разбор XML-содержимого 901
Манипулирование XML-документом в памяти 901
Построение пользовательского интерфейса приложения LINQ to XML 901
Импорт файла Inventory, xml 902
Определение вспомогательного класса LINQ to XML 902
Оснащение пользовательского интерфейса вспомогательными методами 904
Резюме 905
Глава 25. Введение в Windows Communication Foundation 906
API-интерфейсы распределенных вычислений 906
Роль DCOM 907
Роль служб СОМ+/Enterprise Services 908
Роль MSMQ 909
Содержание
23
Роль .NET Remotlng 909
Роль веб-служб XML 910
Именованные каналы, сокеты и Р2Р 913
Роль^УСК 913
Обзор средств WCF 914
Обзор архитектуры, ориентированной на службы 914
WCF: итоги 915
Исследование основных сборок WCF 916
Шаблоны проектов WCF в Visual Studio 917
Шаблон проекта WCF Service 918
Базовая композиция приложения WCF 919
Понятие ABC в WCF 920
Понятие контрактов WCF 920
Понятие привязок WCF 921
Понятие адресов WCF 924
Построение службы WCF 925
Атрибут [ServiceContract] 926
Атрибут [OperationContract] 927
Служебные типы как контракты операций 928
Хостинг службы WCF 928
УстановкаАВСвнутрифайлаАрр.соп:£1д 929
Кодирование с использованием типа ServiceHost 930
Указание базового адреса 930
Подробный анализ типа ServiceHost 932
Подробный анализ элемента <system.serviceModel> 933
Включение обмена метаданными 934
Построение клиентского приложения WCF 936
Генерация кода прокси с использованием svcutil.exe 937
Генерация кода прокси с использованием Visual Studio 2010 938
Конфигурирование привязки на основе TCP 939
Упрощение конфигурационных настроек в WCF 4.0 940
Конечные точки по умолчанию в WCF 4.0 941
Предоставление одной службы WCF с использованием множества привязок 942
Изменение установок для привязки WCF 943
Конфигурация поведения МЕХ по умолчанию в WCF 4.0 944
Обновление клиентского прокси и выбор привязки 945
Использование шаблона проекта WCF Service Library 946
Построение простой математической службы 947
Тестирование службы WCFс помощью WcfTestClient.exe 947
Изменение конфигурационных файлов с помощью SvcConfigEditor.exe 948
Хостинг службы WCF в виде службы Windows 949
Спецификация ABC в коде 950
Включение МЕХ 951
Создание программы установки для службы Windows 952
Установка службы Windows 953
Асинхронный вызов службы на стороне клиента 954
Проектирование контрактов данных WCF 955
Использование веб-ориентированного шаблона проекта WCF Service 956
Реализация контракта службы 958
Роль файла *.svc 959
Содержимое файла Web. с on fig 959
Тестирование службы 960
Резюме 960
24 Содержание
Глава 26. Введение в Windows Workflow Foundation 4.0 961
Определение бизнес-процесса 962
Роль WF 4.0 962
Построение простого рабочего потока 963
Просмотр полученного кода XAML 965
Исполняющая среда WF 4.0 967
Хостинг рабочего потока с использованием класса Workf lowlnvoker 967
Хостинг рабочего потока с использованием класса Workf lowApplication 970
Переделка первого рабочего потока 971
Знакомство с действиями Windows Workflow 4.0 971
Действия потока управления 971
Действия блок-схемы 972
Действия обмена сообщениями 973
Действия исполняющей среды и действия-примитивы 973
Действия транзакций 974
Действия над коллекциями и действия обработки ошибок 974
Построение рабочего потока в виде блок-схемы 975
Подключение действий к блок-схеме 975
Работа с действием InvokeMethod 976
Определение переменных уровня рабочего потока 977
Работа с действием FlowDecision 978
Работа с действием TerminateWorkf low 978
Построение условия "true" 979
Работа с действием ForEach<T> 979
Завершение приложения 981
Промежуточные итоги 982
Изоляция рабочих потоков в выделенных библиотеках 984
Определение начального проекта 984
Импорт сборок и пространств имен 985
Определение аргументов рабочего потока 986
Определение переменных рабочего потока 986
Работа с действием Assign 987
Работа с действиями IfnSwitch 987
Построение специального действия кода 988
Использование библиотеки рабочего потока 991
Получение выходного аргумента рабочего потока 992
Резюме 993
Часть VI. Построение настольных пользовательских
приложений с помощью WPF 995
Глава 27. Введение в Windows Presentation Foundation и XAML 996
Мотивация, лежащая в основе WPF 997
Унификация различных API-интерфейсов 997
Обеспечение разделения ответственности через XAML 998
Обеспечение оптимизированной модели визуализации 998
Упрощение программирования сложных пользовательских интерфейсов 999
Различные варианты приложений WPF 1000
Традиционные настольные приложения 1000
WPF-приложения на основе навигации 1001
Приложения ХВАР 1002
Отношения между WPF и Silverlight 1003
Исследование сборок WPF 1004
Содержание 25
Роль класса Application 1005
Роль класса Wi ndow 1007
Роль класса System.Windows.Controls.ContentControl 1007
Роль класса System.Windows.Controls.Control 1008
Роль класса System.Windows.FrameworkElement 1009
Роль класса System.Windows.UIElement 1010
Роль класса System.Windows.Media.Visual 1010
Роль класса System. Windows. DependencyObject 1010
Роль класса System.Windows.Threading.DispatcherObject 1011
Построение приложения WPF без XAML 1011
Создание строго типизированного окна 1013
Создание простого пользовательского интерфейса 1013
Взаимодействие с данными уровня приложения 1015
Обработка закрытия объекта Window 1016
Перехват событий мыши 1017
Перехват клавиатурных событий 1018
Построение приложения WPF с использованием только XAML 1019
Определение Ma inWindow в XAML 1020
Определение объекта Application в XAML 1021
Обработка файлов XAML с помощью msbuild.exe 1022
Трансформация разметки в сборку .NET 1023
Отображение XAML-данных окна на код С# 1024
PoльBAML 1025
Отображение XAML-данных приложения на код С# 1026
Итоговые замечания о процессе трансформирования XAML в сборку 1027
Синтаксис XAML для WPF 1027
Введение в Kaxaml 1027
Пространства имен XAML XML и "ключевые слова" XAML 1029
Управление объявлениями классов и переменных-членов 1031
Элементы XAML, атрибуты XAML и преобразователи типов 1032
Понятие синтаксиса XAML "свойство-элемент" 1033
Понятие присоединяемых свойств XAML 1034
Понятие расширений разметки XAML 1034
Построение приложений WPF с использованием файлов отделенного кода 1036
Добавление файла кода для класса MainWindow 1036
Добавление файла кода для класса МуАрр 1037
Обработка файлов кода с помощью msbuild.exe 1038
Построение приложений WPF с использованием Visual Studio 2010 1038
Шаблоны проектов WPF 1039
Знакомство с инструментами визуального конструктора WPF 1039
Проектирование графического интерфейса окна 1042
Реализация события Loaded 1044
Реализация события Click объекта Button 1045
Реализация события Closed 1046
Тестирование приложения 1046
Резюме 1047
Глава 28. Программирование с использованием элементов управления WPF 1048
Обзор библиотеки элементов управления WPF 1048
Работа с элементами управления WPF в Visual Studio 2010 1049
Элементы управления Ink API 1051
Элементы управления документами WPF 1051
Общие диалоговые окна WPF 1052
Подробные сведения находятся в документации 1052
26 Содержание
Управление компоновкой содержимого с использованием панелей 1053
Позиционирование содержимого внутри панелей Canvas 1054
Позиционирование содержимого внутри панелей WrapPanel 1056
Позиционирование содержимого внутри панелей StackPanel 1058
Позиционирование содержимого внутри панелей Grid 1059
Позиционирование содержимого внутри панелей DockPanel 1060
Включение прокрутки в типах панелей 1061
Построение главного окна с использованием вложенных панелей 1062
Построение системы меню 1063
Построение панели инструментов 1064
Построение строки состояния 1065
Завершение дизайна пользовательского интерфейса 1065
Реализация обработчиков событий MouseEnter/MouseLeave 1066
Реализация логики проверки правописания 1066
Понятие управляющих команд WPF 1067
Внутренние объекты управляющих команд 1067
Подключение команд к свойству Command 1068
Подключение команд к произвольным действиям 1069
Работа с командами Open и Save 1071
Построение пользовательского интерфейса WPF с помощью Expression Blend 1072
Ключевые аспекты IDE-среды Expression Blend 1073
Использование элемента TabControl 1077
Построение вкладки Ink API 1079
Проектирование элемента Tool Bar 1080
Элемент управления RadioButton 1082
Элемент управления InkCanvas 1084
Элемент управления С оmboB ox 1086
Сохранение, загрузка и очистка данных InkCanvas 1087
Введение в интерфейс Documents API 1088
Блочные элементы и встроенные элементы 1088
Диспетчеры компоновки документа 1089
Построение вкладки Documents 1089
Наполнение FlowDocument с использованием Blend 1091
Наполнение FlowDocument с помощью кода 1091
Включение аннотаций и "клейких" заметок 1093
Сохранение и загрузка потокового документа 1094
Введение в модель привязки данных WPF 1095
Построение вкладки Data Binding 1096
Установка привязки данных с использованием Blend 1096
Свойство DataContext 1097
Преобразование данных с использованием IValueConverter 1099
Установка привязок данных в коде 1099
Построение вкладки DataGrid 1100
Резюме 1102
Глава 29. Службы визуализации графики WPF 1103
Службы графической визуализации WPF 1103
Опции графической визуализации WPF 1104
Визуализация графических данных с использованием фигур 1105
Добавление прямоугольников, эллипсов и линий на поверхность Canvas 1107
Удаление прямоугольников, эллипсов и линий с поверхности Canvas 1110
Работа с элементами Polyline и Polygon 1110
Работа с элементом Path 1111
Кисти и перья WPF 1115
Содержание 27
Конфигурирование кистей с использованием Visual Studio 2010 1115
Конфигурирование кистей в коде 1117
Конфигурирование перьев 1118
Применение графических трансформаций 1118
Первый взгляд на трансформации 1119
Трансформация данных Canvas 1120
Работа с фигурами в Expression Blend 1122
Выбор фигуры для визуализации из палитры инструментов 1122
Преобразование фигур в пути 1123
Комбинирование фигур 1123
Редакторы кистей и трансформаций 1123
Визуализация графических данных с использованием рисунков и геометрий 1125
Построение кисти DrawingBrush с использованием объектов Geometry 1126
Рисование с помощью DrawingBrush 1127
Включение типов Drawing в Drawinglmage 1128
Генерация сложной векторной графики с использованием Expression Design 1128
Экспорт документа Expression Design в XAML 1129
Визуализация графических данных с использованием визуального уровня 1130
Базовый класс Vi s u a 1 и производные дочерние классы 1130
Первый взгляд на класс DrawingVisual 1131
Визуализация графических данных в специальном диспетчере компоновки 1133
Реагирование на операции проверки попадания 1134
Резюме 1136
Глава 30. Ресурсы, анимация и стили WPF 1137
Система ресурсов WPF 1137
Работа с двоичными ресурсами 1138
Программная загрузка изображения 1139
Работа с объектными (логическими) ресурсами 1142
Роль свойства Resources 1143
Определение ресурсов уровня окна 1143
Расширение разметки {StaticResource} 1145
Изменение ресурса после извлечения 1145
Расширение разметки {DynamicResource} 1146
Ресурсы уровня приложения 1146
Определение объединенных словарей ресурсов 1147
Определение сборки из одних ресурсов 1149
Извлечение ресурсов в Expression Blend 1150
Службы анимации WPF 1152
Роль классов анимации 1152
Свойства То, From и By 1153
Роль базового класса Timeline 1154
Написание анимации в коде С# 1154
Управление темпом анимации 1155
Запуск в обратном порядке и циклическое выполнение анимации 1156
Описание анимации в XAML 1157
Роль раскадровки 1158
Роль триггеров событий 1158
Анимация с использованием дискретных ключевых кадров s 1159
Роль стилей WPF 1160
Определение и применение стиля 1160
Переопределение настроек стиля 1161
Автоматическое применение стиля с помощью TargetType 1161
Создание подклассов существующих стилей 1162
28 Содержание
Роль безымянных стилей 1163
Определение стилей с триггерами 1164
Определение стилей с множеством триггеров 1164
Анимированные стили 1165
Программное применение стилей 1165
Генерация стилей с помощью Expression Blend 1166
Работа с визуальными стилями по умолчанию 1167
Резюме 1169
Глава 31. Шаблоны элементов управления WPF и пользовательские
элементы управления 1170
Роль свойств зависимости 1170
Проверка существующего свойства зависимости 1172
Важные замечания относительно оболочек свойств CLR 1175
Построение специального свойства зависимости 1176
Добавление процедуры проверки достоверности данных 1179
Реакция на изменение свойства 1179
Маршрутизируемые события 1181
Роль маршрутизируемых пузырьковых событий 1182
Продолжение или прекращение пузырькового распространения 1182
Роль маршрутизируемых туннелируемых событий 1183
Логические деревья, визуальные деревья и шаблоны по умолчанию 1185
Программный просмотр логического дерева 1185
Программный просмотр визуального дерева 1187
Программный просмотр шаблона по умолчанию для элемента управления 1188
Построение специального шаблона элемента управления в Visual Studio 2010 1191
Шаблоны как ресурсы 1192
Включение визуальных подсказок с использованием триггеров 1193
Роль расширения разметки {TemplateBinding} 1194
Роль класса ContentPresenter 1196
Включение шаблонов в стили 1196
Построение специальных элементов UserControl с помощью Expression Blend 1197
Создание проекта библиотеки UserControl 1198
Создание WPF-приложения JackpotDeluxe 1204
Извлечение UserControl из геометрических объектов 1204
Роль визуальных состояний .NET 4.0 1205
Завершение приложения JackpotDeluxe 1209
Резюме 1212
Часть VII. Построение веб-приложений с использованием ASP.NET 1213
Глава 32. Построение веб-страниц ASP.NET 1214
Роль протокола HTTP 1214
Цикл запрос / ответ HTTP 1215
HTTP — протокол без поддержки состояния 1215
Веб-приложения и веб-серверы 1216
Роль виртуальных каталогов IIS 1216
Веб-сервер разработки ASP NET 1217
Роль языка HTML 1217
Структура HTML-документа 1218
Роль формы HTML 1219
Инструменты визуального конструктора HTML в Visual Studio 2010 1219
Построение формы HTML 1220
Роль сценариев клиентской стороны 1221
Пример сценария клиентской стороны 1223
Содержание 29
Обратная отправка веб-серверу 1224
Обратные отправки в ASP.NET 1225
Набор средств API-интерфейса ASP NET 1225
Основные средства ASP.NET 1.0-1.1 1225
Основные средства ASP.NET 2.0 1227
Основные средства ASP NET 3.5 (и .NET 3.5 SP1) 1228
Основные средства ASP.NET 4.0 1228
Построение однофайловой веб-страницы ASP.NET 1229
Ссылка на сборку AutoLotDAL.dll 1229
Проектирование пользовательского интерфейса 1230
Добавление логики доступа к данным 1231
Роль директив ASP NET 1233
Анализ блока script 1235
Анализ объявлений элементов управления ASP NET 1235
Цикл компиляции для однофайловых страниц 1236
Построение веб-страницы ASP.NET с использованием файлов кода 1237
^Ссылка на сборку AutoLotDAL.dll 1239
Обновление файла кода 1240
Цикл компиляции многофайловых страниц 1240
Отладка и трассировка страниц ASP NET 1241
Веб-сайты и веб-приложения ASP.NET 1242
Структура каталогов веб-сайта ASP.NET 1243
Ссылаемые сборки 1244
Роль папки App_Code 1244
Цепочка наследования типа Page 1245
Взаимодействие с входящим запросом HTTP 1246
Получение статистики браузера 1247
Доступ к входным данным формы 1248
Свойство IsPostBack 1248
Взаимодействие с исходящим ответом HTTP 1249
Выдача HTML-содержимого 1250
Перенаправление пользователей 1250
Жизненный цикл веб-страницы ASP.NET 1251
Роль атрибута AutoEventWireup 1252
Событие Error 1253
Роль файла Web. con fig 1254
Утилита администрирования веб-сайтов ASP.NET 1255
Резюме 1256
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1257
Природа веб-элементов управления 1257
Обработка событий серверной стороны 1258
Свойство AutoPostBack 1259
Базовые классы Control и WebControl 1260
Перечисление содержащихся элементов управления 1260
Динамическое добавление и удаление элементов управления 1262
Взаимодействие с динамически созданными элементами управления 1263
Функциональность базового класса WebControl 1264
Основные категории веб-элементов управления ASP NET 1265
Краткая информация о System.Web.UI.HtmlControls 1266
Документация по веб-элементам управления 1267
Построение веб-сайта ASPNET Cars e 1268
Работа с мастер-страницами 1268
Определение страницы содержимого Default.aspx 1274
30 Содержание
Проектирование страницы содержимого Inventory, aspx 1276
Проектирование страницы содержимого BuildCar. aspx 1279
Роль элементов управления проверкой достоверности 1282
Класс RequiredFieldValidator 1283
Класс RegularExpressionValidator 1284
Класс RangeValidator 1284
Класс CompareValidator 1284
Создание итоговой панели проверки достоверности 1285
Определение групп проверки достоверности 1286
Работа с темами 1288
Файлы*, skin 1289
Применение тем ко всему сайту 1291
Применение тем на уровне страницы 1291
Свойство SkinID 1291
Программное назначение тем 1292
Резюме 1293
Глава 34. Управление состоянием в ASP.NET 1294
Проблема поддержки состояния 1294
Приемы управления состоянием ASP. NET 1296
Роль состояния представления ASP NET 1296
Демонстрация работы с состоянием представления 1297
Добавление специальных данных в состояние представления 1299
Роль файла Global.asax 1299
Глобальный обработчик исключений "последнего шанса" 1301
Базовый класс HttpApplication 1302
Различие между свойствами Application и Session 1303
Поддержка данных состояния уровня приложения 1303
Модификация данных приложения 1305
Обработка останова веб-приложения 1306
Работа с кэшем приложения 1307
Работа с кэшированием данных 1307
Модификация файла *. a sрх 1309
Поддержка данных сеанса 1311
Дополнительные члены HttpSessionState 1314
Cookie-наборы 1315
Создание cookie-наборов 1315
Чтение входящих cookie-данных 1316
Роль элемента <sessionState> 1317
Хранение данных сеанса на сервере состояния сеансов ASP NET 1317
Хранение информации о сеансах в выделенной базе данных 1318
Интерфейс ASPNET Profile API 1319
База данных ASPNETDB.mdf 1319
Определение пользовательского профиля BWeb.config 1320
Программный доступ к данным профиля 1322
Группирование данных профиля и сохранение специальных объектов 1323
Резюме 1325
Часть VIII. Приложения 1327
Приложение А. Программирование с помощью Windows Forms 1328
Приложение Б. Независимая от платформы разработка .NET-приложений
с помощью Mono 1369
Предметный указатель 1386
Об авторе
Эндрю Троелсен (Andrew Trolesen) с любовью вспоминает свой самый первый
компьютер Atari 400, оснащенный кассетным устройством хранения и черно-белым
телевизионным монитором "(который его родители разрешили ему поставить у себя в спальне,
за что им большое спасибо). Еще он благодарен ушедшему в небытие журналу Compute!,
степени бакалавра в области математической лингвистики и трем годам
формального изучения санскрита. Все это оказало значительное влияние на его сегодняшнюю
карьеру.
В настоящее время Эндрю работает в центре Intertech, занимающемся
консультированием и обучением работе с .NET и Java (www .intertech. com).
На его счету уже несколько написанных книг, в числе которых Developer's Workshop to
COM and ATL 3.0 {Wordware Publishing', 2000 г.), COMand.NET'Interoperability (Apress, 2002 r.)
и Visual Basic 2008 and the .NET 3.5 Platform: An Advanced Guide (Apress, 2008 г.).
0 техническом редакторе
Энди Олсен (Andy Olsen) — независимый консультант и инструктор, проживающий
в Великобритании. Энди работает с .NET, начиная с самой первой бета-версии этого
продукта, и занимается активным исследованием новых функциональных
возможностей, которые появились в .NET 4.O. Он живет у моря в городе Суонси вместе со своей
женой Джейн и детьми Эмили и Томом. Любит делать пробежки вдоль побережья
(регулярно останавливаясь на чашечку кофе по пути), кататься на лыжах и следить за
лебедями и орликами. Связаться с ним можно по адресу andyo@olsensof t. com.
Благодарности
По какой-то непонятной причине (а может и целому ряду причин) настоящее
издание книги оказалось гораздо более трудным в написании, чем ожидалось. Если бы не
помощь и поддержка многих хороших людей, скорее всего, оно не вышло бы в свет так
скоро.
Прежде всего, огромное спасибо техническому редактору Энди Олсену. Помимо
указания на пропущенные точки с запятой он привнес массу замечательных
предложений, позволивших сделать первоначальные примеры кода более понятными и точными.
Спасибо тебе, Энди!
Далее хочу выразить благодарность всей команде литературных редакторов, а
именно — Мэри Бер (Магу Вепг), Патрику Мидеру (Patrick Meader), Кэти Стэнс (Katie Stence)
и Шэрон Тердеман (Sharon Terdeman), которые выявили и устранили массу
грамматических ошибок.
Особая благодарность Дебре Кэлли (Debra Kelly) из Apress. Это наш первый
совместный проект и, несмотря на многочисленные опоздания с предоставлением глав,
путаницу с электронными письмами и постоянное добавление обновлений, я все равно очень
надеюсь, что она согласится работать со мной снова. Спасибо тебе, Дебра!
И, наконец, напоследок спасибо моей жене Мэнди. Как всегда, ты поддерживаешь
меня в здравом уме во время написания всех моих проектов.
Введение
Первое издание настоящей книги появилось вместе с выпуском Microsoft второй
бета-версии .NET 1.0 (летом 2001 г.) и с тех пор постоянно переиздавалось в том
или ином виде. С того самого времени автор с чрезвычайным удовольствием и
благодарностью наблюдал за тем, как данная работа продолжала пользоваться
популярностью в прессе и, самое главное, среди читателей. Через некоторое время, в 2002 г., она
даже была номинирована на премию Jolt Award (которую, увы, получить так и не
удалось, но книга попала в число финалистов), а в 2003 г. — еще и на премию Referenceware
Excellence Award, где стала лучшей книгой года по программированию.
Даже еще более важно то, что автору стали приходить электронные письма от
читателей со всего мира. Общаться с множеством людей и узнавать, что данная книга
как-то помогла им в карьере, просто замечательно. В связи с этим, хотелось бы
отметить, что настоящая книга с каждым разом становится все лучше именно благодаря
читателям, которые присылают различные предложения по улучшению, указывают на
допущенные в тексте опечатки и обращают внимание на прочие промахи.
Автор был просто ошеломлен, узнав, что эта книга использовалась и продолжает
использоваться на занятиях в колледжах и университетах и является обязательной для
прочтения на многих предвыпускных и выпускных курсах в области вычислительной
техники.
Автор благодарит прессу, читателей, преподавателей и всех остальных и желает им
успешного программирования!
С самого первого выпуска настоящей книги автор прилагал все усилия и обновлял
книгу так, чтобы она отражала текущие возможности каждой выходившей версии
платформы .NET. В настоящем издании материал был полностью пересмотрен и расширен с
целью охвата новых средств языка С# 2010 и платформы .NET 4.O. Вы найдете
информацию по таким новым компонентам, как Dynamic Language Runtime (DLR), Task Parallel
Library (TPL), Parallel LINQ (PLINQ) и ADO.NET Entity Framework (EF). Кроме того, в книге
описан ряд менее значительных (но очень полезных) обновлений, наподобие
именованных и необязательных аргументов в С# 2010, типа класса Lazy<T> и т.д.
Помимо описания новых компонентов и возможностей, в книге по-прежнему
предоставляется весь необходимый базовый материал по языку С# в целом, основам
объектно-ориентированного программирования (ООП), конфигурированию сборок,
получению доступа к базам данных (через ADO.NET), а также процессу построения настольных
приложений с графическим пользовательским интерфейсом, веб-приложений и
распределенных систем (и многим другим темам).
Как и в предыдущих изданиях, в этом издании весь материал по языку
программирования С# и библиотекам базовых классов .NET подается в дружественной и понятной
читателю манере. Автор никогда не понимал, зачем другие технические авторы
стараются писать свои книги так, чтобы те больше напоминали сложный научный труд, а
не легкое для восприятия пособие. В новом издании основное внимание уделяется
предоставлению информации, которой необходимо владеть для того, чтобы разрабатывать
программные решения прямо сегодня, а не глубокому изучению малоинтересных
эзотерических деталей.
Введение 33
Автор и читатели - одна команда
Авторам книг по технологиям приходится писать для очень требовательной группы
людей. Всем известно, что детали разработки программных решений с помощью любой
платформы (.NET, Java и СОМ) очень сложны и сильно зависят от отдела, компании,
клиентской базы и поставленной задачи. Кто-то работает в сфере электронных
публикаций, кто-то занимается разработкой систем для правительства и региональных
органов власти, а кто-то сотрудничает с НАСА или военными департаментами. Что касается
автора настоящей книги, то сам он занимается разработкой детского образовательного
ПО (возможно, вам приходилось слышать об Oregon Trail или Amazon Trail), а также
различных многоуровневых систем и проектов в медицинской и финансовой сфере. А это
значит, что код, который придется писать читателю, скорее всего, будет иметь мало
чего общего с тем, с которым приходится иметь дело автору.
Поэтому в настоящей книге автор специально старался избегать приведения
примеров, свойственных только какой-то конкретной области производства или
программирования. Из-за этого все концепции, связанные с С#, ООП, CLR и библиотеками базовых
классов .NET, объясняются на общих примерах. В частности, здесь везде применяется
одна и та же тема, которая так или иначе близка каждому — автомобили. Остальное
остается за читателем.
Задача автора состоит в том, чтобы максимально доступно объяснить читателю
язык программирования С# и основные концепции платформы .NET настолько хорошо,
а также описать все возможные инструменты и стратегии, которые могут
потребоваться для продолжения обучения по прочтении данной книги.
Задача читателя состоит в том, чтобы усвоить всю эту информацию и
научиться применять ее на практике при разработке своих программных решений. Конечно,
скорее всего, проекты, которые понадобится выполнять в будущем, не будут связаны
с автомобилями и их дружественными именами, но именно в этом и состоит вся суть
применения получаемых знаний на практике! После изучения представленных в этой
книге концепций можно будет спокойно создавать решения .NET, удовлетворяющие
требованиям любой среды программирования.
Краткий обзор содержания
Эта книга логически разделена на восемь частей, в каждой из которых
содержится ряд взаимосвязанных между собой глав. Те, кто читал предыдущие издания данной
книги, сразу же отметят ряд отличий. Например, новые средства языка С# больше не
описываются в отдельной главе. Вместо этого они рассматриваются в тех главах, в
которых их появление является вполне естественным. Кроме того, по просьбе читателей
был значительно расширен материал по технологии Windows Presentation Foundation
(WPF). Ниже приведено краткое описание содержимого каждой из частей и глав
настоящей книги.
Часть I. Общие сведения о языке С# и платформе .NET
Назначением первой части этой книги является общее ознакомление читателя с
природой платформы .NET и различными средствами разработки, которые могут
применяться при построении приложений .NET (многие из которых распространяются с
открытым исходным кодом), а также некоторыми основными концепциями языка
программирования С# и системы типов .NET.
34 Введение
Глава 1. Философия .NET
Первая глава выступает в роли своего рода основы для изучения всего остального
излагаемого в данной книге материала. В начале в ней рассказывается о традиционной
разработке приложений Windows и недостатках, которые существовали в этой сфере
ранее. Главной целью данной главы является ознакомление читателя с набором ключевых
составляющих .NET: общеязыковой исполняющей средой (Common Language Runtime —
CLR), общей системой типов (Common Type System — CTS), общеязыковой
спецификацией (Common Language Specification — CLRS) и библиотеками базовых классов. Здесь
читатель сможет получить первоначальное впечатление о том, что собой представляет
язык программирования С#, и том, как выглядит формат сборок .NET, а также узнать о
независимой от платформы природе .NET (о которой более' детально рассказывается в
приложении Б).
Глава 2. Создание приложений на языке С#
Целью этой главы является ознакомление читателя с процессом компиляции
файлов исходного кода на С# с применением различных средств и методик. Будет
показано, как использовать компилятор командной строки С# (csc.exe) и файлы ответов, а
также различные редакторы кода и интегрированные среды разработки, в том числе
Notepad++, SharpDevelop, Visual C# 2010 Express и Visual Studio 2010. Кроме того, вы
узнаете о том, как устанавливать на машину разработки локальную копию
документации .NET Framework 4.0 SDK.
Часть II. Главные конструкции программирования на С#
Темы, представленные в этой части книги, довольно важны, поскольку подходят для
разработки приложений .NET любого типа (т.е. веб-приложений, настольных
приложений с графическим пользовательским интерфейсом, библиотек кода и служб Windows).
Здесь читатель ознакомится с основными конструкциями языка С# и некоторыми
деталями объектно-ориентированного программирования (ООП), обработкой исключений
на этапе выполнения, а также автоматическим процессом сборки мусора в .NET.
Глава 3. Главные конструкции программирования на С#; часть I
В этой главе начинается формальное изучение языка программирования С#. Здесь
читатель узнает о роли метода Main () и многочисленных деталях работы с
внутренними типами данных в .NET, в том числе — о манипуляциях текстовыми данными с
помощью System. String и System. Text. StringBuilder. Кроме того, будут описаны
итерационные конструкции и конструкции принятия решений, операции сужения и
расширения, а также ключевое слово unchecked.
Глава 4. Главные конструкции программирования на С#; часть II
В этой главе завершается рассмотрение ключевых аспектов С#. Будет показано, как
создавать перегруженные методы в типах и определять параметры с использованием
ключевых слов out, ref и par am s. Также рассматриваются появившиеся в С# 2010
концепции именованных аргументов и необязательных параметры. Кроме того, будут
описаны создание и манипулирование массивами данных, определение нулевых типов
(с помощью операций ? и ??) и отличия типов значения (включающих перечисления и
специальные структуры) от ссылочных типов.
Глава 5. Определение инкапсулированных типов классов
В этой главе начинается рассмотрение концепций объектно-ориентированного
программирования (ООП) в языке С#. Вначале объясняются базовые понятия ООП (та-
Введение 35
кие как инкапсуляция, наследование и полиморфизм). Затем показано, как создавать
надежные типы классов с применением конструкторов, свойств, статических членов,
констант и доступных только для чтения полей. Наконец, рассматриваются частичные
определения типов, синтаксис инициализации объектов и автоматические свойства.
Глава 6. Понятия наследования и полиморфизма
Здесь читатель сможет ознакомиться с двумя такими основополагающими
концепциями ООП, как наследование и полиморфизм, которые позволяют создавать
семейства взаимосвязанных типов классов. Будет описана роль виртуальных и абстрактных
методов (а также абстрактных базовых классов) и полиморфных интерфейсов. И,
наконец, в главе рассматривается роль одного из главных базовых классов в .NET —
System.Object.
Глава 7. Структурированная обработка исключений
В этой главе рассматривается решение проблемы аномалий, возникающих в коде во
время выполнения, за счет применения методики структурированной обработки
исключений. Здесь описаны ключевые слова, предусмотренные для этого в С# (try, catch,
throw и finally), а также отличия между исключениями уровня приложения и уровня
системы. Кроме того, рассматриваются различные инструменты, предлагаемые в Visual
Studio 2010, которые предназначены для проведения отладки исключений.
Глава 8. Время жизни объектов
В этой главе рассказывается об управлении памятью CLR-средой с использованием
сборщика мусора .NET. Будет описана роль корневых элементов приложений,
поколений объектов и типа System.GC. Будет показано, как создавать самоочищаемые
объекты (с применением интерфейса IDisposable) и обеспечивать процесс финализации
(с помощью метода System. Object. Finalize ()). Вы узнаете о появившемся в .NET 4.0
классе Lazy<T>, который позволяет определять данные так, чтобы они не размещались
в памяти до тех пор, пока вызывающая сторона не запросит их. Этот класс позволяет
не загромождать память такими объектами, которые пока не требуются.
Часть III. Дополнительные конструкции программирования на С#
В этой части читателю предоставляется возможность углубить знания языка С# за
счет изучения других более сложных (но очень важных) концепций. Здесь завершается
ознакомление с системой типов .NET описанием типов делегатов и интерфейсов. Кроме
того, описана роль обобщений и дано краткое введение в язык LINQ (Language Integrated
Query). Также рассматриваются некоторые более сложные средства С# (такие как
методы расширения, частичные методы и приемы манипулирования указателями).
Глава 9. Работа с интерфейсами
Материал этой главы предполагает наличие понимания концепций
объектно-ориентированной разработки и посвящен программированию с использованием
интерфейсов. Здесь будет показано, как определять классы и структуры, поддерживающие
множество поведений, как обнаруживать эти поведения во время выполнения и как
выборочно скрывать какие-то из них за счет явной реализации интерфейсов. Помимо
создания специальных интерфейсов, рассматриваются вопросы реализации
стандартных интерфейсов из состава .NET и их применения для построения объектов, которые
могут сортироваться, копироваться, перечисляться и сравниваться.
36 Введение
Глава 10. Обобщения
Эта глава посвящена обобщениям. Программирование с использованием обобщений
позволяет создавать типы и члены типов, содержащие заполнители, которые
заполняются вызывающим кодом. В целом, обобщения позволяют значительно улучшить
производительность приложений и безопасность в отношении типов. В главе рассматриваются
типы обобщений из пространства имен System.Collections .Generic, а также
показано, как создавать собственные обобщенные методы и типы (с ограничениями и без).
Глава 11. Делегаты, события и лямбда-выражения
Благодаря этой главе, станет понятно, что собой представляет тип делегата. Любой
делегат в .NET представляет собой объект, который указывает на другие методы в
приложении. С помощью делегатов можно создавать системы, позволяющие
многочисленным объектам взаимодействовать между собой в обоих направлениях. После изучения
способов применения делегатов в .NET, будет показано, как применять в С# ключевое
слово event, которое упрощает манипулирование делегатами. Кроме того,
рассматривается роль лямбда-операции (=>) и связь между делегатами, анонимными методами и
лямбда-выражениями.
Глава 12. Расширенные средства языка С#
В этой главе описаны расширенные средства языка С#, в том числе перегрузка
операций, создание специальных процедур преобразования (явного и неявного) для типов,
построение и взаимодействие с индексаторами типов, работа с расширяющими
методами, анонимными типами, частичными методами, а также указателями С# с
использованием в коде контекста unsafe.
Глава 13. LINQ to Object
В этой главе начинается рассмотрение LINQ (Language Integrated Query — язык
интегрированных запросов). Эта технология позволяет создавать строго типизированные
выражения запросов, применять их к ряду различных целевых объектов LINQ и тем
самым манипулировать данными в самом широком смысле этого слова. Глава посвящена
API-интерфейсу LINQ to Objects, который позволяет применять LINQ-выражения к
контейнерам данных (т.е. массивам, коллекциям и специальным типам). Эта информация
будет полезна позже при рассмотрении других дополнительных API-интерфейсов, таких
как LINQ to XML, LINQ to DataSet, PLINQ и LINQ to Entities.
Часть IV. Программирование с использованием сборок .NET
Эта часть книги посвящена деталям формата сборок .NET. Здесь вы узнаете не только
о способах развертывания и конфигурирования библиотек кода .NET, но также о
внутреннем устройстве двоичного образа .NET. Будет описана роль атрибутов .NET и
определения информации о типе во время выполнения. Кроме того, рассматривается роль
среды DLR (Dynamic Language Runtime — исполняющая среда динамического языка) в
.NET 4.0 и ключевого слова dynamic в С# 2010. Наконец, объясняется, что собой
представляют контексты объектов, как устроен CIL-код и как создавать сборки в памяти.
Глава 14. Конфигурирование сборок .NET
На самом высоком уровне термин сборка применяется для описания любого
двоичного файла * . dll или * . ехе, который создается с помощью компилятора .NET. В
действительности возможности сборок намного шире. В этой главе будет показано, чем
отличаются однофайловые и многофайловые сборки, как создавать и развертывать сборки
обеих разновидностей, как делать сборки приватными и разделяемыми с использовани-
Введение 37
ем XML-файлов *.configH специальных сборок политик издателя. Кроме того, в главе
описан глобальный кэш сборок (Global Assembly Cache — CAG), а также изменения CAG
в версии .NET 4.0.
Глава 15. Рефлексия типов, позднее связывание
и программирование с использованием атрибутов
В главе 15 продолжается изучение сборок .NET. В ней показано, как обнаруживать
типы во время выполнения с использованием пространства имен System.Reflection.
Типы из этого пространства имен позволяют создавать приложения, способные
считывать метаданные сборки на лету. Кроме того, в главе рассматривается динамическая
загрузка и создание типов во время выполнения с помощью позднего связывания, а также
роль атрибутов .NET (стандартных и специальных). Для закрепления материала в конце
главы приводится пример построения расширяемого приложения Windows Forms.
Глава 16. Процессы, домены приложений и контексты объектов
В этой главе рассказывается о создании загруженных исполняемых файлов .NET.
Целью главы является иллюстрация отношений между процессами, доменами
приложений и контекстными границами. Все эти темы подготавливают базу для изучения
процесса создания многопоточных приложений в главе 19.
Глава 17. Язык CIL и роль динамических сборок
В этой главе подробно рассматривается синтаксис и семантика языка CIL, а также
роль пространства имен System. Re flection. Em it, с помощью типов из которого
можно создавать программное обеспечение, способное генерировать сборки .NET в памяти
во время выполнения. Формально сборки, которые определяются и выполняются в
памяти, называются динамическими сборками. Их не следует путать с динамическими
типами, которые являются темой главы 18.
Глава 18. Динамические типы и исполняющая среда динамического языка
В .NET 4.0 появился новый компонент исполняющей среды .NET, который
называется исполняющей средой динамического языка (Dynamic Language Runtime — DLR).
С помощью DLR в .NET и ключевого слова dynamic в С# 2010 можно определять
данные, которые в действительности разрешаются во время выполнения. Использование
этих средств значительно упрощает решение ряда очень сложных задач по
программированию приложений .NET В главе рассматриваются некоторые практические
способы применения динамических данных, включая более гладкое использование API-
интерфейсы рефлексии .NET, а также упрощенное взаимодействие с унаследованными
библиотеками СОМ.
Часть V. Введение в библиотеки базовых классов .NET
В этой части рассматривается ряд наиболее часто применяемых служб,
поставляемых в составе библиотек базовых классов .NET, включая создание многопоточных
приложений, файловый ввод-вывод и доступ к базам данных с помощью ADO.NET.
Здесь также показано, как создавать распределенные приложения с помощью Windows
Communication Foundation (WCF) и приложения с рабочими потоками, которые
используют API-интерфейсы Windows Workflow Foundation (WF) и LINQ to XML.
Глава 19. Многопоточность и параллельное программирование
Эта глава посвящена созданию многопоточных приложений. В ней демонстрируется
ряд приемов, которые можно применять для написания кода, безопасного в отноше-
38 Введение
нии потоков. В начале главы кратко напоминается о том, что собой представляет тип
делегата в .NET для упрощения понимания предусмотренной в нем внутренней
поддержки для асинхронного вызова методов. Затем рассматриваются типы пространства
имен System. Threading и новый API-интерфейс TPL (Task Parallel Library — библиотека
параллельных задач), появившийся в .NET 4.O. С применением этого API-интерфейса
можно создавать .NET-приложения, распределяющие рабочую нагрузку среди
доступных ЦП в исключительно простой манере. В главе также описан API-интерфейс PINQ
(Parallel LINQ), который позволяет создавать масштабируемые LINQ-запросы.
Глава 20. Файловый ввод-вывод и сериализация объектов
Пространство имен System. 10 позволяет взаимодействовать существующей
структурой файлов и каталогов. В главе будет показано, как программно создавать (и удалять)
систему каталогов и перемещать данные в различные потоки (файловые, строковые и
находящиеся в памяти). Кроме того, рассматриваются службы .NET, предназначенные
для сериализации объектов. Сериализация представляет собой процесс, который
позволяет сохранять данные о состоянии объекта (или набора взаимосвязанных объектов) в
потоке для использования в более позднее время, а десериализация — процесс
извлечения данных о состоянии объекта из потока в память для последующего использования
в приложении. В главе описана настройка процесса сериализации с применением
интерфейса ISerializable и набора атрибутов .NET.
Глава 21. AD0.NET, часть I: подключенный уровень
В этой первой из трех посвященных базам данных главам дано введение в API-
интерфейс ADO.NET. Рассматривается роль поставщиков данных .NET и
взаимодействие с реляционной базой данных с применением так называемого подключенного уровня
ADO.NET, который представлен объектами подключения, объектами команд, объектами
транзакций и объектами чтения данных. В этой главе также приведен пример создания
специальной базы данных и первой версии специальной библиотеки доступа к данным
(AutoLotDAL. dll), неоднократно применяемой в остальных примерах книги.
Глава 23. AD0.NET, часть II: автономный уровень
В этой главе продолжается описание способов работы с базами данных и
рассказывается об автономном ypoeHeADO.NET. Рассматривается роль типа DataSet и объектов
адаптеров данных, а также многочисленных средств Visual Studio 2010, которые
способны упростить процесс создания приложений, управляемых данными. Будет
показано, как связывать объекты DataTable с элементами пользовательского интерфейса, а
также как применять запросы LINQ к находящимся в памяти объектам DataSet с
использованием API-интерфейса LINQ to DataSet.
Глава 23. AD0.NET, часть III: Entity Framework
В этой главе завершается изучение ADO.NET и рассматривается роль технологии
Entity Framework (EF), которая позволяет создавать код доступа к данным с
использованием строго типизированных классов, напрямую отображающихся на
бизнес-модель. Здесь будут описаны роли таких входящих в состав EF компонентов, как службы
объектов EF, клиент сущностей и контекст объектов. Будет показано устройство файла
* . edmx, а также взаимодействие с реляционными базами данных с применением API-
интерфейса LINQ to Entities. Кроме того, в главе создается последняя версия
специальной библиотеки доступа к данным (AutoLotDAL.dll), которая будет использоваться в
нескольких последних главах книги.
Введение 39
Глава 24. Введение в LINQ to XML
В главе 14 были даны общие сведения о модели программирования LINQ и об
API-интерфейсе LINQ to Objects. В этой главе читателю предлагается углубить свои
знания о технологии LINQ и научиться применять запросы LINQ к XML-документам.
Сначала будут рассмотрены сложности, которые существовали в .NET первоначально
в области манипулирования XML-данными, на примере применения типов из сборки
System.Xml. dll. Затем будет показано, как создавать XML-документы в памяти,
обеспечивать их сохранение на жестком диске и перемещаться по их содержимому с
использованием модели программирования LINQ (LINQ to XML).
Глава 25. Введение в Windows Communication Foundation
В этой главе читатель узнает об API-интерфейсе Windows Communication Foundation
(WCF), который позволяет создавать распределенные приложения симметричным
образом, какими бы не были лежащие в их основе низкоуровневые детали. Будет показано,
как создавать службы, хосты и клиентские приложения WCF. Службы WCF являются
чрезвычайно гибкими, поскольку позволяют использовать для клиентов и хостов
конфигурационные файлы на основе XML, в которых декларативно задаются необходимые
адреса, привязки и контракты. Кроме того, рассматриваются полезные сокращения,
которые появились в .NET 4.O.
Глава 26. Введение в Windows Workflow Foundation 4.0
API-интерфейс Windows Workflow Foundation (WF) вызывает больше всего путаницы
у разработчиков-новичков. В версии .NET 4.0 первоначальный вариант API-интерфейса
WF (появившийся в .NET 3.0) полностью переделан. В этой главе описана роль
приложений, поддерживающих рабочие потоки, и способы моделирования бизнес-процессов
с применением API-интерфейса WF 4.O. Рассматривается библиотека действий,
поставляемая в составе WF 4.0, а также показано, как создавать специальные действия.
Часть VI. Построение настольных пользовательских
приложений с помощью WPF
В .NET 3.0 был предложен замечательный API-интерфейс под названием Windows
Presentation Foundation (WPF). Он быстро стал заменой модели программирования
настольных приложений Windows Forms. WPF позволяет создавать настольные
приложения с векторной графикой, интерактивной анимацией и операциями привязки данных
с использованием декларативной грамматики разметки XAML. Более того, архитектура
элементов управления WPF позволяет легко изменять внешний вид и поведение
любого элемента управления с помощью правильно оформленного XAML-кода. В настоящем
издании модели программирования WPF посвящено целых пять глав.
Глава 27. Введение в Windows Presentation Foundation и XAML
Технология WPF позволяет создавать чрезвычайно интерактивные и
многофункциональные интерфейсы для настольных приложений (и косвенно для веб-приложений).
В отличие от Windows Forms, в WPF множество ключевых служб (наподобие двухмерной
и трехмерной графики, анимации, форматированных документов и т.п.) интегрируется
в одну универсальную объектную модель. В главе предлагается введение в WPF и язык
XAML (Extendable Application Markup Language — расширяемый язык разметки
приложений). Будет показано, как создавать WPF-приложения без использования только кода
без XAML, с использованием одного лишь XAML и с применением обоих подходов
вместе, а также приведен пример создания специального XAML-редактора, который
пригодиться при изучении остальных глав, посвященных WPF
40 Введение
Глава 28. Программирование с использованием элементов управления WPF
В этой главе читатель научится работать с предлагаемыми в WPF элементами
управления и диспетчерами компоновки. Будет показано, как создавать системы меню,
разделители окон, панели инструментов и строки состояния. Также в главе
рассматриваются API-интерфейсы (и связанные с ними элементы управления), входящие в состав
WPF — Documents API, Ink API и модель привязки данных. В главе приводится
начальное описание IDE-среды Expression Blend, которая значительно упрощает процесс
создания многофункциональных пользовательских интерфейсов для приложений WPF.
Глава 29. Службы визуализации графики WPF
В API-интерфейсе WPF интенсивно используется графика, в связи с чем WPF
предоставляет три пути визуализации графических данных — фигуры, рисунки и визуальные
объекты. В главе подробно описаны все эти пути. Кроме того, рассматривается набор
важных графических примитивов (таких как кисти, перья и трансформации), а
применение Expression Blend для создания графики. Также показано, как выполнять
операции проверки попадания (hit-testing) в отношении графических данных.
Глава 30. Ресурсы, анимация и стили WPF
В этой главе освещены три важных (и связанных между собой) темы, которые
позволят углубить знания API-интерфейса Windows Presentation Foundation. В первую очередь
рассказывается о роли логических ресурсов. Система логических (также называемых
объектными) ресурсов позволяет назначать наиболее часто используемым в WPF-
приложении объектам имена и затем ссылаться на них. Кроме того, будет показано, как
определять, выполнять и управлять анимационной последовательностью. И, наконец,
в главе рассматривается роль стилей в WPF Подобно тому, как для веб-страниц могут
применяться таблицы стилей CSS и механизм тем ASP.NET, в приложениях WPF для
набора элементов управления может быть определен общий вид и поведение.
Глава 31. Шаблоны элементов управления WPF
и пользовательские элементы управления
В этой главе завершается изучение модели программирования WPF и
демонстрируется процесс создания специализированных элементов управления. Сначала
рассматриваются две важных темы, связанные с созданием любого специального
элемента — свойства зависимости и маршрутизируемые событиях. Затем описывается роль
шаблонов по умолчанию и способы их просмотра в коде во время выполнения. Наконец,
рассказывается о том, как создавать специальные классы UserControl с помощью
Visual Studio 2010 и Expression Blend, в том числе и с применением .NET 4.0 Visual
State Manager (VSM).
Часть VII. Построение веб-приложений с использованием ASP.NET
Эта часть посвящена деталям построения веб-приложений с применением API-
интерфейса ASP.NET. Данный интерфейс был разработан Microsoft специально для
предоставления возможности моделировать процесс создания настольных
пользовательских интерфейсов путем наложения стандартного объектно-ориентированной,
управляемой событиями платформы поверх стандартных запросов и ответов HTTP.
Глава 32. Построение веб-страниц ASP.NET
В этой главе начинается изучение процесса разработки веб-приложений с помощью
ASP.NET. Как будет показано, вместо кода серверных сценариев теперь применяются
самые настоящие объектно-ориентированные языки (наподобие С# и VB.NET). В главе
Введение 41
рассматривается типовой процесс создания веб-страницы ASP.NET, лежащая в основе
модель программирования и другие важные аспекты ASP.NET, вроде того, как выбира-
еть веб-сервер и работать с файлами Web. с on fig.
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET
В отличие от предыдущей главы, посвященной созданию объектов Page из ASP NET, в
данной главе рассказывается об элементах управления, которые заполняют внутреннее
дерево элементов ASP.NET. Здесь описаны основные веб-элементы управления ASP.NET,
включая элементы управления проверкой достоверности, элементы управления
навигацией по сайту и различные операции привязки данных. Кроме того, рассматривается
роль мастер-страниц и механизма применения тем ASP.NET, который является
серверным аналогом традиционных таблиц стилей.
Глава 34. Управление состоянием в ASP.NET
В этой главе рассматриваются разнообразные способы управления состоянием в
.NET. Как и в классическом ASP, в ASP.NET можно создавать cookie-наборы и
переменные уровня приложения и уровня сеанса. Кроме того, в ASP.NET есть еще одно средство
для управления состоянием, которое называется кэшем приложения. Вдобавок в главе
рассказывается о роли базового класса HttpApplication и демонстрируется
динамическое переключение поведения веб-приложения с помощью файла Web. с on fig.
Часть VIII. Приложения
В этой заключительной части книги рассматриваются две темы, которые не совсем
вписывались в контекст основного материала. В частности, здесь кратко
рассматривается более ранняя платформа Windows Forms для построения графических интерфейсов
настольных приложений, а также использование платформы Mono для создания
приложений .NET, функционирующих под управлением операционных систем, отличных от
Microsoft Windows.
Приложение А. Программирование с помощью Windows Forms
Исходный набор инструментов для построения настольных пользовательских
интерфейсов, который поставляется в рамках платформы .NET с самого начала, называется
Windows Forms. В этом приложении описана роль этого каркаса и показано, как с его
помощью создавать главные окна, диалоговые окна и системы меню. Кроме того, здесь
рассматриваются вопросы наследования форм и визуализации двухмерной графики с
помощью пространства имен System. Drawing. В конце приложения приводится
пример создания программы для рисования (средней сложности), иллюстрирующий
практическое применение всех описанных концепций.
Приложение Б. Независимая от платформы разработка
.NET-приложений с помощью Mono
Приложение Б посвящено использованию распространяемой с открытым исходным
кодом реализации платформы .NET под названием Mono. Она позволяет разрабатывать
многофункциональные приложения .NET, которые можно создавать, развертывать и
выполнять под управлением самых разных операционных систем, включая Mac OS X,
Solaris, AIX и многочисленные дистрибутивы Linux. Так как Mono в основном
эмулирует платформу .NET от Microsoft, ее функциональные возможности вполне очевидны.
Поэтому в приложении основное внимание уделено не возможностям платформы Mono,
а процессу ее установки, предлагаемым в ее составе инструментам для разработки, а
также используемому механизму исполняющей среды.
42 Введение
Исходный код примеров
Исходный код всех рассматриваемых в настоящей книге примеров доступен для
загрузки на сайте издательства по адресу:
http://www.williamspublishing.com
От издательства
Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим ваше
мнение и хотим знать, что было сделано нами правильно, что можно было сделать
лучше и что еще вы хотели бы увидеть изданным нами. Нам интересно услышать и любые
другие замечания, которые вам хотелось бы высказать в наш адрес.
Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам
бумажное или электронное письмо, либо просто посетить наш Web-сервер и оставить свои
замечания там. Одним словом, любым удобным для вас способом дайте нам знать,
нравится или нет вам эта книга, а также выскажите свое мнение о том, как сделать наши
книги более интересными для вас.
Посылая письмо или сообщение, не забудьте указать название книги и ее авторов, а
также ваш обратный адрес. Мы внимательно ознакомимся с вашим мнением и
обязательно учтем его при отборе и подготовке к изданию последующих книг.
Наши координаты:
E-mail: info@williamspublishing.com
WWW: http://www.williamspublishing.com
Информация для писем из:
России: 127055, г. Москва, ул. Лесная, д. 43, стр. 1
Украины: 03150, Киев, а/я 152
ЧАСТЬ I
Общие сведения
о языке С#
и платформе .NET
В этой части...
Глава 1. Философия .NET (
Глава 2. Создание приложений на языке С#
ГЛАВА 1
Философия .NET
Каждые несколько лет современному программисту требуется серьезно обновлять
свои знания, чтобы оставаться в курсе всех последних новейших технологий.
Языки (C++, Visual Basic 6.0, Java), библиотеки приложений (MFC, ATL, STL),
архитектуры (COM, CORBA, EJB) и API-интерфейсы, которые провозглашались универсальными
средствами для разработки программного обеспечения, постепенно начинают
затмевать более совершенные или, в самом крайнем случае, более новые технологии.
Какое бы чувство расстройства не возникало в связи с необходимостью обновления
своей внутренней базы знаний, избежать его, честно говоря, не возможно. Поэтому
целью данной книги является рассмотрение деталей тех решений, которые предлагаются
Microsoft в сфере разработки программного обеспечения сегодня, а именно — деталей
платформы .NET и языка программирования С#.
Задача настоящей главы заключается в изложении концептуальных базовых
сведений для обеспечения возможности успешного освоения всего остального материала
книги. Здесь рассматриваются такие связанные с .NET концепции, как сборки, общий
промежуточный язык (Common Intermediate Language — CIL) и оперативная
компиляция (Just-In-Time Compilation — JIT). Помимо предварительного ознакомления с
некоторыми ключевыми словами, также разъясняются взаимоотношения между
различными компонентами платформы .NET, такими как общеязыковая исполняющая среда
(Common Language Runtime — CLR), общая система типов (Common Type System — CTS)
и общеязыковая спецификация (Common Language Specification — CLS).
Кроме того, в настоящей главе описан набор функциональных возможностей,
поставляемых в библиотеках базовых классов .NET 4.0, для обозначения которых иногда
используют аббревиатуру BCL (Base Class Libraries — библиотеки базовых классов) или
FCL (Framework Class Libraries — библиотеки классов платформы). И, наконец,
напоследок в главе кратко затрагивается тема независимой от языков и платформ сущности
.NET (да, это действительно так — .NET не ограничивается только операционной
системой Windows). Как не трудно догадаться, многие из этих тем будут более подробно
освещены в остальной части книги.
Предыдущее состояние дел
Прежде чем переходить к изучению специфических деталей мира .NET, не
помешает узнать о некоторых вещах, которые послужили стимулом для создания Microsoft
текущей платформы. Чтобы настроиться надлежащим образом, давайте начнем эту
главу с краткого урока истории, чтобы вспомнить об истоках и ограничениях, которые
существовали в прошлом (ведь признание существования проблемы является первым
шагом на пути к ее решению). После завершения этого краткого экскурса в историю мы
обратим наше внимание на те многочисленные преимущества, которые предоставляет
язык С# и платформа .NET.
Глава 1. Философия .NET 45
Подход с применением языка С и API-интерфейса Windows
Традиционно разработка программного обеспечения для операционных систем
семейства Windows подразумевала использование языка программирования С в
сочетании с API-интерфейсом Windows (Application Programming Interface — интерфейс
прикладного программирования). И хотя то, что за счет применения этого проверенного
временем подхода было успешно создано очень много приложений, мало кто станет
спорить по поводу того, что процесс создания приложений с помощью одного только
API-интерфейса является очень сложным занятием.
Первая очевидная проблема состоит в том, что С представляет собой очень
лаконичный язык. Разработчики программ на языке С вынуждены мириться с необходимостью
"вручную" управлять памятью, безобразной арифметикой указателей и ужасными
синтаксическими конструкциями. Более того, поскольку С является структурным языком
программирования, ему не хватает преимуществ, обеспечиваемых
объектно-ориентированным подходом (здесь можно вспомнить о спагетти-подобном коде). Из-за сочетания
тысяч глобальных функций и типов данных, определенных в API-интерфейса Windows,
с языком, который и без того выглядит устрашающе, совсем не удивительно, что
сегодня в обиходе присутствует столь много дефектных приложений.
Подход с применением языка C++ и платформы MFC
Огромным шагом вперед по сравнению с подходом, предполагающим применение
языка С прямо с API-интерфейсом, стал переход на использование языка
программирования C++. Язык C++ во многих отношениях может считаться
объектно-ориентированной надстройкой поверх языка С. Из-за этого, хотя в случае его применения
программисты уже могут начинать пользоваться преимуществами известных "главных столпов
ООП" (таких как инкапсуляция, наследование и полиморфизм), они все равно
вынуждены иметь дело с утомительными деталями языка С (вроде необходимости
осуществлять управление памятью "вручную", безобразной арифметики указателей и ужасных
синтаксических конструкций).
Невзирая на сложность, сегодня существует множество платформ для
программирования на C++. Например, MFC (Microsoft Foundation Classes — библиотека базовых
классов Microsoft) предоставляет в распоряжение разработчику набор классов C++, которые
упрощают процесс создания приложений Windows. Основное предназначение MFC
заключается в представлении "разумного подмножества" исходного API-интерфейса
Windows в виде набора классов, "магических" макросов и многочисленных средств
для автоматической генерации программного кода (обычно называемых мастерами).
Несмотря на очевидную пользу данной платформы приложений (и многих других
основанных на C++ наборов средств), процесс программирования на C++ остается трудным
и чреватым допущением ошибок занятием из-за его исторической связи с языком С.
Подход с применением Visual Basic 6.0
Благодаря искреннему желанию иметь возможность наслаждаться более простой
жизнью, многие программисты перешли из "мира платформ" на базе С (C++) в мир
менее сложных и более дружественных языков наподобие Visual Basic 6.0 (VB6). Язык
VB6 стал популярным благодаря предоставляемой им возможности создавать сложные
пользовательские интерфейсы, библиотеки программного кода (вроде СОМ-серверов) и
логику доступа к базам данных с приложением минимального количества усилий. Во
многом как и в MFC, в VB6 сложности API-интерфейса Windows скрываются из вида за
счет предоставления ряда интегрированных мастеров, внутренних типов данных,
классов и специфических функций VB.
46 Часть I. Общие сведения о языке С# и платформе .NET
Главный недостаток языка VB6 (который с появлением платформы .NET был
устранен) состоит в том, что он является не полностью объектно-ориентированным, а
скорее — просто "объектным". Например, VB6 не позволяет программисту устанавливать
между классами отношения "подчиненности" (т.е. прибегать к классическому
наследованию) и не обладает никакой внутренней поддержкой для создания
параметризованных классов. Более того, VB6 не предоставляет возможности для построения
многопоточных приложений, если только программист не готов опускаться до уровня вызовов
API-интерфейса Windows (что в лучшем случае является сложным, а в худшем —
опасным подходом).
На заметку! Язык Visual Basic, используемый внутри платформы .NET (и часто называемый языком
VB.NET), имеет мало чего общего с языком VB6. Например, в современном языке VB
поддерживается перегрузка операций, классическое наследование, конструкторы типов и обобщения.
Подход с применением Java
Теперь пришел черед языка Java. Язык Java представляет собой
объектно-ориентированный язык программирования, который своими синтаксическими корнями уходит
в C++. Как многим известно, достоинства Java не ограничиваются одной лишь только
поддержкой независимости от платформ. Java как язык не имеет многих из тех
неприятных синтаксических аспектов, которые присутствуют в C++, а как платформа —
предоставляет в распоряжение программистам большее количество готовых пакетов с
различными определениями типов внутри. За счет применения этих типов программисты
на Java могут создавать "на 100% чистые Java-приложения" с возможностью
подключения к базе данных, поддержкой обмена сообщениями, веб-интерфейсами и богатым
настольными интерфейсами для пользователей (а также многими другими службами).
Хотя Java и представляет собой очень элегантный язык, одной из потенциальных
проблем является то, что применение Java обычно означает необходимость
использования Java в цикле разработки и для взаимодействия клиента с сервером. Надежды на
появление возможности интегрировать Java с другими языками мало, поскольку это
противоречит главной цели Java — быть единственным языком программирования для
удовлетворения любой потребности. В действительности, однако, в мире существуют
миллионы строк программного кода, которым бы идеально подошло смешивание с
более новым программным кодом на Java. К сожалению, Java делает выполнение этой
задачи проблематичной. Пока в Java предлагаются лишь ограниченные возможности
для получения доступа к отличным от Java API-интерфейсам, поддержка для истинной
межплатформенной интеграции остается незначительной.
Подход с применением СОМ
Модель COM (Component Object Model — модель компонентных объектов) была
предшествующей платформой для разработки приложений, которая предлагалась Microsoft,
и впервые появилась в мире программирования приблизительно в 1991 г. (или в 1993 г.,
если считать моментом ее появления рождение версии OLE 1.0). Она представляет
собой архитектуру, которая, по сути, гласит следующее: в случае построения типов в
соответствии с правилами СОМ, будет получаться блок многократно используемого
двоичного кода. Такие двоичные блоки кода СОМ часто называют "серверами СОМ".
Одним из главным преимуществ двоичного СОМ-сервера является то, что к нему
можно получать доступ независимым от языка образом. Это означает, что
программисты на C++ могут создавать СОМ-классы, пригодные для использования в VB6,
программисты на Delphi — применять СОМ-классы, созданные с помощью С, и т.д. Однако,
Глава 1. Философия .NET 47
как не трудно догадаться, подобная независимость СОМ от языка является несколько
ограниченной. Например, никакого способа для порождения нового СОМ-класса с
использованием уже существующего не имеется (поскольку СОМ не обладает поддержкой
классического наследования). Вместо этого для использования типов СОМ-класса
требуется задавать несколько неуклюжее отношение принадлежности (has-a).
Еще одним преимуществом СОМ является прозрачность расположения. За счет
применения конструкций вроде системного реестра, идентификаторов приложений (AppID),
заглушек, прокси-объектов и исполняющей среды СОМ программисты могут избегать
необходимости иметь дело с самими сокетам, RPC-вызовам и другими
низкоуровневыми деталями при создании распределенного приложения. Например, рассмотрим
следующий программный код СОМ-клиента наУВб:
' Данный тип MyCOMClass мог бы быть написан на любом
' поддерживающем СОМ языке и размещаться в любом месте
' в сети (в том числе и на локальной машине) .
Dim obj as MyCOMClass
Set obj = New MyCOMClass ' Местонахождение определяется с помощью AppID.
obj.DoSomeWork
Хотя COM и можно считать очень успешной объектной моделью, ее внутреннее
устройство является чрезвычайно сложным (и требует затрачивания программистами
многих месяцев на его изучение, особенно теми, которые программируют на C++). Для
облегчения процесса разработки двоичных СОМ-объектов программисты могут использовать
многочисленные платформы, поддерживающие СОМ. Например, в ATL (Active Template
Library — библиотека активных шаблонов) для упрощения процесса создания СОМ-
серверов предоставляется набор специальных классов, шаблонов и макросов на C++.
Во многих других языках приличная часть инфраструктуры СОМ тоже скрывается
из вида. Поддержки одного только языка, однако, для сокрытия всей сложности СОМ не
хватает. Даже при выборе относительно простого поддерживающего СОМ языка вроде
VB6, все равно требуется бороться с "хрупкими" записями о регистрации и
многочисленными деталями развертывания (в совокупности несколько саркастично
называемыми адом DLL).
Сложность представления типов данных СОМ
Хотя СОМ, несомненно, упрощает процесс создания программных приложений с
помощью различных языков программирования, независимая от языка природа СОМ не
является настолько простой, насколько возможно хотелось бы.
Некоторая доля этой сложности является следствием того факта, что приложения,
которые сплетаются вместе с помощью разнообразных языков, получаются совершенно
не связанными с синтаксической точки зрения. Например, синтаксис JScript во
многом похож на синтаксис С, а синтаксис VBScript представляет собой подмножество
синтаксиса VB6. СОМ-серверы, которые создаются для выполнения в исполняющей среде
СОМ+ (представляющей собой компонент операционной системы Windows, который
предлагает общие службы для библиотек специального кода, такие как транзакции,
жизненный цикл объектов, безопасность и т.д.), имеют совершенно не такой вид и
поведение, как ориентированные на использование в веб-сети ASP-страницы, в которых
они вызываются. В результате получается очень запутанная смесь технологий.
Более того, что, пожалуй, даже еще важнее, каждый язык и/или технология
обладает собственной системой типов (которая может быть совершенно не похожа на систему
типов другого языка или технологии). Помимо того факта, что каждый API-интерфейс
поставляется с собственной коллекцией готового кода, даже базовые типы данных
могут не всегда интерпретироваться идентичным образом. Например, тип CComBSTR в ATL
48 Часть I. Общие сведения о языке С# и платформе .NET
представляет собой не совсем то же самое, что тип String в VB6, и оба они не имеют
совершенно ничего общего с типом char* в С.
Из-за того, что каждый язык обладает собственной уникальной системой типов,
СОМ-программистам обычно требуется соблюдать предельную осторожность при
создании общедоступных методов в общедоступных классах СОМ. Например, при
возникновении у разработчика на C++ необходимости в создании метода, способного
возвращать массив целых чисел в приложении VB6, ему пришлось бы полностью погружаться
в сложные вызовы API-интерфейса СОМ для построения структуры SAFE ARRAY, которое
вполне могло бы потребовать написания десятков строк кода. В мире СОМ тип данных
SAFEARRAY является единственным способом для создания массива, который могли бы
распознавать все платформы СОМ. Если разработчик на C++ вернет просто
собственный массив C++, у приложения VB6 не будет никакого представления о том, что с ним
делать.
Подобные сложности могут возникать и при построении методов,
предусматривающих выполнение манипуляций над простыми строковыми данными, ссылками на
другие объекты СОМ и даже обычными булевскими значениями. Мягко говоря,
программирование с использованием СОМ является очень несимметричной дисциплиной.
Решение .NET
Немало информации для короткого урока истории. Главное понять, что жизнь
программиста Windows-приложений раньше была трудной. Платформа .NET Framework
являет собой достаточно радикальную "силовую" попытку сделать жизнь программистов
легче. Как можно будет увидеть в остальной части настоящей книги, .NET Framework
представляет собой программную платформу для создания приложений на базе
семейства операционных систем Windows, а также многочисленных операционных систем
производства не Microsoft, таких как Mac OS X и различные дистрибутивы Unix и Linux.
Для начала не помешает привести краткий перечень некоторых базовых
функциональных возможностей, которыми обладает .NET
• Возможность обеспечения взаимодействия с существующим программным
кодом. Эта возможность, несомненно, является очень хорошей вещью, поскольку
позволяет комбинировать существующие двоичные единицы СОМ (т.е.
обеспечивать их взаимодействие) с более новыми двоичными единицами .NET и наоборот.
С выходом версии .NET 4.0 эта возможность стала выглядеть даже еще проще,
благодаря добавлению ключевого слова dynamic (о котором будет более подробно
рассказываться в главе 18).
• Поддержка для многочисленных языков программирования. Приложения .NET
можно создавать с помощью любого множества языков программирования (С#,
Visual Basic, F#, S# и т.д.).
• Общий исполняющий механизм, используемый всеми поддерживающими .NET
языками. Одним из аспектов этого механизма является наличие хорошо
определенного набора типов, которые способен понимать каждый поддерживающий
.NET язык.
• Полная и тотальная интеграция языков. В .NET поддерживается межъязыковое
наследование, межъязыковая обработка исключений и межъязыковая отладка кода.
• Обширная библиотека базовых классов. Эта библиотека позволяет избегать
сложностей, связанных с выполнением прямых вызовов к API-интерфейсу, и
предлагает согласованную объектную модель, которую могут использовать все
поддерживающие .NET языки.
Глава 1. Философия .NET 49
• Отсутствие необходимости в предоставлении низкоуровневых деталей СОМ.
В двоичной единице .NET нет места ни для интерфейсов IClassFactory, IUnknown
и IDispatch, ни для кода IDL, ни для вариантных типов данных (подобных BSTR,
SAFEARRAY и т.д.).
• Упрощенная модель развертывания. В .NET нет никакой необходимости
заботиться о регистрации двоичной единицы в системном реестре. Более того, в .NET
позволяется делать так, чтобы многочисленные версии одной и той же сборки
* , dll могли без проблем сосуществовать на одной и той же машине.
Как не трудно догадаться по приведенному выше перечню, платформа .NET не имеет
ничего общего с СОМ (за исключением разве что того факта, что обе этих платформы
являются детищем Microsoft). На самом деле единственным способом, которым может
обеспечиваться взаимодействие между типами .NET и СОМ, будет использование
уровня функциональной совместимости.
Главные компоненты платформы
.NET (CLR, CTS и CLS)
Теперь, когда о некоторых из предоставляемых .NET преимуществах уже известно,
давайте вкратце ознакомимся с тремя ключевыми (и связанными между собой)
сущностями, которые делают предоставление этих преимуществ возможным: CLR, CTS и CLS.
С точки зрения программиста .NET представляет собой исполняющую среду и
обширную библиотеку базовых классов. Уровень исполняющей среды называется
общеязыковой исполняющей средой (Common Language Runtime) или, сокращенно, средой CLR.
Главной задачей CLR является автоматическое обнаружение, загрузка и управление
типами .NET (вместо программиста). Кроме того, среда CLR заботится о ряде
низкоуровневых деталей, таких как управление памятью, обслуживание приложения, обработка
потоков и выполнение различных проверок, связанных с безопасностью.
Другим составляющим компонентом платформы .NET является общая система
типов (Common Type System) или, сокращенно, система CTS. В спецификации CTS
представлено полное описание всех возможных типов данных и программных конструкций,
поддерживаемых исполняющей средой, того, как эти сущности могут взаимодействовать
друг с другом, и того, как они могут представляться в формате метаданных .NET
(которые более подробно рассматриваются далее в этой главе и полностью — в главе 15).
Важно понимать, что любая из определенных в CTS функциональных
возможностей может не поддерживаться в отдельно взятом языке, совместимом с .NET. Поэтому
существует еще общеязыковая спецификация (Common Language Specification) или,
сокращенно, спецификация CLS, в которой описано лишь то подмножество общих типов
и программных конструкций, каковое способны воспринимать абсолютно все
поддерживающие .NET языки программирования. Следовательно, в случае построения типов
.NET только с функциональными возможностями, которые предусмотрены в CLS,
можно оставаться полностью уверенным в том, что все совместимые с .NET языки смогут
их использовать. И, наоборот, в случае применения такого типа данных или
конструкции программирования, которой нет в CLS, рассчитывать на то, что каждый язык
программирования .NET сможет взаимодействовать с подобной библиотекой кода .NET,
нельзя. К счастью, как будет показано позже в этой главе, существует очень простой
способ указывать компилятору С#, чтобы он проверял весь код на предмет
совместимости с CLS.
50 Часть I. Общие сведения о языке С# и платформе .NET
Роль библиотек базовых классов
Помимо среды CLR и спецификаций CTS и CLS, в составе платформы .NET
поставляется библиотека базовых классов, которая является доступной для всех языков
программирования .NET. В этой библиотеке не только содержатся определения различных
примитивов, таких как потоки, файловый ввод-вывод, системы графической
визуализации и механизмы для взаимодействия с различными внешними устройствами, но
также предоставляется поддержка для целого ряда служб, требуемых в большинстве
реальных приложений.
Например, в библиотеке базовых классов содержатся определения типов, которые
способны упрощать процесс получения доступа к базам данных, манипулирования
XML-документами, обеспечения программной безопасности и создания веб-, а
также обычных настольных и консольных интерфейсов. На высоком уровне взаимосвязь
между CLR, CTS, CLS и библиотекой базовых классов выглядит так, как показано на
рис. 1.1.
Доступ
к базе данных
Организация
потоковой обработки
Библиотека базовых классов
Настольные
графические
API-интерфейсы
Файловый
ввод-вывод
Безопасность
API-интерфейсы
для работы
с веб-содержимым
API-интерфейсы
для удаленной работы
и другие
Общеязыковая исполняющая среда (CLR)
Общая система типов (CTS)
Общеязыковая спецификация (CLS)
Рис. 1.1. Отношения между CLR, CTS, CLS и библиотеками базовых классов
Что привносит язык С#
Из-за того, что платформа .NET столь радикально отличается от предыдущих
технологий, в Microsoft разработали специально под нее новый язык программирования
С#. Синтаксис этого языка программирования очень похож на синтаксис языка Java.
Однако сказать, что С# просто переписан с Java, будет неточно. И язык С#, и язык Java
просто оба являются членами семейства языков программирования С (в которое также
входят языки С, Objective С, C++ и т.д.) и потому имеют схожий синтаксис.
Правда состоит в том, что многие синтаксические конструкции в С# моделируются
согласно различным особенностям Visual Basic 6.0 и C++. Например, как и в VB6, в С#
поддерживается понятие формальных свойств типов (в противоположность
традиционным методам get и set) и возможность объявлять методы, принимающие переменное
количество аргументов (через массивы параметров). Как и в C++, в С# допускается
перегружать операции, а также создавать структуры, перечисления и функции обратного
вызова (посредством делегатов).
Глава 1. Философия .NET 51
Более того, по мере изучения излагаемого в этой книге материала, можно будет
быстро заметить, что в С# поддерживается целый ряд функциональных возможностей,
которые традиционно встречаются в различных функциональных языках
программирования (например, LISP или Haskell) и к числу которых относятся лямбда-выражения
и анонимные типы. Кроме того, с появлением технологии LINQ в С# стали
поддерживаться еще и конструкции, которые делают его довольно уникальным в мире
программирования. Несмотря на все это, наибольшее влияние на него все-таки оказали именно
языки на базе С.
Благодаря тому факту, что С# представляет собой собранный из нескольких языков
гибрид, он является таким же "чистым" с синтаксической точки зрения, как и язык
Java (а то и "чище" его), почти столь же простым, как язык VB6, и практически таким
же мощным и гибким как C++ (только без ассоциируемых с ним громоздких элементов).
Ниже приведен неполный список ключевых функциональных возможностей языка С#,
которые присутствуют во всех его версиях.
• Никаких указателей использовать не требуется! В программах на С# обычно не
возникает необходимости в манипулировании указателями напрямую (хотя
опуститься к этому уровню все-таки можно, как будет показано в главе 12).
• Управление памятью осуществляется автоматически посредством сборки мусора.
По этой причине ключевое слово delete в С# не поддерживается.
• Предлагаются формальные синтаксические конструкции для классов,
интерфейсов, структур, перечислений и делегатов.
• Предоставляется аналогичная C++ возможность перегружать операции для
пользовательских типов, но без лишних сложностей (например, заботиться о "возврате
*this для обеспечения связывания" не требуется).
• Предлагается поддержка для программирования с использованием атрибутов.
Такой подход в сфере разработки позволяет снабжать типы и их членов
аннотациями и тем самым еще больше уточнять их поведение.
С выходом версии .NET 2.0 (примерно в 2005 г.), язык программирования С# был
обновлен и стал поддерживать многочисленные новые функциональные возможности,
наиболее заслуживающие внимания из которых перечислены ниже.
• Возможность создавать обобщенные типы и обобщенные элементы-члены. За
счет применения обобщений можно создавать очень эффективный и безопасный
для типов код с многочисленными метками-заполнителями, подстановка
значений в которые будет происходить в момент непосредственного взаимодействия с
данным обобщенным элементом.
• Поддержка для анонимных методов, каковые позволяют предоставлять
встраиваемую функцию везде, где требуется использовать тип делегата.
• Многочисленные упрощения в модели "делегат-событие", в том числе
возможность применения ковариантности, контравариантности и преобразования групп
методов. (Если какие-то из этих терминов пока не знакомы, не стоит пугаться; все
они подробно объясняются далее в книге.)
• Возможность определять один тип в нескольких файлах кода (или, если
необходимо, в виде представления в памяти) с помощью ключевого слова partial.
В версии .NET 3.5 (которая вышла примерно в 2008 г.) в язык программирования
С# снова были добавлены новые функциональные возможности, наиболее важные из
которых описаны ниже.
• Поддержка для строго типизированных запросов (также называемых запросами
LINQ), которые применяются для взаимодействия с различными видами данных.
52
Часть I. Общие сведения о языке С# и платформе .NET
• Поддержка для анонимных типов, которые позволяют моделировать форму типа,
а не его поведение.
• Возможность расширять функциональные возможности существующего типа с
помощью методов расширения.
• Возможность использовать лямбда-операцию (=>), которая даже еще больше
упрощает работу с типами делегатов в .NET.
• Новый синтаксис для инициализации объектов, который позволяет
устанавливать значения свойств во время создания объектов.
В текущем выпуске платформы .NET версии 4.0 язык С# был опять обновлен и
дополнен рядом новых функциональных возможностей. Хотя приведенный ниже перечень
новых конструкций может показаться довольно ограниченным, по ходу прочтения
настоящей книги можно будет увидеть, насколько полезными они могут оказаться.
• Поддержка необязательных параметров в методах, а также именованных
аргументов.
• Поддержка динамического поиска членов во время выполнения посредством
ключевого слова dynamic. Как будет показано в главе 18, эта поддержка
предоставляет в распоряжение универсальный подход для осуществления вызова членов "на
лету", с помощью какой бы платформы они не были реализованы (COM, IronRuby,
IronPython, HTML DOM или службы рефлексии .NET). ,
• Вместе с предыдущей возможностью в .NET 4.0 значительно упрощается
обеспечение взаимодействия приложений на С# с унаследованными серверами СОМ,
благодаря устранению зависимости от сборок взаимодействия (interop assemblies)
и предоставлению поддержки необязательных аргументов ref.
• Работа с обобщенными типами стала гораздо понятнее, благодаря появлению
возможности легко отображать обобщенные данные на и из общих коллекций
System.Object с помощью ковариантности и контравариантности.
Возможно, наиболее важным моментом, о котором следует знать, программируя на
С#, является то, что с помощью этого языка можно создавать только такой код, который
будет выполняться в исполняющей среде .NET (использовать С# для построения
"классического" СОМ-сервера или неуправляемого приложения с вызовами API-интерфейса и
кодом на С и C++ нельзя). Официально код, ориентируемый на выполнение в
исполняющей среде .NET, называется управляемым кодом (managed code), двоичная единица, в
которой содержится такой управляемый код — сборкой (assembly; о сборках будет более
подробно рассказываться позже в настоящей главе), а код, который не может
обслуживаться непосредственно в исполняющей среде .NET — неуправляемым кодом (unma-
naged code).
Другие языки программирования
с поддержкой .NET
Следует понимать, что С# является не единственным языком, который может
применяться для построения .NET-приложений. При установке доступного для бесплатной
загрузки комплекта разработки программного обеспечения Microsoft .NET 4.0 Framework
Software Development Kit (SDK), равно как и при установке Visual Studio 2010, для
выбора становятся доступными пять управляемых языков: С#, Visual Basic, C++/CLI, JScript
.NET и F#.
Глава 1. Философия .NET 53
На заметку! F# — это новый язык .NET, основанный на семействе функциональных языков ML и
главным образом — на OCaml. Хотя он может применяться в качестве чисто функционального
языка, в нем также предлагается поддержка для конструкций ООП и библиотек базовых
классов .NET. Тем,.кому интересно узнать больше о нем, могут посетить его официальную
веб-страницу по следующему адресу: http: //msdn. microsoft. com/f sharp.
Помимо управляемых языков, предлагаемых Microsoft, существуют .NET-компиля-
торы, которые предназначены для таких языков, как Smalltalk, COBOL и Pascal (и это
далеко не полный перечень). Хотя в настоящей книге все внимание практически
полностью уделяется лишь С#, следующий веб-сайт тоже вызвать интерес:
http://www.dotnetlanguages.net
Щелкнув на ссылке Resources (Ресурсы) в самом верху домашней страницы этого
сайта, можно получить доступ к списку всех языков программирования .NET и
соответствующих ссылок, по которым для них можно загружать различные компиляторы
(рис. 1.2).
C # - Page- Safety-
al Р Д Mercury ItonPyUmn p* DEDICATED
.NET Languages ™
Forth SML.NET S# Ron Nermji k INVESTIG
News | FAQ J Resources | Contact | RSS Feed | Recent Comments Feed
Resources
Following is a listing of resources that you may find useful either to own or bookmark during
your navigation through the .NET language space. If you feel that there's a resource that
other .NET developers should know about, please contact nw.
.NET Language Sites
• At»
• APL ч
• ASP.NET: ASM to И.
• AsmL i
• ASP (Gotham)
• Bask
о VB .NET (Microsoft)
о V8.NET (Mono)
• BETA
• Boo
• BtueOragon
• С
о tec
Ф Internet! Protected Mode On
fu -r ^100%
Рис. 1.2. Один из многочисленных сайтов с документацией
по известным языкам программирования .NET
Хотя настоящая книга ориентирована главным образом на тех, кого интересует
разработка программ .NET с использованием С#, все равно рекомендуется посетить
указанный сайт, поскольку там наверняка можно будет найти много языков для .NET,
заслуживающих отдельного изучения в свободное время (вроде LISP .NET).
54 Часть I. Общие сведения о языке С# и платформе .NET
Жизнь в многоязычном окружении
В начале процесса осмысления разработчиком нейтральной к языкам природы
платформы .NET, у него возникает множество вопросов и, прежде всего, следующий: если
все языки .NET при компиляции преобразуются в управляемый код, то почему
существует не один, а множество компиляторов? Ответить на этот вопрос можно по-разному.
Мы, программисты, бываем очень привередливы, когда дело касается выбора языка
программирования. Некоторые предпочитают языки с многочисленными точками с
запятой и фигурными скобками, но с минимальным набором ключевых слов. Другим
нравятся языки, предлагающие более "человеческие" синтаксические лексемы (вроде языка
Visual Basic). Кто-то не желает отказываться от своего опыта работы на мэйнфреймах и
предпочитает переносить его и на платформу .NET (использовать COBOL .NET).
А теперь ответьте честно: если бы в Microsoft предложили единственный
"официальный" язык .NET, например, на базе семейства BASIC, то все ли программисты были
бы рады такому выбору? Или если бы "официальный" язык .NET основывался на
синтаксисе Fortran, то сколько людей в мире просто бы проигнорировало платформу .NET?
Поскольку среда выполнения .NET демонстрирует меньшую зависимость от языка,
используемого для построения управляемого программного кода, программисты .NET
могут, не меняя своих синтаксических предпочтений, обмениваться скомпилированными
сборками со своими коллегами, другими отделами и внешними организациями (не
обращая внимания на то, какой язык .NET в них применяется).
Еще одно полезное преимущество интеграции различных языков .NET в одном
унифицированном программном решении вытекает из того простого факта, что каждый
язык программирования имеет свои сильные (а также слабые) стороны. Например,
некоторые языки программирования обладают превосходной встроенной поддержкой
сложных математических вычислений. В других лучше реализованы финансовые или
логические вычисления, взаимодействие с мэйнфреймами и т.п. А когда преимущества
конкретного языка программирования объединяются с преимуществами платформы
.NET, выигрывают все.
Конечно, в реальности велика вероятность того, что будет возможность тратить
большую часть времени на построение программного обеспечения с помощью
предпочитаемого языка .NET. Однако, после освоения синтаксиса одного из языков .NET, изучение
синтаксиса какого-то другого языка существенно упрощается. Вдобавок это довольно
выгодно, особенно тем, кто занимается консультированием по разработке ПО. Например,
тому, у кого предпочитаемым языком является С#, в случае попадания в клиентскую
среду, где все построено на Visual Basic, это все равно позволит эксплуатировать
функциональные возможности .NET Framework и разбираться в общей структуре кодовой
базы с минимальным объемом усилий и беспокойства. Сказанного вполне достаточно.
Что собой представляют сборки в .NET
Какой бы язык .NET не выбирался для программирования, важно понимать, что хотя
двоичные ^ЕТ-единицы имеют такое же файловое расширение, как и двоичные
единицы СОМ-серверов и неуправляемых программ Win32 (* . dll или * . ехе), внутренне они
устроены абсолютно по-другому. Например, двоичные ^ЕТ-единицы * .dll не
экспортируют методы для упрощения взаимодействия с исполняющей средой СОМ (поскольку
.NET — это не СОМ). Более того, они не описываются с помощью библиотек СОМ-типов
и не регистрируются в системном реестре. Пожалуй, самым важным является то, что
они содержат не специфические, а наоборот, не зависящие от платформы инструкции
на промежуточном языке (Intermediate Language — IL), а также метаданные типов. На
рис. 1.3 показано, как все это выглядит схематически.
Исходный код
наС#
Исходный код
HaPerl.NET
Исходный код
HaCOBOL.NET
Исходный код
на C++/CLI
Глава 1. Философия .NET 55
Компилятор С#
Компилятор Perl .NET
Компилятор COBOL .NET
Компилятор C++/CLI
Инструкции IL
и метаданные
(* .dll или *.ехе)
Рис. 1.3. Все .NET-компиляторы генерируют IL-инструкции и метаданные
На заметку! Относительно сокращения "IL" уместно сказать несколько дополнительных слов. В ходе
разработки .NET официальным названием для IL было Microsoft Intermediate Language (MSIL).
Однако в вышедшей последней версии .NET это название было изменено на CIL (Common
Intermediate Language — общий промежуточный язык). Поэтому при прочтении литературы по
.NET следует помнить о том, что IL, MSIL и CIL обозначают одно и то же. Для отражения
современной терминологии в настоящей книге будет применяться аббревиатура CIL.
При создании файла * .dll или * . ехе с помощью .NET-компилятор а получаемый
большой двоичный объект называется сборкой (assembly). Все многочисленные детали
.NET-сборок будет подробно рассматриваться в главе 14. Для облегчения повествования
об исполняющей среде здесь, однако, все-таки необходимо рассказать хотя бы об
основных свойствах этого нового формата файлов.
Как уже упоминалось, в сборке содержится CIL-код, который концептуально похож
на байт-код Java тем, что не компилируется в ориентированные на конкретную
платформу инструкции до тех пор, пока это не становится абсолютно необходимым. Обычно
этот момент "абсолютной необходимости" наступает тогда, когда к какому-то блоку CIL-
инструкций (например, к реализации метода) выполняется обращение для его
использования в исполняющей среде .NET.
Помимо CIL-инструкций, в сборках также содержатся метаданные, которые
детально описывают особенности каждого имеющегося внутри данной двоичной .NET-
единицы "типа". Например, при наличии класса по имени SportsCar они будут
описывать детали наподобие того, как выглядит базовый класс этого класса SportsCar,
какие интерфейсы реализует SportsCar (если вообще реализует), а также, подробно,
какие члены он поддерживает Метаданные .NET всегда предоставляются внутри сборки
и автоматически генерируются компилятором соответствующего распознающего .NET
языка.
И, наконец, помимо CIL и метаданных типов, сами сборки тоже описываются с
помощью метаданных, которые официально называются манифестом (manifest). В каждом
таком манифесте содержится информация о текущей версии сборки, сведения о
культуре (применяемые для локализации строковых и графических ресурсов) и перечень
ссылок на все внешние сборки, которые требуются для правильного функционирования.
Разнообразные инструменты, которые можно использовать для изучения типов,
метаданных и манифестов сборок, рассматриваются в нескольких последующих главах.
56 Часть I. Общие сведения о языке С# и платформе .NET
Однофайловые и многофайловые сборки
В большом количестве случаев между сборками .NET и файлами двоичного кода
(* . dll или * . ехе) соблюдается простое соответствие "один к одному". Следовательно,
получается, что при построении * . dll-библиотеки .NET, можно спокойно полагать, что
файл двоичного кода и сборка представляют собой одно и то же, и что, аналогичным
образом, при построении исполняемого приложения для настольной системы на файл
* . ехе можно ссылаться как на саму сборку. Однако, как будет показано в главе 14, это
не совсем так. С технической точки зрения, сборка, состоящая из одного единственного
модуля * . dll или * . ехе, называется однофайловой сборкой. В однофайловых сборках
все необходимые CIL-инструкции, метаданные и манифесты содержатся в одном
автономном четко определенном пакете.
Многомофайловые сборки, в свою очередь, состоят из множества файлов двоичного
кода .NET, каждый из которых называется модулем (module). При построении
многофайловой сборки в одном из ее модулей (называемом первичным или главным (primary)
модулем) содержится манифест всей самой сборки (и, возможно, CIL-инструкции и
метаданные по различным типам), а во всех остальных — манифест, CIL-инструкции и
метаданные типов, охватывающие уровень только соответствующего7 модуля. Как
нетрудно догадаться, в главном модуле содержится описание набора требуемых
дополнительных модулей внутри манифеста сборки.
На заметку! В главе 14 будет более подробно разъясняться, в чем состоит различие между одно-
файловыми и многофайловыми сборками. Однако следует иметь в виду, что Visual Studio 2010
может применяться только для создания однофайловых сборок. В тех редких случаях
возникновения необходимости в создании именно многофайловой сборки требуется использовать
соответствующие утилиты командной строки.
Роль CIL
Теперь давайте немного более подробно посмотрим, что же собой представляет CIL-
код, метаданные типов и манифест сборки. CIL является таким языком, который стоит
выше любого конкретного набора ориентированных на определенную платформу
инструкций. Например, ниже приведен пример кода на С#, в котором создается модель
самого обычного калькулятора. Углубляться в конкретные детали синтаксиса пока не
нужно, главное обратить внимание на формат такого метода в этом классе Calc, как
Add().
//Класс Calc.cs
using System;
namespace CalculatorExample
{
//В этом классе содержится точка для входа в приложение.
class Program {
static void Main()
{
Calc с = new Calc () ;
int ans = с Add A0, 84);
Console.WriteLine(0 + 84 is {0 } . ", ans);
// Обеспечение ожидания нажатия пользователем
// клавиши <Enter> перед выходом.
Console.ReadLine();
/ / Калькулятор на С#.
class Calc
Глава 1. Философия .NET 57
{
public int Add(int x, int y)
{ return x + y; }
}
}
После выполнения компиляции файла с этим кодом с помощью компилятора С#
(csc.exe) получится однофайловая сборка * .ехе, в которой будет содержаться
манифест, CIL-инструкции и метаданные, описывающие каждый из аспектов класса Calc и
Program.
На заметку! О том, как выполнять компиляцию кода с помощью компилятора С#, а также
использовать графические IDE-среды, подобные Microsoft Visual Studio 2010, Microsoft Visual C# 2010
Express и SharpDevelop, будет более подробно рассказываться в главе 2.
Например, открыв данную сборку в утилите ildasm.exe (которая более подробно
рассматривается далее в настоящей главе), можно увидеть, что метод Add () был
преобразован в CIL так, как показано ниже:
.method public hidebysig instance int32 Add(int32 x, int32 y) cil managed
{
// Code size 9 @x9)
// Размер кода 9 @x9)
.maxstack 2
.locals mit (int32 V_0)
IL_0000: nop
IL_0001: ldarg.l
IL_0002: ldarg.2
IL_0003: add
IL_0004: stloc.O
IL_0005: br.s IL_0007
IL_0007: ldloc.O
IL_0008: ret
} // end of method Calc::Add
// конец метода Calc::Add
He стоит беспокоиться, если пока совершенно не понятно, что собой представляет
результирующий CIL-код этого метода, потому что в главе 17 будут рассматриваться
все необходимые базовые аспекты языка программирования CIL. Главное заметить, что
компилятор С# выдает CIL-код, а не ориентированные на определенную платформу
инструкции. Теперь напоминаем, что так себя ведут все .NET-компиляторы. Чтобы
убедиться в этом, давайте попробуем создать то же самое приложение с использованием
языка Visual Basic, а не С#.
'Класс Calc.vb
Imports System
Namespace CalculatorExample
1 В VB "модулем" называется класс, в котором
1 содержатся только статические члены.
Module Program
Sub Main ()
Dim с As New Calc
Dim ans As Integer = c.AddA0, 84)
Console.WriteLine(0 + 84 is {0}.", ans)
Console.ReadLine()
End Sub
End Module
Class Calc
58 Часть I. Общие сведения о языке С# и платформе .NET
Public Function Add(ByVal x As Integer, ByVal у As Integer) As Integer
Return x + у
End Function
End Class
End Namespace
В случае изучения CIL-кода этого метода Add () можно будет обнаружить похожие
инструкции (лишь слегка подправленные компилятором Visual Basic, vbc . exe):
.method public instance int32 Add(int32 x, int32 y) cil managed
{
// Code size 8 @x8)
// Размер кода 8 @x8)
.maxstack 2
.locals mit (int32 V_0)
IL_0000: ldarg.l
IL_0001: ldarg.2
IL_0002: add.ovf
IL_0003: stloc.O
IL_0004: br.s IL_0006
IL_0006: ldloc.O
IL_0007: ret
} // end of method Calc: :Add
// конец метода Calc::Add
Исходный код. Файлы с кодом Calc . cs и Calc . vb доступны в подкаталоге Chapter l.
Преимущества CIL
На этом этапе может возникнуть вопрос о том, какую выгоду приносит компиляция
исходного кода в CIL, а не напрямую в набор ориентированных на конкретную
платформу инструкций. Одним из самых важных преимуществ такого подхода является
интеграция языков. Как уже можно было увидеть, все компиляторы .NET генерируют
примерно одинаковые CIL-инструкции. Благодаря этому все языки могут
взаимодействовать в рамках четко обозначенной двоичной "арены".
Более того, поскольку CIL не зависит от платформы, .NET Framework тоже
получается не зависящей от платформы, предоставляя те же самые преимущества, к которым
привыкли Java-разработчики (например, единую кодовую базу, способную работать во
многих операционных системах). На самом деле уже существует международный
стандарт языка С#, а также подмножество платформы .NET и реализации для многих
операционных систем, отличных от Windows (более подробно об этом речь пойдет в конце
настоящей главы). В отличие от Java, однако, .NET позволяет создавать приложения на
предпочитаемом языке.
Компиляция CIL-кода в инструкции,
ориентированные на конкретную платформу
Из-за того, что в сборках содержатся CIL-инструкции, а не инструкции,
ориентированные на конкретную платформу, CIL-код перед использованием должен
обязательно компилироваться на лету. Объект, который отвечает за компиляцию CIL-кода
в понятные ЦП инструкции, называется оперативным dust-in-time — JIT)
компилятором. Иногда его "по-дружески" называют Jitter. Исполняющая среда .NET использует
JIT-компилятор в соответствии с целевым ЦП и оптимизирует его согласно лежащей в
основе платформе.
Глава 1. Философия .NET 59
Например, в случае создания .NET-приложения, предназначенного для
развертывания на карманном устройстве (например, на мобильном устройстве, функционирующем
под управлением Windows), соответствующий JIT-компилятор будет оптимизирован под
функционирование в среде с ограниченным объемом памяти, а в случае
развертывания сборки на серверной системе (где объем памяти редко представляет проблему),
наоборот — под функционирование в среде с большим объемом памяти. Это дает
разработчикам возможность писать единственный блок кода, который будет автоматически
эффективно компилироваться JIT-компилятором и выполняться на машинах с разной
архитектурой.
Более того, при компиляции CIL-инструкций в соответствующий машинный код JIT-
компилятор будет помещать результаты в кэш в соответствии с тем, как того требует
целевая операционная система. То есть при вызове, например, метода PrintDocument ()
в первый раз соответствующие С11>инструкции будут компилироваться в
ориентированные на конкретную платформу инструкции и сохраняться в памяти для
последующего использования, благодаря чему при вызове PrintDocument () в следующий раз
компилировать их снова не понадобится.
На заметку! Можно также выполнять "предварительную ЛТ-компиляцию" при инсталляции
приложения с помощью утилиты командной строки ngen.exe, которая поставляется в составе
набора .NET Framework 4.0 SDK. Применение такого подхода позволяет улучшить показатели по
времени запуска для приложений, насыщенных графикой.
Роль метаданных типов в .NET
Помимо CIL-инструкций, в сборке .NET содержатся исчерпывающие и точные
метаданные, которые описывают каждый определенный в двоичном файле тип (например,
класс, структуру или перечисление), а также всех его членов (например, свойства,
методы или события). К счастью, за генерацию новейших и наилучших метаданных по
типам всегда отвечает компилятор, а не программист. Из-за того, что метаданные .NET
являются настолько детальными, сборки представляют собой полностью
самоописываемые (self-describing) сущности.
Чтобы увидеть, как выглядит формат метаданных типов в .NET, давайте
рассмотрим метаданные, которые были сгенерированы для приведенного выше метода Add ()
из класса Calc на языке С# (метаданные для версии метода Add () на языке Visual Basic
будут выглядеть похоже):
TypeDef #2 @2000003)
TypDefName: CalculatorExample.Calc @2000003)
Flags : [NotPublic] [AutoLayout] [Class]
[AnsiClass] [BeforeFieldlnit] @0100001)
Extends : 01000001 [TypeRef] System.Object
Method #1 @6000003)
Methodllame
Flags
PVA
ImplFlags
CallCnvntn
hasThis
ReturnType: 14
2 Arguments
Argument #1: 14
Argument #2: 14
Add @6000003)
[Public] [HideBySig] [ReuseSlot] @0000086)
0x00002090
[IL] [Managed] @0000000)
[DEFAULT]
60 Часть I. Общие сведения о языке С# и платформе .NET
2 Parameters
A) ParamToken : @8000001) Name : x flags: [none] @0000000)
B) ParamToken : @8000002) Name : у flags: [none] @0000000)
Метаданные используются во многих операциях самой исполняющей среды .NET, a
также в различных средствах разработки. Например, функция IntelliSense,
предлагаемая в таких средствах, как Visual Studio 2010, работает за счет считывания метаданных
сборки во время проектирования. Кроме того, метаданные используются в различных
утилитах для просмотра объектов, инструментах отладки и в самом компиляторе языка
С#. Можно с полной уверенностью утверждать, что метаданные играют ключевую роль
во многих .NET-технологиях, в том числе в Windows Communication Foundation (WCF),
рефлексии, динамическом связывании и сериализации объектов. Более подробно о роли
метаданных .NET будет рассказываться в главе 17.
Роль манифеста сборки
И, наконец, последним, но не менее важным моментом, о котором осталось
вспомнить, является наличие в сборке .NET и таких метаданных, которые описывают саму
сборку (они формально называются манифестом). Помимо прочих деталей, в
манифесте документируются все внешние сборки, которые требуются текущей сборке для
корректного функционирования, версия сборки, информация об авторских правах и т.д.
Как и за генерацию метаданных типов, за генерацию манифеста сборки тоже всегда
отвечает компилятор. Ниже приведены некоторые наиболее существенные детали
манифеста, сгенерированного в результате компиляции приведенного ранее в этой главе
файла двоичного кода Calc.cs (здесь предполагается, что компилятору было указано
назначить сборке имя Calc . ехе):
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
.ver 4:0:0:0
}
.assembly Calc
{
.hash algorithm 0x00008004
.ver 1:0:0:0
}
.module Calc.exe
.imagebase 0x00400000
.subsystem 0x00000003
.file alignment 512
.corflags 0x00000001
В двух словах, в этом манифесте представлен список требуемых для Calc. ехе
внешних сборок (в директиве . assembly extern), а также различные характеристики самой
сборки (наподобие номера версии, имени модуля и т.д.). О пользе данных манифеста
будет гораздо более подробно рассказываться в главе 14.
Что собой представляет общая система типов (CTS)
В каждой конкретной сборке может содержаться любое количество различающихся
типов. В мире .NET "тип" представляет собой просто общий термин, который
применяется для обозначения любого элемента из множества (класс, интерфейс, структура,
перечисление, делегат}. При построении решений с помощью любого языка .NET,
скорее всего, придется взаимодействовать со многими из этих типов. Например, в сборке
Глава 1. Философия .NET 61
может содержаться один класс, реализующий определенное количество интерфейсов,
метод одного из которых может принимать в качестве входного параметра
перечисление, а возвращать структуру.
Вспомните, что CTS (общая система типов) представляет собой формальную
спецификацию, в которой описано то, как должны быть определены типы для того, чтобы они
могли обслуживаться в CLR-среде. Внутренние детали CTS обычно интересуют только
тех, кто занимается разработкой инструментов и/или компиляторов для платформы
.NET. Абсолютно всем .NET-программистам, однако, важно уметь работать на
предпочитаемом ими языке с пятью типами из CTS. Краткий обзор этих типов приведен ниже.
Типы классов
В каждом совместимом с .NET языке поддерживается, как минимум, понятие типа
класса (class type), которое играет центральную роль в объектно-ориентированном
программировании (ООП). Каждый класс может включать в себя любое количество членов
(таких как конструкторы, свойства, методы и события) и точек данных (полей). В С#
классы объявляются с помощью ключевого слова class.
// Тип класса С# с одним методом.
class Calc
{
public int Add(int x, int y)
{ return x + y; }
}
В главе 5 будет более подробно показываться, как можно создавать типы классов
CTS в С#, а пока в табл. 1.1 приведен краткий перечень характеристик, которые
свойственны типам классов.
Таблица 1.1. Характеристики классов CTS
Характеристика 0пи
классов
Запечатанные Запечатанные (sealed), или герметизированные, классы не могут
выступать в роли базовых для других классов, т.е. не допускают наследования
Реализующие Интерфейсом (interface) называется коллекция абстрактных членов, кото-
интерфейсы рые обеспечивают возможность взаимодействия между объектом и
пользователем этого объекта. CTS позволяет реализовать в классе любое
количество интерфейсов
Абстрактные Экземпляры абстрактных (abstract) классов не могут создаваться напря-
или мую, и предназначены для определения общих аспектов поведения для
конкретные производных типов. Экземпляры же конкретных (concrete) классы могут
создаваться напрямую
Степень видимости Каждый класс должен конфигурироваться с атрибутом видимости
(visibility). По сути, этот атрибут указывает, должен ли класс быть
доступным для использования внешним сборкам или только изнутри
определяющей сборки
Типы интерфейсов
Интерфейсы представляют собой не более чем просто именованную коллекцию
определений абстрактных членов, которые могут поддерживаться (т.е. реализоваться) в
данном классе или структуре. В С# типы интерфейсов определяются с помощью
ключевого слова interface, как, например, показано ниже:
62 Часть I. Общие сведения о языке С# и платформе .NET
// Тип интерфейса в С# обычно объявляется
// общедоступным, чтобы позволить типам в других
// сборках реализовать его поведение.
public interface IDraw
{
void Draw();
}
Сами по себе интерфейсы мало чем полезны. Однако когда они реализуются в
классах или структурах уникальным образом, они позволяют получать доступ к
дополнительным функциональным возможностям за счет добавления просто ссылки на них в
полиморфной форме. Тема программирования с использованием интерфейсов подробно
рассматривается в главе 9.
Типы структур
Понятие структуры тоже сформулировано в CTS. Тем, кому приходилось работать с
языком С, будет приятно узнать, что таким пользовательским типам удалось "выжить"
в мире .NET (хотя на внутреннем уровне они и ведут себя несколько иначе). Попросту
говоря, структура может считаться "облегченным" типом класса с основанной на
использовании значений семантикой. Более подробно об особенностях структур будет
рассказываться в главе 4. Обычно структуры лучше всего подходят для моделирования
геометрических и математических данных, и в С# они создаются с помощью ключевого
слова struct.
/ / Тип структуры в С#.
struct Point
i
//В структурах могут содержаться поля.
public int xPos, yPos;
//В структурах могут содержаться параметризованные конструкторы.
public Point (int x, int у) { xPos = x; yPos = y; }
// В структурах могут определяться методы.
public void PrintPosition ()
{
Console.WriteLine (" ({ 0}, {1})", xPos, yPos);
}
}
Типы перечислений
Перечисления (enumeration) представляют собой удобную программную
конструкцию, которая позволяет группировать данные в пары "имя-значение". Например,
предположим, что требуется создать приложение видеоигры, в котором игроку бы
позволялось выбирать персонажа одной из трех следующих категорий: Wizard (маг), Fighter
(воин) или Thief (вор). Вместо того чтобы использовать и отслеживать числовые
значения для каждого варианта, в этом случае гораздо удобнее создать соответствующее
перечисление с помощью ключевого слова enum:
// Тип перечисления С#.
public enum CharacterType
{
Wizard = 100,
Fighter = 200,
Thief = 300
}
Глава 1. Философия .NET 63
По умолчанию для хранения каждого элемента выделяется блок памяти,
соответствующий 32-битному целому, однако при необходимости (например, при
программировании с расчетом на устройства, обладающие малыми объемами памяти, вроде
мобильных устройств Windows) это значение можно изменить. Кроме того, в CTS необходимо,
чтобы перечислймые типы наследовались от общего базового класса System.Enum. Как
будет показано в главе 4, в этом базовом классе присутствует ряд весьма интересных
членов, которые позволяют извлекать, манипулировать и преобразовывать базовые
пары "имя-значение" программным образом.
Типы делегатов
Делегаты (delegate) являются .NET-эквивалентом безопасных в отношении
типов указателей функций в стиле С. Главное отличие заключается в том, что делегат в
.NET представляет собой класс, который наследуется от System.MulticastDelegate, a
не просто указатель на какой-то конкретный адрес в памяти. В С# делегаты
объявляются с помощью ключевого слова delegate.
// Этот тип делегата в С# может 'указывать' на любой метод,
// возвращающий целое число и принимающий два целых
// числа в качестве входных данных.
public delegate int BinaryOp(int x, int y);
Делегаты очень удобны, когда требуется обеспечить одну сущность возможностью
перенаправлять вызов другой сущности и образовывать основу для архитектуры
обработки событий .NET. Как будет показано в главах 11 и 19, делегаты обладают
внутренней поддержкой для групповой адресации (т.е. пересылки запроса сразу множеству
получателей) и асинхронного вызова методов (т.е. вызова методов во вторичном потоке).
Члены типов
Теперь, когда было приведено краткое описание каждого из сформулированных в
CTS типов, пришла пора рассказать о том, что большинство из этих типов способно
принимать любое количество членов (member). Формально в роли члена типа может
выступать любой элемент из множества {конструктор, финализатор, статический
конструктор, вложенный тип, операция, метод, свойство, индексатор, поле, поле только для
чтения, константа, событие).
В спецификации CTS описываются различные "характеристики", которые могут
быть ассоциированы с любым членом. Например, каждый член может обладать
характеристикой, отражающей его доступность (т.е. общедоступный, приватный или
защищенный). Некоторые члены могут объявляться как абстрактные (для навязывания
полиморфного поведения производным типам) или как виртуальные (для определения
фиксированной, но допускающей переопределение реализации). Кроме того, почти все
члены также могут делаться статическими членами (привязываться на уровне класса)
или членами экземпляра (привязываться на уровне объекта). Более подробно о том, как
можно создавать членов, будет рассказываться в ходе нескольких следующих глав.
На заметку! Как будет описано в главе 10, в языке С# также поддерживается создание
обобщенных типов и членов.
Встроенные типы данных
И, наконец, последним, что следует знать о спецификации CTS, является то, что в
ней содержится четко определенный набор фундаментальных типов данных. Хотя в
каждом отдельно взятом языке для объявления того или иного встроенного типа данных
64 Часть I. Общие сведения о языке С# и платформе .NET
из CTS обычно предусмотрено свое уникальное ключевое слово, все эти ключевые слова
в конечном итоге соответствуют одному и тому же типу в сборке mscorlib. dll.
В табл. 1.2 показано, как ключевые типы данных из CTS представляются в
различных .NET-языках.
Таблица 1.2. Встроенные типы данных, описанные в CTS
Тип данных в CTS
Ключевое слово
в Visual Basic
Ключевое
слово в С#
Ключевое слово в С++и CLI
System.ByteByte
System.SByteSByte
System.Intl6
System.Int32
System.Int64
System.UIntl6
System.UInt32
System.UInt64
System.SingleSingle
System.DoubleDouble
System.Ob]ectObject
System.CharChar
System.StringString
System.DecimalDecimal
System.BooleanBoolean
Byte
SByte
Short
Integer
Long
UShort
Ulnteger
ULong
Single
Double
Object
Char
String
Decimal
Boolean
byte
sbyte
short
int
long
ushort
uint
ulong
float
double
object
char
String
decimal
bool
unsigned
char
signed char
short
int или long
int64
unsigned
unsigned
unsigned
unsigned
float
double
objectA
wchar t
StringA
Decimal
bool
short
int или
long
int64
Из-за того факта, что уникальные ключевые слова в любом управляемом языке
являются просто сокращенными обозначениями реального типа из пространства имен
System, больше не нужно беспокоиться ни об условиях переполнения и потери
значимости (overflow/underflow) в случае числовых данных, ни о внутреннем представлении
строк и булевских значений в различных языках. Рассмотрим следующие фрагменты
кода, в которых 32-битные числовые переменные определяются в С# и Visual Basic с
использованием соответствующих ключевых слов из самих языков, а также
формального типа из CTS:
// Определение числовых переменных в С#.
int 1=0;
System.Int32 j = 0;
1 Определение числовых переменных в VB.
Dim 1 As Integer = 0
Dim j As System.Int32 = 0
Что собой представляет общеязыковая
спецификация (CLS)
Как известно, в разных языках программирования одни и те же программные
конструкции выражаются своим уникальным, специфическим для конкретного языка
образом. Например, в С# конкатенация строк обозначается с помощью знака "плюс" (+),
Глава 1. Философия .NET 65
а в VB для этого обычно используется амперсанд (&). Даже в случае выражения в двух
отличных языках одной и той же программной идиомы (например, функции, не
возвращающей значения), очень высока вероятность того, что с виду синтаксис будет
выглядеть очень по-разному:
//Не возвращающий ничего метод в С#.
public void MyMethod ()
{
// Какой-нибудь интересный код...
}
' Не возвращающий ничего метод в VB.
Public Sub MyMethod ()
' Какой-нибудь интересный код...
End Sub
Как уже показывалось, подобные небольшие вариации в синтаксисе для
исполняющей среды .NET являются несущественными благодаря тому, что соответствующие
компиляторы (в данном случае — csc.exe и vbc.exe) генерируют схожий набор CIL-
инструкций. Однако языки могут еще отличаться и по общему уровню функциональных
возможностей. Например, в каком-то из языков .NET может быть или не быть
ключевого слова для представления данных без знака, а также поддерживаться или не
поддерживаться типы указателей. Из-за всех таких вот возможных вариаций было бы просто
замечательно иметь в распоряжении какие-то опорные требования, которым должны
были бы отвечать все поддерживающие .NET языки.
CLS (Common Language Specification — общая спецификация для языков
программирования) как раз и представляет собой набор правил, которые во всех подробностях
описывают минимальный и полный комплект функциональных возможностей, которые
должен обязательно поддерживать каждый отдельно взятый .NET-компилятор для того,
чтобы генерировать такой программный код, который мог бы обслуживаться CLR и к
которому в то же время могли бы единообразным образом получать доступ все языки,
ориентированные на платформу .NET. Во многих отношениях CLS может считаться
просто подмножеством всех функциональных возможностей, определенных в CTS.
В конечном итоге CLS является своего рода набором правил, которых должны
придерживаться создатели компиляторов при желании, чтобы их продукты могли без
проблем функционировать в мире .NET Каждое из этих правил имеет простое название
(например, "Правило CLS номер 6") и описывает, каким образом его действие касается
тех, кто создает компиляторы, и тех, кто (каким-либо образом) будет взаимодействовать
с ними. Самым главным в CLS является правило J, гласящее, что правила CLS касаются
только тех частей типа, которые делаются доступными за пределами сборки, в которой
они определены.
Из этого правила можно (и нужно) сделать вывод о том, что все остальные правила в
CLS не распространяются на логику, применяемую для построения внутренних рабочих
деталей типа .NET. Единственными аспектами типа, которые должны соответствовать
CLS, являются сами определения членов (т.е. соглашения об именовании, параметры
и возвращаемые типы). В рамках логики реализации члена может применяться любое
количество и не согласованных с CLS приемов, поскольку для внешнего мира это не
будет играть никакой роли.
Для иллюстрации ниже приведен метод Add () на языке С#, который не отвечает
правилам CLS, поскольку в его параметрах и возвращаемых значениях используются
данные без знака (что не является требованием CLS):
class Calc
I
// Использование данных без знака внешним образом
// не соответствует правилам CLS!
66 Часть I. Общие сведения о языке С# и платформе .NET
public ulong Add(ulong x, ulong y)
{ return x + y; }
}
Однако если бы мы просто использовали данные без знака внутренним образом, как
показано ниже:
class Calc
{
public int Adddnt x, mt y)
{
// Поскольку переменная ulong используется здесь
// только внутренне, правила CLS не нарушаются.
ulong temp = 0;
return x + у;
}
}
тогда правила CLS были бы соблюдены и все языки .NET могли бы обращаться к
данному методу Add ().
Разумеется, помимо правила 1 в CLS содержится и много других правил. Например,
в CLS также описано, каким образом в каждом конкретном языке должны
представляться строки текста, оформляться перечисления (подразумевающие использование
базового типа для хранения), определяться статические члены и т.д. К счастью, для того,
чтобы быть умелым разработчиком .NET, запоминать все эти правила вовсе не
обязательно. Опять-таки, очень хорошо разбираться в спецификациях CTS и CLS необходимо
только создателям инструментов и компиляторов.
Забота о соответствии правилам CLS
Как можно будет увидеть в ходе прочтения настоящей книги, в С# на самом деле
имеется ряд программных конструкций, которые не соответствуют правилам CLS.
Хорошая новость, однако, состоит в том, что компилятор С# можно заставить
выполнять проверку программного кода на предмет соответствия правилам CLS с помощью
всего лишь единственного атрибута. NET:
/ / Указание компилятору С# выполнять проверку
//на предмет соответствия CLS.
[assembly: System.CLSCompliant(true)]
Детали программирования с использованием атрибутов более подробно
рассматриваются в главе 15. А пока главное понять просто то, что атрибут [CLSCompliant]
заставляет компилятор С# проверять каждую строку кода на предмет соответствия
правилам CLS. В случае обнаружения нарушения каких-нибудь правил CLS компилятор
будет выдавать ошибку и описание вызвавшего ее кода.
Что собой представляет общеязыковая
исполняющая среда (CLR)
Помимо спецификаций CTS и CLS, для получения общей картины на данный
момент осталось рассмотреть еще одну аббревиатуру — CLR, которая расшифровывается
как Common Language Runtime (общеязыковая исполняющая среда). С точки зрения
программирования под термином исполняющая среда может пониматься коллекция
внешних служб, которые требуются для выполнения скомпилированной единицы
программного кода. Например, при использовании платформы MFC для создания нового
Глава 1. Философия .NET 67
приложения разработчики осознают, что их программе требуется библиотека времени
выполнения MFC (т.е. mfc42 . dll). Другие популярные языки тоже имеют свою
исполняющую среду: программисты, использующие язык VB6, к примеру, вынуждены
привязываться к одному или двум модулям исполняющей среды (вроде msvbvm60 .dll), a
разработчики на Java — к виртуальной машине Java (JVM).
В составе .NET предлагается еще одна исполняющая среда. Главное отличие между
исполняющей средой .NET и упомянутыми выше средами, состоит в том, что
исполняющая среда .NET обеспечивает единый четко определенный уровень выполнения,
который способны использовать все совместимые с .NET языки и платформы.
Основной механизм CLR физически имеет вид библиотеки под названием
mscoree .dll (и также называется общим механизмом выполнения исполняемого кода
объектов — Common Object Runtime Execution Engine). При добавлении ссылки на
сборку для ее использования загрузка библиотеки mscoree . dll осуществляется
автоматически и затем, в свою очередь, приводит к загрузке требуемой сборки в память.
Механизм исполняющей среды отвечает за выполнение целого ряда задач. Сначала,
что наиболее важно, он отвечает за определение места расположения сборки и
обнаружение запрашиваемого типа в двоичном файле за счет считывания содержащихся
там метаданных. Затем он размещает тип в памяти, преобразует CIL-код в
соответствующие платформе инструкции, производит любые необходимые проверки на предмет
безопасности и после этого, наконец, непосредственно выполняет сам запрашиваемый
программный код.
Помимо загрузки пользовательских сборок и создания пользовательских типов,
механизм CLR при необходимости будет взаимодействовать и с типами, содержащимися
в библиотеках базовых классов .NET. Хотя вся библиотека базовых классов поделена на
ряд отдельных сборок, главной среди них является сборка ms cor lib .dll. В этой сборке
содержится большое количество базовых типов, охватывающих широкий спектр
типичных задач программирования, а также базовых типов данных, применяемых во всех
языках .NET. При построении .NET-решений доступ к этой конкретной сборке будет
предоставляться автоматически.
На рис. 1.4 схематично показано, как выглядят взаимоотношения между исходным
кодом (предусматривающим использование типов из библиотеки базовых классов),
компилятором .NET и механизмом выполнения .NET.
Различия между сборками,
пространствами имен и типами
Каждый из нас понимает важность библиотек программного кода. Главная цель
таких библиотек, как MFC, Java Enterprise Edition или ATL, заключается в предоставлении
разработчикам набора готовых, правильно оформленных блоков программного кода,
чтобы они могли использовать их своих приложениях. Язык С#, однако, не
поставляется с какой-либо специфической библиотекой кода. Вместо этого от разработчиков,
использующих С#, требуется применять нейтральные к языкам библиотеки, которые
поставляются в .NET. Для поддержания всех типов в библиотеках базовых классов в
хорошо организованном виде в .NET широко применяется понятие пространства имен
(namespace).
Под пространством имен понимается группа связанных между собой с
семантической точки зрения типов, которые содержатся в сборке. Например, в пространстве имен
System. 10 содержатся типы, имеющие отношение к операциям ввода-вывода, в
пространстве имен System. Data — основные типы для работы с базами данных, и т.д.
68 Часть I. Общие сведения о языке С# и платформе .NET
Исходный код .NET
несовместимом
с .NET языке
Механизм выполнения .NET
(mscoree.dll)
Загрузчик классов
ЛТ-компилятор
г
Соответствующие
платформе
инструкции
■
■
Выполнение члена
Рис. 1.4. Механизм mscoree.dll в действии
Очень важно понимать, что в одной сборке (например, ms cor lib. dll) может
содержаться любое количество пространств имен, каждое из которых, в свою очередь, может
иметь любое число типов.
Чтобы стало понятнее, на рис. 1.5 показан снимок окна предлагаемой в Visual Studio
2010 утилиты Object Browser. Эта утилита позволяет просматривать сборки, на которые
имеются ссылки в текущем проекте, пространства имен, содержащиеся в каждой из
этих сборок, типы, определенные в каждом из этих пространств имени, и члены
каждого из этих типов. Важно обратить внимание на то, что в mscorlib.dll содержится
очень много самых разных пространств имен (вроде System. 10), и что в каждом из них
содержатся свои семантически связанные типы (такие как BinaryReader) .
Компилятор .NET
Сборка * .dll или *.ехе
(с CIL-инструкциями,
метаданными и манифестом)
Библиотеки
базовых классов
(mscorlib.dll и др.)
Глава 1. Философия .NET 69
Object Browse/ X Q
Browse: My Solution • ...
|<Search> - J |
! j3 CSharpAdder
i> >J Microsoft.CSharp
{} Microsoft.Win32
{} Microsoft.Win32.SafeHandies
i {) System.Collections
• {} System.Collections.Concurrent
{} System.Collections.Generic
{} System.Collectiora.ObjectModel
{} System.Configuratton.Assemblie5
; {} System.Deployment.Internal
{} System.Diagnostics
t> {} System.Diagnostics.CodeAnarysis
> {} System.Diagnostics.Contracts
0 System.Diagnostics.Eventing
> {} System.Diagnostics.SymbolStore
О System.Globalization
л {} System.IO
■*jU!
> -*J BinaryWrrter
«tj BufferedStream
4 BinaryReader(SystemJO.Stream, System.Text.Encoding) -»
♦ BinaryReader(SystemJO.Stream)
♦ CloseO
Ф DisposeO
y* Dispose(bool)
v FillBuffer(mt)
♦ PeekCharQ
* Read(byte(L int int)
# Read(char[L int int)
* ReadO
ft Read7BitEncodedIntO
♦ ReedBooleanO
* ReadByteO
Ф ReadBytes(rnt)
V ReadCharO
* ReadChars(int)
♦ BadBwrwM
V ReadDoubleQ
Уш1шш&ЬАЯй4ШШй1штшжтйш тиггпшчгггигпгиипгп-пппппт! и -11
[public class Binary Reader
1 Member of System .10
1 Summary:
1 Reads primitive data types as binary values in a specific encoding.
Рис. 1.5. В одной сборке может содержаться произвольное количество пространств имен
Птавное отличие между таким подходом и зависящими от конкретного языка
библиотеками вроде MFC состоит в том, что он обеспечивает использование во всех языках,
ориентированных на среду выполнения .NET, одних и тех лее пространств имен и одних
и тех лее типов. Например, в трех приведенных ниже программах иллюстрируется
создание постоянно применяемого примера "Hello World" на языках С#, VB и C++/CLI.
// Hello world на языке С#
using System;
public class MyApp
{
static void Mam()
{
Console.WriteLine ("Hi from C#");
' Hello world на языке VB
Imports System
Public Module MyApp
Sub Main ()
Console.WriteLine ("Hi from VB")
End Sub
End Module
// Hello world на языке C++/CLI
#include "stdafx.h"
using namespace System;
int main(array<System::String Л> Aargs)
{
Console::WriteLine("Hi from C++/CLI"),
return 0;
Обратите внимание, что в каждом из языков применяется класс Console, определенный
в пространстве имен System. Если отбросить незначительные синтаксические отличия, то
в целом все три программы выглядят очень похоже, как по форме, так и по логике.
70 Часть I. Общие сведения о языке С# и платформе .NET
Очевидно, что главной задачей любого планирующего использовать .NET
разработчика является освоение того обилия типов, которые содержатся в (многочисленных)
пространствах имен .NET. Самым главным пространством имен, с которого следует
начинать, является System. В этом пространстве имен содержится набор ключевых
типов, которые любому разработчику .NET нужно будет эксплуатировать снова и снова.
Фактически создание функционального приложения на С# невозможно без добавления
хотя бы ссылки на пространство имен System, поскольку все главные типы данных
(вроде System. Int32, System. String и т.д.) содержатся именно здесь. В табл. 1.3 приведен
краткий список некоторых (но, конечно же, не всех) предлагаемых в .NET пространств
имен, которые были поделены на группы на основе функциональности.
Таблица 1.3. Некоторые пространства имен в .NET
Пространство имен в .NET
Описание
System
System.Collections
System.Collections.Generic
System.Data
System.Data.Common
System.Data.EntityClient
System.Data.SqlClient
System.10
System.10.Compression
System.10.Ports
System.Reflection
System.Refleetion.Emit
System.Runtime.InteropServices
System.Drawing
System.Windows.Forms
System.Windows
System.Windows.Controls
System.Windows.Shapes
System.Linq
System.Xml.Linq
System.Data.DataSetExtensions
System.Web
Внутри пространства имен System содержится
множество полезных типов, позволяющих иметь дело с
внутренними данными, математическими вычислениями,
генерированием случайных чисел, переменными среды
и сборкой мусора, а также ряд наиболее часто
применяемых исключений и атрибутов
В этих пространствах имен содержится ряд
контейнерных типов, а также несколько базовых типов и
интерфейсов, которые позволяют создавать специальные
коллекции
Эти пространства имен применяются для
взаимодействия с базами данных с помощью AD0.NET
В этих пространствах содержится много типов,
предназначенных для работы с операциями файлового ввода-
вывода, сжатия данных и манипулирования портами
В этих пространствах имен содержатся типы, которые
поддерживают обнаружение типов во время
выполнения, а также динамическое создание типов
В этом пространстве имен содержатся средства, с
помощью которых можно позволить типам .NET
взаимодействовать с "неуправляемым кодом" (например, DLL-
библиотеками на базе С и серверами СОМ) и наоборот
В этих пространствах имен содержатся типы,
применяемые для построения настольных приложений с
использованием исходного набора графических инструментов
.NET (Windows Forms)
Пространство System.Windows является корневым
среди этих нескольких пространств имен, которые
представляют собой набор графических инструментов
Windows Presentation Foundation (WPF)
В этих пространствах имен содержатся типы,
применяемые при выполнении программирования с
использованием API-интерфейса LINQ
Это пространство имен является одним из многих,
которые позволяют создавать веб-приложения ASP.NET
Глава 1. Философия .NET 71
Окончание табл. 1.3
Пространство имен в .NET Описание
System. ServiceModel Это пространство имен является одним из многих,
которые позволяется применять для создания
распределенных приложений с помощью API-интерфейса Windows
Communication Foundation (WCF)
System. Workflow. Runtime Эти два пространства имен являются главными предста-
System. Workflow.Activities вителями многочисленных пространств имен, в которых
содержатся типы, применяемые для построения
поддерживающих рабочие потоки приложений с помощью
API-интерфейса Windows Workflow Foundation (WWF)
System.Threading В этом пространстве имен содержатся многочисленные
System.Threading. Tasks типы для построения многопоточных приложений,
способных распределять рабочую нагрузку среди нескольких ЦП.
System. Security Безопасность является неотъемлемым свойством мира
.NET. В относящихся к безопасности пространствах
имен содержится множество типов, которые позволяют
иметь дело с разрешениями, криптографической
защитой и т.д
System. Xml В этом ориентированном на XML пространстве имен
содержатся многочисленные типы, которые можно
применять для взаимодействия с XML-данными
Роль корневого пространства Microsoft
При изучении перечня, приведенного в табл. 1.3, нетрудно было заметить, что
пространство имен System является корневым для приличного количества вложенных
пространств имен (таких как System. 10, System. Data и т.д.). Как оказывается, однако,
помимо System в библиотеке базовых классов предлагается еще и ряд других корневых
пространств имен наивысшего уровня, наиболее полезным из которых является
пространство имен Microsoft.
В любом пространстве имен, которое находится внутри пространства имен
Microsoft (как, например, Microsoft.CSharp, Microsoft.ManagementConsole и
Microsoft .Win32), содержатся типы, применяемые для взаимодействия
исключительно с теми службами, которые свойственны только лишь операционной системе
Windows. Из-за этого не следует предполагать, что данные типы могут с тем же
успехом применяться и в других поддерживающих .NET операционных системах вроде Мае
OS X. В настоящей книге детали вложенных в Microsoft пространств имен подробно
рассматриваться не будут, поэтому заинтересованным придется обратиться к
документации .NET Framework 4.0 SDK.
На заметку! В главе 2 будет показано, как пользоваться документацией .NET Framework 4.0 SDK,
в которой содержатся детальные описания всех пространств имен, типов и членов,
встречающихся в библиотеках базовых классов.
Получение доступа к пространствам имен программным образом
Не помешает снова вспомнить, что пространства имен представляют собой не более
чем удобный способ логической организации взаимосвязанных типов для упрощения
работы с ними. Давайте еще раз обратимся к пространству имен System. С
человеческой точки зрения System. Console представляет класс по имени Console, который
72 Часть I. Общие сведения о языке С# и платформе .NET
содержится внутри пространства имен под названием System. Но с точки зрения
исполняющей .NET это не так. Механизм исполняющей среды видит только одну лишь
сущность по имени System. Console.
В С# ключевое слово using упрощает процесс добавления ссылок на типы,
содержащиеся в определенном пространстве имен. Вот как оно работает. Предположим, что требуется
создать графическое настольное приложение с использованием API-интерфейса Windows
Forms. В главном окне этого приложения должна визуализироваться гистограмма с
информацией, получаемой из базы данных, и отображаться логотип компании. Поскольку
для изучения типов в каждом пространстве имен требуются время и силы, ниже показаны
некоторые из возможных кандидатов на использование в такой программе:
// Все пространства имен, которые можно использовать
// для создания подобного приложения.
using System; // Общие типы из библиотеки базовых классов.
using System.Drawing; // Типы для визуализации графики.
using System.Windows.Forms; // Типы для создания элементов пользовательского
// интерфейса с помощью Windows Forms,
using System.Data; // Общие типы для работы с данными,
using System. Data. SqlClient; // Типы для доступа к данным MS SQL Server.
После указания ряда необходимых пространств имен (и добавления ссылки на
сборки, в которых они находятся), можно свободно создавать экземпляры типов, которые в
них содержатся. Например, при желании создать экземпляр класса Bitmap
(определенного в пространстве имен System. Drawing), можно написать следующий код:
// Перечисляем используемые в данном файле
// пространства имен явным образом.
using System;
using System.Drawing;
class Program
{
public void DisplayLogo ()
{
// Создаем растровое изображение
// размером 20*20 пикселей.
Bitmap companyLogo = new BitmapB0, 20);
}
}
Благодаря импортированию в этом коде пространства имен System. Drawing,
компилятор сможет определить, что класс Bitmap является членом данного пространства
имен. Если пространство имен System. Drawing не указать, компилятор сообщит об
ошибке. При желании переменные также можно объявлять с использованием
полностью уточненного имени:
II Пространство имен System.Drawing здесь не указано!
using System;
class Program
{
public void DisplayLogo ()
{
// Используем полностью уточненное имя.
System.Drawing.Bitmap companyLogo =
new System.Drawing.BitmapB0, 20);
}
}
Глава 1. Философия .NET 73
Хотя определение типа с использованием полностью уточненного имени позволяет
делать код более удобным для восприятия, трудно не согласиться с тем, что
применение поддерживаемого в С# ключевого слова using, в свою очередь, позволяет
значительно сократить количество печатаемых знаков. Поэтому в настоящей книге мы будем
стараться избегать использования полностью уточненных имен (если только не будет
возникать необходимости в устранении какой-то очевидной неоднозначности) и
стремиться пользоваться более простым подходом, т.е. ключевым словом using.
Важно помнить о том, что ключевое слово using является просто сокращенным
способом указания полностью уточненного имени. Поэтому любой из этих подходов
приводит к получению одного и того же CIL-кода (с учетом того факта, что в CIL-коде всегда
применяются полностью уточненные имена) и не сказывается ни на
производительности, ни на размере сборки.
Добавление ссылок на внешние сборки
Помимо указания пространства имен с помощью поддерживаемого в С# ключевого
слова using, компилятору С# необходимо сообщить имя сборки, в которой
содержится само CIL-onpeделение упоминаемого типа. Как уже отмечалось, многие из ключевых
пространств имен .NET находятся внутри сборки mscorlib.dll.
Класс System. Drawing. Bitmap, однако, содержится в отдельной сборке по имени
System. Drawing, dll. Подавляющее большинство сборок в .NET Framework
размещено в специально предназначенном для этого каталоге, который называется глобальным
кэшем сборок (Global Assembly Cache — GAC). На машине Windows по умолчанию GAC
может располагаться внутри каталога ?Qwindir%\Assembly, как показано на рис. 1.6.
VT Favorite
■ Desktop
Ц] Recent Places
Л Libraries
0 Documents
J* Music
W Pictures
Щ Videos
«$ Homegroup
'■ Computer
f» Mongo Drive (C:)
^ CD Dnve (F:)
Assembly Name
j JUSystem.Data.SqlXml
j sASystem.Deployment
jfO System.Design
db System.DirectoryServices
i£i System. DirectoryServices-AccountManagement 3.5.0.0
#Ь System.DirectoryServtces.Protocois
j iASyst em.Drawing.Design
ж) System.EnterpriseServices
40 System.EnterpriseServices
I lASystem.IdentityModel
ЯХ System.ldentityModel.Selectors
ifl System JO.Log
dll System.Management
Ж1 System. Management-Automatior
Version Cut.., Public Key Token
2.0.0.0
2.0.0.0
20.0.0
2.0.0.0
3.5.0.0
2.0.0.0
2.0.0.0
2.0.0.0
2.0.0.0
2.0.0,0
5.0.0.0
3.0,0.0
3.0.0,0
2.0 0.0
1.0.0.0
Ь77а5с561934е089
b036f7flld50a3a
b03f5f7flld50a3a
b03f5f7flld50a3a
Ь77а5с5б1934е089
b03f5f7flld50a3a
b03f5f7flld50a3a
b03f5f7flld50a3a
b03f5f7flld50a3a
b03f5f7flld50a3a
Ь77а5с561934е089
Ь77а5с561934е089
b03f5r7flld50a3a
b03f5f7flld50a3a
31bf3856ad364e35
Рис. 1.6. Многие библиотеки .NET размещены в GAC
В зависимости от того, какое средство применяется для разработки приложений
.NET, на выбор может оказываться доступными несколько различных способов для
уведомления компилятора о том, какие сборки требуется включить в цикл компиляции. Эти
способы подробно рассматриваются в следующей главе, а здесь их детали опущены.
На заметку! С выходом версии .NET 4.0 в Microsoft решили выделить под сборки .NET 4.0
специальное место, находящееся отдельно от каталога С : \Windows\Assembly. Более подробно
об этом будет рассказываться в главе 14.
74 Часть I. Общие сведения о языке С# и платформе .NET
Изучение сборки с помощью утилиты ildasm. exe
Тем, кого начинает беспокоить мысль о необходимости освоения всех пространств
имен в .NET, следует просто вспомнить о том, что уникальным пространство имен
делает то, что в нем содержатся типы, которые как-то связаны между собой с
семантической точки зрения. Следовательно, если потребность в создании
пользовательского интерфейса, более сложного, чем у простого консольного приложения,
отсутствует, можно смело забыть о таких пространствах имен, как System. Windows . Forms,
System.Windows и System.Web (и ряда других), а при создании приложений для
рисования — о пространствах имен, которые касаются работы с базами данных. Как и в
случае любого нового набора уже готового кода, опыт приходит с практикой.
Утилита ildasm.exe (Intermediate Language Disassembler — дизассемблер
промежуточного языка), которая поставляется в составе пакета .NET Framework 4.0 SDK,
позволяет загружать любую сборку .NET и изучать ее содержимое, в том числе
ассоциируемый с ней манифест, CIL-код и метаданные типов. По умолчанию эта утилита
установлена в каталоге С: \Program Files\Microsoft SDKs\Windows\v7 . 0A\bin (если
здесь ее нет, поищите на компьютере файл по имени ildasm.exe).
На заметку! Утилиту ildasm.exe легко запустить, открыв в Visual Studio 2010 окно Command
Prompt (Командная строка), введя в нем слово ildasm и нажав клавишу <Enter>.
Р Calcexe-ILDASM
Btc View Help
la yy ЕЯ
► MANIFEST
й Щ CakulatorExample
f JE CakulatorExample.Calc
► .class public auto ansi beforefieUin*
!•■ ■ .ctor: votd()
; ■ Add : int32(int32,int32)
й £ Calculator Example. Program
► .class public auto ansi beforefieldinit
■ .ctor: voJd()
U Main : void()
После запуска этой утилиты нужно выбрать
в меню File (Файл) команду Open (Открыть) и
найти сборку, которую требуется изучить. Для
целей иллюстрации здесь предполагается, что
нужно изучить сборку Calc. exe, которая была
сгенерирована на основе приведенного ранее в
этой главе файла Calc. сs (рис. 1.7). Утилита
ildasm.exe представляет структуру любой
сборки в знакомом древовидном формате.
Просмотр CIL-кода
Рис. 1.7. Утилита ildasm.exe
позволяет просматривать содержащийся внутри
сборки .NET CIL-код, манифест и
метаданные типов
Помимо содержащихся в сборке
пространств имен, типов и членов, утилита
ildasm. exe также позволяет просматривать и
CIL-инструкции, которые лежат в основе
каждого конкретного члена. Например, в
результате двойного щелчка на методе Main () в классе Program открывается отдельное окно с
CIL-кодом, лежащим в основе этого метода, как показано на рис. 1.8.
Просмотр метаданных типов
Для просмотра метаданных типов, которые содержатся в загруженной в текущий
момент сборке, необходимо нажать комбинацию клавиш <Ctrl+M>. На рис. 1.9 показаны
метаданные метода Calc. Add ().
Просмотр метаданных сборки (манифеста)
И, наконец, чтобы просмотреть содержимое манифеста сборки, необходимо дважды
щелкнуть на значке MANIFEST (рис. 1.10).
Глава 1. Философия .NET 75
f? Cakulatorbcampte.CalcApp~Main: voidQ
J ( Find Find Ned
.method priuate hidebysig static uoid Main() cil managed
.entrypoint
| // Code size Л2 <0x2a)
И .maxstack 3
\ .locals init (class CalculatorExample.Calc и 0,
int32 U 1)
J IL ••••: nop
] IL 0001: newobj instance uoid CalculatorExample.Calc::
Щ IL tOM: stloc.i
U IL 0007: ldloc.O
J IL 0000: Idc.iij.s 10
J IL 000a: ldc.iU.s 84
1 IL 000c: calluirt instance int32 CalculatorExample.Calc:
[ IL_0011: stloc.1
1" 1IS[ ШЗШ ^
pj
1
.ctor()
1
:Add(int32,
int32)
w\
Рис. 1.8. Просмотр лежащего в основе CIL-кода
/7" Metalnfo
ьь'щйш
Method 01 @6000003)
HethodName
Flags
RUfl
ImplFlags
CallCnuntn
hasThis
ReturnType
2 Arguments
Argument 01:
Argument 02:
2 Parameters
A) ParamToken
B) ParamToken
Method 02 @6000004)
Add @6000003)
[Public] [HideBySig] [ReuseSlot]
0x00002090
[IL] [Managed] @0000000)
[DEFAULT]
14
H
@8000001) Mame : x Flags: [none] @0000
@8000002) Name : у Flags: [none] @0000
.ctor @6000004)
Рис. 1.9. Просмотр метаданных типов с помощью ildasm.exe
/7 MANIFEST
Find Find Next
// Metadata version: u4.0.3 0128
assembly extern mscorlib
< i=|
.publickeytoken - (B7 7A 5C 56 19 34 EO 89 )
.ver 4:8:0:0 —
}
.assembly Calc
<
.custom instance uoid [mscorlib]System.Runtime.CompilerSeruice<
.custom instance uoid [mscorlib]System.Runtime.CompilerSeruice* -
Рис. 1.10. Просмотр данных манифеста с помощью ildasm.exe
Несомненно, утилита ildasm. ехе обладает большим, чем было показано здесь
количеством функциональных возможностей; все остальные функциональные возможности
этой утилиты будут демонстрироваться позже в книге при рассмотрении
соответствующих аспектов.
Изучение сборки с помощью утилиты Reflector
Хотя утилита ildasm.exe и применяется очень часто для просмотра деталей
двоичного файла .NET, одним из ее недостатков является то, что она позволяет
просматривать только лежащий в основе CIL-код, но не реализацию сборки с использованием
76 Часть I. Общие сведения о языке С# и платформе .NET
предпочитаемого управляемого языка. К счастью, в Интернете для загрузки доступно
множество других утилит для просмотра и декомпиляции объектов .NET, в том числе и
популярная утилита Reflector.
Эта утилита распространяется бесплатно и доступна по адресу http://www.
red-gate.com/products/reflector. После распаковки из ZIP-архива ее можно
запускать и подключать к любой представляющей интерес сборке, выбирая в меню File
(Файл) команду Open (Открыть). На рис. 1.11 показано ее применение на примере
приложения Calc.exe.
Рис. 1.11. Утилита Reflector является очень популярной программой для просмотра объектов
Важно обратить внимание на то, что в утилите reflector.exe поддерживается
окно Dissembler (Дизассемблер), которое можно открыть нажатием клавиши пробела, а
также элемент раскрывающегося списка, который позволяет просматривать лежащую в
основе кодовую базу на желаемом языке (разумеется, в том числе и на CIL).
Остальные интригующие функциональные возможности этой утилиты предлагается
изучить самостоятельно.
На заметку! Следует иметь в виду, что в остальной части настоящей книги для иллюстрации
различных концепций будет применяться как утилита ildasm.exe, так и утилита reflector.exe.
Поэтому загрузите утилиту Reflector, если это еще не было сделано.
Развертывание исполняющей среды .NET
Нетрудно догадаться, что сборки .NET могут выполняться только на той машине,
на которой установлена платформа .NET Framework. Для разработчиков программного
обеспечения .NET это не должно оказываться проблемой, поскольку их машина
надлежащим образом конфигурируется еще во время установки распространяемого
бесплатно пакета NET Framework 4.0 SDK (а также таких коммерческих сред для разработки
.NET-приложений, как Visual Studio 2010).
В случае развертывания сборки на компьютере, на котором платформа .NET не была
установлена, сборка запускаться не будет. Для таких ситуаций Microsoft предлагает
специальный установочный пакет dotNetFx4 0_Full_x8 6 .exe, который может бесплатно
Глава 1. Философия .NET 77
поставляться и устанавливаться вместе со специальным программным обеспечением.
Этот пакет доступен для загрузки на сайте Microsoft в общем разделе загружаемых
продуктов (http: //www.microsoft. com/downloads).
После установки пакета dotNetFx40_Full_x86.exe на целевой машине появятся
необходимые библиотеки базовых классов .NET, исполняющая среда .NET (mscoree.dll)
и дополнительная инфраструктура .NET (такая как GAC).
На заметку! Операционные системы Windows Vista и Windows 7 изначально сконфигурированы с
необходимой инфраструктурой исполняющей среды .NET. В случае развертывания приложения
в среде какой-то другой операционной системы производства Microsoft, например, Windows XP,
нужно будет позаботиться об установке и настройке на целевой машине среды .NET.
Клиентский профиль исполняющей среды .NET
Установочная программа dotNetFx4 0_Full_x8 6.exe имеет объем примерно
77 Мбайт. Если для конечного пользователя обеспечивается возможность развертывать
приложение с компакт-диска, это не будет представлять проблемы, поскольку
установочная программа сможет просто запускать исполняемый файл тогда, когда машина не
сконфигурирована надлежащим образом.
Если пользователь должен будет загружать dotNetFx4 0_Full_x8 6 .exe по
медленному соединению с Интернетом, ситуация несколько усложняется. Для разрешения
подобной проблемы в Microsoft разработали альтернативную установочную программу —
так называемый клиентский профиль (dotNetFx40_Client_x8 6.exe), который тоже
доступен для бесплатной загрузки на сайте Microsoft.
Эта установочная программа, как не трудно догадаться по ее названию,
предусматривает выполнение установки подмножества библиотек базовых классов .NET в
дополнение к необходимой инфраструктуре исполняющей среды. Поскольку она имеет
гораздо меньший размер (примерно 34 Мбайт), установку тех же самых библиотек, что
появляются при полной установке .NET, на целевой машине она не обеспечивает. При
желании не охватываемые ею сборки могут быть добавлены на целевую машину при
выполнении пользователем обновления Windows (с помощью службы Windows Update).
На заметку! Как у полного, так и у клиентского профиля исполняющей среды имеются
64-разрядные аналоги, которые называются, соответственно, dotNetFx40_Full_x86_x64.exe и
dotNetFx40 Client x86 x64.exe.
Не зависящая от платформы природа .NET
В завершение настоящей главы хотелось бы сказать несколько слов о не зависящей
от платформы природе .NET. К удивлению большинства разработчиков, сборки .NET
могут разрабатываться и выполняться в средах операционных систем производства не
Microsoft, в частности — в Mac OS X, различных дистрибутивах Linux, Solaris, а
также на устройствах типа iPhone производства Apple (через API-интерфейс MonoTbuch).
Чтобы понять, что делает подобное возможным, необходимо рассмотреть еще одну
используемую в мире .NET аббревиатуру — CLI, которая расшифровывается как Common
Language Infrastructure (Общеязыковая инфраструктура).
Вместе с языком программирования С# и платформой .NET в Microsoft был также
разработан набор официальных документов с описанием синтаксиса и семантики
языков С# и CIL, формата сборок .NET, ключевых пространств имен и технических деталей
работы гипотетического механизма исполняющей среды .NET (названного виртуальной
системой выполнения — Virtual Execution System (VES)).
78 Часть I. Общие сведения о языке С# и платформе .NET
Все эти документы были поданы в организацию Ecma International (http: //www.
ecma-international.org) и утверждены в качестве официальных международных
стандартов. Среди них наибольший интерес представляют:
• документ ЕСМА-334, в котором содержится спецификация языка С#;
• документ ЕСМА-335, в котором содержится спецификация общеязыковой
инфраструктуры (CLI).
Важность этих документов становится очевидной с пониманием того факта, что они
предоставляют третьим сторонам возможность создавать дистрибутивы платформы
.NET для любого количества операционных систем и/или процессоров. Среди этих двух
спецификаций документ ЕСМА-335 является более "объемным", причем настолько, что
был разбит на шесть разделов, которые перечислены в табл. 1.4.
Таблица 1.4. Разделы спецификации CLI
Разделы документа п
ЕСМА-335 Предназначение
Раздел I. Концепции В этом разделе описана общая архитектура CLI, в том числе правила
и архитектура CTS и CLS и технические детали функционирования механизма среды
выполнения .NET
Раздел II. Определение В этом разделе описаны детали метаданных и формат сборок в .NET
метаданных и семантика
Раздел III. Набор В этом разделе описан синтаксис и семантика кода CIL
инструкций CIL
Раздел IV. Профили В этом разделе дается общий обзор тех минимальных и полных биб-
и библиотеки лиотек классов, которые должны поддерживаться в дистрибутиве
.NET
Раздел V. В этом разделе описан формат обмена деталями отладки
Раздел VI. Дополнения В этом разделе представлена коллекция дополнительных и более
конкретных деталей, таких как указания по проектированию библиотек
классов и детали по реализации компилятора CIL
Следует иметь в виду, что в разделе IV (Профили и библиотеки) описан лишь
минимальный набор пространств имен, в которых содержатся ожидаемые от дистрибутива
CLI службы (наподобие коллекций, консольного ввода-вывода, файлового ввода-вывода,
многопоточной обработки, рефлексии, сетевого доступа, ключевых средств защиты и
возможностей для манипулирования XML-данными). Пространства имен, которые
упрощают разработку веб-приложений (ASP.NET), доступ к базам данных (ADO.NET) и
создание настольных приложений с графическим пользовательским интерфейсом (Windows
Forms /Windows Presentation Foundation) в CLI не описаны.
Хорошая новость состоит в том, что в главных дистрибутивах .NET библиотеки CLI
дополняются совместимыми с Microsoft эквивалентами ASP.NET, ADO.NET и Windows
Forms, чтобы предоставлять полнофункциональные платформы для разработки
приложений производственного уровня. На сегодняшний день популярностью пользуются
две основных реализации CLI (помимо самого предлагаемого Microsoft и рассчитанного
на Windows решения). Хотя настоящая книга и посвящена главным образом созданию
.NET-приложений с помощью поставляемого Microsoft дистрибутива .NET, в табл. 1.5
приведена краткая информация касательно проектов Mono и Portable.NET.
Глава 1. Философия .NET 79
Таблица 1.5. Дистрибутивы .NET, распространяемые с открытым исходным кодом
Дистрибутив Описание
http: / /www. mono-pro j ect. com Проект Mono представляет собой распространяемый
с открытым исходным кодом дистрибутив CLI, который
ориентирован на различные версии Linux (например,
openSuSE, Fedora и т.п.), а также Windows и устройства
Mac OS X и iPhone
http: //wwwmdotgnu. org Проект Portable.NET представляет собой еще один
распространяемый с открытым исходным кодом
дистрибутив CLI, который может работать в целом ряде
операционных систем. Он нацелен охватывать как
можно больше операционных систем (Windows, AIX, BeOS,
Mac OS X, Solaris и все главные дистрибутивы Linux)
Как в Mono, так и в Portable.NET предоставляется ЕСМА-совместимый
компилятор С#, механизм исполняющей среды .NET, примеры программного кода,
документация, а также многочисленные инструменты для разработки приложений, которые по
своим функциональным возможностям эквивалентны поставляемым в составе .NET
Framework 4.0 SDK. Более того, Mono и Portable.NET поставляются с компиляторами
VB.NET, Java и С.
На заметку! Описание приемов создания межплатформенных .NET-приложений с помощью Mono
можно найти в приложении Б.
Резюме
Целью этой главы было предоставить базовые теоретические сведения, необходимые
для изучения остального материала настоящей книги. Сначала были рассмотрены
ограничения и сложности, которые существовали в технологиях, предшествовавших
появлению .NET, а потом показано, как .NET и С# упрощают существующее положение
вещей.
Главную роль в .NET, по сути, играет механизм выполнения (mscoree . dll) и
библиотека базовых классов (mscorlib.dll вместе с сопутствующими файлами).
Общеязыковая среда выполнения (CLR) способна обслуживать любой двоичный файл .NET
(сборку), который отвечает правилам управляемого программного кода. Как было показано в
этой главе, в каждой сборке (помимо метаданных типов и манифеста) содержатся CIL-
инструкции, которые с помощью JIT-компилятора преобразуются в инструкции,
ориентированные на конкретную платформу. Помимо этого, здесь рассматривалась роль
общеязыковой спецификации (CLS) и общей системы типов (CTS).
После этого было рассказано о таких полезных утилитах для просмотра объектов,
как ildasm.exe и reflector.exe, а также о том, как сконфигурировать машину для
обслуживания приложений .NET с помощью полного и клиентского профилей. И,
наконец, напоследок было вкратце упомянуто о преимуществах не зависящей от платформы
природы С# и .NET, о чем более подробно пойдет речь в приложении Б.
ГЛАВА 2
Создание приложений
на языке С#
Программисту, использующему язык С#, для разработки .NET-приложений на
выбор доступно много инструментов. Целью этой главы является совершение
краткого обзорного тура по различным доступным средствам для разработки .NET-
приложений, в том числе, конечно же, Visual Studio 2010. Глава начинается с рассказа
о том, как работать с компилятором командной строки С# (esc. exe), и самым
простейшим из всех текстовых редакторов Notepad (Блокнот), который входит в состав
операционной системы Microsoft Windows, а также приложением Notepad++, доступным для
бесплатной загрузки.
Хотя для изучения приведенного в настоящей книге материала вполне хватило бы
компилятора csc.exe и простейшего текстового редактора, читателя наверняка
заинтересует использование многофункциональных интегрированных сред разработки
(Integrated Development Environment — IDE). В этой главе также описана бесплатная
IDE-среда с открытым исходным кодом SharpDevelop, предназначенная для
разработки приложений .NET. Как будет показано, по своим функциональным возможностям эта
IDE-среда не уступает многим коммерческим аналогам. Кроме того, в главе кратко
рассматривается IDE-среда Visual C# 2010 Express (распространяемая бесплатно), а также
ключевая функциональность Visual Studio 2010.
На заметку! В ходе этой главы будут встречаться синтаксические конструкции С#, которые пока
еще не рассматривались. Официальное изучение языка С# начнется в главе 3
Роль комплекта .NET Framework 4.0 SDK
Одним из мифов в области разработки .NET-приложений является то, что
программистам якобы обязательно требуется приобретать копию Visual Studio для того,
чтобы разрабатывать программы на С#. На самом деле, создавать .NET-программу любого
рода можно с помощью распространяемого бесплатно и доступного для загрузки
комплекта инструментов для разработки программного обеспечения .NET Framework 4.0
SDK (Software Development Kit). В этом пакете поставляются многочисленные
управляемые компиляторы, утилиты командной строки, примеры кода, библиотеки классов .NET
и полная справочная система.
На заметку! Программа установки .NET Framework 4.0 SDK (dotnetf x4 Of ullsetup. exe)
доступна на странице загрузки .NET по адресу http: //msdn.microsoft.com/netframework.
Глава 2. Создание приложений на языке С# 81
Тем, кто планирует использовать Visual Studio 2010 или Visual C# 2010 Express,
следует иметь в виду, что в установке .NET Framework 4.0 SDK нет никакой необходимости.
При установке любого из упомянутых продуктов этот пакет SDK устанавливается
автоматически и сразу же предоставляет все необходимое.
Если использование IDE-среды от Microsoft для проработки материала настоящей
книги не планируется, обязательно установите .NET Framework 4.0 SDK, прежде чем
двигаться дальше.
Окно командной строки в Visual Studio 2010
При установке .NET Framework 4.0 SDK, Visual Studio 2010 или Visual C# 2010
Express на локальном жестком диске создается набор новых каталогов, в каждом из
которых содержатся разнообразные инструменты для разработки .NET-приложений.
Многие из этих инструментов работают в режиме командной строки, и чтобы
использовать их в любом каталоге, нужно сначала соответствующим образом зарегистрировать
пути к ним в операционной системе.
Для этого можно обновить переменную среды PATH вручную, но лучше пользоваться
предлагаемым в Visual Studio окном командной строки (Command Prompt). Чтобы
открыть это окно (рис. 2.1), необходимо выбрать в меню Start (Пуск) пункт All Programs^
Microsoft Visual Studio 2010^Visual Studio Tools (Все программы^ Microsoft Visual Studio
20Ю1^Инструменты Visual Studio).
[Setting environment for using Microsoft Visual Studio 2010 x86 tools.
C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC>
Рис. 2.1. Окно командной строки в Visual Studio 2010
Преимущество применения именно этого окна командной строки связано с тем,
что оно уже сконфигурировано на предоставление доступа к каждому из инструментов
для разработки .NET-приложений. При условии, что на компьютере развернута среда
разработки .NET, можно попробовать ввести следующую команду и нажать клавишу
<Enter>:
esc -?
Если все в порядке, появится список аргументов командной строки, которые
может принимать работающий в режиме командной строки компилятор С# (esc означает
C-sharp compiler].
Создание приложений на С#
с использованием esc. ехе
В действительности необходимость в создании крупных приложений с
использованием одного лишь компилятора командной строки С# может никогда не возникнуть,
тем не менее, важно понимать в общем, как вручную компилировать файлы кода.
Существует несколько причин, по которым освоение этого процесса может оказаться
полезным.
Часть I. Общие сведения о языке С# и платформе .NET
Самой очевидной причиной является отсутствие Visual Studio 2010 или какой-то
другой графической IDE-среды.
Работа может выполняться в университете, где использование инструментов для
генерации кода и IDE-сред обычно запрещено.
Планируется применение автоматизированных средств разработки, таких как
msbuild. ехе, которые требуют знать опции командной строки для используемых
инструментов.
Возникло желание углубить свои познания в С#. В графических IDE-средах в
конечном итоге все заканчивается предоставлением компилятору esc. ехе
инструкций относительно того, что следует делать с входными файлами кода С#. В этом
отношении изучение происходящего "за кулисами'' позволяет получить
необходимые знания.
Еще одно преимущество подхода с использованием одного лишь компилятора
esc. ехе состоит в том, что он позволяет обрести навыки и чувствовать себя более
уверенно при работе с другими инструментами командной строки, входящими в состав
.NET Framework 4.0 SDK. Как будет показано далее, целый ряд важных утилит работает
исключительно в режиме командной строки.
Чтобы посмотреть, как создавать .NET-приложение без IDE-среды, давайте
построим с помощью компилятора С# и текстового редактора Notepad простую
исполняемую сборку по имени TestApp.exe. Сначала необходимо подготовить
исходный код. Откройте программу Notepad (Блокнот), выбрав в меню Start (Пуск) пункт
All Programs1^ Accessories1^ Notepad (Все программы ^Стандартные ^Блокнот), и
введите следующее типичное определение класса на С#:
// Простое приложение на языке С#.
using System;
class TestApp
{
static void Main()
{
Console.WriteLine ("Testing! 1, 2, 3");
}
}
После окончания ввода сохраните файл (например, в каталоге С: \CscExample) под
именем TestApp. cs. Теперь давайте ознакомимся с ключевыми опциями компилятора С#.
На заметку! По соглашению всем файлам с кодом на С# назначается расширение * .cs. Имя
файла не нуждается в специальном отображении на имя какого-либо типа или типов.
Указание целевых входных и выходных параметров
Первым делом важно разобраться с тем, как указывать имя и тип создаваемой
сборки (т.е., например, консольное приложение по имени MyShell.exe, библиотека кода
по имени MathLib.dll или приложение Windows Presentation Foundation по имени
Halo8.exe). Каждый из возможных вариантов имеет соответствующий флаг, который
нужно передать компилятору esc. ехе в виде параметра командной строки (табл. 2.1).
На заметку! Параметры, передаваемые компилятору командной строки (а также большинству
других утилит командной строки), могут сопровождаться префиксом в виде символа дефиса (-)
или символа косой черты (/).
Глава 2. Создание приложений на языке С# 83
Таблица 2.1. Выходные параметры, которые может принимать компилятор С#
Параметр
Описание
/out
/target:exe
/target:library
/targetrmodule
/target:winexe
Этот параметр применяется для указания имени создаваемой
сборки По умолчанию сборке присваивается то же имя, что у входного
файла *. cs
Этот параметр позволяет создавать исполняемое консольное
приложение. Сборка такого типа генерируется по умолчанию, потому при
создании подобного приложения данный параметр можно опускать
Этот параметр позволяет создавать однофайловую сборку * . dll
Этот параметр позволяет создавать модуль. Модули являются
элементами многофайловых сборок (и будут более подробно
рассматриваться в главе 14)
Хотя приложения с графическим пользовательским интерфейсом
можно создавать с применением параметра /target: exe, параметр
/target: winexe позволяет предотвратить открытие окна консоли
под остальными окнами
Чтобы скомпилировать TestApp. cs в консольное приложение TextApp.exe,
перейдите в каталог, в котором был сохранен файл исходного кода:
cd C:\CscExample
Введите следующую команду (обратите внимание, что флаги должны обязательно
идти перед именем входных файлов, а не после):
esc /target:exe TestApp.cs
Здесь флаг /out не был указан явным образом, поэтому исполняемый файл получит
имя TestApp. exe из-за того, что именем входного файла является TestApp. Кроме того,
для почти всех принимаемых компилятором С# флагов поддерживаются сокращенные
версии написания, наподобие /t вместо /target (полный список которых можно
увидеть, введя в командной строке команду esc -?).
esc /t:exe TestApp.cs
Более того, поскольку флаг /t: exe используется компилятором как выходной
параметр по умолчанию, скомпилировать TestApp. cs также можно с помощью следующей
простой команды:
esc TestApp.cs
Теперь можно попробовать запустить приложение TestApp . exe из командной
строки, введя имя его исполняемого файла, как показано на рис. 2.2.
J Administrator Visual Studio Command Prompt
'■*»«*' *'*" »■ ■-, f-
t: \CscExamp1 e>TestApp.exe
[Testing! 1, 2, 3
]C:\CscExamp1e>
i
Рис. 2.2. Приложение TestApp в действии
84 Часть I. Общие сведения о языке С# и платформе .NET
Добавление ссылок на внешние сборки
Давайте посмотрим, как скомпилировать приложение, в котором используются
типы, определенные в отдельной сборке .NET. Если осталось неясным, каким образом
компилятору С# удалось понять ссылку на тип System. Console, вспомните из главы 1,
что во время процесса компиляции происходит автоматическое добавление ссылки на
mscorlib.dll (если по какой-то необычной причине нужно отключить эту функцию,
следует передать компилятору csc.exe параметр /nostdlib).
Модифицируем приложение TestApp так, чтобы в нем открывалось окно
сообщения Windows Forms. Для этого откройте файл TestApp. cs и измените его следующим
образом:
using System;
// Добавить эту строку:
using System.Windows.Forms;
class TestApp
{
static void Main()
{
Console.WriteLine("Testing! 1, 2, 3");
// И добавить эту строку:
MessageBox.Show("Hello...");
Рис. 2.3. Первое
приложение
Windows Forms
}
Обратите внимание на импорт пространства имен System.
Windows .Forms с помощью поддерживаемого в С# ключевого
слова using (о котором рассказывалось в главе 1). Вспомните, что
явное перечисление пространств имен, которые используются внутри
файла * .cs, позволяет избегать необходимости указывать
полностью уточненные имена типов.
Далее в командной строке нужно проинформировать компилятор
esc. ехе о том, в какой сборке содержатся используемые
пространства имен. Поскольку применялся класс MessageBox из
пространства имен System. Windows . Forms, значит, нужно указать
компилятору на сборку System.Windows .Forms .dll, что делается с помощью
флага /reference (или его сокращенной версии /г):
esc /r:System.Windows.Forms.dll TestApp.cs
Если теперь снова попробовать запустить приложение, то помимо консольного
вывода в нем должно появиться еще и окно с сообщением, как показано на рис. 2.3.
Добавление ссылок на несколько внешних сборок
Кстати, как поступить, когда необходимо указать esc. ехе несколько внешних
сборок? Для этого нужно просто перечислить все сборки через точку с запятой. В
рассматриваемом примере ссылаться на несколько сборок не требуется, но ниже приведена
команда, которая иллюстрирует перечисление множества сборок:
esc /r: System. Windows . Forms ,dll;System. Drawmg.dll *.cs
На заметку! Как будет показано позже в настоящей главе, компилятор С# автоматически
добавляет ссылки на ряд ключевых сборок .NET (таких как System. Windows . Forms . dll), даже
если они не указаны с помощью флага /г.
Глава 2. Создание приложений на языке С# 85
Компиляция нескольких файлов исходного кода
В текущем примере приложение TestApp. exe создавалось с использованием
единственного файла исходного кода * . cs. Хотя определять все типы .NET в одном файле * . cs
вполне допустимо, в большинстве случаев проекты формируются из нескольких файлов
*. cs для придания кодовой базе большей гибкости. Чтобы стало понятнее, давайте
создадим новый класс и сохраним его в отдельном файле по имени HelloMsg. cs.
// Класс HelloMessage
using System;
using System.Windows.Forms;
class HelloMessage
{
public void Speak()
{
MessageBox.Show("Hello...");
}
}
Изменим исходный класс TestApp так, чтобы в нем использовался класс этого
нового типа, и закомментируем прежнюю логику Windows Forms:
using System;
// Эта строка больше не нужна:
// using System.Windows.Forms;
class TestApp
{
static void Mam ()
{
Console .WriteLme ("Testing ' 1, 2, 3");
// Эта строка тоже больше не нужна:
// MessageBox.Show("Hello...");
// Используем класс HelloMessage:
HelloMessage h = new HelloMessage();
h. Speak () ;
}
}
Чтобы скомпилировать файлы исходного кода на С# , необходимо их явно
перечислить как входные файлы:
esc /r:System.Windows.Forms.dll TestApp.cs HelloMsg.cs
В качестве альтернативного варианта компилятор С# позволяет использовать
групповой символ (*) для включения в текущую сборку всех файлов * .cs, которые
содержатся в каталоге проекта:
esc /г:System.Windows.Forms.dll *.cs
Вывод, получаемый после запуска этой программы, идентичен предыдущей
программе. Единственное отличие между этими двумя приложениями связано с
разнесением логики по нескольким файлам.
Работа с ответными файлами в С#
Как не трудно догадаться, для создания сложного приложения С# из командной
строки потребовалось бы вводить утомительное количество входных параметров для
уведомления компилятора о том, как он должен обрабатывать исходный код. Для облег-
86 Часть I. Общие сведения о языке С# и платформе .NET
чения этой задачи в компиляторе С# поддерживается использование так называемых
ответных файлов (response files).
В ответных файлах С# размещаются все инструкции, которые должны
использоваться в процессе компиляции текущей сборки. По соглашению эти файлы имеют
расширение * . rsp (сокращение от response— ответ). Чтобы посмотреть на них в действии,
давайте создадим ответный файл по имени TestApp. rsp, содержа ищи следующие
аргументы (комментарии в данном случае обозначаются символом #):
# Это ответный файл для примера
# TestApp.exe из главы 2.
# Ссылки на внешние сборки:
/г:System.Windows.Forms.dll
# Параметры вывода и подлежащие компиляции файлы
# (здесь используется групповой символ):
/target:exe /out:TestApp.exe *.cs
Теперь при условии сохранения данного файла в том же каталоге, где находятся
подлежащие компиляции файлы исходного кода на С#, все приложение можно будет
создать следующим образом (обратите внимание на применение символа @):
esc @TestApp.rsp
В случае необходимости допускается также указывать и несколько ответных
* . rsp файлов в качестве входных параметров (например, esc @FirstFile.rsp
@SecondFile . rsp @ThirdFile . rsp). При таком подходе, однако, следует иметь в
виду, что компилятор обрабатывает параметры команд по мере их поступления.
Следовательно, аргументы командной строки, содержащиеся в поступающем позже
файле * . rsp, могут переопределять параметры из предыдущего ответного файла.
Еще важно обратить внимание на то, что все флаги, перечисляемые явным образом
перед ответным файлом, будут переопределяться настройками, которые содержатся в
этом файле. То есть в случае ввода следующей команды:
esc /outiMyCoolApp.exe @TestApp.rsp
имя сборки будет по-прежнему выглядеть как TestApp. exe (а не MyCoolApp. exe) из-за
того, что в ответном файле TestApp. rsp содержится флаг /out: TestApp. exe. В
случае перечисления флагов после ответного файла они будут переопределять настройки,
содержащиеся в этом файле.
На заметку! Действие флага /reference является кумулятивным. Где бы не указывались
внешние сборки (перед, после или внутри ответного файла), в конечном итоге каждая из них все
равно будет добавляться к остальным.
Используемый по умолчанию ответный файл (esc. rsp)
Последним моментом, связанным с ответными файлами, о котором необходимо
упомянуть, является то, что с компилятором С# ассоциирован ответный файл esc. rsp,
который используется по умолчанию и размещен в том же самом каталоге, что и файл
esc .exe (обычно это С: \Windows\Microsof t .NET\Framework\<BepcMH>, где на месте
элемента <версия> идет номер конкретной версии платформы). Открыв файл esc. rsp
в программе Notepad (Блокнот), можно увидеть, что в нем с помощью флага /г: указано
множество сборок .NET, в том числе различные библиотеки для разработки
веб-приложений, программирования с использованием технологии LINQ и обеспечения доступа к
данным и прочие ключевые библиотеки (помимо, конечно же, самой главной
библиотеки mscorlib.dll).
Глава 2. Создание приложений на языке С# 87
При создании программ на С# с применением с~.с . ехе ссылка на этот ответный файл
добавляется автоматически, даже когда указан специальный файл * . rsp. Из-за
наличия такого ответного файла по умолчанию, рассматриваемое приложение TestApp. ехе
можно скомпилировать и помощью следующей команды (поскольку в esc . rsp уже
содержится ссылка на System. Windows . Forms . dll):
esc /out: TestApp .ехе *.cr.
Для отключения функции автоматического чтения файла esc. rsp укажите опцию
/noconfig:
esc @TestApp.rsp /noconfig
На заметку! В случае добавления (с помощью опции /г) ссылок на сборки, которые на самом деле
не используются, компилятор их проигнорирует Поэтому беспокоиться по поводу "разбухания
кода" не нужно.
Понятно, что у компилятора командной строки С# имеется множество других
параметров, которые можно применять для управления генерацией результирующей сборки
.NET. Другие важные возможности будут демонстрироваться по мере необходимости
далее в книге, а полные сведения об этих параметрах можно всегда найти в документации
.NET Framework 4.0 SDK
Исходный код. Код приложения CscExample доступен в подкаталоге Chapter 2.
Создание приложений .NET
с использованием Notepad++
Еще одним текстовым редактором, о котором следует кратко упомянуть, является
распространяемое с открытым исходным кодом бесплатное приложение Notepad++.
Загрузить его можно по адресу http: //notepad-plus . sourceforge .net/. В отличие
от простого редактора Notepad (Блокнот), поставляемого в составе Windows,
приложение Notepad++ позволяет создавать код на множестве различных языков и
поддерживает установку разнообразных дополнительных подключаемых модулей. Помимо этого,
Notepad++ обладает рядом других замечательных достоинств, в том числе:
• изначальной поддержкой для использования ключевых слов С# (и их кодирования
цветом включительно);
• поддержкой для свертывания синтаксиса (syntax folding), позволяющей
сворачивать и разворачивать группы операторов в коде внутри редактора (и подобной
той, что предлагается в Visual Studio 2010/C# 2010 Express);
• возможностью увеличивать и уменьшать масштаб отображения текста с
помощью колесика мыши (имитирующего действие клавиши <Ctrl>);
• настраиваемой функцией автоматического завершения (autocompletion)
различных ключевых слов С# и названий пространств имен .NET.
Чтобы активизировать поддержку функции автоматического завершения кода на С#
(рис. 2.4), необходимо одновременно нажать клавиши <Ctrl> и пробела.
На заметку! Список вариантов, предлагаемых для автоматического завершения кода в
отображаемом окне, можно изменять и расширять. Для этого необходимо открыть файл С: \Program
Files\Notepad++\plugms\APIs\cs . xml для редактирования и добавить в него любые
дополнительные записи.
88 Часть I. Общие сведения о языке С# и платформе .NET
t:\CscE.4antpleNHellcMjg.« - Ncte^d - ♦
' File Edit Search View Format Language Settings Macro Run TextFX Plugins Window ?
o..|H^oe&l4'tufcl*ci|#lk|4t.|- s 1. 1 .. ♦
Ы HetoMag cs |
i!li|X><
• // Tbe HelloMessage class
using System;
using System.Windows.Forms;
using
clas;
3<
й с
System.Web.Configuration
System.Web.Hosting
System.Web.Mail
I
System.Web.Services
C* 148 chars 172 bytes 12 lines
Ln:4 Col: 7 Sel: 0 @ bytes) in 0 ranges
Dos\Windows ANSI
Рис. 2.4. Использование функции автоматического завершения кода в Notepad++
Более подробно о приложении Notepad++ здесь рассказываться не будет. Чтобы узнать
о нем больше, воспользуйтесь предлагаемым в его меню ? пунктом Help (Справка).
Создание приложений.NET
с помощью SharpDevelop
Нельзя не согласиться с тем, что написание кода С# в приложении Notepad++,
несомненно, является шагом в правильном направлении по сравнению с использованием
редактора Notepad (Блокнот) и командной строки. Тем не менее, в Notepad++
отсутствуют богатые возможности IntelliSense, визуальные конструкторы для построения
графических пользовательских интерфейсов, шаблоны проектов, инструменты для работы с
базами данных и многое другое. Для удовлетворения перечисленных потребностей
больше подходит такой рассматриваемый далее продукт для разработки .NET-приложений,
как SharpDevelop (также называемый #Develop).
Продукт SharpDevelop представляет собой распространяемую с открытым исходным
кодом многофункциональную IDE-среду, которую можно применять для создания .NET-
сборок с помощью С# ,VB, CIL, а также Python-образного .NET-языка под названием
Boo. Помимо того, что эта IDE-среда предлагается совершенно бесплатно, интересно
обратить внимание на тот факт, что она сама реализована полностью на С#. Для
установки среды SharpDevelop необходимо загрузить и скомпилировать ее файлы * . cs
вручную или запустить готовую программу setup. ехе. Оба дистрибутива доступны по
адресу http://www.sharpdevelop.com/.
IDE-среда SharpDevelop обладает массой достоинств в плане улучшения
продуктивности. Наиболее важными из них являются:
• поддержка для множества языков и типов проектов .NET;
• функция IntelliSense, завершение кода и возможность использования только
определенных фрагментов кода;
• диалоговое окно Add Reference (Добавление ссылки), позволяющее легко
добавлять ссылки на внешние сборки, в том числе и те, что находятся в глобальном
кэше сборок (Global Assembly Cache — GAC);
• визуальный конструктор Windows Forms;
• встроенные утилиты для просмотра объектов и определения кода;
• визуальные утилиты для проектирования баз данных;
• утилита для преобразования кода на С# в код на VB (и наоборот).
Глава 2. Создание приложений на языке С# 89
Впечатляюще для бесплатной IDE-среды, не так ли? Ниже кратко рассматриваются
некоторые наиболее интересные достоинства из перечисленных выше.
На заметку! На момент написания книги в текущей версии SharpDevelop пока не поддерживались
средства С# 2010 / .NET 4.0. Периодически заглядывайте на веб-сайт SharpDevelop, чтобы
проверить, не появились ли следующие выпуски среды.
Создание простого тестового проекта
После установки SharpDevelop за счет выбора в меню File (Файл) пункта New^Solution
(Создать1^ Решение) можно указывать, какой тип проекта требуется сгенерировать
(и на каком языке .NET). Например, предположим, что нужно создать проект по имени
MySDWinApp типа Windows Application (Приложение Windows) на языке С# (рис. 2.5).
Рис. 2.5. Диалоговое окно создания нового проекта в SharpDevelop
Как и в Visual Studio, в SharpDevelop предлагается окно элементов управления для
конструктора графических пользовательских интерфейсов Windows Forms (позволяющее
перетаскивать элементы управления на поверхность конструктора) и окно Properties
(Свойства), позволяющее настраивать внешний вид и поведение каждого из элементов
графического пользовательского интерфейса. На рис. 2.6 показан пример настройки
элемента управления Button (Кнопка); обратите внимание, что для этого был
выполнен щелчок на вкладке Design (Конструктор), отображаемой в нижней части открытого
файла кода.
После щелчка на вкладке Source (Исходный код) в нижней части окна конструктора
форм, как не трудно догадаться, будет предлагаться функция IntelliSense, функция
завершения кода и встроенная справка (рис. 2.7).
В дизайне SharpDevelop имитируются многие функциональные возможности,
которые предоставляются в IDE-средах .NET производства Microsoft (и о которых пойдет
речь далее). Поэтому более подробно здесь эта IDE-среда SharpDevelop
рассматриваться не будет. Для получения дополнительной информации можно воспользоваться меню
Help (Справка).
90 Часть I. Общие сведения о языке С# и платформе .NET
) MySDWinApp - SharpDevelop
шшш
£ile £drt tfew Rroject
Debug Search Fojmat Tools Window Help
*? C* : Ш Jtt ■ ► ! I Default layout - J
Tools
"ft Pointer
| |Э Button
pCheckBcw
FfComboBox
Ia Label
l0Ra<*©Button
_W TextBox
pSCheckedUstBox
И]$ DateTimePicker
. DomainUpDown
Q FlowLayoutPenel
^.(GfOupBcor
П HScrollBsr
Д LinkLabeJ
Data
Ж
Custom Con
[53 Projects~|JJjTools I
^йЫНхтл? I
f
| * MySDWmApp
ШШ1
Sour» | Desen
(Output
v Debug
~"*[ll"
;; button 1 System Windows Forms Button
i|ELi|E.,r 1Л
|f ImageAfcon MddtoCenter
Imagehdex fane)
; knegeKey fione)
Imagebst (none)
Д X FlightToLeft No
j fiSHHHHHI Click Me! [*1
•' i Textron MiddleCenter
: The text associated w4h the control.
^ Errors [I] Output pTTaskLirt |ШDefinition View |
l^j*Properties f^Classes|
Inl coll chl
Рис. 2.6. Конструирование приложения типа Windows Forms графическим образом в SharpDevelop
•^MySDWinApp MamForm
v ♦MainFormi)
;
)
// 1000: Add constructor cede after the InitializeCqrrpc
//
this.I
'АНолТгагврагегсу
'Anchor
^♦ApplyAutoScaling
"•AutoScale
'AutoScaieBaseSze
'AutoScaleDimensiors
'AutoScaJeF actor
[public vlrtuaTFool AutoScrolf"
iets or sets a value indicating whether the form enables autoscrolling
leturns: true to enable autoscroinq on the form; otherwise, false The default is false.
Source fbeegn
Рис. 2.7. В SharpDevelop поддерживается много утилит для генерации кода
Создание приложений .NET с использованием
Visual С# 2010 Express
Летом 2004 г. компания Microsoft представила совершенно новую линейку IDE-сред
по общим названием "Express" (http: //mscin.microsoft.com/express). На
сегодняшний день на рынке предлагается несколько членов этого семейства (все они
распространяются бесплатно и поддерживаются и обслуживаются компанией Microsoft).
• Visual Web Developer 2010 Express. "Облегченный" инструмент для разработки
динамических веб-сайтов ASP.NET и служб WCF.
• Visual Basic 2010 Express. Упрощенный инструмент для программирования,
идеально подходящий для программистов-новичков, которые хотят научиться
создавать приложения с применением дружественного к пользователям синтаксиса
Visual Basic.
Глава 2. Создание приложений на языке С# 91
• Visual C# 2010 IZxpress и Visual C++ 2010 Express. IDE-среды, ориентированные
специально на студентов и всех прочих желающих обучиться основам
программирования с использованием предпочитаемого синтаксиса.
• SQL Server Express. Система для управления базами данных начального уровня,
предназначенная для любителей, энтузиастов и учащихся-разработчиков.
Некоторые уникальные функциональные
возможности Visual C# 2010 Express
В целом продукты линейки Express представляют собой усеченные версии своих
полнофункциональных аналогов в линейке Visual Studio 2010 и ориентированы
главным образом на любителей .NET и занимающихся изучением .NET студентов. Как и в
SharpDevelop, в Visual C# 2010 Express предлагаются разнообразные инструменты для
просмотра объектов, визуальный конструктор Windows Forms, диалоговое окно Add
References (Добавление ссылок), возможности IntelliSense и шаблоны для расширения
программного кода.
Помимо этого в Visual C# 2010 Express доступно несколько (важных)
функциональных возможностей, которые в SharpDevelop в настоящее время отсутствуют. К их числу
относятся:
• развитая поддержка для создания приложений Window Presentation Foundation
(WPF) с помощью XAML;
• функция IntelliSense для новых синтаксических конструкций С# 2010, в том числе
именованных аргументов и необязательных параметров;
• возможность загружать дополнительные шаблоны, позволяющие разрабатывать
приложения ХЬох 360, приложения WPF с интеграцией Twitter, и многое другое.
На рис. 2.8 показан пример, иллюстрирующий создание с помощью Visual C# Express
XAML-разметки для проекта WPF
: Common WPF Controls
;АВ WPF Centre*
There ire no usable controls in this 91
Drag in item onto this text to «dd it ti
;jjf Background
О Binding
jf BrndingGroop
■^ Brt/rupEffect
|3f BitmapEffectlnput
:-ff BofdetBrush
■;3f BorderThidtness
:3f CacheMode
I d Design ■ fl .BXAMl |
'-'<Uindo»i x:Class-*MpfApplicatienl.n1^ Calendar
>:»Jni--rittp://5criejMS.iiicr / Click
%alnr.:K'"bttp://schemes.*i!tf[ ClickMode
Titlt'-'MainWindow" Hcigtit-jjji <£_
^ <Grid>
<8utton (■.:ri,w*«"«ybutt<
_ </Wirtdow>
f
■■
U-, ji
щ 1!
JEEEBL
;.3 Solution WpfApplicBtionl' A project)
j 7! Wpf Application 1
it Properties
i' сЛ References
t> M Appjcaml
> '»- MainWindowjtaml
100% -j« ^
InS Grid 'A.noWGnd i
Рис. 2.8. Visual C# Express обладает встроенной поддержкой API-интерфейсов .NET 4.0
92 Часть I. Общие сведения о языке С# и платформе .NET
Поскольку по внешнему виду и поведению IDE-среда Visual C# 2010 Express очень
похожа на Visual Studio 2010 (и, в некоторой степени, на SharpDevelop), более подробно
она здесь рассматриваться не будет. Ее вполне допустимо использовать для
проработки дальнейшего материала книги, но при этом обязательно следует иметь в виду, что
в ней не поддерживаются шаблоны проектов для создания веб-сайтов ASP.NET. Чтобы
иметь возможность строить веб-приложения, необходимо загрузить продукт Visual
Web Developer 2010, который также доступен на сайте http://msdn.microsoft.com/
express.
Создание приложений .NET
с использованием Visual Studio 2010
Профессиональные разработчики программного обеспечения .NET наверняка
располагают самым серьезным в этой сфере продуктом производства Microsoft, который
называется Visual Studio 2010 и доступен по адресу http : //msdn .microsoft. com/
vstudio. Этот продукт представляет собой самую функционально насыщенную и
наиболее приспособленную под использование на предприятиях IDE-среду из всех,
что рассматривались в настоящей главе. Такая мощь, несомненно, имеет свою цену,
которая варьируется в зависимости от версии Visual Studio 2010. Как не трудно
догадаться, каждая версия поставляется со своим уникальным набором функциональных
возможностей.
На заметку! Количество версий в семействе продуктов Visual Studio 2010 очень велико. В
оставшейся части книги предполагается, что в качестве предпочитаемой IDE-среды используется
версия Visual Studio 2010 Professional.
Хотя далее предполагается наличие копии Visual Studio 2010 Professional, это вовсе
не обязательно для проработки излагаемого в настоящей книге материала. В худшем
случае может встретиться описание опции, которая отсутствует в используемой IDE-
среде. Однако весь приведенный в книге код будет прекрасно компилироваться, какой
бы инструмент не применялся.
На заметку! После загрузки кода для настоящей книги можно открывать любой пример в Visual
Studio 2010 (или Visual C# 2010 Express), дважды щелкая на соответствующем файле решения
* . sin. Если на машине не установлено ни Visual Studio 2010, ни С#20010 Express, файлы * . cs
потребуется вставлять вручную в рабочую область проекта внутри используемой IDE-среды.
Некоторые уникальные функциональные
возможности Visual Studio 2010
Как не трудно догадаться, Visual Studio 2010 также поставляется с графическими
конструкторами, поддержкой использования отдельных фрагментов кода, средствами
для работы с базами данных, утилитами для просмотра объектов и проектов, а
также встроенной справочной системой. Но, в отличие от многих из уже рассмотренных
IDE-сред, в Visual Studio 2010 предлагается множество дополнительных возможностей,
наиболее важные из которых перечислены ниже:
• графические редакторы и конструкторы XML;
• поддержка разработки программ Windows, ориентированных на мобильные
устройства;
Глава 2. Создание приложений на языке С# 93
• поддержка разработки программ Microsoft Office;
• поддержка разработки проектов Windows Workflow Foundation;
• встроенная поддержка рефакторинга кода;
• инструменты визуального конструирования классов.
По правде говоря, в Visual Studio 2010 предлагается настолько много возможностей,
что для их полного описания понадобилась бы отдельная книга. В настоящей книге
такие цели не преследуются. Тем не менее, в нескольких следующих разделах наиболее
важные функциональные возможности рассматриваются чуть подробнее, а другие по
мере необходимости будут описаны далее в книге.
Ориентирование на .NET Framework в диалоговом окне New Project
Те, кто следует указаниям этой главы, сейчас могут попробовать создать новое
консольное приложение на С# (по имени Vs2010Example), выбрав в меню File (Файл) пункт
New^Project (Создать^Проект). Как можно увидеть на рис. 2.9, в Visual Studio 2010
поддерживается возможность выбора версии .NET Framework B.0, 3.x или 4.0), для
которой должно создаваться приложение, с помощью раскрывающегося списка,
отображаемого в правом верхнем углу диалогового окна New Project (Новый проект). Для всех
описываемых в настоящей книге проектов, в этом списке можно оставлять выбранным
предлагаемый по умолчанию вариант .NET Framework 4.0.
Рис. 2.9. В Visual Studio 2010 позволяется выбирать определенную целевую
версию .NET Framework
Использование утилиты Solution Explorer
Утилита Solution Explorer (Проводник решений), доступная через меню View (Вид),
позволяет просматривать набор всех файлов с содержимым и ссылаемых сборок,
которые входят в состав текущего проекта (рис. 2.10).
Обратите внимание, что внутри папки References (Ссылки) в окне Solution Explorer
отображается список всех сборок, на которые в проекте были добавлены ссылки. В
зависимости от типа выбираемого проекта и целевой версии .NET Framework, этот список
выглядит по-разному.
94 Часть I. Общие сведения о языке С# и платформе .NET
Solution Explorer
i^g| Solution 'VsiOlOExample' A project)
d 3 Vs2010Examp4e
■ЗА Properties
л ^j References
*0 Microsoft.CSharp
чЭ System
чЗ System. С о re
•a System.Data
<J System.Data.DataSetExtensions
-O System Xm\
*£3 System.Xml.Linq
«^ Program.cs
Рис. 2.10. Окно утилиты Solution Explorer
Добавление ссылок на внешние сборки
Если необходимо сослаться на дополнительные сборки, щелкните на папке References
правой кнопкой мыши и выберите в контекстном меню пункт Add Reference (Добавить
ссылку). После этого откроется диалоговое окно, позволяющее выбрать желаемые сборки
(в Visual Studio это аналог параметра /reference в компиляторе командной строки). На
вкладке .NET этого окна, показанной на рис. 2.11, отображается список наиболее
часто используемых сборок .NET; на вкладке Browse (Обзор) предоставляется возможность
найти сборки .NET, которые находятся на жестком диске; на вкладке Recent (Недавние)
приводится перечень сборок, на которые часто добавлялись ссылки в других проектах.
О Add Reference
COM ''Jp'rajecbj Browte] Recent I
Component Name
System.IdentityModel
System.IdentityModel.Sele...
System.Management
System.Management.Instr...
System.Messaging
System.Net
System.Numerics
System.Printing
System.Runtime
System. Runtime. Remoting
System.Runttme.Sertalizati...
Version
4.0.0.0
4.0.0.0
4ЛД0
4.0.0.0
4.0.0.0
4.0.0.0
4.0.0.0
4.0.0.0
4.0.0.0
4.0.0.0
4.0.0.0
Runtime
V4.0.21006
v4.021006
v4.0.21006
v4.0.21006
v4.0.21006
v4.0.21006
v4.021006
v4.0.21006
у4.0.Л006
v4.021006
V4.021006
Path
C:\Program
G\Program —
CAProgram
CAProgram
C:\Program
CAProgram
CAProgram
CAProgram
C:\Program
CAProgram
CAProgram »
►
:«««П I
Рис. 2.11. Диалоговое окно Add Reference
Просмотр свойств проекта
И, наконец, напоследок важно обратить внимание на наличие в окне утилиты
Solution Explorer пиктограммы Properties (Свойства). Двойной щелчок на ней
приводит к открытию редактора конфигурации проекта, окно которого называется Project
Properties (Свойства проекта) и показано на рис. 2.12.
Глава 2. Создание приложений на языке С# 95
Piatfoffrc Ы/А
Default
Vs2D10Example
Output type:
•ramework4 Client Profile
I Console Application
I I
Resources
Specify how application resources will be managed:
Assembly Information...
» Icon and manifest
« a»w^#<4 m ^пгЧялч rv *m*4**4 finrf »«4a4 if ♦«
111\тш^-ъшш£Шшвшшят
I Vs2010Example
> Q| Project References
a <} Vs2010Example
^ j^J Program
a [jj Base Types
Рис. 2.12. Окно Project Properties
Различные возможности, доступные в окне Project Properties, более подробно
рассматриваются по ходу данной книги. В этом окне можно устанавливать различные
параметры безопасности, назначать сборке надежное имя, развертывать приложение,
вставлять необходимые для приложения ресурсы и конфигурировать события, которые
должны происходить перед и после компиляции сборки.
Утилита Class View
Следующей утилитой, с которой необходимо
познакомиться, является Class View (Просмотр
классов), доступ к которой тоже можно получать
через меню View. Эта утилита позволяет
просматривать все типы, которые присутствуют в
текущем проекте, с объектно-ориентированной точки
зрения (а не с точки зрения файлов, как это
позволяет делать утилита Solution Explorer). В верхней
панели утилиты Class View отображается список
пространств имен и их типов, а в нижней
панели — члены выбранного в текущий момент типа,
как показано на рис. 2.13.
В результате двойного щелчка на типе или члене
типа в окне утилиты Class View в Visual Studio
будет автоматически открываться соответствующий
файл кода С#, с размещением курсора мыши на
соответствующем месте. Еще одной замечательной функциональностью утилиты Class
View в Visual Studio 2010 является возможность открывать любую ссылаемую сборку и
просматривать содержащиеся внутри нее пространства имен, типы и члены (рис. 2.14).
Утилита Object Browser
В Visual Studio 2010 доступна еще одна утилита для изучения множества сборок, на
которые имеются ссылки в текущем проекте. Называется эта утилита Object Browser
(Браузер объектов) и получить к ней доступ можно, опять-таки, через меню View.
После открытия ее окна останется просто выбрать сборку, которую требуется изучить
(рис. 2.15).
>♦ -ObjectO
* Equa!s(object, object)
"-♦ Equals(object)
V GetHashCodeQ
Ф GetTypeO
qj+ MemberwJseOoneQ
J
Рис. 2.13. Окно утилиты Class View
96 Часть I. Общие сведения о языке С# и платформе .NET
^ Vs2010Example
л Q| Project References
> .*3 Microsoft.CSharp
J J mscorlib
> О Microsoft. Win32
» {} Microsoft,Win32.SafeHandles
^ (} System
t> ^[^
> J Action
ft AccessViolationException(System.Runtime.Serielizstion.SerielizationInfol
% Acce$sViolationException(string, System.Exception)
v AccessVk)lationException(string)
V AccessViolationExceptionO
□шва-.
Рис. 2.14. Утилита Class View может также применяться
для просмотра ссылаемых сборок
Object Browser
Browse All Components
I <Search>
0 a mscorlib [2.0.0.0]
-СЭ mscorlib [2.0.5.0]
л j mscorlib [4.0.0.0]
!> {} Microsoft.Win32
t> {} Microsoft.Win32.SafeHandles
{} System
d {} System.Collections
•% Bit Array
Л% CaselnsensitiveComparer
> "tj CaselnsensitiveHashCodeProvider
ij CollectionBasc
-', Comparer
l> 4$ DictionaryBase
L> Tj^ DictionaryEntry
r, JH Ul.rhf.kU
I
> Adapter(System.Collections.ILi5t)
♦ Add(object)
V AddRange(System.CollectionsJCollection)
♦ ArrayList(System.CollectionsJCollection)
♦ ArrayListOnt)
♦ ArrayListO
<* BinarySearch(object, System.Collections.IComparer)
t BinarySearch(object)
♦ BinarySearch(int, int, object, System.Collections.IComparer)
public class Array Li st
Member of System.Collections
Summary:
Implements the System.CollectionsJList interface using an array whose size is dynamically
increased as required.
Рис. 2.15. Окно утилиты Object Browser
Встроенная поддержка рефакторинга программного кода
Одной из главных функциональных возможностей Visual Studio 2010 является
встроенная поддержка для проведения рефакторинга существующего кода. Если объяснять
упрощенно, то под рефакторингом (refactoring) подразумевается формальный
механический процесс улучшения существующего кода. В прежние времена рефакторинг
требовал приложения массы ручных усилий. К счастью, теперь в Visual Studio 2010 можно
достаточно хорошо автоматизировать этот процесс.
За счет использования меню Refactor (Рефакторинг), которое становится доступным
при открытом файле кода, а также соответствующих клавиатурных комбинаций
быстрого вызова, смарт-тегов (smart tags) и/или вызывающих контекстные меню щелчков,
можно существенно видоизменять код с минимальным объемом усилий. В табл. 2.2
перечислены некоторые наиболее распространенные приемы рефакторинга, которые
распознаются в Visual Studio 2010.
Глава 2. Создание приложений на языке С# 97
Таблица 2.2. Приемы рефакторинга, поддерживаемые в Visual Studio 2010
Прием рефакторинга
Описание
Extract Method
(Извлечение метода)
Encapsulate Field
(Инкапсуляция поля)
Extract Interface
(Извлечение интерфейса)
Reorder Parameters
(Переупорядочивание параметров)
Remove Parameters
(Удаление параметров)
Rename
(Переименование)
Позволяет определять новый метод на основе
выбираемых операторов программного кода
Позволяет превращать общедоступное поле в приватное,
инкапсулированное в форму свойство С#
Позволяет определять новый тип интерфейса на основе
набора существующих членов типа
Позволяет изменять порядок следования аргументов в
члене
Позволяет удалять определенный аргумент из текущего
списка параметров
Позволяет переименовывать используемый в коде метод,
поле, локальную переменную и т.д. по всему проекту
Чтобы увидеть процесс рефакторинга в действии, давайте модифицируем метод
Main (), добавив в него следующий код:
static void Main(string [ ] args)
{
// Настройка консольного интерфейса (CUI).
Console.Title = "My Rocking App11;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.BackgroundColor = ConsoleColor.Blue;
Console WriteLine (M***************************************'4 •
Console.WriteLine("***** Welcome to My Rocking App! *******");
Console WriteLine ("***************************************") •
Console.BackgroundColor = ConsoleColor.Black;
// Ожидание нажатия клавиши <Enter>.
Console.ReadLine ();
}
В таком, как он есть виде, в этом коде нет ничего неправильного, но давайте
представим, что возникло желание сделать так, чтобы данное приветственное сообщение
отображалось в различных местах по всей программе. В идеале вместо того, чтобы
заново вводить ту же самую отвечающую за настройку консольного интерфейса логику,
было бы неплохо иметь вспомогательную функцию, которую можно было бы вызывать
для решения этой задачи. С учетом этого, попробуем применить к существующему коду
прием рефакторинга Extract Method (Извлечение метода).
Для этого выделите в окне редактора все содержащиеся внутри Main ()
операторы, кроме последнего вызова Console .ReadLine (), и щелкните на выделенном коде
правой кнопкой мыши. Выберите в контекстном меню пункт Refactor^ Extract Method
(РефакторингаИзвлечь метод), как показано на рис. 2.16.
В открывшемся далее окне назначьте новому методу имя ConfigureCUI () . После
этого метод Main() станет вызывать новый только что сгенерированный метод
ConfigureCUI (), внутри которого будет содержаться выделенный ранее код:
class Program
{
static void Main(string[] args)
{
ConfigureCUI ();
// Ожидание нажатия клавиши <Enter>.
98
Часть I. Общие сведения о языке С# и платформе .NET
Console.ReadLine();
}
private static void ConfigureCUl()
{
// Настройка консольного интерфейса (CUI).
Console.Title = "My Rocking App";
Console.Title = "Мое приложение";
Console.ForegroundColor = ConsoleColor.Yellow;
Console.BackgroundColor = ConsoleColor.Blue;
Сonsole.WriteLine("********************
Console.WriteLine("***** Welcome to My Rocking App!
Console.WriteLine("**************************~****^
Console.BackgroundColor = ConsoleColor.Black;
************
k- ****** И
);
}
Этот лишь один простой пример применения предлагаемых в Visual Studio 2010
встроенных возможностей для рефакторинга, и по ходу настоящей книги будет
встречаться еще немало подобных примеров.
Ц Program.cs* X Щ
ij4Vs2010Example.Program * e*Main(string[] args)
1 '-1 ciass Hrogran ■ ■■■ —
static void Main(string[] args)
{
Console.Title - "My Rockir
Console.ForegroundColor -
Console.BackgroundColor ■
Console.WriteLineC****** h
(Jonsole.WriteLitM!("*****"*
Con sole.BackgroundColor m
// Wait for Enter key to t
Console.ReadLine();
1 > '
1}
1~00% ~«|«
Refactor
Organize Usings
£) Create Unit Tests.,,
Generate Sequence Diagram...
<% Insert Snippet.,,
% Surround With,..
J Go To Definition
Find AH References
_,-, View Call Hierarchy
Breakpoint
•i Run To Cursor
* Cut
-J Copy
A Paste
Outlining
1
CtrklCX
Ctrl+K. S
F12
Ctrl+K, R
CtrkrCT
►
Ctri+FlO
Ctri+X
Ctrt+C
Ctri+V
«t/ Rename...
Чф*-' Extract Method... к
V1 Encapsulate Field...
_..: Extract Interface...
* ч Remove Parameters...
а.ь Reorder Parameters.,.
J
■
F2
CtrUR, M
Ctri+R, E
CtrkRJ
Ctrl+RV
Qri+R, 0
J
.
t
Рис. 2.16. Активизация рефакторинга кода
Возможности для расширения и окружения кода
В Visual Studio 2010 (а также в Visual C# 2010 Express) можно вставлять готовые
блоки кода С# выбором соответствующих пунктов в меню, вызовом контекстных меню
по щелчку правой кнопкой мыши и/или использованием соответствующих
клавиатурных комбинаций быстрого вызова. Число доступных шаблонов для расширения кода
впечатляет. В целом их можно поделить на две основных группы.
• Шаблоны для вставки фрагментов кода (code snippet). Эти шаблоны позволяют
вставлять общие блоки кода в месте расположения курсора мыши.
• Шаблоны для окружения кода (Surround With). Эти шаблоны позволяют помещать
блок избранных операторов в рамки соответствующего контекста.
Чтобы посмотреть на эту функциональность в действии, давайте предположим, что
требуется обеспечить проход по поступающим в метод Main () параметрам в цикле
foreach.
Глава 2. Создание приложений на языке С# 99
Вместо того чтобы вводить необходимый код вручную, можно активизировать
фрагмент кода f oreach. После выполнения этого действия IDE-среда поместит шаблон кода
f oreach в месте, где в текущий момент находится курсор мыши.
Поместим курсор мыши после первой открывающей фигурной скобки в методе
Main (). Одним из способов активизации фрагмента кода является выполнение щелчка
правой кнопкой мыши и выбора в контекстном меню пункта Insert Snippet (Вставить
фрагмент кода) (или Surround With (Окружить с помощью)). Это приводит к отображению
списка всех относящихся к данной категории фрагментов кода (для закрытия
контекстного меню достаточно нажать клавишу <Esc>). В качестве клавиатурной комбинации
быстрого вызова можно просто ввести имя интересующего фрагмента кода, которым в
данном случае является f oreach. На рис. 2.17 видно, что пиктограмма,
представляющая фрагмент кода, внешне немного напоминает клочок бумаги.
Щ Program.cs* X ЩШ
j$Vs2Q10Example.Program - J ,j<*Main(string[] args)
fusing System;
using System.Collections.Generic;
I using System.Linq;
[using System.Text;
"namespace Vs2010Example
1С
^ class Program
i (
static void Main(string[] args)
{
Рис. 2.17. Активизация фрагмента кода
Отыскав фрагмент кода, который требуется активизировать, нажмите два раза
клавишу <ТаЬ>. Это приведет к автоматическому завершению всего фрагмента кода и
оставлению ряда меток-заполнителей, в которых останется только ввести необходимые
значения, чтобы фрагмент был готов. Нажимая клавишу <ТаЬ>, можно переходить от
одной метки-заполнителя к другой и заполнять пробелы (по завершении нажмите
клавишу <Esc> для выхода из режима редактирования фрагмента кода).
В результате щелчка правой кнопкой мыши и выбора в контекстном меню пункта
Surround With (Окружить с помощью) будет тоже появляться список возможных
вариантов. При использовании средства Surround With обычно сначала выбирается блок
операторов кода для представления того, что должно применяться для их окружения
(например, блок try/catch). Обязательно уделите время изучению предопределенных
шаблонов расширения кода, поскольку они могут радикально ускорить процесс
разработки программ.
На заметку! Все шаблоны для расширения кода представляют собой XML-описания кода,
подлежащие генерации в IDE-среде. В Visual Studio 2010 (а также в Visual C# 2010 Express) можно
создавать и собственные шаблоны кода. Дополнительные сведения доступны в статье "Investigating
Code Snippet Technology" ("Исследование технологии применения фрагментов кода") на сайте
http: //msdn .microsoft. com.
щ
100 Часть I. Общие сведения о языке С# и платформе .NET
Solution Explorer
|*Э Solution 'Vs2ti view Class Diagram )
73I u.iflint^HH ■ ■ '
3 Vs2010ExafflpVT-
ill Properties
,j> References
СЭ Microsoft.С Sharp
■*<3 System
4J System.Core
нЭ System.Data
чЭ System.Data.DataSetExtensions
*<Э System.Xml
-O SystemJCml.Linq
^3 Program.es
Рис. 2.18 Вставка файла диаграммы
классов
Утилита Class Designer
В Visual Studio 2010 имеется возможность
конструировать классы визуальным образом (в Visual
С# 2010 Express такой возможности нет). Для этого
в составе Visual Studio 2010 поставляется утилита
под названием Class Designer (Конструктор
классов), которая позволяет просматривать и изменять
отношения между типами (классами,
интерфейсами, структурами, перечислениями и делегатами) в
проекте. С помощью этой утилиты можно визуально
добавлять или удалять члены из типа с отражением
этих изменений в соответствующем файле кода на
С#, а также в диаграмме классов.
Для работы с этой утилитой сначала необходимо
вставить новый файл диаграммы классов. Делать
это можно несколькими способами, одним из которых является щелчок на кнопке View
Class Diagram (Просмотр диаграммы классов) в правой части окна Solution Explorer,
как показано на рис. 2.18 (при этом важно, чтобы в окне был выбран проект, а не
решение).
После выполнения этого действия появляются пиктограммы, представляющие
классы, которые входят в текущий проект. Щелкая внутри них на значке с изображением
стрелки для того или иного типа, можно отображать или скрывать члены этого типа
(рис. 2.19).
На заметку! С помощью панели Class Designer (Конструктор классов) можно настраивать
параметры отображения поверхности конструктора желаемым образом.
ClassDiagraml.cd* X Ц
С
< rzz
*ЙГ_ 11®]
Class
a Methods
& ConfigureCUI
ll^ Main
^ i i
i-rzr-z
1
Рис. 2.19. Просмотр диаграммы классов
Эта утилита работает вместе с двумя другими средствами Visual Studio 2010 —
окном Class Details (Детали класса), которое можно открыть путем выбора в меню View
(Вид) пункта Other Windows (Другие окна), и панелью Class Designer Toolbox (Элементы
управления конструктора классов), которую можно отобразить выбором в меню View
(Вид) пункта Toolbox (Элементы управления). В окне Class Details не только
отображаются детали выбранного в текущий момент элемента в диаграмме, но также можно
изменять его существующие члены и вставлять новые на лету, как показано на рис. 2.20.
Глава 2. Создание приложений на языке С# 101
Class Details - Program
* *
if
%*
-3
&
*f
Name
г> Л% ConfigureCUI
dV Main
-•♦ <;addmethod>
* Properties
3* < add property*
| *g Class Details Д
void
void
private
private
»..*,x]
Hide
и (II
и
1
I Toolbox
1 d Class Designer
1^ Pointer
В Class
E Enum
£j Interface
* J Abstract Class
□ Struct
О Delegate
4- Inheritance
*т_ Association
1 У& Comment
1 л General
▼ п x]
Л
1
d
Class
Рис. 2.20. Окно Class Details
Что касается панели Class Designer Toolbox, которую,
как уже было сказано, можно активизировать через меню
View (Вид), то она позволяет вставлять в проект новые типы
(и создавать между ними желаемые отношения) визуальным
образом (рис. 2.21). (Следует иметь в виду, что для просмотра
этой панели требуется, чтобы окно диаграммы классов было
активным.) По мере выполнения этих действий IDE-среда
автоматически создает незаметным образом соответствующие
новые определения типов на С#.
Для примера давайте перетащим из панели Class Designer
Toolbox в окно Class Designer новый элемент Class (Класс), в
отрывшемся окне назначим ему имя Саг, а затем с помощью
окна Class Details добавим в него общедоступное поле типа
string по имени PetName (рис. 2.22).
Взглянув на С#-определение класса Саг после этого,
можно увидеть, что оно было соответствующим образом
обновлено (добавленные комментарии не считаются):
public class Car
{
// Использовать общедоступные данные обычно
//не рекомендуется, но здесь это упрощает пример.
public string petName;
}
Теперь давайте активизируем утилиту Class Designer еще раз и перетащим на
поверхность конструктора новый элемент типа Class, присвоив ему имя SportsCar. Затем
выберем в Class Designer Toolbox пиктограмму Inheritance (Наследование) и щелкнем в
верхней части пиктограммы SportsCar. Далее, не отпуская левую кнопку мыши,
перетащим курсор мыши на поверхность пиктограммы класса Саг и отпустим ее. Правильное
выполнение всех перечисленных выше действий приведет к тому, что класс SportsCar
станет наследоваться от класса Саг, как показано на рис. 2.23.
Рис. 2.21. Панель
Designer Toolbox
(Class Det
-
- Ц- X
Name
* Properties
*£ <add property>
I * Fields
У [petName
♦ <add field*
* Events
Type
Summary
И
Рис. 2.22. Добавление поля с помощью окна Class Details
102 Часть I. Общие сведения о языке
ClassDtagraml.cd* X
Class
S Methods
a* ConfigureCUI
аФ Main
L^.'.umju.niiinuM - МММ
[^ > ►___
Рис. 2.23. Визуальное наследования одного класса от другого
Чтобы завершить данный пример, осталось обновить сгенерированный класс
SportsCar, добавив в него общедоступный метод с именем GetPetName ():
public class SportsCar : Car
{
public string GetPetName()
{
petName = "Fred";
return petName;
}
}
Все эти (и другие) визуальные инструменты Visual 2010 придется еще много раз
использовать в книге. Однако уже сейчас должно появиться чуть большее понимание
основных возможностей этой IDE-среды.
На заметку! Концепция наследования подробно рассматривается в главе 6.
Интегрируемая система документации .NET Framework 4.0
И, наконец, последним средством в Visual Studio 2010, которым необходимо
обязательно уметь пользоваться с самого начала, является полностью интегрируемая
справочная система. Поставляемая с .NET Framework 4.0 SDK документация представляет
собой исключительно хороший, очень понятный и насыщенный полезной информацией
источник. Из-за огромного количества предопределенных типов .NET (насчитывающих
тысячи), необходимо погрузиться в исследование предлагаемой документации. Не
желающие делать это обрекают себя как разработчика .NET на длительное, мучительное
и болезненное существование.
При наличии соединения с Интернетом просматривать документацию .NET
Framework 4.0 SDK можно в онлайновом режиме по следующему адресу:
http://msdn.microsoft.com/library
Разумеется, при отсутствии постоянного соединения с Интернетом такой подход
оказывается не очень удобным. К счастью, ту же самую справочную систему
можно установить локально на своем компьютере. Имея уже установленную копию Visual
Studio 2010, необходимо выбрать в меню Start (Пуск) пункт All Programs^ Microsoft
С# и платформе .NET
1 Сж
Class
1 S Fields
Ф petName
^ „
Y
,f ■■■■"•■:■■■ '
j SportsCar
| Class
J +Car
®)
>
"Щ
Глава 2. Создание приложений на языке С# 103
Visual Studio 2010^Visual Studio Tools^ Manage Help (Все программы1^Microsoft Visual
Studio 2010 ^Утилиты Visual Studio ^Управление настройками справочной системы).
Затем можно приступать к добавлению интересующей справочной документации, как
показано на рис. 2.24 (если на диске хватает места, имеет смысл добавить всю
возможную документацию).
Ь Hdp Library Manage:
Find Content on Disk
Actions
! v .NET Development
.NET Framework 4 M&
v visual Studio 2010
Application Lifecycle Management ЙЙЙ
Extending Application Lifecycle Management Md
JScript AfW
Office Development ^
Settings | Help
Status
Click Add to select content for offline help.
Update
Рис. 2.24. В окне Help Library Manager (Управление библиотекой
справочной документации) можно загрузить локальную копию
документации .NET Framework 4.0 SDK
На заметку! Начиная с версии .NET Framework 4.0, просмотр справочной системы
осуществляется через текущий веб-браузер, даже в случае локальной установки документации .NET
Framework 4.0 SDK.
После локальной установки справочной системы простейшим способом для
взаимодействия с ней является выделение интересующего ключевого слова С#, имени типа
или имени члена в окне представления кода внутри Visual Studio 2010 и нажатие
клавиши <F1>. Это приводит к открытию окна с документацией, касающейся конкретного
выбранного элемента. Например, если выделено ключевое слово string в определении
класса Саг, после нажатия клавиши <F1> появится страница со справочной
информацией об этом ключевом слове.
Еще одним полезным компонентом справочной системы является доступное для
редактирования поле Search (Искать), которое отображается в левой верхней части
экрана. В этом поле можно вводить имя любого пространства имен, типа или члена и
тем самым сразу же переходить в соответствующее место в документации. При
попытке найти подобным образом пространство имен System.Reflection, например, можно
будет узнать о деталях этого пространства имен, изучить содержащиеся внутри него
типы, просмотреть примеры кода с ним и т.д. (рис. 2.25).
В каждом узле внутри дерева описаны типы, которые содержатся в данном
пространстве имен, их члены и параметры этих членов. Более того, при просмотре страницы
справки по тому или иному типу всегда сообщается имя сборки и пространства имен, в
котором содержится запрашиваемый тип (соответствующая информация отображается
в верхней части данной страницы). В остальной части настоящей книги ожидается, что
читатель будет заглядывать в эту очень важную справочную систему и изучать
дополнительные детали рассматриваемых сущностей.
104 Часть I. Общие сведения о языке С# и платформе .NET
Q SyJtwn.Reflection Names...
«- С ft tt hUp://127.0.0.1-.47873/help/l/ms.hefp?me№^
► О- A^
Library Horn*
Visual Studio 2010
.NET Framework 4
NET Framework Cl*« library
System. Retire t ion Namespace
ArnbiquouiMetchEvception Clan
Assembry CU«
Assembly Algorithm] dAttnbutr Class
AssembtyCompanyAttribute Class
AssemblyConf iguratiortAttribtrte Class
AssembryCopynghtAttnbute Class
AssembtyCuKureAttribtrte Class
AssemblyDefaultAliasAttrtbute Class
AssembryOelaySignAttribute Class
AssemblyDescriptionAttribute Class
AsiembryFiteVersionAttobute Class
AssembJyFlagsAttnbute CUss
AssembrylnformationalVersionAttnbute Class
AssembtyKeyFiteAttrrbute Class
AssemblyKeyNameAttribute Class-
Assembh/Name Class
AsiemblyNameFlags Enumeration
i i-irrmmtiitiihliwniaiiif fitiri
System.Reflection Namespace
OO^Eual Studio Ш
Send Feedback
The System.Reflection namespace contains types that retrieve information about assemblies, modules,
members, parameters, and other entities in managed code by examining their metadata. These types also can
be used to manipulate instances of loaded types, for example to hook up events or to invoke methods. To
dynamically create types, use the System.Reftection.Emit namespace.
Classes
Am big u ousM a tch Exception
Description
The exception that is thrown when binding to a
member results in more than one member matching
the binding criteria. This dass cannot be inherited.
Represents an assembry, which is a reusable,
versionable, and self-describing building block of a
common language runtime application.
Рис. 2.25. Поле Search позволяет быстро находить представляющие интерес элементы
На заметку! Не лишним будет напомнить еще раз о важности использования документации .NET
Framework 4.0 SDK. Ни одна книга, какой бы объемной она ни была, не способна охватить все
аспекты платформы .NET. Поэтому необходимо научиться пользоваться справочной системой;
впоследствии это окупится с лихвой.
Резюме
Итак, нетрудно заметить, что у разработчиков появилась масса новых "игрушек".
Целью настоящей главы было проведение краткого экскурса в основные средства,
которые программист на С# может использовать во время разработки. В начале было
рассказано о том, как генерировать сборки .NET с применением бесплатного компилятора
С# и редактора "Блокнот" (Notepad). Затем было приведено краткое описание
приложения Notepad++ и показано, как его использовать для редактирования и компиляции
файлов кода * . cs.
И, наконец, здесь были рассмотрены три таких многофункциональных IDE-среды,
как SharpDevelQp (распространяется с открытым исходным кодом), Microsoft Visual
С# 2010 Express и Microsoft Visual Studio 2010 Professional. Функциональные
возможности каждого из этих продуктов в настоящей главе были описаны лишь кратко, поэтому
каждый волен заняться более детальным изучением избранной IDE-среды в свободное
время (многие дополнительные функциональные возможности Visual Studio 2010 будут
рассматриваться далее в настоящей книге).
ЧАСТЬ II
DiaBHbie конструкции
программирования
наС#
В этой части...
Глава 3. Главные конструкции программирования на С#: часть I
Глава 4. Главные конструкции программирования на С#: часть II
Глава 5. Определение инкапсулированных типов классов
Глава 6. Понятия наследования и полиморфизма
Глава 7. Структурированная обработка исключений
Глава 8. Время жизни объектов
ГЛАВА О
Вгавные конструкции
программирования
на С#: часть I
В настоящей главе начинается формальное исследование языка программирования
С#. Здесь предлагается краткий обзор отдельных тем, в которых необходимо
разобраться, чтобы успешно изучить платформу .NET Framework. В первую очередь
поясняется то, как создавать объект приложения и как должна выглядеть структура метода
Main (), который является входной точкой в любой исполняемой программе. Далее
рассматриваются основные типы данных в С# (и их аналоги в пространстве имен System),
в том числе типы классов System. String и System. Text. StringBuilder.
После представления деталей основных типов данных в .NET рассказывается о ряде
методик, которые можно применять для их преобразования, в том числе об операциях
сужения (narrowing) и расширения (widening), а также использовании ключевых слов
checked H.unchecked.
Кроме того, в этой главе описана роль ключевого слова var в языке С#, которое
позволяет неявно определять локальную переменную. И, наконец, в главе приводится
краткий обзор ключевых операций, итерационных конструкций и конструкций
принятия решений, применяемых для создания рабочего кода на С#.
Разбор простой программы на С#
В языке С# вся логика программы должна содержаться внутри определения какого-
то типа (в главе 1 уже говорилось, что тип представляет собой общий термин, которым
обозначается любой элемент из множества {класс, интерфейс, структура,
перечисление, делегат}). В отличие от многих других языков, в С# не допускается создавать ни
глобальных функций, ни глобальных элементов данных. Вместо этого требуется, чтобы
все члены данных и методы содержались внутри определения типа. Для начала
давайте создадим новый проект типа Console Application (Консольное приложение) по имени
SimpleCSharpAppB. Как показано ниже, в исходном коде Program, cs ничего особо
примечательного нет:
using System;
using System. Collections . Generic-
using System.Linq;
using System.Text
namespace SimpleCSharpApp
Глава 3. Главные конструкции программирования на С#: часть I 107
{
class Program
{
static void Main(string [ ] args)
{
}
}
Имея такой код, далее модифицируем метод Main () в классе Program, добавив в него
следующие операторы:
class Program
{
static void Main(string[] args)
{
// Вывод простого сообщения пользователю.
Console.WriteLine("***** My First C# App *****");
Console.WriteLine("Hello World!");
.Console.WriteLine();
// Ожидание нажатия клавиши <Enter>
// перед завершением работы.
Console.ReadLine();
}
}
В результате получилось определение типа класса, поддерживающее единственный
метод по имени Main (). По умолчанию классу, в котором определяется метод Main (),
в Visual Studio 2010 назначается имя Program; при желании это имя легко изменить.
Класс, определяющий метод Main (), должен обязательно присутствовать в каждом
исполняемом приложении на С# (будь то консольная программа, настольная программа
для Windows или служба Windows), поскольку он применяется для обозначения точки
входа в приложение.
Формально класс, в котором определяется метод Main (), называется объектом
приложения. Хотя в одном исполняемом приложении допускается иметь более одного
такого объекта (это может быть удобно при проведении модульного тестирования), при этом
обязательно необходимо информировать компилятор о том, какой из методов Main ()
должен использоваться в качестве входной точки. Для этого нужно либо указать опцию
main в командной строке, либо выбрать соответствующий вариант в раскрывающемся
списке на вкладке Application (Приложение) окна редактора свойств проекта в Visual
Studio 2010 (см. главу 2).
Обратите внимание, что в сигнатуре метода Main () присутствует ключевое слово
static, которое более подробно рассматривается в главе 5. Пока достаточно знать, что
область действия статических (static) членов охватывает уровень всего класса (а не
уровень отдельного объекта) и потому они могут вызываться без предварительного
создания нового экземпляра класса.
На заметку! С# является чувствительным к регистру языком программирования. Следовательно,
Main и main или Readline и ReadLine будут представлять собой далеко не одно и то же.
Поэтому необходимо запомнить, что все ключевые слова в С# вводятся в нижнем регистре
(например, public, lock, class, dynamic), а названия пространств имен, типов и членов
всегда начинаются (по соглашению) с заглавной буквы, равно как и любые вложенные в них
слова (как, например, Console .WriteLine, System.Windows . Forms .MessageBox и
System. Data.SqlClient). Как правило, при каждом получении от компилятора ошибки,
связанной с "неопределенными символами", требуется проверить регистр символов.
108 Часть II. Главные конструкции программирования на С#
Помимо ключевого слова static, данный метод Main () имеет еще один параметр,
который представляет собой массив строк (string [ ] args). Хотя в текущий момент
этот массив никак не обрабатывается, в данном параметре в принципе может
содержаться любое количество аргументов командной строки (процесс получения доступа к
которым будет описан чуть ниже). И, наконец, данный метод Main () был
сконфигурирован с возвращаемым значением void, которое свидетельствует о том, что решено не
определять возвращаемое значение явным образом с помощью ключевого слова return
перед выходом из области действия данного метода.
Логика Program содержится внутри самого метода Main (). Здесь используется класс
Console из пространства имен System. В число его членов входит статический метод
WriteLine (), который позволяет отправлять строку текста и символ возврата
каретки на стандартное устройство вывода. Кроме того, здесь вызывается метод Console.
ReadLine (), чтобы окно командной строки, запускаемое в IDE-среде Visual Studio 2010,
оставалось видимым во время сеанса отладки до тех пор, пока не будет нажата клавиша
<Enter>.
Варианты метода Main ()
По умолчанию в Visual Studio 2010 будет генерироваться метод Main () с
возвращаемым значением void и массивом типов string в качестве единственного входного
параметра. Однако такой метод Main () является далеко не единственно возможным
вариантом. Вполне допускается создавать собственные варианты входной точки в
приложение с помощью любой из приведенных ниже сигнатур (главное, чтобы они
содержались внутри определения какого-то класса или структуры на С#):
// Возвращаемый тип int и массив строк в качестве параметра.
static int Main(string[] args)
{
// Должен обязательно возвращать значение перед выходом!
raturn 0;
}
//Ни возвращаемого типа, ни параметров.
static void Main()
{
}
// Возвращаемый тип int, но никаких параметров.
static int Main()
{
// Должен обязательно возвращать значение перед выходом!
return 0;
}
На заметку! Метод Main () может также определяться как общедоступный (public), а не
приватный (private), каковым он считается, если не указан конкретный модификатор доступа. В Visual
Studio 2010 метод Main () автоматически определяется как неявно приватный. Это гарантирует
отсутствие у приложений возможности напрямую обращаться к точке входа друг друга.
Очевидно, что выбор способа создания метода Main () зависит от двух моментов.
Во-первых, он зависит от того, нужно ли, чтобы системе после окончания выполнения
метода Main () и завершения работы программы возвращалось какое-то значение; в
этом случае необходимо возвращать тип данных int, а не void. Во-вторых, он зависит
от необходимости обработки предоставляемых пользователем параметров командной
строки; в этом случае они должны сохраняться в массиве strings. Рассмотрим все
возможные варианты более подробно.
Глава 3. Главные конструкции программирования на С#: часть I 109
Спецификация кода ошибки в приложении
Хотя в большинстве случаев методы Main () возвращают void в качестве
возвращаемого значения, способность возвращать int из Main () позволяет согласовать С#
с другими языками на базе С. По соглашению, возврат значения 0 свидетельствует о
том, что выполнение программы прошло успешно, а любого другого значения
(например, -1) — что в ходе выполнения программы произошла ошибка (следует иметь в виду,
что значение 0 возвращается автоматически даже в случае, если метод Main ()
возвращает void).
В операционной системе Windows возвращаемое приложением значение
сохраняется в переменной среды по имени %ERRORLEVEL%. В случае создания приложения, в
коде которого предусмотрен запуск какого-то исполняемого модуля (см. главу 16),
получить значение %ERRORLEVEL% можно с помощью статического свойства System.
Diagnostics.Process.ExitCode.
Из-за того, что возвращаемое приложением значение в момент завершения его
работы передается системе, приложение не может получать и отображать свой конечный
код ошибки во время выполнения. Однако просмотреть код ошибки по завершении
выполнения программы все-таки можно. Чтобы увидеть, как это делается, модифицируем
метод Main () следующим образом:
// Обратите внимание, что теперь возвращается int, а не void.
static int Main(string [ ] args)
{
// Вывод сообщения и ожидание нажатия клавиши <Enter>.
Console.WriteLine(»***** My First C# App *****");
Console.WriteLine("Hello World!");
Console.WriteLine();
Console.ReadLine();
// Возврат произвольного кода ошибки.
return -1;
}
Теперь давайте обеспечим перехват возвращаемого Main () значения с помощью
командного файла. Для этого перейдите в окне проводника Windows в каталог, где
хранится скомпилированное приложение (например, в C:\SimpleCSharpApp\bin\Debug),
создайте в нем новый текстовый файл (по имени SimpleCSharpApp.bat) и добавьте в
него следующие инструкции:
@echo off
rem Командный файл для приложения SimpleCSharpApp.exe,
rem перехватывающий возвращаемое им значение.
SimpleCSharpApp
@if "%ERRORLEVEL%" == " goto success
:fail
echo This application has failed1
rem Выполнение этого приложения не удалось 1
echo return value = %ERRORLEVEL%
goto end
:success
echo This application has succeeded!
rem Выполнение этого приложения прошло успешно!
echo return value = %ERRORLEVEL%
goto end
:end
echo All Done . ,
rem Все сделано.
110 Часть II. Главные конструкции программирования на С#
Теперь откройте окно командной строки в Visual Studio 2010 и перейдите в
каталог, где находится исполняемый файл приложения и созданный только что файл * .bat.
Запустите командный файл, набрав его имя и нажав <Enter>. После этого на экране
должен появиться вывод, подобный показанному на рис. 3.1, поскольку метод Main ()
сейчас возвращает значение -1. Если бы он возвращал значение 0, в окне консоли
появилось бы сообщение This application has succeeded!.
I Visual Studio 2010 Command Prompt
С:\SimpleCSharpApp\bi n\Debug>SimpleCSharpApp.bat
:***** My First C# App *****
IgHello World!
prhis application has failed!
return value = -1
|a11 Done.
::\Si mpleCSharpApp\bi n\Debug>
Рис. З.1. Перехват значения, возвращаемого приложением,
с помощью командного файла
В большинстве приложений на С# (если не во всех) в качестве возвращаемого Main ()
значения будет использоваться void, которое, как уже известно, неявно подразумевает
возврат кода ошибки 0. Из-за этого все демонстрируемые далее в настоящей книге
методы Main () будут возвращать именно void (никакие командные файлы для перехвата
кода возврата в последующих проектах не используются).
Обработка аргументов командной строки
Теперь, когда стало более понятно, что собой представляет возвращаемое значение
метода Main (), давайте посмотрим на входной массив данных string. Предположим,
что теперь требуется обновить приложение так, чтобы оно могло обрабатывать любые
возможные параметры командной строки. Одним из возможных способов для
обеспечения такого поведения является использование поддерживаемого в С# цикла for (все
итерационные конструкции С# более подробно рассматриваются далее в главе):
static int Main(string [ ] args)
{
// Обработка любых входящих аргументов.
for(int i = 0; i < args.Length; i++)
Console .WnteLine ("Arg: {0}", args[i]);
Console.ReadLine ();
return -1;
}
Здесь с помощью свойства Length типа System.Array производится проверка,
содержатся ли в массиве string какие-то элементы. Как будет показано в главе 4, все
массивы в С# на самом деле относятся к классу System.Array и потому имеют общий
набор членов. При проходе по массиву значение каждого элемента выводится в окне
консоли. Процесс предоставления аргументов в командной строке сравнительно прост
и показан на рис. 3.2.
В качестве альтернативы, вместо стандартного цикла for для прохода по входному
массиву строк можно также использовать ключевое слово f о reach.
Глава 3. Главные конструкции программирования на С#: часть I 111
| Visual Studio 2010 Command Prompt *■ SimpleCSHaipApp /argl ~ar$2
>:ШШ
fC:\SimpleCSharpApp\bin\Debug>Simp1eCSharpApp /argl -arg2
***** My First c# App *****
IHello World!
rg: /argl
rg: -arg2
Рис. З.2. Предоставление аргументов в командной строке
Ниже приведен соответствующий пример:
// Обратите внимание, что в случае использования
// цикла foreach проверять размер массива
//не требуется.
static int Main(string [ ] args)
// Обработка любых входящих аргументов с помощью foreach.
foreach (string arg in args)
Console.WriteLine("Arg: {0}", arg);
Console.ReadLine();
return -1;
И, наконец, получать доступ к аргументам командной строки можно с помощью
статического метода GetCommandLineArgs (), принадлежащего типу System.Environment.
Возвращаемым значением этого метода является массив строк (string). В первом
элементе этого массива содержится имя самого приложения, а во всех остальных —
отдельные аргументы командной строки. Важно обратить внимание, что при этом определять
метод Main () так, чтобы он принимал в качестве входного параметра массив string,
больше не требуется, хотя никакого вреда от этого не будет.
public static int Main(string[] args)
{
// Получение аргументов с использованием System.Environment.
string[] theArgs = Environment.GetCommandLineArgs();
foreach (string arg in theArgs)
Console.WriteLine("Arg: {0}", arg);
Console.ReadLine ();
return -1;
}
Разумеется, то, на какие аргументы командной строки должна реагировать
программа (если вообще должна), и в каком формате они должны предоставляться (например, с
префиксом - или /), можно выбирать самостоятельно. В приведенном выше коде
просто передавался набор опций, которые выводились прямо в командной строке. Но что
если бы создавалась новая видеоигра, и приложение программировалось на обработку
опции, скажем, -godmode? Например, при запуске пользователем приложения с этим
флагом можно было бы предпринимать в его отношении соответствующие меры.
Указание аргументов командной строки в Visual Studio 2010
В реальном мире конечный пользователь имеет возможность предоставлять
аргументы командной строки при запуске программы. Для целей тестирования приложения
в процессе разработки также может потребоваться указывать возможные флаги команд-
112 Часть II. Главные конструкции программирования на С#
ной строки. В Visual Studio 2010 для этого необходимо дважды щелкнуть на
пиктограмме Properties (Свойства) в окне Solution Explorer, выбрать в левой части окна вкладку
Debug (Отладка) и указать в текстовом поле Command line arguments (Аргументы
командной строки) желаемые значения (рис. 3.3).
Configuration: l^ft«^jDebug) *J Platform; [Active (*86)
Debug*
m Start project
Start external program:
3S
€.i Start browser with URL ,
Start Options
Reference Paths
Signing
Security
Command line arguments: ! -godmode -argl -arg2
Working directory:
Q
И Use remote machine
Рис. 3.3. Указание аргументов командной строки в Visual Studio 2010
Указанные аргументы командной строки будут автоматически передаваться методу
Main () при проведении отладки или запуске приложения в IDE-среде Visual Studio.
Интересное отклонение от темы:
некоторые дополнительные члены
класса System.Environment
В классе Environment помимо GetCommandLineArgs () предоставляется ряд других
чрезвычайно полезных методов. В частности, этот класс позволяет с помощью
различных статических членов получать детальные сведения, касающиеся операционной
системы, под управлением которой в текущий момент выполняется .NET-приложение. Чтобы
оценить пользу от класса System.Environment, модифицируем метод Main () так,
чтобы в нем вызывался вспомогательный метод по имени ShowEnvironmentDetails ():
static int Main(string [ ] args) •
{
// Вспомогательный метод для класса Program.
ShowEnvironmentDetails();
Console.ReadLine();
return -1;
Теперь реализуем этот метод в классе Program, чтобы в нем вызывались различные
члены класса Environment:
static void ShowEnvironmentDetails ()
{
// Отображение информации о дисковых устройствах
//на данной машине и прочих интересных деталей.
Глава 3. Главные конструкции программирования на С#: часть I 113
foreach (string drive in Environment.GetLogicalDrives ())
Console.WriteLine("Drive: {0}", drive); // диски
Console.WriteLine ("OS: {0}", Environment.OSVersion) ; // ОС
Console.WriteLine("Number of processors: {0}",
Environment.ProcessorCount); // количество процессоров
Console.WriteLine(".NET Version: {0}",
Environment.Version); // версия .NET
}
На рис. 3.4 показано возможное тестовое выполнение данного метода. Если на
вкладке Debug в Visual Studio 2010 аргументы командной строки не указаны, в окне
консоли они, соответственно, тоже появляться не будут.
И С \Windo*s\j>sterTt37vcmd,e*e 1, ЯкМгУМмЯ
{***** My First сё Арр *****
Hello World I
Urg: -godmode
|Arg: -argl
flArg: -arg2
IDnve: C:\
JDrive: D:\
IDrive: E:\
■Drive: F:\
■Drive: H:\
JOS: Microsoft Windows NT 6.1.7600.0
^Number of processors: 4
■.NET Version: 4.0.20506.1
*': ' >
*!
,
.
J
Рис. З.4. Отображение переменных среды
Помимо показанных в предыдущем примере, у класса Environment имеются и
другие члены. В табл. 3.1 перечислены некоторые наиболее интересные из них; полный
список со всеми деталями можно найти в документации .NET Framework 4.0 SDK.
Таблица 3.1. Некоторые свойства System.Environment
Свойство
Описание
ExitCode
MachineName
NewLine
StackTrace
SystemDirectory
UserName
Позволяет получить или установить код возврата приложения
Позволяет получить имя текущей машины
Позволяет получить символ новой строки, поддерживаемый
в текущей среде
Позволяет получить текущие данные трассировки стека для
приложения
Возвращает полный путь к системному каталогу
Возвращает имя пользователя, который запустил данное приложение
Исходный код. Проект SimpleCSharpApp доступен в подкаталоге Chapter 3.
Класс System. Console
Почти во всех примерах приложений, создаваемых в начальных главах книги, будет
интенсивно использоваться класс System. Console. И хотя в действительности
консольный пользовательский интерфейс (Console User Interface — CUI) не является настолько
114 Часть II. Главные конструкции программирования на С#
привлекательным, как графический (Graphical User Interface — GUI) или веб-интерфейс,
ограничение первых примеров консольными программами, позволяет уделить больше
внимания синтаксису С# и ключевым характеристикам платформы .NET, а не сложным
деталям построения графических пользовательских интерфейсов или веб-сайтов.
Класс Console инкапсулирует в себе возможности, позволяющие манипулировать
вводом, выводом и потоками ошибок в консольных приложениях. В табл. 3.2
перечислены некоторые наиболее интересные его члены.
Таблица 3.2. Некоторые члены класса System. Console
Член Описание
Веер () Этот метод вынуждает консоль подавать звуковой сигнал
определенной частоты и длительности
BackgroundColor Эти свойства позволяют задавать цвет изображения и фона для теку-
ForegroundColor щего вывода. В качестве значения им может присваиваться любой из
членов перечисления ConsoleColor
Buf f erHeight Эти свойства отвечают за высоту и ширину буферной области консоли
BufferWidth
Title Это свойство позволяет устанавливать заголовок для текущей консоли
WindowHeight Эти свойства позволяют управлять размерами консоли по отношению
WindowWidth к установленному буферу
WindowTop
WindowLeft
Clear () Этот метод позволяет очищать установленный буфер и область
изображения консоли
Базовый ввод-вывод с помощью класса Console
Помимо членов, перечисленных в табл. 3.2, в классе Console имеются методы,
которые позволяют захватывать ввод и вывод; все они являются статическими и
потому вызываются за счет добавления к имени метода в качестве префикса имени самого
класса (Console). К их числу относится уже показанный ранее метод WriteLine (),
который позволяет вставлять в поток вывода строку текста (вместе с символом возврата
каретки); метод Write (), который позволяет вставлять в поток вывода текст без
символа возврата каретки; метод ReadLine (), который позволяет получать информацию из
потока ввода вплоть до нажатия клавиши <Enter>; и метод Read (), который позволяет
захватывать из потока ввода одиночный символ.
Рассмотрим пример выполнения базовых операций ввода-вывода с
использованием класса Console, для чего создадим новый проект типа Console Application по имени
BasicConsolelO и модифицируем метод Main () внутри него так, чтобы в нем
вызывался вспомогательный метод GetUserData():
class Program
{
static void Main(string [ ] args)
}
Console.WriteLine ("***** Basic Console I/O *****");
GetUserDataO ;
Console.ReadLine();
Глава 3. Главные конструкции программирования на С#: часть I 115
Теперь реализуем этот метод в классе Program вместе с логикой, приглашающей
пользователя вводить некоторые сведения и отображающей их на стандартном
устройстве вывода. Для примера у пользователя будет запрашиваться имя и возраст (который
для простоты будет трактоваться как текстовое значение, а не привычное числовое}.
static void GetUserData ()
{
// Получение информации об имени и возрасте.
Console.Write("Please enter your name: ") ; // Запрос на ввод имени
string userName = Console.ReadLine();
Console.Write("Please enter your age: ") ; // Запрос на ввод возраста
string userAge = Console.ReadLine();
// Изменение цвета изображения, просто ради интереса.
ConsoleColor prevColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Yellow;
// Отображение полученных сведений в окне консоли.
Console.WriteLine ("Hello {0}! You are {1} years old.", userName, userAge);
// Восстановление предыдущего цвета.
Console.ForegroundColor = prevColor;
}
После запуска этого приложения входные данные будут выводиться в окне консоли
(с использованием указанного специального цвета}.
Форматирование вывода, отображаемого в окне консоли
В ходе первых нескольких глав можно было заметить, что внутри различных
строковых литералов часто встречались обозначения вроде { 0 } и {1}. Дело в том, что в .NET
для форматирования строк поддерживается стиль, немного напоминающий стиль
оператора printf () в С. Попросту говоря, при определении строкового литерала с
сегментами данных, значения которых остаются неизвестными до этапа времени выполнения,
внутри него допускается указывать метку-заполнитель с использованием синтаксиса в
виде фигурных скобок. Во время выполнения на месте каждой такой
метки-заполнителя подставляется передаваемое в Console .WriteLine () значение (или значения}.
В качестве первого параметра методу WriteLine () всегда передается строковый
литерал, в котором могут содержаться метки-заполнители вида {0}, {1}, {2} и т.д.
Следует запомнить, что отсчет в окружаемых фигурными скобками
метках-заполнителях всегда начинается с нуля. Остальными передаваемыми WriteLine () параметрами
являются просто значения, которые должны подставляться на месте соответствующих
меток-заполнителей.
На заметку! Если количество пронумерованных уникальным образом заполнителей превышает
число необходимых для их заполнения аргументов, во время выполнения будет генерироваться
исключение, связанное с форматом.
Метка-заполнитель может повторяться в пределах одной и той же строки. Например,
для создания строки " 9, Number 9, Number 9 " можно было бы написать такой код:
// Вывод строки "9, Number 9, Number 9"
Console.WriteLine("{0}, Number {0}, Number {0}", 9);
Также следует знать о том, что каждый заполнитель допускается размещать в любом
месте внутри строкового литерала, и вовсе не обязательно, чтобы следующий после него
заполнитель имел более высокий номер. Например:
// Отображает: 20, 10, 3 0
Console.WriteLineCIl}, {0}, {2}", 10, 20, 30);
116 Часть II. Главные конструкции программирования на С#
Форматирование числовых данных
Если требуется использовать более сложное форматирование для числовых данных,
в каждый заполнитель можно включить различные символы форматирования,
наиболее полезные из которых перечислены в табл. 3.3.
Таблица 3.3. Символы для форматирования числовых данных в .NET
Символ
форматирования
Описание
С или с Применяется для форматирования денежных значений. По умолчанию этот
флаг идет перед символом локальной культуры (например, знаком доллара
[ $ ], если речь идет о культуре US English)
D или d Применяется для форматирования десятичных чисел. Этот флаг может
также задавать минимальное количество цифр для представления значения
Е или е Применяется для экспоненциального представления. Регистр этого флага
указывает, в каком регистре должна представляться экспоненциальная
константа — в верхнем (Е) или в нижнем (е)
F или f Применяется для представления числовых данных в формате с
фиксированной точкой. Этот флаг может также задавать минимальное количество цифр
для представления значения
G или g Расшифровывается как general (общий (формат)). Этот символ может
применяться для представления числа в фиксированном или экспоненциальном
формате
N или п Применяется для базового числового форматирования (с запятыми)
X или х Применяется для представления числовых данных в шестнадцатеричном
формате. В случае использования символа X в верхнем регистре, в
шестнадцатеричном представлении будут содержаться символы верхнего регистра
Все эти символы форматирования присоединяются к определенной
метке-заполнителю в виде суффикса после двоеточия (например, {0:С}, {1:d}, {2:Х}.). Для примера
изменим метод Main () так, чтобы в нем вызывалась новая вспомогательная функция
по имени FormatNumericalData (), и затем реализуем его в классе Program для
обеспечения форматирования значения с фиксированной точкой различными способами.
// Использование нескольких дескрипторов формата.
static void FormatNumericalData()
{
Console.WriteLine("The value 99999 in various formats:");
Console.WriteLineC'c format: {0:c}", 99999);
Console.WriteLine ("d9 format: {0:d9}", 99999);
Console.WriteLine ("f3 format: {0:f3}", 99999);
Console.WriteLine("n format: {0:n}", 99999);
}
// Обратите внимание, что использование X и х
// определяет, будут символы отображаться
//в верхнем или нижнем регистре.
Console.WriteLine ("E format: {0:E}", 99999)
Console.WriteLine ("e format: {0:e}", 99999)
Console.WriteLine ("X format: {0:X}", 99999)
Console.WriteLine ("x format: {0:x}", 99999)
На рис. 3.5 показан вывод этого приложения.
Глава 3. Главные конструкции программирования на С#: часть I 117
Помимо символов, позволяющих управлять
форматированием числовых данных, в .NET
поддерживается несколько лексем, которые
можно использовать в строковых литералах и
которые позволяют управлять
позиционированием содержимого и добавлением в него
пробелов. Более того, лексемы, применяемые для
числовых данных, допускается применять и
для форматирования других типов данных
(например, для перечислений или типа DateTime).
Вдобавок можно создавать специальный класс
(или структуру) и определять в нем
специальную схему форматирования за счет реализации
интерфейса ICustomFormatter.
По ходу настоящей книги будут встречаться
и другие примеры форматирования; тем, кто всерьез заинтересовался темой
форматирования строк в .NET, следует обязательно изучить посвященный форматированию
строк раздел в документации .NET Framework 4.0.
Исходный код. Проект BasicConsolelO доступен в подкаталоге Chapter 3.
Форматирование числовых данных
в приложениях, отличных от консольных
Напоследок хотелось бы отметить, что символы форматирования строк* .NET могут
использоваться не только в консольных приложениях. Тот же синтаксис форматирования
можно применять и в вызове статического метода string.Format (). Это может быть
удобно при генерации во время выполнения текстовых данных, которые должны
использоваться в приложении любого типа (например, в настольном приложении с
графическим пользовательским интерфейсом, в веб-приложении ASP.NET или веб-службах XML).
Для примера предположим, что требуется создать графическое настольное
приложение и применить форматирование к строке, отображаемой в окне сообщения внутри
него:
static void DisplayMessage ()
{
// Использование string.Format() для форматирования строкового литерала.
string userMessage = string.Format(00000 in hex is {0:x}", 100000);
// Для компиляции этой строки кода требуется
// ссылка на Sy stem. Windows. Forms .dll1
System.Windows.Forms.MessageBox.Show(userMessage);
}
Обратите внимание, что string.Format () возвращает новый объект string,
который форматируется в соответствии с предоставляемыми флагами. После этого
текстовые данные могут использоваться любым желаемым образом.
Системные типы данных и их
сокращенное обозначение в С#
Как и в любом языке программирования, в С# поставляется собственный набор
основных типов данных, которые должны применяться для представления локальных
переменных, переменных экземпляра, возвращаемых значений и входных параметров.
***** Basic Console I/O ***** ~1 ' I
Please enter your name: Saku
Please enter your age: 1
Hello Saku! You are 1 years old.
The value 99999 in various formats:
с format: $99,999.00
d9 format: 000099999
f3 format: 99999.000
n format: 99,999.00
E format: 9.999900E+004
e format: 9.999900e+004
X format: 1869F
x format: 1869f
iPress any key to continue . . . ш
Рис. З.5. Базовый консольный ввод-вывод
(с форматированием строк .NET)
118 Часть II. Главные конструкции программирования на С#
Однако в отличие от других языков программирования, в С# эти ключевые слова
представляют собой нечто большее, чем просто распознаваемые компилятором лексемы.
Они, по сути, представляют собой сокращенные варианты обозначения полноценных
типов из пространства имен System. В табл. 3.4 перечислены эти системные типы
данных вместе с охватываемыми ими диапазонами значений, соответствующими
ключевыми словами на С# и сведениями о том, отвечают ли они требованиям общеязыковой
спецификации CLS (Common Language Specification).
На заметку! Как рассказывалось в главе 1, с кодом.NET, отвечающим требованиям CLS, может
работать любой управляемый язык программирования. В случае включения в программы данных,
которые не соответствуют требованиям CSL, другие языки могут не иметь возможности
использовать их.
Таблица 3.4. Внутренние типы данных С#
Сокращенный
вариант
обозначения в С#
bool
sbyte
byte
short
ushort
int
uint
long
ulong
char
float
double
decimal
Отвечает
ли
требованиям CLS
Да
Нет
Да
Да
Нет
Да
Нет
Да
Нет
Да
Да
Да
Да
Системный тип
System.Boolean
System.SByte
System.Byte
System.Int16
System.UIntl6
System.Int32
System.UInt32
System.Int64
System.UInt64
System.Char
System.Single
System.Double
System.Decimal
Диапазон значений
true или false
от-128
ДО 127
отО
до 255
от -32 768
до 32 767
отО
до 65 535
от-2 147 483 648
до 2 147 483 647
отО
до 4 294 967 295
от -9 223 372 036 854
775 808 до
9 223 372 036 854 775 807
отО до
18 446 744073 709 551615
от U+0000
дои+ffff
от+1,5x105
до 3,4x1038
от 5,0x1024
до 1,7x10308
от ±1,0x1 Ое-28
до ±7,9x1028
Описание
Представляет
признак
истинности или ложности
8-битное число
со знаком
8-битное число
без знака
16-битное число
со знаком
16-битное число
без знака
32-битное число
со знаком
32-битное число
без знака
64-битное число
со знаком
64-битное число
без знака
Одиночный
16-битный символ
Unicode
32-битное число с
плавающей точкой
64-битное число с
плавающей точкой
96-битное число
со знаком
Глава 3. Главные конструкции программирования на С#: часть I 119
Окончание табл. 3.4
Сокращенный Отвечает
вариант ли требова- Системный тип Диапазон значений Описание
обозначения в С# . ниям CLS
string Да System.String Ограничивается объе- Представляет ряд
мом системной памяти символов в
формате Unicode
object Да System.Object Позволяет сохранять Служит базовым
любой тип в объектной классом для всех
переменной типов в мире .NET
На заметку! По умолчанию число с плавающей точкой трактуется как относящееся к типу double.
Из-за этого для объявления переменной типа float сразу после числового значения должен
указываться суффикс f или F (например, 5. 3F). Неформатированные целые числа по
умолчанию трактуются как относящиеся к типу данных int. Поэтому для типа данных long
необходимо использовать суффикс 1 или L (например, 4L).
Каждый из числовых типов, такой как short или int. отображается на
соответствующую структуру в пространстве имен System. Структуры, попросту говоря,
представляют собой типы значений, которые размещаются в стеке. Типы string и object,
с другой стороны, являются ссылочными типами, а это значит, что данные,
сохраняемые в переменных такого типа, размещаются в управляемой куче. Более подробно о
типах значений и ссылочных типах будет рассказываться в главе 4. Пока что важно
просто понять то, что типы-значения могут размещаться в памяти очень быстро и
обладают фиксированным и предсказуемым временем жизни. ,
Объявление и инициализация переменных
При объявлении локальной переменой (например, переменной, действующей в
пределах какого-то члена) должен быть указан тип данных, за которым следует имя самой
переменной. Чтобы посмотреть, как это выглядит, давайте создадим новый проект типа
Console Application по имени BasicDataTypes и модифицируем класс Program так,
чтобы в нем использовался следующий вспомогательный метод, вызываемый в Main ():
static void LocalVarDeclarations()
{
Console.WriteLine("=> Data Declarations:"); // Объявления данных
// Локальные переменные объявляются следующим образом:
// типДанных имяПеременной;
int mylnt;
string myString;
Console.WriteLine();
}
Следует иметь в виду, что в случае использования локальной переменной до
присваивания ей начального значения компилятор сообщит об ошибке. Поэтому
рекомендуется всегда присваивать начальные значения локальным элементам данных во время
их объявления. Делать это можно как в одной строке, так и в двух, разнося объявление
и присваивание на два отдельных оператора кода.
static void LocalVarDeclarations()
{
Console.WriteLine("=> Data Declarations:"); // Объявления данных
120 Часть II. Главные конструкции программирования на С#
// Локальные переменные объявляются и инициализируются следующим образом:
// типДанных имяПеременной = начальноеЗначение;
int mylnt = 0;
// Объявлять локальные переменные и присваивать им начальные
// значения можно также в двух отдельных строках.
string myString;
myString = "This is my character data";
Console.WriteLine();
}
Допускается объявление сразу нескольких переменных одинакового базового типа в
одной строке кода, как показано ниже на примере трех переменных типа bool:
static void LocalVarDeclarations ()
{
Console.WriteLine ("=> Data Declarations:");
int mylnt = 0;
string myString;
myString = "This is my character data";
// Объявление трех переменных типа bool в одной строке.
bool Ы = true, Ь2 = false, ЬЗ = Ы;
Console.WriteLine();
}
Поскольку ключевое слово bool в С# является сокращенным вариантом
обозначения такой структуры из пространства имен System, как Boolean, размещать любой тип
данных можно также с использованием его полного имени (естественно, это же
касается всех остальных ключевых слов, представляющих типы данных в С#). Ниже
приведена окончательная версия реализации LocalVarDeclarations ().
static void LocalVarDeclarations ()
{
Console.WriteLine ("=> Data Declarations:");
Console.WriteLine ( "=> Объявления данных:");
// Локальные переменные объявляются и инициализируются следующим образом:
// типДанных имяПеременной = начальноеЗначение;
int mylnt = 0;
string myString;
myString = "This is my character data";
// Объявление трех переменных типа bool в одной строке.
bool Ы = true, Ь2 = false, ЬЗ = Ы;
// Использование типа данных System для объявления переменной bool.
System .'Boolean Ь4 = false;
Console.WriteLine ("Your data: {0}, {1}, {2}, {3}, {4}, {5}",
mylnt, myString, Ы, Ь2, ЬЗ, Ь4);
Console.WriteLine();
}
Внутренние типы данных и операция new
Все внутренние (intrinsic) типы данных поддерживают так называемый
конструктор по умолчанию (см. главу 5). Это позволяет создавать переменные за счет
использования ключевого слова new и тем самым автоматически устанавливать для них
значения, которые являются принятыми для них по умолчанию:
Глава 3. Главные конструкции программирования на С#: часть I 121
• значение false для переменных типа bool;
• значение 0 для переменных числовых типов (или 0. О для типов с плавающей
точкой);
• одиночный пустой символ для переменных типа string;
• значение 0 для переменных типа Biglnteger;
• значение 1/1/0001 12 : 00 : 00 AM для переменных типа DateTime;
• значение null для переменных типа объектных ссылок (включая string).
На заметку! Упомянутый в предыдущем списке тип данных Biglnteger является нововведением
в .NET 4.0 и будет более подробно рассматриваться далее в главе.
Хотя код на С# в случае использования ключевого слова new при создании
переменных базовых типов получается более громоздким, синтаксически он вполне корректен,
как, например, приведенный ниже код:
static void NewingDataTypes ()
{
Console.WriteLine ("=> Using new to create variables:");
// Использование ключевого слова
// new для создания переменных
bool b = new bool (); // Установка в false.
int i = new int() ; // Установка в 0.
double d = new double () ; // Установка в 0.
DateTime dt = new DateTime(); // Установка в 1/1/0001 12:00:00 AM
Console.WriteLine ("{0}, {1}, {2}, {3}", b, l, d, dt) ;
Console.WriteLine();
}
Иерархия классов типов данных
Очень интересно отметить то, что даже элементарные типы данных в .NET имеют
вид иерархии классов. Если вы не знакомы с концепциями наследования, ищите всю
необходимую информацию в главе 6. Пока важно усвоить лишь то, что типы, которые
находятся в самом верху иерархии, обеспечивают некоторое поведение по умолчанию,
которое передается унаследованным от них типам. На рис. 3.6 схематично показаны
отношения между ключевыми системными типами.
Обратите внимание, что каждый из этих типов в конечном итоге наследуется от
класса System.Object, в котором содержится набор методов (таких как ToString (),
Equals () и GetHashCode ()), являющихся общими для всех поставляемых в
библиотеках базовых классов .NET типов (все эти методы подробно рассматриваются в главе 6).
Также важно отметить, что многие из числовых типов данных унаследованы от
класса System.ValueType. Потомки ValueType автоматически размещаются в стеке
и потому обладают очень предсказуемым временем жизни и являются довольно
эффективными. Типы, у которых в цепочке наследования не присутствует класс System.
ValueType (вроде System.Type, System.String, System.Array, System.Exception и
System. Delegate), в стеке не размещаются, а попадают в кучу и подвергаются
автоматической сборке мусора.
Не погружаясь глубоко в детали классов System. Object и System. ValueType,
сейчас главное уяснить то, что поскольку любое ключевое слово в С# (например, int)
представляет собой сокращенный вариант обозначения соответствующего системного
типа (в данном случае System. Int32), приведенный ниже синтаксис является вполне
допустимым.
122 Часть II. Главные конструкции программирования на С#
Object
Туре
String
Array
Exception
Delegate
MulticastDelegate
ValueType
Любой тип,
унаследованный
от ValueType,
является
структурой или
перечислением,
а не классом.
Boolean
Byte
Char
Decimal
Double
Intl6
Int32
Int64
SByte
Перечисления и структуры
Uintl6
UInt32
UInt64
Void
DateTime
Guid
TimeSpan
Single
Рис. З.6. Иерархия классов системных типов
Причина в том, что тип System. Int32 (представляемый как int в С#) в конечном
итоге все равно унаследован от класса System.Object и, следовательно, в нем может
вызываться любой из его общедоступных членов с помощью такой вспомогательной
функции.
static void ObjectFunctionality ()
{
Console.WriteLine("=> System.Object Functionality:");
// Функциональные возможности System.Object
// Ключевое слово int в С# в действительности представляет
// собой сокращенный вариант обозначения типа System.Int32,
// который наследует от System.Object следующие члены:
Console.WriteLine (2.GetHashCodeO = {0}", 12.GetHashCode() ) ;
Console.WriteLine(2.EqualsB3) = {0}" , 12.EqualsB3) ) ;
Console.WriteLine(2.ToStringO = {0}", 12.ToString());
Console.WriteLine(2.GetType() = {0}", 12.GetType());
Console.WriteLine();
Глава 3. Главные конструкции программирования на С#: часть I 123
На рис. 3.7 показано, как будет выглядеть вывод в случае вызова данного метода в
Main ().
***** Fun with Basic Data Types ***
:=> System.Object Functionality:
12.GetHashCodeO = 12
12.EqualsB3) = False
12.ToStringО = 12
12.GetType() = System.Int32
Press any key to continue . . .
Рис. З.7. Все типы (даже числовые) расширяют класс System.Object
Члены числовых типов данных
Продолжая обсуждение встроенных типов данных в С#, нельзя не упомянуть о том,
что числовые типы в .NET поддерживают свойства MaxValue и MinValue, которые
позволяют получать информацию о диапазоне значений, хранящихся в данном типе.
Помимо свойств MinValue/MaxValue каждый числовой тип может иметь и другие
полезные члены. Например, тип System. Double позволяет получать значения эпсилон
(бесконечно малое) и бесконечность (представляющие интерес для тех, кто занимается
решением математических задач). Ниже для иллюстрации приведена соответствующая
вспомогательная функция.
static void DataTypeFunctionality ()
{
Console.WriteLine("=> Data type Functionality:");
// Функциональные возможности типов данных
// Максимальное значение типа int
Console.WriteLine("Max of int: {0}", int.MaxValue);
// Минимальное значение типа int
Console.WriteLine ("Min of int: {0}", int.MinValue);
// Максимальное значение типа double
Console.WriteLine ("Max of double: {0}
// Минимальное значение типа double
Console.WriteLine ("Min of double: {0}1
double.MaxValue);
double.MinValue);
// Значение эпсилон типа double
Console.WriteLine("double.Epsilon: {0}", double.Epsilon);
// Значение плюс бесконечность типа double
Consolе.WriteLine("double.Positivelnfinity: {0}",
double.Positivelnfinity);
// Значение минус бесконечность типа double
Console.WriteLine("double.Negativelnfinity: { 0} ",
double.Negativelnfinity);
Console.WriteLine() ;
Члены System.Boolean
Теперь рассмотрим тип данных System.Boolean. Единственными значениями,
которые могут присваиваться типу bool в С#, являются true и false. Нетрудно
догадаться, что свойства MinValue и MaxValue в нем не поддерживаются, но зато
поддерживаются такие свойства, как TrueString и FalseString (которые выдают, соответственно,
124 Часть II. Главные конструкции программирования на С#
строку "True" и "False"). Чтобы стало понятнее, давайте добавим во вспомогательный
метод DataTypeFunctionality () следующие операторы кода:
Console.WriteLine("bool.Falsestring: {0 } ", bool.Falsestring);
Console.WriteLine("bool.TrueString: {0}", bool.TrueString);
На рис. 3.8 показано, как будет выглядеть вывод после вызова DataType
Functionality () BMain().
| C:\Windo*j\system32\cmd.e4e
***** Fun with Basic Data Types *****
U> Data type Functionality:
Max of int: 2147483647
Min of int: -2147483648
Max of double: 1.79769313486232E+308
Min of double: -1.79769313486232E+308
double.Epsilon: 4.94065645841247E-324
double.Posi tivelnfi ni ty: Infi ni ty
double.Negativelnfinity: -Infinity
bool.FalseString: False
bool.TrueString: True
Press any key to continue . . .
Рис. З.8. Демонстрация избранных функциональных
возможностей различных типов данных
Члены System.Char
Текстовые данные в С# представляются с помощью ключевых слов string и char,
которые являются сокращенными вариантами обозначения типов System. String и
System.Char (оба они основаны на кодировке Unicode). Как известно, string
позволяет представлять непрерывный набор символов (например, "Hello"), a char — только
конкретный символ в типе string (например, ' Н ').
Помимо возможности хранить один элемент символьных данных, тип System. Char
обладает массой других функциональных возможностей. В частности, с помощью его
статических методов можно определять, является данный символ цифрой, буквой,
знаком пунктуации или чем-то еще. Например, рассмотрим приведенный ниже метод.
static void CharFunctionality ()
{
Console.WriteLine ("=> char type Functionality:");
// Функциональные возможности типа char
char myChar = ' a' ;
Console.WriteLine("char.IsDigit ('a1) : {0}",
char.IsDigit(myChar));
Console.WriteLine("char.IsLetter('a1): {0}",
char.IsLetter(myChar));
Console.WriteLine("char.IsWhiteSpace ( 'Hello There1, 5): {0}",
char.IsWhiteSpace("Hello There", 5) ) ;
Console.WriteLine("char.IsWhiteSpace ( 'Hello There', 6): {0}",
char.IsWhiteSpace ("Hello There", 6) ) ;
Console.WriteLine("char.IsPunctuation('?'): {0}",
char.IsPunctuation('?'));
Console.WriteLine();
Как видно в этом фрагменте кода, многие из членов System.Char могут
вызываться двумя способами: с указанием конкретного символа и с указанием целой строки с
числовым индексом, ссылающимся на позицию, в которой находится проверяемый
символ.
Глава 3. Главные конструкции программирования на С#: часть I 125
Синтаксический разбор значений из строковых данных
Типы данных .NET предоставляют возможность генерировать переменную
лежащего в их основе типа на основе текстового эквивалента (т.е. выполнять синтаксический
разбор). Эта возможность чрезвычайно полезна при преобразовании некоторых из
предоставляемых пользователем данных (например, значений, выбираемых в
раскрывающемся списке внутри графического пользовательского интерфейса). Ниже приведен
пример метода ParseFromStrings (), демонстрирующий выполнение синтаксического
разбора.
static void ParseFromStrings ()
{
Console .WriteLine ( "=> Data type parsing:11);
// Синтаксический разбор типов данных
bool b = bool.Parse("True");
Console.WriteLine("Value of b: {0}", b);
double d = double.Parse("99.884");
Console.WriteLine("Value of d: {0}", d) ,
int i = int.Parse("8");
Console.WriteLine("Value of i: {0}", l) ,
char с = Char.Parse("w") ;
Console.WriteLine("Value of c: {0}", c) ,
Console.WriteLine();
}
Типы System.DateTime и System. TimeSpan
В пространстве имен System имеется несколько полезных типов данных, для
которых в С# ключевых слов не предусмотрено. К этим типам относятся структуры DateTime
и TimeSpan (а также показанные на рис. 3.6 типы System. Guid и System. Void,
изучением которых можете заняться самостоятельно).
В типе DateTime содержатся данные, представляющие конкретное значение даты
(месяц, день, год) и времени, которые могут форматироваться различными способами с
применением членов, доступных в этом типе.
static void UseDatesAndTimes ()
{
Console.WriteLine ("=> Dates and Times:");
// Отображение значений даты и времени
// Этот конструктор принимает в качестве
// аргументов сведения о годе, месяце и дне.
DateTime dt = new DateTime B010, 10, 17);
// Какой это день месяца?
Console.WriteLine("The day of {0} is {1}", dt.Date, dt.DayOfWeek);
// Сейчас месяц декабрь.
dt = dt.AddMonthsB);
Console.WriteLine("Daylight savings: {0}",
dt.IsDaylightSavingTime());
// Этот конструктор принимает в качестве аргументов
// сведения о часах, минутах и секундах.
TimeSpan ts = new TimeSpan D, 30, 0) ;
Console.WriteLine(ts);
// Вычитаем 15 минут из текущего значения TimeSpan
//и отображаем результат.
Console.WriteLine(ts.Subtract(new TimeSpan @, 15, 0) ) ) ;
}
126 Часть II. Главные конструкции программирования на С#
Пространство имен System.Numerics в .NET 4.0
В версии .NET 4.0 предлагается новое пространство имен под названием System.
Numerics, в котором определена структура по имени Biglnteger. Тип данных
Biglnteger служит для представления огромных числовых значений (вроде
национального долга США), не ограниченных ни верхней, ни нижней фиксированной границей.
На заметку! В пространстве System. Numerics доступна и вторая структура по имени Complex,
которая позволяет моделировать математически сложные числовые данные (такие как мнимые
и реальные числа, или гиперболические тангенсы). Дополнительные сведения об этой
структуре можно найти в документации .NET Framework 4.0 SDK.
Хотя в большинстве приложений .NET необходимость в использовании структуры
Biglnteger может никогда не возникать, если все-таки это случится, в первую очередь
в свой проект нужно добавить ссылку на сборку System. Numerics . dll. Выполните
следующие шаги.
1. В Visual Studio выберите в меню Project (Проект) пункт Add Reference (Добавить
ссылку).
2. В открывшемся после этого окне перейдите на вкладку .NET.
3. Найдите и выделите сборку System. Numerics в списке представленных
библиотек.
4. Щелкните на кнопке ОК.
После этого добавьте следующую директиву в файл, в котором будет использоваться
тип данных Biglnteger:
// Здесь определен тип Biglnteger:
using System.Numerics;
Теперь можно создать переменную Biglnteger с использованием операции new.
Внутри конструктора можно указать числовое значение, в том числе с плавающей
точкой. Вспомните, что при определении целочисленный литерал (вроде 500) по умолчанию
трактуется исполняющей средой как относящийся к типу int. а литерал с плавающей
точкой (такой как 55.333) — как относящийся к типу double. Как же тогда установить
для Biglnteger большое значение, не переполняя типы данных, которые используются
по умолчанию для неформатированных числовых значений?
Простейший подход состоит в определении большого числового значения в виде
текстового литерала, который затем может быть преобразован в переменную типа
Biglnteger с помощью статического метода Parse (). При необходимости можно
также передать байтовый массив непосредственно конструктору класса Biglnteger.
На заметку! После присваивания значения переменной Biglnteger изменять ее больше не
разрешается, поскольку содержащиеся в ней данные являются неизменяемыми. Тем не менее, в
классе Biglnteger предусмотрено несколько членов, которые возвращают новые объекты
Biglnteger на основе модификаций надданными (вроде статического метода Multiply (),
который будет использоваться в следующем примере кода).
В любом случае после определения переменной Biglnteger обнаруживается, что в
этом классе доступны члены, очень похожие на членов других внутренних типов
данных в С# (например, float и int). Помимо этого в классе Biglnteger еще есть
несколько статических членов, которые позволяют применять базовые математические
выражения (такие как сложение и умножение) к переменным Biglnteger.
Глава 3. Главные конструкции программирования на С#: часть I 127
Ниже приведен пример работы с классом Biglnteger.
static void UseBiglnteger()
{
Console.WriteLine("=> Use Biglnteger:");
// Использование Biglnteger
Biglnteger biggy =
Biglnteger.Parse("9999999999999999999999999999999 999 999999999999");
Console.WriteLine("Value of biggy is {0}", biggy);
// Значение переменной biggy
Console.WriteLine("Is biggy an even value?: {0}", biggy.IsEven);
// Является ли значение biggy четным?
Console.WriteLine("Is biggy a power of two?: {0}", biggy.IsPowerOfTwo);
// Является ли biggy степенью двойки?
Biglnteger reallyBig = Biglnteger.Multiply(biggy,
Biglnteger.Parse ("88888888888888888888888 88888888888 6888888 88"));
Console.WriteLine("Value of reallyBig is {0}", reallyBig);
// Значение переменной reallyBig
}
Важно обратить внимание, что к типу данных Biglnteger применимы внутренние
математические операции в С#, такие как +, -, и *. Следовательно, вместо того чтобы
вызывать метод Biglnteger.MultiplyO для перемножения двух больших чисел,
можно использовать такой код:
Biglnteger reallyBig2 = biggy * reallyBig;
К этому моменту уже должно быть ясно, что ключевые слова, представляющие
базовые типы данных в С#, обладают соответствующим типами в библиотеках базовых
классов .NET, и что каждый из этих типов предоставляет определенные
функциональные возможности. Подробные описания всех членов этих типов данных можно найти в
документации .NET Framework 4.0 SDK.
Исходный код. Проект BasicDataTypes доступен в подкаталоге Chapter 3.
Работа со строковыми данными
В System. String предоставляется набор методов для определения длины
символьных данных, поиска подстроки в текущей строке, преобразования символов из верхнего
регистра в нижний и наоборот, и т.д. В табл. 3.5 перечислены некоторые наиболее
интересные члены этого класса.
Таблица 3.5. Избранные члены класса System. String
Член Описание
Length Свойство, которое возвращает длину текущей строки
Compare () Статический метод, который позволяет сравнить две строки
Contains () Метод, который позволяет определить, содержится ли в строке
определенная подстрока
Equals () Метод, который позволяет проверить, содержатся ли в двух строковых
объектах идентичные символьные данные
Format () Статический метод, позволяющий сформатировать строку с использованием
других элементарных типов данных (например, числовых данных или других
строк) и обозначений типа {0}, о которых рассказывалось ранее в этой главе
128 Часть II. Главные конструкции программирования на С#
Окончание табл. 3.5
Член Описание
Insert () Метод, который позволяет вставить строку внутрь другой определенной
строки
PadLef t () Методы, которые позволяют дополнить строку какими-то символами,
PadRight () соответственно, справа или слева
Remove () Методы, которые позволяют получить копию строки с соответствующими
Replace () изменениями (удалением или заменой символов)
Split () Метод, возвращающий массив string с присутствующими в данном
экземпляре подстроками внутри, которые отделяются друг от друга элементами из
указанного массива char или string
Trim () Метод, который позволяет удалять все вхождения определенного набора
символов с начала и конца текущей строки
ToUpper () Методы, которые позволяют создавать копию текущей строки в формате,
ToLower () соответственно, верхнего или нижнего регистра
Базовые операции манипулирования строками
Работа с членами System. String выглядит так, как и следовало ожидать. Все, что
требуется — это просто объявить переменную типа string и воспользоваться
предоставляемой этим типом функциональностью через операцию точки. Однако при этом
следует иметь в виду, что некоторые из членов System. String представляют собой
статические методы и потому должны вызываться на уровне класса (а не объекта). Для примера
давайте создадим новый проект типа Console Application по имени FunWithStrings,
добавим в него следующий метод и вызовем его в Main () :
static void BasicStringFunctionality ()
{
Console.WriteLme ("=> Basic String functionality:11);
// Базовые функциональные возможности типа String
string firstName = "Freddy";
Console. WriteLme ("Value of firstName: { 0 } " , firstName);
// Значение переменной firstName
Console .WriteLme ("firstName has {0} characters.", firstName .Length) ;
// Длина значения переменной firstname
Console. WriteLme ("firstName in uppercase: {0}", {0}", firstName .ToUpper ()) ;
// Значение переменной firstName в верхнем регистре
Console .WriteLme ( "firstName in lowercase: {0}", {0}", firstName .ToLower ()) ;
// Значение переменной firstName в нижнем регистре
Console .WriteLme ("firstName contains the letter y? : {0}", {0}",
// Содержитеч ли в значении firstName буква у
firstName.Contains("у"));
Console.WriteLme ("firstName after replace: {0}", firstName.Peplace ("dy", "") ) ;
// Значение firstName после замены
Console . WriteLme () ;
}
Здесь объяснять особо нечего: в приведенном методе на локальной переменной типа
string производится вызов различных членов вроде ToUpper () и Contains () для
получения различных форматов и выполнения различных преобразований. На рис. 3.9
показано, как будет выглядеть вывод.
Глава 3. Главные конструкции программирования на С#: часть I 129
| CWindowj\iystem32\cmd.exe
Т ' _i | П^^Д'га^Г
I***** Fun with Strings *****
U> Basic String functionality:
|lvalue of firstName: Freddy
TfirstName has 6 characters.
firstName in uppercase: FREDDY
rfirstName in lowercase: freddy
rfirstName contains the letter y?:
firstName after replace: Fred
Рис. З.9. Базовые операции манипулирования строками
Вывод, получаемый в результате вызова метода Replace (), может привести в
некоторое замешательство. Переменная f irstName на самом деле не изменилась, а
вместо этого метод возвращает обратно новую строку в измененном формате. Мы еще
вернемся к неизменяемой природе строк немного позже, после исследования ряда других
моментов.
Конкатенация строк
Переменные string могут сцепляться вместе для создания строк большего размера
с помощью такой поддерживаемой в С# операции, как +. Как известно, подобный прием
называется конкатенацией строк. Для примера рассмотрим следующую
вспомогательную функцию:
static void StringConcatenation ()
{
Console. WriteLine ("=> String concatenation:11);
// Конкатенация строк
string si = "Programming the " ;
string s2 = "PsychoDrill (PTP)";
string s3 = si + s2;
Console.WriteLine (s3) ;
Console.WriteLine ();
}
Возможно, будет интересно узнать, что символ + в С# при обработке компилятором
приводит к добавлению вызова статического метода String. Concat (). После
компиляции приведенного выше кода и открытия результирующей сборки в утилите ildasm. ехе
(см. главу 1) можно будет увидеть такой CIL-код, как показан на рис. 3.10.
ffl FunWithStrings.Prognim:^StringConcetenetion: voidQ
Find Find Next
IL 000b: nop
IL_Mtc: ldstr
IL 0011: stlOC.t
IL_W2: ldstr
IL_M17: stloc.1
IL 0018: ldlOC.e
IL 0019: ldloc.1
=1
"Programing the "
"PsychoDrill (PTP)"
IL_M1f:
IL 0020:
stloc.2
ldloc.2
nil 11mn run
Рис. 3.10. Операция + в С# приводит к добавлению вызова метода String. Concat ()
По этой причине конкатенацию строк также можно выполнять вызовом метода
String. Concat () напрямую (что на самом деле особых преимуществ не дает, а
фактически лишь увеличивает количество подлежащих вводу строк кода):
130 Часть II. Главные конструкции программирования на С#
static void StringConcatenation ()
{
Console.WriteLine("=> String concatenation:");
string si = "Programming the " ;
string s2 = "PsychoDrill (РТР)"; ^
string s3 = String.Concat(si, s2) ;
Console.WriteLine(s3);
Console.WriteLine() ;
}
Управляющие последовательности символов
Как и в других языках на базе С, в С# строковые литералы могут содержать
различные управляющие последовательности символов (escape characters), которые
позволяют уточнять то, как символьные данные должны выводиться в выходном потоке.
Начинается каждая такая управляющая последовательность с символа обратной косой
черты, за которым следует интерпретируемый знак. В табл. 3.6 перечислены некоторые
часто применяемые управляющие последовательности.
Таблица 3.6. Управляющие последовательности, которые могут применяться
в строковых литералах
Управляющая 0писа
последовательность
\ ' Вставляет в строковый литерал символ одинарной кавычки
\ " Вставляет в строковый литерал символ двойной кавычки
\\ Вставляет в строковый литерал символ обратной косой черты. Может
быть полезной при определении путей к файлам и сетевым ресурсам
\а Заставляет систему выдавать звуковой сигнал, который в консольных
приложениях может служить своего рода звуковой подсказкой пользователю
\п Вставляет символ новой строки (на платформах Windows)
\г Вставляет символ возврата каретки
\t Вставляет в строковый литерал символ горизонтальной табуляции
Например, если необходимо, чтобы в выводимой строке после каждого слова шел
символ табуляции, можно воспользоваться управляющей последовательностью \t. Если
нужно создать один строковый литерал с символами кавычек внутри, другой — с
определением пути к каталогу и третий со вставкой трех пустых строк после вывода
символьных данных, можно применить такие управляющие последовательности, как \ ", \ \
и \п. Кроме того, ниже приведен еще один пример, в котором для привлечения
внимания каждый строковый литерал снабжен звуковым сигналом.
static void EscapeChars ()
{
Console.WriteLine ("=> Escape characters:\a");
string strWithTabs = "Model\tColor\tSpeed\tPet Name\a ";
Console.WriteLine(strWithTabs);
Console.WriteLine("Everyone loves V'Hello World\"\a ");
Console.WriteLine("C:\\MyApp\\bin\\Debug\a ") ;
// Добавить 4 пустых строки и снова выдать звуковой сигнал.
Console.WriteLine("All finished.\n\n\n\a ");
Console.WriteLine ();
Глава 3. Главные конструкции программирования на С#: часть I 131
Определение дословных строк
За счет добавления к строковому литералу префикса @ можно создавать так
называемые дословные строки (verabtim string). Дословные строки позволяют отключать
обработку управляющих последовательностей в литералах и выводить объекты string в
том виде, в каком они есть. Эта возможность наиболее полезна при работе со строками,
представляющими пути к каталогам и сетевым ресурсам. Таким образом, вместо
использования управляющей последовательности \ \ можно написать следующий код:
// Следующая строка будет воспроизводиться дословно,
//т.е. с отображением всех управляющих
// последовательностей символов.
Console.WriteLine(@"C:\MyApp\bin\Debug");
Также важно отметить, что дословные строки могут применяться для сбережения
пробелов в строках, разнесенных на несколько строк:
// При использовании дословных строк пробелы сохраняются.
string myLongString = @"This is a very
very
very
long string";
Console.WriteLine(myLongString);
С использованием дословных строк можно также напрямую вставлять в литералы
символы двойной кавычки, просто дублируя лексему ":
Console. WriteLine (@"Cerebus said ""Darrr! Pret-ty sun-sets1111");
Строки и равенство
Как будет подробно объясняться в главе 4, ссылочный тип (reference type)
представляет собой объект, размещаемый в управляемой куче, которая подвергается
автоматическому процессу сборки мусора. По умолчанию при выполнении проверки на предмет
равенства ссылочных типов (с помощью таких поддерживаемых в С# операций, как == и ! =)
значение true будет возвращаться в том случае, если обе ссылки указывают на один
и тот же объект в памяти. Хотя string представляет собой ссылочный тип, операции
равенства для него были переопределены так, чтобы давать возможность сравнивать
значения объектов string, а не сами объекты в памяти, на которые они ссылаются.
static void StringEquality ()
{
Console.WriteLine("=> String equality:");
// Равенство строк
string si = "Hello!";
string s2 = "Yo'";
Console.WriteLine ("si = {0}", si);
Console.WriteLine ("s2 = {0}", s2) ;
Console.WriteLine ();
// Выполнение проверки на предмет равенства данных строк.
Console.WriteLine ("si == s2: {0}", si == s2) ;
Console.WriteLine ("si == Hello!: {0}", si == "Hello!");
Console.WriteLine ("si == HELLO!: {0}", si == "HELLO!");
Console.WriteLine ("si == hello!: {0}", si == "hello!");
Console.WriteLine("si.Equals (s2) : {0}", si.Equals (s2));
Console.WriteLine("Yo.Equals (s2) : {0}", "Yo!".Equals(s2));
Console.WriteLine ();
}
132 Часть II. Главные конструкции программирования на С#
В С# операции равенства предусматривают выполнение в отношении объектов
string посимвольной проверки с учетом регистра. Следовательно, строки "Hello! ",
"HELLO ! " и "hello !" не равны между собой. Кроме того, из-за наличия у string связи
с System. String, проверку на предмет равенства можно выполнять также с помощью
поддерживаемого классом String метода Equals () и других поставляемых в нем
операций. И, наконец, поскольку каждый строковый литерал (например, "Yo") является
самым настоящим экземпляром System. String, доступ к функциональным
возможностям для работы со строками можно получать также для фиксированной
последовательности символов.
Неизменная природа строк
Один из интересных аспектов System. String состоит в том, что после присваивания
объекту string первоначального значения символьные данные больше изменяться не
могут. На первый взгляд это может показаться заблуждением, ведь строкам постоянно
присваиваются новые значения, а в типе System. String доступен набор методов,
которые, похоже, только то и делают, что позволяют изменять символьные данные тем или
иным образом (например, преобразовывать их в верхний или нижний регистр). Если,
однако, присмотреться внимательнее к тому, что происходит "за кулисами", то можно
будет увидеть, что методы типа string на самом деле возвращают совершенно новый
объект string в измененном виде:
static void StringsArelmmutable ()
{
// Установка первоначального значения для строки.
string si = "This is my string.";
Console.WriteLine ("si = {0}", si);
// Преобразование si в верхний регистр?
string upperString = si.ToUpper();
Console.WriteLine("upperString = {0}", upperString);
// Нет! si по-прежнему остается в том же формате!
Console.WriteLine("si = {0}", si);
}
На рис. 3.11 показано, как будет выглядеть вывод приведенного выше кода, по
которому легко убедиться в том, что исходный объект string (si) не преобразуется в
верхний регистр при вызове ToUpper (), а вместо этого возвращается его копия в
измененном соответствующим образом формате.
Рис. 3.11. Строки остаются неизменными
Тот же самый закон неизменности строк действует и при использовании в С#
операции присваивания. Чтобы удостовериться в этом, давайте закомментируем (или
удалим) весь существующий код в StringsArelmmutable () (чтобы уменьшить объем
генерируемого CIL-кода) и добавим следующие операторы:
Глава 3. Главные конструкции программирования на С#: часть I 133
static void StringArelmmutable ()
{
string s2 = "My other string";
s2 = "New string value";
}
Скомпилируем приложение и загрузим результирующую сборку в утилиту
ildasm.exe (см. главу 1). На рис. 3.12 показан CIL-код, который будет сгенерирован
для метода StringsArelmmutable ().
Г /7 FunWithStrings.Program:5trmgsAreImmut»bte: votdO LEili^LJeSel 1
PRndi Findltort ~
.method priuate hidebysig static uoid StringsArelmmutableO cil managed
I !<
I // Code size 21 @x15)
I.maxstack 1
.locals init ([0] string s2)
II 0000: nop
IL 0001: ldstr "Ну other string"
IL_O0M: stloc.O
IL 0007: ldstr "New string ualue"
IL 000c: stloc.O
IL OOOd: ldloc.8
IL_tlte: call uoid [mscorlib]System.Console::WriteLine(string)
IL_0013: nop
IL_M14: ret
I i> // end of method Program::StringsflreImmutable
Рис. 3.12. Присваивание значения объекту string приводит
к созданию нового объекта string
Хотя низкоуровневые детали CIL пока подробно не рассматривались, важно обратить
внимание на наличие многочисленных вызов кода операции ldstr (загрузка строки).
Этот код операции ldstr в CIL предусматривает выполнение загрузки нового объекта
string в управляемую кучу. В результате предыдущий объект, в котором содержалось
значение "My other string", будет в конечном итоге удален сборщиком мусора.
Так что же конкретно необходимо вынести из всего этого? Если кратко: класс string
может оказываться неэффективным и приводить к "разбуханию" кода в случае
неправильного использования, особенно при выполнении конкатенации строк. Когда же
необходимо представлять базовые символьные данные, такие как номер карточки
социального страхования, имя и фамилия или простые фрагменты текста, используемые
внутри приложения, он является идеальным вариантом.
В случае создания приложения, предусматривающего интенсивную работу с
текстовыми данными, представление обрабатываемых данных с помощью объектов string
будет очень плохой идеей, поскольку практически наверняка (и часто не напрямую)
будет приводить к созданию ненужных копий данных string. Как тогда должен
поступать программист? Ответ на этот вопрос ищите ниже.
Тип System.Text.StringBuilder
Из-за того, что тип string может оказаться неэффективным при необдуманном
использовании, в библиотеках базовых классов .NET поставляется еще пространство имен
System.Text. Внутри этого (достаточно небольшого) пространства имен предлагается
класс по имени StringBuilder. Как и в классе System. String, в StringBuilder
содержатся методы, которые позволяют, например, заменять и форматировать сегменты.
Чтобы использовать этот класс в файлах кода на С#, первым делом необходимо
позаботиться об импорте следующего пространства имен:
// Здесь определен класс StringBuilder:
using System.Text;
134 Часть II. Главные конструкции программирования на С#
Уникальным в StringBuilder является то, что при вызове его членов
производится непосредственное изменение внутренних символьных данных объекта (что, конечно
же, более эффективно), а не получение копии этих данных в измененном формате. При
создании экземпляра StringBuilder начальные значения для объекта можно задавать
с помощью не одного, а нескольких конструкторов. Тем, кто не знаком с понятием
конструктора, сейчас важно уяснить лишь то, что конструкторы позволяют создавать
объект с определенным начальным состоянием за счет применения ключевого слова new.
Ниже приведен пример применения StringBuilder.
static void FunWithStringBuilder ()
{
Console.WriteLine ("=> Using the StringBuilder:");
StringBuilder sb = new StringBuilder("**** Fantastic Games ****");
sb.Append("\n");
sb.AppendLine("Half Life");
sb.AppendLine("Beyond Good and Evil");
sb.AppendLine("Deus Ex 2");
sb.AppendLine("System Shock") ;
Console.WriteLine(sb.ToString());
sb.Replace(", "Invisible War");
Console.WriteLine(sb.ToString());
Console.WriteLine("sb has {0} chars.", sb.Length); .
Console.WriteLine();
}
Здесь сначала создается объект StringBuilder с первоначальным значением
н**** Fantastic Games****". Далее можно добавлять символы к внутреннему
буферу, а также заменять (или удалять) каким угодно образом. По умолчанию изначально в
StringBuilder может храниться строка длиной не более 16 символов (она
автоматически расширяется по мере необходимости), однако это исходное значение легко
изменить, передавая конструктору соответствующий дополнительный аргумент:
// Создание объекта StsingBuilder с исходным
// размером в 256 символов.
StringBuilder sb = new StringBuilder("**** Fantastic Games ****", 256);
В случае добавления большего количества символов, чем было указано в качестве
лимита, объект StringBuilder будет копировать свои данные в новый экземпляр и
создавать для него буфер размером, равным указанному лимиту. На рис. 3.13 показан
вывод приведенной выше вспомогательной функции.
| C:\Windows\system32\c/nd.exe
***** Fun with Strings *****
j=> Using the StringBuilder:
!**** Fantastic Games ****
Half Life
Beyond Good and Evil
Deus Ex 2
, System Shock
J**** Fantastic Games ****
pHalf Life
pBeyond Good and Evil
Deus Ex Invisible War
System Shock
;b as 96 chars.
Рис. 3.13. Класс StringBuilder работает
более эффективно, чем string
Глава 3. Главные конструкции программирования на С#: часть I 135
Исходный код. Проект FunWithStrings доступен в подкаталоге Chapter 3.
Сужающие и расширяющие
преобразования типов данных
Теперь, когда известно, как взаимодействовать со встроенными типами данных,
давайте рассмотрим связанную тему — преобразование типов данных. Создадим новый
проект типа Console Application по имени TypeConversions и определим в нем
следующий класс:
class Program
{
static void Main(string[ ] args)
{
Console.WriteLine ("***** Fun with type conversions *****");
// Добавление двух переменных типа short
//и отображение результата.
short numbl = 9, numb2 = 10;
Console.WriteLine("{0} + {1} = {2}", numbl, numb2, Add(numbl, numb2));
Console.ReadLine();
}
static int Add(int x, int y)
{ return x + y; }
}
Обратите внимание на то, что метод Add () ожидает поступления двух параметров
типа int. Тем не менее, в методе Main.() ему на самом деле передаются две переменных
типа short. Хотя это может показаться несоответствием типов, программа будет
компилироваться и выполняться без ошибок и возвращать в результате, как и ожидалось,
значение 19.
Причина, по которой компилятор будет считать данный код синтаксически
корректным, связана с тем, что потеря данных здесь невозможна. Поскольку максимальное
значение C2 767), которое может содержать тип short, вполне вписывается в рамки
диапазона типа int (максимальное значение которого составляет 2 147 483 647),
компилятор будет неявным образом расширять каждую переменную типа short до типа
int. Формально термин "расширение" применяется для обозначения неявного
восходящего приведения (upward cast), которое не приводит к потере данных.
На заметку! Расширяющие и сужающие преобразования, поддерживаемые для каждого типа
данных в С#, описаны в разделе "Type Conversion Tables" ("Таблицы преобразования типов")
документации .NET Framework 4.0 SDK.
Хотя в предыдущем примере подобное неявное расширение типов было полезно,
в других случаях оно может стать источником возникновения ошибок компиляции.
Например, давайте установим для numbl и numb2 значения, которые (при сложении
вместе) будут превышать максимальное значение short, а также сделаем так, чтобы
значение, возвращаемое методом Add (), сохранялось в новой локальной переменной
short, а не просто напрямую выводилось в окне консоли:
static void Main(string [ ] args)
{
Console.WriteLine("***** Fun with type conversions *****");
136 Часть II. Главные конструкции программирования на С#
// Следующий код вызовет ошибку компиляции!
short numbl = 30000, numb2 = 30000;
short answer = Add(numbl, numb2);
Console.WriteLine ("{0} + {1} = {2}", numbl, numb2, answer);
Console.ReadLine();
}
В таком случае компилятор сообщит об ошибке:
Cannot implicitly convert type 'int' to 'short1. An explicit conversion exists
(are you missing a cast?)
He удается неявным образом преобразовать тип 'int* в 'short'. Существует
возможность выполнения преобразования явным образом (не была ли пропущена
операция по приведению типов?)
Проблема в том, что хотя метод Add () способен возвращать переменную int со
значением 60000 (поскольку это значение вполне вписывается в диапазон допустимых
значений типа System. Int32), сохранение этого значения в переменной типа short
невозможно, потому что оно выходит за рамки допустимого диапазона для этого типа.
Формально это означает, что CLR-среде не удастся применить операцию сужения. Как
не трудно догадаться, операция сужения представляет собой логическую
противоположность операции расширения, поскольку предусматривает сохранение большего
значения внутри переменной меньшего типа данных.
Важно отметить, что все сужающие преобразования приводят к выдаче
компилятором ошибки, даже когда имеются веские основания полагать, что операция
сужающего преобразования должна на самом деле пройти успешно. Например, следующий код
тоже приведет к генерации компилятором ошибки:
// Еще один код, при выполнении которого
// компилятор будет сообщать об ошибке!
static void NarrowingAttempt()
{
byte myByte = 0;
int mylnt = 200;
myByte = mylnt;
Console.WriteLine("Value of myByte: {0}", myByte);
}
Здесь значение, содержащееся в переменной типа int (по имени mylnt),
вписывается в диапазон допустимых значений типа byte, следовательно, операция сужения
по идее не должна приводить к генерации ошибки во время выполнения. Однако из-за
того, что язык С# создавался с таким расчетом, чтобы он заботился о безопасности
типов, компилятор все-таки сообщит об ошибке.
Если нужно уведомить компилятор о готовности мириться с возможной в результате
операции сужения потерей данных, необходимо применить операцию явного
приведения типов, которая в С# обозначается с помощью (). Ниже показан
модифицированный код Program, а на рис. 3.14 — его вывод.
class Program
{
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with type conversions *****");
short numbl = 30000, numb2 = 30000;
// Явное приведение int к short (с разрешением потери данных) .
short answer = (short)Add(numbl, numb2);
Console.WriteLine ("{0} + {1} = {2}", numbl, numb2, answer);
Глава 3. Главные конструкции программирования на С#: часть I 137
NarrowingAttempt();
Console.ReadLine();
}
static int Add(int x, int y)
{ return x .+ y; }
static void NarrowingAttempt ()
{
byte myByte = 0;
int mylnt = 200;
// Явное приведение int к byte (без потери данных).
myByte = (byte)mylnt;
Console.WriteLine("Value of myByte: {0}", myByte);
| C:\Windows\system32\cmd.e>
1—**T.r.^^ffii^HP
***** Fun with type conversions *****
30000 + 30000 = -5536
Value of myByte: 200
Eress any key to continue . . . m
■■"i '"' -7iiH
Рис. 3.14. При сложении чисел некоторые данные были потеряны
Перехват сужающих преобразований данных
Как было только что показано, явное указание операции приведения заставляет
компилятор производить операцию сужающего преобразования даже тогда, когда это
чревато потерей данных. В методе NarrowingAttempt () это не было проблемой,
поскольку значение 200 вписывается в диапазон допустимых значений типа byte. Однако
при сложении двух значений типа short в методе Main () конечный результат оказался
совершенно не приемлемым C0 000 + 30 000 = -5536?). Для создания приложений, в
которых потеря данных должна быть недопустимой, в С# предлагаются такие ключевые
слова, как checked и unchecked, которые позволяют гарантировать, что потеря данных
не окажется незамеченной.
Чтобы посмотреть, как применяются эти ключевые слова, давайте добавим в Program
новый метод, суммирующий две переменных типа byte, каждой из которых присвоено
значение, не выходящее за пределы допустимого максимума B55 для данного типа). По
идее, после сложения значений этих двух переменных (с приведением результата int к
типу byte) должна быть получена точная сумма.
static void ProcessBytes ()
{
byte Ы = IOC-
byte Ь2 = 250;
byte sum = (byte)Add(bl, b2);
// В sum должно содержаться значение 350.
// Однако там оказывается значение 94!
Console.WriteLine("sum = {0}", sum);
}
Удивительно, но при изучении вывода данного приложения обнаруживается, что в
sum содержится значение 94 (а не 350, как ожидалось). Объясняется это очень просто.
Из-за того, что в System.Byte может храниться только значение из диапазона от 0 до
255 включительно, в sum будет помещено значение переполнения C50 - 256 = 94).
138 Часть II. Главные конструкции программирования на С#
По умолчанию, в случае, когда не предпринимается никаких соответствующих
исправительных мер, условия переполнения (overflow) и потери значимости (underflow)
происходят без выдачи ошибки. Обрабатывать условия переполнения и потери
значимости в приложении можно двумя способами. Это можно делать вручную, полагаясь на
свои знания и навыки в области программирования.
Недостаток такого подхода в том, что даже в случае приложения максимальных усилий
человек все равно остается человеком, и какие-то ошибки могут ускользнуть от его глаз.
К счастью, в С# предусмотрено ключевое слово checked. Если оператор (или блок
операторов) заключен в контекст checked, компилятор С# генерирует дополнительные
CIL-инструкции, обеспечивающие проверку на предмет условий переполнения, которые
могут возникать в результате сложения, умножения, вычитания или деления двух
числовых типов данных.
В случае возникновения условия переполнения во время выполнения будет
генерироваться исключение System.OverflowException. Детали обеспечения
структурированной обработки исключения и использования в связи с этим ключевых слов try и
catch будут даны в главе 7. А пока, не вдаваясь особо в детали, можно изучить
показанный ниже модифицированный код:
static void ProcessBytes ()
{
byte Ы = IOC-
byte Ь2 = 250;
//На этот раз компилятору указывается добавлять CIL-код,
// необходимый для выдачи исключения в случае возникновения
// условий переполнения или потери значимости.
try
{
byte sum = checked ( (byte) Add (Ы, b2) ) ;
Console.WriteLine("sum = {0}", sum);
}
catch (OverflowException ex)
{
Console.WriteLine(ex.Message);
}
}
Здесь следует обратить внимание на то, что возвращаемое значение метода Add ()
было заключено в контекст checked. Благодаря этому, в связи с выходом значения sum
за пределы диапазона допустимых значений типа byte во время выполнения теперь
будет генерироваться исключение, а через свойство Message выводиться сообщение об
ошибке, как показано на рис. 3.15.
| C\Winde*s\systerri32\cmd
I***** Fun with type conversions
[30000 + 30000 = -5536
'alue of myByte: 200
rithmetic operation resulted in an overflow.
Press any key to continue . . .
Рис. 3.15. Ключевое слово checked вынуждает CLR-среду
генерировать исключения в случае потери данных
Если проверка на предмет возникновения условий переполнения должна
выполняться не для одного, а для целого блока операторов, контекст checked можно определить
следующим образом:
Глава 3. Главные конструкции программирования на С#: часть I 139
try
{
checked
{
byte sum = (byte) Add (Ы, b2) ;
Console.WriteLine("sum = {0}"
sum) ;
catch (OverflowException ex)
{
Console.WriteLine(ex.Message);
}
И в том и в другом случае интересующий код будет автоматически проверяться на
предмет возникновения возможных условий переполнения, и при обнаружении
таковых приводить к генерации соответствующего исключения.
Настройка проверки на предмет возникновения
условий переполнения в масштабах проекта
Если создается приложение, в котором переполнение никогда не должно проходить
незаметно, может выясниться, что обрамлять ключевым словом checked приходится
раздражающе много строк кода. На такой случай в качестве альтернативного
варианта в компиляторе С# поддерживается флаг /checked. При активизации этого флага
проверке на предмет возможного переполнения будут автоматически подвергаться все
имеющиеся в коде арифметические операции, без применения для каждой из них
ключевого слова checked. Обнаружение переполнения точно так же приводит к генерации
соответствующего исключения во время выполнения.
Для активизации этого флага в Visual Studio 2010 необходимо открыть
страницу свойств проекта, перейти на вкладку Build (Сборка), щелкнуть на кнопке Advanced
(Дополнительно) и в открывшемся диалоговом окне отметить флажок Check for arithmetic
overflow/underflow (Выполнять проверку на предмет арифметического переполнения и
потери значимости), как показано на рис. 3.16.
General -- ——-—
Language Version:
Internal Compiler Error Reporting: j prompt
! default
f?j Check for arithmetic overflow/underflow
[_=] Do not reference mscoriib.dll
Debug Info:
Fife Alignment
DLL Base Address:
Г"^П
Рис. 3.16. Включение функции проверки на предмет переполнения
и потери значимости в масштабах всего проекта
Ключевое слово unchecked
Теперь давайте посмотрим, что можно сделать, если функция проверки на предмет
переполнения и потери значимости в масштабах всего проекта включена, но есть ка-
140 Часть II. Главные конструкции программирования на С#
кой-то блок кода, в котором потеря данных является допустимой. Из-за того, что
действие флага /checked распространяется на всю арифметическую логику, в С#
предусмотрено ключевое слово unchecked, которое позволяет отключить выдачу связанного
с переполнением исключения в отдельных случаях. Применяется это ключевое слово
похожим на checked образом, поскольку может быть указано как для одного оператора,
так и для целого блока:
// При условии, что флаг /checked активизирован, этот
// блок не будет приводить к генерации исключения во время выполнения.
unchecked
{
byte sum = (byte) (bl + Ь2) ;
Console.WriteLine("sum = { 0} ", sum);
}
Итак, чтобы подвести итог по использованию в С# ключевых слов checked и
unchecked, следует отметить, что по умолчанию арифметическое переполнение в
исполняющей среде .NET игнорируется. Если необходимо обработать отдельные
операторы, то должно использоваться ключевое слово checked, а если нужно перехватывать
все связанные с переполнением ошибки в приложении, то понадобится активизировать
флаг /checked. Что касается ключевого слова unchecked, то его можно применять при
наличии блока кода, в котором переполнение является допустимым (и, следовательно,
не должно приводить к генерации исключения во время выполнения).
Роль класса System. Convert
В завершении темы преобразования типов данных стоит отметить, что в
пространстве имен System имеется класс по имени Convert, который тоже может применяться
для расширения и сужения данных:
static void NarrowWithConvert ()
{
byte myByte = 0;
int mylnt = 200; .
myByte = Convert.ToByte(mylnt);
Console.WriteLine("Value of myByte: {0}", myByte);
}
Одно из преимуществ подхода с применением класса System.Convert связано с
тем, что он позволяет выполнять преобразования между типами данных нейтральным
к языку образом (например, синтаксис приведения типов в Visual Basic полностью
отличается от предлагаемого для этой цели в С#). Однако, поскольку в С# есть операция
явного преобразования, использование класса Convert для преобразования типов
данных обычно является делом вкуса.
Исходный код. Проект TypeConversions доступен в подкаталоге Chapter 3.
Неявно типизированные локальные переменные
Вплоть до этого момента в настоящей главе при определении локальных
переменных тип данных, лежащий в их основе, всегда указывался явно:
static void DeclareExplicitVars ()
{
// Явно типизированные локальные переменные
// объявляются следующим образом:
// dataType variableName = mitialValue;
Глава 3. Главные конструкции программирования на С#: часть I 141
int mylnt = 0;
bool myBool = true;
string myString = "Time, marches on..." ;
}
Хотя указывать явным образом тип данных для каждой переменной всегда считается
хорошим стилем, в С# также поддерживается возможность неявной типизации
локальных переменных с помощью ключевого слова var. Ключевое слово var можно
использовать вместо указания конкретного типа данных (такого как int, bool или string).
В этом случае компилятор автоматически выводит лежащий в основе тип данных на
основе первоначального значения, которое используется для инициализации
локальных данных.
Чтобы посмотреть, как это выглядит на практике, давайте создадим новый проект
типа Console Application (Консольное приложение) по имени ImplicitlyTypedLocalVars
и объявим в нем те же локальные переменные, что использовались в предыдущем
методе, следующим образом:
static void DeclarelmplicitVars ()
{
// Неявно типизированные локальные переменные объявляются следующим образом:
// var variableName = initialValue;
var mylnt = 0;
var myBool = true;
var myString = "Time, marches on...";
}
На заметку! На самом деле лексема var ключевым словом в С# не является. С ее помощью можно
объявлять переменные, параметры и поля и не получать никаких ошибок на этапе компиляции.
При использовании этой лексемы в качестве типа данных, однако, она по контексту
воспринимается компилятором как ключевое слово. Поэтому ради простоты здесь будет применяться
термин "ключевое слово var", а не более сложное понятие "контекстная лексема var".
В этом случае компилятор имеет возможность вывести по первоначально
присвоенному значению, что переменная mylnt в действительности относится к типу System.
Int32, переменная myBool — к типу System.Boolean, а переменная myString — к типу
System. String. Чтобы удостовериться в этом, можно вывести имя типа каждой из этих
переменных посредством рефлексии. Как будет показано в главе 15, под рефлексией
понимается процесс определения состава типа во время выполнения. Например, с
помощью рефлексии можно определить тип данных неявно типизированной локальной
переменной. Для этого модифицируем наш метод, добавив в его код следующие операторы:
static void DeclarelmplicitVars ()
{
// Неявно типизированные локальные переменные.
var mylnt = 0;
var myBool = true;
var myString = "Time, marches on...";
// Вывод имен типов, лежащих в основе этих переменных.
Console.WriteLine("mylnt is a: {0}", mylnt.GetType().Name);
Console.WriteLine("myBool is a: {0}", myBool.GetType().Name);
Console.WriteLine("myString is a: {0}", myString.GetType().Name);
}
На заметку! Следует иметь в виду, что такую неявную типизацию можно использовать для любых
типов, включая массивы, обобщенные типы (см. главу 10) и пользовательские специальные
типы. Далее в книге будут встречаться и другие примеры применения неявной типизации.
142 Часть II. Главные конструкции программирования на С#
Если теперь вызвать метод DeclareImplicitVars () в Main (), то получится вывод,
показанный на рис. 3.17.
ЯВ C:\Windows\system32Vcmdexe
^mksiMia^
***** Fun with Implicit Typing *****
Ptorvlnt is a: Int32
ool is a: Boolean
tring is a: String
jPress any key to continue ....
E
Рис. 3.17. Применение рефлексии в отношении
неявно типизированных локальных переменных
Ограничения, связанные с неявно типизированными переменными
Разумеется, с использованием ключевого слова var связаны различные ограничения.
Самое первое и важное из них состоит в том, что неявная типизация применима
только для локальных переменных в контексте какого-то метода или свойства. Применять
ключевое слово var для определения возвращаемых значений, параметров или данных
полей специального типа не допускается. Например, ниже показано определение
класса, которое приведет к выдаче сообщений об ошибках на этапе компиляции:
class ThisWillNeverCompile
{
// Ошибка! var не может применяться
// для определения полей!
private var mylnt = 10;
// Ошибка! var не может применяться
// для определения возвращаемого значения
// или типа параметра!
public var MyMethod (var x, var y) {}
Кроме того, локальным переменным, объявленным с помощью ключевого слова var,
обязательно должно быть присвоено начальное значение в самом объявлении, причем
присваивать в качестве начального значения null не допускается! Последние
ограничение вполне понятно, поскольку на основании одного лишь значения null компилятор
не сможет определить, на какой тип в памяти указывает переменная.
// Ошибка! Должно быть присвоено значение!
var myData;
// Ошибка! Значение должно присваиваться в самом объявлении*
var mylnt;
mylnt = 0;
// Ошибка! Присваивание null в качестве
// начального значения не допускается!
var myObj = null;
Присваивание значения null локальной переменной с выведенным после
начального присваивания типом вполне допустимо (при условии, переменная отнесена к
ссылочному типу):
// Все в порядке, поскольку SportsCar
// является переменной ссылочного типа!
var myCar = new SportsCar ();
myCar = null;
Глава 3. Главные конструкции программирования на С#: часть I 143
Более того, значение неявно типизированной локальной переменной может быть
присвоено другим переменным, причем как неявно, так и явно типизированным:
// Здесь тоже все в порядке!
var mylnt = 0;
var anotherInt = mylnt;
string myString = "Wake up!";
var myData = myString;
Кроме того, неявно типизированную локальную переменную можно возвращать
вызывающему методу, при условии, что возвращаемый тип этого метода совпадает с тем,
что лежит в основе определенных с помощью var данных:
static int GetAnlntO
{
var retVal = 9;
return retVal;
}
И, наконец, последний, но от того не менее «важный момент: определять неявно
типизированную локальную переменную как допускающую значение null с
использованием лексемы ? в С# нельзя (типы данных, допускающие значение null,
рассматриваются в главе 4).
// Определять неявно типизированные переменные как допускающие значение null
// нельзя, поскольку таким переменным изначально не разрешено присваивать null!
var? nope = new SportsCar();
var? stillNo = 12;
var? noWay = null;
Неявно типизированные данные являются строго типизированными
Следует иметь в виду, что неявная типизация локальных переменных приводит к
получению строго типизированных данных. Таким образом, применение ключевого
слова var в С# отличается от техники, используемой в языках сценариев (таких как
JavaScript или Perl), а также от применения типа данных Variant в СОМ, где
переменная на протяжении своего существования в программе может хранить значения разных
типов (это часто называется динамической типизацией).
На заметку! В .NET 4.0 появилась возможность динамической типизации в С# с использованием
нового ключевого слова dynamic. Более подробно об этом аспекте языка будет
рассказываться в главе 18.
Выведение типа позволяет языку С# оставаться строго типизированным и
оказывает влияние только на объявление переменных во время компиляции. После этого
данные трактуются как объявленные с выведенным типом; присваивание такой
переменной значения другого типа будет приводить к возникновению ошибок на этапе
компиляции.
static void ImplicitTypinglsStrongTyping()
{
// Компилятор знает, что s имеет тип System.String.
var s = "This variable can only hold string data!";
s = "This is fine.. . ";
// Можно вызывать любой член лежащего в основе типа.
string upper = s.ToUpper();
// Ошибка! Присваивание числовых данных строке невозможно!
s = 44;
}
144 Часть II. Главные конструкции программирования на С#
Польза от неявно типизированных локальных переменных
Теперь, когда был показан синтаксис, используемый для объявления неявно
типизируемых локальных переменных, наверняка возник вопрос, в каких ситуациях его
полезно применять? Самое важное, что необходимо знать — использование var для
объявления локальных переменных просто так особой пользы не приносит, а в
действительности может даже вызвать путаницу у тех, кто будет изучать данный код,
поскольку лишает возможности быстро определить тип данных и, следовательно, понять, для
чего предназначена переменная. Поэтому если точно известно, что переменная должна
относиться к типу int, лучше сразу объявить ее с указанием этого типа.
В наборе технологий LINQ, как будет показано в начале главы 13, применяются так
называемые выражения запросов (query expression), которые позволяют получать
динамически создаваемые результирующие наборы на основе формата запроса. В таких
выражениях неявная типизация чрезвычайно полезна, так как в некоторых случаях явное
указание типа попросту не возможно. Ниже приведен соответствующий пример кода
LINQ, в котором предлагается определить базовый тип данных subset:
static void QueryOverlnts ()
{
int[] numbers = { 10, 20, 30, 40, 1, 2, 3, 8 };
// Запрос LINQ
var subset = from i in numbers where i < 10 select i;
Console.Write("Values in subset: " ) ;
//Значения в subset
foreach (var i in subset)
{
Console.Write("{0} ", l) ;
}
Console.WriteLine ();
// К какому типу относится subset?
Console.WriteLine("subset is a: {0}", subset.GetType().Name);
Console.WriteLine("subset is defined in: {0}", subset.GetType().Namespace);
}
Если интересно, проверьте свои предположения по поводу типа данных subset,
выполнив предыдущий код (subset целочисленным массивом не является). В любом
случае должно быть понятно, что неявная типизация занимает свое место в рамках набора
технологий LINQ. На самом деле, можно даже утверждать, что единственным случаем,
когда применение ключевого слова var вполне оправдано, является определение
данных, возвращаемых из запроса LINQ. Нужно всегда помнить о том, что если в
точности известно, что переменная должна представлять собой переменную типа int, лучше
всегда сразу же объявлять ее с указанием этого типа. Излишняя неявная типизация
(с помощью var) считается плохим стилем в производственном коде.
Исходный код. Проект ImplicitlyTypedLocalVars доступен в подкаталоге Chapter 3.
Итерационные конструкции в С#
Все языки программирования предлагают способы для повторения блоков кода
до тех пор, пока не будет соблюдено какое-то условие завершения. Какой бы язык не
использовался ранее, операторы, предлагаемые для осуществления итераций в С#, не
должны вызывать особых недоумений и требовать особых объяснений. В частности,
четырьмя поддерживаемыми в С# для выполнения итераций конструкциями являются:
Глава 3. Главные конструкции программирования на С#: часть I 145
• цикл for;
• цикл foreach/in;
• цикл while;
• цикл do/while.
Давайте рассмотрим каждую из этих конструкций по очереди, предварительно
создав новый проект типа Console Application по имени IterationsAndDecisions.
Цикл for
Если требуется проходить по блоку кода фиксированное количество раз, приличную
гибкость демонстрирует оператор for. Он позволяет указать, сколько раз должен
повторяться блок кода, а также задать конечное условие, при котором его выполнение
должно быть завершено. Ниже приведен пример применения этого оператора:
// База для цикла.
static void ForAndForEachLoop ()
{
// Переменная i доступна только в контексте этого цикла for.
for(int i = 0; i < 4; i + + )
{
Console.WriteLine("Number is: {0} " , 1) ;
}
// Здесь переменная i уже не доступна.
}
Все приемы, освоенные в языках С, C++ и Java, применимы также при создании
операторов f or в С#. Как и в других языках, в С# можно создавать сложные конечные
условия, определять бесконечные циклы и использовать ключевые слова goto, continue и
break. Предполагается, что читатель уже знаком с данной итерационной конструкцией.
Дополнительные сведения об использовании ключевого слова for в С# можно найти в
документации .NET Framework 4.0 SDK.
Цикл f oreach
Ключевое слово f oreach в С# позволяет проходить в цикле по всем элементам
массива (или коллекции, как будет показано в главе 10} без проверки его верхнего предела.
Ниже приведено два примера использования цикла f oreach, в одном из которых
производится проход по массиву строк, а в другом — проход по массиву целых чисел.
// Проход по элементам массива с помощью foreach.
static void ForAndForEachLoop ()
{
string[] carTypes = {"Ford", "BMW", "Yugo", "Honda" };
foreach (string с in carTypes)
Console.WriteLine(c);
int[] mylnts = { 10, 20, 30, 40 };
foreach (int 1 in mylnts)
Console.WriteLine(l);
}
Помимо прохода по простым массивам, foreach также позволяет осуществлять
итерации по системным и определяемым пользователем коллекциям. Рассмотрение
соответствующих деталей откладывается до главы 9, поскольку этот способ применения
foreach требует понимания особенностей программирования с использованием
интерфейсов, таких как IEnumerator и IEnumerable.
146 Часть II. Главные конструкции программирования на С#
Использование var в конструкциях f oreach
В итерационных конструкциях f oreach также можно применять неявную
типизацию. Нетрудно догадаться, что компилятор в таких случаях будет корректно выводить
соответствующий "тип типа". Рассмотрим приведенный ниже метод, в котором
производится проход по локальному массиву целых чисел:
static void VarlnForeachLoop ()
{
intU mylr.Ls = { 10, 20, 30, 40 };
// Использование var в стандартном цикле foreach.
foreach (var lt^m in mylnts)
{
Console .WnteLine ( "Item value: { 0 } " , item); // вывод значения элемента
}
}
Следует отметить, что в данном примере веской причины для использования
ключевого слова var в цикле foreach нет, поскольку четко видно, что итерация
осуществляется по массиву целых чисел. Но, опять-таки, в модели программирования LINQ
использование var в цикле foreach будет очень полезно, а иногда и вообще обязательно.
Конструкции while и do/while
Конструкцию while удобно применять, когда требуется, чтобы блок
операторов выполнялся до тех пор, пока не будет удовлетворено какое-то конечное условие.
Естественно, нужно позаботиться о том, чтобы это условие когда-нибудь
действительно достигалось, иначе получится бесконечный цикл. Ниже приведен пример, в котором
на экран будет выводиться сообщение In while loop (В цикле while) до тех пор, пока
пользователь не завершит цикл вводом в командной строке слова yes:
static void ExecuteWhileLoop ()
{
string userlsDone = "";
// Проверка на соответствие строке в нижнем регистре,
while(userlsDone.ToLower () |= "yes")
{
, Console.Write("Are you done? [yes] [no]: ") ; // запрос окончания
userlsDone = Console.ReadLine ();
Console.WriteLine ("In while loop");
}
}
Оператор do/while тесно связан с циклом while. Как и обычный while, цикл do/while
применяется тогда, когда какое-то действие должно выполняться неопределенное
количество раз. Разница между этими двумя циклами состоит в том, что цикл do /while гарантирует
выполнение соответствующего блока кода хотя бы один раз (в то время как цикл while
может его вообще не выполнить, если условие с самого начала оказывается ложным).
static void ExecuteDoWhileLoop ()
{
string userlsDone = " " ;
do
{
Console.WriteLine ("In do/while loop");
Console.Write("Are you done? [yes] [no]: ");
userlsDone = Console.ReadLine();
} while(userlsDone.ToLower() != "yes"); // Обратите внимание на точку с запятой!
Глава 3. Главные конструкции программирования на С#: часть I 147
Конструкции принятия решений
и операции сравнения
Теперь, когда было показано, как обеспечить выполнение блока операторов в цикле,
давайте рассмотрим следующую связанную концепцию, а именно — управление
выполнением программы. Для изменения хода выполнения программы в С# предлагаются две
следующих конструкции:
• оператор if /else;
• оператор switch.
Оператор if /else
Сначала рассмотрим хорошо знакомый оператор if/else. В отличие от языков С
и C++, в С# этот оператор может работать только с булевскими выражениями, но не с
произвольными значениями вроде -1 и 0. С учетом этого, для получения литеральных
булевских значений в операторах if /else обычно применяются операции,
перечисленные в табл. 3.7.
Таблица 3.7. Операции сравнения в С#
р Пример использования Описание
сравнения
if (age == 30) Возвращает true, только если выражения одинаковы
!= if("Foo" != myStr) Возвращает true, только если выражения разные
< if (bonus < 2000) Возвращает true, только если выражение слева
> if (bonus > 2000) (bonus) меньше, больше, меньше или равно либо
<= if (bonus <= 2000) больше или равно выражению справа B000)
>= if (bonus >= 2000)
Программисты, ранее работавшие с С и C++, должны иметь в виду, что старые
приемы проверки неравенства значения нулю в С# работать не будут. Например,
предположим, что требуется проверить, состоит ли текущая строка из более чем нуля символов.
У программистов на С и C++ может возникнуть соблазн написать код следующего вида:
static yoid ExecutelfElse ()
{
// Такой код недопустим, поскольку Length возвращает int, а не bool.
string stringData = "My textual data";
if(stringData.Length)
{
Console.WriteLine("string is greater than 0 characters");
}
}
Если необходимо использовать свойство String. Length для определения
истинности или ложности, вычисляемое в условии выражение потребуется изменить так, чтобы
оно давало в результате булевское значение:
// Такой код является допустимым, поскольку
// условие будет возвращать true или false.
if(stringData.Length > 0)
{
Console.WriteLine("string is greater than 0 characters");
}
148 Часть II. Главные конструкции программирования на С#
В операторе if могут применяться сложные выражения, и он может содержать
операторы else, обеспечивая выполнение более сложных проверок. Синтаксис похож
на применяемый в аналогичных ситуациях в языках С (C++) и Java. При построении
сложных выражений в С# используется вполне ожидаемый набор условных операций,
описанный в табл. 3.8.
Таблица 3.8. Условные операции в С#
Операция Пример Описание
&& if (age == 30 && name == "Fred") Условная операция AND (И).
Возвращает true, если все
выражения истинны
I | if (age ==30 | | name == "Fred") Условная операция OR (ИЛИ).
Возвращает true, если истинно хотя
бы одно из выражений
! if (ImyBool) Условная операция NOT (HE).
Возвращает true, если выражение
ложно, или false, если истинно
На заметку! Операции && и | | поддерживают сокращенный путь выполнения, если это
необходимо. То есть сразу же после определения, что некоторое сложное выражение является ложным,
оставшиеся подвыражения вычисляться не будут.
Оператор switch
Еще одной простой конструкцией, предназначенной в С# для реализации выбора,
является оператор switch. Как и в остальных С-подобных языках, в С# этот оператор
позволяет организовать выполнение программы на основе заранее определенного
набора вариантов. Например, в приведенном ниже методе Main () для каждого из двух
возможных вариантов выводится свое сообщение (блок default обрабатывает
неверный выбор).
// Переход на основе выбранного числового значения.
static void ExecuteSwitch()
{
Console.WriteLine ( [C#], 2 [VB]");
Console.Write("Please pick your language preference: ");
string langChoice = Console.ReadLine ();
int n = int.Parse(langChoice);
switch (n)
{
case 1: Console.WriteLine("Good choice, C# is a fine language.");
break;
case 2: Console.WriteLine("VB: OOP, multithreading, and more1");
break;
default: Console.WriteLine("Well...good luck with that!");
break;
На заметку! В языке С# каждый блок case, в котором содержатся выполняемые операторы (блок
default в том числе), должен завершаться оператором break или goto, во избежание
сквозного прохода.
Глава 3. Главные конструкции программирования на С#: часть I 149
Одна из замечательных особенностей оператора switch в С# заключается в том, что
помимо числовых данных он также позволяет производить вычисления и со строковыми
данными. Ниже для примера приведена модифицированная версия предыдущего
оператора switch (обратите внимание, что при этом подвергать пользовательские данные
синтаксическому разбору и преобразованию их в числовые значения не требуется).
static void ExecuteSwitchOnString ()
{
Console.WriteLine("C# or VB" );
Console.Write("Please pick your language preference: ");
string langChoice = Console.ReadLine ();
switch (langChoice)
{
case "C#": Console.WriteLine("Good choice, C# is a fine language.");
break;
case "VB": Console.WriteLine("VB: OOP, multithreading and more1");
break;
default: Console.WriteLine("Well... good luck with that!");\
break;
}
}
Исходный код. Проект IterationsAndDecisions доступен в подкаталоге Chapter 3.
На этом рассмотрение поддерживаемых в С# ключевых слов для организации
циклов и принятия решений, а также общих операций, которые можно использовать при
написании сложных операторов, завершено. Здесь предполагалось, что у читателя
имеется опыт работы с аналогичными ключевыми словами (if, for, switch и т.д.) в других
языках программирования. Дополнительные сведения по данной теме можно найти в
документации .NET Framework 4.0 SDK.
Резюме
Задачей настоящей главы было описание многочисленных ключевых аспектов языка
программирования С#. Сначала рассматривались типичные конструкции,
используемые в любом приложении. Затем была описана роль объекта приложения и рассказано
о том, что в каждой исполняемой программе на С# должен обязательно присутствовать
тип, определяющий метод Main () , который служит входной точкой в приложении.
Внутри метода Main () обычно создается набор объектов, которые, работая вместе,
приводят приложение в действие.
Далее были рассмотрены базовые типы данных в С# и разъяснено, что
используемые для их представления ключевые слова (вроде int) на самом деле являются
сокращенными обозначениями полноценных типов из пространства имен System (в данном
случае System. Int32). Благодаря этому, каждый тип данных в С# имеет набор
встроенных членов. Также была описана роль операций расширения и сужения типов и таких
ключевых слов, как checked и unchecked.
Кроме того, рассматривались особенности неявной типизации с помощью ключевого
слова var. Как было отмечено, неявная типизация наиболее полезна в модели
программирования LINQ. И, наконец, в главе кратко рассматривались различные конструкции
С#, предназначенные для создания циклов и принятия решений. Теперь, когда базовые
характеристики языка С# известны, можно переходить к изучению его ключевой
функциональности, а также объектно-ориентированных возможностей.
ГЛАВА 4
Вгавные конструкции
программирования
на С#: часть II
В этой главе будет завершен обзор ключевых аспектов языка программирования
С#. Сначала рассматриваются различные детали, касающиеся построения
методов в С#, в частности, ключевые слова out, ref и params. Кроме того, будут описаны
две новых функциональных возможности С#, которые появились в .NET 4.0 —
необязательные и именованные параметры.
Затем будет рассматриваться перегрузка методов, манипулирование массивами с
использованием синтаксиса С# и функциональность связанного с массивами класса
System.Array.
Вдобавок в главе показано, как создавать перечисления и структуры в С#, и
подробно описаны отличия между типами значений и ссылочными типами. И, наконец,
рассматривается роль нулевых (nullable) типов данных и операций ? и ? ?. После изучения
материалов настоящей главы можно переходить к ознакомлению с
объектно-ориентированными возможностями языка С#.
Методы и модификаторы параметров
Для начала давайте изучим детали, касающиеся определения методов в С#. Как и
метод Main () (см. главу 3), специальные методы могут как принимать, так и не принимать
параметров, а также возвращать или не возвращать значения вызывающей стороне.
Как будет показано в следующих нескольких главах, методы могут быть реализованы в
контексте классов или структур (а также прототипированы внутри типов интерфейсов)
и снабжаться различными ключевыми словами (internal, virtual, public, new и т.д.)
для уточнения их поведения. До настоящего момента в этой книге формат каждого из
демонстрировавшихся методов в целом выглядел так:
// Статические методы могут вызываться
// напрямую без создания экземпляра класса.
class Program
• {
// static возвращаемыйТип ИмяМетода (параметры) {...}
static int Add(mt x, int у) { return x + у; }
}
Глава 4. Главные конструкции программирования на С#: часть II 151
Хотя определение метода в С# выглядит довольно понятно, существует несколько
ключевых слов, с помощью которых можно управлять способом передачи аргументов
интересующему методу. Все эти ключевые слова описаны в табл. 4.1.
Таблица 4.1. Модификаторы параметров в С#
Модификатор параметра Описание
i ■
(отсутствует) Если параметр не сопровождается модификатором,
предполагается, что он должен передаваться по значению, т.е. вызываемый
метод должен получать копию исходных данных
out Выходные параметры должны присваиваться вызываемым
методом (и, следовательно, передаваться по ссылке). Если
параметрам out в вызываемом методе значения не присвоены,
компилятор сообщит об ошибке
ref Это значение первоначально присваивается вызывающим кодом
и при желании может повторно присваиваться в вызываемом
методе (поскольку данные также передаются по ссылке). Если
параметрам ref в вызываемом методе значения не присвоены,
компилятор никакой ошибки генерировать не будет
params Этот модификатор позволяет передавать в виде одного
логического параметра переменное количество аргументов. В каждом
методе может присутствовать только один модификатор params и он
должен обязательно указываться последним в списке параметров.
В реальности необходимость в использовании модификатора
params возникает не особо часто, однако он применяется во
многих методах внутри библиотек базовых классов
Чтобы посмотреть, как эти ключевые слова используются на практике, давайте
создадим новый проект типа Console Application по имени FunWithMethods и с его
помощью изучим роль каждого из этих ключевых слов.
Стандартное поведение при передаче параметров
По умолчанию параметр передается по значению. Попросту говоря, если аргумент
не снабжается каким-то конкретным модификатором параметра, функции передается
копия данных. Как будет объясняться в конце настоящей главы, то, как именно
выглядит эта копия, зависит от того, к какому типу относится параметр — типу значения или
ссылочному типу. Пока что давайте создадим внутри класса Program следующий метод,
который оперирует двумя числовыми типами данных, передаваемыми по значению:
//По умолчанию аргументы передаются по значению.
public static int Add(int x, int y)
{
int ans = x + y;
// Вызывающий метод не увидит эти изменения, поскольку
// изменяться в таком случае будет лишь копия исходных данных.
х = 10000;
у = 88888;
return ans;
}
Числовые данные подпадают под категорию типов значения. Поэтому в случае
изменения значений параметров внутри члена вызывающий метод будет оставаться в полном
неведении об этом, поскольку значения будут изменяться лишь в копии исходных данных.
152 Часть II. Главные конструкции программирования на С#
static void Main(string [ ] args)
{
Console.WriteLine("*****Fun with Methods *****\n");
// Передача двух переменных по значению.
int x = 9, у = 10;
Console.WriteLine("Before call: X: {0}, Y: {1}", x, y) ; // до вызова
Console.WriteLine("Answer is: {0}", Add(x, y) ) ; // ответ
Console.WriteLine("After call: X: {0}, Y: {1}", x, y) ; // после вызова
Console.ReadLine();
}
Как и следовало ожидать, значения х и у до и после вызова Add () будут выглядеть
совершенно идентично:
***** Fun W1th Methods *****
Before call: X: 9, Y: 10
Answer is: 19
After call: X: 9, Y: 10
Модификатор out
Теперь посмотрим, как используются выходные параметры. Методы, которым при
определении (с помощью ключевого слова out) указано принимать выходные
параметры, должны перед выходом обязательно присваивать им соответствующие значения
(в противном случае компилятор сообщит об ошибке).
В целях иллюстрации ниже приведена альтернативная версия метода Add (),
которая предусматривает возврат суммы двух целых чисел с использованием
модификатора out (обратите внимание, что физическим возвращаемым значением метода теперь
является void).
// Выходные параметры должны предоставляться вызываемым методом.
public static void Add (int x, int y, out int ans)
{
ans = x + y;
}
В вызове метода с выходными параметрами тоже должен использоваться
модификатор out. Локальным переменным, передаваемым в качестве выходных параметров,
присваивать начальные значения не требуется (после вызова эти значения все равно
будут утрачены). Причина, по которой компилятор позволяет передавать на первый
взгляд неинициализированные данные, связана с тем, что в вызываемом методе
операция присваивания должна выполняться обязательно. Ниже приведен пример.
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Methods *****");
// Присваивать первоначальное значение локальным
// переменным, используемым в качестве выходных
// параметров, не требуется, при условии, что в первый раз
// они используются в качестве выходных аргументов.
int ans;
Add(90, 90, out ans);
Console.WriteLine("90 + 90 = {0}", ans);
Console.ReadLine();
}
Этот пример был представлен исключительно для иллюстрации; на самом деле нет
совершенно никаких оснований возвращать значение операции суммирования в выход-
Глава 4. Главные конструкции программирования на С#: часть II 153
ном параметре. Но сам модификатор out в С# действительно является очень полезным:
он позволяет вызывающему коду получать в результате одного вызова метода сразу
несколько значений.
// Возврат множества выходных параметров.
public static void FillTheseVals (out int a, out string b, out bool c)
{
a = 9;
b = "Enjoy your string.";
с = true;
}
В вызывающем коде в таком случае может находиться обращение к методу
FillTheseValues (), как показано ниже. Обратите внимание, что модификатор out
должен использоваться как при вызове, так и при реализации данного метода.
static void Main(string[ ] args)
{
Console.WriteLine ("***** Fun with Methods *****");
int i; string str; bool b;
FillTheseValues (out i, out str, out b) ;
Console.WriteLine ("Int is: {0}", l); // целое число
Console.WriteLine ("String is: {0}", str); // строка
Console.WriteLine ("Boolean is: {0}", b) ; // булевское значение
Console.ReadLine();
}
И, наконец, не забывайте, что в любом методе, в котором определяются выходные
параметры, перед выходом им обязательно должны быть присвоены действительные
значения. Следовательно, для следующего кода компилятор будет выдавать ошибку,
поскольку выходному параметру в области действия метода никакого значения присвоено
не было:
static void ThisWontCompile (out int a)
{
Console. WriteLine("Error' Forgot to assign output arg!");
// Ошибка! Забыли присвоить значение выходному аргументу!
}
Модификатор ref
Теперь посмотрим, как в С# используется модификатор ref (от "reference" - ссылка).
Параметры, сопровождаемые таким модификатором, называются ссылочными и
применяются, когда нужно позволить методу выполнять операции и обычно также изменять
значения различных элементов данных, объявляемых в вызывающем коде (например,
в процедуре сортировки или обмена). Обратите внимание на следующие отличия между
ссылочными и выходными параметрами.
• Выходные параметры не нужно инициализировать перед передачей методу.
Причина в том, что метод сам должен присваивать значения выходным
параметрам перед выходом.
• Ссылочные параметры нужно обязательно инициализировать перед передачей
методу. Причина в том, что они подразумевают передачу ссылки на уже
существующую переменную. Если первоначальное значение ей не присвоено, это
будет равнозначно выполнению операции над неинициализированной локальной
переменной.
154 Часть II. Главные конструкции программирования на С#
Давайте рассмотрим применение ключевого слова ref на примере метода,
меняющего две строки местами:
// Ссылочные параметры.
public static void SwapStrings(ref string si, ref string s2)
{
string tempStr = si;
si = s2;
s2 = tempStr;
}
Этот метод может быть вызван следующим образом:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Methods *****");
string si = "Flip";
string s2 = "Flop";
Console.WriteLine("Before: {0}, {1} ", si, s2) ; //до
SwapStrings (ref si, ref s2);
Console.WriteLine("After: {0}, [1} ", si, s2); // после
Console.ReadLine ();
}
Здесь в вызывающем коде производится присваивание первоначальных значений
локальным строкам (si и s2). Благодаря этому после выполнения вызова SwapStrings ()
в строке si будет содержаться значение "Flop", а в строке s2 — значение "Flip":
Before: Flip, Flop
After: Flop, Flip
На заметку! Поддерживаемое в С# ключевое слово ref рассматривается в разделе "Типы
значения и ссылочные типы" далее в главе. Как будет показано, поведение этого ключевого слова
немного меняется в зависимости от того, является аргумент типом значения (структурой) или
ссылочный типом (классом).
Модификатор params
В С# поддерживается использование массивов параметров за счет применения
ключевого слова params. Для овладения этой функциональностью требуются хорошие
знания массивов С#, основные сведения о которых приведены в разделе "Массивы в С#"
далее в главе.
Ключевое слово params позволяет передавать методу переменное количество
аргументов одного типа в виде единственного логического параметра. Аргументы,
помеченные ключевым словом params, могут обрабатываться, если вызывающий код на их
месте передает строго типизированный массив или разделенный запятыми список
элементов. Конечно, это может вызывать путаницу. Чтобы стало понятнее, предположим,
что требуется создать функцию, которая бы позволила вызывающему коду передавать
любое количество аргументов и возвращала бы их среднее значение.
Если прототипировать соответствующий метод так, чтобы он принимал массив
значений типа double, вызывающий код должен будет сначала определить массив, затем
заполнить его значениями и только потом, наконец, передать. Однако если определить
метод CalculateAverage () так, чтобы он принимал массив параметров (params) типа
double, тогда вызывающий код может просто передать разделенный запятыми список
значений double, а исполняющая среда .NET автоматически упакует этот список в
массив типа double.
Глава 4. Главные конструкции программирования на С#: часть II 155
// Возвращение среднего из некоторого количества значений double.
static double CalculateAverage(params double [ ] values)
{
// Вывод количества значений
Console. WriteLine ("You sent me {0} doubles.11, values . Length) ;
double sum = 0;
if(values.Length == 0)
return sum;
for (int i = 0; l < values.Length; i++)
sum += values [i];
return (sum / values.Length);
}
Метод определен таким образом, чтобы принимать массив параметров со
значениями типа double. По сути, этот метод ожидает произвольное количество (включая ноль)
значений double и вычисляет по ним среднее значение. Благодаря этому, он может
вызываться любым из показанных ниже способов:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Methods *****••);
// Передача разделенного запятыми списка значений double...
double average;
average = CalculateAverageD.0, 3.2, 5.7, 64.22, 87.2);
Console.WriteLine("Average of data is: { 0}" , average);
// Среднее значение получается таким:
// . . .или массива значений double.
doublet] data = { 4.0, 3.2, 5.7 };
average = CalculateAverage(data);
Console.WriteLine("Average of data is: {0}", average);
// Среднее из 0 равно О!
Console.WriteLine("Average of data is: {0}", CalculateAverage());
Console.ReadLine();
}
Если бы в определении CalculateAverage () не было модификатора params, первый
способ вызова этого метода приводил бы к ошибке на этапе компиляции, поскольку тогда
компилятор искал бы версию CalculateAverage (), принимающую пять аргументов double.
На заметку! Во избежание какой бы то ни было неоднозначности, в С# требуется, чтобы в любом
методе поддерживался только один аргумент params, который должен быть последним в
списке параметров.
Как не трудно догадаться, такой подход является просто более удобным для
вызывающего кода, поскольку в случае его применения необходимый массив создается
самой CLR-средой. К моменту, когда этот массив попадает в область действия
вызываемого метода, он будет трактоваться как полноценный массив .NET, обладающий всеми
функциональными возможностями базового типа System.Array. Ниже показано, как
мог бы выглядеть вывод приведенного выше метода:
You sent me 5 doubles.
Average of data is: 32.864
You sent me 3 doubles.
Average of data is: 4.3
You sent me 0 doubles .
Average of data is: 0
156 Часть II. Главные конструкции программирования на С#
Определение необязательных параметров
С выходом версии .NET 4.0 у разработчиков приложений на С# теперь появилась
возможность создавать методы, способные принимать так называемые необязательные
аргументы (optional arguments). Это позволяет вызывать единственный метод, опуская
необязательные аргументы, при условии, что подходят их значения, установленные по
умолчанию.
На заметку! Как будет показано в главе 18, главным стимулом для добавления необязательных
аргументов послужила необходимость в упрощении взаимодействия с объектами СОМ. В
нескольких объектных моделях Microsoft (например, Microsoft Office) функциональность
предоставляется через объекты СОМ, многие из которых были написаны давно и рассчитаны на
использование необязательных параметров.
Чтобы посмотреть, как работать с необязательными аргументами, давайте создадим
метод по имени EnterLogDataO с одним необязательным параметром:
static void EnterLogData(string message, string owner = "Programmer")
{
Console.Beep();
Console.WriteLine ("Error: {0}", message);
Console.WriteLine("Owner of Error: {0}", owner);
}
Последнему аргументу string был присвоено используемое по умолчанию значение
"Programmer" с применением операции присваивания внутри определения
параметров. В результате метод EnterLogData () можно вызывать в Main () двумя способами:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Methods *****");
EnterLogData("Oh no! Grid can't find data");
EnterLogData("Oh no! I can't find the payroll data", "CFO");
Console.ReadLine();
}
Поскольку в первом вызове EnterLogData () не было указано значение для второго
аргумента string, будет использоваться его значение по умолчанию — "Programmer".
Еще один важный момент, о котором следует помнить, состоит в том, что значение,
присваиваемое необязательному параметру, должно быть известно во время
компиляции и не может вычисляться во время выполнения (в этом случае на этапе компиляции
сообщается об ошибке). Для иллюстрации предположим, что понадобилось
модифицировать метод EnterLogData (), добавив в него еще один необязательный параметр:
// Ошибка1 Значение, используемое по умолчанию для необязательного
// аргумента, должно быть известно во время компиляции!
static void EnterLogData(string message,
string owner = "Programmer", DateTime timeStamp = DateTime.Now)
{
Console.Beep ();
Console.WriteLine ("Error: {0}", message);
Console.WriteLine("Owner of Error: {0}", owner);
Console.WriteLine("Time of Error: {0 } ", timeStamp);
}
Этот код скомпилировать не получится, потому что значение свойства Now класса
DateTime вычисляется во время выполнения, а не во время компиляции.
Глава 4. Главные конструкции программирования на С#: часть II 157
На заметку! Во избежание возникновения любой неоднозначности, необязательные параметры
должны всегда размещаться в конце сигнатуры метода. Если необязательный параметр
окажется перед обязательными, компилятор сообщит об ошибке.
Вызов методов с использованием именованных параметров
Еще одной функциональной возможностью, которая добавилась в С# с выходом
версии .NET 4.0, является поддержка так называемых именованных аргументов (named
arguments). По правде говоря, на первый взгляд может показаться, что такая языковая
конструкция способна лишь запутать код. Это действительно может оказаться
именно так! Во многом подобно необязательным аргументам, стимулом для включения
поддержки именованных параметров главным образом послужило желание упростить
работу с уровнем взаимодействия с СОМ (см. главу 18).
Именованные аргументы позволяют вызывать метод с указанием значений
параметров в любом желаемом порядке. Следовательно, вместо того, чтобы передавать
параметры исключительно в соответствии с позициями, в которых они определены (как
приходится поступать в большинстве случаев), можно указывать имя каждого
аргумента, двоеточие и конкретное значение. Чтобы продемонстрировать применение
именованных аргументов, добавим в класс Program следующий метод:
static void DisplayFancyMessage(ConsoleColor textColor,
ConsoleColor backgroundColor, string message)
{
// Сохранение старых цветов для обеспечения возможности
//их восстановления сразу после вывода сообщения.
ConsoleColor oldTextColor = Console.ForegroundColor;
ConsoleColor oldbackgroundColor = Console.BackgroundColor;
// Установка новых цветов и вывод сообщения.
Console.ForegroundColor = textColor;
Console.BackgroundColor = backgroundColor;
Console.WriteLine(message);
// Восстановление предыдущих цветов.
Console.ForegroundColor = oldTextColor;
Console.BackgroundColor = oldbackgroundColor;
}
Возможно, ожидается, что при вызове методу DisplayFancyMessage () должны
передаваться две переменных типа ConsoleColor со следующим за ним значением типа
string. Однако за счет применения именованных аргументов DisplayFancyMessage ()
вполне можно вызвать и так, как показано ниже:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Methods *****");
DisplayFancyMessage (message: "Wow! Very Fancy indeed!11,
textColor: ConsoleColor.DarkRed,
backgroundColor: ConsoleColor.White);
DisplayFancyMessage(backgroundColor: ConsoleColor.Green,
message: "Testing...11,
textColor: ConsoleColor.DarkBlue);
Console.ReadLine();
}
158 Часть II. Главные конструкции программирования на С#
Одной малоприятной особенностью применения именованных аргументов является
то, что в вызове метода позиционные параметры должны быть перечислены перед
любыми именованными параметрами. Другими словами, именованные аргументы должны
всегда размещаться в конце вызова метода. Ниже показан пример, иллюстрирующий
сказанное.
// Здесь все в порядке, поскольку позиционные
// аргументы идут перед именованными.
DisplayFancyMessage(ConsoleColor.Blue,
message: "Testing...11,
backgroundColor: ConsoleColor.White);
// Здесь присутствует ошибка, поскольку позиционные
// аргументы идут после именованных.
DisplayFancyMessage(message: "Testing...",
backgroundColor: ConsoleColor.White,
ConsoleColor.Blue);
Помимо этого ограничения может возникать вопрос, а когда вообще понадобится эта
языковая конструкция? Зачем нужно менять позиции трех аргументов метода?
Как оказывается, если в методе необходимо определять необязательные аргументы,
то эта конструкция может оказаться очень полезной. Для примера перепишем метод
DisplayFancyMessage () так, чтобы он поддерживал необязательные аргументы и
предусматривал для них подходящие значения по умолчанию:
static void DisplayFancyMessage(ConsoleColor textColor = ConsoleColor.Blue,
ConsoleColor backgroundColor = ConsoleColor.White,
string message = "Test Message")
{
}
Из-за того, что каждый аргумент теперь имеет значение по умолчанию, в
вызывающем коде с помощью именованных аргументов можно указывать только тот параметр
или параметры, для которых не должны применяться значения по умолчанию. То есть,
если нужно, чтобы значение "Hello! " появлялось в виде текста голубого цвета на
белом фоне, в вызывающем коде можно использовать следующую строку:
DisplayFancyMessage(message: "Hello!")
Если же необходимо, чтобы строка "Test Message" выводилась синим цветом на
зеленом фоне, то должен применяться такой код:
DisplayFancyMessage(backgroundColor: ConsoleColor.Green);
Как не трудно заметить, необязательные аргументы и именованные параметры
действительно часто работают бок о бок. Чтобы завершить рассмотрение деталей
построения методов в С#, необходимо обязательно ознакомиться с понятием перегрузки
методов.
Исходный код. Приложение FunWithEnums доступно в подкаталоге Chapter 4.
Перегрузка методов
Как и в других современных объектно-ориентированных языках программирования,
в С# можно перегружать (overload) методы. Перегруженными называются методы с
набором одинаково именованных параметров, отличающиеся друг от друга количеством
(или типом) параметров.
Глава 4. Главные конструкции программирования на С#: часть II 159
Чтобы оценить полезность перегрузки методов, давайте представим себя на месте
разработчика, использующего Visual Basic 6.0, и предположим, что требуется создать
набор методов, возвращающих сумму значений различных типов (Integer, Double
и т.д.). Из-за того, что в VB6 перегрузка методов не поддерживается, для решения этой
задачи придется определить уникальный набор методов, каждый из которых, по сути,
будет делать одно и тоже (возвращать сумму аргументов):
1 Примеры кода на VB6.
Public Function Addlnts(ByVal x As Integer, ByVal у As Integer) As Integer
Addlnts = x + у
End Function
Public Function AddDoubles(ByVal x As Double, ByVal у As Double) As Double
AddDoubles = x + у
End Function
Public Function AddLongs (ByVal x As Long, ByVal у As Long) As Long
AddLongs = x + у
End Function
Такой код не только труден в сопровождении, но и заставляет помнить имя
каждого метода. С применением перегрузки, однако, можно дать возможность вызывающему
коду вызывать только единственный метод по имени Add () . При этом важно
обеспечить, чтобы каждая версия метода имела отличающийся набор аргументов (различий в
одном только возвращаемом типе не достаточно).
На заметку! Как будет показано в главе 10, в С# возможно создавать обобщенные методы, которые
переводят концепцию перегрузки на новый уровень. С использованием обобщений для
реализаций методов можно определять так называемые "заполнители", которые будут заполняться
во время вызова этих методов.
Чтобы попрактиковаться с перегруженными методами, создадим новый проект
Console Application (Консольное приложение) по имени Methodoverloading и добавим
в него следующее определение класса на С#:
// Код на С#.
class Program
{
static void Main(string [ ] args) { }
// Перегруженный метод Add() .
static int Add(int x, int y)
{ return x + y; }
static double Add(double x, double y)
{ return x + y; }
static long Add(long x, long y)
{ return x + y; }
}
Теперь можно вызывать просто метод Add () с требуемыми аргументами, а
компилятор будет самостоятельно находить правильную реализацию, подлежащую вызову, на
основе предоставляемых ему аргументов:
static void Main(string[] args)
{
Console.WriteLine (••***** Fun with Method Overloading *~*>A\n");
// Вызов int-версии Add()
Console.WriteLine(AddA0, 10));
// Вызов long-версии Add()
' Console.WnteLine(Add(900000000000, 900000000000));
160 Часть II. Главные конструкции программирования на С#
// Вызов double-версии Add()
Console.WriteLine(AddD.3, 4.4));
Console . ReadLme () ;
}
IDE-среда Visual Studio 2010 обеспечивает помощь при вызове перегруженных
методов. При вводе имени перегруженного метода (как, например, хорошо знакомого
Console .WriteLine ()) в списке IntelliSense предлагаются все его доступные версии.
Обратите внимание, что по списку можно легко перемещаться, щелкая на кнопках со
стрелками вниз и вверх (рис. 4.1).
Program.cs* X Щ
I jtfMethodOvertckading.Program 'I ^Main(stringQ args)
// Calls int version of Add()
Console.WriteLine(Add(ie, 16));
// Calls long version of Add()
Ijl .1
// Calls double version of Add()
Console.WriteLine(AddD.3, 4.4));
Console.WriteLine(
[a 2 of 19 T void Console,WriteLfnefbool value) |
} I ^ The value to write. |
+ l0v«? г loaded AddQ methods!
Рис. 4.1. Окно IntelliSense, отображаемое в Visual Studio 2010 при работе
с перегруженными методами
На заметку! Приложение MethodOverloading доступно в подкаталоге Chapter 4.
На этом обзор основных деталей создания методов с использованием синтаксиса С#
завершен. Теперь давайте посмотрим, как в С# создавать и манипулировать массивами,
перечислениями и структурами, и завершим главу изучением того, что собой
представляют "нулевые типы данных" и такие поддерживаемые в С# операции, как ? и ??.
Массивы в С#
Как, скорее всего, уже известно, массивом (array) называется набор элементов
данных, доступ к которым получается по числовому индексу. Если говорить более
конкретно, то любой массив, по сути, представляет собой ряд связанных между собой
элементов данных одинакового типа (например, массив int, массив string, массив SportCar
и т.п.). Объявление массива в С# осуществляется довольно понятным образом. Чтобы
посмотреть, как именно, давайте создадим новый проект типа Console Application (no
имени FunWithArrays) и добавим в него вспомогательный метод SimpleArrays (),
вызываемый из Main ().
class Program
{
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Arrays *****");
SimpleArrays();
Console.ReadLine();
}
I
Глава 4. Главные конструкции программирования на С#: часть II 161
static void SimpleArrays ()
{
Console. WriteLine ("=> Simple Array Creation.11);
// Создание массива int с тремя элементами {0, 1, 2}.
int[] mylnts = new int[3];
// Инициализация массива с 100 элементами string,
// проиндексированными от 0 до 99.
string[] booksOnDotNet = new string[100];
Console.WriteLine ();
}
}
Внимательно почитайте комментарии в коде. При объявлении массива с помощью
такого синтаксиса указываемое в объявлении число обозначает общее количество
элементов, а не верхнюю границу. Кроме того, нижняя граница в массиве всегда
начинается с 0. Следовательно, в результате объявления int [ ] mylnts = new int [3] получается
массив, содержащий три элемента, проиндексированных по позициям 0, 1,2.
После определения переменной массива можно переходить к его заполнению
элементами от индекса к индексу, как показано ниже на примере метода SimpleArrays () :
static void SimpleArrays ()
{
Console .WriteLine ("=> Simple Array Creation.");
// Создание и заполнение массива тремя
// целочисленными значениями.
int[] mylnts = new int[3];
mylnts[0] = 100;
mylnts[1] = 200;
mylnts[2] = 300;
// Отображение значений.
foreach (int i in mylnts)
Console.WriteLine A);
Console.WriteLine ();
}
На заметку! Следует иметь в виду, что если массив только объявляется, но явно не
инициализируется, каждый его элемент будет установлен в значение, принятое по умолчанию для
соответствующего типа данных (например, элементы массива типа bool будут устанавливаться в false,
а элементы массива типа int — в 0).
Синтаксис инициализации массивов в С#
Помимо заполнения массива элемент за элементом, можно также заполнять его с
использованием специального синтаксиса инициализации массивов. Для этого
необходимо перечислить включаемые в массив элемент в фигурных скобках ({ }). Такой
синтаксис удобен при создании массива известного размера, когда нужно быстро задать
его начальные значения. Ниже показаны альтернативные версии объявления массива.
static void Arraylnitialization ()
{
Console .WriteLine ("=> Array Initialization.");
// Синтаксис инициализации массива с помощью
// ключевого слова new.
string[] stringArray = new string[]
{ "one", "two", "three" };
Console.WriteLine("stringArray has {0} elements", stringArray.Length);
162 Часть II. Главные конструкции программирования на С#
// Синтаксис инициализации массива без применения
// ключевого слова new.
bool [ ] boolArray = { false, false, true };
Console.WriteLine("boolArray has {0} elements", boolArray.Length);
// Синтаксис инициализации массива с указанием ключевого
// слова new и желаемого размера.
int[] intArray = new int[4] { 20, 22, 23, 0 };
Console.WriteLine("intArray has {0} elements", intArray.Length);
Console.WriteLine();
}
Обратите внимание, что в случае применения синтаксиса с фигурными скобками
размер массива указывать не требуется (как видно на примере создания переменной
stringArray), поскольку этот размер автоматически вычисляется на основе
количества элементов внутри фигурных скобок. Кроме того, применять ключевое слово new не
обязательно (как при создании массива boolArray).
В объявлении intArray указанное числовое значение обозначает количество
элементов в массиве, а не верхнюю границу. Если между объявленным размером и
количеством инициализаторов имеется несоответствие, на этапе компиляции будет выдано
сообщение об ошибке. Ниже приведен соответствующий пример:
// Размер не соответствует количеству элементов!
int[] intArray = new int[2] { 20, 22, 23, 0 };
Неявно типизированные локальные массивы
В предыдущей главе рассматривалась тема неявно типизированных локальных
переменных. Вспомните, что ключевое слово var позволяет определить переменную так,
чтобы лежащий в ее основе тип выводился компилятором. Аналогичным образом
можно также определять неявно типизированные локальные массивы. С использованием
такого подхода можно определить новую переменную массива без указания типа
элементов, содержащихся в массиве.
static void DeclarelmplicitArrays ()
{
Console .WriteLine ("=> Implicit Array Initialization.");
// В действительности а - массив типа int[].
var a = new[] { 1, 10, 100, 1000 };
Console.WriteLine("a is a: {0}", a.ToString());
// В действительности Ь - массив типа double [] .
var b = new[] { 1, 1.5, 2, 2.5 };
Console.WriteLine(Mb is a: {0}", b.ToString());
// В действительности с - массив типа string [] .
var с = new[] { "hello", null, "world" };
Console.WriteLine("c is a: {0}", с.ToString());
Console.WriteLine();
}
Разумеется, как и при создании массива с использованием явного синтаксиса С#,
элементы, указываемые в списке инициализации массива, должны обязательно иметь
один и тот же базовый тип (т.е. должны все быть intr string или SportsCar). В
отличие от возможных ожиданий, неявно типизированный локальный массив не получает
по умолчанию тип SystemObject, поэтому приведенный ниже код вызывает ошибку на
этапе компиляции:
// Ошибка! Используются смешанные типы!
var d = new[J { 1, "one", 2, "two", false };
Глава 4. Главные конструкции программирования на С#: часть II 163
Определение массива объектов
В большинстве случаев при определении массива тип элемента, содержащегося в
массиве, указывается явно. Хотя на первый взгляд это выглядит довольно понятно,
существует одна важная особенность. Как будет объясняться в главе 6, в основе каждого
типа в системе типов .NET (в том числе фундаментальных типов данных) в конечном
итоге лежит базовый класс System.Object. В результате получается, что в случае
определения массива объектов находящиеся внутри него элементы могут представлять
собой что угодно. Например, рассмотрим показанный ниже метод ArrayOfObjects ()
(который можно вызвать в Main () для целей тестирования).
static void ArrayOfObjects ()
{
Console. WriteLine ("=> Array of Objects.");
// В массиве объектов могут содержаться элементы любого типа.
object[] myObjects = new object[4];
myObjects[0] =10;
myObjects[l] = false;
myObjects[2] = new DateTimeA969, 3, 24);
myObjects[3] = "Form & Void";
foreach (object obj in myObjects)
{
// Вывод имени типа и значения каждого элемента массива.
Console.WriteLine("Туре: {0}, Value: {1}", obj.GetType (), obj ) ;
}
Console.WriteLine();
}
В коде сначала осуществляется проход циклом по содержимому массива myObjects,
а затем с использованием метода GetType () из System. Ob j ect выводится имя базового
типа и значения всех элементов. Вдаваться в детали метода System. Ob ject.GetTypeO
на этом этапе пока не требуется; сейчас главное уяснить только то, что с помощью этого
метода можно получить полностью уточенное имя элемента (получение информации о
типах и службы рефлексии подробно рассматриваются в главе 15). Ниже показано, как
будет выглядеть вывод после вызова ArrayOf Ob j ects ().
=> Array of Objects.
Type: System.Int32, Value: 10
Type: System.Boolean, Value: False
Type: System.DateTime, Value: 3/24/1969 12:00:00 AM
Type: System.String, Value: Form & Void
Работа с многомерными массивами
В дополнение к одномерным массивам, которые демонстрировались до сих пор, в
С# также поддерживаются две разновидности многомерных массивов. Многомерные
массивы первого вида называются прямоугольными массивами и содержат несколько
измерений, где все строки имеют одинаковую длину. Объявляются и заполняются такие
массивы следующим образом:
static void RectMultidimensionalArray()
{
Console. WriteLine ("=> Rectangular multidimensional array.");
// Прямоугольный многомерный массив.
int[, ] myMatrix;
myMatrix = new int[6,6];
164 Часть II. Главные конструкции программирования на С#
// Заполнение массива F * б) .
for (int i = 0; i < 6/ i++)
for (int j = 0; j < 6; j++)
myMatrix[i, j] = i * j;
// Вывод массива F * б) .
for (int i = 0; l < 6; i++)
{
for (int j = 0; j < 6; j++)
Console.Write(myMatrix[l, j] + "\t");
Console.WriteLine();
}
Console.WriteLine();
}
Многомерные массивы второго вида называются зубчатыми (jagged) массивами и
содержат некоторое количество внутренних массивов, каждый из которых может иметь
собственный уникальный верхний предел, например:
static void JaggedMultidimensionalArray()
{
Console. WriteLine ("=> Jagged multidimensional array.11);
// Зубчатый многомерный массив (т.е. массив массивов) .
// Здесь он будет состоять из 5 других массивов.
int [ ] [ ] myJagArray = new int [ 5 ] [ ] ;
// Создание зубчатого массива.
for (int i=0; i < myJagArray.Length; i++)
myJagArray [l] = new int[i + 7] ;
// Вывод всех строк (не забывайте, что по умолчанию
// каждый элемент устанавливается в 0) .
for (int i = 0; i < 5; i++)
{
for(int j = 0; j < myJagArray[l].Length; j++)
Console.Write(myJagArray[l] [j] + " ") ;
Console.WriteLine ();
}
Console.WriteLine ();
}
На рис. 4.2 показано, как будет выглядеть вывод после вызова в Main () методов
RectMultidimensionalArray () и JaggedMultidimensionalArray().
Of С .Window ;\systeml2\cmd.exe
г'****"*
i^iflin
Fun with Arrays *
Г1=> Rectangular multidimensional array.
ю о о о о
0 12 3 4
0 2 4 6 8
0 3 6 9 12
0 4 8 12 16
0 5 10 15 20
|=> Jagged multidimensional array.
■0 0 0 0 0 0 0
ДО 0000000
looooooooo
0 000000000
poooooooooo
■Press any key to continue . . .
0
5
10
15
20
25
Рис. 4.2. Прямоугольные и зубчатые многомерные массивы
Глава 4. Главные конструкции программирования на С#: часть II 165
Использование массивов в качестве аргументов
и возвращаемых значений
После создания массив можно передавать как аргумент или получать в виде
возвращаемого значения члена. Например, ниже приведен код метода Print Array (), который
принимает в качестве входного параметра массив значений int и выводит каждое из
них в окне консоли, а также метод GetString (), который заполняет массив
значениями string и возвращает его вызывающему коду.
static void PrintArray (int [ ] mylnts)
{
for(int i = 0; i < mylnts.Length; i++)
Console.WriteLine("Item {0} is {1} ", i, mylnts[i]);
}
static string[] GetStringArray()
{
string[] theStrings = {"Hello", "from", "GetStringArray"};
return theStrings;
}
Вызываются эти методы так, как показано ниже:
static void PassAndReceiveArrays ()
{
Console.WriteLine("=>Arrays as params and return values.");
// Передача массива в качестве параметра.
int[] ages = {20, 22, 23, 0} ;
PrintArray(ages);
// Получение массива в качестве возвращаемого значения.
string[] strs = GetStringArray();
foreach(string s in strs)
Console.WriteLine(s);
Console.WriteLine() ;
}
К этому моменту должно уже быть понятно, как определять, заполнять и изучать
содержимое массивов в С#. В завершение картины рассмотрим роль класса System. Array.
Базовый класс System. Array
Каждый создаваемый массив получает большую часть функциональности от класса
System. Array. Общие члены этого класса позволяют работать с массивом с
использованием полноценной объектной модели. В табл. 4.2 приведено краткое описание
некоторых наиболее интересных членов класса System.Array (полное описание этих и
других членов данного класса можно найти в документации .NET Framework 4.0 SDK).
Таблица 4.2. Некоторые члены класса System. Array
Член класса
System. Array
Описание
Clear () Статический метод, который позволяет устанавливать для всего ряда элементов
в массиве пустые значения @ — для чисел, null — для объектных ссылок и
false — для булевских выражений)
СоруТо () Метод, который позволяет копировать элементы из исходного массива в целевой
Length Свойство, которое возвращает информацию о количестве элементов в массиве
166 Часть II. Главные конструкции программирования на С#
Окончание табл. 4.2
Член класса
System. Array
Описание
Rank Свойство, которое возвращает информацию о количестве измерений в массиве
Reverse () Статическое свойство, которое представляет содержимое одномерного массива
в обратном порядке
Sort () Статический метод, который позволяет сортировать одномерный массив
внутренних типов. В случае реализации элементами в массиве интерфейса
IComparer также позволяет сортировать и специальные типы (см. главу 9)
Давайте теперь посмотрим, как некоторые из этих членов выглядят в действии.
Ниже приведен вспомогательный метод, в котором с помощью статических методов
Reverse () и Clear () в окне консоли выводится информация о массиве типов string:
static void SystemArrayFunctionality ()
{
Console.WriteLine ("=> Working with System.Array.");
// Инициализация элементов при запуске.
string[] gothicBands = {"Tones on Tail", "Bauhaus", "Sisters of Mercy"};
// Вывод элементов в порядке их объявления.
Console.WriteLine("-> Here is the array:");
for (int i = 0; i < gothicBands.Length; i++)
{
// Вывод элемента.
Console.Write(gothicBands [i] + " , " ) ;
}
Console.WriteLine("\n");
// Изменение порядка следования элементов на обратный...
Array.Reverse(gothicBands);
Console.WriteLine("-> The reversed array");
// ... и их вывод.
for (int i=0; i < gothicBands.Length; i++)
{
// Вывод элемента.
Console.Write(gothicBands[i] + ", ");
}
Console.WriteLine("\n");
// Удаление всех элементов, кроме одного.
Console.WriteLine("-> Cleared out all but one...");
Array.Clear(gothicBands, 1, 2) ;
for (int i = 0; l < gothicBands.Length; i++)
{
// Вывод элемента.
Console.Write(gothicBands[i] + ", ");
}
Console.WriteLine();
}
В результате вызова этого метода в Main () можно получить следующий вывод:
=> Working with System.Array.
-> Here is the array:
Tones on Tail, Bauhaus, Sisters of Mercy,
-> The reversed array
Sisters of Mercy, Bauhaus, Tones on Tail,
-> Cleared out all but one. . .
Sisters of Mercy, , ,
Глава 4. Главные конструкции программирования на С#: часть II 167
Обратите внимание, что многие из членов класса System.Array определены как
статические и потому, следовательно, могут вызываться только на уровне класса
(например, методы Array. Sort () и Array. Reverse ()). Таким методам передается массив,
подлежащий обработке. Остальные члены System.Array (вроде свойства Length)
действуют на уровне объекта и потому могут вызываться прямо на массиве.
Исходный код. Приложение FunWithArrays доступно в подкаталоге Chapter 4.
Тип enum
Как рассказывалось в главе 1, в состав системы типов .NET входят классы,
структуры, перечисления, интерфейсы и делегаты. Для начала рассмотрим роль перечислений
(enum), создав новый проект типа Console Application по имени FunWithEnums.
При построении той или иной системы зачастую удобно создавать набор
символических имен, которые отображаются на известные числовые значения. Например,
в случае создания системы начисления заработной платы может понадобиться
ссылаться на сотрудников определенного типа (EmpType) с помощью таких констант, как
VicePresident (вице-президент), Manager (менеджер), Contractor (подрядчик) и Grunt
(рядовой сотрудник). Для этой цели в С# поддерживается понятие специальных
перечислений. Например, ниже показано специальное перечисление по имени EmpType:
// Специальное перечисление.
enum EmpType
{
Manager, // = О
Grunt, /7 = 1
Contractor, /7 = 2
VicePresident /7 = 3
}
В перечислении EmpType определены четыре именованных константы, которые
соответствуют дискретным числовым значениям. По умолчанию первому элементу
присваивается значение 0, а всем остальным элементам значения присваиваются
согласно схеме п+1. При желании исходное значение можно изменять как угодно. Например,
если в данном примере нумерация членов EmpType должна идти с 102 до 105,
необходимо поступить следующим образом:
// Начинаем нумерацию со значения 102.
enum EmpType
{
Manager = 102,
Grunt, // = 103
Contractor, // = 104
VicePresident // = 105
}
Нумерация в перечислениях вовсе не обязательно должна быть последовательной и
содержать только уникальные значения. Например, вполне допустимо (по той или иной
причине) сконфигурировать перечисление EmpType показанным ниже образом:
// Значения элементов в перечислении вовсе не обязательно
// должны идти в последовательном порядке.
enum EmpType
{
Manager = 10,
Grunt = 1,
Contractor = 100,
VicePresident = 9
}
168 Часть II. Главные конструкции программирования на С#
Управление базовым типом, используемым
для хранения значений перечисления
По умолчанию для хранения значений перечисления используется тип System.
Int32 (который в С# называется просто int); однако при желании его легко заменить.
Перечисления в С# можно определять аналогичным образом для любых других
ключевых системных типов (byte, short, int или long). Например, чтобы сделать для
перечисления EmpType базовым тип byte, а не int, напишите следующий код:
//На этот раз ЕшрТуре отображается на тип byte.
enum EmpType : byte
{
Manager = 10,
Grunt = 1,
Contractor = 100,
VicePresident = 9
}
Изменять базовый тип перечислений удобно в случае создания таких приложений
.NET, которые будут развертываться на устройствах с небольшим объемом памяти
(таких как поддерживающие .NET сотовые телефоны или устройства PDA), чтобы
экономить память везде, где только возможно. Естественно, если для перечисления в
качестве базового типа указан byte, каждое значение в этом перечислении ни в коем случае
не должно выходить за рамки диапазона его допустимых значений. Например,
приведенная ниже версия EmpType вызовет ошибку на этапе компиляции, поскольку
значение 999 не вписывается в диапазон допустимых значений типа byte:
// Компилятор сообщит об ошибке! Значение 999
// является слишком большим для типа byte1
enum EmpType : byte
{
Manager = 10,
Grunt = 1,
Contractor = 100,
VicePresident = 999
}
Объявление переменных типа перечислений
После указания диапазона и базового типа перечисление можно использовать
вместо так называемых "магических чисел". Поскольку перечисления представляют собой
не более чем просто определяемый пользователем тип данных, их можно применять в
качестве возвращаемых функциями значений, параметров методов, локальных
переменных и т.д. Для примера давайте создадим метод по имени AskForBonus (),
принимающий в качестве единственного параметра переменную EmpType. На основе
значения этого входного параметра в окне консоли будет выводиться соответствующий ответ
на запрос о надбавке к зарплате.
class Program
{
static void Main(string[] args)
{
Console.WriteLine ("**** Fun with Enums *****");
// Создание типа Contractor.
EmpType emp = EmpType.Contractor;
AskForBonus(emp);
Console.ReadLine();
}
Глава 4. Главные конструкции программирования на С#: часть II 169
// Использование перечислений в качестве параметра.
static void AskForBonus(EmpType e)
{
switch (e)
{
case EmpType.Manager:
Console.WriteLine ("How about stock options instead?");
//He желаете ли взамен фондовые опционы?
break;
case EmpType.Grunt:
Console.WriteLine("You have got to be kidding...");
// Шутить изволите. . .
break;
case EmpType.Contractor:
Console.WriteLine("You already get enough cash...");
// У вас уже достаточно наличности...
break;
case EmpType.VicePresident:
Console.WriteLine("VERY GOOD, Sir1"); // Очень хорошо, сэр!
break;
}
}
}
Обратите внимание, что при присваивании значения переменной перечисления
перед значением (Grunt) должно обязательно указываться имя перечисления (EmpType).
Из-за того, что перечисления представляют собой фиксированные наборы пар "имя/
значение", устанавливать для переменной перечисления значение, которое не
определено напрямую в перечисляемом типе, не допускается:
static void ThisMethodWillNotCompile()
{
// Ошибка! Значения SalesManager в перечислении EmpType нет!
EmpType emp = EmpType.SalesManager;
// Ошибка! Забыли указать имя перечисления EmpType перед значением Grunt!
emp = Grunt;
}
Тип System. Enum
Интересный аспект перечислений в .NET связан с тем, что функциональность они
получают от типа класса System.Enum. В этом классе предлагается набор методов,
которые позволяют опрашивать и преобразовывать заданное перечисление. Одним из
наиболее полезных среди них является метод Enum.GetUnderlyingType (), который
возвращает тип данных, используемый для хранения значений перечислимого типа
(в рассматриваемом объявлении EmpType это System.Byte):
static void Main(string [ ] args)
{
Console.WriteLine("**** Fun with Enums *****");
// Создание типа Contractor.
EmpType emp = EmpType.Contractor;
AskForBonus(emp);
// Отображение информации о типе, который используется
// для хранения значений перечисления.
Console.WriteLine("EmpType uses a {0} for storage",
Enum.GetUnderlyingType(emp.GetType()));
Console.ReadLine() ;
}
170 Часть II. Главные конструкции программирования на С#
Заглянув в окно Object Browser (Браузер объектов) в Visual Studio 2010, можно
удостовериться, что метод Enum. GetUnderlyingType () требует передачи в качестве
первого параметра System.Type. Как будет подробно разъясняться в главе 16, Туре
представляет описание метаданных для заданной сущности .NET.
Один из возможных способов получения метаданных (как показывалось ранее)
предусматривает применение метода Get Туре (), который является общим для всех типов
в библиотеках базовых классов .NET. Другой способ состоит в использовании операции
typeof, поддерживаемой в С#. Одно из преимуществ этого способа связано с тем, что
он не требует объявления переменной сущности, описание метаданных которой
требуется получить:
//На этот раз для извлечения информации
//о типе применяется операция typeof.
Console.WriteLine("EmpType uses a {0} for storage",
Enum.GetUnderlyingType(typeof(EmpType)));
Динамическое обнаружение пар "имя/значение" перечисления
Помимо метода Enum.GetUnderlyingType (), все перечисления С# также
поддерживают метод по имени ToStringO, который возвращает имя текущего значения
перечисления в виде строки. Ниже приведен соответствующий пример кода:
static void Main(string [ ] args)
{
Console.WriteLine ("**** Fun with Enums *****");
EmpType emp = EmpType.Contractor;
// Выводит строку "emp is a Contractor".
Console.WriteLine("emp is a {0}.", emp.ToString());
Console.ReadLine() ;
}
Чтобы выяснить не имя, а значение определенной переменной перечисления, можно
просто привести ее к лежащему в основе типу хранения. Ниже показан пример, как это
делается:
static void Main(string[] args)
{
Console.WriteLine("**** Fun with Enums *****");
EmpType emp = EmpType.Contractor;
// Выводит строку "Contractor = 100".
Console.WriteLine("{0} = {1}", emp.ToString() , (byte)emp);
Console.ReadLine();
}
На заметку! Статический метод Enum. Format () позволяет производить более точное
форматирование за счет указания флага, представляющего желаемый формат. Более подробную
информацию о нем можно найти в документации .NET Framework 4.0 SDK.
В System. Enum также предлагается еще один статический метод по имени
GetValues (). Этот метод возвращает экземпляр System. Array. Каждый элемент в
массиве соответствует какому-то члену в указанном перечислении. Для примера
рассмотрим показанный ниже метод, который выводит в окне консоли пары "имя/значение",
имеющиеся в передаваемом в качестве параметра перечислении:
// Этот метод отображает детали любого перечисления.
static void EvaluateEnum(System.Enum e)
{
Глава 4. Главные конструкции программирования на С#: часть II 171
Console.WriteLine ("=> Information about {О}11,
e.GetType().Name);
Console.WriteLine("Underlying storage type: { 0}" ,
Enum.GetUnderlyingType(e.GetType()))/
// Получение всех пар "имя/значение"
// для входного параметра.
Array enumData = Enum.GetValues(e.GetType());
Console.WriteLine("This enum has {0} members.", enumData.Length),
// Количество членов:
/Л Вывод строкового имени и ассоциируемого значения
//с использованием флага D для форматирования (см. главу 3) .
for(int i=0; i < enumData.Length; i++)
{
Console.WriteLine("Name:
enumData.GetValue(i) ) ,
}
Console.WriteLine() ;
{0}, Value: {0:D}
}
Чтобы протестировать этот новый метод, давайте обновим Main () так, чтобы в нем
создавались переменные нескольких объявленных в пространстве имен System типов
перечислений (вместе с перечислением EmpType):
static void Main(string[] args)
{
Console.WriteLine("**** Fun with Enums
EmpType e2 = EmpType.Contractor;
// Эти типы представляют собой перечисления
//из пространства имен System.
DayOfWeek day = DayOfWeek.Monday;
ConsoleColor cc = ConsoleColor.Gray;
• * * * * ii
EvaluateEnum(e2);
EvaluateEnum(day);
EvaluateEnum(cc);
Console.ReadLine();
}
На рис. 4.3 показано, как будет выглядеть
вывод в этом случае.
Как можно будет убедиться по ходу настоящей
книги, перечисления очень широко
применяются во всех библиотеках базовых классов .NET.
Например, в ADO.NET множество перечислений
используется для обозначения состояния
соединения с базой данных (например, открыто оно
или закрыто) и состояния строки в DataTable
(например, является она измененной, новой или
отсоединенной). Поэтому в случае применения
любых перечислений следует всегда помнить о
наличии возможности взаимодействовать с
парами "имя/значение" в них с помощью членов
System.Enum.
Исходный код. Проект FunWithEnums доступен в
подкаталоге Chapter 4.
| C:\Windows\iysteml
шшш
[**** Fun with Enums
Information about EmpType
{Underlying storage type: System.Byte
TThis enum has 4 members.
{Name: Grunt, Value: 1
[Name: VicePresident, Value: 9
(Name: Manager, Value: 10
Name: Contractor, Value: 100
j=> Information about DayOfWeek
I [Underlying storage type: System.Int32
IfThis enum has 7 members.
Sunday, Value: 0
Monday, Value: 1
ilName: Tuesday, Value: 2
(Name: Wednesday, Value: 3
[Name: Thursday, Value: 4
lame: Friday, Value: 5
lame: Saturday, Value: 6
:> Information about ConsoleColor
| {underlying storage type: System.Int32
IfThis enum has 16 members.
Name: Black. Value: 0
Wame: DarkBlue, Value: 1
[Name: DarkGreen, Value: 2
Name: DarkCyan, Value: 3
JName: DarkRed, Value: 4
Name: DarkMaqenta, Value: 5
Name: Dark Ye How, Value: б
Name: Gray, Value: 7
|Name: DarkGray Value: 8
Blue, Value: 9
[Name: Green, Value: 10
■Name: Cyan, Value: 11
Name: Red, Value: 12
Name: Magenta, Value: 13
■Name: Yellow, Value: 14
white, Value: 15
ijinitiiiiiiiiiiiiUMtinii
Рис. 4.З. Динамическое получение пар
"имя/значение" типов перечислений
172 Часть II. Главные конструкции программирования на С#
Типы структур
Теперь, когда роль типов перечислений должна быть ясна, давайте рассмотрим
использование типов структур (struct) в .NET. Типы структур прекрасно подходят для
моделирования математических, геометрических и прочих "атомарных" сущностей в
приложении. Они (как и перечисления) представляют собой определяемые
пользователем типы, однако (в отличие от перечислений) просто коллекциями пар "имя/значение" -
не являются. Вместо этого они скорее представляют собой типы, способные содержать
любое количество полей данных и членов, выполняющих над ними какие-то операции.
На заметку! Если вы ранее занимались объектно-ориентированным программированием, можете
считать структуры "облегченными классами", поскольку они тоже предоставляют возможность
определять тип, поддерживающий инкапсуляцию, но не могут применяться для построения
семейства взаимосвязанных типов. Когда есть потребность в создании семейства
взаимосвязанных типов через наследование, нужно применять типы классов.
На первый взгляд процесс определения и использования структур выглядит очень
просто, но, как известно, сложности обычно скрываются в деталях. Чтобы приступить
к изучению основных аспектов структур, давайте создадим новый проект по имени
FunWithStructures. В С# структуры создаются с помощью ключевого слова struct.
Поэтому далее определим в проекте с использованием этого ключевого слова структуру
Point и добавим в нее две переменных экземпляра типа int и несколько методов для
взаимодействия с ними.
struct Point
{
// Поля структуры.
public int X;
public int Y;
// Добавление 1 к позиции (X, Y) .
public void Increment ()
{
X++; Y++;
}
// Вычитание 1 из позиции (X, Y) .
public void Decrement ()
{
X--; Y—;
}
// Отображение текущей позиции.
public void Display()
{
Console.WriteLineCX = {0}, Y= {1}", X, Y) ;
}
}
Здесь определены два целочисленных поля (X и Y) с использованием ключевого слова
public, которое представляет собой один из модификаторов управления доступом (см.
главу 5). Объявление данных с использованием ключевого слова public гарантирует
наличие у вызывающего кода возможности напрямую получать к ним доступ из данной
переменной Point (через операцию точки).
На заметку! Обычно определение общедоступных (public) данных внутри класса или структуры
считается плохим стилем. Вместо этого рекомендуется определять приватные (private)
данные и получать к ним доступ и изменять их с помощью общедоступных свойств. Более
подробно об этом будет рассказываться в главе 5.
Глава 4. Главные конструкции программирования на С#: часть II 173
Ниже приведен код метода Main (), с помощью которого можно протестировать тип
Point.
static void Main(string[] args)
I
Console.WriteLine("***** a First Look at Structures *****");
// Создание Point с первоначальными значениями X и Y.
Point myPoint;
myPoint.X = 34 9;
myPoint.Y =76;
myPoint.Display();
// Настройка значений Х и Y.
myPoint.Increment ();
myPoint.Display();
Console.ReadLine ();
}
Вывод, как и следовало ожидать, выглядит следующим образом:
***** д First Look at Structures *****
X = 349, Y = 76
X = 350, Y = 77
Создание переменных типа структур
Для создания переменной типа структуры на выбор доступно несколько вариантов.
Ниже просто создается переменная Point с присваиванием значений каждому из ее
общедоступных элементов данных типа полей перед вызовом ее членов. Если значения
общедоступным элементам структуры (в данном случае X и Y) не присвоены перед ее
использованием, компилятор сообщит об ошибке.
// Ошибка! Полю Y не было присвоено значение.
Point pi;
pl.X = 10;
pi.Display();
// Все в порядке1 Обоим полям были присвоены
// значения перед использованием.
Point p2;
р2.Х = 10;
p2.Y = 10;
' р2.Display();
В качестве альтернативного варианта переменные типа структур можно создавать с
применением ключевого слова new, поддерживаемого в С#, что предусматривает вызов
для структуры конструктора по умолчанию. По определению используемый по
умолчанию конструктор не принимает никаких аргументов. Преимущество подхода с вызовом
для структуры конструктора по умолчанию состоит в том, что в таком случае каждому
элементу данных полей автоматически присваивается соответствующее значение по
умолчанию:
// Установка для всех полей значений по умолчанию
//за счет применения конструктора по умолчанию.
Point pi = new Point () ;
// Выводит Х=0, Y=0
pi.Display();
Создавать структуру можно также с помощью специального конструктора, что
позволяет указывать значения для полей данных при создании переменной, а не уста-
174 Часть II. Главные конструкции программирования на С#
навливать их для каждого из них по отдельности. В главе 5 специальные конструкторы
будут рассматриваться более подробно; здесь же, чтобы в общем посмотреть, как их
использовать, изменим структуру Point и добавим в нее следующий код:
struct Point
{
// Поля структуры.
public int X;
public int Y;
// Специальный конструктор.
public Point (int XPos, int YPos)
{
X = XPos;
Y = YPos;
}
}
После этого переменные типа Point можно создавать так, как показано ниже:
// Вызов специального конструктора.
Point p2 = new Point E0, 60);
// Выводит Х=50, Y=60
р2.Display ();
Как упоминалось ранее, на первый взгляд процесс работы со структурами выглядит
довольно понятно. Однако чтобы лучше разобраться в нем, необходимо знать, в чем
состоит разница между типами значения и ссылочными типами в .NET.
Исходный код. Проект FunWithStructures доступен в подкаталоге Chapter 4.
Типы значения и ссылочные типы
На заметку! В приведенном далее обсуждении типов значения и ссылочных типов предполагается
наличие базовых знаний объектно-ориентированного программирования. Дополнительные
сведения по этому поводу даны в главах 5 и 6.
В отличие от массивов, строк и перечислений, структуры в С# не имеют
эквивалентного представления с похожим названием в библиотеке .NET (т.е. класса вроде System.
Structure не существует), но зато они все неявно унаследованы от класса System.
ValueType. Попросту говоря, роль класса System. ValueType заключается в
гарантировании размещения производного типа (например, любой структуры) в стеке, а не в куче
с автоматически производимой сборкой мусора. Данные, размещаемые в стеке, могут
создаваться и уничтожаться очень быстро, поскольку срок их жизни зависит только от
контекста, в котором они определены. За данными, размещаемыми в куче, наблюдает
сборщик мусора .NET, и время их существования зависит от целого ряда различных
факторов, которые более подробно рассматриваются в главе 8.
С функциональной точки зрения единственной задачей System.ValueType
является переопределение виртуальных методов, объявленных в System.Object, так, чтобы
в них использовалась семантика, основанная на значениях, а не на ссылках. Как,
возможно, уже известно, под переопределением понимается изменение реализации
виртуального (или, что тоже возможно, абстрактного) метода, определенного внутри базового
класса. Базовым классом для ValueType является System.Object. В действительности
Глава 4. Главные конструкции программирования на С#: часть II 175
методы экземпляра, определенные в System. ValueType, идентичны тем, что
определены в System.Object:
// Структуры и перечисления неявным образом
// расширяют возможности System.ValueType.
public abstract class ValueType : object
{
public virtual bool Equals(object obj ) ;
public virtual int GetHashCode ();
public Type GetTypeO ;
public virtual string ToStringO;
}
Из-за того, что в типах значения используется семантика, основанная на
значениях, время жизни структуры (которая включает все числовые типы данных, наподобие
int, float и т.д., а также любое перечисление или специальную структуру) получается
очень предсказуемым. При выходе переменной типа структуры за пределы контекста,
в котором она определялась, она сразу же удаляется из памяти.
// Локальные структуры извлекаются из стека
// после завершения метода.
static void LocalValueTypes ()
{
//В действительности int представляет
// собой структуру System.Int32.
int i = 0;
//В действительности Point представляет
// собой тип структуры.
Point p = new Point () ;
} // Здесь i и р изымаются из стека.
Типы значения, ссылочные типы и операция присваивания
Когда один тип значения присваивается другому, получается почленная копия
данных полей. В случае простого типа данных вроде System. Int32 единственным
копируемым членом является числовое значение. Однако в ситуации с типом Point
копироваться в новую переменную структуры будут два значения: X и Y. Чтобы
удостовериться в этом, давайте создадим новый проект типа Console Application по имени
ValueAndReferenceTypes, скопируем в новое пространство имен предыдущее
определение Point и добавим в Program следующий метод:
// Присваивание двух внутренних типов значения друг другу
// приводит к созданию двух независимых переменных в стеке.
static void ValueTypeAssignment()
{
Console.WriteLine("Assigning value types\n");
Point pi = new Point A0, 10);
Point p2 = pi;
// Вывод обеих переменных Point,
pi.Display();
p2.Display();
// Изменение значение pl.X и повторный вывод.
// Значение р2.Х не изменяется.
pl.X = 100;
Console.WriteLine("\n=> Changed pi.X\n");
pi.Display();
p2.Display();
}
176 Часть II. Главные конструкции программирования на С#
В коде сначала создается переменная типа Point (pi), которая затем присваивается
другой переменной типа Point (p2). Из-за того, что Point представляет собой тип
значения, в стеке размещаются две копии My Point, каждая из которых может
подвергаться отдельным манипуляциями. Поэтому при изменении значения pi .X значение р2 .X
остается неизменным. Ниже показано, как будет выглядеть вывод в сл}£чае выполнения
этого кода:
Assigning value types
X = 10, Y = 10
X = 10, Y = 10
=> Changed pi.X
X = 100, Y = 10
X = 10, Y = 10
В отличие от присваивания одного типа значения другому, в случае применения
операции присваивания в отношении ссылочных типов (т.е. всех экземпляров класса)
происходит переадресация на то, на что ссылочная переменная указывает в памяти.
Чтобы увидеть это на примере, давайте создадим новый тип класса по имени PointRef
с точно такими же членами, как у структуры Point, только переименуем конструктор в
соответствие с именем этого класса:
// Классы всегда представляют собой ссылочные типы.
class PointRef
{
// Те же члены, что и в структуре Point. . .
// Здесь нужно не забыть изменить имя конструктора на PointRef.
public PointRef(int XPos, int YPos)
{
X = XPos;
Y = YPos;
}
}
Теперь воспользуемся этим типом PointRef в показанном ниже новом методе.
Обратите внимание, что за исключением применения класса PointRef, а не
структуры Point, код в целом выглядит точно так же, как и код приведенного ранее метода
ValueTypeAssignment().
static void ReferenceTypeAssignment ()
{
Console.WriteLine("Assigning reference types\n");
PointRef pi = new PointRef A0, 10);
PointRef p2 = pi;
// Вывод обеих переменных PointRef.
pi.Display();
p2.Display();
// Изменение значения pl.X и повторный вывод.
pl.X = 100;
Console.WriteLine("\n=> Changed pi.X\n");
pi.Display();
p2.Display();
}
В данном случае создаются две ссылки, указывающие на один и тот же объект в
управляемой куче. Поэтому при изменении значения X по ссылке р2 значение pi; X
остается прежним. Ниже показано, как будет выглядеть вывод в случае вызова этого
нового метода из Main ().
Глава 4. Главные конструкции программирования на С#: часть II 177
Assigning reference types
X = 10, Y = 10
X = 10, Y = 10
=> Changed pl.X
X = 100, Y = 0
X = 100, Y = 10
Типы значения, содержащие ссылочные типы
Теперь, когда стало более понятно, в чем состоят основные отличия между типами
значения и ссылочными типами, давайте рассмотрим более сложный пример. Сначала
предположим, что в распоряжении имеется следующий ссылочный тип (класс) с
информационной строкой (infoString), которая может устанавливаться с помощью
специального конструктора:
class Shapelnfo
{
public string infoString;
public Shapelnfo(string info)
{ infoString = info; }
1
Пусть необходимо, чтобы переменная этого типа класса содержалась внутри типа
значения по имени Rectangle, и чтобы вызывающий код мог устанавливать значение
внутренней переменной экземпляра Shapelnfo, для чего также можно предусмотреть
специальный конструктор. Ниже показано полное определение типа Rectangle.
struct Rectangle
{
// Структура Rectangle содержит член ссылочного типа.
public Shapelnfo rectlnfo;
public int rectTop, rectLeft, rectBottom, rectRight;
public Rectangle(string info, int top, int left, int bottom, int right)
{
rectlnfo = new Shapelnfo(info);
rectTop = top; rectBottom = bottom;
rectLeft = left; rectRight = right;
}
public void Display()
{
Console.WnteLine ("String = {0}, Top = {1}, Bottom = {2}," +
"Left = {3}, Right = {4}",
rectlnfo.infoString, rectTop, rectBottom, rectLeft, rectPight);
}
}
Ссылочный тип содержится внутри типа значения, и отсюда, естественно, вытекает
важный вопрос о том, что же произойдет в результате присваивания одной переменной
типа Rectangle другой? Исходя из того, что известно о типах значения, можно верно
предположить, что целочисленные данные (которые в действительности образуют
структуру) должны являться независимой сущностью для каждой переменной Rectangle. Но
что будет с внутренним ссылочным типом? Будут ли копироваться все данные о
состоянии этого объекта, или же только ссылка на него? Чтобы получить ответ на этот вопрос,
определим показанный ниже метод и вызовем его в Main ().
178 Часть II. Главные конструкции программирования на С#
static void ValueTypeContainingRefType()
{
// Создание первой переменной Rectangle.
Console.WriteLine("-> Creating rl11);
Rectangle rl = new Rectangle ("First Rect", 10, 10, 50, 50);
// Присваивание второй переменной Rectangle ссылки на rl.
Console.WriteLine ("-> Assigning r2 to rl");
Rectangle r2 = rl;
// Изменение некоторых значений в г2.
Console.WriteLine ("-> Changing values of r2");
r2.rectlnfo.infoString = "This is new info1";
r2.rectBottom = 4 4 4 4;
// Вывод обеих переменных,
rl.Display();
r2.Display();
}
Ниже показан вывод, полученный в результате вызова этого метода:
-> Creating rl
-> Assigning r2 to rl
-> Changing values of r2
String = This is new info!, Top = 10, Bottom = 50, Left = 10, Right = 50
String = This is new info!, Top = 10, Bottom = 4444, Left = 10, Right = 50
Нетрудно заметить, что при изменении значения информационной строки с
использованием ссылки г2, значение ссылки rl остается прежним. По умолчанию, когда
внутри типа значения содержатся другие ссылочные типы, операция присваивания
приводит к копированию ссылок. В результате получаются две независимых структуры, в
каждой из которых содержится ссылка, указывающая на один и тот же объект в памяти
(т.е. поверхностная копия). При желании получить детальную копию, при которой в
новый объект копируются полностью все данные состояния внутренних ссылок, в
качестве одного из способов можно реализовать интерфейс ICloneable (см. главу 9).
Исходный код. Проект ValueAndReferenceTypes доступен в подкаталоге Chapter 4.
Передача ссылочных типов по значению
Очевидно, что ссылочные типы и типы значения могут передаваться членам в виде
параметров. Способ передачи ссылочного типа (например, класса) по ссылке, однако
довольно сильно отличается от способа его передачи по значению. Чтобы посмотреть,
в чем разница, давайте создадим новый проект типа Console Application по имени
Ref TypeValTypeParams и определим в нем следующий простой класс Person:
class Person
{
public string personName;
public int personAge;
// Конструкторы.
public Person(string name, int age)
{
personName = name;
personAge = age;
}
public Person () {}
Глава 4. Главные конструкции программирования на С#: часть II 179
public void Display ()
{
Console.WriteLine ("Name: {0}, Age: {1}", personName, personAge);
}
}
Теперь создадим метод, позволяющий вызывающему коду передавать тип Person по
значению (обратите внимание, что никакие модификаторы для параметров, подобные
out или ref, здесь не используются):
static void SendAPersonByValue(Person p)
{
// Изменение значения возраста в р.
p.personAge =99;
// Увидит ли вызывающий код это изменение?
р = new Person("Nikki", 99);
}
Важно обратить внимание, что метод SendAPersonByValue () пытается присвоить
входному аргументу Person ссылку на новый объект Person, а также изменить
некоторые данные состояния. Испробуем этот метод, вызвав его в Main ():
static void Main(string[] args)
{
// Передача ссылочных типов по значению.
Console.WriteLine ("***** Passing Person object by value *****");
Person fred = new Person ("Fred", 12);
Console.WriteLine("\nBefore by value call, Person is:"); // перед вызовом
fred.Display();
SendAPersonByValue(fred) ;
Console.WriteLine("\nAfter by value call, Person is:"); // после вызова
fred. Display();
Console.ReadLine();
}
Ниже показан результирующий вывод:
***** Passing Person object by value *****
Before by value call, Person is:
Name: Fred, Age: 12
After by value call, Person is:
Name: Fred, Age: 99
Как видите, значение PersoneAge изменилось. Кажется, что такое поведение
противоречит смыслу передачи параметра "по значению". Из-за удавшейся попытки
изменить состояние входного объекта Person возникает вопрос о том, что же тогда
было скопировано? А вот что: ссылка на объект вызывающего кода. Поскольку метод
SendAPersonByValue () теперь указывает на тот же самый объект, что и вызывающий
код, получается, что данные состояния объекта можно изменять. Что нельзя делать, так
это изменять объект, на который указывает ссылка.
Передача ссылочных типов по ссылке
Теперь создадим метод SendAPersonByReference (), в котором ссылочный тип
передается по ссылке (обратите внимание, что здесь для параметра используется
модификатор ref):
public static void SendAPersonByReference(ref Person p)
{
// Изменение некоторых данных в р.
p.personAge = 555;
180 Часть II. Главные конструкции программирования на С#
// Теперь р указывает на новый объект в куче.
р = new Person("Nikki", 222);
}
Нетрудно догадаться, что такой подход предоставляет вызывающему коду полную
свободу в плане манипулирования входным параметром. Вызывающий код может не
только изменять состояние объекта, но и переопределять ссылку так, чтобы она
указывала на новый тип Person. Давайте испробуем новый метод SendAPersonByRef erence (),
вызвав его в методе Main (), как показано ниже.
static void Main (string [ ] args)
{
// Передача ссылочных типов по ссылке.
Console. WriteLine ("***** Passing Person object by reference *****");
Person mel = new Person ("Mel", 23) ;
Console.WriteLine("Before by ref call, Person is:");
mel.Display();
SendAPersonByReference (ref mel);
Console.WriteLine("After by ref call, Person is:");
mel.Display ();
Console.ReadLine();
}
Ниже показано, как в этом случае выглядит вывод:
***** Passing Person object by reference *****
Before by ref call, Person is:
Name: Mel, Age: 2 3
After by ref call, Person is:
Name: Nikki, Age: 999
Как не трудно заметить, после вызова объект по имени Mel возвращается как объект
по имени Nikki, поскольку методу удалось изменить тип, на который указывала входная
ссылка в памяти. Ниже перечислены главные моменты, которые можно вынести из
всего этого и о которых следует всегда помнить при передаче ссылочных типов.
• В случае передачи ссылочного типа по ссылке вызывающий код может изменять
значения данных состояния объекта, а также сам объект, на который указывает
входная ссылка.
• В случае передачи ссылочного типа по значению вызывающий код может
изменять только значения данных состояния объекта, но не сам объект, на который
указывает входная ссылка.
Исходный код. Проект RefTypeValTypeParams доступен в подкаталоге Chapter 4.
Заключительные детали относительно типов
значения и ссылочных типов
В завершение темы ссылочных типов и типов значения ознакомьтесь с табл. 4.3, где
приведено краткое описание всех основных отличий между типами значения и
ссылочными типами.
Несмотря на все эти отличия, не следует забывать о том, что типы значения и
ссылочные типы обладают способностью реализовать интерфейсы и могут
поддерживать любое количество полей, методов, перегруженных операций, констант, свойств и
событий.
Глава 4. Главные конструкции программирования на С#: часть II 181
Таблица 4.3. Отличия между типами значения и ссылочными типами
Интересующий вопрос
Тип значения
Ссылочный тип
Где размещается этот тип?
Как представляется переменная?
Какой тип является базовым?
Может ли этот тип выступать в
роли базового для других типов?
Каково по умолчанию поведение
при передаче параметров?
Может ли в этом типе
переопределяться метод System.
Object.Finalize()?
Можно ли для этого типа
определять конструкторы?
Когда переменные этого типа
прекращают свое существование?
В стеке
В виде локальной копии
Должен обязательно
наследоваться от System.
ValueType
Нет. Типы значения всегда
являются запечатанными,
поэтому наследовать от них
нельзя
Переменные этого типа
передаются по значению (т.е.
вызываемой функции
передается копия переменной)
Нет. Типы значения, никогда
не размещаются в куче и
потому в финализации не
нуждаются
Да, но при этом следует
помнить, что имеется
зарезервированный конструктор
по умолчанию (это значит,
что все специальные
конструкторы должны
обязательно принимать аргументы)
Когда выходят за рамки
того контекста, в котором
определялись
В управляемой куче
В виде ссылки,
указывающей на занимаемое
соответствующим экземпляром
место в памяти
Может наследоваться от
любого другого типа (кроме
System.ValueType),
главное чтобы тот не был
запечатанным (см. главу 6)
Да. Если тип не является
запечатанным, он может
выступать в роли базового типа
для других типов
В случае типов значения
объект копируется по
значению, а в случае ссылочных
типов ссылка копируется по
значению
Да, неявным образом
(см. главу 8)
Безусловно!
Когда объект подвергается
сборке мусора
Нулевые типы в С#
В заключение настоящей главы давайте рассмотрим роль нулевых типов
данных (nullable data types) на примере создания консольного приложения по имени
NullableTypes. Как уже известно, типы данных CLR обладают фиксированным
диапазоном значений и имеют соответствующий представляющий их тип в
пространстве имен System. Например, типу данных System.Boolean могут присваиваться только
значения из набора {true, false}. Вспомните, что все числовые типы данных (а также
тип Boolean) представляют собой типы значении. Таким типам значение null никогда
не присваивается, поскольку оно служит для установки пустой ссылки на объект:
static void Main(string [ ] args)
{
// Компилятор сообщит об ошибке!
// Типам значения не может присваиваться значение null!
182 Часть II. Главные конструкции программирования на С#
bool myBool = null;
int mylnt = null;
// Здесь все в порядке, потому что строки представляют собой ссылочные типы.
string myString = null;
}
Возможность создавать нулевые типы появилась еще в версии .NET 2.0. Попросту
говоря, нулевой тип может принимать все значения лежащего в его основе типа плюс
значение null. Если объявить нулевым, например, тип bool, его допустимыми
значениями будут true, false и null. Это может оказаться чрезвычайно удобным при
работе с реляционными базами данных, поскольку в их таблицах довольно часто
встречаются столбцы с неопределенными значениями. Помимо нулевого типа данных в С#
больше не существует никакого удобного способа для представления элементов данных,
не имеющих значения.
Чтобы определить переменную нулевого типа, необходимо присоединить к имени
лежащего в основе типа данных знак вопроса (?). Обратите внимание, что
применение такого синтаксиса является допустимым лишь в отношении типов значения. При
попытке создать нулевой ссылочный тип (в том числе нулевой тип string)
компилятор будет сообщать об ошибке. Как и ненулевым переменным, нулевым локальным
переменным должно быть присвоено начальное значение, прежде чем их можно будет
использовать.
static void LocalNullableVariables ()
{
// Определение нескольких локальных переменных с нулевыми типами.
int? nullablelnt = 10;
double? nullableDouble = 3.14;
bool? nullableBool = null;
char? nullableChar = 'a1;
int?[] arrayOfNullablelnts = new int? [10];
// Ошибка! Строки относятся к ссылочным типам!
// string? s = "oops";
}
Синтаксис с использованием знака ? в качестве суффикса в С#
представляет собой сокращенный вариант создания экземпляра обобщенного типа структуры
System.Nullable<T>. Хотя об обобщениях будет подробно рассказываться лишь в
главе 10, уже сейчас важно понять, что тип System.Nullable<T> имеет набор членов,
которые могут применяться во всех нулевых типах.
Например, выяснить программным образом, было ли нулевой переменной
действительно присвоено значение null, можно с помощью свойства HasValue или операции
! =, а получить присвоенное нулевому типу значение — либо напрямую, либо через
свойство Value. На самом деле, из-за того, что суффикс ? является всего лишь
сокращенным вариантом использования типа Nullable<T>, метод LocalNullableVariables ()
вполне можно было бы реализовать и следующим образом:
static void LocalNullableVariablesUsingNullable ()
{
// Определение нескольких нулевых типов за счет использования Nullable<T>.
Nullable<int> nullablelnt = 10;
Nullable<double> nullableDouble = 3.14;
Nullable<bool> nullableBool = null;
Nullable<char> nullableChar = 'a';
Nullable<int>[] arrayOfNullablelnts = new int? [10];
}
Глава 4. Главные конструкции программирования на С#: часть II 183
Работа с нулевыми типами
Как упоминалось ранее, нулевые типы данных особенно полезны при
взаимодействии с базами данных, поскольку некоторые столбцы внутри таблиц данных в них могут
преднамеренно делаться пустыми (с неопределенными значениями). Для примера
рассмотрим приведенный ниже класс, в котором имитируется процесс получения
доступа к базе данных с таблицей, два столбца в которой могут принимать значения null.
Обратите внимание, что в методе GetlntFromDatabase () значение нулевой
целочисленной переменной экземпляра не присваивается, а в методе GetBoolFromDatabase ()
значение члену bool? присваивается:
class DatabaseReader
{
// Нулевые поля данных.
public int? numericValue = null;
public bool? boolValue = true;
// Обратите внимание на использование
// нулевого возвращаемого типа.
public int? GetlntFromDatabase()
{ return numericValue; }
// Здесь тоже обратите внимание на использование
// нулевого возвращаемого типа.
public bool? GetBoolFromDatabase ()
{ return boolValue; }
}
Теперь давайте создадим следующий метод Main (), в котором будет вызываться
каждый из членов класса DatabaseReader и выясняться, какие значения были им
присвоены, с помощью членов Has Value и Value, а также поддерживаемой в С# операции
равенства (точнее — операции "не равно"):
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Nullable Data *****\n");
DatabaseReader dr = new DatabaseReader ();
// Получение значения int из "базы данных".
int? i = dr.GetlntFromDatabase();
if (i.HasValue)
// вывод значения i
Console.WriteLine("Value of 'i' is: {0}", i.Value);
else
// значение i не определено
Console.WriteLine("Value of 'i1 is undefined.");
// Получение значения bool из "базы данных" .
bool? b = dr.GetBoolFromDatabase();
if (b != null)
// вывод значения b
Console.WriteLine("Value of 'b' is: {0}", b.Value);
else
// значение b не определено
Console.WriteLine("Value of 'b' is undefined.");
Console.ReadLine();
184 Часть II. Главные конструкции программирования на С#
Операция'??'
Последним аспектом нулевых типов, о котором следует знать, является
использование с ними операции ??, поддерживаемой в С#. Эта операция позволяет присваивать
значение нулевому типу, если извлеченное значение равно null. Для примера
предположим, что в случае возврата методом GetlntFromDatabase () значения null
(разумеется, этот метод запрограммирован всегда возвращать null, но тут главное уловить
общую идею) локальной целочисленной переменной нулевого типа должно присваиваться
значение 100:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Nullable Data *****\n");
DatabaseReader dr = new DatabaseReader();
// В случае возврата GetlntFromDatabase()
// значения null локальной переменной должно
// присваиваться значение 100.
int myData = dr.GetlntFromDatabase() ?? 100;
Console.WriteLine("Value of myData: {0}", myData);
Console.ReadLine();
}
Преимущество подхода с применением операции ? ? в том, что он обеспечивает
более компактную версию кода, чем применение традиционной условной конструкции
if /else. При желании можно написать следующий функционально эквивалентный
код, который в случае null устанавливает значение переменной равным 100:
// Более длинная версия применения синтаксиса ? : ??.
int? moreData = dr.GetlntFromDatabase();
if (ImoreData.HasValue)
moreData = 100;
Console.WriteLine("Value of moreData: {0}", moreData);
Исходный код. Приложение NullableType доступно в подкаталоге Chapter 4.
Резюме
В настоящей главе сначала рассматривался ряд ключевых слов С#, которые
позволяют создавать специальные методы. Вспомните, что по умолчанию параметры
передаются по значению, однако их можно передавать и по ссылке за счет добавления к ним
модификатора ref или out. Также было рассказано о роли необязательных и
именованных параметров и о том, как определять и вызывать методы, принимающие массивы
параметров.
В главе рассматривалась перегрузка методов, определение массивов, перечислений
и структур в С# и их представления в библиотеках базовых классов .NET. Были описаны
некоторые детали, касающиеся т ипов значения и ссылочных типов, в том числе то,
как они ведут себя при передаче в качестве параметров методам, и то, каким образом
взаимодействовать с нулевыми типами данных с помощью операций ? и ? ?.
На этом первоначальное знакомство с языком программирования С#
завершено. В следующей главе начинается изучение деталей объектно-ориентированной
разработки.
ГЛАВА 5
Определение
инкапсулированных
типов классов
В предыдущих двух главах мы исследовали ряд основных синтаксических
конструкций, присущих любому приложению .NET, которое вам придется
разрабатывать. Здесь мы приступим к изучению объектно-ориентированных возможностей С#.
Первое, что предстоит узнать — это процесс построения четко определенных типов
классов, которые поддерживают любое количество конструкторов. После описания
основ определения классов и размещения объектов в остальной части главы
рассматривается тема инкапсуляции. По ходу изложения вы узнаете, как определяются свойства
класса, а также какова роль статических полей, синтаксиса инициализации объектов,
полей, доступных только для чтения, константных данных и частичных классов.
Знакомство с типом класса С#
Что касается платформы .NET, то наиболее фундаментальной программной
конструкцией является тип класса. Формально класс — это определяемый пользователем
тип, который состоит из данных полей (часто именуемых переменными-членами) и
членов, оперирующих этими данными (конструкторов, свойств, методов, событий и т.п.).
Все вместе поля данных класса представляют "состояние" экземпляра класса (иначе
называемого объектом). Мощь объектных языков, подобных С#, состоит в их
способности группировать данные и связанную с ними функциональность в определении класса,
что позволяет моделировать программное обеспечение на основе сущностей реального
мира.
Для начала создадим новое консольное приложение С# по имени SimpleClassExample.
Затем добавим в проект новый файл класса (по имени Car.cs), используя пункт меню
Projects Add Class (Проект^ Добавить класс), выберем пиктограмму Class (Класс) в
результирующем диалоговом окне, как показано на рис. 5.1, и щелкнем на кнопке Add
(Добавить).
Класс определятся в С# с помощью ключевого слова class. Вот как выглядит
простейшее из возможных объявление класса:
class Car
{
}
186 Часть II. Главные конструкции программирования на С#
Add New Item - SimpleClasiExample ( & ЦИМИЦ
1 Visual Studic
Installed Templates
J л Visual C# Items
Data
General
Web
Windows Forms
WPF
Reporting
1 Online Templates
1
■
Sort by j Default ... *\ :d (ill [ Search Installed Temptates... Г]|
СП Code File Visual C# hems j C,Mi
1 ' 1 Type Visual C# Items
С*Ц Class N Visual C# Items | An empty class definition
'4 1 Class I
«W* ADO.NET EntotyOTjerrt3ene...V>sual C# Items
3cw Interface v,suar c# Items
!
I
<rj 1
1 Name Car.cs
1 Add ]| Cancel | 1
Рис. 5.1. Добавление нового типа класса С#
После определения типа класса нужно определить набор переменных-членов,
которые будут использоваться для представления его состояния. Например, вы можете
решить, что объекты Саг (автомобили) должны иметь поле данных типа int,
представляющее текущую скорость, и поле данных типа string для представления
дружественного названия автомобиля. С учетом этих начальных положений дизайна класс Саг
будет выглядеть следующим образом:
class Car
{
// 'Состояние' объекта Саг.
public string petName;
public int currSpeed;
}
Обратите внимание, что эти переменные-члены объявлены с использованием
модификатора доступа public. Общедоступные (public) члены класса доступны
непосредственно, как только создается объект данного типа. Как вам, возможно, уже известно,
термин "объект" служит для представления экземпляра данного типа класса,
созданного с помощью ключевого слова new.
На заметку! Поля данных класса редко (если вообще когда-нибудь) должны определяться с
модификатором public. Чтобы обеспечить целостность данных состояния, намного лучше объявлять
данные приватными (private) или, возможно, защищенными (protected) и открывать
контролируемый доступ к данным через свойства типа (как будет показано далее в этой главе). Однако
чтобы сделать первый пример насколько возможно простым, оставим данные общедоступными.
После определения набора переменных-членов, представляющих состояние класса,
следующим шагом в проектировании будет создание членов, которые моделируют его
поведение. Для данного примера класс Саг определяет один метод по имени SpeedUp () и
еще один — по имени PrintState(). Модифицируйте код класса следующим образом:
class Car
{
// 'Состояние' объекта Саг.
public string petName;
Глава 5. Определение инкапсулированных типов классов 187
public int currSpeed;
// Функциональность Car.
public void PrintStateO
{
Console.WriteLine ("{0 } is going {1} MPH.11, petName, currSpeed);
}
public void SpeedUp(int delta)
{
currSpeed += delta;
}
}
Метод PrintState () — это более или менее диагностическая функция, которая
просто выводит текущее состояние объекта Саг в окно командной строки. Метод SpeedUp ()
повышает скорость Саг, увеличивая ее на величину, переданную во входящем
параметре типа int. Теперь обновите код метода Main(), как показано ниже:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Class Types *****\n");
// Разместить в памяти и сконфигурировать объект Саг.
Car myCar = new Car();
myCar.petName = "Henry";
myCar.currSpeed = 10;
// Повысить скорость автомобиля в несколько раз
//и вывести новое состояние.
for (int i = 0; i <= 10; i++)
{
myCar.SpeedUp E);
myCar.PrintState ();
}
Console.ReadLine();
}
Запустив программу, вы увидите, что переменная Car (myCar) поддерживает свое
текущее состояние на протяжении жизни всего приложения, как показано в следующем
выводе:
***** Fun W1th Class Types *****
Henry is going 15 MPH.
Henry is going 20 MPH.
Henry is going 25 MPH.
Henry is going 30 MPH.
Henry is going 35 MPH.
Henry is going 40 MPH.
Henry is going 45 MPH.
Henry is going 50 MPH.
Henry is going 55 MPH.
Henry is going 60 MPH.
Henry is going 65 MPH.
Размещение объектов с помощью ключевого слова new
Как было показано в предыдущем примере кода, объекты должны быть размещены
в памяти с использованием ключевого слова new. Если ключевое слово new не указать
и попытаться воспользоваться переменной класса в следующем операторе кода, будет
получена ошибка компиляции. Например, следующий метод Main() компилироваться
не будет:
188 Часть II. Главные конструкции программирования на С#
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Class Types *****\n");
// Ошибка1 Забыли использовать new для создания объекта!
Car myCar;
myCar.petName = "Fred";
}
Чтобы корректно создать объект с использованием ключевого слова new, можно
определить и разместить в памяти объект Саг в одной строке кода:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Class Types *****\n");
Car myCar = new Car();
myCar.petName = "Fred";
}
В качестве альтернативы, определение и размещение в памяти экземпляра класса
может осуществляться в разных строках кода:
static void Main(string[] args)
{
Console.WriteLine (••***** Fun with Class Types *****\n");
Car myCar;
myCar = new Car() ;
myCar.petName = "Fred";
}
Здесь первый оператор кода просто объявляет ссылку на еще не созданный объект
типа Саг. Только после явного присваивания ссылка будет указывать на
действительный объект в памяти.
В любом случае, к этому моменту мы получили простейший тип класса, который
определяет несколько элементов данных и некоторые базовые методы. Чтобы
расширить функциональность текущего класса Саг, необходимо разобраться с ролью
конструкторов.
Понятие конструктора
Учитывая, что объект имеет состояние (представленное значениями его
переменных-членов), программист обычно желает присвоить осмысленные значения полям
объекта перед тем, как работать с ним. В настоящий момент тип Саг требует
присваивания значений полям perName и currSpeed. Для текущего примера это не слишком
проблематично, поскольку общедоступных элементов данных всего два. Однако нередко
классы состоят из нескольких десятков полей. Ясно, что было бы нежелательно писать
20 операторов инициализации для всех 20 элементов данных такого класса.
К счастью, в С# поддерживается механизм конструкторов, которые позволяют
устанавливать состояние объекта в момент его создания. Конструктор (constructor) — это
специальный метод класса, который вызывается неявно при создании объекта с
использованием ключевого слова new. Однако в отличие от "нормального" метода, конструктор
никогда не имеет возвращаемого значения (даже void) и всегда именуется идентично
имени класса, который он конструирует.
Роль конструктора по умолчанию
Каждый класс С# снабжается конструктором по умолчанию, который при
необходимости может быть переопределен. По определению такой конструктор никогда не при-
Глава 5. Определение инкапсулированных типов классов 189
нимает аргументов. После размещения нового объекта в памяти конструктор по
умолчанию гарантирует установку всех полей в соответствующие стандартные значения
(значениях по умолчанию для типов данных С# описаны в главе 3).
Если вы не удовлетворены такими присваиваниями по умолчанию, можете
переопределить конструктор по умолчанию в соответствии со своими нуждами. Для
иллюстрации модифицируем класс С#, как показано ниже:
class Car
{
// 'Состояние' объекта Саг.
public string petName;
public int currSpeed;
// Специальный конструктор по умолчанию.
public Car ()
{
petName = "Chuck";
currSpeed = 10;
}
}
В данном случае мы заставляем объекты Саг начинать свою жизнь под именем
Chuck и скоростью 10 миль в час. При этом создавать объекты со значениями по
умолчанию можно следующим образом: а
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Class Types *****\n");
// Вызов конструктора по умолчанию.
Car chuck = new Car();
// Печатает "Chuck is going 10 MPH."
chuck.PrintState();
}
Определение специальных конструкторов
Обычно помимо конструкторов по умолчанию в классах определяются
дополнительные конструкторы. При этом пользователь объекта обеспечивается простым и
согласованным способом инициализации состояния объекта непосредственно в момент его
создания. Взгляните на следующее изменение класса Саг, который теперь
поддерживает целых три конструктора:
class Car
{
// 'Состояние' объекта Саг.
public string petName;
public int currSpeed;
// Специальный конструктор по умолчанию.
public Car()
{
petName = "Chuck";
currSpeed = 10;
}
// Здесь currSpeed получает значение
//no умолчанию типа int (t)) .
public Car(string pn)
{
petName = pn;
}
190 Часть II. Главные конструкции программирования на С#
// Позволяет вызывающему коду установить полное состояние Саг.
public Car(string pn, int cs)
{
petName = pn;
currSpeed = cs;
}
}
Имейте в виду, что один конструктор отличается от другого (с точки зрения
компилятора С#) количеством и типом аргументов. В главе 4 было показано, что определение
методов с одним и тем же именем, но разным количеством и типами аргументов,
называется перегрузкой. Таким образом, класс Саг имеет перегруженный конструктор,
чтобы предоставить несколько способов создания объекта во время объявления. В любом
случае, теперь можно создавать объекты Саг, используя любой из его общедоступных,
конструкторов. Например:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Class Types *****\n");
// Создать объект Car no имени Chuck со скоростью 10 миль в час.
Car chuck = new Car();
chuck.PrintState();
// Создать объект Car no имени Mary со скоростью 0 миль в час.
Car тагу = new Car ("Mary") ;
тагу.PrintState();
// Создать объект Car no имени Daisy со скоростью 75 миль в час.
Car daisy = new Car ("Daisy", 75);
daisy.PrintState ();
}
Еще раз о конструкторе по умолчанию
Как вы только что узнали, все классы "бесплатно" снабжаются конструктором по
умолчанию. Таким образом, если добавить в текущий проект новый класс по имени
Motorcycle, определенный следующим образом:
class Motorcycle
{
public void PopAWheelyO
{
Console.WriteLine("Yeeeeeee Haaaaaeewww'");
}
}
то сразу можно будет создавать экземпляры Motorcycle с помощью конструктора по
умолчанию:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Class Types *****\n");
Motorcycle mc = new Motorcycle();
mc.PopAWheely();
}
Однако, как только определен специальный конструктор, конструктор по умолчанию
молча удаляется из класса и становится недоступным! Воспринимайте это так: если вы
не определили специального конструктора, компилятор С# снабжает класс конструктором
по умолчанию, чтобы позволить пользователю объекта размещать его в памяти с набором
Глава 5. Определение инкапсулированных типов классов 191
данных, имеющих значения по умолчанию. В случае же, когда определяется уникальный
конструктор, компилятор предполагает, что вы решили взять власть в свои руки.
Таким образом, чтобы позволить пользователю объекта создавать экземпляр типа
посредством конструктора по умолчанию, а также специального конструктора,
понадобится явно переопределить конструктор по умолчанию. И, наконец, в подавляющем
большинстве случаев реализация конструктора класса по умолчанию намеренно
остается пустой, поскольку все, что требуется — это создание объекта со значениями всех
полей по умолчанию. Внесем в класс Motorcycle следующие изменения:
class Motorcycle
{
public int driverlntensity;
public void PopAWheelyO
{
for (int 1=0; l <= driverlntensity; i++)
{
Console.WriteLine("Yeeeeeee Haaaaaeewww! ") ;
}
}
// Вернуть конструктор по умолчанию, который будет устанавливать
// для всех членов данных значения по умолчанию.
public Motorcycle() {}
// Специальный конструктор.
public Motorcycle (int intensity)
{ driverlntensity = intensity; }
}
Роль ключевого слова this
В языке С# имеется ключевое слово this, которое обеспечивает доступ к
текущему экземпляру класса. Одно из возможных применений ключевого слова this состоит
в том, чтобы разрешать неоднозначность контекста, которая может возникнуть, когда
входящий параметр назван так же, как поле данных данного типа. Разумеется, в
идеале необходимо просто придерживаться соглашения об именовании, которое не может
привести к такой неоднозначности; однако чтобы проиллюстрировать такое
использование ключевого слова this, добавим в класс Motorcycle новое поле типа string
(по имени name), представляющее имя водителя. После этого добавим метод по имени
SetDriverNameO, реализованный следующим образом:
class Motorcycle
{
public int driverlntensity;
// Новые члены, представляющие имя водителя.
public string name;
public void SetDriverName(string name)
{ name = name; }
}
Хотя этот код нормально компилируется, Visual Studio 2010 отобразит
предупреждающее сообщение о том, что переменная присваивается сама себе! Чтобы проиллюст-'
рировать это, добавим в Main() вызов SetDriverNameO и выведем значение поля name.
Обнаружится, что значением поля name осталась пустая строка!
// Усадим на Motorcycle байкера по имени Tiny?
Motorcycle с = new Motorcycle E);
с.SetDriverName("Tiny");
c.PopAWheely();
Console.WriteLine("Rider name is {0}", c.name); // Выводит пустое значение name!
192 Часть II. Главные конструкции программирования на С#
Проблема в том, что реализация SetDriverNameO выполняет присваивание
входящему параметру его же значения, поскольку компилятор предполагает, что name здесь
ссылается на переменную, существующую в контексте метода, а не на поле name в
контексте класса. Для информирования компилятора, что необходимо установить
значение поля данных текущего объекта, просто используйте this для разрешения этой
неоднозначности:
public void SetDnverName (string name)
{ this.name = name; }
Имейте в виду, что если неоднозначности нет, то вы не обязаны использовать
ключевое слово this, когда классу нужно обращаться к собственным данным или членам.
Например, если переименовать член данных name типа string в driverName (что
также потребует обновления метода Main()), то применение this станет не обязательным,
поскольку исчезает неоднозначность контекста:
class Motorcycle
{
public int driverlntensity;
public string driverName;
public void SetDriverName(string name)
{
// Эти два оператора функционально эквивалентны.
driverName = name;
this.driverName = name;
}
Помимо небольшого выигрыша от использования this в неоднозначных
ситуациях, это ключевое слово может быть полезно при реализации членов, поскольку такие
IDE-среды, как SharpDevelop и Visual Studio 2010, включают средство IntelliSense, когда
вводится this. Это может здорово помочь, когда вы забыли название члена класса и
хотите быстро вспомнить его определение. Взгляните на рис. 5.2.
Motorcycle.cs* X
иЩЩ
»I ♦SetDriverName(itring name)
^StmpleOassExample.Motorcycle
.using System.Text;
''namespace SimpleClassExample
"' class Motorcycle
{
public int driverlntensity;
public string driverName;
public void SetDriverName(string nai
{ this.JdriverName - name; }
<Ctrl*Art*Space>
public H
driverlntensity
f ori ♦ driverName
{ ♦ Equals
i <* GetHashCode
> j ■• GetType
)* MemberwiseClone
rstrd:#E
; ♦ SetDnverName
: ♦ ToString
Intensity; i++)
teee Haaeaaeewww Iм);
void MotorcyclePopAWhedyQl
Рис. 5.2. Активизация средства IntelliSense для this
Глава 5. Определение инкапсулированных типов классов • 193
На заметку! Применение ключевого слова this внутри реализации статического члена
приводит к ошибке компиляции. Как будет показано, статические члены оперируют на уровне класса
(а не объекта), а на этом уровне нет текущего объекта, потому и не существует this!
Построение цепочки вызовов конструкторов
с использованием this
Другое применение ключевого слова this состоит в проектировании класса,
использующего технику под названием сцепление конструкторов или цепочка
конструкторов (constructor chaining). Этот шаблон проектирования полезен, когда имеется класс,
определяющий несколько конструкторов. Учитывая тот факт, что конструкторы часто
проверяют входящие аргументы на соблюдение различных бизнес-правил, возникает
необходимость в избыточной логике проверки достоверности внутри множества
конструкторов. Рассмотрим следующее измененное объявление класса Motorcycle:
class Motorcycle
{
public int driverlntensity;
public string driverName;
public Motorcycle () { }
// Избыточная логика конструктора!
public Motorcycle (int intensity)
{
if (intensity > 10)
{
intensity = 10;
}
driverlntensity = intensity;
}
public Motorcycle(int intensity, string name)
{
if (intensity > 10)
{
intensity = 10;
}
driverlntensity = intensity;
driverName = name;
}
}
Здесь (возможно, стараясь обеспечить безопасность гонщика) в каждом
конструкторе предпринимается проверка, что уровень мощности не превышает 10. Хотя все это
правильно и хорошо, в двух конструкторах появляется избыточный код. Это далеко от
идеала, поскольку придется менять код в нескольких местах в случае изменения правил
(например, если предельное значение мощности будет установлено равным 5).
Один из способов исправить создавшуюся ситуацию состоит в определении в классе
Motorcycle метода, который выполнит проверку входных аргументов. Если поступить
так, то каждый конструктор должен будет вызывать этот метод перед присваиванием
значений полям. Хотя такой подход позволяет изолировать код, который придется
обновлять при изменении бизнес-правил, теперь появилась другая избыточность:
class Motorcycle
{
public int driverlntensity;
public string driverName;
194 Часть II. Главные конструкции программирования на С#
// Конструкторы.
public Motorcycle() { }
public Motorcycle (int intensity)
{
Setlntensity(intensity);
}
public Motorcycle(int intensity, string name)
{
Setlntensity(intensity);
driverName = name;
}
public void Setlntensity(int intensity)
{
if (intensity > 10)
{
intensity = 10;
}
driverlntensity = intensity;
Более ясный подход предусматривает назначение конструктора, который
принимает максимальное количество аргументов, в качестве "ведущего конструктора", с
реализацией внутри него необходимой логики проверки достоверности. Остальные
конструкторы смогут использовать ключевое слово this, чтобы передать входные аргументы
ведущему конструктору и при необходимости предоставить любые дополнительные
параметры. В результате беспокоиться придется только о поддержке единственного
конструктора для всего класса, в то время как остальные конструкторы остаются в основном
пустыми.
Ниже приведена финальная реализация класса Motorcycle (с дополнительным
конструктором в целях иллюстрации). При связывании конструкторов в цепочку обратите
внимание, что ключевое слово this располагается вне тела конструктора (и отделяется
от его имени двоеточием):
class Motorcycle
{
public int driverlntensity;
public string driverName;
// Связывание конструкторов в цепочку.
public Motorcycle () {}
public Motorcycle (int intensity)
: this(intensity, "") { }
public Motorcycle(string name)
: this@, name) { }
// Это 'ведущий конструктор' , выполняющий всю реальную работу.
public Motorcycle(int intensity, string name)
{
if (intensity > 10)
{
intensity = 10;
}
driverlntensity = intensity;
driverName = name;
Глава 5. Определение инкапсулированных типов классов 195
Имейте в виду, что применение ключевого слова this для связывания вызовов
конструкторов в цепочку вовсе не обязательно. Однако использование такой техники
позволяет получить лучше сопровождаемое и более краткое определение кода. С
помощью этой техники также можно упростить решение программистских задач, поскольку
реальная работа'делегируется единственному конструктору (обычно имеющему
максимальное количество параметров), в то время как остальные просто передают ему
ответственность.
Обзор потока конструктора
Напоследок отметим, что как только конструктор передал аргументы выделенному
ведущему конструктору (и этот конструктор обработал данные), вызывающий
конструктор продолжает выполнение всех остальных операторов. Чтобы прояснить мысль,
модифицируем конструкторы класса Motorcycle, добавив вызов Console.WriteLine():
class Motorcycle
{
public int driverlntensity;
public string driverName;
// Связывание конструкторов в цепочку.
public Motorcycle ()
Console.WriteLine ("In default ctor");
public Motorcycle (int intensity) : this(intensity, "")
Console.WriteLine ("In ctor taking an int");
public Motorcycle(string name) : this@, name)
Console.WriteLine("In ctor taking a string");
// Это 'ведущий конструктор' , выполняющий всю реальную работу.
public Motorcycle(int intensity, string name)
Console.WriteLine("In master ctor ");
if (intensity > 10)
{
intensity = 10;
}
driverlntensity = intensity;
driverName = name;
}
}
Теперь изменим метод Main(), чтобы он обрабатывал объект Motorcycle, как
показано ниже:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Class Types *****\n");
// Создание Motorcycle.
Motorcycle с = new Motorcycle E);
с.SetDriverName("Tiny");
c.PopAWheely();
Console.WriteLine("Rider name is {0}", с.driverName); // вывод имени гонщика
Console.ReadLine();
196 Часть II. Главные конструкции программирования на С#
Вывод, полученный в результате выполнения предыдущего метода Main(), выглядит
следующим образом:
***** Fun with Class Types *****
In master ctor
In ctor taking an int
Yeeeeeee Haaaaaeewww!
Yeeeeeee Haaaaaeewww!
Yeeeeeee Haaaaaeewww!
Yeeeeeee Haaaaaeewww!
Yeeeeeee Haaaaaeewww!
Yeeeeeee Haaaaaeewww!
Rider name is Tiny
Поток логики конструкторов описан ниже.
• Создается объект за счет вызова конструктора, который принимает один
аргумент типа int.
• Конструктор передает полученные данные ведущему конструктору, добавляя
необходимые дополнительные начальные аргументы, не указанные вызывающим
кодом.
• Ведущий конструктор присваивает входные данные полям объекта.
• Управление возвращается первоначально вызванному конструктору, который
выполняет остальные операторы кода.
В построении цепочки конструкторов замечательно то, что этот шаблон
программирования работает с любой версией языка С# и платформой .NET. Однако если в случае
если целевой платформой является .NET 4.0 и выше, можно еще более упростить задачу
программирования за счет использования необязательных аргументов в качестве
альтернативы традиционным цепочкам конструкторов.
Еще раз об необязательных аргументах
Вы главе 4 вы узнали об необязательных и именованных аргументах. Вспомните, что
необязательные аргументы позволяют определять значения по умолчанию для входных
аргументов. Если вызывающий код удовлетворяют эти значения по умолчанию,
указывать уникальные значения не обязательно, но это можно делать, когда объект требуется
снабдить специальными данными. Рассмотрим следующую версию класса Motorcycle,
который теперь предоставляет несколько возможностей конструирования объектов,
используя единственное определение конструктора.
class Motorcycle
{
// Единственный конструктор, использующий необязательные аргументы.
public Motorcycle(int intensity = 0, string name = "")
{
if (intensity > 10)
{
intensity = 10;
}
driverlntensity = intensity;
driverName = name;
}
}
Глава 5. Определение инкапсулированных типов классов 197
С этим единственным конструктором можно создать объект Motorcycle, используя
ноль, один или два аргумента. Вспомните, что синтаксис именованных аргументов
позволяет, по сути, пропускать приемлемые установки по умолчанию (см. главу 4).
static void MakeSomeBikes()
{
// driverName = "", driverlntensity = 0
Motorcycle ml = new Motorcycle();
Console.WriteLine("Name= {0}, Intensity= {1}",
ml.driverName, ml.driverlntensity);
// driverName = "Tiny" , driverlntensity = 0
Motorcycle m2 = new Motorcycle(name:"Tiny");
Console.WriteLine("Name= {0}, Intensity= {1}",
m2.driverName, m2.driverlntensity);
// driverName = "", driverlntensity = 7
Motorcycle m3 = new Motorcycle G) ;
Console.WriteLine("Name= {0}, Intensity= {1}",
m3.driverName, m3.driverlntensity);
}
Хотя применение необязательных/именованных аргументов — очень удобный путь
упрощения определения набора конструкторов, используемых определенным классом,
следует всегда помнить, что этот синтаксис привязывает компиляцию приложений к
С# 2010, а их выполнение — к .NET 4.O. Если требуется построить классы, которые
должны выполняться на платформе .NET любой версии, лучше придерживаться
классической технологии цепочек конструкторов.
В любом случае, теперь можно определить класс с данными полей (переменными-
членами) и различными членами, которые могут быть созданы с помощью любого
количества конструкторов. Теперь давайте формализуем роль ключевого слова static.
Исходный код. Проект SimpleClassExample доступен в подкаталоге Chapter 5.
Понятие ключевого слова static
Класс С# может определять любое количество статических членов с
использованием ключевого слова static. При этом соответствующий член должен вызываться
непосредственно на уровне класса, а не на объектной ссылке. Чтобы проиллюстрировать
разницу, обратимся к System.Console. Как уже было показано, метод WriteLine()
не вызывается на уровне объекта:
// Ошибка! WriteLine() не является методом уровня объекта1
Console с = new Console ();
с.WriteLine("I can't be printed...");
Вместо этого статический член WriteLine () предваряется именем класса:
// Правильно! WriteLine() - статический метод.
Console.WriteLine("Thanks...");
Проще говоря, статические члены — это элементы, задуманные (проектировщиком
класса) как общие, так что нет нужды создавать экземпляр типа при их вызове. Когда
в любом классе определены только статические члены, такой класс можно считать
"обслуживающим классом". Например, если воспользоваться браузером объектов Visual
Studio 2010 (выбрав пункт меню View^Object Browser (Вид1^Браузер объектов)) для
просмотра пространства имен System сборки mscorlib.dll, можно увидеть, что все члены
классов Console, Math, Environment и GC представляют свою функциональность через
статические члены.
198 Часть II. Главные конструкции программирования на С#
Определение статических методов
Предположим, что имеется новый проект консольного приложения по имени
StaticMethods, и в нем — класс по имени Teenager, определяющий статический метод
Complain(). Этот метод возвращает случайную строку, полученную вызовом
статической вспомогательной функции по имени GetRandomNumber ():
class Teenager
{
public static Random r = new Random();
public static int GetRandomNumber(short upperLimit)
{
return r.Next(upperLimit);
}
public static string Complain ()
{
stnng[] messages = {"Do I have to?", "He started it!",
"I 'm too tired...", "I hate school'",
"You are sooooooo wrong!"};
return messages[GetRandomNumberE)];
}
}
Обратите внимание, что переменная-член System.Random и вспомогательная
функция GetRandomNumber () также были объявлены статическими членами класса
Teenager, учитывая правило, что статические члены, такие как метод Complain(),
могут оперировать только другими статическими членами.
На заметку! Здесь следует повторить: статические члены могут оперировать только статическими
данными и вызывать статические методы определяющего их класса. Попытка использования
нестатических данных класса или вызова нестатического метода класса внутри реализации
статического члена приводит к ошибке времени компиляции.
Подобно любому статическому члену, для вызова Complain () нужен префикс — имя
определяющего его класса:
static void Main(string [ ] args)
{
Console.WriteLine("***** Fun with Class Types *****\n");
for (int l =0; l < 5; i++)
Console.WriteLine(Teenager.Complain());
Console.ReadLine();
}
Исходный код. Проект StaticMethods доступен в подкаталоге Chapter 5.
Определение статических полей данных
В дополнение к статическим методам, в классе (или структуре) также могут быть
определены статические поля, такие как переменная-член Random, представленная в
предыдущем классе Teenager. Знайте, что когда класс определяет нестатические
данные (правильно называемые данными экземпляра), то каждый объект этого типа
поддерживает независимую копию поля. Например, представим класс, который
моделирует депозитный счет, определенный в новом проекте консольного приложения по имени
StaticData:
Глава 5. Определение инкапсулированных типов классов 199
// Простой класс депозитного счета.
class SavingsAccount
{
public double currBalance;
public SavingsAccount(double balance)
{
currBalance = balance;
}
}
При создании объектов SavingsAccount память под поле currBalance выделяется
для каждого объекта. Статические данные, с другой стороны, распределяются однажды
и разделяются всеми объектами того же класса. Чтобы проиллюстрировать удобство
статических данных, добавьте в класс SavingsAccount статический элемент данных по
имени currlnterestRate, принимающий значение по умолчанию 0.04:
// Простой класс депозитного счета.
class SavingsAccount
{
public double currBalance;
// Статический элемент данных.
public static double currlnterestRate = 0.04;
public SavingsAccount(double balance)
{
currBalance = balance;
Если создать три экземпляра SavingsAccount, как показано ниже:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Static Data *****\n");
SavingsAccount si = new SavingsAccount E0);
SavingsAccount s2 = new SavingsAccountA00);
SavingsAccount s3 = new SavingsAccountA0000.75);
Console.ReadLine();
}
то размещение данных в памяти будет выглядеть примерно так, как показано на рис. 5.3.
SavingsAccount:SI
currBalance=50
SavingsAccount:S2
currBalance=100
SavingsAccount:S3
currBalance=10000.75
/
curr!nterestRate=.04
/
/
Рис. 5.З. Статические данные размещаются один раз и разделяются
между всеми экземплярами класса
Теперь давайте изменим класс SavingsAccount, добавив к нему два статических
метода для получения и установки значения процентной ставки:
200 Часть II. Главные конструкции программирования на С#
// Простой класс депозитного счета.
class SavingsAccount
{
public double currBalance;
// Статический элемент данных.
public static double currlnterestRate = 0.04;
public SavingsAccount(double balance)
{
currBalance = balance;
}
// Статические члены для установки/получения процентной ставки.
public static void SetlnterestRate(double newRate )
{ currlnterestRate = newRate; }
public static double GetlnterestRate ()
{ return currlnterestRate; }
}
Рассмотрим следующее применение класса:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Static Data *****\n");
SavingsAccount si = new SavingsAccount E0);
SavingsAccount s2 = new SavingsAccount A00);
// Вывести текущую процентную ставку.
Console.WriteLine("Interest Rate is: {0}", SavingsAccount.GetlnterestRate());
// Создать новый объект; это не 'сбросит' процентную ставку.
SavingsAccount s3 = new SavingsAccountA0000.75) ;
Console.WriteLine("Interest Rate is: {0}", SavingsAccount.GetlnterestRate()) ;
Console.ReadLine();
}
Вывод предыдущего метода Main() показан ниже:
••••• Fun W1th Static Data *****
In static ctor1
Interest Rate is: 0.04
Interest Rate is: 0.04
Как видите, при создании новых экземпляров класса SavingsAccount значение
статических данных не сбрасывается, поскольку CLR выделяет для них место в памяти
только один раз. После этого все объекты типа SavingsAccount оперируют одним и тем
же значением.
При проектировании любого класса С# одна из задач связана с выяснением того,
какие части данных должны быть определены как статические члены, а какие — нет.
Хотя на этот счет не существует строгих правил, помните, что поле статических данных
разделяется между всеми объектами данного класса. Поэтому, если необходимо, чтобы
часть данных совместно использовалась всеми объектами, статические члены будут
самым подходящим вариантом.
Предположим, что переменная currlnterestRate не определена с ключевым
словом static. Это означает, что каждый объект SavingAccount имеет собственную
копию currlnterestRate. Пусть создано 100 объектов SavingAccount, и
требуется изменить значение процентной ставки. Это потребует стократного вызова методв
SetlnterestRate ()! Ясно, что такой способ моделирования общих для объектов класса
данных нельзя считать удобным. Еще раз: статические данные идеальны, когда
имеется значение, которое должно быть общим для всех объектов данной категории.
Глава 5. Определение инкапсулированных типов классов 201
Определение статических конструкторов
Вспомните, что конструкторы служат для установки значений поля данных объекта
во время его создания. Таким образом, если вы хотите присвоить значение
статическому члену внутри конструктора уровня экземпляра, то удивитесь, обнаружив, что это
значение будет сбрасываться каждый раз, когда создается новый объект! Например,
предположим, что класс SavingsAccount изменен следующим образом:
class SavingsAccount
{
public double currBalance;
public static double currlnterestRate;
public SavingsAccount(double balance)
{
currlnterestRate = 0.04;
currBalance = balance;
}
}"
При выполнении предыдущего метода Main() обнаруживается, что
переменная currlnterestRate будет сбрасываться при каждом создании нового объекта
SavingsAccount, всегда возвращаясь к значению 0.04. Ясно, что установка значений
статических данных в нормальном конструкторе экземпляра сводит на нет весь их
смысл. Всякий раз, когда создается новый объект, данные уровня класса
сбрасываются! Один из способов правильной установки статического поля состоит в использовании
синтаксиса инициализации члена, как это делалось изначально:
class SavingsAccount
{
public double currBalance;
// Статические данные.
public static double currlnterestRate = 0.04;
}
Этот подход гарантирует, что статическое поле будет установлено только однажды,
независимо от того, сколько объектов будет создано. Однако что, если значение
статических данных нужно получить во время выполнения? Например, в типичном
банковском приложении значение переменной — процентной ставки должно быть прочитано
из базы данных или внешнего файла. Решение подобных задач требует контекста
метода (такого как конструктор), чтобы можно было выполнить операторы кода.
Именно по этой причине в С# предусмотрена возможность определения
статического конструктора. Взгляните на следующее изменение в коде:
class SavingsAccount
{
public double currBalance;
public static double currlnterestRate;
public SavingsAccount(double balance)
{
currBalance = balance;
}
// Статический конструктор.
static SavingsAccount ()
{
Console.WriteLine("In static ctor1");
currlnterestRate = 0.04;
}
202 Часть II. Главные конструкции программирования на С#
Упрощенно, статический конструктор — это специальный конструктор, который
является идеальным местом для инициализации значений статических данных, когда их
значение не известно на момент компиляции (например, когда его нужно прочитать из
внешнего файла или сгенерировать случайное число). Ниже приведено несколько
интересных моментов, касающихся статических конструкторов.
• В отдельном классе может быть определен только один статический конструктор.
Другими словами, статический конструктор нельзя перегружать.
• Статический конструктор не имеет модификатора доступа и не может принимать
параметров.
• Статический конструктор выполняется только один раз, независимо от того,
сколько объектов отдельного класса создается.
• Исполняющая система вызывает статический конструктор, когда создает
экземпляр класса или перед первым обращением к статическому члену этого класса.
• Статический конструктор выполняется перед любым конструктором уровня
экземпляра.
Учитывая сказанное, при создании новых объектов Savings Ac count значения
статических данных сохраняются, поскольку статический член устанавливается только один
раз внутри статического конструктора, независимо от количества созданных объектов.
Определение статических классов
Ключевое слово static возможно также применять прямо на уровне класса. Когда
класс определен как статический, его нельзя создать с использованием ключевого слова
new, и он может включать в себя только статические члены или поля. Если это правило
нарушить, возникнет ошибка компиляции.
На заметку! Классы или структуры, предоставляющие только статическую функциональность,
часто называют служебными (utility). При проектировании такого класса рекомендуется применять
ключевое слово static к определению класса.
На первый взгляд это может показаться довольно бесполезным средством, учитывая
невозможность создания экземпляров класса. Однако следует учесть, что класс, не
содержащий ничего кроме статических членов и/или константных данных, прежде всего,
не нуждается в выделении памяти. Рассмотрим следующий новый статический тип:
// Статические классы могут содержать только статические члены!
static class TimeUtilClass
{
public static void PrintTime ()
{ Console.WriteLine(DateTime.Now.ToShortTimeString()); }
public static void PrintDateO
{ Console.WriteLine (DateTime.Today.ToShortDateString()); }
}
Учитывая, что этот класс определен с ключевым словом static, создавать
экземпляры TimeUtilClass с помощью ключевого слова new нельзя. Напротив, вся
функциональность доступна на уровне класса:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Static Data *****\n");
// Это работает нормально.
TimeUtilClass.PrintDate();
TimeUtilClass.PrintTime();
Глава 5. Определение инкапсулированных типов классов 203
// Ошибка компиляции! Создавать экземпляр статического класса нельзя!
TimeUtilClass u = new TimeUtilClass () ;
}
До появлении статических классов в .NET 2.0 единственный способ предотвратить
создание экземпляров класса, предлагающего только статическую функциональность,
состоял либо в переопределении конструктора по умолчанию с модификатором private,
либо в объявлении класса как абстрактного с использованием ключевого слова abstract
(подробности об абстрактных типах ищите в главе 6). Рассмотрим следующие подходы:
class TimeUtilClass2
{
// Переопределение конструктора по умолчанию как
// приватного для предотвращения создания экземпляров.
private TimeUtilClass2 (){}
public static void PrintTime()
{ Console.WriteLine(DateTime.Now.ToShortTimeString ()); }
public static void PrintDateO
{ Console.WriteLine(DateTime.Today.ToShortDateString ()); }
}
// Определение типа как абстрактного для предотвращения создания экземпляров.
abstract class TimeUtilClass3
{
public static void PrintTime()
{ Console.WriteLine(DateTime.Now.ToShortTimeString()); }
public static void PrintDateO
{ Console.WriteLine(DateTime.Today.ToShortDateString()); }
}
Хотя эти конструкции допустимы и сейчас, использование статических классов
представляет собой более ясное и более безопасное в отношении типов решение,
учитывая тот факт, что предыдущие два приема допускают объявление нестатических
членов внутри класса без ошибок. В результате возникнет большая проблема! При наличии
класса, который больше не допускает создания экземпляров, в распоряжении окажется
фрагмент функциональности (т.е. все нестатические члены), который не может быть
использован. К этому моменту должно быть ясно, как определять простые классы,
включающие конструкторы, поля и различные статические (и нестатические) члены.
Обладая такими базовыми знаниями, можем приступать к ознакомлению с тремя
основными принципами объектно-ориентированного программирования.
Исходный код. Проект StaticData доступен в подкаталоге Chapter 5.
Основы объектно-ориентированного
программирования
Все основанные на объектах языки (С#, Java, C++, Smalltalk, Visual Basic и т.п.)
должны отвечать трем основным принципам объектно-ориентированного
программирования (ООП), которые перечислены ниже.
• Инкапсуляция. Как данный язык скрывает детали внутренней реализации
объектов и предохраняет целостность данных?
• Наследование. Как данный язык стимулирует многократное использование кода?
• Полиморфизм. Как данный язык позволяет трактовать связанные объекты
сходным образом?
204 Часть II. Главные конструкции программирования на С#
Прежде чем погрузиться в синтаксические детали реализации каждого принципа,
важно понять базовую роль каждого из них. Ниже предлагается обзор каждого
принципа, а в остальной части этой и последующих главах рассматриваются детали.
Роль инкапсуляции
Первый основной принцип ООП называется инкапсуляцией. Этот принцип
касается способности языка скрывать излишние детали реализации от пользователя объекта.
Например, предположим, что используется класс по имени DatabaseReader, который
имеет два главных метода: Open() HClose().
// Этот класс инкапсулирует детали открытия и закрытия базы данных.
DatabaseReader dbReader = new DatabaseReader ();
dbReader.Open(@"C:\AutoLot.mdf");
// Сделать что-то с файлом данных и закрыть файл.
dbReader.Close() ;
Фиктивный класс DatabaseReader инкапсулирует внутренние детали нахождения,
загрузки, манипуляций и закрытия файла данных. Программистам нравится
инкапсуляция, поскольку этот принцип ООП упрощает кодирование. Нет необходимости
беспокоиться о многочисленных строках кода, которые работают "за кулисами", чтобы
реализовать функционирование класса DatabaseReader. Все, что потребуется — это
создать экземпляр и отправлять ему соответствующие сообщения (например, "открыть
файл по имени AutoLot.mdf, расположенный на диске С:").
С идеей инкапсуляции программной логики тесно связана идея защиты данных.
В идеале данные состояния объекта должны быть специфицированы с
использованием ключевого слова private (или, возможно, protected). Таким образом, внешний мир
должен вежливо попросить, если захочет изменить или получить лежащее в основе
значение. Это хороший принцип, поскольку общедоступные элементы данных можно
легко повредить (даже нечаянно, а не преднамеренно). Чуть позже будет дано формальное
определение этого аспекта инкапсуляции.
Роль наследования
Следующий принцип ООП — наследование — касается способности языка позволять
строить новые определения классов на основе определений существующих классов. По
сути, наследование позволяет расширять поведение базового (или родительского)
класса, наследуя основную функциональность в производном подклассе (также именуемом
дочерним классом). На рис. 5.4 показан простой пример.
Прочесть диаграмму на рис. 5.4 можно так: "шестиугольник является фигурой,
которая является объектом". При наличии классов, связанных этой формой наследования,
между типами устанавливается отношение "является" ("is-a"). Такое отношение
называют классическим наследованием.
Здесь можно предположить, что Shape определяет некоторое количество членов,
общих для всех наследников (скажем, значение для представления цвета фигуры и другие
значения, задающие высоту и ширину). Учитывая, что класс Hexagon расширяет Shape,
он наследует основную функциональность, определенную классами Shape и Object, a
также определяет дополнительные собственные детали, касающиеся шестиугольников
(какими бы они ни были).
На заметку! На платформе .NET класс System.Object всегда находится на вершине любой
иерархии классов и определяет базовую функциональность, которая подробно описана в главе 6.
Глава 5. Определение инкапсулированных типов классов 205
Object
Class
т)
Рис. 5.4. Отношение "является" ("is-a")
Есть и другая форма повторного использования кода в мире ООП: модель
включения/делегации, также известная под названием отношение "имеет" ("has-a") или
агрегация. Эта форма повторного использования не применяется для установки отношений
"родительский-дочерний". Вместо этого такое отношение позволяет одному классу
определять переменную-член другого класса и опосредованно представлять его
функциональность (при необходимости) пользователю объекта.
Например, предположим, что снова моделируется автомобиль. Может понадобиться
выразить идею, что автомобиль "имеет" радиоприемник. Было бы нелогично пытаться
наследовать класс Саг от Radio или наоборот (ведь Саг не "является" Radio). Взамен
имеются два независимых класса, работающих совместно, причем класс Саг создает и
представляет функциональность Radio:
class Radio
{
public void Power(bool turnOn)
{
Console.WriteLine("Radio on: {0}
turnOn)/
}
class Car
{
// Car 'имеет' Radio.
private Radio myRadio = new Radio();
public void TurnOnRadio(bool onOff)
{
// Делегированный вызов внутреннего объекта.
myRadio.Power(onOff);
}
Обратите внимание, что пользователь объекта не имеет понятия, что класс Саг
использует внутренний объект Radio.
static void Main(string [ ] args)
{
// Вызов передается Radio внутренне.
Car viper = new Car () ;
viper.TurnOnRadio(false);
206 Часть II. Главные конструкции программирования на С#
Роль полиморфизма
Последний принцип ООП — полиморфизм. Он обозначает способность языка
трактовать связанные объекты в сходной манере. В частности, этот принцип ООП
позволяет базовому классу определять набор членов (формально называемый полиморфным
интерфейсом), которые доступны всем наследникам. Полиморфный интерфейс класса
конструируется с использованием любого количества виртуальных или абстрактных
членов (подробности ищите в главе 6).
По сути, виртуальный член — это член базового класса, определяющий реализацию
по умолчанию, которая может быть изменена (или, говоря более формально,
переопределена) в производном классе. В отличие от него, абстрактный метод — это член
базового класса, который не предусматривает реализации по умолчанию, а
предлагает только сигнатуру. Когда класс наследуется от базового класса, определяющего
абстрактный метод, этот метод обязательно должен быть переопределен в производном
классе. В любом случае, когда производные классы переопределяют члены,
определенные в базовом классе, они по существу переопределяют свою реакцию на один и тот же
запрос.
Чтобы увидеть полиморфизм в действии, давайте представим некоторые детали
иерархии фигур, показанной на рис. 5.4. Предположим, что в классе Shape определен
виртуальный метод Draw(), не принимающий параметров. Учитывая тот факт, что
каждая фигура должна визуализировать себя уникальным образом, подклассы (такие как
Hexagon и Circle) вольны переопределить этот метод по своему усмотрению (рис. 5.5).
Object Г*.
Class
Вызов Draw () на объекте
Circle приводит к рисованию
двумерного круга.
Вызов Draw () на объекте
Hexagon приводит к рисованию
двумерного шестиугольника.
Рис. 5.5. Классический полиморфизм
Когда полиморфный интерфейс спроектирован, можно сделать ряд предположений в
коде. Например, учитывая, что классы Hexagon и Circle унаследованы от общего
родителя (Shape), массив типов Shape может содержать всех наследников базового класса.
Более того, учитывая, что Shape определяет полиморфный интерфейс для всех
производных типов (в данном примере — метод Draw()), можно предположить, что каждый
член массива обладает этой функциональностью.
Рассмотрим следующий метод Main(), который заставляет массив
типов-наследников Shape визуализировать себя с использованием метода Draw():
class Program
{
static void Main(string[] args)
{
Shape[] myShapes = new Shape[3];
myShapes[0] = new Hexagon ();
myShapes[1] = new Circle ()/
myShapes[2] = new Hexagon ();
Shape
Class
a Methods
♦ Draw
' 'Щ
i Circle
Class
■J- «•Shape
3
-—| Hexagon
I Ciass
Глава 5. Определение инкапсулированных типов классов 207
foreach (Shape s in myShapes)
{
// Используется полиморфный интерфейс!
s.Draw ();
}
Console.ReadLine ();
}
На этом краткое знакомство с основными принципами ООП завершено. Оставшаяся
часть главы посвящена дальнейшим подробностям инкапсуляции в С#. Детали
наследования и полиморфизма рассматриваются в главе 6.
Модификаторы доступа С#
При работе с инкапсуляцией всегда следует принимать во внимание то, какие
аспекты типа видимы различным частям приложения. В частности, типы (классы,
интерфейсы, структуры, перечисления и делегаты), а также их члены (свойства, методы,
конструкторы и поля) определяются с использованием определенного ключевого слова,
управляющего "видимостью" элемента другим частям приложения. Хотя в С#
определены многочисленные ключевые слова для управления доступом, их значение может
отличаться в зависимости от места применения (к типу или члену). В табл. 5.1 описаны
роли и применение модификаторов доступа.
Таблица 5.1. Модификаторы доступа С#
ф р К чему может быть применен
доступа
Назначение
public
Типы или члены типов
private Члены типов или вложенные типы
protected Члены типов или вложенные типы
internal Типы или члены типов
protected Члены типов или вложенные типы
internal
Общедоступные (public)
элементы не имеют ограничений доступа.
Общедоступный член может быть доступен
как из объекта, так и из любого
производного класса. Общедоступный тип может
быть доступен из других внешних сборок
Приватные (private) элементы могут
быть доступны только в классе (или
структуре), в котором они определены
Защищенные (protected) элементы могут
использоваться классом, который определил
их, и любым дочерним классом. Однако
защищенные элементы не доступны внешнему
миру через операцию точки (.)
Внутренние (internal) элементы
доступны только в пределах текущей сборки.
Таким образом, если в библиотеке
классов .NET определен набор внутренних
типов, то другие сборки не смогут ими
пользоваться
Когда ключевые слова protected и
internal комбинируются в объявлении
элемента, такой элемент доступен внутри
определяющей его сборки,
определяющего класса и всех его наследников
208 Часть II. Главные конструкции программирования на С#
В этой главе рассматриваются только ключевые слова public и private. В
последующих главах будет рассказываться о роли модификаторов internal и protected
internal (удобных при построении библиотек кода .NET) и модификатора protected
(удобного при создании иерархий классов).
Модификаторы доступа по умолчанию
По умолчанию члены типов являются неявно приватными (private) и неявно
внутренними (internal). Таким образом, следующее определение класса автоматически
установлено как internal, в то время как конструктор по умолчанию этого типа
автоматически является private:
// Внутренний класс с приватным конструктором по умолчанию.
class Radio
{
Radio () { }
}
Чтобы позволить другим частям программы обращаться к членам объекта, эти
члены потребуется пометить как общедоступные (public). К тому же, если необходимо
открыть Radio внешним сборкам (опять-таки, это удобно при построении библиотек кода
.NET; см. главу 14), следует добавить к нему модификатор public.
// Общедоступный класс с приватным конструктором по умолчанию.
public class Radio
{
Radio () { }
}
Модификаторы доступа и вложенные типы
Как было показано в табл. 5.1, модификаторы доступа private, protected и
protected internal могут применяться к вложенному типу. Вложение типов детально
рассматривается в главе 6. Пока же достаточно знать, что вложенный (nested) тип — это
тип, объявленный непосредственно внутри объявления класса или структуры. Для
примера ниже приведено приватное перечисление (по имени Color), вложенное в
общедоступный класс (по имени
SportsCarepublic class SportsCar
{
// Нормально! Вложенные типы могут быть помечены как private,
private enum CarColor
{
Red, Green, Blue
}
}
Здесь допускается применять модификатор доступа private к вложенному типу.
Однако не вложенные типы (вроде SportsCar) могут определяться только с
модификаторами public или internal. Поэтому следующее определение класса неверно:
// Ошибка1 Не вложенный тип не может быть помечен как private1
private class SportsCar
{}
После ознакомления с модификаторами доступа можно приступать к формальным
исследованиям первого принципа ООП.
Глава 5. Определение инкапсулированных типов классов 209
Первый принцип: службы инкапсуляции С#
Концепция инкапсуляции вращается вокруг принципа, гласящего, что внутренние
данные объекта.не должны быть напрямую доступны через экземпляр объекта. Вместо
этого, если вызывающий код желает изменить состояние объекта, то должен делать это
через методы доступа (accessor, или метод get) и изменения (mutator, или метод set). В С#
инкапсуляция обеспечивается на синтаксическом уровне с использованием ключевых
слов public, private, internal и protected. Чтобы проиллюстрировать необходимость
в службах инкапсуляции, предположим, что создано следующее определение класса:
// Класс с единственным общедоступным полем.
class Book
{
public int numberOfPages;
}
Проблема с общедоступными данными состоит в том, что сами по себе эти данные
не имеют возможности "понять", является ли присваиваемое значение допустимым в
рамках существующих бизнес-правил системы. Как известно, верхний предел
значений для типа int в С# довольно велик B 147 483 647), поэтому компилятор разрешит
следующее присваивание:
//Хм Ничего себе — мини-новелла!
static void Main(string[] args)
{
Book miniNovel = new Book();
miniNovel.numberOfPages = 30000000;
}
Хотя границы типа данных int не превышены, ясно, что мини-новелла на 30
миллионов страниц выглядит несколько неправдоподобно. Как видите, общедоступные
поля не дают возможности перехватывать ошибки, связанные с преодолением верхних
(или нижних) логических границ. Если в текущей системе установлено
бизнес-правило, гласящее, что книга должна иметь от 1 до 1000 страниц, его придется обеспечить
программно. По этой причине общедоступным полям обычно нет места в определении
класса производственного уровня.
На заметку! Говоря точнее, члены класса, представляющие состояние объекта, не должны
помечаться как public. В то же время, как будет показано далее в главе, вполне допускается иметь
общедоступные константы и поля только для чтения.
Инкапсуляция предоставляет способ предохранения целостности данных о
состоянии объекта. Вместо определения общедоступных полей (которые легко приводят к
повреждению данных), необходимо выработать привычку определения приватных
данных, управление которыми осуществляется опосредованно, с применением одной из
двух техник:
• определение пары методов доступа и изменения;
• определение свойства .NET.
Какая бы техника не была выбрана, идея состоит в том, что хорошо
инкапсулированный класс должен защищать свои данные и скрывать подробности своего
устройства от любопытных глаз из внешнего мира. Это часто называют программированием
черного ящика. Преимущество такого подхода состоит в том, что объект может
свободно изменять внутреннюю реализацию любого метода. За счет обеспечения
неизменности сигнатуры метода, работа существующего кода, который использует этот метод, не
нарушается.
210 Часть II. Главные конструкции программирования на С#
Инкапсуляция с использованием традиционных
методов доступа и изменения
В оставшейся части этой главы будет построен довольно полный класс,
моделирующий обычного сотрудника. Для начала создадим новое консольное приложение по
имени EmployeeApp и добавим в него новый файл класса (под названием Employee.cs),
используя пункт меню Projects Add class (Проекте Добавить класс). Дополним класс
Employee следующими полями, методами и конструкторами:
class Employee
{
// Поля данных.
private string empName;
private int empID;
private float currPay;
// Конструкторы.
public Employee () { }.
public Employee(string name, int id, float pay)
{
empName = name;
empID = id;
currPay = pay;
}
// Методы.
public void GiveBonus(float amount)
{
currPay += amount;
}
public void DisplayStats ()
{
Console.WriteLine("Name: {0}", empName);
Console.WriteLine ("ID: {0}", empID);
Console.WriteLine("Pay: {0}", currPay); (
}
}
Обратите внимание, что поля класса Employee определены с ключевым словом
private. С учетом этого, поля empName, empID и currPay напрямую через объектную
переменную не доступны:
static void Main(string[] args)
{
// Ошибка! Невозможно напрямую обращаться к приватным полям объекта!
Employee emp = new Employee ();
emp.empName = "Marv";
}
Если необходимо, чтобы внешний мир мог взаимодействовать с полным именем
сотрудника, по традиции понадобится определить методы доступа (метод get) и изменения
(метод set). Роль метода get состоит в возврате вызывающему коду значения лежащих в
основе статических данных. Метод set позволяет вызывающему коду изменять текущее
значение лежащих в основе статических данных при условии соблюдения бизнес-правил.
Для целей иллюстрации инкапсулируем поле empName. Для этого к существующему
классу Employee следует добавить показанные ниже общедоступные члены. Обратите
внимание, что метод SetNameO выполняет проверку входящих данных, чтобы
удостовериться, что строка имеет длину не более 15 символов. Если это не так, на консоль
выводится сообщение об ошибке и происходит возврат без изменения значения поля
empName.
Глава 5. Определение инкапсулированных типов классов 211
На заметку! В классе производственного уровня в логике конструктора следовало бы
предусмотреть проверку длины строки с именем сотрудника. Пока опустим эту деталь, но улучшим код
позже, при рассмотрении синтаксиса свойств .NET.
class Employee
{
// Поля данных.
private string empName;
// Метод доступа (метод get) .
public string GetName()
{
return empName;
}
// Метод изменения (метод set) .
public void SetName(string name)
{
// Перед присваиванием проверить входное значение,
if (name.Length > 15)
Console. WriteLine ("Error' Name must be less than 16 characters!11);
else
empName = name;
}
}
Эта техника требует наличия двух уникально именованных методов для управления
единственным элементом данных. Для иллюстрации модифицируем метод Main()
следующим образом:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Encapsulation *****\n");
Employee emp = new Employee("Marvin", 456, 30000);
emp.GiveBonusA000);
emp.DisplayStats();
// Использовать методы get/set для взаимодействия с именем объекта.
emp.SetName("Marv");
Console.WriteLine("Employee is named: {0}", emp.GetName ());
Console.ReadLine();
}
Благодаря коду в методе SetName (), попытка присвоить строку длиннее 15 символов
приводит к выводу на консоль жестко закодированного сообщения об ошибке:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Encapsulation *****\n");
// Длиннее 15 символов! На консоль выводится сообщение об ошибке.
Employee emp2 = new Employee ();
emp2.SetName("Xena the warrior princess");
Console.ReadLine();
}
Пока все хорошо. Приватное поле empName инкапсулировано с использованием
двух методов GetName() и SetName(). Для дальнейшей инкапсуляции данных в
классе Employee понадобится добавить ряд дополнительных методов (например, GetID(),
SetID(), GetCurrentPayO, SetCurrentPayO). Каждый метод, изменяющий данные,
может иметь в себе несколько строк кода для проверки дополнительных бизнес-правил.
212 Часть II. Главные конструкции программирования на С#
Хотя можно поступить именно так, для инкапсуляции данных класса в С# предлагается
удобная альтернативная нотация.
Инкапсуляция с использованием свойств .NET
Вдобавок к возможности инкапсуляции полей данных с использованием
традиционной пары методов get/set, в языках .NET имеется более предпочтительный способ
инкапсуляции данных с помощью свойств. Прежде всего, имейте в виду, что свойства —
это всего лишь упрощенное представление "реальных" методов доступа и изменения.
Это значит, что разработчик класса по-прежнему может реализовать любую
внутреннюю логику, которую нужно выполнить перед присваиванием значения (например,
преобразовать в верхний регистр, очистить от недопустимых символов, проверить границы
числовых значений и т.д.).
Ниже приведен измененный класс Employee, который теперь обеспечивает
инкапсуляцию каждого поля с применением синтаксиса свойств вместо традиционных методов
get/set.
class Employee
{
// Поля данных.
private string empName;
private int empID;
private float currPay;
// Свойства.
public string Name
{
get { return empName; }
set
{
if (value.Length > 15)
Console.WriteLine ("Error! Name must be less than 16 characters!11);
else
empName = value;
}
}
// Можно было бы добавить дополнительные бизнес-правила для установки
// этих свойств, но в данном примере в этом нет необходимости.
public int ID
{
get { return empID; }
set { empID = value; }
}
public float Pay
{
get { return currPay; }
set { currPay = value; }
}
- }
Свойство С# состоит из определений контекстов чтения get (метод доступа) и set
(метод изменения), вложенных непосредственно в контекст самого свойства. Обратите
внимание, что свойство указывает тип данных, которые оно инкапсулирует, как тип
возвращаемого значения. Кроме того, в отличие от метода, в определении свойства не
используются скобки (даже пустые). Обратите внимание на комментарий к текущему
свойству ID:
Глава 5. Определение инкапсулированных типов классов 213
// int представляет тип инкапсулируемых свойством данных.
// Тип данных должен быть идентичен связанному полю (empID).
public int ID // Обратите внимание на отсутствие скобок.
{
get { return empID; }
set { empID = value; }
}
В контексте set свойства используется лексема value, которая представляет
входное значение, присваиваемое свойству вызывающим кодом. Эта лексема не является
настоящим ключевым словом С#, а представляет собой то, что называется
контекстуальным ключевым словом. Когда лексема value находится внутри контекстаcset, она
всегда обозначает значение, присваиваемое вызывающим кодом, и всегда имеет тип,
совпадающий с типом самого свойства. Поэтому свойство Name может проверить
допустимую длину string следующим образом:
public string Name
{
get { return empName; }
set
{
// Здесь value имеет тип string,
if (value.Length > 15)
Console.WriteLine("Error! Name must be less than 16 characters1");
else
empName = value;
}
}
При наличии этих свойств вызывающему коду кажется, что он имеет дело с
общедоступным элементом данных; однако "за кулисами" при каждом обращении
вызывается корректный get или set, обеспечивая инкапсуляцию:
static void Main(string [ ] args)
{
Console.WriteLine("***** Fun with Encapsulation *****\n");
Employee emp = new Employee("Marvin", 456, 30000);
emp.GiveBonusA000);
emp.DisplayStats();
// Установка и получение свойства Name.
emp.Name = "Marv";
Console.WriteLine("Employee is named: {0}", emp.Name);
Console.ReadLine();
}
Свойства (в противоположность методам доступа и изменения) также облегчают
манипулирование типами, поскольку способны реагировать на внутренние операции С#.
Для иллюстрации представим, что тип класса Employee имеет внутреннюю приватную
переменную-член, хранящую возраст сотрудника. Ниже показаны необходимые
изменения (обратите внимание на использование цепочки вызовов конструкторов):
class Employee
{
// Новое поле и свойство.
private int empAge;
public int Age
{
get { return empAge; }
set { empAge = value; }
}
214 Часть II. Главные конструкции программирования на С#
// Обновленные конструкторы.
public Employee () {}
public Employee(string name, int id, float pay)
:this(name, 0, id, pay){}
public Employee(string name, int age, int id, float pay)
{
empName = name;
empID = id;
empAge = age;
currPay = pay;
}
// Обновленный метод DisplayStats() теперь учитывает возраст.
public void DisplayStats()
{
Console.WriteLine("Name: {0}", empName);
Console.WriteLine ("ID: {0}", empID);
Console.WriteLine ("Age : {0}", empAge);
Console.WriteLine("Pay: {0}", currPay);
}
}
Теперь предположим, что создан объект Employee по имени joe. Необходимо, чтобы
в день рождения сотрудника возраст увеличивался на 1 год. Используя традиционные
методы set /get, пришлось бы написать код вроде следующего:
Employee joe = new Employee() ;
joe.SetAge(joe.GetAge() + 1);
Однако если empAge инкапсулируется через свойство по имени Age, можно записать
проще:
Employee joe = new Employee() ;
joe.Age++;
Использование свойств внутри определения класса
Свойства, а в особенности их часть set — это общепринятое место для размещения
бизнес-правил класса. В настоящее время класс Employee имеет свойство Name,
которое гарантирует длину имени не более 15 символов. Остальные свойства (ID, Pay и Age)
также могут быть обновлены с добавлением соответствующей логики.
Хотя все это хорошо, но следует принимать во внимание также и то, что обычно
происходит внутри конструктора класса. Конструктор получает входные параметры,
проверяет корректность данных и затем выполняет присваивания внутренним
приватным полям. В настоящее время ведущий конструктор не проверяет входные строковые
данные на допустимый диапазон, поэтому можно было бы изменить его следующим
образом:
public Employee (string name, int age, int id, float pay)
{
// Это может оказаться проблемой...
if (name.Length > 15)
Console.WriteLine ("Error' Name must be less than 16 characters1");
else
empName = name;
empID = id;
empAge = age;
currPay = pay;
}
Глава 5. Определение инкапсулированных типов классов 215
Наверняка вы заметили проблему, связанную с этим подходом. Свойство Name и
ведущий конструктор предпринимают одну и ту же проверку ошибок! В результате
получается дублирование кода. Чтобы упростить код и разместить всю проверку
ошибок в центральном месте, для установки и получения данных внутри класса разумно
всегда использовать свойства. Ниже показан соответствующим образом обновленный
конструктор:
public Employee (string name, int age, int id, float pay)
{
// Уже лучше! Используйте свойства для установки данных класса.
// Это сократит количество дублированных проверок ошибок.
Name = name;
Age = age;
ID = id;
Pay = pay;
}
Помимо обновления конструкторов с целью использования свойств для
присваивания значений, имеет смысл сделать это повсюду в реализации класса, чтобы
гарантировать неукоснительное соблюдение бизнес-правил. Во многих случаях единственное
место, где можно напрямую обращаться к приватным данным — это внутри самого
свойства. С учетом сказанного модифицируем класс Employee, как показано ниже:
class Employee
{
// Поля данных.
private string empName;
private int empID;
private float currPay;
private int empAge;
// Конструкторы.
public Employee () { }
public Employee(string name, int id, float pay)
:this(name, 0, id, pay) { }
public Employee(string name, int age, int id, float pay)
{
Name = name;
Age = age;
ID = id;
Pay = pay;
}
// Методы.
public void GiveBonus(float amount)
{ Pay += amount; }
public void DisplayStats ()
{
Console.WriteLine("Name: {0}", Name);
Console.WriteLine("ID: {0}", ID);
Console.WriteLine("Age: {0}", Age);
Console.WriteLine("Pay: {0}", Pay);
}
// Свойства остаются прежними...
Внутреннее представление свойств
Многие программисты склонны именовать традиционные методы доступа и
изменения с применением префиксов get _ и set _ (например, get _ Name() и set _ NameO).
216 Часть II. Главные конструкции программирования на С#
В языке С# такое соглашение об именовании само по себе не проблематично. Однако
важно понимать, что "за кулисами" свойства представлены в коде CIL с использованием
тех же самых префиксов.
Например, открыв сборку EmployeeApp.exe в утилите ildasm.exe, можно увидеть,
что каждое свойство отображается на скрытые методы get_XXX () и set_XXX (),
вызываемые внутри CLR (рис. 5.6).
Р Н.\Му Books\C* BoottXC» and the .NET Platfor..
fFite" View Help
its
H:\My Books\C# Book\C# and the .NET Platform 5th ed\First С
► MANIFEST
Щ EmployeeApp
В £ EmployeeApp. Employ ее
► .class private auto ansi beforefieldinit
V currPay : private float32
V empAge : private ht32
s/ empID : private int32
v empName : private string
■ .ctor: void(string,int32,float32)
■ .ctor: void(string,int32,int32Jfloat32)
■ .ctor : void()
■ DisplayStats : void()
■ GiveBonus: void(float32)
■ get_ID : mt32()
■ get.Name: strmg()
■ get_Pay : float32()
■ set_Age : void(int32)
■ setJD : void(int32)
Ш set_Name : void(string)
■ set_Pay : void(float32)
к Aae : ins.tancejnt_3.20
assembly EmployeeApp
Рис. 5.6. Свойство внутреннее представлено методами get/set
Предположим, что тип Employee теперь имеет приватную переменную-член по
имени empSSN для представления номера карточки социального страхования (SSN) лица;
этой переменной будет манипулировать свойство по имени SocialSecurityNumber
(также предположим, что специальный конструктор и метод DisplayStats () обновлены
с учетом нового элемента данных).
// Добавим поддержку нового поля, представляющего SSN сотрудника.
class Employee
{
private string empSSN;
public string SocialSecurityNumber
{
get { return empSSN; }
set { empSSN = value; }
}
// Конструкторы.
public Employee() {}
public Employee(string name, int id, float pay)
:this(name, 0, id, pay, ""){}
public Employee (string name, int age, int id, float pay, string ss-n)
Name = name;
Age = age;
ID = id;
Pay = pay;
Глава 5. Определение инкапсулированных типов классов 217
SocialSecurityNumber = ssn;
}
public void DisplayStats ()
{
Console.WriteLine ("Name: {0}", Name);
Console.WriteLine("ID: {0}", ID);
Console.WriteLine ("Age: {0}", Age);
Console.WriteLine("Pay: {0}", Pay);
Console.WriteLine("SSN: {0}", SocialSecurityNumber);
}
}
Если в этом классе также определить два метода с именами getSocialSecurity
Number () и set_SocialSecurityNumber(), возникнет ошибка времени компиляции:
// Помните, что свойство в действительности отображается на пару get_/set_!
class Employee
{
public string get_SocialSecurityNumber()
{
return empSSN;
}
public void set_SocialSecurityNumber(string ssn)
{
empSSN = ssn;
}
}
На заметку! В библиотеках базовых классов .NET всегда отдается предпочтение использованию
для инкапсуляции свойств, а не традиционных методов доступа и изменения. Поэтому при
построении специальных классов, которые интегрируются с платформой .NET, избегайте
объявления традиционных методов get и set.
Управление уровнями видимости операторов get/set свойств
Если не указать иного, видимость логики get и set управляется исключительно
модификаторами доступа из объявления свойства:
// Учитывая объявление свойства, логика get и set является общедоступной.
public string SocialSecurityNumber
{
get { return .empSSN; }
set { empSSN = value; }
}
В некоторых случаях было бы удобно задавать уникальные уровни доступа для
логики get и set. Для этого просто добавьте префикс — ключевое слово доступа к
соответствующим ключевым словам get или set (контекст без префикса получает видимость
объявления свойства):
// Пользователи объекта могут только получать значение, однако класс
// Employee и производные типы могут также устанавливать значение.
public string SocialSecurityNumber
{
get { return empSSN; }
protected set { empSSN = value; }
218 Часть II. Главные конструкции программирования на С#
В этом случае логика set свойства SocialSecurityNumber может быть вызвана
только текущим классом и его производными классами, а потому не может быть
вызвана из экземпляра объекта. Ключевое слово protected будет детально описываться
в следующей главе, при рассмотрении наследования и полиморфизма.
Свойства, доступные только для чтения и только для записи
При инкапсуляции данных может понадобиться сконфигурировать свойство,
доступное только для чтения. Для этого нужно просто опустить блок set. Аналогично,
если требуется создать свойство, доступное только для записи, следует опустить блок
get. Например (хоть это и не требуется в рассматриваемом примере), вот как сделать
свойство SocialSecurityNumber доступным только для чтения:
public string SocialSecurityNumber
{
get { return empSSN; }
}
После этого единственным способом модификации номера карточки социального
страхования будет передача его в аргументе конструктора. Теперь попытка установить
новое значение SSN для сотрудника внутри ведущего конструктора приведет к ошибке
компиляции:
public Employee(string name, int age, int id, float pay, string ssn)
{
Name = name;
Age = age;
ID = id;
Pay = pay;
// Теперь это невозможно, поскольку свойство
// предназначено только для чтения!
SocialSecurityNumber = ssn;
}
Если сделать это свойство доступным только для чтения, в логике конструктора
останется лишь пользоваться лежащей в основе переменной-членом ssn.
Статические свойства
В С# также поддерживаются статические свойства. Вспомните из начала этой главы,
что статические члены доступны на уровне класса, а не на уровне экземпляра (объекта)
этого класса. Например, предположим, что в классе Employee определен статический
элемент данных для представления названия организации, нанимающей сотрудников.
Инкапсулировать статическое свойство можно следующим образом:
// Статические свойства должны оперировать статическими данными!
class Employee
{
private static string companyName;
public static string Company
{
get { return companyName; }
set { companyName = value; }
}
}
Глава 5. Определение инкапсулированных типов классов 219
Манипулировать статическими свойствами можно точно так же, как статическими
методами:
// Взаимодействие со статическим свойством.
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Encapsulation *****\n");
// Установить компанию.
Employee.Company = "My Company";
Console.WriteLine("These folks work at {0}.", Employee.Company);
Employee emp = new Employee("Marvin", 24, 456, 30000, 11-11-1111");
emp.GiveBonusA000);
emp.DisplayStats() ;
Console.ReadLine();
}
И, наконец, вспомните, что классы могут поддерживать статические конструкторы.
Поэтому если нужно гарантировать, что имя в статическом поле companyName всегда
будет установлено в My Company, понадобится написать следующий код:
// Статические конструкторы используются для инициализации статических данных.
public class Employee
{
private Static companyName As string
static Employee ()
{
companyName = "My Company";
}
}
Используя такой подход, нет необходимости явно вызывать свойство Company для
того, чтобы установить начальное значение:
// Автоматическая установка значения "My Company" через статический конструктор.
static void Main(string [ ] args)
{
Console.WriteLine("These folks work at {0}", Employee.Company);
}
В завершение исследований инкапсуляции с использованием свойств С# следует
запомнить, что эти синтаксические сущности служат для тех же целей, что и
традиционные методы get/set. Преимущество свойств состоит в том, что пользователи
объектов могут манипулировать внутренними данными, применяя единый именованный
элемент
Исходный код. Проект EmployeeApp доступен в подкаталоге Chapter 5.
Понятие автоматических свойств
С выходом платформы .NET 3.5 в языке С# появился еще один способ определения
простых служб инкапсуляции с минимальным кодом, а именно — синтаксис
автоматических свойств. Для целей иллюстрации создадим новый проект консольного
приложения С# по имени AutoProps. Добавим в него новый файл класса С# (Car.cs), в котором
определен показанный ниже класс, инкапсулирующий единственную порцию данных с
использованием классического синтаксиса свойств.
220 Часть II. Главные конструкции программирования на С#
// Тип Саг, использующий стандартный синтаксис свойств.
class Car
{
private string carName = string.Empty;
public string PetName
{
get { return carName; }
set { carName = value; }
}
}
Хотя большинство свойств С# содержат в своем контексте бизнес-правила, не так
уж редко бывает, что некоторые свойства не делают буквально ничего, помимо простого
присваивания и возврата значений, как в приведенном выше коде. В таких случаях было
бы слишком громоздко многократно определять приватные поля и некоторые простые
определения свойств. Например, при построении класса, которому нужно 15 приватных
элементов данных, в конечном итоге получаются 15 связанных с ними свойств,
которые, по сути, представляют собой не более чем тонкие оболочки для инкапсуляции.
Чтобы упростить процесс простой инкапсуляции данных полей, можно применять
синтаксис автоматических свойств. Как следует из названия, это средство
перекладывает работу по определению лежащего в основе приватного поля и связанного свойства
С# на компилятор, используя небольшое усовершенствование синтаксиса. Рассмотрим
переделанный класс Саг, в котором этот синтаксис применяется для быстрого создания
трех свойств:
class Car
{
// Автоматические свойства'
public string PetName { get; set; }
public int Speed { get; set; }
public string Color { get; set; }
}
При определении автоматических свойств указывается модификатор доступа,
лежащий в основе тип данных, имя свойства и пустые контексты get/set. Во время
компиляции тип будет оснащен автоматически сгенерированным полем и соответствующей
реализацией логики set/get.
На заметку! Имя автоматически сгенерированного лежащего в основе приватного поля в коде С#
не доступно. Единственный способ увидеть его — воспользоваться таким инструментом, как
ildasm.exe.
Однако в отличие от традиционных свойств С#, создавать автоматические свойства,
предназначенные только для чтения или только для записи, нельзя. Хотя может
показаться, что для этого достаточно опустить get или set в объявлении свойства, как
показано ниже:
// Свойство только для чтения? Ошибка!
public int MyReadOnlyProp { get; }
// Свойство только для записи? Ошибка!
public int MyWriteOnlyProp { set; }
Но на самом деле это приведет к ошибке компиляции. Определяемое автоматическое
свойство должно поддерживать функциональность и чтения, и записи. Тем не менее,
можно реализовать автоматическое свойство с более ограничивающими контекстами
get или set:
Глава 5. Определение инкапсулированных типов классов 221
// Контекст get общедоступный, a set — защищенный.
// Контекст get/set можно также объявить приватным,
public int SomeOtherProperty { get; protected set; }
Взаимодействие с автоматическими свойствами
Поскольку компилятор будет определять лежащие в основе приватные поля во время
компиляции, класс с автоматическими свойствами всегда должен использовать
синтаксис свойств для установки и чтения лежащих в основе значений. Это важно отметить,
потому что многие программисты напрямую используют приватные поля внутри
определения класса, что в данном случае невозможно. Например, если бы класс Саг
включал метод DisplayStatsO, он должен был бы реализовать этот метод, используя имя
свойства:
class Car
{
// Автоматические свойства!
public string PetName { get; set; }
public int Speed { get; set; }
public string Color { get; set; }
public void DisplayStatsO
{
Console.WriteLine("Car Name: {0}", PetName);
Console.WriteLine("Speed: {0}", Speed);
Console.WriteLine("Color: {0}", Color);
}
}
При использовании объекта, определенного с автоматическими свойствами, можно
присваивать и получать значения, используя ожидаемый синтаксис свойств:
static void Main(string[ ] args)
{
Console.WriteLine("***** Fun with Automatic Properties *****\n");
Car с = new Car () ;
c.PetName = "Frank";
c.Speed = 55;
с Color = "Red";
Console.WriteLine("Your car is named {0}? That's odd...",
c.PetName);
c.DisplayStats();
Console.ReadLine();
}
Замечания относительно автоматических
свойств и значений по умолчанию
При использовании автоматических свойств для инкапсуляции числовых и
булевских данных можно сразу применять автоматически сгенерированные свойства типа
прямо в своей кодовой базе, поскольку их скрытым полям будут присвоены безопасные
значения по умолчанию, которые могут быть использованы непосредственно. Однако
будьте осторожны, если синтаксис автоматического свойства применяется для
упаковки переменной другого класса, потому что скрытое поле ссылочного типа также будет
установлено в значение по умолчанию, т.е. null.
Рассмотрим следующий новый класс по имени Garage, в котором используются два
автоматических свойства:
222 Насть II. Главные конструкции программирования на С#
class Garage
{
// Скрытое поле int установлено в О!
public int NumberOfCars { get; set; }
// Скрытое поле Car установлено в null!
public Car MyAuto { get; set; }
}
Имея установленные С# значения по умолчанию для полей данных, значение
NumberOfCars можно вывести в том виде, как оно есть (поскольку ему автоматически
присвоено значение О). Однако если напрямую обратиться к MyAuto, то во время
выполнения сгенерируется исключение ссылки на null, потому что лежащей в основе
переменной-члену типа Саг не был присвоен новый объект:
static void Main(string[] args)
{
Garage g = new Garage () ;
// Нормально, печатается значение по умолчанию, равное 0.
Console.WriteLine("Number of Cars: {0}", g.NumberOfCars);
// Ошибка времени выполнения! Лежащее в основе поле в данный момент равно null!
Console.WriteLine(g.MyAuto.PetName);
Console.ReadLine();
}
Учитывая, что лежащие в основе приватные поля создаются во время компиляции,
в синтаксисе инициализации полей С# нельзя непосредственно размещать экземпляр
ссылочного типа с помощью new. Это должно делаться конструкторами класса, что
обеспечит создание объекта безопасным образом. Например:
class Garage
{
// Скрытое поле установлено в 0!
public int NumberOfCars { get; set; }
// Скрытое поле установлено в null!
public Car MyAuto { get; set; }
// Для переопределения значений по умолчанию, присвоенных
// скрытым полям, должны использоваться конструкторы,
public Garage()
{
MyAuto = new Car ();
NumberOfCars = 1;
}
public Garage (Car car, int number)
{
MyAuto = car;
NumberOfCars = number;
}
}
После этой модификации объект Car можно поместить в объект Garage, как
показано ниже:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Automatic Properties *****\n");
// Создать автомобиль.
Car с = new Car () ;
Глава-5. Определение инкапсулированных типов классов 223
c.PetName = "Frank";
с.Speed =55;
с.Color = "Red";
c.DisplayStats();
// Поместить автомобиль в гараж.
Garage g = new Garage () ;
g.MyAuto = с;
// Вывод количества автомобилей в гараже.
Console.WriteLine("Number of Cars in garage: {0}", g.NumberOfCars);
// Вывод названия автомобиля.
Console.WriteLine("Your car is named: {0}", g.MyAuto.PetName);
Console.ReadLine();
}
Нельзя не согласиться с тем, что это очень полезное свойство языка
программирования С#, поскольку свойства для класса можно определить с использованием
простого синтаксиса. Естественно, если свойство помимо получения и установки лежащего
в основе приватного поля требует дополнительного кода (такого как логика проверки
достоверности, запись в журнал событий, взаимодействие с базой данных), придется
определить его как "нормальное" свойство .NET вручную. Автоматические свойства С#
никогда не делают ничего кроме предоставления простой инкапсуляции для лежащих в
основе приватных данных (сгенерированных компилятором).
Исходный код. Проект AutoProps доступен в подкаталоге Chapter 5,
Понятие синтаксиса инициализации объектов
Как можно было видеть на протяжении этой главы, при создании нового
объекта конструктор позволяет указывать начальные значения. Также было показано, что
свойства позволяют получать и устанавливать лежащие в основе данные в безопасной
манере. При работе с классами, которые написаны другими, включая классы из
библиотеки базовых классов .NET, нередко можно заметить, что в них есть более одного
конструктора, позволяющего устанавливать каждую порцию данных внутреннего
состояния. Учитывая это, программист обычно старается выбрать наиболее подходящий
конструктор, после чего присваивает недостающие значения, используя доступные в
классе свойства.
Чтобы облегчить процесс создания и запуска объекта, в С# предлагается синтаксис
инициализатора объекта. С помощью этого механизма можно создать новую
объектную переменную и присвоить значения множеству свойств и/или общедоступных
полей в нескольких строках кода. Синтаксически инициализатор объекта выглядит как
список значений, разделенных запятыми, помещенный в фигурные скобки ({}). Каждый
элемент в списке инициализации отображается на имя общедоступного поля или
свойства инициализируемого объекта.
Рассмотрим пример применения этого синтаксиса. Создадим новое консольное
приложение по имени Object Initializers. Ниже показан класс Point, в котором
используются автоматические свойства (что вообще-то не обязательно в данном примере, но
помогает сократить код):
class Point
{
public int X { get; set; }
public int Y { get; set; }
224 Часть II. Главные конструкции программирования на С#
public Point(int xVal, int yVal)
{
X = xVal;
Y = yVal;
}
public Point () { }
public void DisplayStats ()
{
Console.WriteLine("[{0}, {1}]M, X, Y) ;
}
}
Теперь посмотрим, как создавать объекты Point:
static void Main(string[] args)
{
Console.WnteLine ("***** Fun with Object Init Syntax *****\nM);
// Создать объект Point с установкой каждого свойства вручную.
Point firstPomt = new Point ();
firstPoint.X = 10;
firstPoint.Y = 10;
firstPoint.DisplayStats();
// Создать объект Point с использованием специального конструктора.
Point anotherPoint = new PointB0, 20);
anotherPoint.DisplayStats();
// Создать объект Point с использованием синтаксиса инициализатора объекта.
Point finalPoint = new Point { X = 30, Y = 30 };
finalPoint.DisplayStats();
Console.ReadLine();
}
При создании последней переменной Point не используется специальный
конструктор (как это принято делать традиционно), а вместо этого устанавливаются значения
общедоступных свойств X и Y. "За кулисами" вызывается конструктор типа по
умолчанию, за которым следует установка значений указанных свойств. В конечном счете,
синтаксис инициализации объектов — это просто сокращенная нотация синтаксиса
создания переменной класса с помощью конструктора по умолчанию, с последующей
установкой свойств данных состояния.
Вызов специальных конструкторов с помощью
синтаксиса инициализации
В предыдущих примерах типы Point инициализировались неявным вызовом
конструктора по умолчанию этого типа:
// Здесь конструктор по умолчанию вызывается неявно.
Point finalPoint = new Point { X = 30, Y = 30 };
При желании конструктор по умолчанию можно вызывать и явно:
// Здесь конструктор по умолчанию вызывается явно
Point finalPoint = new Point () { X = 30, Y = 30 };
Имейте в виду, что при конструировании типа с использованием нового
синтаксиса инициализации можно вызывать любой конструктор, определенный в классе. В
настоящий момент в типе Point определен двухаргументный конструктор для установки
позиции (х, у). Таким образом, следующее объявление Point в результате приведет к
установке X равным 100 и Y равным 100, независимо от того факта, что в аргументах
конструктора указаны значения 10 и 16:
Глава 5. Определение инкапсулированных типов классов 225
// Вызов специального конструктора.
Point pt = new Point A0, 16) { X = 100, Y = 100 };
Имея текущее определение типа Point, вызов специального конструктора с
применением синтаксиса инициализации не особенно полезен (и чересчур многословен).
Однако если тип Point предоставляет новый конструктор, позволяющий
вызывающему коду установить цвет (через специальное перечисление PointColor), комбинация
специальных конструкторов и синтаксиса инициализации объекта становится ясной.
Изменим Point следующим образом:
public enum PointColor
{ LightBlue, BloodRed, Gold }
class Point
{
public int X { get; set; }
public int Y { get; set; }
public PointColor Color{ get; set; }
public Point(int xVal, int yVal)
{
X = xVal;
Y = yVal;
Color = PointColor.Gold;
}
public Point(PointColor ptColor)
{
Color = ptColor;
}
public Point ()
: this(PointColor.BloodRed){ }
public void DisplayStats ()
{
Console.WnteLine("[{0}, {1}]M, X, Y) ;
Console.WriteLine ("Point is {0}", Color);
}
}
С помощью этого нового конструктора можно создать золотую точку (в позиции
(90, 20)), как показано ниже:
// Вызов более интересного специального конструктора
/ / с синтаксисом инициализации
Point goldPoint = new Point (PointColor .Gold) { X = 90, Y = 20 };
Console.WriteLine("Value of Point is: {0 } ", goldPoint.DisplayStats ());
Инициализация вложенных типов
Как было ранее кратко упомянуто в этой главе (и будет подробно рассматриваться
в главе 6), отношение "имеет" ("has-a") позволяет составлять новые классы, определяя
переменные-члены существующих классов. Например, предположим, что имеется класс
Rectangle, который использует тип Point для представления координат верхнего
левого и нижнего правого углов. Поскольку автоматические свойства устанавливают все
внутренние переменные классов в null, новый класс будет реализован с
использованием "традиционного" синтаксиса свойств.
class Rectangle
{
private Point topLeft = new Point ();
private Point bottomRight = new Point ();
226 Часть II, Главные конструкции программирования на С#
public Point TopLeft
get { return topLeft; }
set { topLeft = value; }
public Point BottomRight
get { return bottomRight; }
set { bottomRight = value; }
public void DisplayStats ()
Console.WriteLine(M[TopLeft: {0}, {1}, {2} BottomRight: {3}, {4}, {5}]",
topLeft.X, topLeft.Y, topLeft.Color,
bottomRight.X, bottomRight.Y, bottomRight.Color);
}
}
С помощью синтаксиса инициализации объекта можно было бы создать новую
переменную Rectangle и установить внутренние экземпляры Point следующим образом:
// Создать и инициализировать Rectangle.
Rectangle myRect = new Rectangle
{
TopLeft = new Point { X = 10, Y = 10 },
BottomRight = new Point { X = 200, Y = 200}
};
Преимущество синтаксиса инициализации объектов в том, что он в основном
сокращает объем кода (предполагая, что нет подходящего конструктора). Вот как выглядит
традиционный подход для установки того же экземпляра Rectangle:
// Традиционный подход.
Rectangle r = new Rectangle ();
Point pi = new Point ();
pl.X = 10;
pl.Y = 10;
r.TopLeft = pi;
Point p2 = new Point () ;
p2.X = 200;
p2.Y = 200;
r.BottomRight = p2;
Хотя поначалу синтаксис инициализации объекта может показаться не слишком
привычным, как только вы освоитесь с кодом, то будете удивлены, насколько быстро
можно устанавливать состояние нового объекта с минимальными усилиями.
В завершение главы рассмотрим три небольшие темы, которые способствуют
лучшему пониманию построения хорошо инкапсулированных классов: константные
данные, поля, доступные только на чтения, и определения частичных классов.
Исходный код. Проект Objectlnitialozers доступен в подкаталоге Chapter 5.
Работа с данными константных полей
В С# имеется ключевое слово const для определения константных данных, которые
никогда не могут изменяться после начальной установки. Как и можно было
предположить, это полезно при определении наборов известных значений, логически
привязанных к конкретному классу или структуре, для использования в приложениях.
Глава 5. Определение инкапсулированных типов классов 227
Предположим, что создается служебный класс по имени MyMathClass, в котором
нужно определить значение PI (будем считать его равным 3.14). Начнем с создания
нового проекта консольного приложения по имени ConstData. Учитывая, что другие
разработчики не должны иметь возможность изменять значение PI в коде, его можно
смоделировать с" помощью следующей константы:
namespace ConstData
{
class MyMathClass
{
public const double PI = 3.14;
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Const *****\nM);
Console.WriteLine("The value of PI is: { 0 } " , MyMathClass.PI);
// Ошибка! Нельзя изменять константу!
MyMathClass.PI = 3.1444;
Console.ReadLine();
}
}
}
Обратите внимание, что обращение к константным данным, определенным в
классе MyMathClass, осуществляется с использованием префикса в виде имени класса (т.е.
MyMathClass.PI). Это связано с тем, что константные поля класса являются неявно
статическими. Однако допустимо определять и обращаться к локальным константным
переменным внутри члена типа, например:
static void LocalConstStringVariable()
{
// Локальные константные данные доступны непосредственно.
const string fixedStr = "Fixed string Data";
Console.WriteLine(fixedStr);
// Ошибка!
fixedStr = "This will not work!";
}
Независимо от того, где определяется константный элемент данных, следует всегда
помнить, что начальное значение константы всегда должно быть указано в момент ее
определения. Таким образом, если модифицировать класс MyMathClass так, что
значение PI будет присваиваться в конструкторе класса, то возникнет ошибка времени
компиляции:
class MyMathClass
{
// Попытка установить PI в конструкторе?
public const double PI;
public MyMathClass()
{
// Ошибка1
PI = 3.14;
}
}
Причина этого ограничения в том, что значение константных данных должно быть
известно во время компиляции. Конструкторы же, как известно, вызываются во время
выполнения.
228 Насть II. Главные конструкции программирования на С#
Понятие полей только для чтения
Близко к понятию константных данных лежит понятие данных полей, доступных
только для чтения (которые не следует путать со свойствами только для чтения).
Подобно константам, поля только для чтения не могут быть изменены после
начального присваивания. Однако, в отличие от констант, значение, присваиваемое такому
полю, может быть определено во время выполнения, и потому может быть на законном
основании присвоено в контексте конструктора, но нигде более. Это может быть очень
полезно, когда значение поля неизвестно вплоть до момента выполнения (возможно,
потому, что для получения значения необходимо прочитать внешний файл), но нужно
гарантировать, что оно не будет изменено после первоначального присваивания. Ддя
иллюстрации рассмотрим следующее изменение в классе MyMathClass:
class MyMathClass
{
// Поля только для чтения могут присваиваться
//в конструкторах, но нигде более.
public readonly double PI;
public MyMathClass ()
{
PI = 3.14;
}
}
Любая попытка выполнить присваивание полю, помеченному как readonly, вне
контекста конструктора приведет к ошибке компиляции:
class MyMathClass
{
public readonly double PI;
public MyMathClass ()
{
PI = 3.14;
}
// Ошибка!
public void ChangePIO
{ PI = 3.14444; }
}
Статические поля только для чтения
В отличие от константных полей, поля только для чтения не являются неявно
статическими. Поэтому если необходимо представить PI на уровне класса, то для этого
понадобится явно использовать ключевое слово static. Если значение статического поля
только для чтения известно во время компиляции, то начальное присваивание
выглядит очень похожим на константу:
class MyMathClass
{
public static readonly double PI = 3.14;
}
class Program
{
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Const *****");
Console.WriteLine("The value of PI is: {0}", MyMathClass.PI);
Console.ReadLine();
}
}
Глава 5. Определение инкапсулированных типов классов 229
Однако если значение статического поля только для чтения не известно до момента
выполнения, можно прибегнуть к использованию статического конструктора, как было
описано ранее в этой главе:
class MyMathClass
{
public static readonly double PI;
static MyMathClass ()
{ PI = 3.14; }
}
Исходный код. Проект ConstData доступен в подкаталоге Chapter 5.
Понятие частичных типов
В этой главе осталось еще разобраться с ролью ключевого слова partial. Класс
производственного уровня легко может содержать многие сотни строк кода. К тому же,
учитывая, что типичный класс определен внутри одного файла *.cs, может получиться
очень длинный файл. В процессе создания классов нередко большая часть кода может
быть проигнорирована, будучи однажды написанной. Например, данные полей,
свойства и конструкторы, как правило, остаются неизменными во время эксплуатации, в то
время как методы имеют тенденцию модифицироваться довольно часто.
При желании можно разнести единственный класс на несколько файлов С#,
чтобы изолировать рутинный код от более ценных полезных членов. Для примера
загрузим ранее созданный проект EmployeeApp в Visual Studio и откроем файл Employee.cs
для редактирования. Сейчас этот единственный файл содержит код для всех аспектов
класса:
class Employee
{
// Поля данных
// Конструкторы
// Методы
// Свойства
}
Механизм частичных классов позволяет вынести конструкторы и поля данных в
совершенно новый файл по имени Employee.Internal.cs (обратите внимание, что имя
файла не имеет значения; здесь оно выбрано в соответствии с назначением класса).
Первый шаг состоит в добавлении ключевого слова partial к текущему определению
класса и вырезании кода, который должен быть помещен в новый файл:
// Employee.cs
partial class Employee
{
// Методы
// Свойства
}
Предполагая, что новый класс добавлен к проекту, можно переместить поля данных
и конструкторы в новый файл посредством простой операции вырезания и вставки.
Кроме того, необходимо добавить ключевое слово partial к этому аспекту
определения класса.
// Employee.Internal.cs
partial class Employee
{
230 Часть II, Главные конструкции программирования на С#
// Поля данных
// Конструкторы
}
На заметку! Помните, что каждый аспект определения частичного класса должен быть помечен
ключевым словом partial!
Скомпилировав модифицированный проект, вы не должны заметить никакой
разницы. Основная идея, положенная в основу частичного класса, реализуется только во
время проектирования. Как только приложение скомпилировано, в сборке оказывается
один цельный класс. Единственное требование при определении частичных типов
связано с тем, что разные части должны иметь одно и то же имя и находиться в пределах
одного и того же пространства имен .NET.
Откровенно говоря, определения частичных классов применяются нечасто. Однако
среда Visual Studio постоянно использует их в фоновом режиме. Позже в этой книге,
когда речь пойдет о разработке приложений с графическим пользовательским
интерфейсом посредством Windows Forms, Windows Presentation Foundation или ASP.NET, будет
показано, что Visual Studio изолирует сгенерированный визуальным конструктором код
в частичном классе, позволяя сосредоточиться на специфичной программной логике
приложения.
Исходный код. Проект EmployeeApp доступен в подкаталоге Chapter 5.
Резюме
Цель этой главы заключалась в ознакомлении с ролью типов классов С#. Вы видели,
что классы могут иметь любое количество конструкторов, которые позволяют
пользователю объекта устанавливать состояние объекта при его создании. В главе также было
проиллюстрировано несколько приемов проектирования классов (и связанных с ними
ключевых слов). Ключевое слово this используется для получения доступа к текущему
объекту, ключевое слово static позволяет определять поля и члены, привязанные к
классу (а не объекту), а ключевое слово const (и модификатор readonly) дает
возможность определять элементы данных, которые никогда не изменяются после
первоначальной установки.
Большая часть главы была посвящена деталям первого принципа ООП:
инкапсуляции. Здесь вы узнали о модификаторах доступа С# и роли свойств типа, синтаксиса
инициализации объектов и частичных классов. Обладая всеми этими знаниями, теперь
вы готовы к тому, чтобы перейти к следующей главе, в которой узнаете о построении
семейства взаимосвязанных классов с использованием наследования и полиморфизма.
ГЛАВА 6
Понятия наследования
и полиморфизма
В предыдущей главе рассматривался первый принцип ООП — инкапсуляция. Вы
узнали, как построить отдельный правильно спроектированный тип класса с
конструкторами и различными членами (полями, свойствами, константами и
полями, доступными только для чтения). В настоящей главе мы сосредоточимся на
остальных двух принципах объектно-ориентированного программирования: наследовании и
полиморфизме.
Прежде всего, вы узнаете, как строить семейства связанных классов с применением
наследования^ Как будет показано, эта форма повторного использования кода
позволяет определять общую функциональность в родительском классе, которая может быть
использована и, возможно, изменена в дочерних классах. По пути вы узнаете, как
устанавливать полиморфный интерфейс в иерархиях классов, используя виртуальные и
абстрактные члены. Завершается глава рассмотрением роли начального родительского
класса в библиотеках базовых классов .NET — System.Object.
Базовый механизм наследования
Вспомните из предыдущей главы, что наследование — это аспект ООП, облегчающий
повторное использование кода. Строго говоря, повторное использование кода
существует в двух видах: наследование (отношение "является") и модель включения/делегации
(отношение "имеет"). Начнем главу с рассмотрения классической модели наследования
типа "является".
При установке между классами отношения "является" строится зависимость между
двумя или более типами классов. Базовая идея, лежащая в основе классического
наследования, заключается в том, что новые классы могут создаваться с использованием
существующих классов в качестве отправной точки. Давайте начнем с очень простого
примера, создав новый проект консольного приложения по имени Basiclnheritance.
Предположим, что спроектирован класс по имени Саг, моделирующий некоторые
базовые детали автомобиля:
// Простой базовый класс.
class Car
{
public readonly int maxSpeed;
private int currSpeed;
public Car(int max)
{
maxSpeed = max;
}
232 Часть II. Главные конструкции программирования на С#
public Car()
{
maxSpeed = 55;
}
public int Speed
{
get { return currSpeed; }
set
{
currSpeed = value;
if (currSpeed > maxSpeed)
{
currSpeed = maxSpeed;
}
}
}
}
Обратите внимание, что в Car применяется инкапсуляция для управления доступом
к приватному полю currSpead с использованием общедоступного свойства по имени
Speed. Имея такое определение, с типом Саг можно работать следующим образом:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Basic Inheritance *****\n");
// Создать экземпляр типа Car и установить максимальную скорость.
Car myCar = new Car (80);
// Установить текущую скорость и вывести ее на консоль.
myCar .Speed = 50;
Console.WriteLine("My car is going {0} MPH", myCar.Speed);
Console.ReadLine();
}
Указание родительского класса для существующего класса
Теперь предположим, что планируется построить новый класс по имени MiniVan.
Подобно базовому классу Саг, необходимо, чтобы MiniVan поддерживал максимальную
скорость, текущую скорость и свойство по имени Speed, позволяющее пользователю
модифицировать состояние объекта. Ясно, что классы Саг и MiniVan взаимосвязаны;
фактически можно сказать, что MiniVan "является" Саг. Отношение "является"
(формально называемое классическим наследованием) позволяет строить новые
определения классов, расширяющие функциональность существующих классов.
Существующий класс, который будет служить основой для нового класса,
называется базовым или родительским классом. Назначение базового класса состоит в
определении всех общих данных и членов для классов, которые расширяют его. Расширяющие
классы формально называются производными или дочерними классами. В С# для
установки между классами отношения "является" используется операция двоеточия в
определении класса:
// MiniVan 'является' Саг.
class MiniVan : Car
{
}
Так в чем же состоит выигрыш от наследования MiniVan от базового класса Саг?
Попросту говоря, объекты MiniVan имеют доступ ко всем общедоступным членам,
определенным в базовом классе.
Глава 6. Понятия наследования и полиморфизма 233
На заметку! Хотя конструкторы обычно определяются как общедоступные, производный класс
никогда не наследует конструкторы своего родительского класса.
Учитывая отношение между этими двумя типами классов, класс MiniVan можно
использовать следующим образом:
static void Main(string [ ] args)
{
Console.WriteLine("***** Basic Inheritance *****\n");
// Создать объект MiniVan.
MiniVan myVan = new MiniVan();
myVan.Speed = 10;
Console.WriteLine("My van is going {0} MPH",
myVan.Speed) ;
Console.ReadLine();
}
Обратите внимание, что хотя к классу MiniVan не добавлены никакие члены,
имеется прямой доступ к public-свойству Speed родительского класса, и таким образом, его
код используется повторно. Это намного лучше, чем создавать класс MiniVan, имеющий
в точности те же члены, что и Саг, такие как свойство Speed. В случае дублирования
кода в этих двух классах придется сопровождать два фрагмента одинакового кода, что
очевидно является непроизводительным расходом времени.
Всегда помните, что наследование предохраняет инкапсуляцию, а потому
следующий код вызовет ошибку компиляции, поскольку приватные члены никогда не могут
быть доступны через ссылку на объект:
static void Main(string[] args)
{
Console.WriteLine("***** Basic Inheritance *****\nM);
// Создать объект MiniVan.
MiniVan myVan = new MiniVan();
myVan.Speed = 10;
Console.WriteLine("My van is going {0} MPH",
myVan.Speed) ;
// Ошибка! Доступ к приватным членам невозможен!
myVan.currSpeed = 55;
Console.ReadLine();
}
Кстати говоря, если в MiniVan будет определен собственный набор членов, он не
получит доступа ни к одному приватному члену базового класса Саг. Опять-таки,
приватные члены могут быть доступны только в классе, в котором они определены.
// MiniVan унаследован от Саг.
class MiniVan : Car
{
public void TestMethodO
{
//OK! Доступ к public-членам родителя в производном типе возможен.
Speed = 10;
// Ошибка! Нельзя осуществлять доступ к private-членам родителя
//из производного типа!
currSpeed =10;
}
}
234 Часть II. Главные конструкции программирования на С#
О множественном наследовании
ГЬворя о базовых классах, важно иметь в виду, что язык С# требует, чтобы любой
конкретный класс имел в точности один непосредственный базовый класс. Невозможно
создать тип класса, который напрямую унаследован от двух и более базовых классов
(эта техника, поддерживаемая в неуправляемом C++, называется множественным
наследованием). Попытка создать класс, в котором указано два непосредственных
родительских класса, как показано в следующем коде, приводит к ошибке компиляции:
//Не разрешается! Платформа .NET не допускает
// множественное наследование классов!
class WontWork
: BaseClassOne, BaseClassTwo
{}
Как будет показано в главе 9, платформа .NET позволяет конкретному классу или
структуре реализовывать любое количество дискретных интерфейсов. Благодаря этому,
тип С# может представлять набор поведений, избегая сложностей, присущих
множественному наследованию. Кстати говоря, хотя класс может иметь только один
непосредственный базовый класс, допускается наследование одного интерфейса от множества
других интерфейсов. Используя эту технику, можно строить изощренные иерархии
интерфейсов, моделирующих сложные поведения (см. главу 9).
Ключевое слово sealed
В С# поддерживается еще одно ключевое слово — sealed, которое предотвращает
наследование. Если класс помечен как sealed (запечатанный), компилятор не
позволяет наследовать от него. Например, предположим, решено, что нет смысла в дальнейшем
наследовании от класса MiniVan:
// Класс Minivan не может быть расширен!
sealed class MiniVan : Car
{
}
Если вы (или коллега по команде) попытаетесь унаследовать от этого класса, то
получите ошибку времени компиляции:
// Ошибка! Нельзя расширять класс,
// помеченный ключевым словом sealed!
class DeluxeMiniVan
: MiniVan
{}
Чаще всего запечатывание имеет смысл при проектировании служебного класса.
Например, в пространстве имен System определено множество запечатанных классов.
В этом легко убедиться, открыв окно Object Browser в Visual Studio 2010 (через меню
View (Вид)) и выбрав класс String, определенный в пространстве имен System внутри
сборки mscorlib.dll. На рис. 6.1 обратите внимание на использование ключевого
слова sealed, выделенного в окне Summary (Сводка).
Таким образом, подобно MiniVan, если попытаться построить новый класс,
расширяющий System.String, возникнет ошибка компиляции:
// Ошибка! Нельзя расширять класс, помеченный как sealed!
class MyString
: String
{}
Глава 6. Понятия наследования и полиморфизма 235
Res ol veEve nt H an d ler [ <§
RuntimeArgumentHc
RuntimeReldHandle
RuntimeMethodHan<
RuntimeTypeHandle
SByte
SerializableAttribute
Single
StackOverflowExcepti
STAThreadAttribute
PI
StringComparer
StringComparison рйЙ
-♦ CtoneO
■'♦ Compare($tring, int string, int, int, System.StringComparison) i_.
Ф Compare(string, int, string, int >nt System.Globalization.Ciirturi
Ф Comparefstring, int. string, int «nt, bool, System.Globalizatron.C
Ф CompareEtring, int, string, int int bool)
■'♦ Compare(string, int string, int int)
"♦ Compare(string, string, bool, System.Globalization.Culturelnfo) ,
'"
public sealed class String
Member of S
Summary:
Represents text as a series of Unicode characters.
Рис. 6.1. В библиотеках базовых классов определено множество запечатанных типов
На заметку! В главе 4 было показано, что структуры С# всегда неявно запечатаны (см. табл. 4.3).
Поэтому ни унаследовать одну структуру от другой, ни класс от структуры, ни структуру от
класса не получится. Структуры могут использоваться только для моделирования отдельных
атомарных, определенных пользователем типов. Для реализации отношения "является"
необходимо применять классы.
Как можно догадаться, существует множество других деталей наследования, о
которых вы узнаете в оставшейся части главы. А пока просто имейте в виду, что операция
двоеточия позволяет устанавливать между классами отношения
"базовый-производный", а ключевое слово sealed предотвращает наследование.
Изменение диаграмм классов Visual Studio
В главе 2 кратко упоминалось, что среда Visual Studio 2010 позволяет устанавливать
между классами отношения "базовый-производный" визуально во время
проектирования. Чтобы использовать этот аспект IDE-среды, первый шаг состоит во включении
нового файла диаграммы классов в текущий проект. Для этого выберите в меню пункт
ProjectoAdd New Item (ПроектОДобавить новый элемент) и затем пиктограмму Class
Diagram (Диаграмма классов); на рис. 6.2 имя файла ClassDiagraml.cd было изменено
на Cars.cd.
Add New Item - вагкЙ^^^^^Н
■ Sort by:
I Installed Templates ;__J
J * Visual C# Items
Code
1 Data
General
Web
Windows Forms
WPf
Reporting
ш
Ш
m
j §
1 Online Templates [J§|
; Щ
1 Name Cars^d
Default -1
Settings File
Text File
Assembly Information File
Class Diagram
Applicabon Manifest File
Windows Script Host
Debugger Visualize»
!»■
b
i Class Diagram i
Visual C# Items
Visual C* Items
Visual C* Items
Visual C* items
V.sualC# Items
Visual C# hems L_
Я
Visual C# hems IJ
irch In;tallfd Templates... 1
Claw Diagram
Type: Visual C# hems
A blank class diagram ! ' jj
ill
: 1
il
1
Add )[_ Ceocei
Рис. 6.2. Вставка в проект новой диаграммы классов
236 Часть II. Главные конструкции программирования на С#
После щелчка на кнопке Add [Добавить) появится пустая поверхность
проектирования. Для добавления классов к диаграмме просто перетаскивайте каждый файл из
окна Solution Explorer на эту поверхность. Также помните, что удаление элемента в
визуальном конструкторе (за счет его выбора и нажатия клавиши <Delete>), не приводит к
удалению ассоциированного исходного кода, а просто убирает элемент из поверхности
проектирования. Текущая иерархия классов показана на рис. 6.3.
Carsxd X
ЕЗЯ
Саг
Class
8 Fields
£р currSpeed
♦ maxSpeed
9 Properties
* Speed
S Methods
:* Car (+1 overload)
?
MiniVan gf
Class
-♦Car
!
Рис. 6.З. Визуальный конструктор Visual Studio
На заметку! Итак, если необходимо автоматически добавить все текущие типы проекта на
поверхность проектирования, выберите узел Project (Проект) в Solution Explorer и щелкните на кнопке
View Class Diagram (Показать диаграмму классов) в правом верхнем углу окна Solution Explorer.
Помимо простого отображения отношений между типами внутри текущего
приложения, вспомните из главы 2, что можно также создавать совершенно новые типы и
наполнять их членами, используя панель инструментов Class Designer (Конструктор
классов) и окно Class Details (Детали класса).
Если хотите использовать эти визуальные инструменты в процессе дальнейшего
чтения книги — пожалуйста. Однако всегда анализируйте сгенерированный код, чтобы
четко понимать, что эти инструменты делают за вашей спиной.
Исходный код. Проект Basiclnheritance доступен в подкаталоге Chapter 6.
Второй принцип ООП: подробности о наследовании
Ознакомившись с базовым синтаксисом наследования, давайте рассмотрим более
сложный пример и узнаем о многочисленных деталях построения иерархий классов.
Для этого воспользуемся классом Employee, спроектированным в главе 5. Для начала
создадим новое консольное приложение С# по имени Employees.
Выберите пункт меню Project ^Add Existing Item (ПроектОДобавить существующий
элемент) и перейдите к месту нахождения файлов Employee.cs и Employee.Internals.cs,
которые были созданы в примере EmployeeApp из предыдущей главы. Выберите оба
Глава 6. Понятия наследования и полиморфизма 237
файла (щелкая на них при нажатой клавише <Ctrl>) и щелкните на кнопке ОК. Среда
Visual Studio 2010 отреагирует копированием каждого файла в текущий проект
Прежде чем начать построение производных классов, следует уделить внимание
одной детали. Поскольку первоначальный класс Employee был создан в проекте по имени
EmployeeApp, этот класс находится в идентично названном пространстве имен .NET.
Пространства имен подробно рассматриваются в главе 14, а пока для простоты просто
переименуйте текущее пространство имен (в обоих файлах) на Employees, чтобы оно
соответствовало имени нового проекта:
//Не забудьте изменить название пространства имен в обоих файлах!
namespace Employees
{
partial class Employee
}
На заметку! Чтобы подстраховаться, скомпилируйте и запустите новый проект, нажав <Ctrl+F5>.
Пока программа ничего не делает, однако это позволит убедиться в отсутствии ошибок
компиляции.
Нашей целью будет создание семейства классов, моделирующих различные типы
сотрудников компании. Предположим, что необходимо воспользоваться
функциональностью класса Employee при создании двух новых классов (Salesperson и Manager).
Иерархия классов, которую мы вскоре построим, будет выглядеть примерно так, как
показано на рис. 6.4 (имейте в виду, что в случае использования синтаксиса
автоматических свойств С# отдельные поля в диаграмме не будут видны).
Employee
Class
В Fields
*
Ф
,<
,<*
*
*
company Name
currPay
empAge
empID
empName
empSSN
s Properties
ЯГ
dr
ar
sar
sff
ar
Age
Company
ID
Name
Pay
SocialSecurityNumber
В Methods
*
*
-♦
DisplayStats
Employee (♦ 3 overloads)
GiveBonus
Class
•*• Employee
S Fields
ф numberOfOptions
s Properties
Ш StockOptions
Salesperson S
Class
+ Employee
3 Fields
$ numberOfSales
a Properties
2sP Sal «Number
Рис. 6.4. Начальная иерархия классов
238 Часть II. Главные конструкции программирования на С#
Как показано на рис. 6.4, класс Salesperson "является" Employee (как и Manager).
Вспомните, что в модели классического наследования базовые классы (вроде Employee)
используются для определения характеристик, общих для всех наследников. Подклассы
(такие как Salesperson и Manager) расширяют общую функциональность, добавляя
дополнительную специфическую функциональность.
Для нашего примера предположим, что класс Manager расширяет Employee, храня
количество опционов на акции, в то время как класс Salesperson поддерживает
количество продаж. Добавьте новый файл класса (Manager.ее), определяющий тип Manager
следующим образом:
// Менеджерам нужно знать количество их опционов на акции.
class Manager : Employee
{
public int StockOptions { get; set; }
}
Затем добавьте новый файл класса (SalesPerson.cs), в котором определен класс
Salesperson с соответствующим автоматическим свойством:
// Продавцам нужно знать количество продаж.
class Salesperson : Employee
{
public int SalesNumber { get; set; }
}
Теперь, после установки отношения "является", Salesperson и Manager
автоматически наследуют все общедоступные члены базового класса Employee. Для иллюстрации
обновите метод Main() следующим образом:
// Создание объекта подкласса и доступ к функциональности базового класса.
static void Main(string[] args)
{
Console.WriteLine("***** The Employee Class Hierarchy *****\n");
Salesperson danny = new Salesperson();
danny.Age =31;
danny.Name = "Danny";
danny.SalesNumber = 50;
Console.ReadLine();
}
Управление созданием базового класса
с помощью ключевого слова base
Сейчас объекты Salesperson и Manager могут быть созданы только с
использованием "бесплатного" конструктора по умолчанию (см. главу 5). Памятуя об этом,
предположим, что к типу Manager добавлен новый конструктор, который принимает шесть
аргументов и вызывается следующим образом:
static void Main(string [] args)
{
// Предположим, что у Manager есть конструктор со следующей сигнатурой:
// (string fullName, int age, int empID,
// float currPay, string ssn, int numbOfOpts)
Manager chucky = new Manager("Списку", 50, 92, 100000, 33-23-2322", 9000);
Console.ReadLine ();
}
Глава 6. Понятия наследования и полиморфизма 239
Если взглянуть на список параметров, то ясно видно, что большинство из них
должно быть сохранено в переменных-членах, определенных в базовом классе Employee.
В этом случае для класса Manager можно реализовать специальный конструктор
следующего вида:
public Manager(string fullName, int age, int empID,
float currPay, string ssn, int numbOfOpts)
{
// Это свойство определено в классе Manager.
StockOptions = numbOfOpts;
// Присвоим входные параметры, используя
// унаследованные свойства родительского класса.
ID = empID;
Age = age;
Name = fullName;
Pay = currPay;
// Здесь возникнет ошибка компиляции, поскольку
// свойство SSN доступно только для чтения!
SocialSecurityNumber = ssn;
}
Первая проблема такого подхода состоит в том, что если определить какое-то
свойство как доступное только для чтения (например, SocialSecurityNumber), то присвоить
значение входного параметра string соответствующему полю не удастся, как можно
видеть в финальном операторе специального конструктора.
Вторая проблема состоит в том, что был неявно создан довольно неэффективный
конструктор, учитывая тот факт, что в С#, если не указать иного, конструктор
базового класса вызывается автоматически перед выполнением логики производного
конструктора. После этого момента текущая реализация имеет доступ к многочисленным
public-свойствам базового класса Employee для установки его состояния. Таким
образом, в действительности во время создания объекта Manager выполняется семь
действий (обращений к пяти унаследованным свойствам и двум конструкторам).
Для оптимизации создания производного класса необходимо хорошо реализовать
конструкторы подкласса, чтобы они явно вызывали специальный конструктор
базового класса вместо конструктора по умолчанию. Поступая подобным образом, можно
сократить количество вызовов инициализаций унаследованных членов (что уменьшит
время обработки). Давайте модифицируем специальный конструктор класса Manager,
применив ключевое слово base:
public Manager(string fullName, int age, int empID,
float currPay, string ssn, int numbOfOpts)
: base (fullName, age, empID, currPay, ssn)
{
// Это свойство определено в классе Manager.
StockOptions = numbOfOpts;
}
Здесь ключевое слово base ссылается на сигнатуру конструктора (подобно
синтаксису, используемому для сцепления конструкторов на единственном классе с
использованием ключевого слова this, как было показано в главе 5), что всегда указывает на
то, что производный конструктор передает данные конструктору непосредственного
родителя. В данной ситуации явно вызывается конструктор с пятью параметрами,
определенный в Employee, что избавляет от излишних вызовов во время создания
экземпляра базового класса. Специальный конструктор Salesperson выглядит в основном
идентично:
240 Часть II. Главные конструкции программирования на С#
//В качестве общего правила, все подклассы должны явно вызывать
// соответствующий конструктор базового класса.
public Salesperson(string fullName, int age, int empID,
float currPay, string ssn, int numbOfSales)
: base(fullName, age, empID, currPay, ssn)
{
// Это касается нас!
SalesNumber = numbOfSales;
}
На заметку! Ключевое слово base можно использовать везде, где подкласс желает обратиться к
общедоступному или защищенному члену, определенному в родительском классе. Применение
этого ключевого слова не ограничивается логикой конструктора. Вы увидите примеры
использования base в такой манере далее в главе, во время рассмотрения полиморфизма.
И, наконец, вспомните, что как только в определении класса появляется специальный
конструктор, конструктор по умолчанию из класса молча удаляется. Следовательно, не
забудьте переопределить конструктор по умолчанию для типов Salesperson и Manager.
Например:
// Вернуть классу Manager конструктор по умолчанию.
public Salesperson () {}
Хранение фамильных тайн: ключевое слово protected
Как уже должно быть известно, общедоступные (public) элементы непосредственно
доступны отовсюду, в то время как приватные (private) могут быть доступны только в
классе, где они определены. Вспомните из главы 5, что С# следует примеру многих
других современных объектных языков и предлагает дополнительное ключевое слово для
определения доступности членов, а именно — protected (защищенный).
Когда базовый класс определяет защищенные данные или защищенные члены, он
устанавливает набор элементов, которые могут быть доступны непосредственно
любому наследнику. Например, чтобы позволить дочерним классам Salesperson и Manager
непосредственно обращаться к разделу данных, определенному в Employee, можете
изменить исходный класс Employee следующим образом:
// Защищенные данные состояния.
partial class Employee
{
// Теперь производные классы могут напрямую обращаться к этой информации.
protected string empName;
protected int empID;
protected float currPay;
protected int empAge;
protected string empSSN;
protected static string companyName;
}
Преимущество определения защищенных членов в базовом классе состоит в том, что
производным типам больше не нужно обращаться к данным опосредованно, используя
общедоступные методы и свойства. Возможным минусом такого подхода, конечно же,
является то, что когда производный тип имеет прямой доступ к внутренним данным своего
родителя, возникает вероятность непреднамеренного нарушения существующих бизнес-
правил, которые реализованы в общедоступных свойствах. При определении
защищенных членов создается уровень доверия между родительским и дочерним классами,
поскольку компилятор не перехватит никаких нарушений существующих бизнес-правил.
Глава 6. Понятия наследования и полиморфизма 241
И, наконец, имейте в виду, что с точки зрения пользователя объекта защищенные
данные трактуются как приватные (поскольку пользователь находится "вне" семейства).
Потому следующий код некорректен:
static void Main(string [ ] args)
{
// Ошибка! Доступ к защищенным данным через экземпляр объекта невозможен!
Employee emp = new Employee();
emp.empName = "Fred";
}
На заметку! Хотя protected-поля данных могут нарушить инкапсуляцию, объявлять protected-
методы достаточно безопасно (и полезно). При построении иерархий классов очень часто
приходится определять набор методов, которые используются только производными типами.
Добавление запечатанного класса
Вспомните, что запечатанный (sealed) класс не может быть расширен другими
классами. Как уже упоминалось, эта техника чаще всего применяется при проектировании
служебных классов. Тем не менее, при построении иерархий классов можно
обнаружить, что некоторая ветвь в цепочке наследования нуждается в "отсечении", поскольку
дальнейшее ее расширение не имеет смысла. Например, предположим, что в
приложение добавлен еще один класс (PTSalesPerson), который расширяет существующий тип
Salesperson. На рис. 6.5 показано текущее добавление.
Manager
Class
•* Employee
Salesperson
Class
■♦ Employee
PTSalesPerson
Class
-* Salesperson
m
)
9\
_J
Рис. 6.5. Класс PTSalesPerson
Класс PTSalesPerson представляет продавца, который работает с частичной
занятостью. Предположим, что необходимо гарантировать отсутствие возможности
наследования от класса PTSalesPerson. (В конце концов, какой смысл в "частичной занятости
от частичной занятости"?) Для предотвращения наследования от класса используется
ключевое слово sealed:
sealed class PTSalesPerson : Salesperson
{
public PTSalesPerson(string fullName, int age, int empID,
float currPay, string ssn, int numbOfSales)
:base (fullName, age, empID, currPay, ssn, numbOfSales)
}
// Остальные члены класса..,
242 Часть II. Главные конструкции программирования на С#
Учитывая, что запечатанные классы не могут быть расширены, может возникнуть
вопрос: каким образом повторно использовать функциональность, если класс помечен
как sealed? Чтобы построить новый класс, использующий функциональность
запечатанного класса, единственным вариантом является отказ от классического
наследования в пользу модели включения/делегации (т.е. отношения "имеет").
Реализация модели включения/делегации
Вспомните, что повторное использование кода возможно в двух вариантах. Только что
было рассмотрено классическое отношение "является". Перед тем, как мы обратимся к
третьему принципу ООП (полиморфизму), давайте поговорим об отношении "имеет" (еще
известном под названием модели включения/делегации или агрегации). Предположим,
что создан новый класс, который моделирует пакет льгот для сотрудников:
// Этот новый тип будет работать как включаемый класс.
class BenefitPackage
{
// Предположим, что есть другие члены, представляющие
// медицинские/стоматологические программы и т.д.
public double ComputePayDeduction()
{
return 125.0;
}
}
Очевидно, что было бы довольно нелепо устанавливать отношение "является" между
классом Benef itPackage и типами сотрудников. (Employee "является" Benef itPackage?
Вряд ли). Однако должно быть ясно, что какие-то отношения между ними должны быть
установлены. Короче говоря, понадобится выразить идею, что каждый сотрудник
"имеет" BenefitPackage. Для этого можно модифицировать определение класса Employee
следующим образом:
// Сотрудники имеют льготы.
partial class Employee
{
// Содержит объект BenefitPackage.
protected BenefitPackage empBenefits = new BenefitPackage();
}
Таким образом, один объект успешно содержит в себе другой объект. Однако
чтобы представить функциональность включенного объекта внешнему миру, потребуется
делегация. Делегация — это просто акт добавления общедоступных членов к
включающему классу, которые используют функциональность включенного объекта. Например,
можно было бы обновить класс Employee, чтобы он представлял включенный объект
empBenefits с помощью специального свойства, а также пользоваться его
функциональностью внутренне, через новый метод по имени GetBenefitCost():
public partial class Employee
{
// Содержит объект BenefitPackage.
protected BenefitPackage empBenefits = new BenefitPackage();
// Представляет некоторое поведение, связанное с включенным объектом.
public double GetBenefitCost ()
{ return empBenefits.ComputePayDeduction(); }
Глава 6. Понятия наследования и полиморфизма 243
// Представляет объект через специальное свойство.
public BenefitPackage Benefits
{
get { return empBenefits; }
set { empBenefits = value; }
}
}
В следующем обновленном методе Main() обратите внимание на взаимодействие с
внутренним типом Benef itsPackage, который определен в типе Employee:
static void Main(string [ ] args)
{
Console.WriteLine ("***** The Employee Class Hierarchy *****\nM);
Manager chucky = new Manager("Списку", 50, 92, 100000, 33-23-2322", 9000);
double cost = chucky.GetBenefitCost();
Console.ReadLine();
}
Определения вложенных типов
В предыдущей главе была кратко упомянута концепция вложенных типов, которая
является разновидностью только что рассмотренного отношения "имеет". В С# (как и
в других языках .NET) допускается определять тип (перечисление, класс, интерфейс,
структуру или делегат) непосредственно внутри контекста класса или структуры. При
этом вложенный (или "внутренний") тип считается членом охватывающего (или
"внешнего") класса, и в глазах исполняющей системы им можно манипулировать как любым
другим членом (полем, свойством, методом и событием). Синтаксис, используемый для
вложения типа, достаточно прост:
public class OuterClass
{
// Общедоступный вложенный тип может использоваться повсюду.
public class PublicInnerClass {}
// Приватный вложенный тип может использоваться
// только членами включающего класса.
private class PrivatelnnerClass {}
}
Хотя синтаксис ясен, понять, для чего это может потребоваться, не так-то просто.
Чтобы разобраться с этой техникой, рассмотрим характерные особенности вложенных
типов.
• Вложенные типы позволяют получить полный контроль над уровнем доступа
внутреннего типа, поскольку они могут быть объявлены как приватные
(вспомните, что не вложенные классы не могут быть объявлены с использованием
ключевого слова private).
• Поскольку вложенный тип является членом включающего класса, он может иметь
доступ к приватным членам включающего класса.
• Часто вложенные типы удобны в качестве вспомогательных для внешнего класса
и не предназначены для использования внешним миром.
Когда тип включает в себя другой тип класса, он может создавать
переменные-члены этого типа, как любой другой элемент данных. Однако если вложенный тип нужно
использовать вне включающего типа, его понадобится квалифицировать именем
включающего типа. Взгляните на следующий код:
244 Часть II. Главные конструкции программирования на С#
static void Main(string [ ] args)
{
// Создать и использовать общедоступный вложенный класс. Верно!
OuterClass.PublicInnerClass inner;
inner = new OuterClass.PublicInnerClass ();
// Ошибка компилятора! Доступ к приватному классу невозможен!
OuterClass.PrivatelnnerClass inner2;
inner2 = new OuterClass.PrivatelnnerClass ();
}
Чтобы использовать эту концепцию в рассматриваемом примере с сотрудниками,
предположим, что теперь определение Bene fit Package вложено непосредственно в
класс Employee:
partial class Employee
{
public class BenefitPackage
{
// Предположим, что есть другие члены, представляющие
// медицинские/стоматологические программы и т.д.
public double ComputePayDeduction ()
{
return 125.0;
}
Вложение может иметь произвольную глубину. Например, пусть требуется создать
перечисление по имени BenefitPackageLevel, документирующее различные уровни
льгот, которые могут быть предоставлены сотруднику. Чтобы программно установить
тесную связь между Employee, BenefitPackage и BenefitPackageLevel, можно
вложить перечисление следующим образом:
// В Employee вложен BenefitPackage.
public partial class Employee
{
// В BenefitPackage вложено BenefitPackageLevel.
public class BenefitPackage
{
public enum BenefitPackageLevel
{
Standard, Gold, Platinum
}
public double ComputePayDeduction ()
{
return 125.0;
}
Из-за отношений вложения обратите внимание на то, как приходится использовать
это перечисление:
static void Main(string [ ] args)
{
// Определить уровень льгот.
Employee.BenefitPackage.BenefitPackageLevel myBenefitLevel =
Employee.BenefitPackage.BenefitPackageLevel.Platinum;
Console.ReadLine()
}
Глава 6. Понятия наследования и полиморфизма 245
Блестяще! К этому моменту вы познакомились с множеством ключевых слов (и
концепций), которые позволяют строить иерархии взаимосвязанных типов через
классическое наследование, включение и вложенные типы. Если пока не все детали ясны, не
переживайте. На протяжении оставшейся части книги вы построите еще много
дополнительных иерархий. А теперь давайте перейдем к рассмотрению последнего принципа
ООП: полиморфизма.
Третий принцип ООП:
поддержка полиморфизма в С#
Вспомните, что в базовом классе Employee был определен метод по имени
GiveBonusO со следующей первоначальной реализацией:
public partial class Employee
{
public void GiveBonus(float amount)
{
currPay += amount;
}
}
Поскольку этот метод был определен с ключевым словом public, теперь можно
раздавать бонусы продавцам и менеджерам (а также продавцам с частичной занятостью):
static void Main(string[] args)
{
Console.WriteLine ("***** The Employee Class Hierarchy *****\n");
// Дать каждому сотруднику бонус?
Manager списку = new Manager("Списку", 50, 92, 100000, 33-23-2322", 9000);
chucky.GiveBonusC00);
chucky.DisplayStats();
Console.WriteLine();
Salesperson fran = new Salesperson ("Fran", 43, 93, 3000, "932-32-3232", 31);
fran.GiveBonusB00);
fran.DisplayStats();
Console.ReadLine();
}
Проблема текущего кода состоит в том, что общедоступно унаследованный метод
GiveBonusO работает идентично для всех подклассов. В идеале при подсчете бонуса
для штатного продавца и частично занятого продавца должно приниматься во
внимание количество продаж. Возможно, менеджеры должны получать дополнительные
опционы на акции вместе с денежным вознаграждением. Учитывая это, вы однажды
столкнетесь с интересным вопросом: "Как сделать так, чтобы связанные типы
по-разному реагировали на один и тот же запрос?". Попробуем отыскать на него ответ.
Ключевые слова virtual и override
Полиморфизм предоставляет подклассу способ определения собственной версии
метода, определенного в его базовом классе, с использованием процесса, который
называется переопределением метода (method overriding). Чтобы пересмотреть текущий
дизайн, нужно понять значение ключевых слов virtual и override. Если базовый класс
желает определить метод, который может быть (но не обязательно) переопределен в
подклассе, он должен пометить его ключевым словом virtual:
246 Часть II. Главные конструкции программирования на С#
partial class Employee
{
// Этот метод теперь может быть переопределен производным классом.
public virtual void GiveBonus(float amount)
{
currPay += amount;
}
}
На заметку! Методы, помеченные ключевым словом virtual, называются виртуальными
методами.
Когда класс желает изменить реализацию деталей виртуального метода, он делает это с
помощью ключевого слова override. Например, Salesperson и Manager могли бы
переопределить GiveBonus (), как показано ниже (предполагая, что PTSalesPerson не будет
переопределять GiveBonus (), а потому просто наследует версию, определенную Salesperson):
class Salesperson : Employee
{
// Бонус продавца зависит от количества продаж.
public override void GiveBonus(float amount)
{
int salesBonus = 0;
if (numberOfSales >= 0 && numberOfSales <= 100)
salesBonus = 10;
else
{
if (numberOfSales >= 101 && numberOfSales <= 200)
salesBonus = 15;
else
salesBonus = 20;
}
base.GiveBonus(amount * salesBonus);
}
}
class Manager : Employee
{
public override void GiveBonus(float amount)
{
base.GiveBonus(amount);
Random r = new Random () ;
numberOfOptions += r.Next E00);
}
}
Обратите внимание на использование каждым переопределенным методом
поведения по умолчанию через ключевое слово base. Таким образом, полностью повторять
реализацию логики GiveBonus () вовсе не обязательно, а вместо этого можно повторно
использовать (и, возможно, расширять) поведение по умолчанию родительского класса.
Также предположим, что текущий метод DisplayStatus () класса Employee объявлен
виртуальным. При этом каждый подкласс может переопределять этот метод в расчете
на отображение количества продаж (для продавцов) и текущих опционов на акции (для
менеджеров). Например, рассмотрим версию метода DisplayStatus () в классе Manager
(класс Salesperson должен реализовать DisplayStatus () аналогичным образом, чтобы
вывести на консоль количество продаж):
Глава 6. Понятия наследования и полиморфизма 247
public override void DisplayStats ()
{
base.DisplayStats ();
Console.WriteLine("Number of Stock Options: {0}", numberOfOptions);
}
Теперь, когда каждый подкласс может интерпретировать, что именно эти
виртуальные методы означают для него, каждый экземпляр объекта ведет себя как более
независимая сущность:
static void Main(string[] args)
{
Console.WriteLine ("***** The Employee Class Hierarchy *****\nM);
// Лучшая система бонусов!
Manager списку = new Manager("Списку", 50, 92, 100000, 33-23-2322", 9000);
chucky.GiveBonusC00);
chucky.DisplayStats();
Console.WriteLine();
Salesperson fran = new Salesperson("Fran", 43, 93, 3000, n932-32-3232n, 31);
fran.GiveBonusB00);
fran.DisplayStats();
Console.ReadLine();
}
Ниже показан результат тестового запуска приложения в нынешнем виде:
***** The Employee Class Hierarchy *****
Name: Chucky
ID: 92
Age: 50
Pay: 100300
SSN: 333-23-2322
Number of Stock Options: 9337
Name: Fran
ID: 93
Age: 4 3
Pay: 5000
SSN: 932-32-3232
Number of Sales: 31
Переопределение виртуальных членов в Visual Studio 2010
Как вы уже, возможно, заметили, при переопределении члена класса необходимо
помнить типы всех параметров, а также соглашения о передаче параметров (ref, out и
params). В Visual Studio 2010 имеется очень полезное средство, которое можно
использовать при переопределении виртуального члена. Если набрать слово override внутри
контекста типа класса (и нажать клавишу пробела), то IntelliSense автоматически отобразит
список всех переопределяемых членов родительского класса, как показано на рис. 6.6.
После выбора члена и нажатия клавиши <Enter> среда IDE реагирует
автоматическим заполнением шаблона метода вместо вас. Обратите внимание, что также
добавляется оператор кода, который вызывает родительскую версию виртуального члена (эту
строку можно удалить, если она не нужна). Например, при использовании этой техники
во время переопределения метода DisplayStatusO добавится следующий
автоматически сгенерированный код:
public override void DisplayStats ()
{
base.DisplayStats () ;
}
248 Часть II. Главные конструкции программирования на С#
$$Employees,SalesPerson
^DtsplayStatsQ
else
{
if (nuwberOfSales >- 101 && numberOfSales <= 2вв)
salesBonus = 15;
else
salesBonus ■ 20;
}
base.GiveBonus(amount * salesBonus);
)
>
public override void DisplayStats()
{
base.DisplayStats();
Console.WriteLine("Number of Sales: {0}", SalesNumber);
)
I ♦|Equals(objectobj)
j ♦ GetHashCodeO
; ♦ ToStringO
Рис. 6.6. Быстрый просмотр переопределяемых методов в Visual Studio 2010
Запечатывание виртуальных членов
Вспомните, что ключевое слово sealed применяется к типу класса для
предотвращения расширения другими типами его поведения через наследование. Ранее класс
PTSalesPerson был запечатан на основе предположения, что разработчикам не имеет
смысла дальше расширять эту линию наследования.
Иногда требуется не запечатывать класс целиком, а просто предотвратить
переопределение некоторых виртуальных методов в производных типах. Например,
предположим, что продавцы с частичной занятостью не должны получать определенные
бонусы. Чтобы предотвратить переопределение виртуального метода GiveBonus() в
классе PTSalesPerson, можно запечатать этот метод в классе Salesperson следующим
образом:
// Salesperson запечатал метод GiveBonus()■
class Salesperson : Employee
{
public override sealed void GiveBonus(float amount)
{
}
}
Здесь Salesperson действительно переопределяет виртуальный метод GiveBonus(),
определенный в классе Employee, однако он явно помечен как sealed. Поэтому
попытка переопределения этого метода в классе PTSalesPerson приведет к ошибке во время
компиляции:
sealed class PTSalesPerson : Salesperson
{
public PTSalesPerson(string fullName, int age, int empID,
float currPay, string ssn, int numbOfSales)
:base (fullName, age, empID, currPay, ssn, numbOfSales)
}
Глава 6. Понятия наследования и полиморфизма 249
// Ошибка! Этот метод переопределять нельзя1
public override void GiveBonus(float amount)
{
}
}
Абстрактные классы
В настоящее время базовый класс Employee спроектирован так, что поставляет
различные данные-члены своим наследникам, а также предлагает два виртуальных метода
(GiveBonus() и DisplayStatusO), которые могут быть переопределены наследниками.
Хотя все это хорошо и замечательно, у данного дизайна есть один неприятный
побочный эффект: можно непосредственно создавать экземпляры базового класса Employee:
// Что это будет означать?
Employee X = new Employee() ;
В нашем примере базовый класс Employee имеет единственное назначение —
определить общие члены для всех подклассов. По всем признакам вы не намерены
позволять кому-либо создавать прямые экземпляры этого класса, поскольку тип Employee
слишком общий по своей природе. Например, если кто-то скажет: "Я сотрудник!", то тут
же возникнет вопрос: "Какой конкретно сотрудник?" (консультант, инструктор,
административный работник, редактор, советник в правительстве и т.п.).
Учитываяi что многие базовые классы склонны быть довольно неопределенными
сущностями, намного лучший дизайн для рассматриваемого примера не должен
разрешать непосредственное создание в коде нового объекта Employee. В С# можно добиться
этого с использованием ключевого слова abstract в определении класса, создавая,
таким образом, абстрактный базовый класс:
II Превращение класса Employee в абстрактный
// для предотвращения прямого создания экземпляров.
abstract partial class Employee
{
}
После этого попытка создать экземпляр класса Employee приведет к ошибке во
время компиляции:
// Ошибка! Нельзя создавать экземпляр абстрактного класса!
Employee X = new Employee();
На первый взгляд может показаться очень странным, зачем определять класс,
экземпляр которого нельзя создать непосредственно. Однако вспомните, что базовые классы
(абстрактные или нет) очень полезны тем, что содержат общие данные и общую
функциональность унаследованных типов. Используя эту форму абстракции, можно также
моделировать общую "идею" сотрудника, а не обязательно конкретную сущность. Также
следует понимать, что хотя непосредственно создать абстрактный класс нельзя, он все
же присутствует в памяти, когда создан экземпляр его производного класса. Таким
образом, совершенно нормально (и принято) для абстрактных классов определять любое
количество конструкторов, вызываемых опосредованно при размещении в памяти
экземпляров производных классов.
Теперь получилась довольно интересная иерархия сотрудников. Позднее в этой
главе, при рассмотрении правил приведения типов С#, мы добавим немного больше
функциональности к этому приложению. А пока на рис. 6.7 показан основной дизайн типов
на данный момент.
Исходный код. Проект Employees доступен в подкаталоге Chapter 6.
250 Часть II. Главные конструкции программирования на С#
Полиморфный интерфейс
Когда класс определен как абстрактный базовый (с помощью ключевого слова
abstract), в нем может определяться любое количество абстрактных членов. Абстрактные
члены могут использоваться везде, где необходимо определить член, которые не
предлагает реализации по умолчанию. За счет этого вы навязываете полиморфный интерфейс
каждому наследнику, возлагая на них задачу реализации конкретных деталей
абстрактных методов. Полиморфный интерфейс абстрактного базового класса просто ссылается на
его набор виртуальных и абстрактных методов. На самом деле это интереснее, чем может
показаться на первый взгляд, поскольку данная особенность ООП позволяет строить легко
расширяемое и гибкое программное обеспечение. Для иллюстрации реализуем (и слегка
модифицируем) иерархию фигур, кратко описанную в главе 5 при обзоре принципов ООП.
Для начала создадим новый проект консольного приложения С# по имени Shapes.
Обратите внимание на рис. 6.8, что типы Hexagon и Circle расширяют базовый
класс Shape. Подобно любому базовому классу, в Shape определен набор членов (в
данном случае свойство PetName и метод Draw()), общих для всех наследников.
Подобно иерархии классов сотрудников, нужно запретить непосредственное
создание экземпляров Shape, поскольку этот тип представляет слишком абстрактную
концепцию. Чтобы предотвратить прямое создание экземпляров Shape, можно определить
его как абстрактный класс. Также, учитывая, что производные типы должны
уникальным образом реагировать на вызов метода Draw(), давайте пометим его как virtual и
определим реализацию по умолчанию.
Employee
Abstract Class
О Fields
5 Properties
t Methods
6 Nested Types
tft
d Methods
% ComputePayDeduction
В Nested Types
BenefitPackageLevel
Erwm
Standard
Gold
Platinum
Г»
Class
•♦ Employee
Я Fields
* Properties
a Methods
Salesperson
Class
■* Employee
v.- . . .,.„,,
1
PTSafesFerson
Sealed Class
■» Salesperson
i
„„,., ,J
>
Hexagon
Class
■+ Shape
\ Abstract Class
| & Properties
^ PetName
! "Methods
:Ф Draw
! ■'♦ Shape (♦
if
IL .j
1 overload)
i
Circle
Class
■♦Shape
\
-
T
ThreeDCircle
Class
«•Circle
J
®)
Рис. 6.7. Иерархия классов Employee
Рис. 6.8. Иерархия классов фигур
Глава 6. Понятия наследования и полиморфизма 251
// Абстрактный базовый класс иерархии.
abstract class Shape
{
public Shape(string name = "NoName")
{ PetName - name; }
public string PetName { get; set; }
// Единственный виртуальный метод.
public virtual void Draw()
{
Console.WriteLine ( "Inside Shape.Draw ()") ;
}
}
Обратите внимание, что виртуальный метод Draw() предоставляет реализацию по
умолчанию, которая просто выводит ан консоль сообщение, информирующее о том, что
вызван метод Draw() базового класса Shape. Теперь вспомните, что когда метод
помечен ключевым словом virtual, он предоставляет реализацию по умолчанию, которую
автоматически наследуют все производные типы. Если дочерний класс так решит, он
может переопределить такой метод, но он не обязан это делать. Учитывая это,
рассмотрим следующую реализацию типов Circle и Hexagon:
// Circle не переопределяет Draw().
class Circle : Shape
{
public Circle () {}
public Circle(string name) : base(name) {}
}
// Hexagon переопределяет Draw().
class Hexagon : Shape
{
public Hexagon () {}
public Hexagon(string name) : base(name){}
public override void Draw()
{
Console.WriteLine("Drawing {0} the Hexagon", PetName);
}
}
Польза от абстрактных методов становится совершенно ясной, как только вы
запомните, что подклассы никогда не обязаны переопределять виртуальные методы (как в
случае Circle). Поэтому если создать экземпляр типа Hexagon и Circle,
обнаружится, что Hexagon знает, как правильно "рисовать" себя (или, по крайней мере, выводит
на консоль соответствующее сообщение). Однако реакция Circle слегка приведет в
замешательство:
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Polymorphism *****\n");
Hexagon hex = new Hexagon("Beth");
hex.Draw();
Circle cir = new Circle("Cindy");
// Вызывает реализацию базового класса1
cir.Draw () ;
Console.ReadLine();
}
Вывод этого метода Main() выглядит следующим образом:
***** pun W1th Polymorphism *****
Drawing Beth the Hexagon
Inside Shape.Draw ()
252 Насть II. Главные конструкции программирования на С#
Ясно, что это не особо интеллектуальный дизайн для текущей иерархии. Чтобы
заставить каждый класс переопределить метод Draw(), можно определить Draw() как
абстрактный метод класса Shape, а это означает отсутствие какой-либо реализации
по умолчанию. Для пометки метода как абстрактного в С# служит ключевое слово
abstract. He забывайте, что абстрактные методы не предусматривают вообще
никакой реализации:
abstract class Shape
{
// Вынудить все дочерние классы определить свою визуализацию.
public abstract void Draw();
}
На заметку! Абстрактные методы могут определяться только в абстрактных классах. Попытка
поступить иначе приводит к ошибке во время компиляции.
Методы, помеченные как abstract, являются чистым протоколом. Они просто
определяют имя, возвращаемый тип (если есть) и набор параметров (при необходимости).
Здесь абстрактный класс Shape информирует типы-наследники о том, что у него есть
метод по имени Draw(), который не принимает аргументов и ничего не возвращает.
О необходимых деталях должен позаботиться наследник.
С учетом этого метод Draw() в классе Circle теперь должен быть обязательно
переопределен. В противном случае Circle также должен быть абстрактным типом и
оснащен ключевым словом abstract (что очевидно не подходит в данном примере). Ниже
показаны необходимые изменения в коде:
// Если не реализовать здесь абстрактный метод Draw() , то Circle также
// должен считаться абстрактным, и тогда должен быть помечен как abstract!
class Circle : Shape
{
public Circle () {}
public Circle(string name) : base(name) {}
public override void Draw()
{
Console.WriteLine("Drawing {0} the Circle", PetName);
}
}
Выражаясь кратко, теперь делается предположение о том, что любой
унаследованный от Shape класс должен иметь уникальную версию метода Draw(). Для
демонстрации полной картины полиморфизма рассмотрим следующий код:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Polymorphism *****\n");
// Создать массив совместимых с Shape объектов.
Shape [] myShapes = {new Hexagon(), new Circle (), new Hexagon("Mick"),
new Circle("Beth"), new Hexagon("Linda")};
// Пройти циклом по всем элементам и взаимодействовать
//с полиморфным интерфейсом.
foreach (Shape s in myShapes)
{
s.Draw ();
}
Console.ReadLine ();
}
Глава 6. Понятия наследования и полиморфизма 253
Ниже показан вывод этого метода Main():
***** pun W1th Polymorphism *****
Drawing NoName the Hexagon
Drawing NoName the Circle
Drawing Mick the Hexagon
Drawing Beth the Circle
Drawing Linda the Hexagon
Этот метод Main () иллюстрирует использование полиморфизма в чистом виде. Хотя
невозможно напрямую создавать экземпляры абстрактного базового класса (Shape),
можно свободно сохранять ссылки на объекты любого подкласса в абстрактной базовой
переменной. Таким образом, созданный массив объектов Shape может хранить
объекты, унаследованные от базового класса Shape (попытка поместить в массив объекты,
несовместимые с Shape, приводит к ошибке во время компиляции).
Учитывая, что все элементы в массиве my Shapes действительно наследуются от
Shape, известно, что все они поддерживают один и тот же полиморфный интерфейс
(или, говоря конкретно — все они имеют метод Draw()). Выполняя итерацию по
массиву ссылок Shape, исполняющая система сама определяет, какой конкретный тип имеет
каждый его элемент. И в этот момент вызывается корректная версия метода Draw().
Эта техника также делает очень простой и безопасной задачу расширения текущей
иерархии. Например, предположим, что от абстрактного базового класса Shape
унаследовано еще пять классов (Triangle, Square и т.д.). Благодаря полиморфному
интерфейсу, код внутри цикла foreach не потребует никаких изменений, если компилятор
увидит, что в массив myShapes помещены только Shape-совместимые типы.
Сокрытие членов
Язык С# предоставляет средство, логически противоположное переопределению
методов, которое называется сокрытием (shadowing). Выражаясь формально, если
производный класс определяет член, который идентичен члену, определенному в базовом
классе, то производный класс скрывает родительскую версию. В реальном мире такая
ситуация чаще всего возникает при наследовании от класса, который создавали не вы
(и не ваша команда), например, в случае приобретения пакета программного
обеспечения .NET у независимого поставщика.
Для иллюстрации предположим, что вы получили от коллеги класс по
имени ThreeDCitcle, в котором определен метод по имени Draw(), не принимающий
аргументов:
class ThreeDCircle
{
public void Draw ()
{
Console.WriteLine("Drawing a 3D Circle");
}
}
Вы обнаруживаете, что ThreeDCircle "является" Circle, поэтому наследуете его от
существующего типа Circle:
class ThreeDCircle : Circle
{
public void Draw()
{
Console.WriteLine("Drawing a 3D Circle");
}
}
254 Насть II. Главные конструкции программирования на С#
После компиляции в окне ошибок Visual Studio 2010 появляется предупреждение
(рис. 6.9).
Рис. 6'.9. Мы только что скрыли член родительского класса
Проблема в том, что в производном классе (ThreeDCircle) присутствует метод,
идентичный унаследованному методу. Точное предупреждение компилятора в этом случае
будет таким:
'phapes.ThreeDCircle.Draw() ' hides inherited member ' Shapes.Circle.Draw()'.
To make the current member override that implementation, add the override
keyword. Otherwise add the new keyword.
'Shapes .ThreeDCircle. Draw () ' скрывает унаследованный член 'Shapes .Circle. Draw () '.
Чтобы заставить текущий член переопределить эту реализацию, добавьте ключевое
слово override. В противном случае добавьте ключевое слово new.
Существует два способа решения этой проблемы. Можно просто обновить
родительскую версию Draw(), используя ключевое слово override (как рекомендует
компилятор). При таком подходе тип ThreeDCircle может расширять родительское поведение
по умолчанию, как и требовалось. Однако если доступ к коду, определяющему базовый
класс, отсутствует (как обычно случается с библиотеками от независимых
поставщиков), то нет возможности модифицировать метод Draw(), сделав его виртуальным.
В качестве альтернативы можно добавить ключевое слово new в определение члена
Draw() производного типа (ThreeDCircle в данном случае). Делая это явно, вы
устанавливаете, что реализация производного типа преднамеренно спроектирована так,
чтобы игнорировать родительскую версию (в реальном проекте это может помочь, если
внешнее программное обеспечение .NET каким-то образом конфликтует с вашим
программным обеспечением).
// Это класс расширяет Circle и скрывает унаследованный метод Draw().
class ThreeDCircle : Circle
{
// Скрыть любую реализацию Draw() , находящуюся выше в иерархии.
public new void Draw()
{
Console.WriteLine("Drawing a 3D Circle");
}
}
Можно также применить ключевое слово new к любому члену типа, унаследованному
от базового класса (полю, константе, статическому члену или свойству). В качестве еще
одного примера предположим, что ThreeDCircle () желает скрыть унаследованное поле
shapeName:
// Этот класс расширяет Circle и скрывает унаследованный метод Draw() .
class ThreeDCircle : Circle
{
Глава 6. Понятия наследования и полиморфизма 255
// Скрыть поле shapeName, определенное выше в иерархии.
protected new string shapeName;
// Скрыть любую реализацию Draw() , находящуюся выше в иерархии.
public new void Draw()
{
Console.WriteLine("Drawing a 3D Circle");
}
}
И, наконец, имейте в виду, что всегда можно обратиться к реализации базового
класса скрытого члена, используя явное приведение (описанное в следующем разделе).
Например, это демонстрируется в следующем коде:
static void Main(string [ ] args)
{
// Здесь вызывается метод Draw() из класса ThreeDCircle.
ThreeDCircle о = new ThreeDCircle();
о.Draw();
// Здесь вызывается метод Draw() родителя!
( (Circle)о) .Draw ();
Console.ReadLine();
Исходный код. Проект Shapes доступен в подкаталоге Chapter 6.
Правила приведения к базовому
и производному классу
Теперь, когда вы научились строить семейства взаимосвязанных типов классов,
следует познакомиться с правилами, которым подчиняются операции приведения классов.
Для этого вернемся к иерархии классов Employee, созданной ранее в главе. На
платформе .NET конечным базовым классом служит System.Object. Поэтому все, что создается,
"является" Object и может трактоваться как таковой. Учитывая этот факт, в объектной
переменной можно хранить ссылку на экземпляр любого типа:
void CastingExamples ()
{
// Manager "является" System.Object, поэтому можно сохранять
// ссылку на Manager в переменной типа object.
object frank = new Manager("Frank Zappa", 9, 3000, 40000, 11-11-1111", 5);
}
В примере Employees типы Manager, Salesperson и PTSalesPerson расширяют
класс Employee, поэтому можно хранить любой из этих объектов в допустимой ссылке
на базовый класс. Это значит, что следующий код также корректен:
void CastingExamples ()
{
// Manager "является" System.Object, поэтому можно сохранять
// ссылку на Manager в переменной типа object.
object frank = new Manager("Frank Zappa", 9, 3000, 40000, 11-11-1111", 5);
// Manager также "является" Employee.
Employee moonUnit = new Manager("MoonUnit Zappa", 2, 3001, 20000, 01-11-1321", 1) ;
// PTSalesPerson "является" Salesperson.
Salesperson ]ill = new PTSalesPerson ("Jill", 834, 3002, 100000, 11-12-1119", 90);
}
256 Часть II. Главные конструкции программирования на С#
Первое правило приведения между типами классов гласит, что когда два класса
связаны отношением "является", всегда можно безопасно сохранить производный тип в
ссылке базового класса. Формально это называется неявным приведением, поскольку
оно "просто работает" в соответствии с законами наследования. Это делает возможным
построение некоторых мощных программных конструкций. Например, предположим,
что в текущем классе Program определен новый метод:
static void GivePromotion(Employee emp)
{
// Повысить зарплату...
// Предоставить место на парковке компании...
Console.WriteLine ("{0 } was promoted!", emp.Name);
}
Поскольку этот метод принимает единственный параметр типа Employee, можно
эффективно передавать этому методу любого наследника от класса Employee, учитывая
отношение "является":
static void CastingExamples ()
{
// Manager "является" System.Object, поэтому можно сохранять
// ссылку на Manager в переменной типа object.
object frank = new Manager("Frank Zappa", 9, 3000, 40000, 11-11-1111", 5);
// Manager также "является" Employee.
Employee moonUnit = new Manager("MoonUnit Zappa", 2, 3001, 20000, 01-11-1321", 1) ;
GivePromotion(moonUnit);
// PTSalesPerson "является" Salesperson.
Salesperson jill = new PTSalesPerson ("Jill", 834, 3002, 100000, 11-12-1119", 90);
GivePromotion(jill);
}
Предыдущий код компилируется, благодаря неявному приведению от типа базового
класса (Employee) к производному классу. Однако что если нужно также вызвать метод
GivePromotion () для объекта frank (хранимого в данный момент в обобщенной ссылке
System.Object)? Если вы передадите объект frank непосредственно в GivePromotion(),
как показано ниже, то получите ошибку во время компиляции:
// Ошибка!
object frank = new Manager("Frank Zappa", 9, 3000, 40000, 11-11-1111", 5) ;
GivePromotion(frank);
Проблема в том, что предпринимается попытка передать переменную, которая
является не Employee, а более общим объектом System.Object. Поскольку он находится
выше в цепочке наследования, чем Employee, компилятор не допустит неявного
приведения, стараясь обеспечить максимально возможную безопасность типов.
Несмотря на то что вы можете определить, что объектная ссылка указывает на
Employee-совместимый класс в памяти, компилятор этого сделать не может,
поскольку это не будет известно вплоть до времени выполнения. Чтобы удовлетворить
компилятор, понадобится выполнить явное приведение. Второе правило приведения гласит:
необходимо явно выполнять приведение "вниз", используя операцию приведения С#.
Базовый шаблон, которому нужно следовать при выполнении явного приведения,
выглядит примерно так:
(Класс_к_которому_нужно_привести) существующаяСсылка
Таким образом, чтобы передать переменную object методу GivePromotion(),
потребуется написать следующий код:
// Корректно!
GivePromotion((Manager)frank);
Глава 6. Понятия наследования и полиморфизма 257
Ключевое слово as
Помните, что явное приведение происходит во время выполнения, а не во время
компиляции. Поэтому показанный ниже код:
// Нет! Приводить frank к типу Hexagon нельзя, хотя код скомпилируете*!
Hexagon hex = (Hexagon)frank;
компилируется нормально, но вызывает ошибку времени выполнения, или, более
формально — исключение времени выполнения. В главе 7 будут рассматриваться
подробности структурированной обработки исключений, а пока следует лишь отметить, что при
выполнении явного приведения можно перехватывать возможные ошибки приведения,
применяя ключевые слова try и catch (см. главу 7):
// Перехват возможной ошибки приведения.
try
{
Hexagon hex = (Hexagon)frank;
}
catch (InvalidCastException ex)
{
Console.WriteLine(ex.Message);
}
Хотя это хороший пример защитного (defensive) программирования, С#
предоставляет ключевое слово as для быстрого определения совместимости одного типа с другим во
время выполнения. С помощью ключевого слова as можно определить совместимость,
проверив возвращенное значение на равенство null. Взгляните на следующий код:
// Использование as для проверки совместимости.
Hexagon hex2 = frank as Hexagon;
if (hex2 == null)
Console.WriteLine("Sorry, frank is not a Hexagon...");
Ключевое слово is
Учитывая, что метод GivePromotionO был спроектирован для приема любого
возможного типа, производного от Employee, может возникнуть вопрос — как этот метод
может определить, какой именно производный тип был ему передан? И, кстати, если
входной параметр имеет тип Employee, как получить доступ к специализированным
членам типов Salesperson и Manager?
В дополнение к ключевому слову as, в С# предлагается ключевое слово is,
которое позволяет определить совместимость двух типов. В отличие от ключевого слова
as, если типы не совместимы, ключевое слово is возвращает false, а не null-ссылку.
Рассмотрим следующую реализацию метода GivePromotionO:
static void GivePromotion(Employee emp)
{
Console.WriteLine ("{0 } was promoted!", emp.Name);
if (emp is Salesperson)
{
Console.WriteLine ("{0 } made {1} sale(s)!", emp.Name,
((Salesperson)emp).SalesNumber);
Console.WriteLine ();
}
if (emp is Manager)
{
Console.WriteLine ("{0 } had {1} stock options ..." , emp.Name,
((Manager)emp).StockOptions);
258 Часть II. Главные конструкции программирования на С#
Console.WriteLine ();
}
}
Здесь во время выполнения производится проверка, на что именно в памяти
указывает ссылка типа базового класса. Определив, что принят Salesperson или Manager,
можно применить явное приведение и получить доступ к специализированным членам
класса. Также обратите внимание, что окружать операции приведения конструкцией
try/catch не обязательно! поскольку внутри контекста if, выполнившего проверку
условия, уже известно, что приведение безопасно.
Родительский главный класс System.Object
В завершение этой главы исследуем детали устройства родительского главного
класса всей платформы .NET — Object. Возможно, вы уже заметили в предыдущих
разделах, что базовые классы всех иерархий (Car, Shape, Employee) никогда явно не
указывали свои родительские классы:
// Кто родитель Саг?
class Car
{...}
В мире .NET каждый тип в конечном итоге наследуется от базового класса по имени
System.Object (который в С# может быть представлен ключевым словом object). Класс
Object определяет набор общих членов для каждого типа в каркасе. Фактически, при
построении класса, который явно не указывает своего родителя, компилятор
автоматически наследует его от Object. Если нужно очень четко прояснить свои намерения,
можно определить класс, производный от Object, следующим образом:
// Явное наследование класса от System.Object.
class Car : object
Как и в любом другом классе, в System.Object определен набор членов. В
следующем формальном определении С# обратите внимание, что некоторые из этих членов
определены как virtual, а это говорит о том, что данный член может быть
переопределен в подклассе, в то время как другие помечены как static (и потому вызываются
только на уровне класса):
public class Object
{
// Виртуальные члены.
public virtual bool Equals(object obj ) ;
protected virtual void Finalize();
public virtual int GetHashCode() ;
public virtual string ToStringO;
// Уровень экземпляра, не виртуальные члены.
public Type GetTypeO;
protected object MemberwiseClone ();
// Статические члены.
public static bool Equals(object objA, object objB);
public static bool ReferenceEquals(object objA, object objB);
}
В табл. 6.1 приведен перечень функциональности, предоставляемой некоторыми
часто используемыми методами.
Глава 6. Понятия наследования и полиморфизма 259
Таблица 6.1. Основные методы System.Object
Метод экземпляра
Назначение
Equals ()
FinalizeO
GetHashCodeO
ToStringO
GetTypeO
MemberwiseCloneO
По умолчанию этот метод возвращает true, только если
сравниваемые элементы ссылаются в точности на один и тот же объект в
памяти. Таким образом, Equals () используется для сравнения объектных
ссылок, а не состояния объекта. Обычно этот метод
переопределяется, чтобы возвращать true, только если сравниваемые объекты
имеют одинаковые значения внутреннего состояния.
Следует отметить, что в случае переопределения Equals ()
потребуется также переопределить метод GetHashCodeO, потому что эти
методы используются внутренне типами Hashtable для извлечения
подобъектов из контейнера.
Также вспомните из главы 4, что в классе ValueType этот метод
переопределен для всех структур, чтобы он работал для сравнения на
базе значений
На данный момент можно считать, что этот метод (будучи
переопределенным) вызывается для освобождения любых размещенных
ресурсов перед удалением объекта. Сборка мусора CLR более подробно
рассматривается в главе 8
Этот метод возвращает значение int, идентифицирующее
конкретный экземпляр объекта
Этот метод возвращает строковое представление объекта, используя
формат Пространство имен>.<имя типа> (так называемое
полностью квалифицированное имя). Этот метод часто
переопределяется в подклассе для возврата строки, состоящей из пар
"имя/значение", которая представляет внутреннее состояние объекта, вместо
полностью квалифицированного имени
Этот метод возвращает объект Туре, полностью описывающий объект,
на который в данный момент производится ссылка. Коротко говоря,
это метод идентификации типа во время выполнения (Runtime Type
Identification — RTTI), доступный всем объектам (подробно обсуждается
в главе 15)
Этот метод возвращает полную (почленную) копию текущего объекта и
часто используется для клонирования объектов (см. главу 9)
Чтобы проиллюстрировать поведение по умолчанию, обеспечиваемое базовым
классом Object, создадим новое консольное приложение С# по имени ObjectOverrides.
Добавим в проект новый тип класса С#, содержащий следующее пустое определение
типа по имени Person:
// Помните: Person расширяет Object.
class Person {}
Теперь дополним метод Main() взаимодействием с унаследованными членами
System.Object, как показано ниже:
class Program
static void Main(string[] args)
{
Console.WriteLine("***** Fun with System.Object ***'
Person pi = new Person () ;
// Использовать унаследованные члены System.Object.
Console.WriteLine("ToString: {0}", pi.ToString ());
"An") ;
260 Часть II. Главные конструкции программирования на С#
Console.WriteLine ("Hash code: {0}", pi.GetHashCode());
Console.WriteLine ("Type: {0}", pi.GetType());
// Создать другую ссылку на pi.
Person p2 = pi;
object о = p2;
// Указывают ли ссылки на один и тот же объект в памяти?
if (о.Equals(pi) && р2.Equals (о) )
{
Console.WriteLine("Same instance1"); // один и тот же экземпляр
}
Console.ReadLine ();
}
}
Вывод этого метода Main() выглядит следующим образом:
••••• pun W1th System.Object *****
ToString: ObjectOverrides. Person
Hash code: 46104728
Type: ObjectOverrides. Person
Same instance!
Первым делом, обратите внимание, что реализация ToString() по умолчанию
возвращает полностью квалифицированное имя текущего типа (ObjectOverrides.Person).
Как будет показано позже, при рассмотрении построения специальных пространств
имен в главе 14, каждый проект С# определяет "корневое пространство имен",
название которого совпадает с именем проекта. Здесь мы создали проект под названием
ObjectOverrides, поэтому тип Person (как и класс Program) помещен в пространство
имен ObjectOverrides.
Поведение Equals () по умолчанию заключается в проверке того, указывают ли
две переменных на один и тот же объект в памяти. Здесь создается новая переменная
Person по имени pi. В этот момент новый объект Person помещается в память
управляемой кучи (managed heap). Переменная р2 также относится к типу Person. Однако вы
не создаете новый экземпляр, а вместо этого присваиваете этой переменной ссылку pi.
Таким образом, pi и р2 указывают на один и тот же объект в памяти, как и переменная
о (типа object). Учитывая, что pi, р2 и о указывают на одно и то же местоположение в
памяти, проверка эквивалентности дает положительный результат.
Хотя готовое поведение System.Object во многих случаях может удовлетворять всем
потребностям, довольно часто специальные типы переопределяют некоторые из этих
унаследованных методов. Для иллюстрации модифицируем класс Person, добавив
некоторые свойства, представляющие имя, фамилию и возраст лица; все они могут быть
установлены с помощью специального конструктора:
// Помните: Person расширяет Object.
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public Person(string fName, string IName, int personAge)
{
FirstName = fName;
LastName = IName;
Age = personAge;
}
public Person () {}
}
Глава 6. Понятия наследования и полиморфизма 261
Переопределение System.Object.ToStringO
Многие создаваемые классы (и структуры) выигрывают от переопределения
ToStringO для возврата строки с текстовым представлением текущего состояния
экземпляра типа. Помимо прочего, это может быть довольно полезно при отладке. Как
вы решите конструировать эту строку — дело персонального вкуса; однако
рекомендованный подход состоит в разделении двоеточиями пар "имя/значение" и взятии всей
строки в квадратные скобки (многие типы из библиотек базовых классов .NET следуют
этому принципу). Рассмотрим следующую переопределенную версию ToStringO для
нашего класса Person:
public override string ToStringO
{
string myState;
myState = string.Format("[First Name: {0}; Last Name: {1}; Age: {2}]",
FirstName, LastName, Age);
return myState;
}
Эта реализация ToStringO довольно прямолинейна, учитывая, что класс Person
состоит всего их трех фрагментов данных состояния. Однако всегда нужно помнить,
что правильное переопределение ToStringO должно также учитывать все данные,
определенные выше в цепочке наследования.
Когда вы переопределяете ToStringO для класса, расширяющего специальный
базовый класс, первое, что следует сделать — получить возврат ToStringO от
родительского класса, используя слово base. Получив строковые данные родителя, можно
добавить к ним специальную информацию производного класса.
Переопределение System.Object.Equals()
Давайте также переопределим поведение Object. Equals () для работы с
семантикой на основе значений. Вспомните, что по умолчанию Equals () возвращает true,
только если два сравниваемых объекта ссылаются на один и тот же экземпляр объекта
в памяти. Для класса Person может быть полезно реализовать Equals () для возврата
true, когда две сравниваемых переменных содержат одинаковые значения (т.е.
фамилию, имя и возраст).
Прежде всего, обратите внимание, что входной аргумент метода Equals () — это
общий System.Object. С учетом этого первое, что нужно сделать — удостовериться, что
вызывающий код действительно передал тип Person, и для дополнительной
подстраховки проверить, что входной параметр не является null-ссылкой.
Установив, что передан размещенный Person, один подход состоит в реализации
Equals () для выполнения сравнения поле за полем данных входного объекта с
соответствующими данными текущего объекта:
public override bool Eguals(object obj)
{
if (obj is Person && ob] != null)
{
Person temp;
temp = (Person)obj;
if (temp.FirstName == this.FirstName
&& temp.LastName == this.LastName
&& temp.Age == this.Age)
{
return true;
}
262 Часть II. Главные конструкции программирования на С#
else
{
return false;
}
}
return false;
}
Здесь производится сравнение значения входного объекта с внутренними
значениями текущего объекта (обратите внимание на применение ключевого слова this). Если
имя, фамилия и возраст, записанные в двух объектах, идентичны, значит, есть два
объекта с одинаковыми данными, и потому возвращается true. Любые другие возможные
результаты возвратят false.
Хотя этот подход действительно работает, представьте, насколько трудоемкой была
бы реализация специального метода Equals () для нетривиальных типов, которые могут
содержать десятки полей данных. Распространенным сокращением является
использование собственной реализации ToStringO. Если у класса имеется правильная
реализация ToStringO, которая учитывает все поля данных вверх по цепочке наследования,
можно просто сравнить строковые данные объектов:
public override bool Equals(object obj)
{
// Больше нет необходимости приводить obj к типу Person,
// поскольку у всех имеется метод ToStringO .
return obj .ToStringO == this .ToString () ;
}
Обратите внимание, что в этом случае нет необходимости проверять входной
аргумент на принадлежность к корректному типу (в нашем примере — Person), поскольку
все классы в .NET поддерживают метод ToStringO. Еще лучше то, что больше не нужно
выполнять проверку равенства свойства за свойством, поскольку теперь просто
проверяются значения, возвращенные методом ToStringO.
Переопределение System.Object. GetHashCodeO
Когда класс переопределяет метод Equals О, вы также обязаны переопределить
реализацию по умолчанию GetHashCode(). Говоря упрощенно, хеш-код — это числовое
значение, представляющее объект как определенное состояние. Например, если
созданы две переменных string, хранящие значение Hello, они должны давать один и тот
же хеш-код. Однако если одна переменная string хранит строку в нижнем регистре
(hello), должны быть получены разные хеш-коды.
По умолчанию System.Object.GetHashCodeO использует текущее местоположение
объекта в памяти для порождения хеш-значения. Тем не менее, при построении
специального типа, который нужно хранить в коллекции Hashtable (из пространства имен
System.Collections), этот член должен быть всегда переопределен, поскольку Hashtable
внутри вызывает Equals () HGetHashCodeO, чтобы извлечь правильный объект.
На заметку! Точнее говоря, класс System.Collections.Hashtable внутренне вызывает
метод GetHashCodeO для получения общего представления местоположения объекта, но
последующий вызов Equals () определяет точное соответствие.
Хотя мы не собираемся помещать Person в System.Collections.Hashtable, для
полноты давайте переопределим GetHashCodeO. Существует немало алгоритмов,
которые могут применяться для создания хеш-кода, одни из которых причудливы, а
другие — не очень. В большинстве случаев можно сгенерировать значение хеш-кода,
полагаясь на реализацию System.String.GetHashCode().
Глава 6. Понятия наследования и полиморфизма 263
Исходя из того, что класс String уже имеет солидный алгоритм хеширования,
использующий символьные данные String для сравнения хеш-значений, если вы можете
идентифицировать часть данных полей класса, которая должна быть уникальной для
всех экземпляров (вроде номера карточки социального страхования), просто
вызовите GetHashCodeO на этой части полей данных. Поскольку в классе Person определено
свойство SSN, можно написать следующий код:
// Вернуть хеш-код на основе уникальных строковых данных.
public override int GetHashCodeO
{
return this.ToString () .GetHashCode();
}
Если же выбрать уникальный строковый элемент данных затруднительно, но есть
переопределенный метод ToString(), вызовите GetHashCodeO на собственном
строковом представлении:
// Возвращает хеш-код на основе значения ToString () персоны,
public override int GetHashCodeO
{
return this.ToString () .GetHashCode();
}
Тестирование модифицированного класса Person
Теперь, когда виртуальные члены Object переопределены, давайте обновим Main О
для добавления проверки внесенных изменений.
static void Main(string [ ] args)
{
Console.WriteLine("***** Fun with System.Object *****\n");
// ПРИМЕЧАНИЕ: эти объекты идентичны для проверки
// методов Equals () и GetHashCodeO .
Person pi = new Person("Homer", "Simpson", 50);
Person p2 = new Person("Homer", "Simpson", 50);
// Получить строковые версии объектов.
Console.WriteLine("pi.ToString () = {0}", pi.ToString ());
Console.WriteLine("p2.ToString () = {0}", p2.ToString ());
// Проверить переопределенный метод Equals ().
Console.WriteLine ("pi = p2?: {0}", pi.Equals(p2));
// Проверить хеш-коды.
Console.WriteLine ("Same hash codes?: {0}", pi.GetHashCode () == p2.GetHashCode ()) ;
Console.WriteLine();
// Изменить возраст р2 и проверить снова.
p2.Age =45;
Console.WriteLine ("pi.ToStringO = {0}", pi. ToString ()) ;
Console.WriteLine("p2.ToString () = {0}", p2.ToString());
Console.WriteLine ("pi = p2?: {0}", pi.Equals(p2));
Console.WriteLine("Same hash codes?: {0}", pi.GetHashCode() == p2.GetHashCode());
Console.ReadLine ();
}
Ниже показан вывод:
***** Fun with System.Object *****
pi.ToString () = [First Name: Homer; Last Name: Simpson; Age: 50]
p2.ToString () = [First Name: Homer; Last Name: Simpson; Age: 50]
pi = p2?: True
Same hash codes?: True
264 Часть II. Главные конструкции программирования на С#
pi.ToString () = [First Name: Homer; Last Name: Simpson; Age: 50]
p2. ToString () = [First Name: Homer; Last Name: Simpson; Age: 45]
pi = p2?: False
Same hash codes?: False
Статические члены System.Object
В дополнение к только что рассмотренным членам уровня экземпляра, в System.
Object также определены два очень полезных статических члена, которые проверяют
эквивалентность на основе значений или на основе ссылок. Рассмотрим следующий
код:
static void StaticMembersOfObject ()
{
// Статические члены System.Object.
Person рЗ = new Person("Sally", "Jones", 4);
Person p4 = new Person("Sally", "Jones", 4);
Console.WriteLine ("P3 and P4 have same state: {0}", object.Equals(p3, p4));
Console.WriteLine ("P3 and P4 are pointing to same object: {0}",
object.ReferenceEquals(p3, p4) ) ;
}
Здесь можно просто передать два объекта (любого типа) и позволить классу System.
Object автоматически определить детали. Эти методы могут быть очень полезны при
переопределении эквивалентности для специального типа, когда нужно сохранить
возможность быстрого выяснения того, указывают ли две ссылочных переменных на одно
и то же местоположение в памяти (через статический метод ReferenceEquals ()).
Исходный код. Проект ObjectOverrides доступен в подкаталоге Chapter 6.
Резюме
В этой главе рассматривалась роль и подробности наследования и полиморфизма.
Были представлены многочисленные новые ключевые слова и лексемы для поддержки
каждой этой техники. Например, вспомните, что двоеточие применяется для
установки родительского класса для заданного типа. Родительские типы могут определять
любое количество виртуальных и/или абстрактных членов для установки полиморфного
интерфейса. Производные типы переопределяют эти члены, используя ключевое слово
override.
В дополнение к построению многочисленных иерархий классов, в главе также
рассматривалось явное приведение между базовым и производным типом. Кроме того,
было дано описание главного класса среди всех родительских типов библиотеки
базовых классов .NET— System.Object.
ГЛАВА 7
Структурированная
обработка исключений
В настоящей главе речь пойдет о способах обработки аномалий, возникающих во
время выполнения, в коде на С# с применением методики так называемой
структурированной обработки исключений (structured exception handling — SEH). Здесь
будут описаны не только ключевые слова в С#, которые предназначены для этого (try,
catch, throw, finally), но и отличия исключений уровня приложения и системы, а
также роль базового класса System. Exception. Вдобавок будет показано, как создавать
специальные исключения, и рассмотрены инструменты, доступные в Visual Studio 2010
для выполнения отладки.
Ода ошибкам и исключениям
Чтобы не нашептывало наше (порой раздутое) эго, ни один программист не
идеален. Написание кода программного обеспечения является сложным делом, и из-за этой
сложности довольно часто даже самые лучшие программы поставляются с различными,
так сказать, проблемами. В одних случаях причиной этих проблем служит "плохо
написанный" код (например, в нем происходит выход за пределы массива), а в других — ввод
пользователями неправильных данных, которые не были предусмотрены в коде
приложения (например, приводящий к присваиванию полю для ввода телефонного номера
значения вроде "Списку").
Что бы ни служило причиной проблем, в конечном итоге приложение начинает
работать не так, как ожидается. Прежде чем переходить к рассмотрению
структурированной обработки исключений, давайте сначала ознакомимся с тремя наиболее часто
применяемыми для описания аномалий терминами.
• Программные ошибки (bugs). Так обычно называются ошибки, которые
допускает программист. Например, предположим, что приложение создается с помощью
неуправляемого языка C++. Если динамически выделяемая память не
освобождается, что чревато утечкой памяти, появляется программная ошибка.
• Пользовательские ошибки (user errors). В отличие от программных ошибок,
пользовательские ошибки обычно возникают из-за тех, кто запускает приложение, а
не тех, кто его создает. Например, ввод конечным пользователем в текстовом поле
неправильно оформленной строки может привести к генерации ошибки
подобного рода, если в коде не была предусмотрена возможность обработки
некорректного ввода.
266 Часть II. Главные конструкции программирования на С#
• Исключения (exceptions). Исключениями, или исключительными ситуациями,
обычно называются аномалии, которые могут возникать во время выполнения
1 и которые трудно, а порой и вообще невозможно, предусмотреть во время
программирования приложения. К числу таких возможных исключений относятся
попытки подключения к базе данных, которой больше не существует, попытки
открытия поврежденного файла или попытки установки связи с машиной,
которая в текущий момент находится в автономном режиме. В каждом из этих
случаев программист (и конечный пользователь) мало что может сделать с подобными
"исключительными" обстоятельствами.
По приведенным выше описаниям должно стать понятно, что структурированная
обработка исключений в .NET представляет собой методику, предназначенную для
работы с исключениями, которые могут возникать на этапе выполнения. Даже в случае
программных и пользовательских ошибок, которые ускользнули от глаз программиста,
однако, CLR будет часто автоматически генерировать соответствующее исключение с
описанием текущей проблемы. В библиотеках базовых классов .NET определено
множество различных исключений, таких как FormatException, IndexOutOfRangeException,
FileNotFoundException, ArgumentOutOfRangeException и т.д.
В терминологии .NET под "исключением" подразумеваются программные ошибки,
пользовательские ошибки и ошибки времени выполнения, несмотря на то, что мы,
программисты, можем считать каждый из этих видов ошибок совершенно отдельным
типом проблем. Прежде чем погружаться в детали, давайте посмотрим, какую роль
играет структурированная обработка исключений, и чем она отличается от традиционных
методик обработки ошибок.
На заметку! Чтобы упростить примеры кода, абсолютно все исключения, которые может выдавать
тот или иной метод из библиотеки базовых классов, перехватываться не будут. В реальных
проектах следует поступать согласно существующим требованиям.
Роль обработки исключений в .NET
До появления .NET обработка ошибок в среде операционной системы Windows
представляла собой весьма запутанную смесь технологий. Многие программисты
включали собственную логику обработки ошибок в контекст интересующего приложения.
Например, команда разработчиков могла определять набор числовых констант для
представления известных сбойных ситуаций и затем применять эти константы в
качестве возвращаемых значений методов. Для примера рассмотрим следующий фрагмент
кода на языке С.
/* Типичный механизм отлавливания ошибок в С. */
#define E_FILENOTFOUND 1000
int SomeFunction ()
{
// Предполагаем, что в этой функции происходит нечто
// такое, что приводит к возврату следующего значения.
return E_FILENOTFOUND/
}
void main ()
{
int retVal = SomeFunction ();
if(retVal == E_FILENOTFOUND)
printf ("Cannot find file...");
// He удается найти файл.. .
}
Глава 7. Структурированная обработка исключений 267
Такой подход далеко не идеален из-за того факта, что константа EFILENOTFOUND
представляет собой не более чем просто числовое значение, но уж точно не агента,
способного помочь в решении проблемы. В идеале хотелось бы, чтобы название ошибки,
сообщение с ее описанием и другой полезной информацией подавалось в одном удобном
пакете (что как раз и происходит в случае применения структурированной обработки
исключений).
Помимо приемов, изобретаемых самими разработчиками, в API-интерфейсе
Windows определены сотни кодов ошибок с помощью #define и HRESULT, а также
множество вариаций простых булевских значений (bool, BOOL, VARIANTBOOL и т.д.).
Более того, многие разработчики СОМ-приложений на языке C++ (а также VB 6) явно
или неявно применяют небольшой набор стандартных СОМ-интерфейсов (наподобие
ISupportErrorlnfo, IErrorlnfo или ICreateErrorlnfо) для возврата СОМ-клиенту
понятной информации об ошибках.
Очевидная проблема со всеми этими более старыми методиками — отсутствие
симметрии. Каждая из них более-менее вписывается в рамки какой-то одной технологии,
одного языка и, пожалуй, даже одного проекта. Чтобы положить конец всему этому
безумству, в .NET была предложена стандартная методика для генерации и выявления
ошибок в исполняющей среде, называемая структурированной обработкой исключений
(SEH).
Прелесть этой методики состоит в том, что она позволяет разработчикам
использовать в области обработки ошибок унифицированный подход, который является общим
для всех языков, ориентированных на платформу .NET. Благодаря этому, программист
на С# может обрабатывать ошибки почти таким же с синтаксической точки зрения
образом, как и программист на VB и программист на C++, использующий C++/CLI.
Дополнительное преимущество состоит в том, что синтаксис, который требуется
применять для генерации и перехвата исключений за пределами сборок и машин, тоже
выглядит идентично. Например, при написании на С# службы Windows Communication
Foundation (WCF) генерировать исключение SOAP для удаленного вызывающего кода
можно с использованием тех же ключевых слов, которые применяются для генерации
исключения внутри методов в одном и том же приложении.
Еще одно преимущество механизма исключений .NET состоит в том, что в
отличие от запутанных числовых значений, просто обозначающих текущую проблему, они
представляют собой объекты, в которых содержится читабельное описание проблемы,
а также детальный снимок стека вызовов на момент, когда изначально возникло
исключение. Более того, конечному пользователю можно предоставлять справочную ссылку,
которая указывает на определенный URL-адрес с описанием деталей ошибки, а также
специальные данные, определенные программистом.
Составляющие процесса обработки исключений в .NET
Программирование со структурированной обработкой исключений подразумевает
использование четырех следующих связанных между собой сущностей:
• тип класса, который представляет детали исключения;
• член, способный генерировать (throw) в вызывающем коде экземпляр класса
исключения при соответствующих обстоятельствах;
• блок кода на вызывающей стороне, ответственный за обращение к члену, в
котором может произойти исключение;
• блок кода на вызывающей стороне, который будет обрабатывать (или
перехватывать (catch)) исключение в случае его возникновения.
268 Часть II. Главные конструкции программирования на С#
При генерации и обработке исключений в С# используются четыре ключевых слова
(try, catch, throw и finally). Любой объект, отражающий обрабатываемую
проблему, должен обязательно представлять собой класс, унаследованный от базового класса
System. Exception (или от какого-то его потомка). По этой причине давайте сначала
рассмотрим роль этого базового класса в обработке исключений.
Базовый класс System.Exception
Все определяемые на уровне пользователя и системы исключения в конечном
итоге всегда наследуются от базового класса System.Exception, который, в свою очередь,
наследуется от класса System. Object. Ниже показано, как в целом выглядит этот класс
(обратите внимание, что некоторые его члены являются виртуальными и,
следовательно, могут переопределяться в производных классах):
public class Exception : ISenalizable, _Exception
{
// Общедоступные конструкторы.
public Exception(string message, Exception innerException);
public Exception(string message);
public Exception ();
// Методы.
public virtual Exception GetBaseException ();
public virtual void GetObjectData(Serializationlnfо info,
StreamingContext context);
// Свойства.
public virtual IDictionary Data { get; }
public virtual string HelpLink { get; set; }
public Exception InnerException { get; }
public virtual string Message { get; }
public virtual string Source { get; set; }
public virtual string StackTrace { get; }
public MethodBase TargetSite { get; }
}
Нетрудно заметить, что многие из содержащихся в System.Exception свойств
являются по своей природе доступными только для чтения. Это объясняется тем, что для
каждого из них значения, используемые по умолчанию, обычно поставляются в
производных классах. Например, в производном классе IndexOutOfRangeException
поставляется сообщение по умолчанию "Index was outside the bounds of the array" ("Индекс
вышел за границы массива").
На заметку! В классе Exception реализованы два интерфейса .NET. Хотя интерфейсы подробно
рассматриваются в главе 9, сейчас главное понять, что интерфейс _Exception позволяет
сделать так, чтобы исключение .NET обрабатывалось неуправляемым кодом (таким как
приложение СОМ), а интерфейс ISerializable — чтобы объект исключения сохранялся за
пределами границ (например, границ машины).
В табл. 7.1 приведено краткое описание некоторых наиболее важных.свойств класса
System.Exception.
Глава 7. Структурированная обработка исключений 269
Таблица 7.1. Ключевые свойства System.Exception
Свойство
Описание
Data
HelpLink
InnerException
Message
Source
StackTrace
TargetSite
Это свойство, доступное только для чтения, позволяет извлекать
коллекцию пар "ключ/значение" (представленную объектом, реализующим
интерфейс iDictionary), которая предоставляет дополнительную
определяемую программистом информацию об исключении. По умолчанию эта
коллекция является пустой
Это свойство позволяет получать или устанавливать URL-адрес, по которому
доступен справочный файл или веб-сайт с детальным описанием ошибки
Это свойство, доступное только для чтения, может применяться для
получения информации о предыдущем исключении или исключениях, которые
послужили причиной возникновения текущего исключения. Запись
предыдущих исключений осуществляется путем их передачи конструктору
самого последнего исключения
Это свойство, доступное только для чтения, возвращает текстовое
описание соответствующей ошибки. Само сообщение об ошибке задается в
передаваемом конструктору параметре
Это свойство позволяет получать или устанавливать имя сборки или
объекта, который привел к выдаче исключения
Это свойство, доступное только для чтения, содержит строку с описанием
последовательности вызовов, которая привела к возникновению
исключения. Как нетрудно догадаться, это свойство очень полезно во время
отладки или для сохранения информации об ошибке во внешнем журнале
ошибок
Это свойство, доступное только для чтения, возвращает объект
MethodBase с описанием многочисленных деталей метода, который
привел к выдаче исключения (вызов вместе с ним ToString () позволяет
идентифицировать этот метод по имени)
Простейший пример
Для иллюстрации пользы от структурированной обработки исключений
необходимо создать класс, который будет выдавать исключение при надлежащих (или,
можно сказать, исключительных) обстоятельствах. Создадим новый проект типа Console
Application (Консольное приложение) на С# по имени SimpleException и определим в
нем два класса (Саг (автомобиль) и Radio (радиоприемник)), связав их между собой
отношением принадлежности ("has-a"). В классе Radio определим единственный метод,
отвечающий за включение и выключение радиоприемника:
class Radio
public void TurnOn(bool on)
{
if(on)
Console .WriteLme ("Jamming. ..") ; // работает
else
Console.WriteLine("Quiet time..."); // отключен
}
}
В классе Car (показанном ниже) помимо использования класса Radio через
принадлежность/делегирование, сделаем так, чтобы в случае превышения объектом Саг пре-
270 Часть II. Главные конструкции программирования на С#
допределенной максимальной скорости (отражаемой с помощью константы экземпляра
MaxSpeed) двигатель выходил из строя, приводя его в нерабочее состояние (отражаемое
приватной переменной экземпляра bool по имени carlsDead). Кроме того, включим в
Саг свойства для представления текущей скорости и указанного пользователем
"дружественного названия" автомобиля и различные конструкторы для установки состояния
нового объекта Саг. Ниже приведено полное определение Саг вместе с поясняющими
комментариями.
public class Car
{
/ / Константа, отражающая допустимую максимальную скорость.
public const int MaxSpeed = 100;
// Свойства автомобиля.
public int CurrentSpeed {get; set;}
public string PetName {get; set;}
//He вышел ли автомобиль из строя?
private bool carlsDead;
//В автомобиле есть радиоприемник.
private Radio theMusicBox = new Radio();
// Конструкторы.
public Car () { }
public Car(string name, int speed)
{
CurrentSpeed = speed;
PetName = name;
}
public void CrankTunes(bool state)
{
// Запрос делегата к внутреннему объекту.
theMusicBox.TurnOn(state);
}
// Проверка, не перегрелся ли автомобиль.
public void Accelerate (int delta)
{
if (carlsDead)
Console.WriteLine ("{0} is out of order...", PetName); // вышел из строя
else
{
CurrentSpeed += delta;
if (CurrentSpeed > MaxSpeed)
{
Console.WriteLine ("{0 } has overheated1", PetName); // перегрелся
CurrentSpeed = 0;
carlsDead = true;
}
else
// Вывод текущей скорости.
Console.WriteLine("=> CurrentSpeed = {0}", CurrentSpeed);
}
}
}
Теперь реализуем метод Main (), в котором объект Car будет превышать заданную
максимальную скорость (установленную равной 100 в классе Саг), как показано ниже:
Глава 7. Структурированная обработка исключений 271
static void Main(string [ ] args)
{
Console.WriteLine("***** simple Exception Example ****+");
Console.WriteLine("=> Creating a car and stepping on it1");
Car myCar = new Car ( "Zippy" , 20);
myCar.CrankTunes(true);
for (int i = 0; i < 10; i++)
myCar.AccelerateA0);
Console.ReadLine();
}
Вьгоод будет выглядеть следующим образом:
***** Simple Exception Example *****
=> Creating a car and stepping on it!
Jamming...
=> CurrentSpeed = 30
=> CurrentSpeed = 40
=> CurrentSpeed = 50
=> CurrentSpeed = 60
=> CurrentSpeed = 70
=> CurrentSpeed = 80
=> CurrentSpeed = 90
=> CurrentSpeed = 100
Zippy has overheated!
Zippy is out of order. . .
Генерация общего исключения
Имея функционирующий класс Саг, давайте рассмотрим простейший способ
генерации исключения. Текущая реализация Accelerate () предусматривает просто
отображение сообщения об ошибке, когда предпринимается попытка разогнать автомобиль
(объект Саг) до скорости, превышающей максимальный предел.
Для изменения этого метода так, чтобы при попытке разогнать автомобиль до
скорости, превышающий установленный в классе Саг предел, генерировалось исключение,
потребуется создать и сконфигурировать новый экземпляр класса System.Exception и
установить значение доступного только для чтения свойства Message через конструктор
класса. Чтобы объект ошибки отправлялся обратно вызывающей стороне, в С#
используется ключевое слово throw. Ниже показан код модифицированного метода Accelerate ().
//На этот раз, в случае превышения пользователем указанного
//в MaxSpeed предела должно генерироваться исключение.
public void Accelerate(int delta)
{
if (carlsDead)
Console.WriteLine ("{0 } is out of order...", PetName);
else
{
CurrentSpeed += delta;
if (CurrentSpeed >= MaxSpeed)
{
carlsDead = true;
CurrentSpeed = 0;
// Использование ключевого слова throw для генерации исключения.
throw new Exception(string.Format("{0} has overheated!", PetName));
}
else
Console.WriteLine("=> CurrentSpeed = {0}", CurrentSpeed);
}
}
272 Часть II. Главные конструкции программирования на С#
Прежде чем переходить к рассмотрению перехвата данного исключения в
вызывающем коде, необходимо отметить несколько интересных моментов. При генерации
исключения то, как будет выглядеть ошибка и когда она должна выдаваться, решает
программист. В рассматриваемом примере предполагается, что при попытке увеличить
скорость автомобиля (объекта Саг), который уже вышел из строя, должен
генерироваться объект System. Exception для уведомления о том, что метод Accelerate () не
может быть продолжен (это предположение может оказаться как подходящим, так и нет, в
зависимости от создаваемого приложения).
В качестве альтернативы метод Accelerate () можно было бы реализовать и так,
чтобы он производил автоматическое восстановление, не выдавая перед этим никакого
исключения. По большому счету, исключения должны генерироваться только в случае
возникновения более критичных условий (например, отсутствии нужного файла,
невозможности подключиться к базе данных и т.п.). Принятие решения о том, что должно
служить причиной генерации исключения, требует серьезного продумывания и поиска
веских оснований на стадии проектирования. Для преследуемых сейчас целей давайте
считать, что попытка увеличить скорость неисправного автомобиля является вполне
оправданной причиной для выдачи исключения.
Перехват исключений
Поскольку теперь метод Accelerate () способен генерировать исключение,
вызывающий код должен быть готов обработать его, если оно вдруг возникнет. При
вызове метода, который может генерировать исключение, должен использоваться блок
try/catch. После перехвата объекта исключения можно вызывать различные его
члены и извлекать детальную информацию о проблеме.
Что делать с этими деталями дальше по большей части нужно решать
самостоятельно. Может возникнуть желание занести их в специальный файл отчета, записать в
журнал событий Windows, отправить по электронной почте системному
администратору или отобразить конечному пользователю. Давайте для простоты выведем их в окне
консоли.
// Обработка сгенерированного исключения.
static void Main(string[] args)
{
Console.WriteLine ("***** Simple Exception Example *****");
Console.WriteLine("=> Creating a car and stepping on it!");
Car myCar = new Car ("Zippy", 20);
myCar.CrankTunes(true);
// Разгон до скорости, превышающей максимальный
// предел автомобиля, для выдачи исключения.
try
{
for(int 1 = 0; i < 10; i++)
myCar.AccelerateA0);
}
catch (Exception e)
{
Console.WriteLine ("\n*** Error! ***"); // ошибка
Console.WriteLine("Method: {0}", e.TargetSite); // метод
Console.WriteLine("Message: {0}", e.Message); // сообщение
Console.WriteLine("Source: {0}", e.Source); // источник
}
// Ошибка была обработана, продолжается выполнение следующего оператора.
Console.WriteLine("\n***** Out of exception logic *****");
Console . ReadLme () ;
}
Глава 7. Структурированная обработка исключений 273
По сути, блок try представляет собой раздел операторов, которые в ходе выполнения
могут выдавать исключение. Если обнаруживается исключение, управление переходит
к соответствующему блоку catch. С другой стороны, в случае, если код внутри блока
try не приводит к генерации исключения, блок catch полностью пропускается, и все
проходит "гладко". Ниже показано, как будет выглядеть вывод в результате тестового
выполнения данной программы.
•ж*** Simple Exception Example *****
=> Creating a car and stepping on it!
Jamming...
=> CurrentSpeed = 30
=> CurrentSpeed = 40
=> CurrentSpeed = 50
=> CurrentSpeed = 60
=> CurrentSpeed = 70
=> CurrentSpeed = 80
=> CurrentSpeed = 90
*** Error! ***
Method: Void Accelerate(Int32)
Message: Zippy has overheated!
Source: SimpleException
***** out of exception logic *****
Как здесь видно, после обработки исключения приложение может продолжать свою
работу с того оператора, который идет сразу после блока catch. В некоторых случаях
исключение может оказаться достаточно серьезным и стать причиной для завершения
работы приложения. Чаще всего, однако, логика внутри обработчика исключений
позволяет приложению спокойно продолжать работу (хотя, возможно, и менее
функциональным образом, например, без возможности устанавливать соединение с
каким-нибудь удаленным источником данных).
Конфигурирование состояния исключения
В настоящий момент объект System. Exception, сконфигурированный в методе
Accelerate (), просто устанавливает значение, предоставляемое свойству Message
(через параметр конструктора). Как показывалось ранее в табл. 7.1, в классе Exception
доступно множество дополнительных членов (TargetSite, StackTrace, HelpLink и
Data), которые могут помочь еще больше уточнить природу проблемы. Чтобы
усовершенствовать текущий пример, давайте рассмотрим возможности каждого из этих
членов более подробно.
Свойство TargetSite
Свойство System.Exception .TargetSite позволяет получать различные детали о
методе, в котором было сгенерировано данное исключение. Как было показано в
предыдущем методе Main () , вывод значения свойства TargetSite приводит к
отображению возвращаемого значения, имени и параметров выдавшего исключение
метода. Вместо простой строки свойство TargetSite возвращает строго типизированный
объект System.Reflection.MethodBase. Объект такого типа может применяться для
сбора многочисленных деталей, связанных с проблемным методом, а также классом, в
котором он содержится. Для примера изменим предыдущую логику в блоке catch
следующим образом:
static void Main(string[] args)
{
274 Часть II. Главные конструкции программирования на С#
// Свойство TargetSite на самом деле
// возвращает объект MethodBase.
catch (Exception e)
{
Console. WriteLme ("\n*** Error1 ***");
Console .WriteLme ("Member name: {0}", e . TargetSite) ; // имя члена
Console .WriteLme ("Class defining member: {0}",
e.TargetSite.DeclaringType); // класс, определяющий член
Console.WriteLme ("Member type: {0}",
e.TargetSite.MemberType); // тип члена
Console .WriteLme ("Message: {0}", e.Message); // сообщение
Console .WriteLme ("Source : {0}", e.Source); // источник
}
Console .WriteLme (" \n***** Out of exception logic •** + **");
Console.ReadLine();
}
На этот раз в коде с помощью свойства MethodBase . DeclaringType получается
полностью определенное имя выдавшего ошибку класса (в данном случае SimpleException.
Car), а с помощью свойства Member Type объекта MethodBase выясняется тип члена
(свойство или метод), в котором возникло исключение. Ниже показано, как теперь будет
выглядеть вывод в результате выполнения логики в блоке catch.
*** Error! ***
Member name: Void Accelerate(Int32)
Class defining member: SimpleException.Car
Member type: Method
Message: Zippy has overheated!
Source: SimpleException
Свойство StackTrace
Свойство System. Exception. StackTrace позволяет определить последовательность
вызовов, которая привела к возникновению исключения. Значение этого свойства
никогда самостоятельно не устанавливается — это делается автоматически во время
создания исключения. Чтобы проиллюстрировать это, модифицируем логику в блоке catch
следующим образом:
catch(Exception e)
{
Console .WriteLme ("Stack: {0}", e . StackTrace) ; // вывод стека
}
Если теперь снова запустить программу, можно будет увидеть в окне консоли
следующие данные трассировки стека (номера строк и пути к файлам, конечно же, на
разных машинах выглядят по-разному):
Stack: at SimpleException.Car.Accelerate (Int32 delta)
in c:\MyApps\SimpleException\car.cs:line 65 at SimpleException.Program.Main()
in с:\MyApps\SimpleException\Program.cs:line 21
Строка, возвращаемая из StackTrace, отражает последовательность вызовов,
которая привела к выдаче данного исключения. Обратите внимание, что самый нижний
номер в этой строке указывает на место возникновения первого вызова в
последовательности, а самый верхний — на место, где точно находится породивший проблему член.
Очевидно, что такая информация очень полезна при выполнении отладки или
просмотре журналов в конкретном приложении, поскольку позволяет прослеживать весь путь,
приведший к возникновению ошибки.
Глава 7. Структурированная обработка исключений 275
СВОЙСТВО HelpLink
Хотя свойства Targetsite и StackTrace позволяют программистам понять, почему
возникло то или иное исключение, пользователям выдаваемая ими информация мало
что дает. Как уже показывалось ранее, для получения удобной для человеческого
восприятия и потому пригодной для отображения конечному пользователю информации
может применяться свойство System.Exception .Message. Кроме него, также может
использоваться свойство HelpLink, которое позволяет направить пользователя на
конкретный URL-адрес или стандартный справочный файл Windows, где содержатся более
детальные сведения о возникшей проблеме.
По умолчанию значением свойства HelpLink является пустая строка. Присваивание
этому свойству какого-то более интересного значения должно делаться перед
генерацией исключения типа System.Exception. Чтобы посмотреть, как это делается, изменим
метод Car .Accelerate () следующим образом:
public void Accelerate (int delta)
{
if (carlsDead)
Console.WriteLine ("{0} is out of order...", PetName);
else
{
CurrentSpeed += delta;
if (CurrentSpeed >= MaxSpeed)
{
carlsDead = true;
CurrentSpeed = 0;
// Создание локальной переменной перед
// выдачей объекта Exception для получения
// возможности обращения к свойству HelpLink.
Exception ex =
new Exception(string.Format("{0} has overheated!", PetName));
ex.HelpLink = "http://www.CarsRUs.com";
throw ex;
}
else
// Вывод текущей скорости
Console.WriteLine("=> CurrentSpeed = {0}", CurrentSpeed);
}
}
Теперь можно модифицировать логику в блоке catch так, чтобы информация из
данного свойства HelpLink выводилась в окне консоли:
catch(Exception e)
{
// Ссылка для справки
Console.WriteLine("Help Link: {0}", e.HelpLink);
}
Свойство Data
Доступное в классе System.Exception свойство Data позволяет заполнять объект
исключения соответствующей вспомогательной информацией (например, датой и
временем возникновения исключения). Оно возвращает объект, реализующий интерфейс
по имени IDictionary, который определен в пространстве имен System.Collections.
В главе 9 более подробно рассматривается программирование с использованием интер-
276 Часть II. Главные конструкции программирования на С#
фейсов, а также пространство имен System.Collections. На данный момент важно
понять лишь то, что коллекции типа словарей позволяют создавать наборы значений,
извлекаемых по ключу. Модифицируем метод Car .Accelerate (), как показано ниже:
public void Accelerate(int delta)
{
if (carlsDead)
Console.WriteLine ("{0} is out of order...", PetName);
else
{
CurrentSpeed += delta;
if (CurrentSpeed >= MaxSpeed)
{
carlsDead = true;
CurrentSpeed = 0;
// Создание локальной переменной перед выдачей
// объекта Exception для обращения к свойству HelpLink.
Exception ex =
new Exception(string.Format("{0} has overheated!", PetName));
ex.HelpLink = "http://www.CarsRUs.com";
// Вставка специальных дополнительных данных,
// имеющих отношение к ошибке.
ex.Data.Add("TimeStamp",
string.Format("The car exploded at {0}", DateTime.Now));
// дата и время
ex.Data.Add("Cause", "You have a lead foot."); // причина
throw ex;
}
else
Console.WriteLine ("=> CurrentSpeed = {0}", CurrentSpeed);
}
}
Для успешного перечисления пар "ключ/значение" необходимо не забыть
сослаться на пространство имен System.Collections с помощью директивы using,
поскольку будет использоваться тип DictionaryEntry в файле с классом, реализующем метод
Main ():
using System.Collections;
Затем потребуется обновить логику catch так, чтобы в ней выполнялась проверка
на предмет того, не равно ли null значение свойства Data (null является значением по
умолчанию). После этого остается только воспользоваться свойствами Key и Value типа
DictionaryEntry для вывода специальных данных в окне консоли.
catch (Exception e)
{
//По умолчанию поле данных является пустым, поэтому
// выполняется проверка на предмет равенства null.
Console.WriteLine ("\n-> Custom Data:");
if (e.Data != null)
{
foreach (DictionaryEntry de in e.Data)
Console.WriteLine ("-> {0}: {1}", de.Key, de.Value);
}
}
Ниже показано, как теперь будет выглядеть вывод программы:
Глава 7. Структурированная обработка исключений 277
***** Simple Exception Example *****
=> Creating a car and stepping on it!
Jamming...
=> CurrentSpeed = 30
=> CurrentSpeed = 40
=> CurrentSpeed = 50
=> CurrentSpeed = 60
=> CurrentSpeed = 70
=> CurrentSpeed = 80
■=> CurrentSpeed = 90
*** Error! ***
Member name: Void Accelerate(Int32)
Class defining member: SimpleException.Car
Member type: Method
Message: Zippy has overheated!
Source: SimpleException
Stack: at SimpleException.Car.Accelerate (Int32 delta)
at SimpleException.Program.Main(String[] args)
Help Link: http://www.CarsRUs.com
-> Custom Data:
-> TimeStamp: The car exploded at 1/12/2010 8:02:12 PM
-> Cause: You have a lead foot.
***** out of exception logic *****
Свойство Data очень полезно, так как позволяет формировать специальную
информацию об ошибке, не прибегая к созданию совершенно нового класса, который
расширяет базовый класс Exception (до выхода версии .NET 2.0 это было единственным
возможным вариантом). Однако каким бы полезным ни было свойство Data, разработчики
.NET-приложений все равно довольно часто предпочитают создавать строго
типизированные классы исключений, в которых специальные данные обрабатываются с
помощью строго типизированных свойств.
При таком подходе вызывающий код получает возможность перехватывать
конкретный производный от Exception тип, а не углубляться в коллекцию данных в поиске
дополнительных деталей. Чтобы понять, как это работает, сначала необходимо
разобраться с отличиями между исключениями уровня системы и уровня приложений.
Исходный код. Проект SimpleException доступен в подкаталоге Chapter 7.
Исключения уровня системы
(System. SystemException)
В библиотеке базовых классов .NET содержится много классов, которые в конечном
итоге наследуются от System.Exception. Например, в пространстве имен System
определены ключевые классы исключений, такие как ArgumentOutOfRangeException,
IndexOutOfRangeException, StackOverflowException и т.д. В других
пространствах имен есть исключения, отражающие их поведение (например, в пространстве
имен System. Drawing. Printing содержатся исключения, возникающие при печати,
в System. 10 — исключения, возникающие во время ввода-вывода, в System. Data —
исключения, связанные с базами данных, и т.д.).
Исключения, которые генерируются самой платформой .NET, называются
исключениями уровня системы. Эти исключения считаются неустранимыми фатальными
ошибками. Они наследуются прямо от базового класса System. SystemException, который, в
свою очередь, наследуется от System.Exception (а тот — от класса System.Object):
278 Часть II. Главные конструкции программирования на С#
public class SystemException : Exception
{
// Различные конструкторы.
}
Из-за того, что в System.SystemException никакой дополнительной
функциональности помимо набора специальных конструкторов больше не предлагается, может
возникнуть вопрос о том, а зачем он тогда вообще существует. Попросту говоря, когда тип
исключения наследуется от System. SystemException, это дает возможность понять,
что сущностью, которая сгенерировала исключение, является исполняющая среда .NET,
а не кодовая база функционирующего приложения. В этом можно довольно легко
удостовериться с помощью ключевого слова is:
// Действительно1 Исключение NullReferenceException
// является исключением типа SystemException.
NullReferenceException nullRefEx = new NullReferenceException();
Console.WriteLine("NullReferenceException is-a SystemException? : {0}",
nullRefEx is SystemException);
Исключения уровня приложения
(System.ApplicationException)
Поскольку все исключения .NET представляют собой типы классов, вполне
допускается создавать собственные исключения, предназначенные для конкретного
приложения. Из-за того, что базовый класс System. SystemException представляет
исключения, генерируемые CLR-средой, может сложиться впечатление о том, что специальные
исключения тоже должны наследоваться от System.Exception. Поступать
подобным образом действительно допускается, однако рекомендуется наследовать их не от
System.Exception, а от System.ApplicationException:
public class ApplicationException : Exception
{
// Различные конструкторы.
}
Как и в SystemException, в классе ApplicationException никаких
дополнительных членов кроме набора конструкторов, не предлагается. С точки зрения
функциональности единственной целью System. ApplicationException является указание на
источник ошибки. То есть при обработке исключения, унаследованного от System.
ApplicationException, программист может смело полагать, что исключение было
вызвано кодом функционирующего приложения, а не библиотекой базовых классов .NET
или механизмом исполняющей среды .NET.
Создание специальных исключений, способ первый
Хотя для уведомления о возникновении ошибки во время выполнения можно
всегда генерировать экземпляры System.Exception (как было показано в первом
примере), иногда гораздо выгоднее создавать строго типизированное исключение, способное
предоставлять уникальные детали по текущей проблеме. Например, предположим, что
понадобилось создать специальное исключение (по имени CarlsDeadException) для
предоставления деталей об ошибке, возникающей из-за увеличения скорости
неисправного автомобиля. Для получения любого специального исключения в первую
очередь необходимо создать новый класс, унаследованный от класса System.Exception
или System. ApplicationException (по соглашению, имена всех классов исключений
оканчиваются суффиксом Exception; в действительности это является рекомендуемым
практическим приемом в .NET).
Глава 7. Структурированная обработка исключений 279
На заметку! Как правило, классы всех специальных исключений должны быть сделаны
общедоступными, т.е. public (вспомните, что по умолчанию для не вложенных типов используется
модификатор доступа internal, а не public). Объясняется это тем, что исключения часто
передаются за пределы сборки, следовательно, они должны быть доступны вызывающему коду.
Чтобы увидеть все на конкретном примере, давайте создадим новый проект типа
Console Application (Консольное приложение) по имени CustomException и скопируем
в него приведенные ранее файлы Car.cs и Radio, cs, выбрав в меню Project (Проект)
пункт Add Existing Item (Добавить существующий элемент) и изменив для ясности
название пространства имен, в котором определяются типы Саг и Radio, с SimpleException
на CustomException. После этого добавим в него следующее определение класса:
// Это специальное исключение описывает детали условия
// выхода автомобиля из строя.
public class CarlsDeadException : ApplicationException
{}
Как и в любой другой класс, в этот класс можно включать любое количество
специальных членов, которые могли бы вызываться в блоке catch, а также
переопределять в нем любые виртуальные члены, которые поставляются в родительских классах.
Например, реализовать CarlsDeadException можно было бы за счет переопределения
виртуального свойства Message.
Вместо заполнения словаря данных (через свойство Data) при выдаче исключения,
конструктор позволяет отправителю передавать данные о дате и времени и причине
возникновения ошибки, которые могут быть получены с помощью строго
типизированных свойств:
public class CarlsDeadException : ApplicationException
{
private string messageDetails = String.Empty;
public DateTime ErrorTimeStamp {get; set;}
public string CauseOfError {get; set;}
public CarlsDeadException(){}
public CarlsDeadException(string message,
string cause, DateTime time)
{
messageDetails = message;
CauseOfError = cause;
ErrorTimeStamp = time;
}
// Переопределение свойства Exception.Message.
public override string Message
{
get
{
return string.Format("Car Error Message: {0}", messageDetails);
}
}
}
Здесь класс CarlsDeadException включает в себя приватное поле (message
Details), которое предоставляет данные о текущем исключении, которые могут
устанавливаться с помощью специального конструктора. Генерация этого исключения из
метода Accelerate () производится довольно легко и заключается просто в выделении,
настройке и выдаче исключения типа CarlsDeadException, а не общего типа System.
Exception (обратите внимание, что в таком случае заполнять коллекцию данных
вручную не понадобится):
280 Часть II. Главные конструкции программирования на С#
// Выдача специального исключения CarlsDeadException.
public void Accelerate (int delta)
{
CarlsDeadException ex =
new CarlsDeadException (string.Format("{0} has overheated!", PetName),
"You have a lead foot", DateTime.Now);
ex.HelpLink = "http://www.CarsRUs.com";
throw ex;
}
Для перехвата такого поступающего исключения теперь можно модифицировать блок
catch, чтобы в нем перехватывалось именно исключение типа CarlsDeadException
(хотя из-за того, что System. CarlsDeadException является потомком System.
Exception, перехват в нем исключения типа System.Exception также допустим).
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Custom Exceptions *****\n");
Car myCar = new Car ("Rusty", 90);
try
{
// Отслеживание исключения.
myCar.AccelerateE0);
}
catch (CarlsDeadException e)
{
Console.WriteLine(e.Message) ;
Console.WriteLine(e.ErrorTimeStamp);
Console.WriteLine(e.CauseOfError) ;
}
Console.ReadLine ();
}
Теперь, когда известно, как в общем выглядит процесс создания специального
исключения, может возникнуть вопрос о том, когда к нему следует прибегать. Обычно
необходимость в создании специальных исключений возникает, только если ошибка тесно
связана с генерирующим ее классом (например, специальный файловый класс может
выдавать набор специальных ошибок, связанных с файлами, класс Саг — ошибки,
связанные с автомобилем, объект доступа к данным — ошибки, связанные с отдельной
таблицей в базе данных, и т.д.). Их создание позволяет обеспечить вызывающий код
возможностью обрабатывать многочисленные исключения за счет описания каждой
ошибки по отдельности.
Создание специальных исключений, способ второй
В предыдущем примере в специальном типе CarlsDeadException
переопределялось свойство System.Exception.Message для настройки специального сообщения об
ошибке и поставлялось два специальных свойства для предоставления
дополнительных фрагментов данных. В реальности, однако, переопределять виртуальное свойство
Message вовсе не требуется, поскольку можно также просто передавать поступающее
сообщение конструктору родителя, как показано ниже:
public class CarlsDeadException : ApplicationException
{
public DateTime ErrorTimeStamp { get; set; }
public string CauseOfError { get; set; }
public CarlsDeadException () { }
Глава 7. Структурированная обработка исключений 281
// Передача сообщения конструктору родителя.
public CarlsDeadException(string message,
string cause, DateTime time)
:base(message)
{
CauseOfError = cause;
ErrorTimeStamp = time;
}
}
Обратите внимание, что на этот раз никакая строковая переменная для
представления сообщения не определяется и никакое свойство Message не переопределяется.
Вместо этого производится передача соответствующего параметра конструктору
базового класса. С таким дизайном специальный класс исключения представляет
собой уже нечто большее, чем просто класс с уникальным именем, унаследованный от
System.ApplicationException (и, при необходимости, имеющий дополнительные
свойства), поскольку не содержит никаких переопределений базового класса.
Не стоит удивляться, если многие (а то и все) специальные классы исключений
придется создавать именно по такой простой схеме. Во многих случаях роль специального
исключения состоит не в предоставлении дополнительной функциональности помимо
той, что унаследована от базовых классов, а в обеспечении строго именованного типа,
четко описывающего природу ошибки и тем самым позволяющего клиенту
использовать разную логику обработки для разных типов исключений.
Создание специальных исключений, способ третий
Если планируется создать действительно заслуживающий внимания специальный
класс исключения, необходимо позаботиться о том, чтобы он соответствовал
наилучшим рекомендациям .NET. В частности это означает, что он должен:
• наследоваться от ApplicationException;
• сопровождаться атрибутом [System. Serializable];
• иметь конструктор по умолчанию;
• иметь конструктор, который устанавливает значение унаследованного свойства
Message;
• иметь конструктор для обработки "внутренних исключений";
• иметь конструктор для обработки сериализации типа.
Исходя из рассмотренного на текущий момент базового материла по .NET, роль
атрибутов и сериализации объектов может быть совершенно не понятна, в чем ничего
страшного нет, потому что эти темы будут подробно раскрываться далее в книге (в
главе 15, которая посвящена атрибутам, и в главе 20, в которой рассматриваются
службы сериализации). В завершение изучения специальных исключений ниже приведена
последняя версия класса CarlsDeadException, в которой поддерживается каждый из
упомянутых выше специальных конструкторов:
[Serializable]
public class CarlsDeadException : ApplicationException
{
public CarlsDeadException () { }
public CarlsDeadException(string message) : base ( message ) { }
public CarlsDeadException(string message,
System.Exception inner) : base ( message, inner ) { }
protected CarlsDeadException (
System.Runtime.Serialization.SerializationInfo info,
282 Часть II. Главные конструкции программирования на С#
System.Runtime.Serialization.StreamingContext context)
: base ( info, context ) { }
// Далее могут идти любые дополнительные специальные
// свойства, конструкторы и члены данных.
}
Поскольку специальные исключения, создаваемые в соответствии с наилучшими
практическими рекомендациям .NET, отличаются только именами, не может не
радовать тот факт, что в Visual Studio 2010 поставляется специальный шаблон фрагмента
кода под названием Exception (рис. 7.1), который позволяет автоматически
генерировать новый класс исключения, отвечающий требованиям наилучших практических
рекомендаций .NET. (Как рассказывалось в главе 2, для активизации фрагмента кода
необходимо ввести его имя, которым в данном случае является exception, и два раза
нажать клавишу <ТаЬ>.)
C*rIsDeadExcept«on.cs* X Q
"TjCustomtxceptioaCadsDeadExceptbo *I ♦CarlsDeadEKception(stringmessage, stnngcause, 0 'I
ex
I- > <Ctri+Att*Space>
exception j
Code snippet for exception I
1
Рис. 7.1. Шаблон фрагмента кода под названием exception
Исходный код. Проект CustomException находится в подкаталоге Chapter 7.
Обработка многочисленных исключений
В простейшем варианте блок try сопровождается только одним блоком catch.
В реальности, однако, часто требуется, чтобы операторы в блоке try могли
приводить к срабатыванию нескольких возможных исключений. Чтобы рассмотреть
пример, создадим новый проект типа Console Application (Консольное приложение) на С#
по имени ProcessMultipleExpceptions. Добавим в него файлы Car.cs, Radio, cs и
CarIsDeadException.es из предыдущего примера CustomException (выбрав в меню
Project пункт Add Existing Item) и соответствующим образом модифицируем названия
пространств имен.
Далее изменим в классе Саг метод Accelerate () так, чтобы он выдавал и такое
готовое исключение из библиотеки базовых классов, как ArgumentOutOf RangeException,
в случае передачи недействительного параметра (которым будет считаться любое
значение меньше нуля). Обратите внимание, что конструктор этого класса исключения
принимает имя проблемного аргумента в качестве первого параметра string, следом
за которым идет сообщение с описанием ошибки:
// Выполнение проверки аргумента на предмет действительности перед продолжением.
public void Accelerate (int delta)
{
if(delta < 0)
// Скорость должна быть больше нуля!
throw new
ArgumentOutOfRangeException("delta", "Speed must be greater than zero1");
IK extern
Глава 7. Структурированная обработка исключений 283
Теперь можно модифицировать логику в блоке catch так, чтобы в ней
предусматривалось специфическая реакция на исключение каждого типа:
static void Main(string[] args)
{
Console.WriteLine("***** Handling Multiple Exceptions *****\n");
Car myCar = new Car ("Rusty", 90) ;
try
// Отслеживание исключения ArgumentOutOfRangeException.
myCar.Accelerate(-10);
catch (CarlsDeadException e)
Console.WriteLine(e.Message);
catch (ArgumentOutOfRangeException e)
Console.WriteLine (e.Message);
Console.ReadLine ();
}
При создании множества блоков catch следует иметь в виду, что в случае
выдачи исключения оно будет обрабатываться "первым доступным" блоком catch. Чтобы
рассмотреть пример, изменим предыдущую логику, добавив еще один блок catch,
пытающийся обрабатывать все остальные исключения помимо CarlsDeadException
и ArgumentOutOfRangeException за счет перехвата исключения обобщенного типа
System.Exception, как показано ниже:
// Этот код компилироваться не будет!
static void Main(string[] args)
{
Console.WriteLine ("***** Handling Multiple Exceptions *****\n");
Car myCar = new Car ("Rusty", 90) ;
try
// Приведение в действие исключения ArgumentOutOfRangeException.
myCar.Accelerate(-10);
catch(Exception e)
// Обработка всех остальных исключений?
Console.WriteLine(e.Message);
catch (CarlsDeadException e)
Console.WriteLine(e.Message);
catch (ArgumentOutOfRangeException e)
Console.WriteLine(e.Message);
Console.ReadLine();
}
Такая логика по обработке исключений будет приводить к ошибкам на этапе
компиляции. Проблема в том, что первый блок catch может обрабатывать любые
исключения, унаследованные от System.Exception, в том числе, следовательно, исключения
284 Часть II. Главные конструкции программирования на С#
типа CarlsDeadException и ArgumentOutOfRangeException. Из-за этого два
последних блока catch получаются недостижимыми.
При структурировании блоков catch необходимо помнить о том, что в первом блоке
должно обрабатываться наиболее конкретное исключение (т.е. исключение
максимально производного типа в цепочке наследования типов исключений), а в последнем
блоке — наиболее общее (т.е. исключение базового типа в текущей цепочке наследования,
каковым в данном случае является System.Exception).
Таким образом, если необходимо определить блок catch, способный обрабатывать
любые ошибки помимо CarlsDeadException и ArgumentOutOfRangeException, можно
написать следующий код:
// Этот код скомпилируется без проблем.
static void Main(string[] args)
{
Console.WriteLine ("***** Handling Multiple Exceptions *****\n");
Car myCar = new Car ("Rusty", 90);
try
{
// Приведение в действие исключения ArgumentOutOfRangeException.
myCar.Accelerate (-10);
}
catch (CarlsDeadException e)
{
Console.WriteLine(e.Message);
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine(e.Message);
}
// В этом блоке будут перехватываться любые другие исключения
// помимо CarlsDeadException и ArgumentOutOfRangeException.
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Console.ReadLine();
}
На заметку! Везде, где только возможно, следует отдавать предпочтение перехвату конкретных
классов исключений, а не общих исключений типа System.Exception. Хотя поначалу
может казаться, что это упрощает жизнь (поскольку охватывает все вещи, с которыми не хочется
возиться), со временем из-за того, что обработка более серьезной ошибки не была напрямую
предусмотрена в коде, могут возникать очень странные сбои во время выполнения. Не
следует забывать о том, что последний блок catch, который отвечает за обработку исключений
System.Exception, имеет тенденцию оказываться чрезвычайно общим.
Общие операторы catch
В С# поддерживается так называемый "общий" (универсальный) блок catch, в
котором объект исключения, генерируемый тем или иным членом, явным образом не
получается.
// Общий блок catch.
static void Main(string[] args)
{
Console.WriteLine("***** Handling Multiple Exceptions *****\n");
Глава 7. Структурированная обработка исключений 285
Car myCar = new Car ( "Rusty" , 90) ;
try
{
myCar.Accelerate(90);
}
catch
{
Console.WriteLine("Something bad happened...");
}
Console.ReadLine();
}
Очевидно, что это не самый информативный способ обработки исключений,
поскольку нет никакой возможности для получения более детальных сведений о
возникшей ошибке (таких как имя метода, стек вызовов или специальное сообщение). Тем не
менее, в С# все-таки можно применять конструкцию подобного рода, так как она
может оказаться полезной, когда требуется обработать все ошибки в чрезвычайно общей
манере.
Передача исключений
При перехвате исключения внутри блока try допускается передавать (rethrow)
исключение вверх по стеку вызовов предшествующему вызывающему коду. Для этого
достаточно воспользоваться в блоке catch ключевым словом throw. Это позволит передать
исключение вверх по цепочке логики вызовов, что может оказаться полезным, если блок
catch способен обрабатывать текущую ошибку только частично.
// Передача ответственности.
static void Main(string[] args)
{
try
{
// Логика, касающаяся увеличения скорости
// автомобиля...
}
catch(CarlsDeadException e)
{
// Выполнение любой частичной обработки данной ошибки
//и передача дальнейшей ответственности.
throw;
}
}
Следует иметь в виду, что в приведенном примере кода конечным получателем
исключения CarlsDeadException будет CLR-среда из-за его передачи в методе Main ().
Следовательно, конечному пользователю будет отображаться системное диалоговое
окно с информацией об ошибке. Обычно передача частичного обработанного
исключения вызывающему коду осуществляется только в случае, если он способен
обрабатывать поступающее исключение более элегантно.
Обратите внимание на неявную передачу объекта CarlsDeadException и на
применение ключевого слова throw без аргументов. Никакого нового объекта исключения не
создается, а производится просто передача самого исходного объекта исключения (со
всей его исходной информацией). Это позволяет сохранить контекст первоначального
целевого объекта.
286 Часть II. Главные конструкции программирования на С#
Внутренние исключения
Нетрудно догадаться, что вполне возможно генерировать исключение во время
обработки какого-то другого исключения. Например, предположим, что производится
обработка исключения CarlsDeadException в определенном блоке catch, и в ходе этого
процесса обработки предпринимается попытка записать данные трассировки стека в
файл carErrors.txt на диске С: (для получения доступа к таким ориентированным на
работу с вводом-выводом типам в директиве using должно быть указано пространство
имен System. 10).
catch(CarlsDeadException e)
{
// Попытка открыть файл carErrors.txt на диске С:
FileStream fs = File.Open(@"C:\carErrors.txt", FileMode.Open);
}
Теперь, если указанный файл на диске С: отсутствует, вызов File. Open () приведет
к генерации исключения FileNotFoundException. Позже в книге будет более подробно
рассказываться о пространстве имен System. 10 и о том, как программно определить,
существует ли файл на жестком диске, перед тем как пытаться открыть его (это
позволит вообще избежать генерации исключения). Тем не менее, чтобы не отходить от темы
исключений, давайте считать, что такое исключение все-таки генерируется.
Если во время обработки исключения возникает какое-то другое исключение,
согласно наилучшим практическим рекомендациям, необходимо сохранить новый объект
исключения как "внутреннее исключение" в новом объекте того же типа, что у
исходного исключения. Причина, по которой необходимо выделять новый объект для
обрабатываемого исключения, связана с тем, что документировать внутреннее исключение
допускается только через параметр конструктора. Рассмотрим следующий код:
catch (CarlsDeadException e)
{
try
{
FileStream fs = File.Open(@"C:\carErrors.txt", FileMode.Open);
}
catch (Exception e2)
{
// Генерация исключения, записывающего новое
// исключение, а также сообщение первого исключения.
throw new CarlsDeadException(e.Message, e2) ;
}
}
В данном случае важно обратить внимание, что конструктору CarlsDeadException
в качестве второго параметра передается объект FileNotFoundException. После
настройки этоп объект передается вверх по стеку вызовов следующему вызывающему
коду, которым будет метод Main ().
Поскольку после Main () никакого "следующего вызывающего кода", который мог бы
перехватить исключение, не существует, пользователю будет отображаться системное
диалоговое окно с сообщением об ошибке. Во многом подобно передаче исключения,
запись внутренних исключений обычно осуществляется только тогда, когда
вызывающий код способен обрабатывать данное исключение более элегантно. В этом случае в
вызывающем коде внутри catch может использоваться свойство InnerException для
извлечения деталей объекта внутреннего исключения.
Глава 7. Структурированная обработка исключений 287
Блок finally
В контексте try/catch можно также определять необязательный блок finally. Это
гарантирует, что некоторый набор операторов будет выполняться всегда, независимо
от того, возникло исключение (любого типа) или нет. Для целей иллюстрации
предположим, что перед выходом из метода Main () должно всегда производиться выключение
радиоприемника в автомобиле, невзирая ни на какие обрабатываемые исключения.
static void Main(string[] args)
{
Console.WriteLine ("***** Handling Multiple Exceptions *****\n");
Car myCar = new Car ("Rusty", 90);
try
// Логика, связанная с увеличением скорости автомобиля.
catch(CarlsDeadException e)
// Обработка исключения CarlsDeadException.
catch(ArgumentOutOfRangeException e)
// Обработка исключения ArgumentOutOfRangeException.
catch(Exception e)
// Обработка любых других исключений.
finally
// Это код будет выполняться всегда, независимо
//от того, будет возникать и обрабатываться
// какое-нибудь исключение или нет.
myCar.CrankTunes(false);
}
Console.ReadLine();
}
Если бы не был добавлен блок finally, тогда в случае возникновения любого
исключения радиоприемник не выключался бы (что может как быть, так и не быть
проблемой). В более реалистичном сценарии, когда необходимо удалить объекты, закрыть
файл, отключиться от базы данных (или чего-то подобного), блок finally представляет
собой идеальное место для выполнения надлежащей очистки.
Какие исключения могут выдавать методы
Из-за того, что каждый метод в .NET Framework может генерировать любое
количество исключений (в зависимости от обстоятельств), возникает вполне логичный вопрос
о том, как узнать, какие исключения может выдавать тот или иной метод из библиотеки
базовых классов? Ответ прост: нужно заглянуть в документацию .NET Framework 4.0
SDK. В этой документации вместе с описанием каждого метода перечислены и все
исключения, которые он может генерировать. В качестве более быстрой альтернативы,
для просмотра списка всех исключений, которые способен выдавать тот или иной член
библиотеки базовых классов, можно воспользоваться Visual Studio 2010, просто наведя
курсор мыши на имя интересующего члена в окне кода, как показано на рис. 7.2.
288 Насть II. Главные конструкции программирования на С#
^Ш Program.cs X В
1 J$ProcessMultiple£xceptions.Program
^*Mam(stringQ args) • j
finally
{
// This will always occur. Exception or not.
myCar.CrankTunes(false);
}
т
Console. ReadL.'.ne ();
}
}
Л
*
1
string Console.ReadLineQ
Reads the next tine of characters from the standard input stream.
Exceptions:
SystemJOJOException
System.OutOfMemoryException
5ystem.ArgurnentOutOfRange£xceptiori
►
-
Рис. 7.2. Просмотр списка исключений, которые может генерировать определенный метод
Тем, кто перешел на .NET с Java, важно понять, что в .NET члены типов не про-
тотипируются с набором исключений, которые они могут выдавать (другими словами,
контролируемые исключения в С# не поддерживаются). Хорошо это или плохо, но
обрабатывать каждое исключение, генерируемое определенным членом, не требуется.
К чему приводят необрабатываемые исключения
К этому моменту наверняка возник вопрос о том, что произойдет, если выданное
исключение обработано не будет? Для примера предположим, что в логике внутри Main ()
объект Саг разгоняется до скорости, превышающей допустимый максимальный предел,
и что логика try/catch отсутствует:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Handling Multiple Exceptions *****\n»);
Car myCar = new Car ("Rusty", 90) ;
myCar.Accelerate E00);
Console.ReadLine();
}
Игнорирование исключения в таком случае станет серьезным препятствием для
конечного пользователя приложения, поскольку ему будет отображаться диалоговое окно
с сообщением о необработанном исключении, показанное на рис. 7.3.
ProcessMultipleExceptions has stopped
working
A problem caused the program to stop working correctly.
Windows will close the program and notify you if a solution is
available.
Debug
Close program
Рис. 7.З. Последствия не обрабатывания исключений
Глава 7. Структурированная обработка исключений 289
Отладка необработанных исключений
с помощью Visual Studio
Среда Visual-Studio 2010 предлагает набор инструментов, которые помогают
отлаживать необработанные специальные исключения. Для примера предположим, что
скорость объекта Саг была увеличена до предела, превышающего допустимый максимум.
После запуска сеанса отладки в Visual Studio 2010 (выбором в меню Debug (Отладка)
пункта Start (Начать)), выполнение программы на месте выдачи не перехватываемого
исключения будет автоматически прерываться. Кроме того, откроется окно (рис. 7.4), в
котором будет отображаться значение свойства Message.
Cw.cs X I
JfProcessMultipleExcepbons.Car
* Accelerator* delta)
else
// We need to call the HelpLink property, thus we need
// to create a local variable before throwing the Exception object.
л ' I CarhOeadExceptlonwasunhandled
"You have а 15^^°°*'М1^^^^
ex. Helpline" "http://J **
throw ex-
у has overheated!
Console.Wr
ffcuaty has overheated!"}
"You have a lead foot*
{10/11/2009 9:i6r00PM}
й!
Search for more Help Online...
View Detail...
Copy exception detail to the clipboard
Рис. 7.4. Отладка необработанных специальных исключений в Visual Studio 210
На заметку! Если обработать исключение, сгенерированное каким-то методом из библиотеки
базовых классов .NET не удается, отладчик Visual Studio 2010 прерывает выполнение программы
на операторе, который вызвал этот проблемный метод.
Щелкнув в этом окне на ссылке View Detail (Показать подробности), можно увидеть
дополнительные сведения о состоянии объекта (рис. 7.5).
Exception snapshot
л ProcessMurtipleExceptions.CarkDeadExceptU {"Rusty has overheated!"}
[ProcessMukipleExceptionsXarlsDeadExc {"Rusty has overheated!"}
CauseOfError You have a lead foot
Data {System.Collections.ListDictionarylnternal}
ErrorTimeStamp {10/11/2009 9:48:25 PM}
BNH H http://www.CarsRUs.com :▼]
InnerException null
Message Rusty has overheated!
Source ProceuMurtipleExceptions
StackTrace at ProcessMultipleExceptions.Car.Accelerate(Int32
• TergetSite {Void Accelerate(lnt32)}
Рис. 7.5. Просмотр детальной информации об исключении
Исходный код. Проект ProcessMultipleExceptions доступен в подкаталоге Chapter 7.
290 Часть II. Главные конструкции программирования на С#
Несколько слов об исключениях,
связанных с поврежденным состоянием
(Corrupted State Exceptions)
В завершении изучения предлагаемой в С# поддержки для структурированной
обработки исключений следует упомянуть о появлении в .NET 4.0 совершенно нового
пространства имен под названием System.Runtime.ExceptionServices (которое
поставляется в составе сборки mscorlib.dll). Это довольно небольшое пространство имен
включает в себя всего два типа класса, которые могут применяться, когда необходимо
снабдить различные методы в приложении (вроде Main ()) возможностью перехвата
и обработки "исключений, связанных с поврежденным состоянием" (Corrupted State
Exceptions — CSEZ).
Как говорилось в главе 1, платформа .NET всегда размещается в среде
обслуживающей операционной системы (такой как Microsoft Windows). Имея опыт
программирования приложений для Windows, можно вспомнить, что низкоуровневый API-интерфейс
Windows обладает очень уникальным набором правил по обработке ошибок времени
выполнения, которые немного похожи на предлагаемые в .NET приемы
структурированной обработки исключений.
В API-интерфейсе Windows можно перехватывать ошибки чрезвычайно низкого
уровня, которые как раз и представляют собой ошибки, связанные с "поврежденным
состоянием". Попросту говоря, если ОС Windows передает такую ошибку, это означает,
что с программой что-то серьезно не так, причем настолько, что нет никакой надежды
на восстановление, и единственно возможной мерой является завершение ее работы.
На заметку! При работе с .NET ошибка CSE может появиться только в случае использования в
коде С# служб вызова платформы (для непосредственного взаимодействия с API-интерфейсом
Windows) или применения поддерживаемого в С# синтаксиса указателей (см. главу 12).
До выхода версии .NET 4.0 подобные низкоуровневые ошибки, специфичные для
операционной системы, можно было перехватывать только с помощью блока catch,
предусматривающего перехват общих исключений System.Exception. Однако с этим
подходом была связана проблема: если каким-то образом возникало исключение CSE,
которое перехватывалось в таком блоке catch, в .NET не предлагалось (и до сих пор не
предлагается) никакого элегантного кода для восстановления.
Теперь, с выходом версии .NET 4.0, среда CLR больше не разрешает автоматический
перехват исключений CSE в приложениях .NET. В большинстве случаев это именно то
поведение, которое нужно. Если же необходимо получать уведомления о таких ошибках
уровня ОС (обычно при использовании унаследованного кода, нуждающегося в таких
уведомлениях), понадобится применять атрибут [HandledProcessCorruptedState
Exceptions].
Хотя роль атрибутов в .NET рассматривается позже в книге (в главе 15), сейчас
важно понять, что данный атрибут может применяться к любому методу в приложении, и в
результате его применения соответствующий метод получит возможность иметь дело с
подобными низкоуровневыми ошибками, специфическими для операционной системы.
Чтобы увидеть хотя бы простой пример, давайте создадим следующий метод Main (), не
забыв перед этим импортировать в файл кода С# пространство имен System.Runtime.
ExceptionServices:
[HandledProcessCorruptedStateExceptions]
static int Main(string[ ] args)
{
Глава 7. Структурированная обработка исключений 291
try
{
// Предполагаем, что в Ма±п() вызывается метод,
// который отвечает за выполнение всей программы.
RunMyApplication();
}
catch (Exception ex)
{
// Если мы добрались сюда, значит, что-то произошло.
// Поэтому просто отображаем сообщение
//и выходим из программы.
Console.WriteLine("Ack! Huge problem: {0}", ex.Message);
return -1;
}
return 0;
}
Задача приведенного выше метода Main () практически сводится только к вызову
второго метода, отвечающего за выполнение всего приложения. В данном примере
будем полагать, что в этом втором методе RunMyApplication () интенсивно используется
логика try/catch для обработки любой ожидаемой ошибки. Поскольку метод Main ()
был помечен атрибутом [HandledProcessCorruptedStateExceptions], в случае
возникновения ошибки CSE перехват System.Exception получается последним шансом
сделать хоть что-то перед завершением работы программы.
Метод Main () здесь возвращает значение int, а не void. Как объяснялось в главе 3,
по соглашению возврат операционной системе нулевого значения свидетельствует о
завершении работы приложения без ошибок, в то время как возврат любого другого
значения (обычно отрицательного числа) — о том, что в ходе его выполнения возникла
какая-то ошибка.
В настоящей книге обработка подобных низкоуровневых ошибок, специфических для
операционной системы Windows, рассматриваться не будет, а потому и о роли System.
Runtime .ExceptionServices тоже подробно рассказываться не будет. Исчерпывающие
сведения по этому поводу можно найти в документации .NET 4.0 Framework SDK.
Резюме
В этой главе была показана роль, которую играет структурированная обработка
исключений. Если необходимо, чтобы из метода вызывающему коду отправлялся объект,
описывающий ошибку, в методе нужно выделить, сконфигурировать и выдать
конкретное исключение производного от System.Exception типа с применением такого
поддерживаемого в С# ключевого слова, как throw. В вызывающем коде любые
поступающие исключения обрабатываются с помощью ключевого слова catch и необязательного
блока finally.
Для получения собственных специальных исключений, по сути, требуется создать
класс, унаследованный от класса System.ApplicationException. Этот новый класс
будет представлять исключения, генерируемые приложением, которое выполняется в
настоящий момент. Объекты ошибок, унаследованные от System. SystemException,
в свою очередь, позволяют представлять критические (и фатальные) ошибки, которые
выдает CLR. Напоследок в главе были продемонстрированы различные инструменты
внутри Visual Studio 2010, которые можно применять для создания специальных
исключений (в соответствии с наилучшими практическими рекомендациями .NET), а
также для их отладки.
ГЛАВА 8
Время жизни объектов
К этому моменту было предоставлено немало сведений о создании специальных
типов классов в С#. Теперь речь пойдет о том, как CLR-среда управляет
размещенными экземплярами классов (т.е. объектами) с помощью процесса сборки мусора
(garbage collection). Программистам на С# никогда не приходится непосредственно удалять
управляемый объект из памяти (в языке С# нет даже ключевого слова вроде delete).
Вместо этого объекты .NET размещаются в области памяти, которая называется
управляемой кучей (managed heap), откуда они автоматически удаляются сборщиком мусора,
когда наступает "определенный момент в будущем".
После рассмотрения ключевых деталей процесса сборки мусора в настоящей
главе будет показано, как программно взаимодействовать со сборщиком мусора с
помощью класса System.GC, и как с применением виртуального метода System.Object.
Finalize () и интерфейса IDisposable создавать классы, способные освобождать
внутренние неуправляемые ресурсы в определенное время.
Кроме того, будут описаны некоторые новые функциональные возможности
сборщика мусора, появившиеся в версии .NET 4.0, включая фоновую сборку мусора и
отложенную (ленивую) инициализацию с использованием обобщенного класса System.Lazyo.
После изучения материалов настоящей главы должно появиться вполне четкое
представление об управлении объектами .NET в среде CLR.
Классы, объекты и ссылки
Перед изучением тем, излагаемых в настоящей главе, сначала необходимо немного
больше прояснить различие между классами, объектами и ссылками. Вспомните, что
класс представляет собой ни что иное, как схему, которая описывает то, каким образом
экземпляр данного типа должен выглядеть и вести себя в памяти. Определяются классы
в файлах кода (которым по соглашению назначается расширение *. cs). Для примера
создадим новый проект типа Console Application (Консольное приложение) на С# по
имени SimpleGC и определим в нем следующий простой класс Саг:
// Содержимое файла Car.cs
public class Car
{
public int CurrentSpeed {get; set;}
public string Petllame {get; set; }
public Car () {}
public Car(string name, int speed)
{
PetName = name;
CurrentSpeed = speed;
}
Глава 8. Время жизни объектов 293
public override string ToStringO
{
return string.Format ( "{0 } is going {1} MPH",
petName, currSp);
PetName, CurrentSpeed);
}
}
Как только класс определен, с использованием ключевого слова new,
поддерживаемого в С#, можно размещать в памяти любое количество его объектов. Однако при этом
следует помнить, что ключевое слово new возвращает ссылку на объект в куче, а не
фактический объект. Если ссылочная переменная объявляется как локальная переменная в
контексте метода, она сохраняется в стеке для дальнейшего использования в
приложении. Для вызова членов объекта к сохраненной ссылке должна применяться операция
точки С#.
class Program
{
static void Main(string[] args)
{
Console.WriteLine ("***** GC Basics *****");
// Создание нового объекта Car в управляемой куче.
// Возвращается ссылка на этот объект (refToMyCar).
Car refToMyCar = new Car ( "Zippy", 50);
// Применение к переменной с этой ссылкой С#-операции
// точки (.)для вызова членов данного объекта.
Console.WriteLine(refToMyCar.ToString ());
Console.ReadLine();
На рис. 8.1 схематично показаны отношения между классами, объектами и
ссылками на них.
Стек
refToMyCar
Управляемая куча
„ (Объект}
V Car J
Рис. 8.1. Ссылки на объекты в управляемой куче
На заметку! Вспомните из главы 4, что структуры представляют собой типы значения, которые
всегда размещаются прямо в стеке и никогда не попадают в управляемую кучу .NET. Размещение в
куче происходит только при создании экземпляров классов.
Базовые сведения о времени жизни объектов
При создании приложений на С# можно смело полагать, что исполняющая среда
.NET будет сама заботиться об управляемой куче без непосредственного вмешательства
со стороны программиста. На самом деле "золотое правило" по управлению памятью в
.NET звучит просто.
294 Часть II. Главные конструкции программирования на С#
Правило. Размещайте объект в управляемой куче с использованием ключевого слова new и
забывайте об этом.
После создания объект будет автоматически удален сборщиком мусора тогда, когда
в нем отпадет необходимость. Разумеется, возникает вопрос о том, каким образом
сборщик мусора определяет момент, когда в объекте отпадает необходимость? В двух словах
на этот вопрос можно ответить так: сборщик мусора удаляет объект из кучи тогда, когда
тот становится недостижимым ни в одной части программного кода. Например,
добавим в класс Program метод, который размещает в памяти объект Саг:
public static void MakeACarO
{
// Если myCar является единственной ссыпкой на объект
// Саг, тогда при возврате результата данным
// методом объект Саг *может* быть уничтожен.
Car myCar = new Car();
}
Обратите внимание, что ссылка на объект Car (myCar) была создана
непосредственно внутри метода MakeACar () и не передавалась за пределы определяющей области
действия (ни в виде возвращаемого значения, ни в виде параметров ref /out). Поэтому
после завершения вызова данного метода ссылка myCar окажется недостижимой, а
объект Саг, соответственно — кандидатом на удаление сборщиком мусора. Следует, однако,
понять, что иметь полную уверенность в немедленном удалении этого объекта из
памяти сразу же после выполнения метода MakeACar () нельзя. Все, что в данный момент
можно предполагать, так это то, что когда в CLR-среде будет в следующий раз
производиться сборка мусора, объект myCar может подпасть под процесс уничтожения.
Как станет ясно со временем, программирование в среде с автоматической сборкой
мусора значительно облегчает разработку приложений. Программистам на C++ хорошо
известно, что если они специально не позаботятся об удалении размещаемых в куче
объектов, вскоре обязательно начнут возникать "утечки памяти". На самом деле
отслеживание проблем, связанных с утечкой памяти, является одним из самых длительных
(и утомительных) аспектов программирования в неуправляемых средах. Благодаря
назначению ответственным за уничтожение объектов сборщика мусора, обязанности по
управлению памятью, по сути, сняты с плеч программиста и возложены на CLR-среду.
На заметку! Тем, кто ранее применял для разработки приложений технологию СОМ, следует знать,
что в .NET объекты не снабжаются никаким внутренним счетчиком ссылок и потому не
поддерживают использования методов вроде AddRef () или Release ().
CIL-код, генерируемый для ключевого слова new
При обнаружении ключевого слова new компилятор С# вставляет в реализацию
метода CIL-инструкцию newob j. Если скомпилировать текущий пример кода и заглянуть
в полученную сборку с помощью утилиты ildasm.exe, то можно обнаружить внутри
метода MakeACar () следующие CIL-операторы:
.method private hidebysig static void MakeACarO cil managed
{
// Code size 8 @x8)
// Размер кода 8 @x8)
.maxstack 1
Глава 8. Время жизни объектов 295
.locals init ([0] class SimpleGC.Car)
IL_0000: nop
IL_0001: newobj instance void SimpleGC.Car::.ctor ()
IL_0006: stloc.O
IL_0007: ret
} // end of method Program::MakeACar
// конец метода Program::MakeACar
Прежде чем ознакомиться с точными правилами, которые определяют момент,
когда объект должен удаляться из управляемой кучи, давайте более подробно рассмотрим
роль CIL-инструкции newobj. Дна начала важно понять, что управляемая куча
представляет собой нечто большее, чем просто случайный фрагмент памяти, к которому
CLR получает доступ. Сборщик мусора .NET "убирает" кучу довольно тщательно, причем
(при необходимости) даже сжимает пустые блоки памяти с целью оптимизации. Чтобы
ему было легче это делать, в управляемой куче поддерживается указатель (обычно
называемый указателем на следующий объект или указателем на новый объект), который
показывает, где точно будет размещаться следующий объект.
Таким образом, инструкция newobj заставляет CLR-среду выполнить
перечисленные ниже ключевые операции.
• Вычислить, сколько всего памяти требуется для размещения объекта (в том числе
памяти, необходимой для членов данных и базовых классов).
• Проверить, действительно ли в управляемой куче хватает места для обслуживания
размещаемого объекта. Если хватает, вызвать указанный конструктор и вернуть
вызывающему коду ссылку на новый объект в памяти, адрес которого совпадает
с последней позицией указателя на следующий объект.
• И, наконец, перед возвратом ссылки вызывающему коду переместить указатель
на следующий объект, чтобы он указывал на следующую доступную позицию в
управляемой куче.
Весь описанный процесс схематично изображен на рис. 8.2.
static void Main(string[] args)
{
Car cl = new Car();
Car c2 = new Car();
Управляемая куча
Cl C2
Указатель на
следующий объект
Рис. 8.2. Детали размещения объектов в управляемой куче
Из-за постоянного размещения объектов приложением пространство в управляемой
куче может со временем заполниться. В случае если при обработке следующей
инструкции newob] среда CLR обнаруживает, что в управляемой куче не хватает пространства
для размещения запрашиваемого типа, она приступает к сборке мусора и тем самым
пытается освободить хоть сколько-то памяти. Поэтому следующее правило, касающееся
сборки мусора, тоже звучит довольно просто.
Правило. В случае нехватки в управляемой куче пространства для размещения запрашиваемого
объекта начинает выполняться сборка мусора.
296 Часть II. Главные конструкции программирования на С#
Однако то, каким именно образом начнет выполняться сборка мусора, зависит от
версии .NET, под управлением которой функционирует приложение. Различия будут
описаны позже в настоящей главе.
Установка объектных ссылок в null
Если ранее приходилось создавать СОМ-объекты в Visual Basic 6.0, то должно быть
известно, что по завершении их использования предпочтительнее устанавливать эти
ссылки в Nothing. На внутреннем уровне счетчик ссылок на объект СОМ уменьшалось
на единицу, и когда он становился равным нулю, объект можно было удалять из
памяти. Аналогичным образом программисты на C/C++ часто предпочитают устанавливать
для переменных указателей значение null, гарантируя, что они больше не будут
ссылаться на неуправляемую память.
Из-за упомянутых фактов, вполне естественно, может возникнуть вопрос о том, что
же происходит в С# после установки объектных ссылок в null. Для примера изменим
метод MakeACar () следующим образом:
static void MakeACar ()
{
Car myCar = new Car () ;
myCar = null;
}
Когда объектные ссылки устанавливаются в null, компилятор С# генерирует CIL-
код, который заботится о том, чтобы ссылка (в рассматриваемом примере myCar)
больше не ссылалась ни на какой объект. Если теперь снова воспользоваться утилитой
ildasm. ехе и заглянуть с ее помощью в CIL-код измененного метода MakeACar (),
можно обнаружить в нем код операции ldnull (который заталкивает значение null в
виртуальный стек выполнения) со следующим за ним кодом операции stloc.O (который
присваивает переменной ссылку null):
.method private hidebysig static void MakeACar () cil managed
{
// Code size 10 (Oxa)
// Размер кода 10 (Oxa)
.maxstack 1
.locals mit ([0] class SimpleGC.Car myCar)
IL_0000: nop
IL_0001: newob] instance void SimpleGC.Car::.ctor()
IL_0006: stloc.O
IL_0007: ldnull
IL_0008: stloc.O
IL_0009: ret
} // end of method Program::MakeACar
// конец метода Program::MakeACar
Однако обязательно следует понять, что установка ссылки в null никоим образом
не вынуждает сборщик мусора немедленно приступить к делу и удалить объект из кучи,
а просто позволяет явно разорвать связь между ссылкой и объектом, на который она
ранее указывала. Благодаря этому, присваивание ссылкам значения null в С# имеет
гораздо меньше последствий, чем в других языках на базе С (или VB 6.0), и совершенно
точно не будет причинять никакого вреда.
Глава 8. Время жизни объектов 297
Роль корневых элементов приложения
Теперь снова вернемся к вопросу о том, каким образом сборщик мусора определяет
момент, когда объект уже более не нужен. Чтобы разобраться в стоящих за этим
деталях, необходимо знать, что собой представляет корневые элементы приложения
(application roots). Попросту говоря, корневым элементом (root) называется ячейка в памяти,
в которой содержится ссылка на размещающийся в куче объект. Строго говоря,
корневыми могут называться элементы любой из перечисленных ниже категорий.
• Ссылки на глобальные объекты (хотя в С# они не разрешены, CIL-код позволяет
размещать глобальные объекты).
• Ссылки на любые статические объекты или статические поля.
• Ссылки на локальные объекты в пределах кодовой базы приложения.
• Ссылки на передаваемые методу параметры объектов.
• Ссылки на объекты, ожидающие финализации (об этом подробно рассказываться
далее в главе).
• Любые регистры центрального процессора, которые ссылаются на объект.
Во время процесса сборки мусора исполняющая среда будет исследовать объекты
в управляемой куче, чтобы определить, являются ли они по-прежнему достижимыми
(т.е. корневыми) для приложения. Для этого среда CLR будет создавать графы
объектов, представляющие все достижимые для приложения объекты в куче. Более подробно
объектные графы будут описаны при рассмотрении процесса сериализации объектов в
главе 20. Пока главное усвоить то, что графы применяются для документирования всех
достижимых объектов. Кроме того, следует иметь в виду, что сборщик мусора никогда
не будет создавать граф для одного и того же объекта дважды, избегая необходимости
выполнения подсчета циклических ссылок, который характерен для программирования
в среде СОМ.
Чтобы увидеть все это на примере, предположим, что в управляемой куче содержится
набор объектов с именами A, B,C,D,E,FhG. Во время сборки мусора эти объекты (а
также любые внутренние объектные ссылки, которые они могут содержать) будут
исследованы на предмет наличия у них активных корневых элементов. После построения графа
все недостижимые объекты (которыми в примере пусть будут объекты С и F) помечаются
как являющиеся мусором. На рис. 8.3 показано, как примерно выглядит граф объектов в
только что описанном сценарии (линии со стрелками следует воспринимать как "зависит
от" или "требует"; например, "Е зависит от G и В", 4А не зависит ни от чего" и т.д.).
После того как объект помечен для уничтожения (в данном случае это объекты С и
F, поскольку в графе объектов они во внимание не принимаются), они будут удалены
из памяти. Оставшееся пространство в куче будет после этого сжиматься до
компактного состояния, что, в свою очередь, вынудит CLR изменить набор активных корневых
элементов приложения (и лежащих в их основе указателей) так, чтобы они ссылались
на правильное место в памяти (это делается автоматически и прозрачно). И, наконец,
указатель на следующий объект тоже будет подстраиваться так, чтобы указывать на
следующий доступный участок памяти. На рис. 8.4 показано, как выглядит конечный
результат этих изменений в рассматриваемом сценарии.
На заметку! Собственно говоря, сборщик мусора использует две отдельных кучи, одна из которых
предназначена специально для хранения очень больших объектов. Доступ к этой куче во время
сборки мусора получается реже из-за возможных последствий в плане производительности, в
которые может выливаться изменение места размещения больших объектов. Невзирая на этот
факт, управляемая куча все равно может спокойно считаться единой областью памяти.
298 Часть II. Главные конструкции программирования на С#
Управляемая куча
А
В
С
D
Е
F
G
Указатель на следующий объект
0 (№
Рис. 8.3. Графы объектов создаются для определения объектов,
достижимых для корневых элементов приложения
Управляемая куча
А
В
D
Е
G
Указатель на следующий объект
Рис. 8.4. Очищенная и сжатая до компактного состояния куча
Поколения объектов
При попытке обнаружить недостижимые объекты CLR-среда не проверяет буквально
каждый находящийся в куче объект. Очевидно, что на это уходила бы масса времени,
особенно в более крупных (реальных) приложениях.
Для оптимизации процесса каждый объект в куче относится к определенному
"поколению". Смысл в применении поколений выглядит довольно просто: чем дольше объект
находится в куче, тем выше вероятность того, что он там и будет оставаться. Например,
класс, определенный в главном окне настольного приложения, будет оставаться в
памяти вплоть до завершения выполнения программы. С другой стороны, объекты, которые
были размещены в куче лишь недавно (как, например, те, что находятся в пределах
области действия метода), вероятнее всего будут становиться недостижимым довольно
быстро. Исходя из этих предположений, каждый объект в куче относится к одному из
перечисленных ниже поколений.
• Поколение О. Идентифицирует новый только что размещенный объект, который
еще никогда не помечался как подлежащий удалению в процессе сборки мусора.
• Поколение 1. Идентифицирует объект, который уже "пережил" один процесс
сборки мусора (был помечен как подлежащий удалению в процессе сборки мусора, но
не был удален из-за наличия достаточного места в куче).
• Поколение 2. Идентифицирует объект, которому удалось пережить более одного
прогона сборщика мусора.
Глава 8. Время жизни объектов 299
На заметку! Поколения 0 и 1 называются эфемерными (недолговечными). В следующем разделе
будет показано, что в ходе процесса сборки мусора эфемерные поколения действительно
обрабатываются по-другому.
Сборщик мусора сначала анализирует все объекты, которые относятся к поколению 0.
Если после их удаления остается достаточное количество памяти, статус всех
остальных (уцелевших) объектов повышается до поколения 1. Чтобы увидеть, как поколение,
к которому относится объект, влияет на процесс сборки мусора, обратите внимание на
рис. 8.5, где схематически показано, как набору уцелевших объектов поколения О (А, В
и Е) назначается статус объектов следующего поколения после освобождения
требуемого объема памяти.
Поколение О
А
В
С
D
Е
F
G
Поколение 1
U №-
А
В
Е
Рис. 8.5. Объектам поколения 0, которые уцелели после сборки
мусора, назначается статус объектов поколения 1
Если все объекты поколения 0 уже были проверены, но все равно требуется
дополнительное пространство, проверяться на предмет достижимости и подвергаться
процессу сборки мусора начинают объекты поколения 1. Объектам поколения 1, которым
удалось уцелеть после этого процесса, затем назначается статус объектов поколения 2.
Если же сборщику мусора все равно требуется дополнительная память, тогда на
предмет достижимости начинают проверяться и объекты поколения 2. Объектам, которым
удается пережить сборку мусора на этом этапе, оставляется статус объектов поколения
2, поскольку более высокие поколения просто не поддерживаются.
Из всего вышесказанного важно сделать следующий вывод: из-за отнесения объектов
в куче к определенному поколению, более новые объекты (вроде локальных переменных)
будут удаляться быстрее, а более старые (такие как объекты приложений) — реже.
Параллельная сборка мусора
в версиях .NET 1.0 - .NET 3.5
До выхода версии .NET 4.0 очистка неиспользуемых объектов в исполняющей
среде производилась с применением техники параллельной сборки мусора. В этой модели
при выполнении сборки мусора для любых объектов поколения 0 или 1 (т.е. эфемерных
поколений) сборщик мусора временно приостанавливал все активные потоки внутри
текущего процесса, чтобы приложение не могло получить доступ к управляемой куче
вплоть до завершения процесса сборки мусора.
Потоки более подробно рассматриваются в главе 19, а пока можно считать поток
просто одним из путей выполнения внутри функционирующей программы. По заверше-
300 Часть II. Главные конструкции программирования на С#
нии цикла сборки мусора приостановленным потокам разрешалось снова продолжать
работу. К счастью, в .NET 3.5 (и предшествующих версиях) сборщик мусора был хорошо
оптимизирован и потому связанные с этим короткие перерывы в работе приложения
редко становились заметными (а то и вообще никогда).
Как и оптимизация, параллельная сборка мусора позволяла производить очистку
объектов, которые не были обнаружены ни в одном из эфемерных поколений, в
отдельном потоке. Это сокращало (но не устраняло) необходимость в приостановке активных
потоков исполняющей средой .NET. Более того, параллельная сборка мусора позволяла
программам продолжать размещать объекты в куче во время сборки объектов не
эфемерных поколений.
Фоновая сборка мусора в версии .NET 4.0
В .NET 4.0 сборщик мусора по-другому решает вопрос с приостановкой потоков при
очистке объектов в управляемой куче, используя при этом технику фоновой сборки
мусора. Несмотря на ее название, это вовсе не означает, что вся сборка мусора теперь
происходит в дополнительных фоновых потоках выполнения. На самом деле в случае
фоновой сборки мусора для объектов, относящихся к не эфемерному поколению,
исполняющая среда .NET теперь может производить сборку объектов эфемерных поколений
в отдельном фоновом потоке.
Механизм сборки мусора в .NET 4.0 был улучшен так, чтобы на приостановку
потока, связанного с деталями сборки мусора, требовалось меньше времени. Благодаря
этим изменениям, процесс очистки неиспользуемых объектов поколения 0 или 1 стал
оптимальным. Он позволяет получать более высокие показатели по производительности
приложений (что действительно важно для систем, работающих в реальном времени и
нуждающихся в небольших и предсказуемых перерывах на сборку мусора).
Однако следует понимать, что ввод такой новой модели сборки мусора никоим
образом не отражается на способе построения приложений .NET Теперь практически всегда
можно просто позволять сборщику мусора .NET выполнять работу без
непосредственного вмешательства со своей стороны (и радоваться тому, что разработчики в Microsoft
продолжают улучшать процесс сборки мусора прозрачным образом).
Тип System.GC
В библиотеках базовых классов доступен класс по имени System. GC, который
позволяет программно взаимодействовать со сборщиком мусора за счет обращения к его
статическим членам. Необходимость в непосредственном использовании этого
класса в разрабатываемом коде возникает крайне редко (а то и вообще никогда). Обычно
единственным случаем, когда нужно применять члены System. GC, является создание
классов, предусматривающих использование на внутреннем уровне неуправляемых
ресурсов. Это может быть, например, класс, работающий с основанным на С интерфейсом
Windows API за счет применения протокола вызовов платформы .NET, или какая-то
низкоуровневая и сложная логика взаимодействия с СОМ. В табл. 8.1 приведено краткое
описание некоторых наиболее интересных членов класса System.GC (полные сведения
можно найти в документации .NET Framework 4.0 SDK).
На заметку! В .NET 3.5 с пакетом обновлений Service Pack 1 появилась дополнительная возможность
получать уведомления перед началом процесса сборки мусора за счет применения ряда новых
членов. И хотя данная возможность может оказаться полезной в некоторых сценариях, в
большинстве приложений она не нужна, поэтому здесь подробно не рассматривается. Всю
необходимую информацию об уведомлениях подобного рода можно найти в разделе "Garbage Collection
Notifications" ("Уведомления о сборке мусора") документации .NET Framework 4.0 SDK.
Глава 8. Время жизни объектов 301
Таблица 8.1. Некоторые члены класса System.gc
Член
Описание
AddMemorуPressure(),
RemoveMemoryPressure()
Collect()
CollectionCount()
GetGeneration()
GetTotalMemory()
MaxGeneration
SuppressFinalize()
WaitForPendingFinalizers ()
Позволяют указывать числовое значение, отражающее
"уровень срочности", который вызывающий объект применяет в
отношении к сборке мусора. Следует иметь в виду, что эти
методы должны изменять уровень давления в тандеме и,
следовательно, никогда не устранять больше давления, чем было
добавлено
Заставляет сборщик мусора провести сборку мусора. Должен
быть перегружен так, чтобы указывать, объекты какого
поколения подлежат сборке, а также какой режим сборки
использовать (с помощью перечисления GCCollectionMode)
Возвращает числовое значение, показывающее, сколько раз
объектам данного поколения удалось переживать процесс
сборки мусора
Возвращает информацию о том, к какому поколению в
настоящий момент относится объект
Возвращает информацию о том, какой объем памяти (в
байтах) в настоящий момент занят в управляемой куче. Булевский
параметр указывает, должен ли вызов сначала дождаться
выполнения сборки мусора, прежде чем возвращать результат
Возвращает информацию о том, сколько максимум
поколений поддерживается в целевой системе. В .NET 4.0
поддерживается всего три поколения: 0, 1 и 2
Позволяет устанавливать флаг, указывающий, что для
данного объекта не должен вызываться его метод Finalize ()
Позволяет приостанавливать выполнение текущего потока до
тех пор, пока не будут финализированы все объекты,
предусматривающие финализацию. Обычно вызывается сразу же
после вызова метода GC. Collect ()
Рассмотрим применение System. GC для получения касающихся сборки мусора
деталей на примере следующего метода Main (), в котором используются сразу несколько
членов System. GC:
static void Main(string [ ] args)
{
Console.WriteLine("***** Fun with System.GC *****");
// Вывод подсчитанного количества байтов в куче.
Console.WriteLine("Estimates bytes on heap: {0}", GC.GetTotalMemory(false));
// Отсчет для MaxGeneration начинается с нуля,
// поэтому для удобства добавляется 1.
Console.WriteLine("This OS has @} object generations.\n",
(GC.MaxGeneration + 1));
Car refToMyCar = new Car("Zippy", 100);
Console.WriteLine(refToMyCar.ToString());
// Вывод информации о поколении объекта refToMyCar.
Console.WriteLine("Generation of refToMyCar is: @ 1",
GC.GetGeneration(refToMyCar));
Console.PeadLine ();
302 Часть II. Главные конструкции программирования на С#
Принудительная активизация сборки мусора
Сборщик мусора .NET предназначен в основном для того, чтобы управлять
памятью вместо разработчиков. Однако в очень редких случаях требуется принудительно
запустить сборку мусора с помощью метода GC.Collect(). Примеры таких ситуаций
приведены ниже.
• Приложение приступает к выполнению блока кода, прерывание которого
возможным процессом сборки мусора является недопустимым.
• Приложение только что закончило размещать чрезвычайно большое количество
объектов и нуждается в как можно скорейшем освобождении большого объема памяти.
Если выяснилось, что выполнение сборщиком мусора проверки на предмет наличия
недостижимых объектов может быть выгодным, можно инициировать процесс сборки
мусора явным образом, как показано ниже:
static void Main(string [ ] args)
{
// Принудительная активизация процесса сборки мусора и
// ожидание завершения финализации каждого из объектов.
GC.Collect ();
GC.WaitForPendingFinalizers ();
}
В случае принудительной активизации сборки мусора не забывайте вызвать метод
GC. WaitForPendingFinalizers (). Это дает возможность всем финализируемым
объектам (рассматриваются в следующем разделе) произвести любую необходимую очистку
перед продолжением работы программы. Метод GC.WaitForPendingFinalizers ()
незаметно приостанавливает выполнение вызывающего "потока" во время процесса
сборки мусора, что очень хорошо, поскольку исключает вероятность вызова в коде каких-
либо методов на объекте, который в текущий момент уничтожается.
Методу GC . Collect () можно передать числовое значение, отражающее старейшее
поколение объектов, в отношении которого должен проводиться процесс сборки мусора.
Например, чтобы CLR-среда анализировала только объекты поколения 0, необходимо
использовать следующий код:
static void Main(string [ ] args)
{
// Исследование только объектов поколения 0.
GC.Collect @);
GC.WaitForPendingFinalizers ();
}
Вдобавок методу Collect () во втором параметре может передаваться значение
перечисления GCCollectionMode, которое позволяет более точно указать, каким образом
исполняющая среда должна принудительно инициировать сборку мусора. Ниже
показаны значения, доступные в этом перечислении:
public enum GCCollectionMode
{
Default, // Текущим значением по умолчанию является Forced.
Forced, // Указывает исполняющей среде начать сборку мусора немедленно1
Optimized // Позволяет исполняющей среде выяснить, оптимален
//ли настоящий момент для удаления объектов.
}
Глава 8. Время жизни объектов 303
Как и при любой сборке мусора, в случае вызова GC.CollectO уцелевшим объектам
назначается статус объектов более высокого поколения. Чтобы удостовериться в этом,
модифицируем метод Main () следующим образом:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with System.GC *****");
// Отображение примерного количества байтов в куче.
Console.WriteLine("Estimated bytes on heap: {0}",
GC.GetTotalMemory(false));
// Отсчет значения MaxGeneration начинается с нуля.
Console.WriteLine("This OS has {0} object generations.\n",
(GC.MaxGeneration + 1) ) ;
Car refToMyCar = new Car("Zippy", 100);
Console.WriteLine(refToMyCar.ToString());
// Вывод информации о поколении, к которому
// относится refToMyCar.
Console.WriteLine ("\nGeneration of refToMyCar is: {0}",
GC.GetGeneration(refToMyCar));
// Создание большого количества объектов для целей тестирования.
object[] tonsOfObjects = new object[50000];
for (int i = 0; i < 50000; i++)
tonsOfObjects [i] = new object ();
// Выполнение сборки мусора в отношении только
// объектов, относящихся к поколению 0.
GC.Collect @, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
// Вывод информации о поколении, к которому
// относится refToMyCar.
Console.WriteLine("\nGeneration of refToMyCar is: {0}",
GC.GetGeneration(refToMyCar));
// Выполнение проверки, удалось ли
// tonsOfObjects[9000] уцелеть
// после сборки мусора.
if (tonsOfObjects[9000] !=null)
{
// Вывод поколения tonsOfObjects [ 9000] .
Console.WriteLine("Generation of tonsOfObjects[9000] is: {0}",
GC.GetGeneration(tonsOfObjects [ 9000]));
}
else
// tonsOfObjects[9000] больше не существует.
Console.WriteLine("tonsOfObjects[9000] is no longer alive.");
// Вывод информации о том, сколько раз в отношении
// объектов каждого поколения выполнялась сборка мусора.
Console.WriteLine("\nGen 0 has been swept {0} times",
GC.CollectionCount@));
Console.WriteLine("Gen 1 has been swept {0} times",
GC.CollectionCountA));
Console.WriteLine("Gen 2 has been swept {0} times",
GC.CollectionCountB));
Console.ReadLine();
304 Часть II. Главные конструкции программирования на С#
Дя целей тестирования был создан очень большой массив типа object (состоящий
из 50 000 элементов). Как можно увидеть по приведенному ниже выводу, хотя в данном
методе Main () и был сделан только один явный запрос на выполнение сборки мусора
(с помощью метода GC.CollectO), среда CLR в фоновом режиме провела несколько
таких сборок.
***** Fun with System.GC *****
Estimated bytes on heap: 70240
This OS has 3 object generations.
Zippy is going 100 MPH
Generation of refToMyCar is: 0
Generation of refToMyCar is: 1
Generation of tonsOfObjects [ 9000] is: 1
Gen 0 has been swept 1 times
Gen 1 has been swept 0 times
Gen 2 has been swept 0 times
К этому моменту детали жизненного цикла объектов должны выглядеть более
понятно. В следующем разделе исследование процесса сборки мусора продолжается, и будет
показано, как создавать финализируемые (finalizable) и высвобождаемые (disposable)
объекты. Очень важно отметить, что описанные далее приемы подходят только в случае
построения управляемых классов, внутри которых используются неуправляемые ресурсы.
Исходный код. Проект SimpleGC доступен в подкаталоге Chapter 8.
Создание финализируемых объектов
В главе 6 уже рассказывалось о том, что в самом главном базовом классе .NET —
System.Object — имеется виртуальный метод по имени Finalize (). В предлагаемой
по умолчанию реализации он ничего особенного не делает:
// Класс System.Object
public class Object
{
protected virtual void Finalize () {}
}
За счет его переопределения в специальных классах устанавливается
специфическое место для выполнения любой необходимой данному типу логики по очистке. Из-за
того, что метод Finalize () по определению является защищенным (protected),
вызывать его напрямую из класса экземпляра с помощью операции точки не допускается.
Вместо этого метод Finalize () (если он поддерживается) будет автоматически
вызываться сборщиком мусора перед удалением соответствующего объекта из памяти.
На заметку! Переопределять метод Finalize О в типах структур нельзя. Это вполне логичное
ограничение, поскольку структуры представляют собой типы значения, которые изначально
никогда не размещаются в управляемой памяти и, следовательно, никогда не подвергаются
процессу сборки мусора. Однако в случае создания структуры, которая содержит ресурсы,
нуждающиеся в очистке, вместо этого метода можно реализовать интерфейс iDisposable
(рассматривается далее в главе).
Разумеется, вызов метода Finalize () будет происходить (в конечном итоге) либо
во время естественной активизации процесса сборки мусора, либо во время его
принудительной активизации программным образом с помощью GC. Collect (). Помимо
Глава 8. Время жизни объектов 305
этого, финализатор типа будет автоматически вызываться и при выгрузке из памяти
домена, который отвечает за обслуживание приложения. Некоторым по опыту работы
с .NET уже может быть известно, что домены приложений (AppDomaln) применяются
для обслуживания исполняемой сборки и любых необходимых внешних библиотек кода.
Те, кто еще не знаком с этим понятием .NET, узнают все необходимое после прочтения
главы 16. Пока что необходимо обратить внимание лишь на то, что при выгрузке
домена приложения из памяти CLR-среда будет автоматически вызывать финализаторы
для каждого финализируемого объекта, который был создан во время существования
AppDomaln.
Что бы не подсказывали инстинкты разработчика, в подавляющем большинстве
классов С# необходимость в создании явной логики по очистке или специального фи-
нализатора возникать не будет. Объясняется это очень просто: если в классах
используются лишь другие управляемые объекты, все они рано или поздно все равно будут
подвергаться сборке мусора. Единственным случаем, когда может возникать потребность в
создании класса, способного выполнять после себя процедуру очистки, является
работа с неуправляемыми ресурсами (такими как низкоуровневые файловые дескрипторы,
низкоуровневые неуправляемые соединения с базами данных, фрагменты
неуправляемой памяти и т.п.). Внутри .NET неуправляемые ресурсы появляются в результате
непосредственного вызова API-интерфейса операционной системы с помощью служб PInvoke
(Platform Invocation Services — службы вызова платформы) или применения очень
сложных сценариев взаимодействия с СОМ. Ознакомьтесь со следующим правилом сборки
мусора.
Правило. Единственная причина переопределения Finalize () связана с использованием в
классе С# каких-то неуправляемых ресурсов через PInvoke или сложных процедур взаимодействия с
СОМ (обычно посредством членов типа System. Runtime . InteropServices . Marshal).
Переопределение System.Object. Finalize ()
В тех редких случаях, когда создается класс С#, в котором используются
неуправляемые ресурсы, очевидно, понадобится обеспечить предсказуемое освобождение
соответствующей памяти. Для примера создадим новый проект типа Console Application
на С# по имени SimpleFinalize, вставим в него класс MyResourceWrapper, в котором
будет использоваться какой-то неуправляемый ресурс. Теперь необходимо
переопределить метод Finalize(). Как ни странно, применять для этого ключевое слово override
в С# не допускается:
class MyResourceWrapper
{
// Ошибка на этапе компиляции'
protected override void Finalize () { }
}
Вместо этого для достижения того же эффекта должен применяться синтаксис
деструктора (подобно C++). Объясняется это тем, что при обработке синтаксиса финализато-
ра компилятор автоматически добавляет в неявно переопределяемый метод Finalize ()
приличное количество требуемых элементов инфраструктуры.
Финализаторы в С# очень похожи на конструкторы тем, что именуются идентично
классу, внутри которого определены. Помимо этого, они сопровождаются префиксом в
виде тильды (~). В отличие от конструкторов, однако, они никогда не снабжаются
модификатором доступа (поскольку всегда являются неявно защищенными), не принимают
никаких параметров и не могут быть перегружены (в каждом классе может
присутствовать только один финализатор).
306 Часть II. Главные конструкции программирования на С#
Ниже приведен пример написания специального финализатора для класса
MyResourceWrapper, который при вызове заставляет систему выдавать звуковой
сигнал. Очевидно, что данный пример предназначен только для демонстрационных целей.
В реальном приложении финализатор будет только освобождать любые неуправляемые
ресурсы и не будет взаимодействовать ни с какими другими управляемыми
объектами, даже теми, на которые ссылается текущий объект, поскольку рассчитывать на то,
что они все еще существуют на момент вызова данного метода Finalize () сборщиком
мусора нельзя.
// Переопределение System.Object.Finalize()
//с использованием синтаксиса финализатора.
class MyResourceWrapper
{
-MyResourceWrapper()
{
// Здесь производится очистка неуправляемых ресурсов.
// Обеспечение подачи звукового сигнала при
// уничтожении объекта (только в целях тестирования).
Console.Beep();
}
}
Если теперь просмотреть CIL-код данного деструктора с помощью утилиты ildasm.
ехе, обнаружится, что компилятор добавляет некоторый код для проверки ошибок. Во-
первых, он помещает операторы из области действия метода Finalize () в блок try (см.
главу 7), и, во-вторых, добавляет соответствующий блок finally, чтобы гарантировать
выполнение метода FinalizeO в базовых классах, какие бы исключения не возникали
в контексте try.
.method family hidebysig virtual instance void
Finalize() cil managed
// Code size 13 (Oxd)
// Размер кода 13(Oxd)
axstack 1
■try
I
IL_0000
IL_0005
IL_000a
void
IL_000f
IL_0010
IL 0011
ldc.i4 0x4e20
ldc.i4 0x3e8
call
[mscorlib]System.Co
nop
nop
leave.s IL 001b
} // end .try
finally
{
IL_0013: ldarg.O
IL_0014:
call instance void [mscorlib]System.Object::Finalize()
IL_0 019: nop
IL_001a: endfinally
} // end handler
IL 001b: nop
IL_001c: ret
} // end of method MyResourceWrapper::Finalize
// конец метода MyResourceWrapper::Finalize
Глава 8. Время жизни объектов 307
При тестировании класса MyResourceWrapper система выдает звуковой сигнал во
время завершения работы приложения, так как среда CLR автоматически вызывает фи-
нализаторы при выгрузке AppDomain.
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Finalizers *****\n");
Console.WriteLine("Hit the return key to shut down this app");
Console.WriteLine("and force the GC to invoke Finalize ()");
Console.WriteLine("for finalizable objects created in this AppDomain.");
// Нажмите клавишу <Enter>, чтобы завершить приложение
// и заставить сборщик мусора вызвать метод Finalize ()
// для всех финализируемых объектов, которые
// были созданы в домене этого приложения.
Console.ReadLine ();
MyResourceWrapper rw = new MyResourceWrapper ();
}
Исходный код. Проект SimpleFinalize доступен в подкаталоге Chapter 8.
Описание процесса финализации
Чтобы не делать лишнюю работу, следует всегда помнить, что задачей метода
Finalize () является забота о том, чтобы объект .NET мог освобождать неуправляемые
ресурсы во время сборки мусора. Следовательно, при создании типа, в котором никакие
неуправляемые сущности не используются (так бывает чаще всего), от финализации
оказывается мало толку. На самом деле, всегда, когда возможно, следует стараться
проектировать типы так, чтобы в них не поддерживался метод FinalizeO по той очень
простой причине, что выполнение финализации отнимает время.
При размещении объекта в управляемой куче исполняющая среда автоматически
определяет, поддерживается ли в нем какой-нибудь специальный метод Finalize () .
Если да, тогда она помечает его как финализируемый (finalizable) и сохраняет
указатель на него во внутренней очереди, называемой очередью финализации (finalizatlon
queue). Эта очередь финализации представляет собой просматриваемую сборщиком
мусора таблицу, где перечислены объекты, которые перед удалением из кучи должны
быть обязательно финализированы.
Когда сборщик мусора определяет, что наступило время удалить объект из памяти,
он проверяет каждую запись в очереди финализации и копирует объект из кучи в еще
одну управляемую структуру, называемую таблицей объектов, доступных для
финализации (finalization reachable table). После этого он создает отдельный поток для вызова
метода FinalizeO в отношении каждого из упоминаемых в этой таблице объектов при
следующей сборке мусора. В результате получается, что для окончательной
финализации объекта требуется как минимум два процесса сборки мусора.
Из всего вышесказанного следует, что хотя финализация объекта действительно
позволяет гарантировать способность объекта освобождать неуправляемые ресурсы, она
все равно остается недетерминированной по своей природе, и по причине
дополнительной выполняемой незаметным образом обработки протекает гораздо медленнее.
Создание высвобождаемых объектов
Как было показано, методы финализации могут применяться для освобождения
неуправляемых ресурсов при активизации процесса сборки мусора. Однако многие
неуправляемые объекты являются "ценными элементами" (например, низкоуровневые
308 Часть II. Главные конструкции программирования на С#
соединения с базой данных или файловые дескрипторы) и часто выгоднее освобождать
их как можно раньше, еще до наступления момента сборки мусора. Поэтому вместо
переопределения FinalizeO в качестве альтернативного варианта также можно
реализовать в классе интерфейс I Disposable, который имеет единственный метод по имени
Dispose() :
public interface IDisposable
{
void Dispose ();
}
Принципы программирования с использованием интерфейсов детально
рассматриваются в главе 9. Если объяснять вкратце, то интерфейс представляет собой коллекцию
абстрактных членов, которые может поддерживать класс или структура. Когда
действительно реализуется поддержка интерфейса IDisposable, то предполагается, что после
завершения работы с объектом метод Dispose () должен вручную вызываться
пользователем этого объекта, прежде чем объектной ссылке будет позволено покинуть
область действия. Благодаря этому объект может выполнять любую необходимую очистку
неуправляемых ресурсов без попадания в очередь финализации и без ожидания того,
когда сборщик мусора запустит содержащуюся в классе логику финализации.
На заметку! Интерфейс IDisposable может быть реализован как в классах, так и в структурах
(в отличие от метода Finalize (), который допускается переопределять только в классах),
потому что метод Dispose () вызывается пользователем объекта (а не сборщиком мусора).
Рассмотрим пример использования этого интерфейса. Создадим новый проект
типа Console Application по имени SimpleDispose и добавим в него следующую
модифицированную версию класса MyResourceWrapper, в которой теперь предусмотрена
реализация интерфейса IDisposable, а не переопределение метода System.Object.
Finalize ():
// Реализация интерфейса IDisposable.
public class MyResourceWrapper : IDisposable
{
// После окончания работы с объектом пользователь
// объекта должен вызывать этот метод.
public void Dispose ()
{
// Освобождение неуправляемых ресурсов. . .
// Избавление от других содержащихся внутри
//и пригодных для очистки объектов.
// Только для целей тестирования.
Console.WriteLine ("***** In Dispose1 *****");
}
}
Обратите внимание, что метод Dispose () отвечает не только за освобождение
неуправляемых ресурсов типа, но и за вызов аналогичного метода в отношении любых
других содержащихся в нем высвобождаемых объектов. В отличие от Finalize (), в нем
вполне допустимо взаимодействовать с другими управляемыми объектами. Объясняется
это очень просто: сборщик мусора не имеет понятия об интерфейсе IDisposable и
потому никогда не будет вызывать метод Dispose (). Следовательно, при вызове данного
метода пользователем объект будет все еще существовать в управляемой куче и иметь
доступ ко всем остальным находящимся там объектам. Логика вызова этого метода
выглядит довольно просто:
Глава 8. Время жизни объектов 309
class Program
{
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Dispose *****\n");
// Создание высвобождаемого объекта и вызов метода
// Dispose () для освобождения любых внутренних ресурсов.
MyResourceWrapper rw = new MyResourceWrapper ();
rw.Dispose();
Console.ReadLine ();
}
}
Разумеется, перед тем как пытаться вызывать метод Dispose () на объекте, нужно
проверить, поддерживает ли соответствующий тип интерфейс I Disposable. И хотя в
документации .NET Framework 4.0 SDK всегда доступна информация о том, какие типы
в библиотеке базовых классов реализуют I Disposable, такую проверку удобнее
выполнять программно с применением ключевого слова is или as (см. главу 6).
class Program
{
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Dispose *****\n");
MyResourceWrapper rw = new MyResourceWrapper ();
if (rw is IDisposable)
rw.Dispose();
Console.ReadLine();
}
}
Этот пример раскрывает еще одно правило относительно работы с подвергаемыми
сборке мусора типами.
Правило. Для любого создаваемого напрямую объекта, если он поддерживает интерфейс
IDisposable, следует всегда вызывать метод Dispose (). Необходимо исходить из того,
что в случае, если разработчик класса решил реализовать метод Dispose (), значит, классу
надлежит выполнять какую-то очистку.
К приведенному выше правилу прилагается одно важное пояснение. Некоторые из
типов, которые поставляются в библиотеках базовых классов и реализуют интерфейс
IDisposable, предусматривают использование для метода Dispose () (несколько
сбивающего с толку) псевдонима, чтобы заставить отвечающий за очистку метод звучать
более естественно для типа, в котором он определяется. Для примера можно взять класс
System. 10. FileStream, в котором реализуется интерфейс IDisposable (и,
следовательно, поддерживается метод Dispose ()), но при этом также определяется и метод
Close (), каковой применяется для той же цели.
// Предполагается, что было импортировано пространство имен System.10...
static void DisposeFileStream()
{
FileStream fs = new FileStream("myFile.txt", FileMode.OpenOrCreate);
// Мягко говоря, сбивает с толку!
// Вызовы этих методов делают одно и то же!
fs.Close () ;
f s . Dispose () ;
}
310 Часть II. Главные конструкции программирования на С#
И хотя "закрытие" (close) файла действительно звучит более естественно, чем его
"освобождение" (dispose), нельзя не согласиться с тем, что подобное дублирование
отвечающих за одно и то же методов вносит путаницу. Поэтому при использовании этих
нескольких типов, в которых применяются псевдонимы, просто помните о том, что если
тип реализует интерфейс IDisposable, то вызов метода Dispose () всегда является
правильным образом действия.
Повторное использование ключевого слова using в С#
При работе с управляемым объектом, который реализует интерфейс IDisposable,
довольно часто требуется применять структурированную обработку исключений,
гарантируя, что метод Dispose () типа будет вызываться даже в случае возникновения
какого-то исключения:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Dispose *****\n");
MyResourceWrapper rw = new MyResourceWrapper();
try
{
// Использование членов rw.
}
finally
{
// Обеспечение вызова метод Dispose() в любом случае,
//в том числе при возникновении ошибки.
rw.Dispose ();
}
}
Хотя это является замечательными примером "безопасного программирования",
истина состоит в том, что очень немногих разработчиков прельщает перспектива
заключать каждый очищаемый тип в блок try/finally лишь для того, чтобы гарантировать
вызов метода Dispose (). Для достижения аналогичного результата, но гораздо менее
громоздким образом, в С# поддерживается специальный фрагмент синтаксиса,
который выглядит следующим образом:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Dispose *****\n");
// Метод Dispose() вызывается автоматически
// при выходе за пределы области действия using.
using(MyResourceWrapper rw = new MyResourceWrapper())
{ .
// Использование объекта rw.
}
}
Если теперь просмотреть CIL-код этого метода Main () с помощью утилиты ildasm. ехе,
то обнаружится, что синтаксис using в таких случаях на самом деле расширяется до
логики try/finally, которая включает в себя и ожидаемый вызов Dispose ():
.method private hidebysig static void Main(string [ ] args) cil managed
{
. try
{
} // end .try
Глава 8. Время жизни объектов 311
finally
{
IL_0012: callvirt instance void
SimpleFinalize.MyResourceWrapper::Dispose()
} // end handler
} // end of method Program::Main
На заметку! При попытке применить using к объекту, который не реализует интерфейс
IDisposable, на этапе компиляции возникнет ошибка.
Хотя применение такого синтаксиса действительно избавляет от необходимости
вручную помещать высвобождаемые объекты в рамки try/ finally, в настоящее время,
к сожалению, ключевое слово using в С# имеет двойное значение (поскольку служит и
для добавления ссылки на пространства имен, и для вызова метода Dispose ()). Тем не
менее, при работе с типами.NET, которые поддерживают интерфейс IDisposable,
данная синтаксическая конструкция будет гарантировать автоматический вызов метода
Dispose () в отношении соответствующего объекта при выходе из блока using.
Кроме того, в контексте using допускается объявлять несколько объектов одного и
того же типа. Как не трудно догадаться, в таком случае компилятор будет вставлять
код с вызовом Dispose () для каждого объявляемого объекта.
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Dispose *****\nM);
// Использование разделенного запятыми списка для
// объявления нескольких подлежащих освобождению объектов.
using(MyResourceWrapper rw = new MyResourceWrapper(),
rw2 = new MyResourceWrapper())
{
// Использование объектов rw и rw2.
}
}
Исходный код. Проект SimpleDispose доступен в подкаталоге Chapter 8.
Создание финализируемых
и высвобождаемых типов
К этому моменту были рассмотрены два различных подхода, которые можно
применять для создания класса, способного производить очистку и освобождать
внутренние неуправляемые ресурсы. Первый подход заключается в переопределении метода
System. Ob ject.Finalize() и позволяет гарантировать то, что объект будет очищать
себя сам во время процесса сборки мусора (когда бы тот не запускался) без
вмешательства со стороны пользователя. Второй подход предусматривает реализацию
интерфейса IDisposable и позволяет обеспечить пользователя объекта возможностью очищать
объект сразу же по окончании работы с ним. Однако если пользователь забудет вызвать
метод Dispose (), неуправляемые ресурсы могут оставаться в памяти на
неопределенный срок.
Как не трудно догадаться, оба подхода можно комбинировать и применять вместе
в определении одного класса, получая преимущества от обеих моделей. Если пользо-
312 Часть II. Главные конструкции программирования на С#
ватель объекта не забыл вызвать метод Dispose (), можно проинформировать
сборщик мусора о пропуске финализации, вызвав метод GC.SuppressFinalize (). Если
же пользователь забыл вызвать этот метод, объект рано или поздно будет подвергнут
финализации и получит возможность освободить внутренние ресурсы. Преимущество
такого подхода в том, что при этом внутренние ресурсы будут так или иначе, но всегда
освобождаться.
Ниже приведена очередная версия класса MyResourceWrapper, которая теперь
предусматривает выполнение и финализации и освобождения и содержится внутри
проекта типа Console Application по имени FinalizableDisposableClass.
// Сложный упаковщик ресурсов.
public class MyResourceWrapper : IDisposable
{
// Сборщик мусора будет вызывать этот метод, если
// пользователь объекта забыл вызвать метод Dispose () .
~MyResourceWrapper()
{
// Освобождение любых внутренних неуправляемых
// ресурсов. Метод Dispose () HE должен вызываться
//ни для каких управляемых объектов.
}
// Пользователь объекта будет вызывать этот метод для
// того, чтобы освободить ресурсы как можно быстрее.
public void Dispose ()
{
// Здесь осуществляется освобождение неуправляемых ресурсов
//и вызов Dispose()для остальных высвобождаемых объектов.
// Если пользователь вызвал Dispose (), то финализация не нужна,
// поэтому далее она подавляется.
GC.SuppressFinalize(this);
}
}
Здесь важно обратить внимание на то, что метод Dispose () был модифицирован
так, чтобы вызывать метод GC . SuppressFinalize (). Этот метод информирует CLR-
среду о том, что вызывать деструктор при подвергании данного объекта сборке мусора
больше не требуется, поскольку неуправляемые ресурсы уже были освобождены
посредством логики Dispose ().
Формализованный шаблон очистки
В текущей реализации MyResourceWrapper работает довольно хорошо, но все
равно еще остается несколько небольших недочетов. Во-первых, методам Finalize () и
Dispose () требуется освобождать одни и те же неуправляемые ресурсы, а это
чревато дублированием кода, которое может существенно усложнить его сопровождение.
Поэтому в идеале не помешало бы определить приватную вспомогательную функцию,
которая могла бы вызываться в любом из этих методов.
Во-вторых, нелишне позаботиться о том, чтобы метод Finalize () не пытался
избавиться от любых управляемых объектов, а метод Dispose () — наоборот, обязательно
это делал. И, наконец, в-третьих, не помешало бы позаботиться о том, чтобы
пользователь объекта мог спокойно вызывать метод Dispose () множество раз без получения
ошибки. В настоящий момент в методе Dispose () никаких подобных мер
предосторожностей пока не предусмотрено.
Для решения подобных вопросов с дизайном в Microsoft создали формальный
шаблон очистки, который позволяет достичь оптимального баланса между надежностью,
Глава 8. Время жизни объектов 313
удобством в обслуживании и производительностью. Ниже приведена окончательная
версия MyResourceWrapper, в которой применяется упомянутый формальный шаблон.
public class MyResourceWrapper : IDisposable
{
// Используется для выяснения того, вызывался ли уже метод Dispose () .
private bool disposed = false;
public void Dispose ()
{
// Вызов вспомогательного метода.
// Значение true указывает на то, что очистка
// была инициирована пользователем объекта.
Cleanup(true);
// Подавление финализации.
GC.SuppressFinalize (this);
}
private void Cleanup(bool disposing)
{
// Проверка, выполнялась ли очистка.
if (! this.disposed)
{
// Если disposing равно true, должно осуществляться
// освобождение всех управляемых ресурсов,
if (disposing)
{
// Здесь осуществляется освобождение управляемых ресурсов.
}
// Очистка неуправляемых ресурсов.
}
disposed = true;
}
-MyResourceWrapper()
{
// Вызов вспомогательного метода.
// Значение false указывает на то, что
// очистка была инициирована сборщиком мусора.
Cleanup(false);
}
}
Обратите внимание, что в MyResourceWrapper теперь определяется приватный
вспомогательный метод по имени Cleanup (). Передача ему в качестве аргумента значения
true свидетельствует о том, что очистку инициировал пользователь объекта,
следовательно, требуется освободить все управляемые и неуправляемые ресурсы. Когда очистка
инициируется сборщиком мусора, при вызове Cleanup () передается значение false,
чтобы освобождения внутренних высвобождаемых объектов не происходило (поскольку
рассчитывать на то, что они по-прежнему находятся в памяти, нельзя). И, наконец,
перед выходом из Cleanup () для переменной экземпляра типа bool (по имени disposed)
устанавливается значение true, что дает возможность вызывать метод Dispose ()
много раз без появления ошибки.
На заметку! После "освобождения" (dispose) объекта клиент по-прежнему может вызывать на нем
какие-нибудь члены, поскольку объект пока еще находится в памяти. Следовательно, в
показанном сложном классе-упаковщике ресурсов не помешало бы снабдить каждый член
дополнительной логикой, которая бы, по сути, гласила: "если объект освобожден, ничего не делать,
а просто вернуть управление".
314 Часть II. Главные конструкции программирования на С#
Чтобы протестировать последнюю версию класса MyResourceWrapper, добавим в
метод финализации вызов Console. Веер ():
~MyResourceWrapper()
{
Console.Веер();
// Вызов вспомогательного метода.
// Указание значения false свидетельствует о том,
// что очистка была инициирована сборщиком мусора.
Cleanup(false);
}
Давайте обновим метод Main () следующим образом:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Dispose() / Destructor Combo Platter *****");
// Вызов метода Dispose () вручную; метод финализации
//в таком случае вызываться не будет.
MyResourceWrapper rw = new MyResourceWrapper();
rw.Dispose ();
// Пропуск вызова метода Dispose () ; в таком случае будет
// вызываться метод финализации и выдаваться звуковой сигнал.
MyResourceWrapper rw2 = new MyResourceWrapper() ;
}
Обратите внимание, что в первом случае производится явный вызов метода
Dispose () на объекте rw и потому вызов деструктора подавляется. Однако во втором
случае мы "забываем" вызвать метод Dispose () на объекте rw2 и потому по окончании
выполнения приложения услышим однократный звуковой сигнал. Если
закомментировать вызов метода Dispose () на объекте rw, звуковых сигналов будет два.
Исходный код. Проект FinalizableDisposableClass доступен в подкаталоге Chapter 8.
На этом тема управления объектами CLR-средой посредством сборки мусора
завершена. И хотя некоторые дополнительные (и довольно экзотические) детали,
касающиеся сборки мусора (вроде слабых ссылок и восстановления объектов), остались не
рассмотренными, полученных базовых сведений должно оказаться достаточно, чтобы
продолжить изучение самостоятельно. Напоследок в главе предлагается исследовать
совершенно новую функциональную возможность, появившуюся в .NET 4.0, которая
называется отложенной (ленивой) инициализацией.
Отложенная инициализация объектов
На заметку! В настоящем разделе предполагается наличие у читателя знаний о том, что собой
представляют обобщения и делегаты в .NET. Исчерпывающие сведения о делегатах и
обобщениях можно найти в главах 10 и 11.
При создании классов иногда возникает необходимость предусмотреть в коде
определенную переменную-член, которая на самом деле может никогда не понадобиться из-за
того, что пользователь объекта не будет обращаться к методу (или свойству), в котором
она используется. Вполне разумное решение, однако, на практике его реализация
может оказываться очень проблематичной в случае, если инициализация интересующей
переменной экземпляра требует большого объема памяти.
Глава 8. Время жизни объектов 315
Для примера представим, что требуется создать класс, инкапсулирующий операции
цифрового музыкального проигрывателя, и помимо ожидаемых методов вроде Play (),
Pause () и Stop () его нужно также обеспечить способностью возврата коллекции
объектов Song (через класс по имени All Tracks), которые представляют каждый из
имеющихся в устройстве цифровых музыкальных файлов.
Чтобы получить такой класс, создадим новый проект типа Console Application no
имени La z у Object Instantiation и добавим в него следующие определения типов
классов:
// Представляет одну композицию.
class Song
{
public string Artist { get; set; }
public string TrackName { get; set; }
public double TrackLength { get; set; }
}
// Представляет все композиции в проигрывателе.
class AllTracks
{
//В нашем проигрывателе может содержаться
// максимум 10 000 композиций.
public AllTracks ()
{
// Предполагаем, что здесь производится заполнение
// массива объектов Song.
Console. WriteLine ("Filling up the songs!11);
}
}
// Класс MediaPlayer включает объект AllTracks.
class MediaPlayer
{
// Предполагаем, что эти методы делают нечто полезное.
public void Play() { /* Воспроизведение композиции */ }
public void Pause() { /* Приостановка воспроизведения композиции */ }
public void Stop() { /* Останов воспроизведения композиции */ }
private AllTracks allSongs = new AllTracks();
public AllTracks GetAllTracks()
{
// Возвращаем все композиции.
return allSongs;
}
}
В текущей реализации MediaPlayer делается предположение о том, что
пользователю объекта понадобится получать список объектов с помощью метода GetAllTracks ().
А что если пользователю объекта не нужен этот список? Так или иначе, но переменная
экземпляра AllTracks будет приводить к созданию 10 000 объектов Song в памяти:
static void Main(string [ ] args)
{
//В этом вызывающем коде получение всех композиций не
// производится, но косвенно все равно создаются
// 10 000 объектов!
MediaPlayer myPlayer = new MediaPlayer();
myPlayer.Play();
Console.ReadLine ();
316 Часть II. Главные конструкции программирования на С#
Понятно, что создания 10 000 объектов, которыми никто не будет пользоваться,
лучше избежать, так как это изрядно прибавит работы сборщику мусора .NET. Хотя можно
вручную добавить код, который обеспечит создание объекта all Songs только в случае
его использования (например, за счет применения шаблона с методом фабрики),
существует и более простой путь.
С выходом .NET 4.0 в библиотеках базовых классов появился очень интересный
обобщенный класс по имени Lazy о, который находится в пространстве имен System
внутри сборки mscorlib.dll. Этот класс позволяет определять данные, которые не
должны создаваться до тех пор, пока они на самом деле не начнут использоваться в
кодовой базе. Поскольку он является обобщенным, при первом использовании в нем
должен быть явно указан тип элемента, который должен создаваться. Этим типом
может быть как любой из типов, определенных в библиотеках базовых классов .NET, так
и специальный тип, самостоятельно созданный разработчиком. Для обеспечения
отложенной инициализации переменной экземпляра AllTracks можно просто заменить
следующий фрагмент кода:
// Класс MediaPlayer включает объект AllTracks
class MediaPlayer
{
private AllTracks allSongs = new AllTracks ();
public AllTracks GetAllTracks()
{
// Возврат всех композиций.
return allSongs;
}
}
таким кодом:
// Класс MediaPlayer включает объект Lazy<AllTracks>.
class MediaPlayer
{
private Lazy<AllTracks> allSongs = new Lazy<AllTracks>();
public AllTracks GetAllTracks()
{
// Возврат всех композиций.
return allSongs.Value;
}
}
Помимо того, что переменная экземпляра AllTrack теперь имеет тип Lazyo, важно
отметить, что реализация предыдущего метода GetAllTracks () тоже изменилась. В
частности, теперь требуется использовать доступное только для чтения свойство Value
класса Lazy о для получения фактических хранимых данных (в этом случае — объект
AllTracks, обслуживающий 10 000 объектов Song).
Кроме того, обратите внимание, как благодаря этому простому изменению,
показанный ниже модифицированный метод Main() будет незаметно размещать
объекты Song в памяти только в случае, когда был действительно выполнен вызов метода
GetAllTracks().
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Lazy Instantiation *****\n");
// Никакого размещения объекта AllTracks!
MediaPlayer myPlayer = new MediaPlayer();
myPlayer.Play();
Глава 8. Время жизни объектов 317
// Размещение объекта AllTracks происходит
// только в случае вызова метода GetAllTracks().
MediaPlayer yourPlayer = new MediaPlayer();
AllTracks yourMusic = yourPlayer.GetAllTracks();
Console.ReadLine();
}
Настройка процесса создания данных Lazyo
При объявлении переменной Lazyo фактический внутренний тип данных
создается с помощью конструктора по умолчанию:
// Конструктор по умолчанию для AllTracks вызывается
// при использовании переменной LazyO.
private Lazy<AllTracks> allSongs = new Lazy<AllTracks>();
В некоторых случаях подобное поведение может быть вполне подходящим, но что
если класс AllTracks имеет дополнительные конструкторы, и нужно позаботиться о
том, чтобы вызывался подходящий из них? Более того, а что если необходимо, чтобы
при создании переменной Lazy () выполнялась какая-то дополнительная работа
(помимо просто создания объекта AllTracks)? К счастью, класс Lazy () позволяет
предоставлять в качестве необязательного параметра обобщенный делегат, который указывает,
какой метод должен вызываться во время создания находящегося внутри него типа.
Этим обобщенным делегатом является тип System. Funco, который может
указывать на метод, возвращающий тот же тип данных, что создается соответствующей
переменной Lazyo, и способный принимать вплоть до 16 аргументов (которые
типизируются с помощью параметров обобщенного типа). В большинстве случаев необходимость
указывать параметры для передачи методу, на который ссылается Fun со, возникать
не будет Более того, чтобы значительно упростить применение Fun со , лучше
использовать лямбда-выражения (отношения между делегатами и лямбда-выражениями
подробно рассматриваются в главе 11).
С учетом всего сказанного, ниже приведена окончательная версия MediaPlayer, в
которой теперь при создании внутреннего объекта AllTracks добавляется небольшой
специальный код. Следует запомнить, что данный метод должен обязательно
возвращать новый экземпляр указанного в Lazyo типа перед выходом, а использовать
можно любой конструктор (в коде для AllTracks по-прежнему вызывается конструктор по
умолчанию).
class MediaPlayer
{
// Использование лямбда-выражения для добавления
// дополнительного кода при создании объекта AllTracks.
private Lazy<AllTracks> allSongs = new Lazy<AllTracks> ( () =>
{
Console.WriteLine ("Creating AllTracks object!11);
return new AllTracks ();
}
) ;
public AllTracks GetAllTracks ()
{
// Возврат всех композиций.
return allSongs.Value;
}
}
318 Часть II. Главные конструкции программирования на С#
Хочется надеяться, что удалось продемонстрировать выгоду, которую способен
обеспечить класс Lazyo. В сущности, этот новый обобщенный класс позволяет
делать так, чтобы дорогостоящие объекты размещались в памяти только тогда, когда
они будут действительно нужны пользователю объекта. Если эта тема заинтересовала,
можно заглянуть в посвященный классу System. Lazyo раздел в документации .NET
Framework 4.0 SDK и найти там дополнительные примеры программирования
отложенной инициализации.
Исходный код. Проект LazyObjectInstantiation доступен в подкаталоге Chapter 8.
Резюме
Цель настоящей главы состояла в разъяснении, что собой представляет процесс
сборки мусора. Было показано, что сборщик мусора активизируется только тогда,
когда ему не удается получить необходимый объем памяти из управляемой кучи (или
когда происходит выгрузка домена соответствующего приложения из памяти). После его
активизации поводов для волнения возникать не должно, поскольку разработанный в
Microsoft алгоритм сборки мусора хорошо оптимизирован и предусматривает
использование поколений объектов, дополнительных потоков для финализации объектов и
управляемой кучи для обслуживания больших объектов.
В главе также было показано, каким образом программно взаимодействовать со
сборщиком мусора, применяя класс System.GC. Как отмечалось, единственным
случаем, когда в подобном может возникнуть необходимость, является создание финализи-
руемых или высвобождаемых типов классов, в которых используются неуправляемые
ресурсы.
Вспомните, что под финализируемыми типами подразумеваются классы, в
которых переопределен виртуальный метод System.Object.FinalizeO для обеспечения
очистки неуправляемых ресурсов на этапе сборки мусора, а под высвобождаемыми —
классы (или структуры), в которых реализован интерфейс IDisposable, вызываемый
пользователем объекта по окончании работы с ним. Кроме того, в главе был
продемонстрирован формальный шаблон "очистки", позволяющий совмещать оба эти подхода.
И, наконец, в главе был описан появившийся в .NET 4.0 обобщенный класс по
имени Lazyo. Как здесь было показано, данный класс позволяет отложить создание
дорогостоящих (в плане потребления памяти) объектов до тех пор, пока у вызывающей
стороны действительно не возникнет потребность в их использовании. Это помогает
сократить количество объектов, сохраняемых в управляемой куче, и облегчает нагрузку
на сборщик мусора.
ЧАСТЬ III
Дополнительные
конструкции
программирования
наС#
В этой части...
Глава 9. Работа с интерфейсами
Глава 10. Обобщения
Глава 11. Делегаты, события и лямбда-выражения
Глава 12. Расширенные средства языка С#
Глава 13. LINQ to Objects
ГЛАВА 9
Работа с интерфейсами
Материал этой главы основан на начальных знаниях объектно-ориентированной
разработки и посвящен концепциям программирования с использованием
интерфейсов. В главе будет показано, как определять и реализовать интерфейсы, а также
описаны преимущества, которые дает построение типов, поддерживающих несколько
видов поведения. Также будут рассмотрены и другие связанные с этим темы, наподобие
того, как получать ссылки на интерфейсы, как реализовать интерфейсы явным образом
и как создавать иерархии интерфейсов.
Помимо этого, конечно же, будут описаны стандартные интерфейсы, которые
предлагаются в библиотеках базовых классов .NET. Как будет показано, специальные классы
и структуры могут реализовать эти готовые интерфейсы, что позволяет поддерживать
несколько дополнительных поведений, таких как клонирование, перечисление и
сортировка объектов.
Что собой представляют типы интерфейсов
Для начала ознакомимся с формальным определением типа интерфейса.
Интерфейс (interface) представляет собой не более чем просто именованный набор
абстрактных членов. Как упоминалось в главе 6, абстрактные методы являются чистым
протоколом, поскольку не имеют никакой стандартной реализации. Конкретные члены,
определяемые интерфейсом, зависят от того, какое поведение моделируется с его
помощью. Это действительно так. Интерфейс выражает поведение, которое данный класс
или структура может избрать для поддержки. Более того, как будет показано далее в
главе, каждый класс (или структура) может поддерживать столько интерфейсов, сколько
необходимо, и, следовательно, тем самым поддерживать множество поведений.
Нетрудно догадаться, что в библиотеках базовых классов .NET поставляются сотни
предопределенных типов интерфейсов, которые реализуются в различных классах и
структурах. Например, как будет показано в главе 21, в состав ADO.NET входит
множество поставщиков данных, которые позволяют взаимодействовать с определенной
системой управления базами данных. Это означает, что в ADO.NET на выбор доступно
множество объектов соединения (SqlConnection, OracleConnection, OdbcConnection
и т.д.).
Несмотря на то что каждый из этих объектов соединения имеет уникальное имя,
определяется в отдельном пространстве имен и (в некоторых случаях) упаковывается в
отдельную сборку, все они реализуют один общий интерфейс IDbConnection:
// Интерфейс IDbConnection имеет типичный ряд членов,
// которые поддерживают все объекты соединения.
public interface IDbConnection : IDisposable
{
Глава 9. Работа с интерфейсами 321
// Методы
IDbTransaction BeginTransaction ();
IDbTransaction BeginTransaction(IsolationLevel ll);
void ChangeDatabase(string databaseName);
void Close (_) ;
IDbCommand CreateCommand();
void Open ();
// Свойства
string ConnectionString { get; set;}
int ConnectionTimeout { get; }
string Database { get; }
ConnectionState State { get; }
}
На заметку! По соглашению имена всех интерфейсов в .NET сопровождаются префиксом в виде
заглавной буквы "I". При создании собственных специальных интерфейсов рекомендуется тоже
следовать этому соглашению.
Вдумываться, что именно делают все эти члены, пока не нужно. Сейчас главное
просто понять, что в интерфейсе IDbConnection предлагается набор членов, которые
являются общими для всех объектов соединений ADO.NET. Исходя из этого, можно точно
знать, что каждый объект соединения поддерживает такие члены, как Open(),Close(),
CreateCommand () и т.д. Более того, поскольку методы этого интерфейса всегда
являются абстрактными, в каждом объекте соединения они могут быть реализованы
собственным уникальным образом.
Другим примером может служить пространство имен System. Windows . Forms. В этом
пространстве имени определен класс по имени Control, который является базовым
для целого ряда интерфейсных элементов управления Windows Forms (DataGridView,
Label, StatusBar, TreeView и т.д.). Этот класс реализует интерфейс IDropTarget,
определяющий базовую функциональность перетаскивания:
public interface IDropTarget
{
// Методы
void OnDragDrop(DragEventArgs e);
void OnDragEnter(DragEventArgs e) ;
void OnDragLeave(EventArgs e);
void OnDragOver(DragEventArgs e) ;
}
С учетом этого интерфейса можно верно предполагать, что любой класс, который
расширяет System. Windows . Forms . Control, будет поддерживать четыре метода с
именами OnDragDrop(), OnDragEnter(), OnDragLeave() и OnDragOver().
В остальной части книги будут встречаться десятки таких интерфейсов,
поставляемых в библиотеках базовых классов .NET. Эти интерфейсы могут быть реализованы
в собственных классах и структурах, чтобы получать типы, тесно интегрированные с
.NET.
Сравнение интерфейсов и абстрактных базовых классов
Из-за материала, приведенного в главе 6, тип интерфейса может показаться очень
похожим на абстрактный базовый класс. Вспомните, что когда класс помечается как
абстрактный, в нем может определяться любое количество абстрактных членов для
предоставления полиморфного интерфейса для всех производных типов. Даже если в
типе класса действительно определяется набор абстрактных членов, в нем также может
322 Часть III. Дополнительные конструкции программирования на С#
спокойно определяться любое количество конструкторов, полей, неабстрактных членов
(с реализацией) и т.п. Интерфейсы, с другой стороны, могут содержать только
определения абстрактных членов.
Полиморфный интерфейс, предоставляемый абстрактным родительским классом,
обладает одним серьезным ограничением: определяемые в абстрактном родительском
классе члены поддерживаются только в производных типах. В более крупных
программных системах, однако, довольно часто разрабатываются многочисленные
иерархии классов, не имеющие никаких общих родительских классов кроме System.Object.
Из-за того, что абстрактные члены в абстрактном базовом классе подходят только для
производных типов, получается, что настраивать типы в разных иерархиях так, чтобы
они поддерживали один и тот же полиморфный интерфейс, невозможно. Для примера
предположим, что определен следующий абстрактный класс:
abstract class CloneableType
{
// Только производные типы могут поддерживать этот
// "полиморфный интерфейс". Классы в других иерархиях
//не будут иметь доступа к этому абстрактному члену.
public abstract object Clone ();
}
Из-за такого определения поддерживать метод Clone () могут только члены, которые
расширяют CloneableType. В случае создания нового набора классов, которые не
расширяют CloneableType, использовать в них этот полиморфный интерфейс,
соответственно, не получится. Кроме того, как уже неоднократно упоминалось, в С# не
поддерживается возможность наследования от нескольких классов. Следовательно, создать класс
MiniVan, унаследованный и от Саг, и от CLoneableType, тоже не получится:
// В С# наследование от нескольких классов не допускается.
public class MiniVan : Car, CloneableType
{
}
Нетрудно догадаться, что здесь на помощь приходят типы интерфейсов. После
определения интерфейс может быть реализован в любом типе или структуре, в любой
иерархии и внутри любого пространства имен или сборки (написанной на любом языке
программирования .NET). Очевидно, что это делает интерфейсы чрезвычайно
полиморфными. Для примера рассмотрим стандартный интерфейс .NET по имени ICloneable,
определенный в пространстве имен System. Этот интерфейс имеет единственный метод
Clone ():
public interface ICloneable
{
object Clone ();
}
Если заглянуть в документацию .NET Framework 4.0 SDK, можно обнаружить, что
очень многие по виду несвязанные типы (System. Array, System. Data . SqlClient.
SqlConnection, System.OperatingSystem, System. String и т.д.) реализуют этот
интерфейс. В результате, хотя у этих типов нет общего родителя (кроме System. Object), с
ними все равно можно работать полиморфным образом через интерфейс ICloneable.
Например, если есть метод по имени С1опеМе(), принимающий интерфейс
ICloneable в качестве параметра, этому методу можно передавать любой объект,
который реализует упомянутый интерфейс. Рассмотрим следующий простой класс Program,
определенный в проекте типа Console Application (Консольное приложение) по имени
ICloneableExample:
Глава 9. Работа с интерфейсами 323
class Program
{
static void Main(string [ ] args)
{
Console,WriteLine("***** A First Look at Interfaces *****\n");
// Все эти классы поддерживают интерфейс ICloneable.
string myStr = "Hello";
OperatingSystem unixOS =
new OperatingSystem(PlatformID.Unix, new Version ());
System.Data.SqlClient.SqlConnection sqlCnn =
new System.Data.SqlClient.SqlConnection ();
// Поэтому все они могут передаваться методу,
// принимающему ICloneable в качестве параметра.
CloneMe(myStr);
CloneMe(unixOS);
CloneMe(sqlCnn);
Console.ReadLine ();
}
private static void CloneMe(ICloneable c)
{
// Клонируем любой получаемый тип
//и выводим его имя в окне консоли.
object theClone = c.CloneO;
Console.WriteLine("Your clone is a: {0}",
theClone.GetType().Name);
}
}
При запуске этого приложения в окне консоли будет выводиться имя каждого класса
посредством метода GetType (), унаследованного от System.Object. (В главе 16 более
подробно рассказывается об этом методе и предлагаемых в .NET службах рефлексии.)
Исходный код. Проект ICloneableExample доступен в подкаталоге Chapter 9.
Другим ограничением традиционных абстрактных базовых классов является то, что
в каждом производном типе должен обязательно поддерживаться соответствующий
набор абстрактных членов и предоставляться их реализация. Чтобы увидеть, в чем
заключается проблема, давайте вспомним иерархию фигур, которая приводилась в главе 6.
Предположим, что в базовом классе Shape определен новый абстрактный метод по
имени GetNumberOf Points (), позволяющий производным типам возвращать информацию
о количестве вершин, которое требуется для визуализации фигуры:
abstract class Shape
{
// Каждый производный класс теперь должен
// обязательно поддерживать такой метод!
public abstract byte GetNumberOfPoints ();
}
Очевидно, что изначально единственным типом, который в принципе имеет
вершины, является Hexagon. Но теперь, из-за внесенного обновления, каждый производный
класс (Circle, Hexagon и ThreeDCircle) должен предоставлять конкретную
реализацию метода GetNumberOf Points (), даже если в этом нет никакого смысла.
В этом случае снова на помощь приходит тип интерфейса. Определив интерфейс,
представляющий поведение "наличие вершин", можно будет просто вставить его в тип
Hexagon и не трогать типы Circle и ThreeDCircle.
324 Часть III. Дополнительные конструкции программирования на С#
Определение специальных интерфейсов
Теперь, имея более четкое представление о роли интерфейсов, давайте рассмотрим
пример определения и реализации специальных интерфейсов. Создадим новый проект
типа Console Application по имени Customlnterface и, выбрав в меню Project (Проект)
пункт Add Existing Item (Добавить существующий элемент), вставим в него файл или
файлы, содержащие определения типов фигур (файл Shapes.cs в примерах кода),
которые были созданы в главе 6. После этого переименуем пространство имен, в котором
содержатся определения отвечающих за фигуры типов, в Customlnterface (просто
чтобы не импортировать их из этого пространства имен в новый проект):
namespace Customlnterface
{
// Здесь должны идти определения
// созданных ранее типов фигур...
}
Теперь вставим в проект новый интерфейс по имени I Pointy, выбрав в меню Project
пункт Add Existing Item, как показано на рис. 9.1.
Name: IPoinlycs
Add Cancel
Рис. 9.1. Интерфейсы, как и классы, могут определяться в любом файле * . cs
На синтаксическом уровне любой интерфейс определяется с помощью ключевого
слова interface. В отличие от классов, базовый класс (даже System.Object) для
интерфейсов никогда не указывается (хотя, как будет показано позже в главе, базовые
интерфейсы указываться могут). Более того, модификаторы доступа для членов
интерфейсов тоже никогда не указываются (поскольку все члены интерфейсов всегда
являются неявно общедоступными и абстрактными). Ниже показано, как должно выглядеть
определение специального интерфейса IPointyHaC#:
// Этот интерфейс определяет поведение "наличие вершин".
public interface IPointy
{
// Этот член является неявно общедоступным и абстрактным.
byte GetNumberOfPoints ();
}
Глава 9. Работа с интерфейсами 325
Вспомните, что при определении членов интерфейсов область их реализации не
задается. Интерфейсы являются чистым протоколом, и потому реализация в них
никогда не предоставляется (за это отвечает поддерживающий класс или структура).
Следовательно, использование показанной ниже версии I Pointy привело бы к
возникновению различных ошибок на этапе компиляции:
// Внимание! Полно ошибок!
public interface IPointy
{
// Ошибка! Интерфейсы не могут иметь поля!
public int numbOfPoints;
// Ошибка! Интерфейсы не могут иметь конструкторы!
public IPointy() { numbOfPoints = 0;};
// Ошибка! В интерфейсах не может предоставляться
// реализация методов!
byte GetNumberOfPoints () { return numbOfPoints; }
}
В любом случае, в начальном интерфейсе IPointy определен только один метод.
Однако в .NET допустимо определять в типах интерфейсов любое количество
прототипов свойств. Например, можно было бы создать интерфейс IPointy так, чтобы в нем
использовалось доступное только для чтения свойство, а не традиционный метода
доступа:
// Определение в IPointy свойства, доступного только для чтения.
public interface IPointy
{
// Свойство, доступное как для чтения, так и для записи,
//в этом интерфейсе может выглядеть так:
// retVal PropName { get; set; }
//а свойство, доступное только для записи - так:
// retVal PropName { set; }
byte Points { get; }
}
На заметку! Типы интерфейсов также могут содержать определения событий (см. главу 11) и
индексаторов (см. главу 12).
Сами по себе типы интерфейсов довольно бесполезны, поскольку представляют
собой не более чем просто именованную коллекцию абстрактных членов. Например,
размещать типы интерфейсов таким же образом, как классы или структуры, не
разрешается:
// Внимание! Размещать типы интерфейсов не допускается!
static void Main(string[] args)
{
IPointy p = new IPointy(); // Компилятор сообщит об ошибке!
}
Интерфейсы ничего особого не дают, если не реализуются в каком-то классе или
структуре. Здесь IPointy представляет собой интерфейс, который выражает поведение
"наличие вершин". Стоящая за его созданием идея выглядит довольно просто:
некоторые классы в иерархии фигур (например, Hexagon) должны иметь вершины, а
некоторые (вроде Circle) — нет.
326 Часть III. Дополнительные конструкции программирования на С#
Реализация интерфейса
Чтобы расширить функциональность какого-то класса или структуры за счет
обеспечения в нем поддержки интерфейсов, необходимо предоставить в его определении
список соответствующих интерфейсов, разделенных запятыми. Следует иметь в виду,
что непосредственный базовый класс должен быть обязательно перечислен в этом
списке первым, сразу же после двоеточия. Когда тип класса наследуется прямо от
System.Object, допускается перечислять только лишь поддерживаемый им интерфейс
или интерфейсы, поскольку компилятор С# автоматически расширяет типы
возможностями System.Object в случае, если не было указано иначе. Из-за того, что
структуры всегда наследуются от класса System. ValueType (см. главу 4), интерфейсы просто
должны перечисляться после определения структуры. Ниже приведены примеры.
// Этот класс унаследован от System.Object
//и реализует единственный интерфейс.
public class Pencil : IPointy
// Этот класс тоже унаследован от System.Object
//и реализует единственный интерфейс.
public class SwitchBlade : object, IPointy
// Этот класс унаследован от специального базового
// класса и реализует единственный интерфейс.
public class Fork : Utensil, IPointy
// Эта структура неявно унаследована от System.ValueType
//и реализует два интерфейса.
public struct Arrow : IClonable, IPointy
Важно понимать, что реализация интерфейса работает по принципу "все или
ничего". Поддерживающий тип не способен выбирать, какие члены должны быть
реализованы, а какие — нет. Из-за того, что в интерфейсе IPointy определено лишь одно
доступное только для чтения свойство, нагрузка на поддерживающий тип получается не
такой уж большой. Однако в случае реализации интерфейса с десятью членами (такого
как показанный ранее IDbConnection) типу придется отвечать за воплощение деталей
всех десяти абстрактных сущностей.
Давайте вернемся к рассматриваемому примеру и добавим в него новый тип класса
по имени Triangle, унаследованный от Shape и поддерживающий интерфейс IPointy.
Обратите внимание, что реализация доступного только для чтения свойства Points в
нем предусматривает просто возврат соответствующего количества вершин (в данном
случае 3).
// Новый производный от Shape класс по имени Triangle.
class Triangle : Shape, IPointy
{
public Triangle () { }
public Triangle(string name) : base(name) { }
public override void Draw()
{ Console.WriteLine("Drawing {0} the Triangle", PetName); }
}
Глава 9. Работа с интерфейсами 327
// Реализация интерфейса IPointy.
public byte Points
{
get { return 3; }
}
}
Модифицируем существующий тип Hexagon так, чтобы он тоже поддерживал
интерфейс IPointy:
// Hexagon теперь реализует IPointy.
class Hexagon : Shape, IPointy
public Hexagon () { }
public Hexagon(string name)
public override void Draw()
{ Console.WriteLine("Drawing {0
base(name){ }
the Hexagon", PetName);
// Реализация интерфейса IPointy.
public byte Points
get { return 6; }
}
}
Чтобы подвести итог всему изученному к этому моменту, на рис. 9.2 приведена
созданная с помощью Visual Studio 2010 диаграмма классов, на которой все совместимые
с IPointy классы представлены с помощью обозначения в виде "леденца на палочке".
Обратите внимание на диаграмме, что в Circle и ThreeDCircle интерфейс IPointy
не реализован, потому что предоставляемое им поведение для этих классов не имеет
смысла.
Shape
Abstract Class
:;
J IPointy
J=
Circle
Class
■+ Shape
i?;
Triangle
Class
-* Shape
z
f
ThreeDCircle
Class
+ Circle
в]
J
U IPointy
Class
•* Shape
Рис. 9.2. Иерархия фигур, теперь с интерфейсами
На заметку! Чтобы скрыть или отобразить имена интерфейсов в визуальном конструкторе классов,
щелкните правой кнопкой мыши на значке, представляющем интерфейс, и выберите в
контекстном меню пункт Collapse (Свернуть) или Expand (Развернуть).
328 Часть III. Дополнительные конструкции программирования на С#
Вызов членов интерфейса на уровне объектов
Теперь, когда уже есть несколько классов, поддерживающих интерфейс I Pointy,
давайте посмотрим, как взаимодействовать с новой функциональностью. Самый простой
способ взаимодействия с функциональными возможностями, предлагаемыми заданным
интерфейсом, предусматривает вызов его членов прямо на уровне объектов (при
условии, что члены этого интерфейса не реализованы явным образом, о чем более подробно
рассказывается в разделе "Устранение конфликтов на уровне имен за счет реализации
интерфейсов явным образом" далее в главе). Например, рассмотрим следующий метод
Main ():
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Interfaces *****\n");
// Вызов свойства Points, определенного в IPointy.
Hexagon hex = new Hexagon();
Console.WriteLine("Points: {0}", hex.Points); // вывод числа вершин
Console.ReadLine ();
}
Такой подход конкретно в данном случае вполне подходит, поскольку здесь точно
известно, что в типе Hexagon был реализован запрашиваемый интерфейс и,
следовательно, поддерживается свойство Points. Однако в других случаях определить, какие
интерфейсы поддерживает данный тип, может быть невозможно. Например,
предположим, что имеется массив, содержащий 50 совместимых с Shape типов, при этом
интерфейс IPointy поддерживает только частью из них. Очевидно, что при попытке
вызвать свойство Points для типа, в котором не был реализован IPointy, будет возникать
ошибка. Как динамически определить, поддерживает ли класс или структура нужный
интерфейс?
Одним из способов для определения во время выполнения того, поддерживает ли тип
конкретный интерфейс, является применение операции явного приведения. В случае
если тип не поддерживает запрашиваемый интерфейс, будет генерироваться
исключение InvalidCastException, для аккуратной обработки которого можно использовать
методику структурированной обработки исключений, как, например, показано ниже:
static void Main(string[] args)
{
// Перехват возможного исключения InvalidCastException.
Circle с = new Circle ("Lisa");
IPointy ltfPt = null;
try
{
ltfPt = (IPointy)c;
Console.WriteLine(ltfPt.Points);
}
catch (InvalidCastException e)
{
Console.WriteLine(e.Message);
}
Console.ReadLine ();
}
Разумеется, можно использовать логику try/catch и надеяться на лучшее, но в
идеале все-таки правильнее выяснять, какие интерфейсы поддерживаются, перед
вызовом их членов. Давайте рассмотрим два способа, которыми это можно делать.
Глава 9. Работа с интерфейсами 329
Получение ссылок на интерфейсы с помощью ключевого слова as
Определить, поддерживает ли данный тип тот или иной интерфейс, можно с
использованием ключевого слова as, которое впервые рассматривалось в главе 6. Если объект
удается интерпретировать как указанный интерфейс, то возвращается ссылка на
интересующий интерфейс, а если нет, то ссылка null. Следовательно, перед продолжением
в коде необходимо предусмотреть проверку на null:
static void Main(string [ ] args)
{
// Можно ли интерпретировать hex2 как IPointy?
Hexagon hex2 = new Hexagon("Peter");
IPointy itfPt2 = hex2 as IPointy;
if (itfPt2 != null)
// Вывод числа вершин.
Console.WriteLine ("Points: {0}", itfPt2.Points);
else
// Это не интерфейс IPointy.
Console.WriteLine ("OOPS! Not pointy...");
Console.ReadLine ();
}
Обратите внимание, что в случае применения ключевого слова as использовать
логику try/catch нет никакой необходимости, поскольку возврат ссылки, отличной от
null, означает, что вызов осуществляется с использованием действительной ссылки на
интерфейс.
Получение ссылок на интерфейсы с помощью ключевого слова is
Проверить, был ли реализован нужный интерфейс, можно также с помощью
ключевого слова is (которое тоже впервые упоминалось в главе 6). Если запрашиваемый
объект не совместим с указанным интерфейсом, возвращается значение false, а если
совместим, то можно спокойно вызывать члены этого интерфейса без применения
логики try/catch.
Для примера предположим, что имеется массив типов Shape, некоторые из членов
которого реализуют интерфейс IPointy. Ниже показано, как можно определить, какие
из элементов в этом массиве поддерживают данный интерфейс с помощью ключевого
слова is внутри обновленной соответствующим образом версии метода Main ():
static void Main(string [ ] args)
{
Console.WriteLine("***** Fun with Interfaces *****\n");
// Создание массива типов Shapes.
Shape[] myShapes = { new Hexagon (), new Circle(), new Triangle("Joe"),
new Circle("JoJo")} ;
for(int i = 0; i < myShapes.Length; i++)
{
// Вспомините, что в базовом классе Shape определен абстрактный
// метод Draw(), поэтому все фигуры знают, как себя рисовать.
myShapes[i].Draw();
//У каких фигур есть вершины?
if (myShapes[i] is IPointy)
// Вывод числа вершин.
Console.WriteLine("-> Points: {0}", ((IPointy) myShapes[l]).Points);
else
330 Часть III. Дополнительные конструкции программирования на С#
// Это не интерфейс IPointy.
Console.WriteLine ("-> {0}\'s not pointy!", myShapes[i].PetName);
Console.WriteLine();
}
Console.ReadLine ();
}
Ниже приведен вывод, полученный в результате выполнения этого кода:
***** Fun W1th Interfaces *****
Drawing NoName the Hexagon
-> Points : 6
Drawing NoName the Circle
-> NoName's not pointy!
Drawing Joe the Triangle
-> Points: 3
Drawing JoJo the Circle
-> JoJo's not pointy!
Использование интерфейсов
в качестве параметров
Благодаря тому, что интерфейсы являются допустимыми типами .NET, можно
создавать методы, принимающие интерфейсы в качестве параметров, вроде метода
CloneMe (), который был показан ранее в главе. Для примера предположим, что
определен еще один интерфейс по имени IDraw3D:
// Моделирует способность визуализировать тип в трехмерном формате.
public interface IDraw3D
{
void Draw3D();
}
Далее сконфигурируем две из трех наших фигур (а именно — Circle и Hexagon)
таким образом, чтобы они поддерживали это новое поведение:
// Circle поддерживает IDraw3D.
class ThreeDCircle : Circle, IDraw3D
{
public void Draw3D()
{ Console.WriteLine("Drawing Circle in 3D!"); }
}
// Hexagon поддерживает IPointy и IDraw3D.
class Hexagon : Shape, IPointy, IDraw3D
{
public void Draw3D()
{ Console.WriteLine("Drawing Hexagon in 3D!"); }
}
На рис. 9.3 показано, как после этого выглядит диаграмма классов в Visual
Studio 2010.
Если теперь определить метод, принимающий интерфейс IDraw3D в качестве
параметра, то ему можно будет передавать, по сути, любой объект, реализующий интерфейс
IDraw3D (при попытке передать тип, не поддерживающий необходимый интерфейс,
компилятор сообщит об ошибке). Например, давайте определим в классе Program
следующий метод:
Глава 9. Работа с интерфейсами 331
// Будет рисовать любую фигуру, поддерживающую IDraw3D.
static void DrawIn3D(IDraw3D itf3d)
{
Console.WriteLine ("-> Drawing IDraw3D compatible type");
itf3d.Draw3D();
Circle
Class
•♦Shape
.1 . ... _ LL
0 IDrawi
ThreeDCiri
Class
-» Circle
" .
D
fc
(iT
~i
: Shape
j Abstract Class
<Z» IPoi
! Triangle
| Class
•♦Shape
T
1
nty
№
Щ
^J
IPointy
Interface
В Properties
ч
IPointy
IDraw3D
Hexagon
Class
-«•Shape
■Щ
®
IDraw3D
Interlace
"Methods
♦ Draw3D
v
a
Рис. 9.З. Обновленная иерархия фигур
Теперь можно проверить, поддерживает ли элемент в массиве Shape новый
интерфейс, и если да, то передать его методу DrawIn3D () на обработку:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Interfaces *****\n");
Shape[] myShapes = { new Hexagon (), new Circle (),
new Triangle (), new Circle("JoJo") } ;
for(int i = 0; i < myShapes.Length; i++)
{
// Можно ли нарисовать эту фигуру в трехмерном формате?
if(myShapes [i] is IDraw3D)
DrawIn3D((IDraw3D)myShapes [ 1]);
}
}
Ниже показано, как будет выглядеть вывод в результате выполнения этой
модифицированной версии приложения. Обратите внимание, что в трехмерном формате
отображается только объект Hexagon, поскольку все остальные члены массива Shape не
реализуют интерфейса IDraw3D.
***** pun with Interfaces *****
Drawing NoName the Hexagon
-> Points: 6
-> Drawing IDraw3D compatible type
Drawing Hexagon in 3D!
Drawing NoName the Circle
-> NoName's not pointy!
Drawing Joe the Triangle
-> Points: 3
Drawing JoJo the Circle
-> JoJo's not pointy!
332 Часть III. Дополнительные конструкции программирования на С#
Использование интерфейсов в качестве
возвращаемых значений
Интерфейсы можно также использовать и в качестве возвращаемых значений
методов. Для примера создадим метод, который принимает в качестве параметра массив
объектов System.Object и возвращает ссылку на первый элемент, поддерживающий
интерфейс I Pointy:
// Этот метод возвращает первый объект в массиве,
// который реализует интерфейс IPointy.
{
foreach (Shape s in shapes)
{
if (s is IPointy)
return s as IPointy;
}
return null;
}
Взаимодействовать с этим методом можно следующим образом:
static void Main(string[ ] args)
{
Console.WriteLine ("***** Fun with Interfaces *****\n");
// Создание массива объектов Shapes.
Shape [ ] myShapes = { new Hexagon(), new Circle (),
new Triangle("Joe"), new Circle("JoJo")};
// Получение первого элемента, который имеет вершины.
// Ради безопасности не помешает предусмотреть проверку
// firstPointyltem на предмет равенства null.
IPointy firstPointyltem = FindFirstPointyShape(myShapes);
// Вывод числа вершин.
Console.WriteLine("The item has {0} points", firstPointyltem.Points);
}
Массивы типов интерфейсов
Вспомните, что один и тот же интерфейс может быть реализован во множестве
типов, даже если они находятся не в одной и той же иерархии классов и не имеют
никакого общего родительского класса, помимо System. Object. Это позволяет формировать
очень мощные программные конструкции. Например, давайте создадим в текущем
проекте три новых типа класса, два из которых (Knife (нож) и Fork (вилка)) будут
представлять кухонные принадлежности, а третий (PitchFork (вилы)) — инструмент для работы
в саду (рис. 9.4).
Имея определения типов PitchFork, Fork и Knife, можно определить массив
объектов, совместимых с IPointy. Поскольку все эти члены поддерживают один и тот же
интерфейс, можно выполнять проход по массиву и интерпретировать каждый его элемент
как совместимый с IPointy объект, несмотря на разницу между иерархиями классов.
static void Main(string [ ] args)
{
//В этом массиве могут содержаться только типы,
// которые реализуют интерфейс IPointy.
IPointy[] myPointyObjects = {new Hexagon (), new Knife(),
new Triangle (), new Fork(), new PitchFork()};
Глава 9. Работа с интерфейсами 333
foreach(IPointy i in myPointyObjects)
// Вывод числа вершин.
Console.WriteLine("Object has {0} points.", 1.Points);
Console.ReadLine() ;
Shape
Abstract Class
■
J
' IPointy
( Circle
Class
•♦Shape
^ ..
U IDrawi
D
ThreeDCkcle
Class
■* Circle
®1
)
'*!
J
Tria
Class
-♦Shape
Щ. I
~IPoint>
IDrawBD
Hexagon
Class
-♦Shape
V
—J
О IPointy
IPointy
Fork
r
yn IPointy
PrtchFork
Class
ч..г _\ ■ _.^_
rs
1
Knife
I Class
f»
Рис. 9.4. Вспомните, что интерфейсы могут "встраиваться" в любой тип внутри любой
части иерархии классов
Исходный код. Проект Customlnterf асе доступен в подкаталоге Chapter 9.
Реализация интерфейсов с помощью
Visual Studio 2010
Хотя программирование с применением интерфейсов и является очень мощной
технологией, реализация интерфейсов может сопровождаться довольно приличным
объемом ввода. Поскольку интерфейсы представляют собой именованные наборы
абстрактных членов, для каждого метода интерфейса в каждом типе, который должен
поддерживать такое поведение, требуется вводить и определение, и реализацию.
К счастью, в Visual Studio 2010 поддерживаются различные инструменты, которые
существенно упрощают процесс реализации интерфейсов. Для примера давайте
вставим в текущий проект еще один класс по имени PomtyTestClass. При реализации
для него интерфейса IPointy (или любого другого подходящего интерфейса) можно
будет заметить, как по окончании ввода имени интерфейса (или при размещении на
нем курсора мыши в окне кода) первая буква будет выделена подчеркиванием (или,
согласно формальной терминологии, снабжена так называемой контекстной меткой —
смарт-тегом (smart tag)). В результате щелчка на этом смарт-теге появится
раскрывающийся список с различными возможными вариантами реализации этого интерфейса
(рис. 9.5).
334 Часть III. Дополнительные конструкции программирования на С#
PointyTestCliss.es* X Щ
^iCustomlnterface.PointyTestClass
fusing System;
| using System.Collections.Ge
using Systea.Linq;
Lusing System.Text;
^namespace Customlnterface
ф class PointyTestClass :
i i
l У
1}
Рис. 9.5. Реализация интерфейсов в Visual Studio 2010
Обратите внимание, что в этом списке предлагаются два варианта, причем второй
из них (реализация интерфейса явным образом) подробно рассматривается в
следующем разделе. Пока что выберем первый вариант. В этом случае Visual Studio 2010
сгенерирует (внутри именованного раздела кода) показанный ниже удобный для
дальнейшего обновления код-заглушку (обратите внимание, что в реализации по умолчанию
предусмотрена генерация исключения System.NotlmplementedException, что вполне
можно удалить).
namespace Customlnterface
{
class PointyTestClass : IPointy
{
#region IPointy Members
public byte Points
{
get { throw new NotlmplementedException (); }
}
#endregion
}
}
На заметку! В Visual Studio 2010 также поддерживается опция рефакторинга типа выделения
интерфейса (Extract Interface), которая доступа в меню Refactoring (Рефакторинг). Она
позволяет извлекать определение нового интерфейса из существующего определения класса.
Устранение конфликтов на уровне имен за счет
реализации интерфейсов явным образом
Как было показано ранее в главе, единственный класс или структура может
реализовать любое количество интерфейсов. Из-за этого всегда существует вероятность
реализации интерфейсов с членами, имеющими идентичные имена, и, следовательно,
возникает необходимость в устранении конфликтов на уровне имен. Чтобы ознакомиться с
различными способами решения этой проблемы, давайте создадим новый проект типа
Console Application по имени Interf aceNameClash и добавим в него три специальных
интерфейса, представляющих различные места, в которых реализующий их тип может
визуализировать свой вывод:
IPointy
ш s
Implement interface IPomty* к
Explicitly implement interface IPointy'
Глава 9. Работа с интерфейсами 335
// Прорисовывание изображения в форме.
public interface IDrawToForm
void Draw();
// Отправка изображения в буфер в памяти.
public interface IDrawToMemory
void Draw();
// Вывод изображения на принтере.
public interface IDrawToPrinter
void Draw () ;
Обратите внимание, что в каждом из этих интерфейсов присутствует метод по
имени Draw () с идентичной сигнатурой (без аргументов). Если теперь необходимо, чтобы
каждый из этих интерфейсов поддерживался в одном классе по имени Octagon,
компилятор позволит использовать следующее определение:
class Octagon : IDrawToForm, IDrawToMemory, IDrawToPrinter
{
public void Draw()
{
// Совместно используемая логика рисования.
Console.WriteLine("Drawing the Octagon...");
Хотя компиляция этого кода пройдет гладко, одна возможная проблема в нем все-
таки присутствует. Попросту говоря, предоставление единой реализации метода Draw ()
не позволяет предпринимать уникальные действия на основе того, какой интерфейс
получен от объекта Octagon. Например, следующий код будет приводить к вызову одного
и того же метода Draw (), какой бы интерфейс не был получен:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Interface Name Clashes *****\n");
//Во всех этих случаях будет вызываться один и тот же метод DrawQ !
Octagon oct = new Octagon ();
oct.Draw ();
IDrawToForm itfForm = (IDrawToForm)oct;
itfForm.Draw ();
IDrawToPrinter itfPriner = (IDrawToPrinter)oct;
itfPriner.Draw();
IDrawToMemory itfMemory = (IDrawToMemory)oct;
ltfMemory.Draw();
Console.ReadLine();
}
Очевидно, что код, требуемый для визуализации изображения в окне, довольно
сильно отличается от того, который необходим для визуализации изображения на
сетевом принтере или в области памяти. При реализации нескольких интерфейсов,
имеющих идентичные члены, можно разрешать подобный конфликт на уровне имен за счет
применения так называемого синтаксиса явной реализации интерфейсов. Например,
модифицируем тип Octagon следующим образом:
336 Часть III. Дополнительные конструкции программирования на С#
class Octagon : IDrawToForm, IDrawToMemory, IDrawToPrinter
{
// Явная привязка реализаций DrawQ к конкретному интерфейсу.
void IDrawToForm.Draw ()
Console.WriteLine("Drawing to form...");
void IDrawToMemory.Draw ()
Console.WriteLine("Drawing to memory...");
void IDrawToPrinter.Draw ()
Console.WriteLine("Drawing to a printer...");
Как здесь показано, при явной реализации члена интерфейса схема, которой нужно
следовать, в общем случае выглядит следующим образом:
возвращаемыйТип ИмяИнтерфейса .ИмяМетода (параметры)
Обратите внимание, что в этом синтаксисе указывать модификатор доступа не
требуется, поскольку члены, реализуемые явным образом, автоматически считаются
приватными. Например, следующий код является недопустимым:
// Ошибка! Модификатора доступа быть не должно!
public void IDrawToForm.Draw()
{
Console.WriteLine("Drawing to form...");
}
Из-за того, что реализуемые явным образом члены всегда неявно считаются
приватными, они перестают быть доступными на уровне объектов. На самом деле, если
применить к типу Octagon операцию точки, никаких членов Draw () в списке IntelliSense
отображаться не будет (рис. 9.6).
Program.cs" X
J$InterfaceNameCfcash.Program
—
* I j^Maintstringfl args)
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Interface Name Clashes ***
// All of these invocations call the
// sane Draw() method!
Octagon oct - new Octagon();
oct.
IDraj
itfri .
IOr J * E4UalS
|<Ctrl+Alt+Space>
j^fpj ■'♦ GetHashCode к
Юг J ■• GetType **
itfH ♦ ToString
Con sole.Read LineG;
|ioForm)oct;
tDrawToPrinter)oct;
3rawToMemory)oct;
*\n
ID
Рис. 9.6. Реализуемые явным образом члены интерфейсов перестают
быть доступными на уровне объектов
Как и следовало ожидать, для получения доступа к необходимым функциональным
возможностям в таком случае потребуется явное приведение, например:
Глава 9. Работа с интерфейсами 337
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Interface Name Clashes *****\n");
Octagon oct = new Octagon ();
// Теперь для получения доступа к членам Draw()
// должно использоваться приведение.
IDrawToForm itfForm = (IDrawToForm)oct;
ltfForm.Draw() ;
// Сокращенная версия на случай, если переменную интерфейса
//не планируется использовать позже.
( (IDrawToPrinter)oct) .Draw() ;
// Можно было бы также использовать ключевое слово as.
if (oct is IDrawToMemory)
( (IDrawToMemory)oct) .Draw();
Console.ReadLine();
}
Хотя такой синтаксис довольно полезен, когда необходимо устранить конфликты на
уровне имен, приемом явной реализации интерфейсов можно пользоваться и просто
для сокрытия более "сложных" членов на уровне объектов. В таком случае при
применении операции точки пользователь объекта будет видеть только некоторую часть общей
функциональности типа. Те же пользователи, которым необходим доступ к более
сложным поведениям, все равно смогут получать их из желаемого интерфейса через явное
приведение.
Исходный код. Проект InterfaceNameClash доступен в подкаталоге Chapter 9.
Проектирование иерархий интерфейсов
Интерфейсы могут быть организованы в иерархии. Как и в иерархии классов, в
иерархии интерфейсов, когда какой-то интерфейс расширяет существующий, он
наследует все абстрактные члены своего родителя (или родителей). Конечно, в отличие от
классов, производные интерфейсы никогда не наследуют саму реализацию. Вместо
этого они просто расширяют собственное определение за счет добавления дополнительных
абстрактных членов.
Использовать иерархию интерфейсов может быть удобно, когда нужно расширить
функциональность определенного интерфейса без нарушения уже существующих
кодовых баз. Для примера создадим новый проект типа Console Application по имени
InterfaceHierarchyn добавим в него новый набор отвечающих за визуализацию
интерфейсов так, чтобы IDrawable был корневым интерфейсом в дереве этого семейства:
public interface IDrawable
{
void Draw ();
}
Поскольку в интерфейсе IDrawable определяется лишь базовое поведение
рисования, мы теперь можем создать производный от него интерфейс, который расширяет
его функциональность, добавляя возможность выполнения визуализации в других
форматах, например:
public interface IAdvancedDraw : IDrawable
{
void DrawInBoundingBox(int top, int left, int bottom, int right);
void DrawUpsideDown ();
}
338 Часть III. Дополнительные конструкции программирования на С#
При таком дизайне для реализации интерфейса IAdvancedDraw в классе
потребуется реализовать каждый из определенных в цепочке наследования членов (т.е. Draw (),
DrawInBoundingBox () и DrawUpsideDown()):
public class Bitmaplrnage : IAdvancedDraw
{
public void Draw()
Console.WriteLine("Drawing...");
public void DrawInBoundingBox (int top, int left, int bottom, int right)
Console.WriteLine("Drawing in a box...");
public void DrawUpsideDown ()
Console.WriteLine("Drawing upside down'");
}
Теперь при использовании Bitmaplrnage можно вызывать каждый метод на уровне
объекта (поскольку все они являются общедоступными), а также извлекать ссылку на
каждый поддерживаемый интерфейс явным образом с помощью приведения:
static void Main(string [ ] args)
{
Console.WriteLine ("*****Simple Interface Hierarchy *****");
// Выполнение вызова на уровне объекта.
Bitmaplrnage myBitmap = new Bitmaplrnage();
myBitmap.Draw();
myBitmap.DrawInBoundingBoxA0, 10, 100, 150);
myBitmap.DrawUpsideDown ();
// Получение IAdvancedDraw явным образом.
IAdvancedDraw iAdvDraw;
iAdvDraw = (IAdvancedDraw)myBitmap;
iAdvDraw.DrawUpsideDown();
Console.ReadLine ();
}
Исходный код. Проект InterfaceHierarchy доступен в подкаталоге Chapter 9.
Множественное наследование в случае типов интерфейсов
В отличие от классов, один интерфейс может расширять сразу несколько базовых
интерфейсов, что позволяет проектировать очень мощные и гибкие абстракции. Для
примера создадим новый проект типа Console Application по имени MI Interf aceHierarchy
и добавим в него еще одну коллекцию интерфейсов, моделирующих различные
связанные с визуализацией и фигурами абстракции. Обратите внимание, что интерфейс
I Shape в этой коллекции расширяет оба интерфейса IDrawable и I Printable.
// Множественное наследование в случае типов
// интерфейсов является вполне допустимым.
interface IDrawable
{
void Draw ();
}
Глава 9. Работа с интерфейсами 339
interface IPrintable
{
void Print();
void Draw(); // <-- Здесь возможен конфликт имен!
}
// Множественное наследование интерфейса. Все в порядке!
public interface IShape : IDrawable, IPrintable
{
int GetNumberOfSides();
}
На рис. 9.7 показано, как теперь выглядит иерархия интерфейсов.
IDrawable
~®\
ZJ
I*
Interface
•* IDrawable
-* IPrintable
Рис. 9.7. В отличие от классов, интерфейсы могут расширять •
сразу несколько базовых интерфейсов
Теперь главный вопрос состоит в том, сколько методов потребуется реализовать при
создании класса, поддерживающего IShape? Ответ будет несколько. Если нужно
предоставить простую реализацию метода Draw (), понадобится только реализовать три его
члена, как показано ниже на примере типа Rectangle:
class Rectangle : IShape
{
public int GetNumberOfSides ()
{ return 4; }
public void Draw()
{ Console.WriteLine("Drawing. ..") ; }
public void Print()
{ Console.WriteLine("Prining..."); }
При желании предоставить более конкретные реализации для каждого метода
Draw () (что в данном случае имеет больше всего смысла) придется разрешать конфликт
на уровне имен счет применения синтаксиса реализации интерфейсов явным образом,
как показано ниже на примере типа Square:
class Square : IShape
{
// Применение синтаксиса явной реализации для устранения
// конфликта между именами членов.
void IPrintable.Draw ()
{
// Рисование на принтере ...
}
void IDrawable.Draw()
{
// Рисование на экране ...
}
340 Часть III. Дополнительные конструкции программирования на С#
public void Print ()
{
// Печать.. .
}
public int GetNumberOfSides()
{ return 4; }
}
К этому моменту процесс определения и реализации специальных интерфейсов на
С# должен стать более понятным. По правде говоря, на привыкание к
программированию с применением интерфейсов может уйти некоторое время.
Однако уже сейчас важно уяснить, что интерфейсы являются фундаментальным
компонентом .NET Framework. Какого бы типа приложение не разрабатывалось (веб-
приложение, приложение с настольным графическим интерфейсом, библиотека
доступа к данными и т.п.), работа с интерфейсами будет обязательной частью этого процесса.
Подводя итог всему изложенному, отметим, что интерфейсы могут приносить
чрезвычайную пользу в следующих случаях.
• При наличии единой иерархии, в которой только какой-то набор производных
типов поддерживает общее поведение.
• При необходимости моделировать общее поведение, которое должно
встречаться в нескольких иерархиях, не имеющих общего родительского класса помимо
System.Object.
Теперь, когда мы разобрались со специфическими деталями построения и
реализации специальных интерфейсов, необходимо посмотреть, какие стандартные
интерфейсы предлагаются в библиотеках базовых классов .NET.
Исходный код. Проект MI Inter faceHierarchy доступен в подкаталоге Chapter 9.
Создание перечислимых типов
(IEnumerable и IEnumerator)
Прежде чем приступать к исследованию процесса реализации существующих
интерфейсов .NET, давайте сначала рассмотрим роль типов IE numerable и IEnumerator.
Вспомните, что в С# поддерживается ключевое слово f oreach, которое позволяет
осуществлять проход по содержимому массива любого типа:
// Итерация по массиву элементов.
int[] myArrayOflnts = {10, 20, 30, 40};
foreach(int i in myArrayOflnts)
{
Console.WriteLine (i);
}
Хотя может показаться, что данная конструкция подходит только для массивов, на
самом деле с ее помощью можно анализировать любой тип, который поддерживает
метод GetEnumerator (). Для примера создадим новый проект типа Console Application no
имени CustomEnumerator и добавим в него файлы Car.cs и Radio.cs, которые были
определены в примере SimpleException в главе 7 (выбрав в меню Project (Проект) пункт
Add Existing Item (Добавить существующий элемент)).
На заметку! Может возникнуть желание переименовать содержащее типы Саг и Radio
пространство имен в CustomEnumerator, чтобы не импортировать в новый проект пространство имен
CustomException.
Глава 9. Работа с интерфейсами 341
Вставим новый класс Garage (гараж), обеспечивающий сохранение ряда объектов
Саг (автомобиль) внутри System.Array:
// Garage содержит набор объектов Саг.
public class Garage
{
private Car [ ] carArray = new Car[4];
// Заполнение какими-то объектами Car при запуске.
public Garage()
{
carArray[0] = new Car("Rusty", 30);
carArray[1] = new Car("Clunker", 55);
carArray[2] = new Car ("Zippy", 30);
carArray [3] = new Car ("Fred", 30);
}
}
В идеале удобно было бы осуществлять проход по элементам объекта Garage как по
массиву значений данных с применением конструкции f oreach:
// Такой вариант кажется целесообразным...
public class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** Fun with IEnumerable / IEnumerator *****\n");
Garage carLot = new Garage();
// Проход по всем объектам Car в коллекции?
foreach (Car с in carLot)
{
Console.WriteLine ("{0} is going {1} MPH",
c.PetName, c. Speed);
}
Console.ReadLine ();
}
}
К сожалению, в этом случае компилятор сообщит, что в классе Garage не реализован
метод GetEnumerator (). Этот метод формально определен в интерфейсе IEnumerable,
который находится глубоко внутри пространства имен System.Collections.
На заметку! В следующей главе будет рассказываться о роли обобщений и пространства имен
System.Collections.Generic. В этом пространстве имен содержатся обобщенные
версии Enumerable и IEnumerator, которые предоставляют более безопасный в отношении
типов способ для реализации прохода по подобъектам.
Классы или структуры, которые поддерживают такое поведение, позиционируются
как способные предоставлять содержащиеся внутри них подэлементы вызывающему
коду (в данном примере это само ключевое слово foreach):
// Этот интерфейс информирует вызывающий код о том,
// что подэлементы объекта могут перечисляться.
public interface IEnumerable
{
IEnumerator GetEnumerator ();
}
Метод GetEnumerator () возвращает ссылку на еще один интерфейс под
названием System. Collections . IEnumerator. Этот интерфейс обеспечивает инфраструктуру,
342 Часть III. Дополнительные конструкции программирования на С#
позволяющую вызывающему коду проходить по внутренним объектам, которые
содержатся в совместимом с IEnumerable контейнере:
// Этот интерфейс позволяет вызывающему коду получать
// содержащиеся в контейнере внутренние подэлементы.
public interface IEnumerator
{
bool MoveNext (); // Перемещение внутренней позиции курсора.
object Current { get;} // Извлечение текущего элемента
// (свойство, доступное только для чтения).
void PesetO; // Сброс курсора перед первым членом.
}
При модификации типа Garage для поддержки этих интерфейсов можно пойти
длинным путем и реализовать каждый метод вручную. Хотя, конечно же, предоставлять
специализированные версии методов GetEnumerator (), MoveNext (), Current и Reset ()
вполне допускается, существует более простой путь. Поскольку в типе System.Array
(как и во многих других классах коллекций) интерфейсы IEnumerable и IEnumerator
уже реализованы, можно просто делегировать запрос к System.Array показанным
ниже образом:
using System.Collections;
public class Garage : IEnumerable
{
// В System.Array интерфейс IEnumerator уже реализован!
private Car [ ] carArray = new Car[4];
public Garage ()
{
carArray = new Car[4];
carArrayfO] = new Car ( "FeeFee", 200, 0) ;
carArray[l] = new Car ( "Clunker", 90, 0) ;
carArray[2] = new Car ( "Zippy", 30, 0) ;
carArray[3] = new Car ("Fred", 30, 0) ;
}
public IEnumerator GetEnumerator()
{
// Возврат интерфейса IEnumerator объекта массива.
return carArray.GetEnumerator();
}
}
Изменив тип Garage подобным образом, теперь можно спокойно использовать его
внутри конструкции foreach. Более того, поскольку метод GetEnumerator () был
определен как общедоступный, пользователь объекта тоже может взаимодействовать с
IEnumerator:
// Работа с IEnumerator вручную
IEnumerator i = carLot.GetEnumerator() ;
i.MoveNext() ;
Car myCar = (Car)l.Current;
Console.WriteLine ("{0 } is going {1} MPH", myCar.PetName, myCar. CurrentSpeed);
Если нужно скрыть функциональные возможности IEnumerable на уровне объекта,
достаточно применить синтаксис явной реализации интерфейса:
public IEnumerator IEnumerable.GetEnumerator ()
{
// Возврат интерфейса IEnumerator объекта массива.
return carArray.GetEnumerator();
Глава 9. Работа с интерфейсами 343
После этого обычный пользователь объекта не будет видеть метода GetEnumerator ()
в Garage, хотя конструкция f oreach будет все равно получать интерфейс незаметным
образом, когда это необходимо.
Исходный код. Проект CustomEnumerator доступен в подкаталоге Chapter 9.
Создание методов итератора с помощью ключевого слова yield
Раньше, когда требовалось создать специальную коллекцию (вроде Garage),
способную поддерживать перечисление элементов посредством конструкции f oreach,
реализация интерфейса IEnumerable (и возможно IEnumerator) была единственным
доступным вариантом. Потом, однако, появился альтернативный способ для создания типов,
способных работать с циклами f oreach, который предусматривает использование так
называемых итераторов (iterator).
Попросту говоря, итератором называется такой член, который указывает, каким
образом должны возвращаться внутренние элементы контейнера при обработке в цикле
f oreach. Хотя метод итератора должен все равно носить имя GetEnumerator (), а его
возвращаемое значение относиться к типу IEnumerator, необходимость в реализации
каких-либо ожидаемых интерфейсов в специальном классе при таком подходе отпадает.
Для примера давайте создадим новый проект типа Console Application по имени
CustomEnumeratorWithYield и вставим в него типы Car, Radio и Garage из
предыдущего примера (при желании переименовав пространство имен в соответствии с
текущим проектом), после чего модифицируем тип Garage показанным ниже образом:
public class Garage
{
private Car[] carArray = new Car[4];
// Метод итератора.
public IEnumerator GetEnumerator ()
{
foreach (Car с in carArray)
{
yield return c;
}
}
}
Обратите внимание, что в данной реализации GetEnumerator () проход по подэле-
ментам осуществляется с использованием внутренней логики foreach, а каждый
объект Саг возвращается вызывающему коду с применением синтаксиса yield return.
Ключевое слово yield служит для указания значения или значений, которые должны
возвращаться конструкции foreach в вызывающем коде. При достижении оператора
yield return производится сохранение текущего местоположении в контейнере, и при
следующем вызове итератора выполнение начинается уже с этого места (детали будут
описаны позже).
Использовать ключевое слово foreach в методах итераторов для возврата
содержимого не требуется. Метод итератора можно также определять следующим образом:
public IEnumerator GetEnumerator ()
{
yield return carArray[0];
yield return carArray[1];
yield return carArray[2];
yield return carArray[3];
}
344 Часть III. Дополнительные конструкции программирования на С#
В этой реализации важно обратить внимание, что метод GetEnumerator () явным
образом возвращает вызывающему коду новое значение после каждого прогона. В
данном примере подобный подход не особо удобен, поскольку в случае добавления
большего числа объектов в переменную экземпляра carArray метод GetEnumerator () вышел
бы из-под контроля. Тем не менее, такой синтаксис все-таки может быть довольно
полезным, когда необходимо возвращать из метода локальные данные для последующей
обработки внутри f о reach.
Создание именованного итератора
Еще интересен тот факт, что ключевое слово yield формально может применяться
внутри любого метода, как бы ни выглядело его имя. Такие методы (называемые
именованными итераторами) уникальны тем, что могут принимать любое количество
аргументов. При создании именованного итератора необходимо очень хорошо понимать,
что метод будет возвращать интерфейс IE numerable, а не ожидаемый совместимый с
IEnumerator тип. Для примера добавим к типу Garage следующий метод:
public IEnumerable GetTheCars(bool ReturnRevesed)
{
// Возврат элементов в обратном порядке.
if (ReturnRevesed)
{
for (int i = carArray. Length; 1 != 0; 1 —)
yield return carArray[l-l];
}
}
else
{
// Возврат элементов в том порядке,
//в котором они идут в массиве.
foreach (Car с in carArray)
{
yield return с;
}
}
}
Обратите внимание, что добавленный новый метод позволяет вызывающему коду
получать подэлементы как в прямом, так и в обратном порядке, если во входном
параметре передается значение true. Теперь с ним можно взаимодействовать следующим
образом:
static void Main(string[ ] args)
{
Console.WriteLine ("***** Fun with the Yield Keyword *****\n");
Garage carLot = new Garage ();
// Получение элементов с помощью GetEnumerator().
foreach (Car с in carLot)
{
Console.WriteLine(" {0} is going {1} MPH" ,
c.PetName, с.CurrentSpeed) ;
}
Console.WriteLine();
// Получение элементов (в обратном порядке)
//с помощью именованного итератора.
Глава 9. Работа с интерфейсами 345
foreach (Car с in carLot.GetTheCars(true) )
{
Console.WriteLine("{0} is going {1} MPH", c.PetName, с.CurrentSpeed);
}
Console.ReadLine() ;
}
Нельзя не согласиться с тем, что именованные итераторы представляют собой очень
полезные конструкции, поскольку позволяют определять в единственном специальном
контейнере сразу несколько способов для запрашивания возвращаемого набора.
Внутреннее представление метода итератора
Столкнувшись с методом итератора, компилятор С# динамически генерирует
внутри соответствующего типа (в данном случае Garage) определение вложенного класса.
В этом сгенерированном вложенном классе, в свою очередь, автоматически
реализуются такие члены, как GetEnumerator (), MoveNext () и Current (но, как ни странно, не
метод Reset (), и при попытке его вызвать возникает исключение времени выполнения).
Если загрузить текущее приложение в утилиту ildasm.exe, можно обнаружить в нем
два вложенных типа, в каждом из которых будет содержаться логика, необходимая для
конкретного метода итератора. На рис. 9.8 видно, что эти сгенерированные
автоматически компилятором типы имеют имена <GetEnumerator>d 0 и <GetTheCars>d 6.
Р H:\My Books\C« Book\C# and the .NET Platform 5th ed\First DraftXCha...
file View Help
H:\My Books\C# Book\C# and the .NET Platform 5th ed\First Draft\Chapter _09\Code\CustomEi
► MANIFEST
W CustomEnumeratorWithYield
tf CustomEnumeratorWithYield. Car
a £ CustomEnumeratorWithYield.Garage
► .class public auto ansi beforefieldinit
► implements [mscorlib]System.Collections.IEnumerable
i £ <GetTheCars>d_6
v/ car Array : private class CustomEnumerator With Yield. Car[]
■ .ctor : void()
■ GetEnumerator: class [mscorlib]System.Collections.IEnumerator()
■ GetTheCars : class [mscorlib]System.Collections.IEnumerable(bool)
: CustomEnumeratorWithYield.Program
E CustomEnumeratorWithYield. Radio
.assembly CustomEnumeratorWithYield
Рис. 9.8. Методы итераторов внутренне реализуются с помощью
автоматически сгенерированного вложенного класса
Воспользовавшись утилитой ildasm.exe для просмотра реализации метода
GetEnumerator () в типе Garage, можно обнаружить, что он был реализован так, чтобы
в нем "за кулисами" использовался тип <GetEnumerator>d 0 (в методе GetTheCars ()
аналогичным образом используется вложенный тип <GetTheCars>d 6):
.method public hidebysig instance class
[mscorlib]System.Collections.IEnumerator
GetEnumerator() cil managed
newobj instance void
CustomEnumeratorWithYield.Garage/'<GetEnumerator>d 0'
: :.ctor (int32)
} // end of method Garage::GetEnumerator
346 Часть III. Дополнительные конструкции программирования на С#
В завершение темы построения перечислимых объектов запомните, что для того,
чтобы специальные типы могли работать с ключевым словом foreach, в контейнере
должен обязательно присутствовать метод по имени GetEnumerator (), который уже
был формально определен типом интерфейса I Enumerable. Реализация данного метода
обычно осуществляется за счет делегирования внутреннему члену, который отвечает за
хранение подобъектов; однако можно также использовать синтаксис yield return и
предоставлять с его помощью множество методов "именованных итераторов".
Исходный код. Проект CustomEnumeratorWithYield доступен в подкаталоге Chapter 9.
Создание клонируемых объектов (iCloneable)
Как уже рассказывалось в главе 6, в System. Ob j ect имеется член по имени
MemberwiseClone (). Он представляет собой метод и позволяет получить
поверхностную копию (shallow copy) текущего объекта. Пользователи объекта не могут вызывать
этот метод напрямую, поскольку он является защищенным, но сам объект вполне
может это делать во время так называемого процесса клонирования. Для примера давайте
создадим новый проект типа Console Application по имени CloneablePoint и добавим
в него класс Point, представляющий точку.
// Класс Point.
public class Point
{
public int X { get; set; }
public int Y { get; set; }
public Point (int xPos, int yPos) { X = xPos; Y = yPos;}
public Point () {}
// Переопределение Object.ToStringO.
public override string ToStringO
{ return string.Format("X = {0}; Y = {1}", X, Y ); }
}
Как уже должно быть известно из материала о ссылочных типах и типах значения
(см. главу 4), в случае присваивания одной переменной ссылочного типа другой
получается две ссылки, указывающие на один и тот же объект в памяти. Следовательно,
показанная ниже операция присваивания будет приводить к получению двух ссылок,
указывающих на один и тот же объект Point в куче, при этом внесение изменений с
использованием любой из этих ссылок будет оказывать воздействие на тот же самый
объект в куче:
static void Main(string [] args)
{
Console.WriteLine ("***** Fun with Object Cloning *****\n");
// Две ссылки на один и тот же объект!
Point pi = new Point E0, 50);
Point p2 = pi;
р2.Х = 0;
Console.WriteLine (pi);
Console.WriteLine(p2);
Console.ReadLine();
}
Глава 9. Работа с интерфейсами 347
Чтобы обеспечить специальный тип способностью возвращать идентичную
копию самого себя вызывающему коду, можно реализовать стандартный интерфейс
I Clone able. Как уже показывалось в начале настоящей главы, этот интерфейс имеет
единственный метод по имени Clone ():
public interface ICloneable
{
object Clone ();
}
На заметку! По поводу полезности интерфейса ICloneable в сообществе .NET ведутся горячие
споры. Проблема связана с тем, что в формальной спецификации никак явно не говорится о
том, что объекты, реализующие данный интерфейс, должны обязательно возвращать
детальную копию (deep copy) объекта (т.е. внутренние ссылочные типы объекта должны приводить к
созданию совершенно новых объектов с идентичным состоянием). Из-за этого с технической
точки зрения возможно, что объекты, реализующие ICloneable, на самом деле будут
возвращать поверхностную копию интерфейса (т.е. внутренние ссылки будут указывать на один
и тот же объект в куче), что вызывает приличную путаницу. В рассматриваемом примере
предполагается, что метод Clone () должен быть реализован так, чтобы возвращать полную,
детальную копию объекта.
Разумеется, реализация метода Clone () в различных объектах может выглядеть по-
разному. Однако базовая функциональность обычно остается неизменной и
заключается в копировании переменных экземпляра в новый экземпляр объекта того же типа и в
возврате его пользователю. Для примера изменим класс Point следующим образом:
// Класс Point теперь поддерживает возможность клонирования.
public class Point : ICloneable
{
public int X { get; set; }
public int Y { get; set; }
public Point (int xPos, int yPos) { X = xPos; Y = yPos; }
public Point () { }
// Переопределение Object.ToString () .
public override string ToString ()
{ return string.Format("X = {0} ; Y = {1} ", X, Y ) ; }
// Возврат копии текущего объекта,
public object Clone ()
{ return new Point(this.X, this.Y); }
}
Теперь можно создавать точные автономные копии типа Point так, как показано
ниже:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Object Cloning *****\n");
// Обратите внимание, что Clone() возвращает
// простой тип объекта. Для получения производного
// типа требуется явное приведение.
Point рЗ = new Point A00, 100);
Point p4 = (Point)p3.Clone ();
// Изменение р4.Х (которое не приводит к изменению рЗ.х) .
р4.Х = 0;
348 Часть III. Дополнительные конструкции программирования на С#
// Вывод объектов на консоль.
Console.WriteLine (рЗ);
Console.WriteLine (p4);
Console.ReadLine();
}
Хотя текущая реализация Point отвечает всем требованиям, ее все равно можно
немного улучшить. В частности, поскольку Point не содержит никаких внутренних
переменных ссылочного типа, реализацию метода Clone () можно упростить следующим
образом:
public object Clone()
{
// Копируем каждое поле Point почленно.
return this.MemberwiseClone() ;
}
Следует, однако, иметь в виду, что если бы в Point все-таки содержались
внутренние переменные ссылочного типа, метод MemberwiseClone () копировал бы ссылки на
эти объекты (т.е. создавал бы поверхностную копию). Тогда для поддержки построения
детальной копии потребовалось бы создавать во время процесса клонирования новый
экземпляр каждой из переменных ссылочного типа. Давайте рассмотрим
соответствующий пример.
Более сложный пример клонирования
Теперь предположим, что в классе Point содержится переменная экземпляра
ссылочного типа по имени PointDescription, предоставляющая удобное для восприятия
имя вершины, а также ее идентификационный номер в виде System.Guid
(глобально уникальный идентификатор GUID представляет собой статистически уникальное
128-битное число). Соответствующая реализация показана ниже.
// Этот класс описывает точку.
public class PointDescription
{
public string PetName {get; set;}
public Guid PointID {get; set;}
public PointDescription ()
{
PetName = "No-name";
PointID = Guid.NewGuidO ;
}
}
Как здесь видно, для начала был модифицирован сам класс Point, чтобы его метод
ToStringO принимал во внимание подобные новые фрагменты данных о состоянии.
Кроме того, был определен и создан ссылочный тип PointDescription. Чтобы
позволить "внешнему миру" указывать желаемое дружественное имя (PetName) для Point,
необходимо также изменить аргументы, передаваемые перегруженному конструктору.
public class Point : ICloneable
{
public int X { get; set; }
public int Y { get; set; }
public PointDescription desc = new PointDescription();
public Point (int xPos, int yPos, string petName)
{
X = xPos; Y = yPos;
desc.PetName = petName;
}
Глава 9. Работа с интерфейсами 349
public Point(int xPos, int yPos)
{
X = xPos; Y = yPos;
}
public Eoint () { }
// Переопределение Object.ToString () .
public override string ToString ()
{
return string. Format ("X = {0}; Y = {1}; Name = {2};\nID = {3}\n",
X, Y, desc.PetName, desc.PointID);
}
// Возврат копии текущего объекта,
public object Clone ()
{ return this.MemberwiseClone (); }
}
Обратите внимание, что метод Clone () пока еще не обновлялся. Следовательно, в
текущей реализации при запросе клонирования пользователем объекта будет
создаваться поверхностная (почленная) копия. Чтобы удостовериться в этом, изменим метод
Main () следующим образом:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Object Cloning *****\n");
Console.WriteLine("Cloned p3 and stored new Point in p4");
Point p3 = new Point A00, 100, "Jane");
Point p4 = (Point) p3.Clone () ;
//До изменения.
Console.WriteLine("Before modification:");
Console.WriteLine ("p3: {0}", p3);
Console.WriteLine ("p4 : {0}", p4);
p4.desc.PetName = "My new Point;
p4.X = 9;
// После изменения.
Console.WriteLine("\nChanged p4.desc.petName and p4.X");
Console.WriteLine("After modification:") ;
Console.WriteLine("p3: {0}", p3);
Console.WriteLine("p4: {0}", p4);
Console.ReadLine();
}
Ниже показано, как будет выглядеть вывод в таком случае. Обратите внимание, что
хотя типы значения на самом деле изменились, у внутренних ссылочных типов остались
те же самые значения, поскольку они "указывают" на одинаковые объекты в памяти.
(В частности, дружественное имя у обоих объектов сейчас выглядит как My new Point.).
***** pun W1th Object Cloning *****
Cloned p3 and stored new Point in p4
Before modification:
p3: X = 100; Y = 100; Name = Jane;
ID = 133d66a7-0837-4bd7-95c6-b22ab0434509
p4: X = 100; Y = 100; Name = Jane;
ID = 133d66a7-0837-4bd7-95c6-b22ab0434509
Changed p4.desc.petName and p4.X
After modification:
p3: X = 100; Y = 100; Name = My new Point;
ID = 133d66a7-0837-4bd7-95c6-b22ab0434509°
p4: X = 9; Y = 100; Name = My new Point;
ID = 133d66a7-0837-4bd7-95c6-b22ab0434509
350 Часть III. Дополнительные конструкции программирования на С#
Для того чтобы метод Clone () создавал полную детальную копию внутренних
ссылочных типов, необходимо настроить возвращаемый методом MemberwiseClone ()
объект, чтобы он принимал во внимание имя текущего объекта Point (тип System.Guid на
самом деле представляет собой структуру, поэтому в действительности числовые
данные будут копироваться). Ниже показан один из возможных вариантов реализации.
// Теперь необходимо подстроить код таким образом, чтобы
// в нем принимался во внимание член PointDescription.
public object Clone ()
{
// Сначала получаем поверхностную копию.
Point newPoint = (Point)this.MemberwiseClone() ;
// Теперь заполняем пробелы.
PointDescription currentDesc = new PointDescription();
currentDesc.PetName = this.desc.PetName;
newPoint.desc = currentDesc;
return newPoint;
}
Если теперь запустить приложение и посмотреть на его вывод (который показан
ниже), то будет видно, что возвращаемый методом Clone () объект Point сейчас
действительно начал копировать свои внутренние переменные экземпляра ссылочного типа
(обратите внимание, что дружественные имена у рЗ и р4 теперь стали уникальными).
***** pun with Object Cloning *****
Cloned рЗ and stored new Point in p4
Before modification:
p3: X = 100; Y = 100; Name = Jane;
ID = 51f64f25-4b0e-47ac-ba35-37d263496406
p4: X = 100; Y = 100; Name = Jane;
ID = 0d3776b3-bl59-490d-b022-7f3f60788e8a
Changed p4.desc.petName and p4.X
After modification:
p3: X = 100; Y = 100; Name = Jane;
ID = 51f64f25-4b0e-47ac-ba35-37d263496406
p4: X = 9; Y = 100; Name = My new Point;
ID = 0d3776b3-bl59-490d-b022-7f3f60788e8a
Подведем итог по процессу клонирования. При наличии класса или структуры, в
которой не содержится ничего кроме типов значения, достаточно реализовать метод
Clone () с использованием MemberwiseClone (). Однако если есть специальный тип,
поддерживающий другие ссылочные типы, необходимо создать новый объект,
принимающий во внимание каждую из переменных экземпляра ссылочного типа.
Исходный код. Проект CloneablePoint доступен в подкаталоге Chapter 9.
Создание сравнимых объектов (IComparable)
Интерфейс System. IComparable обеспечивает поведение, которое позволяет
сортировать объект на основе какого-то указанного ключа. Формально его определение
выглядит так:
// Этот интерфейс позволяет объекту указывать
// его отношения с другими подобными объектами,
public interface IComparable
{
int CompareTo(object o) ;
}
Глава 9. Работа с интерфейсами 351
На заметку! В обобщенной версии этого интерфейса (IComparable<T>) предлагается более
безопасный в отношении типов способ для обработки сравнений объектов. Обобщения будут
более подробно рассматриваться в главе 10.
Для примера создадим новый проект типа Console Application по имени
ComparableCar и вставим в него следующую обновленную версию класса Саг (обратите
внимание, что здесь просто добавлено новое свойство для представления уникального
идентификатора каждого автомобиля и модифицированный конструктор):
public class Car
{
public int CarlD {get; set;}
public Car(string name, int currSp, int id)
{
CurrentSpeed = currSp;
PetName = name;
CarlD = id;
}
}
Теперь создадим массив объектов Car, как показано ниже:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Object Sorting *****\n");
// Создание массива объектов Car.
Car[] myAutos = new Car [5];
myAutos[0] = new Car("Rusty", 80, 1) ;
myAutos[l] = new Car ("Mary", 40, 234);
myAutos[2] = new Car("Viper", 40, 34);
myAutos[3] = new Car ("Mel", 40, 4);
myAutos[4] = new Car("Chucky", 40, 5) ;
Console.ReadLine();
}
В классе System. Array определен статический метод Sort (). При вызове этого
метода на массиве внутренних типов (int, short, string и т.д.) элементы массива могут
сортироваться в числовом или алфавитном порядке, поскольку эти внутренние типы
данных реализуют интерфейс IComparable. Однако что будет происходить в случае
передачи методу Sort () массива типов Саг, как показано ниже?
// Будет ли выполняться сортировка автомобилей?
Array.Sort(myAutos);
В случае выполнения этого тестового кода в исполняющей среде будет возникать
исключение, потому что в классе Саг необходимый интерфейс не поддерживается.
При создании специальных типов для обеспечения возможности сортировки
массивов, которые содержат элементы этих типов, можно реализовать интерфейс
IComparable. При реализации деталей CompareTo () решение о том, что должно
браться за основу в операции упорядочивания, необходимо принимать самостоятельно. Для
рассматриваемого типа Саг с логической точки зрения наиболее подходящим на эту
роль "кандидатом" является внутренняя переменная CarlD:
// Упорядочивание элементов при итерации Саг
// может производиться на основе CarlD.
public class Car : IComparable
{
352 Часть III. Дополнительные конструкции программирования на С#
// Реализация IComparable.
int IComparable.CompareTo(object obj)
{
Car temp = obj as Car;
if (temp != null)
{
if (this.CarlD > temp.CarlD)
return 1;
if (this.CarlD < temp.CarlD)
return -1;
else
return 0;
}
else
throw new ArgumentException ("Parameter is not a Car!11);
// Параметр не является объектом типа Car!
}
}
Как здесь показано, логика CompareTo () состоит в сравнении входного объекта
с текущим экземпляром по конкретному элементу данных. Возвращаемое значение
CompareTo () служит для выяснения того, является данный объект меньше, больше или
равным объекту, с которым он сравнивается (табл. 9.1).
Таблица 9.1. Значения, которые может возвращать CompareTo ()
Возвращаемое значение Описание
Любое число меньше нуля Обозначает, что данный экземпляр находится перед указанным
объектом в порядке сортировки
Нуль Обозначает, что данный экземпляр равен указанному объекту
Любое число больше нуля Обозначает, что данный экземпляр находится после указанного
объекта в порядке сортировки
Предыдущую реализацию CompareTo () можно упростить, благодаря тому, что в С#
тип данных int (который представляет собой сокращенный вариант обозначения типа
System. Int32 в CLR) реализует интерфейс IComparable. Реализовать CompareTo () в
Саг можно следующим образом:
int IComparable.CompareTo(object ob])
{
Car temp = obj as Car;
if (temp != null)
return this.CarlD.CompareTo(temp.CarlD);
else
throw new ArgumentException ("Parameter is not a Car!11);
// Параметр не является объектом типа Car1
}
Далее в обоих случаях, чтобы тип Саг понимал, каким образом сравнивать себя с
подобными объектами, можно написать следующий код:
// Использование интерфейса IComparable.
static void Main(string[] args)
{
// Создание массива объектов Car.
Глава 9. Работа с интерфейсами 353
// Отображение текущего массива.
Console.WriteLine ("Here is the unordered set of cars:");
foreach(Car с in myAutos)
Console.WriteLine("{0} {1}", c.CarlD, c.PetName);
// Сортировка массива с применением интерфейса ХСохорагаЫе.
Array.Sort(myAutos);
// Отображение отсортированного массива.
Console.WriteLine("Here is the ordered set of cars:");
foreach (Car с in myAutos)
Console.WriteLine("{0} {1}", c.CarlD, c.PetName);
Console.ReadLine ();
}
Ниже показан вывод после выполнения приведенного выше метода Main ():
***** Fun with Object Sorting *****
Here is the unordered set of cars:
1 Rusty
234 Mary
34 Viper
4 Mel
5 Списку
Here is the ordered set of cars:
1 Rusty
4 Mel
5 Списку
34 Viper
234 Mary
Указание множества критериев для сортировки (IComparer)
В предыдущей версии класса Саг в качестве основы для порядка сортировки
использовался идентификатор автомобиля (carlD). В другой версии для этого могло бы
применяться дружественное название автомобиля (для перечисления автомобилей в
алфавитном порядке). А что если возникнет желание создать класс Саг, способный
производить сортировку и по идентификатору, и по дружественному названию? Для этого
должен использоваться другой стандартный интерфейс по имени I Comparer, который
поставляется в пространстве имен System. Collections и определение которого
выглядит следующим образом:
// Общий способ для сравнения двух объектов.
interface IComparer
{
int Compare(object ol, object o2);
}
На заметку! В обобщенной версии этого интерфейса (IComparere<T>) предлагается более
безопасный в отношении типов способ для обработки сравнений между объектами. Обобщения
более подробно рассматриваются в главе 10.
В отличие от I Comparable, интерфейс I Compare r обычно реализуется не в самом
подлежащем сортировке типе (Саг в рассматриваемом случае), а в наборе
соответствующих вспомогательных классов, по одному для каждого порядка сортировки (по
дружественному названию, идентификатору и т.д.). В настоящее время типу Саг
(автомобиль) уже "известно", как ему следует сравнивать себя с другими автомобилями по
354 Часть III. Дополнительные конструкции программирования на С#
внутреннему идентификатору. Следовательно, чтобы позволить пользователю объекта
производить сортировку массива объектов Саг еще и по значению petName,
понадобится создать дополнительный вспомогательный класс, реализующий IComparer. Ниже
показан весь необходимый для этого код (перед его использованием важно не забыть
импортировать в файл кода пространство имен System.Collections).
// Этот вспомогательный класс предназначен для
// обеспечения возможности сортировки массива
// объектов Саг по дружественному названию.
public class PetNameComparer : IComparer
I
// Проверка дружественного названия каждого объекта.
int IComparer.Compare(object ol, object o2)
{
Car tl = ol as Car;
Car t2 = o2 as Car;
if (tl != null && t2 != null)
return String.Compare(tl.PetName, t2.PetName);
else
throw new ArgumentException ("Parameter is not a Car!");
// Параметр не является объектом типа Car1
}
}
Теперь можно использовать этот вспомогательный класс в коде. В System.Array
имеется набор перегруженных версий метода Sort (), в одной из которых принимается
в качестве параметра объект, реализующий интерфейс IComparer.
static void Main(string [ ] args)
{
// Теперь выполнение сортировки по дружественному названию.
Array.Sort(myAutos, new PetNameComparer());
// Вывод отсортированного массива в окне консоли.
Console. WriteLine ("Ordering by pet name:");
foreach(Car с in myAutos)
Console.WriteLine ("{0} {1}", CarlD, c.PetName);
}
Использование специальных свойств
и специальных типов для сортировки
Следует отметить, что можно также использовать специальное статическое свойство
для оказания пользователю объекта помощи с сортировкой типов Саг по какому-то
конкретному элементу данных. Для примера добавим в класс Саг статическое доступное
только для чтения свойство по имени SortByPetName, которое возвращает экземпляр
объекта, реализующего интерфейс IComparer (в этом случае PetNameComparer):
// Обеспечение поддержки для специального свойства,
// способного возвращать правильный интерфейс IComparer.
public class Car : IComparable
{
// Свойство, воэвращажщее компаратор SortByPetName.
public static IComparer SortByPetName
{ get { return (IComparer)new PetNameComparer() ; } }
}
Глава 9. Работа с интерфейсами 355
Теперь в коде пользователя объекта сортировка по дружественному названию может
выполняться с помощью строго ассоциированного свойства, а не автономного класса
PetNameComparer:
// Сортировка по дружественному названию теперь немного проще.
Array.Sort(myAutos, Car.SortByPetName);
Исходный код. Проект ComparableCar доступен в подкаталоге Chapter 9.
К этому моменту должны быть понятны не только способы определения и
реализации собственных интерфейсов, но и то, какую пользу они могут приносить. Следует
отметить, что интерфейсы встречаются в каждом крупном пространстве имен .NET, и в
остальной части книги придется неоднократно иметь дело с различными
стандартными интерфейсами.
Резюме
Интерфейс может быть определен как именованная коллекция абстрактных членов.
Из-за того, что никаких касающихся реализации деталей в интерфейсе не
предоставляется, интерфейс часто рассматривается как поведение, которое может поддерживаться
тем или иным типом. В случае, когда один и тот же интерфейс реализуют два или более
класса, каждый из типов может обрабатываться одинаково (что называется
обеспечением полиморфизма на основе интерфейса), даже если эти типы расположены в разных
иерархиях классов.
Для определения новых интерфейсов в С# предусмотрено ключевое слово interface.
Как было показано в этой главе, в любом типе может обеспечиваться поддержка для
любого количества интерфейсов за счет предоставления соответствующего разделенного
запятыми списка их имен. Более того, также допускается создавать интерфейсы,
унаследованные от нескольких базовых интерфейсов.
Помимо возможности создания специальных интерфейсов, в библиотеках .NET
предлагается набор стандартных (поставляемых вместе с платформой) интерфейсов. Как
было показано в этой главе, можно создавать специальные типы, реализующие
предопределенные интерфейсы, и получать доступ к желаемым возможностям, таким как
клонирование, сортировка и перечисление.
ГЛАВА 10
Обобщения
Самым элементарным контейнером на платформе .NET является тип System.
Array. Как было показано в главе 4, массивы С# позволяют определять наборы
типизированных элементов (включая массив объектов типа System.Object, по сути
представляющий собой массив любых типов) с фиксированным верхним пределом. Хотя
базовые массивы могут быть удобны для управления небольшими объемами известных
данных, бывает также немало случаев, когда требуются более гибкие структуры
данных, такие как динамически растущие и сокращающиеся контейнеры или
контейнеры, которые хранят только элементы, отвечающие определенному критерию (например,
элементы, унаследованные от заданного базового класса, реализующие определенный
интерфейс или что-то подобное).
После появления первого выпуска платформы .NET программисты часто
использовали пространство имен System.Collections для получения более гибкого способа
управления данными в приложениях. Однако, начиная с версии .NET 2.0, язык
программирования С# был расширен поддержкой средства, которое называется
обобщением (generic). Вместе с ним библиотеки базовых классов пополнились совершенно новым
пространством имен, связанным с коллекциями — System. Col lections. Generic.
Как будет показано в настоящей главе, обобщенные контейнеры во многих
отношениях превосходят свои необобщенные аналоги, обеспечивая высочайшую безопасность
типов и выигрыш в производительности. После общего знакомства с обобщениями
будут описаны часто используемые классы и интерфейсы из пространства имен System.
Collections.Generic. В оставшейся части этой главы будет показано, как строить
собственные обобщенные типы. Вы также узнаете о роли ограничений (constraint), которые
позволяют строить контейнеры, исключительно безопасные в отношении типов.
На заметку! Можно также создавать обобщенные типы делегатов; об этом пойдет речь в
следующей главе.
Проблемы, связанные
с необобщенными коллекциями
С момента появления платформы .NET программисты часто использовали
пространство имен System.Collecitons из сборки mscorlib.dll. Здесь разработчики
платформы предоставили набор классов, позволявших управлять и организовывать большие
объемы данных. В табл. 10.1 документированы некоторые наиболее часто
используемые классы коллекций, а также основные интерфейсы, которые они реализуют.
Глава 10. Обобщения 357
Таблица 10.1. Часто используемые классы из System.Collections
Класс
Назначение
Основные реализуемые
интерфейсы
ArrayList Представляет коллекцию динамически
изменяемого размера, содержащую объекты в
определенном порядке
Hashtable Представляет коллекцию пар "ключ/значение",
организованных на основе хеш-кода ключа
Queue Представляет стандартную очередь,
работающую по алгоритму FIFO ("первый
вошел — первый вышел")
SortedList Представляет коллекцию пар
"ключ/значение", отсортированных по ключу и доступных
по ключу и по индексу
Stack Представляет стек LIFO ("последний
вошел — первый вышел"), поддерживающий
функциональность заталкивания (push) и
выталкивания (pop), а также считывания (peek)
IList, ICollection,
IEnumerable и ICloneable
IDictionary, ICollection,
IEnumerable и ICloneable
ICollection, IEnumerable
иICloneable
IDictionary, Icollection,
IEnumerable и ICloneable
ICollection, IEnumerable
иICloneable
Интерфейсы, реализованные этими базовыми классами коллекций,
представляют огромное "окно" в их общую функциональность. В табл. 10.2 представлено
описание общей природы этих основных интерфейсов, часть из которых поверхностно
рассматривалась в главе 9.
Таблица 10.2. Основные интерфейсы, поддерживаемые классами System.Collections
Интерфейс
Назначение
ICollection Определяет общие характеристики (т.е. размер, перечисление и
безопасность к потокам) всех необобщенных типов коллекций
ICloneable Позволяет реализующему объекту возвращать копию самого себя
вызывающему коду
IDictionary Позволяет объекту необобщенной коллекции представлять свое содержимое
в виде пар "имя/значение"
IEnumerable Возвращает объект, реализующий интерфейс IEnumerator
(см. следующую строку в таблице)
IEnumerator Позволяет итерацию в стиле f oreach по элементам коллекции
IList Обеспечивает поведение добавления, удаления и индексирования элементов
в списке объектов
В дополнение к этим базовым классам (и интерфейсам) добавляются
несколько специализированных типов коллекций, таких как BitVector32, ListDictionary,
StringDictionary и StringCollection, определенных в пространстве имен System.
Collections.Specialized из сборки System.dll. Это пространство имен также
содержит множество дополнительных интерфейсов и абстрактных классов, которые
можно использовать в качестве отправной точки при создании специальных классов
коллекций.
358 Часть III. Дополнительные конструкции программирования на С#
Хотя за последние годы с применением этих "классических" классов коллекций
(и интерфейсов) было построено немало успешных приложений .NET, опыт показал, что
применение этих типов может быть сопряжено с множеством проблем.
Первая проблема состоит в том, что использование классов коллекций System.
Collections и System.Collections.Specialized приводит к созданию
низкопроизводительного кода, особенно если осуществляются манипуляции со структурами данных
(т.е. типами значения). Как вскоре будет показано, при сохранении структур в
классических классах коллекций среде CLR приходится выполнять массу операций
перемещения данных в памяти, что может значительно снизить скорость выполнения.
Вторая проблема связана с тем, что эти классические классы коллекций не
являются безопасными к типам, так как они (по большей части) были созданы для работы
с System.Object и потому могут содержать в себе все что угодно. Если разработчику
.NET требовалось создать безопасную в отношении типов коллекцию (т.е. контейнер,
который может содержать только объекты, реализующие определенный интерфейс), то
единственным реальным вариантом было создание совершенно нового класса
коллекции собственноручно. Это не слишком трудоемкая задача, но довольно утомительная.
Учитывая эти (и другие) проблемы, разработчики .NET 2.0 добавили новый набор
классов коллекций, собранных в пространстве имен System. Col lections. Generic.
В любом новом проекте, который создается с помощью платформы .NET 2.0 и
последующих версий, предпочтение должно отдаваться соответствующим обобщенным классам
перед унаследованными необобщенными.
На заметку! Следует повториться: любое приложение, которое строится на платформе версии
.NET 2.0 и выше, должно игнорировать классы из пространства имен System.Collect ions,
а использовать вместо них классы из пространства имен System.Collections.Generic.
Прежде чем будет показано, как использовать обобщения в своих программах, стоит
глубже рассмотреть недостатки необобщенных коллекций; это поможет лучше понять
проблемы, которые был призван решить механизм обобщений. Давайте создадим новое
консольное приложение по имени IssuesWithNongenericCollections и затем
импортируем пространство имен System.Collections в начале кода С#:
using System.Collections;
Проблема производительности
Как уже должно быть известно из главы 4, платформа .NET поддерживает две
обширные категории данных: типы значения и ссылочные типы. Поскольку в .NET
определены две основных категории типов, однажды может возникнуть необходимость
представить переменную одной категории в виде переменной другой категории. Для этого в С#
предлагается простой механизм, называемый упаковкой (boxing), который служит для
сохранения данных типа значения в ссылочной переменной. Предположим, что в
методе по имени SimpleBoxUnboxOperation() создана локальная переменная типа int:
static void SimpleBoxUnboxOperation ()
{
// Создать переменную ValueType (int).
int mylnt = 25;
}
Если далее в приложении понадобится представить этот тип значения в виде
ссылочного типа, значение следует упаковать, как показано ниже:
private static void SimpleBoxUnboxOperation ()
{
Глава 10. Обобщения 359
// Создать переменную ValueType (int).
int mylnt = 25;
// Упаковать int в ссылку на object,
object boxedlnt = mylnt;
}
Упаковку можно определить формально как процесс явного присваивания типа
значения переменной System.Object. При упаковке значения CLR-среда размещает в куче
новый объект и копирует значение типа значения (в данном случае — 25) в этот
экземпляр. В качестве результата возвращается ссылка на вновь размещенный в куче
объект. При таком подходе не нужно использовать набор классов-оболочек для временной
трактовки данных стека как объектов, размещенных в куче.
Противоположная операция также возможна, и она называется распаковкой
(unboxing). Распаковка — это процесс преобразования значения, хранящегося в
объектной ссылке, обратно в соответствующий тип значения в стеке. Синтаксически
операция распаковки выглядит как нормальная операция приведения, однако ее семантика
несколько отличается. Среда CLR начинает с проверки того, что полученный тип
данных эквивалентен упакованному типу; и если это так, копирует значение обратно в
находящуюся в стеке переменную. Например, следующие операции распаковки работают
успешно при условии, что типом boxedlnt в действительности является int:
private static void SimpleBoxUnboxOperation()
{
// Создать переменную ValueType (int).
int mylnt = 25;
// Упаковать int в ссылку на object,
object boxedlnt = mylnt;
// Распаковать ссылку обратно в int.
int unboxedlnt = (int)boxedlnt;
}
Когда компилятор С# встречает синтаксис упаковки/распаковки, он генерирует CIL-
код, содержащий коды операций box/unbox. Заглянув в сборку с помощью утилиты
ildasm.exe, можно найти там следующий CIL-код:
.method private hidebysig static void SimpleBoxUnboxOperation () cil managed
{
// Code size 19 @x13)
.maxstack 1
.locals init ([0] int32 mylnt, [1] object boxedlnt, [2] int32 unboxedlnt)
IL_0000: nop
IL_0001: ldc.i4.s 25
IL_0003: stloc.O
IL_0004: ldloc.O
IL_0005: box [mscorlib] System. Int32
IL_000a: stloc.l
IL_000b: ldloc.l
IL_000c: unbox.any [mscorlib]System.Int32
IL_0011: stloc.2
IL_0012: ret
} // end of method Program::SimpleBoxUnboxOperation
Помните, что в отличие от обычного приведения распаковка должна производиться
только в соответствующий тип данных. Попытка распаковать порцию данных в
некорректную переменную приводит к генерации исключения InvalidCastException. Для
полной безопасности следовало бы поместить каждую операцию распаковки в
конструкцию try/catch, однако делать это для абсолютно каждой операции распаковки в
360 Часть III. Дополнительные конструкции программирования на С#
приложении может оказаться довольно трудоемкой задачей. Взгляните на следующий
измененный код, который выдаст ошибку, поскольку предпринята попытка
распаковать упакованный int в long:
private static void SimpleBoxUnboxOperation ()
{
// Создать переменную ValueType (int).
int mylnt = 25;
// Упаковать int в ссылку на object,
object boxedlnt = mylnt;
// Распаковать в неверный тип данных, чтобы инициировать
// исключение времени выполнения.
try
{
long unboxedlnt = (long)boxedlnt;
}
catch (InvalidCastException ex)
{
Console.WriteLine(ex.Message);
}
}
На первый взгляд упаковка/распаковка может показаться довольно
несущественным средством языка, представляющим скорее академический интерес, чем
практическую ценность. На самом деле процесс упаковки /распаковки очень полезен, поскольку
позволяет предположить, что все можно трактовать как System.Object, причем CLR
берет на себя все заботы о деталях, связанных с организацией памяти.
Давайте посмотрим на практическое применение этих приемов. Предположим, что
создан необобщенный класс System. Col lections. Array List для хранения множества
числовых (расположенных в стеке) данных. Члены ArrayList прототипированы для
работы с данными System.Object. Теперь рассмотрим методы Add(), Insert (), Remove(),
а также индексатор класса:
public class ArrayList : object,
IList, ICollection, IEnumerable, ICloneable
{
public virtual int Add(object value);
public virtual void Insert(int index, object value);
public virtual void Remove(object obj);
public virtual object this[int index] {get; set; }
}
Класс ArrayList ориентирован на работу с экземплярами object, которые
представляют данные, расположенные в куче, поэтому может показаться странным, что
следующий код компилируется и выполняется без ошибок:
static void WorkWithArrayList ()
{
// Типы значений упаковываются автоматически
// при передаче методу, запросившему объект.
ArrayList mylnts = new ArrayList ();
mylnts.Add(lO),
mylnts.AddB0),
mylnts.AddC5);
Console.ReadLine();
Глава 10. Обобщения 361
Несмотря на непосредственную передачу числовых данных в методы, которые
принимают тип object, исполняющая среда автоматически упаковывает их в данные,
расположенные в стеке.
При последующем извлечении элемента из ArrayList с использованием
индексатора типа потребуется распаковать операцией приведения объект, находящийся в куче, в
целочисленное значение, расположенное в стеке. Помните, что индексатор ArrayList
возвращает System.Object, а не System.Int32:
static void WorkWithArrayList()
{
// Типы значений автоматически упаковываются, когда
// передаются члену, принимающему объект.
ArrayList mylnts = new ArrayList();
mylnts.Add(lO);
mylnts.AddB0);
mylnts.AddC5);
// Распаковка происходит, когда объект преобразуется
// обратно в расположенные в стеке данные.
int i = (int)mylnts[0];
// Теперь значение вновь упаковывается,
// так как WriteLine() требует объектных типов'
Console.WriteLine("Value of your int: {0}", 1);
Console.ReadLine();
}
Обратите внимание, что расположенные в стеке значения System.Int32
упаковываются перед вызовом ArrayList.AddO, чтобы их можно было передать в требуемом
виде System.Object. Кроме того, объекты System.Object распаковываются
обратно в System.Int32 после их извлечения из ArrayList с использованием индексатора
типа только для того, чтобы вновь быть упакованными для передачи в метод Console.
WriteLine(), поскольку этот метод оперирует переменными System.Object.
Хотя упаковка и распаковка очень удобна с точки зрения программиста, этот
упрощенный подход к передаче данных между стеком и кучей влечет за собой проблемы
производительности (это касается как скорости выполнения, так и размера кода), а
также недостаток безопасности в отношении типов. Чтобы понять, в чем состоят проблемы
с производительностью, взгляните на перечень действий, которые должны быть
выполнены при упаковке и распаковке простого целого числа.
1. Новый объект должен быть размещен в управляемой куче.
2. Значение данных, находящихся в стеке, должно быть передано в выделенное
место в памяти.
3. При распаковке значение, которое хранится в объекте, находящемся в куче,
должно быть передано обратно в стек.
4. Неиспользуемый больше объект в куче будет (в конечном) итоге удален
сборщиком мусора.
Хотя существующий метод Main() не является основным узким местом в смысле
производительности, вы определенно это почувствуете, если ArrayList будет
содержать тысячи целочисленных значений, к которым программа обращается на
регулярной основе. В идеальном случае хотелось бы манипулировать расположенными в стеке
данными внутри контейнера, не имея проблем с производительностью. Было бы
хорошо иметь возможность извлекать данные из контейнера, обходясь без конструкций
try/catch (именно это обеспечивают обобщения).
362 Часть III. Дополнительные конструкции программирования на С#
Проблемы с безопасностью типов
Проблема безопасности типов уже затрагивалась, когда речь шла об операции
распаковки. Вспомните, что данные должны быть распакованы в тот же тип, который
был для них объявлен перед упаковкой. Однако есть и другой аспект безопасности
типов, который следует иметь в виду в мире без обобщений: тот факт, что классы из
System.Collections могут хранить все что угодно, поскольку их члены прототипирова-
ны для работы с System.Object. Например, в следующем методе контейнер ArrayList
хранит произвольные фрагменты несвязанных данных:
static void ArrayListOfRandomObjects()
{
// ArrayList может хранить все что угодно.
ArrayList allMyObject = new ArrayList();
allMyObjects.Add(true);
allMyObjects.Add(new OperatingSystem(PlatformlD.MacOSX, new VersionA0, 0) ) ) ;
allMyObjects.AddF6);
allMyObjects.AddC.14) ;
}
В некоторых случаях действительно необходим исключительно гибкий контейнер,
который может хранить буквально все. Однако в большинстве ситуаций понадобится
безопасный в отношении типов контейнер, который может оперировать только
определенным типом данных, например, контейнер, который хранит только подключения к
базе данных, битовые образы или объекты, совместимые с I Pointy.
До появления обобщений единственным способом решения этой проблемы было
создание вручную строго типизированных коллекций. Предположим, что создана
специальная коллекция, которая может содержать только объекты типа Person:
public class Person
{
public int Age {get; set;}
public string FirstName {get; set;}
public string LastName {get; set;}
public Person () { }
public Person(string firstName, string lastName, int age)
{
Age = age;
FirstName = firstName;
LastName = lastName;
}
public override string ToStringO
{
return string.Format("Name: {0} {1}, Age: {2}",
FirstName, LastName, Age);
}
}
Чтобы построить коллекцию только элементов Person, можно определить
переменную-член System.Collection.ArrayList внутри класса, именуемого PeopleCollection,
и сконфигурировать все члены для работы со строго типизированными объектами
Person вместо объектов типа System.Object. Ниже приведен простой пример
(реальная коллекция производственного уровня должна включать множество
дополнительных членов и расширять абстрактный базовый класс из пространства имен
System. Col lections):
Глава 10. Обобщения 363
public class PeopleCollection : IEnumerable
{
private ArrayList arPeople = new ArrayList() ;
// Приведение для вызывающего кода.
public Person GetPerson(int pos)
{ return (Person)arPeople[pos]; }
// Вставка только объектов Person.
public void AddPerson (Person p)
{ arPeople.Add(p); }
public void ClearPeople ()
{ arPeople.Clear(); }
public int Count
{ get { return arPeople.Count; } }
// Поддержка перечисления с помощью foreach.
IEnumerator IEnumerable.GetEnumerator ()
{ return arPeople.GetEnumerator(); }
}
Обратите внимание, что класс PeopleCollection реализует интерфейс IEnumerable,
который делает возможной итерацию в стиле foreach по всем содержащимся в
коллекции элементам. Кроме того, методы GetPersonO HAddPerson() прототипированы на
работу только с объектами Person, а не битовыми образами, строками, подключениями
к базе данных или другими элементами. За счет создания таких классов обеспечивается
безопасность типов; при этом компилятору С# позволяется определять любую попытку
вставки элемента неподходящего типа:
static void UsePers-onCollection ()
{
Console.WriteLine("***** Custom Person Collection *****\n");
PersonCollection myPeople = new PersonCollection ();
myPeople.AddPerson(new Person("Homer", "Simpson", 40));
myPeople.AddPerson(new Person("Marge", "Simpson", 38));
myPeople.AddPerson(new Person("Lisa", "Simpson", 9));
myPeople.AddPerson(new Person("Bart", "Simpson", 7));
myPeople.AddPerson(new Person("Maggie", "Simpson", 2));
// Это вызовет ошибку при компиляции!
// myPeople.AddPerson(new Car());
foreach (Person p in myPeople)
Console.WriteLine(p);
}
Хотя подобные специальные коллекции гарантируют безопасность типов, такой
подход все же обязывает создавать (почти идентичные) специальные коллекции для
каждого уникального типа данных, который планируется хранить. Таким образом, если
нужна специальная коллекция, которая будет способна оперировать только классами,
унаследованными от базового класса Саг, понадобится построить очень похожий класс
коллекции:
public class CarCollection : IEnumerable
{
private ArrayList arCars = new ArrayList ();
// Приведение для вызывающего кода.
public Car GetCar(int pos)
{ return (Car) arCars[pos]; }
// Вставка только объектов Саг.
public void AddCar(Car с)
{ arCars.Add(с); }
364 Часть III. Дополнительные конструкции программирования на С#
public void ClearCarsO
{ arCars.Clear(); }
public int Count
{ get { return arCars.Count; } }
// Поддержка перечисления с помощью foreach.
IEnumerator IEnumerable.GetEnumerator()
{ return arCars.GetEnumerator(); }
}
Однако эти специальные контейнеры мало помогают в решении проблем упаковки/
распаковки. Даже если создать специальную коллекцию по имени IntCollection,
предназначенную для работы только с элементами System.Int32, все равно придется
выделить некоторый тип объекта для хранения данных (т.е. System.Array и ArrayList):
public class IntCollection : IEnumerable
{
private ArrayList arlnts = new ArrayList () ;
// Распаковать для вызывающего кода.
public int Getlnt(int pos)
{ return (int)arlnts[pos]; }
// Операция упаковки!
public void Addlnt(int i)
{ arlnts.Add(i); }
public void ClearlntsO
{ arlnts.Clear(); }
public int Count
{ get { return arlnts.Count; } }
IEnumerator IEnumerable.GetEnumerator ()
{ return arlnts.GetEnumerator(); }
}
Независимо от того, какой тип выбран для хранения целых чисел, дилеммы
упаковки нельзя избежать, применяя необобщенные контейнеры.
В случае использования классов обобщенных коллекций исчезают все описанные
выше проблемы, включая затраты на упаковку/распаковку и недостаток
безопасности типов. Кроме того, необходимость в построении собственного специального класса
обобщенной коллекции возникает редко. Вместо построения специальных коллекций,
которые могут хранить людей, автомобили и целые числа, можно обратиться к
обобщенному классу коллекции и указать тип хранимых элементов. В показанном ниже
методе класс Lis to (из пространства имен System. Col lection. Generic) используется
для хранения различных типов данных в строго типизированной манере (пока не
обращайте внимания на детали синтаксиса обобщений):
static void UseGenericList ()
{
Console.WriteLine ("***** Fun with Generics *****\n");
// Этот List<> может хранить только объекты Person.
List<Person> morePeople = new List<Person> ();
morePeople.Add (new Person ("Frank", "Black", 50));
Console.WriteLine(morePeople[0]);
// Этот List<> может хранить только целые числа.
List<int> morelnts = new List<int>();
morelnts.AddA0);
morelnts.AddB);
int sum = morelnts [0] + morelnts [1] ;
// Ошибка компиляции! Объект Person не может быть добавлен к списку целых!
// morelnts.Add(new Person ());
Глава 10. Обобщения 365
Первый объект Listo может хранить только объекты Person. Поэтому выполнять
приведение при извлечении элементов из контейнера не требуется, что делает этот
подход более безопасным в отношении типов. Второй Listo может хранить только целые,
и все они размещены в стеке; другими словами, здесь не происходит никакой скрытой
упаковки/распаковки, как это имеет место в необобщенном ArrayList.
Ниже приведен краткий список преимуществ обобщенных контейнеров перед их
необобщенными аналогами.
• Обобщения обеспечивают более высокую производительность, поскольку не
страдают от проблем упаковки /распаковки.
• Обобщения более безопасны в отношении типов, так как могут содержать только
объекты указанного типа.
• Обобщения значительно сокращают потребность в специальных типах
коллекций, потому что библиотека базовых классов предлагает несколько готовых
контейнеров.
Исходный код. Проект IssuesWithNonGenericCollections доступен в подкаталоге
Chapter 10.
Роль параметров обобщенных типов
Обобщенные классы, интерфейсы, структуры и делегаты буквально разбросаны по
всей базовой библиотеке классов .NET, и они могут быть частью любого пространства
имен .NET.
На заметку! Обобщенными могут быть только классы, структуры, интерфейсы и делегаты, но не
перечисления.
Отличить обобщенный элемент в документации .NET Framework 4.0 SDK или
браузере объектов Visual Studio 2010 от других элементов очень легко по наличию пары
угловых скобок с буквой или другой лексемой. На рис. 10.1 показано множество
обобщенных элементов в пространстве имен System.Collections.Generic, включая
выделенный класс List<T>.
Щ Object Browser X Ц
I Вляме .NET Framework 4
■ Slch/^
л (} System.Collections.Generic
> ^ Comparer<T>
> ^ Dictionary<TKey. TVeluo
!> "j)t Dictionery<TKey, TValue>
> *$ Dictionary<TKey, TValuo
t> "!► Dictionary<TKey, TValuo
S> ^$ Dictionary*TKey, TValue>
- „. | 4"
нкнммр
Enumerator
KeyCollection
KeyCollection.Enumerator
ValueCollection
> -ф> Dictionary<TKey,TValue>.ValueCollection.Enumerator
> fy EqualttyComparer<T>
> -° ICollection<T>
t> *"° lComparer<T>
> "° roictionary<TKey, TValue>
^ 'M° lEnumerable<T>
> *"° IEnumerator<7>
r> *^> lEqualityComparer<T>
» —° IList<T>
!> ^X KeyNotFoundException
b p KeyValuePafr<TKey,TVatue>
tIESffl
> -$> List<T>.Enumerator
► Add(T)
4> AddRangeCSystem.Collectioni.Generic.lEnumerabl
♦ AsReadOntyQ
♦ BinarySearch(int, tnt, T, System.Collections.Generic
■'♦ BinarySearchfT)
♦ BinarySearch(T, System.Collections.GenericIComp
♦ Clean!
'• Contair»(T)
♦ ConvertAII<T0utput>(Sy5tem.Converter<T,T0utp
♦ CopyTo(int, T[ 1 int, int)
♦ CopyTo(T[ D
JLfJ^IoflLLinfl .
public class Lirt<T>
Member of Syttcm.Co
Summary:
Represents a strongly typed list of objects that can
be accessed by index. Provides methods to search
sort, and manipulate lists.
■
Рис. 10.1. Обобщенные элементы, поддерживающие параметры типа
366 Часть III. Дополнительные конструкции программирования на С#
Формально эти лексемы можно называть параметрами типа, однако в более
дружественных к пользователю терминах их можно считать просто местами
подстановки (placeholder). Конструкцию <Т> можно воспринимать как типа Т. Таким образом,
IEnumerable<T> можно читать как IEnumerable типа Т, или, говоря иначе,
перечисление типа Т.
На заметку! Имя параметра типа (места подстановки) не важно, и это — дело вкуса того, кто
создает обобщенный элемент. Тем не менее, обычно для представления типов используется т, ТКеу
или К — для представления ключей, а также TValue или V — для представления значений.
При создании обобщенного объекта, реализации обобщенного интерфейса или
вызове обобщенного члена должно быть указано значение для параметра типа. Как в
этой главе, так и в остальной части книги будет показано немало примеров. Однако
для начала следует ознакомиться с основами взаимодействия с обобщенными типами
и членами.
Указание параметров типа для обобщенных классов и структур
При создании экземпляра обобщенного класса или структуры параметр типа
указывается, когда объявляется переменная и когда вызывается конструктор. В предыдущем
фрагменте кода было показано, что UseGenericListO определяет два объекта Listo:
// Этот Listo может хранить только объекты Person.
List<Person> morePeople = new List<Person> () ;
Этот фрагмент можно трактовать как Listo объектов Т, где Т — тип Person, или
более просто — список объектов персон. Однажды указав параметр типа
обобщенного элемента, его нельзя изменить (помните: обобщения предназначены для
поддержки безопасности типов). После указания параметра типа для обобщенного класса или
структуры все вхождения заполнителей заменяются указанным значением.
Просмотрев полное объявление обобщенного класса List<T> в браузере объектов
Visual Studio 2010, можно заметить, что заполнитель Т используется в определении
повсеместно. Ниже приведен частичный листинг (обратите внимание на выделенные
элементы):
// Частичный листинг класса List<T>.
namespace System.Collections.Generic
{
public class List<T> :
IList<T>, ICollection<T>, IEnumerable<T>,
IList, ICollection, IEnumerable
{
public void Add(T item);
public ReadOnlyCollection<T> AsReadOnly();
public int BinarySearch (T item);
public bool Contains (T item);
public void CopyTo(T[] array);
public int Findlndex(System.Predicate<T> match);
public T FindLast(System.Predicate<T> match);
public bool Remove(T item);
public int RemoveAll(System.Predicate<T> match);
public T[] ToArrayO;
public bool TrueForAll(System.Predicate<T> match);
public T this [int index] { get; set; }
}
}
Глава 10. Обобщения 367
Когда создается List<T> с указанием объектов Person, это все равно, как если бы
тип List<T> был определен следующим образом:
namespace System.Collections.Generic
{
public class List<Person> :
IList<Person>, ICollection<Person>, IEnumerable<Person>,
IList, ICollection, IEnumerable
{
public void Add(Person item);
public ReadOnlyCollection<Person> AsReadOnly();
public int BinarySearch(Person item);
public bool Contains(Person item);
public void CopyTo(Person[] array);
public int Findlndex(System.Predicate<Person> match);
public Person FindLast(System.Predicate<Person> match);
public bool Remove(Person item);
public int RemoveAll(System.Predicata<Person> match);
public Person [] ToArrayO;
public bool TrueForAll(System.Predicate<Person> match);
public Person this[int index] { get; set; }
}
}
Разумеется, при создании программистом обобщенной переменной List<T>
компилятор на самом деле не создает буквально совершенно новую реализацию класса
List<T>. Вместо этого он обрабатывает только члены обобщенного типа, к которым
действительно производится обращение.
На заметку! В следующей главе рассматриваются обобщенные делегаты, которые также при
создании требуют указания параметра типа.
Указание параметров типа для обобщенных членов
Для необобщенного класса или структуры вполне допустимо поддерживать
несколько обобщенных членов (т.е. методов и свойств). В таких случаях указывать значение
заполнителя нужно также и во время вызова метода. Например, System.Array
поддерживает несколько обобщенных методов (которые появились в .NET 2.0). В частности,
статический метод Sort() теперь имеет обобщенный конструктор по имени Sort<T>().
Рассмотрим следующий фрагмент кода, в котором Т — это тип int:
int[] mylnts = { 10, 4, 2, 33, 93 };
// Указание заполнителя для обобщенного метода Sort<>().
Array.Sort<int>(mylnts);
foreach (int i in mylnts)
{
Console.WriteLine(i);
}
Указание параметров типов для обобщенных интерфейсов
Обобщенные интерфейсы обычно реализуются при построении классов или
структур, которые должны поддерживать различные поведения платформы (т.е.
клонирование, сортировку и перечисление). В главе 9 рассматривалось множество
необобщенных интерфейсов, таких как IComparable, IEnumerable, IEnumerator и IComparer.
Вспомните, как определен необобщенный интерфейс IComparable:
368 Часть III. Дополнительные конструкции программирования на С#
public interface IComparable
{
int CompareTo(object obj);
}
В той же главе 9 этот интерфейс был реализован в классе Саг для обеспечения
сортировки в стандартном массиве. Однако код требовал нескольких проверок
времени выполнения и операций приведения, потому что параметром был общий тип
System.Object:
public class Car : IComparable
{
// Реализация IComparable.
int IComparable.CompareTo (object obj )
{
Car temp = obj as Car;
if (temp ' = null)
{
if (this.CarlD > temp.CarlD)
return 1;
if (this.CarlD < temp.CarlD)
return -1;
else
return 0;
}
else
throw new ArgumentException("Parameter is not a Car!");
}
}
Теперь воспользуемся обобщенным аналогом этого интерфейса:
public interface IComparable<T>
-{
int CompareTo(T obj);
}
В этом случае код реализации будет значительно яснее:
public class Car : IComparable<Car>
{
// Реализация IComparable<T>.
int IComparable<Car>.CompareTo (Car obj)
{
if (this.CarlD > ob].CarlD)
return 1;
if (this.CarlD < obj.CarlD)
return -1;
else
return 0;
}
}
Здесь уже не нужно проверять, относится ли входной параметр к типу Саг, потому
что он может быть только Саг! В случае передачи несовместимого типа данных
возникает ошибка времени компиляции.
Получив начальные сведения о том, как взаимодействовать с обобщенными элементами,
а также ознакомившись с ролью параметров типа (т.е. заполнителей), можно приступать к
изучению классов и интерфейсов из пространства имен System.Collect ions.Generic.
Глава 10. Обобщения 369
Пространство имен
System.Collections .Generic
Основная часть пространства имен System.Collections.Generic располагается в
сборках mscorlib.dll и system.dll. В начале этой главы кратко упоминались
некоторые из необобщенных интерфейсов, реализованных необобщенными классами
коллекций. Не должно вызывать удивление, что в пространстве имен System.Collections.
Generic определены обобщенные замены для многих из них.
В действительность есть много обобщенных интерфейсов, которые расширяют
свои необобщенные аналоги. Это может показаться странным; однако благодаря этому
реализации новых классов также поддерживают унаследованную функциональность,
имеющуюся у их необобщенных аналогов. Например, IEnumerable<T> расширяет
IEnumerable. В табл. 10.3 документированы основные обобщенные интерфейсы, с
которыми придется иметь дело при работе с обобщенными классами коллекций.
На заметку! Если вы работали с обобщениями до выхода .NET 4.0, то должны быть знакомы с
типами lSet<T> и SortedSet<T>, которые более подробно рассматриваются далее в этой главе.
Таблица 10.3. Основные интерфейсы, поддерживаемые классами из пространства
имен System.Collections.Generic
Интерфейс Назначение
System.Collections
ICollection<T> Определяет общие характеристики (например, размер, перечисление
и безопасность к потокам) для всех типов обобщенных коллекций
IComparer<T> Определяет способ сравнения объектов
IDictionary<TKey, Позволяет объекту обобщенной коллекции представлять свое со-
TValue> держимое посредством пар "ключ/значение"
IEnumerable<T> Возвращает интерфейс IEnumerator<T> для заданного объекта
IEnumerator<T> Позволяет выполнять итерацию в стиле f oreach по элементам
коллекции
IList<T> Обеспечивает поведение добавления, удаления и индексации
элементов в последовательном списке объектов
ISet<T> Предоставляет базовый интерфейс для абстракции множеств
В пространстве имен System.Collectiobs.Generic также определен набор
классов, реализующих многие из этих основных интерфейсов. В табл. 10.4 описаны часто
используемые классы из этого пространства имен, реализуемые ими интерфейсы и их
базовая функциональность.
Пространство имен System.Collections.Generic также определяет ряд
вспомогательных классов и структур, работающих в сочетании со специфическим контейнером.
Например, тип LinkedListNode<T> представляет узел внутри обобщенного контейнера
LinkedList<T>, исключение KeyNotFoundException генерируется при попытке
получить элемент из коллекции с указанием несуществующего ключа, и т.д.
Следует отметить, что mscorlib.dll и System.dll — не единственные сборки,
которые добавляют новые типы в пространство имен System.Collections.Generic.
Например, System.Core.dll добавляет класс HashSet<T>. Детальные сведения о
пространстве имен System.Collections.Generic доступны в документации .NET
Framework 4.0 SDK.
370 Часть III. Дополнительные конструкции программирования на С#
Таблица 10.4. Классы из пространства имен System.Collections.Generic
Обобщенный класс
Поддерживаемые основные
интерфейсы
Назначение
Dictionary<TKey,
TValue>
List<T>
LinkedList<T>
Queue<T>
SortedDictionary<TKey,
TValue>
SortedSet<T>
Stack<T>
ICollection<T>,
IDictionary<TKey, TValue>,
IEnumerable<T>
ICollection<T>,
IEnumerable<T>, IList<T>
ICollection<T>,
IEnumerable<T>
ICollection (Это не опечатка!
Это интерфейс необобщенной
коллекции!), IEnumerable<T>
ICollection<T>,
IDictionary<TKey, TValue>,
IEnumerable<T>
ICollection<T>,
IEnumerable<T>, ISet<T>
ICollection (Это не опечатка!
Это интерфейс необобщенной
коллекции!), IEnumerable<T>
Представляет
обобщенную коллекцию ключей и
значений
Динамически
изменяемый последовательный
список элементов
Представляет
двунаправленный список
Обобщенная реализация
очереди — списка,
работающего по алгоритму
"первые вошел —
первый вышел" (FIFO)
Обобщенная
реализация сортированного
множества пар "ключ/
значение"
Представляет коллекцию
объектов,
поддерживаемых в сортированном
порядке без дублирования
Обобщенная реализация
стека — списка,
работающего по алгоритму
"последний вошел —
первый вышел" (LIFO)
В любом случае следующей задачей будет научиться использовать некоторые из этих
обобщенных классов коллекций. Но прежде давайте рассмотрим языковые средства С#
(впервые появившиеся в .NET 3.5), которые упрощают наполнение данными
обобщенных (и необобщенных) коллекций.
Синтаксис инициализации коллекций
В главе 4 был представлен синтаксис инициализации объектов, который
позволяет устанавливать свойства для новой переменной во время ее конструирования. Тесно
связан с этим синтаксис инициализации коллекций. Это средство языка С#
позволяет наполнять множество контейнеров (таких как ArrayList или List<T>)
элементами, используя синтаксис, похожий на тот, что применяется для наполнения базового
массива.
На заметку! Синтаксис инициализации коллекций может применяться только к классам,
которые поддерживают метод Add(), формализованный интерфейсами lCollection<T>/
ICollection.
Глава 10. Обобщения 371
Рассмотрим следующие примеры:
// Инициализация стандартного массива.
int[] myArrayOflnts = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// Инициализация обобщенного списка Listo элементов int.
List<int> myGenericList = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// Инициализация ArrayList числовыми данными.
ArrayList myList = new ArrayList { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Если контейнер управляет коллекцией классов или структур, можно смешивать
синтаксис инициализации объектов с синтаксисом инициализации коллекций,
создавая некоторый функциональный код. Возможно, вы помните класс Point из главы 5,
в котором были определены два свойства X и Y. Чтобы построить обобщенный список
List<T> объектов Р, можно написать такой код:
List<Point> myListOfPoints = new List<Point>
{
new Point { X = 2, Y = 2 },
new Point { X = 3, Y = 3 },
new Point(PointColor.BloodRed){ X = 4, Y = 4 }
};
foreach (var pt in myListOfPoints)
{
Console.WriteLine (pt);
}
Преимущество этого синтаксиса в экономии большого объема клавиатурного ввода.
Хотя вложенные фигурные скобки затрудняют чтение, если не позаботиться о
форматировании, представьте объем кода, который бы потребовался для наполнения
следующего списка List<T> объектов Rectangle, если бы не было синтаксиса инициализации
коллекций (вспомните, как в главе 4 создавался класс Rectangle, который содержал два
свойства, инкапсулирующих объекты Point):
List<Rectangle> myListOfRects = new List<Rectangle>
{
new Rectangle {TopLeft = new Point { X = 10, Y = 10 },
BottomRight = new Point { X = 200, Y = 200}},
new Rectangle {TopLeft = new Point { X = 2, Y = 2 },
BottomRight = new Point { X = 100, Y = 100}},
new Rectangle {TopLeft = new Point { X = 5, Y = 5 },
BottomRight = new Point { X = 90, Y = 75}}
};
foreach (var r in myListOfRects)
{
Console.WriteLine(r);
}
Работа с классом List<T>
Для начала создадим новый проект консольного приложения по имени
FunWithGenericCollections. Проекты этого типа автоматически ссылаются на
сборки mscorlib.dll и System.dll, что обеспечивает доступ к большинству обобщенных
классов коллекций. Обратите внимание, что в первоначальном файле кода С# уже
импортируется пространство имен System.Collections.Generic.
Первый обобщенный класс, который мы рассмотрим — это List<T>, который
уже использовался ранее в этой главе. Из всех классов пространства имен System.
Collections.Generic класс List<T> будет применяться наиболее часто, потому что
он позволяет динамически изменять размер своего содержимого. Чтобы проиллюстри-
372 Часть III. Дополнительные конструкции программирования на С#
ровать основы этого типа, добавьте в класс Program метод UseGenericListO, в
котором List<T> используется для манипуляций множеством объектов Person; вы должны
помнить, что в классе Person определены три свойства (Age, FirstName и LastName) и
специальная реализация метода ToStringO.
private static void UseGenericListO
{
// Создать список объектов Person и заполнить его с помощью
// синтаксиса инициализации объектов/коллекций.
List<Person> people = new List<Person> ()
{
new Person {FirstName= "Homer", LastName="Simpson", Age=47},
new Person {FirstName= "Marge", LastName="Simpson", Age=45},
new Person {FirstName= "Lisa", LastName="Simpson", Age=9},
new Person {FirstName= "Bart", LastName="Simpson", Age=8}
};
// Вывести на консоль количество элементов в списке.
Console.WriteLine("Items in list: {0}", people.Count);
// Перечислить список,
foreach (Person p in people)
Console.WriteLine(p);
// Вставить новую персону.
Console.WriteLine("\n->Inserting new person.");
people.Insert B, new Person { FirstName = "Maggie", LastName = "Simpson", Age = 2 });
Console.WriteLine("Items in list: {0}", people.Count);
// Скопировать данные в новый массив.
Person[] arrayOfPeople = people.ToArray() ;
for (int i=0; i < arrayOfPeople.Length; i++)
{
Console.WriteLine("First Names: {0}", arrayOfPeople[l].FirstName);
}
}
Здесь вы используете синтаксис инициализации для наполнения вашего List<T>
объектами, как сокращенную нотацию вызовов Add () п раз. Выведя количество
элементов в коллекции (а также пройдясь по каждому элементу), вы вызываете Insert ().
Как можно видеть, Insert () позволяет вам вставить новый элемент в List<T> по
указанному индексу.
И наконец, обратите внимание на вызов метода ToArray(), который возвращает
массив объектов Person, основанный на содержимом исходного List<T>. Затем вы
выполняете проход по всем элементам этого массива, используя синтаксис индекса
массива. Если вы вызовете этот метод из Main(), то получите следующий вывод:
***** Fun with Generic Collections *****
Items in list: 4
Name: Homer Simpson, Age: 47
Name: Marge Simpson, Age: 4 5
Name: Lisa Simpson, Age: 9
Name: Bart Simpson, Age: 8
->Inserting new person.
Items in list: 5
First Names: Homer
First Names: Marge
First Names: Maggie
First Names: Lisa
First Names: Bart
Глава 10. Обобщения 373
В классе List<T> определено множество дополнительных членов,
представляющих интерес, поэтому за дополнительной информацией обращайтесь в документацию
.NET Framework 4.0 SDK. Теперь рассмотрим еще несколько обобщенных коллекций:
Stack<T>, Queue<T> и SortedSet<T>. Это даст более полное понимание базовых
вариантов хранения данных в приложении.
Работа с классом Stack<T>
Класс Stack<T> представляет коллекцию элементов, работающую по алгоритму
"последний вошел — первый вышел" (LIFO). Как и можно было ожидать, в Stack<T>
определены члены Push() иРор(), предназначенные для вставки и удаления элементов в
стеке. Приведенный ниже метод создает коллекцию Stack<T> объектов Person:
static void UseGenericStack()
{
Stack<Person> stackOfPeople = new Stack<Person>();
stackOfPeople.Push(new Person
{ FirstName = "Homer", LastName = "Simpson", Age = 47 });
stackOfPeople.Push(new Person
{ FirstName = "Marge", LastName = "Simpson", Age =45 });
stackOfPeople.Push(new Person
{ FirstName = "Lisa", LastName = "Simpson", Age =9 });
// Просмотреть верхний элемент, вытолкнуть его и просмотреть снова.
Console.WriteLine("First person is: {0}", stackOfPeople.Peek ());
Console.WriteLine("Popped off {0}", stackOfPeople.Pop ());
Console.WriteLine("\nFirst person is: {0}", stackOfPeople.Peek());
Console.WriteLine("Popped off {0}", stackOfPeople.Pop ());
Console.WriteLine("\nFirst person item is: {0}", stackOfPeople.Peek());
Console.WriteLine("Popped off {0}", stackOfPeople.Pop());
try
{
Console.WriteLine("\nFirst person is: {0}", stackOfPeople.Peek());
Console.WriteLine("Popped off {0}", stackOfPeople . Pop ()) ;
}
catch (InvalidOperationException ex)
{
Console.WriteLine("\nError! {0}", ex.Message); // стек пуст
}
}
В коде строится стек, содержащий информацию о трех людях, добавленных по
порядку имен: Homer, Marge и Lisa. Заглядывая (peek) в стек, вы всегда видите объект,
находящийся на его вершине; поэтому первый вызов Реек() вернет третий объект Person.
После серии вызовов Рор() и Реек() стек, наконец, опустошается, после чего вызовы
Peek() и Рор() приводят к генерации системного исключения. Вывод этого примера
показан ниже:
***** Fun with Generic Collections *****
First person is: Name: Lisa Simpson, Age: 9
Popped off Name: Lisa Simpson, Age: 9
First person is: Name: Marge Simpson, Age: 45
Popped off Name: Marge Simpson, Age: 45
First person item is: Name: Homer Simpson, Age: 47
Popped off Name: Homer Simpson, Age: 47
Error1 Stack empty.
374 Часть III. Дополнительные конструкции программирования на С#
Работа с классом Queue<T>
Очереди — это контейнеры, гарантирующие доступ к элементам в стиле "первый
вошел — первый вышел" (FIFO). К сожалению, людям приходится сталкиваться с
очередями каждый день: очереди в банк, очереди в кинотеатр, очереди в кафе. Когда нужно
смоделировать сценарий, в котором элементы обрабатываются в режиме FIFO, класс
Queue<T> подходит наилучшим образом. В дополнение к функциональности,
предоставляемой поддерживаемыми интерфейсами, Queue определяет основные члены,
которые перечислены в табл. 10.5.
Таблица 10.5. Члены типа Queue<T>
Член Назначение
Dequeue () Удаляет и возвращает объект из начала Queue<T>
Enqueue () Добавляет объект в конец Queue<T>
Peek () Возвращает объект из начала Queue<T>, не удаляя его
Теперь давайте посмотрим на эти методы в работе. Можно снова вернуться к
классу Person и построить объект Queue<T>, эмулирующий очередь людей, которые
ожидают заказа кофе. Для начала представим, что имеется следующий статический
метод:
static void GetCoffee(Person p)
{
Console.WriteLine ( "{0} got coffee!", p.FirstName);
}
Кроме того, есть также дополнительный вспомогательный метод, который вызывает
GetCof fee () внутренне:
static void UseGenericQueue ()
{
// Создать очередь из трех человек.
Queue<Person> peopleQ = new Queue<Person> ();
peopleQ.Enqueue (new Person {FirstIIame= "Homer",
LastName="Simpson", Age=47});
peopleQ.Enqueue (new Person {FirstName= "Marge",
LastName="Simpson", Age=45});
peopleQ.Enqueue(new Person {FirstName= "Lisa",
LastName="Sirrpson", Age=9});
// Кто первый в очереди?
Console.WriteLine("{0} is first in line!", peopleQ.Peek().FirstName);
// Удалить всех из очереди.
GetCoffee(peopleQ.Dequeue ()),
GetCoffee(peopleQ.Dequeue()),
GetCoffee(peopleQ.Dequeue()),
// Попробовать извлечь кого-то из очереди снова?
try
{
GetCoffee(peopleQ.Dequeue() ) ;
}
catch(InvalidOperationException e)
{
Console.WriteLine("Error' {0}", e.Message); // очередь пуста
}
}
Глава 10. Обобщения 375
Здесь вы вставляете три элемента в класс Queue<T>, используя метод Enqueue().
Вызов Peek () позволяет вам просматривать (но не удалять) первый элемент,
находящийся в данный момент в Queue. Наконец, вызов Dequeue () удаляет элемент из
очереди и посылает его вспомогательной функции GetCof fee () для обработки. Заметьте, что
если вы пытаетесь удалять элементы из пустой очереди, генерируется исключение
времени выполнения. Приведем вывод, который вы получаете при вызове этого метода:
***** Fun with Generic Collections *****
Homer is first in line1
Homer got coffee!
Marge got coffee!
Lisa got coffee!
Error! Queue empty.
Работа с классом SortedSet<T>
Последний из классов обобщенных коллекций, который мы рассмотрим здесь,
появился в версии .NET 4.0. Класс SortedSet<T> удобен тем, что при вставке или
удалении элементов он автоматически обеспечивает сортировку элементов в наборе. Класс
SortedSet<T> понадобится информировать о том, как должны сортироваться
объекты, за счет передачи его конструктору аргумента — объекта, реализующего интерфейс
IComparer<T>.
Начнем с создания нового класса по имени SortPeopleByAge, реализующего
IComparer<T>, где Т — тип Person. Вспомните, что этот интерфейс определяет
единственный метод по имени Compare(), в котором можно запрограммировать логику
сравнения элементов. Ниже приведена простая реализация этого класса:
class SortPeopleByAge : IComparer<Person>
{
public int Compare(Person firstPerson, Person secondPerson)
{
if (firstPerson.Age > secondPerson.Age)
return 1;
if (firstPerson.Age < secondPerson.Age)
return -1;
else
return 0;
}
}
Теперь добавим в класс Program следующий новый метод, который должен будет
вызван в Main():
private static void UseSortedSet ()
{
// Создать несколько людей разного возраста.
SortedSet<Person> setOfPeople = new SortedSet<Person>(new SortPeopleByAge ())
{
new Person {FirstName= "Homer", LastName="Simpson", Age=47},
new Person {FirstName= "Marge", LastName="Simpson", Age=45},
new Person {FirstName= "Lisa", LastName="Simpson", Age=9},
new Person {FirstName= "Bart", LastName="Simpson", Age=8}
};
// Обратите внимание, что элементы отсортированы по возрасту.
foreach (Person p in setOfPeople)
{
Console.WriteLine(p);
}
376 Часть III. Дополнительные конструкции программирования на С#
Console.WriteLine() ;
// Добавить еще несколько людей разного возраста.
setOfPeople.Add(new Person { FirstName = "Saku", LastName = "Jones", Age =1 });
setOfPeople.Add(new Person { FirstName = "Mikko", LastName = "Jones", Age = 32 });
// Элементы по-прежнему отсортированы по возрасту.
foreach (Person p in setOfPeople)
{
Console.WriteLine(p);
}
}
После запуска приложения видно, что список объектов будет всегда упорядочен
по значению свойства Age, независимо от порядка вставки и удаления объектов в
коллекцию:
***** Fun with Generic Collections *****
Name: Bart Simpson, Age: 8
Name: Lisa Simpson, Age: 9
Name: Marge Simpson, Age: 4 5
Name: Homer Simpson, Age: 4 7
Name: Saku Jones, Age: 1
Name: Bart Simpson, Age: 8
Name: Lisa Simpson, Age: 9
Name: Mikko Jones, Age: 32
Name: Marge Simpson, Age: 4 5
Name: Homer Simpson, Age: 4 7
Великолепно! Теперь вы должны почувствовать себя увереннее, причем не только в
отношении преимуществ обобщенного программирования вообще, но также в
использовании обобщенных типов из библиотеки базовых классов .NET. В завершении этой
главы будет также показано, как и для чего строить собственные обобщенные типы и
обобщенные методы.
Исходный код. Проект FunWithGenericCollections доступен в подкаталоге Chapter 10.
Создание специальных обобщенных методов
Хотя большинство разработчиков обычно используют существующие обобщенные
типы из библиотек базовых классов, можно также строить собственные обобщенные
методы и специальные обобщенные типы. Чтобы понять, как включать обобщения в
собственные проекты, начнем с построения обобщенного метода обмена,
предварительно создав новое консольное приложение по имени GenericMethods.
Построение специальных обобщенных методов представляет собой более развитую
версию традиционной перегрузки методов. В главе 2 было показано, что перегрузка —
это определение нескольких версий одного метода, отличающихся друг от друга
количеством или типами параметров.
Хотя перегрузка — полезное средство объектно-ориентированного языка, при этом
возникает проблема, вызванная появлением огромного числа методов, которые по
существу делают одно и то же. Например, предположим, что требуется создать методы,
которые позволяют менять местами два фрагмента данных. Можно начать с написания
простого метода для обмена двух целочисленных значений:
Глава 10. Обобщения 377
// Обмен двух значений int.
static void Swap(ref int a, ref int b)
{
int temp;
temp = a;
a = b;
b = temp;
}
Пока все хорошо. А теперь представим, что нужно поменять местами два объекта
Person; для этого понадобится новая версия метода Swap():
// Обмен двух объектов Person.
static void Swap(ref Person a, ref Person b)
{
Person temp;
temp = a;
a = b;
b = temp;
}
Уже должно стать ясно, куда это ведет. Если также потребуется поменять местами два
значения с плавающей точкой, две битовые карты, два объекта автомобилей, придется
писать дополнительные методы, что в конечном итоге превратится в кошмар при
сопровождении. Правда, можно построить один (необобщенный) метод, оперирующий
параметрами типа object, но тогда возникнут проблемы, которые были описаны ранее в этой
главе, те. упаковка, распаковка, недостаток безопасности типов, явное приведение и т.п.
Всякий раз, когда имеется группа перегруженных методов, отличающихся только
входными аргументами — это явный признак того, что за счет применения обобщений
удастся облегчить себе жизнь. Рассмотрим следующий обобщенный метод Swap<T>,
который может менять местами два значения Т:
// Этот метод обменивает между собой значения двух
// элементов типа, переданного в параметре <Т>.
static void Swap<T>(ref T a, ref T b)
{
Console.WriteLine("You sent the Swap() method a {0}", typeof(T));
T temp;
temp = a;
a = b;
b = temp;
}
Обратите внимание, что обобщенный метод определен за счет спецификации
параметра типа после имени метода и перед списком параметров. Здесь устанавливается,
что метод Swap () может оперировать любыми двумя параметрами типа <Т>. Чтобы
немного прояснить картину, имя подставляемого типа выводится на консоль с
использованием операции typeof (). Теперь рассмотрим следующий метод Мат(),
обменивающий значениями целочисленные и строковые переменные:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Custom Generic Methods *****\n");
// Обмен двух значений int.
int a = 10, b = 90;
Console.WriteLine("Before swap: {0}, {1}", a, b) ;
Swap<int>(ref a, ref b) ;
Console.WriteLine("After swap: {0}, {1}", a, b) ;
Console.WriteLine();
378 Часть III. Дополнительные конструкции программирования на С#
// Обмен двух строк,
string si = "Hello", s2 = "There";
Console.WriteLine("Before swap: {0} {1}!", si, s2) ;
Swap<string> (ref si, ref s2);
Console.WriteLine("After swap: {0} {1}!", si, s2);
Console.ReadLine();
}
Ниже показан вывод этого примера:
***** Fun with Custom Generic Methods *****
Before swap: 10, 90
You sent the SwapO method a System. Int32
After swap: 90, 10
Before swap: Hello There 1
You sent the SwapO method a System. String
After swap: There Hello!
Основное преимущество этого подхода в том, что нужно будет сопровождать только
одну версию Swap<T>(), хотя она может оперировать любыми двумя элементами
определенного типа, причем в безопасной к типам манере. Еще лучше то, что находящиеся
в стеке элементы остаются в стеке, а расположенные в куче — соответственно, в куче.
Выведение параметра типа
При вызове таких обобщенных методов, как Swap<T>, можно опускать параметр тип,
если (и только если) обобщенный метод требует аргументов, поскольку компилятор
может вывести параметр типа из параметров членов. Например, добавив к Main ()
следующий код, можно обменивать значения System.Boolean:
// Компилятор самостоятельно выведет тип System.Boolean.
bool Ы = true, Ь2 = falser-
Console .WriteLine ("Before swap: {0}, {1}", Ы, b2);
Swap(ref Ы, ref b2) ;
Console.WriteLine ("After swap: {0}, {1}", Ы, b2) ;
Несмотря на то что компилятор может определить параметр типа на основе типа
данных, использованного в объявлении Ы и Ь2, стоит выработать привычку всегда
указывать параметр типа явно:
Swap<bool> (ref Ы, ref Ь2);
Это даст понять неопытным программистам, что данный метод на самом деле
является обобщенным. Более того, выведение типов параметров работает только в том
случае, если обобщенный метод имеет, по крайней мере, один параметр. Например,
предположим, что в классе Program определен следующий обобщенный метод:
static void DisplayBaseClass<T> ()
{
// BaseType — это метод, используемый в рефлексии;
// он будет рассматриваться в главе 15.
Console.WriteLine("Base class of {0} is: {1}.",
typeof(T), typeof(T).BaseType);
}
При его вызове потребуется указать параметр типа:
static void Main(string [ ] args)
{
// Необходимо указать параметр типа если метод не принимает параметров.
Глава 10. Обобщения 379
DisplayBaseClass<int>();
DisplayBaseClass<string>();
// Ошибка на этапе компиляции! Нет параметров?
// Значит, необходимо указать тип для подстановки!
// DisplayBaseClass() ;
Console.ReadLine();
}
В настоящее время обобщенные методы Swap<T> и DisplayBaseClass<T>
определены в классе Program приложения. Конечно, как и любой другой метод, если вы
захотите определить эти члены в отдельном классе (MyGenericMethods), то можно поступить
так:
public static class MyGenericMethods
{
public static void Swap<T>(ref T a, ref T b)
{
Console.WriteLine("You sent the SwapO method a {0}",
typeof(T));
T temp;
temp = a;
a = b;
b = temp;
}
public static void DisplayBaseClass<T> ()
{
Console.WriteLine ("Base class of {0} is: {1}.",
typeof(T), typeof(T).BaseType);
}
}
Статические методы Swap<T>HDisplayBaseClass<T> находятся в контексте нового
статического типа класса, поэтому потребуется указать имя типа при вызове каждого
члена, например:
MyGenericMethods.Swap<int> (ref a, ref b) ;
Разумеется, методы не обязательно должны быть статическими. Если бы Swap<T> и
DisplayBaseClass<T> были методами уровня экземпляра (определенными в
нестатическом классе), нужно было бы просто создать экземпляр MyGenericMethods и вызывать
их с использованием объектной переменной:
MyGenericMethods с = new MyGenericMethods();
c.Swap<int>(ref a, ref b) ;
Исходный код. Проект CustomGenericMethods доступен в подкаталоге Chapter 10.
Создание специальных обобщенных
структур и классов
Теперь, когда известно, как определяются и вызываются обобщенные методы,
давайте посмотрим, как конструировать обобщенную структуру (процесс построения
обобщенного класса идентичен) в новом проекте консольного приложения по имени
GenericPoint. Предположим, что строится обобщенная структура Point, которая
поддерживает единственный параметр типа, определяющий внутреннее представление
координат (х, у). Вызывающий код должен иметь возможность создавать типы Point<T>
следующим образом:
380 Часть III. Дополнительные конструкции программирования на С#
// Точка с координатами int.
Point<int> p = new Point<int>A0, 10);
// Точка с координатами double.
Point<double> р2 = new Point<double>E.4, 3.3);
А вот полное определение Point<T> с последующим анализом:
// Обобщенная структура Point.
public struct Point<T>
{
// Обобщенные данные состояния.
private T xPos;
private T yPos;
// Обобщенный конструктор.
public Point(T xVal, T yVal)
{
xPos = xVal;
yPos = yVal;
}
// Обобщенные свойства.
public T X
{
get { return xPos; }
set { xPos = value; }
}
public T Y
{
get { return yPos; }
set { yPos = value; }
}
public override string ToStringO
{
return string.Format (" [{0 }, {1}]", xPos, yPos);
}
// Сбросить поля в значения по умолчанию для заданного параметра типа.
public void ResetPointO
{
xPos = default(T);
yPos = default(T);
Ключевое слово default в обобщенном коде
Как видите, Point<T> использует параметр типа в определении данных полей,
аргументов конструктора и определении свойств. Обратите внимание, что в дополнение
к переопределению ToStringO, в Point<T> определен метод по имени ResetPointO, в
котором используется некоторый новый синтаксис:
// Ключевое слово default перегружено в С#.
// При использовании с обобщениями оно представляет
// значение по умолчанию для параметра типа.
public void ResetPointO
{
X = default(T);
Y = default(T);
}
Глава 10. Обобщения 381
С появлением обобщений ключевое слово default обрело второй смысл. В
дополнение к использованию с конструкцией switch оно теперь может применяться для
установки значения по умолчанию для параметра типа. Это очень удобно, учитывая, что
обобщенный тип не знает заранее, что будет подставлено вместо заполнителя в угловых
скобках, и потому не может безопасно строить предположений о значениях по
умолчанию. Умолчания для параметров типа следующие:
• значение по умолчанию числовых величин равно 0;
• ссылочные типы имеют значение по умолчанию null;
• поля структур устанавливаются в 0 (для типов значений) или в null (для
ссылочных типов).
Для Point<T> можно было установить значение X и Y в 0 напрямую, исходя из
предположения, что вызывающий код будет применять только числовые значения. Однако
за счет использования синтаксиса default(T) повышается общая гибкость
обобщенного типа. В любом случае теперь можно использовать методы Point<T> следующим
образом:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Generic Structures *****\n");
// Объект Point, в котором используются int.
Point<int> p = new Point<int>A0, 10);
Console.WriteLine ("p.ToString () ={ 0} ", p.ToStringO ) ;
p.ResetPoint();
Console.WriteLine("p.ToString()={0}", p.ToString ());
Console.WriteLine ();
// Объект Point, в котором используются double.
Point<double> p2 = new Point<double>E.4, 3.3);
Console.WriteLine("p2.ToString()={0}", p2.ToString ());
p2.ResetPoint();
Console.WriteLine("p2.ToString()={0}", p2.ToString ());
Console.ReadLine();
}
Ниже показан вывод этого примера:
***** Fun with Generic Structures *****
p.ToString()=[10, 10]
p.ToStnng() = [0, 0]
p2.ToStnng() = [5.4, 3.3]
p2.ToStnng() = [0, 0]
Исходный код. Проект GenericPoint доступен в подкаталоге Chapter 10.
Обобщенные базовые классы
Обобщенные классы могут служить базовыми для других классов, и потому
определять любое количество виртуальных или абстрактных методов. Однако типы-нас-
| ледники должны подчиняться нескольким правилам, чтобы гарантировать сохранение
природы обобщенной абстракции. Во-первых, если необобщенный класс расширяет
I обобщенный класс, то класс-наследник должен указывать параметр типа:
382 Часть III. Дополнительные конструкции программирования на С#
// Предположим, что создан специальный класс обобщенного списка.
public class MyList<T>
{
private List<T> listOfData = new List<T>();
}
// Необобщенные типы должны указывать параметр типа
// при наследовании от обобщенного базового класса.
public class MyStnngList : MyList<string>
{}
Во-вторых, если обобщенный базовый класс определяет обобщенные или
абстрактные методы, то тип-наследник должен переопределять обобщенные методы, используя
указанный параметр типа:
// Обобщенный класс с виртуальным методом.
public class MyList<T>
{
private List<T> listOfData = new List<T>();
public virtual void PrintList(T data) { }
}
public class MyStnngList : MyList<string>
{
// Должен подставлять параметр типа, использованный
//в родительском классе, в унаследованные методы.
public override void PrintList(string data) { }
}
В-третьих, если тип-наследник также является обобщенным, дочерний класс может
(дополнительно) повторно использовать тот же заполнитель типа в своем определении.
Однако имейте в виду, что любые ограничения, наложенные на базовый класс, должны
соблюдаться в типе-наследнике. Например:
// Обратите внимание на ограничение — конструктор по умолчанию.
public class MyList<T> where T : new()
{
private List<T> listOfData = new List<T>();
public virtual void PrintList(T data) { }
}
// Тип-наследник должен соблюдать ограничения.
public class MyReadOnlyList<T> : MyList<T> where T : new()
{
public override void PrintList(T data) { }
}
При решении повседневных программистских задач вряд ли часто придется
создавать иерархии специальных обобщенных классов. Тем не менее, это вполне возможно
(до тех пор, пока вы придерживаетесь правил).
Ограничение параметров типа
Как показано в этой главе, любой обобщенный элемент имеет, по крайней мере, один
параметр типа, который должен быть указан при взаимодействии с обобщенным
типом или членом. Одно это позволит строить безопасный в отношении типов код;
однако платформа .NET позволяет использовать ключевое слово where для"указания особых
требований к определенному параметру типа.
С помощью ключевого слова this можно добавлять набор ограничений к
конкретному параметру типа, которые компилятор С# проверит во время компиляции. В
частности, параметр типа можно ограничить, как описано в табл. 10.6.
Глава 10. Обобщения 383
Таблица 10.6. Возможные ограничения параметров типа для обобщений
Ограничение обобщения Назначение
where T : struct Параметр типа <Т> должен иметь в своей цепочке
наследования System.ValueType. Другими словами, <Т>
должен быть структурой
where T : class Параметр типа <Т> должен не иметь System .ValueType
в своей цепочке наследования (т.е. <т>
должен быть ссылочным типом)
where T : new() Параметр типа <Т> должен иметь конструктор по
умолчанию. Это очень полезно, если обобщенный тип должен
создавать экземпляры параметра типа, поскольку не
удается предположить формат специальных конструкторов.
Обратите внимание, что в типе со многими
ограничениями это ограничение должно указываться последним
where T : ИмяБазовогоКласса Параметр типа <Т> должен быть наследником класса,
указанного в ИмяБазовогоКласса
where T : ИмяИнтерфейса Параметр типа <Т> должен реализовать интерфейс,
указанный в ИмяИнтерфейса. Можно задавать несколько
интерфейсов, разделяя их запятыми
Если только не требуется строить какие-то исключительно безопасные к типам
специальные коллекции, возможно, никогда не придется использовать ключевое слово
where в проектах С#. Так или иначе, но в следующих нескольких примерах (частичного)
кода демонстрируется работа с ключевым словом where.
Примеры использования ключевого слова where
Будем исходить из того, что создан специальный обобщенный класс, и необходимо
гарантировать наличие в параметре типа конструктора по умолчанию. Это может быть
полезно, когда специальный обобщенный класс должен создавать экземпляры Т, потому
что конструктор по умолчанию — это единственный конструктор, потенциально общий
для всех типов. Также подобного рода ограничение Т позволит производить проверку во
время компиляции; если Т — ссылочный тип, то компилятор напомнит программисту о
необходимости переопределения конструктора по умолчанию в объявлении класса (если
помните, конструкторы по умолчанию удаляются из классов, в которых определяются
собственные конструкторы).
// Класс MyGenericClass унаследован от object, примем содержащиеся
//в нем элементы должны иметь конструктор по умолчанию,
public class MyGenericClass<T> where T : new()
{
Обратите внимание, что конструкция where указывает параметр типа, на который
накладывается ограничение, а за ним следует операция двоеточия. После этой
операции перечисляются все возможные ограничения (в данном случае — конструктор по
умолчанию). Ниже показан еще один пример:
// MyGenericClass унаследован от Object, причем содержащиеся
// в нем элементы должны относиться к классу, реализующему IDrawable.
//и поддерживать конструктор по умолчанию.
public class MyGenericClass<T> where T : class, IDrawable, new()
{...}
384 Насть III. Дополнительные конструкции программирования на С#
В данном случае к Т предъявляются три требования. Во-первых, это должен быть
ссылочный тип (не структура), что помечено лексемой class. Во-вторых, Т должен реа-
лизовывать интерфейс IDrawable. В-третьих, он также должен иметь конструктор по
умолчанию. Множество ограничений перечисляются в списке, разделенном запятыми;
однако имейте в виду, что ограничение new() всегда должно идти последним! По этой
причине следующий код не скомпилируется:
// Ошибка! Ограничение new() должно быть последним в списке!
public class MyGenericClass<T> where T : new(), class, IDrawable
{
}
В случае создания обобщенного класса коллекции с несколькими параметрами типа,
можно указывать уникальный набор ограничений для каждого параметра с помощью
отдельной конструкции where:
// <К> должен расширять SomeBaseClass и иметь конструктор по умолчанию, в то время
// как <Т> должен быть структурой и реализовывать обобщенный интерфейс IComparable.
public class MyGenericClass<K, T> where К : SomeBaseClass, new()
where T : IComparable<T>
{
}
Необходимость построения полностью нового обобщенного класса коллекции
возникает редко; однако ключевое слово where также допускается применять и в обобщенных
методах. Например, если необходимо гарантировать, чтобы метод Swap<T>() работал
только со структурами, обновите код следующим образом:
// Этот метод обменяет местами любые структуры, но не классы.
static void Swap<T>(ref T a, ref T b) where T : struct
{
}
Обратите внимание, что если ограничить метод SwapO подобным образом,
обменивать местами объекты string (как это делалось в коде примера) уже не получится,
поскольку string является ссылочным типом.
Недостаток ограничений операций
В конце этой главы следует упомянуть об одном моменте относительно обобщенных
методов и ограничений. При создании обобщенных методов может оказаться
сюрпризом появление ошибок компиляции во время применения любых операций С# (+, -, *,
== и т.д.) к параметрам типа. Например, подумайте о пользе от класса, который может
выполнять Add(), SubstractO, MultiplyO HDevideO над обобщенными типами:
// Ошибка на этапе компиляции' Нельзя применять
// арифметические операции к параметрам типа!
public class Bas±cMath<T>
{
public T Add(T argl, T arg2)
{ return argl + arg2; }
public T Subtract (T argl, T arg2)
{ return argl - arg2; }
public T Multiply(T argl, T arg2)
{ return argl * arg2; }
public T Divide(T argl, T arg2)
{ return argl / arg2; }
}
Глава 10. Обобщения 385
К сожалению, приведенный выше класс BasicMath<T> не скомпилируется. Хотя это
может показаться серьезным недостатком, следует снова вспомнить, что обобщения
являются общими. Естественно, числовые данные работают достаточно хорошо с
бинарными операциями С#. Однако если <Т> будет специальным классом или структурой, то
компилятор мог бы предположить, что этот класс поддерживает операции +, -, * и /.
В идеале язык С# должен был бы позволять ограничивать обобщенные типы
поддерживаемыми операциями, например:
// Код только для иллюстрации!
public class BasicMath<T> where T : operator +, operator -,
operator *, operator /
{
public T Add(T argl, T arg2)
{ return argl + arg2; }
public T Subtract (T argl, T arg2)
{ return argl - arg2; }
public T Multiply (T argl, T arg2)
{ return argl * arg2; }
public T Divide (T argl, T arg2)
{ return argl / arg2; }
}
К сожалению, ограничения операций в текущей версии С# не поддерживаются.
Однако можно (хотя это и потребует дополнительной работы) достичь желаемого
эффекта, определив интерфейс, поддерживающий эти операции (интерфейсы С# могут
определять операции) и затем указать ограничение интерфейса для обобщенного класса.
На этом первоначальный обзор построения специальных обобщенных типов завершен.
В следующей главе мы вновь обратимся к теме обобщений, когда будем рассматривать
тип делегата .NET.
Резюме
Настоящая глава начиналась с рассмотрения использования классических
контейнеров, включенных в пространство имен System.Collections. Хотя эти типы будут
и далее поддерживаться для обратной совместимости, новые приложения .NET
выиграют от применения вместо них новых обобщенных аналогов из пространства имен
System.Collections.Generic.
Как вы видели, обобщенный элемент позволяет специфицировать заполнители
(параметры типа), которые указываются во время создания (или вызова — в случае
обобщенных методов). По сути, обобщения предлагают решение проблем упаковки и
обеспечения безопасности типов, которые досаждали при разработке программного
обеспечения для .NET 1.1. Вдобавок обобщенные типы в значительной мере исключают
необходимость в построении специальных типов коллекций.
Хотя чаще всего обобщенные типы, представленные в библиотеках базовых классов
.NET, просто будут использоваться, можно также создавать и собственные обобщенные
типы (и обобщенные методы). При этом есть возможность задавать любое количество
ограничений (с помощью ключевого слова where), чтобы повышать уровень
безопасности в отношении типов и гарантировать выполнение операций над типами в "известном
количестве"; это обеспечивает предоставление определенных базовых возможностей.
ГЛАВА 11
Делегаты, события
и лямбда-выражения
Вплоть до этого момента большинство разработанных приложений добавляли
различные порции кода к методу Main(), тем или иным способом посылающие
запросы заданному объекту. Однако многие приложения требуют, чтобы объект мог
обращаться обратно к сущности, которая создала его — посредством механизма
обратного вызова. Хотя механизмы обратного вызова могут применяться в любом приложении,
они особенно важны в графических интерфейсах пользователя, где элементы
управления (такие как кнопки) нуждаются в вызове внешних методов при надлежащих условиях
(выполнен щелчок на кнопке, курсор мыши находится на поверхности кнопки и т.п.).
На платформе .NET тип делегата является предпочтительным средством
определения и реагирования на обратные вызовы в приложении. По сути, тип делегата .NET —
это безопасный к типам объект, который "указывает" на метод или список методов,
которые могут быть вызваны позднее. Однако в отличие от традиционного указателя
на функцию C++, делегаты .NET представляют собой классы, обладающие встроенной
поддержкой группового выполнения и асинхронного вызова методов.
В этой главе будет показано, как создавать и управлять типами делегатов, а также
использовать ключевое слово event в С#, которое облегчает работу с типами делегатов.
По пути вы также изучите несколько языковых средств С#, ориентированных на
делегаты и события, включая анонимные методы и групповые преобразования методов.
Завершается глава исследованием лямбда-выраженияй (lambda expressions).
Используя лямбда-операцию С# (=>), теперь можно указывать блок операторов кода (и
параметры для передачи этим операторам) везде, где требуется строго типизированный делегат.
Как будет показано, лямбда-выражение — это немногим более чем маскировка
анонимного метода, и представляет собой упрощенный подход к работе с делегатами.
Понятие типа делегата .NET
Прежде чем приступить к формальному определению делегатов .NET, давайте
оглянемся немного назад. В интерфейсе Windows API часто использовались указатели на
функции в стиле С для создания сущностей, именуемых функциями обратного вызова
(callback functions) или просто обратными вызовами (callbacks). С помощью обратных
вызовов программисты могли конфигурировать одну функцию таким образом, чтобы
она осуществляла обратный вызов другой функции в приложении. Применяя такой
подход, разработчики Windows смогли обрабатывать щелчки на кнопках, перемещения
курсора мыши, выбор пунктов меню и общие двусторонние коммуникации между
программными сущностями в памяти.
Глава 11. Делегаты, события и лямбда-выражения 387
Проблема со стандартными функциями обратного вызова в стиле С заключается в
том, что они представляют собой лишь немногим более чем простой адрес в памяти.
В идеале обратные вызовы могли бы конфигурироваться для включения
дополнительной безопасной к типам информации, такой как количество и типы параметров и
возвращаемого значения (если оно есть) метода, на который они указывают. К сожалению,
это невозможно с традиционными функциями обратного вызова и это, как следовало
ожидать, является постоянным источником ошибок, аварийных завершений и прочих
неприятностей во время выполнения. Тем не менее, обратные вызовы — удобная вещь.
В .NET Framework обратные вызовы по-прежнему возможны, и их
функциональность обеспечивается в гораздо более безопасной и объектно-ориентированной манере
с использованием делегатов. По сути, делегат — это безопасный в отношении типов
объект, указывающий на другой метод (или, возможно, список методов) приложения,
который может быть вызван позднее. В частности, объект делегата поддерживает три
важных фрагмента информации:
• адрес метода, на котором он вызывается;
• аргументы (если есть) этого метода;
• возвращаемое значение (если есть) этого метода.
На заметку! Делегаты .NET могут указывать как на статические, так и на методы экземпляра.
Как только делегат создан и снабжен необходимой информацией, он может
динамически вызывать методы, на которые указывает, во время выполнения. Каждый делегат
в .NET Framework (включая специальные делегаты) автоматически снабжается
способностью вызывать свои методы синхронно или асинхронно. Этот факт значительно
упрощает задачи программирования, поскольку позволяет вызывать метод во вторичном
потоке выполнения без ручного создания и управления объектом Thread.
На заметку! Асинхронное поведение типов делегатов будет рассматриваться во время
исследования пространства имен System.Threading в главе 19.
Определение типа делегата в С#
Для определения делегата в С# используется ключевое слово delegate. Имя
делегата может быть любым. Однако сигнатура определяемого делегата должна
соответствовать сигнатуре метода, на который он будет указывать. Например, предположим, что
планируется построить тип делегата по имени BinaryOp, который может указывать на
любой метод, возвращающий целое число и принимающий два целых числа в качестве
входных параметров:
// Этот делегат может указывать на любой метод, который
// принимает два целых и возвращает целое значение.
public delegate int BinaryOp(int x, int y);
Когда компилятор С# обрабатывает тип делегата, он автоматически генерирует
запечатанный (sealed) класс, унаследованный от System.MulticastDelegate. Этот класс
(в сочетании с его базовым классом System.Delegate) предоставляет необходимую
инфраструктуру для делегата, чтобы хранить список методов, подлежащих вызову в
более позднее время. Например, если просмотреть делегат BinaryOp с помощью утилиты
ildasm.exe, обнаружится класс, показанный на рис. 11.1.
388 Часть III. Дополнительные конструкции программирования на С#
File View Help
v H:\My Books\C# Book\C# and the .NET Platform 5th ed\First Draft\Chapter_l l\Code\SimpleDelegate\obj\x86\Debug\Simpl<
j ► MANIFEST
й Щ SimpleDelegate
► .class public auto ansi sealed
► extends [mscorlib]5ystem.MulticastDelegate
I ■ .ctor: void(object, native int)
■ Beginlnvoke : class [mscorlib]5ystem.IAsyncResult(int32,int32,class [mscorlibjSystem.AsyncCallbackjobject)
■ Endlnvoke : int32(class[mscorlib]System.IAsyncResult)
j ■ Invoke : int32(int32,int32)
ffi-flE SimpleDelegate.Program
.assembly SimpleDelegate
Рис. 11.1. Ключевое слово delegate представляет запечатанный класс,
унаследованный от System.MulticastDelegate
Как видите, сгенерированный компилятором класс BinaryOp определяет три
общедоступных метода. Invoke () — возможно, главный из них, поскольку он используется
для синхронного вызова каждого из методов, поддерживаемых объектом делегата; это
означает, что вызывающий код должен ожидать завершения вызова, прежде чем
продолжить свою работу. Может показаться странным, что синхронный метод Invoke () не
должен вызываться явно в коде С#. Как вскоре будет показано, Invoke () вызывается
"за кулисами", когда применяется соответствующий синтаксис С#.
Методы Beginlnvoke() и Endlnvoke() предлагают возможность вызова текущего
метода асинхронным образом, в отдельном потоке выполнения. Имеющим опыт в
многопоточной разработке должно быть известно, что одной из основных причин,
вынуждающих разработчиков создавать вторичные потоки выполнения, является
необходимость вызова методов, которые требуют определенного времени для завершения. Хотя в
библиотеках базовых классов .NET предусмотрено целое пространство имен,
посвященное многопоточному программированию (System.Threading), делегаты предлагают эту
функциональность в готовом виде.
Каким же образом компилятор знает, как следует определить методы Invoke(),
Beginlnvoke () и Endlnvoke()? Чтобы разобраться в процессе, ниже приведен код
сгенерированного компилятором типа класса BinaryOp (полужирным курсивом выделены
элементы, указанные определенным типом делегата):
sealed class BinaryOp : System.MulticastDelegate
{
public int Invoke(int x, int y) ;
public IAsyncResult Beginlnvoke(int x, int y,
AsyncCallback cb, object state);
public int Endlnvoke(IAsyncResult result);
Первым делом, обратите внимание, что параметры и возвращаемый тип для метода
Invoke () в точности соответствуют определению делегата BinaryOp. Начальные
параметры для членов Beginlnvoke () (в данном случае — два целых) также основаны на
делегате BinaryOp; однако Beginlnvoke () всегда будет предоставлять два финальных
параметра (типа AsyncCallback и object], используемых для облегчения
асинхронного вызова методов. И, наконец, возвращаемый тип Endlnvoke () идентичен исходному
объявлению делегата и всегда принимает единственный параметр — объект,
реализующий интерфейсIAsyncResult.
Глава 11. Делегаты, события и лямбда-выражения 389
Давайте рассмотрим другой пример. Предположим, что определен тип делегата,
который может указывать на любой метод, возвращающий string и принимающий три
входных параметра System.Boolean:
public delegate string MyDelegate(bool a, bool b, bool c) ;
На этот раз сгенерированный компилятором класс будет выглядеть так:
sealed class MyDelegate : System.MulticastDelegate
{
public string Invoke (bool a, bool b, bool c) ;
public IAsyncResult Beginlnvoke(bool a, bool b, bool c,
AsyncCallback cb, object state);
public string Endlnvoke(IAsyncResult result);
}
Делегаты также могут "указывать" на методы, содержащие любое количество
параметров out и ref (как и массивов параметров, помеченных ключевым словом params).
Например, предположим, что имеется следующий тип делегата:
public delegate string MyOtherDelegate (out bool a, ref bool b, int c) ;
Сигнатуры методов Invoke () и Beginlnvoke () выглядят так, как и следовало
ожидать; однако взгляните на метод Endlnvoke (), который теперь включает набор
аргументов out/ref, определенных типом делегата:
sealed class MyOtherDelegate : System.MulticastDelegate
{
public string Invoke (out bool a, ref bool b, int c) ;
public IAsyncResult Beginlnvoke (out bool a, ref bool b, int c,
AsyncCallback cb, object state);
public string Endlnvoke (out bool a, ref bool b, IAsyncResult result);
}
Чтобы подытожить: определение типа делегата С# порождает запечатанный класс с
тремя сгенерированными компилятором методами, типы параметров и возвращаемых
значений которых основаны на объявлении делегата. Базовый шаблон может быть
описан с помощью следующего псевдокода:
// Это только псевдокод!
public sealed class ИмяДелегата : System.MulticastDelegate
{
public возвращаемоеЗначениеДелегата Invoke (всеВходныеВ.е£и0и1ПараметрыДелегата) ;
public IAsyncResult Beginlnvoke (всеВходныеВ.е£иОиЬПараметрыДелегата,
AsyncCallback cb, object state);
public возвращаемоеЗначениеДелегата Endlnvoke (всеВходныеЯе£и0иИ1араметрыДелегата,
IAsyncResult result);
}
Базовые классы System.MulticastDelegate
и System.Delegate
При построении типа, использующего ключевое слово delegate, неявно
объявляется тип класса, унаследованного от System.MulticastDelegate. Этот класс
обеспечивает своих наследников доступом к списку, который содержит адреса методов,
поддерживаемых типом делегата, а также несколько дополнительных методов (и несколько
перегруженных операций), чтобы взаимодействовать со списком вызовов.
Ниже показаны некоторые избранные методы System.MulticastDelegate:
390 Часть III. Дополнительные конструкции программирования на С#
public abstract class MulticastDelegate : Delegate
{
// Возвращает список методов, на которые "указывает" делегат.
public sealed override Delegate[] GetlnvocationList ();
// Перегруженные операции.
public static bool operator ==(MulticastDelegate dl, MulticastDelegate d2) ;
public static bool operator ! = (MulticastDelegate dl, MulticastDelegate d2) ;
// Используются внутренне для управления списком методов, поддерживаемых делегатом.
private IntPtr _invocationCount;
private object _invocationList;
}
Класс System.MulticastDelegate получает дополнительную функциональность от
своего родительского класса System.Delegate. Ниже показан фрагмент определения
класса:
public abstract class Delegate : ICloneable, ISerializable
{
// Методы для взаимодействия со списком функций.
public static Delegate Combine(params Delegate[] delegates);
public static Delegate Combine(Delegate a, Delegate b) ;
public static Delegate Remove(Delegate source, Delegate value);
public static Delegate RemoveAll(Delegate source, Delegate value);
// Перегруженные операции.
public static bool operator ==(Delegate dl, Delegate d2) ;
public static bool operator !=( Delegate dl, Delegate d2);
// Свойства, показывающие цель делегата.
public Methodlnfo Method { get; }
public object Target { get; }
}
Запомните, что вы никогда не сможете напрямую наследовать от этих базовых
классов в коде (при попытке сделать это выдается ошибка компиляции). Тем не менее, при
использовании ключевого слова delegate неявно создается класс, который
"является" MulticastDelegate. В табл. 11.1 описаны основные члены, общие для всех типов
делегатов.
Таблица 11.1. Основные члены System.MultcastDelegate/System.Delegate
Член Назначение
Method Это свойство возвращает объект System.Reflection.Method,
который представляет детали статического метода,
поддерживаемого делегатом
Target Если метод, подлежащий вызову, определен на уровне объекта (т.е.
не является статическим), то Target возвращает объект,
представляющий метод, поддерживаемый делегатом. Если возвращенное
Target значение равно null, значит, подлежащий вызову метод
является статическим
Combine () Этот статический метод добавляет метод в список, поддерживаемый
делегатом. В С# этот метод вызывается за счет использования
перегруженной операции += в качестве сокращенной нотации
GetlnvokationListO Этот метод возвращает массив типов System.Delegate, каждый из
которых представляет определенный метод, доступный для вызова
Remove () Эти статические методы удаляют метод (или все методы) из списка
RemoveAl 1 () вызовов делегата. В С# метод Remove () может быть вызван неявно,
посредством перегруженной операции -=
Глава 11. Делегаты, события и лямбда-выражения 391
Простейший пример делегата
При первоначальном знакомстве делегаты могут показаться очень
сложными. Рассмотрим- для начала очень простое консольное приложение под названием
SimpleDelegate, в котором используется ранее показанный тип делегата BinaryOp.
Ниже приведен полный код.
namespace SimpleDelegate
{
// Этот делегат может указывать на любой метод,
// принимающий два целых и возвращающий целое.
public delegate int BinaryOp(int x, int y) ;
// Этот класс содержит методы, на которые будет указывать BinaryOp.
public class SimpleMath
{
public static int Add(int x, int y)
{ return x + y; }
public static int Subtract (int x, int y)
{ return x - y; }
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine ("***** Simple Delegate Example *****\n");
// Создать объект делегата BinaryOp, "указывающий" на SimpleMath.Add().
BinaryOp b = new BinaryOp(SimpleMath.Add);
// Вызвать метод AddQ непрямо с использованием объекта делегата.
Console.WriteLine(0 + 10 is {0}", bA0, 10));
Console.ReadLine();
}
}
}
Обратите внимание на формат объявления типа делегата BinaryOp: он определяет,
что объекты делегата BinaryOp могут указывать на любой метод, принимающий два
целых и возвращающий целое (действительное имя метода, на который он указывает,
не существенно). Здесь создан класс по имени SimpleMath, определяющий два
статических метода, которые соответствуют шаблону, определенному делегатом BinaryOp.
Когда нужно вставить целевой метод в заданный объект делегата, просто передайте
имя этого метода конструктору делегата:
// Создать делегат BinaryOp, который "указывает" на SimpleMath.Add () .
BinaryOp b = new BinaryOp(SimpleMath.Add);
С этого момента указанный метод можно вызывать с использованием синтаксиса,
который выглядит как прямой вызов функции:
// Invoke() действительно вызывается здесь!
Console.WriteLine(0 + 10 is {0}", bA0, 10));
"За кулисами" исполняющая система на самом деле вызывает сгенерированный
компилятором метод Invoke () на производном классе MulticastDelegate. В этом можно
убедиться, открыв сборку в утилите ildasm.exe и просмотрев код CIL в методе Main():
.method private hidebysig static void Main(string [ ] args) cil managed
{
callvirt instance int32 SimpleDelegate.BinaryOp::Invoke(int32, int32)
}
392 Часть III. Дополнительные конструкции программирования на С#
Хотя С# и не требует явного вызова Invoke () в коде, это вполне можно сделать. То
есть следующий оператор кода является допустимым:
Console.WriteLine(0 + 10 is {0}", b.InvokeA0, 10));
Вспомните, что делегаты .NET безопасны в отношении типов. Поэтому, попытка
передать делегату метод, не соответствующий шаблону, приводит к ошибке времени
компиляции. Чтобы проиллюстрировать это, предположим, что в классе SimpleMath
теперь определен дополнительный метод по имени SquareNumber(), принимающий
единственный целочисленный аргумент:
public class SimpleMath
{
public static int SquareNumber (int a)
{ return a * a; }
}
Учитывая, что делегат В i па г у Op может указывать только на методы,
принимающие два целых и возвращающие целое, следующий код неверен и компилироваться не
будет:
// Ошибка компиляции! Метод не соответствует шаблону делегата1
BinaryOp Ь2 = new BinaryOp(SimpleMath.SquareNumber);
Исследование объекта делегата
Давайте усложним текущий пример, создав статический метод (по имени
DisplayDelegatelnfoO) в классе Program. Этот метод будет выводить на консоль
имена методов, поддерживаемых объектом делегата, а также имя класса, определяющего
метод. Для этого будет реализована итерация по массиву System.Delegate,
возвращенному GetInvocationList(), с обращением к свойствам Target и Method каждого
элемента.
static void DisplayDelegatelnfо (Delegate delObj)
{
// Вывести на консоль имена каждого члена списка вызовов делегата.
foreach (Delegate d in delObj.GetlnvocationList ())
{
Console.WriteLine("Method Name: {0}", d.Method); // имя метода
Console.WriteLine("Type Name: {0}", d.Target); // имя типа
}
}
Исходя из предположения, что в метод Main() добавлен вызов этого
вспомогательного метода, вывод приложения будет выглядеть следующим образом:
***** simple Delegate Example *****
Method Name: Int32 Add(Int32, Int32)
Type Name:
10 + 10 is 20
Обратите внимание, что имя типа (SimpleMath) в настоящий момент не
отображается при обращении к свойству Target. Причина в том, что делегат BinaryOp указывает
на статический метод, и потому просто нет объекта, на который нужно ссылаться!
Однако если сделать методы Add() и Substract() нестатическими (удалив в их
объявлениях ключевые слова static), можно будет создавать экземпляр типа SimpleMath и
указывать методы для вызова с использованием ссылки на объект:
Глава 11. Делегаты, события и лямбда-выражения 393
static void Main(string [ ] args)
{
Console.WriteLine("***** Simple Delegate Example *****\nM);
// Делегаты .NET могут указывать на методы экземпляра.
SimpleMath.m = new SimpleMath();
BinaryOp b = new BinaryOp(m.Add);
// Вывести сведения об объекте.
DisplayDelegatelnfо(b);
Console.WriteLine(0 + 10 is {0}", bA0, 10));
Console.ReadLine();
}
В этом случае вывод будет выглядеть, как показано ниже:
***** simple Delegate Example *****
Method Name: Int32 Add(Int32, Int32)
Type Name: SimpleDelegate.SimpleMath
10 + 10 is 20
Исходный код. Проект SimpleDelegate доступен в подкаталоге Chapter 11.
Отправка уведомлений о состоянии
объекта с использованием делегатов
Ясно, что предыдущий пример SimpleDelegate был предназначен только для
целей иллюстрации, потому что нет особого смысла создавать делегат только для того,
чтобы просуммировать два числа. Рассмотрим более реалистичный пример, в котором
делегаты используются для определения класса Саг, обладающего способностью
информировать внешние сущности о текущем состоянии двигателя. Для этого понадобится
предпринять перечисленные ниже действия.
• Определение нового типа делегата, который будет отправлять уведомления
вызывающему коду.
• Объявление переменной-члена этого делегата в классе Саг.
• Создание вспомогательной функции в С а г, которая позволяет вызывающему коду
указывать метод для обратного вызова.
• Реализация метода Accelerate () для обращения к списку вызовов делегата при
нужных условиях.
Для начала создадим проект консольного приложения по имени CarDelegate. Затем
определим новый класс Саг, который изначально выглядит следующим образом:
public class Car
{
// Данные состояния.
public int CurrentSpeed { get; set; }
public int MaxSpeed { get; set; }
public string PetName { get; set; }
// Исправен ли автомобиль?
private bool carlsDead;
// Конструкторы класса,
public Car() { MaxSpeed = 100; }
public Car (string name, int maxSp, int currSp)
I
CurrentSpeed = currSp;
394 Часть III. Дополнительные конструкции программирования на С#
MaxSpeed = maxSp;
PetName = name;
}
}
Ниже показаны обновления, связанные с реализацией трех первых пунктов:
public class Car
{
// 1. Определить тип делегата.
public delegate void CarEngineHandler(string msgForCaller);
112. Определить переменную-член типа этого делегата.
private CarEngineHandler listOfHandlers;
// 3. Добавить регистрационную функцию для вызывающего кода.
public void RegisterWithCarEngine(CarEngineHandler methodToCall)
{
listOfHandlers = methodToCall;
}
}
Обратите внимание, что в этом примере типы делегатов определяются
непосредственно в контексте класса Саг. Исследование библиотек базовых классов покажет, что
это довольно обычно — определять делегат внутри класса, который будет с ними
работать. Наш тип делегата — CarEngineHandler — может указывать на любой метод,
принимающий значение string в качестве параметра и имеющий void в качестве типа
возврата. ,
Кроме того, была объявлена приватная переменная-член делегата (по имени
listOfHandlers) и вспомогательная функция (по имени RegisterWithCarEngine()),
которая позволяет вызывающему коду добавлять метод к списку вызовов делегата.
На заметку! Строго говоря, можно было бы определить переменную-член делегата как public,
избежав необходимости в добавлении дополнительных методов регистрации. Однако за счет
определения этой переменной-члена делегата как private усиливается инкапсуляция и
обеспечивается более безопасное к типам решение. Опасности объявления переменных-членов делегатов как
public еще будут рассматриваться в этой главе, когда речь пойдет о ключевом слове event.
Теперь необходимо создать метод Accelerate(). Вспомните, что здесь стоит задача
позволить объекту Саг отправлять касающиеся двигателя сообщения любому
подписавшемуся слушателю. Ниже показаны необходимые изменения в коде.
public void Accelerate(int delta)
{
// Если этот автомобиль сломан, отправить сообщение об этом,
if (carlsDead)
{
if (listOfHandlers '= null)
listOfHandlers("Sorry, this car is dead...");
}
else
{
CurrentSpeed += delta;
// Автомобиль почти сломан?
if A0 == (MaxSpeed - CurrentSpeed) && listOfHandlers != null)
{
listOfHandlers("Careful buddy! Gonna blow!");
}
Глава 11. Делегаты, события и лямбда-выражения 395
if (CurrentSpeed >= MaxSpeed)
carlsDead = true;
else
Console.WriteLine("CurrentSpeed = {0}", CurrentSpeed);
}
}
Обратите внимание, что прежде чем вызывать методы, поддерживаемые переменной-
членом listOf Handlers, она проверяется на равенство null. Причина в том, что
размещать эти объекты вызовом вспомогательного метода RegisterWithCarEngine () —
задача вызывающего кода. Если вызывающий код не вызовет этот метод, а мы попытаемся
обратиться к списку вызовов делегата, то получим исключение NullRef erenceException
и нарушим работу исполняющей системы (что очевидно нехорошо). Теперь, имея всю
инфраструктуру делегатов, рассмотрим обновления класса Program:
class Program
{
static void Main(string[] args)
{
Console.WriteLine ("***** Delegates as event enablers *****\n");
// Сначала создадим объект Car.
Car cl = new Car ("SlugBug", 100, 10);
// Теперь сообщим ему, какой метод вызывать,
// когда он захочет отправить сообщение.
cl.RegisterWithCarEngine(new Car.CarEngineHandler (OnCarEngineEvent)) ;
// Ускорим (это инициирует события).
Console.WriteLine ("***** Speeding up *****");
for (int l = 0; l < 6; i + +)
cl.AccelerateB0);
Console.ReadLine() ;
}
// Это цель для входящих сообщений.
public static void OnCarEngineEvent (string msg)
{
Console.WriteLine("\n***** Message From Car Object *****");
Console.WriteLine("=> {0}", msg);
Console WriteLine("***********************************\n" ) *
}
}
Метод Main() начинается с создания нового объекта Car. Поскольку мы
заинтересованы в событиях, связанных с двигателем, следующий шаг заключается в вызове
специальной регистрационной функции RegisterWithCarEngine(). Вспомните, что
этот метод ожидает получения экземпляра вложенного делегата CarEngineHandler, и
как с любым делегатом, метод, на который он должен указывать, задается в параметре
конструктора.
Трюк в этом примере состоит в том, что интересующий метод находится в классе
Program! Обратите внимание, что метод OnCarEngineEvent () полностью соответствует
связанному делегату в том, что принимает string и возвращает void. Ниже показан
вывод этого примера:
***** Delegates as event enablers *****
***** Speeding up *****
CurrentSpeed = 30
CurrentSpeed = 50
CurrentSpeed = 70
396 Часть III. Дополнительные конструкции программирования на С#
***** Message From Car Object *****
=> Careful buddy! Gonna blow!
***********************************
CurrentSpeed = 90
***** Message From Car Object *****
=> Sorry, this car is dead.. .
***********************************
Включение группового вызова
Вспомните, что делегаты .NET обладают встроенной возможностью группового
вызова. Другими словами, объект делегата может поддерживать целый список методов
для вызова, а не просто единственный метод. Для добавления нескольких методов
к объекту делегата используется перегруженная операция +=, а не прямое
присваивание. Чтобы включить групповой вызов в типе Саг, можно модифицировать метод
RegisterWithCarEngineO следующим образом:
public class Car
{
// Добавление поддержки группового вызова.
// Обратите внимание на использование операции +=,
// а не операции присваивания (=) .
public void RegisterWithCarEngine(CarEngineHandler methodToCall)
{
listOfHandlers += methodToCall;
}
}
После этого простого изменения вызывающий код теперь может регистрировать
множественные цели для одного и того же обратного вызова. Здесь второй обработчик
выводит входное сообщение в верхнем регистре, просто для примера:
class Program
{
static void Main(string [ ] args)
{
Console.WriteLine ("***** Delegates as event enablers *****\n");
Car cl = new Car ("SlugBug", 100, 10);
// Регистрируем несколько обработчиков событий.
cl.RegisterWithCarEngine (new Car.CarEngineHandler(OnCarEngineEvent));
cl.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent2));
// Ускорим (это инициирует события).
Console.WriteLine ("***** Speeding up *****");
for (int i = 0; i < 6; i++)
cl.AccelerateB0);
Console.ReadLine() ;
}
// Теперь есть ДВА метода, которые будут вызваны Саг
// при отправке уведомлений.
public static void OnCarEngineEvent(string msg)
{
Console.WriteLine ("\n***** Message From Car Object *****");
Console.WriteLine("=> {0}", msg);
Console. WriteLine (••******************* ****************\n") .
}
Глава 11. Делегаты, события и лямбда-выражения 397
public static void OnCarEngineEvent2(string msg)
{
Console.WriteLine("=> {0}", msg.ToUpper ());
}
}
В терминах кода CIL операция += разрешает вызывать статический метод Delegate.
Combine () (фактически, его можно вызывать и напрямую, но операция += предлагает
более простую альтернативу). Ниже показан CIL-код метода RegisterWithCarEngine().
.method public hidebysig instance void RegisterWithCarEngine()
(class CarDelegate.Car/AboutToBlow clientMethod) cil managed
{
// Code size 25 @x19)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.O
IL_0002: dup
IL_0003: ldfld class CarDelegate.Car/CarEngineHandler CarDelegate.
Car:rlistOfHandlers
IL_0008: ldarg.l
IL0009:call class [mscorlib]System.Delegate [mscorlib]
System.Delegate::Combine(class [mscorlib]System.Delegate,
class [mscorlib]System.Delegate)
IL_000e: castclass CarDelegate.Car/CarEngineHandler
IL_0013: stfld class CarDelegate.Car/CarEngineHandler CarDelegate.
Car::listOfHandlers
IL_0018: ret
}// end of method Car::RegisterWithCarEngine
Удаление целей из списка вызовов делегата
В классе Delegate также определен метод Remove(), позволяющий вызывающему
коду динамически удалять отдельные члены из списка вызовов объекта делегата. Это
позволяет вызывающему коду легко "отписываться" от определенного уведомления во
время выполнения. Хотя в коде можно непосредственно вызывать Delegate.Remove(),
разработчики С# могут использовать также перегруженную операцию -= в качестве
сокращения.
Давайте добавим в класс Саг новый метод, который позволяет вызывающему коду
исключать метод из списка вызовов:
public class Car
public void UnRegisterWithCarEngine(CarEngineHandler methodToCall)
{
listOfHandlers -= methodToCall;
}
}
Опять-таки, синтаксис -- представляет собой просто сокращенную нотацию для
ручного вызова метода Delegate.Remove(), что иллюстрирует следующий код CLI для
метода UnRegisterWithCarEventO класса Саг:
.method public hidebysig instance void UnRegisterWithCarEvent (class
CarDelegate.Car/AboutToBlow clientMethod) cil managed
{
// Code size 25 @x19)
.maxstack 8
IL_0000: nop
398 Часть III. Дополнительные конструкции программирования на С#
IL_0001: ldarg.O
IL_0002: dap
IL_0003: ldfld class CarDelegate.Car/CarEngineHandler CarDelegate.Car:rlistOfHandlers
IL_0008: ldarg.l
IL_0009: call class [mscorlib]System.Delegate [mscorlib]
System. Delegate : .-Remove (class [mscorlib] System. Delegate,
class [mscorlib]System.Delegate)
IL_000e: castclass CarDelegate.Car/CarEngineHandler
IL_0013: stfld class CarDelegate.Car/CarEngineHandler CarDelegate.Car:rlistOfHandlers
IL_0018: ret
} // end of method Car::UnRegisterWithCarEngine
В текущей версии класса Car прекратить получение уведомлений от второго
обработчика можно за счет изменения метода Main() следующим образом:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Delegates as event enablers *****\n");
// Сначала создадим объект Car.
Car cl = new Car("SlugBug", 100, 10);
cl.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent));
// Сохраним объект делегата для последующей отмены регистрации.
Car.CarEngineHandler handler2 = new Car.CarEngineHandler(OnCarEngineEvent2);
cl.RegisterWithCarEngine(handler2);
// Ускорим (это инициирует события).
Console.WriteLine ("***** Speeding up *****");
for (int i = 0; l < 6; i + +)
cl.AccelerateB0);
// Отменим регистрацию второго обработчика,
cl.UnRegisterWithCarEngine(handler2) ;
// Сообщения в верхнем регистре больше не выводятся.
Console.WriteLine ("***** Speeding up *****");
for (int i = 0; l < 6; i + + )
cl.AccelerateB0);
Console.ReadLine();
}
Одно отличие Main() состоит в том, что на этот раз создается объект Саг.
CarEngineHandler, который сохраняется в локальной переменной, чтобы иметь
возможность позднее отменить подписку на получение уведомлений. Тогда при следующем
ускорении Саг уже больше не будет выводиться версия входящего сообщения в верхнем
регистре, поскольку эта цель исключена из списка вызовов делегата.
Исходный код. Проект CarDelegate доступен в подкаталоге Chapter 11.
Синтаксис групповых преобразований методов
В предыдущем примере CarDelegate явно создавались экземпляры объекта
делегата Car.CarEngineHandler, чтобы регистрировать и отменять регистрацию на
получение уведомлений:
static void Main(string[] args)
{
Console.WriteLine ("***** Delegates as event enablers *****\n");
Car cl = new Car("SlugBug", 100, 10);
cl.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent));
Глава 11. Делегаты, события и лямбда-выражения 399
Car.CarEngineHandler handler2 =
new Car.CarEngineHandler(OnCarEngineEvent2);
cl.RegisterWithCarEngine(handler2);
}
Если нужно вызывать любые унаследованные члены MulticastDelegate или
Delegate, то наиболее простым способом сделать это будет ручное создание
переменной делегата. Однако в большинстве случаев обращаться к внутренностям объекта
делегата не понадобится. Объект делегата будет нужен только для того, чтобы передать
имя метода как параметр конструктора.
Для простоты в С# предлагается сокращение, которое называется групповое
преобразование методов (method group conversion). Это средство позволяет указывать прямое
имя метода, а не объект делегата, когда вызываются методы, принимающие делегаты
в качестве аргументов.
На заметку! Как будет показано далее в этой главе, синтаксис группового преобразования
методов можно также использовать для упрощения регистрации событий С#.
Для целей иллюстрации создадим новое консольное приложение по имени
CarDelegateMethodGroupConversion и добавим в него файл, содержащий класс
Саг, который был определен в проекте CarDelegate. В показанном ниже коде класса
Program используется групповое преобразование методов для регистрации и отмены
регистрации подписки на уведомления.
class Program
{
static void Main (string [ ] args)
{
Console.WriteLine (»***** Method Group Conversion *****\n");
Car cl = new Car () ;
// Зарегистрировать простое имя метода.
cl.RegisterWithCarEngine(CallMeHere);
Console.WriteLine("***** Speeding up *****");
for (int l = 0; l < 6; i++)
cl.AccelerateB0);
// Отменить регистрацию простого имени метода.
cl.UnRegisterWithCarEngine(CallMeHere);
// Уведомления больше не поступают!
for (int 1 = 0; i < 6; i + +)
cl.AccelerateB0);
Console.ReadLine() ;
}
static void CallMeHere(string msg)
{
Console.WriteLine("=> Message from Car: {0}", msg);
}
}
Обратите внимание, что мы не создаем непосредственно объект делегата, а просто
указываем метод, который соответствует ожидаемой сигнатуре делегата (в данном
случае — метод, возвращающий void и принимающий единственную строку). Имейте в
виду, что компилятор С# по-прежнему обеспечивает безопасность типов. Поэтому, если
метод CallMeHere() не принимает string и не возвращает void, компилятор сообщит
об ошибке.
400 Часть III. Дополнительные конструкции программирования на С#
Исходный код. Проект CarDelegateMethodGroupConversion доступен в подкаталоге
Chapter 11.
Понятие ковариантности делегатов
Как вы могли заметить, каждый из делегатов, созданных до сих пор, указывал на
методы, возвращающие простые числовые типы данных (или void). Однако предположим,
что имеется новое консольное приложение по имени DelegateCovariance,
определяющее тип делегата, который может указывать на методы, возвращающие объект
пользовательского класса (не забудьте включить в новый проект определение класса Саг):
// Определение типа делегата, указывающего на методы, которые возвращают объект Саг.
public delegate Car ObtainCarDelegate ();
Разумеется, цель для делегата можно определить следующим образом:
namespace DelegateCovariance
{
class Program
{
// Определение типа делегата, указывающего на методы,
// которые возвращают объект Саг.
public delegate Car ObtainCarDelegate();
static void Main(string[] args)
{
Console.WriteLine ("***** Delegate Covariance *****\n");
ObtainCarDelegate targetA = new ObtainCarDelegate (GetBasicCar);
Car с = targetA() ;
Console.WriteLine("Obtained a {0}", c);
Console.ReadLine() ;
}
public static Car GetBasicCar()
{
return new Car("Zippy", 100, 55);
}
}
}
А теперь пусть необходимо унаследовать новый класс от типа Саг по имени
SportsCar и создать тип делегата, который может указывать на методы,
возвращающие этот тип класса. До появления .NET 2.0 для этого пришлось бы определять
полностью новый делегат, учитывая то, что делегаты настолько безопасны к типам, что не
подчиняются базовым законам наследования:
// Определение нового типа делегата, указывающего на методы,
// которые возвращают объект SportsCar.
public delegate SportsCar ObtainSportsCarDelegate();
Поскольку теперь есть два типа делегатов, следует создать экземпляр каждого из
них, чтобы получать типы Саг и SportsCar:
class Program
{
public delegate Car ObtainCarDelegate();
public delegate SportsCar ObtainSportsCarDelegate();
public static Car GetBasicCar()
{ return new Car(); }
public static SportsCar GetSportsCar()
{ return new SportsCar() ; }
Глава 11. Делегаты, события и лямбда-выражения 401
static void Main(string [ ] args)
{
Console.WriteLine ("***** Delegate Covariance *****\n");
ObtainCarDelegate targetA = new ObtainCarDelegate(GetBasicCar);
Car с = targetA () ;
Console.WriteLine("Obtained a {0}", c) ;
ObtainSportsCarDelegate targetB =
new ObtainSportsCarDelegate(GetSportsCar);
SportsCar sc = targetB ();
Console.WriteLine("Obtained a {0}", sc);
Console.ReadLine() ;
}
}
Учитывая законы классического наследования, было бы идеально построить один
тип делегата, который мог бы указывать на методы, возвращающие объекты Саг и
SportsCar (в конце концов, SportsCar "является" Саг). Ковариантность (которую
также называют свободными делегатами (relaxed delegates)) делает это вполне возможным.
По сути, ковариантность позволяет построить единственный делегат, который может
указывать на методы, возвращающие связанные классическим наследованием типы
классов.
На заметку! С другой стороны, контравариантность позволяет создать единственный делегат,
который может указывать на многочисленные методы, принимающие объекты, связанные
классическим наследованием. Дополнительные сведения ищите в документации .NET Framework 4.0 SDK.
class Program
{
// Определение единственного типа делегата, указывающего на методы,
// которые возвращают объект Oar или SportsCar.
public delegate Car ObtainVehicleDelegate();
public static Car GetBasicCar()
{ return new Car(); }
public static SportsCar GetSportsCar ()
{ return new SportsCar (); }
static void Main(string[] args)
{
Console.WriteLine ("***** Delegate Covariance *****\n");
ObtainVehicleDelegate targetA = new ObtainVehicleDelegate(GetBasicCar);
Car с = targetA () ;
Console.WriteLine("Obtained a {0}", c) ;
// Ковариантность позволяет такое присваивание цели.
ObtainVehicleDelegate targetB = new ObtainVehicleDelegate(GetSportsCar) ;
SportsCar sc = (SportsCar)targetB ();
Console.WriteLine("Obtained a {0}", sc) ;
Console.ReadLine();
}
}
Обратите внимание, что тип делегата ObtainVehicleDelegate определен так,
чтобы указывать на методы, возвращающие только строго типизированные объекты типа
Саг. Однако с помощью ковариантности можно указывать на методы, которые также
возвращают производные типы. Для получения доступа к членам производного типа
просто выполните явное приведение.
Исходный код. Проект DelegateCovariance доступен в подкаталоге Chapter 11.
402 Часть III. Дополнительные конструкции программирования на С#
Понятие обобщенных делегатов
Вспомните из предыдущей главы, что язык С# позволяет определять обобщенные
типы делегатов. Например, предположим, что необходимо определить делегат, который
может вызывать любой метод, возвращающий void и принимающий единственный
параметр. Если передаваемый аргумент может изменяться, это моделируется через
параметр типа. Для иллюстрации рассмотрим следующий код нового консольного
приложения по имени GenericDelegate:
namespace GenericDelegate
{
// Этот обобщенный делегат может вызывать любой метод, который
// возвращает void и принимает единственный параметр типа.
public delegate void MyGenericDelegate<T> (T arg);
class Program
{
static void Main(string [ ] args)
{
Console.WriteLine ("***** Generic Delegates *****\n");
// Регистрация целей.
MyGenericDelegate<string> strTarget =
new MyGenericDelegate<string>(StringTarget);
strTarget("Some string data");
MyGenericDelegate<int> intTarget =
new MyGenericDelegate<int>(IntTarget);
intTarget (9);
Console.ReadLine();
static void StringTarget(string arg)
Console.WriteLine("arg in uppercase is: {0}", arg.ToUpper ());
static void IntTarget (int arg)
Console.WriteLine("++arg is: {0}", ++arg);
}
}
Обратите внимание, что в MyGenericDelegate<T> определен единственный
параметр, представляющий аргумент для передачи цели делегата. При создании экземпляра
этого типа необходимо указать значение параметра типа вместе с именем метода,
который может вызывать делегат. Таким образом, если указать тип string, целевому методу
будет отправлено строковое значение:
// Создать экземпляр MyGenericDelegate<T>
// со string в качестве параметра типа.
MyGenericDelegate<string> strTarget =
new MyGenericDelegate<string> (StringTarget);
strTarget("Some string data");
Имея формат объекта strTarget, метод StringTarget теперь должен принимать в
качестве параметра единственную строку:
static void StringTarget(string arg)
{
Console.WriteLine("arg in uppercase is: {0}", arg.ToUpper ());
}
Глава 11. Делегаты, события и лямбда-выражения 403
Эмуляция обобщенных делегатов без обобщений
Обобщенные делегаты предоставляют более гибкий способ спецификации
метода, подлежащего вызову в безопасной к типам манере. До появления обобщений
(в .NET 2.0) тога же конечного результата можно было достичь с использованием
параметра System.Object:
public delegate void MyDelegate(object arg);
Хотя это позволяет посылать любой аргумент цели делегата, это не обеспечивает
безопасность типов и не избавляет от бремени упаковки/распаковки. Для примера
предположим, что созданы два экземпляра MyDelegate, и оба они указывают на один и
тот же метод MyTarget. Обратите внимание на упаковку/распаковку и отсутствие
безопасности типов.
class Program
{
static void Main(string[] args)
{
// Регистрация с "традиционным" синтаксисом делегатов.
MyDelegate d = new MyDelegate(MyTarget);
d("More string data");
// Синтаксис групповых преобразований методов.
MyDelegate d2 = MyTarget;
d2 (9); // Дополнительные издержки на упаковку.
Console.ReadLine ();
}
// Из-за отсутствия безопасности типов необходимо
// определить лежащий в основе тип перед приведением.
static void MyTarget(object arg)
{
if(arg is int)
{
int i = (int)arg; // Дополнительные издержки на распаковку.
Console.WriteLine("++arg is: {0}", ++i) ;
}
if(arg is string)
{
string s = (string)arg;
Console.WriteLine("arg in uppercase is: {0}", s.ToUpper());
}
}
}
Когда целевому методу посылается тип значения, это значение упаковывается и
снова распаковывается при получении методом. Также, учитывая, что входящий параметр
может быть чем угодно, перед приведением должна производиться динамическая
проверка лежащего в основе типа. С помощью обобщенных делегатов необходимую
гибкость можно получить без упомянутых проблем.
Исходный код. Проект GenericDelegate доступен в подкаталоге Chapter 11.
На этом первоначальный экскурс в тип делегата .NET завершен. Мы еще вернемся
к некоторым дополнительным деталям работы с делегатами в конце этой главы и еще
раз — в главе 19, когда будем рассматриваться многопоточность. А теперь переходим к
связанной теме — ключевому слову event в С#.
404 Часть III. Дополнительные конструкции программирования на С#
Понятие событий С#
Делегаты — весьма интересные конструкции в том смысле, что позволяют объектам,
находящимся в памяти, участвовать в двустороннем общении. Однако работа с
делегатами напрямую может порождать довольно однообразный код (определение делегата,
определение необходимых переменных-членов и создание специальных методов
регистрации и отмены регистрации для предохранения инкапсуляции).
Более того, если делегаты используются в качестве механизма обратного вызова в
приложениях напрямую, существует еще одна проблема: если не определить делегат —
переменную-член класса как приватную, то вызывающий код получит прямой доступ
к объектам делегатов. В этом случае вызывающий код может присвоить переменной
новый объект-делегат (фактически удалив текущий список функций, подлежащих
вызову), и что еще хуже — вызывающий код сможет напрямую обращаться к списку вызовов
делегата. Чтобы проиллюстрировать проблему, рассмотрим следующую переделанную
(и упрощенную) версию предыдущего примера CarDelegate:
public class Car
{
public delegate void CarEngineHandler(string msgForCaller);
// Теперь этот член public!
public CarEngineHandler listOfHandlers;
// Просто вызвать уведомление Exploded.
public void Accelerate (int delta)
{
if (listOfHandlers '= null)
listOfHandlers("Sorry, this car is dead...");
}
}
Обратите внимание, что теперь нет делегата — приватной переменной-члена,
инкапсулированной с помощью специальных методов регистрации. Поскольку эти члены
сделаны общедоступными, вызывающий код может непосредственно обращаться к
члену listOfHandlers и переназначить этот тип на новые объекты CarEngineHandler,
после чего вызывать делегат, когда вздумается:
class Program
{
static void Main(string [ ] args)
{
Console.WriteLine ("***** Agh' No Encapsulation' *****\n");
// Создать Car.
Car myCar = new Car();
// Есть прямой доступ к делегату!
myCar.listOfHandlers = new Car.CarEngineHandler(CallWhenExploded);
myCar.Accelerate A0);
// Назначаем ему совершенно новый объект...
//В лучшем случае получается путаница.
myCar.listOfHandlers = new Car.CarEngineHandler(CallHereToo);
myCar.Accelerate A0);
// Вызывающий код может также напрямую вызвать делегат!
myCar.listOfHandlers.Invoke ("nee, nee, nee...");
Console.ReadLine();
}
static void CallWhenExploded(string msg)
{ Console.WriteLine(msg); }
static void CallHereToo(string msg)
{ Console.WriteLine(msg); }
}
Глава 11. Делегаты, события и лямбда-выражения 405
Общедоступные члены-делегаты нарушают инкапсуляцию, что не только затруднит
сопровождение кода (и отладку), но также сделает приложение уязвимым в смысле
безопасности. Ниже показан вывод текущего примера:
***** Agh I No Encapsulation1 *****
Sorry, this car is dead. . .
Sorry, this car is dead. . .
nee, hee, hee . . .
Очевидно, что вряд ли имеет смысл предоставлять другим приложениям право
изменять то, на что указывает делегат, или вызывать его члены напрямую.
Исходный код. Проект PublicDelegateProblem доступен в подкаталоге Chapter 11.
Ключевое слово event
В качестве сокращения, избавляющего от необходимости строить специальные
методы для добавления и удаления методов в списке вызовов делегата, в С#
предусмотрено ключевое слово event. Обработка компилятором ключевого слова event приводит
к автоматическому получению методов регистрации и отмены регистрации наряду со
всеми необходимыми переменными-членами для типов делегатов. Эта переменная-
член делегата всегда объявляется приватной, и потому не доступна напрямую объекту,
инициировавшему событие. Точнее говоря, ключевое слово event — это не более чем
синтаксическое украшение, позволяющее экономить на наборе кода.
Определение события представляет собой двухэтапный процесс. Во-первых, нужно
определить делегат, который будет хранить список методов, подлежащих вызову при
возникновении события. Во-вторых, необходимо объявить событие (используя
ключевое слово event) в терминах связанного типа делегата.
Чтобы проиллюстрировать ключевое слово event, создадим новое консольное
приложение по имени С a rE vents. В классе Саг будут определены два события под
названиями AboutToBlow и Exploded. Эти события ассоциированы с единственным типом
делегата по имени CarEngineHandler. Ниже показаны начальные изменения в классе
Саг:
public class Car
{
// Этот делегат работает в сочетании с событиями Саг.
public delegate void CarEngineHandler(string msg) ;
// Car может посылать следующие события.
public event CarEngineHandler Exploded;
public event CarEngineHandler AboutToBlow;
}
Отправка события вызывающему коду состоит просто в указании имени события
вместе со всеми необходимыми параметрами, определенными в ассоциированном
делегате. Чтобы удостовериться, что вызывающий код действительно зарегистрировал
событие, его следует проверить на равенство null перед вызовом набора методов
делегата. Ниже приведена новая версия метода Accelerate () класса Саг:
public void Accelerate(int delta)
{
// Если автомобиль сломан, инициировать событие Exploded.
if (carlsDead)
{
if (Exploded '= null)
406 Часть III. Дополнительные конструкции программирования на С#
Exploded("Sorry, this car is dead...");
}
else
{
CurrentSpeed += delta;
// Почти сломан?
if A0 == MaxSpeed - CurrentSpeed
&& AboutToBlow != null)
{
AboutToBlow("Careful buddy! Gonna blow!");
}
// Все в порядке!
if (CurrentSpeed >= maxSpeed)
carlsDead = true;
else
Console.WriteLine("CurrentSpeed = {0}", CurrentSpeed);
}
}
Таким образом, объект Car сконфигурирован для отправки двух специальных
событий, без необходимости определения специальных функций регистрации или
объявления переменных-членов. Чуть ниже будет продемонстрировано использование
этого нового объекта, но сначала давайте рассмотрим архитектуру событий немного
подробнее.
"За кулисами" событий
Событие С# в действительности развертывается в два скрытых метода, один из
которых имеет префикс add_, а другой — remove. За этим префиксом следует имя
события С#. Например, событие Exploded превращается в два скрытых метода CIL
с именами add_Exploded () и remove_Exploded (). Если заглянуть в CIL-код метода
add_AboutToBlow (), можно обнаружить там вызов метода Delegate.Combine(). Ниже
показан частичный код CIL:
.method public hidebysig specialname instance void
add_AboutToBlow(class CarEvents.Car/CarEngineHandler 'value1)
cil managed synchronized
{
call class [mscorlib]System.Delegate
[mscorlib]System.Delegate::Combine(
class [mscorlib]System.Delegate, class [mscorlib]System.Delegate)
}
Как и следовало ожидать, remove_AboutToBlow() неявно вызывает Delegate.
Remove ():
.method public hidebysig specialname instance void
remove_AboutToBlow(class CarEvents.Car/CarEngineHandler 'value1)
cil managed synchronized
{
call class [mscorlib]System.Delegate
[mscorlib] System. Delegate: : Remove (
class [mscorlib]System.Delegate, class [mscorlib]System.Delegate)
Глава 11. Делегаты, события и лямбда-выражения 407
И, наконец, в CIL-коде, представляющем само событие, используются
директивы .addon и .removeon для отображения имен корректных методов addXXX () и
remove_XXX () для вызова:
.event CarEvents.Car/EngineHandler AboutToBlow
{
.addon void CarEvents.Car::add_AboutToBlow
(class CarEvents.Car/CarEngineHandler)
.removeon void CarEvents.Car::remove_AboutToBlow
(class CarEvents.Car/CarEngineHandler)
}
Теперь, когда вы разобрались, как строить класс, способный отправлять события С#
(и уже знаете, что события — это лишь способ сэкономить время на наборе кода),
следующий большой вопрос связан с организацией прослушивания входящих событий на
стороне вызывающего кода.
Прослушивание входящих событий
События С# также упрощают акт регистрации обработчиков событий на стороне
вызывающего кода. Вместо того чтобы специфицировать специальные
вспомогательные методы, вызывающий код просто использует операции += и -= непосредственно
(что приводит к внутренним вызовам методов addXXX () или removeXXX ()). Для
регистрации события руководствуйтесь следующим шаблоном:
// ИмяОбъекта.ИмяСобытия += new СвязанныйДелегат(функцияДляВыэова);
//
Car.EngineHandler d = new Car.CarEngineHandler(CarExplodedEventHandler)
myCar.Exploded += d;
Для отключения от источника событий служит операция -= в соответствии со
следующим шаблоном:
// ИмяОбъекта.ИмяСобытия -= new Свяэ анныйДе легат (функцияДляВыэова) ;
//
myCar.Exploded -= d;
Следуя этому очень простому шаблону, переделаем метод Main(), применив на этот
раз синтаксис регистрации методов С#:
class Program
{
static void Main(string [ ] args)
{
Console.WriteLine("***** Fun with Events *****\n");
Car cl = new Car ("SlugBug", 100, 10);
// Зарегистрировать обработчики событий.
cl.AboutToBlow += new Car.CarEngineHandler(CarlsAlmostDoomed);
cl.AboutToBlow += new Car.CarEngineHandler(CarAboutToBlow);
Car.CarEngineHandler d = new Car.CarEngineHandler(CarExploded);
cl.Exploded += d;
Console.WriteLine ("***** Speeding up *****");
for (int i = 0; i < 6; i++)
cl.AccelerateB0);
// Удалить метод CarExploded из списка вызовов.
cl.Exploded -= d;
Console.WriteLine("\n***** Speeding up *****");
for (int i = 0; i < 6; i++)
cl.AccelerateB0);
Console.ReadLine() ;
}
408 Часть III. Дополнительные конструкции программирования на С#
public static void CarAboutToBlow(string msg)
{ Console.WriteLine(msg); }
public static void CarlsAlmostDoomed(string msg)
{ Console.WriteLine("=> Critical Message from Car: {0}", msg); }
public static void CarExploded(string msg)
{ Console.WriteLine(msg); }
}
Чтобы еще более упростить регистрацию событий, можно применить групповое
преобразование методов. Ниже показана очередная модификация Main().
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Events *****\n");
Car cl = new Car ("SlugBug", 100, 10);
// Регистрация обработчиков событий.
cl.AboutToBlow += CarlsAlmostDoomed;
cl.AboutToBlow += CarAboutToBlow;
cl.Exploded += CarExploded;
Console.WriteLine ("***** Speeding up *****");
for (int i = 0; l < 6; i++)
cl.AccelerateB0);
cl.Exploded -= CarExploded;1
Console.WriteLine("\n***** Speeding up *****");
for (int i = 0; l < 6; i++)
cl.AccelerateB0);
Console.ReadLine() ;
}
Упрощенная регистрация событий
с использованием Visual Studio 2010
Среда Visual Studio 2010 предоставляет помощь в процессе регистрации
обработчиков событий. В случае применения синтаксиса += во время регистрации событий
открывается окно IntelliSense, приглашающее нажать клавишу <ТаЬ> для
автоматического заполнения экземпляра делегата (рис. 11.2).
Program.cs*
^fCarEvents.Program
vHooklntoEventiQ
public static void HookIntoEvents()
{
Car newCar » new Car();
newCar.AboutToBlow +-
}
I new Car.CarEngineHandler(newCar_AboutToBlow); (Press TAB to insert) |
100% -j <
Рис. 11.2. Выбор делегата с помощью IntelliSense
После нажатия клавиши <ТаЬ> появляется возможность ввести имя обработчика
событий, который нужно сгенерировать (или просто принять имя по умолчанию), как
показано на рис. 11.3.
Снова нажав <ТаЬ>, вы получите заготовку кода цели делегата в корректном
формате (обратите внимание, что этот метод объявлен статическим, потому что событие было
зарегистрировано внутри статического метода MainO):
static void newCar_AboutToBlow(string msg)
{
// Add your code '
Глава 11. Делегаты, события и лямбда-выражения 409
Щ1 Program.cs* X Qyj
^CarEvents-Program »J ^HooldntoEventsQ * 1
ш"^
public static void HookIntoEvents() * \
{..
Car nevrCar » new Car()j
newCar.AboutToBlow
+-new Car/tarEngineH.andler(MS^rJ^ST^^Ej)J Щ
} i^ 1 Press TAB to generate handler 'newCar_At>outToBlow' in this class i 'J
} ■ "" ■""■' * *" "*■ " u " ' '■' 'J' u u
Рис. 11.3. Формат цели делегата IntelliSense
Средство IntelliSense доступно для всех событий .NET из библиотек базовых
классов. Это средство интегрированной среды разработки замечательно экономит время,
но не избавляет от необходимости поиска в справочной системе .NET правильного
делегата для использования с определенным событием, а также формата целевого метода
делегата.
Исходный код. Проект CarEvents доступен в подкаталоге Chapter 11.
Создание специальных аргументов событий
По правде говоря, есть еще одно последнее усовершенствование, которое можно
внести в текущую итерацию класса Саг и которое отражает рекомендованный Microsoft
шаблон событий. Если вы начнете исследовать события, посылаемые определенным
типом из библиотек базовых классов, то обнаружите, что первым параметром
лежащего в основе делегата будет System.Object, в то время как вторым параметром — тип,
являющийся потомком System.EventArgs.
Аргумент System.Object представляет ссылку на объект, который отправляет
событие (такой как Саг), а второй параметр — информацию, относящуюся к
обрабатываемому событию. Базовый класс System. Event Args представляет событие, которое не
посылает никакой специальной информации:
public class EventArgs
public static readonly System.EventArgs Empty;
public EventArgs();
}
Для простых событий можно передать экземпляр EventArgs непосредственно.
Однако чтобы передавать какие-то специальные данные, потребуется построить
подходящий класс, унаследованный от EventArgs. Для примера предположим, что есть
класс по имени CarEventArgs, поддерживающий строковое представление сообщения,
отправленного получателю:
public class CarEventArgs : EventArgs
{
public readonly string msg;
public CarEventArgs(string message)
{
msg = message;
}
}
Теперь понадобится модифицировать делегат CarEngineHandler, как показано ниже
(само событие не изменяется):
410 Часть III. Дополнительные конструкции программирования на С#
public class Car
{
public delegate void CarEngineHandler(object sender, CarEventArgs e) ;
}
Здесь при инициализации события из метода Accelerate() нужно использовать
ссылку на текущий Саг (через ключевое слово this) и экземпляр типа CarEventArgs.
Например, рассмотрим следующее обновление:
public void Accelerate (int delta)
{
// Если этот автомобиль сломан, инициировать событие Exploded.
if (carlsDead)
{
if (Exploded != null)
Exploded(this, new CarEventArgs("Sorry, this car is dead..."));
}
}
Все, что потребуется сделать на вызывающей стороне — это обновить обработчики
событий для получения входных параметров и получения сообщения через поле,
доступное только для чтения. Например:
public static void CarAboutToBlow(object sender, CarEventArgs e)
{
Console.WriteLine ("{0 } says: {1}", sender, o.msg);
}
Если получатель желает взаимодействовать с объектом, отправившим событие,
можно выполнить явное приведение System.Object. С помощью такой ссылки можно
вызвать любой общедоступный метод объекта, который отправил событие:
public static void CarAboutToBlow (object sender, CarEventArgs e)
{
// Чтобы подстраховаться, произведем проверку во время выполнения перед приведением.
if (sender is Car)
{
Car с = (Car)sender;
Console.WriteLine("Critical Message from {0}: {1}", c.PetName, e.msg);
}
}
Исходный код. Проект PrimAndProperCarEvents доступен в подкаталоге Chapter 11.
Обобщенный делегат EventHandler<T>
Учитывая, что очень много специальных делегатов принимают объект в первом
параметре и наследников EventArgs — во втором, можно еще более упростить
предыдущий пример, используя обобщенный тип EventHandler<T>, где Т — специальный
тип-наследник EventArgs. Рассмотрим следующую модификацию типа Саг (обратите
внимание, что строить специальный делегат больше не нужно):
public class Car
{
public event EventHandler<CarEventArgs> Exploded;
public event EventHandler<CarEventArgs> AboutToBlow;
}
Глава 11. Делегаты, события и лямбда-выражения 411
Метод Main() может затем использовать EventHandler<CarEventArgs> везде, где
ранее указывался CarEngineHandler:
static void Main(string[] args)
{
Console.WriteLine("***** Prim and Proper Events *****\n");
// Создать Car обычным образом.
Car cl = new Car ("SlugBug", 100, 10);
// Зарегистрировать обработчики событий.
cl.AboutToBlow += CarlsAlmostDoomed;
cl.AboutToBlow += CarAboutToBlow;
EventHandler<CarEventArgs> d = new EventHandler<CarEventArgs>(CarExploded);
cl.Exploded += d;
}
Итак, вы ознакомились с основными аспектами работы с делегатами и событиями
на языке С#. Хотя этого вполне достаточно для решения практически любых задач,
связанных с обратными вызовами, в завершение главы рассмотрим ряд финальных
упрощений, а именно — анонимные методы и лямбда-выражения.
Исходный код. Проект PrimAndProperCarEvents (Generic) доступен в подкаталоге
Chapter 11.
Понятие анонимных методов С#
Как было показано выше, когда вызывающий код желает прослушивать входящие
события, он должен определить специальный метод в классе (или структуре),
соответствующий сигнатуре ассоциированного делегата. Ниже приведен пример:
class Program
{
static void Main (string[] args)
{
SomeType t = new SomeTypeO ;
// Предположим, что SomeDeletage может указывать на методы,
// которые не принимают аргументов и возвращают void.
t.SomeEvent += new SomeDelegate(MyEventHandler);
}
// Обычно вызывается только объектом SomeDelegate.
public static void MyEventHandler()
{
// Что-то делать по возникновении события.
}
}
Однако если подумать, то такие методы, как MyEventHandler (), редко
предназначены для обращения из любой другой части программы помимо делегата. Если речь
идет о производительности, несложно вручную определить отдельный метод для вызова
объектом делегата.
Чтобы справиться с этим, можно ассоциировать событие непосредственно с
блоком операторов кода во время регистрации события. Формально такой код называется
анонимным методом. Для иллюстрации синтаксиса напишем метод Main(), который
обрабатывает события, посланные из типа Саг, с использованием анонимных методов
вместо специальных именованных обработчиков событий:
412 Часть III. Дополнительные конструкции программирования на С#
class Program
{
static void Main(string[] args)
{
Console.WriteLine ("***** Anonymous Methods *****\n");
Car cl = new Car ("SlugBug", 100, 10);
// Зарегистрировать обработчики событий в виде анонимных методов.
cl.AboutToBlow += delegate {
Console.WriteLine("Eek! Going too fast!");
};
cl .AboutToBlow += delegate(object sender, CarEventArgs e) {
Console.WriteLine("Message from Car: {0}", e.msg);
};
cl.Exploded += delegate(object sender, CarEventArgs e) {
Console.WriteLine("Fatal Message from Car: {0}", e.msg);
};
// Это в конечном итоге инициирует события,
for (int i = 0; i < 6; i + + )
cl.Accelerate B0);
Console.ReadLine();
}
}
На заметку! Последняя фигурная скобка анонимного метода должна завершаться точкой с
запятой. Если забыть об этом, во время компиляции возникнет ошибка.
Обратите внимание, что в классе Program теперь не определяются специальные
статические обработчики событий вроде CarAboutToBlowO или CarExploded(). Вместо
этого с помощью синтаксиса += определяются встроенные неименованные (анонимные)
методы, к которым вызывающий код будет обращаться во время обработки события.
Базовый синтаксис анонимного метода соответствует следующему псевдокоду:
class Program
{
static void Main(string [ ] args)
{
SomeType t = new SomeTypeO ;
t.SomeEvent += delegate (optionallySpecifledDelegateArgs)
{ /* операторы */ };
}
}
Обратите внимание, что при обработке первого события AboutToBlow внутри
предыдущего метода Main() аргументы, передаваемые из делегата, не указываются:
cl .AboutToBlow += delegate {
Console.WriteLine ("Eek! Going too fast!");
};
Строго говоря, вы не обязаны принимать входные аргументы, посланные
определенным событием. Однако если планируется использовать эти входные аргументы, нужно
указать параметры, прототипированные типом делегата (как показано во второй
обработке событий AboutToBlow и Exploded). Например:
cl.AboutToBlow += delegate(object sender, CarEventArgs e) {
Console.WriteLine("Critical Message from Car: {0}", e.msg);
};
Глава 11. Делегаты, события и лямбда-выражения 413
Доступ к локальным переменным
Анонимные методы интересны тем, что могут обращаться к локальным переменным
метода, в котором они определены. Формально такие переменные называются
внешними (outer) переменными анонимного метода. Ниже отмечены некоторые важные
моменты, касающиеся взаимодействия между контекстом анонимного метода и контекстом
определяющего их метода.
• Анонимный метод не имеет доступа к параметрам ref и out определяющего их
метода.
• Анонимный метод не может иметь локальных переменных, имена которых
совпадают с именами локальных переменных объемлющего метода.
• Анонимный метод может обращаться к переменным экземпляра (или статическим
переменным) из контекста объемлющего класса.
• Анонимный метод может объявлять локальные переменные с теми же именами,
что и у членов объемлющего класса (локальные переменные имеют отдельный
контекст и скрывают внешние переменные-члены).
Предположим, что метод Main() определяет локальную переменную по имени
aboutToBlowCounter типа int. Внутри анонимных методов, обрабатывающих событие
AboutToBlow, мы увеличим значение этого счетчика на 1 и выведем результат на
консоль перед завершением Main():
static void Main(string[] args)
{
Console.WriteLine ("***** Anonymous Methods *****\n");
int aboutToBlowCounter = 0;
// Создать Car обычным образом.
Car cl = new Car ("SlugBug", 100, 10);
// Зарегистрировать обработчики событий в виде анонимных методов.
cl.AboutToBlow += delegate
{
aboutToBlowCounter++;
Console.WriteLine("Eek! Going too fast!");
};
cl.AboutToBlow += (object sender, CarEventArgs e)
{
aboutToBlowCounter++ ;
Console.WriteLine("Critical Message from Car: {0}", msg);
};
Console.WriteLine("AboutToBlow event was fired {0} times.", aboutToBlowCounter);
Console.ReadLine();
}
Запустив этот модифицированный метод Main(), вы обнаружите, что финальный
вывод Console.WriteLine() сообщит о двукратном вызове AboutToBlow.
Исходный код. Проект AnonymousMethods доступен в подкаталоге Chapter 11.
Понятие лямбда-выражений
Чтобы завершить знакомство с архитектурой событий .NET, рассмотрим
лямбда-выражения. Как объяснялось ранее в этой главе, С# поддерживает способность
обрабатывать события "встроенным образом", назначая блок операторов кода непосредственно
событию с использованием анонимных методов вместо построения отдельного мето-
414 Часть III. Дополнительные конструкции программирования на С#
да, подлежащего вызову лежащим в основе делегатом. Лямбда-выражения — это всего
лишь лаконичный способ записи анонимных методов, в конечном итоге упрощающий
работу с типами делегатов .NET.
Чтобы подготовить фундамент для изучения лямбда-выражений, создадим новое
консольное приложение по имени SimpleLambdaExpressions. Теперь займемся
методом FindAlK) обобщенного типа List<T>. Этот метод может быть вызван, когда нужно
извлечь подмножество элементов из коллекции, и он имеет следующий прототип:
// Метод класса System.Collections.Generic.List<T>.
public List<T> FindAll(Predicate<T> match)
Как видите, этот метод возвращает объект List<T>, представляющий подмножество
данных. Также обратите внимание, что единственный параметр FindAll () —
обобщенный делегат типа System. Predicate<T>. Этот делегат может указывать на любой метод,
возвращающий bool и принимающий единственный параметр:
// Этот делегат используется методом FindAll () для извлечения подмножества,
public delegate bool Predicate<T>(T obj);
Когда вызывается FindAll (), каждый элемент в List<T> передается методу,
указанному объектом Predicate<T>. Реализация этого метода будет производить некоторые
вычисления для проверки соответствия элемента данных указанному критерию, возвращая true
или false. Если метод вернет true, то текущий элемент будет добавлен в List<T>,
представляющий искомое подмножество. Прежде чем посмотреть, как лямбда-выражения
упрощают работу с FindAll (), давайте решим эту задачу в длинной нотации, используя
объекты делегатов непосредственно. Добавим метод (по имени TraditionalDelegateSyntaxO)
к типу Program, который взаимодействует с System.Predicate<T> для обнаружения
четных чисел в списке List<T> целочисленных значений:
class Program
{
static void Main(string [ ] args)
{
Console.WriteLine("***** Fun with Lambdas *****\n");
TraditionalDelegateSyntaxO ;
Console.ReadLine();
}
static void TraditionalDelegateSyntaxO
{
// Создать список целых чисел.
List<int> list = new List<int>();
list. AddRange (new int [ ] { 20, 1, 4, 8, 9, 44 });
// Вызов FindAll() с использованием традиционного синтаксиса делегатов.
Predicate<int> callback = new Predicate<int>(IsEvenNumber);
List<int> evenNumbers = list.FindAll(callback);
Console.WriteLine("Here are your even numbers:");
foreach (int evenNumber in evenNumbers)
{
Console.Write("{0}\t", evenNumber);
}
Console.WriteLine ();
}
// Цель для делегата PredicateO.
static bool IsEvenNumber(int i)
{
// Это четное число?
return (i % 2) == 0;
}
}
Глава 11. Делегаты, события и лямбда-выражения 415
Здесь имеется метод (IsEvenNumber ()), отвечающий за проверку входного
целочисленного параметра на предмет четности или нечетности через операцию С# взятия
модуля % (получения остатка от деления). В результате запуска приложения на консоль
выводятся числа .20, 4, 8 и 44.
Хотя этот традиционный подход к работе с делегатами функционирует ожидаемым
образом, метод IsEvenNumber () вызывается только при очень ограниченных условиях;
в частности, когда вызывается FindAllO, который взваливает на нас полный багаж
определения метода. Если бы вместо этого применялся анонимный метод, код стал бы
существенно яснее. Рассмотрим следующий новый метод в классе Program:
static void AnonymousMethodSyntax()
{
// Создать список целых.
List<int> list = new List<int>();
list.AddRange(new int [ ] { 20, 1, 4, 8, 9, 44 });
// Теперь использовать анонимный метод.
List<int> evenNumbers = list.FindAll(delegate(int i)
{ return (i^2) ==0; } ) ;
// Вывод четных чисел.
Console.WriteLine("Here are your even numbers:");
foreach (int evenNumber in evenNumbers)
{
Console.Write ("{0}\t", evenNumber);
}
Console.WriteLine();
}
В этом случае вместо прямого создания типа делегата Predicate<T> с последующим
написанием отдельного метода, метод встраивается как анонимный. Хотя это шаг в
правильном направлении, мы все еще обязаны использовать ключевое слово delegate (или
строго типизированный Predicate<T>), и должны убедиться в соответствии списка
параметров. Также, согласитесь, синтаксис, используемый для определения анонимного
метода, выглядит несколько тяжеловесно, что особенно проявляется здесь:
List<int> evenNumbers = list.FindAll (
delegate(int i)
{
return (i % 2) == 0;
}
);
Для дальнейшего упрощения вызова FindAllO можно применять
лямбда-выражения. Используя этот новый синтаксис, вообще не приходится иметь дело с лежащим в
основе объектом делегата. Рассмотрим следующий новый метод в классе Program:
static void LambdaExpressionSyntax()
{
// Создать список целых.
List<int> list = new List<int>();
list.AddRange (new int[] { 20, 1, 4, 8, 9, 44 });
// Теперь используем лямбда-выражение С#.
List<int> evenNumbers = list. FindAll (l => (l % 2) == 0) ;
// Вывод четных чисел.
Console.WriteLine("Here are your even numbers:");
foreach (int evenNumber in evenNumbers)
{
Console.Write("{0}\t", evenNumber);
}
Console.WriteLine();
}
416 Часть III. Дополнительные конструкции программирования на С#
Здесь обратите внимание на довольно странный оператор кода, передаваемый
методу FindAll(), который в действительности и является лямбда-выражением. В этой
модификации примера вообще нет никаких следов делегата Predicate<T> (как и
ключевого слова delegate). Все, что специфицировано вместо них — это
лямбда-выражение: i => (i ць 2) == 0.
Прежде чем разбирать синтаксис дальше, пока просто усвойте, что
лямбда-выражения могут применяться везде, где использовался анонимный метод или строго
типизированный делегат (обычно в более лаконичном виде). "За кулисами" компилятор
С# транслирует выражение в стандартный анонимный метод, использующий тип
делегата Predicate<T> (в чем можно убедиться с помощью утилиты ildasm.exe или
reflector.exe). В частности, следующий оператор кода:
// Это лямбда-выражение...
List<int> evenNumbers = list.FindAll(i => (i Ъ 2) == 0) ;
компилируется в примерно такой код С#:
// . . .превращается в следующий анонимный метод.
List<int> evenNumbers = list.FindAll(delegate (int l)
{
return (l na 2) == 0;
});
Анализ лямбда-выражения
Лямбда-выражение начинается со списка параметров, за которым следует лексема
=> (лексема С# для лямбда-выражения позаимствована из лямбда-вычислений), а за
ней — набор операторов (или единственный оператор), который будет обрабатывать
параметры. На самом высоком уровне лямбда-выражение можно представить
следующим образом:
АргументыДляОбработки => ОбрабатывающиеОператоры
То, что находится внутри метода LambdaExpressionSyntaxO, следует понимать так:
// i — список параметров.
// (i % 2) = 0 - набор операторов для обработки i.
List<int> evenNumbers = list. FindAll (i => (l nu 2) == 0) ;
Параметры лямбда-выражения могут быть типизированы явно или неявно. В
настоящий момент тип данных, представляющий параметр i (целое), определяется
неявно. Компилятор способен понять, что i — целое, на основе контекста всего
лямбда-выражения, поместив тип данных и имя переменной в пару скобок, как показано ниже:
// Теперь установим тип параметров явно.
List<int> evenNumbers = list.FindAll ( (int i) => (i % 2) == 0) ;
Как было показано, если лямбда-выражение имеет одиночный неявно
типизированный параметр, то скобки в списке параметров могут быть опущены. При желании быть
последовательным в использовании параметров лямбда-выражений, можно всегда
заключать список параметров в скобки, чтобы выражение выглядело так:
List<int> evenNumbers = list. FindAll ( (i) => (l qd 2) == 0) ;
И, наконец, обратите внимание, что сейчас выражение не заключено в скобки
(разумеется, вычисление остатка от деления помещается в скобки, чтобы гарантировать его
выполнение перед проверкой равенства). Лямбда-выражение с оператором,
заключенным в скобки, выглядит следующим образом:
// Теперь заключим в скобки и выражение.
List<int> evenNumbers = list.FindAll((i) => ((l % 2) == 0));
Глава 11. Делегаты, события и лямбда-выражения 417
Теперь, когда известны разные способы построения лямбда-выражения, как его
представить в понятных человеку терминах? Оставив чистую математику в стороне,
можно привести следующее объяснение:
// Список параметров (в данном случае — единственное целое по имени i)
// будет обработан выражением (i % 2) =0.
List<int> evenNumbers = list.FindAll( (i) => (A % 2) == 0) ) ;
Обработка аргументов внутри множества операторов
Наше первое лямбда-выражение состояло из единственного оператора, который в
результате вычисления дает булевское значение. Однако, как должно быть хорошо
известно, многие цели делегатов должны выполнять множество операторов кода. По этой
причине С# позволяет строить лямбда-выражения, состоящие из нескольких блоков
операторов. Когда выражение должно обрабатывать параметры в нескольких строках
кода, понадобится выделить контекст этих операторов с помощью фигурных скобок.
Рассмотрим следующую модификацию метода LambdaExpressionSyntax():
static void LambdaExpressionSyntax()
{
// Создать список целых.
List<int> list = new List<int>();
list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });
// Обработать каждый аргумент в группе операторов кода.
List<int> evenNumbers = list.FindAll((i) =>
{
Console.WriteLine("value of l is currently: {0}", i);
bool lsEven = ((i пь 2) == 0) ;
return lsEven;
});
// Вывод четных чисел.
Console.WriteLine("Here are your even numbers:");
foreach (int evenNumber in evenNumbers)
{
Console.Write ("{0}\t", evenNumber);
}
Console.WriteLine();
}
В этом случае список параметров (опять состоящий из единственного целого i)
обрабатывается набором операторов кода. Помимо вызова Console.WriteLineO, оператор
вычисления остатка от деления разбит на два оператора для повышения
читабельности. Предположим, что каждый из рассмотренных выше методов вызывается в Main():
static void Main(string [ ] args)
{
Console.WriteLine("***** Fun with Lambdas *****\n");
TraditionalDelegateSyntax ();
AnonymousMethodSyntax();
Console.WriteLine();
LambdaExpressionSyntax();
Console.ReadLine();
}
Запуск этого приложения даст следующий вывод:
***** Fun with Lambdas *****
Here are your even numbers:
20 4 8 44
418 Часть III. Дополнительные конструкции программирования на С#
Here are your even numbers :
20 4 8 44
value of l is currently: 20
value of l is currently: 1
value of l is currently: 4
value of l is currently: 8
value of l is currently: 9
value of l is currently: 44
Here are your even numbers:
20 4 8 44
Исходный код. Проект SimpleLambdaExpressions доступен в подкаталоге Chapter 11.
Лямбда-выражения с несколькими параметрами и без параметров
Показанные выше лямбда-выражения обрабатывало единственный параметр.
Однако это вовсе не обязательно, поскольку лямбда-выражения могут обрабатывать
множество аргументов или вообще не иметь аргументов. Для иллюстрации первого
сценария создадим консольное приложение по имени LambdaExpressionsMultipleParams
со следующей версией класса SimpleMath:
public class SimpleMath
{
public delegate void MathMessage (string msg, int result);
private MathMessage mmDelegate;
public void SetMathHandler(MathMessage target)
{mmDelegate = target; }
public void Add (int x, int y)
{
if (mmDelegate != null)
mmDelegate.Invoke("Adding has completed!", x + y) ;
}
}
Обратите внимание, что делегат MathMessage принимает два параметра. Чтобы
представить их в виде лямбда-выражения, метод Main О может быть реализован так:
static void Main(string [ ] args)
{
// Регистрация делегата как лямбда-выражения.
SimpleMath m = new SimpleMath();
m.SetMathHandler((msg, result) =>
{Console.WriteLine("Message: {0}, Result: {1}", msg, result);});
// Это приведет к выполнению лямбда-выражения.
m.AddA0, 10) ;
Console.ReadLine();
}
Здесь используется выведение типа компилятором, поскольку для простоты два
параметра не типизированы строго. Однако можно было бы вызвать SetMathHandler ()
следующим образом:
m.SetMathHandler((string msg, int result) =>
{Console.WriteLine("Message: {0}, Result: {1}", msg, result);});
И, наконец, если лямбда-выражение используется для взаимодействия с делегатом,
вообще не принимающим параметров, то это можно сделать, указав в качестве
параметра пару пустых скобок. Таким образом, предполагая, что определен следующий тип
делегата:
Глава 11. Делегаты, события и лямбда-выражения 419
public delegate string VerySimpleDelegate();
вот как можно обработать результат вызова:
// Вывод на консоль строки "Enjoy your string!".
VerySimpleDelegate d =
new VerySimpleDelegate ( () => {return "Enjoy your string!11;} );
Console.WriteLine(d.Invoke() ) ;
Исходный код. Проект LambdaExpressionsMultipleParams доступен в подкаталоге
Chapter 11.
Усовершенствование примера PrimAndProperCarEvents
за счет использования лямбда-выражений
Учитывая то, что главное предназначение лямбда-выражений состоит в
обеспечении возможности в чистой, сжатой манере определить анонимный метод (и тем самым
упростить работу с делегатами), давайте переделаем проект PrimAndProperCarEvents,
созданный ранее в этой главе. Ниже приведена упрощенная версия класса Program
этого проекта, в которой используется синтаксис лямбда-выражений (вместо простых
делегатов) для перехвата всех событий, поступающих от объекта Саг.
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** More Fun with Lambdas *****\n") ;
// Создание объекта Car обычным образом.
Car cl = new Car("SlugBug", 100, 10);
// Использование лямбда-выражений.
cl.AboutToBlow += (sender, e) => { Console.WriteLine(e.msg); };
cl.Exploded += (sender, e) => { Console.WriteLine(e.msg); };
// Ускорим (это инициирует события).
Console.WriteLine("\n***** Speeding up *****");
for (int i = 0; i < 6; i++)
cl.AccelerateB0);
Console.ReadLine();
}
}
Теперь общая роль лямбда-выражений должна проясниться, и становится понятно,
что они обеспечивают "функциональную манеру" работы с анонимными методами и
типами делегатов. К новой лямбда-операции (=>) необходимо привыкнуть, однако
помните, что любые лямбда-выражения сводятся к следующему простому уравнению:
АргументыДляОбработки => ОбрабатывахлциеИхОператоры
Исходный код. Проект CarEventsWithLambdas доступен в подкаталоге Chapter 11.
Резюме
В этой главе вы ознакомились с несколькими способами двустороннего
взаимодействия множества объектов. Во-первых, было рассмотрено ключевое слово delegate,
используемое для неявного конструирования класса-наследника System.MuIticastDelegate.
420 Часть III. Дополнительные конструкции программирования на С#
Как было показано, объект делегата поддерживает список методов для вызова тогда,
когда ему об этом укажут. Такие вызовы могут выполняться синхронно (с
использованием метода Invoke()) или асинхронно (через методы BeginlnvokeO и EndlnvokeO).
Асинхронная природа типов делегатов .NET будет рассмотрена в главе 19.
Во-вторых, вы ознакомились с ключевым словом event, которое в сочетании с
типом делегата может упростить процесс отправки уведомлений о событиях ожидающим
объектам. Как видно в результирующем коде CIL, модель событий .NET отображается
на скрытые обращения к типам System.Delegate/System.MulticastDelegate. В этом
свете ключевое слово event является необязательным и просто позволяет сэкономить
на наборе кода.
В главе также рассматривалось средство языка С#, которое называется анонимными
методами. С помощью этой синтаксической конструкции можно явно ассоциировать
блок операторов кода с заданным событием. Как было показано, анонимные методы
могут игнорировать параметры, переданные событием, и имеют доступ к "внешним
переменным" определяющего их метода. Вы также ознакомились с упрощенным способом
регистрации событий с применением групповых преобразований методов.
И, наконец, в завершение главы было дано описание лямбда-операции => в С#. Как
было показано, этот синтаксис значительно сокращает нотацию написания анонимных
методов, когда набор аргументов может быть передан на обработку группе операторов.
ГЛАВА 12
Расширенные
средства языка С#
В этой главе рассматриваются некоторые более сложные синтаксические
конструкции языка программирования С#. Сначала будет показано, как реализуется
и используется метод-индексатор. Этот механизм С# позволяет строить специальные
типы, обеспечивающие доступ к внутренним подтипам с применением синтаксиса,
похожего на синтаксис массивов. Затем вы узнаете о том, как перегружать различные
операции (+, -, <, > и т.д.) и как создавать специальные процедуры явного и неявного
преобразования типов (а также причины, по которым это может понадобиться).
Далее рассматриваются три темы, которые особенно полезны при работе с API-
интерфейсами LINQ (хотя это применимо и вне контекста LINQ), а именно:
расширяющие методы, частичные методы и анонимные типы.
И в завершение вы узнаете, как создавать контекст "небезопасного" кода, чтобы
напрямую манипулировать неуправляемыми указателями. Хотя использовать
указатели в приложениях С# приходится исключительно редко, понимание того, как это
делается, может пригодиться в определенных ситуациях со сложными сценариями
взаимодействия.
Понятие методов-индексаторов
Программисты хорошо знакомы с процессом доступа к индивидуальным элементам,
содержащимся в стандартных массивах, через операцию индекса ([ ]). Например:
static void Main(string[] args)
{
// Цикл по аргументам командной строки с использованием операции индекса.
for(int i = 0; i < args.Length; i++)
Console.WriteLine("Args: {0}", args[i]);
// Объявление массива локальных целых.
int[] mylnts = { 10, 9, 100, 432, 9874};
// Использование операции индекса для доступа к элементам.
for(int D = 0; ] < mylnts.Length; j++)
Console.WriteLine("Index {0} = {1} ", j, mylnts[j]);
Console.ReadLine();
}
Приведенный код не должен быть для вас чем-то новым. В С# имеется возможность
проектировать специальные классы и структуры, которые могут быть индексированы
подобно стандартному массиву, посредством определения метода-индексатора. Это
422 Часть III. Дополнительные конструкции программирования на С#
конкретное языковое средство наиболее полезно при создании специальных типов
коллекций (обобщенных и необобщенных).
Прежде чем ознакомиться с реализацией специального индексатора, начнем с
рассмотрения его в действии. Предположим, что вы добавили поддержку
метода-индексатора к пользовательскому типу PeopleCollection, разработанному в главе 10 (в
проекте CustomNonGenericCollection). Рассмотрим следующее его применение в новом
консольном приложении по имени Simplelndexer:
// Индексаторы позволяют обращаться к элементам в стиле массива.
class Program
{
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Indexers *****\n");
PeopleCollection myPeople = new PeopleCollection ();
// Добавление объектов с помощью синтаксиса индексатора.
myPeople[0] = new Person("Homer", "Simpson", 40);
myPeople [1] = new Person("Marge", "Simpson", 38);
myPeople[2] = new Person ("Lisa", "Simpson", 9);
myPeople[3] = new Person ("Bart", "Simpson", 7) ;
myPeople[4] = new Person("Maggie", "Simpson", 2) ;
// Получение и вывод на консоль элементов с использованием индексатора.
for (int i = 0; i < myPeople.Count; i++)
{
Console.WriteLine("Person number: {0}", i);
Console.WriteLine("Name: {0} {1}",
myPeople [i] .FirstName, myPeople[i] .LastName);
Console.WriteLine("Age: {0}", myPeople[i].Age);
Console.WriteLine();
}
}
}
Как видите, в отношении доступа к подэлементам контейнера индексаторы ведут
себя подобно специальным коллекциям, поддерживающим интерфейсы IEnumerator и
IE numerable (либо их обобщенные версии). Главное отличие, конечно, в том, что вместо
доступа к содержимому с использованием конструкции f о reach можно манипулировать
внутренней коллекцией подобъектов подобно стандартному массиву.
Но тут возникает серьезный вопрос: как сконфигурировать класс PeopleCollection
(или любой другой класс либо структуру) для поддержки этой функциональности?
Индексатор представляет собой несколько видоизмененное определение свойства. В его
простейшей форме индексатор создается с использованием синтаксиса this [ ]. Ниже
показано необходимое изменение класса PeopleCollection из главы 10:
// Добавим индексатор к существующему определению класса.
public class PeopleCollection : IEnumerable
{
private ArrayList arPeople = new ArrayList();
// Специальный индексатор для этого класса.
public Person this[int index]
{
get { return (Person)arPeople[index]; }
set { arPeople.Insert(index, value); }
}
}
Глава 12. Расширенные средства языка С# 423
Помимо использования ключевого слова this, индексатор выглядит как объявление
любого другого свойства С#. Например, роль конструкции get состоит в возврате
корректного объекта вызывающему коду. Здесь мы фактически и делаем это, делегируя
запрос к индексатору объекта ArrayList. В противоположность этому, конструкция set
отвечает за размещение входящего объекта в контейнере по определенному индексу; в
данном примере это достигается вызовом метода Insert () объекта ArrayList.
Как видите, индексаторы — это просто еще одна форма синтаксиса, учитывая, что
та же функциональность может быть обеспечена с использованием "нормальных"
общедоступных методов вроде AddPerson () или GetPerson (). Тем не менее, поддержка
методов-индексаторов в специальных типах коллекций позволяет их легко интегрировать
с библиотеками базовых классов .NET.
Хотя создание методов-индексаторов — обычное дело при построении специальных
коллекций, следует помнить, что обобщенные типы предлагают эту функциональность
в готовом виде. В следующем методе используется обобщенный список List<T>
объектов Person. Обратите внимание, что индексатор List<T> можно просто применять
непосредственно.
static void UseGenericListOfPeople ()
{
List<Person> myPeople = new List<Person> ();
myPeople.Add(new Person("Lisa", "Simpson", 9));
myPeople.Add(new Person("Bart", "Simpson", 7));
// Заменим первую персону с помощью индексатора.
myPeople[0] = new Person("Maggie", "Simpson", 2);
// Теперь получим и отобразим каждый элемент через индексатор.
for (int i = 0; i < myPeople .Count; i++)
{
Console.WriteLine("Person number: {0}", l) ;
Console.WriteLine("Name: {0} {1}", myPeople[i].FirstName,
myPeople[i].LastName);
Console.WriteLine("Age: {0}", myPeople[l].Age);
Console.WriteLine();
}
}
Исходный код. Проект Simplelndexer доступен в подкаталоге Chapter 12.
Индексация данных с использованием строковых значений
В текущем классе PeopleCollection определен индексатор, позволяющий
вызывающему коду идентифицировать подэлементы с применением числовых значений. Однако
надо понимать, что это не обязательное требование метода-индексатора. Предположим,
что решено хранить объекты Person, используя System. Collections . Generic .
Dictionary<TKey, TValue> вместо ArrayList. Учитывая, что типы ListDictionary
позволяют производить доступ к содержащимся в них типам с использованием
строкового маркера (такого как фамилия персоны), индексатор можно было бы определить
следующим образом:
public class PeopleCollection : IEnumerable
{
private Dictionary<string, Person> listPeople =
new Dictionary<string, Person>();
424 Часть III. Дополнительные конструкции программирования на С#
// Этот индексатор возвращает персону по строковому индексу.
public Person this[string name]
{
get { return (Person)listPeople[name]; }
set { listPeople[name] = value; }
}
public void ClearPeople ()
{ listPeople.Clear(); }
public int Count
{ get { return listPeople.Count; } }
IEnumerator IEnumerable.GetEnumerator()
{ return listPeople.GetEnumerator(); }
}
Теперь вызывающий код может взаимодействовать с содержащимися внутри
объектами Person, как показано ниже:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Indexers *****\n");
PeopleCollection myPeople = new PeopleCollection ();
myPeople["Homer"] = new Person("Homer", "Simpson", 40);
myPeople["Marge"] = new Person("Marge", "Simpson", 38);
// Получит "Homer" и вывести данные на консоль.
Person homer = myPeople["Homer"];
Console.WriteLine(homer.ToString() ) ;
Console.ReadLine();
}
Опять-таки, если использовать обобщенный тип Dictionary<TKey, TValue>
напрямую, получится функциональность метода-индексатора в готовом виде, без построения
специального необобщенного класса, поддерживающего строковый индексатор.
Исходный код. Проект Stringlndexer доступен в подкаталоге Chapter 12.
Перегрузка методов-индексаторов
Имейте в виду, что методы-индексаторы могут быть перегружены в отдельном
классе или структуре. То есть если имеет смысл позволить вызывающему коду обращаться к
подэлементам с использованием числового индекса или строкового значения, в одном и
том же типе можно определить несколько индексаторов. Например, если вы когда-либо
программировали с применением ADO.NET (встроенный API-интерфейс .NET для
доступа к базам данных), то вспомните, что тип Data Set поддерживает свойство по имени
Tables, которое возвращает строго типизированную коллекцию DataTableCollection.
В свою очередь, в DataTableCollection определены три индексатора для получения
объектов DataTable — по порядковому номеру, по дружественным строковым именам
и необязательному пространству имен:
public sealed class DataTableCollection : InternalDataCollectionBase
{
// Перегруженные индексаторы.
public DataTable this[string name] { get; }
public DataTable this[string name, string tableNamespace] { get; }
public DataTable this[int index] { get; }
Глава 12. Расширенные средства языка С# 425
Следует отметить, что множество типов из библиотек базовых классов
поддерживают методы-индексаторы. Поэтому даже если текущий проект не требует построения
специальных индексаторов для классов и структур, помните, что многие типы уже
поддерживают этот, синтаксис.
Многомерные индексаторы
Можно также создавать метод-индексатор, принимающий несколько параметров.
Предположим, что имеется специальная коллекция, хранящая подэлементы
двумерного массива. В этом случае метод-индексатор можно сконфигурировать следующим
образом:
public class SomeContainer
{
private int[,] my2DintArray = new int[10, 10];
public int this[int row, int column]
{ /* установить или получить значение из двумерного массива */ }
}
Если только не строится очень специализированный класс коллекций, то вряд ли
понадобится создавать многомерные индексаторы. Здесь снова пример ADO.NET
показывает, насколько полезной может быть эта конструкция. Класс DataTable в ADO.NET —
это, по сути, коллекция строк и столбцов, похожая на распечатанную таблицу или
электронную таблицу Microsoft Excel.
Хотя объекты DataTable обычно наполняются без вашего участия, посредством
связанных с ними "адаптеров данных", в приведенном ниже коде показано, как
вручную создать находящийся в памяти объект DataTable, содержащий три столбца (для
имени, фамилии и возраста). Обратите внимание, что после добавления одной строки
в DataTable с помощью многомерного индексатора производится обращение к всем
столбцам первой (и единственной) строки. (Чтобы реализовать это, в файл кода
понадобится импортировать пространство имен System.Data.)
static void MultilndexerWithDataTable ()
{
// Создать простой объект DataTable с тремя столбцами.
DataTable myTable = new DataTable ();
myTable.Columns.Add(new DataColumn("FirstName"));
myTable.Columns.Add(new DataColumn("LastName"));
myTable.Columns.Add(new DataColumn("Age"));
// Добавить строку к таблице.
myTable.Rows.Add("Mel", "Appleby", 60);
// Использовать многомерный индексатор для вывода деталей первой строки.
Console.WriteLine("First Name: {0}", myTable.Rows[0][0]);
Console.WriteLine("Last Name: {0}", myTable.Rows[0][1]);
Console.WriteLine("Age : {0}", myTable.Rows[0][2]);
}
Начиная с главы 21, мы продолжим рассмотрение ADO.NET, так что не пугайтесь,
если что что-то в приведенном выше коде покажется незнакомым. Этот пример просто
иллюстрирует, что методы-индексаторы могут поддерживать множество измерений, а
при правильном использовании могут упростить взаимодействие с подобъектами,
содержащимися в специальных коллекциях.
Определения индексаторов в интерфейсных типах
Индексаторы могут определяться в типе интерфейса, позволяя поддерживающим
типам предоставлять их специальные реализации.
426 Часть III. Дополнительные конструкции программирования на С#
Ниже показан пример такого интерфейса, который определяет протокол для
получения строковых объектов с использованием числового индексатора:
public interface IStringContainer
{
// Этот интерфейс определяет индексатор, возвращающий
// строки по числовому индексу.
string this[int index] { get; set; }
}
При таком определении интерфейса любой класс или структура, реализующие его,
должны поддерживать индексатор чтения/записи, манипулирующий подэлементами
через числовое значение.
На этом первая главная тема настоящей главы завершена. Хотя понимание
синтаксиса индексаторов С# важно, как объяснялось в главе 10, обычно единственным
случаем, когда программисту нужно строить специальный обобщенный класс коллекции,
является ситуация, когда необходимо добавить ограничения к параметрам-типам. Если
придется строить такой класс, добавление специального индексатора может заставить
класс коллекции выглядеть и вести себя подобно стандартному классу коллекции из
библиотеки базовых классов .NET.
А теперь давайте рассмотрим языковое средство, позволяющее строить специальные
классы и структуры, которые уникальным образом реагируют на встроенные операции
С# — перегрузку операций.
Понятие перегрузки операций
В С#, подобно любому языку программирования, имеется готовый набор лексем,
используемых для выполнения базовых операций над встроенными типами. Например,
известно, что операция + может применяться к двум целым, чтобы дать их сумму:
// Операция + с целыми.
int a = 10 0;
int b = 240;
int с = а + Ь; //с теперь равно 340
Здесь нет ничего нового, но задумывались ли вы когда-нибудь о том, что одна и
та же операция + может применяться к большинству встроенных типов данных С#?
Например, рассмотрим такой код:
// Операция + со строками.
string si = "Hello";
string s2 = " world!";
string s3 = si + s2; // s3 теперь содержит "Hello world!"
По сути, функциональность операции + уникальным образом базируются на
представленных типах данных (строках или целых в данном случае). Когда операция +
применяется к числовым типам, мы получаем арифметическую сумму операндов. Однако
когда та же операция применяется к строковым типам, получается конкатенация
строк.
Язык С# предоставляет возможность строить специальные классы и структуры,
которые также уникально реагируют на один и тот же набор базовых лексем (вроде
операции +). Имейте в виду, что абсолютно каждую встроенную операцию С# перегружать
нельзя. В табл. 12.1 описаны возможности перегрузки основных операций.
Глава 12. Расширенные средства языка С# 427
Таблица 12.1. Возможности перегрузки операций С#
Операция С# Возможность перегрузки
true, Этот набор унарных операций может быть перегружен
+, -,
fal.
+i -i
==,
i
se
*
i =
~,
/,
<,
++I "■
%, &,
>i <=.
-, 1
I, '
, >
:, » Эти бинарные операции могут быть перегружены
Эти операции сравнения могут быть перегружены. С# требует
совместной перегрузки "подобных" операций (т.е. < и >, <= и >=,
==и !=)
[ ] Операция [ ] не может быть перегружена. Как было показано
ранее в этой главе, однако, аналогичную функциональность
предлагают индексаторы
() Операция () не может быть перегружена. Однако, как будет
показано далее в этой главе, ту же функциональность
предоставляют специальные методы преобразования
+=, -=, *=, /=, %=, &=, | =, Сокращенные операции присваивания не могут перегружаться;
А=, «=, »= однако вы получаете их автоматически, перегружая
соответствующую бинарную операцию
Перегрузка бинарных операций
Чтобы проиллюстрировать процесс перегрузки бинарных операций, представим
следующий простой класс Point, определенный в новом консольном приложении по имени
OverloadedOps:
// Простой класс С# для повседневного пользования.
public class Point
{
public int X {get; set;}
public int Y {get; set;}
public Point(int xPos, int yPos)
{
X = xPos;
Y = yPos;
}
public override string ToStringO
return string.Format("[{0}, {1}]", this.X, this.Y);
}
Теперь, логически рассуждая, имеет смысл складывать экземпляры Point вместе.
Например, если сложить вместе две переменных Point, получится новая Point с
суммарными значениями х и у. Кстати, также может быть полезно иметь возможность вычитать
одну Point из другой. В идеале пригодилась бы возможность написать такой код:
// Сложение и вычитание двух точек?
static void Main(string [ ] args)
{
Console.WriteLine("***** Fun with Overloaded Operators *****\n");
// Создать две точки.
Point ptOne = new Point A00, 100);
Point ptTwo = new Point D0, 40);
Console.WriteLine("ptOne = {0}", ptOne);
Console.WriteLine("ptTwo = {0}", ptTwo);
428 Часть III. Дополнительные конструкции программирования на С#
// Сложить две точки, чтобы получить большую?
Console.WriteLine("ptOne + ptTwo: {0} ", ptOne + ptTwo);
// Вычесть одну точку из другой, чтобы получить меньшую?
Console.WriteLine("ptOne - ptTwo: {0} ", ptOne - ptTwo);
Console.ReadLine();
}
Однако в том виде, как он есть, класс Point приведет к ошибкам этапа компиляции,
поскольку типу Point не известно, как реагировать на операции + и -.
Чтобы оснастить специальный тип возможностью уникально реагировать на
встроенные операции, в С# служит ключевое слово operator, которое может
использоваться только в сочетании со статическими методами. При перегрузке бинарной операции
(вроде + и -) чаще всего передаются два аргумента того же типа, что и определяющий
их класс (в данном примере — Point); это иллюстрируется в следующей модификации
кода:
// Более интеллектуальный тип Point.
public class Point
{
// Перегруженная операция +
public static Point operator + (Point pi, Point p2)
{ return new Point(pi.X + p2.X, pi. Y + p2.Y); }
// Перегруженная операция -
public static Point operator - (Point pi, Point p2)
{ return new Point(pl.X - p2.X, pl.Y - p2.Y); }
}
Логика, положенная в основу операции +, состоит просто в возврате нового
экземпляра Point на основе сложения соответствующих полей входных параметров Point.
Поэтому, когда вы напишете ptl + pt2, "за кулисами" произойдет следующий скрытый
вызов статического метода operator+:
// Псевдокод: Point рЗ = Point.operator+ (pi, p2)
Point рЗ = pi + р2;
Аналогично, pi - р2 отображается на следующее:
// Псевдокод: Point р4 = Point. operator- (pi, p2)
Point р4 = pi - р2;
После этого дополнения программа скомпилируется, и мы получим возможность
складывать и вычитать объекты Point:
ptOne = [100, 100]
ptTwo = [40, 40]
ptOne + ptTwo: [140, 140]
ptOne - ptTwo: [60, 60]
При перегрузке бинарной операции вы не обязаны передавать ей два параметра
одинакового типа. Если это имеет смысл, один из аргументов может отличаться. Например,
ниже показана перегруженная операция +, которая позволяет вызывающему коду
получить новый объект Point на основе числового смещения:
public class Point
{
public static Point operator + (Point pi, int change)
{
return new Point(pi.X + change, pl.Y + change);
}
Глава 12. Расширенные средства языка С# 429
public static Point operator + (int change, Point pi)
{
return new Point(pi.X + change, pl.Y + change);
Обратите внимание, что если нужно передавать аргументы в любом порядке,
потребуются обе версии метода (т.е. нельзя просто определить один из методов и
рассчитывать, что компилятор автоматически будет поддерживать другой). Теперь можно
использовать эти новые версии операции + следующим образом:
// Выводит [110, 110]
Point biggerPoint = ptOne + 10;
Console.WriteLine("ptOne + 10 = {0}", biggerPoint);
// Выводит [120, 120]
Console.WriteLine(0 + biggerPoint = {0}", 10 + biggerPoint);
Console.WriteLine () ;
А как насчет операций += и -=?
Перешедших на С# с языка C++ может удивить отсутствие возможности перегрузки
операций сокращенного присваивания (+=, -+ и т.д.). Не беспокойтесь. В терминах С#
операции сокращенного присваивания автоматически эмулируются при перегрузке
соответствующих бинарных операций. Таким образом, если в классе Point уже
перегружены операции + и -, можно написать следующий код:
// Перегрузка бинарных операций автоматически обеспечивает
// перегрузку сокращенных операций,
static void Main (string[] args)
{
// Операция += автоматически перегружена
Point ptThree = new Point (90, 5) ;
Console.WriteLine("ptThree = {0}", ptThree);
Console.WriteLine("ptThree += ptTwo: {0}", ptThree += ptTwo);
// Операция -= автоматически перегружена
Point ptFour = new Point @, 500);
Console.WriteLine("ptFour = {0}", ptFour);
Console.WriteLine ("ptFour -= ptThree: {0}", ptFour -= ptThree);
Console.ReadLine ();
}
Перегрузка унарных операций
В С# также допускается перегружать и унарные операции, такие как ++ и —. При
перегрузке унарной операции также определяется статический метод через ключевое
слово operator, однако в этом случае просто передается единственный параметр того
же типа, что и определяющий его класс/структура. Например, дополните Point
следующими перегруженными операциями:
public struct Point
{
// Прибавить 1 к значениям X/Y входного объекта Point.
public static Point operator ++(Point pi)
{ return new Point(pi.X+l, pl.Y+1); }
// Вычесть 1 из значений X/Y входного объекта Point.
public static Point operator —(Point pi)
{ return new Point(pi.X-l, pl.Y-1); }
430 Часть III. Дополнительные конструкции программирования на С#
В результате появляется возможность выполнять инкремент и декремент значений
X и Y класса Point, как показано ниже:
static void Main(string [ ] args)
{
// Применение унарных операций ++ и — к Point.
Point ptFive = new Point A, 1) ;
Console.WriteLine("++ptFive = {0}", ++ptFive); // [2, 2]
Console.WriteLine("—ptFive = {0}", —ptFive); // [1, 1]
// Применение тех же операций для постфиксного инкремента/декремента.
Point ptSix = new Point B0, 20);
Console.WriteLine("ptSix++ = {0}", ptSix++); // [20, 20]
Console.WriteLine ("ptSix-- = {0}", ptSix—) ; // [21, 21]
Console.ReadLine();
}
В предыдущем примере кода обратите внимание, что специальные операции ++ и
— применяются двумя разными способами. В C++ допускается перегружать операции
префиксного и постфиксного инкремента/декремента по отдельности. В С# это
невозможно; тем не менее, возвращаемое значение инкремента/декремента автоматически
обрабатывается правильно (т.е., для перегруженной операции ++ выражение pt++ имеет
значение ^модифицированного объекта, в то время как ++pt имеет новое значение,
примененное перед использованием выражения).
Перегрузка операций эквивалентности
Как вы должны помнить из главы 6, метод System.Object .Equals () может быть
перегружен для выполнения сравнений объектов на основе значений (а не
ссылок). Если вы решите переопределить Equals () (часто вместе со связанным методом
System. Object. GetHashCode ()), это позволит легко переопределить и операции
проверки эквивалентности (== и ! =). Для иллюстрации рассмотрим модифицированное
определение типа Point:
// Этот вариант Point также перегружает операции == и '=.
public class Point
{
public override bool Equals(object o)
{
return o.ToStringO == this.ToString();
}
public override int GetHashCode ()
{ return this.ToString () .GetHashCode(); }
// Теперь перегрузим операции == и !=.
public static bool operator ==(Point pi, Point p2)
{ return pi.Equals(p2); }
public static bool operator !=(Point pi, Point p2)
{ return 'pi.Equals (p2); }
}
Обратите внимание, что для выполнения нужной работы реализации операций ==
и ! = просто вызывают перегруженный метод Equals (). Теперь класс Point можно
использовать следующим образом:
// Использование перегруженных операций эквивалентности.
static void Main(string [ ] args)
{
Console.WriteLine("ptOne == ptTwo : {0}", ptOne == ptTwo);
Глава 12. Расширенные средства языка С# 431
Console.WriteLine("ptOne !=ptTwo : {0}", ptOne != ptTwo);
Console.ReadLine();
}
Как видите, сравнение двух объектов с применением хорошо знакомых операций ==
и ! = выглядит намного интуитивно понятней, чем вызов Object .Equals (). При
перегрузке операций эквивалентности для определенного класса помните, что С# требует,
чтобы в случае перегрузки операции == обязательно перегружалась также и операция
! = (компилятор напомнит, если вы забудете это сделать).
Перегрузка операций сравнения
В главе 9 было показано, как реализовать интерфейс I Comparable для выполнения
сравнений двух сходных объектов. Вдобавок для того же класса можно перегрузить
операции сравнения (<,>,<= и >=). Подобно операциям эквивалентности, С# и здесь требует,
чтобы в случае перегрузки операции < обязательно перегружалась также и операция >.
После перегрузки в классе Point этих операций сравнения пользователь объекта
сможет сравнивать объекты Point следующим образом:
// Использование перегруженных операций < и >.
static void Main(string[] args)
{
Console.WriteLine ("ptOne < ptTwo : {0}", ptOne < ptTwo);
Console.WriteLine ("ptOne > ptTwo : {0}", ptOne > ptTwo);
Console.ReadLine();
}
Предполагая, что интерфейс IComparable уже реализован, перегрузка операций
сравнения становится тривиальной. Ниже показано модифицированное определение
класса.
// Объекты Point также можно сравнивать с помощью операций сравнения.
public class Point : IComparable
{
public int CompareTo(object obj)
{
if (obj is Point)
{
Point p = (Point)obj;
if (this.X > p.X && this.Y > p.Y)
return 1;
if (this.X < p.X && this.Y < p.Y)
return -1;
else
return 0;
}
else
throw new ArgumentException ();
}
public static bool operator <(Point pi, Point p2)
{ return (pi.CompareTo(p2) < 0) ; }
public static bool operator >(Point pi, Point p2)
{ return (pi.CompareTo(p2) > 0) ; }
public static bool operator <= (Point pi, Point p2)
{ return (pi.CompareTo (p2) <= 0) ; }
public static bool operator >=(Point pi, Point p2)
{ return (pi.CompareTo (p2) >= 0) ; }
}
432 Часть III. Дополнительные конструкции программирования на С#
Внутреннее представление перегруженных операций
Подобно любому программному элементу С#, перегруженные операции имеют
специальное представление в синтаксисе CIL. Чтобы начать исследование того, что
происходит "за кулисами", откройте сборку OverloadedOps .exe в утилите ildasm.exe. Как
видно на рис. 12.1, перегруженные операции внутренне представлены в виде скрытых
методов (op_Addition (), op_Subtraction (), op_Equality () и т.д.).
ft НЛМу
File View Help
■ ToString : stringQ
■ get_X : int32()
■ get_Y : rt32()
; a|
Q op_Addbon : class OverloadedOps.Port(int32,class OverloadedOps.Port)
О op.AddJbon : dass OverloadedOps.Port(dass OverioadedOps.Port,class OverloadedOps.Port)
U op_Decrement: class OverloadedOps.Port(class OverloadedOps.Port)
; Q op_Equalty : booKdass OverloadedOps.Port,class OverloadedOps.Port)
! В op_GreaterThan : booKclass OverloadedOps.Port,dass OverloadedOps.Port)
В op.GreaterThanOrEqual: booKdass OverloadedOps.Port,class OverloadedOps.Port)
В opjncrement: dass OverbadedOps.Port(dass OverloadedOps.Port)
В opjnequafcty : booKdass OverloadedOps.Port,dass OverloadedOps.Port)
В op_LessThan : booKdass OverloadedOps.PoW,dass OverloadedOps.Port)
В op_LessThanOrEqual: booKdass OverloadedOps.Port,class OverloadedOps.Port)
В op.Subtracoon : dass OverloadedOps.Port(dass OverloadedOps.Port,class OverloadedOps.Port)
Hiiiiriii
assembly OverloadedOps
Рис. 12.1. В терминах CIL перегруженные операции отображаются на скрытые методы
Если посмотреть на инструкции CIL для метода opAddition (того, что принимает
два параметра Point), легко заметить, что компилятор также вставил модификатор
метода specialname:
.method public hidebysig specialname static
class OverloadedOps.Point
op_Addition (class OverloadedsOps.Point pi,
class OverloadedOps.Point p2) cil managed
{
}
В действительности любая операция, которую можно перегрузить, превращается в
специально именованный метод в коде CIL. В табл. 12.2 приведены отображения на CIL
для наиболее распространенных операций С#.
Таблица 12.2. Отображение операций С# на специально именованные методы CIL
Встроенная операция С#
Представление CIL
++
+
op_Decrement()
op_Increment()
op_Addition()
op_Subtraction()
op_Multiply()
op_Division()
op_Equality()
op_GreaterThan()
op_LessThan()
Глава 12. Расширенные средства языка С# 433
Окончание табл. 12.2
Встроенная операция С#
Представление CIL
i =
>=
<=
op_Inequality()
op_GreaterThanOrEqual()
op_LessThanOrEqual()
op_SubtractionAssignment()
op_AdditionAssignment()
Финальные соображения относительно перегрузки операций
Как уже было показано, С# предлагает возможность строить типы, которые могут
уникальным образом реагировать на различные встроенные, хорошо известные
операции. Теперь перед добавлением поддержки этого поведения в классы необходимо
убедиться в том, что операции, которые вы собираетесь перегружать, имеют хоть какой-то
смысл в реальном мире.
Например, предположим, что перегружена операция умножения для класса Mini Van
(минивэн). Что вообще должно означать перемножение двух объектов MiniVan? He
слишком много. Фактически, если коллеги по команде увидят следующее
использование объектов MiniVan, то будут весьма озадачены:
// Это не слишком понятно.. .
MiniVan newVan = myVan * yourVan;
Перегрузка операций обычно полезна только при построении служебных типов.
Строки, точки, прямоугольники, функции и шестиугольники — подходящие кандидаты
на перегрузку операций. Люди, менеджеры, автомобили, подключения к базе данных
и веб-страницы — нет. В качестве эмпирического правила: если перегруженная
операция затрудняет пользователю понимание функциональности типа, не делайте этого.
Используйте это средство с умом.
Также имейте в виду, что даже если вы не хотите перегружать операции в
специальных классах, это уже сделано в многочисленных типах из библиотек базовых классов.
Например, сборка System.Drawing.dll предлагает определение Point, применяемое
в Windows Forms, в котором перегружено множество операций. Обратите внимание на
значок операции в браузере объектов Visual Studio 2010 (рис. 12.2).
А
*fj ImageAnimator
^$ ImageConverter
*{$ ImageFormatConverter
jtf- KnownColor
^Pen
^Pens
+ Point
> C3 Base Types
!> СД Derived Types
-ty PointCorrverter
+ PointF
■■у Rectangle
L
-if, explicit operator(System.Drawing.Point)
Щ implicit operator(System.Drawing.Point)
Щ operator !=(System.Drawing.Point, System.Drawing.Point)
УЩ operator -(System.Drawing.Poirrt, System.Drawing.Size)
H7, operator = = (System.Drawing.Point System.Drawing.Pointj
Jf IsEmpty
J
public static System.Drawing.Point operator +
(System.Drawing.Point pt, System.Drawing.Size sz)
Member of System Drawing.Point
I Summery.
rJ Translates a System.Drawing.Poirrt by a given System.Drawing.Size.
■
Рис. 12.2. Множество типов в библиотеках базовых классов включают
уже перегруженные операции
434 Часть III. Дополнительные конструкции программирования на С#
Исходный код. Проект OverloadedOps доступен в подкаталоге Chapter 12.
Понятие преобразований пользовательских типов
Теперь обратимся к теме, близкой к перегрузке операций — преобразованию
пользовательских типов. Чтобы заложить фундамент для последующей дискуссии, давайте
кратко опишем нотацию явного и неявного преобразования между числовыми данными
и связанными с ними типами классов.
Числовые преобразования
В терминах встроенных числовых типов (sbyte, int, float и т.п.) явное
преобразование требуется при попытке сохранить большее значение в меньшем контейнере,
поскольку это может привести к потере данных. По сути, это означает, что вы говорите
компилятору: "Я знаю, что делаю". В противоположность этому неявное преобразование
происходит автоматически, когда вы пытаетесь поместить меньший тип в целевой тип,
и в результате этой операции не происходит потеря данных:
static void Main()
{
int a = 123;
long b = a; // Неявное преобразование int в long
int с = (int) b; // Явное преобразование long в int
}
Преобразования между связанными типами классов
Как было показано в главе 6, типы классов могут быть связаны классическим
наследованием (отношение "является" ("is а")). В этом случае процесс преобразования С#
позволяет выполнять приведение вверх и вниз по иерархии классов. Например, класс-
наследник всегда может быть неявно приведен к базовому типу. Однако если
необходимо хранить тип базового класса в переменной типа класса-наследника, понадобится
явное приведение:
// Два связанных типа классов,
class Basel}
class Derived : Basel}
class Program
I
static void Main(string [] args)
I
// Неявное приведение наследника к предку.
Base myBaseType;
myBaseType = new Derived();
// Для хранения базовой ссылке в ссылке
//на наследника нужно явное преобразование.
Derived myDerivedType = (Derived)myBaseType;
}
}
Это явное приведение работает благодаря тому факту, что классы Base и Derived
связаны отношением классического наследования. Однако что если есть два типа
классов из разных иерархий без общего предка (кроме System.Object), которые требуют
преобразования друг в друга? Если они не связаны классическим наследованием, явное
приведение здесь не поможет.
Глава 12. Расширенные средства языка С# 435
Рассмотрим типы значений, такие как структуры. Предположим, что определены
две структуры .NET с именами Square и Rectangle. Учитывая, что они не могут
полагаться на классическое наследование (поскольку всегда запечатаны), нет естественного
способа выполнить приведение между этими, на первый взгляд, связанными типами.
Наряду с возможностью создания в структурах вспомогательных методов (вроде
Rectangle.ToSquare ()), язык С# позволяет строить специальные процедуры
преобразования, которые позволят типам реагировать на операцию приведения (). Таким
образом, если корректно сконфигурировать эти структуры, можно будет использовать
следующий синтаксис для явного преобразования между ними:
// Преобразовать Rectangle в Square!
Rectangle rect;
rect.Width = 3;
rect.Height =10;
Square sq = (Square)rect;
Создание специальных процедур преобразования
Начнем с создания нового консольного приложения по имени CustomCconversions.
В С# предусмотрены два ключевых слова — explicit и implicit, которые можно
применять для управления реакцией на попытки выполнить преобразования. Предположим,
что имеются следующие определения классов:
public class Rectangle
{
public int Width {get; set;}
public int Height {get; set;}
public Rectangle (int w, int h)
{
Width = w; Height = h;
}
public Rectangle(){}
public void Draw()
{
for (int i = 0; l < Height; i++)
{
for (int j = 0; j < Width; j++)
{
Console.Write("*");
}
Console.WriteLine();
}
}
public override string ToStringO
{
return string.Format(" [Width = {0}; Height = {1}]",
Width, Height);
}
}
public class Square
{
public int Length {get; set;}
public Square(int 1)
{
Length = 1;
}
public Square () {}
436 Часть III. Дополнительные конструкции программирования на С#
public void Draw()
{
for (int i = 0; l < Length; i++)
{
for (int j = 0; j < Length; j++)
{
Console.Write("*");
}
Console.WriteLine();
}
}
public override string ToStringO
{ return string.Format("[Length = {0}]", Length); }
// Rectangle можно явно преобразовать в Square,
public static explicit operator Square(Rectangle r)
{
Square s = new Square();
s.Length = r.Height;
return s;
}
}
Обратите внимание, что эта итерация типа Squire определяет явную операцию
преобразования. Подобно процессу перегрузки операций, процедуры преобразования
используют ключевое слово operator в сочетании с ключевым словом explicit или
implicit и должны быть статическими. Входным параметром является сущность, из
которой выполняется преобразование, в то время как тип операции — сущность, к
которой оно производится.
В этом случае предполагается, что квадрат (геометрическая фигура с четырьмя
равными сторонами) может быть получен из высоты прямоугольника. Таким образом,
преобразовать Rectangle в Square можно следующим образом:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Conversions *****\nM);
// Создать Rectangle.
Rectangle r = new RectangleA5, 4);
Console.WriteLine(r.ToString());
r.Draw();
Console.WriteLine();
// Преобразовать г в Square на основе высоты Rectangle.
Square s = (Square)r;
Console.WriteLine(s.ToString());
s.Draw ();
Console.ReadLine();
}
Вывод этой программы показан на рис. 12.3.
Хотя, может быть, не слишком полезно преобразовывать Rectangle в Square в
пределах одного контекста, предположим, что есть функция, спроектированная так, чтобы
принимать параметров Square:
// Этот метод требует параметр типа Square.
static void DrawSquare(Square sq)
{
Console.WriteLine(sq.ToString());
sq.Draw();
Глава 12. Расширенные средства языка С# 437
Имея операцию явного преобразования в тип Square, можно передавать типы
Rectangle для обработки этому методу, используя явное приведение:
static void Main(string[] args)
{
// Преобразовать Rectangle в Square для вызова метода.
Rectangle rect = new Rectangle A0, 5) ;
DrawSquare((Square)rect);
Console.ReadLine();
}
as C\Windo*s\system32\cmd.exe 1
i***** рип w-jth Conversions
[Width = 15; Height = 4]
***************
***************
I***************
***************
■[Length = 4]
■****
■****
****
I****
■Press any key to continue .
in
■
Ь^ЁУи^яГ
*****
- - -
_^»
-
j
Рис. 12.3. Преобразование Rectangle в Square
Дополнительные явные преобразования типа Square
Теперь, когда можно явно преобразовывать объекты Rectangle в объекты Square,
давайте рассмотрим несколько дополнительных явных преобразований. Учитывая, что
квадрат симметричен по всем сторонам, может быть полезно предусмотреть процедуру
преобразования, которая позволит вызывающему коду привести целочисленный тип к
типу Square (который, разумеется, будет иметь длину стороны, равную переданному
целому). Аналогично, что если вы захотите модифицировать Square так, чтобы
вызывающий код мог выполнять приведение из Square в System. Int32? Логика вызова
выглядит следующим образом.
static void Main(string[] args)
{
// Преобразование int в Square.
Square sq2 = (Square)90;
Console.WriteLine(Msq2 = {0}", sq2);
// Преобразование Square в int.
int side = (int)sq2;
Console.WriteLine("Side length of sq2 = {0}", side);
Console.ReadLine();
}
Ниже показаны необходимые изменения в классе Square:
public class Square
{
public static explicit operator Square(int sideLength)
{
438 Часть III. Дополнительные конструкции программирования на С#
Square newSq = new Square();
newSq.Length = sideLength;
return newSq;
}
public static explicit operator int (Square s)
{return s.Length;}
}
По правде говоря, преобразование Square в int может показаться не слишком
интуитивно понятной (или полезной) операцией. Однако это указывает на один очень
важный факт, касающийся процедур пользовательских преобразований: компилятор не
волнует, что и куда преобразуется, до тех пор, пока пишется синтаксически
корректный код. Таким образом, как и с перегруженными операциями, возможность создания
операций явного приведения еще не означает, что вы обязаны их создавать. Обычно
эта техника наиболее полезна при создании типов структур .NET, учитывая, что они не
могут участвовать в отношениях классического наследования (где приведение
достается бесплатно).
Определение процедур неявного преобразования
До сих пор вы создавали различные пользовательские операции явного
преобразования. Однако что, если понадобится неявное преобразование?
static void Main(string[] args)
{
// Попытка выполнить неявное приведение?
Square s3 = new Square ();
s3.Length =83;
Rectangle rect2 = s3;
Console.ReadLine();
}
Этот код не скомпилируется, если для типа Rectangle не будет предусмотрена
процедура неявного преобразования. Ловушка здесь вот в чем: не допускается иметь
одновременно функции явного и неявного преобразования, если они не отличаются по типу
возвращаемого значения или списку параметров. Это может показаться ограничением,
однако вторая ловушка состоит в том, что когда тип определяет процедуру неявного
преобразования, никто не запретит вызывающему коду использовать синтаксис явного
приведения!
Запутались? Для того чтобы прояснить ситуацию, давайте добавим к классу
Rectangle процедуру неявного преобразования, используя для этого ключевое слово
implicit (обратите внимание, что в следующем коде предполагается, что ширина
результирующего Rectangle вычисляется умножением стороны Square на 2):
public class Rectangle
{
public static implicit operator Rectangle(Square s)
{
Rectangle r = new Rectangle ();
r.Height = s.Length;
// Предположим, что длина нового Rectangle
// будет равна (Length x 2)
г.Width = s.Length * 2;
return r;
}
}
Глава 12. Расширенные средства языка С# 439
После такой модификации можно будет выполнять преобразование между типами:
static void Main(string [ ] args)
{
// Неявное преобразование работает!
Square s3 = new Square ();
s3.Length = 7;
Rectangle rect2 = s3;
Console.WriteLine("rect2 = {0}", rect2);
DrawSquare(s3);
// Синтаксис явного преобразования также работает!
Square s4 = new Square ();
s4.Length =3; ■
Rectangle rect3 = (Rectangle)s4;
Console.WriteLine("rect3 = {0}", rect3);
Console.ReadLine() ;
}
Внутреннее представление процедур
пользовательских преобразований
Подобно перегруженным операциям, методы, квалифицированные ключевыми
словами implicit или explicit, имеют специальные имена в терминах CIL: oplmplicit
и opExplicit, соответственно (рис. 12.4).
р НЛМу Ex>ki\C* Вох£* and the ,NF Г atform 5th ed'circt
Dr»«SChapter_12\Code\CustomC..
File i/iew Help
й Щ CustomConverstons
Ш £ CustomConverstons.Program
* Щ. CustomConverstons.Rectangle
й Щ- CustomConverstons.Square
► .class public auto ansi beforefieldinit
v <Length>k_BacttngFtoW : private W32
■ ,ctor : votoX)
■ .ctor : votd(nt32)
■ Draw: votoX)
■ ToString: strinoX)
Щ get Length : jnt32Q
аГ
^
(J op_Explclt: dass CustomConverstons.Square(int32)
Ё1 op_Expkit: mt32(class CustomConverstons.Square)
■ set .Length : void(Jnt32)
A Length : instance int32()
мэдгштшитта
■?1Й|1М1
•assembly CustomConverstons
Рис. 12.4. Представление CIL определяемых пользователем процедур преобразования
На заметку! В браузере объектов Visual Studio 2010 операции пользовательских преобразований
отображаются с использованием значков "явная операция" и " неявная операция".
На этом рассмотрение определений операций пользовательского преобразования
завершено. Как и с перегруженными операциями, здесь следует помнить, что данный
фрагмент синтаксиса представляет собой просто сокращенное обозначение
"нормальных" функций-членов, и в этом смысле является необязательным. Однако в случае
правильного применения пользовательские структуры могут использоваться более
естественно, поскольку трактуются как настоящие типы классов, связанные наследованием.
Исходный код. Проект CustomConversions доступен в подкаталоге Chapter 12.
440 Часть III. Дополнительные конструкции программирования на С#
Понятие расширяющих методов
В .NET 3.5 появилась концепция расширяющих методов (extension method), которая
позволила добавлять новую функциональность к предварительно скомпилированным
типам "на лету". Известно, что как только тип определен и скомпилирован в сборку
.NET, его определение становится более-менее окончательным. Единственный способ
добавления новых членов, обновления или удаления членов состоит в
перекодировании и перекомпиляции кодовой базы в обновленную сборку (или же можно прибегнуть
к более радикальным мерам, таким как использование пространства имен System.
Reflection.Emit для динамического изменения скомпилированного типа в памяти).
Теперь в С# можно определять расширяющие методы. Суть расширяющих методов
в том, что они позволяют существующим скомпилированным типам (а именно —
классам, структурам или реализациям интерфейсов), а также типам, которые в данный
момент компилируются (такие как типы в проекте, содержащем расширяющие методы),
получать новую функциональность без необходимости в непосредственном изменении
расширяемого типа.
Эта техника может оказаться полезной, когда нужно внедрить новую
функциональность в типы, исходный код которых не доступен. Также она может пригодиться, когда
необходимо заставить тип поддерживать набор членов (в интересах полиморфизма),
но вы не можете модифицировать его исходное объявление. Механизм расширяющих
методов позволяет добавлять функциональность к предварительно скомпилированным
типам, создавая иллюзию, что она была у него всегда.
На заметку! Имейте в виду, что расширяющие методы на самом деле не изменяют
скомпилированную кодовую базу! Эта техника лишь добавляет члены к типу в контексте текущего
приложения.
При определении расширяющих методов первое ограничение состоит в том, что
они должны быть определены внутри статического класса (см. главу 5), и потому
каждый расширяющий метод должен быть объявлен с ключевым словом static. Второй
момент состоит в том, что все расширяющие методы помечаются таковыми
посредством ключевого слова this в виде модификатора первого (и только первого) параметра
данного метода. Третий момент — каждый расширяющий метод может быть вызван
либо от текущего экземпляра в памяти, либо статически, через определенный
статический класс! Звучит странно? Давайте рассмотрим полный пример, чтобы прояснить
картину.
Определение расширяющих методов
Создадим новое консольное приложение по имени ExtensionMethods. Теперь
предположим, что строится новый служебный класс по имени MyExtensions, в котором
определены два расширяющих метода. Первый позволяет любому объекту из библиотек
базовых классов .NET получить новый метод по имени DisplayDef iningAssembly (),
который использует типы из пространства имен System.Reflection для отображения
сборки указанного типа.
На заметку! API-интерфейс рефлексии формально рассматривается в главе 15. Если эта тема
является новой, просто знайте, что рефлексия позволяет исследовать структуру сборок, типов и
членов типов во время выполнения.
Второй расширяющий метод по имени ReverseDigits () позволяет любому
экземпляру System. Int32 получить новую версию себя, но с обратным порядком следования
Глава 12. Расширенные средства языка С# 441
цифр. Например, если на целом значении 1234 вызвать ReverseDigits (),
возвращенное целое значение будет равно 4 321. Взгляните на следующую реализацию класса (не
забудьте импортировать пространство имен System.Reflection):
static class MyExtensions
{
// Этот метод позволяет любому объекту отобразить
// сборку, в которой он определен.
public static void DisplayDefiningAssembly(this object obj)
{
Console.WriteLine("{0} lives here: => {l}\n", ob].GetType().Name,
Assembly.GetAssembly(ob].GetType()).GetName().Name);
}
// Этот метод позволяет любому целому изменить порядок следования
// десятичных цифр на обратный. Например, 56 превратится в 65.
public static int ReverseDigits(this int i)
{
// Транслировать int в string и затем получить все его символы.
char[] digits = i.ToString() .ToCharArray ();
// Изменить порядок элементов массива.
Array.Reverse(digits);
// Вставить обратно в строку.
string newDigits = new string(digits);
// Вернуть модифицированную строку как int.
return int.Parse(newDigits) ;
}
}
Обратите внимание, что первый параметр каждого расширяющего метода
квалифицирован ключевым словом this, перед определением типа параметра. Первый
параметр расширяющего метода всегда представляет расширяемый тип. Учитывая, что
DisplayDef iningAssembly () прототипирован расширять System. Ob j ect, любой тип в
любой сборке теперь получает этот новый член. Однако ReverseDigits ()
прототипирован только для расширения целочисленных типов, и потому если что-то другое
попытается вызвать этот метод, возникнет ошибка времени компиляции.
Знайте, что каждый расширяющий метод может иметь множество параметров, но
только первый параметр может быть квалифицирован как this. Например, вот как
выглядит перегруженный расширяющий метод, определенный в другом служебном классе
по имени TestUtilClass:
static class TesterUtilClass
{
// Каждый Int32 теперь имеет метод Foo() . . .
public static void Foo(this int i)
{ Console.WriteLine ("{0} called the Foo() method.", i); }
// ...который перегружен для приема параметра string!
public static void Foo(this int i, string msg)
{ Console.WriteLine ("{0} called Foo() and told me: {1}", i, msg); }
}
Вызов расширяющих методов на уровне экземпляра
После определения этих расширяющих методов теперь все объекты (в том числе,
конечно же, все содержимое библиотек базовых классов .NET) имеют метод по имени
DisplayDef iningAssembly () , в то время как типы System. Int32 (и только целые) —
методы ReverseDigits () и Foo ():
442 Часть III. Дополнительные конструкции программирования на С#
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Extension Methods *****\n");
// В int появилась новая идентичность1
int mylnt = 12345678;
mylnt.DisplayDefiningAssembly();
// To же и у DataSet!
System.Data.DataSet d = new System.Data.DataSet ();
d.DisplayDefiningAssembly();
// И у SoundPlayerl
System.Media.SoundPlayer sp = new System.Media.SoundPlayer();
sp.DisplayDefiningAssembly();
// Использовать новую функциональность int.
Console.WriteLine("Value of mylnt: {0}", mylnt);
Console.WriteLine("Reversed digits of mylnt: {0}", mylnt.ReverseDigits()) ;
mylnt.Foo();
mylnt.Foo("Ints that Foo? Who would have thought it!");
bool b2 = true;
// Ошибка I Booleans не имеет метода Foo() I
// b2.Foo() ;
Console.ReadLine();
}
Ниже показан вывод этой программы:
••••• Fun with Extension Methods *****
Int32 lives here: => mscorlib
DataSet lives here: => System.Data
SoundPlayer lives here: => System
Value of mylnt: 12345678
Reversed digits of mylnt: 87654321
12345678 called the Foo() method.
12345678 called Foo () and told me: Ints that Foo? Who would have thought it!
Вызов расширяющих методов статически
Вспомните, что первый параметр расширяющего метода помечен ключевым словом
this, а за ним следует тип элемента, к которому метод применяется. Если вы
посмотрите, что происходит "за кулисами" (с помощью инструмента вроде ildasm.exe), то
обнаружите, что компилятор просто вызывает "нормальный" статический метод, передавая
переменную, на которой вызывается метод, в первом параметре (т.е. в качестве
значения this). Ниже показаны примерные подстановки кода.
private static void Main(string[] args)
{
Console.WriteLine("***** Fun with Extension Methods *****\n");
int mylnt = 12345678;
MyExtensions.DisplayDefiningAssembly(mylnt);
System.Data.DataSet d = new DataSet();
MyExtensions.DisplayDefiningAssembly(d);
System.Media.SoundPlayer sp = new SoundPlayer();
MyExtensions.DisplayDefiningAssembly(sp);
Console.WriteLine("Value of mylnt: {0}", mylnt);
Console.WriteLine("Reversed digits of mylnt: {0}",
MyExtensions.ReverseDigits(mylnt));
TesterUtilClass.Foo(mylnt);
TesterUtilClass.Foo(mylnt, "Ints that Foo? Who would have thought it!");
Console.ReadLine();
}
Глава 12. Расширенные средства языка С# 443
Учитывая, что вызов расширяющего метода из объекта (что похоже на вызов метода
уровня экземпляра) — это просто эффект "дымовой завесы", создаваемый
компилятором, расширяющие методы всегда можно вызвать как нормальные статические методы,
используя привычный синтаксис С# (как показано выше).
Контекст расширяющего метода
Как только что объяснялось, расширяющие методы — это, по сути, статические
методы, которые могут быть вызваны от экземпляра расширяемого типа. Поскольку
это — разновидность синтаксического "украшения", важно понимать, что в отличие от
"нормального" метода, расширяющий метод не имеет прямого доступа к членам типа,
который он расширяет. Иначе говоря, расширение — это не наследование. Взгляните на
следующий простой тип Саг:
public class Car
{
public int Speed;
public int SpeedUp ()
{
return ++Speed;
}
}
Построив расширяющий метод для типа Car по имени SlowDown (), вы не
получите прямого доступа к членам Саг внутри контекста расширяющего метода, поскольку
это не является классическим наследованием. Таким образом, следующий код вызовет
ошибку компиляции:
public static class CarExtensions
{
public static int SlowDown(this Car c)
{
// Ошибка! Этот метод не унаследован от Саг!
return --Speed/
}
}
Проблема в том, что расширяющий метод SlowDown () пытается обратиться к
полю Speed типа Саг. Однако поскольку SlowDown () — статический член класса
CarExtension, в его контексте отсутствует Speed!
Тем не менее, допустимо использовать параметр, квалифицированный словом this,
для обращения к общедоступным (и только общедоступным) членам расширяемого типа.
Таким образом, следующий код успешно скомпилируется, как и следовало ожидать:
public static class CarExtensions
{
public static int SlowDown(this Car c)
{
// Скомпилируется успешно!
return --с.Speed;
}
}
Теперь можно создать объект Car и вызывать методы SpeedUp () и SlowDown (), как
показано ниже:
static void UseCarO
{
Car с = new Car();
Console.WriteLine("Speed: {0}", с.SpeedUp());
Console.WriteLine("Speed: {0}", с.SlowDown ());
}
444 Часть III. Дополнительные конструкции программирования на С#
Импорт типов, которые определяют расширяющие методы
В случае выделения набора статических классов, содержащих расширяющие
методы, в уникальное пространство имен другие пространства имен в этой сборке
используют стандартное ключевое слово using для импорта не только самих статических
классов, но также и каждого из поддерживаемых расширяющих методов. Об этом следует
помнить, поскольку если не импортировать явно корректное пространство имен, то
расширяющие методы будут недоступны в таком файле кода С#. Хотя на первый взгляд
может показаться, что расширяющие методы глобальны по своей природе, на самом деле
они ограничены пространствами имен, в которых они определены, или пространствами
имен, которые их импортируют. Таким образом, если поместить определения
рассматриваемых статических классов (MyExtensions, TesterUtilClass и CarExtensions) в
пространство имен MyExtensionMethods, как показано ниже:
namespace MyExtensionMethods
{
static class MyExtensions
static class TesterUtilClass
static class CarExtensions
}
то другие пространства имен в проекте должны явно импортировать пространство
MyExtensionMethods для получения расширяющих методов, определенных этими
типами. Поэтому следующий код вызовет ошибку во время компиляции:
// Единственная директива using,
using System;
namespace MyNewApp
{
class JustATest
{
void SomeMethod()
{
// Ошибка! Для расширения int методом Foo() необходимо
// импортировать пространство имен MyExtensionMethods!
int i=0;
i.Foo ();
}
}
}
Поддержка расширяющих методов средством IntelliSense
Учитывая тот факт, что расширяющие методы не определены буквально на
расширяемом типе, при чтении кода есть шансы запутаться. Например, предположим, что
имеется импортированное пространство имен, в котором определено несколько
расширяющих методов, написанных кем-то из команды разработчиков. При написании
своего кода вы создаете переменную расширенного типа, применяете операцию точки
и обнаруживаете десятки новых методов, которые не являются членами исходного
определения класса!
Глава 12. Расширенные средства языка С# 445
К счастью, средство IntelliSense в Visual Studio маркирует все расширяющие методы
уникальным значком с изображением синей стрелки вниз (рис. 12.5).
..;ExtensionMethods. Program
^UseCerQ
1
}
static void UseCar()
{
Car с ■ new Car()j
100% '] 4
♦ Equals
♦ GetHashCode
♦ GetType
♦^ SlowDown
V Speed
V SpeedUp
♦ ToString
(extension) void objectDisplayDef iningAssemblyO I
Рис. 12.5. Отображение расширяющих методов в IntelliSense
Если метод помечен этим значком, это означает, что он определен вне исходного
определения класса, через механизм расширяющих методов.
Исходный код. Проект ExtensionMethods доступен в подкаталоге Chapter 12.
Построение и использование библиотек расширений
В предыдущем примере производилось расширение функциональности различных
типов (таких как System. Int32) для использования в текущем консольном
приложении. Представьте, насколько было бы полезно построить библиотеку кода .NET,
определяющую расширения, на которые могли бы ссылаться многие приложения. К счастью,
сделать это очень легко.
Подробности создания и конфигурирования специальных библиотек будут
рассматриваться в главе 14; а пока, если хотите реализовать самостоятельно приведенный здесь
пример, создайте проект библиотеки классов по имени MyExtensionLibrary. Затем
переименуйте начальный файл кода С# на MyExtensions . cs и скопируйте определение
класса MyExtensions в новое пространство имен:
namespace MyExtensionsLibrary
{
// Не забудьте импортировать System.Reflection!
public static class MyExtensions
{
// Та же реализация, что и раньше.
public static void DisplayDefiningAssembly(this object obj)
{...}
// Та же реализация, что и раньше.
public static int ReverseDigits(this int 1)
}
На заметку! Чтобы можно было экспортировать расширяющие методы из библиотеки кода .NET,
определяющий их тип должен быть объявлен с ключевым словом public (вспомните, что по
умолчанию действует модификатор доступа internal).
446 Часть III. Дополнительные конструкции программирования на С#
После этого можно скомпилировать библиотеку и ссылаться на сборку
MyExtensionsLibrary.dll внутри новых проектов .NET. Это позволит использовать
новую функциональность System.Object и System. Int32 в любом приложении,
которое ссылается на библиотеку.
Чтобы проверить сказанное, создадим новый проект консольного
приложения (по имени MyExtensionsLibraryClient) и добавим к нему ссылку на сборку
MyExtensionsLibrary.dll. В начальном файле кода укажем на использование
пространства имен MyExtensionsLibrary и напишем простой код, который вызывает эти
новые методы на локальном значении int:
using System;
// Импортируем наше специальное пространство имен,
using MyExtensionsLibrary;
namespace MyExtensionsLibraryClient
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** Using Library with Extensions *****\n");
// Теперь эти расширяющие методы определены внутри внешней
// библиотеки классов .NET.
int mylnt = 987;
mylnt.DisplayDefiningAssembly() ;
Console.WriteLine("{0} is reversed to {1}",
mylnt, mylnt.ReverseDigits()) ;
Console.ReadLine();
}
}
}
В Microsoft рекомендуют размещать типы, которые имеют расширяющие методы, в
отдельной сборке (внутри выделенного пространства имен). Причина проста —
сокращение сложности программной среды. Например, если вы напишете базовую
библиотеку для внутреннего использования в компании, причем в корневом пространстве имен
этой библиотеки определено 30 расширяющих методов, то в конечном итоге все
приложения будут видеть эти методы в списках IntelliSense (даже если они и не нужны).
Исходный код. Проекты MyExtensionsLibrary и MyExtensionsLibraryClient доступны
в подкаталоге Chapter 12.
Расширение интерфейсных типов через расширяющие методы
Итак, было показано, каким образом расширять классы (а также структуры, которые
следуют тому же синтаксису) новой функциональностью через расширяющие методы.
Чтобы завершить исследование расширяющих методов С#, следует отметить, что
новыми методами можно также расширять и интерфейсные типы; однако семантика такого
действия определенно несколько отличается от того, что можно было бы ожидать.
Создадим новое консольное приложение по имени Interf aceExtensions, а в нем —
простой интерфейсный тип (IBasicMath), включающий единственный метод по
имени Add (). Затем подходящим образом реализуем этот интерфейс в каком-нибудь типе
класса (MyCalc). Например:
// Определение обычного интерфейса на С#.
interface IBasicMath
{
Глава 12. Расширенные средства языка С# 447
int Add(int x, int у) ;
}
// Реализация IBasicMath.
class MyCalc : IBasicMath
{
public int "Add (int x, int y)
{
return x + y;
}
}
Теперь предположим, что доступ к коду с определением IBasicMath отсутствует, но
к нему нужно добавить новый член (например, метод вычитания), чтобы расширить его
поведение. Можно попробовать написать следующий распшряющий класс:
static class MathExtensions
{
// Расширить IBasicMath методом вычитания?
public static int Subtract(this IBasicMath ltf,
int x, int y);
}
Однако такой код вызовет ошибку во время компиляции. В случае расширения
интерфейса новыми членами должна также предоставляться реализация этих членов!
Это кажется противоречащим самой идее интерфейсных типов, поскольку интерфейсы
не включают реализации, а только определения. Тем не менее, класс MathExtensions
должен быть определен следующим образом:
static class MathExtensions
{
// Расширить IBasicMath этим методом с этой реализацией.
public static int Subtract (this IBasicMath itf, int x, int y)
{
return x - y;
Теперь может показаться, что допустимо создать переменную типу IBasicMath и
непосредственно вызвать Substract (). Опять-таки, если бы такое было возможно (а на
самом деле нет), то это нарушило бы природу интерфейсных типов .NET. На самом деле
приведенный код говорит вот что: "Любой класс в моем проекте, реализующий
интерфейс IBasicMath, теперь имеет метод Substract (), реализованный представленным
образом". Как и раньше, все базовые правила соблюдаются, а потому пространство
имен, определяющее MyCalc, должно иметь доступ к пространству имен,
определяющему MathExtensions. Рассмотрим следующий метод Main ():
static void Main(string[] args)
{
Console.WriteLine("***** Extending an interface *****\n");
// Вызов членов IBasicMath из объекта MyCalc.
MyCalc с = new MyCalc ();
Console.WriteLine ( + 2 = {0}", c.Addd, 2) ) ;
Console.WriteLine( - 2 = {0}", с.SubtractA, 2) ) ;
// Для вызова расширения можно выполнить приведение к IBasicMath.
Console.WriteLine(0 - 9 = {0}", ((IBasicMath)с) .Subtract C0, 9));
// Это не будет работать!
// IBasicMath ltfBM = new IBasicMath();
// ltfBM.SubtractA0, 10);
Console.ReadLine();
448 Часть III. Дополнительные конструкции программирования на С#
На этом исследование расширяющих методов С# завершено. Помните, что это
конкретное языковое средство может быть очень полезным, когда нужно расширить
функциональность типа, даже если нет доступа к первоначальному исходному коду (или
если тип запечатан), в целях поддержания полиморфизма. Во многом подобно неявно
типизированным локальным переменным, расширяющие методы являются ключевым
элементом работы с API-интерфейсом LINQ. Как будет показано в следующей главе,
множество существующих типов в библиотеках базовых классов расширены новой
функциональностью через расширяющие методы, что позволяет им интегрироваться в
программную модель LINQ.
Исходный код. Проект InterfaceExtension доступен в подкаталоге Chapter 12.
Понятие частичных методов
Начиная с версии .NET 2.0, строить определения частичных классов стало
возможно с использованием ключевого слова partial (см. главу 5). Вспомните, что эта деталь
синтаксиса позволяет разбивать полную реализацию типа на несколько файлов кода
(или других мест, таких как память). До тех пор, пока каждый аспект частичного типа
имеет одно полностью квалифицированное имя, конечным результатом будет
"нормальный" скомпилированный класс, находящийся в созданной компилятором сборке.
Язык С# расширяет роль ключевого слова partial, позволяя его применять на
уровне метода. По сути, это дает возможность прототипировать метод в одном файле, а
реализовать в другом. При наличии опыта работы в C++, это может напомнить отношения
между файлами заголовков и реализаций C++. Тем не менее, частичные методы С#
обладают рядом важных ограничений.
• Частичные методы могут определяться только внутри частичного класса.
• Частичные методы должны возвращать void.
• Частичные методы могут быть статическими или методами экземпляра.
• Частичные методы могут иметь аргументы (включая параметры с
модификаторами this, ref или params, но не с out).
• Частичные методы всегда неявно приватные (private).
Еще более странным является тот факт, что частичный метод может быть как
помещен, так и не помещен в скомпилированную сборку! Для прояснения картины давайте
рассмотрим пример.
Первый взгляд на частичные методы
Для оценки влияния определения частичного метода создадим проект
консольного приложения по имени PartialMethods. Затем определим новый класс CarLocator
внутри файла С# по имени CarLocator. cs:
// CarLocator.es
partial class CarLocator
{
// Этот член всегда будет частью класса CarLocator.
public bool CarAvailablelnZipCode (string zipCode)
{
// Этот вызов *может* быть частью реализации данного метода.
VerifyDuplicates(zipCode);
// Некоторая интересная логика взаимодействия с базой данных...
return true;
}
Глава 12. Расширенные средства языка С# 449
// Этот член *может* быть частью класса CarLocator!
partial void VerifyDuplicates(string make);
}
Обратите внимание, что метод Verif yDuplicates () определен с
модификатором partial и не имеет определения тела внутри этого файла. Кроме того,
метод CarAvailablelnZipCode () содержит вызов Verif yDuplicates () внутри своей
реализации.
Скомпилировав это приложение в таком, как оно есть виде, и открыв
скомпилированную сборку в утилите ildasm.exe или refatcor.exe, вы не обнаружите там
никаких следов Verif yDuplicates () в классе CarLocator, равно как никаких вызовов
Verif yDuplicates () внутри CarAvailablelnZipCode ()! С точки зрения компилятора
в данном проекте класс CarLocator определен в следующем виде:
internal class CarLocator
{
public bool CarAvailablelnZipCode(string zipCode)
{
return true;
}
}
Причина столь странного усечения кода связана с тем, что частичный метод
Verif yDuplicates () не имеет реальной реализации. Добавив в проект новый файл
(например, CarLocatorlmpl .cs) с определением остальной порции частичного метода:
// CarLocatorlmpl.cs
partial class CarLocator
{
partial void VerifyDuplicates(string make)
{
// Assume some expensive data validation
// takes place here.. .
}
}
вы обнаружите, что во время компиляции будет принят во внимание полный комплект
класса CarLocator, как показано в следующем примерном коде С#:
internal class CarLocator
{
public bool CarAvailablelnZipCode(string zipCode)
{
this.VerifyDuplicates(zipCode);
return true;
}
private void VerifyDuplicates(string make)
{
}
}
Как видите, когда метод определен с ключевым словом partial, то компилятор
принимает решение о том, нужно ли его включить в сборку, в зависимости от
наличия у этого метода тела или же просто пустой сигнатуры. Если у метода нет тела, все
его упоминания (вызовы, описания метаданных, прототипы) на этапе компиляции
отбрасываются.
В некоторых отношениях частичные методы С# — это строго типизированная
версия условной компиляции кода (через директивы препроцессора #if, #elif и #endif).
Однако основное отличие состоит в том, что частичный метод будет полностью проиг-
450 Часть III. Дополнительные конструкции программирования на С#
норирован во время компиляции (независимо от настроек сборки), если отсутствует его
соответствующая реализация.
Использование частичных методов
Учитывая ограничения, присущие частичным методам, самое важное из которых
связано с тем, что они должны быть неявно private и всегда возвращать void, сразу
представить множество полезных применений этого средства языка может быть
трудно. По правде говоря, из всех языковых средств С# частичные методы кажутся
наименее востребованными.
В текущем примере метод Verif yDuplicates () помечен как частичный в
демонстрационных целях. Однако предположим, что этот метод, будучи реализованным,
выполняет некоторые очень интенсивные вычисления.
Снабжение этого метода модификатором partial дает возможность другим
разработчикам классов создавать детали реализации по своему усмотрению. В данном случае
частичные методы предоставляют более ясное решение, чем применение директив
препроцессора, поддерживая "фиктивные" реализации виртуальных методов либо
генерируя объекты исключений NotlmplementedException.
Наиболее распространенным применением этого синтаксиса является определение
так называемых легковесных событий. Эта техника позволяет проектировщикам
классов представлять привязки для методов, подобно обработчикам событий, относительно
которых разработчики могут самостоятельно решать — реализовывать их или нет. В
соответствии с принятым соглашением, имена таких методов-обработчиков легковесных
событий имеют префикс On. Например:
// CarLocator.EventHandler.cs
partial class CarLocator
{
public bool CarAvailablelnZipCode(string zipCode)
{
OnZipCodeLookup(zipCode);
return true;
>
// Обработчик "легковесного" события,
partial void OnZipCodeLookup(string make);
}
Если разработчик класса пожелает получать уведомления о вызове метода
CarAvailablelnZipCode (), он может предоставить реализацию метода OnZipCodeLookup ().
В противном случае ничего делать не потребуется.
Исходный код. Проект PartialMethods доступен в подкаталоге Chapter 12.
Понятие анонимных типов
Как объектно-ориентированный программист, вы знаете преимущества классов
в отношении представления состояния и функциональности заданной программной
сущности. То есть, когда нужно определить класс, который предполагает многократное
использование и предоставляет обширную функциональность через набор методов,
событий, свойств и специальных конструкторов, то разработка нового класса С# является
общепринятой и зачастую обязательной практикой.
Глава 12. Расширенные средства языка С# 451
Однако есть и другие случаи, когда может понадобиться определить класс просто
для моделирования набора инкапсулированных (и каким-то образом связанных)
элементов данных, без ассоциированных с ними методов, событий или другой специальной
функциональности. Более того, что если этот тип будет использоваться только внутри
текущего приложения, и не предназначен для многократного применения? Для такого
"временного" типа ранние версии С# все равно требовали построения вручную нового
определения класса:
internal class SomeClass
{
// Определить набор приватных переменных-членов...
// Создать свойство для каждой приватной переменной. . .
// Переопределить ToStringO для учета каждой переменной-члена...
// Переопределить GetHashCode() и Equals () для работы
// с эквивалентностью на основе значений.. .
}
Хотя само по себе построение такого класса — не сверхсложная задача, но
инкапсуляция изрядного количества членов часто оказывается довольно трудоемкой (хотя
здесь в некоторой степени помогают автоматические свойства). Теперь доступно
замечательное сокращение для подобных ситуаций (анонимные типы), которое во многих
отношениях является естественным расширением синтаксиса анонимных методов С#
(см. главу 11).
Анонимный тип определяется с использованием ключевого слова var (см. главу 3)
в сочетании с синтаксисом инициализации объекта (см. главу 5). Для иллюстрации
создадим новое консольное приложение по имени AnonymousTypes. Модифицируем
метод Main (), добавив следующий анонимный класс, который моделирует простой тип
автомобиля:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Anonymous Types *****\n");
// Создать анонимный тип, представляющий автомобиль.
var myCar = new { Color = "Bright Pink", Make = "Saab", CurrentSpeed =55 };
// Вывести на консоль цвет и производителя.
Console.WriteLine("My car is a {0} {1}.", myCar.Color, myCar.Make);
Console.ReadLine();
}
Обратите внимание, что переменная myCar должна быть типизирована неявно, что
вполне разумно, поскольку мы не моделируем концепцию автомобиля с
использованием строго типизированного определения класса. Во время компиляции компилятор С#
автоматически сгенерирует уникально именованный класс. Учитывая тот факт, что это
имя класса невидимо в коде С#, применение неявной типизации посредством
ключевого слова var обязательно.
Кроме того, также нужно указать (с помощью синтаксиса инициализации
объектов) набор свойств, моделирующих данные, которые должны быть инкапсулированы.
Однажды определенные, эти значения могут быть получены с применением
стандартного синтаксиса вызова свойств С#.
Внутреннее представление анонимных типов
Все анонимные типы автоматически наследуются от System.Object и потому
поддерживают все члены, предоставленные этим базовым классом. Учитывая это, можно
вызывать ToString () , GetHashCode () , Equals () или GetType () на неявно
типизированном объекте myCar. Предположим, что в классе Program определена следующая
статическая вспомогательная функция:
452 Часть III. Дополнительные конструкции программирования на С#
static void ReflectOverAnonymousType(object obj)
{
Console.WriteLine("obj is an instance of: {0}", obj.GetType().Name);
Console.WriteLine("Base class of {0} is {1}",
Ob] . GetType () . Name, ob;j . GetType () . BaseType) ;
Console.WriteLine("obj.ToStringO = {0}", ob].ToString());
Console.WriteLine("obj.GetHashCode() = {0}", obj.GetHashCode() ) ;
Console.WriteLine();
}
Теперь вызовем этот метод в Main(), передав ему объект myCar в качестве
параметра:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Anonymous Types *****\n");
// Создать анонимный тип, представляющий автомобиль.
var myCar = new {Color = "Bright Pink", Make = "Saab", CurrentSpeed = 55};
// Отобразить то, что сгенерировал компилятор.
ReflectOverAnonymousType(myCar);
Console.ReadLine();
}
Вывод будет выглядеть примерно так:
••••• pun W1th Anonymous Types *****
obj is an instance of: of AnonymousTypeOv3
Base class of of AnonymousTypeO ч 3 is System. Object
ob] .ToString () = { Color = Bright Pink, Make = Saab, CurrentSpeed = 55 }
obj.GetHashCode () =2038548792
Прежде всего, обратите внимание, что объект myCar имеет тип of
AnonymousTypeO ч 3 (конкретное имя типа может отличаться). Помните, что назначаемое
типу имя полностью определяется компилятором и напрямую в коде С# недоступно.
Возможно, наиболее важно здесь то, что каждая пара "имя/значение", определенная с
использованием синтаксиса инициализации объектов, отображается на идентично
именованное свойство, доступное только для чтения, и соответствующее приватное поле
заднего плана, также предназначенное только для чтения. Следующий код С# примерно
отражает сгенерированный компилятором класс, используемый для представления объекта
myCar (который можно увидеть с помощью утилиты reflector. ехе или ildasm. exe):
internal sealed class of AnonymousTypeO<<Color>] TPar,
<Make>j TPar, <CurrentSpeedy TPar>
{
// Поля только для чтения
private readonly <Color>] TPar <Color>i Field;
private readonly <CurrentSpeed>] TPar <CurrentSpeed>i Field;
private readonly <Make>j TPar <Make>i Field;
// Конструктор по умолчанию
public of AnonymousTypeO (<Color>] TPar Color,
<Make>] TPar Make, <CurrentSpeed>j TPar CurrentSpeed);
// Переопределенные методы
public override bool Equals(object value);
public override int GetHashCode();
public override string ToStringO;
// Свойства только для чтения
public <Color>] TPar Color { get; }
public <CurrentSpeed>j TPar CurrentSpeed { get; }
public <Make>j TPar Make { get; }
Глава 12. Расширенные средства языка С# 453
Реализация методов ToStr±ng() и GetHashCode ()
Все анонимные типы автоматически наследуются от System. Ob j ect и предоставляют
переопределенные версии методов Equals (), GetHashCode () и ToString (). Реализация
ToString () просто строит строку из каждой пары "имя/значение". Например:
public override string ToString()
{
StringBuilder builder = new StringBuilder ();
builder.Append("{ Color = ") ;
builder.Append(this.<Color>i Field);
builder.Append(", Make = ") ;
builder.Append(this.<Make>i Field);
builder.Append(", CurrentSpeed = ") ;
builder.Append(this.<CurrentSpeed>i Field);
builder.Append(" }");
return builder.ToString ();
}
Реализация GetHashCode () вычисляет хеш-значение, используя каждую переменную-
член анонимного типа в качестве входной для типа System.Collections . Generic .
EqualityComparer<T>. Используя эту реализацию GetHashCode (), два анонимных
типа породят одинаковое хеш-значение тогда (и только тогда), когда они имеют
одинаковый набор свойств, которым присвоены одинаковые значения. При такой реализации
анонимные типы хорошо подходят для помещения в контейнер Hashtable.
Семантика эквивалентности анонимных типов
Хотя реализация переопределенных методов ToString () и GetHashCode ()
достаточно проста, реализация метода Equals () может вызвать вопросы. Например, если
определено две переменных "анонимных автомобилей" с одинаковым набором пар "имя/
значение", должны ли эти переменные трактоваться как эквивалентные? Чтобы увидеть
результат такого сравнения, дополним класс Program следующим новым методом:
static void EqualityTest ()
{
// Создать два анонимных класса с идентичным набором пар "имя/значение".
var firstCar = new { Color = "Bright Pink", Make = "Saab", CurrentSpeed = 55 };
var secondCar = new { Color = "Bright Pink", Make = "Saab", CurrentSpeed =55 };
// Считать ли их эквивалентными на основе использования Equals()?
if (firstCar.Equals(secondCar) )
Console.WriteLine("Same anonymous object!"); // один и тот же объект
else
Console.WriteLine("Not the same anonymous object!"); // разные объекты
// Можно ли проверить их эквивалентность с помощью ==?
if (firstCar == secondCar)
Console.WriteLine("Same anonymous object1");
else
Console.WriteLine ("Not the same anonymous object1");
// Имеют ли эти объекты в основе одинаковый тип?
if (firstCar.GetType() .Name == secondCar.GetType () .Name)
Console. WriteLine ("We are both the same type1"); // один и тот же тип
else
Console.WriteLine("We are different types!"); // разные типы
// Отобразить все детали.
Console.WriteLine();
ReflectOverAnonymousType(firstCar);
ReflectOverAnonymousType(secondCar);
}
454 Часть III. Дополнительные конструкции программирования на С#
На рис. 12.6 показан (несколько неожиданный) вывод, полученный в результате
вызова этого метода в Main ().
■В C:\Windo*s\$ystem32\cmd.e«
Same anonymous obje
Not the same anonym
We are both the sain
obj is an instance of: of__AnonymousTypeO*3
■Base class of of AnonymousTypeO is System.Object
lobi.ToStringO = { Color = Bright Pink, Make = Saab, CurrentSpeed = 55 }
■obj.GetHashCodeO = 2038548792
bobj is an instance of: of AnonymousTypeO*3
■Base class of of AnonymousTypeO*3 is System.Object
lobj.ToStringO = { Color = Bright Pink, Make = Saab, CurrentSpeed = 55 }
■obj.GetHashCodeO = 2Q38548792
■Press any key to continue . . .
Рис. 12.6. Эквивалентность анонимных типов
После запуска этого тестового кода вы увидите, что первая проверка, при которой
вызывается Equals (), возвращает true, и потому на консоль выводится сообщение
"Same anonymous object! ". Причина в том, что сгенерированный компилятором
метод Equals () при проверке эквивалентности использует семантику на основе значений
(т.е. проверяет значения каждого поля сравниваемого объекта).
Однако вторая проверка (в которой используется операция ==) приводит к выводу
на консоль строки "Not the same anonymous object! ", что на первый взгляд
выглядит несколько нелогично. Такой результат объясняется тем, что анонимные типы не
получают перегруженной версии операций проверки равенства (== и ! =). Поэтому при
проверке эквивалентности объектов анонимных типов с использованием операций
равенства С# (вместо метода Equals ()), то проверяются ссылки, а не значения,
поддерживаемые объектами.
И последнее (по порядку, но не значению): финальная проверка (где проверяется
имя лежащего в основе типа) показывает, что экземпляры анонимных типов относятся
к одному и тому же сгенерированному компилятором типу класса (в данном примере —
of AnonymousTypeO ч 3), потому что f irstCar и secondCar имеют одинаковый набор
свойств (Color, Make и CurrentSpeed). Это иллюстрирует важный, но тонкий момент:
компилятор генерирует определение нового класса тогда, когда анонимный тип
имеет уникальные имена свойств. Поэтому в случае объявления идентичных анонимных
типов (в смысле — с одинаковыми именами) в одной сборке компилятор сгенерирует
только одно определение анонимного типа.
Анонимные типы, содержащие другие анонимные типы
Можно создавать анонимные типы, состоящие из других анонимных типов.
Например, предположим, что необходимо смоделировать заказ на покупку, который
состоит из временной метки, цены и приобретаемого автомобиля. Ниже показан новый
(несколько более сложный) анонимный тип, представляющий эту сущность:
// Создать анонимный тип, состоящий из другого анонимного типа,
var purchaseltem = new {
TimeBought = DateTime.Now,
ItemBought = new {Color = "Red", Make = "Saab", CurrentSpeed =55},
Price = 34.000} ;
ReflectOverAnonymousType(purchaseltem);
Глава 12. Расширенные средства языка С# 455
К этому моменту синтаксис, используемый для определения анонимных типов,
должен быть понятным, но, скорее всего, остался главный вопрос: где и когда может
понадобиться это новое языковое средство? Если кратко, то объявления анонимных типов
следует применять сдержанно, обычно только в сочетании с набором технологий LINQ
(см. главу 14). Никогда не отказывайтесь от использования строго типизированных
классов и структур просто потому, что это возможно, учитывая многочисленные
ограничения анонимных типов, которые перечислены ниже.
1. Контроль над именами анонимных типов отсутствует.
2. Анонимные типы всегда расширяют System.Object.
3. Поля и свойства анонимного типа всегда доступны только для чтения.
4. Анонимные типы не могут поддерживать события, специальные методы,
специальные операции или специальные переопределения.
5. Анонимные типы всегда неявно запечатаны.
6. Анонимные типы всегда создаются с использованием конструктора по
умолчанию.
Тем не менее, при программировании с использованием набора технологий LINQ вы
обнаружите, что во многих случаях этот синтаксис оказывается очень полезным, когда
нужно быстро смоделировать общую форму сущности, а не ее функциональность.
Исходный код. Проект AnonymousTypes доступен в подкаталоге Chapter 12.
Работа с типами указателей
Последняя тема настоящей главы касается средства С#, которое наименее часто
используется в подавляющем большинстве проектов. В главе 4 было указано, что в .NET
определены две основных категории типов данных: типы значения и ссылочные типы.
На самом деле существует еще и третья категория: типы указателей. Чтобы можно
было работать с типами указателей, в табл. 12.3 описаны специфические ключевые
слова и операции, которые позволят обойти схему управления памятью CLR и взять
бразды правления в собственные руки.
Таблица 12.3. Операции и ключевые слова С#, связанные с указателями
Операция/ назначение
ключевое слово
* Эта операция используется для создания переменной-указателя (те.
переменной, представляющей непосредственное расположение в памяти). Как и в
С (C++), та же самая операция служит для разыменования указателя.
& Эта операция используется для получения адреса переменной в памяти.
-> Эта операция используется для доступа к полям типа, представленного
указателем (небезопасной версией операции точки С#).
[ ] Операция [ ] (в небезопасном контексте) позволяет индексировать слот,
заданный переменной-указателем (вспомните взаимосвязь между переменной-
указателем и операцией [ ] в С (C++)).
++, — В небезопасном контексте операции инкремента и декремента могут
применяться к типам указателей.
456 Часть III. Дополнительные конструкции программирования на С#
Окончание табл. 12.3
Операция/
ключевое слово
Назначение
+, - В небезопасном контексте операции сложения и вычитания могут
применяться к типам указателей.
==, ! =, <, >, В небезопасном контексте операции сравнения и эквивалентности могут при-
<=, >= меняться к типам указателей.
stackalloc В небезопасном контексте ключевое слово stackalloc может быть
использовано для размещения массивов С# непосредственно в стеке.
fixed В небезопасном контексте ключевое слово fixed может быть использовано
для временной фиксации переменной, чтобы можно было определить ее адрес.
Перед погружением в детали следует учесть, что вам очень редко, если вообще
понадобится использовать типы указателей. Хотя С# позволяет перейти на
уровень прямых манипуляций указателями, знайте, что исполняющая система .NET
не имеет абсолютно никакого представления о ваших намерениях. Поэтому, если
вы произведете неверное действие с указателем, то сами будете отвечать за
последствия. С учетом этого предупреждения возникает вопрос: когда вообще может
понадобиться работа с типами указателей? Есть только две такие ситуации.
• Нужно оптимизировать некоторые части приложения, напрямую обращаясь к его
членам вне управления CLR.
• Требуется вызывать методы из динамической библиотеки (* . dll), написанной
на С, или же из сервера СОМ, который требует в качестве параметров типы
указателей. Но даже в этом случае часто можно обойтись без использования типов
указателей, предпочтя им тип System. IntPtr и члены типа System.Runtime .
InteropServices.Marshal.
В случае если все же решено применять это средство языка С#, понадобится
информировать компилятор С# (csc.exe) о своих намерениях, позволив проекту
поддерживать "небезопасный" (unsafe) код. Чтобы сделать это в командной строке, добавьте при
вызове компилятора флаг /unsafe:
esc /unsafe *.cs
В среде Visual Studio 2010 нужно будет перейти на страницу Properties (Свойства)
проекта и на вкладке Build (Сборка) отметить флажок Allow Unsafe Code (Разрешить
небезопасный код), как показано на рис. 12.7. Чтобы поэкспериментировать с типами
указателей, создадим новое консольное приложений по имени Unsaf eCode и разрешим
небезопасный код.
На заметку! В последующих примерах предполагается наличие некоторого опыта манипуляций
указателями в С (C++). Если это не так, можете просто полностью пропустить этот раздел.
Написание небезопасного кода не является распространенной практикой в большинстве
приложений С#.
Ключевое слово unsafe
Если необходимо работать с указателями в С#, следует специально объявить блок
"небезопасного" кода с помощью ключевого слова unsafe (любой код, который не
помечен ключевым словом unsafe, автоматически считается "безопасным").
Глава 12. Расширенные средства языка С# 457
UnsafeCode* X
Application
j Build*
Build Events
Debug
Resources
Services
Settings
Reference Paths
Configuration: [Active(Debug) ▼[
Platform: Active 1x86)
У Define TRACE constant
Platform target
;V) Allow unsafe code
L] Optimiiecode
Errors and warnings
Warning level:
Рис. 12.7. Включение поддержки небезопасного кода в Visual Studio 2010
Например, в следующем классе Program объявляется область небезопасного кода
внутри метода Main ():
class Program
{
static void Main(string[] args)
{
unsafe
{
// Здесь работаем с указателями!
}
// Здесь нельзя работать с указателями!
}
}
В дополнение к объявлению области небезопасного кода внутри метода, можно
строить "небезопасные" структуры, классы, члены типов и параметры. Ниже приведено
несколько примеров (эти типы в текущем проекте определять не нужно):
// Вся эта структура "небезопасна" и может
// использоваться только в небезопасном контексте.
public unsafe struct Node
{
public int Value;
public Node* Left;
public Node* Right;
}
// Структура безопасна, но члены Node2* - нет.
// Технически можно обратиться к Value извне
// небезопасного контекста, но не к Left и Right.
public struct Node2
{
public int Value;
// Это доступно только из небезопасного контекста!
public unsafe Node2* Left;
public unsafe Node2* Right;
}
Методы (статические и уровня экземпляра) также могут быть помечены как
небезопасные. Предположим, известно, что определенный статический метод будет
использовать логику указателей. Чтобы обеспечить возможность вызова этого метода только из
небезопасного контекста, этот метод можно определить следующим образом:
458 Часть III. Дополнительные конструкции программирования на С#
unsafe static void SquarelntPointer (int* mylntPointer)
{
// Возведем значение в квадрат — просто для целей тестирования.
*myIntPointer *= *myIntPointer;
}
Конфигурация метода требует, чтобы вызывающий код обращался к методу
SquarelntPointer (), как показано ниже:
static void Main(string[] args)
{
unsafe
{
int mylnt = 10;
// Нормально, мы в небезопасном контексте.
SquarelntPointer(&mylnt);
Console.WriteLine("mylnt: {0}" , mylnt);
}
int mylnt2 = 5;
// Ошибка компиляции! Должен быть небезопасный контекст!
SquarelntPointer(&mylnt2);
Console.WriteLine("mylnt: {0}", mylnt2);
}
Если вы не хотите заставлять вызывающий код помещать этот вызов в оболочку
небезопасного контекста, можете пометить весь метод Main () ключевым словом unsafe.
В этом случае следующий код скомпилируется:
unsafe static void Main(string [ ] args)
{
int mylnt2 = 5;
SquarelntPointer(&mylnt2);
Console.WriteLine ("mylnt: {0}", mylnt2);
Работа с операциями * и &
Установив небезопасный контекст, можно строить указатели и типы данных,
использующие операцию *, а также получать адрес указателя с помощью операции &. В
отличие от С или C++, в языке С# операция * применяется только к лежащему в основе типу,
а не является префиксом имени каждой переменной указа геля. Например, рассмотрим
следующий код, который иллюстрирует правильный и неправильный способы
объявления указателей на целочисленные переменные:
// Нет1 В С# это неправильно1
int *pi, *pj;
// Да! Так правильно в С#.
int* pi, pj;
Рассмотрим следующий небезопасный метод:
unsafe static void PrintValueAndAddress ()
{
int mylnt;
// Определить указатель на int и присвоить ему адрес mylnt.
int* ptrToMylnt = &mylnt;
// Присвоить значение mylnt, используя обращение через указатель.
*ptrToMyInt = 123;
// Вывести на консоль некоторые значения.
Console.WriteLine("Value of mylnt {0}", mylnt);
Console.WriteLine("Address of mylnt {0:X}", (int)&ptrToMy!nt);
Глава 12. Расширенные средства языка С# 459
Небезопасная и безопасная функция обмена значений
Разумеется, объявление указателей на локальные переменные только для того,
чтобы присвоить им значения (как в предыдущем примере), никогда не понадобится и
вообще неудобно. Чтобы проиллюстрировать более практичный пример небезопасного
кода, предположим, что нужно построить функцию обмена с использованием
арифметики указателей:
unsafe public static void UnsafeSwap(int* 1, int* j)
{
int temp = *i;
*i = *j;
*j = temp;
}
Очень похоже на язык С, не правда ли? Однако если вы читали главу 4, то в
состоянии написать следующую безопасную версию алгоритма обмена с применением
ключевого слова ref:
public static void SafeSwap(ref int 1, ref int j)
{
int temp = i;
i = ц;
j = temp;
}
Функциональность обеих версий метода идентична, а это доказывает, что прямые
манипуляции указателями в С# вовсе не обязательны. Ниже показана логика вызова с
использованием безопасного Main (), но с небезопасным контекстом:
static void Main(string[] args)
{
Console.WriteLine ("***** Calling method with unsafe code *****");
// Значения, подлежащие обмену.
int l = 10, j = 20;
// "Безопасный" обмен значений.
Console.WriteLine("\n***** Safe swap *****");
Console .WriteLine ("Values before safe swap: 1 = {0}, j = {1}", i, j) ;
SafeSwap(ref 1, ref j);
Console .WriteLine ("Values after safe swap: 1 = {0}, j = {1}", i, j);
// "Небезопасный" обмен значений.
Console.WriteLine("\n***** Unsafe swap *****");
Console.WriteLine ("Values before unsafe swap: i = {0}, j = {1}", l, j);
unsafe { UnsafeSwap (&i, &]); }
Console.WriteLine("Values after unsafe swap: l = {0}, j = {1}", l, j) ;
Console.ReadLine() ;
}
Доступ к полям через указатели (операция ->)
Теперь предположим, что определена простая безопасная структура Point:
struct Point
{
public int x;
public int y;
public override string ToStringO
{ return string.Format ( " ({0}, {1})", x, y) ; }
}
460 Часть III. Дополнительные конструкции программирования на С#
При объявлении указателя на тип Point для доступа к общедоступным членам
структуры понадобится применять операцию доступа к полю (в виде ->). Как показано
в табл. 12.3, это — небезопасная версия стандартной (безопасной) операции точки (.).
Фактически, используя операцию обращения к указателю (*), можно разыменовать
указатель и вновь применить операцию точки. Рассмотрим следующий небезопасный
метод:
unsafe static void UsePointerToPoint ()
{
// Доступ к членам через указатель.
Point point;
Point* p = &point;
р->х = 100;
р->у = 200;
Console.WriteLine(p->ToString() ) ;
// Доступ к членам через разыменованный указатель.
Point point2;
Point* p2 = &point2;
(*р2).х = 100;
(*р2) .у = 200;
Console.WriteLine ( (*p2) .ToStringO ) ;
}
Ключевое слово stackalloc
В небезопасном контексте может понадобиться объявить локальную переменную,
выделяющую память непосредственно в стеке вызовов (и потому недоступной для системы
сборки мусора .NET). Для этого в С# предусмотрено ключевое слово stackalloc, которое
является С#-эквивалентом функции all оса библиотеки времени выполнения С.
Ниже приведен простой пример:
unsafe static void UnsafeStackAlloc ()
{
char* p = stackalloc char[256];
for (int k = 0; k < 256; k++)
p[k] = (char)k;
}
Закрепление типа ключевым словом fixed
Как было показано в предыдущем примере, выделение фрагмента памяти в
пределах небезопасного контекста, может быть осуществлено с помощью ключевого слова
stackalloc. Ввиду природы этой операции, выделенная память очищается, как только
метод, который ее выделил, возвращает управление (поскольку память распределена
в стеке). Однако рассмотрим более сложный пример. Во время экспериментов с
операцией -> был создан тип значения по имени Point. Подобно всем типам значений,
выделенная его экземплярам память выталкивается из стека, как только завершается
контекст выполнения. Предположим, что вместо этого структура Point определена как
ссылочный тип:
class PointRef // Переименовано и переписано.
{
public int х;
public int у;
public override string ToStringO
{ return string.Format (" ({0}, {1})", x, y) ; }
}
Глава 12. Расширенные средства языка С# 461
Как вам хорошо известно, если теперь в вызывающем коде объявить переменную
типа Point, то память будет выделена в куче, подверженной сборке мусора. И тут
возникает животрепещущий вопрос: а что если небезопасный контекст пожелает
взаимодействовать, с этим объектом (или любым другим объектом из кучи)? Учитывая, что
сборка мусора может произойти в любой момент, представьте проблему, с которой вы
столкнетесь при обращении к членам Point в тот момент, когда происходит
реорганизация кучи. Теоретически может случиться так, что небезопасный контекст попытается
взаимодействовать с членом, который уже недоступен или был перемещен в куче после
ее реорганизации (что является очевидной проблемой).
Для фиксации переменной ссылочного типа в памяти из небезопасного контекста
в С# предусмотрено ключевое слово fixed. Оператор fixed устанавливает указатель
на управляемый тип и закрепляет эту переменную на время выполнения оператора.
Без fixed от указателей на управляемые переменные было бы мало пользы,
поскольку сборка мусора может перемещать переменные в памяти непредсказуемым образом.
(Фактически компилятор С# не позволит установить указатель на управляемую
переменную без оператора fixed.)
Таким образом, если вы создадите тип Point (теперь в виде класса) и захотите
взаимодействовать с его членами, то должны будете написать следующий код (или, в
противном случае, получить ошибку этапа компиляции):
unsafe public static void UseAndPinPoint ()
{
PointRef pt = new PointRef ();
pt.x = 5;
pt.y = 6;
// Закрепить указатель pt на месте, чтобы он не мог быть
// перемещен или собран сборщиком мусора.
fixed (int* p = &pt.x)
{
// Использовать здесь переменную int*1
}
// Указатель pt теперь не закреплен и готов к сборке мусора.
Console.WriteLine ("Point is: {O}11, pt) ;
}
По сути, ключевое слово fixed позволяет строить оператор, блокирующий
ссылочную переменную в памяти, чтобы ее адрес оставался постоянным на протяжении
работы оператора. Поэтому, взаимодействуя со ссылочным типом из контекста
небезопасного кода, нужно обязательно фиксировать ссылку.
Ключевое слово sizeof
И последнее ключевое слово С#, связанное с небезопасным кодом — это sizeof. Как
и в языке С (C++), ключевое слово sizeof в С# служит для получения размера в байтах
типа значения (но никогда — ссылочного типа), и может применяться только внутри
небезопасного контекста. Как и можно было представить, эта возможность может
пригодиться при взаимодействии с неуправляемыми API-интерфейсами на базе С. Его
применение очевидно:
unsafe static void UseSizeOfOperator ()
{
Console.WriteLine("The size of short is {0 } . ", sizeof (short)); // размер short
Console.WriteLine("The size of int is {0}.", sizeof (int)); // размер int
Console.WriteLine("The size of long is {0}.", sizeof (long)); // размер long
}
462 Часть III. Дополнительные конструкции программирования на С#
Поскольку sizeof вычисляет количество байт для любой сущности,
унаследованной от System. ValueType, также можно получить размер пользовательских структур.
Например, передать структуру Point в sizeof можно следующим образом:
unsafe static void UseSizeOfOperator ()
{
Console.WriteLine("The size of Point is {0}.", sizeof (Point));
}
Исходный код. Проект UnsafeCode доступен в подкаталоге Chapter 12.
На этом обзор наиболее мощных средств языка программирования С# завершен.
В следующей главе будет продемонстрировано применение некоторых из этих
концепций (а именно — расширяющих методов и анонимных типов) в контексте LINQ to
Objects.
Резюме
Целью этой главы было углубление знаний языка программирования С#. Мы начали
с исследования различных применения развитых конструкций (методов-индексаторов,
перегруженных операций и процедур специального преобразования типов).
Затем была рассмотрена роль расширяющих методов, анонимных типов и
частичных методов. Как будет показано в следующей главе, эти средства очень полезны при
работе с API-интерфейсами LINQ (хотя их можно использовать повсюду в коде, если это
покажется удобным). Вспомните, что анонимные методы позволяют быстро
моделировать "форму" типа, в то время как расширяющие методы позволяют добавлять новую
функциональность к типам, без необходимости определения подклассов.
Финальная часть главы была посвящена рассмотрению небольшого набора
малоизвестных ключевых слов (sizeof, checked, unsafe и т.п.), а попутно была
продемонстрирована работа с низкоуровневыми типами указателей. Как было установлено в
процессе рассмотрения этих типов, в большинстве приложений С# никогда не понадобится
их использовать.
ГЛАВА 13
LINQ to Objects
Независимо от типа приложения, которое вы создаете с использованием
платформы .NET, ваша программа определенно нуждается в доступе к некоторой
форме данных в процессе выполнения. Данные могут находиться в самых разных
местах, включая файлы XML, реляционные базы данных, коллекции в памяти,
элементарные массивы. Исторически сложилось, что в зависимости от места хранения данных
программистам приходилось использовать очень разные и никак не связанные API-
интерфейсы. Набор технологий LINQ (Language Integrated Query — язык
интегрированных запросов), появившийся в .NET 3.5, предоставил краткий, симметричный и строго
типизированный способ доступа к широкому разнообразию хранилищ данных. В этой
главе начинается изучение LINQ с рассмотрения LINQ to Objects.
Прежде чем погрузиться в LINQ to Objects, в первой части этой главы мы кратко
просмотрим ключевые программные конструкции С#, которые обеспечили возможность
существования LINQ. По мере чтения главы вы убедитесь, насколько полезны такие
средства, как неявно типизированные переменные, синтаксис инициализации
объектов, лямбда-выражения, расширяющие методы и анонимные типы.
После ознакомления с поддерживающей инфраструктурой в оставшемся материале
главы будет представлена модель LINQ и роль, которую она играет в платформе .NET.
Вы узнаете назначение операций и выражений запросов, которые позволяют
определять операторы, опрашивающие источник данных для выдачи запрошенного
результирующего набора. Попутно рассматриваются многочисленные примеры применения
LINQ, которые иллюстрируют взаимодействие с данными в массивах и коллекциях
различного типа (как обобщенных, так и необобщенных), а также сборки, пространства
имен и типы, представляющие API-интерфейс LINQ to Objects.
На заметку! Предлагаемые в этой главе сведения послужат фундаментов для освоения
последующих глав книги, в которых описаны дополнительные технологии LINQ, включая LINQ to XML
(глава 25), Parallel LINQ (глава 19) и LINQ to Entities (глава 23).
Программные конструкции, специфичные для LINQ
На самом высоком уровне LINQ можно воспринимать как строго типизированный
язык запросов, встроенный непосредственно в грамматику самого языка С#. Используя
LINQ, можно строить любое количество выражений, которые выглядят и ведут себя
подобно SQL-запросам к базе данных. Однако запрос LINQ может применяться к любому
числу хранилищ данных, включая хранилища, которые не имеют ничего общего с
истинными реляционными базами данных.
464 Часть III. Дополнительные конструкции программирования на С#
На заметку! Хотя запросы LINQ внешне похожи на запросы SQL, их синтаксис не идентичен.
Фактически многие запросы LINQ выглядят прямой противоположностью формата запроса к
базе данных! Если вы попытаетесь отобразить LINQ непосредственно на SQL, то определенно
будете разочарованы. Чтобы это не случилось, рекомендуется воспринимать запросы LINQ как
уникальную сущность, которая лишь случайно похожа на SQL.
Когда LINQ впервые был представлен в составе платформы .NET 3.5, языки С# (и VB)
уже были снабжены огромным количеством программных конструкций для поддержки
набора технологий LINQ. В частности, язык С# использует следующие связанные с LINQ
средства:
• неявно типизированные локальные переменные;
• синтаксис инициализации объектов и коллекций;
• лямбда-выражения;
• расширяющие методы;
• анонимные типы.
Эти средства уже детально рассматривались в различных главах настоящей книги.
Однако чтобы освежить все в памяти, давайте быстро рассмотрим каждое из средств по
очереди, просто чтобы удостоверится в правильном их понимании.
Неявная типизация локальных переменных
В главе 3 вы узнали о ключевом слове var в языке С#. Это ключевое слово
позволяет определять локальную переменную без явной спецификации лежащего в основе
типа данных. Тем не менее, такая переменная будет строго типизированной, поскольку
компилятор определит ее корректный тип данных исходя из начального присваивания.
Вспомните следующий код примера из главы 3:
static void DeclarelmplicitVars ()
{
// Неявно типизированные локальные переменные.
var mylnt = 0;
var myBool = true;
var myString = "Time, marches on...";
// Вывод имен типов, лежащих в основе этих переменных.
Console.WriteLine("mylnt is a: {0}", mylnt.GetType().Name);
Console.WriteLine("myBool is a: {0}", myBool.GetType().Name);
Console.WriteLine("myString is a: {0}", myString.GetType().Name);
}
Это средство языка очень удобно и зачастую обязательно, когда используется LINQ.
Как будет показано на протяжении главы, многие запросы LINQ будут возвращать
последовательности типов данных, которые неизвестны к моменту компиляции. Учитывая,
что лежащий в основе тип данных до компиляции приложения не известен, очевидно,
что объявить такую переменную явно не удастся!
Синтаксис инициализации объектов и коллекций
В главе 5 объясняется роль синтаксиса инициализации объектов, который
позволяет создать переменную типа класса или структуры и установить любое количество ее
общедоступных свойств за один прием. В результате получается очень компактный (и
легко читаемый) синтаксис, который может применяться для подготовки объектов к
использованию. Также вспомните из главы 10, что язык С# поддерживает очень похожий
синтаксис инициализации коллекций объектов. Взгляните на следующий фрагмент
Глава 13. LINQ to Objects 465
кода, где синтаксис инициализации коллекций используется для наполнения List<T>
объектами Rectangle, каждый из которых состоит из пары объектов Point,
представляющих точку с координатами (х, у):
List<Rectangle> myListOfRects = new List<Rectangle>
{
new Rectangle {TopLeft = new Point { X = 10, Y = 10 },
BottomRight = new Point { X = 200, Y = 200}},
new Rectangle {TopLeft = new Point { X = 2, Y = 2 },
BottomRight = new Point { X = 100, Y = 100}},
new Rectangle {TopLeft = new Point { X = 5, Y = 5 },
BottomRight = new Point { X = 90, Y = 75}}
};
Хотя ничто не заставляет применять синтаксис инициализации коллекции или
объекта, с его помощью можно получить более компактную кодовую базу. Более того, этот
синтаксис в сочетании с неявной типизацией локальных переменных позволяет
объявлять анонимный тип, что очень удобно для создании проекций LINQ. О проекциях LINQ
речь пойдет далее в этой главе.
Лямбда-выражения
Лямбда-операция С# (=>) была уже полностью описана в главе 11. Вспомните, что
эта операция позволяет построить лямбда-выражение, которое может быть
использовано в любой момент, когда вызывается метод, который требует строго
типизированного делегата в качестве аргумента. Лямбда-выражения значительно упрощают работу
с делегатами .NET, поскольку сокращают объем кода, который должен быть написан
вручную. Лямбда-выражения могут быть описаны следующим образом:
АргументыДляОбработки => ОператорыДляИхОбработки
В главе 11 рассматривались способы взаимодействия с методом FindAllO
обобщенного класса List<T> с применением различных подходов. После работы с простым
делегатом Predicate<T> и анонимным методом С# мы пришли к следующей
(исключительно краткой) итерации, в которой применялось следующее лямбда-выражение:
static void LambdaExpressionSyntax ()
{
// Создать список целых.
List<int> list = new List<int>();
list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });
// Лямбда-выражение С#.
List<int> evenNumbers = list.FindAll (l => (l " 2) == 0) ;
// Вывод на консоль четных чисел.
Console .WriteLine ("Here are your even numbers:11);
foreach (int evenNumber in evenNumbers)
{
Console.Write("{0}\t", evenNumber);
}
Console.WriteLine ();
}
Лямбда-выражения очень полезны при работе с объектной моделью, лежащей в
основе LINQ. Как вы вскоре убедитесь, операции запросов С# LINQ — это просто
сокращенная нотация вызова методов класса по имени System.Linq.Enumerable. Эти
методы обычно всегда требуют передачи в качестве параметров делегатов (в частности,
делегата Func<>), которые используются для обработки данных с целью получения
корректного результирующего набора. За счет использования лямбда-выражений можно
упростить код и позволить компилятору вывести необходимый делегат.
466 Часть III. Дополнительные конструкции программирования на С#
Расширяющие методы
Расширяющие методы С# позволяют добавлять новую функциональность к
существующим классам без необходимости применять наследование. Кроме того,
расширяющие методы позволяют добавлять новую функциональность к запечатанным
классам и структурам, от которых просто невозможно наследовать производные классы.
Вспомните из главы 12, где создавался расширяющий метод, что первый параметр
такого метода квалифицируется операцией this и помечает расширяемый тип. Кроме
того, расширяющие методы должны всегда определяться внутри статического класса, а
потому объявляться с использованием ключевого слова static. Для примера взгляните
на следующий код:
namespace MyExtensions
{
static class ObjectExtensions
{
// Определение расширяющего метода для System.Object.
public static void DisplayDefimngAssembly (this object obj )
{
Console.WriteLine ("{0 } lives here:\n\t->{1}\n", obj.GetType() .Name,
Assembly.GetAssembly(obj.GetType()));
}
}
}
Чтобы использовать это расширение, в приложении должна быть сначала
установлена ссылка на внешнюю сборку, содержащую реализацию расширяющего метода, с
использованием диалогового окна Add Reference (Добавить ссылку) среды Visual Studio
2010. После этого можно просто импортировать определенное пространство имен и
написать следующий код:
static void Main(string [ ] args)
{
// Поскольку все расширяет System.Object, все классы и структуры
// могут использовать это расширение.
int mylnt = 12345678;
mylnt.DisplayDefiningAssembly();
System.Data.DataSet d = new System.Data.DataSet() ;
d.DisplayDefiningAssembly();
Console.ReadLine();
}
При работе с LINQ вам редко придется строить собственные расширяющие
методы, если вообще придется. Однако при создании выражений запросов LINQ на самом
деле применяются многочисленные расширяющие методы, уже определенные Microsoft.
Фактически каждая операция запроса С# LINQ — это сокращенная нотация ручного
вызова лежащего в основе расширяющего метода, обычно определенного служебным
классом System.Linq.Enumerable.
Анонимные типы
И последнее средство языка С#, которое мы здесь кратко пересмотрим — анонимные
типы, которые были подробно описаны в главе 12. Это средство может быть
использовано для быстрого моделирования "формы" данных, позволяя компилятору
генерировать новое определение класса во время компиляции на основе указанного набора пар
"имя/значение". Вспомните, что этот тип будет составлен с использованием
семантики, основанной на значениях, и каждый виртуальный метод System.Obj ect будет coot-
Глава 13. LINQ to Objects 467
ветствующим образом переопределен. Чтобы определить анонимный тип, необходимо
объявить неявно типизированную переменную и указать форму данных с применением
синтаксиса инициализации объекта:
// Создать анонимный тип, состоящий из другого анонимного типа,
var purchaseltem = new {
TimeBought = DateTime.Now,
ItemBought = new {Color = "Red", Make = "Saab", CurrentSpeed =55},
Price = 34.000};
В LINQ анонимные типы часто используются при проектировании новых форм
данных "на лету". Например, может существовать коллекция объектов Person, и нужно с
помощью LINQ получить информацию о возрасте и номере карточки социального
страхования для каждого объекта. Используя проекцию LINQ, можно позволить
компилятору сгенерировать новый анонимный тип, содержащий необходимую информацию.
Роль LINQ
На этом краткий обзор средств языка С#, позволяющих LINQ выполнять свою
работу, завершен. Однако важно понять, зачем вообще нужен язык LINQ? Любой
разработчик программного обеспечения согласится с утверждением, что значительная часть
времени при программировании тратится на получение и манипуляции данными.
Когда говорят о "данных", немедленно приходит на ум информация, хранящаяся
внутри реляционных баз данных. Тем не менее, другими популярными местами
нахождения данных являются документы XML (файлы *. с on fig, локально сохраненные наборы
DataSet, или данные в памяти, возвращенные службами WCF).
Данные могут быть найдены в различных местах и помимо этих двух общепринятых
хранилищ информации. Например, предположим, что имеется массив или обобщенный
тип List<T>, содержащий 300 целых чисел, и нужно получить подмножество, которое
отвечает заданному критерию (например, только четные или только нечетные числа,
только простые числа, только неповторяющиеся числа больше 50). Или, возможно, при
использовании API-интерфейсов рефлексии требуется получить в массиве Туре
только метаданные для каждого класса, унаследованного от определенного родительского
класса. В действительности данные находятся повсюду.
До появления .NET 3.5 взаимодействие с определенной разновидностью данных
требовало от программистов использования очень разных API-интерфейсов. Например, в
табл. 13.1 описаны некоторые распространенные API-интерфейсы, применяемые для
доступа к различным типам данных.
Таблица 13.1. Способы манипуляции различными типами данных
Необходимые данные Как получить
Реляционные данные System.Data.dll, System.Data.SqlClient.dll и т.п.
Данные документов XML System.Xml.dll
Таблицы метаданных Пространство имен System.Reflection
Коллекции объектов Пространства имен System.Array и
System. Collections/System.Collections.Generic
Разумеется, это вполне нормальные подходы к манипулированию данными.
Фактически при программировании с помощью .NET 4.0/C# 2010 вы можете (и будете)
непосредственно использовать ADO.NET, пространства имен XML, службы рефлексии и
различные типы коллекций. Однако основная проблема состоит в том, что каждый из этих
468 Часть III. Дополнительные конструкции программирования на С#
API-интерфейсов представляет собой "изолированный островок", слабо интегрируемый
с другими. Правда, можно (например) сохранить DataSet из ADO.NET в документ XML
и затем манипулировать им через пространства имен System.Xml, но все равно
манипуляции данными остаются довольно асимметричными.
В рамках API-интерфейса LINQ была предпринята попытка предложить
программистам согласованный, симметричный способ получения и манипуляций "данными" в
самом широком смысле этого понятия. Используя LINQ, можно создавать
непосредственно внутри синтаксиса языка С# конструкции, называемые выражениями запросов. Эти
выражения запросов основаны на множестве операций запросов, которые намеренно
сделаны похожими — внешне и по поведению (но не идентичными) — на выражения
SQL.
Фокус, однако, в том, что выражение запроса может применяться для
взаимодействия с многочисленными типами данных, даже данными, которые никак не связаны с
реляционными базами. Строго говоря, "LINQ" — это термин, описывающий общий
подход к доступу к данным. В зависимости от места использования запросов LINQ,
существуют разновидности LINQ, которые перечислены ниже.
• UNQ to Objects. Эта разновидность позволяет применять запросы LINQ к
массивам и коллекциям.
• LLNQ to XML. Эта разновидность позволяет применять LINQ для манипулирования
и опроса документов XML.
• UNQ to DataSet. Эта разновидность позволяет применять запросы LINQ к
объектам DataSet из ADO.NET.
• UNQ to Entities. Эта разновидность позволяет применять запросы LINQ внутри
API-интерфейса ADO.NET Entity Framework (EF).
• Parallel UNQ (он же PUNQ). Эта разновидность позволяет выполнять
параллельную обработку данных, возвращенных запросом LINQ.
Похоже, в Microsoft намерены глубоко интегрировать поддержку LINQ в среду
программирования .NET. Вполне можно ожидать, что с течением времени LINQ станет
неотъемлемой частью библиотек базовых классов .NET, языков и самой среды разработки
Visual Studio.
Выражения LINQ строго типизированы
Очень важно отметить, что выражение запроса LINQ (в отличие от традиционных
операторов SQL) является строго типизированным. Поэтому компилятор С# следит
за этим и гарантирует, что выражения синтаксически оформлены корректно. Попутно
следует отметить, что выражения запросов имеют представление метаданных внутри
использующей их сборки, поскольку операции запросов LINQ всегда обладают
развитой объектной моделью. Такие инструменты, как Visual Studio 2010, могут пользоваться
этими метаданными для обеспечения работы полезных средств вроде IntelliSense,
автозавершения и тому подобного.
Основные сборки LINQ
Как упоминалось в главе 2, в диалоговом окне New Project (Новый, проект) в Visual
Studio 2010 доступна возможность выбора версии платформы .NET, для которой
нужно производить компиляцию. При компиляции для .NET 3.5 и последующих версий
каждый из шаблонов проектов автоматически ссылается на ключевые сборки LINQ, что
легко заметить в Solution Explorer. В табл. 13.2 описана роль ключевых сборок LINQ.
В остальной части книги вы встретите и другие дополнительные библиотеки LINQ.
Глава 13. LINQ to Objects 469
Таблица 13.2. Основные сборки, связанные с LINQ
Сборка Назначение
System.Core.dll Определяет типы, представляющие основной API-
интерфейс LINQ. Это единственная сборка, к которой
нужно иметь доступ, чтобы пользоваться любым API-
интерфейсом LINQ, включая LINQ to Objects
stem.Data.DataSetExtensions.dll Определяет набор типов для интеграции типов ADO.NET
в программную парадигму LINQ (LINQ to DataSet)
System.Xml.Linq.dll Предоставляет функциональность для использования
LINQ с данными документов XML (LINQ to XML)
Для того чтобы работать с LINQ to Objects, потребуется обеспечить, чтобы в
каждом файле кода С#, содержащем запросы LINQ, импортировалось пространство
имен System.Linq (определенное внутри сборки System.Core.dll). В противном случае
возникнет множество проблем. Если вы столкнулись с примерно таким сообщением об
ошибке во время компиляции:
Error 1 Could not find an implementation of the query pattern for source type ' int[] ' .
'Where' not found. Are you missing a reference to 'System.Core.dll' or a using
directive for 'System.Linq'?
Ошибка 1 He удается обнаружить реализацию шаблона запроса для исходного типа 'mt[] '.
'Where' не найдено. Возможно, пропущена ссылка на 'System.Core.dll' или
директива using для 'System.Linq'?
то очень высока вероятность, что в файле С# отсутствует следующая директива:
using System.Linq;
Применение запросов LINQ
к элементарным массивам
Чтобы приступить к изучению LINQ to Objects, давайте построим приложение,
которое будет применять запросы LINQ к различным объектам типа массива. Создадим
консольное приложение по имени LinqOverArray и определим внутри класса Program
статический вспомогательный метод под названием QueryOverStrings (). В этом
методе создадим массив строк, содержащий несколько элементов по своему усмотрению
(например, названия видеоигр). Пусть хотя бы два элемента содержат числовые значения,
и еще несколько — внутренние пробелы.
static void QueryOverStrings ()
{
// Предположим, что имеется массив строк.
string[] currentVideoGames = { "Morrowind", "Uncharted 2",
"Fallout 3", "Daxter", "System Shock 2"};
}
Теперь модифицируем Main() для вызова QueryOverStrings ():
static void Main(string [ ] args)
{
Console.WriteLine("***** Fun with LINQ to Objects *****\n");
QueryOverStrings ();
Console.ReadLine();
}
470 Часть III. Дополнительные конструкции программирования на С#
Имея дело с любым массивом данных, очень часто приходится извлекать из него
подмножество элементов на основе определенного критерия. Скажем, требуется получить
только элементы, содержащие какое-нибудь число (т.е. "System Shock 2", "Uncharted 2"
и "Fallout 3"), либо имеющие больше или меньше определенного количества символов,
либо не содержащие внутренних пробелов (т.е. "Morrowind" или "Daxter") и т.п. Хотя
такие задачи определенно можно решать с использованием членов типа System.Array
и приложением приличных усилий, выражения запросов LINQ значительно упрощают
ситуацию.
Исходя из предположения, что требуется получить из массива только элементы,
содержащие внутри себя пробел, причем в алфавитном порядке, можно построить
следующее выражение запроса LINQ:
static void QueryOverStrings ()
{
// Предположим, что имеется массив строк.
string [] currentVideoGames = {"Morrowind", "Uncharted 2",
"Fallout 3", "Daxter", "System Shock 2"};
// Выражение запроса для нахождения элементов массива, включающих пробелы.
IEnumerable<string> subset = from g in currentVideoGames
where g.Contains(" ") orderby g select g;
// Вывести на консоль результаты,
foreach (string s in subset)
Console.WriteLine("Item: {0}", s);
}
Обратите внимание, что созданное здесь выражение запроса использует операции
LINQ from, in, where, orderby и select. Формальности синтаксиса выражений
запросов будут изложены далее в этой главе. Однако даже сейчас можно прочесть этот
оператор примерно как "предоставить элементы из currentVideoGames, содержащие
пробелы, в алфавитном порядке".
Здесь каждый элемент, соответствующий критерию поиска, получает имя g (от
"game"); однако можно было бы использовать любое допустимое имя переменной С#:
IEnumerable<string> subset = from game in currentVideoGames
where game.Contains(" ") orderby
game select game;
Возвращенная последовательность хранится в переменной по имени subset, тип
которой реализует обобщенную версию интерфейса IEnumerable<T>, где Т — тип
System.String (в конце концов, опрашивается массив элементов string). Получив
результирующий набор, затем можно просто вывести на консоль его элементы, используя
стандартную конструкцию foreach. В результате запуска приложения на выполнение
будет получен следующий вывод:
***** Fun with LINQ to Objects *****
Item: Fallout 3
Item: System Shock 2
Item: Uncharted 2
Решение без использования LINQ
Строго говоря, применение LINQ никогда не бывает обязательным. При желании тот
же результирующий набор можно получить, отказавшись от LINQ и воспользовавшись
такими программными конструкциями, как операторы if и циклы for. Ниже приведен
метод, который генерирует тот же результат, что и QueryOverStrings(), но в намного
более многословной манере:
Глава 13. LINQ to Objects 471
static void QueryOverStringsLongHand()
{
// Предположим, что имеется массив строк,
string [] currentVideoGames = {"Morrowind", "Uncharted 2",
"Fallout Э", "Daxter", "System Shock 2"};
string[] gamesWithSpaces = new string[5];
for (int i=0; i < currentVideoGames.Length; i++)
{
if (currentVideoGames[i] .Contains (" "))
gamesWithSpaces [i] = currentVideoGames[i];
}
// Отсортировать набор.
Array.Sort(gamesWithSpaces);
// Вывести на консоль результат,
foreach (string s in gamesWithSpaces)
{
if ( s != null)
Console.WriteLine ("Item: {0}", s) ;
}
Console.WriteLine();
}
Несмотря на возможные пути улучшения этого метода, факт остается фактом —
запросы LINQ могут радикально упростить процесс извлечения новых подмножеств
данных из источника. Вместо построения вложенных циклов, сложной логики if/else,
временных типов данных и тому подобного, компилятор С# выполнит всю черновую
работу за вас, как только будет создан подходящий запрос LINQ.
Рефлексия результирующего набора LINQ
Теперь предположим, что в классе Program определена дополнительная
вспомогательная функция по имени ReflectOverQueryResultsO, которая выводит на консоль
разнообразные детали о результирующем наборе LINQ (обратите внимание на тип
параметра — System.Object, который позволяет принимать различные типы
результирующих наборов):
static void ReflectOverQueryResults(object resultSet)
{
Console.WriteLine ("***** info about your query *****");
Console.WriteLine("resultSet is of type: {0}", resultSet.GetType().Name); // тип
Console.WriteLine("resultSet location: {0}",
resultSet.GetType() .Assembly.GetName () .Name); // расположение
}
Поместив вызов этого метода в Query Over St rings () непосредственно после вывода
полученного подмножества и запустив приложение, можно увидеть, что это подмножество на
самом деле представляет собой экземпляр обобщенного типа OrderedEnumerable<TElement,
ТКеу> (представленного в CIL-коде как OrderedEnumerable2), который является
абстрактным внутренним типом, находящимся в сборке System.Core.dll:
***** jnfo about your query *****
resultSet is of type: OrderedEnumerableч2
resultSet location: System.Core
На заметку! Многие из типов, представляющих результат LINQ, скрыты в браузере объектов Visual
Studio 2010. Чтобы увидеть внутренние скрытые типы, воспользуйтесь утилитой ildasm.exe
или reflector.exe.
472 Часть III. Дополнительные конструкции программирования на С#
LINQ и неявно типизированные локальные переменные
Хотя в данном примере программы относительно легко догадаться, что
результирующий набор может быть интерпретирован как перечисление объектов string (т.е.
IEnumerable<string>), менее очевидно, что типом подмножества на самом деле
является OrderedEnumerable<TElement, TKey>.
Поскольку результирующие наборы LINQ могут быть представлены с
использованием изрядного числа типов из различных пространств имен LINQ, было бы
утомительно определять подходящий тип для хранения результирующего набора, потому что во
многих случаях лежащий в основе тип не очевиден и даже напрямую недоступен в коде
(и как будет показано, иногда тип генерируется во время компиляции).
Чтобы еще более подчеркнуть это обстоятельство, ниже показан дополнительный
вспомогательный метод, определенный внутри класса Program (который должен быть
вызван из метода Main()):
static void QueryOverlnts ()
{
int[] numbers = {10, 20, 30, 40, 1, 2, 3, 8};
// Вывести только элементы меньше 10.
IEnumerable<int> subset = from 1 in numbers where l < 10 select i;
foreach (int l in subset)
Console.WriteLine("Item: {0}", i);
ReflectOverQueryResults(subset) ;
}
В данном случае переменная subset будет иметь совершенно иной внутренний тип.
На этот раз тип, реализующий интерфейс IEnumerable<int> — это низкоуровневый
класс по имени WhereArrayIterator<T>:
Item: l
Item: 2
Item: 3
Item: 8
***** Info about your query *****
resultSet is of type: WhereArraylterator% 1
resultSet location: System.Core
Поскольку точный тип запроса LINQ определенно неизвестен, в первом примере
результат запроса был представлен как IEnumerable<T>, где Т — тип данных
возвращенной последовательности (string, int и т.п.). Это все еще довольно запутано. Еще
более усложняет картину то, что поскольку IEnumerable<T> расширяет необобщенный
интерфейс IEnumerable, получить результат запроса LINQ допускается следующим
образом:
System.Collections.IEnumerable subset =
from i in numbers where i < 10 select i;
К счастью, неявная типизация существенно проясняет картину при работе с
запросами LINQ:
static void QueryOverlnts ()
{
int[] numbers = {10, 20, 30, 40, 1, 2, 3, 8};
// Здесь используется неявная типизация...
var subset = from i in numbers where i < 10 select i;
// . . .и здесь тоже.
foreach (var i m subset)
Console.WriteLine("Item: {0} ", l);
ReflectOverQueryResults(subset);
}
Глава 13. LINQ to Objects 473
В качестве эмпирического правила: при захвате результатов запроса LINQ всегда
следует применять неявную типизацию. Однако помните, что (в большинстве случаев)
реальное возвращенное значение имеет тип, реализующий интерфейс IEnumerable<T>.
Какой именно тип кроется за этим (OrderedEnumerable<TElement, TKey>,
WhereArrayIterator<T> и т.п.) — не важно, и определять его не обязательно. Как было
показано в предыдущем примере кода, можно просто воспользоваться ключевым
словом var с конструкцией f oreach для проведения итерации по полученным данным.
LINQ и расширяющие методы
Хотя в рассматриваемом примере не требуется явно писать какие-то
расширяющие методы, на самом деле они используются на заднем плане. Выражения запросов
LINQ могут применяться для итерации по содержимому контейнеров данных, которые
реализуют обобщенный интерфейс IEnumerable<T>. Однако класс .NET System.Array
(используемый для представления массива строк и массива целых чисел) не реализует
этот контракт:
// Тип System.Array, похоже, не реализует корректную
// инфраструктуру для выражений запросов!
public abstract class Array : ICloneable, IList, ICollection,
IEnumerable, IStructuralComparable, IStructuralEquatable
{
}
Несмотря на то что System. Array не реализует напрямую интерфейс IEnumerable<T>,
он опосредовано получает необходимую функциональность этого типа (а также
многие другие члены, связанные с LINQ) через статический тип класса System.Linq.
Enumerable.
В этом служебном классе определено множество обобщенных расширяющих
методов (таких как Aggregate<T>(), First<T>, Max<T> и т.д.), которые System.Array (и
другие типы) получают в свое распоряжение на заднем плане. Таким образом, после ввода
операции точки за текущей локальной переменной currentVideoGames обнаружится
значительное количество членов, которые отсутствуют в формальном определении
System.Array (рис. 13.1).
.;;LinqOverArr«). Program
^•QueiyOverttnngsO
stringf] currentVideoGames ■ {"Могrewind", "Uncharted 2",
"Fallout 3", "Dexter", "System Shock 2"};
// 4uild a query expression to find the items in the arra;
// that have an embedded space.
var subset ■ from g in currentVideoGames
where g.Contains(" ")
orderby g
select g;
currentVi^eoGamesJ
** Aggregate-
stat:
;
/,' Print out the п ф ЩШ/Ш
foreach (var s in ; *. .
Cwuolfi.WriteLia* . ' U
i _i^ .л ,'** AsEnumerableo
Console.WriteLine(; *
ReflectOverQueryR* *♦ AsParallel
\ AsParallelo
; *# AsQueryable
ic void QueryOverS *^ AsQueryableo
;•«, Average<>
f] A«um* we have | «ь Cest<>
string[ ] currentvt ф c|one
"Fallout 3". -%СопсЛ<>
string g.«esKith^°"'-S<>
i *• СоруТо щ
(ейетюп) boolKmirn*fable<TSotirce>jyi<TSoufce>{Func<TSoufce,booJ> predicate) 1
Determines whether all elements ol a sequence satisfy a condition.
Exceptions;
System-ArgumeniNullExceptiofl
ted 2",
i < currentVideoGames.Length; i++)
Рис. 13.1. Тип System.Array был расширен членами System.Linq.Enumerable
474 Часть III. Дополнительные конструкции программирования на С#
Роль отложенного выполнения
Другой важный момент, касающийся выражений запросов LINQ, состоит в том, что
на самом деле они не выполняются до тех пор, пока не будет начата итерация по
последовательности. Формально это называется отложенным выполнением. Преимущество
такого подхода заключается в возможности применения одного и того же запроса LINQ
многократно к одному и тому же контейнеру, с полной гарантией получения самых
актуальных результатов. Рассмотрим следующую модификацию метода Query0verlnts():
static void QueryOverlnts ()
{
int[] numbers = { 10, 20, 30, 40, 1, 2, 3, 8 };
// Получить числа меньше 10.
var subset = from i in numbers where i < 10 select i;
// Оператор LINQ здесь выполняется!
foreach (var i in subset)
Console.WriteLine ("{0} < 10", i) ;
Console.WriteLine ();
// Изменить некоторые данные в массиве.
numbers[0] = 4;
// Оператор LINQ снова выполняется!
foreach (var j in subset)
Console.WriteLine ("{0} < 10", j);
Console.WriteLine ();
ReflectOverQueryResults(subset);
}
Ниже показан вывод после запуска этой программы. Обратите внимание, что во
второй итерации по запрошенной последовательности появился дополнительный член,
поскольку для первого элемента массива было установлено значение меньше 10.
1
2
3
8
4
1
2
3
8
<
<
<
<
<
<
<
<
<
10
10
10
10
10
10
10
10
10
Среда Visual Studio 2010 обладает одним очень полезным аспектом: поместив точку
останова перед выполнением запроса LINQ, можно просматривать содержимое в
сеансе отладки. Переместите курсор мыши на переменную результирующего набора LINQ
(subst на рис. 13.2). После этого появляется возможность выполнить запрос в этот
момент, раскрыв узел Results View (Представление результатов).
Pfogrem.cs X I
^LinqOvwArray.Program
* j ^#QueryOverStrings0
/J Build a querj
/7 that have an
У/ Print out the
foreach (var s in sul
Consolfi.WriteLii
Consolo.WriteLine();
ibl
;5»-stem.(.inq.<>dere4E'
■;sMng,si
*.;'
Рис. 13.2. Отладка выражений LINQ в Visual Studio 2010
Глава 13. LINQ to Objects 475
Роль немедленного выполнения
Чтобы выполнить выражение LINQ за пределами логики итерации for each, можно
вызвать любое количество расширяющих методов, определенных типом Enumerable,
таких как ToArry<T>, ToDictionary<TSource,TKey>() и ToList<T>(). Все эти методы
заставляют запрос LINQ выполняться в момент их вызова, чтобы получить снимок
данных. После этого полученным снимком можно манипулировать независимо.
static void ImmediateExecution ()
{
int[] numbers = { 10, 20, 30, 40, 1, 2, 3, 8 };
// Получить данные НЕМЕДЛЕННО как int[].
int[] subsetAsIntArray =
(from l in numbers where l < 10 select l) .ToArray<int> ();
// Получить данные НЕМЕДЛЕННО как List<int>.
List<int> subsetAsListOfInts =
(from l in numbers where i < 10 select i) .ToList<int> () ;
}
Обратите внимание, что все выражение LINQ заключено в скобки для приведения
к корректному лежащему в основе типу (каким бы он ни был); это позволяет вызывать
методы Enumerable.
Также вспомните из главы 10, что когда компилятор С# может однозначно
определить параметр типа обобщенного элемента, то вы не обязаны указывать этот
параметр. Таким образом, ТоАггау<Т> (или ToList<T>) можно было бы вызвать следующим
образом:
int[] subsetAsIntArray =
(from i in numbers where l < 10 select i) . ToArrayO;
Польза от немедленного выполнения очевидна, когда нужно вернуть результат
запроса LINQ внешнему вызывающему коду. Это будет темой следующего раздела главы.
Исходный код. Проект LinqOverArray доступен в подкаталоге Chapter 13.
Возврат результата запроса LINQ
В классе (или структуре) можно определить поле, значением которого будет
результат запроса LINQ. Однако для этого нельзя использовать неявную типизацию (т.е.
ключевое слово var для таких полей применяться не может), и целью запроса LINQ не могут
быть данные уровня экземпляра, а потому он должен быть статическим. Учитывая эти
ограничения, писать код вроде показанного ниже придется редко:
class LINQBasedFieldsAreClunky
{
private static string[] currentVideoGames = {"Morrowind", "Uncharted 2",
"Fallout 3", "Daxter", "System Shock 2"};
// Здесь нельзя использовать неявную типизацию1 Нужно знать тип subset1
private IEnumerable<string> subset = from g in currentVideoGames
where g.Contains(" ") orderby g select g;
public void PrintGamesO
{
foreach (var item in subset)
{
Console.WriteLine(item);
}
}
}
476 Часть III. Дополнительные конструкции программирования на С#
Довольно часто запросы LINQ определяются в контексте метода или свойства. Более
того, для упрощения переменная, предназначенная для хранения результирующего
набора, будет храниться в неявно типизированной локальной переменной с
использованием ключевого слова var. Вспомните из главы 3, что неявно типизированные
переменные не могут применяться для определения параметров, возвращаемых значений или
полей класса либо структуры.
С учетом всего этого может возникнуть вопрос: как вернуть результат запроса
внешнему коду? Все зависит от обстоятельств. Если есть результирующий набор,
состоящий из строго типизированных данных, такой как массив строк или список List<T>
объектов Саг, можно отказаться от ключевого слова var и использовать
правильный тип IEnumerable<T> либо IEnumerable (поскольку IEnumerable<T> расширяет
IEnumerable). Ниже показан пример нового консольного приложения под названием
LinqRetValues.
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** LINQ Transformations *****\n");
IEnumerable<string> subset = GetStringSubset();
foreach (string item in subset)
{
Console.WriteLine(item);
}
Console.ReadLine()/
}
static IEnumerable<string> GetStringSubset ()
{
string[] colors = {"Light Red11, "Green",
"Yellow", "Dark Red", "Red", "Purple"};
// Обратите внимание, что subset представляет собой
// объект, совместимый с IEnumerable<string>.
IEnumerable<string> theRedColors = from c in colors
where с.Contains("Red") select c;
return theRedColors;
}
}
Результат выглядит следующим образом:
LINQ Transformations
Light Red
Dark Red
Red
Возврат результатов LINQ через немедленное выполнение
Этот пример работает ожидаемым образом только потому, что возвращаемое
значение GetStringSubset() и запрос LINQ внутри этого метода строго типизированы.
Если воспользоваться ключевым словом var для определения переменной subset,
то вернуть значение можно будет, только если метод по-прежнему возвращает
IEnumerable<string> (и если неявно типизированная локальная переменная на самом
деле совместима с указанным типом возврата).
Поскольку оперировать IEnumerable<T> было бы несколько неудобно, можно
использовать немедленное выполнение. Например, вместо возврата IEnumerable<string>
возможно просто вернуть string[], при условии трансформирования
последовательности в строго типизированный массив.
Глава 13. LINQ to Objects 477
Ниже показан новый метод класса Program, который именно это и делает:
static string[] GetStringSubsetAsArray ()
{
string[] colors = {"Light Red", "Green",
"Yellow", -"Dark Red", "Red", "Purple"};
var theRedColors = from с in colors
where с.Contains("Red") select c;
// Отобразить результаты в массив,
return theRedColors.ToArray ();
}
При этом вызывающему коду не известно, что полученный им результат поступил от
запроса LINQ, и он просто работает с массивом строк, как ожидалось. Например:
foreach (string item in GetStringSubsetAsArray())
{
Console.WriteLine(item);
}
Немедленное выполнение также важно при попытке вернуть вызывающему коду
результат проекции LINQ. Чуть позже в этой главе мы еще вернемся к этой теме. Однако
сначала давайте посмотрим, как применять запросы LINQ к обобщенным и
необобщенным объектам коллекций.
Исходный код. Проект LinqRetValues доступен в подкаталоге Chapter 13.
Применение запросов LINQ к объектам коллекций
Помимо извлечения результатов из простого массива данных, выражения запросов
LINQ также манипулируют данными внутри членов коллекций из пространства имен
System.Collections.Generic, таких как List<T>. Создадим новое консольное
приложение по имени ListOverCollections и определим базовый класс Саг,
поддерживающий текущую скорость, цвет, производителя и дружественное имя, как показано ниже:
class Car
{
public string PetName {get; set;}
public string Color {get; set;}
public int Speed {get; set;}
public string Make {get; set;}
}
Теперь определим в методе Main() переменную List<T> для хранения элементов
типа Саг и воспользуемся синтаксисом инициализации объектов для заполнения
списка несколькими новыми объектами Саг:
static void Main(string [ ] args)
{
Console.WriteLine (••***** LINQ over Generic Collections *****\n");
// Создать список Listo объектов Car.
List<Car> myCars = new List<Car>() {
new Car{ PetName = "Henry", Color = "Silver", Speed = 100, Make = "BMW"},
new Car{ PetName = "Daisy", Color = "Tan", Speed = 90, Make = "BMW"},
new Car{ PetName = "Mary", Color = "Black", Speed = 55, Make = "VW" },
new Car{ PetName = "Clunker", Color = "Rust", Speed = 5, Make = "Yugo" },
new Car{ PetName = "Melvin", Color = "White", Speed = 43, Make = "Ford"}
};
Console.PeadLine();
}
478 Часть III. Дополнительные конструкции программирования на С#
Доступ к содержащимся в контейнере подобъектам
Применение запроса LINQ к обобщенному контейнеру ничем не отличается от
применения к простому запросу, поскольку LINQ to Objects может использоваться с любым
типом, реализующим IEnumerable<T>. На этот раз цель заключается в построении
выражения запроса для получения объектов Саг из списка myCars, у которых значение
скорости больше 55.
После получения подмножества на консоль будет выводиться имя каждого объекта
Саг за счет обращения к его свойству Pet Name. Предположим, что определен
следующий вспомогательный метод (принимающий параметр List<Car>), который
вызывается в Main():
static void GetFastCars(List<Car> myCars)
{
// Найти в контейнере Listo объекты Car, у которых Speed больше 55.
var fastCars = from с in myCars where c. Speed > 55 select c;
foreach (var car in fastCars)
{
Console.WriteLine ("{0 } is going too fast!", car.PetName);
}
}
Обратите внимание, что выражение запроса выбирает только те элементы из
List<T>, у которых Speed больше 55. Запустив приложение, можно увидеть, что
критерию поиска отвечают только два элемента — "Henry" и "Daisy".
Чтобы построить более сложный критерий, можно запросить только автомобили
марки BMW со значением Speed больше 90. Для этого понадобится создать составной
булевский оператор, в котором используется С#-операция &&:
static void GetFastBMWs(List<Car> myCars)
{
// Найти быстрые автомобили BMW!
var fastCars = from с in myCars where c. Speed > 90 && c.Make == "BMW" select c;
foreach (var car in fastCars)
{
Console.WriteLine ("{ 0 } is going too fast!", car.PetName);
}
}
В этом случае на консоль выводится только одно имя — "Henry".
Применение запросов LINQ к необобщенным коллекциям
Вспомните, что операции запросов LINQ предназначены для работы с любым
типом, реализующим IEnumerable<T> (как непосредственно, так и через расширяющие
методы). Учитывая то, что класс System.Array оснащен всей необходимой
инфраструктурой, может оказаться сюрпризом, что унаследованные (необобщенные) контейнеры
из System.Collections такой поддержки не имеют. К счастью, итерацию по данным,
содержащимся внутри необобщенных коллекций, все равно можно выполнять с
использованием обобщенного расширяющего метода Enumerable.OfType<T>().
Метод OfType<T>() — один из нескольких членов Enumerable, которые не
расширяют обобщенные типы. При вызове этого члена на необобщенном контейнере,
реализующем интерфейс IEnumerable (вроде Array List), просто укажите тип элемента в
контейнере для извлечения совместимого с IEnumerable<T> объекта. Сохранить эти
данные в коде можно в неявно типизированной переменной.
Глава 13. LINQ to Objects 479
Ниже показан новый метод, который заполняет ArrayList множеством объектов Саг
(не забудьте импортировать пространство имен System.Collections в файл Program.cs).
static void LINQOverArrayList ()
{
Console. WriteLine (••***** LINQ no ArrayList *****");
// Необобщенная коллекция автомобилей.
ArrayList myCars = new ArrayList () {
new Car{ PetName = "Henry", Color = "Silver", Speed = 100, Make = "BMW"},
new Car{ PetName = "Daisy", Color = "Tan", Speed = 90, Make = "BMW"},
new Car{ PetName = "Mary", Color = "Black", Speed = 55, Make = "VW"},
new Car{ PetName = "Clunker", Color = "Rust", Speed = 5, Make = "Yugo" },
new Car{ PetName = "Melvin11, Color = "White", Speed = 43, Make = "Ford"}
};
// Трансформировать ArrayList в тип, совместимый с IEnumerable<T>.
var myCarsEnum = myCars.OfType<Car>();
// Создать выражение запроса, нацеленное на совместимый с IEnumerable<T> тип.
var fastCars = from с in myCarsEnum where с. Speed > 55 select c;
foreach (var car in fastCars)
{
Console.WriteLine ("{0} is going too fast!", car.PetName);
}
}
Аналогично предыдущим примерам, этот метод, вызванный в Main(), отобразит
только имена "Henry" и "Daisy" на основе формата нашего запроса LINQ.
Фильтрация данных с использованием Of Туре<т>()
Как уже известно, необобщенные типы способны содержать комбинации любых
элементоЁ в качестве членов контейнеров (таких как ArrayList), поскольку они про-
тотипированы для приема экземпляров System.Object. Например, предположим,
что ArrayList содержит различные элементы, часть из которых являются числами.
Получить подмножество, состоящее только из числовых данных, можно с помощью
метода OfType<T>(), поскольку во время итераций он отфильтрует все элементы, тип
которых отличается от заданного.
static void OfTypeAsFilter ()
{
// Извлечение целых из ArrayList.
ArrayList myStuff = new ArrayList ();
myStuff .AddRange (new object [] { 10, 400, 8, false, newCarO, "string data" });
var mylnts = myStuff.OfType<int>();
// Выведет на консоль 10, 400 и 8.
foreach (int i in mylnts)
{
Console.WriteLine("Int value: {0}", l) ;
}
}
Отлично! Теперь есть возможность применять запросы LINQ к массивам,
обобщенным и необобщенным коллекциям. Эти контейнеры содержат как элементарные типы
С# (целочисленные, строковые данные), так и пользовательские специальные классы.
Следующая задача — изучить множество дополнительных операций LINQ, которые
можно использовать для построения сложных и полезных запросов.
Исходный код. Проект LinqOverCollectons доступен в подкаталоге Chapter 13.
480 Часть III. Дополнительные конструкции программирования на С#
Исследование операций запросов LINQ
В языке С# определено множество операций запросов в готовом виде. Некоторые
наиболее часто используемые из них перечислены в табл. 13.3.
На заметку! Документация .NET Framework 4.0 SDK содержит подробные сведения по каждой
операции С# LINQ (см. раздел "LINQ General Programming Guide" ("Общее руководство
программирования на LINQ")).
Таблица 13.3. Различные операции запросов LINQ
Операции запросов Назначение
from, in Используется для определения основы любого выражения LINQ,
позволяющей извлечь подмножество данных из нужного контейнера
where Используется для определения ограничений о том, т.е. какие
элементы должны извлекаться из контейнера
select Используется для выбора последовательности из контейнера
join, on, equals, into Выполняет соединения на основе указанного ключа. Помните, что
эти "соединения" ничего не делают с данными в реляционных базах
orderby, ascending, Позволяет упорядочить результирующий набор в порядке возраста-
descending ния или убывания
group, by Порождает подмножество с данными, сгруппированными по
указанному значению
В дополнение к частичному списку операций, приведенному в табл. 13.3, класс
System.Linq.Enumerable предлагает набор методов, которые не имеют прямой
сокращенной нотации в форме операций запроса С#, а представлены в виде расширяющих
методов. Эти обобщенные методы могут вызываться для трансформации
результирующего набора в различной манере (Reverse<>(), ToArray<>(), ToList<>() и т.п.).
Некоторые из них используются для извлечения одиночных элементов из
результирующего набора, другие выполняют различные операции над множествами (Distinct<>(),
Union<>(), Intersect<>() и т.п.), третьи агрегируют результаты (Count<>(), Sum<>(),
Min<>(), Max<>() и т.п.).
Чтобы приступить к исследованию более сложных запросов LINQ, создадим новое
консольное приложение под названием FunWithLinqExpressions. Затем определим
массив или коллекцию некоторых простых данных. Для данного проекта создается
массив объектов ProductInfo, определенный в следующем коде:
class ProductInfo
{
public string Name {get; set;}
public string Description {get; set;}
public int NumberlnStock {get; set; }
public override string ToStringO
{
return string.Format("Name={0}, Description={l}, Number in Stock={2}",
Name, Description, NumberlnStock);
}
}
Теперь внутри метода Main() заполним массив объектами ProductInfo:
Глава 13. LINQ to Objects 481
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Query Expressions *****\nM),
// Этот массив будет основой для тестирования...
ProductInfo[] itemsInStock = new[] {
new ProductInfo! Name = "Mac's Coffee",
Description = "Coffee with TEETH",
NumberlnStock =24},
new ProductInfo! Name = "Milk Maid Milk",
Description = "Milk cow's love",
NumberlnStock = 100},
new ProductInfo! Name = "Pure Silk Tofu",
Description = "Bland as Possible",
NumberlnStock = 120},
new ProductInfo! Name = "Cruchy Pops",
Description = "Cheezy, peppery goodness",
NumberlnStock =2},
new ProductInfo! Name = "RipOff Water",
Description = "From the tap to your wallet",
NumberlnStock = 100},
new ProductInfo! Name = "Classic Valpo Pizza",
Description = "Everyone loves pizza!",
NumberlnStock = 73}
};
// Здесь будем вызывать разнообразные методы!
Console.ReadLine ();
}
Базовый синтаксис выборки
Поскольку синтаксическая корректность выражения запроса LINQ проверяется во
время компиляции, следует помнить, что порядок этих операций важен. В простейшем
виде каждый запрос LINQ строится из операций from, in и select. Ниже показан
базовый шаблон, которому нужно следовать:
var результат = from сопоставляемыйЭлемент in контейнер
select сопоставляемыйЭлемент;
Элемент, следующий за операцией from, представляет элемент, соответствующий
критерию запроса LINQ, и назвать его можно по своему усмотрению. Элемент после
операции in представляет контейнер данных для поиска (массив, коллекция или
документ XML). Рассмотрим очень простой запрос, который не делает ничего помимо
извлечения каждого элемента контейнера (это похоже на поведение SQL-оператора SELECT *
в базе данных):
static void SelectEverything(ProductInfo [ ] products)
{
// Получить все!
Console .WriteLine ("All product details:11);
var allProducts = from p in products select p;
foreach (var prod in allProducts)
{
Console.WriteLine(prod.ToString());
}
}
По правде говоря, это выражение запроса не слишком полезно, поскольку выдаст
подмножество, идентичное содержимому входного параметра. При желании из
входящего параметра можно извлечь только значения Name каждого товара, используя
следующий синтаксис выборки:
482 Часть III. Дополнительные конструкции программирования на С#
static void ListProductNames(ProductInfo[] products)
{
// Теперь получить только наименования товаров.
Console.WriteLine ("Only product names:");
var names = from p in products select p.Name;
foreach (var n in names)
{
Console.WriteLine("Name: {0}", n) ;
}
}
Получение подмножества данных
Чтобы получить определенное подмножество из контейнера, можно воспользоваться
операцией where. При этом общий шаблон запроса становится таким:
var результат = from элемент in контейнер where булевскоеВыражение
select элемент;
Обратите внимание, что операция where ожидает выражения, которое возвращает
булевское значение. Например, чтобы получить из массива-аргумента ProductInfo[]
только те позиции, которых на складе есть более 25 единиц, можно написать
следующий код:
static void GetOverstock(ProductInfo[] products)
{
Console.WriteLine("The overstock items!");
// Получить только те товары, которых на складе более 25.
var overstock = from p in products where p.NumberlnStock > 25 select p;
foreach (ProductInfo с in overstock)
{
Console.WriteLine(c.ToString() ) ;
}
}
Как уже было показано ранее в этой главе, при построении конструкции where
допускается применять любые операции С# для получения сложных выражений. Например,
вспомним запрос, который извлекал только автомобили BMW, которые едут со
скоростью выше 100 миль в час:
// Получить машины BMW, движущиеся со скоростью свыше 100 миль в час.
var onlyFastBMWs = from с in myCars
where с.Make == "BMW" && с Speed >= 100 select c;
foreach (Car с in onlyFastBMWs)
{
Console.WriteLine("{0} is going {1} MPH", c.PetName, c.Speed);
}
Проекция новых типов данных
Новые формы данных можно также проектировать на основе существующих
источников. Предположим, что для входящего параметра ProductInfo[] необходимо
получить результирующий набор, который содержит только имя и описание каждого
элемента. Для этого понадобится определить оператор select, который динамически
породит новый анонимный тип:
static void GetNamesAndDescriptions (ProductInfo [ ] products)
{
Console.WriteLine("Names and Descriptions:");
var nameDesc = from p in products select new { p.Name, p.Description };
Глава 13. LINQ to Objects 483
foreach (var item in nameDesc)
{
// Можно также использовать свойства Name и Description напрямую.
Console.WriteLine(item.ToString());
}
}
Всегда помните, что когда имеете дело с запросом LINQ, выполняющим проекцию,
нет никакой возможности узнать лежащий в ее основе тип данных, поскольку он
определяется во время компиляции. В этих случаях обязательным является ключевое слово
var. Кроме того, нельзя создавать методы с неявно типизированными возвращаемыми
значениями. Поэтому следующий код не скомпилируется:
static var GetProjectedSubset ()
{
ProductInfo[] products = new[] {
new ProductInfo! Name = "Mac's Coffee",
Description = "Coffee with TEETH",
NumberlnStock =24},
new ProductInfo! Name = "Milk Maid Milk",
Description = "Milk cow's love",
NumberlnStock = 100},
new ProductInfo! Name = "Pure Silk Tofu",
Description = "Bland as Possible",
NumberlnStock = 120},
new ProductInfo! Name = "Cruchy Pops",
Description = "Cheezy, peppery goodness",
NumberlnStock =2},
new ProductInfo! Name = "RipOff Water",
Description = "From the tap to your wallet",
NumberlnStock = 100},
new ProductInfo! Name = "Classic Valpo Pizza",
Description = "Everyone loves pizza!",
NumberlnStock =73}
};
var nameDesc = from p in products select new ! p.Name, p.Description };
return nameDesc; // Ошибка!
}
Когда требуется вернуть проекцию данных вызывающему коду, один из подходов
предусматривает трансформацию результата запроса в .NET-объект System.Array за
счет применения расширяющего метода ТоАггауО. Таким образом, если
модифицировать выражение запроса следующим образом:
// Теперь возвращаемое значение - Array.
static Array GetProjectedSubset ()
!
// Отобразить набор анонимных объектов на объект Array,
return nameDesc.ToArray ();
}
то его можно вызвать и обработать данные в методе Main() следующим образом:
Array objs = GetProjectedSubset ();
foreach (object o in objs)
!
Console.WriteLine(о); // Вызывает ToStringO на каждом анонимном объекте.
}
484 Часть III. Дополнительные конструкции программирования на С#
Обратите внимание, что должен использоваться объект System.Array, при этом
нельзя применять синтаксис объявления массива С#. Это связано с тем, что лежащий
в основе проекции тип не известен, поскольку речь идет об анонимном классе, который
сгенерирован компилятором. Кроме того, не указывается параметр типа для
обобщенного метода ТоАггу<>Т(), поскольку он тоже не известен до времени компиляции.
Очевидная проблема здесь состоит в утере строгой типизации, поскольку каждый
элемент в объекте Array считается относящимся к типу Object. Тем не 'менее, когда
нужно вернуть результирующий набор LINQ, полученный в результате операции
проекции, трансформация данных в тип Array (или другой подходящий контейнер через
другие члены типа Enumerable) обязательна.
Получение счетчиков посредством Enumerable
При проектировании новых пакетов данных может понадобиться знать
количество элементов перед тем, как вернуть их в последовательности. Для определения числа
элементов, возвращаемых выражением запроса LINQ, используется расширяющий
метод Count () класса Enumerable. Например, следующий метод пересчитает все объекты
string в локальном массиве, имеющие длину более шести символов:
static void GetCountFromQuery ()
{
string[] currentVideoGames = {"Morrowind", "Uncharted 2",
"Fallout 3", "Daxter", "System Shock 2"};
// Получить количество из запроса,
int numb =
(from g in currentVideoGames where g.Length > 6 select g).Count<string>();
// Вывести на консоль количество элементов.
Console.WriteLine ("{0} items honor the LINQ query.", numb);
}
Обращение результирующих наборов
Поменять порядок элементов в результирующем наборе на противоположный
довольно легко с помощью расширяющего метода Reverse<T>() класса Enumerable.
Например, в следующем методе выбираются все элементы из входного массива
ProductInfo[] в обратном порядке:
static void ReverseEverything(ProductInfo[] products)
{
Console.WriteLine("Product in reverse:");
var allProducts = from p in products select p;
foreach (var prod in allProducts.Reverse())
{
Console.WriteLine(prod.ToString());
}
}
Выражения сортировки
В начальных примерах настоящей главы было показано, что выражение запроса
может принимать операцию or derby для сортировки элементов в подмножестве по
заданному значению. По умолчанию принят порядок по возрастанию, поэтому упорядочение
строк производится в алфавитном порядке, числовых значений — от меньшего к
большему, и т.д. Чтобы отсортировать в обратном порядке, укажите операцию descending.
Рассмотрим следующий метод:
Глава 13. LINQ to Objects 485
static void AlphabetizeProductNames(ProductInfo [ ] products)
{
// Получить названия товаров в алфавитном порядке.
var subset = from p in products orderby p.Name select p;
Console.WriteLine("Ordered by Name:");
foreach (var p in subset)
{
Console.WriteLine(p.ToString());
}
}
Хотя порядок по возрастанию принят по умолчанию, можно прояснить намерения,
явно указав операцию ascending:
var subset = from p in products orderby p.Name ascending select p;
Для получения элементов в порядке убьшания служит операция descending:
var subset = from p in products orderby p.Name descending select p;
LINQ как лучшее средство построения диаграмм
Класс Enumerable поддерживает набор расширяющих методов, которые позволяют
использовать два (или более) запроса LINQ в качестве основы для нахождения
объединений, разностей, конкатенации и пересечений данных. Прежде всего, рассмотрим
расширяющий метод Except(). Этот метод возвращает результирующий набор LINQ,
содержащий разность между двумя контейнерами, которым в данном случае является
значение "Yugo":
static void DisplayDiff()
{
List<string> myCars = new List<String> {"Yugo", "Aztec", "BMW"};
List<string> yourCars = new List<String>{"BMW", "Saab", "Aztec" };
var carDiff =(from с in myCars select c)
.Except(from c2 in yourCars select c2);
Console.WriteLine("Here is what you don't have, but I do:");
foreach (string s in carDiff)
Console.WriteLine (s) ; // Выводит Yugo.
}
Метод Intersect () возвращает результирующий набор, содержащий общие
элементы данных для множества контейнеров. Например, следующий метод вернет
последовательность из "Aztec" и "BMW".
static void Displaylntersection ()
{
List<string> myCars = new List<String> { "Yugo", "Aztec", "BMW" };
List<string> yourCars = new List<String> { "BMW", "Saab", "Aztec" };
// Получить общие члены.
var carlntersect = (from с in myCars select c)
.Intersect(from c2 in yourCars select c2);
Console.WriteLine("Here is what we have in common:");
foreach (string s in carlntersect)
Console.WriteLine(s) ; // Выводит Aztec и BMW.
}
Метод Union() возвращает результирующий набор, который включает все члены
множества запросов LINQ. Подобно любому объединению, повторяющиеся члены в нем
встречаются только однажды. Поэтому следующий метод выводит на консоль значения
"Yugo", 'Aztec", "BMW" и "Saab":
486 Часть III. Дополнительные конструкции программирования на С#
static void DisplayUnion ()
{
List<string> myCars = new List<String> { "Yugo", "Aztec", "BMW" };
List<string> yourCars = new List<String> { "BMW", "Saab", "Aztec" };
// Получить объединение двух контейнеров.
var carUnion = (from с in myCars select c)
.Union(from c2 in yourCars select c2);
Console.WriteLine ("Here is everything:");
foreach (string s in carUnion)
Console.WriteLine (s); // Выводит все общие члены.
}
Наконец, расширяющий метод ConcatO возвращает результирующий набор,
представляющий собой прямую конкатенацию результирующих наборов LINQ. Например,
следующий метод выводит на консоль в качестве результата "Yugo", "Aztec", "BMW",
"BMWVSaab" и "Aztec":
static void DisplayConcat ()
{
List<string> myCars = new List<String> { "Yugo", "Aztec", "BMW" };
List<string> yourCars = new List<String> { "BMW", "Saab", "Aztec" };
var carConcat = (from с in myCars select c)
.Concat(from c2 in yourCars select c2);
// Выводит Yugo Aztec BMW BMW Saab Aztec,
foreach (string s in carConcat)
Console.WriteLine (s);
Исключение дубликатов
После вызова расширяющего метода Cone at () очень легко можно получить в
результате излишние элементы, что иногда является тем, что и требовалось. В других случаях
может понадобиться удалить дублированные элементы данных. Для этого необходимо
вызвать расширяющий метод DistinctO, как показано ниже:
static void DisplayConcatNoDups ()
{
List<string> myCars = new List<String> { "Yugo", "Aztec", "BMW" };
List<string> yourCars = new List<String> { "BMW", "Saab", "Aztec" };
var carConcat = (from с in myCars select c)
.Concat(from c2 in yourCars select c2) ;
// Выводит Yugo Aztec BMW Saab.
foreach (string s in carConcat.Distinct ())
Console.WriteLine (s);
}
Агрегатные операции LINQ
Запросы LINQ могут также выполнять различные агрегатные операции над
результирующим набором. Одним из примеров агрегации может служить расширяющий
метод Count (). Другие возможности включают получение среднего, максимума, минимума
или суммы значений за счет использования методов Мах(), Min(), Average(), Sum(),
которые являются членами класса Enumerable. Ниже приведен простой пример:
static void AggregateOps ()
{
double[] winterTemps = { 2.0, -21.3, 8, -4, 0, 8.2 };
// Различные примеры агрегации.
Console.WriteLine("Max temp: {0}",
(from t in winterTemps select t).Max());
Глава 13. LINQ to Objects 487
Console.WriteLine("Min temp: {0}",
(from t in winterTemps select t).Min());
Console.WriteLine("Avarage temp: {0}я,
(from t in winterTemps select t) .Average ());
Console.WriteLine("Sum of all temps: {0}",
(from t in winterTemps select t) .Sum());
}
Эти примеры должны предоставить достаточно сведений, чтобы вы могли
уверенно приступить к построению собственных выражений запросов LINQ. Существуют и
дополнительные операции, которые пока еще не рассматривались; они будут описаны
позже, когда речь пойдет о связанных технологиях LINQ. В завершение вводного
экскурса в LINQ, оставшаяся часть главы посвящена деталям отношений между
операциями запросов LINQ и лежащей в основе объектной моделью.
Исходный код. Проект FunWithLinqExpressions доступен в подкаталоге Chapter 13.
Внутреннее представление
операторов запросов LINQ
К настоящему моменту вам уже знаком процесс построения выражений запросов с
использованием различных операций запросов С# (таких как from, in, where, or derby
и select). Также вам известно, что некоторая функциональность API-интерфейса
LINQ to Objects может быть доступна только при вызове расширяющих методов
класса Enumerable. Реальность, однако, в том, что компилятор С# на этапе компиляции
транслирует все операции С# LINQ в вызовы методов класса Enumerable. Фактически
это означает, что при желании можно строить все операторы LINQ, не используя ничего
помимо лежащей в основе объектной модели.
Подавляющее большинство методов Enumerable прототипированы так, чтобы
принимать в качестве аргументов делегаты. В частности, многие методы требуют
обобщенного делегата по имени Fun с о, определенного в пространстве имен System и
находящегося в сборке System.Core.dll. Например, посмотрите на метол Where() класса
Enumerable, который вызывается автоматически, когда используется операция where
С# LINQ:
// Перегруженные версии метода Enumerable.Where<T>() .
// Обратите внимание, что второй параметр имеет тип System. Funco.
public static IEnumerable<TSource> Where<TSource> (
this IEnumerable<TSource> source, System.Func<TSource,int,bool> predicate)
public static IEnumerable<TSource> Where<TSource> (
this IEnumerable<TSource> source, System.Func<TSource,bool> predicate)
Делегат Funco (как следует из его имени) представляет шаблон функции с
набором аргументов и возвращаемым значением. Просмотрев этот тип в браузере объектов
Visual Studio 2010, можно заметить, что делегат Funco принимает от нуля до четырех
входных аргументов (типизированных как Т1, Т2, ТЗ и Т4, и названных argl, arg2, arg3
и агд4) и возвращает тип, обозначенный TResult:
// Различные форматы делегата Funco.
public delegate TResult Func<Tl,T2,T3,T4,TResult>(Tl argl, T2 arg2, ТЗ агдЗ, Т4 arg4)
public delegate TResult Func<Tl,T2,T3,TResult>(Tl argl, T2 arg2, T3 arg3)
public delegate TResult Func<Tl,T2,TResult>(Tl argl, T2 arg2)
public delegate TResult Func<Tl,TResult> (Tl argl)
public delegate TResult Func<TResult> ()
488 Часть III. Дополнительные конструкции программирования на С#
Поскольку множество членов System.Linq.Enumerable требуют при вызове в
качестве входа делегат, можно либо вручную создать новый тип делегата и разработать для
него необходимые целевые методы, воспользоваться анонимным методом С# либо
определить подходящее лямбда-выражение. Независимо от выбранного подхода, конечный
результат будет одним и тем же.
Хотя простейший способ построения запросов LINQ предусматривает использование
операций запросов С# LINQ, давайте все-таки кратко рассмотрим все возможные
подходы, просто чтобы увидеть связь между операциями запросов С# и лежащим в основе
типом Enumerable.
Построение выражений запросов
с использованием операций запросов
Для начала создадим новое консольное приложение по имени LinqUsingEnumerable.
В классе Program будет определен набор статических вспомогательных методов
(каждый из которых вызывается из метода Main()) для иллюстрации различных способов
построения выражений запросов LINQ.
Первый метод — QueryStringsWithOperators () — обеспечивает наиболее
прямолинейный способ построения выражений запросов и идентичен коду примера
LinqOverArray, приведенному ранее в этой главе:
static void QueryStringWithOperators()
{
Console.WriteLine ("***** Using Query Operators *****");
string[] currentVideoGames = {"Morrowind", "Uncharted 2",
"Fallout 3", "Daxter", "System Shock 2"};
var subset = from game in currentVideoGames
where game.Contains (" ") orderby game select game;
foreach (string s in subset)
Console.WriteLine("Item: {0}", s) ;
}
Очевидное преимущество применения операций запросов С# при построении
выражений запросов связано с тем, что делегаты Funco и вызовы типа Enumerable
остаются вне поля зрения и внимания, поскольку работа компилятора С# состоит в
выполнении необходимой трансляции. Несомненно, создание выражений LINQ с
использованием различных операций запросов (from, in, where или orderby) является
наиболее распространенным и простым подходом.
Построение выражений запросов с использованием
типа Enumerable и лямбда-выражений
Имейте в виду, что используемые здесь операции запросов LINQ — это на самом деле
сокращенные версии вызова различных расширяющих методов, определенных в типе
Enumerable.
Рассмотрим следующий метод QueryStringsWithEnumerableAndLambdasO,
который обрабатывает локальный массив строк, но на этот раз в нем напрямую
применяются расширяющие методы Enumerable:
static void QueryStringsWithEnumerableAndLambdas()
{
Console.WriteLine("***** Using Enumerable / Lambda Expressions *****");
string[] currentVideoGames = {"Morrowind", "Uncharted 2",
"Fallout 3", "Daxter", "System Shock 2"};
Глава 13. LINQ to Objects 489
// Построить выражение запроса с использованием расширяющих методов,
// предоставленных типу Array через тип Enumerable,
var subset = currentVideoGames.Where(game => game.Contains (" "))
.OrderBy(game => game).Select(game => game);
// Вывести "на консоль результаты,
foreach (var game in subset)
Console.WriteLine ("Item: {0}", game);
Console.WriteLine ();
}
Код начинается с вызова расширяющего метода Where () на строковом массиве
currentVideoGames. Вспомните, что класс Array получает этот расширяющий метод
от класса Enumerable. Метод Enumerable.Where() требует параметра-делегата System.
Func<Tl, TResultx Первый параметр типа этого делегата представляет совместимые
с IEnumerable<T> данные для обработки (в рассматриваемом случае это массив строк),
в то время как второй — результирующие данные метода, которые получаются от
единственного оператора, вставленного в лямбда-выражение.
Возвращаемое значение метода Where () в этом примере кода скрыто от глаз, но "за
кулисами" работа происходит с типом OrderedEnumerable. На этом объекте
вызывается обобщенный метод OrderBy (), который также принимает параметр — делегат
Fun с о. На этот раз производится передача всех элементов по очереди через
соответствующее лямбда-выражение. Конечным результатом вызова OrderBy () будет новая
упорядоченная последовательность начальных данных.
И, наконец, производится вызов метода Select () на последовательности,
возвращенной OrderBy (), который в конечном итоге вернет результирующий набор данных,
сохраняемый в неявно типизированной переменной по имени subset.
Приведенный запрос LINQ несколько сложнее понять, чем предыдущий пример с
операциями запросов С# LINQ. Часть этой сложности, конечно, связана с вызовами
через операцию точки. Ниже показан в точности тот же запрос, но с выделением каждого
шага в отдельный фрагмент:
static void QueryStringsWithEnumerableAndLambdas2 ()
{
Console.WriteLine("***** Using Enumerable / Lambda Expressions *****");
string[] currentVideoGames = {"Morrowind", "Uncharted 2",
"Fallout 3", "Daxter", "System Shock 2"};
// Разделить на фрагменты!
var gamesWithSpaces = currentVideoGames.Where(game => game.Contains(" "));
var orderedGames = gamesWithSpaces.OrderBy(game => game);
var subset = orderedGames.Select(game => game);
foreach (var game in subset)
Console.WriteLine("Item: {0}", game);
Console.WriteLine();
}
Как видите, построение выражения запроса LINQ с прямым использованием методов
класса Enumerable намного более многословно, чем с применением операций запросов
С#. Кроме того, поскольку методы Enumerable требуют параметров-делегатов, обычно
необходимо писать лямбда-выражения, чтобы обеспечить обработку входных данных
лежащей в основе целью делегата.
490 Часть III. Дополнительные конструкции программирования на С#
Построение выражений запросов с использованием
типа Enumerable и анонимных методов
Учитывая, что лямбда-выражения С# — это просто сокращенная нотация вызова
анонимных методов, рассмотрим третье выражение запроса во вспомогательном методе
QueryStringsWithAnonymousMethods():
static void QueryStringsWithAnonymousMethods ()
{
Console.WriteLine ("***** Using Anonymous Methods *****");
string[] currentVideoGames = {"Morrowind", "Uncharted 2",
"Fallout 3", "Daxter", "System Shock 2"};
// Построить необходимые делегаты Funco с использованием анонимных методов.
Func<stnng, bool> searchFilter =
delegate(string game) { return game.Contains(" "); };
Func<string, string> itemToProcess = delegate(string s) { return s; };
// Передать делегаты в методы Enumerable,
var subset = currentVideoGames.Where(searchFilter)
.OrderBy(itemToProcess) .Select (itemToProcess);
// Вывести на консоль результаты,
foreach (var game in subset)
Console.WriteLine("Item: {0}", game);
Console.WriteLine();
}
Этот вариант выражения запроса еще более многословен, потому что делегаты
Funco создаются вручную с использованием методов Where(), OrderBy () и Select()
класса Enumerable. Положительная сторона этого подхода связана с тем, что синтаксис
анонимных методов позволяет заключить всю обработку, выполняемую делегатами, в
одном определении метода. Тем не менее, этот метод функционально эквивалентен
методам QueryStringsWithEnumerableAndLambdasO и QueryStringsWithOperatorsO,
которые рассматривались в предыдущих разделах.
Построение выражений запросов с использованием
типа Enumerable и низкоуровневых делегатов
Наконец, чтобы построить выражение запроса, используя по-настоящему
многословный подход, можно отказаться от применения синтаксиса лямбда-выражений и
анонимных методов и непосредственно создавать цели делегатов для каждого типа
Fun с о. Ниже показана финальная итерация выражения запроса, смоделированная
внутри нового типа класса по имени VeryComplexQueryExpression:
class VeryComplexQueryExpression
{
public static void QueryStringsWithRawDelegates()
{
Console.WriteLine ("***** Using Raw Delegates *****");
string[] currentVideoGames = {"Morrowind", "Uncharted 2",
"Fallout 3", "Daxter", "System Shock 2"};
// Построить необходимые делегаты Funco.
Func<stnng, bool> searchFilter = new Func<string, bool> (Filter) ;
Func<string, string> itemToProcess = new Func<string,string>(Processltem);
// Передать делегаты в методы Enumerable,
var subset = currentVideoGames
.Where(searchFilter) .OrderBy(itemToProcess) .Select(itemToProcess) ;
// Вывести на консоль результаты,
foreach (var game in subset)
Console.WriteLine ("Item: {0}", game);
Глава 13. LINQ to Objects 491
Console.WriteLine();
}
// Цели делегатов.
public static bool Filter(string game) {return game.Contains (" ");}
public static string Processltem(string game) { return game; }
}
Чтобы протестировать эту итерацию логики обработки строк, нужно вызвать этот
метод в Main () класса Program следующим образом:
VeryComplexQueryExpression.QueryStringsWithRawDelegates ();
Если теперь запустить приложение, чтобы опробовать все возможные подходы,
вывод окажется идентичным, независимо от выбранного пути. Относительно
выражений запросов и их скрытого представления следует помнить перечисленные ниже
моменты.
• Выражения запросов создаются с использованием различных операций
запросов С#.
• Операции запросов — это просто сокращенная нотация вызова расширяющих
методов, определенных в типе System.Linq.Enumerable.
• Многие методы Enumerable принимают в качестве параметров делегаты (в
частности, Func<>).
• Любой метод, ожидающий параметр-делегат, может принимать вместо него
лямбда-выражение.
• Лямбда-выражения — это просто замаскированные анонимные методы (которые
значительно улучшают читабельность).
• Анонимные методы являются сокращенной нотацией для размещения
низкоуровневого делегата и ручного построения целевого метода делегата.
Приведенная выше информация поможет понять, что на самом деле происходит "за
кулисами" дружественных к пользователю операций запросов С#.
Исходный код. Проект LinqOverArrayUsingEnumerable доступен в подкаталоге Chapter 13.
Резюме
LINQ — это набор взаимосвязанных технологий, которые были разработаны для
обеспечения единой, симметричной манеры взаимодействия с различными формами
данных. Как объяснялось на протяжении этой главы, LINQ может взаимодействовать с
любым типом, реализующим интерфейс IEnumerable<T>, включая простые массивы, а
также обобщенные и необобщенные коллекции данных.
Было показано, что работа с технологиями LINQ обеспечивается несколькими
средствами языка С#. Например, учитывая тот факт, что выражения запросов LINQ могут
возвращать любое количество результирующих наборов, принято использовать
ключевое слово var для представления лежащего в основе типа данных. Кроме того, лямбда-
выражения, синтаксис инициализации объектов и анонимные типы позволяют строить
очень функциональные и компактные запросы LINQ.
Что более важно, вы увидели, что операции запросов С# LINQ на самом деле
являются просто сокращенными нотациями вызовов статических членов типа System.Linq.
Enumerable. Как было показано, большинство членов Enumerable оперируют типами
делегатов Func<T>, которые могут принимать адреса литеральных методов, анонимные
методы или лямбда-выражения в качестве ввода для выполнения запроса.
ЧАСТЬ IV
Программирование
с использованием
сборок .NET
В этой части...
Глава 14. Конфигурирование сборок .NET
Глава 15. Рефлексия типов, позднее связывание и программирование
с использованием атрибутов
Глава 16. Процессы, домены приложений и контексты объектов
Глава 17. Язык CIL и роль динамических сборок
Глава 18. Динамические типы и исполняющая среда динамического языка
ГЛАВА 14
Конфигурирование
сборок .NET
Каждое из приложений, продемонстрированных в предыдущих главах,
разрабатывалось как обычное "автономное" приложение с размещением всей специальной
логики внутри единственного исполняемого файла (* . ехе). Одной из главных
особенностей платформы .NET является возможность повторного использования двоичного
кода, когда в приложениях эксплуатируются типы, содержащиеся внутри нескольких
внешних сборок (также называемых библиотеками кода). В этой главе речь пойдет о
создании, развертывании и конфигурировании сборок в .NET.
Сначала будет показано, как создавать пространства имен в .NET, и в чем состоит
разница между однофайловыми и многофайловыми, а также приватными и
разделяемыми сборками. Затем будет описано, как в исполняющей среде определяется
местонахождение сборки, и что собой представляет глобальный кэш сборок (Global Assembly
Cache — GAC), конфигурационные файлы приложений (файлы * . con fig), сборки
политик издателя и пространство имен System. Configuration.
Определение специальных пространств имен
Прежде чем углубляться в детали развертывания и конфигурирования сборок,
сначала необходимо узнать о том, как создавать специальные пространства имен в .NET.
Вплоть до этого момента в книге разрабатывались лишь небольшие
демонстрационные программы, в которых использовались существующие пространства имен в мире
.NET (в частности, пространство имен System). При создании собственных
приложений, однако, может быть удобно группировать свои взаимосвязанные типы в
специальные пространства имен. В С# это делается с помощью ключевого слова namespace.
Определение специальных пространств имен явным образом начинает играть даже еще
более важную роль при создании dll-сборок в .NET, поскольку другие разработчики
должны иметь возможность импортировать эти специальные пространства имен, чтобы
использовать содержащиеся в них типы.
Чтобы увидеть все это на практике, создадим новый проект типа Console Application
(Консольное приложение) по имени СиstomNamespaces. Предположим, что
требуется разработать коллекцию геометрических классов Square (квадрат), Circle (круг) и
Hexagon (шестиугольник) и, благодаря имеющимся в них схожим чертам,
сгруппировать их вместе и разместить внутри сборки CustomNamespaces . ехе в уникальном
пространстве имен MyShapes. Для реализации на выбор доступны два основных подхода.
Во-первых, можно определить все соответствующие классы в единственном файле С#
(ShapesLib. cs), как показано ниже:
Глава 14. Конфигурирование сборок .NET 495
// Файл Shapeslib.cs
using System;
namespace MyShapes
{
// Класс Circle
public class Circle{ /* Интересные члены. . . */ }
// Класс Hexagon
public class Hexagon{ /* Более интересные члены. . . */ }
// Класс Square
public class Square{ /* Даже еще более интересные члены... */ }
}
Во-вторых, можно разбить одно пространство имен на несколько файлов кода С#.
Чтобы обеспечить размещение типов в одной и той же логической группе, нужно просто
упаковать определения всех данных классов в контекст одного и того же пространства
имен:
// Файл Circle.cs
using System;
namespace MyShapes
{
// Класс Circle
public class Circle { /* Интересные методы... */ }
}
// Файл Hexagon.сs
using System;
namespace MyShapes
{
// Класс Hexagon
public class Hexagon { /* Более интересные методы... */ }
}
// Файл Square.cs
using System;
namespace MyShapes
{
// Класс Square
public class Square { /* Даже еще более интересные методы... */ }
}
В обоих случаях важно обратить внимание, что пространство MyShapes выступает
в роли своего рода концептуального "контейнера" для данных классов. Если в другом
пространстве имен (например, СиstomNamespaces) возникает необходимость в импорте
типов, определенных внутри данного пространства имен, достаточно просто
воспользоваться ключевым словом using точно так же, как и при использовании типов,
определенных в библиотеках базовых классов .NET:
II Использование типов, определенных в пространстве имен MyShape.
using System;
using MyShapes;
namespace CustomNamespaces
{
public class Program
{
static void Main(string [ ] args)
{
Hexagon h = new Hexagon () ;
Circle с = new Circle ();
Square s = new Square ();
}
496 Часть IV. Программирование с использованием сборок .NET
В этом примере предполагается, что файл (или файлы) с кодом С#, в котором
содержится определение пространства имен MyShapes, является частью того же проекта типа
Console Application, что и файл с определением пространства имен CustomNamespaces.
Другими словами, все эти файлы будут использоваться для компиляции единственной
исполняемой сборки .NET.
Если пространство имен MyShapes определено внутри какой-то внешней сборки,
для успешной компиляции может потребоваться добавить ссылку на эту библиотеку.
Создание приложений, предусматривающих взаимодействие с внешними
библиотеками, рассматривается далее в настоящей главе.
Устранение конфликтов на уровне имен за счет
использования полностью уточненных имен
С технической точки зрения применять в С# ключевое слово using при ссылках на
типы, определенные во внешних пространствах имен, вовсе не требуется. Вместо этого
можно использовать полностью уточненное имя типа, под которым, как
рассказывалось в главе 1, понимается имя типа с идущим перед ним в качестве префикса
названием пространства имен, где этот тип определен:
// Обратите внимание, что пространство имен MyShapes больше не импортируется.
using System;
namespace CustomNamespaces
{
public class Program
{
static void Main(string[] args)
{
MyShapes.Hexagon h = new MyShapes.Hexagon ();
MyShapes.Circle с = new MyShapes.Circle ();
MyShapes.Square s = new MyShapes.Square ();
}
}
}
Обычно необходимости в применении полностью уточненного имени не возникает.
Это требует большего объема клавиатурного ввода, при этом никак не влияет на размер
и скорость выполнения кода. С другой стороны, в CIL-коде типы всегда определяются с
использованием полностью уточненных имен. В результате применение в С# ключевого
слова using, по сути, позволяет просто экономить время.
Тем не менее, иногда применение полностью уточненных имен может оказываться
очень удобным (а то и вообще необходимым) для избегания конфликтов на уровне имен,
которые могут возникать при использовании множества пространств имен и наличии в
этих пространствах имен одинаково названных типов. Для примера предположим, что
появилось новое пространство имен под названием My3DShapes, в котором содержатся
определения трех классов, способных визуализировать фигуры в трехмерном формате:
// Еще одно пространство имен для работы с фигурами.
using System;
namespace My3DShapes
{
// Класс трехмерного круга.
public class Circle { }
// Класс трехмерного многоугольника.
public class Hexagon { }
// Класс трехмерного квадрата.
public class Square { }
}
Глава 14. Конфигурирование сборок .NET 497
Если теперь модифицировать класс Program, как показано ниже, компилятор
сообщит о множестве ошибок, связанных с тем, что в обоих пространствах имен
присутствуют классы с идентичными именами:
// Много неоднозначностей!
using System;
using MyShapes;
using My3DShapes;
namespace CustomNamespaces
{
public class Program
{
static void Main (string [ ] args)
{
//На какое пространство имен производится ссылка?
Hexagon h = new Hexagon(); // Компилятор сообщит об ошибке!
Circle с = new Circle(); // Компилятор сообщит об ошибке!
Square s = new Square(); // Компилятор сообщит об ошибке!
}
}
}
Использование полностью уточненных имен позволяет устранить все эти
неоднозначности:
// Неоднозначности устранены.
static void Main(string[] args)
{
My3DShapes.Hexagon h = new My3DShapes.Hexagon ();
My3DShapes.Circle с = new My3DShapes.Circle();
MyShapes.Square s = new MyShapes.Square();
}
Устранение конфликтов на уровне имен
за счет использования псевдонимов
В С# ключевое слово using также позволяет создавать псевдоним для полностью
уточенного имени типа. В этом случае определяется маркер, на месте которого во время
компиляции должно подставляться полностью уточненное имя. Определение
псевдонимов является вторым способом разрешения конфликтов на уровне имен.
using System;
using MyShapes;
using My3DShapes;
// Устранение неоднозначности за счет применения специального псевдонима.
using The3DHexagon = My3DShapes.Hexagon;
namespace CustomNamespaces
{
class Program
{
static void Main(string[] args)
{
// Здесь на самом деле создается
// класс My 3D Shapes .Hexagon.
The3DHexagon h2 = new The3DHexagon();
}
}
- 1
498 Часть IV. Программирование с использованием сборок .NET
Такой альтернативный синтаксис using позволяет создавать псевдонимы и для
имеющих длинные названия пространств имен. Например, одним из подобных
пространств имен в библиотеке базовых классов является System. Runtime. Serialization.
Formatters .Binary, в котором содержится член по имени BinaryFormatter. При
желании экземпляр BinaryFormatter можно создать, как показано ниже:
using bfHome = System.Runtime.Serialization.Formatters.Binary;
namespace MyApp
{
class ShapeTester
{
static void Main(string[] args)
{
bfHome.BinaryFormatter b = new bfHome.BinaryFormatter();
}
}
}
Допускается также применением обычной директивы using:
using System.Runtime.Serialization.Formatters.Binary;
namespace MyApp
{
class ShapeTester
{
static void Main(string [ ] args)
{
BinaryFormatter b = new BinaryFormatter ();
}
}
}
На данном этапе беспокоиться о том, для чего служит класс BinaryFormatter, не
нужно (этот класс подробно рассматривается в главе 20). Пока что главное уяснить то,
что ключевое слово using в С# позволяет создавать псевдонимы для длинных
полностью уточненных имен и потому может применяться для разрешения конфликтов на
уровне имен, которые могут возникать в результате импорта пространств имен,
содержащих типы с идентичными именами.
На заметку! Следует иметь в виду, что чрезмерное использование псевдонимов в С# может
привести к получению запутанной кодовой базы. Если другие программисты в команде не знают
об этих специальных псевдонимах, они могут посчитать, что данные псевдонимы ссылаются на
типы в библиотеках базовых классов .NET, и прийти в замешательство, не найдя их описаний в
документации .NET 4.0 Framework SDK.
Создание вложенных пространств имен
При организации типов допускается определять пространства имен внутри других
пространств имен. В библиотеках базовых классов .NET подобное встречается во
многих местах и обеспечивает размещение типов на более глубоких уровнях. Например,
пространство имен 10 находится внутри пространства имен System и охватывает
возможности System. 10. Чтобы создать корневое пространство имен, содержащее внутри
себя существующее пространство имен My3DShapes, необходимо модифицировать код
следующим образом:
// Создание вложенного пространства имен.
namespace Chapterl4
Глава 14. Конфигурирование сборок .NET 499
{
namespace My3DShapes
{
// Класс трехмерного круга.
class Circle { }
// Класс трехмерного шестиугольника.
class Hexagon { }
// Класс трехмерного квадрата.
class Square { }
}
}
Во многих случаях роль корневого пространства имен заключается просто в
предоставлении дальнейшего уровня контекста, и потому внутри него самого не могут
содержаться определения каких-либо типов (как в случае пространства имен Chapter 14).
Когда дело обстоит именно так, вложенные пространства могут определяться внутри
него следующим компактным способом:
// Создание вложенного пространства имен (другой способ).
namespace Chapterl4.My3DShapes
{
// Класс трехмерного круга.
class Circle { }
// Класс трехмерного шестиугольника.
class Hexagon { }
// Класс трехмерного квадрата.
class Square { }
}
Из-за того, что теперь пространство My3DShapes вложено в корневое пространство
имен Chapterl4, необходимо соответствующим образом обновить все существующие
директивы using и псевдонимы типов:
using Chapterl4.My3DShapes;
using The3DHexagon = Chapterl4.My3DShapes.Hexagon;
Пространство имен, используемое
по умолчанию в Visual Studio 2010
В завершение темы пространств имен стоит обратить внимание, что при создании
нового проекта на С# в Visual Studio 2010 используемому по умолчанию
пространству имен назначается точно такое же название, как у проекта. После этого при
вставке в проект новых файлов кода (выбирая в меню Project (Проект) пункт Add New Item
(Добавить новый элемент)) все соответствующие типы автоматически помещаются
внутрь этого пространства имен. Чтобы изменить его название, откройте окно свойств
проекта, перейдите в нем на вкладку Application (Приложение) и введите желаемое имя в
поле Default namespace (Пространство имен по умолчанию), как показано на рис. 14.1.
После такого изменения любой новый элемент, который будет вставляться в проект,
будет размещаться внутри пространства имен MyDefaultNamespace (разумеется, для
использования его типов в другом пространстве имен понадобится указать
соответствующую директиву using).
Пока все идет хорошо. Теперь, когда мы рассмотрели некоторые детали упаковки
специальных типов в организованные пространства имен, давайте посмотрим на
преимущества и формат сборок .NET, а после этого перейдем к исследованию деталей
создания, развертывания и конфигурирования приложений.
Исходный код. Проект CustomNamespaces доступен в подкаталоге Chapter 14.
500 Часть IV. Программирование с использованием сборок .NET
CustomNamespaces* X Щ
рннаниийши
Application*
Build
Build Events
Debug
Resources
Services
Settings
Reference Paths
Signing
'
•] Platfoim. [N/A
Assembly name:
CustomNamespaces
Target framework:
Default namespace:
MyDefaultNamespacej J
Output type
NET Framework 4 Client Profile
| Console Application
Startup object
(Not set}
Resources
Specify how application resources will be managed:
' Icon and manifest
A manifest determines specific settings for an application. To embed a custom manifest, first add И •»-
Рис. 14.1. Изменение названия пространства имен, используемого по умолчанию
Роль сборок .NET
Приложения .NET создаются за счет складывания вместе некоторого количества сборок.
Сборка представляет собой поддерживающий версии самоописываемый двоичный файл,
обслуживаемый CLR (Common Language Runtime — общеязьшовая исполняющая среда).
Хотя сборки .NET имеют точно такие же файловые расширения (* . ехе или * . dll), как и
старые двоичные файлы Windows (в том числе и унаследованные серверы СОМ), внутри
они устроены совсем иначе. Поэтому прежде чем углубляться в изучение дальнейшего
материала, давайте сначала посмотрим, какие преимущества предлагает формат сборок.
Сборки повышают возможность повторного использования кода
При построении консольных приложений в предыдущих главах могло показаться, что
вся функциональность этих приложений содержалась внутри создававшейся
исполняемой сборки. На самом деле во всех этих приложениях использовались многочисленные
типы из всегда доступной библиотеки кода .NET по имени ms cor lib. dll (вспомините,
что компилятор С# добавляет ссылку Hamscorlib.dll автоматически), а в некоторых
случаях также и из библиотеки по имени System.Windows.Forms.dll.
Как известно, библиотека кода (также называемая библиотекой классов]
представляет собой файл с расширением * .dll, в котором содержатся типы, предназначенные
для использования во внешних приложениях. При создании исполняемых сборок,
несомненно, придется использовать много системных и специальных библиотек кода по
мере разработки текущего приложения. Однако следует учесть, что библиотека кода не
обязательно имеет вид файла с расширением * . dll. Вполне допускается (хотя и не
часто), чтобы в исполняемой сборке применялись типы, определенные внутри какого-то
внешнего исполняемого файла. В таком случае упоминаемый файл * .ехе тоже может
считаться библиотекой кода. Какой бы вид не имела библиотека кода, платформа .NET
позволяет многократно использовать типы не зависящим от языка образом. Например,
можно не только размещать типы на различных языках, но и наследовать от них.
Базовый класс, определенный на языке С#, может расширять класс, написанный на
Visual Basic. Интерфейсы, определенные на языке Pascal .NET, могут реализовать
структуры, определенные в С#, и т.д. Главное понять, что разбиение единого монолитного
исполняемого файла на несколько сборок .NET открывает возможности для повторного
использования кода в не зависящей от языка манере.
Глава 14. Конфигурирование сборок .NET 501
Сборки определяют границы типов
Вспомните, что полностью уточненное имя типа получается за счет добавления к
его собственному имени (которое может, например, выглядеть как Console) в качестве
префикса названия пространства имен, к которому тип относится (например, System).
Сборка, в которой тип размещен, далее определяет его идентичность. Например, две
сборки с уникальными именами (скажем, MyCars.dll и YourCars.dll), в обеих из
которых определено пространство имен (CarLibrary), содержащее класс по имени
SportsCar, в мире .NET будут считаться совершенно отдельными типами.
Сборки являются единицами, поддерживающими версии
Сборкам .NET присваивается состоящий из четырех частей числовой номер
версии в формате кстаришй номер>.<младший номер>.<номер сборки>.<номер редакции>.
(Если номер версии явно не указан, сборке автоматически назначается номер 1.0.0.0
из-за применяемых по умолчанию в Visual Studio настроек проекта.) Этот номер
вместе с необязательным значением открытого ключа позволяет множеству версий одной
и той же сборки сосуществовать на одной и той же машине. Формально сборки, в
которых предоставляется значение открытого ключа, называются строго именованными.
Как будет показано далее, при наличии строгого имени CLR-среда гарантирует загрузку
корректной версии сборки от имени вызывающего клиента.
Сборки являются самоописываемыми
Сборки считаются самоописываемыми (self-describing) отчасти потому, что содержат
информацию о каждой из внешних сборок, к которой им нужно иметь доступ, чтобы
функционировать надлежащим образом. Следовательно, если сборке требуется доступ
к библиотекам System.Windows.Forms.dll и System.Drawing.dll, это обязательно
будет документировано в ее манифесте. Вспомните из главы 1, что манифестом
называется блок метаданных, который описывает саму сборку (ее имя, версию, необходимые
внешние сборки и т.д.).
Помимо данных манифеста, в сборке также содержатся метаданные, которые
описывают структуру каждого из имеющихся в ней типов (имена членов, реализуемые
интерфейсы, базовые классы, конструкторы и т.п.). Благодаря наличию в самой сборке
такого детального описания, CLR-среде не требуется заглядывать в системный реестр
Windows для выяснения ее местонахождения (что радикально отличается от
предлагавшейся Microsoft ранее модели программирования СОМ). Как будет показано в ходе
настоящей главе, вместо этого в CLR-среде применяется совершенно щрвая схема для
получения информации о местонахождении внешних библиотек кода.
Сборки поддаются конфигурированию
Сборки могут развертываться как "приватные" (private) или как "разделяемые"
(shared). Приватные сборки размещаются в том же каталоге (или подкаталоге), что и
клиентское приложение, в котором они используются. Разделяемые сборки, с другой
стороны, представляют собой библиотеки, предназначенные для использования во многих
приложениях на одной и той же машине, и потому развертываются в специальном
каталоге, который называется глобальным кэшем сборок (Global Assembly Cache — GAC).
При любом способе развертывания для сборок могут быть предусмотрены
специальные конфигурационные XML-файлы. С их помощью можно указать CLR-среде, где
конкретно искать сборки, какую версию той или иной сборки загружать для определенного
клиента или в какой каталог на локальной машине, папку в сети или URL-адрес
заглядывать. Конфигурационные XML-файлы еще будут рассматриваться в настоящей главе.
502 Часть IV. Программирование с использованием сборок .NET
Формат сборки .NET
Ознакомившись с некоторыми преимуществами сборок .NET, давайте более детально
рассмотрим, как эти сборки устроены внутри. С точки зрения структуры любая сборка
.NET (* . dll или * . ехе) включает в себя следующие элементы.
• заголовок файла Windows;
• заголовок файла CLR;
• CIL-код;
• метаданные типов;
• манифест сборки;
• дополнительные встроенные ресурсы.
Хотя первые два элемента представляют собой такие блоки данных, на которые
обычно можно не обращать внимания, краткого рассмотрения они все-таки
заслуживают. Ниже предлагаются краткие описания всех перечисленных элементов.
Заголовок файла Windows
Заголовок файла Widows указывает на тот факт, что сборка может загружаться и
использоваться в операционных системах семейства Windows. Кроме того, этот заголовок
идентифицирует тип приложения (консольное, приложение с графическим
пользовательским интерфейсом или библиотека кода * . dll). Чтобы просмотреть информацию,
содержащуюся в заголовке Windows, необходимо открыть сборку .NET в утилите dumpbin. ехе
(в окне командной строки Visual Studio 2010) с указанием флага /headers:
dumpbin /headers CarLibrary.dll
Ниже показано (не полностью), как будет выглядеть информация в заголовке Windows
сборки CarLibrary. dll, которая разрабатывается далее в главе (можете запустить
утилиту dumpbin. ехе, указав на месте CarLibrary имя любого из ранее созданных файлов
* .dll или * .ехе):
Dump of file CarLibrary.dll
РЕ signature found
File Type: DLL
FILE HEADER VALUES
14C machine (x86)
3 number of sections
4B37DCD8 time date stamp Sun Dec 27 16:16:56 2009
0 file pointer to symbol table
0 number of symbols
EO size of optional header
2102 characteristics
Executable
32 bit word machine
DLL
OPTIONAL HEADER VALUES
10B magic # (PE32)
8.00 linker version
E00 size of code
600 size of initialized data
0 size of uninitialized data
2CDE entry point @0402CDE)
2000 base of code
Глава 14. Конфигурирование сборок .NET 503
4000 base of data
400000 image base @0400000 to 00407FFF)
2000 section alignment
200 file alignment
-«4.00 operating system version
0.00 image version
4.00 subsystem version
0 Win32 version
8000 size of image
200 size of headers
0 checksum
3 subsystem (Windows CUI)
Данные дампа файла CarLibrary.dll
Обнаружена подпись РЕ
Тип файла: DLL
Значения заголовка файла
Машина: 14С (х86)
Количество разделов: 3
Дата и время 4B37DCD8: Воскр Декабрь 21 16:16:56 2009
Файловых указателей на таблицу символов: О
Количество символов:О
Размер необязательного заголовка: ЕО
Характеристики 2102:
Исполняемый файл
Машина с 32-битным словом
DLL
Необязательные значения заголовка
Магическое число (РЕ32): 10В
Версия редактора связей: 8.00
Размер кода: Е00
Размер инициализированных данных: 600
Размер неинициализированных данных: О
Точка входа: 2CDE @0402CDE)
База кода: 2000
База данных: 4000
База образа: 400000 (с 00400000 по 00407FFF)
Выравнивание в разделах: 2000
Выравнивание в файле: 200
Версия операционной системы 4.00
Версия образа: 0.00
Версия подсистемы 4.00
Версия Win32: О
Размер образа: 8000
Размер заголовков: 200
Контрольная сумма: О
Подсистема: 3 (консольный интерфейс пользователя Windows)
Теперь, важно уяснить, что большей части программистов, работающей с .NET,
никогда не придется беспокоиться о формате встраиваемых в сборку .NET данных
заголовков. Если только не требуется разрабатывать новый компилятор для одного из языков
.NET (где такая информация действительно важна), можно не вникать в тонкие детали
данных заголовков. Однако помните, что эта информация используется "за кулисами",
когда Windows загружает двоичный образ в память.
504 Часть IV. Программирование с использованием сборок .NET
Заголовок файла CLR
Заголовок CLR представляет собой блок данных, который должны обязательно
поддерживать все сборки .NET (что они и делают, благодаря компилятору С#) для того,
чтобы они могли обслуживаться в CLR-среде. Вкратце, в этом заголовке определяются
многочисленные флаги, которые позволяют исполняющей среде разобраться в компоновке
управляемого файла. Например, эти флаги указывают, где внутри файла находятся
метаданные и ресурсы, как выглядит версия исполняющей среды, на которую
ориентировалась данная сборка, каково (необязательное) значение открытого ключа, и т.д. Чтобы
просмотреть данные заголовка CLR в сборке .NET, необходимо открыть сборку в утилите
dumpbin.exe с указанием флага /clrheader:
dumpbin /clrheader CarLibrary.dll
Ниже показано, как будут выглядеть данные заголовка CLR в этой сборке .NET:
Dump of file CarLibrary.dll
File Type: DLL
clr Header:
4 8 cb
2.05 runtime version
2164 [ A74] RVA [size] of MetaData Directory
1 flags
IL Only
0 entry point token
0] RVA [size] of Resources Directory
0] RVA [size] of StrongNameSignature Directory
0] RVA [size] of CodeManagerTable Directory
0] RVA [size] of VTableFixups Directory
0] RVA [size] of ExportAddressTableJumps Directory
0] RVA [size] of ManagedNativeHeader Directory
Данные дампа файла CarLibrary.dll
Тип файла: DLL
Заголовок clr:
48 cb
Версия исполняющей среды: 2.05
2164 [ А74] RVA [размер] каталога MetaData
Флагов: 1
Только IL-код
Маркер точки входа: 0
0] RVA [размер] каталога Resources
0] RVA [размер] каталога StrongNameSignature
0] RVA [размер] каталога CodeManagerTable
0] RVA [размер] каталога VTableFixups
0] RVA [размер] каталога ExportAddressTableJumps
0] RVA [размер] каталога ManagedNativeHeader
Опять-таки, разработчики приложений .NET не должны беспокоиться о тонких
деталях заголовков CLR. Главное понимать, что такие данные обязательно содержатся в
каждой сборке .NET и применяются исполняющей средой .NET при загрузке образа в
память. Теперь рассмотрим информацию, которая является гораздо более полезной при
решении повседневных программистских задач.
Summary
2000
2000
2000
0
0
0
0
0
0
.reioc
. rsrc
.text
Сводка
2000
2000
2000
0
0
0
0
0
0
.reloc
. rsrc
. text
[
[
[
[
[
[
Глава 14. Конфигурирование сборок .NET 505
CIL-код, метаданные типов и манифест сборки
В своей основе любая сборка содержит код на языке CIL, который, как уже
рассказывалось ранее, представляет собой промежуточный язык, не зависящий ни от
платформы, ни от процессора. Во время выполнения внутренний CIL-код "на лету"
(посредством ЛТ-компилятора) компилируется в инструкции, соответствующие требованиям
конкретной платформы и процессора. Благодаря этому, сборки .NET в
действительности могут выполняться в рамках разнообразных архитектур, устройств и операционных
систем. (Хотя разработчики .NET-приложений могут спокойно жить и работать, не
разбираясь в деталях языка программирования CIL, в главе 17 все-таки будут приведены
базовые сведения о синтаксисе и семантике этого языка.)
В любой сборке содержатся метаданные, которые полностью описывают формат
находящихся внутри нее типов, а также внешних типов, на которые она ссылается.
В исполняющей среде .NET эти метаданные используются для выяснения того, в
каком месте внутри двоичного файла находятся типы (и их члены), для размещения
типов в памяти и для упрощения процесса удаленного вызова методов. Более подробно
о формате этих метаданных будет рассказываться в главе 16 при рассмотрении служб
рефлексии.
Кроме того, в любой сборке должен обязательно содержаться ассоциируемый с ней
манифест (по-другому еще называемый метаданными сборки). В этом манифесте
описан каждый входящий в состав сборки модуль, версия сборки, а также любые
внешние сборки, на которые ссылается текущая сборка (что отличает сборки от библиотек
типов СОМ, в которых не существовало никакой возможности для документирования
внешних зависимостей). Как будет показано далее в главе, манифест сборки
интенсивно применяется в CLR-среде во время определения места, на которое указывают
представляющие внешние сборки ссылки.
На заметку! Как будет объясняться позже в главе, термин "модуль .NET" применяется для описания
частей в многофайловой сборке.
Необязательные ресурсы сборки
И, наконец, в любой сборке .NET может также содержаться любое количество
вложенных ресурсов, таких как значки приложения, графические файлы, звуковые фрагменты
или таблицы строк. На самом деле платформа .NET поддерживает возможность
создания даже так называемых подчиненных сборок (satellite assemblies), содержащих только
локализованные ресурсы и больше ничего. Эта возможность может быть очень удобной,
когда необходимо разделить ресурсы по конкретным культурам (английской, немецкой
и т.п.) при построении программного обеспечения, которое предназначено для
использования по всему миру. Рассмотрение процесса создания подчиненных сборок выходит за
рамки настоящей книги; о вставке ресурсов приложения в сборку будет немного
рассказываться в главе 30 при описании API-интерфейса Windows Presentation Foundation.
Однофайловые и многофайловые сборки
С технической точки зрения сборка может состоять из нескольких так называемых
модулей (module). Модуль на самом деле представляет собой не более чем просто
общий термин, применяемый для обозначения действительного двоичного файла .NET.
В большинстве ситуаций, однако, сборка состоит из единственного модуля. В этом
случае между (логической) сборкой и лежащим в ее основе (физическим) двоичным файлом
существует соответствие типа "один к одному" (откуда и происходит термин "однофай-
ловая сборка").
506 Часть IV. Программирование с использованием сборок .NET
Однофайловая
сборка
CarLibrary.dll
Манифест
Метаданные типов
CIL-код
Дополнительные ресурсы
Рис. 14.2. Однофайловая сборка
В однофайловых сборках все необходимые элементы
(заголовки Windows и CIL, метаданные типов, манифест
и необходимые ресурсы) размещаются внутри
единственного пакета * . ехе или * . dll. На рис. 14.2
схематично показано устройство однофайловой сборки.
Многофайловая сборка, с другой стороны, состоит из
набора модулей .NET, которые развертываются в виде
одной логической единицы и снабжаются одним
номером версии. Формально один из этих модулей
называется главным модулем и содержит манифест сборки
(а также все необходимые CIL-инструкции, метаданные,
заголовки и дополнительные ресурсы).
В манифесте главного модуля описаны все остальные
связанные модули, от которых зависит его работа.
По соглашению второстепенным модулям в
многофайловой сборке назначается расширение * . netmodule;
обязательным требованием для CLR-среды это, однако,
не является. Во второстепенных модулях * .netmodule
тоже содержится CIL-код и метаданные типов, а также
манифест уровня модуля, в котором перечислены внешние сборки, необходимые
данному модулю.
Главное преимущество многофайловых сборок состоит в том, что они
предоставляют очень эффективный способ для выполнения загрузки содержимого. Например,
предположим, что есть машина, которая ссылается на удаленную многофайловую сборку,
состоящую из трех модулей, главный из которых установлен на клиенте. Если
клиенту понадобится какой-то тип из второстепенного удаленного модуля * .netmodule,
CLR-среда по его требованию начнет немедленно загружать на локальную машину в
специальное место, называемое кэшем загрузки (download cache) только
соответствующий двоичный файл. В случае, если размер каждого модуля * .netmodule составляет
5 Мбайт, преимущество сразу же станет заметным (по сравнению с загрузкой одного
файла размером в 15 Мбайт).
Другим преимуществом многофайловых сборок является то, что они позволяют
создавать модули на различных языках программирования .NET (что может быть удобно в
крупных корпорациях, где в разных отделах отдают предпочтение разным языкам .NET).
После компиляции отдельные модули все могут легко "объединяться" в одну логическую
сборку с помощью компилятора С# командной строки.
В любом случае следует понимать, что модули, которые образуют многофайловую
сборку, не связываются буквально вместе в один файл (большего размера). Вместо этого
многофайловые сборки скорее соотносятся на логическом уровне посредством
информации, содержащейся в манифесте главного модуля. На рис. 14.3 схематично показано
устройство многофайловой сборки, состоящей из трех модулей, каждый из которых
написан на своем языке программирования .NET.
К этому моменту должно стать более понятно, как изнутри выглядит любой
двоичный файл .NET. Зная об этом, теперь можно переходить к рассмотрению способов
построения и конфигурирования различных библиотек кода.
Создание и использование однофайловой сборки
Чтобы приступить к исследованию мира сборок .NET, давайте вначале попробуем
создать однофайловую сборку * . dll (по имени Car Libra r у) с небольшим набором
общедоступных типов.
CSharpCarLib.dll
Манифест
(ссылается на другие
связанные файлы)
Метаданные типов
CIL-код
Глава 14. Конфигурирование сборок .NET 507
Многофайловая сборка
► VbNetCarLib.netmodule
-► PascalCarLib.netmodule
CompanyLogo.bmp
Рис. 14.3. В главном модуле внутри манифеста сборки содержится информация
обо всех остальных вспомогательных модулях
Для построения библиотеки кода в Visual Studio 2010 необходимо создать проект
типа Class Library (Библиотека классов), выбрав в меню File (Файл) пункт New Project
(Новый проект), как показано на рис. 14.4.
Instated Template*
л Visual С*
Windows
Web J
Office
Cloud Service 11
Reporting
Sttverlight
Test
Per user extensions are currently not allc
Name: WA Carlrbrary
Location: | CAMyCode
Solution name: CarLibrary
.
.NET Framework 4 » Sort by: Default
—-* Ж W ИНИЖЗИЯГЭ
35] Windows Forms Application Visual C*
5] ClMiUbrary VisualC*
jj|| ASP.NET Web Application Visual C#
Jl| Empty ASP.NET Web Applica... Visual G»
^ SirverbghtAppication VisualC*
т*шшл.ЫШШтЛшашиаШшкш
• 1
* Types VisualC*
4 A project for creating a C* class library
[ Browse™ j
J Create directory for solution
Add to source control
СЖ [ Cancel |
Рис. 14.4. Создание библиотеки кода на С#
Первым в библиотеку добавим абстрактный базовый класс по имени Саг и
определим в нем различные данные состояния с помощью синтаксиса автоматических
свойств, а также один абстрактный метод по имени TurboBoost (), предусматриваю-
508 Часть IV. Программирование с использованием сборок .NET
щий использование специального перечисления (EngineState), значения в котором
будут представлять текущее состояние двигателя автомобиля:
using System;
using System.Collections .Generic-
using System.Linq;
using System.Text;
namespace CarLibrary
{
// Представляет состояние двигателя.
public enum EngineState
{ engineAlive, engineDead }
// Абстрактный базовый класс в данной иерархии.
public abstract class Car
{
public string PetName {get; set;}
public int CurrentSpeed {get; set;}
public int MaxSpeed {get; set;}
protected EngineState egnState = EngineState.engineAlive;
public EngineState EngineState
{
get { return egnState; }
}
public abstract void TurboBoost();
public Car () {}
public Car(string name, int maxSp, int currSp)
{
PetName = name; MaxSpeed = maxSp; CurrentSpeed = currSp;
}
}
}
Теперь создадим двух непосредственных потомков Car с именами MiniVan (минивэн)
и SportsCar (спортивный автомобиль) и переопределим в каждом из них абстрактный
метод TurboBoost () так, чтобы он предусматривал отображение соответствующего
сообщения в окне сообщений Windows Forms, т.е. вставим в проект новый файл классов
С# по имени DerivedCars . cs со следующим кодом:
using System;
using System.Windows.Forms;
namespace CarLibrary
{
public class SportsCar : Car
{
public SportsCar(){ }
public SportsCar(string name, int maxSp, int currSp)
: base (name, maxSp, currSp) { }
public override void TurboBoost()
{
MessageBox.Show("Ramming speed1", "Faster is better...");
// Черепашья скорость! Чем быстрее, тем :лучше...
}
}
public class MiniVan : Car
{
public MiniVan(){ }
Глава 14. Конфигурирование сборок .NET 509
public MiniVan(string name, int maxSp, int currSp)
: base (name, maxSp, currSp){ }
public override void TurboBoost ()
{
// У минивэнов возможности ускорения всегда плохие!
egriState = EngineState.engineDead;
MessageBox.Show ("Eek!", "Your engine block exploded1");
}
}
}
Обратите внимание, что в каждом из подклассов метод TurboBoost ()
реализуется с использованием класса MessageBox, определение которого содержится в сборке
System.Windows.Forms.dll. Для использования в своей сборке типов, определенных
внутри такой внешней сборки, в проект CarLibrary обязательно понадобится добавить
ссылку на соответствующий двоичный файл с помощью диалогового окна Add Reference
(Добавление ссылки), которое показано на рис. 14.5 (чтобы открыть его, выберите в
меню Project (Проект) пункт Add Reference (Добавить ссылку)).
> Add Reference
NET [СОМ | Projects | Browse | Recentj
Component Name
System. Web.Serv ices
System.Windows.Forms.Dat.,.
System.Windows.Forms.Dat...
S stem,Winders.Forms
System.Windows.Input.Mani...
System.Windcvvs.Presentation
Svstem, Workflow. Activities
System.Worirflow.Compcne.,.
System.Workflcw.Runtime
System.WorkflowServices
System.Xa ml
4 1 1 ■■
Version
4.0.0.0
4.0.0.0
4,0.0.0
4.0JO.O
4.0.0.0
4.0.0.0
4.0.0.0
4.0.0.0
4.0.0.0
4.0.0.0
4.0.0.0
Runtime
v4.0.21006
•/4.0.21006
v4.0.21006
V4.021Q06
v4.0.21006
v4.0.21006
v4.021006
v4.021006
У4Л.21006
v4.021006
у4ЛЛ006
III)
Path
C:\Prograr
C:\Prograr
C:\Prograr, -(
CAPrograr'"""'
C:\Prograr
C:\Prograr
C:\Prograr
C:\Prograr
C:\Prcgrar
C:\Prograr
CAPrograr ▼
►
OK
Cancel
Рис. 14.5. Добавление ссылок на внешние сборки .NET с помощью
диалогового окна Add Reference
Очень валено знать то, что в диалоговом окне Add Reference на вкладке .NET
отображаются далеко не все присутствующие на машине сборки. Специальные сборки и
все сборки, которые размещены в GAC, например, здесь показаны не будут. Скорее, в
этом диалоговом окне предлагается список лишь общих сборок, которые среда Visual
Studio 2010 была изначально запрограммирована отображать. Поэтому при создании
приложений, нуждающихся в использовании какой-то такой сборки, которая не
отображается в окне Add Reference, необходимо перейти на вкладку Browse (Обзор) и вручную
отыскать интересующий файл * . dll или * . ехе.
На заметку! Также следует знать о том, что на вкладке Recent (Недавние ссылки) в диалоговом
окне Add Reference предлагается постоянно обновляемый список сборок, которые
использовались последними. Он может оказываться очень удобным, поскольку во многих проектах .NET
часто приходится применять один и тот же ключевой набор внешних библиотек.
510 Часть IV. Программирование с использованием сборок .NET
Исследование манифеста
Прежде чем переходить к использованию библиотеки CarLibrary.dllB клиентском
приложении, давайте сначала посмотрим, как она устроена изнутри. Для этого
скомпилируем проект и загрузим его в утилиту ildasm. ехе (рис. 14.6).
Р H:\My Book$\C# Book\C# and the .NET Platfor.
File View Help
-иинмяжн
► MANIFEST
ri 9 CarLibrary
ф £ CarLibrary.Car
*""№ CarLibrary.EngineState
й £ CarLibrary.MiniVan
ffi £ CarLibrary,SportsCar
.assembly CarLibrary
<
Рис. 14.6. Проект CarLibrary.dll, загруженный в ildasm.exe
Теперь давайте отобразим манифест сборки CarLibrary.dll, дважды щелкнув
на элементе MANIFEST. В первом блоке любого манифеста всегда перечисляются все
внешние сборки, которые требуются текущей сборки для нормального
функционирования. Вспомните, что в сборке CarLibrary.dll используются типы из внешних сборок
mscorlib.dll и System.Windows.Forms.dll, поэтому обе они и будут отображаться в
манифесте под маркерами .assembly extern:
.assembly extern mscorlib
.publickeytoken
.ver 4:0:0:0
(B7 7A 5C 56 19 34 E0 89 )
.assembly extern System.Windows.Forms
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
.ver 4:0:0:0
{
Здесь каждый блок . assembly extern сопровождается уточняющими директивами
.publickeytoken и .ver. Инструкция .publickeytoken появляется только в том
случае, если сборка была сконфигурирована со строгим именем (более подробно о
строгих именах будет рассказываться позже в настоящей главе, в разделе "Строгие имена").
Маркер .ver отражает числовой идентификатор версии упоминаемой сборки.
После ссылок на внешние сборки идет набор маркеров .custom, в которых
перечислены атрибуты сборки (информация об авторском праве, название компании,
версия сборки и т.д.). Ниже для примера приведена небольшая часть таких данных в
манифесте:
.assembly CarLibrary
.custom instance void .
.custom instance void .
.custom instance void .
.custom instance void .
.custom instance void .
.custom instance void .
.custom instance void .
..AssemblyDescriptionAttribute...
..AssemblyConfigurationAttribute.
..RuntimeCompatibilityAttribute..
..TargetFrameworkAttribute...
..AssemblyTitleAttribute...
..AssemblyTrademarkAttribute...
..AssemblyCompanyAttribute...
Глава 14. Конфигурирование сборок .NET 511
.custom instance void ...AssemblyProductAttribute...
.custom instance void ...AssemblyCopyrightAttribute...
.ver 1:0:0:0
}
.module CarLibrary.dll
Обычно эти параметры устанавливаются визуальным образом в редакторе свойств
текущего проекта. Вернитесь в Visual Studio 2010, дважды щелкните на значке Properties
(Свойства) в окне Solution Explorer, и затем щелкните на кнопке Assembly Information
(Информация о сборке) на вкладке Application (Приложение), которая открывается по
умолчанию. Откроется диалоговое окно Assembly Information (Информация о сборке),
показанное на рис. 14.7.
Assembly Information
Title
Description:
Company:
Product:
Copyright
Trademark:
Assembly version
File version:
CarLibrary
Microsoft
CarLibrary
Copyright © Microsoft 2009
1 0 0 0
1 0 0 0
:
GUID: 892da2c2-c0f7-426e-al9e-0b4fl522a228
Neutral language (None)
П Make assembly COM-Visible
OK
Рис. 14.7. Редактирование информации о сборке
в диалоговом окне Assembly Information
При сохранении изменений это диалоговое окно обновляет соответствующий
образом файл AssemblyInfo.cs, который поддерживается Visual Studio 2010 и может
просматриваться за счет разворачивания в окне Solution Explorer узла Properties (Свойства),
как показано на рис. 14.8.
I Solution Explorer
£Э Solution 'CarLibrary' A project)
3 CarLibrary
л ^£ Properties
Л) AssembryInfo.es
[> ui References 1^
Щ Car.cs
cjaJ DerrvedCars.es
c?5 Solution Explorer
»nx
Рис. 14.8. Использование редактора свойств приводит
к обновлению файла Assemblylnf о. cs
В этом файле С# можно обнаружить набор заключенных в квадратные скобки
атрибутов .NET, подобных приведенным ниже:
512 Часть IV. Программирование с использованием сборок .NET
[assembly: AssemblyTitle("CarLibrary")]
[assembly: AssemblyDescnption ("") ]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Microsoft")]
[assembly: AssemblyProduct("CarLibrary")]
[assembly: AssemblyCopyright("Copyright ©Microsoft 2010")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture ("")]
О роли атрибутов будет более подробно рассказываться в главе 15, поэтому на
данном этапе беспокоиться о деталях не нужно. Главное сейчас понять только то, что
большинство из атрибутов в файле Assemblylnf о. cs будет использоваться для обновления
значений в блоках . custom внутри манифеста сборки.
Исследование CIL-кода
Вспомните, что ориентированных на конкретную платформу инструкций в сборке
не содержится; вместо этого в ней хранятся инструкции на независящем от платформы
общем промежуточном языке (Common Intermediate Language — CIL). Когда
исполняющая среда .NET загружает сборку в память, лежащий в ее основе код CIL (с помощью
JIT-компилятора) компилируется и преобразуется в инструкции, воспринимаемые
целевой платформой. Например, если дважды щелкнуть на методе TurboBoost () в классе
SportsCar, то в ildasm.exe откроется новое окно, в котором будут отображаться
реализующие данный метод CIL-инструкции:
.method public hidebysig virtual instance void
TurboBoost() cil managed
{
// Code size 18 @x12)
.maxstack 8
IL_0000: nop
IL_0001: ldstr "Ramming speed1"
IL_0006: ldstr "Faster is better..."
IL_000b: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult
[System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string, string)
IL_0010: pop x
IL_0011: ret
} // end of method SportsCar::TurboBoost
Опять-таки, хотя большинству разработчиков приложений .NET не требуется
глубоко разбираться в деталях CIL-кода при повседневной работе, в главе 17 все же будут
приведены более подробные сведения о синтаксисе и семантике языка CIL. Понимание
грамматики языка CIL может быть полезно при создании более сложных приложений,
нуждающихся в поддержке более совершенных служб, таких как конструирование
сборок во время выполнения (об этом тоже речь пойдет в главе 17).
Исследование метаданных типов
Если перед созданием приложений, использующих специальную библиотеку .NET,
нажать комбинацию клавиш <Ctrl+M>, в окне ildasm.exe отобразятся метаданные по
каждому из имеющихся внутри сборки CarLibrary.dll типов (рис. 14.9).
Как будет показано в следующей главе, метаданные сборки являются очень важным
элементом платформы .NET и служат основой для многочисленных технологий (вроде
сериализации объектов, позднего связывания, создания расширяемых приложений
и т.д.). В любом случае, теперь, когда мы заглянули внутрь сборки CarLibrary.dll,
можно приступать к созданию клиентских приложений, предусматривающих
использование содержащихся внутри сборки типов.
Г j? Metal
Глава 14. Конфигурирование сборок .NET 513
l°|BJi
ScopeName : CarLibrary.dll
MUID : <4ABFCE8E-ODC1-417A-89C0-0DD233E92BA6>
Global functions
Find Find Next
'••I
Global fields
Global MemberRefs
TypeDef 111 @2000002)
TypDefNane: Cat-Library.Enginestate (82006002)
Flags : [Public] [fiutoLayout] [Class] [Sealed] [finsiClass] @000
Extends : 01000001 [TypeRef] System.Enum
Field 81 @1000001)
Field Name: ualue_ @4000001)
Рис. 14.9. Метаданные типов, имеющихся в сборке CarLibrary.dll
Исходный код. Проект CarLibrary доступен в подкаталоге Chapter 14.
Создание клиентского приложения на С#
Поскольку все типы в CarLibrary были объявлены с ключевым словом public,
они могут использоваться также и в других сборках. Вспомните, что типы могут
объявляться и с таким поддерживаемым в С# ключевым словом, как internal (которое
применяется в С# в качестве модификатора доступа по умолчанию). Типы,
объявляемые как internal, могут использоваться только в сборке, где находится их
определение. Внешние клиенты не могут ни видеть, ни создавать типы, помеченные ключевым
словом internal.
На заметку! В .NET поддерживается возможность определения так называемых "дружественных
сборок", которые позволяют внутренним типам использоваться в указанном наборе сборок.
Более подробную информацию об этом можно найти в документации .NET Framework 4.0 SDK, в
разделе, посвященном описанию класса InternalsVisibleToAttribute. Однако следует
иметь в виду, что прием с созданием дружественных сборок применяется довольно редко.
Чтобы использовать типы из библиотеки CarLibrary, создадим новый проект
типа Console Application на С# по имени CSharpCarClient. Добавим в него ссылку
на CarLibrary.dll на вкладке Browse диалогового окна Add Reference (если сборка
CarLibrary.dll компилировалась в Visual Studio, она будет находиться в подкаталоге
\Bin\Debug внутри папки проекта CarLibrary). После этого можно приступать к
созданию клиентского приложения, предусматривающего использование внешних типов.
Модифицируем исходный файл кода на С#, как показано ниже:
using System;
using System. Collections .Generic-
using System.Linq;
using System.Text;
//He забывайте импортировать пространство имен CarLibrary!
using CarLibrary;
namespace CSharpCarClient
{
514 Часть IV. Программирование с использованием сборок .NET
public class Program
{
static void Main(string [ ] args)
{
Console.WriteLine ("***** C# CarLibrary Client App ****
// Создание спортивного автомобиля.
SportsCar viper = new SportsCar("Viper", 240, 40);
viper.TurboBoost();
// Создание минивэна.
MiniVan mv = new MiniVanO ;
mv.TurboBoost();
Console.ReadLine();
}
*").
> M Properties
:> ijH References
j bin
л ; Debug
J CarLibrary.dll
j CarLibrary.pdb
j j СSharpCarClient exe j
) CSharpCarClientpdb
j CSharpCarClient.v5host.exe
] CSharpCarClient.vshost.exe.manrfest
> ...J obj
[ &$ Solution Explorer Д
}
Этот код выглядит точно так же, как и
код других разрабатывавшихся ранее
приложений. Единственным представляющим
интерес моментом в нем является то, что в
клиентском приложении на С# теперь
используются типы, определенные внутри
отдельной специальной сборки. Давайте попробуем
запустить это приложение. Как и следовало
ожидать, его выполнение приведет к
отображению различных окон с сообщениями.
Обратите внимание, что Visual Studio 2010
поместит копию сборки CarLibrary. dll в под-
папку \bin\Debug проекта CSharpCarClient.
Чтобы удостовериться в этом, щелкните на
кнопке Show All Files (Показать все файлы) в
окне Solution Explorer (рис. 14.10).
Как будет объясняться позже в главе, CarLibrary.dll была сконфигурирована как
приватная сборка (поскольку именно такое поведение применяется автоматически для
всех проектов типа Class Library в Visual Studio 2010). При добавлении ссылок на
приватные сборки в новые приложения (вроде CSharpCarClient.exe), IDE-среда всегда
реагирует помещением копии соответствующей библиотеки в выходной каталог
клиентского приложения.
Рис. 14.10. Все приватные сборки Visual
Studio 2010 копирует в каталог клиентского
приложения
Исходный код. Проект CSharpCarClient доступен в подкаталоге Chapter 14.
Создание клиентского приложения на Visual Basic
Чтобы проверить языковую независимость платформы .NET, создадим еще одно
приложение типа Console Application (по имени visualBasicCarClient), но на этот
раз на языке Visual Basic (рис. 14.11). После создания проекта добавим в него ссылку
на CarLibrary. dll с помощью диалогового окна Add Reference (выбрав в меню Project
пункт Add Reference).
Как и в С#, в Visual Basic необходимо перечислять все пространства имен,
используемые в текущем файле. Вместо ключевого слова using в Visual Basic для этого
предусмотрено ключевое слово Imports.
Глава 14. Конфигурирование сборок .NET 515
Рис. 14.11. Создание консольного приложения на языке Visual Basic
Добавим в файл кода Modulel. vb следующий оператор Imports:
Imports CarLibrary
Module Modulel
Sub Main ()
End Sub
End Module
Обратите внимание, что метод Main () в Visual Basic определяется в рамках типа
Module (который не имеет ничего общего с файлом * . netmodule, используемым в
многофайловых сборках). Если говорить вкратце, то тип Module в Visual Basic служит
просто для обозначения класса, способного содержать только статические методы. В любом
случае, чтобы испробовать типы MiniVan и SportsCar в синтаксисе Visual Basic,
модифицируем метод Main () следующим образом:
Sub Main ()
Console.WriteLine("***** VB CarLibrary Client App *****")
1 Локальные переменные объявляются
1 с помощью ключевого слова Dim.
myMiniVan.TurboBoost ()
Dim mySportsCar As New SportsCar ()
mySportsCar.TurboBoost()
Console.ReadLine ()
End Sub
Если теперь скомпилировать и запустить приложение, на экране будет
отображаться ряд окон с сообщениями. Более того, для этого нового клиентского приложения тоже
будет создана собственная отдельная локальная копия CarLibrary.dll в подкаталоге
bin\Debug.
Межъязыковое наследование в действии
Весьма привлекательным аспектом в разработке для .NET является понятие
межъязыкового наследования. Давайте посмотрим, что под этим имеется в виду, создав на
языке Visual Basic новый класс, унаследованный от класса SportsCar (который был
создан на языке С#). Для этого добавим в текущее приложение на Visual Basic
(выбрав в меню Project пункт Add Class (Добавить класс)) новый файл класса по имени
516 Часть IV. Программирование с использованием сборок .NET
PerformanceCar. vb, с помощью ключевого слова Inherits изменим исходное
определение этого класса так, чтобы он наследовался от типа SportsCar, и, наконец,
переопределим метод TurboBoost () с помощью ключевого слова Overrides.
Imports CarLibrary
' Этот класс на языке VB унаследован от класса
' SportsCar, который был определен на языке С#.
Public Class PerformanceCar
Inherits SportsCar
Public Overrides Sub TurboBoost()
Console.WriteLine ("Zero to 60 in a cool 4.8 seconds...")
End Sub
End Class
Чтобы протестировать этот новый класс, модифицируем код метода Main () в модуле
следующим образом:
Sub Main ()
Dim dreamCar As New PerformanceCar()
' Использование унаследованного свойства.
dreamCar.PetName = "Hank"
dreamCar.TurboBoost()
Console.ReadLine()
End Sub
Обратите внимание, что объект dreamCar способен вызывать любой из
общедоступных членов (вроде свойства PetName), находящихся выше в цепочке наследования,
невзирая на тот факт, что базовый класс был определен на совершенно другом языке
и в совершенно другой сборке! Возможность расширять классы за пределами границ
сборок независимым от языка образом является очень полезным аспектом цикла
разработки в .NET и существенно упрощает использование скомпилированного кода,
написанного людьми, которые не захотели создавать свой разделяемый код на С#.
Исходный код. Проект VisualBasicCarClient доступен в подкаталоге Chapter 14.
Создание и использование многофайловой сборки
После рассмотрения создания и использования однофайловой сборки давайте
изучим аналогичные процессы для многофайловой сборки. Вспомните, что под
многофайловой сборкой понимается коллекция взаимосвязанных модулей, которые
развертываются и снабжаются версией в виде цельной логической единицы. На момент написания
книги в IDE-среде Visual Studio никакого отдельного шаблона проекта для создания
многофайловой сборки на С# не предусматривалось. Для создания такового пока что
должен использоваться компилятор командной строки (esc. ехе).
Рассмотрим этот процесс на примере, создав многофайловую сборку по имени
AirVehicles. В главном модуле этой сборки (airvehicles.dll) будет содержаться
единственный тип класса Helicopter (вертолет) и соответствующий манифест, который
указывает на наличие дополнительного файла * . netmodule по имени uf о. netmodule,
содержащего еще один класс Uf о. Хотя физически оба класса размещаются в отдельных
двоичных файлах, пространство имен у них будет одно — AirVehicles. И, наконец,
оба класса будут создаваться на С# (хотя, конечно же, вполне допустимо использовать
другие языки). Для начала откроем простой текстовый редактор и создадим следующее
определение для класса Uf о, после чего сохраним его в файле с именем uf о. cs:
Глава 14. Конфигурирование сборок .NET 517
using System;
namespace AirVehicles
{
public class Ufo
I
public void AbductHuman()
{
Console.WriteLine ("Resistance is futile");
}
}
}
Чтобы скомпилировать этот класс в .NET-модуль, откройте в Visual Studio 2010
окно командной строки, перейдите в папку, где был сохранен файл uf о. cs, и введите
следующую команду (опция module флага /t указывает, что должен быть создан файл
* . netmodule, а не * . dll или * . ехе):
csc.exe /t:module ufo.cs
В папке с файлом ufo.cs появится новый файл по имени ufo.netmodule. Теперь
давайте создадим новый файл helicopter. cs со следующим определением класса:
using System;
namespace AirVehicles
{
public class Helicopter
{
public void TakeOff()
{
Console.WriteLine("Helicopter taking off!");
}
}
}
Поскольку было решено, что главный модуль в этой многофайловой сборке будет
называться airvehicles .dll, осталось только скомпилировать helicopter, cs с
использованием соответствующих опций /t: library и /out:. Чтобы включить информацию о
двоичном файле ufo .netmodule в манифест сборки, также потребуется добавить
соответствующий флаг /addmodule. Полностью необходимая команда выглядит следующим
образом:
esc /tilibrary /addmodule:ufo.netmodule /out:airvehicles.dll helicopter.es
После выполнения этой команды в каталоге должен появиться главный модуль
airvehicles .dll, а также второстепенный двоичный файл uf о. netmodule.
Исследование файла ufo. netmodule
Теперь откроем файл ufo.netmodule в утилите ildasm.exe. Сразу же можно будет
заметить, что в нем содержится манифест уровня модуля, единственной задачей
которого является перечисление всех внешних сборок, которые упоминаются в кодовой базе.
Поскольку в классе Ufo был практически добавлен только вызов Console . WriteLine (),
обнаружится следующая ключевая информация:
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 EO 89 )
.ver 4:0:0:0
}
.module ufo.netmodule
518 Часть IV. Программирование с использованием сборок .NET
Исследование файла airvehicles. dll
Далее откроем в ildasm.exe файл главного модуля airvehicles .dll и изучим
содержимое манифеста уровня всей сборки. В нем важно обратить внимание, что маркер
.file используется для представления информации о других ассоциируемых с
многофайловой сборкой модулях (в данном случае — ufo .netmodule), а маркеры .class
extern — для перечисления имен внешних типов, которые упоминаются как
используемые во второстепенном модуле (в данном случае — Ufo). Ниже приведена
соответствующая информация.
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 EO 89 )
.ver 4:0:0:0
}
.assembly airvehicles
.hash algorithm 0x00008004
.ver 0:0:0:0
file ufo.netmodule
class extern public AirVehicles.Ufo
.file ufo.netmodule
.class 0x02000002
}
.module airvehicles.dll
Манифест сборки является единственной сущностью, которая связывает
airvehicle,s .dll и ufo.netmodule вместе. В один файл * .dll большего размера эти
два двоичных файла не объединялись.
Использование многофайловой сборки
Пользователям многофайловой сборки нет никакого дела до того, состоит ли сборка,
на которую они ссылаются, из многочисленных модулей. Чтобы не усложнять пример,
построим новое клиентское приложение на языке С# в командной строке. Для начала
создадим новый файл по имени Client.cs со следующим определением модуля и
сохраним его в месте, где находится многофайловая сборка:
using System;
usj-ng AirVehicles;
class Program
{
static void Main()
{
Console.WriteLine ("***** Multifile Assembly Client *****");
Helicopter h = new Helicopter ();
h.TakeOff();
// Благодаря этому коду, модуль *.netmodule будет загружаться по требованию.
Ufo u = new Ufo () ;
u.AbductHuman();
Console.ReadLine();
}
Глава 14. Конфигурирование сборок .NET 519
Чтобы скомпилировать эту исполняемую сборку в командной строке, запустим
компилятор esc. ехе с помощью следующей команды:
esc /rrairvehicles.dll Client.cs
Обратите внимание, что при добавлении ссылки на многофайловую сборку
компилятору необходимо указать только имя главного модуля (второстепенные модули
* .netmodule будут загружаться CLR-средой по требованию, когда возникнет
необходимость в их использовании клиентским кодом). Сами по себе модули * . netmodule не
обладают никаким индивидуальным номером версии и потому не могут напрямую
загружаться CLR-средой. Отдельные модули * . netmodule могут загружаться только
главным модулем (например, файлом, в котором содержится манифест сборки).
На заметку! В Visual Studio 2010 также позволяет добавлять ссылку на многофайловую сборку. Для
этого потребуется открыть окно Add Reference и выбрать главный модуль. Все остальные
связанные с ним второстепенные модули * .netmodule скопируются автоматически по ходу
процесса.
К этому моменту должно стать более понятно, что собой представляет процесс
создания однофайловых и многофайловых сборок. По-правде говоря, в большинстве случаев
требуется создавать только однофайловые сборки. Тем не менее, многофайловые сборки
полезны, когда большой физический двоичный файл необходимо разбить на меньшие
модульные единицы (это довольно удобно в сценариях с удаленной загрузкой). Теперь
давайте разберемся с понятием приватных сборок.
Исходный код. Файлы кода Multif ileAssembly доступны в подкаталоге Chapter 14.
Приватные сборки
С технической точки зрения библиотеки кода, создаваемые до сих пор в этой
главе, развертывались в виде приватных сборок. Приватные сборки должны всегда
размещаться в том же каталоге, что и клиентское приложение, в котором они
используются (т.е. в каталоге приложения), или в каком-то из его подкаталогов. Вспомните,
что при добавлении ссылки на CarLibrary.dll во время создания приложений
CSharpCarClient.exe и VbNetCarClient.exe Visual Studio 2010 (по крайней мере,
после первой компиляции) создает копию CarLibrary.dll внутри каталога каждого из
этих клиентских приложений.
Благодаря этому, при возникновении в какой-то из этих клиентских программ
необходимости в использовании типов, определенных во внешней сборке CarLibrary. dll,
CLR-среда будет просто загружать локальную копию этой сборки. Отсутствие у
исполняющей среды .NET потребности заглядывать в системный реестр при поиске внешних
сборок позволяет перемещать CSharpCarClient. ехе (или VisualBasicCarClient. ехе)
вместе с CarLibrary .dll в любое другое место на машине и все равно успешно запускать
эти приложения (такой процесс часто называют развертыванием с помощью Хсору).
Удаление (или репликация) приложения, в котором используются приватные
сборки, тоже не требует особых усилий: достаточно просто удалить (или скопировать)
папку приложения. В отличие от СОМ-приложений, беспокоиться о десятках остающихся
настроек в системном реестре совершенно не нужно. Более того, удаление приватных
сборок не может нарушить работу каких-то других приложений на машине.
520 Часть IV. Программирование с использованием сборок .NET
Идентификационные данные приватной сборки
Идентификационные данные приватной сборки включают дружественное имя и
номер версии, которые фиксируются в манифесте сборки. Дружественным называется имя
модуля, в котором содержится манифест сборки, без файлового расширения. Например,
заглянув в манифест сборки CarLibrary. dll, там можно будет обнаружить следующее:
.assembly CarLibrary
{
.ver 1:0:0:0
}
По причине изолированной природы приватной сборки CLR-среда, вполне логично,
не использует номер версии при выяснении места ее размещения. Она предполагает,
что приватные сборки не нуждаются в выполнении сложной проверки версии,
поскольку клиентское приложение является единственной сущностью, которой "известно" об
их существовании. Из-за этого на одной машине может размещаться множество копий
одной и той же сборки в различных каталогах приложений.
Процесс зондирования
Исполняющая среда .NET определяет местонахождение приватной сборки с
применением так называемой технологии зондирования, которая в действительности является
менее докучливой, чем может показаться из-за ее названия. В ходе процесса зондирования
производится отображение запроса внешней сборки на место размещения
соответствующего двоичного файла. Запрос внешней сборки, собственно говоря, может быть явным
или неявным. Неявный запрос происходит тогда, когда CLR-среда заглядывает в манифест
для определения, где находится требуемая сборка, по маркерам .assembly extern:
// Неявный запрос на выполнение загрузки.
.assembly extern CarLibrary
{ ... }
Явный запрос осуществляется программно за счет применения метода Load () или
LoadFromO (оба являются членами класса System.Reflection.Assembly) и обычно
предназначен для выполнения позднего связывания или динамического вызова членов
интересующего типа. Более подробно об этом речь пойдет в главе 15, а пока что ниже
приведен простой пример того, как может выглядеть явный запрос:
// Явный запрос на выполнение загрузки, основанный
//на использовании дружественного имени.
Assembly asm = Assembly.Load("CarLibrary");
В обоих случаях CLR-среда извлекает дружественное имя сборки и начинает
зондировать каталог клиентского приложения в поисках интересующего файла (например,
файла по имени CarLibrary.dll). Если ей не удается обнаружить такой файл, она
предпринимает попытку найти исполняемую сборку с таким же 'дружественным
именем (т.е., например, CarLibrary. ехе). Если ей не удается найти ни одного из
упомянутых файлов в каталоге приложения, она генерирует во время выполнения исключение
FileNotFoundException.
На заметку! С технической точки зрения, если копия запрашиваемой сборки не обнаружена в
каталоге клиентского приложения, CLR-среда будет также пытаться найти подкаталог с именем,
совпадающим с дружественным именем запрашиваемой сборки (например, С: \MyClient\
CarLibrary). Обнаружив такой подкаталог, CLR-среда будет загружать запрашиваемую
сборку в память из него.
Глава 14. Конфигурирование сборок .NET 521
Конфигурирование приватных сборок
Хотя допускается развертывания .NET-приложения просто за счет копирования
всех требуемых сборок в одну папку на жестком диске пользователя, скорее всего,
понадобится определить несколько отдельных подкаталогов для группирования
взаимосвязанного содержимого. Например, предположим, что имеется каталог приложения
под названием С: \MyApp, в котором содержится CSharpCarClient. exe. Тогда в этом
каталоге может также присутствовать и подкаталог MyLibraries, содержащий сборку
CarLibrary.dll.
Несмотря на подразумеваемую взаимосвязь между двумя этими каталогами, CLR-
среда не будет зондировать подкаталог MyLibraries, если только ей не будет
предоставлен конфигурационный файл с соответствующим указанием. Конфигурационные
файлы включают в себя различные XML-элементы, которые позволяют влиять на
процесс зондирования. Эти файлы должны обязательно иметь такое же имя, как
запускающее их приложение, и расширение *.config, а также развертываться в каталоге
приложения клиента. То есть если нужно создать конфигурационный файл для приложения
CSharpCarClient.exe, потребуется называть его CSharpCarClient. exe . config и
разместить (в рассматриваемом примере) в каталоге С: \MyApp.
Чтобы увидеть, как этот процесс выглядит на практике, давайте создадим на диске
С: с помощью проводника Windows новый каталог по имени МуАрр, после чего
скопируем в него CSharpCarClient.exe и CarLibrary.dll и запустим программу, дважды
щелкнув на ее исполняемом файле. В этот раз выполнение программы должно пройти
успешно (вспомните, что сборки не регистрируются). Далее создадим в С: \MyApp новый
подкаталог по имени MyLibraries, как показано на рис. 14.12, и переместим сборку
CarLibrary.dllв него.
нИСТ**^^^
JW
Organize
л £| Mongo Drive (С:)
DeusEx
> М Dot Net Stuff
t> i» Games!
> Ji! inetpub
■й - а Ф \\
MyLibraries
■ MyApp
и
CSharpCar
Client.exe
I. MyLibraries
> Jfc PerfLogs
t Program Files
Prnnram Fil^, fKflfil
MyLibraries Date modified: 12/27/2009 5:49 PM
ft File folder
Рис. 14.12. Теперь CarLibrary.dll размещается в подкаталоге MyLibraries
Теперь попробуем запустить программу снова. На этот раз CLR-среде не удается
обнаружить сборку CarLibrary непосредственно в каталоге приложения и потому она
сгенерирует исключение FileNotFoundException.
Чтобы указать CLR-среде, что следует зондировать подкаталог MyLibraries,
создадим новый конфигурационный файл по имени CSharpCarClient. exe . config с
помощью любого текстового редактора и сохраним его в той лее папке, в которой
содержится само приложение CSharpCarClient. exe (в данном примере С: \MyApp). Откроем
этот файл и введем в него следующий код (обратите внимание, что язык XML
чувствителен к регистру символов):
522 Часть IV. Программирование с использованием сборок .NET
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.vl">
<probing private Pa th=llMyLibraries"/>
</assemblyBinding>
</runtime>
</configuration>
Файлы * . conf ig в .NET всегда начинаются с корневого элемента <conf igurationx
Вложенный в него элемент <runtime> может содержать элемент <assemblyBinding>, a
тот, в свою очередь — вложенный элемент <probing>. В данном примере главный
интерес представляет атрибут privatePath, поскольку именно он служит для указания
подкаталогов внутри каталога приложения, которые должна зондировать CLR.
Теперь, когда конфигурационный файл CSharpCarClient .exe . conf ig готов,
давайте снова попробуем запустить клиентское приложение. На этот раз выполнение
CSharpCarClient .exe должно пройти без проблем (если это не так, вернитесь и
проверьте конфигурационный файл еще раз на предмет опечаток).
Обратите особое внимание, что то, какая именно сборка размещается в каждом
подкаталоге, в элементе <probing> не указывается. Другими словами, утверждать, что в
подкаталоге MyLibraries размещена сборка CarLibrary, а в подкаталоге OtherStuf f —
сборка MathLibrary нельзя. Элемент <probing> просто указывает CLR-среде искать
запрашиваемую сборку во всех перечисленных подкаталогах до тех пор, пока не будет
найдено первое совпадение.
На заметку! Очень важно иметь в виду, что атрибут privatePath не допускается применять для
указания ни абсолютного (С: \Папка\Подпапка), ни относительного (. . \\ОднаПапка\\
ДругаяПапка) пути. Чтобы указать каталог, находящийся за пределами каталога клиентского
приложения, необходимо использовать совершенно другой XML-элемент — <codeBase>
(который более подробно рассматривается позже в главе).
В атрибуте privatePath можно указывать несколько подкаталогов в виде
разделенного точками с запятой списка. В данном случае этого делать не требуется, но ниже
приведен пример, в котором CLR-среде указывается просматривать клиентские
подкаталоги MyLibraries и MyLibraries\Tests:
<probmg privatePath="MyLibraries; MyLibraries\Tests"/>
Изменим для целей тестирования имя конфигурационного файла и попробуем
запустить приложение снова. На этот раз выполнение клиентского приложения должно
завершиться неудачей. Вспомните, что имя файла * . conf ig должно включать в
качестве префикса имя соответствующего клиентского приложения. И, наконец, давайте
попробуем открыть конфигурационный файл для редактирования и заменить
символы любого XML-элемента на прописные. В этом случае запуск клиентского приложения
должен завершиться неудачей (язык XML чувствителен к регистру символов).
На заметку! Важно понимать, что CLR-среда будет загружать сборку, которая во время
процесса зондирования обнаруживается первой. Например, если в папке С: \MyApp имеется
копия CarLibrary.dll, то она и будет загружаться в память, а копия, содержащаяся в папке
MyLibraries, будет проигнорирована.
Конфигурационные файлы и Visual Studio 2010
Хотя конфигурационные файлы XML можно всегда создавать вручную в любом
предпочитаемом текстовом редакторе, Visual Studio 2010 позволяет это делать автомати-
Глава 14. Конфигурирование сборок .NET 523
чески прямо во время разработки клиентской программы. Давайте загрузим решение
CSharpCarClient в Visual Studio 2010 и вставим в него новый элемент типа Application
Configuration File (Конфигурационный файл приложения), выбрав в меню Project пункт
Add New Item, как показано на рис. 14.13.
Add New Hem-CSharpCarClient M
litttaled TempUtet
1 л Visual С* Hems
Code
Data
General
Web
Windows Forms
WPF
Reporting
Workflow
BSBHBI
Per user extensions are currently not ail
Name: App.config
Sort by. Default
3 Windows Script Host
4Cl*j Debugger Visualizer
Hffl Installer Class
j Application Configuration File
I 4ijfl Component Class
I 1ЭД Application Manifest File
1 лД Preprocessed Text Template
owed to load. Enable loading of per циаи*
GD
Visual C# hems j"
Visual C* Items
Visual C# hems
Visual C* Items
Visual C# hems
Visual C# Hems
Visual C# Hems
[ Search installed Templates
J Type: Visual C* Items
E j A file for storing application (
and settings values
[ Add
'-^Mf.W1
-3
1
onfiguration
Cancel 1
Рис. 14.13. Вставка нового файла Арр. conf ig в проект Visual Studio 2010
Перед щелчком на кнопке О К важно обратить внимание, что файлу было назначено
имя App.config (его ни в коем случае не следует изменять). Если затем заглянуть в
окно Solution Explorer, можно увидеть, что файл Арр. conf ig был вставлен в текущий
проект (рис. 14.14).
Теперь можно приступать к вводу необходимых XML-элементов для
разрабатываемого клиента. Замечательно, что при каждой компиляции проекта Visual Studio 2010
будет автоматически копировать данные из App.config в каталог \bin\Debug и
назначать создаваемым копиям соответствующие принятым соглашениям имена, вроде
CSharpCarClient .exe . conf ig, как показано на рис. 14.15. Однако подобное будет
происходить только в случае, если конфигурационный файл действительно называется
Арр.config.
Solution Explorer
ЕОЕИННН
^3 Solution 'CSharpCarClient' A project)
a CSharpCarClient
о ,M Properties
эл References
> ,_j bin
-j obj
j App.config
l] Program.cs
■ Solution Explorer
Рис. 14.14. Редактирование файла
Арр. conf ig с целью сохранения
необходимых данных для клиентских
приложений
Solution Explorer
3 Solution 'CSharpCarClient' A project)
л .3 CSharpCarClient
> "Ш Properties
jy| References
* ;■:3 bin
j 3j Debug
j CarLibrary.dll
j CarLibrary.pdb
J CSharpCarClient.exe
CSharpCarCltent.exe.config
) CSharpCarClient.pdb
j CSharpCarClient.vshost.exe
Jj CSharpCarClientA'shost.exe.manifest
_: Obj
j» App.config
i£ Program.cs
^9 -J Solution Explorer I
Рис. 14.15. Содержимое Арр. conf ig
будет копироваться в именуемый
надлежащим образом файл * . conf ig, который
находится в выходном каталоге проекта
524 Часть IV. Программирование с использованием сборок .NET
При таком подходе все, что требуется — это лишь поддерживать файл Арр. con fig,
и тогда среда Visual Studio 2010 сама позаботиться о том, чтобы в каталоге
приложения содержались самые последние и актуальные конфигурационные данные (даже если
проект будет переименован).
На заметку! Использовать файлы Арр. con fig в Visual Studio 2010 рекомендуется всегда. В случае
добавления файла * . conf ig в папку bin\Debug вручную через окно проводника Windows при
следующей компиляции проекта Visual Studio 2010 может удалить либо модифицировать его!
Разделяемые сборки
Теперь, когда уже известно о том, как развертывать и конфигурировать приватные
сборки, можно перейти к рассмотрению роли так называемых разделяемых: сборок.
Подобно приватной сборке, любая разделяемая сборка представляет собой коллекцию
типов и (необязательно) ресурсов. Самое очевидное отличие между разделяемой и
приватной сборкой состоит в том, что одна копия разделяемой сборки может
использоваться сразу в нескольких приложениях на одной и той же машине.
Давайте вспомним все приложения, которые создавались до сих пор в настоящей
книге, и в которые требовалось добавлять ссылку на сборку System. Windows . Forms . dll.
Если заглянуть в каталог любого из этих клиентских приложений, то никакой
приватной копии данной сборки .NET там не обнаружится. Это объясняется тем, что сборка
System. Windows . Forms . dll является разделяемой. Очевидно, что при возникновении
необходимости в создании библиотеки классов, пригодной для использования в
масштабах всей машины, именно такую сборку и нужно использовать.
На заметку! Принятие решения о том, развертывать библиотеку кода как приватную или как
разделяемую сборку, является еще одним вопросом, который должен быть продуман на этапе
проектирования, и ответ на него зависит от множества специфических деталей проекта. Как
правило, при создании библиотек, подлежащих использованию во множестве различных
приложений, может быть удобнее использовать разделяемые сборки, так как их очень легко
обновлять до новых версий (это будет показано далее в главе).
Как не трудно догадаться, разделяемые сборки не развертываются внутри того же
самого каталога, что и приложения, в которых они должны использоваться. Вместо
этого они устанавливаются в так называемом глобальном кэше сборок (Global Assembly
Cache — GAC). Этот кэш размещен в каталоге Windows внутри подкаталога под
названием Assembly (например, путем к нему может быть С : \Windows\Assembly), как
показано на рис. 14.16.
На заметку! Устанавливать в GAC исполняемые сборки (* . ехе) не разрешено. В качестве
разделяемых можно развертывать только сборки с расширением * . dll.
Строгие имена
Прежде чем развертывать сборку в GAC, ей обязательно необходимо назначить
строгое имя (strong name), которое позволяет уникальным образом идентифицировать
издателя данного двоичного файла .NET. Следует иметь в виду, что в роли "издателя"
может выступать как отдельный программист, так и подразделение компании или
вообще целиком вся компания.
Глава 14. Конфигурирование сборок .NET 525
I
fjj jY. ► Computer ►
1 Organize ▼ ^ Open
Jtl inetpub"*
" l MyApp
± PerfLogs
Mongo
Drive (C:) ► Windows ► assembly ►
Share with » Burn Compatibility files
j» Program Files
i, Program Fries (x86)
k ProgramDataTechSmith
Users
addins
t AppCompat
AppPatch
assembly
Boot
fa Branding
Ъ 1 item selected
* Assembly Name '
^Accessibility
Лаооов
Г1 i^AuditPolicyGPMana..
fj dftAudrtPolicyGPMana..
dbBDATunePIA
UUBDATunePIA
:ij ComSvcConfig
db CppCodeProvider
jJUcscompmgd
ifh С ustomMars haters
4Ь С usto m M arshal ers
Ad*>
' dbdfsvc
• ^ 4hehOR
New folder
Version Cut...
2.0.0.0
7Д330...
6100
610 0
6100
61DO
ЗЛЛЛ
III
2000
10.0.45...
2.0.0.0
6100
~M
Public Key Token
b03f5f7flld50a3a
b03f5f7flld50a3a
31bf3856ad364e35
31bf3856ad364e35
31bf3856ad364e35
31bf3856ad364e35
b03f5f7flld50a3a
b03f5f7flld50a3a
b03f5f7flld50a3a
b03f5f7flld50a3a
b03f5f7flld50a3a
31bf3856ad364e35
b03f5f7flld50a3a
31bf3856ad364e35
1-oiah
Ш* a
Proces...
MSIL
ЛЙ6
AMD64
x86
AMD64
MSIL
MSIL
MSIL
x86
AMD64
MSI
MSIL
• t
4
li
1*
'1
Рис. 14.16. Глобальный кэш сборок
В некотором отношении строгое имя является современным .NET-эквивалентом
глобально уникальных идентификаторов (GUID), которые применялись в СОМ. Те, кому
приходилось работать с СОМ, наверняка помнят, что глобально уникальными
идентификаторами приложений называются идентификаторы, которые характеризуют
конкретные СОМ-приложения. В отличие от GUID-значений в СОМ (которые представляют
собой 128-битные числа), строгие имена в .NET основаны (отчасти) на двух
взаимосвязанных криптографических ключах, называемых открытым (public) и секретным
(private) ключом и являющихся гораздо более уникальными и устойчивыми к подделке по
сравнению с простыми идентификаторами GUID.
Формально любое строгое имя состоит из набора взаимосвязанных данных,
большая часть из которых указывается с помощью перечисленных ниже атрибутов уровня
сборки.
• Дружественное имя сборки (которое представляет собой имя сборки без
файлового расширения).
• Номер версии сборки (назначается в атрибуте [AssemblyVersion]).
• Значение открытого ключа (назначается в атрибуте [AssemblyKeyFile]).
• Значение, обозначающее культуру, которое является необязательным и
может предоставляться для локализации приложения (присваивается в атрибуте
[AssemblyCulture]).
• Вставляемая цифровая подпись, созданная с использованием хеш-кода по
содержимому сборки и значения секретного ключа.
Для создания строгого имени сборки сначала генерируются данные открытого и
секретного ключей с помощью поставляемой в составе .NET Framework 4.0 SDK
утилиты sn. ехе. Эта утилита генерирует файл, который обычно оканчивается расширением
* . snk (Strong Name Key — ключ строгого имени) и содержит данные для двух разных, но
математически связанных ключей — "открытого" и "секретного". После указания
местонахождения этого файла * . snk компилятору С# тот запишет полное значение
открытого ключа в манифест сборки с использованием дескриптора .publickey.
Кроме того, компилятор С# генерирует на основе всего содержимого сборки (CIL-
кода, метаданных и т.д.) соответствующий хеш-код. Как упоминалось в главе 6, хеш-
кодом называется числовое значение, которое является статистически уникальным для
526 Часть IV. Программирование с использованием сборок .NET
фиксированных входных данных. Следовательно, в случае изменения какого-то аспекта
сборки .NET (даже одного символа в строковом литерале), компилятор выдает другой
хеш-код. Далее этот хеш-код объединяется с содержащимися внутри файла * . snk
данными секретного ключа для получения цифровой подписи, вставляемой в сборку внутрь
данных заголовка CLR. На рис. 14.17 схематично показано, как выглядит процесс
создания строгого имени.
CarLibrary.dll
Манифест с открытым ключом
Метаданные типов
CIL-код
Цифровая подпись
Рис. 14.17. Во время компиляции на основе данных открытого и секретного ключей
генерируется цифровая подпись, которая затем вставляется в сборку
Важно понимать, что данные секретного ключа сами нигде в манифесте не
встречаются, а служат только для снабжения содержимого сборки цифровой подписью
(вместе с генерируемым хеш-кодом). Суть использования открытого и секретного ключей
состоит просто в исключении вероятности наличия у двух компаний, подразделений
или отдельных программистов одинаковых идентификационных данных в мире .NET.
В любом случае по завершении процесса создания и назначения строгого имени сборка
может устанавливаться в GAC.
На заметку! Строгие имена также обеспечивают определенную степень защиты от возможной
подделки содержимого сборок. Поэтому в .NET наилучшим практическим приемом считается
назначение строгих имен всем сборкам (в том числе и сборкам . ехе), независимо от того, будут
они развертываться в GAC или нет.
Генерирование строгих имен в командной строке
Рассмотрим теперь процесс назначения строгого имени на примере сборки CarLibrary,
которая была создана ранее в этой главе. В нынешнее время предпочтение практически
наверняка будет отдаваться генерированию необходимого файла * . snk в Visual Studio
2010. Однако в прежние времена (примерно до 2003 г.) назначать сборке строгое имя
можно было только в командной строке. Давайте посмотрим, как это делается.
В первую очередь должны быть сгенерированы необходимые данные ключей с
помощью утилиты sn. ехе. Хотя эта утилита обладает множеством опций командной строки,
в настоящий момент основной интерес представляет только флаг -к, который
заставляет эту утилиту генерировать новый файл с информацией об открытом и секретном
ключах. Для примера создадим на диске С: новую папку по имени MyTestKeyPair,
перейдем в нее в окне командной строки Visual Studio 2010 и введем следующую команду,
чтобы сгенерировать файл MyTestKeyPair. snk:
sn -k MyTestKeyPair.snk
Хеш-код сборки
Данные
секретного ключа
Цифровая
подпись
Глава 14. Конфигурирование сборок .NET 527
Теперь, имея данные ключей, необходимо проинформировать компилятор С# о том,
где расположен файл MyTestKeyPair. snk. Как уже рассказывалось ранее в настоящей
главе, при создании любого нового проекта на С# в Visual Studio 2010 в числе
первоначальных файлов (отображаемых в узле Properties окна Solution Explorer) всегда
автоматически создается файл по имени Assembly Info . cs. В этом файле содержится набор
атрибутов, описывающих саму сборку. С помощью атрибута AssemblyKeyFile можно
сообщить компилятору о местонахождении действительного файла * . snk. Путь должен
быть указан в виде строкового параметра, например:
[assembly: AssemblyKeyFile(@"C:\MyTestKeyPair\MyTestKeyPair.snk")]
На заметку! Когда значение атрибута [AssemblyKeyFile] задается вручную, Visual Studio 2010
будет генерировать предупреждение, которое информирует о том, что необходимо
использовать для csc.exe опцию /keyfile или создать файл ключей в окне Properties. Об этом
речь пойдет позже, а пока генерируемое предупреждение можно проигнорировать.
Из-за того, что в состав строгого имени должен обязательно входить номер
версии разделяемой сборки, указание версии для сборки CarLibrary.dll является
важной деталью. В файле Assemblylnf о. cs доступен еще один атрибут под названием
AssemblyVersion. Первоначально в нем в качестве номера версии для сборки
устанавливается значение 1.0.0.0:
[assembly: AssemblyVersion (.0.0.0")]
Вспомните, что в .NET номер версии состоит из четырех частей (старшего номера,
младшего номера, номера сборки и номера редакции). Хотя указывать номер версии
нужно самостоятельно, за счет использования группового символа (*) вместо
конкретных значений можно позволить Visual Studio 2010 автоматически увеличивать номер
сборки и редакции во время каждой компиляции. В рассматриваемом примере этого
делать не требуется, но в принципе это может выглядеть следующим образом:
// Формат номера версии:
// <старший номер>.<младший номер>.<номер сборки>.<номер редакции>
//В каждой из этих частей допускается указывать значения
// от 0 до 65535.
[assembly: AssemblyVersion(.0.*")]
Теперь у компилятора С# есть вся необходимая информация для генерации строгого
имени (поскольку конкретная культура в атрибуте [AssemblyCulture] не указывалась,
компилятор будет использовать культуру, которая установлена на текущей машине).
Давайте скомпилируем библиотеку кода CarLibrary, откроем ее в ildasm.exe и
заглянем в манифест. Теперь можно увидеть новый дескриптор publickey, содержащий всю
информацию об открытом ключе, и дескриптор . ver, отражающий номер версии,
который был задан в атрибуте [AssemblyVersion] (рис. 14.18).
Вот и замечательно! Теперь можно развертывать разделяемую сборку CarLibrary. dl l
в GAC. Однако вспомните о том, что в нынешние дни для создания сборок со строгими
именами у разработчиков приложений .NET есть возможность применять Visual Studio,
а не утилиту командной строки sn. ехе.
Прежде чем опробовать этот способ создания строгих имен, обязательно удалите
(или закомментируйте) следующую строку кода в файле Assembly Info . cs (если она
была добавлена):
// [assembly: AssemblyKeyFile(@"C:\MyTestKeyPair\MyTestKeyPair.snk")]
528 Часть IV. Программирование с использованием сборок .NET
Find Find Next
// — The following custon attribute is added automatically, do not uncoiment
// .custon instance uoid [mscorlibJSysten.Diagnostics.Debuggablenttribute::.ctor(uali
■1!ДД1Н!Иг1- (OB 2d Ot 88 M 80 BO 00 9i| ИО Й0 00 ПА 02 08 88 // .$
aa 2* 88 aa 52 53 *1 31 aa 8*000001 00 01 00 // .$..rsai
70 31 62 7F 01 8B 1B ЕЕ СЙ 57 5H 8Й 9B 2C B6 HS // >1b WT....E
F7 *■ 25 77 F3 D7 21 6C 7D 39 52 E8 5Й 21 83 ИЗ // .в*м..!1>9R.Z!..
9F 7F 51 16 2k 2E 83 2E B9 6Й CM D1 24 D7 18 42 // ..Q.$ j..$..B
B8 Й6 79 27 flD C8 4Й 91 91 DB FH BE 88 59 45 81 // ..y'..J VE.
52 68 86 81 58 D8 4F BE DF 11 13 F4 3D 26 45 ЙЙ // Rh..P.0 ftl .
D1 D1 FD 3F 7B 7C 66 B7 8C 5Й 38 DA 9E 1D 4F 49 // ...?{|f..Z8...01 i
4E 19 2B ЙВ SB 39 E2 F1 СЙ flO 9D F7 ВО С8 53 89 // N.+ ..9 S.
94 88 79 DF 32 1F 68 B4 75 Й9 E8 EF 98 21 38 B9 ) // ..y.2.h.U 10.
.hash algorithm 8x88888684
.ver 1:8:8:8
nodule CarLibrary.dll
Рис. 14.18. Если сборке назначено строгое имя, в ее манифест
записывается информация об открытом ключе
Генерирование строгих имен с помощью Visual Studio 2010
В Visual Studio 2010 можно как указывать путь к какому-нибудь уже
существующему файлу *.snk на странице свойств проекта, так и генерировать новый файл * . snk.
Чтобы создать новый файл * . snk для проекта CarLibrary, дважды щелкните на значке
Properties (Свойства) в окне Solution Explorer, перейдите на вкладку Signing (Подпись),
отметьте флажок Sign the assembly (Подписать сборку) и выберите в раскрывающемся
списке вариант New (Новый), как показано на рис. 14.19.
CarLibrary* X
п [ Ц/А
» Platform: ; N/A
Ш Sign the
Choose a strong name key file
_ < Browse.. > ~\£
When delay signed, the project will not run or be debuggable.
■ ord... j
Рис. 14.19. Создание нового файла * . snk в Visual Stuido 2010
После этого откроется окно с приглашением указать имя для нового файла * . snk
(например, myKeyFile . snk) и флажком Protect my key file with a password (Защитить
файл ключей с помощью пароля), отмечать который в рассматриваемом примере
необязательно (рис. 14.20).
После этого новый файл * . snk появится в окне Solution Explorer, как показано на
рис. 14.21, и при каждой компоновке приложения эти данные будут использоваться для
назначения сборке надлежащего строгого имени.
На заметку! Вспомните, что на вкладке Application (Приложение) в окне Properties имеется
кнопка Assembly Information (Информация о сборке). В результате щелчка на ней открывается
диалоговое окно, в котором можно устанавливать для сборки многочисленные различные
атрибуты, вроде номера версии, информации об авторских правах и т.д
Глава 14. Конфигурирование сборок .NET 529
Create Strong Name Key
Key file name:
myKeyPair.snkj
1 [j Protect my key file with a password
Enter password:
Confirm password;
|__ ___,
L
OK
Щ-ШШГ
] 1
( Cancel 1
Рис. 14.20. Указание имени для нового файла
*. snk в Visual Studio 2010
1 Solution Explorer
>>H
I /5 Solution 'CarLibrary' A project)
1 л ^СлгНЬтяту
3i Properties
> \M References
<£) Cer.cs
С|Ц DerivedCars.es
|Ц myKeyPair.snk
Я - ,J Solution Explorer
-n*|
■LflHI
швзшш
Рис. 14.21. Теперь сборке при каждой компиляции в Visual
Studio 2010 будет автоматически назначаться строгое имя
Установка сборок со строгими именами в GAC
Напоследок осталось лишь установить теперь имеющую строгое имя сборку
CarLibrary.dll в GAC. Хотя предпочитаемым способом для развертывания сборок в
GAC в производственной среде является создание инсталляционного пакета Windows
MSI (или применение какой-то коммерческой инсталляционной программы, подобной
InstallShield), в .NET Framework 4.0 SDK поставляется работающая утилита командной
строки gacutil .exe, которой удобно пользоваться для проведения быстрых тестов.
На заметку! Для взаимодействия с GAC на своей машине необходимо иметь права
администратора, что может требовать внесения соответствующих изменений в параметры контроля учетных
записей пользователей (UAC) в Windows Vista или Windows 7.
В табл. 14.1 перечислены некоторые наиболее важные опции этой утилиты (для
вывода полного списка поддерживаемых ею опций служит флаг /?).
Таблица 14.1. Опции, которые принимает утилита gacutil .exe
Опция
Описание
/i
/и
/1
Инсталлирует сборку со строгим именем в GAC
Удаляет сборку из GAC
Отображает список сборок (или конкретную сборку) в GAC
530 Часть IV. Программирование с использованием сборок .NET
Чтобы инсталлировать сборку со строгим именем с помощью gacutil.exe,
нужно открыть в Visual Studio окно командной строки и перейти в каталог, в котором
содержится библиотека CarLibrary.dll, например:
cd С:\MyCode\CarLibrary\bin\Debug
Затем можно инсталлировать библиотеку с помощью опции -i:
gacutil -i CarLibrary.dll
После этого можно проверить, действительно ли библиотека была развернута,
выполнив следующую команду (обратите внимание, что расширение файла при
использовании опции /1 не указывается):
gacutil -1 CarLibrary
Если все в порядке, в окне консоли должен появиться примерно такой вывод
(разумеется, значение PublicKeyToken будет уникальным):
The Global Assembly Cache contains the following assemblies:
carlibrary, Version=l.0.0.0, Culture=neutral, PublicKeyToken=33a2bc294331e8b9,
processorArchitecture=MSIL
В глобальном кэше сборок (GAC) содержатся следующие сборки:
carlibrary, Version=l.0.0.0, Culture=neutral, PublicKeyToken=33a2bc294331e8b9,
processor-Architecture=MSIL
Просмотр содержимого GAC с помощью проводника Windows
Как было описано в предыдущих изданиях этой книги, посвященных версиям .NET
1.0 — .NET 3.5, на этом этапе обычно нужно было открыть проводник Windows и найти
установленную сборку CarLibrary в GAC, перейдя в папку С: \Windows\assembly.
Однако (и это очень важно) если проделать подобное теперь, выяснится, что
ожидаемого значка CarLibrary в папке С: \Windows\assembly нет, несмотря на то, что
утилита gacutil.ехе подтвердила установку этой библиотеки.
Дело в том, что с выходом версии .NET 4.0 кэш GAC был поделен на две части. В
частности, в папке С: \Windows\assembly теперь размещаются библиотеки базовых
классов .NET 1.0 — .NET 3.5 (а также различные дополнительные сборки, в том числе
библиотеки сторонних производителей). Однако при компиляции сборки в .NET 4.0 и ее
развертывания в GAC с помощью gacutil .ехе она размещается в совершенно новом
месте, а именно — в папке С: \Windows\Microsoft .NET\assembly\GAC_MSIL.
В этой новой папке также имеется набор подкаталогов, каждый из которых
именован в соответствии с дружественным именем конкретной библиотеки кода (например,
\System. Windows . Forms/\System. Core и т.д.). Внутри каждой папки с
дружественным именем размещен еще один подкаталог, который всегда именуется по следующей
схеме:
v4.0_major.minor.build.revision publicKeyTokenValue
v4.0_<старший номер>.<младший номер>.<номер сборки>.<номер редакции> <значение
открытого ключа>
Префикс v4 . О свидетельствует о том, что библиотека компилировалась в версии
.NET 4.O. После этого префикса идет один символ подчеркивания, а за ним — номер
версии изучаемой сборки, каковым в рассматриваемом примере будет 1.0.0.0 для
CarLibrary.dll. Далее за парой символов подчеркивания следует значение маркера
открытого ключа, основанное на строгом имени. На рис. 14.22 показано, как может
выглядеть структура каталогов CarLibrary в среде Windows 7.
Глава 14. Конфигурирование сборок .NET 531
i» | Search у^СЦЛО...* f>\
ss ^ a •
Type
Applied
J| Accessibility
,. AspNetMMCExt
J* Carlibrary
ii *.0_*Л.0,0_ЗЗа2Ьс294331е8Ь9
£t CppCodeProvtder
i FShaip Compiler
CarLibrary.dH Date modified: 12/27/2009 745 PM
Application extension Size 6Я0 KB
'fc
Oate crested; 12/27/2009 7:45 PM
Рис. 14.22. Разделяемая сборка CarLibrary со строгим именем (версии 1.0.0.0)
Использование разделяемой сборки
При создании приложений, использующих разделяемую сборку, единственным
отличием от применения приватной сборки является способ, которым должна добавляться
ссылка на соответствующую библиотеку в Visual Studio 2010. Фактически для этого
используется тот же самый инструмент — диалоговое окно Add Reference. Однако
разница в том, что в этом случае упомянутое диалоговое окно не позволит добавить ссылку
на сборку за счет нахождения нужного файла в папке С: \Windows\Assembly, которая
предназначена только для сборок .NET 3.5 и предшествующих версий.
На заметку! Опытные разработчики приложений .NET наверняка помнят, что и раньше при
переходе в папку С: \Windows\assembly в диалоговом окне Add Reference внутри Visual Studio
не разрешалось добавлять ссылки на разделяемые библиотеки. По это причине приходилось
создавать отдельную копию библиотеки просто для того, чтобы иметь возможность добавлять
на нее ссылку. В Visual Studio 2010 дела обстоят гораздо лучше.
Если необходимо добавить ссылку на
сборку, которая была развернута в GAC
версии .NET 4.0, на вкладке Browse необходимо
перейти в представляющий нужную
библиотеку каталог по имени v4 . 0_ma j or. minor .
build, revision publicKeyTo ken Value,
как показано на рис. 14.23. С учетом
этого немного сбивающего с толку факта,
давайте создадим новый проект типа Console
Application по имени SharedCarLibClient и
добавим в него ссылку на сборку CarLibrary
только что описанным образом. Как и
следовало ожидать, после этого в папке References
внутри окна Solution Explorer появится
соответствующий значок. Выделив этот значок
и выбрав в меню View (Вид) пункт Properties
(Свойства), в окне Properties можно увидеть,
что свойство Copy Local (Локальная копия)
теперь установлено в False.
jO Add Reference
>™^TBrow"TSiL
Loekh: i, v40_1 000_33a2bc294331eab9 ~Ц
■ЕЭ-
Recent Hetns
2» Cad ^Network
^lAraries
Д, AndrewTroeben
•jjHomegroup
jl^ Computer
£ Mongo Drive (C j
|t Windows
1: Microsoft NET
File name
Rtesoityi
at
|Гуре
(Application extension
GAC_MSIL
■ Cartixary
i
J$ DVD RW Drive (D.)
3 DVD/CD-RW Drive (E)
-J&CDObntF)
^ My Passport <H)
Ц AndnewTrodsen i iPhone
Рис. 14.23. Добавление ссылки на
разделяемую сборку CarLibrary (версии 1.0.0.0) в
Visual Studio 2010
532 Часть IV. Программирование с использованием сборок .NET
Несмотря на это, добавим в новое клиентское приложение следующий тестовый
код:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CarLibrary;
namespace SharedCarLibClient
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** Shared Assembly Client *****");
SportsCar с = new SportsCarO ;
c.TurboBoost ();
Console.ReadLine ();
}
}
}
Если теперь скомпилировать это клиентское приложение и с помощью
проводника Windows перейти в каталог, в котором содержится файл SharedCarLibClient.exe,
то можно будет увидеть, что среда Visual Studio 2010 не скопировала CarLibrary.dll
в каталог клиентского приложения. Объясняется это тем, что при добавлении ссылки
на сборку, в манифесте которой присутствует значение .publickey, Visual Studio 2010
считает, что данная обладающая строгим именем сборка, скорее всего, будет
развертываться в GAC, и потому не заботится о копировании ее двоичного файла.
Исследование манифеста SharedCarLibClient
Вспомните, что при генерировании строгого имени для сборки в ее манифест
записывается открытый ключ. В связи с этим, когда в клиентское приложение добавляется
ссылка на строго именованную сборку, в ее манифест записывается и компактное хеш-
значение всего открытого ключа в маркере .publickeytoken. Поэтому, если открыть
манифест SharedCarLibClient.exe в утилите ildasm.exe, можно увидеть там
следующий код (разумеется, значение открытого ключа в маркере .publickeytoken будет
выглядеть по-другому):
.assembly extern CarLibrary
{
.publickeytoken = C3 A2 ВС 29 43 31 E8 B9 )
.ver 1:0:0:0
}
Если теперь сравнить значение открытого ключа, записанного в манифесте
клиента, и отображаемого в GAC, то легко обнаружить, что они совпадают. Как упоминалось
ранее, открытый ключ является одной из составляющих идентификационных данных
строго именованной сборки. Из-за этого CLR-среда будет загружать только версию
1.0.0.0 сборки по имени CarLibrary, из открытого ключа которой может быть
получено хеш-значение ЗЗА2ВС294331Е8В9. В случае невозможности обнаружить сборку,
соответствующую такому описанию в GAC (и приватную сборку по имени CarLibrary в
каталоге клиента), будет сгенерировано исключение FileNotFoundException.
Исходный код. Проект SharedCarLibClient доступен в подкаталоге Chapter 14.
Глава 14. Конфигурирование сборок .NET 533
Конфигурирование разделяемых сборок
Как и приватные сборки, разделяемые сборки можно конфигурировать за счет
добавления в клиентское приложение файла * .config. Разумеется, поскольку
разделяемые сборки развертываются в хорошо известном месте (в поддерживаемом в .NET 4.0
кэше GAC), использовать для них элемент <privatePath>, как для приватных сборок,
не требуется (хотя, если в клиенте применяются и разделяемые, и приватные сборки,
элемент <privatePath> может присутствовать в файле * . conf ig).
Конфигурационные файлы приложения вместе с разделяемыми сборками могут
использоваться, когда необходимо заставить CLR-среду привязаться к другой версии
отдельной сборки, обойдя значение, которое записано в манифесте клиента. Это может
быть удобно по нескольким причинам. Например, предположим, что была выпущена
версия 1.0.0.0 сборки, в которой через какое-то время обнаружился серьезный дефект.
Одна из возможных мер предусматривает переделку клиентского приложения так,
чтобы оно ссылалось на правильную, не содержащую дефекта сборку (с версией, например,
1.1.0.0), и передачу приложения и новой библиотеки на все целевые машины.
Другой вариант состоит в поставке новой библиотеки кода и файла * . conf ig,
который указывает исполняющей среде использовать новую (лишенную дефекта) версию.
После установки этой новой версии в GAC не понадобится ни компилировать, ни
распространять исходное клиентское приложение заново.
Давайте рассмотрим еще один пример. Предположим, что была поставлена первая
лишенная дефектов версия сборки A.0.0.0), а через пару месяцев в нее были
добавлены новые функциональные возможности, в результате чего получилась новая версия
2.0.0.0. Очевидно, что существующие клиентские приложения, которые
компилировались с версией 1.0.0.0, не будут иметь никакого понятия о появлении новых типов,
поскольку в их кодовой базе те никак не упоминаются. В новых клиентских приложениях,
однако, ссылка на новые функциональные возможности, предлагаемые в версии 2.0.0.0,
добавиться должна. В этом случае можно поставить на целевые машины сборку
версии 2.0.0.0 и сделать так, чтобы она могла работать вместе со сборкой версии 1.0.0.0.
В случае необходимости существующие клиенты могут динамически
перенаправляться для загрузки версии 2.0.0.0 (чтобы получить доступ к реализованным в этой версии
улучшениям) с использованием конфигурационного файла, при этом компилировать и
развертывать клиентское приложение заново не понадобится.
Фиксация текущей версии разделяемой сборки
Для иллюстрации динамической привязки к конкретной версии разделяемой
сборки откроем окно проводника Windows и скопируем текущую версию скомпилированной
сборки CarLibrary.dll A.0.0.0) в другой подкаталог (например, CarLibrary Version
1.0.0.0), чтобы сымитировать фиксацию данной версии (рис. 14.24).
Создание разделяемой сборки версии 2.0.0.0
Теперь откроем существующий проект CarLibrary и обновим кодовую базу, добавив
в нее новый тип enum по имени MusicMedia, определяющий четыре возможных
музыкальных проигрывателя:
// Тип музыкального проигрывателя, установленный в автомобиле.
public enum MusicMedia
musicCd,
musicTape,
musicRadio,
musicMp3
534 Часть IV. Программирование с использованием сборок .NET
Qf^f' « MyCode ► CarLibraryVersion 1.0.0.0
■ Open with... Вцгп New folder
„. inetpub
Щ MyCode
s. CarLibrary
CarLibrary Version 1.0.0.0
|> CSharpCarClient
JB CustomMamespaces
** MurtrfileAssembly
|j SharedCarLibClient
1 VisuatBasicCarCtient
jo CarLibrary.dll
CarLibrary.dH Date modify 12/27/2009 6:43 PM
''•(в} Application extension Srze 6.00 KB
Рис. 14.24. Фиксация текущей версии CarLibrary.dll
Добавим в тип Саг новый общедоступный метод, позволяющий вызывающему коду
включать один из определенных музыкальных проигрывателей (не забыв при
необходимости импортировать в Car. cs пространство имен System. Windows . Forms):
public abstract class Car
{
public void TurnOnRadio(bool musicOn, MusicMedia mm)
{
if(musicOn)
MessageBox.Show(string.Format("Jamming {0}", mm));
else
MessageBox.Show("Quiet time...");
}
}
Теперь модифицируем конструкторы класса Car так, чтобы они предусматривали
отображение окна MessageBox с сообщением, подтверждающим то, что используется
сборка CarLibrary версии 2.0.0.0:
public abstract class Car
{
public Car()
{
MessageBox.Show("CarLibrary Version 2.0!")
}
public Car(string name, int maxSp, int currSp)
{
MessageBox.Show("CarLibrary Version 2.0!");
PetName = name; MaxSpeed = maxSp; CurrentSpeed = currSp;
И, наконец, что не менее важно, прежде чем снова компилировать новую
библиотеку, обновим номер версии с 1.0.0.0 на 2.0.0.0. Вспомните, что это можно сделать
визуально, дважды щелкнув на значке Properties в окне Solution Explorer, а затем щелкнув
на кнопке Assembly Information внутри вкладки Application (Приложение). В
отрывшемся диалоговом окне соответствующим образом измените значения в полях Assembly
Version (Версия сборки), как показано на рис. 14.25.
Глава 14. Конфигурирование сборок .NET 535
Assembly Information I Г МШИ
Г***
Prescription:
J Company:
I Product
I Cflpyrigr*
1 Trademark:
| Assembly version
1 file version:
i euro:
Carlibrary
Microsoft
CarLibrary
Copyright С Microsoft 2009
;2j ;0 0 0
1 0 0~~ <f
892da2c2-c0f7-426e-al98-0b4fl522a223
Neutral language: (None) ▼
: ] Make assembr
KOM-Visible
[ . QK [_ Cancel ;
Рис. 14.25. Изменение номера версии сборки CarLibrary.dll на 2.0.0.0
Если теперь заглянуть в каталог \bin\Debug проекта, можно заметить, что в нем
появилась новая версия сборки B.0.0.0), а прежняя версия 1.0.0.0 благополучно
сохранена в подкаталоге CarLibrary Version 1.0.0.0. Инсталлируем новую версию сборки
в GAC для .NET 4.0 с помощью утилиты gacutil.exe, как было описано ранее в
настоящей главе. Обратите внимание, что после этого на машине будут присутствовать две
версии одной и той же сборки (рис. 14.26).
©■ v\ « assembly ► GAC MSB. ► CarLibrary ► v4J0 2.0.0Л_ЗЗа2Ьс294331е8Ь9
_.^^ *J_:;..
-J. Mkrosoft.NET
л J* assembly
> Jb GAC32
.' k GAC.64
л £ GAC.MSIL
t> Jj Accessibility
> t, AspNetMMCExt
<• , CarLibrary
M у41).1.0ЛЛ_ЗЗа2Ьс294331е8Ь9
уЦЛ_2ЛЛЛ_ЗЗа2Ьс294331«вЬ9
■> j. CppCodeProvider
M
CarLibrary.d!l Date modified: 12/27/2009 8:S4 PM Date created: 12/27/2009 8:54 PM
Application extension Sue: 600 KB
Рис. 14.26. Несколько версий одной и той же разделяемой сборки
После запуска приложения SharedCarLibClient. exe окно с сообщением CarLibrary
Version 2.0! не отображается, поскольку в манифесте явным образом запрашивается
версия 1.0.0.0. Каким же образом указать CLR-среде, что должна осуществляться
привязка к версии 2.0.0.0? Ответ на этот вопрос ищите ниже.
На заметку! В Visual Studio 2010 ссылки автоматически переустанавливаются
соответствующим образом при компиляции приложений. Поэтому в случае запуска приложения
SharedCarLibClient.exe в среде Visual Studio 2010, она автоматически подхватит
версию 2.0.0.0. Если вы случайно запустили приложение именно таким образом, просто удалите
текущую ссылку на CarLibrary.dll и выберите версию 1.0.0.0 (которая должна была быть
размещена в папке CarLibrary Version 1.0.0.0).
536 Часть IV. Программирование с использованием сборок .NET
Динамическое перенаправление на конкретную
версию разделяемой сборки
Чтобы заставить CLR-среду загружать отличную от указанной в
манифесте версию разделяемой сборки, можно создать файл *. con fig, содержащий
элемент <dependentAssembly>. В этом элементе необходимо создать подэлемент
<assemblyldentity> с дружественным именем сборки, которая указана в манифесте
клиента сборки (в настоящем примере CarLibrary), и необязательным атрибутом
культуры (которому можно присвоить пустую строку или вообще опустить, если должна
использоваться культура по умолчанию). Помимо этого, в элементе <dependentAssembly>
понадобится создать подэлемент <bindingRedirect> и указать в нем версию, в теку
щий момент заданную в манифесте (в атрибуте oldVersion), и версию, которая
должна загружаться вместо нее из GAC (в атрибуте newVersion).
Давайте создадим в каталоге приложения SharedCarLibClient новый
конфигурационный файл SharedCarLibClient. ехе. conf ig и добавим в него приведенные ниже
XML-данные.
На заметку! Значение открытого ключа на машине читателя, разумеется, будет выглядеть не так,
как показано в приведенном ниже коде разметки; для просмотра открытого ключа необходимо
заглянуть в манифест клиента с помощью утилиты ildasm.exe или через GAC.
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.vl">
<dependentAssembly>
<assemblyIdentity name="CarLibrary"
publicKeyToken= 3A2BC2 94 331E8B9"
culture=llneutral"/>
<bindingRedirect oldVersion= .0.0.0"
newVersion= .0.0.0"/>
</dependentAssemblу>
</assemblyBinding>
</runtime>
</configuration>
Теперь снова попробуем запустить приложение SharedCarLibClient.exe. На этот
раз должно появиться окно с сообщением об использовании версии 2.0.0.0.
В конфигурационном файле клиента допускается создавать несколько элементов
<dependentAssembly>. Хотя в текущем примере в подобном нет никакой
необходимости, давайте предположим, что в манифесте SharedCarLibClient.exe также
упоминается и сборка MathLibrary версии 2.5.0.0. Тогда, для указания, что помимо сборки
CarLibrary версии 2.0.0.0 должна использоваться и сборка MathLibrary версии 3.0.0.0,
можно было бы создать конфигурационный файл SharedCarLibClient. ехе. conf ig со
следующим содержимым:
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.vl">
<!-- Этот раздел отвечает за привязку к CarLibrary -->
<dependentAssembly>
<assemblyIdentity name="CarLibrary"
publicKeyToken= 3A2BC2 94 331E8B9"
culture=""/>
<bindingRedirect oldVersion= .0.0.0"
newVersion= .0.0.0"/>
</dependentAssembly>
Глава 14. Конфигурирование сборок .NET 537
<!— Этот раздел отвечает эа привязку к MathLibrary —>
<dependentAssembly>
<assemblyIdentity name="MathLibrary"
publicKeyToken= 3A2BC2 94 331E8B9"
culture=""/>
<bindingRedirect oldVersion= .5.0.0"
newVersion= .0.0.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
На заметку! В атрибуте oldVers ion можно указывать диапазон номеров прежних версий; например,
<bindingRedirect oldVersion="l.0.0.0-1.2.0.0" newVersion=.0.0.0"/>
заставляет CLR-среду использовать версию 2.0.0.0 вместо любой из прежних версий,
относящихся к диапазону от 1.0.0.0 до 1.2.0.0.
Сборки политик издателя
Следующим моментом, который мы рассмотрим относительно конфигурирования
сборок, является роль так называемых сборок политик издателя (publisher policy
assemblies). Как было только что показано, за счет создания файлов * . con fig можно
заставить исполняющую среду использовать какую-то конкретную версию разделяемой
сборки, обходя ту, что упоминается в манифесте клиента. Все это замечательно, но
давайте представим, что для выполнения обязанностей администратора требуется
переконфигурировать все клиентские приложения на текущей машине так, чтобы в них
использовалась сборка CarLibrary.dll именно версии 2.0.0.0. Из-за строгих правил
по именованию конфигурационных файлов, придется дублировать одно и то же XML-
содержимое во множестве мест (при условии, что места, в которых находятся
использующие CarLibrary исполняемые файлы, действительно известны). Очевидно, что это
будет настоящим кошмаром!
Политики издателя позволяют "издателю" конкретной сборки (в роли которого
может выступать отдельный программист, подразделение или целая компания) поставлять
двоичную версию файла * . con fig, подлежащую установке в GAC вместе с более новой
версией соответствующей сборки. Преимущество такого подхода состоит в том, что при
его применении добавлять отдельные файлы *.configB каталоги клиентских
приложений не нужно. Вместо этого CLR-среда будет считывать текущий манифест и пытаться
найти запрашиваемую версию сборки в GAC. Однако если CLR-среда обнаружит там
сборку политик издателя, она прочитает содержащиеся в ней XML-данные и выполнит
запрашиваемое перенаправление на уровне GAC.
Создавать сборки политик издателя можно в командной строке с помощью такой
поставляемой в .NET утилиты, как al. exe (assembly linker — редактор связей сборки).
Эта утилита поддерживает множество опций, но для создания сборки политик издателя
ей требуется передать только следующие сведения:
• путь к файлу * . config или * .xml, в котором содержатся касающиеся
перенаправления инструкции;
• имя результирующей сборки политик издателя;
• путь к файлу * . snk, который должен использоваться для подписения сборки
политик издателя;
• номера версий, который необходимо назначить создаваемой сборке политик
издателя.
538 Часть IV. Программирование с использованием сборок .NET
Чтобы создать сборку политик издателя для библиотеки CarLibrary. dll,
выполните следующую команду (должна вводиться в одной строке):
al /link: CarLibraryPolicy.xml /out:policy.1.0.CarLibrary.dll /keyf:C:\MyKey\
myKey.snk /v:l.0.0.0
В команде указано, что необходимое XML-содержимое находится в файле по имени
CarLibraryPolicy .xml. Имя выходного файла (которое должно обязательно
соответствовать формату policy. <старший номер>. <младший номер>.<имя
конфигурируемой сборки>) задано с помощью флага /out. Кроме того, обратите внимание, что имя
файла, в котором содержатся значения открытого и секретного ключей, тоже должны
быть указаны с помощью флага /keyf. Помните, что файлы политик издателя
являются разделяемыми и потому должны обязательно иметь строгие имена!
В результате выполнения данной команды создается новая сборка, которую
далее можно помещать в GAC, принуждая всех клиентов использовать библиотеку
CarLibrary.dll версии 2.0.0.0, без создания отдельных конфигурационных файлов
для каждого клиентского приложения. Благодаря такому подходу, несложно обеспечить
перенаправление в масштабах всей машины для всех приложений, использующих
конкретную версию (или набор версий) существующей сборки.
Отключение политик издателя
Теперь предположим, что (исполняя обязанности системного администратора) была
развернута сборка политик издателя (и новейшая версия соответствующей сборки) в
GAC на клиентской машине. При этом, как обычно бывает, девять из десяти
задействованных приложений переключились на использование версии 2.0.0.0 безо всяких
проблем, а одно (по ряду причин) при получении доступа к CarLibrary. dll версии 2.0.0.0
постоянно выдает ошибку (как известно, создание программного обеспечения с полной
обратной совместимостью практически невозможно).
В подобных случаях в .NET можно создавать для проблемного клиента
конфигурационный файл, указывающий CLR-среде игнорировать наличие любых установленных
в GAC файлов политик издателя. Остальные клиентские приложения, способные
работать с новейшей версией сборки .NET продолжают перенаправляться должным образом
через установленную сборку политик издателя. Чтобы отключить политики издателя
на уровне отдельных клиентов, необходимо создать файл * .config (с
соответствующим именем), добавить в него элемент <publisherPolicy> и установить в нем атрибут
apply в по. После этого CLR-среда начинает снова загружать ту версию сборки, которая
была изначально указана в манифесте клиента.
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.vl">
<publisherPolicy apply="no11 />
</assemblyBinding>
</runtime>
</configuration>
Элемент <codeBase>
В конфигурационных файлах приложений с помощью элемента <codeBase>
можно указывать так называемые кодовые базы. Этот элемент заставляет CLR-среду
выполнять поиск зависимых сборок в различных местах (например, в сетевых конечных
точках или каталогах, находящихся на той же машине, но за пределами каталога
клиентского приложения).
Глава 14. Конфигурирование сборок .NET 539
Если в <codeBase> указано место на удаленной машине, загрузка сборки будет
производиться только по требованию и только в определенный каталог внутри GAC,
называемый кэшем загрузки. На основе того, что уже известно о развертывании сборок в
GAC, ясно, что сборки, загружаемые из указанного в <codeBase> места, должны иметь
строгие имена (иначе CLR-среда не смогла бы их инсталлировать в GAC). Просмотреть
содержимое кэша загрузки на своей машине можно, запустив утилиту gacutil.exe с
опцией /ldl:
gacutil /ldl
На заметку! С технической точки зрения элемент <codeBase> допускается использовать и для
поиска сборок, не обладающих строгим именем. В таком случае место размещения сборки
должно указываться относительно каталога клиентского приложения (в таком случае этот
элемент является не более чем альтернативой <privatePath>).
Чтобы посмотреть на элемент <codeBase> в действии, давайте создадим новое
консольное приложение по имени CodeBaseClient, добавим в него ссылку на CarLibrary.dll
версии 2.0.0.0 и обновим первоначальный файл кода, как показано ниже:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CarLibrary;
namespace CodeBaseClient
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** Fun with CodeBases *****");
SportsCar с = new SportsCar();
Console.WriteLine("Sports car has been allocated.");
Console.ReadLine() ;
}
}
}
Из-за того, что сборка CarLibrary.dll была развернута в GAC, в принципе можно
уже запускать приложение в таком, как оно есть виде. Чтобы поработать с элементом
<codeBase>, создадим на диске С: новую папку (например, С: \MyAsms) и поместим в нее
копию сборки CarLibrary.dll версии 2.0.0.0.
Теперь добавим в проект CodeBaseClient файл Арр. conf ig (как объяснялось ранее в
этой главе) и вставим в него следующее XML-содержимое (не забывайте о том, что значение
.publickeytoken у каждого будет выглядеть по-своему, и посмотреть его можно в GAC):
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.vl">
<dependentAssembly>
<assemblyIdentity name="CarLibrary"
publicKeyToken=3A2BC2 94331E8B9" />
<codeBase version=.0.0.0"
href="file:///C:\MyAsms\CarLibrary.dll" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
540 Часть IV. Программирование с использованием сборок .NET
Как здесь показано, элемент <codeBase> размещен внутри элемента <assemblyldentity>,
в котором в атрибутах name и publicKeyToken указано ассоциируемое со сборкой
дружественное имя и значение открытого ключа. В самом элементе <codeBase>
сообщается версия и (в свойстве href) место размещения сборки, которая должна загружаться.
Теперь, благодаря этому, даже в случае удаления сборки CarLibrary. dll версии 2.0.0.0
из GAC, данное клиентское приложение все равно будет успешно выполняться, потому
что CLR-среда сможет находить необходимую внешнюю сборку в каталоге С : \MyAsms.
На заметку! Размещение сборок в произвольных местах на машине для разработки, по сути,
приводит к перестройке системного реестра (и, соответственно, возникновению "ада DLL"), из-за
чего при перемещении или переименовании папки, в которой содержатся двоичные файлы,
текущие связи будут нарушаться. Поэтому элементом <codeBase> следует пользоваться с
осторожностью.
Элемент <codeBase> может быть удобен при добавлении ссылки на сборки,
находящиеся на удаленной машине в сети. Например, предположим, что есть разрешение
на доступ к папке, находящейся по адресу http : //www .MySite . com. В таком случае,
чтобы обеспечить загрузку из нее удаленного файла * . dll в кэш загрузки GAC на
локальной машине, можно обновить элемент <codeBase> следующим образом:
<codeBase version=.О.О.О"
href="http://www.MySite.com/Assemblies/CarLibrary.dll" />
Исходный код. Проект CodeBaseClient доступен в подкаталоге Chapter 14.
Пространство имен System.Configuration
До сих пор во всех показанных файлах * . conf ig применялись лишь хорошо
известные XML-элементы для указания CLR-среде, где следует искать внешние сборки.
Помимо этих элементов, в конфигурационный файл клиента можно также включать
и касающиеся только конкретного приложения данные, не имеющие ничего общего с
механизмом связывания. В связи с этим неудивительно, что в составе .NET Framework
поставляется пространство имен, которое позволяет программно считывать данные из
конфигурационного файла клиента.
Это пространство имен называется System.Conf iguration и включает в себя
небольшой набор типов, которые могут применяться для считывания специальных
настроек из конфигурационного файла клиента. Эти специальные настройки должны
содержаться внутри элемента <appSettings>. Элемент <appSettings>, в свою очередь,
может содержать любое количество элементов <add> с парами получаемых программно
ключей и значений.
Для примера предположим, что имеется файл * . conf ig, предназначенный для
консольного приложения AppConf igReaderApp, в котором определены два касающихся
конкретно этого приложения значения:
<configuration>
<appSettings>
<add key="TextColor" value="Green" />
<add key="RepeatCount" value="8" />
</appSettings>
</configuration>
Чтобы прочитать эти значения с целью использования в клиентском приложении,
можно вызвать на уровне экземпляра метод Get Value (), который является членом
Глава 14. Конфигурирование сборок .NET 541
типа System.Conf iguration . AppSettingsReader. Как показано в следующем коде,
в качестве первого параметра метод GetValue () принимает имя ключа, указанного в
файле * . conf ig, а во втором — тип, к которому этот ключ относится (получаемый
посредством операции type of):
using System;
using System. Collections .Generic-
using System.Ling;
using System.Text;
using System.Configuration;
namespace AppConflgReaderApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** Reading <appSettings> Data *****\n");
// Получаем специальные данные из файла *.config.
AppSettingsReader ar = new AppSettingsReader();
int numbOfTimes = (int)ar.GetValue("RepeatCount", typeof(int));
string textColor = (string)ar.GetValue("TextColor", typeof(string));
Console.ForegroundColor =
(ConsoleColor)Enum.Parse(typeof(ConsoleColor), textColor);
// Теперь выводим сообщение правильным образом.
for(int 1=0; i < numbOfTimes; i++)
Console.WriteLine("Howdy!");
Console.ReadLine();
}
}
}
В остальной части книги будут рассматриваться многие другие важные разделы,
которые встречаются в конфигурационных файлах клиентских или веб-приложений.
Очевидно, что чем глубже погружаться в детали программирования с использованием
.NET, тем важнее становится понимание деталей конфигурационных файлов XML.
Исходный код. Проект AppConf igReaderApp доступен в подкаталоге Chapter 14.
Резюме
В этой главе было показано, каким образом CLR-среде определяет
месторасположение внешних сборок, на которые производятся ссылки. Сначала рассматривались
элементы, которые включаются в состав содержимого сборок: заголовки, метаданные,
манифесты и CIL-код. Затем было показано, как создавать однофайловые и
многофайловые сборки, а также клиентские приложения (на разных языках).
Далее были описаны отличия между приватными и разделяемыми сборками.
Приватные сборки копируются в подкаталог клиента, а разделяемые развертываются
в кэше глобальных сборок (GAC) при условии назначения им строгих имен. И, наконец,
в главе было показано, как конфигурировать приватные и разделяемые сборки с
помощью конфигурационного файла XML, действующего на стороне клиента, и с помощью
сборки политик издателя, действующей на уровне всей машины.
ГЛАВА 15
Рефлексия типов,
позднее связывание
и программирование
с использованием
атрибутов
Как рассказывалось в предыдущей главе, базовой единицей разработки в мире
.NET являются сборки. С применением встроенных средств для просмотра
объектов Visual Studio 2010 (и многих других IDE-сред) легко узнать, какие типы входят в
состав упоминаемых в проекте сборок, а с помощью внешних инструментов, таких как
утилиты ildasm.exe и reflector.exe — проанализировать CIL-код, метаданные
типов и манифест сборки для любого двоичного файла .NET. Помимо такого исследования
сборок .NET на этапе проектирования, ту же самую информацию о них можно получить
и программно с использованием пространства имен System.Reflection. В главе
сначала рассматривается роль рефлексии и необходимость применения метаданных .NET.
В остальной части главы затрагивается ряд других тесно связанных с этим тем, так
или иначе касающихся служб рефлексии. Например, будет показано, как клиентское
приложение .NET посредством динамической загрузки и позднего связывания может
активизировать типы, о которых на момент компиляции ничего не было известно. Кроме
того, будет описано, как вставлять в сборки .NET специальные метаданные с
использованием как системных, так и специальных атрибутов. Для практической демонстрации
всех этих аспектов в завершение главы приводится пример построения нескольких так
называемых "объектов-оснасток", которые можно подключать к расширяемому
приложению Windows Forms.
Необходимость в метаданных типов
Возможность полностью описывать типы (классы, интерфейсы, структуры,
перечисления и делегаты) с помощью метаданных является одной из ключевых в платформе
.NET. Во многих технологиях .NET, таких как сериализация объектов, удаленная работа,
веб-службы XML и Windows Communication Foundation (WCF), эта возможность нужна
Глава 15. Рефлексия типов, позднеесвязываниеипрограммированиесиспользованиематрибутов 543
для выяснения формата типов во время выполнения. Более того, в средствах
обеспечения функциональной совместимости между языками, многочисленных службах
компилятора и предлагаемой в IDE-среде функции IntelliSense везде за основу берется
конкретное описание пиша.
Невзирая на важность (а, возможно, и благодаря ей), метаданные не являются
новинкой, поставляемой только в .NET Framework. В Java, CORBA и СОМ тоже
применяются похожие концепции. Например, в СОМ для описания типов, содержащихся внутри
сервера СОМ, используются специальные библиотеки типов СОМ (которые являются не
более чем просто скомпилированным IDL-код). Как и в СОМ, в библиотеках кода в .NET
поддерживаются метаданные типов, но эти метаданные, конечно же, синтаксически
совершенно не похожи на IDL-метаданные из СОМ.
Вспомните, что утилита ildasm.exe позволяет просматривать метаданные всех
содержащихся в сборке типов, для чего в ней нужно нажать комбинацию клавиш <Ctrl+M>
(см. главу 1). Если открыть в этой утилите любую из сборок * . dll или * . ехе, которые
создавались ранее в книге (например, CarLibrary .dll из предыдущей главы) и нажать
<Ctrl+M>, можно увидеть метаданные всех содержащихся в ней типов, как показано на
рис. 15.1.
Global fields
Global MemberRefs
TypeDef 1И @2000002)
TypDefNane: CarLibrary.HusicHedia @2000002)
Flags : [Public] [flutoLayout] [Class] [Sealed] [ftnsiClass] @000
Extends : 01000001 [TypeRef] System.Enum
Field «1 @4000001)
Field Name: ualue_ @4000001)
Рис. 15.1. Просмотр метаданных сборки с помощью утилиты ildasm.exe
На этом рисунке видно, что метаданные типов .NET отображаются в ildasm.exe
очень подробно (в двоичном формате они гораздо компактнее). В действительности
описание всех метаданных сборки CarLibrary.dll заняло бы несколько страниц. Однако
вполне достаточно будет кратко рассмотреть только некоторые наиболее важные
описания метаданных сборки CarLibrary .dll.
На заметку! Глубоко вдаваться в то, что обозначает синтаксис в каждом из приводимых в следующих
разделах фрагментов метаданных .NET, не стоит. Главное — понять, что метаданные .NET
являются очень описательными и предусматривают перечисление каждого определенного внутри (или
упоминаемого с помощью внешней ссылки) типа, который встречается в данной кодовой базе.
Просмотр (части) метаданных перечисления EngineState
Каждый тип, который определен внутри текущей сборки, сопровождается маркером
TypeDef #n (TypeDef — сокращение от type definition (определение типа)). Если
описываемый тип предусматривает использование еще какого-то типа, определенного в другой
544 Часть IV. Программирование с использованием сборок .NET
сборке .NET, этот второй тип сопровождается маркером TypeRef #n (TypeRef —
сокращение от type reference (ссылка на тип)). Маркер TypeRef, по сути, является указателем
на полное определение метаданных соответствующего типа во внешней библиотеке.
Вкратце, метаданные в .NET представляют собой ряд таблиц, в которых явным
образом перечислены все определения типов (TypeDef) и типы, на которые они ссылаются
(TypeRef), причем те и другие можно просматривать в специальном окне метаданных
утилиты ildasm.exe.
В случае сборки CarLibrary.dll одним из описаний, встречающихся среди
определений типов TypeDef, является метаданные перечисления CarLibrary .EngineState
(номер описания может отличаться, потому что нумерация определений типов зависит
от порядка, в котором компилятор С# обрабатывает файл):
TypeDef #2 @2000003)
TypDefName: CarLibrary.EngineState @2000003)
Flags : [Public] [AutoLayout] [Class] [Sealed] [AnsiClass] @0000101)
Extends : 01000001 [TypeRef] System.Enum
Field #1 @4000006)
Field Name: value @4000006)
Flags : [Public] [SpecialName] [RTSpecialName] @0000606)
CallCnvntn: [FIELD]
Field type: 14
Field #2 @4000007)
Field Name: engineAlive @4000007)
Flags : [Public] [Static] [Literal] [HasDefault] @0008056)
DefltValue: A4) 0
CallCnvntn: [FIELD]
Field type: ValueClass CarLibrary.EngineState
Маркер TypDefName используется для описания имени данного типа, маркер
Extends — для описания базового класса, который лежит в его основе (и каковым в
данном случае является тип System.Enum), а маркер Field #n — для описания каждого
поля, которое входит в его состав. Ради краткости здесь были перечислены только
метаданные поля CarLibrary.EngineState.engineAlive.
Просмотр (части) метаданных типа Саг
Ниже приведена часть метаданных типа Саг, в которой иллюстрируются следующие
аспекты:
• как поля представляются в виде метаданных .NET;
• как методы представляются в виде метаданных .NET;
• как автоматическое свойство представляется в метаданных .NET.
TypeDef #3 @2000004)
TypDefName: CarLibrary.Car @2000004)
Flags : [Public] [AutoLayout] [Class] [Abstract]
[AnsiClass] [BeforeFieldlnit] @0100081)
Extends : 01000002 [TypeRef] System.Object
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 545
Field #2 @400000а)
Field Name: <PetName>k BackingField @400000A)
Flags : [Private] @0000001)
CallCnvntn: [FIELD]
Field type: String
Method #1 @6000001)
MethodName
Flags
RVA
ImplFlags
CallCnvntn
hasThis
ReturnType : String
No arguments.
get_PetName @6000001)
[Public] [HideBySig] [ReuseSlot] [SpecialName] @0000886)
0x000020d0
[IL] [Managed] @0000000)
[DEFAULT]
Method #2 @6000002)
MethodName
Flags
RVA
ImplFlags
CallCnvntn
hasThis
ReturnType
1 Arguments
Argument #1
1 Parameters
A) ParamToken : @8000001) Name : value flags
set_PetName @6000002)
[Public] [HideBySig] [ReuseSlot] [SpecialName] @00008
0x000020e7
[IL] [Managed] @0000000)
[DEFAULT]
Void
56)
String
[none] @0000000)
Property #1 A7000001)
Prop.Name
Flags
CallCnvntn
hasThis
ReturnType :
No arguments.
DefltValue :
Setter :
Getter :
0 Others
PetName A7000001)
[none] @0000000)
[PROPERTY]
String
@6000002) set_PetName
@6000001) get_PetName
Прежде всего, обратите внимание, что в метаданных класса Саг указан базовый
класс типа (System.Object) и различные флаги, которые описывают то, каким
образом был создан тип ([Public], [Abstract] и т.д.). Для метода (такого как конструктор
класса Саг) приводится имя, принимаемые параметры и возвращаемое значение.
Далее важно отметить, что автоматическое поле приводит к генерации
компилятором приватного вспомогательного поля (по имени <PetName>k BackingField) и двух
методов (в случае свойства, доступного для чтения и записи), которыми в
рассматриваемом примере являются getPetName () и setPetName (). И, наконец, само
свойство отображается в метаданных .NET на внутренние методы get /set с использованием
маркеров Setter и Getter.
546 Часть IV. Программирование с использованием сборок .NET
Изучение блока TypeRef
Вспомните, что в метаданных сборки описаны не только внутренние типы (Саг,
EngineStateHT.A.), но и любые внешние типы, на которые эти внутренние типы
ссылаются. Например, поскольку в сборке CarLibrary.dll были определены два
перечисления, в метаданных типа System.Enum будет присутствовать следующий блок TypeRef:
TypeRef #1 @1000001)
Token: 0x01000001
ResolutionScope: 0x23000001
TypeRefName: System.Enum
Просмотр метаданных самой сборки
В окне метаданных утилиты ildasm.exe можно также просматривать
метаданные .NET, описывающие саму сборку, для обозначения которых используется
маркер Assembly. Как показано в приведенном ниже (неполном) листинге, информация,
документируемая внутри таблицы Assembly, совпадает с той, что можно
просматривать через значок MANIFEST. Ниже показана часть метаданных манифеста сборки
CarLibrary.dll (версии 2.0.0.0):
Assembly
Token: 0x20000001
Name : CarLibrary
Public Key : 00 24 00 00 04 80 00 00 // и т.д.
Hash Algorithm : 0x00008004
Major Version: 0x00000002
Minor Version: 0x00000000
Build Number: 0x00000000
Revision Number: 0x00000000
Locale: <null>
Flags : [PublicKey] ... '
Просмотр метаданных внешних сборок, на которые
имеются ссылки в текущей сборке
Помимо маркера Assembly и набора блоков TypeDef и TypeRef, в метаданных .NET
могут применяться маркеры AssemblyRef #n для описания каждой из внешних сборок, на
которые имеются ссылки в текущей сборе. Например, поскольку в сборке CarLibrary. dll
используется класс System. Windows . Forms .MessageBox, в метаданных для сборки
System.Windows . Forms будет присутствовать следующий блок AssemblyRef:
AssemblyRef #2 B3000002)
Token: 0x23000002
Public Key or Token: b7 7a 5c 56 19 34 eO 89
Name: System.Windows.Forms
Version: 4.0.0.0
Major Version: 0x00000004
Minor Version: 0x00000000
Build Number: 0x00000000
Revision Number: 0x00000000
Locale: <null>
HashValue Blob:
Flags: [none] @0000000)
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 547
Просмотр метаданных строковых литералов
И, наконец, последним интересным моментом относительно метаданных .NET
является то, что внутри маркера User Strings обязательно документируются все
присутствующие в кодовой базе строковые литералы:
User Strings
70000001
70000019
70000035
70000065
70000083
700000ab
700000cd
A1) L'Mamming {0}"
A3) L"Quiet time. . . "
B3) L"CarLibrary Version 2.01
A4) L"Pamming speed!"
A9) L"Faster is better..."
A6) L"Time to call AAA"
A6) L"Your car is dead"
На заметку! Как показано в последнем листинге с метаданными, следует всегда помнить, что все
строки четко документируются в метаданных сборки. Это может приводить к серьезным
последствиям для безопасности, если строковые литералы используются для представления
паролей, номеров кредитных карт и прочей секретной информации.
Теперь может возникнуть вопрос (в лучшем случае) о том, как использовать эту
информацию в своих приложениях, или (в худшем случае) о том, а зачем вообще нужны
эти метаданные. Для получения ответа необходимо ознакомиться с
поддерживаемыми в .NET службами рефлексии. Имейте в виду, что польза от рассматриваемых ниже
средств может проясниться только к концу главы, поэтому наберитесь терпения.
На заметку! В окне Metalnfo утилиты ildasm.exe будет отображаться набор маркеров
CustomAttribute, которые служат для описания атрибутов, применяемых внутри кодовой
базы. Роль этих атрибутов более подробно рассматривается далее в этой главе.
Рефлексия
В мире .NET рефлексией (reflection) называется процесс обнаружения типов во время
выполнения. С применением служб рефлексии те же самые метаданные, которые
отображает утилита ildasm.exe, можно получать программно в виде удобной объектной
модели. Например, рефлексия позволяет извлечь список всех типов, которые
содержатся внутри определенной сборки * . dll или * . ехе (или даже внутри файла * . netmodule,
если речь идет о многофайловой сборке), в том числе методов, полей, свойств и
событий, определенных в каждом из них. Можно также динамически обнаруживать набор
интерфейсов, которые поддерживаются данным типом, параметров, которые
принимает данный метод, и других деталей подобного рода (таких как имена базовых классов,
информация о пространствах имен, данные манифеста и т.д.).
Как и в любом другом пространстве имен, в System.Reflection (которое
поставляется в составе сборки mscorlib.dll) содержится набор взаимосвязанных типов.
В табл. 15.1 описаны некоторые наиболее важные из этих типов.
Чтобы понять, каким образом использовать пространство имен System.
Reflection для программного чтения метаданных .NET, необходимо сначала
ознакомиться с классом System.Type.
548 Часть IV. Программирование с использованием сборок .NET
Таблица 15.1. Некоторые типы из пространства имен System.Reflection
Тип
Описание
Assembly
AssemblyName
Eventlnfo
Fieldlnfo
MemberInfo
Methodlnfo
Module
Parameterlnfo
Propertylnfo
В этом абстрактном классе содержатся статические методы, которые
позволяют загружать сборку, исследовать ее и производить с ней различные
манипуляции
Этот класс позволяет выяснить различные детали, связанные с
идентификацией сборки (номер версии, информация о культуре и т.д.)
В этом абстрактном классе хранится информация о заданном событии
В этом абстрактном классе хранится информация о заданном поле
Этот абстрактный базовый класс определяет общее поведение для типов
Eventlnfo, Fieldlnfo, Methodlnfo и Propertylnfo
В этом абстрактном классе содержится информация по заданному методу
Этот абстрактный класс позволяет получить доступ к определенному
модулю внутри многофайловой сборки
В этом классе хранится информация по заданному параметру
В этом абстрактном классе хранится информация по заданному свойству
Класс System.Type
Класс System. Type имеет набор членов, которые могут применяться для изучения
метаданных типа, и большинство из которых возвращает типы из пространства имен
System. Reflection. Например, член Туре. GetMethods () возвращает массив объектов
типа Methodlnfo, член Type.GetFields () — массив объектов типа Fieldlnfo, и т.д.
В табл. 15.2 приведен частичный список членов, которые поддерживает System.Type
(полный перечень можно найти в документации .NET Framework 4.0 SDK).
Таблица 15.2. Некоторые члены System.Type
Тип
Описание
IsAbstract
IsArray
IsClass
IsCOMObject
IsEnum
IsGenericTypeDefinition
IsGenericParameter
Islnterface
IsPrimitive
IsNestedPrivate
IsNestedPublic
IsSealed
IsValueType
GetConstructors()
GetEvents()
GetFields()
Getlnterfaces()
GetMembers()
GetMethods()
GetNestedTypes()
GetProperties()
Эти свойства позволяют выяснять ряд основных деталей об
интересующем типе (например, является ли он абстрактной
сущностью, массивом, вложенным классом и т.д.)
Эти методы позволяют получать массив представляющих
интерес элементов (интерфейсов, методов, свойств и т.д.).
Каждый из этих методов возвращает соответствующий массив
(например, GetFields () возвращает массив Fieldlnfo,
GetMethods () — массив Methodlnfo и тд.). Следует иметь
в виду, что каждый из них имеет также форму единственного
числа (например, GetMethod (), GetProperty () и т.п.),
которая позволяет извлекать только один элемент по имени, а не
целый массив подобных элементов
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 549
Окончание табл. 15.2
Тип Описание
FindMembers () Этот метод возвращает массив объектов типа Memberlnf о
на основе указанных критериев поиска
GetType () Этот статический метод возвращает экземпляр Туре,
обладающий указанным строковым именем
InvokeMember () Этот метод позволяет выполнять "позднее связывание" для
заданного элемента. Понятие позднего связывания
рассматривается позже в настоящей главе
Получение информации о типе с помощью
System. Object. GetType ()
Экземпляр класса Туре можно получить несколькими способами. Единственное, что
нельзя делать — так это напрямую создавать объект Туре с помощью ключевого слова
new, потому что класс Туре является абстрактным. Что касается первого способа, то
вспомните, что System. Object имеет метод GetType (), который возвращает экземпляр
класса Туре, представляющий метаданные текущего объекта:
// Получение информации о типе с использованием экземпляра SportsCar.
SportsCar sc = new SportsCar();
Type t = sc.GetType () ;
Очевидно, что такой подход будет работать только при условии известности типа на
этапе компиляции (в данном случае типа SportsCar) и наличии его экземпляра в
памяти. Из-за этого ограничения должно быть понятно, почему в утилитах вроде ildasm. ехе
информация о типах не получается путем непосредственного вызова для каждого из
них метода System. Object .GetType (), ведь ildasm.exe не компилировалась со
специальными сборками.
Получение информации о типе с помощью typeof ()
Следующий способ для получения информации о типе заключается в применении
операции typeof:
// Получение информации о типе с использованием операции typeof.
Type t = typeof (SportsCar);
В отдичие от метода System.Object .GetType (), операция typeof удобна тем, что
она не требует создания экземпляра объекта перед извлечением информации о типе.
Однако кодовой базе по-прежнему должно быть известно о представляющем интерес
типе на этапе компиляции, поскольку typeof ожидает получения строго
типизированного имени типа, а не его текстового представления.
Получение информации о типе с помощью
System. Туре. GetType ()
Для получения информации о типе более гибким образом можно вызывать
статический метод GetType () класса System. Type и указывать полностью уточненное
строковое имя типа, который требуется изучить. При таком подходе знать тип, из
которого будут извлекаться метаданные, на этапе компиляции не требуется, поскольку
Туре . GetType () принимает в качестве параметра экземпляр System. String, который
присутствует везде.
550 Часть IV. Программирование с использованием сборок .NET
На заметку! Когда речь идет о том, что при вызове метода Туре. GetType () знать тип на этапе
компиляции не нужно, понимается просто то, что этот метод может принимать любое
строковое значение (а не строго типизированную переменную). Разумеется, знать, как выглядит имя
типа в строковом формате, все равно необходимо!
Метод Туре. GetType () имеет перегруженную версию, принимающую
дополнительно два булевских параметра. Один из них отвечает за то, должно ли генерироваться
исключение, когда обнаружить тип не удается, а второй — за то, должен ли учитываться
регистр символов в строке. Для примера рассмотрим следующий код:
// Получение информации типа с использованием статического метода
// Туре.GetType () (указано не генерировать исключение, если не удается
// обнаружить тип SportsCar, и игнорировать регистр символов).
Type t = Type .GetType ("CarLibrary.SportsCar11, false, true);
Обратите внимание в этом примере на то, что в строке, передаваемой методу
GetType (), никак не упоминается сборка, внутри которой содержится интересующий
тип. В таком случае подразумевается, что тип содержится внутри сборки, выполняемой
в текущий момент Если требуется получить метаданные для типа, находящегося в
какой-то внешней приватной сборке, передаваемую строку необходимо сформатировать
так, чтобы в ней присутствовало полностью уточненное имя самого типа, запятая и
следом за ней дружественное имя сборки, внутри которой он находится:
// Получение информации о типе, находящемся внутри внешней сборки.
Type t = null;
t = Type.GetType("CarLibrary.SportsCar, CarLibrary");
Кроме того, в передаваемой GetType () строке может указываться знак плюс (+)
для обозначения вложенного типа. Например, предположим, что требуется получить
информацию о типе перечисления (SpyOptions), вложенного внутри класса по имени
JamesBondCar. Для этого можно воспользоваться следующим кодом:
// Получение информации о типе перечисления,
// вложенного внутри текущей сборки.
Type t = Type.GetType("CarLibrary.JamesBondCar+SpyOptions");
Создание специальной программы
для просмотра метаданных
Чтобы проиллюстрировать базовый процесс рефлексии (и оценить пользу от
System. Type), создадим консольное приложение по имени MyTypeViewer. Это
приложение будет отображать детали методов, свойств, полей и поддерживаемых интерфейсов
(а также другие интересные элементы данных) для любого из типов, содержащихся как
в самом приложении, так и в сборке mscorlib.dll (доступ к которой все приложения
.NET получают автоматически). После создания соответствующего проекта первым
делом необходимо импортировать пространство имен System.Reflection:
// Для выполнения рефлексии должно импортироваться это пространство имен.
using System.Reflection;
Рефлексия методов
Далее потребуется модифицировать класс Program, определив в нем ряд статических
методов так, чтобы каждый из них принимал единственный параметр System.Type
и возвращал void. Сначала определим метод ListMethods (), который будет
отображать имена методов, определенных в указанном типе. Необходимый код приведен
Глава15.Рефлексиятипов,позднеесвязываниеипрограммированиесиспользованиематрибутов 551
ниже. Обратите внимание, что Type.GetMethods () возвращает массив типов System.
Reflection.Methodlnfо, по которому можно осуществлять проход с помощью цикла
foreach.
// Отображение имен методов типа.
static void ListMethods(Type t)
{
Console.WriteLine ("***** Methods *****••);
MethodInfo[] mi = t. Getllethods () ;
foreach(Methodlnfо m in mi)
Console.WriteLine ("->{0}", m.Name);
Console.WriteLine ()/
}
В коде с применением свойства Methodlnf о. Name выводятся имена методов. Как
не трудно догадаться, класс Methndlnfo имеет дополнительные члены, которые
позволяют выяснить, является метод статическим, виртуальным, обобщенным или
абстрактным. Кроме того, тип Methodlnf о позволяет получить информацию о том, какое
значение возвращает метод, и какие параметры он принимает. Чуть позже реализация
ListMethods () будет соответствующим образом улучшена.
Для перечисления имен методов при желании можно было бы также создать LINQ-
запрос. Вспомните из главы 13. что технология LINQ to Object позволяет создавать
строго типизированные запросы и применять их в отношении коллекций объектов,
находящихся в памяти. При обнаружении блоков с циклами или программной логикой
принятия решения хорошим правилом считается использование соответствующего
LINQ-запроса. Например, предыдущий метод можно было бы переписать следующим
образом:
static void ListMethods(Type t)
{
Console.WriteLine("***** Methods *****");
var methodNames = from n in t.GetMethods() select n.Name;
foreach (var name in methodNcimes)
Console.WriteLine("->{0}", name);
Console.WriteLine();
}
Рефлексия полей и свойств
Реализация метода ListFields () выглядит похоже. Единственным заметным
отличием в ней является вызова Type.GetFieldsO и возврат в результате массива
Fieldlnfo. Для простоты осуществляется отображение лишь имени каждого из полей
за счет применения LINQ-запроса:
// Отображение имен полей типа.
static void ListFields(Type t)
{
Console.WriteLine ("***** Fields *****");
Fieldlnfo [] fi = t.GetFieldsO;
var fieldNames = from f in t.GetFieldsO select f.Name;
foreach (var name in fieldNames)
Console.WriteLine ("->{0}", name);
Console.WriteLine ();
}
Логика для отображения имен свойств типа выглядит аналогично:
// Отображение имен свойств типа.
static void ListProps(Type t)
552 Часть IV. Программирование с использованием сборок .NET
Console.WriteLine ("***** Properties *****");
Propertylnfo [ ] pi = t.GetProperties ();
var propNames = from p in t.GetProperties () select p.Name;
foreach (var name in propNames)
Console.WriteLine ("->{0}", name);
Console.WriteLine ();
}
Рефлексия реализуемых интерфейсов
Давайте создадим метод по имени ListlnterfacesO, способный отображать
имена любых из поддерживаемых указываемым типом интерфейсов. Здесь главный
интерес представляет вызов Getlnterfaces (), в результате которого возвращается массив
System. Types, что вполне логично, поскольку интерфейсы в действительности
представляют собой типы.
// Отображение имен реализуемых интерфейсов.
static void Listlnterfaces(Type t)
{
Console.WriteLine ("***** Interfaces *****");
var ifaces = from 1 in t .Getlnterfaces () select 1;
foreach(Type i in ifaces)
Console.WriteLine ("->{0 } ", l.Name);
На заметку! Следует иметь в виду, что большинство из "получающих", т.е. get-методов в System.
Type (GetMethods (), Getlnterfaces () и т.д.), имеют перегруженные версии,
принимающие значения из перечисления BindingFlags. Это позволяет более точно указать, поиск чего
должен производиться (например, только статических членов, только общедоступных членов,
включая приватные члены, и т.д.). Подробную информацию об этом можно найти в
документации .NET Framework 4.0 SDK.
Отображение различных дополнительных деталей
И, наконец, реализуем еще один вспомогательный метод, способный отображать
различные статистические данные о заданном типе (которые показывают, является ли тип
обобщенным, как выглядит его базовый класс, не является ли он запечатанным, и т.д.).
// Просто для предоставления полной картины.
static void ListVariousStats(Type t)
{
Console.WriteLine ("***** Various Statistics *****");
Console.WriteLine(Base class is: {0}, t.BaseType); // Базовый класс.
Console.WriteLine (Is type abstract? {0}, t.IsAbstract); // Абстрактный?
Console.WriteLine(Is type sealed? {0}, t.IsSealed); // Запечатанный?
Console.WriteLine (Is type generic {0},
t. IsGenericTypeDef mition) ; // Обобщенный?
Console.WriteLine (Is type a class type? {0}, t.IsClass); // Класс?
Console.WriteLine ();
}
Реализация метода Main ()
Модифицируем метод Main () в классе Program так, чтобы он запрашивал у
пользователя полностью уточненное имя типа, передавал его методу Туре. Get Type () и затем
отправлял извлеченный в результате его выполнения объект System. Type каждому из
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 553
вспомогательных методов. Весь этот процесс должен повторяться до тех пор, пока
пользователь не введет Q для завершения работы приложения.
static void Main(string[] args)
{
Console.WriteLine("***** Welcome to MyTypeViewer *****••);
string typeName = "";
do
{
Console.WriteLine ("\nEnter a type name to evaluate);
// Запрос имени типа.
Console.Write(or enter Q to quit: );
// Для выхода из программы необходимо ввести Q.
// Получение имени типа.
typeName = Console.ReadLine();
// Пользователь желает выйти из программы?
if (typeName.ToUpper() == "Q" )
{
break;
}
// Отобразить информацию о типе.
try
{
Type t = Type.GetType(typeName);
Console.WriteLine("");
ListVariousStats(t);
ListFields(t);
ListProps(t);
ListMethods(t);
Listlnterfaces(t);
}
catch
{
Console .WriteLine ("Sorry, can't find type11);
// Указанный тип не найден.
}
} while (true);
}
Теперь приложение MyTypeViewer. ехе готово к первому тестовому запуску. Давайте
запустим ее и введем следующие полностью уточненные имена (не забывая, что
выбранный способ вызова Type .GetType () требует ввода строковых имен с учетом регистра):
• System.Int32
• System.Collections.ArrayList
• System.Threading.Thread
• System.Void
• System.10.BinaryWriter
• System.Math
• System.Console
• MyTypeViewer.Program
Ниже показан частичный вывод при указании типа System.Math.
554 Часть IV. Программирование с использованием сборок .NET
***** Welcome to MyTypeViewer *****
Enter a type name to evaluate
or enter Q to quit: System.Math
***** Various Statistics *****
Base class is: System.Object
Is type abstract? True
Is type sealed? True
Is type generic? False
Is type a class type? True
***** Fields *****
->PI
->E
***** properties *****
***** Methods *****
->Acos
->Asin
->Atan
->Atan2
->Ceiling
->Ceiling
->Cos
Рефлексия обобщенных типов
При вызове Туре . Get Type () для получения описаний метаданных обобщенных
типов должен обязательно применяться специальный синтаксис в виде символа обратной
одинарной кавычки (Л) со следующим за ним числовым значением, которое
представляет количество параметров, поддерживаемое данным типом. Например, чтобы
отобразить описание метаданных обобщенного типа System. Collections . Generic. List<T>,
приложению потребуется передать следующую строку:
System.Collections.Generic.Listч1
Здесь используется числовое значение 1, поскольку List<T> имеет только один
параметр. Для применения рефлексии в отношении типа Dictionary<TKey, TValue>,
однако, пришлось бы указать значение 2:
System.Collections.Generic.Dictionary'2
Рефлексия параметров и возвращаемых значений методов
В текущем состоянии приложение работает вполне нормально. Однако давайте
внесем в него одно небольшое улучшение, а именно — обновим вспомогательную
функцию ListMethods () так, чтобы она предусматривала вывод не только имени
соответствующего метода, но также тип возврата и типы принимаемых параметров. В типе
Methodlnfo для этого предусмотрено свойство ReturnType и метод GetParameters ().
Ниже приведен измененный соответствующим образом код. Обратите внимание, что
строка с информацией о типе и имени каждого параметра создается с использованием
вложенного цикла f о reach (а не LINQ-запроса).
static void ListMethods(Type t)
{
Console .WnteLine ("***** Methods *** + *");
Methodlnfo[] mi = t.GetMethods();
foreach (Methodlnfo m in mi)
{
Глава 15. Рефлексиятипов.позднеесвязываниеипрограммированиесиспользованием атрибутов 555
// Получение информации о возвращаемом типе.
string retVal = m.ReturnType.FullName;
string paramlnfo = " ( ";
// Получение информации о принимаемых параметрах.
foreach (Parameterlnfo pi in m.GetParameters ())
{
paramlnfo += string.Format("{0} {1} " ,
pi.ParameterType, pi.Name);
}
paramlnfo += " ) ";
// Отображение общей сигнатуры метода.
Console.WriteLine ("->{0} {1} {2}", retVal, m.Name, paramlnfo);
}
Console.WriteLine ();
}
Если теперь запустить обновленное приложение, то обнаружится, что методы
указанного типа будут описаны гораздо более подробно. Например, в случае передачи ему
в качестве входного параметра имени все того же типа System.Object, можно увидеть
следующее описание его методов:
***** Methods *****
->System.String ToString ( )
->System.Boolean Equals ( System.Object obj )
->System.Boolean Equals ( System.Object objA System.Object objB )
->System.Boolean ReferenceEquals ( System.Object objA System.Object objB )
->System.Int32 GetHashCode ( )
->System.Type GetType ( )
Такая реализация ListMethods () удобна, поскольку позволяет исследовать
непосредственно каждый параметр и возвращаемый тип методов с помощью объектной модели
System.Reflection. Следует иметь в виду, что каждый из типов XXXInfo (Methodlnfo,
Propertylnfo, Eventlnfo и т.д.) имеет переопределенную версию ToString (),
способную отображать сигнатуру запрашиваемого элемента. Следовательно, ListMethods ()
можно было бы реализовать и так, как показано ниже (с использованием LINQ-запроса,
выбирающего все объекты Methodlnfo, а не только значения Name):
public static void ListMethods(Type t)
{
Console.WriteLine ("***** Methods *****");
Methodlnfo[] mi = t.GetMethods();
var methodNames = from n in t.GetMethods() select n;
foreach (var name in methodNames)
Console.WriteLine("->{0 } ", name);
Console.WriteLine ();
}
Весьма занимательно, не так ли? Разумеется, пространство имен System. Reflection
и класс System. Туре позволяют подвергать рефлексии многие другие аспекты типа
помимо тех, которые в настоящей момент охвачены в приложении MyTypeViewer. Они
позволяют также получать информацию о событиях, которые поддерживает тип, любых
обобщенных параметрах, которые способны принимать его члены, и десятки других деталей.
Тем не менее, к настоящему моменту получился своего рода браузер объектов. Его главное
ограничение состоит в том, что он позволяет подвергать рефлексии только текущую
сборку (MyTypeViewer) или всегда доступную сборку mscorlib.dll. Возникает вопрос: как
создать приложение, способное загружать (и подвергать рефлексии) сборки, о которых на
момент компиляции ничего не известно? Об этом пойдет речь в следующем разделе.
556 Часть IV. Программирование с использованием сборок .NET
Исходный код. Проект MyTypeViewer доступен в подкаталоге Chapter 15.
Динамически загружаемые сборки
В предыдущей главе рассказывалось о том, что при поиске внешних сборок, на
которые ссылается текущая сборка, CLR-среда заглядывает в манифест сборки. Во многих
случаях необходимо, чтобы сборки могли загружаться на лету программно, даже если
в манифесте о них не упоминается. Формально процесс загрузки внешних сборок по
требованию называется динамической загрузкой.
В пространстве имен System. Reflection поставляется класс по имени Assembly, с
применением которого можно динамически загружать сборку, а также просматривать
ее собственные свойства. Этот класс позволяет выполнять динамическую загрузку
приватных и разделяемых сборок, причем находящихся в произвольных местах. По сути,
класс Assembly предоставляет методы (в частности, Load () и LoadFrom ()), которые
позволяют программно поставлять ту же информацию, которая встречается в
клиентских файлах * . conf ig.
Чтобы посмотреть, как обеспечивать динамическую загрузку на практике,
создадим новый проект типа Console Application по имени ExternalAssemblyRef lector.
Определим в нем метод Main (), который будет запрашивать у пользователя
дружественное имя сборки для динамической загрузки. Кроме того, обеспечим передачу ссылки
на эту сборку вспомогательному методу по имени DisplayTypes (), который будет
выводить имена всех содержащихся в сборке классов, интерфейсов, структур, перечислений
и делегатов. Необходимый код выглядит на удивление просто.
using System;
using System.Collections .Generic-
using System.Linq;
using System.Text;
using System.Reflections-
using System.10; // Для определения FileNotFoundException.
namespace ExternalAssemblyReflector
{
class Program
{
static void DisplayTypesInAsm(Assembly asm)
{
Console.WriteLine ("\n***** Types in Assembly *****");
Console.WriteLine("->{0}", asm.FullName);
Type [ ] types = asm.GetTypes();
foreach (Type t in types)
Console.WriteLine("Type: {0}", t) ;
Console.WriteLine ( "");
}
static void Main(string [ ] args)
{
Console.WriteLine (***** External Assembly Viewer *****);
string asmName = ;
Assembly asm = null;
do
{
Глава 15. Рефлексиятипов.позднеесвязываниеипрограммированиесиспользованиематрибутов 557
Console.WriteLine("\nEnter an assembly to evaluate");
// Запрос имени интересующей сборки.
Console.Write(or enter Q to quit: );
// Для выхода из программы необходимо ввести Q.
// Получение имени сборки.
asmName = Console.ReadLine();
// Пользователь желает выйти из программы?
if (asmName.ToUpper() == "Q")
{
break;
}
// Выполнение загрузки сборки.
try
{
asm = Assembly.Load(asmName);
DisplayTypesInAsm(asm);
}
catch
{
Console.WriteLine("Sorry, can't find assembly.");
// Сборка не найдена.
}
} while (true);
}
}
}
Обратите внимание, что статическому методу Assembly. Load () передается только
дружественное имя сборки, которую требуется загрузить в память. Следовательно,
чтобы подвергнуть рефлексии сборку CarLibrary.dll с помощью этой программы,
понадобится сначала скопировать двоичный файл CarLibrary. dll в подкаталог bin\Debug
внутри каталога приложения ExternalAssemblyRef lector. После этого можно будет
получить примерно такой вывод:
***** External Assembly Viewer *****
Enter an assembly to evaluate
or enter Q to quit: CarLibrary \
***** Types in Assembly *****
->CarLibrary, Version=2.0.0.0, Culture=neutral, PublicKeyToken=33a2bc294331e8b9
Type: CarLibrary.MusicMedia
Type: CarLibrary.EngineState
Type: CarLibrary.Car
Type: CarLibrary.SportsCar
Type: CarLibrary.MiniVan
Чтобы сделать приложение ExternalAssemblyRef lector более гибким, можно
модифицировать код так, чтобы загрузка внешней сборки производилась с помощью
метода Assembly .LoadFrom (), а не Assembly. Load ():
try
{
asm = Assembly.LoadFrom(asmName);
DisplayTypesInAsm(asm);
}
После этого пользователь сможет вводить абсолютный путь к интересующей сборке
(например, C:\MyApp\MyAsm.dll). По сути, метод Assembly. LoadFrom () позволяет про-
558 Часть IV. Программирование с использованием сборок .NET
граммно предоставлять значение <codeBase>. Теперь консольному приложению можно
передавать полный путь. Таким образом, если сборка Car Library. dll находится в
папке С : \МуСode, будет получен следующий вывод:
***** External Assembly Viewer *****
Enter an assembly to evaluate
or enter Q to quit: C:\MyCode\CarLibrary.dll
***** Types in Assembly *****
->CarLibrary, Version=2.0.0.0, Culture=neutral, PublicKeyToken=33a2bc294331e8b9
Type: CarLibrary.EngineState
Type: CarLibrary.Car
Type: CarLibrary.SportsCar
Type: CarLibrary.MiniVan
Исходный код. Проект ExternalAssemblyRef lector доступен в подкаталоге Chapter 15.
Рефлексия разделяемых сборок
Метод Assembly. Load () имеет несколько перегруженных версий. Одна из них
позволяет указать значение культуры (для локализуемых сборок), а также номер версии и
значение маркера открытого ключа (для разделяемых сборок). Все вместе эти элементы,
которые идентифицируют сборку, называются отображаемым именем (display name),
формат которого выглядит так: сначала идет дружественное имя сборки, за ней строка
разделенных запятыми пар "имя/значение", а потом необязательные спецификаторы
(в любом порядке). Ниже приведен образец, которым следует пользоваться
(необязательные элементы указаны в круглых скобках):
Имя (, Vers ion = <старший номера .<мл а дший номер* .'помер сборки* .<номер редакции>)
(,Culture = <маркер культуры>) (,PublicKeyToken = <маркер открытого ключа>)
При создании отображаемого имени использование PubliuKeyToken=null означает,
что связывание и сопоставление должно выполняться со сборкой, не имеющей строгого
имени, a Culture=" " — что сопоставление должно выполняться с использованием
культуры, которая принята на целевой машине по умолчанию:
// Выполнение загрузки CarLibrary версии 1.0.0.0
//с использованием принятой по умолчанию культуры.
Assembly a =
Assembly. Load (@"CarLibrary, Version=l. 0 . 0 . 0, PublicKeyToken=null, Culture=""" ) ;
Следует также иметь в виду, что в пространстве имен System. Reflection
поставляется тип AssemblyName, который позволяет представлять строковую информацию
наподобие той, что была показана выше, в виде удобной объектной переменной. Обычно
этот класс применяется вместе с классом System. Version, который позволяет
упаковывать в объектно-ориентированную оболочку номер версии сборки. После создания
отображаемое имя может передаваться перегруженной версии метода Assembly. Load ():
// Использование AssemblyName для определения отображаемого имени.
AssemblyName asmName;
asmName = new AssemblyName();
asmlJame .Name = "CarLibrary";
Version v = new Version (.0.0.0");
asmName.Version = v;
Assembly a = Assembly.Load(asmName);
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 559
Для загрузки разделяемой сборки из GAC в параметре Assembly. Load () должно
обязательно указываться значение PublicKeyToken. Например, предположим, что
создано новое консольное приложение по имени SharedAsmRef lector, с помощью которого
нужно загрузить сборку System.Windows.Forms.dll версии 4.0.0.0, поставляемую в
составе библиотек базовых классов .NET. Поскольку количество типов в данной сборке
довольно велико, давайте сделаем так, чтобы это приложение предусматривало вывод
только имен всех общедоступных перечислений за счет применения просто
соответствующего LINQ-запроса:
using System;
using System.Collections .Generic-
using System.Linq;
using System.Text;
using System.Reflection;
using System.10;
using System.Linq;
namespace SharedAsmReflector
{
public class SharedAsmReflector
{
private static void Displaylnfo(Assembly a)
{
Console.WriteLine (***** info about Assembly *****);
Console.WriteLine(Loaded from GAC? {0}, a.GlobalAssemblyCache);
Console.WriteLine(Asm Name: {0}, a.GetName().Name);
Console.WriteLine(Asm Version: {0}, a.GetName().Version);
Console.WriteLine(Asm Culture: {0},
a.GetName().Culturelnfo.DisplayName);
Console.WriteLine(\nHere are the public enums:);
// Использование LINQ-запроса для нахождения
// общедоступных перечислений.
Туре[] types = a.GetTypes();
var publicEnums = from ре in types where pe.IsEnum &&
pe.IsPublic select pe;
foreach (var pe in publicEnums)
{
Console.WriteLine(pe);
}
}
static void Main(string[] args)
{
Console.WriteLine("***** The Shared Asm Reflector App *****\n");
// Выполнение загрузки System.Windows.Forms.dll из GAC.
string displayName = null;
displayName = "System.Windows.Forms," +
"Version=4.0.0.0, " +
"PublicKeyToken=b77a5c561934e089," + @"Culture= ;
Assembly asm = Assembly.Load(displayName);
Displaylnfo(asm);
Console.WriteLine("Done!");
Console.ReadLine();
560 Часть IV. Программирование с использованием сборок .NET
Исходный код. Проект SharedAsmRef lector доступен в подкаталоге Chapter 15.
К этому моменту должно стать более-менее понятно, как использовать некоторые
ключевые члены пространства имен System.Reflection для получения метаданных
во время выполнения. Разумеется, необходимость в самостоятельном создании
специальных браузеров объектов в повседневной работе, скорее всего, будет возникать не
слишком часто. Тем не менее, ни в коем случае не следует забывать о том, что службы
рефлексии обеспечивают основу для применения ряда других полезных методик
программирования, в том числе и позднего связывания.
Позднее связывание
Поздним связыванием (late binding) называется технология, которая позволяет
создавать экземпляр определенного типа и вызывать его члены во время выполнения без
кодирования факта его существования жестким образом на этапе компиляции. При
создании приложения, в котором предусмотрено позднее связывание с типом из какой-
то внешней сборки, добавлять ссылку на эту сборку нет никакой причины, и потому в
манифесте вызывающего кода она непосредственно не указывается.
На первый взгляд увидеть выгоду от позднего связывания не просто. Действительно,
если есть возможность выполнить раннее связывание с объектом (например, добавить
ссылку на сборку и разместить тип с помощью ключевого слова new), следует
обязательно так и поступать. Одна из наиболее веских причин состоит в том, что ранее
связывание позволяет выявлять ошибки во время компиляции, а не во время выполнения.
Тем не менее, позднее связывание тоже играет важную роль в любом создаваемом
расширяемом приложении. Пример создания такого "расширяемого" приложения будет
приведен в конце настоящей главы, в разделе "Создание расширяемого приложения", а
пока рассмотрим роль класса Activator.
Класс System. Activator
Класс System.Activator (определенный в сборке mscorlib.dll) играет ключевую
роль в процессе позднего связывания в .NET. В текущем примере интересует пока что
только его метод Activator . Createlnstance (), который позволят создавать
экземпляр подлежащего позднему связыванию типа. Этот метод имеет несколько
перегруженных версий и потому обеспечивает довольно высокую гибкость. В самой простой версии
Createlnstance () принимает действительный объект Туре, описывающий сущность,
которая должна размещаться в памяти на лету.
Чтобы увидеть, что имеется в виду, давайте создадим новый проект типа Console
Application по имени LateBindingApp, импортируем в него пространства имен
System. 10 и System.Reflection с помощью ключевого слова using и затем изменим
класс Program, как показано ниже.
// Это приложение будет загружать внешнюю сборку и создавать
// объект за счет применения позднего связывания.
public class Program
{
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Late Binding *****");
// Попытка загрузки локальной копии CarLibrary.
Assembly a = null;
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 561
try
{
а = Assembly.Load("CarLibrary");
}
catch(flleNotFoundException ex)
{
Console.WriteLine(ex.Message);
return;
}
if (a != null)
CreateUsingLateBinding(a);
Console.ReadLine();
}
static void CreateUsingLateBinding(Assembly asm)
{
try
{
// Получение метаданных типа Mini van.
Type miniVan = a.GetType("CarLibrary.MiniVan");
// Создание экземпляра Minivan на лету.
object obj = Activator.Createlnstance(miniVan);
Console.WriteLine("Created a {0} using late binding!", obj);
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
Прежде чем запускать данное приложение, необходимо вручную скопировать сборку
CarLibrary. dll в подкаталог bin\Debug внутри каталога этого нового приложения с
помощью проводника Windows. Дело в том, что здесь вызывается метод Assembly. Load (),
а это значит, что CLR-среда будет зондировать только папку клиента (при желании
можно было бы воспользоваться методом Assembly. LoadFrom () и указывать полный путь к
сборке, но в данном случае в этом нет никакой необходимости).
На заметку! Добавлять ссылку на CarLibrary. dll с помощью Visual Studio в данном примере
тоже не следует! Это приведет к добавлению записи о данной библиотеке в манифест клиента.
Вся суть применения позднего связывания состоит в том, чтобы попробовать создать объект, о
котором на момент компиляции ничего не известно.
Обратите внимание, что метод Activator. Createlnstance () возвращает экземпляр
объекта System.Object, а не строго типизированный объект MiniVan. Следовательно, в
случае применения к переменной obj операции точки никаких членов класса MiniVan
увидеть не получится. На первый взгляд может показаться, что эту проблему удастся
решить за счет применения операции явного приведения:
// Приведение для получения доступа к членам MiniVan?
// Нет! Компилятор выдаст ошибку!
object obj = (MiniVan)Activator.Createlnstance(minivan);
Из-за того, что в приложение не была добавлена ссылка на CarLibrary .dll,
применять ключевое слово using для импортирования пространства имен CarLibrary
нельзя, значит и нельзя использовать экземпляр MiniVan в операции приведения.
562 Часть IV. Программирование с использованием сборок .NET
Следует уяснить, что весь смысл применения позднего связывания состоит в том,
чтобы создавать экземпляры объектов, о которых на этапе компиляции ничего не
известно. Из-за этого возникает вопрос: как обеспечить вызов методов MiniVan на ссылке
System.Object? Ответ: конечно же с помощью рефлексии.
Вызов методов без параметров
Предположим, что требуется обеспечить вызов в отношении объекта MiniVan
метода TurboBoost () . Вспомните, что этот метод приводит двигатель в нерабочее
состояние и затем отображает окно с соответствующим сообщением. Первым шагом
является получение для метода TurboBoost () объекта Methodlnfo с
применением Type-. GetMethod () . После получения объекта Methodlnfo можно вызвать и сам
MiniVan . TurboBoost () , воспользовавшись Invoke () . Метод Methodlnfo . Invoke ()
ожидает всех параметров, которые подлежат передаче представляемому Methodlnfo
методу, в виде массива System.Object (поскольку эти параметры могут быть самыми
разнообразными сущностями).
Из-за того, что метод TurboBoost () не требует никаких параметров, методу
Methodlnfo. Invoke () может быть передано просто значение null (свидетельствующее
об отсутствии параметров у данного метода). Исходя из всего этого, модифицируем
метод CreateUsingLateBinding () следующим образом:
static void CreateUsingLateBinding(Assembly asm)
{
try
{
// Получение метаданных типа Mini van.
Type miniVan = asm.GetType ("CarLibrary.MiniVan");
// Создание экземпляра MiniVan на лету.
object ob] = Activator.Createlnstance(miniVan);
Console.WriteLine("Created a {0} using late binding!", obj);
// Получение информации для TurboBoost.
Methodlnfo mi = miniVan.GetMethod("TurboBoost");
// Вызов метода (null означает отсутствие параметров).
mi.Invoke (obj, null);
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Теперь после запуска приложения при вызове метода TurboBoost () появится окно
с сообщением, показанное на рис. 15.2.
our engine block exploded! \e£3td
Eek!
OK |
Рис. 15.2. Вызов метода с применением
позднего связывания
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 563
Вызов методов с параметрами
Когда позднее связывание используется для вызова метода, требующего каких-то
параметров, необходимо предоставить аргументы в виде слабо типизированного
массива object. Вспомните, что в библиотеке CarLibrary.dll версии 2.0.0.0 был определен
следующий метод в классе Саг:
public void TurnOnRadio(bool musicOn, MusicMedia mm)
{
if (musicOn)
MessageBox.Show(string.Format("Jamming {0}", mm));
else
MessageBox.Show("Quiet time...");
}
Этот метод принимает два параметра: булевское значение, которое указывает,
включена ли музыкальная система в автомобиле, и перечисление, отражающее тип
музыкального проигрывателя. Как было показано, это перечисление структурировано
следующим образом:
public enum MusicMedia
{
musicCd, // О
musicTape, // 1
musicRadio, // 2
musicMp3 // 3
}
Ниже приведен код нового метода в классе Program, в котором вызывается метод
TurnOnRadio (). Обратите внимание, что для перечисления MusicMedia используются
лежащие в основе числовые значения.
static void InvokeMethodWithArgsUsingLateBinding(Assembly asm)
{
try
{
// Получение описания метаданных типа SportsCar.
Type sport = asm.GetType("CarLibrary.SportsCar");
// Создание объекта типа SportsCar.
object obj = Activator.Createlnstance(sport);
// Вызов метода TurnOnRadio() с аргументами.
Methodlnfo mi = sport.GetMethod("TurnOnRadio");
mi.Invoke(obj, new object [] { true, 2 });
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
К этому моменту уже должно быть понятно, как в целом выглядят отношения между
рефлексией, динамической загрузкой и поздним связыванием. Разумеется, что помимо
описанной здесь в API-интерфейсе рефлексии предлагается множество другой
функциональности, однако основные сведения уже были предоставлены выше в этой главе.
Может по-прежнему интересовать вопрос: когда выгодно применять все эти приемы
в своих приложениях? Это должно проясниться в конце главы, а в следующем разделе
пока что пойдет речь о роли атрибутов в .NET.
Исходный код. Проект LateBindingApp доступен в подкаталоге Chapter 15.
564 Часть IV. Программирование с использованием сборок .NET
Роль атрибутов .NET
Как показывалось в начале настоящей главы, одной из задач компилятора .NET
является генерирование описаний метаданных для всех типов, которые были определены,
и на которые имеется ссылка в текущем проекте. Помимо стандартных метаданных,
которые помещаются в любую сборку, программисты в .NET могут включать в состав
сборки дополнительные метаданные с использованием атрибутов. В сущности,
атрибуты представляют собой не более чем просто аннотации, которые могут добавляться в
код и применяться к какому-то конкретному типу (классу, интерфейсу, структуре и т.д.),
члену (свойству, методу и т.д.), сборке или модулю.
Концепция аннотирования кода с применением атрибутов является далеко не
новой. Еще в COM IDL (Interface Definition Language — язык описания интерфейсов)
поставлялось множество предопределенных атрибутов, которые позволяли разработчикам
описывать типы, содержащиеся внутри того или иного СОМ-сервера. Однако в СОМ
атрибуты представляли собой немногим более чем просто набор ключевых слов. Когда
требовалось создать специальный атрибут, разработчик в СОМ мог делать это, но затем
он должен был ссылаться на этот атрибут в коде с помощью 128-битного числа (GUID-
идентификатора), что, как минимум, довольно затрудняло дело.
В .NET атрибуты представляют собой типы классов, которые расширяют
абстрактный базовый класс System. Attribute. В поставляемых в .NET пространствах имен
доступно множество предопределенных атрибутов, которые полезно применять в своих
приложениях. Более того, можно также создавать собственные атрибуты и тем самым
дополнительно уточнять поведение своих типов, создавая для атрибута новый тип,
унаследованный от Attribute.
В табл. 15.3 перечислены некоторые из предопределенных атрибутов, предлагаемые
в различных пространствах имен .NET.
Таблица 15.3. Некоторые из предопределенных атрибутов в .NET
Атрибут Описание
[CLSCompliant ] Заставляет элемент, к которому применяется, отвечать требованиям
CLS (Common Language Specification — общеязыковая спецификация).
Вспомните, что типы, которые отвечают требованиям CLS, могут без
проблем использоваться во всех языках программирования .NET
[Dlllmport] Позволяет коду .NET отправлять вызовы в любую неуправляемую
библиотеку кода на С или C++, в том числе и API-интерфейс лежащей в основе
операционной системы. Обратите внимание, что при взаимодействии с
программным обеспечением, работающим на базе СОМ, этот атрибут
не применяется
[Obsolete ] Позволяет указать, что данный тип или член является устаревшим. Когда
другие программисты попытаются использовать элемент с таким
атрибутом, они получат соответствующее предупреждение от компилятора
[Serializable] Позволяет указать, что класс или структура является "сериализируемой",
т.е. способна сохранять свое текущее состояние в потоке
[NonSerialized] Позволяет указать, что данное поле в классе или структуре не должно
сохраняться в процессе сериализации
[WebMethod] Позволяет указать, что метод может вызываться посредством HTTP-
запросов, и CLR-среда должна преобразовывать его возвращаемое
значение в формат XML
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 565
Важно уяснить, что при применении атрибутов в коде размещающиеся внутри них
метаданные, по сути, остаются бесполезными до тех пор, пока не запрашиваются
явным образом в каком-то другом компоненте программного обеспечения посредством
рефлексии. Если этого не происходит, они спокойно игнорируются и не причиняют
никакого вреда.
Потребители атрибутов
Как нетрудно догадаться, в составе NET Framework 4.0 SDK поставляется
множество утилит, которые позволяют производить поиск разнообразных атрибутов. Даже сам
компилятор С# (esc . exe) запрограммирован так, что он проверяет наличие разных
атрибутов во время компиляции. Например, сталкиваясь с атрибутом [CLSCompliant],
он автоматически проверяет соответствующий элемент и удостоверяется в том, что в
нем содержатся только отвечающие требованиям CLS инструкции, а при обнаружении
элемента с атрибутом [Obsolete] отображает внутри окна Error List (Список ошибок) в
Visual Studio 2010 соответствующее предупреждение.
Помимо утилит, предназначенных для использования во время разработки, многие
методы в библиотеках базовых классов .NET тоже изначально запрограммированы так,
чтобы распознавать определенные атрибуты посредством рефлексии. Например, чтобы
информация о состоянии объекта сохранялась в файле, все, что потребуется делать —
это просто добавить в класс или структуру в виде аннотации атрибут [Serializable].
Встретив этот атрибут, метод Serialize () из класса BinaryFormatter автоматически
сохраняет соответствующий объект в файле в компактном двоичном формате.
CLR-среда в .NET тоже выполняет проверки на предмет наличия определенных
атрибутов. Самым известным из них, пожалуй, является атрибут [WebMethod], который
применяется для создания веб-служб XML с помощью ASP.NET. Чтобы можно было
получать доступ к методу посредством HTTP-запросов, а его возвращаемое значение
автоматически преобразовывалось в формат XML, понадобится применить к этому методу
атрибут [WebMethod], а обо всех остальных деталях будет заботиться CLR-среда. Помимо
разработки веб-служб, атрибуты также играют важную роль в функционировании
системы безопасности NET, в Windows Communication Foundation, в обеспечении
функциональной совместимости между СОМ и .NET, и во многом другом.
И, наконец, допускается разрабатывать приложения, способные распознавать
специальные атрибуты, а также любые из тех, что поставляются в составе библиотек
базовых классов .NET. Это позволяет, по сути, создавать набор "ключевых слов", понятных
только определенному множеству сборок.
Применение предопределенных атрибутов в С#
Чтобы посмотреть, как применение атрибутов в С# выглядит на практике, создадим
новое консольное приложение по имени ApplyingAttributes. Предположим, что
требуется создать класс по имени Motorcycle (мотоцикл), который бы мог сохраняться в
двоичном формате. Для этого достаточно применить к определению этого класса
атрибут [Serializable]. Если в классе есть какое-то поле, которое не должно сохраняться,
к нему должен быть применен атрибут [NonSerialized].
// Этот класс может сохраняться на диске.
[Serializable]
public class Motorcycle
{
// Это поле сохраняться не будет.
[NonSerialized]
float weightOfCurrentPassengers;
566 Часть IV. Программирование с использованием сборок .NET
// Эти поля будут сериалиэованы.
bool hasRadioSystem;
bool hasHeadSet;
bool hasSissyBar;
}
На заметку! Действие любого атрибута распространяется только на элемент, который следует
сразу же после него. Например, в классе Motorcycle сериализации не будет подвергнуто
только поле weightOfCurrentPassengers. Все остальные поля будут обязательно сериа-
лизоваться, потому что класс снабжен атрибутом [Serializable].
На данный момент вдаваться в сам процесс сериализации объектов не стоит
(соответствующие детали подробно рассматриваются в главе 20). Главное обратить
внимание на то, что для применения атрибута его имя должно быть заключено в квадратные
скобки.
После компиляции этого класса можно просмотреть дополнительные метаданные с
помощью утилиты ildasm. exe. При этом следует обратить внимание на то, что эти
атрибуты будут сопровождаться маркером serializable (в строке с треугольником
красного цвета внутри класса Motorcycle) и маркером notserialized(B строке,
представляющей поле weightOfCurrentPassengers), как показано на рис. 15.3.
orm 5th ed",First Draft'.
File View Help
v E:\My Books\C# Book\C# and the .NET Platform 5th ed\First Draft\Chapter_15\Code\Applyi
!•••••► MANIFEST
В Щ ApplyingAttributes
й WE ApplyingAttributes. Motorcycle
► .class public auto ansi serializable beforefieldinit
v HasHeadSet: private bool
v hasRadioSystem : private bool
v hasSissyBar: private bool
.ctor: void()
■ ApplyingAttributes. Program
assembly ApplyingAttributes
Рис. 15.3. Просмотр атрибутов в ildasm.exe
Как не трудно догадаться, к каждому элементу может применяться сразу несколько
атрибутов. Например, предположим, что имеется некий унаследованный тип класса на
С# (по имени HorseAndBuggy), который был помечен как сериализируемый, но теперь
считается устаревшим для текущей разработки. В этом случае, вместо того чтобы
удалять определение этого класса из кодовой базы (и подвергаться риску нарушения
работы существующего программного обеспечения), можно просто дополнительно пометить
этот класс атрибутом [Obsolete ]. Для применения к одному элементу нескольких
атрибутов они должны быть представлены в виде разделенного запятыми списка:
[Serializable, Obsolete("Use another vehicle!")]
public class HorseAndBuggy
//
}
В качестве альтернативны применять несколько атрибутов к одному элементу
можно и так, как показано ниже (конечный результат будет тот же):
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 567
[Serializable]
[Obsolete("Use another vehicle!")]
public class HorseAndBuggy
{
// ...
}
Сокращенное обозначение атрибутов в С#
Если вы заглядывали в документацию .NET Framework 4.0 SDK, то наверняка
заметили, что на самом деле имя класса, представляющего атрибут [Obsolete],
выглядит не как Obsolete, а как ObsoleteAttribute. Дело в том, что по соглашению имена
всех атрибутов в .NET (в том числе и специальных, создаваемых разработчиком)
сопровождаются суффиксом Attribute. Для упрощения процесса применения атрибутов
в языке С# вводить этот суффикс не обязательно. То есть приведенная ниже версия
HorseAndBuggy будет работать точно так же, как предыдущая (но требовать немного
больше клавиатурного ввода):
[SerializableAttribute]
[ObsoleteAttribute ("Use another vehicle!")]
public class HorseAndBuggy
{
// ...
}
Имейте в виду, что подобный сокращенный вариант написания атрибутов
допускается только в языке С#, а во всех остальных совместимых с .NET языках он может не
поддерживаться.
Указание параметров конструктора для атрибутов
Обратите внимание, что атрибут [Obsolete] может принимать нечто похожее на
параметр конструктора. Если взглянуть на формальное определение этого атрибута в
окне кода, обнаружится, что этот класс действительно предоставляет конструктор,
получающий System.String:
public sealed class ObsoleteAttribute : Attribute
{
public ObsoleteAttribute(string message, bool error);
public ObsoleteAttribute(string message);
public ObsoleteAttribute ();
public bool IsError { get; )
public string Message { get; }
}
Важно понять, что при предоставлении атрибуту параметров конструктора, этот
атрибут не размещается в памяти до тех пор, пока в отношении данных параметров не
будет произведена рефлексия каким-то другим типом или внешней утилитой. Вместо
этого строковые данные, определяемые на уровне атрибутов, сохраняются внутри
сборки в виде блока метаданных.
Атрибут [Obsolete] в действии
Теперь, когда класс HorseAndBuggy помечен как устаревший, при попытке
разместить его экземпляр:
static void Main(string[] args)
{
HorseAndBuggy mule = new HorseAndBuggy();
}
568 Часть IV. Программирование с использованием сборок .NET
можно будет увидеть, что поставляемые внутри него строковые данные извлекаются и
отображаются в Visual Studio 2010 внутри окна Error List, а при наведении курсора мыши
на этот устаревший тип появляется причиняющая проблему строка кода (рис. 15.4).
Program.cs X
JtAppryingAttributes-Program
" I -УМл'гЧ^ппдИ args)
class Program
{
static void Main(string[] args)
{
} ■ApplyineAttributes.HorseAndBuggy' is obsolete: 'Use another vehicle J'
}
100 %~^"| •>
Рис. 15.4. Атрибуты в действии
В данном случае в роли "другого компонента программного обеспечения кода",
воспроизводящего атрибут [Obsolete] посредством рефлексии, выступает компилятор С#.
К настоящему моменту должны быть ясно следующие важные моменты, касающиеся
атрибутов .NET.
• Атрибуты в .NET представляют собой классы, унаследованные от класса System.
Attribute.
• Применение атрибутов приводит к включению в сборку дополнительных
метаданных.
• Атрибуты, по сути, остаются бесполезными до тех пор, пока какой-то другой агент
не воспроизведет их через рефлексию.
• В С# атрибуты указываются в квадратных скобках.
Теперь посмотрим, каким образом создавать собственные атрибуты и специальный
код, воспроизводящий добавляемые ими метаданные посредством рефлексии.
Исходный код. Проект ApplyingAttributes доступен в подкаталоге Chapter 15.
Создание специальных атрибутов
Первым шагом в процессе создания специального атрибута является создание
нового класса, унаследованного от System.Attribute. Чтобы не отклоняться от
автомобильной темы, используемой повсюду в этой книге, давайте создадим новый проект
типа Class Library (Библиотека классов) по имени AttnbutedCarLibrary, в котором
определим несколько транспортных средств с помощью специального атрибута по
имени VehicleDescriptionAttribute:
// Специальный атрибут.
public sealed class VehicleDescriptionAttribute : System.Attribute
{
public string Description { get; set; }
public VehicleDescriptionAttribute (string vehicalDescnption.)
{
Description = vehicalDescnption;
public VehicleDescriptionAttribute()
Глава15.Рефлексиятипов,позднеесвязываниеипрограммированиесиспользованиематрибутов 569
Как здесь видно, в VehicleDescriptionAttribute поддерживается фрагмент
строковых данных, предусматривающий возможность манипуляций над ним с помощью
автоматического свойства (Description). Помимо того факта, что данный класс
наследуется от System.Attribute, ничего особенного в нем больше нет.
На заметку! Ради безопасности в .NET наилучшим приемом считается запечатывание всех
специальных атрибутов. На самом деле в Visual Studio 2010 даже поставляется специальный
фрагмент кода под названием Attribute, который позволяет быстро генерировать в окне кода
новый производный от Attribute класс. Об использовании фрагментов кода подробно
рассказывалось в главе 2.
Применение специальных атрибутов
Из-за того, что класс VehicleDescriptionAttribute унаследован от System.
Attribute, теперь можно снабжать транспортные средства любыми подходящими
аннотациями. Для целей тестирования добавим в новую библиотеку классов следующие
определения классов:
// Присваивание описания с помощью "именованного свойства".
[Serializable]
[VehicleDescription(Description = "My rocking Harley")] // Мой классный Харли
public class Motorcycle
{
}
[SerializableAttribute]
[ObsoleteAttnbute ("Use another vehicle!")]
// Используйте другое транспортное средство1
[VehicleDescription("The old gray mare, she ain't what she used to be...")]
// Старая серая кобыла, она уже не та, что была раньше. . .
public class HorseAndBuggy
{
}
[VehicleDescription (A very long, slow, but feature-rich auto")]
// Очень длинное, медленное, но богатое возможностями авто
public class Winnebago
{
}
Синтаксис именованных свойств
Обратите внимание, что для присваивания описания классу Motorcycle
применяется новый фрагмент связанного с атрибутами синтаксиса, называемый иженован-
ным свойством. В конструкторе первого атрибута [VehicleDescription]
установка лежащих в основе строковых данных производится за счет применения свойства
Description. В случае применения рефлексии к данному атрибуту каким-то внешним
агентом, свойству Description будет передаваться соответствующее значение
(синтаксис именованного свойства разрешено использовать только в случае предоставления в
атрибуте доступного для записи свойства .NET).
В отличие от Motorcycle, в типах HorseAndBuggy и Winnebago синтаксис
именованного свойства не применяется, и вместо этого передача строковых данных
осуществляется через специальный конструктор. В любом случае после компиляции сборки
AttnbutedCarLibrary можно воспользоваться утилитой ildasm.exe и посмотреть,
какие описания метаданных будут вставлены для каждого из этих типов. Например, на
рис. 15.5 показано, как в ildasm.exe будет выглядеть описание класса Winnebago, в
частности — данные внутри элемента beforefieldinit.
570 Часть IV. Программирование с использованием сборок .NET
// Attribute -'Car! 'Orsry, Ainnebago;..class pubfic auto ansi beforefieidinit
Find
ng) -
L
( 01 00 28 41 20 76 65 72 79 20 6C 6F 6E 67 2C 20 // ..(ft uery long,
73 6C 6F 77 2C 20 62 75 74 20 66 65 61 74 75 72 // slow, but featur
65 2D 72 69 63 60 20 61 75 74 6F 00 00 ) // e rich auto..
ЫшшшьшшшяфтШттшт*шшшшшй > _l
Рис. 15.5. Вставленные данные описания транспортного средства
Ограничение использования атрибутов
По умолчанию действие специальных атрибутов может распространяться на
практически любой аспект кода (методы, классы, свойства и т.д.). Следовательно, если бы
потребовалось, можно было бы использовать VehicleDescription (помимо прочего) и
для уточнения описания методов, свойств или полей:
[VehicleDescription("A very long, slow, but feature-rich auto")]
// Очень длинное, медленное, но авто с полным фаршем
public class Winnebago
{
[VehicleDescription("My rocking CD player")]
// Мой классный проигрыватель компакт-дисков
public void PlayMusic(bool On)
}
}
В одних случаях подобное поведение оказывается именно тем, что нужно, но в
других, однако, может потребоваться создать вместо этого специальный атрибут,
действие которого бы распространялось только на избранные элементы кода. Чтобы
ограничить область действия специального атрибута, необходимо применить к его
определению атрибут [AttributeUsage]. Атрибут [AttributeUsage] позволяет
предоставлять (посредством операции OR) любую комбинацию значений из перечисления
AttributeTargets:
//В этом перечислении описаны целевые объекты,
//на которые распространяется действие атрибута.
public enum AttributeTargets
{
All, Assembly, Class, Constructor,
Delegate, Enum, Event, Field, GenericParameter,
Interface, Method, Module, Parameter,
Property, ReturnValue, Struct
}
Более того, атрибут [AttributeUsage] также позволяет дополнительно
устанавливать свойство AllowMultiple, которое указывает, может ли атрибут применяться к
одному и тому же элементу более одного раза (значением по умолчанию этого
свойства является false). Помимо этого он также позволяет указывать, должен ли атрибут
наследоваться производными классами, за счет применения именованного свойства
Inherited (значением по умолчанию этого свойства является true).
Например, чтобы указать, что атрибут [VehicleDescription] может
применяться к классу или структуре только один раз, необходимо обновить определение
VehicleDescriptionAttribute следующим образом:
Глава 15. Рефлексия типов, позднеесвязываниеипрограммированиесиспользованиематрибутов 571
//На этот раз мы используем атрибут AttributeUsage
// для аннотирования специального атрибута.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct,
Inherited = false)]
public sealed class VehicleDescriptionAttribute : System.Attribute
{
}
Если разработчик попытается применить атрибут [VehicleDescription] не к
классу или структуре, на этапе компиляции возникнет соответствующая ошибка.
Атрибуты уровня сборки и модуля
Атрибуты можно также применять ко всем типам внутри конкретного модуля (если
речь идет о многофайловой сборке, которые были описаны в главе 14) или ко всем
модулям внутри отдельной сборки, используя, соответственно, дескрипторы [module: ] и
[assembly: ]. Например, предположим, что необходимо позаботиться о соответствии
требованиям CLS всех общедоступных членов во всех общедоступных типах,
определенных внутри сборки.
На заметку! В главе 1 рассказывалось о роли сборок, отвечающих требованиями CLS. Вспомните,
что отвечающая требованиям CLS сборка может использоваться во всех языках
программирования .NET. Если в общедоступных типах создаются общедоступные члены, не
соответствующие требованиям, которые указаны в CLS для конструкций программирования (например,
они принимают параметры без знака или указатели), их использование в других языках .NET
может оказаться невозможным. По этой причине при создании библиотек кода на С#, которые
должны быть пригодны для применения во множестве языков .NET, проверка на предмет
соответствия требования CLS является обязательным условием.
Для этого необходимо добавить в самом начале файла исходного кода на С#
показанный ниже атрибут уровня сборки. Имейте в виду, что все атрибуты уровня сборки
или модуля должны быть перечислены за пределами области действия любого из
пространств имен! При добавлении в проект атрибутов уровня сборки (или модуля)
рекомендуется следовать такой схеме в файле кода:
// Сначала должны перечисляться операторы using.
using System;
using System. Collections .Generic-
using System.Linq;
using System.Text;
// Далее необходимо перечислить атрибуты уровня сборки или модуля.
// Следует позаботиться, чтобы все общедоступные типы в данной
// сборке обязательно отвечали требованиям CLS.
[assembly: CLSCompliant(true)]
// Теперь можно перечислять собственные пространства имен и типы.
namespace AttributedCarLibrary
{
// Типы...
}
Если теперь добавить сюда фрагмент кода, который не отвечает требованиям CLS
(например, явный элемент данных без знака):
// Типы ulong не отвечают требованиям CLS.
public class Winnebago
572 Часть IV. Программирование с использованием сборок .NET
public ulong notCompliant;
}
компилятор выдаст соответствующее предупреждение.
Solution Explorer
а*
!ЗЭ Solution 'AttributedCarLibrary' (l project)
л .J9 AttributedCarLibrary
4» t# Properties
fj Assemblylnfo.cs
j^ References
<jfl Classl.cs
-"^ Solution Explorer I
Файл Assembly Info. cs,
генерируемый Visual Studio 2010
По умолчанию в Visual Studio 2010 во все
проекты автоматически добавляется файл по имени
Assembly In f о . cs, который можно просмотреть,
раскрыв в окне Solution Explorer узел Properties
(Свойства), как показано на рис. 15.6.
Этот файл представляет собой очень удобное
место для размещения атрибутов, которые
должны применяться на уровне сборки. В главе 14 во
время обсуждения сборок .NET уже рассказывалось о том, что в манифесте содержатся
метаданные уровня сборки. Большая часть из них берется из атрибутов уровня сборки,
которые описаны в табл. 15.4.
Таблица 15.4. Некоторые атрибуты уровня сборки
Рис. 15.6. Файл Assemblylnf о. cs
Атрибут
Описание
[AssemblyCompany]
[AssemblyCopyright]
[AsseniblyCulture]
[AssemblyDescription]
[AssemblyKeyFile]
[AssemblyProduct]
[AssemblyTrademark]
[AssemblyVersion]
Хранит общую информацию о компании
Хранит любую информацию, которая касается авторских прав на
данный продукт или сборку
Предоставляет информацию о том, какие культуры или языки
поддерживает сборка
Хранит удобное для восприятия описание продукта или модулей,
из которых состоит сборка
Указывает имя файла, в котором содержится пара ключей,
используемых для подписания сборки (т.е. создания строгого имени)
Предоставляет информацию о продукте
Предоставляет информацию о торговой марке
Указывает информацию о версии сборки в формате <старший_
номер.младший_номер.номер_сборки.номер_редакции>
Исходный код. Проект AttributedCarLibrary доступен в подкаталоге Chapter 15.
Рефлексия атрибутов с использованием
раннего связывания
Вспомните, что атрибуты остаются бесполезными до тех пор, пока к их значениям
не применяется рефлексия в каком-то другом компоненте программного обеспечения.
После обнаружения атрибута в этом компоненте может предприниматься любое
необходимое действие. Как и в любом приложении, обнаружение специального атрибута
может быть реализовано с использованием либо раннего, либо позднего связывания.
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 573
Для применения раннего связывания определение интересующего атрибута (в
данном случае VehicleDescriptionAttribute) должно присутствовать в клиентском
приложении уже на этапе компиляции. С учетом того, что специальный атрибут определен
в сборке At tribute dCarLibrary как общедоступный класс, использование раннего
связывания будет наилучшим вариантом.
Чтобы проиллюстрировать на практике рефлексию специальных атрибутов, создадим
новый проект типа Console Application по имени VehicleDescriptionAttributeReader,
добавим в него ссылку на сборку AttributedCarLibrary и поместим в исходный файл
*. cs следующий код:
// Рефлексия атрибутов с использованием раннего связывания.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AttributedCarLibrary;
namespace VehicleDescriptionAttributeReader
{
class Program
{
static void Main(string[ ] args)
{
Console.WriteLine("***** Value of VehicleDescriptionAttribute *****\n");
ReflectOnAttributesUsingEarlyBinding();
Console.ReadLine();
}
private static void ReflectOnAttributesUsingEarlyBinding()
{
// Получение типа, представляющего Winnebago.
Type t = typeof (Winnebago) ;
// Получение всех атрибутов Winnebago.
object[] customAtts = t.GetCustomAttributes(false);
// Вывод описания.
foreach (VehicleDescriptionAttribute v in customAtts)
Console.WriteLine("-> {0}\n", v.Description);
}
}
}
Метод Type.GetCustomAttributes () возвращает массив объектов со всеми
атрибутами, которые применяются к члену, представленному экземпляром Туре
(булевский параметр указывает, должен ли поиск продолжаться вверх по цепочке
наследования). После получения списка атрибутов осуществляется проход по всем классам
VehicleDescriptionAttribute с отображением значения свойства Description.
Исходный код. Проект VehicleDescriptionAttributeReader доступен в подкаталоге
Chapter 15.
Рефлексия атрибутов с использованием
позднего связывания
В предыдущем примере для вывода описания транспортного средства типа
Winnebago применялось ранее связывание. Это было возможно благодаря тому, что тип
класса VehicleDescriptionAttribute определен в сборке AttributedCarLibrary как
574 Часть IV. Программирование с использованием сборок .NET
общедоступный член. Для рефлексии атрибутов также допускается применение
динамической загрузки и позднего связывания.
Рассмотрим пример использования этого подхода, создав новый проект по
имени VehicleDescriptionAttributeReaderLateBinding, скопировав сборку
AttributedCarLibrary.dll в его каталог bin\Debug и модифицировав класс Program
следующим образом:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
namespace VehicleDescriptionAttributeReaderLateBinding
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** Value of VehicleDescriptionAttribute *****\n");
Ref lectAttributesUsingLateBindmg () ;
Console.ReadLine();
}
private static void Ref lectAttributesUsingLateBindmg ()
{
try
{
// Загрузка локальной копии сборки AttributedCarLibrary.
Assembly asm = Assembly.Load("AttributedCarLibrary");
// Получение информации о типе VehicleDescriptionAttribute.
Type vehicleDesc =
asm.GetType("AttributedCarLibrary.VehicleDescriptionAttribute" ) ;
// Получение информации о типе Description.
Propertylnfo propDesc =
vehicleDesc.GetProperty("Description");
// Получение всех типов из сборки.
Туре[] types = asm.GetTypes();
// Проход по типам с получением атрибутов VehicleDescriptionAttribute.
foreach (Type t in types)
{
object [] objs = t.GetCustomAttributes(vehicleDesc, false);
// Проход по атрибутам VehicleDescriptionAttribute и вывод
// описаний с использованием позднего связывания.
foreach (object о in objs)
{
Console.WriteLine("-> {0}: {l}\n",
t.Name, propDesc.GetValue(o, null));
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 575
Тем, кто внимательно прорабатывал приводимые ранее примеры, данный код
должен быть более-менее понятен. Единственным интересным моментом здесь является
использование метода Propertylnfo . GetValue (), который служит для активизации
средства доступа к свойству. Ниже показано, как будет выглядеть вывод в случае
выполнения текущего примера:
***** Value of VehicleDescriptionAttribute *****
-> Motorcycle: My rocking Harley
-> HorseAndBuggy: The old gray mare, she ain't what she used to be...
-> Winnebago: A very long, slow, but feature-rich auto
Исходный код. Проект VehicleDescriptionAttributeReaderLateBinding доступен в
подкаталоге Chapter 15.
Возможное применение на практике рефлексии,
позднего связывания и специальных атрибутов
Хотя в главе и приводилось множество примеров применения данных методик, все
равно может остаться вопрос, когда следует применять рефлексию, динамическое
связывание и специальные атрибуты в своих приложениях. Эта тема может показаться
более теоретической, нежели практической. Чтобы оценить возможность их
применений в реальном мире, необходим какой-то более солидный пример. Поэтому давайте
предположим, что была поставлена задача разработать так называемое расширяемое
приложение, к которому можно было бы подключать сторонние инструменты.
Что именно подразумевается под расширяемым приложением? Рассмотрим IDE-
среду Visual Studio 2010. При разработке в этом приложении были предусмотрены
специальные "ловушки" (hook) для предоставления другим производителям ПО возможности
подключать свои специальные модули. Понятно, что разработчики Visual Studio 2010 не
могли добавить ссылки на несуществующие внешние сборки .NET (т.е. воспользоваться
ранним связыванием), тогда как же им удалось обеспечить в приложении необходимые
методы-ловушки? Ниже описан один из возможных способов решения этой проблемы.
• Во-первых, любое расширяемое приложение должно обязательно обладать каким-
нибудь механизмом для ввода, который даст возможность пользователю указать
подключаемый модуль (например, диалоговым окном или соответствующим
флагом командной строки). Это требует применения динамической загрузки.
• Во-вторых, любое расширяемое приложение должно обязательно быть способным
определять, поддерживает ли модуль функциональные возможности (например,
набор интерфейсов), необходимые для его подключения к среде. Это требует
применения рефлексии.
• В-третьих, любое расширяемое приложение должно обязательно получать ссылку
на требуемую инфраструктуру (например, набор типов интерфейсов) и вызывать
ее члены для приведения в действие лежащих в ее основе функций. Это может
требовать применения позднего связывания.
Если расширяемое приложение изначально программируется так, чтобы
запрашивать определенные интерфейсы, оно получает возможность определять во время
выполнения, может ли активизироваться интересующий тип, и после успешного
прохождения типом такой проверки позволять ему поддерживать дополнительные интерфейсы и
получать доступ к их функциональным возможностям полиморфным образом. Именно
такой подход и предприняли разработчики Visual Studio 2010, причем ничего особо
сложного в нем нет.
576 Часть IV. Программирование с использованием сборок .NET
Создание расширяемого приложения
В следующих подразделах будет рассмотрен завершенный пример создания
расширяемого приложения Windows Forms, которое позволяет наращивать функциональные
возможности за счет использования внешних сборок. Детали построения самих
приложений Windows Forms здесь рассматриваться не будут (обзор API-интерфейса Windows
Forms можно найти в приложении А).
На заметку! API-интерфейс Windows Forms предлагался для разработки настольных приложений
в .NET первоначально. После выхода версии .NET 3.0, однако, предпочитаемой графической
платформой для разработки таких приложений стал API-интерфейс Windows Presentation
Foundation (WPF). Несмотря на это, API-интepфeйcWlndows Forms применяется при разработке
пользовательских интерфейсов в ряде рассмотренных в книге примеров клиентских
приложений, поскольку его код более понятен, чем аналогичный код WPF
Те, кто не знаком с процессом построения приложений Windows Forms, могут просто
открыть прилагаемый демонстрационный код и изучать его по ходу прочтения
дальнейшего текста.
Для удобства ниже перечислены все сборки, которые потребуется создать для
разрабатываемого расширяемого приложения.
• CommonSnappableTypes.dll. Сборка с определениями типов, используемых
каждой интегрируемой оснасткой, на которую напрямую ссылается приложение
Windows Forms.
• CSharpSnapIn.dll. Интегрируемая оснастка на С#, которая будет использовать
типы из сборки CommonSnappableTypes . dll.
• Vb Snap In. dll. Интегрируемая оснастка на Visual Basic, которая будет
использовать типы из сборки CommonSnappableTypes . dll.
• MyExtendableApp. ехе. Приложение Windows Forms, функциональные
возможности которого должны расширяться каждой интегрируемой оснасткой.
Конечно же, в этом приложении будут использоваться динамическая загрузка,
рефлексия и позднее связывание, чтобы обеспечить возможность динамического
получения функциональности сборок, о которых приложению заранее ничего не известно.
Создание сборки CommonSnappableTypes. dll
В первую очередь необходимо создать сборку с типами, которые должна
обязательно использовать каждая оснастка, чтобы иметь возможность подключаться к
расширяемому приложению Windows Forms. Для этого создадим проект типа Class Library
(Библиотека классов), назовем его CommonSnappableTypes и определим в нем два
следующих типа:
namespace CommonSnappableTypes
{
public interface IAppFunctionality
{
void Dolt () ;
}
[AttributeUsage(AttributeTargets.Class) ]
public sealed class CompanylnfoAttribute : System.Attribute
{
public string CompanyName { get; set; }
public string CompanyUrl { get; set; }
}
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 577
Интерфейс IAppFunctionality предоставляет полиморфный интерфейс для всех
оснасток, которые могут подключаться к расширяемому приложению Windows Forms.
Поскольку рассматриваемый пример является демонстрационным, в этом интерфейсе
определяется единственный метод по имени Dolt (). В реальном проекте интерфейс
мог бы генерировать код сценария, визуализировать изображение в окне инструментов
приложения, интегрироваться в главное меню соответствующего приложения и т.п.
Тип CompanyInfoAttribute — это специальный атрибут, который может
применяться к любому классу, желающему подключиться к контейнеру. Как не трудно
догадаться по определению класса, [Companylnfo] позволяет разработчику оснастки
предоставлять общие сведения о месте происхождения компонента.
Создание оснастки на С#
Далее потребуется создать тип, реализующий интерфейс IAppFunctionality. Чтобы
не усложнять пример создания расширяемого приложения, давайте сделаем этот тип
простым. Создадим новый проект типа Class Library на С# по имени CSharpSnapln и
определим в нем тип класса по имени CSharpModule. При этом нужно не забыть
добавить ссылку на сборку CommonSnappableTypes, чтобы этот класс мог использовать
определенные в ней типы (а также на сборку System. Windows . Forms . dll для того, чтобы
он мог отображать осмысленное сообщение). Ниже показан необходимый код.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CommonSnappableTypes;
using System.Windows.Forms;
namespace CSharpSnapln
{
[Companylnfo(CompanyName = "My Company", CompanyUrl = "www.MyCompany.com")]
public class CSharpModule : IAppFunctionality
{
void IAppFunctionality.Dolt()
{
MessageBox. Show ("You have just used the C# snap in!");
}
}
}
Обратите внимание, что для обеспечения поддержки интерфейса IAppFunctionality
был выбран вариант его реализации явным образом. Применять именно такой вариант
вовсе не необязательно; главное понимать, что единственной частью системы, которая
нуждается в непосредственном взаимодействии с данным типом интерфейса, является
обслуживающее приложение Windows. Благодаря явной реализации этого интерфейса,
метод Dolt () не становится доступным прямо из типа CSharpModule.
Создание оснастки на Visual Basic
Чтобы сымитировать стороннего производителя, предпочитающего использовать не
С#, a Visual Basic, создадим новый проект типа Class Library (Библиотека классов) на
Visual Basic по имени Vb Snap In и добавим в него ссылки на те же самые внешние
сборки, что и в предыдущем проекте CSharpSnapln.
На заметку! По умолчанию папка References (Ссылки) для проекта на Visual Basic в окне Solution
Explorer отображаться не будет, поэтому для добавления ссылок в этот проект необходимо
пользоваться пунктом Add Reference (Добавить ссылку) в меню Project (Проект).
578 Часть IV. Программирование с использованием сборок .NET
Необходимый код выглядит просто:
Imports System.Windows.Forms
Imports CommonSnappableTypes
<CompanyInfo (CompanyName:=Chucky's Software, CompanyUrl:=www.ChuckySoft.com)>
Public Class VbSnapIn
Implements IAppFunctionality
Public Sub DoIt() Implements CommonSnappableTypes.IAppFunctionality.Dolt
MessageBox. Show (You have just used the VB snap in!)
End Sub
End Class
Обратите внимание, что в синтаксисе Visual Basic для применения атрибутов
используются не квадратные ([ ]), а угловые (о) скобки; кроме того, для реализации типов
интерфейсов в любом конкретном классе или структуре применяется ключевое слово
Implements.
Создание расширяемого приложения Windows Forms
И, наконец, последний шаг заключается в создании самого расширяемого
приложения Windows Forms (MyExtendableApp), которое позволит пользователю выбирать
желаемую оснастку с помощью стандартного диалогового окна открытия файлов Windows.
Если раньше не приходилось создавать приложение Windows Forms, знайте, что для
начала необходимо выбрать в диалоговом окне New Project (Новый проект) в Visual Studio
2010 проект типа Windows Forms Application (Приложение Windows Forms), как показано
на рис. 15.7.
New Project
и
• , Sort by. Default
.NET Framework 4
aCJfl Windows Forms Application Visual C*
; ^f I WPF Application Visual C*
jH Console Application Visual C*
5] Class Library Visual C*
Pcjij WPF Browser Application Visual C#
l*cjj] Empty Project Vrsual C*
GO!
I
A project for creating an application with a
Windows Forms user interface
Рис. 15.7. Создание нового проекта Windows Forms в Visual Studio 2010
Теперь нужно добавить в него ссылку на сборку CommonSnappableTypes . dll, но йена
библиотеки кода CSharp Snap In. dll и VbSnapIn. dll. Кроме того, необходимо
импортировать в главный файл кода формы (для его открытия щелкните правой кнопкой мыши
в визуальном конструкторе формы и выберите в контекстном меню пункт View Code
(Просмотреть код)) пространства имен System.Reflection и CommonSnappableTypes.
Вспомните, что цель создания данного приложения состоит в том, чтобы увидеть, как
использовать позднее связывание и рефлексию для проверки отдельных двоичных
файлов, создаваемых другими производителям, на предмет их способности выступать в
роли подключаемых оснасток.
Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов 579
Вдаваться в детали разработки приложения Windows Forms пока не будем (это
описано в приложении А), а просто разместим в окне конструктора форм компонент
MenuStrip и определим для него одно главное меню File (Файл) с единственным
подменю Snap In Module (Подключить модуль). Добавим в главное окно элемент ListBox
(переименовав его в IstLoadedSnapIns), чтобы отображать в нем имена загружаемых
пользователем оснасток. На рис. 15.8 показано, как должен выглядеть в конечном итоге
графический пользовательский интерфейс разрабатываемого приложения.
Рис. 15.8. Графический пользовательский интерфейс приложения MyExtendableApp
Код обработки события Click, генерируемого при выборе в меню File пункта Snap
In Module (который создается двойным щелчком на этом пункте меню в визуальном
конструкторе форм), должен отображать диалоговое окно File Open (Открытие
файла) и извлекать путь к выбранному файлу. Предполагая, что пользователь не выбрал
сборку CommonSnappableTypes.dll (поскольку она обеспечивает просто
инфраструктуру), этот путь затем должен пересылаться на обработку вспомогательной функции
LoadExternalModule (), реализация которой показана ниже. Эта функция будет
возвращать false, если не удается найти класс, реализующий IAppFunctionality.
private void snapInModuleToolStripMenuItem_Click(object sender, EventArgs e)
{
// Позволить пользователю выбрать подлежащую загрузке сборку.
OpenFileDialog dig = new OpenFileDialog();
if (dlg.ShowDialogO == DialogResult.OK)
{
if (dig.FileName.Contains("CommonSnappableTypes"))
// CommonSnappableTypes не содержит оснасток.
MessageBox.Show(ConunonSnappableTypes has no snap-ins!);
else if(!LoadExternalModule(dig.FileName))
// He удается обнаружить класс, реализующий IAppFunctionality.
MessageBox.Show(Nothing implements IAppFunctionality!);
}
}
Метод LoadExternalModule () должен решать следующие задачи:
• динамически загружать выбираемую сборку в память;
• определять, содержатся ли в этой сборке типы, реализующие IAppFunctionality;
• создавать соответствующий тип с использованием позднего связывания.
Если тип, реализующий IAppFunctionality, найдет, вызывается метод Dolt () и
полностью уточненное имя этого типа добавляется в ListBox (обратите внимание, что
580 Часть IV. Программирование с использованием сборок .NET
в цикле f oreach осуществляется проход по всем типам, имеющимся в сборке, на случай
наличия в одной сборке нескольких оснасток).
private bool LoadExternalModule(string path)
{
bool foundSnapIn = false;
Assembly theSnapInAsm = null;
try
{
II Динамическая загрузка выбранной сборки.
theSnapInAsm = Assembly.LoadFrom(path);
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
return foundSnapIn;
}
// Получение информации обо всех совместимых
// с IAppFunctionality классах в сборке.
var theClassTypes = from t in theSnapInAsm.GetTypes()
where t.IsClass &&
(t.Getlnterface("IAppFunctionality") ' = null)
select t;
// Создание объекта и вызов метода DoIt().
foreach (Type t in theClassTypes)
{
foundSnapIn = true;
// Использование позднего связывания для создания типа.
IAppFunctionality itfApp =
(IAppFunctionality)theSnapInAsm.Createlnstance (t.FullName, true);
ltfApp.DoIt();
IstLoadedSnapIns.Items.Add(t.FullName);
}
return foundSnapIn;
}
Теперь можно попробовать запустить приложение. В случае выбора сборки
CSharpSnapIn.dll или VbSnapIn.dll должно появляться соответствующее сообщение.
И, наконец, осталось лишь обеспечить отображение метаданных, предоставляемых
атрибутом [Companylnfo]. Для этого модифицируем метод LoadExternalModule () так,
чтобы перед выходом из контекста foreach в нем вызывалась еще одна вспомогательная
функция по имени DisplayCompanyData (), принимающая параметр System.Type:
private bool LoadExternalModule (string path)
foreach (Type t in theClassTypes)
{
// Отображение информации о компании.
DisplayCompanyData (t);
}
return foundSnapIn;
}
Теперь, имя переданный во входном параметре тип, применим в отношении
атрибута [Companylnfo] рефлексию:
Глава 15. Рефлексиятипов.позднеесвязываниеипрограммированиесиспользованиематрибутов 581
private void DisplayCompanyData (Type t)
{
// Получение данных из атрибута [CompanyInfo].
var complnfo = from ci in t .GetCustomAttributes (false) where
(ci.GetType () == typeof(CompanylnfoAttribute))
select ci;
// Отображение этих данных.
foreach (CompanylnfoAttribute с in complnfo)
{
MessageBox.Show (c.CompanyUrl,
string.Format("More info about {0} can be found at",
с.CompanyName));
}
}
На рис. 15.9 показан один из возможных вариантов вывода построенного приложе-
■У My Extensible App!
File
You have just used the VB snap in!
~i3QjD_sJ
Рис. 15.9. Подключение внешних сборок
На этом рассмотрение примера создания расширяемого приложения завершено.
Благодаря этому примеру, должно стать понятно, когда изложенные в главе приемы
могут оказаться полезными в реальном мире, причем не только для разработчиков
специальных инструментов.
Исходный код. Проекты CommonSnappableTypes, CSharpSnapIn, VbSnapIn и
MyExtendableApp доступны в подкаталоге Chapter 15 внутри папки ExtendableApp.
Резюме
Рефлексия является очень интересным аспектом надежной среды
объектно-ориентированного программирования. В мире .NET все, что касается служб рефлексии, главным
образом связано с классом System. Type и пространством имен System. Reflection.
Как было показано в настоящей главе, под рефлексией понимается процесс помещения
типа во время выполнения под "увеличительное стекло" для получения детальной
информации о его характеристиках и возможностях.
Позднее связывание представляет собой процесс создания типа и вызова его членов
без знания заранее того, как конкретно выглядят имена этих членов. Довольно часто
оно является непосредственным результатом динамической загрузки, которая
позволяет программно загружать сборку .NET в память. В демонстрировавшемся в настоящей
главе примере создания расширяемого приложения было показано, что подобная
методика является очень мощной и используется не только разработчиками инструментов,
но и их потребителями. Кроме того, в главе рассказывалось о роли программирования с
применением атрибутов. Снабжение типов атрибутами позволяет включать в исходную
сборку дополнительные метаданные.
ГЛАВА 16
Процессы, домены
приложений
и контексты объектов
Вцредыдущих двух главах рассказывалось о том, какие шаги предпринимает CLR-
среда для определения местонахождения упоминаемых внешних сборок, а также
о том, какую роль в .NET играют метаданные. В настоящей главе будет показано, каким
образом сборки обслуживаются в CLR-среде, и описаны отношения между процессами,
доменами приложений и контекстами объектов.
По сути, доменом приложения (Application Domain — AppDomain) называется
логический подраздел внутри отдельного процесса, в котором обслуживается набор
связанных между собой сборок .NET. Как здесь будет показано, каждый домен приложения,
в свою очередь, делится на отдельные так называемые контекстные границы (context
boundaries), которые применяются для группирования вместе подобных .NET-объектов.
Понятие контекста позволяет CLR-среде обеспечивать надлежащую обработку объектов
с особыми требованиями во время выполнения.
Хотя при решении многих повседневных задач программирования может не
понадобиться напрямую иметь дело с процессами, доменами приложении и контекстами
приложений, понимание этих тем чрезвычайно важно при работе с многочисленными
API-интерфейсами .NET, в том числе Windows Communication Foundation (WCF),
многопоточная и параллельная обработка и сериализация объектов.
Роль процесса Windows
Понятие "процесса" существовало в операционных системах Windows задолго до
появления платформы .NET. Попросту говоря, под процессом понимается выполняющаяся
программа. Однако формально процесс — это концепция уровня операционной
системы, которая используется для описания набора ресурсов (таких как внешние
библиотеки кода и главный поток) и необходимой памяти, используемой выполняющимся
приложением. Для каждого загружаемого в память файла * . ехе в операционной системе
создается отдельный изолированный процесс, который используется на протяжении
всего времени его существования. Благодаря такой изоляции приложений,
исполняющая среда получается гораздо более надежной и стабильной, поскольку выход из строя
одного процесса никак не сказывается на работе других процессов.
Глава 16. Процессы, домены приложений и контексты объектов 583
Более того, доступ напрямую к данным в одном процессе из другого процесса
невозможен, если только не применяется API-интерфейс распределенных вычислений, такой
как Windows Communication Foundation. Из-за всех этих моментов процесс может
считаться фиксированной и безопасной границей выполняющегося приложения.
Каждый процесс Windows получает уникальный идентификатор процесса (Process
ID — PID) и может независимо загружаться й выгружаться операционной системой
(в том числе программно). Как уже наверняка известно, в окне Window&Task Manager
(Диспетчер задач) имеется вкладка Processes (Процессы), на которой можно
просматривать различные статические данные о выполняющихся на данной машине процессах,
в том числе их PID-идентификаторы и имена образов. Чтобы открыть окно диспетчера
задач (рис. 16.1), нажмите комбинацию клавиш <Ctrl+Shift+Esc>.
На заметку! По умолчанию столбец РЮ (Идентификатор процесса) на вкладке Processes не
отображается. Для его включения необходимо выбрать в меню View (Вид) пункт Select Columns
(Выбрать столбцы) и отметить флажок PID (Process Identifier) (ИД (PID)).
Applications ! Process '
Image Kame
System Ide Process
System
iexptere.exe 2
smss.exe
csrss.exe
svchost.exe
wtimit.exe
csrss.exe
wlntogon.exe
services.exe
Isass.exe
Ism.exe
svchost.exe
svchost.exe
svchost.exe
pro'
4
108
268
-32
448
472
504
536
560
584
591
684
760
332
User Name
SYSTEM
SYSTEM
Andrev ..
SYSTEM
SYSTEM
LOCAL...
SYSTEM
SYSTEM
SYSTEM
SYSTEM
SYSTEM
SYSTEM
SYSTEM
NETWO...
NETWO...
CPU
99
'I:
00
00
00
00
00
00
00
00
00
00
00
00
00
Memory (...
24 К
n0 К
8,124К
292K
1,084 К
3,664 К
636 К
1,364 К
916 К
2,560 К
2,072 К
840 К
1,836 К
2,468 К
4,232 К
resr'pt.o"
Per cen ta...
NT Kernel...
Internet...
Windows ...
Client Ser...
HostProc...
Windows...
Oent Ser...
Windows...
Services...
Local Sec...
Local Ses...
HostProc...
HostProc...
HostProc...
J
-
Е Show processes from all users
end Process
CPU Usage: l^c
Physical Memory: 32%
Рис. 16.1. Вкладка Processes в окне диспетчера задач Windows
Роль потоков
В каждом процессе Windows содержится первоначальный "поток", который является
входной точкой для приложения. Детали построения многопоточных приложений в .NET
будут рассматриваться в главе 19, а пока для понимания излагаемого здесь материала
необходимо ознакомиться с несколькими рабочими определениями. Прежде всего,
потоком называется используемый внутри процесса путь выполнения. Формально поток,
который создается первым во входной точке процесса, называется главным потоком
(primary thread). В любой исполняемой программе .NET (консольном приложении,
приложении Windows Forms, приложении WPF и т.д.) входная точка обозначается как метод
Main (). При вызове этого метода главный поток создается автоматически.
Процессы, в которых содержится единственный главный поток выполнения,
изначально являются безопасными к потокам (thread safe), поскольку в каждый отдельный
момент времени доступ к данным приложения в них может получать только один по-
584 Часть IV. Программирование с использованием сборок .NET
ток. Однако подобные однопоточные процессы (особенно с графическим
пользовательским интерфейсом) часто замедленно реагируют на действия пользователя, когда их
единственный поток выполняет какую-то сложную операцию (вроде вывода на печать
длинного текстового файла, сложных математических вычислений или подключения к
удаленному серверу).
Из-за такого потенциального недостатка однопоточных приложений, API-интерфейс
Windows (а также платформа .NET) предоставляет возможность для главного потока
порождать дополнительные вторичные потоки (также называемые рабочими потоками).
Это делается с применением набора функций из API-интерфейса Windows, таких как
CreateThreadO . Каждый поток (первичный или вторичный) в процессе становится
уникальным путем выполнения и может параллельно получать доступ ко всем
разделяемым элементам данных внутри соответствующего процесса.
Как не трудно догадаться, разработчики обычно создают дополнительные потоки
для улучшения общей степени восприимчивости программы к действиям пользователя.
Многопоточные процессы обеспечивают иллюзию того, что выполнение
многочисленных действий происходит примерно в одно и то же время. Например, дополнительный
рабочий поток может порождаться в приложении для выполнения какой-нибудь
трудоемкой задачи (подобной выводу на печать большого текстового файла). После начала
выполнения задачи вторичным потоком основной поток все равно не утрачивает
способности реагировать на действия пользователя, что дает всему процессу возможность
сопровождаться куда более высокой производительностью. Однако такого может и не
происходить: в случае использования слишком большого количества потоков в одном
процессе его производительность может даже ухудшаться из-за возникновения у ЦП
необходимости переключаться между активными потоками в процессе (что отнимает
определенное время).
На некоторых машинах многопоточность по большей части представляет собой не
более чем просто обеспечиваемую операционной системой иллюзию. Машины с одним
(не поддерживающим гиперпотоки) процессором буквально не имеют никакой
возможности обрабатывать множество потоков в точности в одно и то же время. Вместо этого
они выполняют по одному потоку за единицу времени (называемую квантом),
основываясь отчасти на приоритете потока. По истечении выделенного кванта времени
выполнение существующего потока приостанавливается для предоставления другому потоку
возможности выполнить свою задачу. Чтобы поток не забывал, на чем он работал перед
тем, как его выполнение было приостановлено, каждому потоку предоставляется
возможность записывать данные в локальное хранилище потоков (Thread Local Storage —
TLS) и выделяется отдельный стек вызовов, как показано на рис. 16.2.
Одиночный процесс Windows
Разделяемые данные
Поток А
Локальное
хранилище
потоков (TLS)
Стек вызовов
Поток В
Локальное
хранилище
потоков(TLS)
Стек вызовов
Рис. 16.2. Отношения между потоками и процессами в Windows
Глава 16. Процессы, домены приложений и контексты объектов 585
Если тема потоков является для вас новой, не стоит беспокоиться по поводу
деталей. На данном этапе главное запомнить, что любой поток представляет собой просто
уникальный путь выполнения внутри процесса Windows, а каждый процесс обязательно
имеет главный поток (который создается в точке входа в приложение) и может
содержать дополнительные потоки, создаваемые программным образом.
Взаимодействие с процессами
в рамках платформы .NET
Хотя в самих процессах и потоках нет ничего нового, способ, которым с ними можно
взаимодействовать в рамках платформы .NET, довольно прилично изменился (в лучшую
сторону). Чтобы проложить себе путь к пониманию приемов построения многопоточных
сборок (о которых речь пойдет в главе 19), начнем с того, что посмотрим, каким
образом можно взаимодействовать с процессами за счет применения библиотек базовых
классов .NET.
В пространстве имен System. Diagnostics поставляется набор типов, которые
позволяют программно взаимодействовать с процессами и различными связанными
с диагностикой средствами вроде системного журнала событий и счетчиков
производительности. В настоящей главе нас интересуют только те типы, которые позволяют
взаимодействовать с процессами. Некоторые наиболее важные из них перечислены в
табл. 16.1.
Таблица 16.1. Некоторые типы из пространства имен System.Diagnostics
Типы в System.Diagnostics,
которые позволяют Описание
взаимодействовать с процессами
Process Предоставляет доступ к локальным и удаленным процессам,
а также позволяет запускать и останавливать процессы
программным образом
ProcessModule Представляет модуль (* . dll или * . ехе), который должен
загружаться в определенный процесс. Важно понимать, что
этот тип может применяться для представления любого
модуля — COM-, .NET- или традиционного двоичного на базе С
ProcessModuleCollection Позволяет создавать строго типизированную коллекцию
объектов ProcessModule
ProcessStartlnf о Позволяет указывать набор значений, которые должны
использоваться при запуске процесса посредством метода
Process.Start ()
ProcessThread Представляет поток внутри определенного процесса.
Следует иметь в виду, что этот тип применяется для
диагностики набора потоков в процессе, но не для ответвления
внутри него новых потоков
ProcessThreadCollection Позволяет создавать строго типизованную коллекцию
объектов ProcessThread
Класс System. Diagnostics . Process позволяет анализировать процессы, которые
выполняются на какой-то определенной машине (локальной или удаленной). Кроме
того, в нем есть члены, которые позволяют программно запускать и останавливать
процессы, просматривать приоритет процесса, а также получать список активных потоков
586 Часть IV. Программирование с использованием сборок .NET
и/или модулей, которые были загружены в данный процесс. В табл. 16.2 перечислены
некоторые наиболее важные свойства класса System. Diagnostics . Process.
Таблица 16.2. Избранные свойства класса Process
Свойство
Описание
ExitTime
Handle
Id
MachmeName
MainWmdowTitle
Modules
ProcessName
Responding
StartTime
Threads
Это свойство позволяет извлекать значение даты и времени,
ассоциируемое с процессом, который завершил свою работу (и представленное
типом DateTime).
Это свойство возвращает дескриптор (представляемый с помощью
IntPtr), который был назначен процессу операционной системой.
Может оказаться полезным при создании приложений .NET,
нуждающихся во взаимодействии с неуправляемым кодом.
Это свойство позволяет получать идентификатор (PID)
соответствующего процесса.
Это свойство позволяет получать имя компьютера, на котором
выполняется соответствующий процесс.
Это свойство позволяет получать заголовок главного окна процесса
(если у процесса нет главного окна, возвращается пустая строка).
Это свойство позволяет получать доступ к строго типизованной коллекции
ProcessModuleCollection, представляющей набор модулей (* . dll
или * . ехе), которые были загружены в рамках текущего процесса.
Это свойство позволяет получать имя процесса (которое совпадает с
именем самого приложения)
Это свойство позволяет получать значение, показывающее, реагирует ли
пользовательский интерфейс соответствующего процесса на действия
пользователя (и не находится ли он в текущий момент в "зависшем" состоянии).
Это свойство позволяет получать информацию о том, когда был запущен
соответствующий процесс (представленную типом DateTime).
Это свойство позволяет получать информацию о том, какие потоки
выполняются в рамках соответствующего процесса (в виде коллекции
объектов ProcessThread).
Помимо перечисленных выше свойств, класс System. Diagnostics . Process имеет
несколько полезных методов, часть из которых описана в табл. 16.3.
Таблица 16.3. Избранные методы класса Process
Метод
Описание
CloseMainWindow()
GetCurrentProcess()
GetProcesses()
Kill()
Start ()
Этот метод позволяет завершать процесс, обладающий пользовательским
интерфейсом, за счет отправки в его главное окно сообщения о закрытии.
Этот статический метод возвращает новый объект Process,
представляющий процесс, который является активным в текущий момент.
Этот статический метод возвращает массив новых объектов Process,
которые выполняются на текущей машине.
Этот метод позволяет немедленно останавливать соответствующий
процесс.
Этот метод позволяет запускать процесс.
Глава 16. Процессы, домены приложений и контексты объектов 587
Перечисление выполняющихся процессов
Чтобы увидеть, как манипулировать объектами Process, создадим новый проект
типа Console Application (Консольное приложение) на С# по имени ProcessManipulator
и определим в нем внутри класса Program следующий вспомогательный статический
метод (не забыв, конечно же, импортировать пространство имен System.Diagnostics):
public static void ListAllRunningProcesses()
{
// Получение списка всех процессов, которые выполняются
//на текущей машине, упорядоченных по PID.
var runmngProcs =
from proc in Process.GetProcesses (". ") orderby proc.Id select proc;
// Отображение идентификатора и имени каждого процесса.
foreach (var p in runmngProcs)
{
string info = string.Format("-> PID: {0}\tName: {1}",
p.Id, p.ProcessName);
Console.WriteLine (info);
}
Console WriteLine(n************************************\n") •
}
Обратите внимание, что статический метод Process .GetProcesses () возвращает
массив объектов Process, которые представляют выполняющиеся на целевой
машине процессы (используемая здесь нотация в виде точки свидетельствует о том, что в
данном случае целевой машиной является локальный компьютер). После получения
массива объектов Process можно обращаться к любому члену, которые описаны в
табл. 16.2 и 16.3. В примере просто выводятся идентификаторы (PID) и имена всех
процессов в порядке следования их PID-идентификаторов. Добавив в Main () вызов метода
ListAllRunningProcesses ():
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Processes *****\n");
ListAllRunningProcesses ();
Console.ReadLine ();
}
можно будет увидеть список имен и PID-идентификаторов всех процессов,
выполняющихся в текущий момент на локальном компьютере. Ниже показано, как может
выглядеть вывод:
***** pun with Processes *****
->
->
->
->
->
->
->
->
->
->
->
->
'->
->
PID:
PID:
PID:
PID:
PID:
PID:
PID:
PID:
PID:
PID:
PID:
PID:
PID:
PID:
0
4
108
268
432
448
472
504
536
560
584
592
660
684
Name:
Name :
Name:
Name:
Name:
Name:
Name:
Name:
Name:
Name:
Name:
Name:
Name :
Name:
Idle
System
lexplore
smss
csrss
svchost
wininit
csrss
winlogon
services
lsass
Ism
devenv
svchost
588 Часть IV. Программирование с использованием сборок .NET
> PID:
> PID:
> PID:
> PID:
> PID:
> PID:
> PID:
> PID:
> PID:
760
832
844
856
900
924
956
1116
1136
Name:
Name:
Name:
Name:
Name:
Name:
Name:
Name:
Name:
svchost
svchost
svchost
svchost
svchost
svchost
VMwareService
spoolsv
ProcessManipulator.vshost
••••••••••••••••••••••••••••••••••••
Изучение конкретного процесса
Помимо полного списка всех выполняющихся на конкретной машине процессов,
статический метод Process. GetProcessByld () позволяет получать информацию и по
конкретному объекту Process за счет указания ассоциируемого с ним идентификатора (PID).
В случае запроса несуществующего PID генерируется исключение ArgumentException.
Например, чтобы получить информацию об объекте Process, представляющем процесс
с РШ-идентификатором 987, можно написать следующий код:
// Если процесс с PID 987 не существует, сгенерируете* исключение.
static void GetSpeciflcProcess ()
{
Process theProc = null;
try
{
theProc = Process.GetProcessByld(987);
}
catch(ArgumentException ex)
Console.WriteLine(ex.Message);
}
К этому моменту уже было показано, как получать список всех процессов и
сведения о конкретном процессе на машине, выполняя поиск по PID-идентификатору. Класс
Process также позволяет просмотреть набор текущих потоков и библиотек,
используемых в заданном процессе. Давайте посмотрим, как это делается.
Изучение набора потоков процесса
Набор потоков представляется в виде строго типизованной коллекции ProcessThread
Collection, в которой содержится определенное количество отдельных объектов
ProcessThread. Для примера предположим, что в текущее приложение добавлена
следующая вспомогательная статическая функция:
static void EnumThreadsForPid (int pID)
{
Process theProc = null;
try
{
theProc = Process.GetProcessByld(pID);
}
catch(ArgumentException ex)
{
Console.WriteLine(ex.Message);
return;
Глава 16. Процессы, домены приложений и контексты объектов 589
// Отображение статистических данных по каждому потоку
//в указанном процессе.
Console.WriteLine("Here are the threads used by: {0}",
theProc.ProcessName);
ProcessThr.eadCollection theThreads = theProc.Threads;
foreach(ProcessThread pt in theThreads)
{
string info =
string.Format("-> Thread ID: {0}\tStart Time {1}\tPriority {2}",
pt.Id , pt.StartTime.ToShortTimeString (), pt. PnontyLevel) ;
Console.WriteLine(info);
}
Console.WriteLine (n************* ^ ********************** \^\и) •
}
Как не трудно заметить, свойство Threads в System. Diagnostics . Process
предоставляет доступ к классу ProcessThreadCollection. Здесь с его помощью
обеспечивается отображение информации о назначенном идентификаторе, времени запуска и
уровне приоритета каждого из потоков, используемых в указанном клиентом процессе.
Давайте теперь обновим метод Main () в классе Program так, чтобы он запрашивал у
пользователя PID-идентификатор интересующего процесса:
static void Main(string [ ] args)
// Запрашивание у пользователя PID-идентификатора и вывод информации
//о соответствующих активных потоках в окне консоли.
Console.WriteLine ("***** Enter PID of process to investigate *****");
Console.Write("PID: ");
string pID = Console.ReadLine();
int theProcID = int.Parse(pID);
EnumThreadsForPid(theProcID);
Console.ReadLine();
}
Запустив приложение, можно вводить PID-идентификатор любого процесса на
машине и просматривать используемые внутри него потоки. Ниже показан пример
вывода в случае просмотра информации о потоках, используемых в процессе с PID-
идентификатором 108, который (так получилось) отвечает за обслуживание Microsoft
Internet Explorer:
***** Enter PID of process to investigate *****
PID: 108
*
Here are the threads used by: lexplore
>
>
>
>
>
>
>
>
>
>
>
>
>
>
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
ID:
ID:
ID:
ID:
ID:
ID:
ID:
ID:
ID:
ID:
ID:
ID:
ID:
ID:
680
2040
880
3380
3376
3448
3476
2264
2380
2384
2308
3096
3600
1412
Start
Start
Start
Start
Start
Start
Start
Start
Start
Start
Start
Start
Start
Start
Time:
Time:
Time:
Time:
Time:
Time:
Time:
Time:
Time:
Time:
Time:
Time:
Time:
Time:
9:
9:
9:
9:
9:
9:
9:
9:
9:
9:
9:
9:
9:
:05
:05
:05
:05
:05
:05
:05
:05
:05
:05
:05
:07
:45
AM
AM
AM
AM
AM
AM
AM
AM
AM
AM
AM
AM
AM
10:02 AM
Priority:
Priority:
Priority:
Priority:
Priority:
Priority:
Priority:
Priority:
Priority:
Priority:
Priority:
Priority:
Priority:
Priority:
Normal
Normal
Normal
Normal
Normal
Normal
Normal
Normal
Normal
Normal
Normal
Highest
Normal
Normal
590 Часть IV. Программирование с использованием сборок .NET
Помимо Id, StartTime и PriorityLevel, класс ProcessThread имеет
дополнительные члены. Некоторые наиболее интересные из них перечислены в табл. 16.4.
Таблица 16.4. Избранные члены класса ProcessThread
Член Описание
CurrentPriority Позволяет получить информацию о текущем приоритете потока
Id Позволяет получить уникальный идентификатор потока
IdealProcessor Позволяет указать предпочитаемый процессор для выполнения
данного потока
PriorityLevel Позволяет получить или задать уровень приоритета потока
ProcessorAf f inity Позволяет указать процессоры, на которых может выполняться
соответствующий поток
Start Address Позволяет узнать, по какому адресу в памяти операционная
система вызывала функцию, приведшую к запуску данного потока
StartTime Позволяет узнать, когда операционная система запустила поток
ThreadState Позволяет узнать текущее состояние данного потока
TotalProcessorTime Позволяет узнать, сколько всего времени данный поток
использовал процессор
WaitReason Позволяет узнать причину, по которой поток находится в
состоянии ожидания
Прежде чем двигаться дальше, необходимо четко уяснить, что тип ProcessThread
не является сущностью, применяемой для создания, приостановки и уничтожения
потоков в .NET. Он скорее представляет собой средство, позволяющее получать
диагностическую информацию по активным потокам Windows внутри выполняющегося процесса.
Более подробно о том, как создавать многопоточные приложения с применением
пространства имен System.Threading, речь пойдет в главе 19
Изучение набора модулей процесса
Теперь давайте посмотрим, как реализовать проход по загруженным модулям,
которые обслуживаются в рамках конкретного процесса. При обсуждении процессов
модуль — это общий термин, используемый для описания заданной сборки * .dll (или
* . ехе), которая обслуживается в определенном процессе. При получении доступа к
ProcessModuleCollection через свойство Process .Modules можно извлечь список
всех модулей, которые обслуживаются внутри процесса: .NET-, COM- и традиционных
основанных на С библиотек. Для примера создадим показанную ниже дополнительную
вспомогательную функцию, способную перечислять модули в конкретном процессе на
основе предоставляемого PID-идентификатора:
static void EnumModsForPid (int pID)
{
Process theProc = null;
try
{
theProc = Process.GetProcessByld(pID);
}
Глава 16. Процессы, домены приложений и контексты объектов 591
catch(ArgumentException ex)
{
Console.WriteLine(ex.Message);
return;
}
Console.WriteLine ("Here are the loaded modules for: {0}",
theProc.ProcessName);
try
{
ProcessModuleCollection theMods = theProc.Modules;
foreach(ProcessModule pm in theMods)
{
string info = string.Format("-> Mod Name: {0}",
pm.ModuleName);
Console.WriteLine(info);
}
Чтобы посмотреть на вывод, давайте попробуем узнать, как будут выглядеть
загружаемые модули для процесса, обслуживающего текущее демонстрационное
приложение (ProcessManipulator). Для этого сначала необходимо запустить приложение,
выяснить, какой PID-идентификатор был присвоен ProcessManipulator.exe (с помощью
окна Task Manager), и передать это значение методу EnumModsForPid () (разумеется, не
забыв перед этим соответствующим образом обновить метод Main ()). После этого
можно будет оценить, насколько много модулей * .dll (GDI 32 .dll, USER32 .dll, ole32.dll
и т.д.) используется для довольно простого консольного приложения:
Here are the loaded modules for: ProcessManipulator
-> Mod Name: ProcessManipulator.exe
-> Mod Name: ntdll.dll
-> Mod Name: MSCOREE.DLL
-> Mod Name: KERNEL32.dll
-> Mod Name: KERNELBASE.dll
-> Mod Name: ADVAPl32.dll
-> Mod Name: msvcrt.dll
-> Mod Name: sechost.dll
-> Mod Name: RPCRT4.dll
-> Mod Name: SspiCli.dll
-> Mod Name: CRYPTBASE.dll
-> Mod Name: mscoreei.dll
-> Mod Name: SHLWAPI.dll
-> Mod Name: GDI32.dll
-> Mod Name: USER32.dll
-> Mod Name: LPK.dll
-> Mod Name: USP10.dll
-> Mod Name: IMM32.DLL
-> Mod Name: MSCTF.dll
-> Mod Name: clr.dll
-> Mod Name: MSVCR100_CLR04 00.dll
-> Mod Name: mscorlib.ni.dll
-> Mod Name: nlssorting.dll
-> Mod Name: ole32.dll
-> Mod Name: clrjit.dll
-> Mod Name: System.ni.dll
-> Mod Name: System.Core.ni.dll
-> Mod Name: psapi.dll
-> Mod Name: shfolder.dll
-> Mod Name: SHELL32.dll '
************************************
592 Часть IV. Программирование с использованием сборок .NET
Запуск и останов процессов программным образом
И, наконец, последними членами класса System. Diagnostics . Process, которые
осталось рассмотреть, являются методы Start () и Kill (). Эти методы позволяют,
соответственно, программно запускать и завершать процесс. В качестве примера
создадим вспомогательный статический метод StartAndKi 11 Process (), код которого
показан ниже.
На заметку! Для того чтобы запускать новые процессы, необходимо запускать Visual Studio 2010 от
имени администратора. В противном случае во время выполнения будет возникать ошибка
static void StartAndKillProcess ()
{
Process leProc = null;
// Запустить Internet Explorer и перейти на страницу facebook.com.
try
{
leProc = Process . Start ("IExplore . exe", "www.facebook.com11);
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}
Console.Write("--> Hit enter to kill {0}...", leProc.ProcessName);
Console.ReadLine();
// Уничтожить процесс iexplore.exe.
try
{
ieProc.Kill();
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}
}
Статический метод Process . Start () имеет несколько перегруженных версий. Как
минимум, необходимо указать дружественное имя запускаемого процесса (такое как
iexplore.exeB случае Microsoft Internet Explorer). В данном примере используется
версия метода Start (), которая позволяет указывать любое количество дополнительных
аргументов, подлежащих передаче в точку входа программы (т.е. метод Main ()).
После вызова метода Start () возвращается ссылка на только что запущенный
процесс. Чтобы завершить процесс, нужно вызывать на уровне соответствующего
экземпляра метод Kill () . Вызовы Start () и Kill () помещены внутрь блока try/catch, и
предусмотрена обработка любых ошибок InvalidOperationException. Это особенно
важно при вызове метода Kill (), поскольку такая ошибка будет обязательно возникать
в случае завершения процесса до вызова Kill ().
Управление запуском процесса с использованием
класса ProcessStartlnfо
Метод Start () может принимать тип System. Diagnostics . ProcessStartlnf о и
предоставлять дополнительные фрагменты информации относительно запуска
определенного процесса. Ниже показано частичное определение ProcessStartlnfo (полное
определение можно найти в документации .NET Framework 4.0 SDK):
Глава 16. Процессы, домены приложений и контексты объектов 593
public sealed class ProcessStartlnfo : object
{
public ProcessStartlnfo ();
public ProcessStartlnfo(string fileName);
public ProcessStartlnfo(string fileName, string arguments);
public string Arguments { get; set; }
public bool CreateNoWindow { get; set; }
public StringDictionary EnvironmentVariables { get; }
public bool ErrorDialog { get; set; }
public IntPtr ErrorDialogParentHandle { get; set; }
public string FileName { get; set; }
public bool LoadUserProfile { get; set; }
public SecureString Password { get; set; }
public bool RedirectStandardError { get; set; }
public bool RedirectStandardlnput { get; set; }
public bool RedirectStandardOutput { get; set; }
public Encoding StandardErrorEncoding { get; set; }
public Encoding StandardOutputEncoding { get; set; }
public bool UseShellExecute { get; set; }
public string Verb { get; set; }
public string [] Verbs { get; }
public ProcessWindowStyle WindowStyle { get; set; }
public string WorkingDirectory { get; set; }
}
Чтобы просмотреть, как с помощью этого класса настроить запуск процесса,
изменим метод StartAndKillProcess () так, чтобы он теперь предусматривал не только
загрузку браузера Microsoft Internet Explorer и переход на страницу www. f acebook. com, но
и отображение окна этого браузера в развернутом виде:
static void StartAndKillProcess ()
{
Process leProc = null;
// Запуск браузера Internet Explorer и переход на страницу
// facebook.com с отображением окна в развернутом виде.
try
{
ProcessStartlnfo startlnfo = new
ProcessStartlnfo ("IExplore.exe", "www.facebook.com11) ;
startlnfo.WindowStyle = ProcessWindowStyle.Maximized;
leProc = Process.Start(startlnfo) ;
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}
Теперь, когда известна роль процессов Windows и способы взаимодействия с ними в
коде С#, можно спокойно переходить к изучению доменов приложений .NET.
Исходный код. Проект ProcessManipulator доступен в подкаталоге Chapter 16.
594 Часть IV. Программирование с использованием сборок .NET
Домены приложений .NET
В .NET исполняемые файлы не обслуживаются прямо внутри процесса Windows, как
это происходит в случае традиционных неуправляемых приложений. Вместо этого они
обслуживаются в отдельном логическом разделе внутри процесса, который называется
доменом приложения (Application Domain — AppDomain). Как будет показано, в
единственном процессе может содержаться несколько доменов приложений, каждый из
которых обслуживает свой исполняемый файл .NET. Такое дополнительное подразделение
традиционного процесса Windows предоставляет ряд преимуществ, главные из которых
описаны ниже.
• Домены приложений играют ключевую роль в обеспечении нейтральности
платформы .NET по отношению к операционной системе из-за того, что такое
логическое разделение стирает отличия в способе представления загружаемого
исполняемого файла лежащей в основе операционной системой.
• Домены приложений являются гораздо менее дорогостоящими в плане
потребления вычислительных ресурсов и памяти по сравнению с полноценными
процессами. Благодаря этому CLR-среде удается загружать и выгружать домены
приложений намного быстрее, чем формальные процессы, и тем самым значительно
улучшать масштабируемость серверных приложений.
• Домены приложений обеспечивают более глубокий уровень изоляции для
обслуживания загружаемого приложения. В случае выхода из строя какого-то одного
домена приложения внутри процесса, остальные домены приложений все равно
остаются работоспособными.
Как упоминалось выше, в одном процессе может обслуживаться любое количество
доменов с полной изоляцией каждого из них от всех остальных доменов приложений
внутри этого же (или любого другого) процесса. Следует очень четко осознавать, что
приложение, выполняющееся в одном домене приложения, не может получать данные
любого рода (глобальные перемененные или статические поля) из другого домена
приложения, если только не будет использоваться какой-то протокол распределенного
программирования (наподобие Windows Communication Foundation).
Хотя в одном процессе может обслуживаться множество доменов приложений,
обычно такого не происходит. Как минимум, в любом процессе операционной системы
всегда обслуживается так называемый домен приложения по умолчанию (default
application domain). Этот специальный домен приложения создается автоматически CLR-
средой во время запуска процесса. После этого CLR-среда создает все остальные
дополнительные домены приложений по мере необходимости.
Класс System. AppDomain
Платформа .NET позволяет программно осуществлять мониторинг доменов
приложений, создавать новые домены приложений (или выгружать их) во время выполнения,
загружать в домены приложений различные сборки и решать целый ряд других задач
с применением класса AppDomain из пространства имен System, которое находится в
сборке mscorlib. dll. В табл. 16.5 перечислены наиболее полезные методы этого
класса (полную информацию об этом классе и его членах можно, как обычно, найти в
документации .NET Framework 4.0 SDK).
Глава 16. Процессы, домены приложений и контексты объектов 595
Таблица 16.5. Некоторые методы класса AppDomain
Член
Описание
CreateDomain()
Createlnstance()
ExecuteAssembly()
GetAssemblies()
GetCurrentThreadld()
Load()
Unload()
Этот статический метод позволяет создавать новый домен
приложения в текущем процессе
Этот метод позволяет создавать экземпляр типа из внешней
сборки после загрузки соответствующей сборки в вызывающий домен
приложения
Этот метод позволяет запускать сборку * . ехе внутри домена
приложения за счет предоставления имени ее файла
Этот метод позволяет узнать, какие сборки .NET были
загружены в данный домен приложения (двоичные файлы СОМ и С
игнорируются)
Этот статический метод возвращает идентификатор потока,
который является активным в текущем домене приложения
Этот метод применяется для динамической загрузки сборки в
текущий домен приложения
Этот статический метод позволяет выгрузить определенный домен
приложения из конкретного процесса
На заметку! Платформа .NET не позволяет производить выгрузку конкретной сборки из памяти.
Единственным способом для осуществления выгрузки библиотек программным образом
является разрушение обслуживающего домена приложения с помощью метода Unload ().
Кроме того, класс AppDomain имеет свойства, которые могут быть полезны для
проведения мониторинга за каким-то доменом приложения. Наиболее интересные
свойства такого рода кратко описаны в табл. 16.6.
Таблица 16.6. Некоторые свойства класса AppDomain
Событие
Описание
BaseDirectory
CurrentDomain
FriendlyName
MonitoringIsEnabled
Setuplnformation
Позволяет извлечь путь к каталогу, который преобразователь
адресов использует для поиска сборок
Представляет собой статическое свойство и позволяет узнать
домен приложения, используемый для выполняющегося в текущий
момент потока
Позволяет получить дружественное имя текущего домена приложения
Позволяет получить или установить значение, указывающее,
должна ли работать функция мониторинга за использованием ресурсов
ЦП и памяти для текущего процесса. После включения функции
мониторинга для процесса отключить ее нельзя
Позволяет извлечь детали конфигурации определенного
домена приложения, которые предоставляются в виде объекта
AppDomainSetup
И, наконец, класс AppDomain поддерживает набор событий, которые отражают
различные аспекты жизненного цикла домена приложения. Некоторые наиболее полезные
из них перечислены в табл. 16.7.
596 Часть IV. Программирование с использованием сборок .NET
Таблица 16.7. Некоторые события класса AppDomain
Событие Описание
AssemblyLoad Возникает при загрузке сборки в память
AssemblyResolve Возникает, когда преобразователю адресов сборок не удается
обнаружить место расположения требуемой сборки
DomainUnload Возникает перед началом выгрузки домена приложения из
обслуживающего процесса
FirstChanceException Позволяет получать уведомление о том, что в домене приложения
было сгенерировано какое-то исключение, перед началом
выполнения CLR-средой поиска подходящего оператора catch
ProcessExit Возникает в используемом по умолчанию домене приложения
тогда, когда его родительский процесс завершает работу
UnhandledException Возникает при отсутствии обработчика, способного перехватить
данное исключение
Взаимодействие с используемым
по умолчанию доменом приложения
Вспомните, что при запуске исполняемого файла .NET среда CLR автоматически
помещает его в используемый по умолчанию домен приложения внутри
обслуживающего процесса. Это происходит автоматически и прозрачно, потому писать какой-то
специальный код не понадобится. К используемому по умолчанию домену приложения
можно получить доступ в своем приложении с применением статического свойства
AppDomain.CurrentDomain. Затем можно привязываться к любым интересующим
событиям и использовать любые желаемые методы и свойства AppDomain для проведения
диагностики во время выполнения.
Чтобы посмотреть на взаимодействие с используемым по умолчанию доменом
приложения, давайте создадим новый проект типа Console Application по имени
Def aultAppDomainApp. Модифицируем класс Program, включив в него приведенный
ниже код, который предусматривает вывод детальных сведений об используемом по
умолчанию домене приложения с помощью набора членов класса AppDomain.
class Program
{
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with the default app domain *****\n");
DisplayDADStats();
Console.ReadLine();
}
private static void DisplayDADStats()
{
// Получение доступа к домену приложения,
// используемому для текущего потока по умолчанию.
AppDomain defaultAD = AppDomain.CurrentDomain;
// Вывод в окно консоли статистических данных об этом домене.
Console.WriteLine("Name of this domain: {0}",
defaultAD.FriendlyName); // Дружественное имя
Console.WriteLine("ID of domain in this process: {0}",
defaultAD.Id); // Идентификатор
Глава 16. Процессы, домены приложений и контексты объектов 597
Console.WriteLine ("Is this the default domain?: {0}",
defaultAD.IsDefaultAppDomain()); // Используется ли по умолчанию
Console.WriteLine("Base directory of this domain: {0}",
defaultAD.BaseDirectory); // Базовый каталог
}
}
Ниже показано, как будет выглядеть вывод в результате выполнения этого кода:
***** Fun with the default app domain *****
Name of this domain: DefaultAppDomainApp.exe
ID of domain in this process: 1
Is this the default domain?: True
Base directory of this domain: E:\MyCode\DefaultAppDomainApp\bin\Debug\
Обратите внимание, что имя используемого по умолчанию домена будет
совпадать с именем обслуживаемого внутри него исполняемого файла (в данном примере
DefaultAppDomainApp), а значение базового каталога, которое будет использоваться
для поиска требуемых внешних приватных сборок — с текущим месторасположением
этого исполняемого файла.
Перечисление загружаемых сборок
С применением на уровне экземпляра метода GetAssemblies () можно просмотреть
все сборки .NET, загруженные в заданный домен приложения. Этот метод будет
возвращать массив объектов типа Assembly, который, как было показано в предыдущей
главе, является членом пространства имен System. Reflection (и потому требует импорта
этого пространства имен в файл кода С#).
Чтобы увидеть все это на примере, определим в классе Program новый
вспомогательный метод по имени ListAllAssembliesInAppDomain (), который будет получать
список всех загружаемых сборок и отображать дружественное имя и номер версии каждой
из них в окне консоли.
static void ListAllAssembliesInAppDomain ()
{
// Доступ к домену приложения по умолчанию для текущего потока.
AppDomain defaultAD = AppDomain.CurrentDomain;
// Извлечение списка всех сборок, загруженных в этот домен приложения.
Assembly[] loadedAssemblies = defaultAD.GetAssemblies();
Console.WriteLine("***** Here are the assemblies loaded in {0} *****\n",
defaultAD.FriendlyName);
foreach(Assembly a in loadedAssemblies)
{
Console.WriteLine("-> Name: {0}", a.GetName().Name);
Console.WriteLine("-> Version: {0}\n", a.GetName() .Version) ;
}
}
Добавив в метод Main () вызов этого нового члена, можно просмотреть все сборки
.NET, используемые в домене приложения, который обслуживает исполняемый файл:
***** Here are the assemblies loaded in DefaultAppDomainApp.exe *****
-> Name: mscorlib
-> Version: 4.0.0.0
-> Name: DefaultAppDomainApp
-> Version: 1.0.0.0
Важно понимать, что список используемых сборок при написании другого
кода на С# может приобретать совершенно иной вид. Например, изменим метод
598 Часть IV. Программирование с использованием сборок .NET
ListAllAssembliesInAppDomain () так, чтобы в нем использовался LINQ-запрос,
группирующий сборки по имени:
static void ListAllAssembliesInAppDomain()
{
// Доступ к домену приложения по умолчанию для текущего потока.
AppDomain defaultAD = AppDomain.CurrentDomain;
// Извлечение списка всех сборок, загруженных в этот домен приложения.
var loadedAssemblies = from a in defaultAD.GetAssemblies()
orderby a.GetName().Name select a;
Console.WriteLine ("***** Here are the assemblies loaded in {0} *****\n",
defaultAD.FriendlyName);
foreach (var a in loadedAssemblies)
{
Console.WriteLine("-> Name: {0}", a.GetName().Name);
Console.WriteLine("-> Version: {0}\n", a.GetName().Version);
}
}
Запустив приложение, можно увидеть, что в память также загружены сборки
System.Core.dll и System.dll, поскольку они требуются для работы API-интерфейса
LINQ to Objects:
***** Here are the assemblies loaded in DefaultAppDomainApp.exe *****
-> Name: DefaultAppDomainApp
-> Version: 1.0.0.0
-> Name: mscorlib
-> Version: 4.0.0.0
-> Name: System
-> Version: 4.0.0.0
-> Name: System.Core
-> Version: 4.0.0.0
Получение уведомлений о загрузке сборок
Для получения уведомлений от CLR-среды при загрузке новой сборки в
определенный домен приложения необходимо организовать обработку события AssemblyLoad.
Это событие имеет тип делегата AssemblyLoadEventHandler, который может
указывать на любой метод, принимающий System. Ob j ect в первом параметре и
AssemblyLoadEventArgs — во втором.
Для примера добавим в текущий класс Program еще один метод по имени InitDAD (),
который будет инициализировать используемый по умолчанию домен приложения за
счет обработки события AssemblyLoad с помощью подходящего лямбда-выражения:
private static void InitDAD()
{
// Эта логика будет выводить в окно консоли имя любой сборки,
// загружаемой в домен приложения после его создания.
AppDomain defaultAD = AppDomain.CurrentDomain;
defaultAD.AssemblyLoad += (o, s) =>
{
Console.WriteLine("{0} has been loaded!", s.LoadedAssembly.GetName() .Name);
};
}
Как и следовало ожидать, после этого при загрузке любой новой сборки будет
появляться соответствующее уведомление. В примере просто выводится
дружественное имя сборки с использованием свойства LoadedAssembly входящего параметра
AssemblyLoadedEventArgs.
Глава 16. Процессы, домены приложений и контексты объектов 599
Исходный код. Проект DefaultAppDomainApp доступен в подкаталоге Chapter 16.
Создание новых доменов приложений
Вспомните, что один процесс способен обслуживать множество доменов приложений
посредством статического метода Арр Domain. Create Domain (). И хотя необходимость в
создании новых доменов приложений на лету в большинстве приложений .NET
возникает крайне редко, важно в общем понимать, как это делается. Например, в главе 17 будет
показано, что создаваемые динамические сборки должны устанавливаться в
специальный домен приложения. Вдобавок многие API-интерфейсы, связанные с безопасностью
.NET, требуют понимания того, каким образом создавать новые домены приложения для
изолирования сборок на основе предоставляемых учетных данных безопасности.
Давайте рассмотрим пример создания специальных доменов приложений налету (и
загрузки в них новых сборок), для чего создадим проект типа Console Application (Консольное
приложение) по имени CustomAppDomains. Метод AppDomain.CreateDomain () имеет
несколько перегруженных версий. Как минимум, ему требуется предоставить
дружественное имя создаваемого домена приложения. Поместим в класс Program
показанный ниже код. В этом коде используется метод ListAllAssembliesInAppDomain () из
предыдущего примера, но на этот раз ему для анализа в качестве входного аргумента
передается объект AppDomain.
class Program
{
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with Custom App Domains *****\n");
// Отображение всех сборок, которые были загружены в
// используемый по умолчанию домен приложения.
AppDomain defaultAD = AppD^nain.CurrentDomain;
ListAllAssembliesInAppDomain(defaultAD);
// Создание нового домена приложения.
MakeNewAppDomain();
Console.ReadLine();
}
private static void MakeNewAppDomain ()
{
// Создание нового домена приложения в текущем процессе и
// перечисление всех загруженных в него сборок.
AppDomain newAD = AppDomain.CreateDomain("SecondAppDomain");
ListAllAssembliesInAppDomain(newAD);
}
static void ListAllAssembliesInAppDomain(AppDomain ad)
{
// Теперь получение списка всех загруженных сборок, которые
// присутствуют в используемом по умолчанию домене приложения.
var loadedAssemblies = from a in ad.GetAssemblies()
orderby a.GetName().Name select a;
Console.WriteLine("***++ Here are the assemblies loaded in {0} *****\n",
ad.Fri^ndlyNane);
foreach (var a in loadedAssemblies)
{
Console. WriteLine ("-"• Name: {0}", a.GetName().Name);
Console. WriteLine ("--• Version: {0}\n", a . GetName () .Version) ;
600 Часть IV. Программирование с использованием сборок .NET
Запустив приложение, можно увидеть, что в используемый по умолчанию
домен (CustomAppDomains.exe) будут загружены сборки mscorlib.dll, System.dll,
System. Core. dll и СиstomAppDomains . ехе, что связано с кодовой базой С# текущего
проекта. В новый домен приложения будет загружена только сборка ms cor lib .dll, которая
является одной из сборок .NET, загружаемых CLR-средой в каждый домен приложения.
***** Fun with Custom App Domains *****
***** Here are the assemblies loaded in CustomAppDomains.exe *****
-> Name: CustomAppDomains
-> Version: 1.0.0.0
-> Name: mscorlib
-> Version: 4.0.0.0
-> Name: System
-> Version: 4.0.0.0
-> Name: System.Core
-> Version: 4.0.0.0
***** Here are the assemblies loaded in SecondAppDomain *****
-> Name: mscorlib
-> Version: 4.0.0.0
На заметку! Запустив отладку данного проекта (нажатием <F5>), можно увидеть, что в каждый из
доменов приложений будет загружаться много дополнительных сборок, которые необходимы Visual
Studio для поддержки процесса отладки. Если просто запустить проект на выполнение (нажатием
<Ctrl+F5>), будут выводиться только сборки, загруженные в каждый домен приложения.
Тем, кто привык работать с традиционными приложениями Windows, подобное
поведение может показаться нелогичным (ведь оба домена приложений имеют доступ к
одинаковому набору сборок). Однако следует помнить, что любая сборка загружается в
домен приложения, а не напрямую в процесс.
Загрузка сборок в специальные домены приложений
CLR-среда будет всегда загружать сборки в используемый по умолчанию домен
приложения по мере необходимости. Однако в случае создания вручную специальных
доменов приложений, эти сборки можно загружать в данные домены с помощью метода
AppDomain. Load () . Кроме того, существует метод AppDomain.ExecuteAssembly (),
который позволяет загрузить сборку * . ехе и выполнить метод Main ().
Чтобы увидеть все это на примере, давайте представим, что необходимо обеспечить
загрузку сборки CarLibrary.dll в новый вторичный домен приложения. Скопировав
эту библиотеку в папку bin\Debug текущего приложения, можно модифицировать
метод MakeNewAppDomain () (не забыв импортировать пространство имен System. 10 для
получения доступа к классу FileNotFoundException), как показано ниже:
private static void MakeNewAppDomain ()
{
// Создание нового домена приложения в текущем процессе.
AppDomain newAD = AppDomain.CreateDomain("SecondAppDomain");
try
{
// Загрузка в новый домен сборки CarLibrary.dll.
newAD.Load("CarLibrary");
}
catch (FileNotFoundException ex)
{
Console. WnteLine (ex.Message);
}
Глава 16. Процессы, домены приложений и контексты объектов 601
// Вывод списка всех сборок.
ListAllAssembliesInAppDomain(newAD);
}
На этот раз вывод приложения выглядит следующим образом (обратите внимание на
добавление сборки CarLibrary.dll):
***** Fun with Custom App Domains *****
***** Here are the assemblies loaded in CustomAppDomains.exe *****
-> Name: CustomAppDomains
-> Version: 1.0.0.0
-> Name: mscorlib
-> Version: 4.0.0.0
-> Name: System
-> Version: 4.0.0.0
-> Name: System.Core
-> Version: 4.0.0.0
***** Here are the assemblies loaded in SecondAppDomain *****
-> Name: CarLibrary
-> Version: 2.0.0.0
-> Name: mscorlib
-> Version: 4.0.0.0
На заметку! Не забывайте, что при выполнении отладки приложения в каждый из доменов
приложения будет также загружаться множество дополнительных библиотек.
Выгрузка доменов приложений программным образом
Важно отметить, что выгружать отдельные сборки .NET в CLR-среде не разрешено.
Однако с помощью метода App Domain. Unload () можно производить избирательную
выгрузку определенного домена приложения из обслуживающего процесса. В этом случае
вместе с доменом приложения будут выгружаться и все содержащиеся в нем сборки.
Вспомните, что тип AppDomain имеет событие DomainUnload, которое срабатывает
при выгрузке специального домена приложения из содержащего его процесса. Еще
одним интересным событием является ProcessExit, которое срабатывает при выгрузке
из процесса используемого по умолчанию домена (что, вполне очевидно, влечет за собой
завершение самого процесса). Чтобы реализовать программную выгрузку домена newAD
из обслуживающего процесса с получением уведомления о его уничтожении,
модифицируем метод MakeNewAppDomain (), добавив в него следующую логику:
private static void MakeNewAppDomain ()
{
// Создание нового домена приложения в текущем процессе.
AppDomain newAD = AppDomain.CreateDomain("SecondAppDomain");
newAD.DomainUnload += (o, s) =>
{
Console.WriteLine("The second app domain has been unloaded!");
};
try
{
// Загрузка в новый домен сборки CarLibrary.dll.
newAD.Load("CarLibrary");
}
602 Часть IV. Программирование с использованием сборок .NET
catch (FileNotFoundException ex)
{
Console.WriteLine(ex.Message);
}
// Вывод списка всех сборок.
ListAllAssembliesInAppDomain(newAD);
// Уничтожение домена приложения.
АррDomain . Unln=id (newAD) ;
}
Чтобы включить уведомление при выгрузке используемого по умолчанию домена
приложения, в методе Main () необходимо предусмотреть обработку события ProcessEvent
этого домена:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Custom App Domains *****\nM);
// Вывод списка всех сборок, которые были загружены
//в используемый по умолчанию домен приложения.
AppDomain defaultAD = AppDomain.CurrentDomain;
defaultAD.ProcessExit += (о, s) =>
{
Console.WriteLine("Default AD unloaded!");
};
ListAllAssembliesInAppDomain(defaultAD);
MakeNewAppDomain();
Console.ReadLine ();
}
На этом рассмотрение доменов приложений в NET завершено. Напоследок в главе
рассмотрим еще один уровень деления, применяемый для группирования объектов в
контекстные границы.
Исходный код. Проект CustomAppDomains доступен в подкаталоге Chapter 16.
Границы контекстов объектов
Выше было показано, что домены приложений представляют собой логические
разделы внутри процесса, которые используются для обслуживания сборок .NET. Однако
на этом дело не заканчивается, поскольку каждый домен приложения может быть
дополнительно разделен на многочисленные контексты. Вкратце, контекст в .NET
предоставляет возможность закреплять за конкретным объектом "определенное место" в
одном домене приложения.
На заметку! Следует отметить, что если понимание концепции процессов и доменов приложений
является довольно важным, то необходимость работы с контекстами объектов в большинстве
приложений .NET возникает очень редко. Материал по контекстам объектов включен в
настоящую главу лишь для предоставления более полной картины.
Используя контекст, CLR-среда обеспечивает надлежащую и согласованную
обработку объектов, которые предъявляют специальные требования к этапу выполнения.
Она перехватывает вызовы методов, производимых внутри и за пределами
конкретного контекста. Этот уровень перехвата позволяет CLR-среде подстраивать текущий
вызов метода так, чтобы он соответствовал контекстным настройкам конкретного
объекта. Например, в случае определения на С# класса, требующего автоматическо-
Глава 16. Процессы, домены приложений и контексты объектов 603
го обеспечения безопасности в отношении потоков (за счет использования атрибута
[Synchronization]), CLR-среда будет создавать во время его размещения так
называемый "синхронизированный контекст".
Точно так же, как для каждого процесса создается свой используемый по
умолчанию домен приложения, для каждого домена приложения создается применяемый по
умолчанию контекст. Этот контекст (иногда еще называемый нулевым из-за того, что он
всегда создается в любом домене приложения первым) применяется для группирования
вместе объектов .NET, которые не обладают никакими специфическими или
уникальными контекстными потребностями. Как не трудно догадаться, подавляющее
большинство объектов .NET загружается именно в нулевой контекст. В случае если CLR-среда
определяет, что у создаваемого нового объекта имеются специальные потребности, она
создает внутри отвечающего за его обслуживание домена приложения новые границы
контекста. На рис. 16.3 схематично показаны отношения между процессом, доменами
и контекстами.
Процесс.NET
Домен приложения
используемый
по умолчанию
Контекст
по умолчанию
Контекст 1
Контекст 2
'
Домен приложения
AppDomainl
Контекст
по умолчанию
Контекст 1
Контекст 2
Домен приложения
AppDomain2
Контекст
по умолчанию
Рис. 16.3. Взаимоотношения между процессами, доменами приложений
и контекстными границами
Контекстно-свободные и контекстно-зависимые типы
Объекты .NET, которые не требуют никакого особого контекстного сопровождения,
называются контекстно-свободными (context-agile). К таким объектам доступ может
быть получен из любого места внутри обслуживающего домена приложения без
нарушения их требований времени выполнения. Создание контекстно-свободных объектов
не требует приложения никаких особых усилий, поскольку при этом вообще ничего не
нужно делать (ни снабжать их какими-то контекстными атрибутами, ни наследовать от
базового класса System.ContextBoundObject):
// Контекстно-свободный объект загружается в нулевой контекст.
class SportsCar { }
С другой стороны, объекты, которые действительно требуют выделения отдельного
контекста, называются контекстно-зависимыми (context-bound). Они должны
обязательно наследоваться от базового класса System.ContextBoundObject. Этот базовый
класс отражает тот факт, что интересующий объект может функционировать
надлежащим образом только в рамках контекста, в котором он был создан. Из-за роли контекста
в .NET должно стать совершенно понятно, что в случае попадания контекстно-зависи-
604 Часть IV. Программирование с использованием сборок .NET
мого объекта каким-то образом в несоответствующий контекст, обязательно произойдет
что-то плохое, причем в самый неподходящий момент.
Помимо наследования от System. ContextBoundObject, контекстно-зависимые
объекты должны обязательно сопровождаться специальными атрибутами .NET, которые
называются контекстными атрибутами. Все контекстные атрибуты наследуются от
базового класса ContextAttribute. Теперь давайте рассмотрим конкретный пример.
Определение контекстно-зависимого объекта
Предположим, что требуется определить класс (SportsCarTS), который
автоматически обеспечивает безопасность в отношении потоков, даже без помещения внутрь
реализации его членов жестко закодированной логики синхронизации потоков. Чтобы
получить такой класс, необходимо просто унаследовать его от ContextBoundObject и
применить к нему атрибут [Synchronization], как показано ниже.
using System.Runtime.Remoting.Contexts;
// Этот контекстно-зависимый тип будет загружаться только
// в синхронизированный (т.е. безопасный к потокам) контекст.
[Synchronization]
public class SportsCarTS : ContextBoundObject
{}
Типы, которые сопровождаются атрибутом [Synchronization], всегда загружаются
в безопасный к потокам контекст. Зная о специальных контекстных потребностях
класса MyThreadSafeObject, теперь представим, какие проблемы будут возникать в случае
перемещения соответствующего объекта из синхронизированного контекста в не
синхронизированный. Объект сразу же перестанет быть безопасным к потокам и,
следовательно, станет кандидатом на массовое повреждение данных, поскольку доступ к нему
будут пытаться получить одновременно множество потоков. Наследование SportsCarTS
от ContextBoundObject дает гарантию, что CLR-среда никогда не будет пытаться
перемещать объекты SportsCarTS за пределы синхронизированного контекста.
Инспектирование контекста объекта
Хотя необходимость в программном взаимодействии с контекстом возникает в
приложениях очень редко, давайте все-таки рассмотрим один показательный
пример, как это делается. Создадим новый проект типа Console Application по имени
ObjectContextApp и определим в нем контекстно-свободный (SportsCar) и контекстно-
зависимый (SportsCarTS) класс:
using System;
using System.Runtime.Remoting.Contexts; // Для типа Context.
using System.Threading; // Для типа Thread.
// SportsCar не имеет никаких специальных контекстных
// потребностей и будет загружаться в создаваемый по
// умолчанию контекст внутри домена приложения.
class SportsCar
{
public SportsCar()
{
// Получение информации о контексте
//и вывод его идентификатора.
Context ctx = Thread.CurrentContext;
Console.WriteLine("{0} object in context {1}",
this.ToString(), ctx.ContextID);
Глава 16. Процессы, домены приложений и контексты объектов 605
foreach(IContextProperty ltfCtxProp in ctx.ContextProperties)
Console.WriteLine ("-> Ctx Prop: {0}", itfCtxProp.Name);
}
// Тип SportsCarTS требует загрузки
//в синхронизированный контекст.
[Synchronization]
class SportsCarTS : ContextBoundObject
{
public SportsCarTS ()
{
// Получение информации о контексте
//и вывод его идентификатора.
Context ctx = Thread.CurrentContext;
Console.WriteLine ("{0} object in context {1}",
this.ToString (), ctx.ContextID);
foreach(IContextProperty itfCtxProp in ctx.ContextProperties)
Console.WriteLine ("-> Ctx Prop: {0}", itfCtxProp.Name);
}
}
Обратите внимание, что каждый конструктор получает объект Context из
текущего потока выполнения посредством статического свойства Thread. CurrentContext.
Используя объект Context, легко отображать статистические данные о контексте, такие
как назначенный ему идентификатор, а также набор дескрипторов, получаемых через
свойство Context. ContextProperties. Это свойство возвращает объект, реализующий
интерфейс IContextProperty, который предоставляет доступ к каждому дескриптору
через свойство Name. Добавим в метод Main () код для размещения экземпляра каждого
из этих классов в памяти.
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Object Context *****\n");
// При создании этих объектов будет отображаться
// информация об их контексте.
SportsCar sport = new SportsCar ();
Console.WriteLine();
SportsCar sport2 = new SportsCar ();
Console.WriteLine();
SportsCarTS synchroSport = new SportsCarTS ();
Console.ReadLine ();
}
По мере создания объектов конструкторы классов будут выводить в окне консоли
различные фрагменты касающейся их контекста информации (выводимое свойство
LeaseLifeTimeServiceProperty представляет собой низкоуровневую деталь уровня
удаленной обработки .NET, поэтому на него можно не обращать внимания).
***** Fun with Object Context *****
ObjectContextApp.SportsCar object in context 0
-> Ctx Prop: LeaseLifeTimeServiceProperty
ObjectContextApp.SportsCar object in context 0
-> Ctx Prop: LeaseLifeTimeServicePropertfy
ObjectContextApp.SportsCarTS object in context 1
-> Ctx Prop: LeaseLifeTimeServiceProperty
-> Ctx Prop: Synchronization
606 Часть IV. Программирование с использованием сборок .NET
Из-за того, что класс SportsCar не был снабжен атрибутом контекста, CLR-среда
разместила объекты sport и sport2 в контексте с идентификатором 0 (т.е. в контексте
по умолчанию). Однако объект SportsCarTS загружен в уникальный контекст (с
идентификатором 1), поскольку его контекстно-зависимый класс был снабжен атрибутом
[Synchronization].
Исходный код. Проект ObjectContextApp доступен в подкаталоге Chapter 16.
Итоговые сведения о процессах,
доменах приложений и контекстах
К этому моменту должно стало гораздо понятнее, каким образом сборка .NET
обслуживается в CLR-среде. Ниже для удобства перечислены ключевые моменты, которые
следует вынести из всего предыдущего материала.
• В каждом процессе .NET может обслуживаться один и более доменов приложений.
В каждом из этих доменов приложения, в свою очередь, может обслуживаться
любое количество взаимосвязанных сборок .NET. Все домены приложений могут по
отдельности загружаться и выгружаться CLR-средой (или программным образом
с помощью класса System.AppDomain).
• Любой отдельно взятый домен приложения может включать в себя один и более
контекстов. За счет применения контекста CLR-среде удается помещать
информацию об "особых потребностях" объекта в логический контейнер и тем самым
гарантировать принятие их во внимание на этапе выполнения.
Если изложенный в настоящей главе материал показался слишком сложным, не
стоит переживать. По большей части исполняющая среда .NET способна самостоятельно
разбираться в деталях процессов, доменов приложений и контекстов. С другой стороны,
приведенные сведения являются хорошей основой для понимания способов разработки
многопоточных приложений в рамках платформы .NET.
Резюме
Главной задачей настоящей главы было показать, как обслуживаются
исполняемые сборки .NET. Давно уже существующее понятие процесса Windows было
внутренне изменено и адаптировано под потребности CLR. Любой отдельно взятый
процесс (которым на программном уровне можно управлять за счет применения типа
System. Diagnostics . Process) теперь включает в себя один или более доменов
приложений, которые представляют собой изолированные и независимые границы внутри
данного процесса.
Кроме того, вы узнали, что в каждом процессе может обслуживаться несколько
доменов приложений, в каждом из которых, в свою очередь, может обслуживаться и
выполнятся любое количество связанных между собой сборок. Более того, в каждом
домене приложения также может содержаться любое количество контекстов. Благодаря
применению такого дополнительного уровня изоляции типов, CLR-среда обеспечивает
надлежащую обработку объектов с особыми потребностями на этапе выполнения.
ГЛАВА 17
Язык CIL и роль
динамических сборок
При разработке полнофункционального приложения .NET наверняка будет
применяться язык С# (или подобный управляемый язык вроде Visual Basic), из-за
присущей ему продуктивности и удобства в использовании. Как рассказывалось в самой
первой главе, роль управляемого компилятора состоит в преобразовании файлов кода
* . cs в инструкции на языке CIL, метаданные типов и манифест сборки. CIL является
полнофункциональным языком программирования .NET, который обладает
собственным синтаксисом, семантикой и компилятором (ilasm. ехе).
В настоящей главе предлагается краткий экскурс по этому родному языку .NET.
Здесь будет показано, в чем состоят различия между директивой, атрибутом и кодом
операции в CIL, а также роль прямого и обратного проектирования сборки .NET и
различных инструментов программирования на CIL. Также будет показано, как определять
пространства имен, типы и члены с использованием грамматики CIL. В завершение
главы рассматривается роль пространства имен System.Ref lection .Emit и реализация
(с помощью инструкций CIL) динамического создания сборок во время выполнения.
Разумеется, необходимость иметь дело непосредственно с кодом на CIL в
повседневной работе будет возникать только у очень немногих программистов. Глава начинается
с описания причин, по которым изучение синтаксиса и семантики этого
низкоуровневого языка .NET может оказаться полезным.
Причины для изучения грамматики языка CIL
Язык CIL является самым настоящим "родным" языком для платформы .NET. При
создании .NET-сборки с помощью того или иного языка (С#, VB, COBOL.NET и т.д.)
соответствующий компилятор всегда преобразует исходный код на этом языке в код на
CIL. Как и в любом другом языке программирования, в CIL поддерживается множество
связанных со структурированием и реализацией лексем. Поскольку CIL представляет
собой просто еще один язык программирования .NET, не должен удивлять тот факт, что
сборки .NET можно создавать непосредственно на CIL и компилировать их с помощью
компилятора ilasm. ехе, который входит в состав .NET Framework 4.0 SDK.
Хотя желание построить все приложение .NET непосредственно на CIL
действительно будет возникать у очень немногих, все равно этот язык является чрезвычайно
интересным объектом для изучения. Хорошие знания грамматики языка CIL позволяют
совершенствовать приемы разработки .NET-приложений. Разработчики, разбирающиеся
в CIL, способны делать следующее.
608 Часть IV. Программирование с использованием сборок .NET
• Точно знать, на какие лексемы в CIL отображаются ключевые слова из различных
языков программирования .NET.
• Дизассемблировать существующие .NET-сборки, редактировать лежащий в их
основе CIL-код и заново компилировать обновленную кодовую базу в новый
двоичный файл .NET. Например, некоторые сценарии могут требовать внесения
изменений в CIL-код для взаимодействия с расширенными средствами СОМ.
• Строить динамические сборки с использованием пространства имен System.
Reflection .Emit. Этот API-интерфейс позволяет генерировать сборку .NET в
памяти, которая впоследствии может быть сохранена на диске.
• Использовать такие возможности CTS (Common Type System — общая система
типов), которые в управляемых языках более высокого уровня не поддерживаются,
а на уровне CIL действительно доступны. На самом деле CIL является
единственным языком .NET, который позволяет получать доступ ко всем возможностям CTS.
Например, используя чистый код CIL, можно создавать определения глобальных
членов и полей (чего в С# делать не разрешено).
Следует еще раз подчеркнуть, что овладеть навыками работы с С# и
библиотеками базовых классов .NET можно и без изучения деталей CIL-кода. Во многих
отношениях знание языка CIL для программиста, работающего с .NET, аналогично знанию
языка ассемблера для программиста, работающего на С (C++). Те, кто разбирается в
низкоуровневых деталях, способны создавать более совершенные решения для
существующих задач и лучше понимают, как работает базовая среда программирования
(и выполнения). Поэтому всем, кому интересно, предлагаем приступить к изучению
основных аспектов CIL.
На заметку! Важно отметить, что всестороннее и исчерпывающее описание синтаксиса и семантики
CIL в настоящей главе не предлагается. Полная информация по данной теме приведена в
официальной спецификации ЕСМА (ecma-335.pdf), доступной на веб-сайте ЕСМА International
по адресу http://www.ecma-international.org.
Директивы, атрибуты и коды операций в CIL
В начале изучения любого низкоуровневого языка вроде CIL обязательно
встречаются новые (и часто пугающие) названия для хорошо знакомых понятий. Например, на
текущий момент в данной книге приведенный ниже список элементов:
{new, public, this, base, get, set, explicit, unsafe, enum, operator, partial}
почти наверняка покажется набором ключевых слов языка С# (и это правильно). Однако
внимательней присмотревшись к элементам в этом списке, можно заметить, что хотя
каждый из них действительно представляет собой ключевое слово С#, их семантика
радикально отличается. Например, ключевое слово enum позволяет определять
производный от System. Enum тип, а ключевые слова this и base — ссылаться, соответственно,
на текущий объект и его родительский класс. Ключевое слово unsafe используется для
создания блока кода, который не должен подвергаться непосредственному мониторингу
со стороны CLR-среды, а ключевое слово operator —для создания скрытого (имеющего
специальное имя) метода, который должен вызываться при применении какой-то
операции С# (например, знака "плюс").
В отличие от такого высокоуровневого языка, как С#, в CIL не поставляется простой
общий набор ключевых слов. Вместо этого набор лексем, распознаваемых
компилятором CIL, семантически разделен на три следующих основных категории:
Глава 17. Язык CIL и роль динамических сборок 609
• директивы CIL;
• атрибуты CIL;
• коды операций CIL.
Лексемы CIL каждой из этих категорий представляются с помощью определенного
синтаксиса и комбинируются для получения полноценной .NET-сборки.
Роль директив CIL
Прежде всего, в CIL имеется ряд хорошо известных лексем, которые применяются
для описания общей структуры .NET-сборки. Эти лексемы называются директивами.
Директивы CIL позволяют информировать компилятор CIL о том, каким образом
должны определяться пространства имен, типы и члены, входящие в состав сборки.
Синтаксически директивы представляются с использованием префикса в виде точки
(.), например, .namespace, .class, .publickeytoken, .override, .method, .assembly
и т.д. Следовательно, если в файле с расширением * .il (принятое по соглашению
расширение для файлов CIL-кода) есть одна директива .namespace и три директивы
.class, компилятор CIL будет генерировать сборку, в которой определено единственное
пространство имен, содержащее три типа классов .NET.
Роль атрибутов CIL
Во многих случаях сами по себе директивы CIL оказываются недостаточно
описательными для того, чтобы полностью отражать определение того или иного типа или
члена типа .NET. В таких случаях они могут сопровождаться различными
атрибутами CIL, которые уточняют то, каким образом они должны обрабатываться. Например,
директива .class может сопровождаться атрибутами public (уточняющим видимость
типа), extends (явно указывающим базовый класс типа) и implements (позволяющим
перечислить интерфейсы, поддерживаемые данным типом).
На заметку! Не путайте совершенно разные понятия "атрибут CIL" и "атрибут .NET" (см. главу 15).
Роль кодов операций CIL
После определения сборки, пространства имен и набора типов на CIL с помощью
различных директив и соответствующих атрибутов, последнее, что останется сделать —
это предоставить для каждого из типов логику реализации. Для решения этой задачи в
CIL поддерживаются так называемые коды операций (operation codes — opcodes). Как и
в других низкоуровневых языках программирования, коды операций в CIL обычно
имеют непонятный и нечитабельный вид. Например, для загрузки в память переменной
string в CIL должен применяться код операции не с удобным для восприятия именем
вроде LoadString, а со сложным для произношения именем ldstr.
Некоторые коды операций в CIL отображаются вполне естественным образом на
соответствующие аналоги в С# (например, box, unbox, throw и sizeof). Как будет
показано далее в главе, коды операций в CIL всегда применяются только в рамках реализации
членов и, в отличие от директив, никогда не сопровождаются префиксом в виде точки.
Разница между кодами операций и их
мнемоническими эквивалентами в CIL
Как было только что сказано, коды операций вроде ldstr применяются для
реализации членов конкретного типа. Однако на самом деле лексемы, подобные ldstr, явля-
610 Часть IV. Программирование с использованием сборок .NET
ются мнемоническими эквивалентами (mnemonics) самих двоичных кодов операций в
CIL. Чтобы увидеть, в чем состоит отличие, давайте представим, что был написан
следующий метод на С#:
static int Add(int x, int у)
{
return x + у;
}
Код операции сложения двух чисел в CIL выглядит как 0X58. Аналогично, кодом
операции вычитания является 0X59, а кодом операции размещения нового
объекта в управляемой куче — 0X7 3. Получается, что "код CIL', который обрабатывается
JIT-компилятором, фактически принимает вид просто больших объектов двоичных
данных.
К счастью, для каждого двоичного кода операции в CIL существует соответствующий
мнемонический эквивалент. Например, вместо кода 0X58 может использоваться его
мнемонический эквивалент add, вместо кода 0X59 — эквивалент sub, а вместо 0X73 —
newobj. Декомпиляторы CIL вроде ildasm.exe будут всегда преобразовывать все
присутствующие в сборке двоичные коды операций в соответствующие им мнемонические
эквиваленты. Например, ниже показано, как ildasm.exe преобразует приведенный
выше метод Add () на С# в CIL:
.method privatehidebysig static int32 Add(int32 x,
int32 y) cil managed
{
// Code size 9 @x9)
// Размер кода 9 @x9)
.maxstack 2
.locals init ([0] int32 CS$1$0000)
IL_0000
IL_0001
IL 0002
IL_0003
IL_0004
fIL_0005
IL_0007
IL 0008
nop
ldarg.O
ldarg.1
add
stloc.O
br.s IL_0007
ldloc.O
ret
Разумеется, у тех, кто не занимается разработкой низкоуровневого
программного обеспечения .NET (такого как специальные управляемые компиляторы), никогда не
возникает необходимость иметь дело с числовыми двоичными кодами операций CIL.
Обычно, когда программисты приложений .NET говорят о "кодах операций CILT, они
подразумевают их удобные для восприятия, строковые мнемонические эквиваленты, а
не базовые числовые значения (так делается и в книге).
Помещение и извлечение данных из стека в CIL
В языках .NET более высокого уровня (вроде С#) низкоуровневые детали CIL обычно
насколько возможно скрываются из виду. Одной из особенно хорошо скрываемых
деталей является тот факт, что CIL на самом деле является языком программирования,
основанным на использовании стека. При рассмотрении связанных с коллекциями
пространств имен (в главе 10) уже рассказывалось о том, что существует класс Stack<T>,
который может применяться для помещения значения в стек, а также извлечения
самого верхнего значения из стека для последующего использования. Конечно, разработчики
на CIL не используют в буквальном смысле объект типа Stack<T> для загрузки и
выгрузки вычисляемых значений, но применяют стиль помещения и извлечения из стека.
Глава 17. Язык CIL и роль динамических сборок 611
Формально сущность, используемая для хранения набора вычисляемых значений,
называется виртуальным стеком выполнения (virtual execution stack). Далее в главе
будет показано, что в CIL поддерживается набор таких кодов операций, которые
могут применяться для помещения значения в стек; процесс помещения значения в стек
называется загрузкой. Также в CIL поддерживаются дополнительные коды операций,
служащих для перемещения самого верхнего значения из стека в память (например, в
локальную переменную); процесс перемещения значения из стека в память называется
сохранением.
В мире CIL получать доступ к элементам данных, в том числе к определенным
локально переменным, входным аргументам методов и данным полей типа, не разрешено.
Вместо этого требуется явно загружать их в стек и затем извлекать их оттуда для
последующего использования (очень важно запомнить это, поскольку именно оно позволит
понять, почему некоторые блоки кода CIL выглядят несколько громоздко).
На заметку! Вспомните, что CIL-код не выполняется напрямую, а компилируется по требованию. Во
время компиляции CIL-кода многие излишние детали реализации оптимизируются. Более того,
если включена опция оптимизации кода для текущего проекта (на вкладке Build (Сборка) окна
свойств проекта в Visual Studio), компилятор будет еще и удалять излишние детали CIL
Чтобы увидеть, каким образом в CIL функционирует основанная на использовании
стека модель обработки, давайте создадим на С# простой метод PrintMessage (), не
принимающий аргументов и возвращающий void, и предусмотрим в его реализации
вывод в стандартный поток значения локальной переменной:
public void PrintMessage ()
{
string myMessage = "Hello.";
Console.WriteLine(myMessage);
}
CIL-код, в который компилятор С# преобразовал этот метод, содержит в методе
PrintMessage () директиву . locals, определяющую ячейку для хранения локальной
переменной. Локальная строка затем загружается и сохраняется в этой локальной
переменной с использованием кодов операций ldstr (загрузка строки) и stloc. О
(сохранение текущего значения в локальной переменной в ячейке 0).
После этого значение (по индексу 0) загружается в память с помощью кода
операции ldloc. О (загрузка локального аргумента с индексом 0) для использования в
вызове метода System. Console .WriteLine () (представленного кодом операции call).
И, наконец, код операции ret обеспечивает возврат из функции. Ниже показан полный
CIL-код, который компилятор С# генерирует для метода PrintMessage () (с
соответствующими комментариями).
.method public hidebysig instance void PrintMessage () cil managed
{
.maxstack 1
// Определение локальной строковой переменной (с индексом 0) .
.locals init ([0] string myMessage)
// Загрузка в стек строки со значением Hello.
ldstr "Hello."
// Сохранение строкового значения из стека в локальной переменной.
stloc.О
// Загрузка значения с индексом 0.
ldloc.О
612 Часть IV. Программирование с использованием сборок .NET
// Вызов метода с текущим значением.
call void [mscorlib] System.Console : :WnteLine (string)
ret
}
На заметку! Как не трудно заметить, в CIL для комментариев поддерживается синтаксис в виде
двойной косой черты (а также синтаксис в виде /* . . . */). Как и компилятор С#, компилятор
CIL полностью игнорирует все комментарии.
Теперь, когда известно, что собой в общем представляют директивы, атрибуты и
коды операций в CIL, давайте приступим непосредственно к программированию на CIL,
начав с рассмотрения методики двунаправленного проектирования.
Двунаправленное проектирование
В главе 1 было показано, как использовать утилиту ildasm.exe для просмотра
генерируемого компилятором С# кода CIL. Эта утилита также позволяет сбрасывать CIL-
код, содержащийся внутри загруженной сборки, во внешний файл. Полученный
подобным образом CIL-код можно легко редактировать и затем компилировать с помощью
компилятора CIL (ilasm.exe).
На заметку! Вспомните, что для просмотра CIL-кода любой отдельно взятой сборки, а также его
преобразования в примерную кодовую базу на С# также можно применять утилиту reflector.exe.
Формально такой подход называется двунаправленным проектированием (round-
trip engineering) и может оказываться полезным в перечисленных ниже ситуациях.
• Необходимость изменить сборку, исходный код которой больше не доступен.
• Необходимость изменить неэффективный (или предельно некорректный) CIL-код,
полученный в результате использования далекого от идеала компилятора языка
.NET.
• Необходимость при создании сборок, взаимодействующих с СОМ, вернуть
некоторые из СОМ-атрибутов на языке IDL (Interface Definition Language — язык
описания интерфейсов), которые были утрачены в процессе преобразования (вроде
СОМ-атрибута [helpstring]).
Чтобы попробовать процесс двунаправленного проектирования на практике,
создадим в простом текстовом редакторе новый файл кода С# (HelloProgram.cs) и
определим в нем показанный ниже класс (при желании можно создать проект Console
Application в Visual Studio 2010 и удалить из него файл Assemblylnfo. cs, уменьшив
объем генерируемого CIL-кода).
// Простое консольное приложение на языке С#.
using System;
// Обратите внимание, что для упрощения генерируемого CIL-кода
// класс не помещается ни в какое пространство имен.
class Program
{
static void Main(string [ ] args)
{
Console.WriteLine("Hello CIL code1");
Console.ReadLine ();
}
}
Глава 17. Язык CIL и роль динамических сборок 613
Сохраним этот файл в удобном месте (например, в каталоге С: \RoundTrip) и
скомпилируем программу с помощью esc . ехе:
esc HelloProgram.cs
Откроем полученный в результате файл HelloProgram.exe в утилите ildasm.exe
и выберем в меню File (Файл) пункт Dump (Сбросить), чтобы сохранить CIL-код в новом
файле с расширением * . il (HelloProgram.il) в той же папке, где находится
скомпилированная сборка (не изменяя значений, предлагаемых по умолчанию в диалоговом
окне сохранения).
На заметку! При сбросе содержимого сборки в файл утилита ildasm.exe генерирует и файл
* . res. Файлы подобного рода можно спокойно игнорировать (и удалять), поскольку они
использоваться не будут.
Теперь можно просмотреть этот файл в любом текстовом редакторе. Ниже показано
его содержимое (для удобства представления формат был немного изменен, а также
добавлены соответствующие комментарии).
// Внешние сборки, на которые имеются ссылки в текущей сборке.
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 EO 89 )
.ver 4:0:0:0
}
// Текущая сборка.
.assembly HelloProgram
{
/**** Данные TargetFrameworkAttribute удалены для ясности! ****/
.hash algorithm 0x00008004
.ver 0:0:0:0
}
.module HelloProgram.exe
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003
.corflags 0x00000003
// Определение класса Program.
.class private auto ansi beforefleldinit Program
extends [mscorlib]System.Object
{
.method private hidebysig static void Main (string[] args)
cil managed
{
// Назначение данного метода входной точкой
//в этой исполняемой сборке.
.entrypoint
.maxstack 8
IL_0000: nop
IL_0001: ldstr "Hello CIL code!"
IL_0006: call void [mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: call string [mscorlib]System.Console::ReadLine()
IL_0011: pop
IL 0012: ret
614 Часть IV. Программирование с использованием сборок .NET
// Конструктор по умолчанию.
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
.maxstack 8
IL_0000
IL_0001
IL 0006
ldarg.O
call instance void [mscorlib]System.Object::.ctor()
ret
Прежде всего, обратите внимание, что файл * . il начинается с объявления всех
внешних сборок, на которые ссылается текущая скомпилированная сборка. В данном
случае присутствует только одна лексема .assembly extern, которая указывает на
всегда добавляемую внешнюю сборку mscorlib. dll. Разумеется, если бы в библиотеке
классов использовались типы из каких-то других сборок, здесь бы присутствовали и
другие директивы .assembly extern.
Далее идет формальное определение самой сборки HelloProgram.exe, с
назначенным по умолчанию номером версии 0.0.0.0 (поскольку никакой номер версии с
помощью атрибута [Assembly Vers ion] не был указан). Это определение сопровождается
несколькими директивами CIL (.module, . imagebase и т.д.).
После списка внешних сборок и определения текущей сборки идет определение типа
Program. Обратите внимание на наличие внутри директивы . class различных
атрибутов (многие из которых необязательны), таких как extends, который указывает базовый
класс для типа:
.class private auto ansi beforefleldinit Program
extends [mscorlib]System.Object
{ ... }
Большую часть остального CIL-кода занимает описание реализации конструктора,
который должен применяться для данного класса по умолчанию, и метода Main (); оба
они определяются (частично) с помощью директивы .method. После определения этих
членов с помощью соответствующих директив и атрибутов, они реализуются с
помощью различных кодов операций.
Очень важно запомнить, что при взаимодействии с типами .NET (такими как
System. Console) в CIL должно всегда использоваться полностью квалифицированное
имя типа. Более того, к этому полностью квалифицированному имени необходимо
всегда в качестве префикса присоединять дружественное имя сборки, в которой находится
его определение (в квадратных скобках). Например, рассмотрим следующую
реализацию метода Main () в CIL:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 8
IL_0000
IL_0001
IL_0006
IL_000b
IL_000c
IL_0011
IL 0012
nop
ldstr "Hello CIL code1"
call void [mscorlib] System.Console: :WnteLine (string)
nop
call string [mscorlib] System. Console : .-ReadLine ()
pop
ret
При реализации конструктора по умолчанию в CIL-коде используется еще одна
инструкция, связанная с загрузкой — ldarg. 0. В данном случае значение, загружаемое
Глава 17. Язык CIL и роль динамических сборок 615
в стек, представляет собой не специальную указанную нами переменную, а ссылку на
текущий объект (подробнее об этом — чуть позже). Обратите внимание, что в
конструкторе по умолчанию явным образом производится вызов конструктора базового класса,
которым в данном случае является хорошо знакомый класс System.Object.
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.O
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL 0006: ret
}
Роль меток в коде CIL
Наверняка вам бросилось в глаза, что каждая строка в коде реализации
сопровождается префиксом вида ILXXX: (IL0000:, IL0001: и т.д.). Префиксы такого вида
называются метками кода и могут иметь произвольный формат (главное, чтобы они не
дублировались в рамках одного и того же члена). При сбрасывании содержимого сборки
в файл утилита ildasm. ехе автоматически генерирует метки кода в формате ILXXX: в
соответствии с принятым для них соглашением об именовании. При желании их легко
изменить и сделать более описательными:
.method private hidebysig static void Main(string [ ] args) cil managed
{
.entrypoint
.maxstack 8
Nothing_l: nop
Load_String: ldstr "Hello CIL code!"
PrintToConsole: call void
[mscorlib]System.Console::WriteLine(string)
Nothing_2: nop
WaitFor_KeyPress : call string
[mscorlib]System.Console::ReadLine ()
RemoveValueFromStack: pop
Leave_Function: ret
}
В действительности метки кода по большей части совершенно не обязательны.
Единственный случай, когда их действительно требуется применять — при создании
CIL-кода с различными конструкциями ветвления или циклов, поскольку они
позволяют указать, куда должен быть направлен поток логики. В текущем примере можно
вообще удалить все эти автоматически сгенерированные метки безо всяких последствий:
.method private hidebysig static void Main(string [ ] args) cil managed
{
.entrypoint
.maxstack 8
nop
ldstr "Hello CIL code!"
call void [mscorlib]System.Console::WriteLine(string)
nop
call string [mscorlib]System.Console::ReadLine()
pop
ret
}
616 Часть IV. Программирование с использованием сборок .NET
Взаимодействие с CIL: модификация файла *. il
Теперь, когда стало более понятно, как изнутри выглядит типичный файл CIL-кода,
давайте завершим эксперимент с двунаправленным проектированием. Для этого
предположим, что требуется обновить CIL-код в существующем файле * .il, следующим
образом:
• добавить ссылку на сборку System. Windows . Forms . dll;
• загрузить локальную строку в методе Main ();
• вызвать метод System. Windows . Forms .MessageBox. Show (), принимающий
локальную строковую переменную в качестве аргумента.
В первую очередь понадобится добавить новую директиву .assembly (с атрибутом
extern), указывающую на то, что нашей сборке требуется сборка System. Windows .
Forms .dll. Для этого достаточно вставить в файл * . il сразу же после ссылки на
внешнюю сборку ms со г lib следующий код:
.assembly extern System.Windows.Forms
{
.publickeytoken = (B7 7A 5C 56 19 34 EO 89)
.ver 4:0:0:0
}
Значение, присваиваемое директиве .ver, зависит о установленной версия
платформы .NET. Здесь используемая сборка System.Windows.Forms.dll относится к версии
4.0.0.0 и имеет значение открытого ключа В77А5С5 61934Е08 9. Открыв кэш GAC (см.
главу 14) и отыскав там версию сборки System. Windows . Forms . dll, можно подставить
нужный номер версии и значение открытого ключа.
Теперь необходимо изменить текущую реализацию метода Main (). Для этого следует
найти этот метод внутри файла * . i 1 и удалить текущий код его реализации (оставив
только директивы .maxstackH .entrypoint):
.method private hidebysig static void Main(string [ ] args) cil managed
{
.entrypoint
.maxstack 8
// Здесь следует написать новый CIL-код.
}
Требуется поместить новую переменную string в стек и вызвать метод MessageBox.
Show () (вместо Console. WriteLine ()). Вспомните, что при указании имени внешнего
типа должно использоваться его полностью квалифицированное имя (вместе с
дружественным именем сборки). Обратите внимание, что в CIL при вызове каждого метода
необходимо обязательно полностью указывать возвращаемый тип. В результате метод
Main () должен приобрести следующий вид:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 8
ldstr "CIL is way cool"
call valuetype [System.Windows.Forms]
System.Windows.Forms.DialogResult
[System.Windows.Forms]
System.Windows.Forms.MessageBox::Show(string)
pop
ret
}
Глава 17. Язык CIL и роль динамических сборок 617
По сути, CIL-код модифицирован так, что он соответствует такому определению
класса С#:
public class Program
{
static void Main(string [ ] args)
{
System.Windows.Forms.MessageBox.Show("CIL is way cool");
}
}
Компиляция CIL-кода с помощью ilasm. exe
После сохранения измененного файла * . i 1 можно приступать к компиляции новой
сборки .NET за счет применения утилиты ilasm.exe (компилятора CIL). Эта утилита
поддерживает множество опций командной строки (для их просмотра укажите при
запуске опцию -?), наиболее интересные из которых описаны в табл. 17.1.
Таблица 17.1. Опции командной строки, наиболее часто применяемые
при работе с утилитой ilasm. exe
Опция Описание
/debug Включение отладочной информации (такую как имена локальных переменных и
аргументов, а также номера строк)
/dll Генерация в качестве вывода файла * . dll
/exe Генерация в качестве вывода файлТ * . ехе. Используется по умолчанию, поэтому
может быть опущена
/key Компиляция сборки со строгим именем с использованием заданного файла * . snk
/output Имя и расширение выходного файла. Если флаг /output не указан,
результирующее имя файла (без расширения) совпадает с именем первого исходного файла
Чтобы скомпилировать измененный файл HelloProgram.il в новую .NET-сборку
*.ехе, необходимо ввести в окне командной строки Visual Studio 2010 следующую
команду:
ilasm /exe HelloProgram.il /output=NewAssembly.ехе
Если все прошло успешно, на экране должен быть следующий отчет:
Microsoft (R) .NET Framework IL Assembler. Version 4.0.21006.1
Copyright (c) Microsoft Corporation. All rights reserved.
Assembling 'HelloProgram.il' to EXE --> 'NewAssembly.exe'
Source file is UTF-8
Assembled method Program::Main
Assembled method Program::.ctor
Creating PE file
Emitting classes:
Class 1: Program
Emitting fields and methods:
Global
Class 1 Methods: 2;
Emitting events and properties:
Global
Class 1
Writing PE file
Operation completed successfully
618 Часть IV. Программирование с использованием сборок .NET
После этого новое приложение можно запустить. Вполне очевидно, что теперь
сообщение будет отображаться не в окне консоли, а в отдельном окне сообщения. Хотя
вывод этого простого примера не является столь уж впечатлякэщим, один из
практических способов применения двунаправленного программирования на языке CIL он
действительно иллюстрирует.
Создание CIL-кода с помощью SharpDevelop
В главе 2 кратко рассказывалось о такой распространяемой бесплатно IDE-среде,
как SharpDevelop (http: //www.sharpdevelop.com). Выбрав в меню File (Файл) пункт
New Solution (Новое решение)) в этой среде, вдобавок к шаблонам проектов на языках С#
и VB можно увидеть также и проект CIL (рис. 17.1).
New Project
Categories:
;-"Ш BOO
ч Q С»
!■ ;_>■ ILAam
_j Python
[ CM Setup
■ £i SharpDevelop
B-fli VBNet
A project that creates a command Ine apptcation.
Name FunWthCJLCode
Location
C:\L^NAi<^Trod^VDo(xrr^s\ihaipDe^ Prqjed»
SoJutton Name: f^vW^LCode Create directory for solution
Project wl be created at CA...\AndrewTroetoenMDocurnert3\Sh.wpDevelop
; Create II, Cancel |
Рис. 17.1. Шаблон проекта CIL в IDE-среде SharpDevelop
Хотя поддержка средства IntelliSense для проектов CIL в SharpDevelop не
предусмотрена, лексемы CIL здесь все равно снабжаются цветовой кодировкой, а приложение
можно компилировать и запускать непосредственно в IDE-среде (вместо запуска
утилиты ilasm.exe в командной строке). При создании проекта CIL в SharpDevelop
первоначально предоставляется файл * . il, показанный на рис. 17.2.
/ MamCtaseJ |_
11
3
1
■
!~
9
1С
11
12
;...
3 4
1 •
17
< :
.assembly HelloWorld
{
}
.namespace FunWithCILCode
{
-class private auto ansi beforefieldinit MainClass
{
.method public hidebysig static void Main(stringГ1 args) cil managed
{
•entrypoint
.maxstack 1
ldstr "Hello World!"
call void [mscorlibjSystem.Console: :WriteLinei,'string)
ret
}
}
>
_^_ Z» -...- ит -^.- I .
- X
*i
■ 4
tit
:
,
►
Рис. 17.2. Редактор CIL-кода в IDE-среде SharpDevelop
Глава 17. Язык CIL и роль динамических сборок 619
При проработке примеров в остальной части главы рекомендуется применять для
создания CIL-кода среду SharpDevelop. Помимо выделения элементов кода цветом эта
IDE-среда позволяет быстро обнаруживать опечатки в коде посредством специального
окна Errors (Ошибки).
На заметку! В IDE-среде MonoDevelop, которая поставляется бесплатно и предназначена для
разработки приложений .NET на платформе Mono, также поддерживается шаблон проекта CIL.
Дополнительные сведения о MonoDevelop ищите в приложении Б.
Роль peverifу.ехе
При создании либо изменении сборок за счет редактирования CIL-кода
рекомендуется проверять, правильно ли оформлен скомпилированный двоичный образ согласно
требованиям .NET, с помощью утилиты командной строки peverify.exe:
peverifу NewAssembly.ехе
Эта утилита производит проверку корректности кодов операций CIL внутри
указанной сборки. Например, в рамках CIL-кода стек вычислений должен всегда опустошаться
перед выходом из функции. Даже если забыть извлечь оставшиеся значения,
компилятор ilasm. ехе сгенерирует сборку (поскольку компиляторы заботит только синтаксис].
С другой стороны, утилита peverif у. ехе контролирует правильность семантики, и
потому если стек не был очищен перед выходом из функции, она обязательно уведомит об
этом, тем самым позволяя выявить проблему до запуска кодовой базы.
Исходный код. Пример RoundTrip доступен в подкаталоге Chapter 17.
Использование директив и атрибутов в CIL
Теперь, когда было показано, как применять утилиты ildasm.exe и ilasm.exe для
выполнения двунаправленного проектирования, можно переходить к более
детальному изучению синтаксиса и семантики CIL. В следующих подразделах будет поэтапно
рассмотрен весь процесс создания специального пространства имен с набором
типов. Для простоты пока что логика реализации членов к этим типам не добавляется.
Разобравшись с созданием простых типов, можно переключить внимание на процесс
определения "реальных" членов с помощью кодов операций CIL.
Добавление ссылок на внешние сборки в CIL
Создадим с помощью предпочитаемого редактора новый файл по имени CilTypes . il.
Первым шагом в любом CIL-проекте является перечисление внешних сборок, которые
будут использоваться в текущей сборке. В рассматриваемом примере применяются
только типы из сборки mscorlib.dll. Следовательно, потребуется добавить ссылку
только на эту сборку. Для этого необходимо указать в новом файле директиву . assembly
с уточняющим атрибутом external. При добавлении ссылок на сборки, обладающие
строгими именами, наподобие mscorlib.dll, должны также указываться директивы
.publickeytoken и .ver.
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 EO 89 )
.ver 4:0:0:0
}
620 Часть IV. Программирование с использованием сборок .NET
На заметку! Строго говоря, явно добавлять ссылку на сборку ms cor lib. dll не обязательно,
поскольку ilasm.exe добавит ее автоматически. Для всех остальных внешних библиотек .NET,
требуемых в проекте CIL, обязательно должны быть предусмотрены соответствующие
директивы .assembly extern.
Определение текущей сборки в CIL
Следующий шаг состоит в определении интересующей сборки с использованием
директивы . assembly. В самом простом варианте при определении сборки можно
указывать только дружественное имя соответствующего двоичного файла:
// Наша сборка.
.assembly CILTypes { }
Хотя это вполне допустимое определение новой сборки .NET, обычно внутри такого
объявления размещаются дополнительные директивы. В рассматриваемом примере
определение сборки необходимо снабдить номером версии 1.0.0.0 с помощью директивы
.ver (обратите внимание, что числа в номере версии разделяются двоеточиями, а не
точками, как в С#):
// Наша сборка.
.assembly CILTypes
{
.ver 1:0:0:0
}
Из-за того, что CILTypes представляет собой однофайловую сборку, в конце
определения нужно добавить одну директиву .module и указать в ней официальное имя
двоичного файла .NET — CILTypes . dll:
.assembly CILTypes
{
.ver 1:0:0:0
}
// Модуль нашей однофайловой сборки,
.module CILTypes.dll
Помимо . assembly и .module, существуют и другие СIL-директивы, которые
позволяют дополнительно уточнять обитую структуру создаваемого двоичного файла .NET.
В табл. 17.2 перечислены некоторые наиболее часто применяемые из них.
Таблица 17.2. Дополнительные директивы, применяемые для определения сборки
Директива Описание
. mresources Если сборка должна использовать внутренние ресурсы (такие как растровые
изображения или таблицы строк), эта директива позволяет указать имя файла,
в котором содержатся ресурсы, подлежащие включению в сборку
. subsystem Эта CIL-директива служит для указания предпочитаемого пользовательского
интерфейса, внутри которого должна выполняться сборка. Например,
значение 2 указывает, что сборка должна выполняться в приложении с графическим
пользовательским интерфейсом, а значение 3 — в консольном приложении
Определение пространств имен в CIL
После определения внешнего вида и поведения сборки (и требуемых внешних
ссылок) можно переходить к созданию пространства имен .NET (MyNamespace), используя
директиву .namespace:
Глава 17. Язык CIL и роль динамических сборок 621
// Наша сборка имеет единственное пространство имен.
.namespace MyNamespace {}
Как и в С#, в языке CIL можно создавать корневые пространства имен. Хотя корневое
пространство имен в рассматриваемом примере, в принципе, не нужно, ради интереса
посмотрим, как определить корневое пространство имен под названием MyCompany:
.namespace MyCompany
{
.namespace MyNamespace {}
}
Подобно С#, в CIL можно определять вложенные пространства имен:
// Определение вложенного пространства имен.
.namespace MyCompany.MyNamespace{}
Определение типов классов в CIL
Пустые пространства имен не особо полезны, поэтому давайте посмотрим, как в CIL
определяются типы классов. Для определения нового типа класса в CIL применяется
директива .class. Эта простая директива может сопровождаться многочисленными
дополнительными атрибутами, позволяющими уточнить природу типа. Добавим в
рассматриваемое пространство имен общедоступный класс по имени MyBaseClass. Как и
в С#, если базовый класс явно не указан, тип будет автоматически наследоваться от
System.Object:
.namespace MyNamespace
{
// Предполагается, что базовым классом является System.Object.
.class public MyBaseClass {}
}
При создании типа класса, унаследованного не от System.Object, применяется
атрибут extends. Для ссылки на тип, определенный внутри той же самой сборки, в CIL
должно использоваться полностью квалифицированное имя (хотя если базовый тип
находится в пределах той же сборки, допускается не указывать префикс, представляющий
дружественное имя сборки). Таким образом, попытка расширить MyBaseClass так, как
показано ниже, приведет к ошибке на этапе компиляции:
// Этот код не компилируется'
.namespace MyNamespace
{
.class public MyBaseClass {}
.class public MyDerivedClass
extends MyBaseClass {}
}
Чтобы правильно определить родительский класс для MyDerivedClass, необходимо
указать полностью квалифицированное имя MyBaseClass:
// Правильный код.
.namespace MyNamespace
{
.class public MyBaseClass {}
.class public MyDerivedClass
extends MyNamespace.MyBaseClass {}
}
Помимо атрибутов public и extends, определение класса CIL может иметь множество
дополнительных спецификаторов, уточняющих видимость типа, компоновку полей и т.п.
622 Часть IV. Программирование с использованием сборок .NET
В табл. 17.3 описаны некоторые атрибуты, используемые вместе с директивой
.class.
Таблица 17.3. Некоторые атрибуты, которые могут использоваться
вместе с директивой . class
Атрибут Описание
public, private, Эти атрибуты применяются для указания степени видимости
nested assembly, типа. Как не трудно заметить, в CIL предлагается много других
nested f amandassem, возможностей помимо тех, что доступны в С#. За дополнитель-
nested family, ными сведениями обращайтесь в документ ЕСМА 335
nested famorassem,
nested public,
nested private
abstract, Эти два атрибута могут быть присоединены к директиве . class
sealed для определения, соответственно, абстрактного или
запечатанного класса
auto, Эти атрибуты позволяют указать CLR-среде, как данные полей
sequential, должны размещаться в памяти. Для типов классов подходящим
explicit является флаг auto, используемый по умолчанию. Его
изменение может быть полезно в случае использования P/lnvoke для
обращения к неуправляемому коду на С
extends, Эти атрибуты позволяют определять базовый класс для типа
implements (extends) и реализовать для него интерфейс (implements)
Определение и реализация интерфейсов в CIL
Как ни странно, но типы интерфейсов в CIL тоже определяются с использованием
директивы .class. Однако за счет ее сопровождения атрибутом interface тип
реализуется как один из типов интерфейсов CTS. После определения интерфейс может
привязываться к какому-нибудь типу класса или структуры с применением атрибута
implements.
.namespace MyNamespace
{
// Определение интерфейса.
.class public interface IMylnterface {}
//Простой базовый класс
.class public MyBaseClass {}
// Теперь MyDerivedClass реализует интерфейс
// IMylnterface и расширяет класс MyBaseCLass.
.class public MyDerivedClass
extends MyNamespace.MyBaseClass
implements MyNamespace.IMylnterface {}
На заметку! Конструкция extends должна обязательно предшествовать конструкции implements.
Кроме того, в конструкции implements может быть предоставлен разделенный запятыми
список интерфейсов.
Как рассказывалось в главе 9, интерфейсы могут выступать в роли базовых для
других типов интерфейсов, позволяя строить иерархии интерфейсов. Вопреки возможным
ожиданиям, однако, использовать атрибут extends для наследования интерфейса А от
Глава 17. Язык CIL и роль динамических сборок 623
интерфейса В в CIL нельзя. Этот атрибут разрешено применять только для указания
базового класса типа. Для расширения интерфейса должен использоваться атрибут
implements:
// Расширение интерфейсов в CIL.
.class public interface IMylnterface {}
.class public interface IMyOtherlnterface
implements MyNamespace.IMylnterface {}
Определение структур в CIL
Директива .class может использоваться для определения любой структуры CTS при
условии расширения ее типом System. ValueType. В этом случае она должна
обязательно сопровождаться атрибутом sealed (поскольку структуры никогда не могут выступать
в роли базовых для других типов-значений). В случае несоблюдения этого требования
ilasm.exe будет сообщать об ошибке на этапе компиляции.
// При определении структуры должен быть указан атрибут sealed.
.class public sealed MyStruct
extends [mscorlib]System.ValueType{}
В CIL также предусмотрен сокращенный вариант синтаксиса для определения типа
структуры. В случае использования атрибута value новый тип будет автоматически
наследоваться от типа [mscorlib] System. ValueType. Следовательно, тип MyStruct
можно было бы определить и так:
// Сокращенный вариант объявления структуры.
.class public value MyStructU
Определение перечислений в CIL
Перечисления .NET унаследованы от класса System.Enum, который, в свою
очередь, наследуется от System.ValueType (и потому должен обязательно
сопровождаться атрибутом sealed). Чтобы определить перечисление в CIL, необходимо расширить
[mscorlib] System.Enum показанным ниже образом:
// Определение перечисления.
.class public sealed MyEnum
extends [mscorlib]System.Enum{ }
Как и для структур, для перечислений тоже поддерживается сокращенный
синтаксис определения, предусматривающий использование атрибута enum:
// Сокращенный вариант определения перечисления.
.class public sealed enum MyEnum{}
Определение пар имен и значений в перечислении рассматривается чуть позже.
На заметку! Делегаты, которые являются еще одним фундаментальным типом в .NET, тоже имеют свое
специфическое представление в CIL За дополнительными деталями обращайтесь в главу 11.
Определение обобщений в CIL
Обобщенные типы также имеют свое представление в синтаксисе CIL. Вспомните
из главы 10, что каждый обобщенный тип или член может иметь один и более
параметров типа. Например, тип List<T> обладает одним единственным параметром, а
Dictionary<TKey/ TValue> — двумя. В CIL количество параметров типа указывается
с использованием символа обратной одиночной кавычки (ч) со следующим за ним
числом. Как и в С#, сами значения параметров типа заключаются в квадратные скобки.
624 Часть IV. Программирование с использованием сборок .NET
На заметку! На большинстве клавиатур символ ч находится на клавише, расположенной над
клавишей <ТаЬ> (слева от клавиши <1>).
Например, предположим, что необходимо создать переменную List<T>, где на месте
Т должно находиться значение типа System. Int32. В CIL это делается следующим
образом (в области действия любого метода CIL):
// В С#: List<int> mylnts = new List<int>() ;
newobj instance void class [mscorlib]
System.Collections.Generic.Listчl<int32>::.ctor()
Обратите внимание, что данный обобщенный класс определен как List4 Kint32>,
поскольку List<T> имеет только один параметр типа. А вот как можно определить тип
Dictionary<string/ int>:
// В С#: Dictionary<string, int> d = new Dictionary<string, int>();
newob] instance void class [mscorlib]
System.Collections.Generic.Dictionaryч2<string,int32>::.ctor()
В качестве еще одного примера предположим, что возникла необходимость создать
обобщенный тип, использующий в качестве параметра типа еще один обобщенный тип.
Соответствующий CIL-код выглядит следующим образом:
// В С#: List<List<int» mylnts = new List<List<int»() ;
newob] instance void class [mscorlib]
System.Collections.Generic.Listч Kclass
[mscorlib] System. Collections .Generic . List4Kint32>>: : . ctor ()
Компиляция файла ClLTypes. il
Несмотря на то что ни члены, ни код реализации в определенные ранее типы не
добавлялись, данный файл * . il уже можно компилировать в .NET-сборку с расширением
. dll (это является единственным доступным вариантом, поскольку метод Main () не
был определен). Для этого необходимо открыть окно командной строки и ввести
следующую команду для запуска утилиты ilasm.exe:
llasm /dll CilTypes.il
После выполнения этой команды можно загрузить полученный двоичный файл в
ildasm. exe и просмотреть его (рис. 17.3).
/7 CifTypes-dll - IL DASM
| File View Help
|в vHi
! ► MANIFEST
а Щ MyNamespace
ffi U My Namespace. IMy Interface
Ф £ My Namespace. MyBaseClass
ffi J£ MyNamespace.MyDerivedClass
А-Щ: MyNamespace.MyEnum
ffi {3 MyNamespace. MyGenericClassN 1<T>
*™"~"
.assembly ClLTypes
<
1 '
L
[ CZD j В Ы"—1 '
*
*
-
Рис. 17.3. Содержимое сборки ClLTypes . dll
Глава 17. Язык CIL и роль динамических сборок 625
Просмотрев содержимое сборки, можно запустить в отношении нее утилиту
peverify.ехе:
peverify CilTypes.dll
Обратите внимание, что это приведет к выдаче ряда сообщений об ошибках, так как
все типы совершенно пусты. Ниже показан частичный вывод:
Microsoft (R) .NET Framework PE Verifier. Version 4.0.21006.1
Copyright (с) Microsoft Corporation. All rights reserved.
[MD]: Error: Value class has neither fields nor size parameter.
[token:0x02000005]
[MD] : Ошибка: Класс значений не имеет ни полей, ни параметра размера.
[token:0x02000005]
[MD] : Error: Enum has no instance field, [token:0x02000006]
[MD] : Ошибка: Перечисление не имеет полей экземпляра, [token:0x02000006]
Перед тем как приступить к заполнению типа содержимым, необходимо сначала
узнать, какие типы данных поддерживаются в CIL.
Соответствия между типами данных
в библиотеке базовых классов .NET, C# и CIL
В табл. 17.4 показаны соответствия между ключевыми словами С# и базовыми
классами .NET, а также представления этих ключевых слов в CIL. Кроме того, для каждого
типа CIL приведены сокращенные константные обозначения. Именно на такие
константы часто ссылаются многие коды операций CIL.
Таблица 17.4. Соответствия между базовыми классами .NET,
ключевыми словами С# и их представлениями в CIL
Базовый класс .NET
System.SByte
System.Byte
System.Intl6
System.UIntl6
System.Int32
System.UInt32
System.Int64
System.UInt64
System.Char
System.Single
System.Double
System.Boolean
System.String
System.Object
System.Void
Ключевое
слово в C#
sbyte
byte
short
ushort
int
uint
long
ulong
char
float
double
bool
string
object
void
Представление
CIL
int8
unsigned
intl6
unsigned
int32
unsigned
int64
unsigned
char
float32
float64
bool
string
object
void
int8
intl6
int32
int64
Константная
нотация CIL
11
Ul
12
U2
14
U4
18
U8
CHAR
R4
R8
BOOLEAN
-
-
VOID
626 Часть IV. Программирование с использованием сборок .NET
На заметку! Типам System. IntPtr и System. UlntPtr соответствуют ключевые слова int и
unsigned int (это полезно знать, поскольку во многих сценариях взаимодействия с СОМ и
P/lnvoke они используются очень широко).
Определение членов типов в CIL
Как уже известно, типы в .NET могут иметь различные члены. Так, перечисления
могут иметь набор пар имен и значений, а структуры и классы — конструкторы, поля,
методы, свойства, статические члены и т.д. В предшествующих главах уже было
показано, как выглядят определения упомянутых элементов на CIL, но давайте еще раз кратко
рассмотрим, как различные члены отображаются на примитивы CIL.
Определение полей данных в CIL
Перечисления, структуры и классы поддерживают поля данных. В каждом случае
для их определения должна использоваться директива .field. Например, добавим в
перечисление MyEnum три пары имен и значений (обратите внимание, что значения
указываются в круглых скобках):
.class public sealed enum MyEnum{
.field public static literal valuetype
MyNamespace.MyEnum A = int32@)
.field public static literal valuetype
MyNamespace.MyEnum В = int32(l)
.field public static literal valuetype
MyNamespace.MyEnum С = int32B))
}
Здесь видно, что поля, размещаемые в рамках любого типа, который наследуется
от System.Enum, квалифицируются атрибутами static и literal. Как не трудно
догадаться, эти атрибуты указывают, что данные этих полей должны представлять собой
фиксированное значение, доступное только из самого типа (например, MyEnum. А).
На заметку! Значения, присваиваемые полям в перечислении, могут быть предоставлены в шест-
надцатеричном формате, и в этом случае сопровождаться префиксом Ох.
Конечно, если нужно определить какое-то поле данных внутри класса или
структуры, использовать только общедоступные статические литеральные данные вовсе не
обязательно. Например, можно было бы модифицировать класс MyBaseClass таким
образом, чтобы он поддерживал два приватных поля данных уровня экземпляра со
значениями по умолчанию:
.class public MyBaseClass
{
.field private string stringField = "hello!"
.field private int32 intField = mt32D2)
}
Как и в С#, поля данных в классе будут автоматически инициализироваться
соответствующими значениями по умолчанию. Чтобы предоставить пользователю объекта
возможность задавать свои значения во время создания приватных полей данных,
потребуется создать специальные конструкторы.
Глава 17. Язык CIL и роль динамических сборок 627
Определение конструкторов для типов в CIL
В CTS поддерживается создание конструкторов, действующих как на уровне всего
экземпляра, так и на уровне конкретного класса (статических конструкторов). В CIL
первые представляются с помощью лексемы . с tor, а вторые — посредством лексемы
. cctor (class constructor — конструктор класса). Обе лексемы в CIL должны
обязательно сопровождаться атрибутами rtspecialname (return type special name — специальное
имя возвращаемого типа) и specialname. Попросту говоря, эти атрибуты применяются
для обозначения специфической лексемы CIL, которая может интерпретироваться
уникальным образом в любом отдельно взятом языке .NET. Например, в С# возвращаемый
тип в конструкторах не определяется, но в CIL оказывается, что возвращаемым
значением конструктора на самом деле является void:
.class public MyBaseClass
{
.field private string stringField
.field private int32 intField
.method public hidebysig specialname rtspecialname
instance void .ctor(string s, int32 1) cil managed
{
// Добавить код реализации. . .
}
}
Обратите внимание, что здесь директива .ctor снабжена атрибутом instance
(поскольку в данном случае конструктор не является статическим). Атрибуты cil managed
указывают на то, что в рамках данного метода содержится код CIL, а не неуправляемый
код, который может использоваться при выполнении запросов на вызов платформы.
Определение свойств в CIL
Свойства и методы тоже имеют специальный формат представления в CIL. Например,
чтобы модифицировать класс MyBaseClass с целью поддержки общедоступного
свойства по имени TheString, можно написать следующий CIL-код (обратите внимание на
использование атрибута specialname):
.class public MyBaseClass
{
.method public hidebysig specialname
instance string get_TheString() cil managed
// Добавить код реализации...
.method public hidebysig specialname
instance void set_TheString(string 'value') cil managed
// Добавить код реализации...
.property instance string TheString ()
.get instance string
MyNamespace.MyBaseClass::get_TheString()
.set instance void
MyNamespace.MyBaseClass::set_TheString (string)
}
}
628 Часть IV. Программирование с использованием сборок .NET
Вспомните, что в CIL каждое свойство отображается на пару методов, снабженных
префиксами get_ и set_. В директиве .property используются соответствующие
директивы .get и .set для отображения синтаксиса свойств на корректные "специально
именованные" методы.
На заметку! Входной параметр, передаваемый методу set в определении свойства, заключен в
одинарные кавычки и представляет собой имя лексемы, которая должна использоваться в
правой части операции присваивания в рамках метода.
Определение параметров членов
В целом аргументы в CIL задаются (более или менее) тем же образом, что и в С#.
Например, каждый аргумент в CIL определяется за счет указания типа данных, к
которому он относится, и затем самого его имени. Более того, как и в С#, в CIL можно
определять входные, выходные и передаваемые по ссылке параметры. В CIL допускается
определять аргументы, представляющие массивы параметров (равнозначно тому, что в
С# делается с помощью ключевого слова params), и необязательные параметры
(которые в С# не поддерживаются, но зато поддерживаются в VB).
Рассмотрим процесс определения параметров в CIL на примере. Предположим, что
требуется создать метод, принимающий один параметр int32 (по значению), второй
параметр int32 (по ссылке), параметр [mscorlib] System. Collection .ArrayList и
еще один выходной параметр (тоже типа mt32). В С# этот метод выглядел бы
примерно так:
public static void MyMethod(int inputlnt,
ref int reflnt, ArrayList ar, out int outputlnt)
{
outputlnt = 0; // Чтобы удовлетворить требования компилятора С#. . .
}
Отобразив этот метод на CIL, можно увидеть, что ссылочные параметры С# в CIL
снабжаются символом амперсанда (&), который присоединяется в виде суффикса к типу
параметра (int32&); выходные параметры тоже снабжаются суффиксом &, но при этом
дополнительно помечаются лексемой [out]; когда параметр представляет собой
параметр ссылочного типа (подобно [mscorlib] System. Collections .ArrayList), перед
названием его типа данных в качестве префикса добавляется лексема class (которую
ни в коем случае не следует путать с директивой .class).
.method public hidebysig static void MyMethod(int32 inputlnt,
int32& reflnt,
class [mscorlib]System.Collections.ArrayList ar,
[out] int32& outputlnt) cil managed
{
}
Изучение кодов операций в CIL
Последний аспект CIL-кода, который мы рассмотрим в этой главе, связан с ролью,
которую играют различные коды операций. Вспомните, что под кодом операции
понимается лексема CIL, используемая для построения логики реализации члена. Все
поддерживаемые в CIL коды операций (которых немало) могут быть разделены на три
основных категории:
Глава 17. Язык CIL и роль динамических сборок 629
• коды операций, позволяющие управлять выполнением программы;
• коды операций, позволяющие вычислять выражения;
• коды операций, позволяющие получать доступ к значениям в памяти (через
параметры, локальные переменные и т.д.).
В табл. 17.5 приведены некоторые наиболее полезные коды операций, имеющие
непосредственное отношение к логике реализации членов (для удобства они
сгруппированы по функциональности).
Таблица 17.5. Различные коды операций в CIL, которые имеют
отношение к реализации членов
Коды операций Описание
add, sub, mul, Позволяют выполнять сложение, вычитание, умножение и деление двух зна-
div, rem чений (rem возвращает остаток от деления)
and, or, Позволяют выполнять соответствующие побитовые операции над двумя
not, xor значениями
ceq, cgt, clt Позволяют сравнивать два значения в стеке различными способами:
ceq — сравнение на предмет равенства;
cgt — сравнение на предмет того, является ли одно из них больше другого;
clt — сравнение на предмет того, является ли одно из них меньше другого
box, unbox Применяются для преобразования ссылочных типов в типы значения и
наоборот
ret Применяется для выхода из метода и (если необходимо) возврата значения
вызывающему коду
beq, bgt, ble, Применяются (вместе с другими похожими кодами операций) для управления
bit, switch логикой ветвления внутри метода:
beq — позволяет переходить к определенной метке в коде, если при
проверке значения оказываются равными;
bgt — позволяет переходить к определенной метке в коде, если при
проверке одно из значений оказывается больше другого;
Ые — позволяет переходить к определенной метке в коде, если при
проверке одно из значений оказывается меньше и равным другому;
bit — позволяет переходить к определенной метке в коде, если при
проверке одно из значений оказывается меньше другого.
Все связанные с ветвлением коды операций требуют указания в CIL-коде
метки, к которой должен осуществляться переход в случае, если результат
проверки оказывается истинным
call Применяется для вызова члена определенного типа
newarr, Позволяют размещать в памяти, соответственно, новый массив или новый объект
newobj
Коды операций следующей обширной категории (часть из которых перечислена в
табл. 17.6) применяются для загрузки (заталкивания) аргументов в виртуальный стек
выполнения. Обратите внимание, что все эти ориентированные на выполнение загрузки
коды операций сопровождаются префиксом Id (который означает "load" — загрузка).
630 Часть IV. Программирование с использованием сборок NET
Таблица 17.6. Основные коды операций CIL, предназначенные для загрузки в стек
Код операции
Описание
ldarg
(и его
многочисленные варианты)
ldc
(и его
многочисленные варианты)
ldfld
(и его
многочисленные варианты)
ldloc
(и его
многочисленные варианты)
ldobj
ldstr
Позволяет загружать в стек аргумент метода. Помимо основного варианта
ldarg (который работает с индексом, представляющим аргумент),
существует множество других вариантов. Например, есть варианты ldarg,
которые работают с числовым суффиксом (ldarg_0) и позволяют жестко
кодировать загружаемый аргумент. Также есть варианты, позволяющие
жестко кодировать как только один тип данных, к которому относится
аргумент, с помощью константной нотации CIL из табл. 17.4 (например,
ldarg_I4 для int32), так и тип данных и значение вместе (например,
ldarg_I4_5, позволяющий загружать int32 со значением 5)
Позволяет загружать в стек значение константы
Позволяет загружать в стек значение поля уровня экземпляра
Позволяет загружать в стек значение локальной переменной
Позволяет получать все значения размещаемого в куче объекта и
помещать их в стек
Позволяет загружать в стек строковое значение
Помимо кодов операций, связанных с загрузкой, в CIL поддерживаются коды
операций, которые позволяют явным образом извлекать из стека самое верхнее значение.
Как уже было показано в нескольких примерах ранее в главе, извлечение значения из
стека обычно подразумевает его сохранение во временном локальном хранилище с
целью дальнейшего использования (например, параметра для последующего вызова
метода). Из-за этого многие коды операций, которые позволяют извлекать текущее значение
из виртуального стека выполнения, сопровождаются префиксом st (от "store" —
сохранить); в табл. 17.7 перечислены некоторые наиболее часто используемые из шгх.
Таблица 17.7. Различные коды операций CIL, предназначенные для извлечения из стека
Код операции
Описание
pop
starg
stloc
(и его
многочисленные варианты)
stobj
stsfld
Позволяет удалять значение, которое в текущий момент находится на
верхушке стека вычислений, но не сохранять его
Позволяет сохранять самое верхнее значение из стека в аргументе
метода с определенным индексом
Позволяет извлекать текущее значение из верхушки стека вычислений и
сохранять его в списке локальных переменных с определенным индексом
Позволяет копировать значение определенного типа из стека вычислений
в память по определенному адресу
Позволяет заменять значение статического поля значением из стека
вычислений
Глава 17. Язык CIL и роль динамических сборок 631
Следует принимать во внимание, что различные коды операций в CIL могут
предусматривать неявное извлечение значений из стека во время решения поставленной
перед ними задачи. Например, при вычитании одного числа из другого с использованием
кода операции sub должно быть очевидным, что перед собственно вычислением sub
должна извлечь из стека два следующих доступных значения. Результат вычисления
будет снова помещен в стек.
Директива .maxstack
При написании кода реализации методов непосредственно в CIL необходимо
помнить об одной особой директиве, которая называется .maxstack. Эта директива
позволяет указать максимальное количество переменных, которое может помещаться в стек
в любой момент во время выполнения метода. Директива имеет значение по умолчанию
(8), подходящее для подавляющего большинства создаваемых методов. При желании
можно вручную вычислять количество локальных переменных в стеке и указывать его
явно:
.method public hidebysig instance void
Speak() cil managed
{
//Во время выполнения данного метода требуется,
// чтобы в стеке находилось ровно одно значение
// (строковый литерал) .
.maxstack 1
ldstr "Hello there..
call void [mscorlib]System.Console::WriteLine(string)
ret
}
Объявление локальных переменных в CIL
Теперь давайте посмотрим, как в CIL объявлять локальную переменную. Для этого
предположим, что необходимо создать в CIL метод по имени MyLocalVariables (), не
принимающий аргументов и возвращающий void, и определить внутри него три
локальных переменных типа System.String, System.Int32 и System.Object. В С# этот
метод выглядел бы следующим образом (вспомните, что локальные переменные не
получают значений по умолчанию и потому перед использованием должны обязательно
инициализироваться):
public static void MyLocalVariables ()
{
string myStr = "CIL code is fun!11;
int mylnt = 33;
object myObj = new object ();
}
Ha CIL его можно написать так:
.method public hidebysig static void
MyLocalVariables () cil managed
{
.maxstack 8
// Определение трех локальных переменных.
.locals init ([0] string myStr, [1] int32 mylnt, [2] object myObj)
// Загрузка строки в виртуальный стек выполнения.
ldstr "CIL code is fun1"
// Извлечение текущего значения и его сохранение в локальной переменной [0] .
stloc.O
632 Часть IV. Программирование с использованием сборок .NET
// Загрузка константы типа i4 (сокращенный вариант для типа t32)
//со значением 33.
Idc.i4 33
// Извлечение текущего значения и его сохранение
//в локальной переменной [1] .
stloc.1
// Создание нового объекта и его помещение в стек.
newob] instance void [mscorlib]System.Object::.ctor ()
// Извлечение и сохранение текущего значения
//в локальной переменной [2] .
stloc.2
ret
}
Как здесь видно, в первую очередь для размещения локальных переменных в CIL
должна использоваться директива .locals вместе с атрибутом init. Внутри
соответствующих скобок с каждой переменной необходимо ассоциировать определенный
числовой индекс (в примере это [0], [1] и [2]). Далее вместе с каждым из этих
индексов понадобится указать тип данных (обязательно) и имя переменной (необязательно).
После определения локальных переменных останется только загрузить значения в стек
(с помощью различных кодов операций, предназначенных для загрузки) и сохранить их
в этих локальных переменных (посредством различных кодов операций,
предназначенных для сохранения значений).
Отображение параметров на локальные переменные в CIL
Объявление локальных переменных непосредственно в CIL с использованием
директивы .local init уже было показано, а теперь необходимо ознакомиться с
отображением входных параметров на локальные методы. Рассмотрим следующий статический
метод на С#:
public static int Add(int a, int b)
{
return a + b;
}
В CIL этот метод, который настолько просто выглядит в С#, потребует массы
дополнений. Чтобы представить его на CIL, понадобится, во-первых, разместить входные
аргументы (а и Ь) в виртуальном стеке выполнения с помощью кода операции ldarg,
во-вторых, использовать код операции add для извлечения двух значений из стека,
вычисления их суммы и затем опять ее сохранения в стеке, и, в-третьих, извлечь эту
сумму из стека и вернуть ее вызывающему коду с использованием кода операции ret.
Если просмотреть этот метод на С# в утилите ildasm. ехе, то видно, что esc. exe
вставил массу дополнительных лексем, хотя основная часть CIL-кода выглядит довольно
просто:
.method public hidebysig static int32 Add(int32 a,
int32 b) cil managed
{
.maxstack 2
ldarg.0 // Загрузка а в стек.
ldarg.1 // Загрузка b в стек.
add // Сложение обоих значений.
ret
}
Глава 17. Язык CIL и роль динамических сборок 633
Скрытая ссылка this
Обратите внимание, что в CIL-коде для ссылки на входные аргументы (а и Ь)
используются их индексные позиции @ и 1), причем нумерация этих позиций в виртуальном
стеке выполнения начинается с нуля.
При изучении и создании CIL-кода нужно помнить о том, что каждый
нестатический метод, который принимает входные аргументы, автоматически получает
дополнительный входной параметр, представляющий собой ссылку на текущий объект
(похожую на ключевое слово this в С#). Если, например, метод Add () определен не как
статический:
// Более не является статическим!
public int Add(int a, int b)
{
return a + b;
}
то входные аргументы а и b будут загружаться с помощью ldarg. 1 и ldarg. 2 (а не
ldarg.O и ldarg. 1, как ожидалось). Объясняется это тем, что в ячейке с номером О
будет содержаться неявная ссылка this. Ниже приведен псевдокод, который позволит
удостовериться в этом.
// Это ТОЛЬКО псевдокод'
.method public hidebysig static int32 AddTwoIntParams(
MyClass_HiddenThisPointer this, int32 a, int32 b) cil managed
{
ldarg.O // Загрузка MyClass_HiddenThisPointer в стек.
ldarg . 1 // Загрузка а в стек.
ldarg. 2 // Загрузка b в стек.
}
Представление итерационных конструкций в CIL
Итерационные конструкции в языке программирования С# представляются с
помощью таких ключевых слов, как for, foreach, while и do, каждое из которых имеет
специальное представление в CIL. Для примера рассмотрим следующий классический
цикл for:
public static void CountToTenO
{
for (int i = 0; l < 10; i++)
}
Вспомните, что для управления ходом выполнения программы на основе условий в
CIL применяются коды операций br (br, bit и т.д.). В данном примере условие гласит,
что выполнение цикла должно завершаться тогда, когда значение локальной
переменной i становится больше или равно 10. С каждым проходом к значению i добавляется 1,
после чего проверяемое условие вычисляется заново.
Кроме того, при использовании любого из связанных с ветвлением кодов операций в
CIL должна быть определена специальная метка (или две) для обозначения места, куда
будет произведен переход в случае удовлетворения условия. С учетом всего
вышесказанного взглянем на следующий (расширенный) код CIL, сгенерированный в ildasm.exe
(вместе с соответствующими метками):
634 Часть IV. Программирование с использованием сборок .NET
.method public hidebysig static void CountToTen () cil managed
.maxstack 2
.locals
IL 0000
IL 0001
IL 0002
IL 0004
IL 0005
IL 0006
IL 0007
IL 0008
IL 0009
IL 000b
IL OOOd
init ([0] int32 i)
ldc.i4.0
stloc.O
br.s IL 0008
ldloc.O
ldc.i4.1
add
stloc.O
ldloc.O
ldc.i4.s 10
blt.s IL 0004
ret
// Инициализация локальной
// целочисленной переменной i.
// Загрузка этого значения в стек.
// Сохранение этого значения с индексом 0.
// Переход к метке IL_0008.
// Загрузка значения переменной с индексом 0.
// Загрузка значения 1 в стек.
// Добавление значения this в стеке с индексом 0.
// Загрузка значения с индексом 0.
// Загрузка значения 10 в стек.
// Меньше? Если да, то перейти обратно к IL_0004.
Этот CIL-код начинается с определения локальной переменной int32 и ее загрузки
в стек. После этого осуществляется переход туда и обратно между метками IL0008 и
IL0004, во время каждого из которых значение i увеличивается на 1 и проверяется на
предмет того, по-прежнему ли оно меньше 10. Если нет, происходит выход из метода.
Исходный код. Пример CilTypes доступен в подкаталоге Chapter 17.
Создание сборки .NET на CIL
(Ознакомившись с синтаксисом и семантикой языка CIL, пришла пора закрепить
изученный материал, создав .NET-приложение с использованием одной только утилиты
ilasm. exe и предпочитаемого текстового редактора. Это приложение будет состоять из
приватно развертываемой однофайловой сборки *.dll, содержащей определения двух
типов классов, и консольной сборки * . ехе, взаимодействующей с этими типами.
Создание CILCars. dll
В первую очередь необходимо создать сборку * .dll, которую будет использовать
клиент. Для этого откройте текстовый редактор и создайте новый файл *. il по
имени CILCars. il. В этой однофайловой сборке будут использоваться две внешних
сборки .NET. Поэтому для начала понадобится модифицировать файл кода следующим
образом:
// Добавление ссылок на mscorlib.dll
// и System.Windows.Forms.dll.
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
.ver 4:0:0:0
}
.assembly extern System.Windows.Forms
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
.ver 4:0:0:0
}
// Определение однофайловой сборки.
.assembly CILCars
Глава 17. Язык CIL и роль динамических сборок 635
.hash algorithm 0x00008004
.ver 1:0:0:0
}
.module riLCarc.dll
В этой сборке будет содержаться два типа класса. Первый называется CILCar и
имеет два поля данных и специальный конструктор, а второй — CarlnfoHelper и имеет
единственный статический метод по имени DisplayCarlnfo (), принимающий CILCar
в качестве параметра и возвращающий void. Оба они должны размещаться в
пространстве имен CILCars. Класс CILCar может быть реализован в CIL следующим образом:
// Реализация типа CILCars.CILCar.
.namespace CILCars
{
.class public unto anci tef nr.^f lei ilini t CILCar
extends [mscorlib]System.Object
{
// Определение полей данных в CILCar.
.field public string petUame
.field public int32 currSpeed
// Определение специального конструктора, позволяющего
// вызывающему коду присваивать полям данных определенные значения.
.method public hidebybig specialname rtspecialname
instance ' uid .ctor(int32 c, string p) cil managed
{
.ma.-stack Я
// Загрузка первого аргумента в стек и
// вызов конструктора базового класса.
ldarg.O // объект this, а не int32'
call mstdiice void [mscorlib] System. Object::. ctor ()
// Загрузки первого и второго аргумента в стек.
ldarg.O // объект this
ldarg.l // аргумент int32
// Сохранение самого верхнего элемента
//из стека (int 32) в поле currSpeed.
stfld int32 CILCars.CILCar::currSpeed
// Загрузка аргумента string и его
// сохранение в поле petName.
ldarg.O // объект this
ldarg.2 // аргумент string
stfld string CILCars.CILCar:rpetName
ret
Памятуя о том, что первым аргументом для любого нестатического члена является
ссылка на текущий объект, в первом блоке CIL просто загружается ссылка на текущий
объект и вызывается конструктор базового класса. Далее входные аргументы
конструктора помещаются в стек и сохраняются в соответствующих полях данных с помощью
кода операции stfld.
Теперь реализуем второй тип класса в данном пространстве имен — CILCarlnfo.
Главным в этом типе является статический метод Display (). Роль этого метода состоит
в том, чтобы принимать в качестве входного параметра тип CILCar, извлекать
значения из его полей данных и отображать их в окне сообщений Windows Forms. Ниже
показано, как должен выглядеть необходимый для реализации CILCarlnfo код:
636 Часть IV. Программирование с использованием сборок .NET
.class public auto ansi beforefieldinit CILCarlnfo
extends [mscorlib]System.Object
{
.method public hidebysig static void
Display(class CILCars.CILCar c) cil managed
{
.maxstack 8
// Необходима какая-нибудь локальная строковая переменная.
.locals init ([0] string caption)
// Загрузка строки и входного параметра CILCar в стек.
ldstr " { 0 } ' s speed is:11
ldstr "Скорость {0} составляет: "
ldarg.O
// Помещение в стек значения поля petName из CILCar
// и вызов статического метода String.Format().
ldfld string CILCars.CILCar:ipetName
call string [mscorlib]System.String::Format (string, object)
stloc.0
// Загрузка значения поля currSpeed и получение его строкового
// представления (обратите внимание на вызов ToStringO) .
ldarg.O
ldflda int32 CILCars.CILCar::currSpeed
call instance string [mscorlib]System.Int32::ToString()
ldloc.O
// Вызов метода MessageBox.Show() с загруженными значениями.
call valuetype [System.Windows.Forms]
System.Windows.Forms.DialogResult
[System.Windows.Forms]
System.Windows.Forms.MessageBox::Show(string, string)
pop
ret
}
}
Хотя данный CIL-код по объему немного больше, чем код реализации CILCar, все
равно в нем все выглядит вполне понятно. Из-за того, что на этот раз определяется
статический метод, беспокоиться о скрытой ссылке на текущий объект больше не
требуется (поскольку в таком случае код операции Idarg. 0 будет действительно приводить к
загрузке входного аргумента CILCar).
Метод начинается с загрузки в стек строки " { 0 } ' s speed is " и следом за ней
аргумента CILCar. После этого производится загрузка значения petName и вызов
статического метода System. String. Format () для подстановки на месте метки-заполнителя
внутри фигурных скобок дружественного имени CILCar.
Обработка поля currSpeed в целом производится по такой же процедуре, но на этот
раз используется код операции ldflda для загрузки в стек адреса аргумента. После
этого вызывается метод System. Int32 . ToString () для преобразования находящегося
по указанному адресу значения в строку. И, наконец, после завершения необходимого
форматирования обеих строк вызывается метод MessageBox. Show ().
Теперь можно скомпилировать новую сборку * .dll с помощью ilasm.exe:
llasm /dll CILCars.il
и проверить содержащийся внутри нее CIL-код на предмет правильности с
семантической точки зрения с помощью утилиты peverify.exe:
peverify CILCars.dll
Глава 17. Язык CIL и роль динамических сборок 637
Создание CILCarClient. ехе
Далее можно создать простую сборку * . ехе с методом Main () внутри, который будет
создавать объект CILCar и передавать его статическому методу CILCarlnf о. Display ().
Для этого создадим новый файл CarClient.il, добавим в него ссылки на внешние
сборки mscorlib.dll и CILCars .dll (не забыв поместить копию последней в каталог
клиентского приложения) и определим в нем единственный тип (Program), в котором
будут производиться все необходимые манипуляции над сборкой CILCars.dll. Ниже
показан полный код:
// Добавление ссылок на внешние сборки.
assembly extern mscorlib
.publickeytoken = (B7 7A 5C 56 19 34 EO 89)
.ver 4:0:0:0
assembly extern CILCars
.ver 1:0:0:0
// Определение исполняемой сборки.
assembly CarClient
.hash algorithm 0x00008004
.ver 1:0:0:0
module CarClient.exe
// Реализация типа Program.
namespace CarClient
.class private auto ansi beforefleldinit Program
extends [mscorlib]System.Object
{
.method private hidebysig static void Main(string [ ] args) cil managed
{
// Обозначение точки входа в *.ехе.
.entrypoint
.maxstack 8
// Объявление локальной переменной CILCar и помещение
// значений в стек для вызова конструктора.
.locals init ([0] class [CILCars]CILCars.CILCar myCilCar)
ldc.i4 55
ldstr "Junior"
// Создание нового объекта CILCar, его сохранение
//и затем загрузка ссылки на него.
newobj instance void [CILCars]CILCars.CILCar::.ctor(int32, string)
stloc.O
ldloc.O
// Вызов метода Display() и передача ему самого верхнего значения из стека.
call void [CILCars]
CILCars.CILCarlnfo::Display(
class [CILCars]CILCars.CILCar)
ret
}
}
}
638 Часть IV. Программирование с использованием сборок .NET
Единственным кодом операции, на который здесь важно
обратить внимание, является .entrypoint. Как упоминалось ранее в
главе, этот код применяется для обозначения того, какой из
методов должен быть входной точкой в модуле * . ехе. В
действительности, поскольку по .entrypoint CLR-среда определяет начальный
метод для выполнения, этот метод может иметь какое угодно имя,
хотя для него было использовано стандартное имя Main (). В
остальной части CIL-кода этого метода Main () производятся типич-
Рис. 17.4. Сборка ные операции по помещению и извлечению значений из стека.
CILCar в действии Кроме того, для создания CILCar используется код операции
. newob j. В связи с этим вспомните, что для вызова члена типа
непосредственно в CIL применяется синтаксис в виде двойного двоеточия и, как обычно,
указывается полностью квалифицированное имя типа. Теперь можно скомпилировать
новый файл с помощью ilasm. exe, проверить его правильность с точки зрения
семантики с помощью peverify.exe и затем запустить программу:
ilasm CarClient.il
peverify CarClient.exe
CarClient.exe
На рис. 17.4 показан результат выполнения сборки CILCar.
Исходный код. Пример CilCars доступен в подкаталоге chapter 17.
Динамические сборки
Естественно, процесс создания сложных .NET-приложений на CIL будет
довольно "неблагодарным трудом". С одной стороны, CIL представляет собой чрезвычайно
выразительный язык программирования, позволяющий взаимодействовать со всеми
программными конструкциями, которые предусмотрены в CTS. С другой стороны,
написание кода непосредственно на CIL отнимает много времени и сил и чревато
допущением ошибок. И хотя знание — это всегда сила, все же наверняка интересно,
насколько в действительности важно держать правила синтаксиса CIL в голове? Ответ
на этот вопрос зависит от ситуации. Конечно, в большинстве случаев при
программировании .NET-приложений просматривать, редактировать или создавать CIL-код
совсем необязательно. Однако знание основ CIL позволяет перейти к исследованию мира
динамических сборок (в отличие от статических) и оценки роли пространства имен
System.Refleetion.Emit.
В первую очередь может возникнуть вопрос, чем отличаются статические и
динамические сборки? По определению, статической сборкой называется такой двоичный
модуль .NET, который загружается прямо из хранилища на диске, те. на момент
запроса CLR-средой он находится в физическом файле (или файлах, если сборка
многофайловая) где-то на жестком диске. Как не трудно догадаться, при каждой компиляции
исходного кода С# в результате всегда получается статическая сборка.
С другой стороны, динамическая сборка создается в памяти "на лету" за счет
использования типов из пространства имен System.Ref lection .Emit. Пространство имен
System. Reflection. Emit, по сути, позволяет сделать так, чтобы сборка с ее модулями,
определениями типов и логикой реализации на CIL создавалась во время выполнения.
После этого сборку в памяти можно сохранять в дисковый файл, превращая ее в
статическую. Несомненно, для создания динамических сборок с помощью пространства имен
System.Reflection.Emit нужно разбираться в природе кодов операций CIL.
1^^^'^. л»
Junior's speed is:
55
L
OK
Глава 17. Язык CIL и роль динамических сборок 639
Хотя процесс создания динамических сборок является довольно сложным (и нечасто
применяемым) приемом программирования, он полезен в перечисленных ниже ситуациях.
• Создается штструмент для программирования .NET, который должен быть способен
генерировать сборки по требованию на основе вводимых пользователем данных.
• Создается приложение, которое должно уметь генерировать прокси для
удаленных типов на лету на основе получаемых метаданных.
• Требуется возможность загрузки статической сборки и динамической вставки в ее
двоичный образ новых типов.
Некоторые компоненты механизма исполняющей среды .NET тоже предусматривают
генерацию динамических сборок в фоновом режиме. Например, в ASP.NET эта
технология применяется для отображения кода разметки и серверного сценария на объектную
модель исполняющей среды. В LINQ код тоже может генерироваться "на лету" на
основе различных выражений, содержащихся в запросах. Давайте посмотрим, какие типы
предлагаются в пространстве имен System.Reflection.Emit.
Пространство имен System.Reflection.Emit
Для создания динамических сборок необходимо иметь представление о кодах
операций CIL, однако типы, поставляемые в пространстве имен System. Ref lection .Emit,
максимально возможно скрывают сложные детали CIL. Например, вместо того, чтобы
напрямую указывать необходимые директивы и атрибуты CIL для определения типа класса,
можно воспользоваться классом TypeBuilder. Еще один класс, ConstructorBuilder,
позволит определить новый конструктор на уровне экземпляра, не имея дела напрямую
с лексемами specialname, rtspecialname или . ctor. Ключевые члены пространства
имен System. Ref lection. Emit перечислены в табл. 17.8.
Таблица 17.8. Некоторые члены пространства имен System. Ref lection. Emit
Член Описание
AssemblyBuilder Используется для создания сборки (* . dll или * . ехе) во время
выполнения. В сборках * . ехе должен обязательно вызываться
метод ModuleBuilder. SetEntryPoint () для указания метода,
который должен выступать в роли точки входа в модуль. Если точка
входа не задана, генерируется файл * . dll
ModuleBuilder Используется для определения набора модулей внутри текущей сборки
EnumBuilder Используется для создания типа перечисления .NET
TypeBuilder Может применяться для создания в модуле различных классов,
интерфейсов, структур и делегатов во время выполнения
MethodEuilder Используются для создания во время выполнения соответствую-
LocalBuilder щих членов типов (таких как методы, локальные переменные, свой-
PropertyBuild'ir ства, конструкторы и атрибуты)
FieldBuilder
ConstructorBuilder
CustomAttnbuteBuilder
ParameterBuilder
E^entBuilder
iLGenerator Генерирует необходимые коды операций CIL внутри указанного
члена типа
Opcodes Предоставляет множество полей, которые отображаются на
коды операций CIL Используется вместе с различными членами
System.Refleetion.Emit.ILGenerator
640 Часть IV. Программирование с использованием сборок .NET
В целом типы из пространства имен System. Re flection. Em it позволяют
представлять исходные лексемы CIL программным образом во время построения
динамической сборки. Многие из них будут использоваться в приведенном ниже примере, а тип
ILGenerator детально рассматривается в следующем разделе.
Роль типа System.Reflection.Emit.ILGenerator
Роль типа ILGenerator заключается во вставке соответствующих кодов операций
CIL в заданный член типа. Напрямую создавать объекты ILGenerator нельзя, потому
что этот тип не имеет никаких общедоступных конструкторов. Вместо этого объекты
ILGenerator должны получаться за счет вызова конкретных методов Builder-типов
(таких как MethodBuilder и ConstructorBuilder), например:
// Получение ILGenerator из объекта ConstructorBuilder
// по имени myCtorBuilder.
ConstructorBuilder myCtorBuilder =
new ConstructorBuilder(/* ...различные аргументы... */) ;
ILGenerator myCILGen = myCtorBuilder.GetlLGenerator();
После получения ILGenerator можно приступать к генерации с его помощью
низкоуровневых кодов операций CIL, применяя любые его методы (см. табл. 17.9).
Таблица 17.9. Методы типа ILGenerator
Метод
Описание
BeginCatchBlock()
BeginExceptionBlock()
BeginFinallyBlock()
BeginScope()
DeclareLocal()
DefineLabel()
Emit ()
EmitCall()
EmitWriteLine()
EndExceptionBlock()
EndScope()
ThrowException()
UsingNamespace()
Начинает блок catch
Начинает блок генерации нефильтруемого исключения
Начинает блок finally
Начинает лексический контекст
Объявляет локальную переменную
Объявляет новую метку
Имеет несколько перегруженных версий и позволяет
генерировать коды операций CIL
Помещает в поток CIL код операции call или callvirt
Генерирует вызов Console. WriteLine () со значениями
разных типов
Завершает блок исключения
Завершает лексический контекст
Генерирует инструкцию для выдачи исключения
Специфицирует пространство имен, которое должно
использоваться при вычислении локальных и наблюдаемых значений в
текущем активном лексическом контексте
Главным в ILGenerator является метод Emit (), который работает вместе с типом
класса System. Reflection .Emit. OpCodes. Как уже упоминалось ранее в главе,
данный тип имеет множество доступных только для чтения полей, которые отображаются
на исходные коды операций CIL. Полный список его членов можно найти в онлайновой
справке, а примеры применения некоторых из них — чуть позже в настоящей главе.
Глава 17. Язык CIL и роль динамических сборок 641
Создание динамической сборки
Чтобы увидеть, как выглядит процесс определения сборки .NET во время
выполнения, давайте попробуем создать однофайловую динамическую сборку по имени
MyAssembly.dll. Внутри ее главного модуля должен находиться класс HelloWorld.
В этом классе поддерживается конструктор по умолчанию и специальный
конструктор, который используется для установки значения приватной переменной экземпляра
(theMessage) типа string. В классе также есть общедоступный метод экземпляра по
имени SayHello (), который выводит приветственное сообщение в стандартный поток
ввода-вывода, и еще один метод экземпляра по имени GetMsg (), возвращающий
внутреннюю приватную строку. По сути, нужно сгенерировать программно следующий тип
класса:
// Этот класс будет создаваться во время выполнения
//с помощью System. Reflection. Emit.
public class HelloWorld
{
private string theMessage;
HelloWorld() {}
HelloWorld(string s) { theMessage = s; }
public string GetMsg() { return theMessage;}
public void SayHello ()
{
System.Console.WriteLine ("Hello from the HelloWorld class!");
}
}
Создадим в Visual Studio 2010 новый проект типа Console Application (Консольное
приложение) по имени MyAsmBuilder, импортируем в него пространства имен System.
Reflection, System. Reflection .Emit и System. Threading и определим статический
метод по имени CreateMyAsm (), который будет отвечать за решение следующих задач:
• определение характеристик динамической сборки (имя, версию и т.д.);
• реализация типа HelloClass;
• сохранение сгенерированной в памяти сборки в физическом файле.
Метод CreateMyAsm () принимает в качестве единственного параметра тип System.
АррDomain и использует его для получения доступа к типу AssemblyBuilder,
ассоциированному с текущим доменом приложения (домены приложений рассматривались в
главе 17). Код метода CreateMyAsm () показан ниже.
// Передача типа AppDomain из вызывающего кода.
public static void CreateMyAsm(AppDomain curAppDomain)
{
// Установка общих характеристик сборки.
AssemblyName assemblyName = newAssemblyName ();
assemblyName.Name = "MyAssembly";
assemblyName.Version = new Version(.0.0 . 0") ;
// Создание новой сборки в текущем домене приложения.
AssemblyBuilder assembly =
curAppDomain.DefineDynamicAssembly(assemblyName,
AssemblyBuilderAccess.Save);
// Поскольку создается однофайловая сборка, имя модуля
// должно совпадать с именем самой сборки.
ModuleBuilder module =
assembly .Def ineDynamicModule ("MyAssembly11, "MyAssembly.dll") ;
642 Часть IV. Программирование с использованием сборок .NET
// Определение общедоступного класса по имени HelloWorld.
TypeBuilder helloWorldClass =
module.DefineType("MyAssemblу.HelloWorld" ,
TypeAttributes.Public);
// Определение приватной строковой переменной экземпляра по имени theMessage.
FieldBuilder msgField =
helloWorldClass.DefineField("theMessage",
Type.GetType("System.String"), FieldAttributes.Private);
// Создание специального конструктора.
Type[] constructorArgs = new Type[l];
constructorArgs[0] = typeof(string) ;
ConstructorBuilder constructor =
helloWorldClass.DefineConstructor(MethodAttributes.Public,
CallingConventions.Standard,
constructorArgs);
ILGenerator constructorIL = constructor.GetlLGenerator();
constructorlL.Emit(OpCodes.Ldarg_0);
Type objectClass = typeof(object) ;
Constructorlnfo superConstructor = objectClass.GetConstructor(new Type[0]);
constructorlL.Emit(OpCodes.Call, superConstructor);
constructorlL.Emit(OpCodes.Ldarg_0);
constructorIL.Emit(OpCodes.Ldarg_l);
constructorlL.Emit(OpCodes.StfId, msgField);
constructorlL.Emit(OpCodes.Ret);
// Создание конструктора по умолчанию.
helloWorldClass.DefineDefaultConstructor(MethodAttributes.Public);
// Создание метода GetMsgQ .
MethodBuilder getMsgMethod =
helloWorldClass.DefineMethod("GetMsg",
MethodAttributes.Public,
typeof(string), null);
ILGenerator methodIL = getMsgMethod.GetlLGenerator() ;
methodIL.Emit(OpCodes.Ldarg_0);
methodIL.Emit(OpCodes.LdfId, msgField);
methodIL.Emit(OpCodes.Ret);
// Создание метода SayHello.
MethodBuilder sayHiMethod =
helloWorldClass.DefineMethod("SayHello",
MethodAttributes.Public, null, null);
methodIL = sayHiMethod.GetlLGenerator ();
methodIL.EmitWriteLine("Hello from the HelloWorld class1");
methodIL.Emit(OpCodes.Ret);
// Генерация класса HelloWorld.
helloWorldClass.CreateType();
// Сохранение сборки в файле (необязательно).
assembly.Save("MyAssemblу.dll");
Генерация сборки и набора модулей
В теле метода сначала устанавливается минимальный набор характеристик
сборки с использованием типов AssemblyName и Version (из пространства имен System.
Reflection). После этого получается тип AssemblyBuilder через действующий на
уровне экземпляра метод AppDomain . Def ineDynamicAssembly () (вспомните, что
ссылка AppDomain передается методу CreateMyAsm () из вызывающего кода).
Глава 17. Язык CIL и роль динамических сборок 643
// Установка общих характеристик сборки
//и получение доступа к типу AssemblyBuilder.
public static void CreateMyAsm(AppDomain curAppDomain)
{
AssemblyName assemblyName = new AssemblyName ();
assemblyName.Name = "MyAssembly";
assemblyName.Version = new Version(.0.0.0");
// Создание новой сборки в текущем домене приложения.
AssemblyBuilder assembly =
curAppDomain.DefineDynamicAssembly(assemblyName,
AssemblyBuilderAccess.Save);
}
При вызове AppDomain . Def ineDynamicAssembly () должен быть указан желаемый
режим доступа к сборке, который может принимать любое из значений перечисления
AssemblyBuilderAccess (см. табл. 17.10).
Таблица 17.10. Значения перечисления AssemblyBuilderAccess
Значение Описание
Ref lectionOnly Указывает, что динамическая сборка может только воспроизводиться
посредством рефлексии
Run Указывает, что динамическая сборка может только выполняться в памяти,
но не сохраняться на диске
RunAndSave Указывает, что динамическая сборка может выполняться в памяти
и сохраняться на диске
Save Указывает, что динамическая сборка может только сохраняться на диске,
но не выполняться в памяти
Следующая задача состоит в определении набора модулей для новой сборки. Из-за
того, что в данном случае сборка должна представлять одиночный файл, необходимо
определить для нее только один модуль. При создании многофайловой сборки с
использованием метода Def ineDynamicModule () понадобилось бы предоставить второй
необязательный параметр, в котором указать имя модуля (например, myMod.dotnetmodule).
В случае однофайловой сборки имя модуля совпадает с именем самой сборки. В
результате вызова метода Def ineDynamicModule () возвращается ссылка на действительный
тип ModuleBuilder:
// Однофайловая сборка.
ModuleBuilder module =
assembly.DefineDynamicModule("MyAssembly", "MyAssembly.dll");
Роль типа ModuleBuilder
Тип ModuleBuilder играет ключевую роль при разработке динамических
сборок. Он имеет набор членов, позволяющих определять типы, которые должны
содержаться внутри модуля (классы, интерфейсы, структуры и т.д.), а также используемые
встроенные ресурсы (таблицы, изображения и т.п.). В табл. 17.11 перечислены методы
ModuleBuilder, которые являются наиболее полезными при создании динам1гческих
сборок. (Обратите внимание, что каждый из этих методов возвращает соответствующий
тип, который представляет создаваемый тип.)
644 Часть IV. Программирование с использованием сборок .NET
Таблица 17.11. Некоторые методы типа ModuleBuilder
Метод Описание
Def ineEnum () Используется для генерации определения перечисления .NET
Def ineResource () Определяет управляемый вложенный ресурс, который должен
храниться в данном модуле
Def ineType () Создает объект TypeBuilder, который позволяет определить типы
значения, интерфейсы и типы классов (включая делегаты)
Ставным членом класса ModuleBuilder является метод Def ineType (). Помимо
указания имени для типа (в виде простой строки), в нем может использоваться
перечисление System. Reflection . TypeAttributes для описания формата типа. В табл. 17.12
перечислены наиболее важные члены перечисления TypeAttributes.
Таблица 17.12. Некоторые члены перечисления TypeAttributes
Член Описание
Abstract Указывает, что тип является абстрактным
Class Указывает, что тип является классом
Interface Указывает, что тип является интерфейсом
NestedAssembly Указывает, что класс является вложенным в область видимости
сборки, а потому доступен только методам внутри этой сборки
NestedFamAndAssem Указывает, что класс находится в области видимости сборки и
семейства, а потому доступен только методам, которые относятся к
пересечению этого семейства и сборки
NestedFamily Указывает, что класс находится в области видимости семейства, а
потому доступен только методам, которые содержатся внутри его
собственного типа и любых подтипов
NestedFamORAssem Указывает, что класс находится в области видимости семейства или
сборки, а потому доступен только методам, которые присутствуют и в
этом семействе, и в сборке
NestedPrivate Указывает, что класс является вложенным и приватным
NestedPublic Указывает, что класс является вложенным и общедоступным
NotPublic Указывает, что класс не является общедоступным
Public Указывает, что класс является общедоступным
Sealed Указывает, что класс является конкретным и не может быть расширен
Serializable Указывает, что класс может подвергаться сериализации
Генерация типа HelloClass и принадлежащей
ему строковой переменной
Ознакомившись с ролью метода ModuleBuilder .CreateType (), теперь посмотрим,
как можно сгенерировать класс HelloWorld и приватную строковую переменную:
// Определение общедоступного класса по имени MyAssembly.HelloWorld.
TypeBuilder helloWorldClass =
module.DefineType("MyAssembly.HelloWorld", TypeAttributes.Public);
Глава 17. Язык CIL и роль динамических сборок 645
// Определение приватной строковой переменной
// экземпляра по имени theMessage.
FieldBuilder msgField =
helloWorldClass.DefineField("theMessage",
typeof(string),
FieldAttributes.Private);
Обратите внимание, что метод TypeBuilder. Def ineField () предоставляет доступ к
типу FieldBuilder. Класс TypeBuilder имеет и другие методы, которые обеспечивают
доступ к другим типам в стиле Builder. Например, метод Def ineConstructor ()
возвращает тип ConstructorBuilder, метод Def ineProperty () —тип PropertyBuilder,
и т.д.
Генерация конструкторов
Как упоминалось ранее, для определения конструктора текущего типа можно
использовать метод TypeBuilder . Def ineConstructor () . В рассматриваемом примере
при реализации конструктора HelloClass необходимо вставить в тело
конструктора низкоуровневый CIL-код, который будет отвечать за присваивание входного
параметра внутренней приватной строке. Для получения типа ILGenerator
понадобится вызвать метод GetlLGenerator () из соответствующего Builder-типа (в данном
случае — ConstructorBuilder).
Метод Emit () в классе ILGenerator представляет собой ту самую сущность, которая
отвечает за помещение CIL-кода в реализацию членов. В самом методе Emit () часто
используется тип класса Opcodes, который предоставляет доступ к набору кодов
операций CIL посредством предназначенных только для чтения свойств. Например, свойство
Opcodes .Ret сигнализирует о возврате вызова метода, Opcodes .Stfld позволяет
выполнять в отношении переменной экземпляра операцию присваивания, a Opcodes . Call
применяется для вызова конкретного метода (и в рассматриваемом случае может
использоваться для вызова конструктора базового класса). С учетом всего
вышесказанного, логика для реализации конструктора будет выглядеть следующим образом:
// Создание специального конструктора, принимающего
// единственный аргумент типа System.String.
Type[] constructorArgs = new Type[l];
constructorArgs[0] = typeof(string);
ConstructorBuilder constructor =
helloWorldClass.DefineConstructor(MethodAttributes.Public,
CallingConventions.Standard, constructorArgs);
// Вставка в конструктор необходимого CIL-кода.
ILGenerator constructorIL = constructor.GetlLGenerator ();
constructorlL.Emit(OpCodes.Ldarg_0);
Type objectClass = typeof(object);
Constructorlnfo superConstructor =
objectClass.GetConstructor(new Type [0] ) ;
constructorlL.Emit(OpCodes.Call,
superConstructor); // Вызов конструктора базового класса.
// Загрузка указателя на текущий объект в стек.
constructorIL.Emit(OpCodes.Ldarg_0);
// Загрузка входного аргумента в стек
//и его сохранение в msgField.
constructorIL.Emit(OpCodes.Ldarg_l);
constructorlL.Emit(OpCodes.Stfld, msgField); // Установка msgField.
constructorlL.Emit(OpCodes.Ret); // Возврат.
646 Часть IV. Программирование с использованием сборок .NET
Как известно, в результате определения специального конструктора для типа
конструктор по умолчанию незаметно удаляется. Чтобы снова определить конструктор, не
принимающий аргументов, достаточно просто вызвать метод Def ineDef aultConstructor ()
типа TypeBuilder:
// Вставка конструктора по умолчанию заново.
helloWorldClass.DefineDefaultConstructor(MethodAttributes.Public) ;
Этот единственный вызов генерирует стандартный CIL-код для определения
конструктора по умолчанию:
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
.maxstack 1
ldarg.O
call instance void [mscorlib]System.Object::.ctor ()
ret
}
Генерация метода SayHello ()
И, наконец, осталось рассмотреть только процесс генерирования метода SayHello ().
Первой задачей будет получение типа MethodBuilder из переменной helloWorldClass.
После этого можно определить сам метод и получить лежащий в основе тип ILGenerator
для вставки необходимых CIL-инструкций:
// Создание метода SayHello.
MethodBuilder sayHiMethod =
helloWorldClass.DefineMethod("SayHello",
MethodAttributes.Public, null, null);
methodIL = sayHiMethod.GetlLGenerator ();
// Вывод строки в окне консоли.
methodIL.EmitWriteLine ( "Hello there•");
methodIL.Emit(Opcodes.Ret);
Здесь был определен общедоступный метод (MethodAttributes . Public), не
принимающий параметров и ничего не возвращающий (на что указывают значения null в
вызове Def ineMethod ()). Обратите внимание на вызов метода EmitWriteLine (). Этот
вспомогательный член класса ILGenerator автоматически записывает строку в
стандартный поток вывода с приложением минимальных усилий.
Использование динамически сгенерированной сборки
Имея готовую логику для создания и сохранения сборки, осталось создать только
класс, который будет ее запускать. Определим в текущем проекте еще один класс по
имени AsmReader. Внутри методе Main () с помощью метода Thread.GetDoMain ()
необходимо получить ссылку на текущий домен приложения, который будет
использоваться для обслуживания динамически создаваемой сборки. После получения ссылки
можно будет вызывать метод CreateMyAsm ().
Чтобы сделать пример немного интереснее, после вызова CreateMyAsm ()
воспользуемся поздним связыванием (см. главу 15) для загрузки вновь созданной сборки в
память и взаимодействия с членами класса HelloWorld. Модифицируем метод Main (),
как показано ниже.
static void Main(string [ ] args)
{
Console.WriteLine ("***** The Amazing Dynamic Assembly Builder App *****");
Глава 17. Язык CIL и роль динамических сборок 647
// Получение ссылки на домен приложения, в котором выполняется текущий поток.
AppDomain curAppDomain = Thread.GetDomain();
// Создание динамической сборки с помощью вспомогательной функции f(x) .
CreateMyAsm(curAppDomain);
Console.WriteLine("-> Finished creating MyAssembly.dll.");
// Загрузка новой сборки из файла.
Console.WriteLine("-> Loading MyAssembly.dll from file.");
Assembly a = Assembly. Load ("IlyAssembly" ) ;
// Получение типа HelloWorld.
Type hello = a.GetType("MyAssembly.HelloWorld");
// Создание объекта HelloWorld и вызов правильного конструктора.
Console.Write("-> Enter message to pass HelloWorld class: ");
string msg = Console.ReadLine();
object[] ctorArgs = new object[1];
ctorArgs[0] = msg;
object obj = Activator.Createlnstance(hello, ctorArgs);
// Вызов SayHello и отображение возвращаемой строки.
Console.WriteLine ("-> Calling SayHello() via late binding.");
Methodlnfo mi = hello.GetMethod("SayHello");
mi.Invoke(obi, null);
// Вызов метода GetMsg.
mi = hello.GetMethod("GetMsg");
Console.WriteLine(mi.Invoke(obj, null));
}
Итак, создана сборка .NET, которая может генерировать и запускать другие сборки
.NET во время выполнения. На этом рассмотрение CIL и роли динамических сборок
завершено. Эта глава должна была помочь углубить знания системы типов .NET, а также
синтаксиса и семантики языка CIL.
Исходный код. Проект DynamicAsmBuilder доступен в подкаталоге Chapter 17
Резюме
В этой главе был приведен краткий обзор основных деталей синтаксиса и семантики
языка CIL. В отличие от управляемых языков более высокого уровня, таких как С#, в CIL
используется не набор ключевых слов, а директивы (позволяющие определить
структуру сборки и ее типов), атрибуты (дополнительно уточняющие каждую директиву) и коды
операций (применяемые для реализации членов типов).
Было продемонстрировано несколько доступных для программирования на CIL
инструментов (ilasm. exe, SharpDevelop и peverify. exe), а также показано, как изменять
содержимое сборки .NET за счет добавления новых СIL-инструкций с использованием
методики двунаправленного проектирования. Кроме того, рассматривались способы
определения в CIL текущей (и внешней сборки), пространств имен, типов и членов. Был
предложен простой пример создания библиотеки кода и исполняемого файла .NET с
применением только CIL и соответствующих инструментов командной строки.
В конце главы приводился краткий обзор процесса создания динамической сборки.
Используя пространство имен System. Ref lection .Emit, сборку .NET можно
определить в памяти во время выполнения. Однако, как было указано, применение этого
пространства имен требует знаний семантики CIL-кода. Хотя построение динамических
сборок не является распространенной задачей при написании большинства
приложений .NET, такие сборки могут быть очень полезны при разработке инструментальных
средств поддержки и прочих утилит для программирования.
ГЛАВА 18
Динамические типы
и исполняющая среда
динамического языка
В версии .NET 4.0 язык С# получил новое ключевое слово — dynamic. Это
ключевое слово позволяет включать поведение, подобное сценариям, в строго
типизированный мир точек с запятой и фигурных скобок. Используя эту слабую типизацию,
можно значительно упростить некоторые сложные задачи кодирования и получить
возможность взаимодействия с множеством динамических языков (таких как IronRuby и
IronPython), которые поддерживают .NET.
В этой главе будет описано ключевое слово dynamic, а также показано, как слабо
типизированные вызовы отображаются на корректные объекты в памяти, благодаря
DLR (Dynamic Language Runtime — исполняющая среда динамического языка). После
рассмотрения служб, предоставляемых DLR, будут приведены примеры использования
динамических типов для облегчения выполнения вызовов методов с поздним
связыванием (через службы рефлексии) и для упрощения взаимодействия с унаследованными
библиотеками СОМ.
На заметку! Не путайте ключевое слово С# dynamic с концепцией динамической сборки (см.
главу 17). Хотя ключевое слово dynamic может использоваться при построении динамической
сборки, все же это две совершенно независимые концепции.
Роль ключевого слова С# dynamic
В главе 3 вы узнали о ключевом слове var, которое позволяет объявлять локальную
переменную таким образом, что ее действительный тип данных определяется
начальным присваиванием (это называется неявной типизацией). Как только начальное
присваивание выполнено, вы получаете строго типизированную переменную, и любая
попытка присвоить ей несовместимое значение приведет к ошибке компиляции.
Чтобы приступить к исследованию ключевого слова С# dynamic, создадим
консольное приложение по имени DynamicKeyword. После этого поместим в класс Program
показанный ниже метод и удостоверимся, что финальный оператор кода действительно
инициирует ошибку во время компиляции, если убрать с него символы комментария.
Глава 18. Динамические типы и исполняющая среда динамического языка 649
static void ImplicitlyTypedVariable ()
{
// а имеет тип List<int>.
var a = new List<mt> () ;
a.Add(90) ;
// Это вызовет ошибку во время компиляции1
// а = "Hello";
}
Использование неявной типизации просто потому, что она возможна, считается
плохим стилем (если известно, что нужен тип List<int>, то его и следует указывать).
Однако, как было показано в главе 13, неявная типизация очень полезна в сочетании
с LINQ, поскольку многие запросы LINQ возвращают перечисления анонимных классов
(через проекции), которые объявить явно в коде С# не получится. Тем не менее, даже
в этих случаях неявно типизированная переменная на самом деле является строго
типизированной.
Как уже известно из главы 6, System.Object находится на вершине иерархии
классов в .NET Framework и может представлять все, что угодно. После объявления
переменной типа object получается строго типизированный элемент данных, однако то, на
что он указывает в памяти, может отличаться в зависимости от присваивания ссылки.
Для того чтобы получить доступ к членам объекта, на который установлена ссылка в
памяти, необходимо выполнять явное приведение.
Предположим, что есть простой класс по имени Person, в котором определены два
автоматических свойства (FirstName и LastName), инкапсулирующие string. Теперь
взгляните на следующий код:
static void UseObjectVarible ()
{
// Предположим, что есть класс по имени Person.
object о = new Person () { FirstName = "Mike", LastName = "Larson" };
// Для получения доступа к свойствам Person необходимо приводить object к Person.
Console .WriteLme ("Person ' s first name is {0}", ( (Person)o) .FirstName) ;
}
В версии .NET 4.0 язык С# стал поддерживать ключевое слово dynamic. На
самом высоком уровне dynamic можно рассматривать как специализированную форму
System.Object, в том смысле, что типу данных dynamic может быть присвоено любое
значение. На первый взгляд, это порождает ужасную путаницу, поскольку теперь
получается, что доступны три способа определения данных, внутренний тип которых явно
не указан в коде. Например, следующий метод:
static void PrintThreeStrings ()
{
var si = "Greetings";
object s2 = "From";
dynamic s3 = "Minneapolis";
Console.WriteLme ("si is of type: {0}", si.GetType());
Console .WriteLme ("s2 is of type: {0}", s2.GetType());
Console.WriteLme ("s3 is of type: {0}", s3 . GetType ()) ;
}
будучи вызванным в Main(), выведет на консоль следующее:
si is of type: System.String
s2 is of type: System.String
s3 is of type: System.String
650 Часть IV. Программирование с использованием сборок .NET
Динамическую переменную от переменной, объявленной неявно или через ссылку
System.Object, значительно отличает то, что она не является строго
типизированной. Другими словами, динамические данные не типизированы статически. Для
компилятора С# это выглядит так, что элемент данных, объявленный с ключевым словом
dynamic, может получить какое угодно начальное значение, и на протяжении времени
его существования это значение может быть заменено новым (и возможно, не
связанным с первоначальным). Рассмотрим следующий метод и его результирующий вывод:
static void ChangeDynamicDataType ()
i
II Объявить одиночный элемент данных dynamic по имени t.
dynamic t = "Hello!";
Console.WriteLine ("t is of type: {0}", t.GetType());
t = falser-
Console. WriteLine ("t is of type: {0}", t.GetType ());
t = new List<mt>();
Console.WriteLine("t is of type: {0}", t.GetType());
}
Вот как выглядит вывод:
t is of type: System.String
t is of type: System.Boolean
t is of type: System.Collections.Generic.Listч1[System.Int32]
Имейте в виду, ,что приведенный выше код успешно бы скомпилировался и дал
идентичные результаты, если бы переменная t была объявлена с типом System.Object. Тем
не менее, как вскоре будет показано, ключевое слово dynamic предоставляет много
дополнительных возможностей.
Вызов членов на динамически объявленных данных
Теперь, учитывая, что тип данных dynamic может на лету принимать идентичность
любого типа (как переменная типа System.Object), следующий вопрос, который
наверняка возник, связан с вызовом членов на динамической переменной (свойств, методов,
индексаторов, регистрации событий и т.п.). В отношении синтаксиса никаких отличий
нет Нужно просто применить операцию точки к динамической переменной, указать
общедоступный член и передать ему необходимые аргументы.
Однако (и это очень важно) корректность указываемых членов компилятором не
проверяется! Помните, что в отличие от переменной, объявленной как System.Object,
динамические данные не являются статически типизированными. Вплоть до времени
выполнения не известно, поддерживают ли вызываемые динамические данные указанный
член, переданы ли корректные параметры, правильно ли указан член, и т.д. Поэтому,
как бы странно это не выглядело, следующий метод скомпилируется без ошибок:
static void InvokeMembersOnDynamicData ()
i
dynamic textDatal = "Hello";
Console.WriteLine(textDatal.ToUpper());
// Здесь следовало ожидать ошибку компилятора1
//Но все компилируется нормально.
Console.WriteLine(textDatal.toupper());
Console.WriteLine(textDatal.FooA0, "ее", DateTime.Now));
}
Обратите внимание, что во втором вызове WriteLine () производится обращение
к методу по имени toupper () на динамической переменной. Как видите, textDatal
имеет тип string, и потому известно, что у этого типа нет метода с таким именем в
Глава 18. Динамические типы и исполняющая среда динамического языка 651
нижнем регистре. Более того, тип string определенно не имеет метода по имени Foo(),
который принимает int, string и DataTime!
Тем не менее, компилятор С# не о каких ошибках не сообщает Однако если
вызвать этот метод в Main(), возникнет ошибка времени выполнения с примерно таким
сообщением:
Unhandled Exception : Microsoft. CSharp. RuntimeBmder . RuntimeBinderException :
'string' does not contain a definition for 'toupper'
Необработанное исключение: Microsoft. CSharp. RuntimeBmder. RuntimeBinderException:
'string' не содержит определения 'toupper'
Другое значительное отличие между вызовом членов на динамических и строго
типизированных данных состоит в том, что после применения операции точки к
элементу динамических данных средство IntelliSense в Visual Studio 2010 не активизируется.
Вместо этого отображается следующее общее сообщение (рис. 18.1).
| Program.cs* X |
J:$GettingDynamic.Program
• ^♦UseDynemicVarQ
static void UseDynamicVar()
{
dynamic textDatal - "Hello";
Console.WriteLine(textDatal.ToUpper());
textDatal. |
! (dynamic expression)
// You wolJ This operation will be resolved at runtii*
// But they impiHf JUU f'lWf.
-ц
Console.WriteLine(textDatal.toupper());
i"oo%" :] « gj
Рис. 18.1. Динамические данные не активизируют средство IntelliSense
То, что средство IntelliSense недоступно с динамическими данными, имеет смысл.
Однако это означает, что при наборе кода С# с такими переменными следует соблюдать
исключительную осторожность. Любая опечатка или некорректный регистр символов в
имени члена приведет к ошибке времени выполнения, а именно — к генерации
экземпляра класса RuntimeBinderException.
Роль сборки Microsoft.CSharp.dll
Сразу же после создания нового проекта С# в Visual Studio 2010 автоматически
получается комплект ссылок на новую сборку .NET 4.0 по имени Microsoft.CSharp.dll (в
этом легко убедиться, заглянув в папку References (Ссылки) в проводнике решений). Эта
очень маленькая библиотека определяет единственное пространство имен (Microsoft.
CSharp.RuntimeBinder) с двумя классами (рис. 18.2).
л {) Microsoft.CSharp.RuntimeBinder
i> *t$ RuntimeBinderException
t> *^J RuntimeBinderlntematCompilerException
:• -Ot mscorlib
I Assembly Microsoft.CSharp
C:\Program Files (x86)\Reference Assemblies I.
|\Microsoft\FrameworkVNF^rameworlc\v4-0
Рис. 18.2. C6opKaMicrosoft.CSharp.dll
Как можно догадаться по их именам, оба класса представляют собой строго
типизированные исключения. Более общий класс — RuntimeBinderException — представляет
ошибку, которая будет сгенерирована при попытке вызова несуществующего члена на да-
652 Часть IV. Программирование с использованием сборок .NET
намическом типе данных (как в случае методов toupper () и Foo ()). Та же ошибка будет
инициирована, если будут указаны неверные данные параметров для существующего члена.
Поскольку динамические данные столь изменчивы, каждый вызов члена на
переменной, объявленной с ключевым словом dynamic, должен быть помещен в
правильный блок try/catch, и предусмотрена соответствующая обработка ошибок.
static void InvokeMembersOnDynamicData ()
{
dynamic textDatal = "Hello";
try
{
Console.WriteLine(textDatal.ToUpper());
Console.WriteLine (textDatal.toupper() );
Console.WriteLine (textDatal .Foo A0, "ее", DateTime.Now));
}
catch (Microsoft.CSharp.RuntlmeBinder.RuntlmeBinderException ex)
{
Console.WriteLine(ex.Message);
}
}
Вызвав этот метод вновь, можно увидеть, что вызов ToUpper () (обратите внимание
на регистр Т" и "U") работает корректно, однако на консоль выводится следующее
сообщение об ошибке:
HELLO
'string1 does not contain a definition for 'toupper'
HELLO
'string' не содержит определения 'toupper '
Разумеется, процесс помещения всех динамических вызовов методов в блоки try/
catch довольно утомителен. Если вы тщательно следите за написанием кода и
передачей параметров, то это делать не обязательно. Однако перехват исключений удобен,
когда заранее не известно, будет ли член представлен в целевом типе.
Область применения ключевого слова dynamic
Вспомните, что неявно типизированные данные возможны только для локальных
переменных в области определения члена. Ключевое слово var никогда не может
использоваться в качестве возвращаемого значения, параметра или члена
класса/структуры. Однако это не касается ключевого слова dynamic. Взгляните на следующее
определение класса:
class VeryDynamicClass
{
// Поле dynamic.
private static dynamic myDynamicField;
// Свойство dynamic.
public dynamic DynamicProperty { get; set; }
// Тип возврата dynamic и тип параметра dynamic.
public dynamic DynamicMethod(dynamic dynamicParam)
{
// Локальная переменная dynamic.
dynamic dynamicLocalVar = "Local variable";
int mylnt = 10;
if (dynamicParam is int)
{
return dynamicLocalVar;
}
Глава 18. Динамические типы и исполняющая среда динамического языка 653
else
1
return mylnt;
}
}
}
Теперь можно вызывать общедоступные члены, как ожидалось, однако, при
оперировании с динамическими методами и свойствами нет полной уверенности в том,
каким именно будет тип данных! По правде говоря, определение VeryDynamicClass
может оказаться не особенно полезным в реальном приложении, но оно иллюстрирует
область применения ключевого слова dynamic.
Ограничения ключевого слова dynamic
Хотя с использованием ключевого слова dynamic можно определить очень много
вещей, с ним связаны свои ограничения. Хотя они не так уж существенны, имейте в
виду, что элементы динамических данных не могут использовать лямбда-выражения
или анонимные методы С# при вызове метода. Например, следующий код всегда
приводит к ошибке, даже если целевой метод на самом деле принимает параметр-делегат,
который, в свою очередь, принимает значение string и возвращает void:
dynamic a = GetDynamicObject () ;
// Ошибка! Методы на динамических данных не могут использовать лямбда-выражения1
a.Method(arg => Console.WriteLine (arg) ) ;
Чтобы обойти это ограничение, понадобится работать с лежащим в основе
делегатом напрямую, используя технику, описанную в главе 11 (анонимные методы и
лямбда-выражения, и т.д.). Другое ограничение состоит в том, что динамический элемент
данных не может воспринимать расширяющие методы (см. главу 12). К сожалению, это
касается также всех расширяющих методов из API-интерфейсов LINQ. Поэтому
переменная, объявленная с ключевым словом dynamic, имеет очень ограниченное
применение в рамках LINQ to Objects и других технологий LINQ:
dynamic a = GetDynamicObject () ;
// Ошибка1 Динамические данные не могут найти расширяющий метод Select () !
var data = from d in a select d;
Практическое применение ключевого слова dynamic
Учитывая тот факт, что динамические данные не являются строго типизированными,
не проверяются во время компиляции, не имеют возможности инициировать средство
IntelliSense и не могут быть целью запроса LINQ, совершенно корректно предположить,
что использование ключевого слова dynamic только потому, что оно существует — это
очень плохая программистская практика.
Однако в редких случаях ключевое слово dynamic может радикально сократить
объем кода, который придется вводить вручную. В частности, при построении приложения
.NET, которое интенсивно использует позднее связывание (через рефлексию), ключевое
слово dynamic может сэкономить время на наборе кода. Точно также, при разработке
приложения .NET, которое должно взаимодействовать с унаследованными
библиотеками СОМ (такими как продукты Microsoft Office), можно значительно упростить код за
счет применения ключевого слова dynamic.
Как с любым "сокращением", прежде чем его применять, необходимо взвесить все
"за" и "против". Применение ключевого слова dynamic — это компромисс между
краткостью кода и безопасностью типов. Хотя С# в основе своей является строго
типизированным языком, можно выбирать, стоит ли пользоваться динамическим поведением,
654 Часть IV. Программирование с использованием сборок .NET
от вызова к вызову. Помните, что вы не обязаны применять ключевое слово dynamic.
Всегда можно получить тот же конечный результат, написав альтернативный код
вручную (обычно существенно большего объема).
Исходный код. Проект DynamicKeyword доступен в подкаталоге Chapter 18.
Роль исполняющей среды динамического языка (DLR)
Теперь, когда прояснилась суть "динамических данных", давайте исследуем, как
они обрабатываются. В версии .NET 4.0 общеязыковая исполняющая среда (Common
Language Runtime — CLR) получила дополняющую среду времени выполнения, которая
называется исполняющей средой динамического языка (Dynamic Language Runtime —
DLR). Концепция "динамической исполняющей среды" определенно не нова. На самом
деле ее много лет используют такие языки программирования, как Smalltalk, LISP, Ruby
и Python. В основе своей динамическая исполняющая среда предоставляет
динамическим языкам возможность обнаруживать типы целиком во время выполнения, без
каких-либо проверок при компиляции.
При наличии опыта работы со строго типизированными языками (включая С# без
динамических типов), может показаться нежелательным само понятие такой
исполняющей среды. В конце концов, обычно, когда только возможно, лучше получать ошибки
во время компиляции, а не во время выполнения. Тем не менее, динамические языки и
исполняющие среды предлагают ряд интересных возможностей, включая
перечисленные ниже.
• Исключительно гибкая кодовая база. Можно изменять код, не внося
многочисленных модификаций в типы данных.
• Очень простой способ взаимодействия с разнообразными типами объектов,
построенными на разных платформах и языках программирования.
• Способ добавления или удаления членов типа в памяти во время выполнения.
Роль DLR состоит в том, чтобы позволить различным динамическим языка работать
с исполняющей средой .NET и предоставлять им возможность взаимодействия с
другим кодом .NET. Два популярных динамических языка, которые используют DLR — это
IronPython и IronRuby. Эти языки живут в "динамической вселенной", где типы
определяются исключительно во время выполнения. К тому же эти языки имеют доступ ко
всему богатству библиотек базовых классов .NET Еще лучше то, что их кодовая база
может взаимодействовать с С# (и наоборот), благодаря включению ключевого слова
dynamic.
Роль деревьев выражений
Среда DLR использует деревья выражений для описания динамического вызова в
нейтральных терминах. Например, когда DLR встречает код С#, подобный следующему:
dynamic d = GetSomeData ();
d.SuperMethodA2);
то автоматически строит дерево выражения, которое, по сути, гласит: "Вызвать метод
по имени SuperMethod на объекте d, передав 12 в качестве аргумента". Эта
информация (формально называемая рабочей нагрузкой (payload)) затем передается корректному
средству привязки времени выполнения, которое, опять-таки, может быть
динамическим средством привязки С#, динамическим средством привязки IronPython или даже
(как будет показано ниже) унаследованными объектами СОМ.
Глава 18. Динамические типы и исполняющая среда динамического языка 655
Отсюда запрос отображается на необходимую структуру вызовов для целевого
объекта. Замечательным в деревьях выражений является то (помимо того факта, что вам
не нужно создавать их вручную), что они позволяют нам написать фиксированный
оператор кода С#, не заботясь о том, что собой представляет его реальная цель (объект
СОМ, код IronPython или IronRuby, и т.п.). На рис. 18.3 иллюстрируется концепция
деревьев выражений на наивысшем уровне.
dynamic d = GetSomeData ();
d.SuperMethodA2);
Средство
привязки COM
Средство
привязки
IronRuby
или IronPython
Средство
привязки .NET
Дерево
выражения
Исполняющая среда
динамического языка (DLR) NET
Общеязыковая исполняющая среда (CLR) .NET
Рис. 18.3. Деревья выражений фиксируют динамические вызовы в нейтральных
терминах и обрабатываются средствами привязки
Роль пространства имен System.Dynamic
В версии .NET 4.0 к сборке System.Core.dll было добавлено пространство имен
System.Dynamic. По правде говоря, шансы, что вам когда-либо придется
непосредственно использовать типы из этого пространства имен, весьма невелики. Однако если
вы — разработчик языка, который желает обеспечить своему динамическому языку
возможность взаимодействия с DLR, пространство имен System.Dynamic пригодится
для построения специального средства привязки времени выполнения.
Подробные сведения о типах System.Dynamic можно найти в документации .NET
Framework 4.0 SDK. Для практических нужд просто знайте, что это пространство имен
предоставляет необходимую инфраструктуру, позволяющую обеспечить динамическим
языкам взаимодействие с .NET.
Динамический поиск в деревьях выражений во время выполнения
Как уже объяснялось, среда DLR передает деревья выражений целевому объекту,
однако на этот процесс оказывает влияние несколько факторов. Если динамический тип
данных указывает в памяти на объект СОМ, то дерево выражения посылается
низкоуровневому интерфейсу СОМ по имени IDispatch. Как вам может быть известно, этот
интерфейс представляет собой способ, которым СОМ включает собственный набор
динамических служб. Объекты СОМ, однако, могут использоваться в приложении .NET без
применения DLR или ключевого слова С# dynamic. Однако это (как вы убедитесь) ведет
к более сложному кодированию С#.
Если динамические данные не указывают на объект СОМ, то дерево выражения
может быть передано объекту, реализующему интерфейс IDynamicObject. Этот
интерфейс используется "за кулисами", чтобы позволить такому языку, как IronRuby, принять
дерево выражения DLR и отобразить его на специфику языка Ruby.
Наконец, если динамические данные указывают на объект, который не
является объектом СОМ и не реализует интерфейс IDynamic Object, то это — нормальный,
656 Часть IV. Программирование с использованием сборок .NET
повседневный объект .NET. В этом случае дерево выражения передается на обработку
средству привязки исполняющей среды С#. Процесс отображения дерева выражений на
специфику .NET включает участие служб рефлексии.
Как только дерево выражения обработано определенным средством привязки,
динамические данные разрешаются в реальный тип данных в памяти, после чего
вызывается корректный метод со всеми необходимыми параметрами. Теперь давайте
рассмотрим несколько практических применений DLR, начав с упрощения вызовов позднего
связывания .NET
Упрощение вызовов позднего связывания
с использованием динамических типов
Одним из случаев, когда имеет смысл использовать ключевое слово dynamic, может
быть работа со службами рефлексии, а именно — выполнение вызовов методов
позднего связывания. В главе 15 приводилось несколько примеров, когда такого рода
вызовы методов могут быть очень полезны — чаще всего при построении расширяемого
приложения. Там было показано, как использовать метод Activator.CreatelnstanceO
для создания экземпляра object, о котором ничего не известно во время компиляции
(помимо его отображаемого имени). Затем с помощью типов из пространства имен
System.Reflection можно обращаться к членам через механизм позднего связывания.
Вспомните следующий пример из главы 15.
static void CreateUsingLateBinding(Assembly asm)
{
try
{
// Получение метаданных типа Minivan.
Type miniVan = asm.GetType("CarLibrary.MiniVan");
// Создание экземпляра Minivan на лету.
object ob] = Activator.Createlnstance(miniVan);
// Получение информации о TurboBoost.
Methodlnfo mi = miniVan.GetMethod("TurboBoost");
// Вызов метода (null означает отсутствие параметров).
mi.Invoke(obj, null);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Хотя этот код работает, как и ожидалось, нельзя не отметить его громоздкость. Здесь
приходится вручную создавать класс Methodlnfo, вручную запрашивать метаданные
и т.д. Ниже приведена версия этого метода, в которой используется ключевое слово
dynamic и DLR:
static void InvokeMethodWithDynamicKeyword(Assembly asm)
{
try
{
// Получение метаданных типа Minivan.
Type miniVan = asm. GetType ( "CarLibrary .MiniVan11) ;
// Создание экземпляра Minivan на лету и вызов метода,
dynamic obj = Activator.Createlnstance(miniVan);
ob].TurboBoost ();
}
Глава 18. Динамические типы и исполняющая среда динамического языка 657
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Объявив переменную obj с ключевым словом dynamic, всю рутинную работу,
связанную с рефлексией, вы возлагаете на среду DLR!
Использование ключевого слова dynamic для передачи аргументов
Польза от DLR становится еще более очевидной, когда нужно выполнять связанные
вызовы методов, принимающих параметры. Когда используются "длинные" вызовы
рефлексии, аргументы приходится упаковывать в массив элементов object, который
передается методу Invoke () класса Methodlnfo.
Чтобы проиллюстрировать это на примере, создадим новое консольное приложение
С# по имени LateBindingWithDynamic. Добавим к текущему решению проект
библиотеки классов (используя пункт меню File^Add^New Project (Файл1^Добавить1^Новый
проект)) и назовите его MathLibrary. Переименуйте начальный класс Classl.cs в
проекте MathLibrary на SimplaMath.cs и реализуйте класс, как показано ниже:
public class SimpleMath
{
public int Add(int x, int y)
{ return x + y; }
}
Скомпилировав c6opKyMathLibrary.dll, поместите ее копию в папку bin\Debug
проекта LateBindingWithDynamic (щелкнув на кнопке Show All Files (Показать все файлы)
для каждого проекта в Solution Explorer, можно просто перетащить файл между
проектами). После этого окно Solution Explorer должно выглядеть примерно как на рис. 18.4.
^ Solution LateBindingWrthDynamic' B projects)
л .J] LateBindingWithDynamic
t> al Properties
> 03 References
л £> bin
л ''£% Debug
_j LateBindingWithDynamic.exe
j LateBindingWithDynamic,pdb
Lj LateBindingWithDynamic.vshost.exe
j LateBindingWithDynamic.vshostexe.manifest
MathUbrary.dll I
;> Cj obj
cj3 Program.cs
л *yj3 MathLibrary
i> Ш Properties
:> _yi References
л rj bin
л ;.__/ Debug
J MathUbrary.dll
J MarthLibrary.pdb
t> Hj Release
_j obj
cjy SimpleMath.es
Рис. 18.4. Проект LateBindingWithDynamic имеет
приватную копию сборки MathLibrary.dll
658 Часть IV. Программирование с использованием сборок .NET
На заметку! Помните, что главная цель позднего связывания — позволить приложению создать
объект, который не имеет записи MANIFEST. Именно поэтому нужно вручную скопировать
сборку MathLibrary.dll в выходную папку консольного проекта, а не устанавливать
ссылку на сборку через Visual Studio.
Теперь импортируем пространство имен System.Reflection а файл Program.cs
проекта консольного приложения. Добавим следующий метод в класс Program, который
вызывает метод Add(), используя типичные вызовы API-интерфейса рефлексии:
private static void AddWithReflection ()
{
Assembly asm = Assembly.Load("MathLibrary");
try
{
// Получение метаданных типа SimpleMath.
Type math = asm.GetType("MathLibrary.SimpleMath");
// Создание экземпляра SimpleMath на лету,
object obj = Activator.Createlnstance(math);
// Получение информации для метода Add.
Methodlnfo mi = math.GetMethod("Add");
// Вызов метода (с параметрами).
object[] args = { 10, 70 };
Console.WriteLine("Result is: {0}", mi.Invoke(obj, args)); // вывод результата
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Ниже показано, как предыдущая логика метода упрощается за счет использования
ключевого слова dynamic:
private static void AddWithDynamic ()
{
Assembly asm = Assembly.Load("MathLibrary");
try
{
// Получение метаданных типа SimpleMath.
Type math = asm.GetType("MathLibrary.SimpleMath");
// Создание экземпляра SimpleMath на лету.
dynamic obj = Activator.Createlnstance(math);
Console.WriteLine("Result is: {0}", obj.AddA0, 70)); // вывод результата
}
catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException ex)
{
Console.WriteLine(ex.Message);
}
}
Выглядит неплохо! Вызов обоих методов в Main() дает идентичный вывод. Однако
при использовании ключевого слова dynamic сокращается объем работ по
кодированию. Для динамически определенных данных больше не нужно вручную упаковывать
аргументы в массив object, запрашивать метаданные сборки и иметь дело с прочими
деталями подобного рода.
Исходный код. Проект LastBindingWithDynamic доступен в подкаталоге Chapter 18.
Глава 18. Динамические типы и исполняющая среда динамического языка 659
Упрощение взаимодействия с СОМ
посредством динамических данных
Теперь давайте рассмотрим другое полезное применение ключевого слова dynamic —
в контексте проекта взаимодействия с СОМ. Если нет опыта разработки для СОМ, то
имейте в виду, что следующий пример, в котором компилируется библиотека СОМ,
содержит метаданные, подобно библиотеке .NET. Тем не менее, ее формат совершенно
отличается. По этой причине если программа .NET нуждается в использовании объекта
СОМ, то первое, что нужно сделать — это сгенерировать то, что называется "сборкой
взаимодействия" (interop assembly), используя Visual Studio 2010. Делается это
довольно легко. Просто откройте диалоговое окно Add Reference (Добавить ссылку),
перейдите на вкладку СОМ и найдите библиотеку СОМ, которую необходимо использовать
(рис. 18.5).
I
.NET j COM i Projects Его лse j Recent
Component Name
{ AccessibilityCplAdmin 1.0 Type ...
Active DS Type Library
AgControl 3.0 Type Library
AP Client 1.0 HelpPane Type Libr..
AP Client 1.0 Type Library
AppIdPcficyEngineApi 1.0 Type ,..
1 Application Host Administration...
I Assistance Platform Client .1.0 Da...
ATL 2.0 Type Library
ATLContactPicker 1.0 Type Library
AxBrowse
4 '"
Type
1.0
1.0
3 0
10
1.0
10
1.0
1,0
1.0
1.0
1 j
Lib Version
...-,--
Path
. mv,ti ,дтг ■,-
C\W«MfcwtfS-.ste
C:\Windows\SysW
c:\Prcgrarn Files (x
C:\Windows\Sy5te
C:\Windows\Helpl
C:\Windows\Syste
C:\Windows\systei
C:\Wind owsXSyste
C:\Windowc\SysW
C:\Program Files (>
C:\Program Files {> •
Ш
: J
Рис. 18.5. На вкладке COM диалогового окна Add Reference
отображаются все зарегистрированные на машине библиотеки СОМ
В случае выбора библиотеки СОМ среда Visual Studio 2010 отреагирует генерацией
совершенно нового описания .NET для метаданных СОМ. Формально они называются
"сборками взаимодействия" и не содержат никакого кода реализации, помимо самого
минимума, который помогает транслировать события СОМ в события .NET. Однако
эти сборки взаимодействия очень полезны в том, что защищают кодовую базу .NET от
сложностей внутреннего механизма СОМ.
В коде С# можно напрямую работать со сборкой взаимодействия, позволяя CLR
(и DLR, если используется ключевое слово dynamic) автоматически отображать типы
данных .NET на типы СОМ и наоборот. "За кулисами" данные маршализуются между
приложениями .NET и СОМ с использованием оболочки исполняющей среды (Runtime
Callable Wrapper — RCW), которая на самом деле является динамически
сгенерированным прокси. RCW маршализует и трансформирует типы данных .NET в типы СОМ,
изменяя счетчик ссылок СОМ-объекта и отображая все возвращаемые СОМ значения
на их эквиваленты в .NET.
На рис. 18.6 показана общая картина взаимодействия .NET с СОМ.
«i Add Referem
660 Часть IV. Программирование с использованием сборок .NET
Неуправляемый
СОМ-объект
Рис. 18.6. Программы .NET взаимодействуют с объектами СОМ,
используя прокси под названием RCW
Роль первичных сборок взаимодействия
Многие поставщики библиотек СОМ (таких как библиотеки Microsoft COM,
обеспечивающие доступ к объектной модели продуктов Microsoft Office), предоставляют
"официальную" сборку взаимодействия, которая называется первичной сборкой
взаимодействия (primary interop assembly — PIA). Сборки PIA — это оптимизированные сборки
взаимодействия, которые делают яснее (и возможно, расширяют) код, обычно
генерируемый при ссылке на библиотеку СОМ через диалоговое окно Add Reference.
Сборки PIA обычно перечисляются на вкладке .NET диалогового окна Add Reference,
подобно базовым библиотекам .NET. Фактически, если вы ссылаетесь на библиотеку
СОМ из вкладки СОМ диалогового окна Add Reference, то Visual Studio не
генерирует новой библиотеки взаимодействия, как делает это обычно, а вместо этого
использует предоставленную сборку PIA. На рис. 18.7 показана сборка PIA объектной модели
Microsoft Office Excel, которая будет использоваться в следующем примере.
Component Name
Microsoft, mshtml
Microsoft.Office.lnterop.Access
[ Microsoft.OfficeJnteroD.Excel
Microsoft. Off ice Jnterop.FrontPage
Microsoft.Office.Interop.FrontPageEditor
Microsoft.OfficeJnterop.Graph
Microsoft. OfficeJnterop.InfoPath
Microsoft.OfficeJnterop.InfoPath.SemiTrust
Microsoft.Office.Interop.InfoPath.Xml
Microsoft.Office.Interop.MSProject
Microsoft.OfficeJnterop.Outtook
«' I __J
Version ж
7.0.3300.0
11.0.0.0
11.0,0,0
11.0.0.0
11.0.0.0
11.0.0.0
11.0.0.0
11.0.0.0
11.0.0.0
11.0.0.0
11.0.0.0
OK
Cancel
Рис. 18.7. Сборки PIA перечислены на вкладке .NET диалогового окна Add Reference
Глава 18. Динамические типы и исполняющая среда динамического языка 661
Встраивание метаданных взаимодействия
До появления .NET 4.0, когда приложение С# использовало библиотеку СОМ (в виде
сборки PIA или нет), нужно было обеспечить наличие на клиентской машине копии
сборки взаимодействия. Это увеличивало размер установочного пакета приложения, к
тому же в сценарии установки должно было проверяться существование сборки PIA и в
случае ее отсутствия — установка копии в GAC.
Однако в .NET 4.0 теперь можно встраивать данные взаимодействия
непосредственно в скомпилированное приложение .NET. В этом случае поставлять копию сборки
взаимодействия вместе с приложением .NET необязательно, поскольку все необходимые
метаданные взаимодействия жестко встраиваются в приложение .NET.
По умолчанию, после выбора библиотеки COM (PIA или нет) в диалоговом окне Add
Reference интегрированная среда разработки автоматически устанавливает
свойство Embed Interop Types (Встраивать типы взаимодействия) библиотеки в True. Чтобы
увидеть эту установку, необходимо выбрать ссылаемую сборку взаимодействия в папке
References (Ссылки) окна Solution Explorer и открыть ее окно свойств (рис. 18.8).
[Properties
1 Microsof t.Of fice.Interop
(Name)
Aliases
Copy Local
Culture
Description
Bi!H EnitWpMiwni
File Type
1 Identity
Path
Reserved
Runtime Version
Specific Version
Excel Reference Properties
1 Embed Interop Types
1 Indicates whether types defined
1 target assembry.
Microsoft.Off ice.Interop.Exce!
global
False
H True
Assembry
Microsoft.Off iceJnterop.Excel
C:\Program Files (xS6)\Microsoft
True
vl 1.4322
True
* nxl
irr 1
Щ
1
n this assembry will be embedded into the
Рис. 18.8. Среда .NET 4.0 позволяет встраивать часть используемых
сборок взаимодействия в создаваемую сборку .NET
Компилятор С# включит только те части библиотеки взаимодействия, которые
действительно используются. Таким образом, даже если реальная библиотека взаимодействия
содержит .NET-описания сотен СОМ-объектов, будут получены определения только
подмножества, которое действительно используется в написанном коде С#. Помимо
сокращения размеров приложения, поставляемого клиенту, также упрощается процесс
установки, поскольку не понадобится копировать лишние сборки PIA на целевую машину.
Общие сложности взаимодействия с СОМ
До выхода версии .NET 4.0 при написании кода С#, имеющего дело с библиотекой
СОМ (через сборку взаимодействия), неизбежно возникало множество сложностей.
Например, многие С ОМ-библиотеки определяют методы, принимающие необязательные
аргументы, что вплоть до нынешнего выпуска в С# не поддерживалось. Это требовало
указания значения Type.Missing для каждого появления необязательного аргумента.
Например, если метод СОМ принимал пять аргументов, и все они были необязательны,
приходилось писать следующий код С#, чтобы принять значения по умолчанию:
662 Часть IV. Программирование с использованием сборок .NET
myComObj.SomeMethod(
Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing) ;
К счастью, в .NET 4.0 теперь можно писать упрощенный код, учитывая, что
значения Type. Mis sing будут вставлены во время компиляции, если не указано реальное
значение:
myComObj.SomeMethod() ;
В связи с этим стоит отметить, что многие методы СОМ предлагают поддержку
именованных аргументов, которые, как было показано в главе 4, позволяют передавать
значения членам в любом порядке. Поскольку в .NET 4.0 язык С# поддерживает и это
средство, можно очень просто "пропускать" множество необязательных аргументов и
устанавливать только те, которые важны в данном случае.
Другая сложность взаимодействия с СОМ была связана с тем фактом, что многие
методы СОМ спроектированы так, чтобы принимать и возвращать очень
специфический тип данных по имени Variant. Во многом подобный ключевому слову С# dynamic,
типу данных Variant может быть присвоен любой тип данных СОМ на лету (строка,
интерфейсная ссылка, числовое значение и т.п.). До появления ключевого слова dynamic
передача или прием элементов данных типа Variant требовал значительных
ухищрений, обычно связанных с многочисленными операциями приведения.
С появлением .NET 4.0 и Visual Studio 2010, кода свойство Embed Interop Types
устанавливается в True, все типы Variant из СОМ автоматически отображаются на
динамические данные. Это не только сокращает потребность в излишних операциях
приведения при работе с типом данных Variant, но также еще более скрывает некоторые
сложности, присущие СОМ, вроде работы с индексаторами СОМ.
Для того чтобы продемонстрировать упрощение взаимодействия с СОМ за счет
совместной работы необязательных аргументов, именованных аргументов и ключевого
слово dynamic в С#, построим приложение, в котором используется объектная модель
Microsoft Office. При работе с этим примером вы получите шанс применить новые
средства, а также обойтись без них, и затем сравнить объем работ в обоих случаях.
На заметку! В предыдущих изданиях этой книги детально рассматривалась работа с
унаследованными объектами СОМ в проектах .NET с использованием пространства имен System.
InteropServices. В нынешнем издании это не описано, поскольку ключевое слово dynamic
обеспечивает гораздо более простое взаимодействие с объектами СОМ. Исчерпывающие
сведения о взаимодействии с объектами СОМ в строго типизированной (длинной) нотации могут
быть найдены в документации .NET Framework 4.0 SDK.
Взаимодействие с СОМ с использованием
средств языка С# 4.0
Предположим, что имеется приложение Windows Forms с графическим
интерфейсом пользователя (ExportDataToOfficeApp), главная форма которого определяет
элемент управления DataGridView по имени dataGridCars. В той же форме находятся
два элемента управления Button, один из которых обеспечивает открытие диалогового
окна для вставки новой строки данных в сетку, а другой отвечает за экспорт данных
сетки в электронную таблицу Excel. Учитывая тот факт, что приложение Excel
предоставляет программную модель через СОМ, к ней можно привязаться с использованием
уровня взаимодействия. На рис. 18.9 показан завершенный графический интерфейс
пользователя.
Глава 18. Динамические типы и исполняющая среда динамического языка 663
Add New Entry to Inventory
Рис. 18.9. Графический интерфейс пользователя для примера взаимодействия с СОМ
Сетка будет заполняться некоторыми начальными данными в обработчике
события Load формы (класс Саг, используемый в качестве параметра типа для обобщенного
List<T> — это простой класс в проекте, имеющий свойства Color, Make и PetName):
public partial class MainForm : Form
{
List<Car> carsInStock = null;
public MainForm ()
{
InitializeComponent();
}
private void MainForm_Load(object sender, EventArgs e)
{
carsInStock = new List<Car>
{
new Car {Color="Green", Make=,,VW", PetName=,,Mary" },
new Car {Color="Red", Make="Saab", PetName="Mel" },
new Car {Color=,,BlackM , Make="FordM , PetName="Hank" },
new Car {Color="YellowM, Make="BMW", PetName^'Dav/ie"}
};
UpdateGridO ;
}
private void UpdateGridO
{
// Сбросить источник данных.
dataGridCars.DataSource = null;
dataGridCars.DataSource = carsInStock;
}
}
В обработчике события Click кнопки Add New Entry to Inventory (Добавить новую
запись в инвентарную ведомость) открывается специальное диалоговое окно, которое
позволяет пользователю ввести новые данные для объекта Саг; после щелчка на кнопке
О К данные добавляются в сетку. Код этого диалогового окна в книге не показан,
поэтому за подробностями обращайтесь к доступному решению. Если хотите повторить
пример самостоятельно, включите файлы NewCarDialog.es, IlewCarDialog.designer.cs и
NewCarDialog.resx в проект (их можно найти в составе кода примеров для этой главы).
Затем реализуйте обработчик щелчка на кнопке Add New Entry to Inventory, как
показано ниже:
664 Часть IV. Программирование с использованием сборок .NET
private void btnAddNewCar_Click(object sender, EventArgs e)
{
NewCarDialog d = new NewCarDialog();
if (d.ShowDialogO == DialogResult .OK)
{
// Добавить новый автомобиль в список.
carsInStock.Add(d.theCar);
UpdateGridO ;
}
}
Ядром этого примера является обработчик события Click для кнопки Export Current
Inventory to Excel (Экспортировать текущую инвентарную ведомость в Excel). На вкладке
.NET диалогового окна Add Reference добавьте ссылку на первичную сборку
взаимодействия Microsoft.Office.Interop.Excel.dll (как было показано ранее на рис. 18.7).
Добавьте приведенный ниже псевдоним пространства имен в главный файл кода
формы. Имейте в виду, что при взаимодействии с библиотеками СОМ псевдоним
определять не обязательно. Однако, поступив так, вы получите удобный квалификатор для
всех импортированных объектов СОМ, что очень пригодится, если некоторые из этих
СОМ-объектов будут иметь имена, конфликтующие с типами .NET.
// Создать псевдоним для объектной модели Excel,
using Excel = Microsoft.Office.Interop.Excel;
Реализуйте следующий обработчик события Click, чтобы он вызывал
вспомогательную функцию по имени ExportToExcel():
private void btnExportToExcel_Click(object sender, EventArgs e)
{
ExportToExcel(carsInStock);
}
Поскольку библиотека COM была импортирована в Visual Studio 2010, сборка PIA
автоматически сконфигурирована так, что используемые метаданные будут включены
в приложение .NET (вспомните роль свойства Embed Interop Types). Таким образом, все
СОМ-типы Variant будут реализованы как типы данных dynamic. Более того, поскольку
код пишется на С# 4.0, можно использовать необязательные и именованные аргументы.
С учетом всего сказанного вот как будет выглядеть реализация ExportToExcel():
static void ExportToExcel(List<Car> carsInStock)
{
// Загрузить Excel, затем создать новую пустую рабочую книгу.
Excel.Application excelApp = new Excel.Application();
excelApp.Workbooks.Add();
// В этом примере используется единственная рабочий лист.
Excel._Worksheet worksheet = excelApp.ActiveSheet;
// Установить заголовки столбцов в ячейках.
worksheet.Cells [1, "А"] = "Make";
worksheet.Cells[1, "В"] = "Color";
worksheet.Cells [1, "C"] = "Pet Name";
// Отобразить все данные в List<Car> на ячейки электронной таблицы.
int row = 1;
foreach (Car c in carsInStock)
{
row++;
worksheet.Cells [row, "A"] = c.Make;
worksheet.Cells [row, "B"] = c.Color;
worksheet.Cells [row, "C"] = c.PetName;
}
Глава 18. Динамические типы и исполняющая среда динамического языка 665
// Придадить симпатичный вид табличным данным.
worksheet.Range["Al"].AutoFormat(
Excel.XIRangeAutoFormat.xlRangeAutoFormatClassic2);
// Сохранить файл, выйти из Excel и отобразить сообщение пользователю.
worksheet.SaveAs(string.Format(@"{0}\lnventory .xlsx11, Environment.CurrentDirectory) );
excelApp.Quit();
MessageBox.Show("The Inventory.xslx file has been saved to your app folder",
"Export complete1"); // файл Inventory.xslx сохранен в папке приложения
1
Метод начинается с загрузки приложения Excel в память, однако на рабочем столе
компьютера оно не покажется. В данном приложении интересует только использование
объектной модели Excel. Если же необходимо отобразить пользовательский интерфейс
Excel, дополните метод следующей строкой кода:
static void ExportToExcel(List<Car> carsInStock)
{
// Загрузить Excel, затем создать новую пустую рабочую книгу.
Excel.Application excelApp = new Excel.Application ();
// Сделать приложение Excel видимым.
excelApp.Visible = true;
}
После создания пустого рабочего листа к нему добавляются три столбца, названные
по именам свойств класса Саг. После этого ячейки заполняются данными List<Car> и
файл сохраняется под жестко закодированным именем Inventory.xlsx.
Если теперь запустить приложение, добавить несколько записей и экспортировать их
в Excel, в папке bin\Debug приложения Windows Forms появится файл Inventory.xlsx,
который можно открыть в приложении Excel (рис. 18.10).
p^N Щ *? - С* » Inventory .xlsx -
—* Home Insert Page Layout Formulas
rd** Л Calibri -11 - S ш ш
--J 4J В / П ' А' а' Ж Ш Ш Ш
Microsoft Excel
Data Review View Team
General - /L ^Insert '
$ * % f j 3* Delete -
. пя Styles ,.«.,
Тб8 i% - ^Format-
H X
V _ Я У
z * fr-
2*
2 VW Green Mary
3 Saab Red Mel
4 ^'ord Black Hank
5 BMW Yellow Davie
| 6 Saab Sliver Melvtn
7
8
С
н < > м Sheetl Sheet2! / "shecitTT'tJ,
Ready
Рис. 18.10. Экспортированные данные в файле Excel
666 Часть IV. Программирование с использованием сборок .NET
Взаимодействие с СОМ без использования средств языка С# 4.0
Если теперь выбрать сборку Microsoft.Office.Interop.Excel.dll в Solution
Explorer и установить свойство Embed Interop Type в False, появятся сообщения об
ошибках компиляции, поскольку СОМ-данные Variant будут трактоваться не как
динамические данные, а как переменные System.Object. Это потребует добавления в
ExportToExcelO нескольких операций приведения. Кроме того, если проект
скомпилировать в версии Visual Studio 2008, утратятся преимущества
необязательных/именованных параметров, и в этом случае понадобится явно помечать все пропущенные
аргументы. Ниже показана версия метода ExportToExcelO для ранних версий С#.
static void ExportToExcel2008(List<Car> carsInStock)
{
Excel.Application excelApp = new Excel.Application();
// Нужно пометить пропущенные параметры!
excelApp.Workbooks.Add(Type.Missing);
// Нужно привести Object к _Worksheet!
Excel._Worksheet worksheet = (Excel._Worksheet)excelApp.ActiveSheet;
// Нужно привести каждый Object к объекту Range,
// затем вызвать низкоуровневое свойство Value2'
((Excel.Range)excelApp.Cells[1, "A"]).Value2 = "Make";
((Excel.Range)excelApp.Cells [1, "B"]).Value2 = "Color";
((Excel.Range)exc3lApp.Cells[l, "C"]).Value2 = "Pet Name";
int row = 1;
foreach (Car с in carsInStock)
{
row++;
// Нужно привести каждый Object к объекту Range,
//и вызвать низкоуровневое свойство Value2!
( (Excel.Pange)worksheet.Cells[row, "A"]).Value2 = c.Make;
((Excel.Range)worksheet.Cells[row, "B"]).Value2 = c.Color;
((Excel.Range)worksheet.Cells[row, "C"]).Value2 = c.PetName;
}
// Нужно вызвать метод get_Range и укаэаать все пропущенные аргументы1
excelApp.get_Range ("Al", Type.Missing) .AutcFormat(
Excel.XIPangeAutoFormat.xlRangeAutoFormatClassic2,
Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing, Type.Missing);
// Нужно указать все пропущенные аргументы*
worksheet.SaveAs(string.Format(@"{0}\Inventory.xlsx", Environment.CurrentDirectory),
Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing, Type.Missing, Type.Missing);
excelApp.Quit();
MessageBox.Show("The Inventory.xslx file has been saved to your app folder",
"Export complete1"); // файл Inventory.xs]а сохранен в папке приложения
}
Хотя конечный результат идентичен, очевидно, что данная версия метода намного
более многословна. К тому же, поскольку ранние версии С# (точнее, предшествовавшие
.NET 4.0) не позволяют встраивать данные взаимодействия СОМ, обнаружится, что
выходная папка теперь содержит локальные копии множества сборок взаимодействия,
которые должны будут поставлены на машину конечного пользователя (рис. 18.11).
Глава 18. Динамические типы и исполняющая среда динамического языка 667
£3 Solution ExportDataToOfficeApp' 0. project)
л .J] ExportDataToOfficeApp
jj Properties
E» .^ References
j 2^ bin
л \ry Debug
_j ExportDataToOfficeApp.exe
■ j ExportDataToOfficeApp.pdb
j ExportDataToOfficeApp.vshost.exe
j ExportDataToOfficeApp.vshost.exe.manrfest
j Inventory.xlsx
■ Microscft.Gffice.Interop.Efccel.xml
x Microsoft.7be.Interop.dll
[Л officcdlli
1 rjj office.*mI j
!> Pj Release
> Cj obj
CJ^ Car.cs
j> £§] MainForm.cs
t> j£gj NewCarDialog.es
=jjj Program.cs
Рис. 18.11. Если данные взаимодействия не встроены, потребуется
поставлять автономные сборки взаимодействия
На этом рассмотрение ключевого слова С# dynamic и среды DLR завершено.
Наверняка вы смогли оценить, насколько новые средства .NET 4.0 могут упростить
решение сложных задач программирования, и (что возможно, более важно) поняли
сопутствующие компромиссы. Выбор в пользу динамических данных приводит к утере
безопасности типов, поэтому код становится уязвимым для гораздо большего числа ошибок
времени выполнения.
Исходный код. Проект ExportDataToOfficeApp доступен в подкаталоге Chapter 18.
Резюме
Ключевое слово dynamic в С# 4.0 позволяет определять данные, истинная
идентичность которых не известна вплоть до времени выполнения. При работе новой
исполняющей среды динамического языка (DLR) автоматически создаваемое "дерево
выражения" передается соответствующему средству привязки динамического языка, причем
рабочие данные будут распакованы и отправлены корректному члену объекта.
За счет использования динамических данных и DLR многие сложные задачи
программирования С# могут быть радикально упрощены, а особенно — включение
библиотек СОМ в приложения .NET. Кроме того, как было показано в этой главе, .NET 4.0
предлагает ряд дальнейших упрощений взаимодействия с СОМ (которые не имеют
отношения к динамическим данным) — встраивание данных взаимодействия СОМ в
разрабатываемые приложения, а также необязательные и именованные аргументы.
Хотя все эти средства могут упростить код, не забывайте, что динамические
данные существенно снижают безопасность кода С# в отношении типов и открывают путь
для ошибок времени выполнения. Поэтому тщательно взвешивайте все "за" и "против"
использования динамических данных в проектах С# и соответствующим образом
тестируйте их.
ЧАСТЬ V
Введение в библиотеки
базовых классов .NET
В этой части...
Глава 19. Многопоточность и параллельное программирование
Глава 20. Файловый ввод-вывод и сериализация объектов
Глава 21. ADO.NET, часть I: подключенный уровень
Глава 22. ADO.NET, часть II: автономный уровень
Глава 23. ADO.NET Часть III: Entity Framework
Глава 24. Введение в LINQ to XML
Глава 25. Введение в Windows Communication Foundation
Глава 26. Введение в Windows Workflow Foundation 4.0
ГЛАВА 19
Многопоточность
и параллельное
программирование
Вряд ли многим нравится работать с приложением, которое притормаживает во
время выполнения. Более того, никому не понравится, когда запуск некоторой
задачи в приложении (возможно, по щелчку на элементе панели инструментов)
снижает "отзывчивость" других частей приложения. До появления нынешней версии .NET
построение приложении, способных выполнять несколько задач, требовало написания
очень сложного кода и применения API-интерфейсов многопоточности Windows. К
счастью, платформа .NET предоставила множество способов построения программного
обеспечения, которое может решать очень сложные задачи по уникальным путям
выполнения, с минимальными усилиями со стороны разработчика.
Эта глава начинается с обращения к уже известному типу делегата .NET, чтобы
исследовать его внутреннюю поддержку асинхронных вызовов методов. Как вы увидите,
эта техника позволяет вызывать метод во вторичном потоке выполнения, без
необходимости самостоятельно создавать и конфигурировать поток.
Затем будет представлено пространство имен System.Threading. Вы ознакомитесь
с многочисленными типами (Thread, ThreadStart и т.п.), которые позволяют легко
создавать дополнительные потоки выполнения. Кроме того, вы узнаете о различных
примитивах синхронизации, предоставляемых .NET Framework, которые обеспечивают
разделение общих данных несколькими потоками в неизменчивой манере.
В конце главы будет представлена совершенно новая библиотека .NET Task Parallel
Library (TPL) и технология PLINQ (Parallel LINQ). Эти API-интерфейсы предоставляют для
программирования набор высокоуровневых типов, которые отлично справляются с
деталями управления потоками.
Отношения между процессом, доменом
приложения, контекстом и потоком
В главе 16 поток (thread) был определен как путь выполнения внутри
исполняемого приложения. Хотя многие приложения .NET могут успешно и продуктивно работать,
будучи однопоточными, первичный поток сборки (запускаемый CLR при выполнении
MainO) может создавать вторичные потоки для выполнения дополнительных единиц
работы. За счет реализации дополнительных потоков можно строить более отзывчивые
(но не обязательно быстрее выполняемые на одноядерных машинах) приложения.
Глава 19. Многопоточность и параллельное программирование 671
На заметку! В наши дни очень многие компьютеры имеют более одного процессора (или, по
крайней мере, гиперпотоковые одноядерные процессоры). Не запуская несколько потоков,
использовать на полную мощность многоядерные машины не удастся.
Пространство имен System.Threading содержит различные типы, позволяющие
создавать многопоточные приложения. Пожалуй, главным среди них является класс
Thread, поскольку он представляет отдельный поток. Чтобы программно получить
ссылку на поток, выполняемый конкретным его экземпляром, просто вызовите
статическое свойство Thread.CurrentThread:
static void ExtractExecutingThread()
{
// Получить поток, выполняющий данный метод.
Thread currThread = Thread.CurrentThread;
}
На платформе .NET не существует прямого соответствия "один к одному" между
доменами приложений (AppDomain) и потоками. Фактически определенный AppDomain
может иметь несколько потоков, выполняющихся в каждый конкретный момент
времени. Более того, конкретный поток не привязан к одному домену приложений на
протяжении своего времени существования. Потоки могут пересекать границы доменов
приложений, когда это вздумается планировщику Windows и CLR.
Хотя активные потоки могут пересекать границы AppDomain, каждый поток в
каждый конкретный момент времени может выполняться только внутри одного домена
приложений (другими словами, невозможно, чтобы один поток работал в более чем
одном домене приложений сразу). Чтобы программно получить доступ к AppDomain, в
котором работает текущий поток, вызовите статический метод Thread.GetDomain():
static void ExtractAppDomainHostingThread()
{
// Получить AppDomain, в котором работает текущий поток.
AppDomain ad = Thread.GetDomain();
}
Единственный поток также в любой момент может быть перемещен в
определенный контекст, и он может перемещаться в пределах нового контекста по прихоти CLR.
Для получения текущего контекста, в котором выполняется поток, используйте
статическое свойство Thread.CurrentContext (которое возвращает объект System.Runtime.
Remoting.Contexts .Context):
static void ExtractCurrentThreadContext ()
{
// Получить контекст, в котором работает текущий поток.
Context ctx = Thread.CurrentContext;
}
Еще раз: за перемещение потоков между доменами приложений и контекстами
отвечает CLR. Как разработчик .NET, вы всегда остаетесь в счастливом неведении
относительно того, когда завершается каждый конкретный поток (или куда именно он будет
помещен после перемещения). Тем не менее, полезно знать о различных способах
получения лежащих в основе примитивов.
Проблема параллелизма
Один из многих болезненных аспектов многопоточного программирования связан
с ограниченным контролем над использованием потоков операционной системой или
CLR. Например, написав блок кода, который создает новый поток выполнения, нельзя
672 Часть V. Введение в библиотеки базовых классов .NET
гарантировать, что этот поток запустится немедленно. Вместо этого данный код лишь
просит операционную систему запустить поток, как только это будет возможно (обычно,
когда планировщик потоков доберется до него).
Более того, учитывая, что потоки могут перемещаться между границами
приложений и контекстов, когда это нужно CLR, вы должны представлять, какие аспекты
приложения являются изменчивыми в потоках (т.е. речь идет о многопоточном доступе) и
какие операции — атомарными (изменчивые в потоках операции опасны).
Чтобы проиллюстрировать проблему, предположим, что поток вызывает метод
специфического объекта. Теперь представьте, что этот поток приостановлен
планировщиком потока, чтобы позволить другому потоку обратиться к тому же методу того же
объекта.
Если исходный поток еще не завершил свою операцию, второй входящий поток
может увидеть объект в частично модифицированном состоянии. И здесь второй поток,
по сути, читает фиктивные данные, что определенно может привести к очень
странным (и трудно обнаруживаемым) ошибкам, которые еще более трудно воспроизвести и
отладить.
С другой стороны, атомарные операции всегда безопасны в многопоточной среде.
К сожалению, очень мало операций в библиотеках базовых классов .NET являются
гарантированно атомарными. Даже операция присваивания переменной-члену не
является атомарной! Если в документации .NET Framework 4.0 SDK специально не сказано,
что операция является атомарной, ее следует считать изменчивой в потоках и
предпринимать соответствующие меры предосторожности.
Роль синхронизации потоков
Сейчас уже должно быть ясно, что домены многопоточных приложений сами по себе
довольно изменчивы, поскольку множество потоков могут оперировать общими
разделенными ресурсами (более или менее) одновременно. Чтобы защитить ресурсы
приложений от возможного повреждения, разработчики .NET должны использовать поточные
примитивы (такие как блокировки, мониторы и атрибут [Synchronization]) для
контроля доступа среди выполняющихся потоков.
Хотя платформа .NET не может полностью скрыть сложности, связанные с
построением надежных многопоточных приложений, этот процесс все же значительно
упрощен. Используя типы, определенные внутри пространства имен System.Threading, и
библиотеку .NET 4.0 Tksk Parallel Library (TPL), можно порождать дополнительные
потоки с минимальными усилиями. Аналогично, когда наступает момент для блокировки
разделяемых элементов данных, вы найдете для этого подходящие типы, которые
предлагают ту же функциональность, что и примитивы многопоточности Windows API (на
основе намного более ясной объектной модели).
Прежде чем углубиться в пространство имен System.Threading и библиотеку TPL,
важно отметить один удобный способ включения потоков в приложение. При
рассмотрении делегата .NET (см. главу 11) упоминалось, что все делегаты обладают
способностью вызывать члены асинхронно. Это главное преимущество платформы .NET,
учитывая, что одна из наиболее частых причин создания разработчиками потоков — вызов
методов в неблокирующей (т.е. асинхронной) манере.
Краткий обзор делегатов .NET
Вспомните, что тип делегата .NET — это по существу безопасный в отношении
типов, объектно-ориентированный указатель на функцию.
Глава 19. Многопоточность и параллельное программирование 673
На объявление делегата .NET компилятор С# отвечает построением запечатанного
класса, который наследуется от System.MulticastDelegate (который, в свою очередь,
унаследован от System.Delegate). Эти базовые классы предоставляют каждому
делегату возможность поддерживать список адресов методов, которые могут быть вызваны
позднее. Рассмотрим делегат BinaryOp, впервые показанный в главе 11:
// Тип делегата С#.
public delegate int BinaryOp(int x, int y) ;
Исходя из определения, BinaryOp может указывать на любой метод, принимающий
два целых числа (по значению) в качестве аргументов и возвращающий целое число.
После компиляции сборка с определением делегата будет содержать полноценное
определение класса, сгенерированного динамически при построении проекта, на основе
объявления делегата. В случае BinaryOp этот класс более или менее похож на
следующий (записан в псевдокоде):
public sealed class BinaryOp : System.MulticastDelegate
{
public BinaryOp(object target, uint functionAddress);
public void Invoke(int x, int y) ;
public IAsyncResult Beginlnvoke(int x, int y,
AsyncCallback cb, object state);
public int Endlnvoke(IAsyncResult result);
}
Сгенерированный метод Invoke () используется для вызова метода,
поддерживаемого объектом делегата в синхронном режиме. Поэтому вызывающий поток (такой как
первичный поток приложения) должен будет ждать, пока не завершится вызов
делегата. Также вспомните, что в С# метод Invoke () не нужно вызывать в коде напрямую —
он может быть инициирован неявно, "за кулисами", при применении "нормального"
синтаксиса вызова метода.
Рассмотрим следующий пример консольного приложения (SyncDelegateReview),
которое вызывает статический метод Add() в синхронном (т.е. блокирующем) режиме (не
забудьте импортировать пространство имен System.Threading, поскольку будет
вызываться метод Thread.Sleep()).
namespace SyncDelegateReview
{
public delegate int BinaryOp (int x, int y) ;
class Program
{
static void Main(string[] args)
{
Console. WriteLine ("***** Synch Delegate Review *****••);
// Вывести идентификатор выполняющегося потока.
Console.WriteLine("Main () invoked on thread {0}.",
Thread.CurrentThread.ManagedThreadld) ;
// Вызвать Add() в синхронном режиме.
BinaryOp b = new BinaryOp(Add);
// Можно было бы также написать b.InvokeA0, 10) ;
int answer = bA0, 10) ;
// Эти строки не будут выполняться, пока не завершится метод Add().
Console.WriteLine("Doing more work in Main()!");
Console.WriteLine(0 + 10 is {0}.", answer);
Console.ReadLine();
}
674- Часть V. Введение в библиотеки базовых классов .NET
static int Add(int x, int y)
{
// Вывести идентификатор выполняющегося потока.
Console.WriteLine("Add() invoked on thread {0}.",
Thread.CurrentThread.ManagedThreadld);
// Пауза для моделирования длительной операции.
Thread.SleepE000) ;
return x + у;
}
}
}
Внутри метода Add() вызывается статический метод Thread.Sleep() для
приостановки вызывающего потока примерно на пять секунд, чтобы имитировать длительную
задачу. Учитывая, что метод Add() вызывается в синхронном режиме, метод Main() не
выведет результат операции до тех пор, пока не завершится Add(). Обратите внимание,
что метод Man() получает доступ к текущему потоку (через Thread.CurrentThread) и
печатает идентификатор потока, взяв его из свойства ManagedThreadld. Та же логика
повторяется в статическом методе Add(). Как и можно было ожидать, учитывая, что вся
работа в этом приложении выполняется исключительно в первичном потоке,
обнаруживается, что на консоль выводится одно и то же значение идентификатора:
***** Synch Delegate Review *****
Main () invoked on thread 1.
Add () invoked on thread 1.
Doing more work in Main() !
10 + 10 is 20.
Press any key to continue . . .
Запустив эту программу, вы должны заметить пятисекундную задержку перед тем,
как выполнится последний вызов Console.WriteLine() в Main(). Хотя многие (если не
большинство) методов могут вызываться синхронно без болезненных последствий, на
самом деле при необходимости делегаты .NET позволяют выполнять назначенные им
методы асинхронно.
Исходный код. Проект SyncDelegateReview доступен в подкаталоге Chapter 19.
Асинхронная природа делегатов
Если для вас тема многопоточности в новинку, может возникнуть вопрос, что собой
представляет асинхронный вызов метода? Как известно, некоторые программные
операции требуют времени для своего завершения. Хотя приведенный выше метод Add ()
иллюстративен по природе, предположим, что строится однопоточное приложение,
вызывающее метод на удаленном объекте, который выполняет длительный запрос к базе
данных, загружает большой документ либо выводит 500 строк текста во внешний файл.
На протяжении выполнения этих операций приложение "замирает" на некоторый
период времени. И пока эта задача не будет завершена, все поведение программы (вроде
активизации пунктов меню, щелчков в панели инструментов или вывода на консоль)
будет заморожено.
Отсюда вопрос: как заставить делегат выполнить назначенный ему метод в отдельном
потоке выполнения, чтобы имитировать "одновременное" выполнение многочисленных
задач? К счастью, каждый тип делегата .NET автоматически оснащен такой
возможностью. Вдобавок, вы не обязаны углубляться в детали типов пространства имен System.
Threading, чтобы делать это (хотя эти сущности могут успешно работать "рука об руку").
Глава 19. Многопоточность и параллельное программирование 675
Методы Be gin Invoke () и EndInvoke()
Когда компилятор С# обрабатывает ключевое слово delegate, динамически
сгенерированный класс определяет два метода с именами BeginlnvokeO и EndInvoke().
Учитывая определение делегата BinaryOp, эти методы прототипированы следующим
образом:
public sealed class BinaryOp : System.MulticastDelegate
{
// Используется для асинхронного вызова метода,
public IAsyncResult Beginlnvoke(int x, int y,
AsyncCallback cb, object state);
// Используется для получения возвращаемого значения вызванного метода,
public int Endlnvoke(IAsyncResult result);
}
Первый стек параметров, переданный BeginlnvokeO, будет основан на формате
делегата С# (в случае BinaryOp — два целых). Последними двумя аргументами всегда
будут System.AsyncCallback и System.Object. Чуть позже вы узнаете о назначении
этих параметров, а пока передадим null в каждом из них. Также обратите внимание,
что возвращаемое значение Endlnvoke() является целым, как тип возврата BinaryOp,
хотя параметр этого метода имеет тип IAsyncResult.
Интерфейс System.IAsyncResult
Метод BeginlnvokeO всегда возвращает объект, реализующий интерфейс
IAsyncResult, в то время как Endlnvoke() ожидает единственный параметр
совместимого с IAsyncResult типа. Совместимый с IAsyncResult объект, возвращаемый из
BeginlnvokeO — это в основном связывающий механизм, который позволяет
вызывающему потоку получить позже результат вызова асинхронного метода через Endlnvoke ().
Интерфейс IAsyncResult (находящийся в пространстве имен System) определен
следующим образом:
public interface IAsyncResult
{
object AsyncState { get; }
WaitHandle AsyncWaitHandle { get; }
bool CompletedSynchronously { get; }
bool IsCompleted { get; }
}
В простейшем случае можно избежать непосредственного вызова этих членов. Все,
что потребуется сделать — это кэшировать las у ncResu It-совместимый объект,
возвращенный BeginlnvokeO, и передать его Endlnvoke0, имея готовность к получению
результата вызова метода. Как будет показано, члены IAsyncResuIt-совместимого
объекта можно вызывать, когда требуется "большая вовлеченность" в процесс получения
возвращаемого методом значения.
На заметку! Метод, возвращающий void, можно просто вызвать асинхронно и забыть. В таких
случаях нет необходимости кэшировать IAsyncResuIt-совместимый объект или вызывать
Endlnvoke(), поскольку нет возвращаемого значения, которое нужно получить.
676 Часть V. Введение в библиотеки базовых классов .NET
Асинхронный вызов метода
Чтобы заставить делегат BinaryOp вызывать Add() асинхронно,
модифицируем логику предыдущего проекта (можно добавить код к имеющемуся проекту, однако
в коде примеров для данной главы имеется новое консольное приложение по имени
AsyncDelegate). Обновите предыдущий метод Main() следующим образом:
static void Main(string[] args)
{
Console.WriteLine ("***** Async Delegate Invocation *****");
// Вывести идентификатор выполняющегося потока.
Console.WriteLine("Main() invoked on thread {0}.",
Thread.CurrentThread.ManagedThreadld);
// Вызвать Add() во вторичном потоке.
BinaryOp b = new BinaryOp(Add) ;
IAsyncResult lftAR = b.BeginlnvokeA0, 10, null, null);
// Выполнить другую работу в первичном потоке...
Console.WriteLine("Doing more work in Main()!");
// Получить результат метода Add() по готовности,
int answer = b.Endlnvoke(iftAR);
Console.WriteLine (0 + 10 is {0}.", answer);
Console.ReadLine();
}
После запуска этого приложения на консоль выводятся два уникальных
идентификатора потоков, поскольку в текущем домене приложения работает несколько потоков:
***** Async Delegate Invocation *****
Main () invoked on thread 1.
Doing more work in Main() !
Add() invoked on thread 3.
10 + 10 is 20.
В дополнение к уникальным идентификаторам при выполнении этого приложения
вы заметите, что сообщение "Doing more work in Main()!" выводится практически
мгновенно, в то время как вторичный поток продолжит свое дело.
Синхронизация вызывающего потока
Хорошо поразмыслив о текущей реализации Main(), можно понять, что время,
прошедшее между вызовами BeginlnvokeO и Endlnvoke(), очевидно меньше пяти секунд.
Поэтому, как только сообщение "Doing more work in Main()!" будет выведено на
консоль, вызывающий поток блокируется и ожидает, пока вторичный поток завершится,
чтобы получить результат вызова метода Add(). Поэтому на самом деле производится
еще один синхронный вызов:
static void Main(string[] args)
{
BinaryOp b = new BinaryOp(Add);
// После следующего оператора вызывающий поток блокируется,
// пока не будет завершен BeginlnvokeO .
IAsyncResult iftAR = b.BeginlnvokeA0, 10, null, null);
// Этот вызов займет намного меньше 5 секунд!
Console.WriteLine("Doing more work in Main()!");
int answer = b.Endlnvoke(iftAR);
}
Глава 19. Многопоточность и параллельное программирование 677
Очевидно, что асинхронные делегаты утратили бы свою привлекательность, если
бы вызывающий поток при определенных обстоятельствах мог блокироваться. Чтобы
позволить вызывающему потоку определять, когда асинхронно вызванный метод
завершит свою работу, в интерфейсе IAsyncResult предусмотрено свойство IsCompleted.
С его помощью вызывающий поток может определять, действительно ли асинхронный
вызов был завершен, перед вызовом EndInvoke().
Если метод еще не завершился, то IsCompleted вернет false, и вызывающий поток
может продолжать заниматься своей работой. Если Incompleted вернет true, то
вызывающий поток может получить результат в наименее блокирующей манере. Рассмотрим
следующую модификацию метода Main():
static void Main(string [ ] args)
{
BinaryOp b = new BinaryOp(Add);
IAsyncResult iftAR = b.BeginlnvokeA0, 10, null, null);
// Это сообщение продолжит печататься, пока не завершится метод Add().
while('lftAR.IsCompleted)
{
Console.WriteLine("Doing more work in Main()' ");
Thread.Sleep A000);
}
// Теперь известно, что метод Add() завершен,
int answer = b.Endlnvoke(lftAR);
}
Здесь запускается цикл, который продолжает выполнять оператор Console.
WriteLine(), пока не завершится вторичный поток. Когда это произойдет, можно
получить результат метода Add(), уже зная точно, что он закончил работать.
Вызов Thread.SleepA000) не обязателен для корректной работы этого конкретного
приложения; однако, заставляя первичный поток ожидать примерно секунду на каждой
итерации, мы предотвращаем вывод слишком большого количества одного и того же
сообщения на консоль. Ниже показан вывод (он может слегка отличаться, в зависимости
от скорости машины и времени запуска потока): *
***** Async Delegate Invocation *****
Main () invoked on thread 1.
Doing more work in Main() '
Add() invoked on thread 3.
Doing more work in Main() !
Doing more work in Main() '
Doing more work in Main() '
Doing more work in Main() '
Doing more work in Main() !
10 + 10 is 20.
В дополнение к свойству IsCompleted интерфейс IAsyncResuklt предлагает
свойство As у nc Wait Handle для реализации более гибкой логики ожидания. Это
свойство возвращает экземпляр типа WaitHandle, который предоставляет метод по имени
WaitOne(). Преимущество WaitHandle.WaitOneO в том, что можно задавать
максимальное время ожидания. Если это время истекает, то WaitOneO возвращает false.
Взгляните на следующее изменение цикла while, в котором теперь не используется
вызов Thread.Sleep():
while (!lftAR.AsyncWaitHandle.WaitOne A000, true))
{
Console.WriteLine("Doing more work in Main()!");
}
678 Часть V. Введение в библиотеки базовых классов .NET
Хотя эти свойства IAsyncResult предоставляют способ синхронизации
вызывающего потока, все же это не самый эффективный подход. Во многих отношениях
свойство IsCompleted подобно надоедливому менеджеру, который постоянно спрашивает:
"Вы еще это не сделали?". К счастью, делегаты предлагают множество дополнительных
(и более элегантных) приемов получения результата из метода, который был вызван
асинхронно.
Исходный код. Проект AsyncDelegate доступен в подкаталоге Chapter 19.
Роль делегата AsyncCallback
Вместо опроса делегата о том, завершился ли асинхронно вызванный метод, было
бы более эффективно заставить вторичный поток информировать вызывающий
поток о завершении выполнения задания. Чтобы включить такое поведение,
необходимо передать экземпляр делегата System.AsyncCallback в качестве параметра
методу BeginlnvokeOinp сих пор этот параметр был равен null. Если передается объект
AsyncCallback, делегат автоматически вызовет указанный метод по завершении
асинхронного вызова.
На заметку! Метод обратного вызова будет вызван во вторичном потоке, а не в первичном. Это
имеет важное последствие для потоков с графическим интерфейсом пользователя (WPF или
Windows Forms), поскольку элементы управления привязаны к потоку, который их создал, и
могут управляться только им. Далее в этой главе, при рассмотрении библиотеки TPL, будут
показаны некоторые примеры работы потоков из графического интерфейса.
Как и любой делегат, AsyncCallback может вызывать только методы,
соответствующие определенному шаблону, который в данном случае требует единственного
параметра IAsyncResult и ничего не возвращает:
// Целевые методы AsyncCallback должны иметь следующую сигнатуру,
void MyAsyncCallbackMethod(IAsyncResult ltfAR)
Предположим, что имеется другое консольное приложение (AsyncCallbackDelegate),
использующее делегат BinaryOp. Однако на этот раз мы не будем опрашивать делегат,
чтобы выяснить, когда завершится метод Add (). Вместо этого определим статический
метод по имени AddCompleteO для получения уведомления о том, что асинхронный
вызов завершен. Также в этом примере используется булевское статическое поле
уровня класса, которое служит для удержания в активном состоянии первичного потока
Main(), пока не завершится вторичный поток.
На заметку! Использование булевской переменной в данном примере, строго говоря, является
небезопасным для потоков, поскольку к его значению имеют доступ два разных потока. В
данном примере это допустимо; тем не менее, запомните в качестве хорошего эмпирического
правила: вы должны обеспечивать блокировку данных, разделяемых между несколькими
потоками. Далее в главе будет показано, как это делается.
namespace AsyncCallbackDelegate
{
public delegate int BinaryOp(int x, int y) ;
class Program
{
private static bool isDone = false;
static void Main(string [ ] args)
{
Глава 19. Многопоточность и параллельное программирование 679
Console.WriteLine("***** AsyncCallbackDelegate Example *****");
Console.WriteLine ("Main () invoked on thread {0}.",
Thread.CurrentThread.ManagedThreadld);
BinaryOp b = new BinaryOp(Add);
IAsyncResult iftAR = b.BeginlnvokeA0, 10,
new AsyncCallback(AddComplete) , null);
// Предположим, здесь выполняется какая-то другая работа...
while (lisDone)
{
Thread.SleepA000);
Console.WriteLine("Working....");
}
Console.ReadLine();
}
static int Add(int x, int y)
{
Console.WriteLine ("Add() invoked on thread {0}.",
Thread.CurrentThread.ManagedThreadld);
Thread.SleepE000);
return x + y;
}
static void AddComplete(IAsyncResult itfAR)
{
Console.WriteLine("AddComplete() invoked on thread {0}.",
Thread.CurrentThread.ManagedThreadld);
Console.WriteLine("Your addition is complete");
isDone = true;
}
}
}
Статический метод AddCompleteO будет вызван делегатом AsyncCallback по
завершении метода Add (). Запустив эту программу, можно убедиться, что именно
вторичный поток вызывает AddCompleteO:
***** AsyncCallbackDelegate Example *****
Main () invoked on thread 1.
Add() invoked on thread 3.
Working....
Working... .
Working... .
Working....
Working....
AddCompleteO invoked on thread 3.
Your addition is complete
Как и в других примерах настоящей главы, вывод может несколько отличаться.
Фактически, может появиться только одно финальное сообщение "Working..." после
завершения AddCompleteO. Это просто следствие односекундной задержки в Main0.
Роль класса AsyncResult
Сейчас метод AddCompleteO не выводит действительный результат операции
(сложения двух чисел). Причина с том, что цель делегата AsycnCallback (в данном
примере — AsyncResult0) не имеет доступа к исходному делегату BinaryOp, созданному в
контексте Main 0, и потому вызывать EndInvoke() из AdCompleteO нельзя!
680 Часть V. Введение в библиотеки базовых классов .NET
Хотя можно было бы просто объявить переменную BinaryOp, как статический член
класса, чтобы позволить обоим методам обращаться к одному и тому же объекту, более
элегантное решение предусматривает применение входного параметра IAsyncResult.
Входной параметр IAsyncResult, передаваемый цели делегата AsyncCallback —
это на самом деле экземпляр класса AsyncResult (обратите внимание на
отсутствие префикса I), определенного в пространстве имен System.Runtime.Remoting.
Messaging. Статическое свойство AsyncDelegate возвращает ссылку на
первоначальный асинхронный делегат, который был создан где-то в другом месте.
Поэтому для получения ссылки на объект делегата BinaryOp, размещенный в Main(),
нужно привести экземпляр System.Object, возвращенный свойством AsyncDelegate, к
BinaryOp. И здесь можно запустить EndlnvokeO, как и ожидалось:
// Не забудьте импортировать System.Runtime.Remoting.Messaging!
static void AddComplete(IAsyncResult itfAR)
{
Console.WriteLine("AddComplete() invoked on thread {0}.",
Thread.CurrentThread.ManagedThreadld);
Console.WriteLine("Your addition is complete");
// Теперь получить результат.
AsyncResult ar = (AsyncResult)itfAR;
BinaryOp b = (BinaryOp)ar.AsyncDelegate;
Console.WriteLine(0 + 10 is {0}.", b.Endlnvoke(itfAR));
lsDone = true;
}
Передача и прием специальных данных состояния
финальный аспект асинхронных делегатов, который должен быть учтен — это
последний аргумент метода BeginlnvokeO (который до сих пор был равен null). Этот
параметр позволяет передавать дополнительную информацию о состоянии методу
обратного вызова от первичного потока. Поскольку этот аргумент прототипирован как
System.Object, его можно передавать в любом типе данных, если только метод
обратного вызова знает, чего в нем ожидать. Для демонстрации предположим, что первичный
поток желает передать специальное текстовое сообщение методу AddComplete ():
static void Main(string[] args)
{
IAsyncResult lftAR = b.BeginlnvokeA0, 10,
new AsyncCallback(AddComplete),
"Main() thanks you for adding these numbers.");
}
Для получения этих данных в контексте AddComplete () используется свойство
AsyncState входящего параметра IAsyncResult. Обратите внимание, что здесь
понадобится явное приведение, поэтому первичный и вторичный потоки должны
согласовать тип, возвращаемый AsyncState.
static void AddComplete(IAsyncResult itfAR)
{
// Получить информационный объект и привести его к string,
string msg = (string)itfAR.AsyncState;
Console.WriteLine(msg);
lsDone = true;
}
Глава 19. Многопоточность и параллельное программирование 681
Ниже показан вывод последней итерации примера:
***** AsyncCallbackDelegate Example *****
Main () invoked on thread 1.
Add() invoked on thread 3.
Working...
Working.
Working.
Working...
Working.. ,
AddComplete() invoked on thread 3.
Your addition is complete
10 + 10 is 20.
Main() thanks you for adding these numbers.
Working....
Теперь, когда известно, как использовать делегат .NET для автоматического
завершения вторичного потока, обрабатывающего асинхронный вызов метода, давайте
обратимся непосредственно к взаимодействию с потоками, используя пространство имен
System.Threading.
Исходный код. Проект AsyncCallbackDelegate доступен в подкаталоге Chapter 19.
Пространство имен System.Threading
Пространство имен System.Threading в .NET предлагает множество типов,
которые позволяют непосредственно конструировать многопоточные приложения. В
дополнение к типам, позволяющим взаимодействовать с определенным потоком CLR, в
этом пространстве имен определены типы, которые открывают доступ к пулу потоков,
обслуживаемому CLR, простому (не связанному с графическим интерфейсом) классу
Timer и многочисленным типам, используемым для предоставления
синхронизированного доступа к разделенным ресурсам. В табл. 19.1 перечислены некоторые
основные члены этого пространства имен (подробные сведения ищите в документации .NET
Framework 4.0 SDK).
Таблица 19.1. Некоторые члены пространства имен System.Threading
Тип
Назначение
Interlocked
Monitor
Mutex
Parameter!zedThreadStart
Semaphore
Thread
Этот тип предоставляет атомарные операции для переменных,
разделяемых между несколькими потоками
Этот тип обеспечивает синхронизацию потоковых объектов,
используя блокировки и ожидания/сигналы. Ключевое слово С#
lock использует "за кулисами" объект Monitor
Примитив синхронизации, который может быть использован для
синхронизации между границами доменов приложений
Этот делегат позволяет потоку вызывать методы, принимающие
произвольное количество аргументов
Этот тип позволяет ограничить количество потоков, которые
могут иметь доступ к ресурсу или к определенному типу ресурсов
одновременно
Этот тип представляет поток, выполняемый в CLR. Используя
этот тип, можно порождать дополнительные потоки в исходном
домене приложений
682 Часть V. Введение в библиотеки базовых классов .NET
Окончание табл. 19.1
Тип
Назначение
ThreadPool
ThreadPriority
ThreadStart
ThreadState
Timer
TimerCallback
Этот тип позволяет взаимодействовать с поддерживаемым CLR
пулом потоков внутри заданного процесса
Это перечисление представляет уровень приоритета потока
(Highest, Normal и т.п.)
Этот делегат позволяет указать метод для вызова в заданном
потоке. В отличие от делегата ParametrizedThreadStart,
цель ThreadStart всегда должна иметь одинаковый прототип
Это перечисление специфицирует допустимые состояния
потока (Running, Aborted и т.п.)
Этот тип предоставляет механизм выполнения метода через
указанные интервалы
Этот тип делегата используется в сочетании с типами Timer
Класс System.Threading.Thread
Класс Thread является самым элементарным из всех типов пространства имен
System.Threading. Этот класс представляет объектно-ориентированную оболочку
вокруг заданного пути выполнения внутри определенного AppDomaln. Этот тип также
определяет набор методов (как статических, так и уровня экземпляра), которые позволяют
создавать новые потоки внутри текущего AppDomain, а также приостанавливать,
останавливать и уничтожать определенный поток. Список основных статических членов
приведен в табл. 19.2.
Таблица 19.2. Основные статические члены типа Thread
Статический член
Назначение
CurrentContext
CurrentThread
GetDomainO
GetDomainlDO
SleepO
Это свойство только для чтения возвращает контекст, в котором в
данный момент выполняется поток
Это свойство только для чтения возвращает ссылку на текущий
выполняемый поток
Этот метод возвращает ссылку на текущий AppDomain или
идентификатор этого домена, в котором выполняется текущий поток
Этот метод приостанавливает текущий поток на заданное время
Класс Thread также поддерживает несколько методов уровня экземпляра, часть из
которых описана в табл. 19.3.
На заметку! Отмена или приостановка активного потока обычно считается плохой идеей. Когда вы
делаете это, есть шанс (хотя и небольшой), что поток может допустить "утечку" своей рабочей
нагрузки, когда его беспокоят или прерывают.
Глава 19. Многопоточность и параллельное программирование 683
Таблица 19.3. Некоторые члены экземпляра типа Thread
Член уровня экземпляра Назначение
Is Alive Возвращает булевское значение, указывающее на то, запущен ли
поток (и еще не прерван и не отменен)
IsBackground Получает или устанавливает значение, указывающее, является ли
данный поток "фоновым" (подробнее объясняется далее)
Name Позволяет вам установить дружественное текстовое имя потока
Priority Получает или устанавливает приоритет потока, который может
принимать значение из перечисления ThreadPriority
ThreadState Получает состояние данного потока, которому может быть присвоено
значение из перечисления ThreadState
Abort () Инструктирует CLR прервать поток, как только это будет возможно
Interrupt () Прерывает (т.е. приостанавливает) текущий поток на заданный
период ожидания
Join () Блокирует вызывающий поток до тех пор, пока указанный поток
(тот, в котором вызван JoinO) не завершится
Resume () Возобновляет ранее приостановленный поток
Start () Инструктирует CLR запустить поток как можно скорее
Suspend () Приостанавливает поток. Если поток уже приостановлен, вызов
Suspend() не дает эффекта
Получение статистики о текущем потоке
Вспомните, что точка входа исполняемой сборки (т.е. метод Main(J)
запускается в первичном потоке выполнения. Чтобы проиллюстрировать базовое
применение типа Thread, предположим, что имеется новое консольное приложение по имени
ThreadStats. Как известно, статическое свойство Thread.CurrentThread извлекает
объект Thread, представляющий текущий выполняющийся поток. После получения
текущего потока можно вывести разнообразную статистику о нем.
// Не забудьте импортировать пространство имен System.Threading.
static void Main(string [ ] args)
{
Console.WriteLine("***** Primary Thread stats *****\n");
// Получить имя текущего потока.
Thread primaryThread = Thread.CurrentThread;
primaryThread.Name = "ThePnmaryThread";
// Показать детали включающего домена приложений и контекста.
Console.WriteLine("Name of current AppDomain: {0}",
Thread.GetDomain().FriendlyName);
Console.WriteLine("ID of current Context: {0}", Thread.CurrentContext.ContextID);
// Вывести некоторую статистику о текущем потоке.
Console.WriteLine("Thread Name: {0}", primaryThread.Name); // имя потока
Console.WriteLine("Has thread started?: {0}",
primaryThread.IsAlive); // запущен ли поток
Console.WriteLine("Priority Level: {0}",
primaryThread.Priority); // приоритет потока
Console.WriteLine("Thread State: {0}",
primaryThread.ThreadState); // состояние потока
Console.ReadLine();
684 Часть V. Введение в библиотеки базовых классов .NET
Ниже показан вывод этого кода:
***** Primary Thread stats *****
Name of current AppDomain: ThreadStats.exe
ID of current Context: 0
Thread Name: ThePrimaryThread
Has thread started?: True
Priority Level: Normal
Thread State: Running
Свойство Name
Хотя этот код более-менее очевиден, обратите внимание, что класс Thread
поддерживает свойство по имени Name. Если не установить его значение явно, то Name
вернет пустую строку. Присваивание дружественного имени конкретному объекту Thread
может значительно упростить отладку. Во время сеанса отладки в Visual Studio 2010
можно открыть окно Threads (Потоки), выбрав пункт меню Debug^Windows^Threads
(Отладка1^ Окна1^ Потоки). Как показано на рис. 19.1, это окно позволяет быстро
идентифицировать поток, который нужно диагностировать.
Рис. 19.1. Отладка потока в Visual Studio 2010
Свойство Priority
Обратите внимание, что в типе Thread определено свойство по имени Priority. По
умолчанию все потоки имеют уровень приоритета Normal. Однако это можно изменить
в любой момент жизненного цикла потока, используя свойство Thread.Priority и
связанное с ним перечисление System.Threading.ThreadPriority:
public enum ThreadPriority
{
Lowest,
BelowNormal,
Normal, // Значение по умолчанию.
AboveNormal,
Highest
}
При установке уровня приоритета потока равным значению, отличному от принятого
по умолчанию (ThreadPriority.Normal), следует иметь в виду, что это не предоставляет
прямого контроля над тем, как планировщик потоков будет переключать потоки между
собой. На самом деле уровень приоритета потока предоставляет CLR подсказку относительно
важности действий потока. Таким образом, поток с уровнем приоритета ThreadPriority.
Highest не обязательно гарантированно получит наивысший приоритет.
Глава 19. Многопоточность и параллельное программирование 685
Опять-таки, если планировщик потоков занят решением определенной задачи
(например, синхронизацией объекта, переключением потоков или их перемещением), то
уровень приоритета, скорее всего, будет соответствующим образом изменен. Однако,
как бы то ни было, среда CLR прочитает эти значения и проинструктирует
планировщик потоков о том, как наилучшим образом выделять порции времени. Потоки с
идентичным уровнем приоритета должны получать одинаковый объем времени на
выполнение своей работы.
В большинстве случаев редко требуется (если вообще требуется) напрямую менять
уровень приоритета потока. Теоретически можно повысить уровень приоритета для
множества потоков, тем самым предотвращая выполнение низкоприоритетных потоков
на их запрошенных уровнях (поэтому будьте осторожны).
Исходный код. Проект ThreadStats доступен в подкаталоге Chapter 19.
Программное создание вторичных потоков
При программном создании дополнительных потоков для выполнения некоторой
единицы работы необходимо следовать строго регламентированному процессу.
1. Создать метод, который будет точкой входа для нового потока.
2. Создать новый делегат ParametrizedThreadStart (или ThreadStart), передав
конструктору адрес метода, определенного на шаге 1.
3. Создать объект Thread, передав в качестве аргумента конструктора
ParametrizedThreadStart/ThreadStart.
4. Установить начальные характеристики потока (имя, приоритет и т.п.).
- 5. Вызвать метод Thread.Start(). Это запустит поток на методе, который указан
делегатом, созданным на шаге 2, как только это будет возможно.
Согласно шагу 2, можно использовать два разных типа делегатов для "указания"
метода, который выполнит вторичный поток. Делегат ThreadStart относится к
пространству имен System.Threading, начиная с .NET 1.0, и он может указывать на любой
метод, не принимающий аргументов и ничего не возвращающий. Этот делегат
пригодится, когда метод предназначен просто для запуска в фоновом режиме, без
какого-либо дальнейшего взаимодействия.
Очевидное ограничение ThreadStart связано с невозможность передавать ему
параметры для обработки. Тем не менее, тип делегата ParametrizedThreadStart
позволяет передать единственный параметр типа System.Object. Учитывая, что с помощью
System.Object представляется все, что угодно, можно передать любое количество
параметров через специальный класс или структуру. Однако имейте в виду, что делегат
ParametrizedThreadStart может указывать только на методы, возвращающие void.
Работа с делегатом ThreadStart
Чтобы проиллюстрировать процесс построения многопоточного приложения (а также
его пользу), предположим, что есть консольное приложение (SimpleMultiThreadApp),
которое позволяет конечному пользователю выбирать, будет приложение выполнять
свою работу в единственном первичном потоке либо распределит рабочую нагрузку на
два отдельных потока выполнения.
После импортирования пространства имен System.Threading следующий шаг
заключается в определении метода для выполнения работы (возможного) вторичного
потока. Чтобы сосредоточиться на механизме построения многопоточных программ, этот
686 Часть V. Введение в библиотеки базовых классов .NET
метод будет просто выводить на консоль последовательность чисел,
приостанавливаясь примерно на 2 секунды на каждом шаге. Ниже показано полное определение класса
Printer:
public class Printer
{
public void PrintNumbers()
{
// Вывести информацию о потоке.
Console.WriteLine("-> {0} is executing PrintNumbers()",
Thread.CurrentThread.Name) ;
// Вывести числа.
Console.Write("Your numbers: ");
for(int i = 0; i < 10; i++)
{
Console.Write ("{0}, ", i) ;
Thread.SleepB000);
}
Console.WriteLine ();
}
}
Внутри Main() сначала пользователю предлагается решить, сколько потоков
применять для выполнения работы приложения: один или два. Если пользователь
запрашивает один поток, нужно просто вызвать метод PrintNumbers () внутри первичного
потока. Если же пользователь отдает предпочтение двум потокам, необходимо создать
делегат Threads tart, указывающий на PrintNumbers (), передать объект делегата
конструктору нового объекта Thread и вызвать метод Start (), информируя среду CLR, что
этот поток готов к обработке.
Для начала установим ссылку на сборку Systern.Windows .Forms .dll (и импортируем
пространство имен System.Windows.Forms) и отобразим сообщение в Main(), используя
MessageBox.Show() (причина станет понятной после запуска программы). Ниже
показана полная реализация Main():
static void Main(string[] args)
{
Console.WriteLine ("***** The Amazing Thread App *****\n");
Console.Write ("Do you want [1] or [2] threads? ");
string threadCount = Console.ReadLine();
// Назначить имя текущему потоку.
Thread primaryThread = Thread.CurrentThread;
primaryThread.Name = "Primary";
// Вывести информацию о потоке.
Console.WriteLine("-> {0} is executing Main()",
Thread.CurrentThread.Name);
// Создать рабочий класс.
Printer p = new Printer();
switch(threadCount)
{
case ":
// Создать поток.
Thread backgroundThread =
new Thread(new ThreadStart(p.PrintNumbers));
backgroundThread.Name = "Secondary";
backgroundThread.Start();
break;
Глава 19. Многопоточность и параллельное программирование 687
case ":
р.PrintNumbers ();
breaks-
default:
Console.WriteLine("I don't know what you want.
goto case ";
, .you get 1 thread.11);
// Выполнить некоторую дополнительную обработку.
MessageBox.Show("I'm busy'", "Work on main thread...");
Console.ReadLine();
}
Если теперь запустить эту программу в одном потоке, обнаружится, что финальное
окно сообщения не отображает сообщения, пока вся последовательность чисел не будет
выведена на консоль. Поскольку после вывода каждого числа установлена пауза
примерно в 2 секунды, это создаст не слишком приятное впечатление у пользователя. Однако
в случае выбора двух потоков окно сообщения отображается немедленно, поскольку для
вывода чисел на консоль выделен отдельный уникальный объект Thread (рис. 19.2).
-
C:\Windom\sy5tem32\cmd.e
**** The Amazing Thread App *****
|do you want [1] or [2] threads? 2
> Primary is executing Main()
> Secondary is executing PrintNumbers()
[Your numbers: 0, 1. 2. 3. 4.
П
I'm busy!
J
Рис. 19.2. Многопоточные приложения позволяют создавать
более отзывчивые пользовательские интерфейсы
Исходный код. Проект SimpleMultiThreadApp доступен в подкаталоге Chapter 19.
Работа с делегатом ParametrizedThreadStart
Вспомните, что делегат ThreadStart может указывать только на методы,
возвращающие void и не имеющие аргументов. Во многих случаях это подходит, но если
нужно передать данные методу, выполняющемуся во вторичном потоке, то придется
использовать тип делегата ParametrizedThreadStart.
Для начала создадим новое консольное приложение по имени AddWithThreads и
импортируем пространство имен System.Threading. Теперь, учитывая, что Parametrized
ThreadStart может указывать на любой метод, принимающий параметр System.Object,
создадим специальный тип, содержащий число, которое должно быть прибавлено:
class AddParams
{
public int a, b;
public AddParams(int numbl, int numb2)
{
a = numbl;
b = numb2;
}
688 Часть V. Введение в библиотеки базовых классов .NET
Затем создадим в классе Program статический метод, который принимает параметр
AddParams и выводит на консоль сумму двух чисел:
static void Add(object data)
{
if (data is AddParams)
{
Console.WriteLine("ID of thread in Add(): {0}",
Thread.CurrentThread.ManagedThreadld);
AddParams ap = (AddParams)data;
Console.WriteLine ("{0} + {1} is {2}", ap.a, ap.b, ap.a + ap.b);
}
}
Код внутри Main() достаточно очевиден. Просто вместо ThreadStart должен
применяться ParametrizedThreadStart:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Adding with Thread objects *****");
Console.WriteLine("ID of thread in Main(): {0}",
Thread.CurrentThread.ManagedThreadld);
// Создать объект AddParams для передачи вторичному потоку.
AddParams ap = new AddParamsA0, 10);
Thread t = new Thread(new ParametenzedThreadStart (Add) ) ;
t.Start (ap);
// Подождать, пока другие потоки завершатся.
Thread.SleepE);
Console.ReadLine();
}
Класс AutoResetEvent
В этих первых примерах для того, чтобы заставить первичный поток подождать,
пока вторичный поток завершится, применялось несколько грубых способов. Во время
рассмотрения асинхронных делегатов в качестве переключателя использовалась
простая переменная булевского типа. Однако это решение не может быть рекомендуемым,
поскольку оба потока обращаются к одному и тому же элементу данных, что может
привести к его повреждению. Более безопасной, хотя также нежелательной альтернативой
может быть вызов Thread.Sleep() на определенный период времени. Проблема в том,
что нет желания ждать больше, чем необходимо.
Простой и безопасный к потокам способ заставить один поток ожидать завершения
другого потока, предусматривает использование класса AutoResetEvent. В потоке,
который должен ждать (таком как поток метода MainO), создадим экземпляр этого класса и
передадим конструктору false, указав, что уведомления пока не было. В точке, где
требуется ожидать, вызовем метод WaitOne(). Ниже приведен измененный класс Program,
который делает все это, используя статическую переменную-член AutoResetEvent:
class Program
{
private static AutoResetEvent waitHandle = new AutoResetEvent(false);
static void Main(string [ ] args)
{
Console.WriteLine ("***** Adding with Thread objects *****");
Console.WriteLine("ID of thread in Main(): {0}",
Thread.CurrentThread.ManagedThreadld);
AddParams ap = new AddParamsA0, 10);
Глава 19. Многопоточность и параллельное программирование 689
Thread t = new Thread(new ParameterizedThreadStart(Add));
t.Start(ap);
// Подождать здесь уведомления!
waitHandle.WaitOne() ;
Console. WriteLine ("Other thread is done!11);
Console.ReadLine ();
}
Когда другой поток завершит свою работу, он вызовет метод Set() на том же
экземпляре типа AutoResetEvent:
static void Add(object data)
{
if (data is AddParams)
{
Console.WriteLine("ID of thread in Add(): {0}",
Thread.CurrentThread.ManagedThreadld);
AddParams ap = (AddParams)data;
Console.WriteLine("{0} + {1} is {2}", ap.a, ap.b, ap.a + ap.b);
// Сообщить другому потоку о завершении работы.
waitHandle.Set() ;
}
Исходный код. Проект AddWithThreads доступен в подкаталоге Chapter 19.
Потоки переднего плана и фоновые потоки
Теперь, когда известно, как создавать новые потоки выполнения программно с
помощью типов из пространства имен System.Threading, давайте проясним разницу между
потоками переднего плана и фоновыми потоками.
• Потоки переднего плана (foreground threads) обеспечивают предохранение
текущего приложения от завершения. Среда CLR не остановит приложение (что
означает выгрузку текущего домена приложения) до тех пор, пока не будут завершены
все фоновые потоки.
• Фоновые потоки (background threads), иногда называемые потоками-демонами,
воспринимаются средой CLR как расширяемые пути выполнения, которые в
любой момент времени могут игнорироваться (даже если они в текущее время
заняты выполнением некоторой части работы). Таким образом, если все потоки
переднего плана прекращаются, то все фоновые потоки автоматически уничтожаются
при выгрузке домена приложения.
Важно отметить, что потоки переднего плана и фоновые потоки — это не синонимы
первичных и рабочих потоков. По умолчанию каждый поток, создаваемый через метод
Thread.Start(), автоматически становится потоком переднего плана. Это означает, что
домен приложения не выгрузится до тех пор, пока все потоки выполнения не завершат
свою часть работы. В большинстве случаев именно такое поведение и нужно.
Чтобы подтвердить сказанное, предположим, что необходимо вызвать Printer.
PrintNumbers () на вторичном потоке, который должен вести себя как фоновый.
Это означает, что метод, указанный типом Thread (через делегат ThreadStart или
ParametrizedThreadStart), должен быть готов безопасно прерваться, как только
потоки переднего плана закончат свою работу.
690 Часть V. Введение в библиотеки базовых классов .NET
Конфигурирование такого потока предусматривает всего лишь установку свойства
IsBackground в true:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Background Threads *****\n");
Printer p = new Printer ();
Thread bgroundThread = new Thread(new ThreadStart(p.PrintNumbers));
// Теперь это фоновый поток.
bgroundThread.IsBackground = true;
bgroundThread.Start() ;
}
Обратите внимание, что в этом методе Main() не производится вызов Console.
ReadLineO, чтобы оставить консоль видимой до нажатия клавиши <Enter>. Таким
образом, после запуска приложение завершится немедленно, потому что объект Thread
сконфигурирован как фоновый поток. Учитывая, что метод Main() инициирует
создание первичного потока переднего плана, как только логика метода Main() завершится,
домен приложения будет выгружен, прежде чем вторичный поток сможет завершить
свою работу.
Однако если закомментировать строку, которая устанавливает в true свойство
IsBackground, обнаружится, что каждое число выводится на консоль, поскольку все
потоки переднего плана должны завершить свою работу перед тем, как домен
приложения будет выгружен из размещающего процесса.
По большей части конфигурирование потока для выполнения в фоновом режиме
может пригодиться, когда рабочий поток выполняет некритичную работу, потребность в
которой отпадает после завершения главной задачи программы. Например, можно
построить приложение, которое проверяет сервер электронной почты каждые несколько
минут на предмет поступления новых писем, обновляет текущий прогноз погоды или
выполняет какие-то другие некритичные задачи.
Пример проблемы, связанной с параллелизмом
При построении многопоточного приложения необходимо гарантировать, что любая
часть разделяемых данных защищена от возможности изменения их значений
множеством потоков. Учитывая, что все потоки в AppDomain имеют параллельный доступ к
разделяемым данным приложения, представьте, что может случиться, если несколько
потоков одновременно обратятся к одному и тому же элементу данных. Поскольку
планировщик потоков случайным образом будет приостанавливать их работу, что если
поток А будет прерван до того, как завершит свою работу? В вот что: поток В после этого
прочтет нестабильные данные.
Чтобы проиллюстрировать проблему, связанную с параллелизмом, давайте
создадим еще один проект консольного приложения под названием MultiThreadedPrinting.
В этом приложении опять используется созданный ранее класс Printer, но на этот раз
метод PrintNumbers () приостановит текущий поток на случайно сгенерированный
период времени.
public class Printer
{
public void PrintNumbers ()
{
for (int 1 = 0; l < 10; i++)
{
// Отправить поток спать на случайный период времени.
Глава 19. Многопоточность и параллельное программирование 691
Random r = new Random () ;
Thread.SleepdOOO * r.NextE));
Console.Write("{0}, ", i);
}
Console.WriteLine ();
Метод Main() отвечает за создание массива из десяти (уникально именованных)
объектов Thread, каждый из которых производит вызов одного и того же экземпляра
объекта Printer:
class Program
{
static void Main(string[] агдз)
{
Console.WriteLine("*****Synchronizing Threads *****\nM);
Printer p = new Printer();
// Создать 10 потоков, которые указывают на один
//и тот же метод одного и того же объекта.
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++)
{
threads[i] =
new Thread(new ThreadStart(p.PrintNumbers));
threads[i] .Name = string.Format("Worker thread #{0}", i) ;
}
// Теперь запустить их все.
foreach (Thread t in threads)
t. Start () ;
Console.ReadLine ();
Прежде чем посмотреть на тестовые запуски, давайте еще раз проясним проблему.
Первичный поток внутри этого домена приложений начинает свое существование,
порождая десять вторичных рабочих потоков. Каждый рабочий поток должен вызвать
метод PrintNumbers() на одном и том же экземпляре Printer. Учитывая, что
никаких мер для блокировки разделяемых ресурсов этого объекта (консоли) не
предпринималось, есть хороший шанс, что текущий поток будет отключен, прежде чем метод
PrintNumbers () сможет напечатать полные результаты. Поскольку в точности не
известно, когда это может случиться (и может ли вообще), будут получаться
непредвиденные результаты. Например, может появиться следующий вывод:
•••••Synchronizing Threads *****
-> Worker thread #1 is executing PrintNumbers ()
Your numbers: -> Worker thread #0 is executing PrintNumbers()
-> Worker thread #2 is executing PrintNumbers()
Your numbers: -> Worker thread #3 is executing PrintNumbers()
Your numbers: -> Worker thread #4 is executing PrintNumbers()
Your numbers: -> Worker thread #6 is executing PrintNumbers()
Your numbers: -> Worker thread #7 is executing PrintNumbers()
Your numbers: -> Worker thread #8 is executing PrintNumbers()
Your numbers: -> Worker thread #9 is executing PrintNumbers()
Your numbers: Your numbers: -> Worker thread #5 is executing PrintNumbers()
Your numbers: 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 2, 1, 0, 0, 4, 3,
4, 1, 2, 4, 5, 5, 5, 6, 6, 6, 2, 7, 7, 7, 3, 4, 0, 8, 4, 5, 1, 5, 8, 8, 9,
2, 6, 1, 0, 9, 1,
6, 2, 1, 9,
692 Часть V. Введение в библиотеки базовых классов .NET
2, 1, 7, В, 3, 2, 3, 3, 9,
8, 4, 4, 5, 9,
4, 3, 5, 5, 6, 3, 6, 7, 4, 7, 6, В, 7, 4, 8, 5, 5, 6, 6, В, 7, 7, 9,
8, 9,
В, 9,
9,
9,
Запустите приложение еще несколько раз. Вот еще один вариант вывода:
*****Synchronizing Threads *****
-> Worker thread #0 is executing PrintNumbers()
-> Worker thread #1 is executing PrintNumbers()
-> Worker thread #2 is executing PrintNumbers ()
Your numbers: -> Worker thread #4 is executing PrintNumbers ()
Your numbers: -> Worker thread #5 is executing PrintNumbers()
Your numbers: Your numbers: -> Worker thread #6 is executing PrintNumbers()
Your numbers: -> Worker thread #7 is executing PrintNumbers ()
Your numbers: Your numbers: -> Worker thread #8 is executing PrintNumbers ()
Your numbers: -> Worker thread #9 is executing PrintNumbers ()
Your numbers: -> Worker thread #3 is executing PrintNumbers ()
Your numbers: 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7
i 7, 7, 1, 1, В, В, В, В, В, В, В, В, В, В, 9,
9,
9,
9,
9,
9,
9,
9,
9,
9,
На заметку! Если не удается получить непредсказуемый вывод, увеличьте количество потоков с
10 до 100 (например) или добавьте в код еще один вызов Thread. Sleep (). В конце концов,
"вы получите проблему, связаннуючс параллелизмом.
Ясно, что здесь присутствует определенная проблема. Как только каждый поток
требует от Printer печати числовых данные, планировщик потоков меняет их местами
в фоновом режиме. В результате получается несогласованный вывод. Необходим
способ обеспечения в коде синхронизированного доступа к разделяемым ресурсам. Как
и можно было предположить, пространство имен System.Threading предлагает
множество типов, ориентированных на синхронизацию. В языке С# также предусмотрено
специальное ключевое слово для синхронизации разделяемых данных в многопоточном
приложении.
Синхронизация с использованием ключевого слова С# lock
Первая техника, которую вы можете использовать для синхронизованного доступа к
разделяемым ресурсам, состоит в применении ключевого слова С# lock. Это ключевое
слово позволяет определять контекст операторов, которые должны быть
синхронизованными между потоками. В результате входящие потоки не могут прервать текущий
поток, мешая ему завершить свою работу. Ключевое слово lock требует указания
маркера (ссылки на объект), которые должен быть получен потоком для входа в контекст
блокировки.
Глава 19. Многопоточность и параллельное программирование 693
Чтобы предпринять попытку заблокировать приватный метод уровня экземпляра,
нужно просто передать ссылку на текущий тип:
private void SomePrivateMethod()
{
// Использовать текущий объект как маркер потока.
lock(this)
{
// Весь код внутри этого контекста безопасен к потокам.
}
}
Однако если вы заблокируете область кода внутри общедоступного члена,
безопаснее (и вообще лучше) объявить приватный член object для использования в качестве
маркера блокировки:
public class Printer
{
// Маркер блокировки.
private object threadLock = new object ();
public void PrintNumbers ()
{
// Использование маркера блокировки,
lock (threadLock)
{
}
}
}
В любом случае, если посмотреть на метод PrintNumbers (), то можно заметить, что
разделяемый ресурс, за доступ к которому соперничают потоки — это окно консоли. В
результате помещения всего взаимодействия с типом Console в контекст lock, как показано ниже,
создается метод, который позволит конкурирующему потоку завершить свою работу:
public void PrintNumbers ()
{
// Использовать маркер блокировки приватного объекта,
lock (threadLock)
{
// Вывести информацию о потоке.
Console.WriteLine ("-> {0} is executing PrintNumbers()",
Thread.CurrentThread.Name);
// Вывести числа.
Console.Write("Your numbers: ");
for (int i = 0; l < 10; i++)
{
Random r = new Random () ;
Thread.SleepA000 * r.Next E));
Console.Write ("{0}, ", i) ;
}
Console.WriteLine();
}
}
Как только поток войдет в контекст lock, маркер блокировки (в данном случае —
текущий объект) станет недоступным другим потокам до тех пор, пока блокировка не
будет снята по выходе из контекста lock. Таким образом, если поток А захватит маркер
блокировки, другие потоки не смогут войти ни в один из контекстов, использующих тот
же маркер, до тех пор, пока поток А не освободит его.
694 Часть V. Введение в библиотеки базовых классов .NET
На заметку! Чтобы блокировать код в статическом методе, нужно объявить приватную статическую
переменную-член, которая будет служить в качестве маркера блокировки.
Если теперь запустить приложение, можно увидеть, что каждый поток получил
возможность выполнить свою работу до конца:
*****Synchronizi
-> Worker thread
Your numbers: 0,
-> Worker thread
Your numbers: 0,
-> Worker thread
Your numbers: 0,
-> Worker thread
Your numbers: 0,
-> Worker thread
Your numbers: 0,
-> Worker thread
Your numbers: 0,
-> Worker thread
Your numbers: 0,
-> Worker thread
Your numbers: 0,
-> Worker thread
Your numbers: 0,
-> Worker thread
Your numbers: 0,
ng
#0
1,
#1
1,
#3
1,
#2
1,
#4
1,
#5
1,
#7
1,
#6
1,
#8
1,
#9
1,
Threads *****
is
2,
is
2,
is
2,
is
2,
is
2,
is
2,
is
2,
is
2,
is
2,
is
2,
executing PrintNumbers(
3, 4, 5, 6, 7, 8, 9,
executing PrintNumbers(
3, 4, 5, 6, 7, 8, 9,
executing PrintNumbers(
3, 4, 5, 6, 7, 8, 9,
executing PrintNumbers(
3, 4, 5, 6, 7, 8, 9,
executing PrintNumbers(
3, 4, 5, 6, 7, 8, 9,
executing PrintNumbers(
3, 4, 5, 6, 7, 8, 9,
executing PrintNumbers(
3, 4, 5, 6, 7, 8, 9,
executing PrintNumbers (
3, 4, 5, 6, 7, 8, 9,
executing PrintNumbers (
3, 4, 5, 6, 7, 8, 9,
executing PrintNumbers (
3, 4, 5, 6, 7, 8, 9,
Исходный код. Проект MultiThreadedPrinting доступен в подкаталоге Chapter 19.
Синхронизация с использованием типа
Sy s tem. Threading. Monitor
Оператор C# lock — это на самом деле сокращенная нотация для работы с классом
System.Threading.Monitor. При обработке компилятором С# контекст lock на самом
деле преобразуется в следующую конструкцию (в чем легко убедиться с помощью
утилиты ldasm.exe или reflector.exe):
public void PrintNumbers ()
{
Monitor.Enter(threadLock);
try
{
// Вывести информацию о потоке.
Console.WriteLine("-> {0} is executing PrintNumbers()",
Thread.CurrentThread.Name);
// Вывести числа.
Console.Write("Your numbers: ") ;
for (int i = 0; i < 10; i++)
{
Random r = new Random () ;
Thread.SleepA000 * r.NextE));
Console.Write ("{0}, ", i) ;
}
Console.WriteLine ();
}
Глава 19. Многопоточность и параллельное программирование 695
finally
{
Monitor.Exit(threadLock);
}
}
Для начала обратите внимание, что метод Monitor.Enter () является конечным
получателем маркера потока, который указывается как аргумент ключевого слова lock.
Весь код внутри контекста lock помещен в блок try. Соответствующая конструкция
finally гарантирует освобождение маркера блокировки (через метод Monitor.Exit())
независимо от каких бы то ни было исключений времени выполнения. Модифицировав
программу MultiThreadShareData для прямого использования типа Monitor (как
только что было показано), вы обнаружите, что вывод идентичен.
С учетом того, что ключевое слово lock, похоже, требует меньше кода, чем явное
использование типа System.Threading.Monitor, может возникнуть вопрос о
преимуществах его прямого использования. Короткий ответ: преимущество в большем контроле.
Применяя тип Monitor, можно заставить активный поток ожидать в течение
некоторого периода времени (с помощью статического метода Monitor.Wait()),
информировать ожидающие потоки, когда текущий поток завершится (статическими методами
Monitor.Pulse() и Monitor.PulseAllO), и т.д.
Как и можно было ожидать, в значительном числе случаев ключевого слова С# lock
вполне достаточно. Если вас интересуют дополнительные члены класса Monitor,
обращайтесь в документацию .NET Framework 4.0 SDK.
Синхронизация с использованием типа
System.Threading. Interlocked
Не заглядывая в CIL-код, очень трудно поверить, что присваивание и простые
арифметические операции не являются атомарными. По этой причине в пространстве имен
System.Threading предоставляется тип, позволяющий оперировать одним элементом
данных автоматически, с меньшими накладными расходами, чем Monitor. В классе
Interlocked определены статические члены, перечисленные в табл. 19.4.
Таблица 19.4. Некоторые члены типа System.Threading.Interlocked
Член Назначение
CompareExchange () Безопасно проверяет два значения на эквивалентность. Если они
эквивалентны, изменяет одно из значений на третье
Decrement () Безопасно уменьшает значение на 1
Exchange () Безопасно меняет два значения местами
Increment () Безопасно уменьшает значение на 1
Хотя это не видно сразу, процесс атомарного изменения отдельного значения
довольно часто применяется в многопоточной среде. Предположим, что имеется
метод по имени AddOneO, который увеличивает целочисленную переменную-член по
имени intVal. Вместо написания кода синхронизации вроде следующего:
public void AddOneO
{
lock(myLockToken)
{
intVal++;
}
}
696 Часть V. Введение в библиотеки базовых классов .NET
можно воспользоваться статическим методом Interlocked.Increment () и в результате
упростить код. Этому методу нужно передать по ссылке переменную для увеличения.
Обратите внимание, что метод Increment () не только изменяет значение входного
параметра, но также возвращает полученное новое значение:
public void AddOne()
{
int newVal = Interlocked.Increment(ref intVal);
}
В дополнение к Increment и Decrement тип Interlocked позволяет
автоматически присваивать числовые и объектные данные. Например, чтобы присвоить значение
83 переменной-члену, можно обойтись без явного оператора lock (или явной логики
Monitor) и применить вместо этого метод Interlock.Exchange():
public void SafeAssignment()
{
Interlocked.Exchange(ref mylnt, 83);
}
Наконец, если необходимо проверить эквивалентность двух значений и изменить
элемент сравнения в безопасной к потокам манере, то можно воспользоваться методом
Interlocked.CompareExchange(), как показано ниже:
public void CompareAndExchange()
{
// Если значение i равно 83, изменить его на 99.
Interlocked.CompareExchange(ref i, 99, 83);
}
Синхронизация с использованием атрибута [Synchronization]
Последний из примитивов синхронизации, которые здесь рассматриваются — это
атрибут [Synchronization], который является членом пространства имен System.
Runtime.Remoting.Contexts. Этот атрибут уровня класса эффективно блокирует
весь код членов экземпляра объекта, обеспечивая безопасность в отношении потоков.
Когда среда CLR размещает объекты, снабженные атрибутами [Synchronization],
она помещает объект в контекст синхронизации. Как было показано в главе 17,
объекты, которые не должны выходить за границы контекста, должны наследоваться от
ContextBoundObject. Поэтому, чтобы сделать класс Printer безопасным к потокам
(без явного написания кода внутри членов класса), необходимо модифицировать его
следующим образом:
using System.Runtime.Remoting.Contexts;
// Все методы Printer теперь безопасны к потокам!
[Synchronization]
public class Printer : ContextBoundObject
{
public void PrintNumbers()
{
}
}
В некоторых отношениях этот подход выглядит как "ленивый" способ написания
безопасного к потокам кода, учитывая, что не приходится углубляться в детали
относительно того, какие именно аспекты типа действительно манипулируют
чувствительными к потокам данными. Однако главный недостаток этого подхода состоит в том,
Глава 19. Многопоточность и параллельное программирование 697
что даже если определенный метод не использует чувствительные к потокам данные,
CLR будет по-прежнему блокировать вызовы этого метода. Очевидно, что это
приведет к деградации общей функциональности типа, поэтому используйте такую технику
с осторожностью.
Программирование с использованием
обратных вызовов Timer
Многие приложения нуждаются в вызове специфического метода через регулярные
периоды времени. Например, в приложении может понадобиться отображать текущее
время в панели состояния с помощью определенной вспомогательной функции. Другой
пример: нужно, чтобы приложение вызывало вспомогательную функцию периодически,
выполняя некоторые некритичные фоновые задачи, такие как проверка поступления
новых сообщений электронной почты. Для ситуаций вроде этой можно использовать
тип System.Threading.Timer в сочетании с делегатом по имени TimerCallback.
Для целей иллюстрации предположим, что имеется консольное приложение
(TimerApp), которое выводит текущее время каждую секунду до тех пор, пока
пользователь не нажмет клавишу для прекращения приложения. Первый очевидный шаг —
написать метод, который будет вызываться типом Timer (не забудьте импортировать
System.Threading в файл кода):
class Program
{
static void PrintTime(object state)
{
Console.WriteLine("Time is: {0}",
DateTime.Now.ToLongTimeString());
}
static void Main(string[] args)
{
}
}
Обратите внимание, что данный метод имеет единственный параметр типа System.
Object и возвращает void. Это обязательно, поскольку делегат TimerCallback может
вызывать только методы, соответствующие такой сигнатуре. Значение, переданное
цели делегата TimerCallback, может представлять какую угодно информацию (в
случае примера с электронной почтой этот параметр может представлять имя сервера
Microsoft Exchange для взаимодействия в процессе). Также обратите внимание, что
поскольку этот параметр — экземпляр System.Object, ему можно передавать несколько
аргументов, используя System. Array или специальный класс/структуру.
Следующий шаг связан с конфигурированием экземпляра делегата TimerCallback
и передачей его объекту Timer. В дополнение к настройке делегата TimerCallback,
конструктор Timer позволяет указать необязательный информационный параметр для
передачи цели делегата (определенному как System.Object), интервал вызова метода
и период времени ожидания (в миллисекундах), которое должно истечь перед первым
вызовом. Ниже показан пример.
static void Main(string [ ] args)
{
Console.WriteLine("***** Working with Timer type *****\n");
// Создать делегат для типа Timer.
TimerCallback timeCB = new TimerCallback(PrintTime);
698 Часть V. Введение в библиотеки базовых классов .NET
// Установить настройки таймера.
Timer t = new Timer (
timeCB, // Объект-делегат TimerCallback.
null, // Информация для передачи в вызванный метод (null — информация отсутствует)
О, // Период времени ожидания перед запуском (в миллисекундах).
1000) ; // Интервал времени между вызовами (в миллисекундах) .
Console.WriteLine ("Hit key to terminate...");
Console.ReadLine();
}
В этом случае метод PrintTime () будет вызываться приблизительно каждую секунду
и получит дополнительную информацию. Вот вывод примера:
***** Working with Timer type *****
Hit key to terminate. . .
Time is: 6:51:48 PM
Time is: 6:51:49 PM
Time is: 6:51:50 PM
Time is: 6:51:51 PM
Time is: 6:51:52 PM
Press any key to continue . . .
Чтобы передать цели делегата какую-то информацию, замените значение null
второго параметра конструктора соответствующей информацией:
// Установить настройки таймера.
Timer t = new Timer (timeCB, "Hello From Main", 0, 1000);
Получить входные данные можно следующим образом:
static void PrintTime(object state)
{
Console.WriteLine("Time is: {0}, Param is: {1}",
DateTime.Now.ToLongTimeStringO, state.ToString ());
}
Исходный код. Проект TimerApp доступен в подкаталоге Chapter 19.
Пул потоков CLR
Следующей темой, связанной с потоками, которую мы рассмотрим в этой главе,
является роль пула потоков CLR. При асинхронном вызове метода посредством типов
делегатов (через метод BeginlnvokeO) среда CLR на самом деле не создает новый поток.
Для эффективности метод делегата Beginlnvoke () полагается на пул рабочих потоков,
которые поддерживаются исполняющей средой. Чтобы позволить взаимодействовать с
этим пулом ожидающих потоков, в пространстве имен System.Threading предлагается
класс ThreadPool.
Чтобы запросить поток из пула для обработки вызова метода, можно использовать
метод ThreadPool.QueueUserWorkltem(). Этот метод перегружен, чтобы в дополнение
к экземпляру делегата WaitCallback позволить указывать необязательный параметр
System.Object для специальных данных состояния:
public static class ThreadPool
{
public static bool QueueUserWorkltem(WaitCallback callBack);
public static bool QueueUserWorkltem(WaitCallback callBack, object state);
}
Глава 19. Многопоточность и параллельное программирование 699
Делегат WaitCallback может указывать на любой метод, принимающий System.
Object в качестве единственного параметра (представляющего необязательные
данные состояния) и ничего не возвращающий. Обратите внимание, что если не указать
System.Object при вызове QueueUserWorkltemO, среда CLR автоматически передаст
значение null. Чтобы проиллюстрировать работу методов очередей, работающих с
пулом потоков CLR, рассмотрим еще раз программу, использующую тип Printer. В этом
случае, однако, массив объектов Thread вручную создаваться не будет; вместо этого
методу PrintNumbers () будут присваиваться члены пула:
class Program
{
static void Main(string [ ] args)
{
Console.WriteLine("***** Fun with the CLR Thread Pool *****\n");
Console.WriteLine ("Main thread started. ThreadID = {0}",
Thread.CurrentThread.ManagedThreadld);
Printer p = new Printer();
WaitCallback workltem = new WaitCallback(PrintTheNumbers);
// Поставить в очередь метод десять раз.
for (int i = 0; i < 10; i++)
{
ThreadPool.QueueUserWorkltem(workltem, p) ;
}
Console.WriteLine("All tasks queued");
Console.ReadLine();
}
static void PrintTheNumbers(object state)
{
Printer task = (Printer)state;
task.PrintNumbers() ;
}
}
Здесь может возникнуть вопрос: в чем же преимущество использования
поддерживаемого CLR пула потоков по сравнению с явным созданием объектов Thread? Ниже
перечислены эти преимущества.
1. Пул потоков управляет потоками эффективно, уменьшая количество создаваемых,
запускаемых и останавливаемых потоков.
2. Используя пул потоков, можно сосредоточиться на решении задачи, а не на
инфраструктуре потоков приложения.
Тем не менее, в некоторых случаях предпочтительно ручное управление потоками.
• Если нужны потоки переднего плана, или должен быть установлен приоритет
потока. Потоки из пула всегда являются фоновыми с приоритетом по умолчанию
(ThreadPriority. Normal).
• Если требуется поток с фиксированной идентичностью, чтобы можно было
прерывать его или находить по имени.
Исходный код. Проект TimerPoolApp доступен в подкаталоге Chapter 19.
На этом исследование пространства имен System.Threading завершается.
Остальная часть этой главы посвящена новому пополнению библиотек базовых классов
.NET 4.0, а именно — Task Parallel Library.
700 Часть V. Введение в библиотеки базовых классов .NET
Параллельное программирование
на платформе .NET
В последнее время повсеместно распространены компьютеры, оснащенные двумя
или более центральными процессорами (ядрами). Причем они не только широко
распространены, но уже и достаточно дешевы. Когда машина поддерживает несколько
процессоров, она в состоянии выполнять потоки в параллельном режиме, что значительно
увеличивает производительность выполнения приложений.
Так сложилось, что для построения приложения .NET, которое может распределять
рабочую нагрузку между несколькими ядрами, нужно обладать достаточной
квалификацией в многопоточном программировании. Хотя это определенно реально, все же эта
область довольно утомительна и подвержена ошибкам, учитывая неизбежную
сложность построения многопоточных приложений.
С выходом версии .NET 4.0 вы получили в свое распоряжение замечательную новую
библиотеку для параллельного программирования. Используя типы System.Threading.
Tasks, можно строить тонко гранулированный масштабируемый параллельный код без
необходимости непосредственной работы с потоками или пулом потоков. Более того,
при этом для распределения рабочей нагрузки можно использовать строго
типизированные запросы LINQ (с помощью параллельного LINQ, или PLINQ).
Интерфейс Task Parallel Library API
Говоря в общем, типы из пространства System.Threading.Tasks (а также
некоторые связанные с ними типы в System.Threading) объединены общим названием Task
Parallel Libranj (Библиотека параллельных задач), или TPL. Библиотека TPL позволяет
автоматически распределять нагрузку приложений между доступными
процессорами в динамическом режиме, используя пул потоков CLR. Библиотека TPL занимается
распределением работы, планированием потоков, управлением состоянием и прочими
низкоуровневыми деталями. В результате появляется возможность максимизировать
производительность приложений .NET, не имея дела со сложностями прямой работы с
потоками. На рис. 19.3 показаны члены нового пространства имен .NET 4.O.
Н Object Browser X Hj
Browse*. All Components
ШИЯШяшшвашяшшшшшшшшшшшшшшишшш
<Search>
л () Bt
0 4} Parallel
b p ParallelLoopResult
l> *4$ ParallelLoopState
t> % ParallelOptions
> ^t Task
fr 4$ Task<TResurt>
• Ц$ TaskCanceledException
> *!$ TaskCompletionSource<TResurt>
TaskContinuationOpttons
aiP TaskCreattonOpttons
t> •»■{$ TaskFactory
;> 4$ TaskFactory<TResutt>
t> ^ TaskScheduler
Г> ^ TaskSchedulerException
i> .# TaskStatus
"J| UnobservedTaskExceptionEventArgs
j -
1
В
namespace System.Threading.Tasks
Member of mscorlib
Z3EH
-
Рис. 19.3. Члены пространства имен System.Threading
Глава 19. Многопоточность и параллельное программирование 701
Начиная с .NET 4.0, использование TPL является рекомендуемым способом
построения многопоточных приложений. Это вовсе не значит, что теперь понимание
традиционных многопоточных технологий с использованием асинхронных делегатов или классов
из пространства имен System.Threading излишне. На самом деле, чтобы эффективно
использовать TPL, необходимо понимать такие примитивы, как потоки, блокировки и
параллелизм. Более того, многие ситуации, требующие нескольких потоков (такие как
асинхронные вызовы удаленных объектов), могут быть обработаны без использования
TPL. Тем не менее, время, которое понадобится для непосредственной работы с классом
Thread, уменьшается, и существенно.
Наконец, имейте в виду, что возможность что-то делать вовсе не означает
необходимость это делать. В некоторых случаях создание нескольких потоков может замедлить
выполнение программ .NET, а порождение множества излишних параллельных задач
может нанести ущерб производительности. Используйте функциональность TPL, только
когда есть рабочая нагрузка, которая действительно создает узкое место в программах,
например, итерация по сотням объектов, обработка данных из нескольких файлов и т.п.
На заметку! Инфраструктура TPL довольно интеллектуальна. Если TPL определяет, что набор
задач будет иметь лишь минимальный выигрыш (или вообще никакого) от выполнения в
параллельном режиме, то выполняет такие задачи последовательно.
Роль класса Parallel
Главным классом в TPL является System.Threading.Tasks.Parallel. Этот класс
поддерживает набор методов, которые позволяют выполнять итерации по коллекции
данных (точнее, по объектам, реализующим IEnumerable<T>) в параллельном режиме.
Заглянув в документацию .NET Framework 4.0 SDK, вы увидите, что этот класс
поддерживает два статических метода— Parallel.For () и Parallel.ForEach(), для каждого
из которых определены многочисленные перегруженные версии.
Эти методы позволяют создавать тело операторов кода, которое может
выполняться в параллельном режиме. Концептуально эти операторы представляют собой
логику того же рода, которую была бы написана в нормальной циклической конструкции
(с использованием ключевых слов С# for и foreach). Однако их преимущество состоит
в том, что класс Parallel самостоятельно берет потоки из пула потоков (и управляет
конкуренцией).
Оба эти метода требуют указания совместимого с IEnumerable или IEnumerable<T>
контейнера, хранящего данные, которые нужно обработать в параллельном режиме.
Контейнер может быть простым массивом, необобщенной коллекцией (вроде ArrayList),
обобщенной коллекцией (наподобие List<T>) или результатом запроса LINQ.
Вдобавок нужно будет использовать делегаты System.Func<T> и System. Action<T>
для указания целевого метода, который будет вызываться для обработки данных. Вы
уже встречали делегат Func<T> в главе 13, когда рассматривалась технология LINQ to
Objects. Вспомните, что Func<T> представляет метод, который возвращает значение
и принимает различное количество аргументов. Делегат Action<T> очень похож на
Func<T> в том, что позволяет указывать метод, принимающий несколько параметров.
Однако Action<T> указывает метод, который может возвращать только void.
Хотя можно было бы вызывать методы Parallel.For() и Parallel.ForEachO и
передавать строго типизированные объекты делегатов Func<T> или Action<T>, задача
программирования упрощается с использованием подходящих анонимных методов С#
и лямбда-выражений.
702 Часть V. Введение в библиотеки базовых классов .NET
Понятие параллелизма данных
DiaBHoe применение TPL связано с обеспечением параллелизма данных. Этим
термином обозначается задача итерации по массиву или коллекции в параллельном
режиме с использованием методов Parallel.ForO или Parallel.ForEach(). Предположим,
что нужно выполнять некоторые трудоемкие операции, связанные с файловым вводом-
выводом. Например, требуется загрузить в память огромное количество файлов *.jpg,
повернуть содержащиеся в них изображения и сохранить модифицированные данные
в новом местоположении.
В документации .NET Framework 4.0 SDK представлен пример консольного
приложения, решающего эту задачу, но мы повторим его, оснастив графическим
интерфейсом пользователя. Давайте создадим приложение Windows Forms по имени
DataParallelismWithForEach и переименуем Forml.cs в MainForm.cs. После этого
импортируем в главный файл кода следующие пространства имен:
// Эти пространства имен нужны!
using System.Threading.Tasks;
using System.Threading;
using System.10;
Графический интерфейс этого приложения содержит многострочную текстовую
область TextBox и одну кнопку Button (по имени btnProcessImages). Текстовая область
предназначена для ввода данных во время выполнения работы в фоновом режиме, что
иллюстрирует неблокирующую природу параллельной задачи. В обработчике события
Click этого элемента Button в конечном итоге будет использоваться TPL, а пока
напишем следующий блокирующий код:
public partial class MainForm : Form
{
public MainForm ()
InitializeComponent();
private void btnProcessImages_Click(object sender, EventArgs e)
ProcessFiles();
private void ProcessFiles ()
// Загрузить все файлы *.jpg и создать новую папку для модифицированных данных,
string[] files = Directory.GetFiles(@"C:\Users\AndrewTroelsen\Pictures\My Family",
"*.jpg", SearchOption.AllDirectories) ;
string newDir = @"C:\ModifiedPictures";
Directory.CreateDirectory(newDir);
// Обработка графических данных в блокирующей манере.
foreach (string currentFile in files)
{
string filename = Path.GetFileName(currentFile);
using (Bitmap bitmap = new Bitmap(currentFile))
{
bitmap.RotateFlip(RotateFlipType.Rotatel80FlipNone);
bitmap.Save(Path.Combine(newDir, filename));
this.Text = string.Format("Processing {0} on thread {1}", filename,
Thread.CurrentThread.ManagedThreadld) ;
}
}
this.Text = "All done!";
}
}
Глава 19. Многопоточность и параллельное программирование 703
' Processing P'.
Feel free to type here while the images are processed.
b
0»ck to Hip Your Images!
Обратите внимание, что метод Process
Files () поворачивает изображение в каждом
файле *.jpg из папки Pictures\My Family,
которая в данный момент содержит всего
37 файлов (при необходимости укажите
другой путь в вызове Directory.GetFiles ()).
В настоящее время вся работа происходит
в первичном потоке исполняемой
программы. Поэтому после щелчка пользователем на
кнопке программа выглядит зависшей. Более
того, заголовок окна также сообщит о том,
что тот же первичный поток обрабатывает
файл (рис. 19.4).
Чтобы обеспечить обработку файлов на как можно большем числе процессоров
(или ядер), следует переписать цикл foreach, воспользовавшись Parallel.ForEach().
Вспомните, что этот метод имеет множество перегрузок. В его простейшей форме
методу можно передать совместимый с IEnumerable<T> объект, который содержит
элементы, подлежащие обработке (например, массив строк с именами файлов), и делегат
Action<T>, который указывает на метод, выполняющий работу. Ниже показано
важнейшее изменение с использованием лямбда-операции С# вместо литерального объекта
делегата Action<T>:
// Обработка графических данных в параллельном режиме!
Parallel.ForEach(files, currentFile =>
Рис. 19.4. Сейчас вся работа происходит в
первичном потоке
{
string filename = Path.GetFileName(currentFile);
using (Bitmap bitmap = new Bitmap(currentFile))
{
bitmap.RotateFlip(RotateFlipType.Rotatel80FlipNone);
bitmap.Save(Path.Combine(newDir, filename));
this.Text = string.Format("Processing {0} on thread {1}'
Thread.CurrentThread.ManagedThreadld);
filename,
}
),
Если теперь запустить программу, библиотека TPL распределит рабочую нагрузку по
множеству потоков, взятых из пула, используя столько процессоров, сколько возможно.
Однако в заголовке окна не будут отображаться имена уникальных потоков, а при вводе
в текстовой области ничего не будет видно, пока не обработаются все файлы
изображений. Причина в том, что первичный поток пользовательского интерфейса все равно
остается блокированным, ожидая, пока все прочие потоки завершат свою работу.
Класс Task
Чтобы сохранить пользовательский интерфейс отзывчивым, можно напрямую
использовать асинхронные делегаты или члены пространства System.Threading, но
пространство имен System.Threading.Tasks предлагает более простую альтернативу —
класс Task. Этот класс позволяет легко вызывать метод во вторичном потоке и может
применяться вместо работы с асинхронными делегатами. Изменим обработчик события
Click элемента Button следующим образом:
private void btnProcessImages_Click(object sender, EventArgs e)
{
// Запустить новую "задачу" для обработки файлов.
704 Часть V. Введение в библиотеки базовых классов .NET
Task.Factory.StartNew( () =>
{
ProcessFiles ();
});
}
Свойство Factory класса Task возвращает объект TaskFactory. При вызове методу
StartNow() передается делегат Action<T> (здесь это скрыто подходящим
лямбда-выражением), который указывает на метод для вызова в асинхронной манере. После этой
небольшой модификации вы обнаружите, что заголовок окна отображает, какой поток
из пула обрабатывает конкретный файл, а текстовое поле может принимать ввод,
поскольку пользовательский интерфейс больше не блокируется.
Обработка запроса на отмену
В текущий пример можно добавить еще одно усовершенствование — позволить
пользователю останавливать обработку графических данных за счет щелчка на второй
кнопке Cancel (Отмена). К счастью оба метода, Parallel.For() и Parallel.ForEachO,
поддерживают отмену через использование маркеров отмены (cancellation tokens). При
вызове методов на Parallel можно передавать объект ParallelOptions, который, в
свою очередь, содержит объект CancellationTokenSource.
Прежде всего, определим в классе-наследнике Form приватную переменную-член
cancelToken типа CancellationTokenSource:
public partial class MainForm : Form
{
// Новая переменная уровня Form.
private CancellationTokenSource cancelToken =
new CancellationTokenSource();
}
Предполагая, что был добавлен новый элемент Button (по имени btnCancel),
реализуем его обработчик события Click следующим образом:
private void btnCancelTask_Click(object sender, EventArgs e)
{
// Это будет использовано для передачи всем рабочим потокам команды останова!
cancelToken.Cancel ();
}
Теперь можно внести необходимые модификации в метод ProcessFiles(). Ниже
показана окончательная реализация этого метода.
private void ProcessFiles ()
{
// Использовать экземпляр ParallelOptions для сохранения CancellationToken.
ParallelOptions parOpts = new ParallelOptions ();
parOpts.CancellationToken = cancelToken.Token;
parOpts.MaxDegreeOfParallelism = System.Environment.ProcessorCount;
// Загрузить все файлы *.jpg и создать новую папку для модифицированных данных.
string[] files = Directory.GetFiles
(@"C:\Users\AndrewTroelsen\Pictures\My Family", "*.]pg",
SearchOption.AllDirectories);
string newDir = @"C:\ModifledPictures";
Directory.CreateDirectory(newDir);
try
{
// Обработать данные изображения в параллельном режиме!
Глава 19. Многопоточность и параллельное программирование 705
Parallel.ForEach(files, parOpts, currentFile =>
{
parOpts.CancellationToken.ThrowIfCancellationRequested();
string filename = Path.GetFileName(currentFile) ;
using (Bitmap bitmap = new Bitmap(currentFile))
{
bitmap.RotateFlip(RotateFlipType.Rotatel80FlipNone);
bitmap.Save(Path.Combine(newDir, filename));
this.Text = string.Format("Processing {0} on thread {1}", filename,
Thread.CurrentThread.ManagedThreadld);
}
}
);
this.Text = "All done1";
}
catch (OperationCanceledException ex)
{
this.Text = ex.Message;
}
}
Обратите внимание, что метод начинается с конфигурирования объекта Parallel
Options путем установки его свойства CancellationToken в cancelToken.Token.
Кроме того, при вызове методу Parallel.ForEach() во втором параметре передается
объект ParallelOptions.
В контексте логики цикла осуществляется вызов ThrowIfCancellationRequestedO
на маркере. Это гарантирует, что если пользователь щелкнет на кнопке Cancel, то все
потоки будут остановлены, и будет сгенерировано исключение времени выполнения.
Перехватив исключение OperationCanceledException, можно включить сообщение
об ошибке в текст главного окна.
Исходный код. Проект DataParallelismWithForEach доступен в подкаталоге Chapter 19.
Понятие параллелизма задач
В дополнение к обеспечению параллелизма данных, библиотека TPL также может
применяться для простого запуска любого количества асинхронных задач с помощью
метода Parallel.Invoke(). Этот подход немного проще, чем использование
делегатов или типов из пространства имен System.Threading. Тем не менее, если нужна
более высокая степень контроля над выполняемыми задачами, следует отказаться от
Parallel.Invoke() и напрямую работать с классом Task, как это делалось в
предыдущем примере.
Чтобы проиллюстрировать параллелизм задач в действии, создадим новое
приложение Windows Forms по имени MyEBookReader и импортируем в нем пространства имен
System.Threading.Tasks и System.Net.
Это приложение представляет собой модификацию примера из документации
.NET Framework 4.0 SDK, в котором извлекается электронная книга из сайта проекта
Гуттенберга (http://www.gutenberg.org) и затем параллельно выполняется набор
длительных заданий.
Графический интерфейс состоит из многострочной текстовой области Text Box (no
имени txtBook) и двух кнопок Button (btnDownload и btnGetStats). Для каждой
кнопки понадобится обработать событие Click, а в файле кода формы объявить на уровне
класса переменную string по имени theEBook. Ниже показана реализация
обработчика события Click для кнопки btnDownload:
706 Часть V. Введение в библиотеки базовых классов .NET
private void btnDownload_Click (object sender, EventArgs e)
{
WebClient wc = new WebClientO ;
wc.DownloadStringCompleted += (s, eArgs) =>
{
theEBook = eArgs.Result;
txtBook.Text = theEBook;
};
// Загрузка электронной книги "A Tale of Two Cities".
wc.DownloadStringAsync(new Uri("http://www.gutenberg.org/files/98/98-8.txt")) ;
}
Класс WebClient — это член пространства имен System.Net. Он
предоставляет ряд методов для отправки данных и получения данных от ресурса,
идентифицированного URL. Многие из этих методов имеют асинхронные версии, такие как
DownloadStringAsynO- Этот метод автоматически запускает новый поток, взятый из
пула потоков CLR. Когда WebClient завершает получение данных, он инициирует
событие DownloadStringCompleted, которое обрабатывается с использованием лямбда-
выражения С#. Если вызвать синхронную версию этого метода (DownloadStringO), то
форма на некоторое время перестанет реагировать на действия пользователя.
Обработчик события Click кнопки btnGetStats реализован так, чтобы извлекать
индивидуальные слова, содержащиеся в переменной theEBook, и передавать строковый
массив на обработку новой вспомогательной функции:
private void btnGetStats_Click(object sender, EventArgs e)
{
// Получить слова из электронной книги.
string[] words = theEBook.Split (new char[]
{ ' ', '\u000A\ ',', '.', ';', ':', '-', '?', '/' },
StringSplitOptions.RemoveEmptyEntries);
// Найти 10 наиболее часто встречающихся слов.
string[] tenMostCommon = FindTenMostCommon(words);
// Получить самое длинное слово.
string longestWord = FindLongestWord(words);
// Когда все задачи завершены, построить строку,
// показывающую всю статистику в окне сообщений.
StringBuilder bookStats = new StringBuilder("Ten Most Common Words are:\n");
foreach (string s in tenMostCommon)
{
bookStats.AppendLine(s);
}
bookStats.AppendFormat("Longest word is : {0}", longestWord);
bookStats.AppendLine() ;
MessageBox.Show(bookStats.ToString() , "Book info");
}
Метод FindTenMostCommonO использует запрос LINQ для получения списка
объектов string, которые наиболее часто встречаются в массиве string, а метод
FindLongestWord() находит самое длинное слово:
private string[] FindTenMostCommon(string [ ] words)
{
var frequencyOrder = from word in words
where word.Length > 6
group word by word into g
orderby g.Count () descending
select g.Key;
string[] commonWords = (frequencyOrder.Take A0)) .ToArray();
return commonWords;
Глава 19. Многопоточность и параллельное программирование 707
private string FindLongestWord(string[] words)
{
return (from w in words orderby w.Length descending select w) .First ();
}
После запуске этого проекта на выполнение всех задач может потребоваться
некоторое время, в зависимости от количества процессоров машины и их тактовой частоты.
В конце концов, должен появиться следующий вывод (рис. 19.5).
«fa* My EBook Reader
The Prefect Gutenberg EBook of A Tate of Two Gbes by Chafes Щ
This eBook is for the use of anyone anywhere at no cost and with
almost no restrictions whatsoever You may copy t give 1 away or
rente 1 under the tern» of the Project Gutenberg License nduded
wth this eBook or online at www gutenberg.org
Tile A Tale of Two Obes
A Story of the French Revolution
Author: Charles Dickens
Release Date: January. 1994 {EBook S98]
Posting Date: November 28. 2009
Language Engtsh
Character set encodng: ISO-8859-1
~ START OF THIS PROJECT GUTENBERG EBOOK A TALE 01
Рис. 19.5. Статистика загруженной электронной книги
Можете убедиться, что приложение использует все доступные процессоры машины,
вызвав параллельно методы FindTenMostCommonO и FindLongestWordO. Для этого
необходимо модифицировать метод btnGetStats_Click() следующим образом:
private void btnGetStats_Click(object sender, EventArgs e)
// Получить слова из электронной книги.
string[] words = theEBook.Split (
new char[] { ' ', '\u000A\ \\ '.', ';', ':', '
StringSplitOptions.RemoveEmptyEntries);
string[] tenMostCommon = null;
string longestWord = string.Empty;
Parallel.Invoke(
0 =>
{
// Найти 10 наиболее часто встречающихся слов.
tenMostCommon = FindTenMostCommon(words);
'/' },
О =>
{
// Найти самое длинное слово.
longestWord = FindLongestWord(words);
});
// Когда все задачи завершены, построить строку,
// показывающую всю статистику в окне сообщений.
Метод Parallel.InvokeO ожидает параметра-массива делегатов Act ion о,
которые передаются неявно с помощью лямбда-выражения. Хотя вывод идентичен, преиму-
708 Часть V. Введение в библиотеки базовых классов .NET
щество библиотеки TPL состоит в использовании всех доступных процессоров машины
для вызова каждого метода параллельно, если это возможно.
Исходный код. Проект MyEBookReader доступен в подкаталоге Chapter 19.
Запросы параллельного LINQ (PLINQ)
В завершение знакомства с TPL следует отметить, что есть и другой способ
встраивания параллельных задач в ваши приложения .NET. При желании можно
использовать новый набор расширяющих методов, которые позволяют конструировать запрос
LINQ, распределяющий свою нагрузку по параллельным потокам (если это возможно).
Неудивительно, что запросы LINQ, которые спроектированы для параллельного
выполнения, называются запросами Parallel LINQ (PLINQ).
Подобно параллельному коду, написанному с использованием класса Parallel, в PLINQ
имеется опция игнорирования запроса на обработку коллекции в параллельном режиме.
Библиотека PLINQ оптимизирована во многих отношениях, включая определение того,
не будет ли запрос на самом деле эффективнее выполняться в синхронном режиме.
Во время выполнения PLINQ анализирует общую структуру запроса, и если есть
вероятность, что запрос выиграет от параллелизма, он будет выполнен параллельно.
Однако если это ухудшит производительность, PLINQ выполнит запрос
последовательно. Если возникает выбор между потенциально дорогостоящим параллельным
алгоритмом и недорогим последовательным, предпочтение по умолчанию отдается
последовательному алгоритму.
Необходимые расширяющие методы находятся в классе ParallelEnumerable
пространства имен System.Linq. В табл. 19.5 описаны некоторые полезные расширения
PLINQ.
Таблица 19.5. Некоторые члены класса ParallelEnumarable
Член Назначение
AsParallelO Указывает, что остаток запроса должен быть выполнен
параллельно, если это возможно
WithCancellationO Указывает, что PLINQ должен периодически следить за
состоянием представленного маркера отмены и, если понадобится,
отменять выполнение
WithDegreeOf ParallelismO Указывает максимальное количество процессоров, которое
PLINQ должен использовать для распараллеливания запроса
For All () Позволяет обрабатывать результаты параллельно, без
предварительного соединения с потоком-потребителем, как это
происходит при перечислении результата LINQ посредством
ключевого слова foreach
Чтобы посмотреть на PLINQ в действии, создадим приложение Windows Forms
по имени PLINQDataProcessingWithCancellation и импортируем в него
пространства имен System.Threading и System.Threading.Tasks. Эта простая форма
потребует всего двух кнопок с именами btnExecute и btnCancel. Щелчок на кнопке Execute
(Выполнить) приводит к запуску новой задачи (Task), которая выполнит запрос LINQ,
просматривающий очень большой массив целых чисел в поисках элементов, для
которых остаток от деления на 3 равен 0. Ниже показана непараллельная версия этого
запроса:
Глава 19. Многопоточность и параллельное программирование 709
public partial class MainForm : Form
{
private void btnExecute_Click(object sender, EventArgs e)
{
// Запустить новую "задачу" для обработки целых.
Task.Factory.StartNew ( () =>
{
ProcessIntData ();
});
}
private void ProcessIntData ()
{
// Получить очень большой массив целых чисел.
int[] source = Enumerable.Range A, 10000000).ToArray();
// Найти числа, для которых истинно num и 3 == 0,
//и возвратить их в порядке убывания.
int [ ] modThreelsZero = (from num in source where num % 3 == 0
orderby num descending select num) .ToArray ();
MessageBox.Show (string.Format("Found {0} numbers that match query!",
modThreelsZero.Count()));
}
}
Выполнение запроса PLINQ
Чтобы заставить TPL выполнить этот запрос в параллельном режиме (по
возможности), для этого понадобится расширяющий метод AsParallel():
int [ ] modThreelsZero = (from num in source .AsParallel () where num n6 3 == 0
orderby num descending select num).ToArray();
Обратите внимание, что общий формат запроса LINQ идентичен тому, что было
показано в предыдущих главах. Однако, при включенном вызове AsParallel(),
библиотека TPL попытается распределить нагрузку по всем доступным процессорам.
Отмена запроса PLINQ
С помощью объекта CancellationTokenSource можно заставить запрос PLINQ
прекращать обработку при определенных условиях (обычно из-за вмешательства
пользователя). Для этого потребуется объявить на уровне формы объект CancellationTokenSource
по имени cancelToken и реализовать обработчик события Click кнопки btnCancel.
Ниже показаны соответствующие изменения в коде:
public partial class MainForm : Form
{
private CancellationTokenSource cancelToken = new CancellationTokenSource ();
private void btnCancel_Click(object sender, EventArgs e)
{
cancelToken.Cancel ();
}
}
Теперь необходимо информировать запрос PLINQ о том, что он должен ожидать
запроса на отмену выполнения, добавив в цепочку расширяющий метод WithCancellation ()
и передав маркер. Вдобавок нужно поместить этот запрос PLINQ в контекст try/catch
и обработать возможные исключения. Финальная версия метода ProcessInDataO
выглядит следующим образом:
710 Часть V. Введение в библиотеки базовых классов .NET
private void ProcessIntData ()
{
// Получить очень большой массив целых чисел.
int[] source = Enumerable.Range A, 10000000).ToArray();
// Найти числа, для которых истинно num % 3 == 0, и возвратить их в порядке убывания
int[] modThreelsZero = null;
try
{
modThreelsZero = (from num in
source.AsParallel().WithCancellation(cancelToken.Token)
where num nu 3 == 0 orderby num descending
select num).ToArray();
}
catch (OperationCanceledException ex)
{
this.Text = ex.Message;
}
MessageBox.Show(string.Format("Found {0} numbers that match query!",
modThreelsZero.Count ()));
}
На этом первоначальное знакомство с библиотекой Tksk Parallel Library и PLINQ
завершено. Как было сказано, эти новые API-интерфейсы .NET 4.0 быстро завоевывают
популярность при работе с многопоточными приложениями. Тем не менее,
эффективное использование TPL и PLINQ требует основательного понимания концепций и
примитивов многопоточности, представленных в этой главе.
Дополнительные сведения можно найти в разделе "Parallel Programming in the .NET
Framework" ("Параллельное программирование на платформе .NET Framework") в
документации .NET Framework 4.0 SDK. В нем представлен богатый набор примеров
проектов, которые расширят то, что было описано ранее в главе.
Исходный код. Проект PLINQDataProcessingWithCancellation доступен в подкаталоге
Chapter 19.
Резюме
Эта глава началась с описания того, как сконфигурировать типы делегатов .NET для
выполнения метода в асинхронном режиме. Как вы видели, методы BeginlnvokeO и
EndlnvokeO позволяют неявно манипулировать вторичным потоком с минимальными
усилиями с вашей стороны. Далее были представлены интерфейс IAsyncResult и тип
класса AsyncResult. Было показано, что эти типы предлагают различные способы
синхронизации вызывающего потока и получения возможных возвращаемых значений методов.
Следующая часть главы была посвящена рассмотрению роли пространства имен
System.Threading. Как было сказано, когда приложение создает дополнительные
потоки выполнения, в результате оно может выполнять несколько задач (как кажется)
одновременно. Также были продемонстрированы различные способы зашиты
чувствительных к потокам блоков кода для предотвращения повреждения разделяемых ресурсов.
Глава завершилась рассмотрением совершенно новой модели многопоточной
разработки на платформе .NET 4.0 — библиотеки Tksk Parallel Library и PLINQ. Можно смело
утверждать, что эти API-интерфейсы станут предпочтительным инструментом
построения многозадачных систем, поскольку они позволяют программировать с
использованием набора высокоуровневых типов (многие из которых находятся в пространстве
имен System.Threading.Tasks), которые скрывают от разработчика значительную
часть сложности.
ГЛАВА 20
Файловый ввод-вывод
и сериализация объектов
При создании настольных приложений способность сохранять информацию
между пользовательскими сеансами является обязательной. В этой главе
рассматривается множество тем, связанных с вводом-выводом, с точки зрения платформы .NET
Framework. Первая задача связана с исследованием основных типов, определенных в
пространстве имен System. 10, которые позволяют программно модифицировать
структуру каталогов и файлов. Вторая задача состоит в изучении различных способов
чтения и записи символьных, двоичных и располагаемых в памяти структур данных.
Изучив способы манипулирования файлами и каталогами с использованием базовых
типов ввода-вывода, вы ознакомитесь с родственной темой — сериализацией объектов.
Сериализация объектов служит для сохранения и восстановления состояния объекта в
любом типе, унаследованном от System. 10. Stream. Возможность сериализации
объектов критична, когда необходимо копировать объект на удаленную машину с помощью
технологий удаленного взаимодействия, таких как Windows Communication Foundation.
Однако сериализация удобна и сама по себе, и наверняка пригодится во многих
разрабатываемых приложениях .NET (как распределенных, так и нет).
Исследование пространства имен System. 10
Пространство имен System. 10 в .NET — это область библиотек базовых классов,
посвященная службам файлового ввода-вывода, а также ввода-вывода из памяти. Подобно
любому пространству имен, в System. 10 определен набор классов, интерфейсов,
перечислений, структур и делегатов, большинство из которых находятся в mscorlib.dll.
В дополнение к типам, содержащимся внутри mscorlib.dll, в сборке System.dll
определены дополнительные члены пространства имен System. 10 . Обратите внимание, что
все проекты Visual Studio 2010 автоматически устанавливают ссылки на обе сборки.
Многие типы из пространства имен System. 10 сосредоточены на программных
манипуляциях физическими каталогами и файлами. Дополнительные типы
предоставляют поддержку чтения и записи данных в строковые буферы, а также области памяти.
В табл. 20.1 кратко описаны основные (неабстрактные) классы, которые дают понятие
о функциональности System. 10.
712 Часть V. Введение в библиотеки базовых классов .NET
Таблица 20.1. Ключевые члены пространства имен System.10
Неабстрактные классы
ввода-вывода
Назначение
BinaryReader
BinaryWnter
BufferedStream
Directory
Directorylnfo
Drivelnfo
File
Filelnfo
FileStream
FileSystemWatcher
MemoryStream
Path
StreamWriter
StreamReader
StringWriter
StringReader
Эти классы позволяют сохранять и извлекать элементарные типы
данных (целочисленные, булевские, строковые и т.п.) в двоичном виде
Этот класс предоставляет временное хранилище для потока байтов,
которые могут затем быть перенесены в постоянные хранилища
Эти классы используются для манипуляций структурой каталогов
машины. Тип Directory представляет функциональность, используя
статические члены. Тип Directorylnfo обеспечивает аналогичную
функциональность через действительную объектную ссылку
Этот класс предоставляет детальную информацию относительно
дисковых устройств, используемых данной машиной
Эти классы служат для манипуляций множеством файлов данной
машины. Тип File представляет функциональность через статические
члены. Тип Filelnfo обеспечивает аналогичную функциональность
через действительную объектную ссылку
Этот класс обеспечивает произвольный доступ к файлу (т.е.
возможности поиска) с данными, представленными в виде потока байт
Этот класс позволяет отслеживать модификации внешних файлов в
определенном каталоге
Этот класс обеспечивает произвольный доступ к данным,
хранящимся в памяти, а не в физическом файле
Этот класс выполняет операции над типами System.String,
содержащими информацию о пути к файлу или каталогу в независимой от
платформы манере
Эти классы используются для хранении я (и извлечения) текстовой
информации из файла. Эти классы не поддерживают произвольного
доступа к файлу
Подобно классам StreamWriter/StreamReader, эти классы
также работают с текстовой информацией Однако лежащим в основе
хранилищем является строковый буфер, а не физический файл
В дополнение к этим конкретным типам классов в System. 10 определено
несколько перечислений, а также набор абстрактных классов (т.е. Stream, TextReader и
TextWriter), которые определяют разделяемый полиморфный интерфейс для всех
наследников. В этой главе вы узнаете о многих таких типах.
Классы Directory (Directorylnfo)
и File (Filelnfo)
В System. 10 предоставляются четыре класса, которые позволяют манипулировать
индивидуальными файлами, а также взаимодействовать со структурой каталогов
машины. Первые два класса — Directory и File — предлагают операции создания,
удаления, копирования и перемещения с использованием различных статических членов.
Родственные им классы Filelnfo и Directorylnfo предлагают подобную
функциональность в виде методов уровня экземпляра (и потому должны размещаться в памяти с
помощью ключевого слова new). Обратите внимание на рис. 20.1, что классы Directory
Глава 20. Файловый ввод-вывод и сериализация объектов 713
и File непосредственно расширяют System.Object, в то время как Directorylnfo и
Filelnfo наследуются от абстрактного класса FileSystemlnfo.
г~
FileSystemlnfo
Abstract Class
Object
Class
He
Class
(P
£
Directory
Class
®N
Filelnfo
Class
■+ FileSystemlnfo
Directorylnfo QE
Class
•* FileSystemlnfo
Рис. 20.1. Классы для работы с файлами и каталогами
Вообще говоря, Filelnfo и Directorylnfo представляют собой лучший выбор для
получения полных подробностей о файле или каталоге (например, время создания,
возможности чтения/записи и т.п.), поскольку их члены возвращают строго
типизированные объекты. В отличие от этого, члены классов Directory и File обычно возвращают
простые строковые значения, а не строго типизированные объекты.
Абстрактный базовый класс FileSystemlnfo
Классы Directorylnfo и Filelnfo унаследовали значительную часть своего
поведения от абстрактного базового класса FileSystemlnfo. По большей части члены класса
FileSystemlnfo используются для получения общих характеристик (таких как время
создания, различные атрибуты и т.д.) определенного файла или каталога. В табл. 20.2
перечислены некоторые основные свойства, представляющие интерес.
Таблица 20.2. Свойства класса FileSystemlnfo
Свойство
Назначение
Attributes
CreationTime
Exists
Extension
FullName
LastAccessTime
LastWriteTime
Получает или устанавливает ассоциированные с текущим файлом
атрибуты, которые представлены перечислением FileAttributes (доступный
только для чтения, зашифрованный, скрытый или сжатый)
Получает или устанавливает время создания текущего файла или каталога
Может использоваться для определения, существует ли данный файл или
каталог
Извлекает расширение файла
Получает полный путь к файлу или каталогу
Получает или устанавливает время последнего доступа к текущему файлу
или каталогу
Получает или устанавливает время последней записи в текущий файл или
каталог
Name
Получает имя текущего файла или каталога
714 Часть V. Введение в библиотеки базовых классов .NET
В классе FileSystemlnfo также определен метод Delete(). Этот метод реализуется
производными типами для удаления файла или каталога с жесткого диска. Кроме того,
метод Refresh() может быть вызван перед получением информации об атрибутах,
чтобы обеспечить актуальность состояния статистики о текущем файле или каталоге.
Работа с типом Directorylnf о
Первый неабстрактный тип, связанный с вводом-выводом, который мы
рассмотрим здесь — Directorylnfo. Этот класс содержит набор членов, используемых для
создания, перемещения, удаления и перечисления каталогов и подкаталогов. В
дополнение к функциональности, предоставленной базовым классом (FileSystemlnfo),
Directorylnfo предлагает ключевые члены, перечисленные в табл. 20.3.
Таблица 20.3. Ключевые члены типа Directorylnfo
Член Назначение
Create () Создает каталог (или набор подкаталогов) по заданному путево-
CreateSubdirectoryO муимени
Delete () Удаляет каталог и все его содержимое
GetDirectories () Возвращает массив объектов Directorylnfo,
представляющих все подкаталоги в текущем каталоге
Get Files () Извлекает массив объектов Filelnfo, представляющий
множество файлов в заданном каталоге
MoveTo () Перемещает каталог со всем содержимым по новому пути
Parent Извлекает родительский каталог данного каталога
Root Получает корневую часть пути
Работа с типом Directorylnfo начинается с указания определенного пути в
качестве параметра конструктора. Если требуется получить доступ к текущему рабочему
каталогу (т.е. каталогу выполняющегося приложения), применяйте нотацию ".". Вот
некоторые примеры:
// Привязаться к текущему рабочему каталогу.
Directorylnfo dirl = new Directorylnfo(".");
// Привязаться к C:\Windows, используя литеральную строку.
Directorylnfo dir2 = new Directorylnfo(@"C:\Windows");
Во втором примере делается предположение, что переданный в конструктор
путь (C:\Windows) физически существует на машине. При попытке
взаимодействовать с несуществующим каталогом будет сгенерировано исключение System.
10.DirectoryNotFoundException. Таким образом, чтобы указать каталог, который пока
еще не создан, сначала придется вызвать метод Create ():
// Привязаться к несуществующему каталогу, затем создать его.
Directorylnfo dir3 = new Directorylnfo (@"C:\MyCode\Testing");
dir3.Create ();
После создания объекта Directorylnfo можно исследовать его содержимое,
используя любое свойство, унаследованное от FileSystemlnfo. Для иллюстрации создайте
новое консольное приложение по имени DirectoryApp и добавьте в файл кода С# импорт
пространства имен System. 10. Дополните класс Program новым статическим методом,
который создаст новый объект Directorylnfo, отображенный на C:\Windows (при
необходимости подкорректируйте путь), и выведет ряд статистических показателей:
Глава 20. Файловый ввод-вывод и сериализация объектов 715
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Directory (Info) *****\nM);
ShowWindowsDirectorylnfo();
Console.ReadLine();
}
static void ShowWindowsDirectorylnfo()
{
// Вывести информацию о каталоге.
Directorylnfo dir = new Directorylnfo(@"C:\Windows");
Console.WriteLine("***** Directory Info *****");
Console.WriteLine ("FullName: {0}", dir.FullName); // полное имя
Console.WriteLine("Name: {0}", dir.Name); // имя каталога
Console.WriteLine ("Parent: {0}", dir.Parent); // родительский каталог
Console.WriteLine ("Creation: {0}", dir.CreationTime); // время создания
Console.WriteLine("Attributes: {0}", dir.Attributes); // атрибуты
Console.WriteLine ("Root: {0}", dir.Root); // корневой каталог
Console.WriteLine("************************** \n" );
}
}
Вывод будет выглядеть примерно так, как показано ниже:
***** pun wj_th Directory(Info) *****
***** Directory Info *****
FullName: C:\Windows
Name: Windows
Parent:
Creation: 7/13/2009 10:20:08 PM
Attributes: Directory
Root: C:\
••••••••••••••••••••••••••
Перечисление файлов с помощью типа Directorylnfo
В дополнение к получению базовых деталей о существующем каталоге можно
расширить текущий пример использованием некоторых методов типа Directorylnfo. Для
начала применим метод GetFiles () для получения информации обо всех файлах *.jpg,
расположенных в каталоге C:\Windows\Web\Wallpaper.
На заметку! Если на вашей машине нет каталога C:\Windows\Web\Wallpaper, измените код
для чтения файлов из какого-то существующего каталога (например, прочитайте все файлы
*.bmp из каталога C:\Windows).
Метод GetFiles () возвращает массив объектов типа Filelnfo, каждый из которых
представляет детальную информацию о конкретном файле (все подробности о типе
Filelnfo будут описаны далее в этой главе). Предположим, что следующий статический
метод класса Program вызывается в методе Main():
static void DisplaylmageFiles ()
{
Directorylnfo dir = new Directorylnfo(@"C:\Windows\Web\Wallpaper");
// Получить все файлы с расширением *.jpg.
FileInfo[] imageFiles = dir.GetFiles("*.jpg", SearchOption.AllDirectones) ;
// Сколько файлов найдено?
Console.WriteLine("Found {0} *.jpg files\n", imageFiles.Length);
716 Часть V. Введение в библиотеки базовых классов .NET
// Вывести информацию о каждом файле.
foreach (Filelnfo f in imageFiles)
Console.WriteLine(
Console.WriteLine(
Console.WriteLine(
Console.WriteLine(
Console.WriteLine(
Console.WriteLine(
lr ***** •
) ;
File name: {0}", f.Name); // имя
File size: {0}", f.Length); // размер
Creation: {0}", f.CreationTime); // время создания
Attributes: {0}", f.Attributes); // атрибуты
r ****** t!
lr******************
\n") j
}
Обратите внимание на указание в вызове GetFiles () опции поиска; SearchOption.
AllDirectories обеспечивает просмотр всех подкаталогов корня. После запуска этого
приложения получается список файлов, отвечающих критерию поиска.
Создание подкаталогов с помощью типа Directorylnf о
Для программного расширения структуры каталогов служит метод Directorylnf о.
CreateSubdirectory (). С его помощью можно создавать как одиночный подкаталог, так
и множество вложенных подкаталогов за один вызов. Для иллюстрации ниже приведен
метод, который расширяет структуру диска С: дополнительными подкаталогами:
static void ModifyAppDirectory ()
{
Directorylnfo dir = new Directorylnfo (@"C:\");
// Создать \MyFolder в каталоге приложения.
dir.CreateSubdirectory("MyFolder");
// Создать \MyFolder2\Data в каталоге приложения.
dir .CreateSubdirectory (@"MyFolder2\Data") ;
}
Вызвав этот метод в Main() и выполнив программу, в проводнике Windows можно
будет увидеть новые подкаталоги (рис. 20.2).
ПИ^Я Ь > l ninputef » Mongo Drive (G) ► Myfdder2 ►
Organize *■ Include in library » Share with » Burn
И ШЯЯШША ',
Ь MyFolder
- ii MyFolder2
jk Data
PerfLogs
-• , Program Files
Program Files (x86)
ProgramDataTechSmith
> Ji; Users
> * Windows
> JJ CD Drive (R)
>uMy Passport (bt)
Name
Data
Date modified |
2/1/2010 4:06 p[|
1
Рис. 20.2. Результат создания подкаталогов
Хотя получать возвращаемое значение метода CreateSubdirectoryO не
обязательно, имейте в виду, что в случае его успешного выполнения возвращается объект
Directorylnfo, представляющий вновь созданный элемент. Рассмотрим следующую
модификацию предыдущего метода:
Глава 20. Файловый ввод-вывод и сериализация объектов 717
static void ModifyAppDirectory()
{
Directorylnfo dir = new Directorylnfo(".") ;
// Создать \MyFolder в начальном каталоге.
dir.CreateSubdirectory("MyFolder");
// Получить возвращенный объект Directorylnfo.
Directorylnfo myDataFolder = dir.CreateSubdirectory(@MMyFolder2\DataM);
// Напечатать путь ..\MyFolder2\Data.
Console.WriteLine("New Folder is: {0}", myDataFolder);
}
Работа с типом Directory
После опробования типа Directorylnfo в действии можно приступать к изучению
типа Directory. По большей части статические члены Directory повторяют
функциональность, предоставленную членами уровня экземпляра, которые определены в
Directorylnfo. Вспомните, однако, что члены Directory обычно возвращают
строковые данные вместо строго типизированных объектов Filelnfo/Directorylnfo.
Теперь рассмотрим некоторую функциональность типа Directory, приведенная
ниже последняя вспомогательная функция отображает имена всех устройств,
представленных на текущем компьютере (через метод Directory.GetLogicalDrivesO), и
использует статический метод Directory.Delete () для удаления созданных ранее
подкаталогов \MyFolder и \MyFolder2\Data:
static void FunWithDirectoryType ()
{
// Перечислить все дисковые устройства данного компьютера.
string [] drives = Directory.GetLogicalDrivesO;
Console.WriteLine ("Here are your drives:");
foreach (string s in drives)
Console.WriteLine (" — > {0} ", s) ;
// Удалить то, что было ранее создано.
Console.WriteLine ("Press Enter to delete directories");
Console.ReadLine();
try
{
Directory.Delete(@"C:\MyFolder");
// Второй параметр указывает, нужно ли удалять все подкаталоги.
Directory.Delete(@"C:\MyFolder2", true);
}
catch (IOException e)
{
Console.WriteLine(e.Message);
}
}
Исходный код. Проект DirectoryApp доступен в подкаталоге Chapter 20.
Работа с типом Drivelnf о
Пространство имен System. 10 включает класс Drivelnf о. Подобно Directory.
GetLogicalDrivesO, статический метод Drivelnfo.GetDrivesO позволяет получить
имена дисковых приводов машины.
718 Часть V. Введение в библиотеки базовых классов .NET
Однако, в отличие от Directory.GetLogicalDrives (), Drivelnfo предоставляет
множество дополнительных деталей (таких как тип привода, доступное свободное
пространство и метка тома). Рассмотрим следующий класс Program, определенный в новом
консольном приложении по имени DrivelnfoApp (не забудьте импортировать
пространство имен System. 10):
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Drivelnfo *****\nM);
// Получить информацию обо всех приводах.
Drivelnfo[] myDrives = Drivelnfo.GetDrives();
// Напечатать состояние приводов.
foreach(Drivelnfo d in myDrives)
{
Console.WriteLine("Name: {0}", d.Name); // Имя
Console.WriteLine("Type: {0}", d.DriveType); //Тип
// Проверить, смонтирован ли диск.
if (d.IsReady)
{
Console.WriteLine("Free space: {0}", d.TotalFreeSpace); // Свободное
// пространство
Console.WriteLine ("Format: {0}", d.DriveFormat); // Формат
Console.WriteLine("Label: {0}", d.VolumeLabel); //Метка
Console.WriteLine();
Console.ReadLine();
}
}
Ниже показан возможный вывод:
***** pun with Drivelnfo *****
Name: C:\
Type: Fixed
Free space: 587376394240
Format: NTFS
Label: Mongo Drive
Name: D:\
Type: CDRom
Name: E:\
Type: CDRom
Name: F:\
Type: CDRom
Name: H:\
Type: Fixed
Free space: 477467508736
Format: FAT32
Label: My Passport
Пока что были исследованы только некоторые основы поведения классов Directory,
Directorylnfo и Drivelnfo. Далее будет показано, как создавать, открывать,
закрывать и удалять файлы, наполняющие данный каталог.
Исходный код. Проект DrivelnfoApp доступен в подкаталоге Chapter 20.
Глава 20. Файловый ввод-вывод и сериализация объектов 719
Работа с классом Filelnf о
Как было показано в предыдущем примере DirectoryApp, класс Filelnf о
позволяет получать подробности относительно существующих файлов на жестком диске (т.е.
время создания, размер и атрибуты) и предназначен для создания, копирования,
перемещения и удаления файлов. Вдобавок к набору функциональности, унаследованной
от FileSystemlnfo, есть некоторые члены, уникальные для класса Filelnfo, которые
описаны в табл. 20.4.
Таблица 20.4. Основные члены Filelnfo
Член Назначение
AppendText () Создает объект StreamWriter (описанный ниже) и добавляет текст в файл
СоруТо () Копирует существующий файл в новый файл
Create () Создает новый файл и возвращает объект FileStream (описанный ниже)
для взаимодействия с вновь созданным файлом
CreateTextO Создает объект StreamWriter, записывающий новый текстовый файл
Delete () Удаляет файл, к которому привязан экземпляр Filelnfo
Directory Получает экземпляр родительского каталога
DirectoryName Получает полный путь к родительскому каталогу
Length Получает размер текущего файла или каталога
MoveTo () Перемещает указанный файл в новое местоположение, предоставляя
возможность указать новое имя файла
Name Получает имя файла
Open () Открывает файл с различными привилегиями чтения/записи и совместного
доступа
OpenReadO Создает доступный только для чтения объект FileStream
OpenTextO Создает объект StreamReader (описанный ниже) и читает из
существующего текстового файла
OpenWrite () Создает доступный только для записи объект FileStream
Обратите внимание, что большинство методов класса Filelnfo возвращают
специфический объект ввода-вывода (т.е. FileStream и StreamWriter), который позволяет
начать чтение и запись данных в ассоциированный файл в разнообразных форматах.
Скоро мы рассмотрим эти типы; однако прежде чем увидеть работающий пример,
давайте изучим различные способы получения дескриптора файла с использованием
класса File In ft).
Метод Filelnf о. Create ()
Один из способов создания дескриптора файла предусматривает использование
метода Filelnfo. Create():
static void Main(string [ ] args)
{
// Создать новый файл на диске С: .
Filelnfo f = new Filelnfo(@"C:\Test.dat");
FileStream fs = f.Create();
720 Часть V. Введение в библиотеки базовых классов .NET
// Использовать объект FileStream...
// Закрыть файловый поток.
fs.CloseO ;
}
Метод Filelnfo.Create() возвращает тип FileStream, который предоставляет
синхронную и асинхронную операции записи/чтения лежащего в его основе файла. Имейте
в виду, что объект FileStream, возвращенный Filelnfo.CreateO, открывает полный
доступ по чтению и записи всем пользователям.
Также имейте в виду, что после окончания работы с текущим объектом FileStream
следует закрыть его дескриптор, чтобы освободить лежащие в основе потока
неуправляемые ресурсы. Учитывая, что FileStream реализует интерфейс IDisposable, можно
применить контекст С# using и позволить компилятору сгенерировать логику
завершения (подробности ищите в главе 8).
static void Main(string [ ] args)
{
// Определение контекста using для файлового ввода-вывода.
Filelnfo f = new Filelnfo(@"C:\Test.dat") ;
using (FileStream fs = f.CreateO)
{
// Использовать объект FileStream. ..
}
}
Метод Filelnfo.Open()
С помощью метода Filelnfo.OpenO можно открывать существующие файлы, а
также создавать новые файлы с гораздо более высокой точностью, чем Filelnfo.CreateO,
учитывая, что Ореп() обычно принимает несколько параметров для описания общей
структуры файла, с которым будет производиться работа. В результате вызова Ореп()
получается возвращенный им объект FileStream. Взгляните на следующую логику:
static void Main(string [ ] args)
{
// Создать новый файл через Filelnfo.OpenO •
Filelnfo f2 = new Filelnfo(@"C:\Test2.dat") ;
using(FileStream fs2 = f2.Open(FileMode.OpenOrCreate,
FileAccess .ReadWnte, FileShare .None) )
{
// Использовать объект FileStream...
}
}
Эта версия перегруженного метода Ореп() требует трех параметров. Первый
параметр указывает общий тип запроса ввода-вывода (т.е. создать новый файл, открыть
существующий файл и дописать в файл), указываемый в виде перечисления FileMode
(описание членов дано в табл. 20.5):
public enum FileMode
{
CreateNew,
Create,
Open,
OpenOrCreate,
Truncate,
Append
}
Глава 20. Файловый ввод-вывод и сериализация объектов 721
Таблица 20.5. Члены перечисления FileMode
Член Назначение
CreateNew Информирует операционную систему о создании нового файла. Если файл
уже существует, генерируется исключение IOException
Create Информирует операционную систему о создании нового файла. Если файл
уже существует, он будет перезаписан
Open Открывает существующий файл. Если файл не существует, генерируется
исключение FileNotFoundException
OpenOrCreate Открывает файл, если он существует; в противном случае создает новый
Truncate Открывает файл и усекает его до нулевой длины
Append Открывает файл, переходит в его конец и начинает операции чтения (этот
флаг может быть использован только с потоками, доступными лишь для
чтения). Если файл не существует, то создается новый
Второй параметр метода Ореп() — значение перечисления FileAccess —
используется для определения поведения чтения/записи лежащего в основе потока:
public enum FileAccess
{
Read,
Write,
ReadWrite
}
И, наконец, третий параметр метода Open() — FileShare — указывает, как файл
может быть разделен с другими файловыми дескрипторами. Ниже перечислены его
возможные значения:
public enum FileShare
{
Delete,
Inheritable,
None,
Read,
ReadWrite,
Write
}
Методы FileOpen.OpenReadQ и Filelnfo.OpenWriteQ
Хотя метод FileOpen.Open() позволяет получить дескриптор файла довольно
гибким способом, в классе Filelnfo также предусмотрены для этого члены OpenReadO и
OpenWrite(). Как и можно было ожидать, эти методы возвращают объект FileStream,
соответствующим образом сконфигурированный только для чтения или только для
записи, без необходимости применять различные значения перечислений. Подобно
Filelnfo.CreateO и Filelnfo.Open(), методы OpenReadO HOpenWrite() возвращают
объект FileStream (обратите внимание, что в следующем коде предполагается наличие
файлов по имени Test3.dat и Test4.dat на диске С:).
static void Main(string[] args)
{
// Получить объект FileStream с правами только для чтения.
Filelnfo f3 = new Filelnfo (@"C:\Test3.dat");
722 Часть V. Введение в библиотеки базовых классов .NET
using(FileStream readOnlyStream = f3.OpenRead())
{
// Использовать объект FileStream...
}
// Теперь получить объект FileStream с правами только для записи.
Filelnfo f4 = new Filelnfo(@"C:\Test4.dat");
using(FileStream writeOnlyStream = f4.OpenWrite ())
{
// Использовать объект FileStream...
}
}
Метод Filelnfo.OpenText()
Еще один член типа Filelnfo, связанный с открытием файлов — OpenText(). В
отличие от Create (), Open (), OpenRead () и OpenWrite (), метод OpenText () возвращает
экземпляр типа StreamReader, а не FileStream. Исходя из того, что на диске С: уже есть файл
по имени boot.ini, получить доступ к его содержимому можно следующим образом:
static void Main(string[] args)
{
// Получить объект StreamReader.
Filelnfo f5 = new Filelnfo(@"C:\boot.ini");
using(StreamReader sreader = f5.OpenText ())
{
// Использовать объект StreamReader...
}
}
Как вскоре будет показано, тип StreamReader предоставляет способ чтения
символьных данных из лежащего в основе файла.
Методы Filelnfo.CreateTextQ и Filelnfo.AppendTextQ
Последними двумя методами, представляющими интерес, являются CreateTextO и
AppendText(). Оба возвращают объект StreamWriter, как показано ниже:
static void Main(string [ ] args)
{
Filelnfo f6 = new Filelnfo(@"C:\Test6.txt");
using(StreamWriter swriter = f6.CreateText())
{
// Использовать объект StreamWriter...
}
Filelnfo f7 = new Filelnfo(@"C:\FinalTest.txt");
using(StreamWriter swriterAppend = f7.AppendText())
{
// Использовать объект StreamWriter...
}
}
Как и можно было ожидать, тип StreamWriter предлагает способ записи данных в
лежащий в основе файл.
Работа с типом File
Тип File предоставляет функциональность, почти идентичную типу Filelnfo, с
помощью нескольких статических методов. Подобно Filelnfo, тип File поддерживает методы
AppendText(), CreateO, CreateTextO, Open(), OpenReadO, OpenWriteO HOpenText().
Глава 20. Файловый ввод-вывод и сериализация объектов 723
Фактически во многих случаях типы File и Filelnf о могут использоваться
взаимозаменяемым образом. Для иллюстрации каждый из предшествующих примеров
применения FileStream можно упростить, используя вместо него тип File.
static void Main(string[] arqb)
{
II Получить объект FileStream через File.Create () .
using(FileStream fs = File.Create(@"C:\Test.dat"))
{}
// Получить объект FileStream через File.Open().
using(FileStream fs2 = File.Open(@MC:\Test2.dat",
FileMode.OpenOrCreate,
FileAccess .ReadWrite, FileShare .None) )
{}
// Получить объект FileStream с правами только для чтения.
using(FileStream readOnlyStream = File.OpenRead(@"Test3.dat"))
U
// Получить объект FileStream с правами только для записи.
using(FileStream writeOnlyStream = File.OpenWrite(@"Test4.dat"))
{}
// Получить объект StreamReader.
using (StreamPeacler sreader = File .OpenText (@"C : \boot. mi" ) )
{}
// Получить несколько объектов StreamWriter.
using (StreamWriter swriter = File.CreateText(@"C:\Test6.txt"))
{}
using (StreamWriter swriterAppend = File.AppendText(@"C:\FinalTest.txt"))
{}
}
Дополнительные члены File
Тип File также поддерживает несколько уникальных членов, перечисленных в
табл. 20.6, которые могут значительно упростить процесс чтения и записи текстовых
данных.
Таблица 20.6. Методы типа File
Метод Назначение
ReadAllBytes () Открывает указанный файл, возвращает двоичные данные в виде
массива байт и затем закрывает файл
ReadAllLines () Открывает указанный файл, возвращает символьные данные в виде
массива строк, затем закрывает файл
ReadAllText () Открывает указанный файл, возвращает символьные данные в виде
System.StringO, затем закрывает файл
WriteAllBytesO Открывает указанный файл, записывает в него байтовый массив и
закрывает файл
WriteAllLines () Открывает указанный файл, записывает в него массив строк и
закрывает файл
WriteAllTextO Открывает указанный файл, записывает в него данные из указанной
строки и закрывает файл
724 Часть V. Введение в библиотеки базовых классов .NET
Используя эти новые методы типа File, можно осуществлять чтение и запись
пакетов данных с помощью всего нескольких строк кода. Еще лучше то, что эти новые
методы автоматически закрывают лежащий в основе файловый дескриптор. Например,
следующая консольная программа (по имени SimpleFilelO) сохраняет строковые
данные в новом файле на диске С: (и читает их в память) с минимальными усилиями
(предполагается, что был произведен импорт System. 10):
class Program
{
static void Main(string[] args)
{
Console.WriteLine ("***** Simple 10 with the File Type *****\nM);
stnng[] myTasks = {
"Fix bathroom sink", "Call Dave",
"Call Mom and Dad", "Play Xbox 360"};
// Записать все данные в файл на диске С.
File.WriteAllLines(@"C:\tasks.txt", myTasks);
// Прочитать все обратно и распечатать.
foreach (string task in File.ReadAllLines(@"C:\tasks.txt") )
{
Console.WriteLine("TODO: {0}", task);
}
Console.ReadLine();
}
}
Отсюда вывод: когда необходимо быстро получить файловый дескриптор, тип File
сэкономит некоторый объем ввода. Однако преимущество предварительного создания
объекта Filelnf о связано с возможностью исследования файла с использованием
членов абстрактного базового класса FileSystemlnfo.
Исходный код. Проект SimpleFilelO доступен в подкаталоге Chapter 20.
Абстрактный класс Stream
К данному моменту вы уже ознакомились с несколькими способами получения
объектов FileStream, StreamReader и StreamWriter, но еще нужно читать данные и
записывать их в файл, использующий эти типы. Чтобы понять, как это делается,
необходимо изучить концепцию потока. В мире манипуляций вводом-выводом поток (stream)
представляет порцию данных, протекающую от источника к цели. Потоки
предоставляют общий способ взаимодействия с последовательностью байгги независимо от того,
какого рода устройство (файл, сеть, соединение, принтер и т.п.) хранит или отображает
эти байты.
В абстрактном классе System. 10.Stream определен набор членов, которые
обеспечивают поддержку синхронного и асинхронного взаимодействия с хранилищем
(например, файлом или областью памяти).
На заметку! Концепция потока не ограничена файловым вводом-выводом. Точности ради следует
отметить, что библиотеки .NET предоставляют потоковый доступ к сетям, областям памяти и
прочим абстракциям, связанным с потоками.
Опять-таки, потомки класса Stream представляют данные, как низкоуровневые
потоки байт, а непосредственная работа с низкоуровневыми потоками может оказаться
довольно загадочной. Некоторые типы, унаследованные от Stream, поддерживают по-
Глава 20. Файловый ввод-вывод и сериализация объектов 725
иск (seeking), что означает возможность получения и изменения текущей позиции в
потоке. Чтобы приблизиться к пониманию функциональности класса Stream, рассмотрим
список основных его членов, приведенный в табл. 20.7.
Таблица 20.7. Члены абстрактного класса Stream
Член Назначение
CanRead Определяют, поддерживает ли текущий поток чтение, поиск и/или запись
CanWrite
CanSeek
Close () Закрывает текущий поток и освобождает все ресурсы (такие как сокеты и
файловые дескрипторы), ассоциированные с текущим потоком. Внутренне
этот метод является псевдонимом Dispose (). Поэтому закрытие потока
функционально эквивалентно освобождению потока
Flush () Обновляет лежащий в основе источник данных или репозиторий текущим
состоянием буфера с последующей очисткой буфера. Если поток не реализует
буфер, метод не делает ничего
Length Возвращает длину потока в байтах
Position Определяет текущую позицию в потоке
Read () Читает последовательность байт (или одиночный байт) из текущего потока и
ReadBy te () перемещает текущую позицию потока на количество прочитанных байтов
Seek() Устанавливает позицию в текущем потоке
SetLength () Устанавливает длину текущего потока
Write () Пишет последовательность байт (или одиночный байт) в текущий поток и пе-
WriteBy te () ремещает текущую позицию на количество записанных байтов
Работа с классом FileStream
Класс FileStream предоставляет реализацию абстрактного члена Stream в манере,
подходящей для потоковой работы с файлами. Это элементарный поток, и он может
записывать или читать только один байт или массив байтов. Однако
взаимодействовать с членами типа FileStream придется нечасто. Вместо этого, скорее всего, будут
использоваться оболочки потоков, которые облегчают работу с текстовыми данными
или типами .NET. Тем не менее, в целях иллюстрации полезно поэкспериментировать с
возможностями синхронного чтения/записи типа FileStream.
Предположим, что имеется консольное приложение под названием FileStreamApp,
и в файле кода С# выполнен импорт пространств имен System. 10 и System.Text. Цель
состоит в записи простого текстового сообщения в новый файл по имени myMessage .dat.
Однако, учитывая, что FileStream может работать только с низкоуровневыми
байтами, придется закодировать тип System.String в соответствующий байтовый массив.
К счастью, в пространстве имен System.Text определен тип по имени Encoding,
который содержит члены, способные кодировать и декодировать строки в массивы байт
(тип Encoding подробно описан в документации .NET Framework 4.0 SDK).
Закодированный массив байт сохраняется в файле с помощью метода FileStream.
Write(). Чтобы прочитать байты обратно в память, необходимо сбросить внутреннюю
позицию потока (через свойство Position) и вызвать метод ReadByte(). И, наконец, на
консоль выводится низкоуровневый байтовый массив и декодированная строка. Ниже
приведен полный текст метода Main().
726 Часть V. Введение в библиотеки базовых классов .NET
//Не забудьте импортировать пространства имен System.Text и System.10!
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with FileStreams *****\n");
// Получить объект FileStream.
using(FileStream fStream = File.Open(@"C:\myMessage.dat", FileMode.Create))
{
// Закодировать строку в виде массива байт.
string msg = "Hello!";
byte[] msgAsByteArray = Encoding.Default.GetBytes(msg);
// Записать byte[] в файл.
fStream.Write(msgAsByteArray, 0, msgAsByteArray.Length);
// Сбросить внутреннюю позицию потока.
fStream.Position = 0;
// Прочитать типы из файла и вывести на консоль.
Console.Write("Your message as an array of bytes: ");
byte[] bytesFromFile = new byte[msgAsByteArray.Length];
for (int i = 0; i < msgAsByteArray.Length; i++)
{
bytesFromFile[l] = (byte)fStream.ReadByte();
Console.Write(bytesFromFile[i]);
}
// Вывести декодированные сообщения.
Console.Write("\nDecoded Message: " ) ;
Console.WriteLine(Encoding.Default.GetString(bytesFromFile));
}
Console.ReadLine();
}
В этом примере производится не только наполнение файла данными, но также
демонстрируется основной недостаток прямой работы с типом FileStream: приходится
оперировать низкоуровневыми байтами. Другие унаследованные от Stream типы
работают аналогично. Например, чтобы записать последовательность байт в область
памяти, необходимо создать объект MemoryStream. Аналогично, для передачи массива байт
по сетевому соединению используется класс Net works t ream (из пространства имен
System. Net. Sockets).
Как уже упоминалось, в пространстве имен System. 10 доступно множество типов
для чтения и записи, которые инкапсулируют детали работы с типами-наследниками
Stream.
Исходный код. Проект FileStreamApp доступен в подкаталоге Chapter 20.
Работа с классами StreamWriter
и StreamReader
Классы StreamWriter и StreamReader удобны во всех случаях, когда нужно читать
или записывать символьные данные (например, строки). Оба типа работают по
умолчанию с символами Unicode; однако это можно изменить предоставлением правильно
сконфигурированной ссылки на объект System.Text .Encoding. Чтобы не усложнять
пример, предположим, что кодировка по умолчанию Unicode вполне устраивает.
Класс StreamReader, как и StringReader (о котором речь пойдет далее в этой
главе), унаследован от абстрактного класса по имени Text Reader. Базовый класс предла-
Глава 20. Файловый ввод-вывод и сериализация объектов 727
гает очень ограниченный набор функциональности каждому из его наследников, в
частности — возможность читать и "заглядывать" (peek) в символьный поток.
Класс StreamWriter (как и StringWriter, о котором речь пойдет ниже)
наследуется от абстрактного базового класса по имени Тех tW rite г. В этом классе определены
члены, позволяющие производным типам записывать текстовые данные в заданный
символьный поток.
Чтобы приблизить вас к пониманию основных возможностей записи классов
StreamWriter и StringWriter, в табл. 20.8 представлены описания основных членов
абстрактного базового класса ТехtWriter.
Таблица 20.8. Основные члены TextWriter
Член Назначение
Close () Этот метод закрывает объект-писатель и освобождает все связанные с ним
ресурсы. В процессе автоматически сбрасывается буфер (опять-таки, этот
член функционально эквивалентен методу Dispose())
Flush () Этот метод очищает все буферы текущего объекта-писателя и записывает
все буферизованные данные на лежащее в основе устройство, однако, не
закрывает его
NewLine Это свойство задает константу перевода строки для унаследованного класса
писателя. По умолчанию ограничителем строки в Windows является возврат
каретки, за которым следует перевод строки (\г\п)
Write () Этот перегруженный метод записывает данные в текстовый поток без
добавления константы новой строки
WriteLine () Этот перегруженный метод записывает данные в текстовый поток с
добавлением константы новой строки
На заметку! Последние два члена класса TextWriter, скорее всего, покажутся знакомыми. Если
помните, тип System.Console имеет члены Write() и WriteLine(), которые выталкивают
текстовые данные на стандартное устройство вывода. Фактически свойство Console.In
хранит TextWriter, a Console.Out — TextWriter.
Унаследованный класс StreamWriter предоставляет соответствующую
реализацию методов Write(), Close () и Flush(), а также определяет дополнительное свойство
AutoFlush. Когда это свойство установлено в true, оно заставляет StreamWriter
выталкивать данные при каждой операции записи. Имейте в виду, что можно обеспечить
более высокую производительность, установив AutoFlush в false, но при этом всегда
вызывать Close () по завершении работы с StreamWriter.
Запись в текстовый файл
Чтобы увидеть класс StreamWriter в действии, создадим новое консольное
приложение по имени StreamWriterReaderApp и импортируем пространство имен System. 10.
В показанном ниже методе Main() создается новый файл по имени reminders.txt с
помощью метода File.CreateText(). Используя полученный объект StreamWriter, в
новый файл будут добавлены некоторые текстовые данные.
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with StreamWriter / StreamReader *****\n");
728 Часть V. Введение в библиотеки базовых классов .NET
// Получить StreamWriter и записать строковые данные.
using (StreamWnter writer = File .CreateText ( "reminders . txt") )
{
writer.WriteLine("Don't forget Mother's Day this year...");
writer.WriteLine("Don't forget Father's Day this year...");
writer.WriteLine("Don't forget these numbers:");
for(int i = 0; l < 10; i++)
writer.Write (l + " ");
// Вставить новую строку.
writer.Write (writer.NewLine);
}
Console.WriteLine("Created file and wrote some thoughts...");
Console.ReadLine();
}
После выполнения этой программы можно просмотреть содержимое созданного
файла (рис. 20.3). Этот файл находится в папке bin\Debug текущего приложения, поскольку
при вызове CreateText () абсолютный путь не указывался.
| reminders.txt
Don't forget Mother's Day this year...
Don't forget Father's Day this year...
Don't forget these nimbers:
0123456789
100%
Рис. 20.3. Содержимое созданного текстового файла
Чтение из текстового файла
Теперь давайте посмотрим, как программно прочитать данные из файла, используя
соответствующий тип StreamReader. Как вы помните, этот класс унаследован от
абстрактного класса TextReader, который обеспечивает функциональность, описанную
в табл. 20.9.
Таблица 20.9. Основные члены TextReader
Член
Назначение
Peek() Возвращает следующий доступный символ, не изменяя текущей позиции
читателя. Значение -1 указывает на достижение конца потока
Read () Читает данные из входного потока
ReadBlockO Читает указанное максимальное количество символов из текущего потока и
записывает данные в буфер, начиная с заданного индекса
ReadLine () Читает строку символов из текущего потока и возвращает данные в виде
строки (null-строка указывает на признак конца файла (EOF))
ReadToEnd () Читает все символы от текущей позиции до конца потока и возвращает их в
виде одной строки
Если теперь расширить пример приложения StreamWriterReaderApp, чтобы в нем
применялся класс StreamReader, то можно будет прочитать текстовые данные из
файла reminders.txt:
Глава 20. Файловый ввод-вывод и сериализация объектов 729
static void Main(string [ ] args)
{
Console.WriteLine ("***** Fun with StreamWriter / StreamReader *****\n");
// Прочитать данные из файла.
Console.WriteLine ("Here are your thoughts:\n");
using(StreamReader sr = File.OpenText("reminders.txt"))
{
string input = null;
while ((input = sr.ReadLine ()) != null)
{
Console.WriteLine (input);
}
}
Console.ReadLine();
}
После запуска этой программы на консоли отображаются символьные данные из
файла reminders.txt.
Прямое создание экземпляров классов
StreamWriter/StreamReader
Одним из сбивающих с толку аспектов работы с типами, которые входят в
пространство имен System. 10, является то, что одного и того же результата часто
можно достичь с использованием разных подходов. Например, ранее уже было показано,
что с помощью метода CreateText() можно получить объект StreamWriter с типом
File или Filelnfo. В действительности есть и еще один способ работы с объектами
StreamWriter и StreamReader: создавая их напрямую. Например, текущее приложение
можно было бы переделать следующим образом:
static void Main(string [ ] args)
{
Console.WriteLine (••***** Fun with StreamWriter / StreamReader *****\n");
// Получить StreamWriter и записать строковые данные.
using(StreamWriter writer = new StreamWriter("reminders.txt"))
// Прочитать данные из файла.
using (StreamReader sr = new StreamReader("reminders.txt"))
{
}
}
Несмотря на то что существованием множества на первый взгляд идентичных
подходов к файловому вводу-выводу может обескуражить, следует понимать, что в
результате достигается более высокая гибкость. Теперь, когда известно, как перемещать
символьные данные в файл и из файла с использованием классов StreamWriter и
StreamReader, рассмотрим роль классов StringWriter и StringReader.
Исходный код. Проект StreamWriterReaderApp доступен в подкаталоге Chapter 20.
730 Часть V. Введение в библиотеки базовых классов .NET
Работа с классами StringWriter
и StringReader
Классы StringWriter и StringReader можно использоваться для трактовки текстовой
информации как потока символов, находящихся в памяти. Это может помочь, когда
требуется добавить символьную информацию к лежащему в основе буферу. Для иллюстрации в
следующем консольном приложении (по имени StringReaderWriterApp) блок строковых
данных записывается в объект StringWriter вместо файла на локальном жестком диске:
static void Main(string [ ] args)
{
Console.WriteLine (••***** Fun with StringWriter / StringReader *****\n");
// Создать StringWriter и записать символьные данные в память.
using(StringWriter strWriter = new StringWriter())
{
strWriter.WriteLine("Don't forget Mother's Day this year...");
// Получить копию содержимого (хранящегося в строке) и вывести на консоль.
Console.WriteLine("Contents of StringWriter:\n{0}", strWriter);
}
Console.ReadLine();
}
Поскольку и StringWriter, и StreamWriter унаследованы от одного и того же
базового класса (TextWriter), логика записи в какой-то мере похожа. Однако, учитывая
природу StringWriter, имейте в виду, что этот класс позволяет использовать метод
GetStringBuilder() для извлечения объекта System.Text.StringBuilder:
using (StringWriter strWriter = new StringWriter ())
{
strWriter.WriteLine("Don't forget Mother's Day this year...");
Console.WriteLine("Contents of StringWriter:\n{0}", strWriter);
// Получить внутренний StringBuilder.
StringBuilder sb = strWriter.GetStringBuilder ();
sb.Insert @, "Hey1 ! ") ;
Console.WriteLine ("-> {0}", sb.ToString());
sb.Remove@, "Hey!! ".Length);
Console.WriteLine ("-> {0}", sb.ToString ()) ;
}
Когда необходимо выполнить чтение из потока строковых данных, используйте
соответствующий тип StringReader, который (как и можно было ожидать)
функционирует идентично классу StreamReader. Фактически класс StringReader не делает ничего
помимо переопределения унаследованных членов для чтения блока символьных данных
вместо чтения файла:
using (StringWriter strWriter = new StringWriter())
{
strWriter.WriteLine("Don't forget Mother's Day this year...");
Console.WriteLine("Contents of StringWriter:\n{0}", strWriter);
// Читать данные из StringWriter.
using (StringReader strReader = new StringReader(strWriter.ToString()) )
{
string input = null;
while ((input = strReader.ReadLine()) != null)
{
Console.WriteLine(input);
}
}
}
Глава 20. Файловый ввод-вывод и сериализация объектов 731
Исходный код. Проект StringReaderWriterApp доступен в подкаталоге Chapter 20.
Работа с классами BinaryWriter
и BinaryReader
И последний набор классов читателей и писателей, которые рассматриваются в
настоящем разделе — это BinaryWriter и BinaryReader; оба они являются прямыми
наследниками System.Object. Эти типы позволяют читать и записывать дискретные
типы данных в потоки в компактном двоичном формате. В классе BinaryWriter
определен многократно перегруженный метод Write () для помещения типов данных в
лежащий в основе поток. В дополнение к Write (), класс BinaryWriter предоставляет
дополнительные члены, позволяющие получать или устанавливать объекты унаследованных
от Stream типов, а также поддерживает произвольный доступ к данным (табл. 20.10).
Таблица 20.10. Основные члены BinaryWriter
Член Назначение
BaseStream Это свойство, предназначенное только для чтения, обеспечивает доступ
к лежащему в основе потоку, используемому объектом BinaryWriter
Close () Этот метод закрывает двоичный поток
Flush () Этот метод выталкивает буфер двоичного потока
Seek() Этот метод устанавливает позицию в текущем потоке
Write () Этот метод пишет значение в текущий поток
Основные члены класса BinaryReader перечислены в табл. 20.11.
Таблица 20.11. Основные члены BinaryReader
Член Назначение
BaseStream Это свойство, предназначенное только для чтения, обеспечивает доступ
к лежащему в основе потоку, используемому объектом BinaryReader
Close () Этот метод закрывает двоичный поток
PeekChar () Этот метод возвращает следующий доступный символ без перемещения
текущей позиции потока
Read () Этот метод читает заданный набор байт или символов и сохраняет их в
переданном ему массиве
ReadXXXXO В классе BinaryReader определено множество методов чтения,
которые извлекают из потока объекты различных типов (ReadBooleanO,
ReadByteO, Readlnt32() и т.д.)
В следующем примере (консольное приложение по имени Binary Write r Reader)
объекты данных разных типов записываются в файл *.dat:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Binary Writers / Readers *****\n");
// Открыть двоичную запись в файл.
Filelnfo f = new Filelnfo("BinFile.dat") ;
732 Часть V. Введение в библиотеки базовых классов .NET
using(BinaryWriter bw = new BinaryWriter(f.OpenWrite()))
{
// Вывести на консоль тип BaseStream (System.10.FileStream в данном случае).
Console.WriteLine("Base stream is: {0}", bw.BaseStream);
// Создать некоторые данные для сохранения в файле.
double aDouble = 1234.67;
int anlnt = 34567;
string aString = "А, В, C";
// Записать данные.
bw.Write(aDouble);
bw.Write(anlnt);
bw.Write(aString);
}
Console . ReadLme () ;
}
Обратите внимание, что объект FileStream, возвращенный методом Filelnfo.
OpenWrite(), передается конструктору типа BinaryWriter. Используя эту технику,
очень просто организовать по уровням поток перед записью данных. Имейте в виду,
что конструктор BinaryWriter принимает любой тип, унаследованный от Stream (т.е.
FileStream, MemoryStream или BufferedStream). Таким образом, если необходимо
записать двоичные данные в память, просто используйте объект MemoryStream.
Для чтения данных из файла BinFile.dat в классе BinaryReader предлагается ряд
опций. Например, ниже будут вызываться различные члены, выполняющие чтение, для
извлечения каждого фрагмента данных из файлового потока:
static void Main(string [ ] args)
{
Filelnfo f = new Filelnfo("BinFile.dat");
// Читать двоичные данные из потока.
using(BinaryReader br = new BinaryReader(f.OpenRead()) )
{
Console.WriteLine(br.ReadDouble());
Console.WriteLine(br.Readlnt32());
Console.WriteLine(br.ReadString());
}
Console.ReadLine() ;
}
Исходный код. Проект BinaryWriterReader доступен в подкаталоге Chapter 20.
Программное отслеживание файлов
Теперь, когда вы научились использовать различные классы для чтения и записи,
следующая задача — изучить роль класса FileSystemWatcher. Этот тип довольно
полезен, когда требуется программно отслеживать состояние файлов в системе. В частности,
с помощью FileSystemWatcher можно организовать слежение за файлами на предмет
выполнения с ними действий, указанных в перечислении System. 10.NotifyFilters
(подробно описано в документации .NET Framework 4.0 SDK):
public enum NotifyFilters
{
Attributes, CreationTime,
DirectoryName, FileName,
LastAccess, LastWnte,
Security, Size,
}
Глава 20. Файловый ввод-вывод и сериализация объектов 733
Первый шаг, который необходимо предпринять при работе с типом FileSystemWatcher —
это установить свойство Path, чтобы оно указывало имя (и местоположение) каталога,
содержащего файлы, которые нужно отслеживать, а также свойство Filter,
определяющее расширения упомянутых файлов.
Сейчас можно выбрать обработку событий Changed, Created и Deleted — все они
работают в сочетании с делегатом FileSystemE vent Handler. Этот делегат может
вызывать любой метод, соответствующий следующей сигнатуре:
// Делегат FileSystemEventHandler должен указывать
//на метод, соответствующий следующей сигнатуре.
void MyNotificationHandler(object source, FileSystemEventArgs e)
Событие Renamed также может быть обработано делегатом типа RenamedEventHandler,
который может вызывать методы, отвечающие следующей сигнатуре:
// Делегат RenamedEventHandler должен указывать
//на метод, соответствующий следующей сигнатуре.
void MyNotificationHandler(object source, RenamedEventArgs e)
Чтобы проиллюстрировать процесс наблюдения за файлом, предположим, что на
диске С: создан новый каталог по имени MyFolder, содержащий различные файлы
*.txt (с произвольными именами). Следующее консольное приложение (под
названием MyDirectoryWatcher) будет выполнять мониторинг файлов *.txt внутри каталога
MyFolder и выводить сообщения при создании, удалении, модификации или
переименовании файлов:
static void Main(string[] args)
{
Console.WriteLine ("***** The Amazing File Watcher App *****\n");
// Установить путь к каталогу, за которым нужно наблюдать.
FileSystemWatcher watcher = new FileSystemWatcher();
try
{
watcher.Path = @"C:\MyFolder";
}
catch(ArgumentException ex)
{
Console.WriteLine(ex.Message);
return;
}
// Указать предметы наблюдения.
watcher.NotifyFilter = NotifyFilters.LastAccess
I NotifyFilters.LastWrite
I NotifyFilters.FileName
I NotifyFilters.DirectoryName;
// Следить только за текстовыми файлами.
watcher.Filter = "*.txt";
// Добавить обработчики событий.
watcher.Changed += new FileSystemEventHandler(OnChanged);
watcher.Created += new FileSystemEventHandler(OnChanged);
watcher.Deleted += new FileSystemEventHandler(OnChanged);
watcher.Renamed += new RenamedEventHandler(OnRenamed);
// Начать наблюдение за каталогом.
watcher.EnableRaisingEvents = true;
// Ожидать команды пользователя на завершение программы.
Console. WriteLine (@"Press 'q' to quit app.11);
while(Console.Read()!='q');
}
734 Часть V. Введение в библиотеки базовых классов .NET
Два обработчика событий просто сообщают о модификациях файлов:
static void OnChanged(object source, FileSystemEventArgs e)
{
// Показать, что сделано, если файл изменен, создан или удален.
Console.WriteLine ("File: {0} {1},п, e.FullPath, е.ChangeType);
}
static void OnRenamed(object source, RenamedEventArgs e)
{
// Показать, что файл был переименован.
Console.WriteLine("File: {0} renamed to\n{1}", e.OldFullPath, e.FullPath);
}
Чтобы протестировать эту программу, запустите ее и откройте проводник Windows.
Попробуйте переименовать файлы, создать файл *.txt, удалить файл *.txt и т.д.
При этом вы увидите информацию относительно состояния текстовых файлов внутри
MyFolder, как показано ниже:
***** The Amazing File Watcher App *****
Press 'q' to quit app.
File: C:\MyFolder\New Text Document.txt Created!
File: C:\MyFolder\New Text Document.txt renamed to
С:\MyFolder\Hello.txt
File: C:\MyFolder\Hello.txt Changed!
File: C:\MyFolder\Hello.txt Changed!
File: C:\MyFolder\Hello.txt Deleted1
Исходный код. Проект MyDirectoryWatcher доступен в подкаталоге Chapter 20.
На этом знакомство с фундаментальными операциями ввода-вывода,
предлагаемыми платформой .NET, завершено. Эти приемы наверняка будут использоваться во
многих приложениях. Кроме того, значительно упростить задачу сохранения больших
объемов данных, могут службы сериализации объектов.
Понятие сериализации объектов
Термин сериализация описывает процесс сохранения (и, возможно, передачи)
состояния объекта в потоке (т.е. файловом потоке и потоке в памяти). Последовательность
сохраняемых данных содержит всю необходимую информацию, необходимую для
реконструкции (или десериализации) состояния объекта с целью последующего
использования. Применяя эту технологию, очень просто сохранять большие объемы данных
(в различных форматах) с минимальными усилиями. Во многих случаях сохранение
данных приложения с использованием служб сериализации выливается в код меньшего
объема, чем применение классов для чтения/записи из пространства имен System. 10.
Например, предположим, что создано настольное приложение с графическим
интерфейсом, в котором необходимо предоставить конечным пользователям возможность
сохранения их предпочтений (цвета окон, размер шрифта и т.п.). Для этого можно
определить класс по имени UserPref s и инкапсулировать в нем примерно два десятка
полей данных. В случае применения типа System. 10.BinaryWriter придется вручную
сохранять каждое поле объекта UserPrefs. Аналогично, когда вы понадобится загрузить
данные из файла обратно в память, придется использовать System.IO.BinaryReader
и, опять-таки, вручную читать каждое значение, чтобы реконструировать новый объект
UserPrefs.
Сэкономить значительное время можно, снабдив класс UserPrefs атрибутом
[Serializable]:
Глава 20. Файловый ввод-вывод и серйализация объектов 735
[Serializable]
public class UserPrefs
{
public string WindowColor;
public int FontSize;
}
После этого полное состояние объекта может быть сохранено с помощью всего
нескольких строк кода. Пока не погружаясь в детали, взгляните на следующий метод
Main():
static void Main(string[] args)
{
UserPrefs userData = new UserPrefs ();
userData.WindowColor = "Yellow";
userData.FontSize = 0";
// BinaryFormatter сохраняет данные в двоичном формате.
// Чтобы получить доступ к BinaryFormatter, понадобится
// импортировать System.Runtiтле.Serialization.Formatters.Binary.
BinaryFormatter binFormat = new BinaryFormatter();
// Сохранить объект в локальном файле.
using(Stream fStream = new FileStream("user.dat",
FileMode.Create, FileAccess.Write, FileShare.None))
{
binFormat.Serialize(fStream, userData);
}
Console.ReadLine();
}
Хотя сохранять объекты с помощью механизма сериализации объектов .NET
довольно просто, процесс, происходящий при этом "за кулисами", достаточно сложен.
Например, когда объект сохраняется в потоке, все ассоциированные с ним данные (т.е.
данные базового класса и содержащиеся в нем объекты) также автоматически сериали-
зуются. Поэтому, при попытке сериализовать производный класс в игру вступают
также все данные по цепочке наследования. И как будет показано, набор взаимосвязанных
объектов, участвующих в этом, представляется графом объектов.
Службы сериализации .NET также позволяют сохранять граф объектов в
различных форматах. В предыдущем примере кода применялся тип BinaryFormatter,
поэтому состояние объекта UserPrefs сохраняется в компактном двоичном формате. Г)эаф
объектов можно также сохранить в формате SOAP или XML, используя другие типы
форматеров. Эти форматы полезны, когда необходимо гарантировать возможность
передачи хранимых объектов между разными операционными системами, языками и
архитектурами.
На заметку! В WCF предлагается слегка отличающийся механизм для сериализации объектов в и
из операций службы WCR в нем используются атрибуты [DataContract] и [DataMember].
Подробнее об этом речь пойдет в главе 25.
И, наконец, имейте в виду, что граф объектов может быть сохранен в любом типе,
унаследованном от System.10.Stream. В предыдущем примере объект UserPrefs был
сохранен в локальном файле через тип FileStream. Однако если вместо этого
понадобится сохранить объект в определенной области памяти, можно применить тип
MemoryStream. Главное, чтобы последовательность данных корректно представляла
состояние объектов в графе.
736 Часть V. Введение в библиотеки базовых классов .NET
Роль графов объектов
Как упоминалось ранее, когда объект сериализуется, среда CLR учитывает все
связанные объекты, чтобы гарантировать корректное сохранение данных. Этот набор
связанных объектов называется графом объектов. Г^афы объектов представляют простой
способ документирования набора отношений между объектами, и эти отношения не
обязательно отображаются на классические отношения ООП (вроде отношений
"является" и "имеет"), хотя достаточно хорошо моделируют эту парадигму.
Каждый объект в графе получает уникальное числовое значение. Имейте в виду, что
числа, назначенные объектам в графе, являются произвольными и не имеют никакого
значения для внешнего мира. Как только каждому объекту присвоено числовое
значение, граф объектов может записать все наборы зависимостей каждого объекта.
В качестве простого примера предположим, что создан набор классов,
моделирующих автомобили. Существует базовый класс по имени Саг, который "имеет" класс
Radio. Другой класс по имени JamesBondCar расширяет базовый тип Саг. На рис. 20.4
показан возможный граф объектов, который моделирует эти отношения.
Car Radio
Рис. 20.4. Простой граф объектов
При чтении графов объектов для описания соединяющих стрелок можно
использовать выражение "зависит от" или "ссылается на". Таким образом, на рис. 20.1 видно,
что класс Саг ссылается на класс Radio (учитывая отношение "имеет"), JamesBondCar
ссылается на Саг (учитывая отношение "имеет"), как и на Radio (поскольку наследует
эту защищенную переменную-член).
Конечно, среда CLR не рисует картинок в памяти для представления графа
взаимосвязанных объектов. Вместо этого отношение, документированное в предыдущей
диаграмме, представлено математической формулой, которая выглядит примерно так:
[Саг 3, ref 2], [Radio 2], [JamesBondCar 1, ref 3, ref 2]
Если вы проанализируете эту формулу, то опять увидите, что объект 3 (Саг)
имеет зависимость от объекта 2 (Radio). Объект 2 (Radio) — это "одинокий волк",
которому не нужен никто. И, наконец, объект 1 (JamesBondCar) имеет зависимость как от
объекта 3, так и от объекта 2. В любом случае, при сериализации или десериализации
JamesBondCar граф объектов гарантирует, что типы Radio и Саг также будут
участвовать в процессе.
Изящество процесса сериализации состоит в том, что граф, представляющий
отношения между объектами, устанавливается автоматически, "за кулисами". Как будет
показано далее в этой главе, если необходимо вмешаться в конструирование графа
объектов, это можно сделать посредством настройки процесса сериализации через атрибуты
и интерфейсы.
Глава 20. Файловый ввод-вывод и сериализация объектов 737
На заметку! Строго говоря, тип XmlSerializer (описанный далее в главе) не сохраняет
состояния, используя граф объектов; однако он сериализует и десериализует связанные объекты в
предсказуемой манере.
Конфигурирование объектов для сериализации
Чтобы сделать объект доступным для служб сериализации .NET, понадобится
только декорировать каждый связанный класс (или структуру) атрибутом [Serializable].
Если выясняется, что некоторый тип имеет члены-данные, которые не должны (или
не могут) участвовать в схеме сериализации, можно пометить такие поля атрибутом
[NonSerialized]. Это помогает сократить размер хранимых данных, при условии, что в
сериализуемом классе есть переменные-члены, которые не следует "запоминать"
(например, фиксированные значения, случайные значения, кратковременные данные и т.п.).
Определение сериализуемых типов
Для начала создадим новое консольное приложение по имени SimpleSerialize.
Добавим в него новый класс по имени Radio, помеченный атрибутом [Serializable],
у которого исключается одна переменная-член (radioID), помеченная атрибутом
[NonSerialized] и потому не сохраняемая в специфицированном потоке данных.
[Serializable]
public class Radio
{
public bool hasTweeters;
public bool hasSubWoofers;
public double[] stationPresets;
[IlonSerialized]
public string radioID = "XF-552RR6";
}
Затем добавим два дополнительных типа, представляющих базовые классы
JamesBondCar и Саг (оба они также помечены атрибутом [Serializable]), и определим
в них следующие поля данных:
[Serializable]
public class Car
I
public Radio theRadio = new Radio ();
public bool isHatchBack;
}
[Serializable]
public class JamesBondCar : Car
{
public bool canFly;
public bool canSubmerge;
}
Имейте в виду, что атрибут [Serializable] не может наследоваться от
родительского класса. Поэтому при наследовании типа, помеченного [Serializable],
дочерний класс также должен быть помечен [Serializable] или же его нельзя
будет сохранить в потоке. Фактически все объекты в графе объектов должны быть
помечены атрибутом [Serializable]. Попытка сериализовать несериализуемый
объект с использованием BinaryFormatter или SoapFormatter приводит к исключению
SerializationException во время выполнения.
738 Часть V. Введение в библиотеки базовых классов .NET
Общедоступные поля, приватные поля и общедоступные свойства
Обратите внимание, что в каждом из этих классов поля данных определены как
public, это сделано для упрощения примера. Конечно, приватные данные,
представленные общедоступными свойствами, были бы более предпочтительны с точки зрения
ООП. Также для простоты в этих типах не определились никакие специальные
конструкторы, и потому все неинициализированные поля данных получат ожидаемые значения
по умолчанию. Оставив в стороне принципы ООП, можно спросить: какого определения
полей данных типа требуют различные форматеры, чтобы сериализовать их в поток?
Ответ такой: в зависимости от обстоятельств. Если вы сохраняете состояние объекта,
используя BinaryFormatter или SoapFormatter, то разницы никакой. Эти типы
запрограммированы для сериализации всех сериализуемых полей типа, независимо от
того, представлены они общедоступными полями, приватными полями или
приватными полями с соответствующими общедоступными свойствами. Однако вспомните, что
если есть элементы данных, которые не должны сохраняться в графе объектов, можно
выборочно пометить общедоступные или приватные поля атрибутом [NonSerialized],
как сделано со строковыми полями в типе Radio.
Однако ситуация существенно меняется, если вы собираетесь применять тип
XmlSerializer. Этот тип будет сериализовать только сериализуемые
общедоступные поля данных или приватные поля, представленные общедоступными свойствами.
Приватные данные, не представленные свойствами, просто игнорируются. Например,
рассмотрим следующий сериализуемый тип Person:
[Serializable]
public class Person
{
// Общедоступное поле.
public bool isAlive = true;
// Приватное поле.
private int personAge = 21;
// Общедоступное свойство/приватные данные.
private string fName = string.Empty;
public string FirstName
{
get { return fName; }
set { fName = value; }
}
}
При обработке BinaryFormatter или SoapFormatter обнаружится, что поля isAlive,
personAge и fName сохраняются в выбранном потоке. Однако XmlSerializer не
сохранит значения personAge, поскольку эта часть приватных данных не инкапсулирована в
свойство. Чтобы сохранять возраст персоны с помощью XmlSerializer, это поле
понадобится определить как public или же инкапсулировать его в общедоступном свойстве.
Выбор форматера сериализации
Как только типы сконфигурированы для участия в схеме сериализации .NET с
применением необходимых атрибутов, следующий шаг состоит в выборе формата
(двоичного, SOAP или XML) для сохранения состояния объектов. Перечисленные возможности
представлены следующими классами:
• BinaryFormatter
• SoapFormatter
• XmlSerializer
Глава 20. Файловый ввод-вывод и сериализация объектов 739
Тип BinaryFormatter сериализует состояние объекта в поток, используя
компактный двоичный формат. Этот тип определен в пространстве имен System.Runtime.
Serialization.Formatters.Binary, которое входит в сборку mscorlib.dll. Таким
образом, чтобы получить доступ к этому типу, необходимо указать следующую директиву
using:
// Получить доступ к BinaryFormatter в mscorlib.dll.
using System.Runtime.Serialization.Formatters.Binary;
Тип SoapFormatter сохраняет состояние объекта в виде сообщения SOAP
(стандартный XML-формат для передачи и приема сообщений от веб-служб). Этот тип определен в
пространстве имен System.Runtime.Serialization.Formatters.Soap, находящемся в
отдельной сборке. Поэтому для форматирования графа объектов в сообщение SOAP
необходимо сначала установить ссылку на System.Runtime.Serialization.Formatters.
Soap.d 11, используя диалоговое окно Add Reference (Добавить ссылку) в Visual Studio
2010 и затем указать следующую директиву using:
// Необходима ссылка на System.Riintime.Serialization.Formatters.Soap.dll!
using System.Runtime.Serialization.Formatters.Soap;
И, наконец, для сохранения дерева объектов в документе XML имеется тип
XmlSerializer. Чтобы использовать этот тип, нужно указать директиву using для
пространства имен System.Xml. Serialization и установить ссылку на сборку
System.Xml.dll. К счастью, шаблоны проектов Visual Studio 2010 автоматически
ссылаются на System.Xml.dll, так что достаточно просто указать соответствующее
пространство имен:
// Определено внутри System.Xml.dll.
using System.Xml.Serialization;
Интерфейсы IFormatter и IRemotingFormatter
Независимо от того, какой форматер выбран, имейте в виду, каждый из них
наследуется непосредственно от System.Object, так что они не разделяют общего набора
членов от какого-то базового класса сериализации. Однако типы BinaryFormatter
и SoapFormatter поддерживают общие члены через реализацию интерфейсов
IFormatter и IRemotingFormatter (как ни странно, XmlSerializer не реализует ни
одного из них).
В System.Runtime.Serialization.IFormatter определены основные методы
SerializeO HDeserializeO, которые выполняют черновую работу по перемещению
графов объектов в определенный поток и обратно. Помимо этих членов в IFormatter
определено несколько свойств, используемых "за кулисами" реализующим типом:
public interface IFormatter
{
SerializationBinder Binder { get; set; }
StreamingContext Context { get; set; }
ISurrogateSelector SurrogateSelector { get; set; }
object Deserialize(Stream serializationStream);
void Serialize(Stream serializationStream, object graph);
}
Интерфейс System. Runt ime.Remoting. Mess aging. IRemotingFormatter (который
внутри полагается на уровень удаленного взаимодействия .NET Remoting) перегружает
члены SerializeO nDeserializeO в манере, более подходящей для распределенного
сохранения. Обратите внимание, что интерфейс IRemotingFormatter унаследован от
более общего интерфейса IFormatter:
740 Часть V. Введение в библиотеки базовых классов .NET
public interface IRemotingFormatter : IFormatter
• {
object Deserialize(Stream serializationStream, HeaderHandler handler);
void Serialize(Stream serializationStream, object graph,
Header[] headers);
}
Хотя взаимодействовать с этими интерфейсами не понадобится в большинстве
сценариев сериализации, вспомните, что полиморфизм на базе интерфейсов
позволяет подставлять экземпляры BinaryFormatter или SoapFormatter там, где ожидается
IFormatter. Таким образом, если необходимо построить метод, который может сериали-
зовать граф объектов с использованием любого из этих классов, можно записать так:
static void SerializeObjectGraph(IFormatter itfFormat,
Stream destStream, object graph)
{
itfFormat.Serialize(destStream, graph);
}
Точность типов среди форматеров
Наиболее очевидное отличие между тремя форматерами связано с тем, как граф
объектов сохраняется в потоке (двоичном, SOAP или XML). Следует знать также о
некоторых более тонких отличиях, в частности — каким образом форматеры добиваются
точности типов (type fidelity). Когда используется тип BinaryFormatter, он сохраняет не
только данные полей объектов из графа, но также полное квалифицированное имя
каждого типа и полное имя определяющей его сборки (имя, версия, маркер
общедоступного ключа и культура). Эти дополнительные элементы данных делают BinaryFormatter
идеальным выбором, когда необходимо передавать объекты по значению (т.е. полные
копии) между границами машин для использования в .NET-приложениях.
Форматер SoapFormatter сохраняет трассировки сборок-источников за счет
использования пространства имен XML. Например, вспомните тип Person, определенный
ранее в этой главе. Если понадобится сохранить этот тип в сообщении SOAP, вы
обнаружите, что открывающий элемент Person квалифицирован сгенерированным
параметром xlmns. Взгляните на следующее частичное определение, обратив особое внимание
на пространство имен XML под названием al:
<al:Person id="ref-l" xmlns:al=
"http: //schemas .microsoft. com/clr/nsassem/SimpleSerialize/MyApp%2Cnd20
Versionnu3Dl .0.0. 0%2Cnj20Culture%3Dneutral%2C^20РиЫ1сКеуТокепЪЗОпи11м>
<isAlive>true</isAlive>
<personAge>21</personAge>
<fName id=,,ref-3"></fName>
</al:Person>
Однако XmlSerializer не пытается предохранить точную информацию о типе, и
потому не записывает его полного квалифицированного имени или сборки, в которой
он определен. Хотя на первый взгляд это может показаться ограничением, причина
состоит в открытой природе представления данных XML. Ниже показано возможное XML-
представление типа Person:
<?xml version="l.0"?>
<Регson xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<isAlive>true</isAlive>
<PersonAge>21</PersonAge>
<FirstName />
</Person>
Глава 20. Файловый ввод-вывод и сериализация объектов 741
Если необходимо сохранить состояние объекта так, чтобы его можно было
использовать в любой операционной системе (Windows XP, Mac OS X и различных дистрибутивах
Linux), на любой платформе приложений (.NET, Java Enterprise Edition, COM и т.п.) или
в любом языке программирования, придерживаться полной точности типов не следует,
поскольку нельзя рассчитывать, что все возможные адресаты смогут понять
специфичные для .NET типы данных. Учитывая это, SoapFormatter и XmlSerializer являются
идеальным выбором, когда требуется гарантировать как можно более широкое
распространение объектов.
Сериализация объектов с использованием
BinaryFormatter
Чтобы проиллюстрировать, насколько просто сохранить экземпляр JamesBondCar
в физическом файле, воспользуемся типом BinaryFormatter. Двумя ключевыми
методами типа BinaryFormatter, о которых следует знать, являются Serialize () и
Deserialize():
1. SerializeO сохраняет граф объектов в указанный поток в виде
последовательности байтов;
2. Deserialize () преобразует сохраненную последовательность байт в граф
объектов.
Предположим, что после создания экземпляра JamesBondCar и модификации
некоторых данных состояния требуется сохранить этот экземпляр в файле *.dat. Первая
задача — создание самого файла *.dat. Для этого можно создать экземпляр типа System.
IO.FileStream. Затем следует создать экземпляр BinaryFormatter и передать ему
FileStream и граф объектов для сохранения. Взгляните на следующий метод Main():
//Не забудьте импортировать пространства имен
// System.Runtime.Serialization.Formatters.Binary и System.10'
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Object Serialization *****\n");
// Создать JamesBondCar и установить состояние.
JamesBondCar jbc = new JamesBondCar();
jbc.canFly = true;
jbc.canSubmerge = false;
jbc.theRadio.stationPresets = new doublet]{89.3, 105.1, 97.1};
jbc.thePadio.hasTweeters = true;
// Сохранить объект в указанном файле в двоичном формате.
SaveAsBinaryFormat(jbc, "CarData.dat");
Console.ReadLine();
}
Метод SaveAsBinaryFormat() реализован следующим образом:
static void SaveAsBinaryFormat(object objGraph, string fileName)
{
// Сохранить объект в файл CarData.dat в двоичном виде.
BinaryFormatter binFormat = new BinaryFormatter();
using (Stream fStream = new FileStream(fileName,
FileMode.Create, FileAccess.Write, FileShare.None))
{
binFormat.Serialize(fStream, objGraph);
I
Console.WriteLine("=> Saved car in binary format1");
742 Часть V. Введение в библиотеки базовых классов .NET
Как видите, метод BinaryFormatter.Serialize() — это член, отвечающий за
составление графа объектов и передачу последовательности байт в некоторый объект
унаследованного от Stream типа. В данном случае поток представляет физический
файл. Однако можно также сериализовать объекты в любой тип-наследник Stream,
представляющий область памяти, сетевой поток и т.п. После выполнения программы
можно просмотреть содержимое файла CarData.dat, представляющее этот экземпляр
JamesBondCar, в папке bin\Debug текущего проекта. На рис. 20.5 показан этот файл,
открытый в Visual Studio 2010.
в
CarData.so*p* X
S<SQAP-ENV:Envelope xwlnsixsig-http://www.w3.org/29ei/XHLSchema-instance'* xmlns:xsd="l
<SOAP-ENV:Body>
<al:JamesBondCar id-'Vef-l" xmlns:al="http://scheilias.microsoft.com/cIr/nsassew/S:
<canFly>true</canFly>
<canSubraerge>false</canSubmerge>
<theRadio href="#ref-3"/>
<isHatchBack>false</isHatchBack>
</al:3amesBondCar>
<al:Radio id^ref-3" xnlnsial^'http^/scheiiias.iiicrosoft.coii/clr/nsassew/SinipleSei
В
<hasTweeters>true</hasTweeters>
<hasSubwoofers>false</hasSubWoofers>
<stationPresets href«"#ref-4'*/>
</al:Radio>
<S0AP-ENC:Array id-Vef-A*' S0AP-ENC:arrayType=*,xsd:double[3]',>
<it«»>89.3</ite«>
< ite»>105.1</item>
<itee>97.K/iteni>
</SOAP-ENC:Array>
</SOAP-ENV:Body>
</SOAP-EW: Envelope>
Рис. 20.5. Объект JamesBondCar, сериализованный с использованием BinaryFormatter
Десериализация объектов с использованием BinaryFormatter
Теперь предположим, что необходимо прочитать сохраненный объект JamesBondCar
из двоичного файла обратно в объектную переменную. После открытия файла
CataData.dat (методом File.OpenReadO) просто вызовите метод Deserialize()
класса BinaryFormatter. Имейте в виду, что Deserialize() возвращает объект общего
типа System.Object, так что понадобится применить явное приведение, как показано
ниже:
static void LoadFromBinaryFile(string fileName)
{
BinaryFormatter binFormat = new BinaryFormatter();
// Прочитать JamesBondCar из двоичного файла.
using(Stream fStream = File.OpenRead(fileName))
{
JamesBondCar carFromDisk =
(JamesBondCar)binFormat.Deserialize(fStream);
Console.WriteLine("Can this car fly? : {0}", carFromDisk.canFly);
}
Обратите внимание, что при вызове Deserialize() ему передается тип-наследник
Stream, представляющий местоположение сохраненного графа объектов. Приведя
возвращенный объект к правильному типу, вы получаете объект в том состоянии, в каком
он был на момент сохранения.
Глава 20. Файловый ввод-вывод и сериализация объектов 743
Сериализация объектов с использованием
SoapFormatter
Следующий форматер, которым мы воспользуемся, будет SoapFormatter, сериа-
лизующий данные в подходящем конверте SOAP. Протокол SOAP (Simple Object Access
Protocol — простой протокол доступа к объектам) определяет стандартный процесс
вызова методов в независящей от платформы и операционной системы манере.
Предполагая, что ссылка на сборку System.Runtime.Serialization.Formatters.
Soap.dll установлена, а пространство имен System.Runtime.Serialization.
Formatters.Soap импортировано, для сохранения и извлечения JamesBondCar в
виде сообщения SOAP можно просто заменить в предыдущем примере все вхождения
BinaryFormatter на SoapFormatter. Ниже показан новый метод класса Program,
который сериализует объект в локальный файл:
//Не забудьте импортировать пространства имен
// System.Runtime.Serialization.Formatters.Soap
//и установить ссылку на System.Runtimp.Serialization.Formatters.Soap.dll!
static void SaveAsSoapFormat (object objGraph, string fileName)
{
// Сохранить объект в файл CarData.soap в формате SOAP.
SoapFormatter soapFormat = new SoapFormatter();
using(Stream fStream = new FileStream(fileName,
FileMode.Create, FileAccess.Write, FileShare.None) )
{
soapFormat.Serialize(fStream, objGraph);
}
Console. WriteLine ("=> Saved car in SOAP format!11);
}
Как и ранее, для перемещения графа объектов в поток и обратно применяются
методы Serialize () и Deserialize (). После вызова метода SaveAsSoapFormat () в Main()
и запуска приложения можно открыть файл *.soap. В нем находятся XML-элементы,
которые описывают значения состояния текущего объекта JamesBondCar, а также
отношения между объектами в графе через лексемы #ref (рис. 20.6).
MyData-soap* X Q
E<SOAP-ENV:Envelope w»lns:xsi»"http://www.w3.orj»/2eei/XMLSchema-instance" xnlnsixsd^http^jH
3 <S0AP-ENV:Body>
7; <al:StringData id="ref-l" xrolns:al^"http://schefflas.fflicrosoft.com/clr/nsassem/CustoniSerl J
K<First_It€W id»"ref-3',>FIRST DATA BL0OC</First_Ite«> e|
<dataIte«Two id*Vef-4">HC)RE fWTA</dataIteeTwo>
I </al:StringPata>
[ </50AP-ENV:Body>
[</SOAP-ENV:Envelope>
|iPP.%. JJ * 1 *
Рис. 20.6. Объект JamesBondCar, сериализованный с использованием SoapFormatter
Сериализация объектов с использованием
XmlSerializer
В дополнение к двоичному форматеру и форматеру SOAP сборка System.Xml.dll
предлагает третий класс форматера — System.Xml.Serialization.XmlSerializer, —
который может использоваться для сохранения общедоступного состояния
заданного объекта в виде чистой XML-разметки, в противоположность данным XML внут-
744 Часть V. Введение в библиотеки базовых классов .NET
ри сообщения SOAP. Работа с этим типом несколько отличается от работы с типами
SoapFormatter или BinaryFormatter. Рассмотрим следующий код (предполагается,
что было импортировано пространство имен System.Xml.Serialization):
static void SaveAsXmlFormat(object objGraph, string fileName)
{
// Сохранить объект в файле CarData.xml в формате XML.
XmlSerializer xmlFormat = new XmlSerializer(typeof(JamesBondCar) ,
new Type[] { typeof(Radio), typeof(Car) });
using(Stream fStream = new FileStream(fileName,
FileMode.Create, FileAccess.Write, FileShare.None))
{
xmlFormat.Serialize(fStream, objGraph);
}
Console. WriteLine ("=> Saved car in XML format!11);
}
Ключевое отличие состоит в том, что тип XmlSerializer требует указания
информации о типе, представляющей класс, который необходимо сериализовать. В
сгенерированном файле XML находятся показанные ниже данные XML:
<?xml version=.0"?>
<JamesBondCar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<theRadiq>
<hasTweeters>true</hasTweeters>
<hasSubWoofers>false</hasSubWoofers>
<stationPresets>
<double>89.3</double>
<double>105. K/double>
<double>97 . K/double>
</stationPresets>
<radioID>XF-552RR6</radioID>
</theRadio>
<isHatchBack>false</isHatchBack>
<canFly>true</canFly>
<canSubmerge>false</canSubmerge>
</JamesBondCar>
На заметку! Класс XmlSerializer требует, чтобы все сериализованные типы в графе объектов
поддерживали конструктор по умолчанию (поэтому не забудьте его добавить, если определяли
специальные конструкторы). Если этого не сделать, во время выполнения сгенерируется
исключение InvalidOperationException.
Управление генерацией данных XML
Если у вас есть опыт в технологиях XML, то вы хорошо знаете, насколько важно
удостоверяться, что данные внутри документа XML отвечают набору правил, которые
устанавливают действительность данных. Понятие "действительного" документа XML
не имеет отношения к синтаксической правильности элементов XML (вроде того, что
все открывающие элементы должны иметь соответствующие закрывающие элементы).
Действительные документы — это те, что отвечают согласованным правилам
форматирования (например, поле X должно быть выражено как атрибут, но не как подэлемент),
которые обычно определены схемой XML или в файле определения типа документа
(Document-Type Definition — DTD).
Глава 20. Файловый ввод-вывод и сериализация объектов 745
По умолчанию класс XmlSerializer сериализует все общедоступные
поля/свойства как элементы XML, а не как атрибуты XML. Чтобы управлять генерацией
результирующего документа XML с помощью класса XmlSerializer, необходимо декорировать
типы любым количеством дополнительных атрибутов из пространства имен System.
Xml.Serialization. В табл. 20.12 документированы некоторые (но не все) атрибуты,
которые влияют на кодирование данных XML в потоке.
Таблица 20.12. Избранные атрибуты из пространства имен System.Xml.Serialization
Атрибут Назначение
[XmlAttnbute] Поле или свойство будет сериализовано как атрибут XML (а не как подэлемент)
[XmlElement] Поле или свойство будет сериализовано как элемент XML с указанным именем
[XmlEnum] Этот атрибут предоставляет имя элемента, являющееся членом перечисления
[XmlRoot] Этот атрибут управляет тем, как будет сконструирован корневой элемент
(пространство имен и название элемента)
[XmlText] Свойство или поле должно быть сериализовано как текст XML (т. е. содержимое,
находящееся между начальным и конечным дескрипторами корневого элемента)
[XmlType] Этот атрибут предоставляет имя и пространство имен типа XML
В следующем простом примере показано текущее представление данных полей
JamesBondCar в XML:
<?xml ersion="l. 0" encoding="utf-8ll?>
<JamesBondCar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<canFly>tme</canFly>
<canSubmerge>false</canSubmerge>
</JamesBondCar>
Если необходимо указать специальное пространство имен XML, которое
квалифицирует JamesBondCar и закодирует значения canFly и canSubmerge в виде атрибутов
XML, модифицируем определение класса JamesBondCar следующим образом:
[XmlRoot (Namespace = "http://www.MyCompany.com11)]
public class JamesBondCar : Car
{
[XmlAttribute]
public bool canFly;
[XmlAttribute]
public bool canSubmerge;
}
Это порождает показанный ниже XML-документ (обратите внимание на
открывающий элемент <JamesBondCar>):
<?xml version="l.0 ?>
<JamesBondCar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
canFly="true11 canSubmerge=" false"
xmlns="http://www.MyCompany.com">
</JamesBondCar>
746 Часть V. Введение в библиотеки базовых классов .NET
Естественно, для управления генерацией результирующего XML-документа с
помощью XmlSerializer может использоваться и множество других атрибутов. За подробной
информацией обращайтесь к описанию пространства имен System.Xml.Serialization
в документации .NET Framework 4.0 SDK.
Сериализация коллекций объектов
Теперь, когда известно, как сохранять единственный объект в потоке, давайте
посмотрим, каким образом можно сохранить множество объектов. Как вы, возможно,
заметили, метод Serialize () интерфейса IFormatter не предусматривает способа
указать произвольное количество объектов в качестве ввода (допускается только один
объект System.Object). Вдобавок возвращаемое значение DeserializeO также
представляет собой одиночный объект System.Object (то же базовое ограничение касается
и XmlSerializer):
public interface IFormatter
{
object Deserialize(Stream serializationStream);
void Serialize(Stream serializationStream, object graph);
}
Вспомните, что System.Object представляет целое дерево объектов. С учетом этого,
если передать объект, который помечен атрибутом [Serializable] и содержит в себе
другие объекты [Serializable], то с помощью единственного вызова данного метода
будет сохраняться весь набор объектов. К счастью, большинство типов из пространств
имен System.Collections и System.Collections.Generic уже помечены атрибутом
[Serializable]. Таким образом, чтобы сохранить множество объектов, просто добавьте
это множество в контейнер (такой как ArrayList или List<T>) и сериализуйте данный
объект в выбранный поток.
Предположим, что класс JamesBondCar дополнен конструктором, принимающим
два аргумента, для установки нескольких фрагментов данных состояния (обратите
внимание, что должен быть также добавлен конструктор по умолчанию, как того требует
XmlSerializer):
[Serializable,
XmlRoot (Namespace = "http://www.MyCompany.com11)]
public class JamesBondCar : Car
{
public JamesBondCar(bool skyWorthy, bool seaworthy)
{
canFly = skyWorthy;
canSubmerge = seaworthy;
}
// XmlSerializer требует конструктора по умолчанию1
public JamesBondCar(){}
}
Теперь можно сохранять любое количество объектов JamesBondCar, как показано
ниже:
static void SaveListOfCars ()
{
// Сохранить список List<T> объектов JamesBondCar.
List<JamesBondCar> myCars = new List<JamesBondCar>();
Глава 20. Файловый ввод-вывод и сериализация объектов 747
myCars.Add(new JamesBondCar(true, true));
myCars.Add(new JamesBondCar(true, false));
myCars.Add(new JamesBondCar(false, true));
myCars.Add(new JamesBondCar(false, false));
using(Stream fStream = new FileStream("CarCollection.xml",
FileMode.Create, FileAccess.Write, FileShare.None))
{
XmlSerializer xmlFormat = new XmlSerializer(typeof(List<JamesBondCar>));
xmlFormat.Serialize(fStream, myCars);
}
Console. WriteLine ("=> Saved list of cars!11);
}
Поскольку здесь используется XmlSerializer, необходимо указать информацию о
типе для каждого из подобъектов внутри корневого объекта (которым в данном
случае является List<JamesBondCar>). Если бы применялись типы BinaryFormatter или
SoapFormatter, то логика была бы еще проще. Например:
static void SaveListOfCarsAsBinary()
{
// Сохранить объект ArrayList (myCars) в двоичном виде.
List<JamesBondCar> myCars = new List<JamesBondCar>();
BinaryFormatter binFormat = new BinaryFormatter();
using(Stream fStream = new FileStream("AllMyCars.dat",
FileMode.Create, FileAccess.Write, FileShare.None))
{
binFormat.Serialize(fStream, myCars);
}
Console.WriteLine("=> Saved list of cars in binary1");
}
Исходный код. Проект SimpleSerialize доступен в подкаталоге Chapter 20.
Настройка процессов сериализации
SOAP и двоичной сериализации
В большинстве случаев схема сериализации по умолчанию, предоставляемая
платформой .NET, вполне подходит. Нужно лишь применить атрибут [Serializable] к
связанным типам и передать дерево объектов выбранному форматеру для обработки.
Однако в некоторых случаях может понадобиться вмешаться в процесс
конструирования дерева и процесс сериализации. Например, может существовать бизнес-правило,
которое гласит, что все поля данных должны сохраняться в определенном формате, или
же необходимо добавить дополнительные данные в поток, которые напрямую не
отображаются на поля сохраняемого объекта (временные метки, уникальные
идентификаторы и т.п.).
В пространстве имен System.Runtime.Serialization предусмотрено несколько
типов, которые позволяют вмешаться в процесс сериализации объектов. В табл. 20.13
описаны некоторые из основных типов.
748 Часть V. Введение в библиотеки базовых классов .NET
Таблица 20.13. Основные типы пространства имен System.Runtime.Serialization
Тип Назначение
ISerializable Этот интерфейс может быть реализован на типе [Serializable]
для управления его сериализацией и десериализацией
ObjectlDGenerator Этот тип генерирует идентификаторы для членов графа объектов
[OnDeserialized] Этот атрибут позволяет указать метод, который будет вызван
немедленно после десериализации объекта
[OnDeserializing] Этот атрибут позволяет указать метод, который будет вызван перед
процессом десериализации
[OnSerialized] Этот атрибут позволяет указать метод, который будет вызван
немедленно после того, как объект сериализован
[OnSerializing] Этот атрибут позволяет указать метод, который будет вызван перед
процессом сериализации
[OptionalField] Этот атрибут позволяет определить поле типа, которое может быть
пропущено в указанном потоке
Serializationlnfo По существу это "мешок свойств", который поддерживает пары "имя/
значение", представляющие состояние объекта во время процесса
сериализации
Углубленный взгляд на сериализацию объектов
Прежде чем приступить к изучению различных способов настройки процесса
сериализации, полезно внимательнее присмотреться к тому, что происходит "за кулисами".
Когда BinaryFormatter сериализует граф объектов, он отвечает за передачу
следующей информации в указанный поток:
• полностью квалифицированное имя объекта в графе (например, My Ар р.
JamesBondCar);
• имя сборки, определяющей граф объектов (например, MyApp.exe);
• экземпляр класса Serializationlnfo, содержащего все данные состояния,
которые поддерживаются членами графа объектов.
Во время процесса десериализации BinaryFormatter использует ту же информацию
для построения идентичной копии объекта с применением информации, извлеченной
из потока-источника. Процесс, выполняемый SoapFormatter, очень похож.
На заметку! Вспомните, что для обеспечения максимальной мобильности объекта XmlSerializer
не сохраняет полностью квалифицированное имя типа или имя сборки, в которой он
содержится. Этот тип может сохранять только общедоступные данные.
Помимо перемещения необходимых данных в поток и обратно, форматеры
также анализируют члены графа объектов на предмет перечисленных ниже частей
инфраструктуры.
• Проверка пометки объекта атрибутом [Serializable]. Если объект не помечен,
генерируется исключение SerializationException.
• Если объект помечен атрибутом [Serializable], выполняется проверка,
реализует ли объект интерфейс ISerializable. Если да, то вызывается метод
GetObjectData() на этом объекте.
Глава 20. Файловый ввод-вывод и сериализация объектов 749
• Если объект не реализует интерфейс ISerializable, используется процесс се-
риализации по умолчанию, который обрабатывает все поля, не помеченные
[NonSerialized].
В дополнение к определению того, поддерживает ли тип ISerializable,
форматеры также отвечают за исследование типов на предмет поддержки членов, которые
оснащены атрибутами [OnSerializing], [OnSerialized], [OnDeserializing] или
[OnDeserialized]. Мы рассмотрим назначение этих атрибутов чуть позже, а сначала
давайте посмотрим на предназначение ISerializable.
Настройка сериализации с использованием
интерфейса ISerializable
Объекты, которые помечены атрибутом [Serializable], имеют опцию
реализации интерфейса ISerializable. Реализация этого интерфейса позволяет вмешаться в
процесс сериализации и выполнить необходимое форматирование данных до и после
сериализации.
На заметку! После выхода версии .NET 2.0 предпочтительный способ настройки процесса
сериализации начал предусматривать использование атрибутов сериализации. Тем не менее, знание
интерфейса ISerializable важно для сопровождения систем, которые уже существуют.
Интерфейс ISerializable довольно прост, учитывая, что в нем определен
единственный метод GetObjectData():
// Чтобы вмешаться в процесс сериализации,
// необходимо реализовать ISerializable.
public interface ISerializable
{
void GetObjectData(Serializationlnfo info,
StreamingContext context);
}
Метод GetObjectDataO вызывается автоматически заданным форматером во
время процесса сериализации. Реализация этого метода заполняет входной параметр
Serializationlnfo последовательностью пар "имя/значение", которые (обычно)
отображают данные полей сохраняемого объекта. В Serializationlnfo определены
многочисленные вариации перегруженного метода AddValueO, а также небольшой набор
свойств, которые позволяют устанавливать и получать имя типа, определять сборку и
счетчик членов. Ниже показано частичное определение:
public sealed class Serializationlnfo
{
public Serializationlnfo(Type type, IFormatterConverter converter);
public string AssemblyName { get; set; }
public string FullTypeName { get; set; }
public int MemberCount { get; }
public void AddValue(string name, short value);
public void AddValue(string name, ushort value);
public void AddValue(string name, int value);
}
Типы, реализующие интерфейс ISerializable, также должны определять
специальный конструктор со следующей сигнатурой:
// Необходимо предусмотреть специальный конструктор с такой сигнатурой,
// чтобы позволить исполняющей среде устанавливать состояние объекта.
750 Часть V. Введение в библиотеки базовых классов .NET
[Serializable]
class SomeClass : ISerializable
{
protected SomeClass (Serializationlnfo si, StreamingContext ctx) { . . . }
}
Обратите внимание, что видимость конструктора указана как protected. Это
приемлемо, учитывая, что форматер будет иметь доступ к этому члену независимо от его
видимости. Такие специальные конструкторы обычно делают protected (или private),
тем самым гарантируя, что небрежный пользователь объекта никогда не создаст объект
подобным образом. Как видите, первым параметром конструктора является экземпляр
типа Serializationlnfo (который был показан ранее).
Второй параметр этого специального конструктора имеет тип StreamingContext
и содержит информацию относительно источника или места назначения
передаваемых данных. Наиболее информативным членом StreamingContext является
свойство State, представляющее значение из перечисления StreamingContextStates.
Значения этого перечисления представляют базовую композицию текущего потока.
Если только вы не планируется реализовать какие-то низкоуровневые службы
удаленного взаимодействия, то иметь дело с этим перечислением непосредственно
требуется редко. Тем не менее, ниже приведены члены перечисления StreamingContextStates
(подробности ищите в документации .NET Framework 4.0 SDK):
public enum StreamingContextStates
{
CrossProcess,
CrossMachine,
File,
Persistence,
Remoting,
Other,
Clone,
CrossAppDomain,
All
}
Давайте рассмотрим пример настройки процесса сериализации с использованием
ISerializable. Предположим, что имеется новый проект консольного приложения
(по имени CustomSerialization), в котором определен тип класса, содержащего два
элемента данных string. Также представим, что требуется обеспечить сериализа-
цию этих объектов string в верхнем регистре, а десериализацию — в нижнем. Чтобы
удовлетворить этим правилам, можно реализовать интерфейс ISerializable так,
как показано ниже (не забудьте импортировать пространство имен System.Runtime.
Serialization):
[Serializable]
class StringData : ISerializable
{
private string dataltemOne = "First data block";
private string dataItemTwo= "More data";
public StringData () {}
protected StringData(Serializationlnfo si, StreamingContext ctx)
{
// Восстановить переменные-члены из потока.
dataltemOne = si.GetString("First_Item").ToLower();
dataltemTwo = si.GetString("dataltemTwo").ToLower();
}
Глава 20. Файловый ввод-вывод и сериализация объектов 751
void ISerializable.GetObjectData (Serializationlnfo info, StreamingContext ctx)
{
// Наполнить объект Serializationlnfo форматированными данными.
info.AddValue("First_Item", dataltemOne.ToUpper ());
info.AddValue("dataltemTwo", dataltemTwo.ToUpper ());
}
}
Обратите внимание, что при наполнении объекта типа Serializationlnfo внутри
метода GetObjectDataO именовать элементы данных идентично именам внутренних
переменных-членов типа не обязательно. Это очевидно пригодится, если планируется
отвязать данные типа от формата хранения. Однако имейте в виду, что нужно получить
данные внутри специального защищенного конструктора, используя те же имена, что
были назначены в GetObjectDataO.
Чтобы опробовать специализированную сериализацию, предположим, что
экземпляр MyStringData сохраняется с использованием SoapFormatter (обновите
соответствующим образом ссылки на сборки и директивы using):
static void Main(string [ ] args)
{
Console.WriteLine("***** Fun with Custom Serialization *****");
// Вспомните, что этот тип реализует ISerializable.
StringData myData = new StringDataO ;
// Сохранить в локальный файл в формате SOAP.
SoapFormatter soapFormat = new SoapFormatter();
using(Stream fStream = new FileStream("MyData.soap",
FileMode.Create, FileAccess.Write, FileShare.None))
{
soapFormat.Serialize(fStream, myData);
}
Console.ReadLine();
}
Просматривая полученный файл *.soap, вы заметите, что строковые поля
действительно сохранены в верхнем регистре (рис. 20.7).
MyData.soap* X
<SOAP-ENV:Envelope xmlns:xsi-"http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd^'http:>щ1
Э <SOAP-ENV:Body
<al:StringData id*"ref-l" xmlns:al«"http://schemas.microsoft,com/clr/nsassem/CustofflSerl
st_ItiMi id*"ref-3M>FIRST DATA BLOCK</First_Ite*>
italteeTwo id-Vef-4" >M0RE DATA</dataItemTwo>
</al:StringData>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
1
Рис. 20.7. Настройка сериапизации с использованием интерфейса ISerializable
Настройка сериализации с использованием атрибутов
Хотя реализация интерфейса ISerializable является одним из возможных
способов настройки процесса сериализации, с момента выхода версии .NET 2.0
предпочтительным способом такой настройки стало определение методов, оснащенных
атрибутами из следующего перечня: [OnSerializing], [OnSerialized], [OnDeserializing]
и [OnDeserialized]. Использование этих атрибутов дает менее громоздкий код, чем
реализация интерфейса ISerializable, учитывая, что не приходится вручную взаимо-
752 Часть V. Введение в библиотеки базовых классов .NET
действовать с параметром Serializationlnfo. Вместо этого можно напрямую
модифицировать данные состояния, когда форматер работает с вашим типом.
На заметку! Эти атрибуты сериализации определены в пространстве имен System.Runtime.
Serialization.
В случае применения этих атрибутов метод должен быть определен так, чтобы
принимать параметр StreamingContext и не возвращать ничего (иначе будет
сгенерировано исключение времени выполнения). Обратите внимание, что применять
каждый из атрибутов сериализации не обязательно, а можно просто вмешаться в те
стадии процесса сериализации, которые интересуют. Для иллюстрации ниже
приведен новый тип [Serializable], к которому предъявлены те же требования, что и к
StringData, но на этот раз он полагается на использование атрибутов [OnSerializing]
и [OnDeserialized]: '
[Serializable]
class MoreData
{
private string dataltemOne = "First data block";
private string dataItemTwo= "More data";
[OnSerializing]
private void OnSerializing (StreamingContext context)
{
// Вызывается во время процесса сериализации.
dataltemOne = dataltemOne.ToUpper() ;
dataltemTwo = dataltemTwo.ToUpper() ;
}
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
// Вызывается no завершении процесса десериализации.
dataltemOne = dataltemOne.ToLower();
dataltemTwo = dataltemTwo.ToLower ();
}
}
Выполнив сериализацию этого нового типа, вы снова обнаружите, что данные
сохраняются в верхнем регистре, а десериализуются — в нижнем.
Исходный код. Проект CustomSerialization доступен в подкаталоге Chapter 20.
Приведенный пример наглядно продемонстрировал основные детали служб
сериализации объектов, включая различные способы настройки этого процесса. Как было
показано, процессы сериализации и десериализации существенно упрощают задачу
сохранения больших объемов данных, причем с меньшими затратами, чем работа с
различными классами чтения/записи данных из пространства имен System. 10.
Резюме
Эта глава началась с рассмотрения использования типов Directory (Directorylnfo)
и File (FileInfo). Вы узнали, что упомянутые классы позволяют манипулировать
физическими файлами и каталогами на жестком диске. Затем вы ознакомились с
несколькими типами, унаследованными от абстрактного класса Stream. Учитывая, что
типы, унаследованные от Stream, оперируют низкоуровневым потоком байтов,
пространство имен System. 10 предоставляет многочисленные типы для чтения/записи
Глава 20. Файловый ввод-вывод и сериализация объектов 753
(StreamWnter, StringWriter, BinaryWriter и т.п.), которые упрощают этот процесс.
Попутно вы узнали о функциональности, предлагаемой типом DriveType, и научились
наблюдать за файлами с применением типа FileSystemWatcher, а также
взаимодействовать с потоками в асинхронном режиме.
В главе также рассматривались службы сериализации объектов. Было показано, что
платформа .NET использует граф объектов, чтобы учесть полный набор объектов,
которые должны сохраниться в потоке. До тех пор, пока каждый объект в графе помечен
атрибутом [Serializable], данные сохраняются в выбранном формате (двоичном или
SOAP).
Вы также ознакомились с возможностями настройки готового процесса
сериализации посредством двух возможных подходов. Во-первых, вы узнали, как реализовать
интерфейс ISerializable (и поддерживать специальный приватный конструктор), что
позволяет вмешаться в процесс сохранения форматером данных объекта. Во-вторых,
вы ознакомились с набором атрибутов .NET, которые упрощают процесс специальной
сериализации. Все, что нужно — это просто применять атрибуты [OnSerializing],
[OnSerialized], [OnDeserializing] или [OnDeserialized] к методам,
принимающим параметр StreamingContext, и форматеры будут вызывать их на
соответствующих фазах сериализации или десериализации.
ГЛАВА 21
ADO.NET, часть I:
подключенный уровень
Как и следовало ожидать, платформа .NET определяет ряд пространств имен,
которые позволяют непосредственно взаимодействовать с локальными и удаленными
реляционными базами данных. Все вместе эти пространства имен известны как ADO.NET.
В данной главе вначале будет описана в общих чертах роль самой ADO.NET, а затем мы
перейдем к теме поставщиков данных ADO.NET. Платформа .NET поддерживает
множество различных поставщиков данных, каждый из которых оптимизирован на
взаимодействие с конкретной СУБД (Microsoft SQL Server, Oracle, MySQL и т.д.).
После того как мы разберемся с общими возможностями различных поставщиков
данных, мы рассмотрим образец генератора поставщиков данных. Вы увидите, что
имена из пространства имен System.Data.Common (и связанного с ним файла App.config)
позволяют создать единую кодовую базу, которая может динамически выбрать
соответствующий поставщик данных без необходимости перекомпиляции или нового
развертывания кодовой базы данного приложения.
Наверное, наиболее важно то, что в данной главе вы создадите собственную
сборку библиотеки доступа к данным (AutoLotDAL.dll), в которой будут инкапсулированы
различные операции, выполняемые в пользовательской базе данных по имени AutoLot.
Эта библиотека, которая будет расширена в главах 23 и 24, неоднократно пригодится в
последующих главах. И в завершение мы познакомимся с транзакциями баз данных.
Высокоуровневое определение ADO.NET
Если вы уже знакомы с предыдущей моделью Microsoft доступа к данным на основе
COM (Active Data Objects — ADO), то сразу же предупреждаем: ADO.NET имеет очень
мало общего с ADO, за исключением букв "A", "D" и "О". Хотя некоторая взаимосвязь
между этими двумя системами и существует (например, в обеих имеется концепция
объектов подключения и объектов команд), некоторые знакомые по ADO типы (например,
Recordset) больше не существуют. Кроме того, в ADO.NET появился ряд новых типов,
не имеющих прямых эквивалентов в классической ADO (например, адаптер данных).
В отличие от классической ADO, которая была в основном предназначена для тесно
связанных клиент-серверных систем, ADO.NET больше нацелена на автономную работу
с помощью объектов DataSet. Эти типы представляют локальные копии любого
количества взаимосвязанных таблиц данных, каждая из которых содержит набор строк и
столбцов. Объекты DataSet позволяют вызывающей сборке (наподобие веб-страницы
или программы, выполняющейся на настольном компьютере) работать с содержимым
DataSet, изменять его, не требуя подключения к источнику данных, и отправлять об-
Глава 21. ADO.NET, часть I: подключенный уровень 755
* »о 15
i> {} Microsoft.SqlServer.5erver
Ъ {} System.Data
!> {} System.Data .Common
> <} System.Data.Odbc
f> {> System.Data.OteDb
t> {} System.Data.Sqt
> {} System.Data.SqfClient
t> 0 System.Data.SqtTypes
0 О System.Xml
' f ' ill s
C
□
-
ратно блоки измененных данных для обработки с
помощью соответствующего адаптера данных.
Но, пожалуй, самое фундаментальное различие
между классической ADO и ADO.NET сострит в том, что
ADO.NET является управляемой кодовой библиотекой,
и, значит, подчиняется тем же правилам, что и любая
управляемая библиотека. Типы, составляющие ADO.NET,
используют протокол управления памятью CLR,
принадлежат к той же системе типов (классы, интерфейсы,
перечисления, структуры и делегаты), и доступ к ним воз-
_. .TTVn Рис. 21.1. Ьэзовэя сооркэ
можен с помощью любого языка .NET. *n^ ш-т « *. ~ *. ^-. -.
_ ._Л.ТТОТ, ADO.NET-System.Data.dll
С точки зрения программиста, тело ADO.NET
составляет базовая сборка с именем System.Data.dll. В этом двоичном файле находится
значительное количество пространств имен (рис. 21.1), многие из которых представляют
типы конкретного поставщика данных ADO.NET (р которых речь пойдет ниже).
Большинство шаблонов проектов Visual Studio 2010 автоматически ссылаются на
эту ключевую библиотеку доступа к данным. Однако для импортирования нужных
пространств имен необходимо изменить кодовые файлы, например:
using System;
// Задействование некоторых пространств имен AD0.NET
using System.Data;
using System.Data.SqlClient;
namespace MyApp
{
class Program
{
static void Main(string [ ] args)
{
}
}
}
Учтите также, что кроме System.Data.dll, существуют и другие
ориентированные на ADO.NET сборки (например, System.Data.OracleClient.dll и System.Data.
Entity.dll), которые необходимо вручную указывать в текущем проекте с помощью
диалогового окна Add Reference (Добавление ссылки).
Три стороны ADO.NET
Библиотеки ADO.NET можно применять тремя концептуально различными
способами: в подключенном режиме, в автономном режиме и с помощью технологии Entity
Framework. При использовании подключенного уровня (connected layer),
рассматриваемого в данной главе, кодовая база явно подключается к соответствующему
хранилищу данных и отключается от него. При таком способе использования ADO.NET обычно
происходит взаимодействие с хранилищем данных с помощью объектов подключения,
объектов команд и объектов чтения данных.
Автономный уровень (disconnected layer), который будет рассмотрен в главе 22,
позволяет работать с набором объектов DataTable (содержащихся в DataSet), который
представляет на стороне клиента копию внешних данных. При получении DataSet с
помощью соответствующего объекта адаптера данных подключение открывается и
закрывается автоматически. Понятно, что этот подход помогает быстро освобождать
подключения для других вызовов и повышает масштабируемость систем.
756 Часть V. Введение в библиотеки базовых классов .NET
Получив объект Data Set, вызывающий код может просматривать и обрабатывать
данные без затрат на сетевой трафик. А если нужно занести изменения в хранилище
данных, то адаптер данных (вместе с набором операторов SQL) задействуется для
обновления данных — при этом подключение открывается заново для проведения
обновлений в базе, а затем сразу же закрывается.
После выпуска .NET 3.5 SP1 в ADO.NET появилась поддержка новую API, которая
называется Entity Framework (сокращенно EF). Технология EF показывает, что многие
низкоуровневые детали работы с базами данных (например, сложные SQL-запросы) скрыты
от программиста и отрабатываются за него при генерации соответствующего LINQ-
запроса (например, LINQ с Entities). Этот аспект ADO.NET будет рассмотрен в главе 23.
Поставщики данных ADO.NET
В отличие от других API для работы с базами данных, с которыми вам, возможно,
приходилось работать раньше, в ADO.NET нет единого набора типов, которые
взаимодействуют с различными системами управления базами данных (СУБД). Вместо этого в
ADO.NET имеются различные поставщики данных (data provider), каждый из которых
оптимизирован для взаимодействия с конкретной СУБД. Первая выгода этого подхода
состоит в том, что можно запрограммировать особый поставщик данных для доступа
к любым уникальным особенностям конкретной СУБД. Еще одна выгода —
конкретный поставщик данных может напрямую подключиться к механизму соответствующей
СУБД, не пользуясь междууровневым слоем отображения.
В первом приближении поставщик данных можно рассматривать как набор типов,
определенных в данном пространстве имен, который предназначен для взаимодействия
с конкретным источником данных. Однако независимо от используемого поставщика
данных, каждый из них определяет набор классов, обеспечивающих основную
функциональность. В табл. 21.1 приведены некоторые общие основные объекты, их базовые
классы (определенные в пространстве имен System.Data.Common) и основные
интерфейсы (определенные в пространстве имен System.Data), которые они реализуют.
Таблица 21.1. Основные объекты поставщиков данных ADO.NET
Тип объекта Базовый класс оответствующие Назначение
Connection DbConnection IDbConnection Позволяет подключаться к
хранилищу данных и
отключаться от него. Кроме того,
объекты подключения
обеспечивают доступ к
соответствующим объектам транзакций
Command DbCommand IDbCommand Представляет SQL-запрос или
хранимую процедуру. Кроме
того, объекты команд
предоставляют доступ к объекту
чтения данных конкретного
поставщика данных
DataReader DbDataReader IDataReader, Предоставляет доступ кдан-
IDataRecord ным только для чтения в
прямом направлении с помощью
курсора на стороне сервера
Глава 21. ADO.NET, часть I: подключенный уровень 757
Окончание табл. 21.1
Тип объекта
Базовый класс
Соответствующие
интерфейсы
Назначение
DataAdapter . DbDataAdapter
Parameter DbParameter
Transaction DbTransaction
IDataAdapter,
IDbDataAdapter
IDataParameter,
IDbDataParameter
IDbTransaction
Пересылает наборы данных
из хранилища данных к
вызывающему процессу и обратно.
Адаптеры данных содержат
подключение и набор из
четырех внутренних объектов
команд для выборки, вставки,
изменения и удаления
информации в хранилище данных
Представляет именованный
параметр в
параметризованном запросе
Инкапсулирует транзакцию в
базе данных
Конкретные имена этих основных классов различаются у различных поставщиков
(например, SqlConnection, OracleConnection, OdbcConnection и MySqlConnection),
но все эти объекты порождены от одного и того же базового класса (в случае объектов
подключения это DbConnection), который реализует идентичные интерфейсы (вроде
IDbConnection). Поэтому если вы научитесь работать с одним поставщиком данных,
то легко справитесь и с остальными.
На заметку! В AD0.NET термин "объект подключения" на самом деле относится к конкретному
типу, порожденному от DbConnection; объекта подключения "вообще" нет. То же можно
сказать и об "объекте команды", "объекте адаптера данных" и т.д. По соглашению имена
объектов в конкретном поставщике данных имеют префиксы соответствующей СУБД (например,
SqlConnection, OracleConnection, SqlDataReader и т.д.).
На рис. 21.2 подробно показано, что означают поставщики данных в ADO.NET. На
этой диаграмме "Клиентская сборка" может быть практически любым типом
приложения .NET: консольная программа, приложение с использованием Windows-форм,
вебстраница ASP.NET, WCF-служба, кодовая библиотека .NET и т.д.
Кроме типов, показанных на рис. 21.2, поставщики данных содержат и другие типы.
Но эти основные объекты определяют общие свойства всех поставщиков данных.
Поставщики данных ADO.NET от Microsoft
Дистрибутив .NET, поставляемый Microsoft, содержит множество различных
поставщиков данных, например, поставщик в стиле Oracle, SQL Server и OLE DB/ODBC.
В табл. 21.2 перечислены пространства имен и содержащие их сборки для всех
поставщиков данных Microsoft ADO.NET.
Таблица 21.2. Поставщики данных Microsoft AD0.NET
Поставщик данных
Пространство имен
Сборка
OLEDB
Microsoft SQL Server
Microsoft SQL Server Mobile
ODBC
System. Data. OleDb
System. Data.SqlClient
System. Data.SqlServerCe
Sys tern. Data. Odbc
System.Data.dll
System.Data.dll
System. Data.SqlServerCe.dll
System.Data.dll
758 Часть V. Введение в библиотеки базовых классов .NET
Клиентская
сборка
ч^~^~р
•
Поставщик данных платформы .NET
Объект Connection
Транзакция
r\£L
иоъект Command
Коллекция параметров
Объект DataReader
Объект DataAdapter
Команда Select
Команда Insert
Команда Update
Команда Delete
Рис. 21.2. Поставщики данных ADO.NET предоставляют доступ к конкретным СУБД
На заметку! Поставщик данных, выполняющий непосредственное отображение на механизм Jet
(и, следовательно, на Microsoft Access), отсутствует. Если вам нужно взаимодействие с файлом
данных Access, это можно сделать с помощью поставщика данных OLE DB или ODBC.
Поставщик данных OLE DB, состоящий из типов, которые определены в
пространстве имен System.Data.OleDb, обеспечивает доступ к данным, находящимся в любом
хранилище данных, если оно поддерживает классический протокол OLE DB на
основе СОМ. Этот поставщик позволяет взаимодействовать с любой базой данных,
совместимой с OLE DB — для этого потребуется лишь настроить сегмент Provider в строке
подключения.
Однако поставщик OLE DB неявно взаимодействует с различными СОМ-объектами,
что может снизить производительность приложения. В общем-то, поставщик данных
OLE DB нужен только для взаимодействия с какой-нибудь СУБД, для которой нет
специального поставщика данных .NET. Но, учитывая тот факт, что сейчас у любой
мало-мальски известной СУБД доступен для загрузки и специальный поставщик данных ADO.NET,
следует рассматривать System.Data.OleDb как устаревшее пространство имен, которое
вряд ли понадобится в мире .NET 4.0 (если к тому же учесть модель генератора
поставщиков данных, введенную в .NET 2.0, о которой будет рассказано чуть ниже).
На заметку! В одном случае использование типов из System.Data.OleDb все-таки
необходимо, если понадобится взаимодействие с Microsoft SQL Server версии 6.5 или более ранней.
Пространство имен System.Data.SqlClient может взаимодействовать только с Microsoft
SQL Server версии 7.0 или более поздней.
Поставщик данных Microsoft SQL Server предоставляет прямой доступ к хранилищам
данных Microsoft SQL Server и только к хранилищам данных SQL Server (версии 7.0 или
больше). Пространство имен System.Data.SqlClient содержит типы, необходимые для
поставщика SQL Server, и предлагает ту же базовую функциональность, что и постав-
Глава 21. ADO.NET, часть I: подключенный уровень 759
щик OLE DB. Основное различие между ними состоит в том, что поставщик SQL Server
не задействует уровень OLE DB и дает существенный выигрыш в производительности.
А поставщик данных Microsoft SQL Server позволяет получить доступ к уникальным
возможностям этой конкретной СУБД.
Остальные поставщики, предоставляемые Microsoft (System.Data.OracleClient,
System.Data.Odbc и System.Data.SqlClientCe), обеспечивают взаимодействие с
ODBC-подключениями и доступ к SQL Server версии Mobile (которая обычно
применяется на портативных устройствах наподобие Windows Mobile). Типы ODBC, определенные
в пространстве имен System.Data.Odbc, обычно бывают нужны, только если требуется
взаимодействие с СУБД, для которой нет специального поставщика данных .NET. Так
бывает нередко, т.к. ODBC является широко распространенной моделью, которая
предоставляет доступ к целому ряду хранилищ данных.
0 сборке System. Data. OracleClient. dll
В предьщущих версиях платформы .NETимелась сборка System.Data.OracleClient.dll,
которая, как понятно из названия, предоставляла поставщик данных для
взаимодействия с базами данных Oracle. Однако в .NET 4.0 эта сборка помечена как устаревшая, а
в дальнейшем будет считаться не рекомендуемой.
Вы можете подумать, что ADO.NET постепенно концентрируется только на
хранилищах данных от Microsoft, но это не так. Просто Oracle поставляет собственную сборку
.NET, которая разработана на тех же общих принципах, что и поставщики данных,
предоставляемые Microsoft. Если вам понадобится эта сборка, зайдите на страницу
загрузки веб-сайта Oracle по адресу www.oracle.com/technology/tech/windows/odpnet/
index.html.
Получение сторонних поставщиков данных ADO.NET
Кроме поставщиков данных, поставляемых Microsoft (а также собственной
библиотеки .NET для Oracle), существует и множество сторонних поставщиков данных для
различных СУБД, как коммерческих, так и с открытым исходным кодом. Скорее всего,
у вас не возникнет трудностей при получении поставщика данных ADO.NET
непосредственно от разработчика СУБД, но на всякий случай возьмите на заметку следующий
сайт: http://www.sqlsummit.com/DataProv.htm.
Это один из множества сайтов, где собрана документация на все известные
поставщики данных ADO.NET и ссылки на дополнительную информацию и сайты
загрузки. Здесь перечислены различные поставщики ADO.NET: SQLite, IBM DB2, MySQL,
PostgreSQL, TurboDB, Sybase и многие другие.
Однако, несмотря на наличие множества поставщиков данных ADO.NET, в примерах
этой главы будет использоваться поставщик данных Microsoft SQL Server (System.Data.
SqlClient.dll). Этот поставщик позволяет взаимодействовать с Microsoft SQL Server
версий 7.0 и выше, в том числе и с SQL Server Express Edition. Если вы хотите
использовать ADO.NET для работы с другой СУБД, проблем возникнуть не должно, если вы
уясните материал, изложенный ниже.
Дополнительные пространства имен ADO.NET
Кроме пространств имен .NET, которые определяют типы конкретных поставщиков
данных, в библиотеках базовых классов .NET содержатся дополнительные пространства
имен, ориентированные HaADO.NET. Некоторые из них перечислены в табл. 21.3
(сборки и пространства имен, относящиеся к Entity Framework, будут описаны в главе 24).
760 Часть V. Введение в библиотеки базовых классов .NET
Таблица 21.3. Дополнительные пространства имен для работы с ADO.NET
Пространство имен Назначение
Microsoft.SqlServer.Server Содержит типы для работы службы интеграции CLR и SQL
Server 2005
System.Data Определяет основные типы ADO.NET, используемые всеми
поставщиками данных, в том числе общие интерфейсы и
разнообразные типы, представляющие автономный уровень
(например, DataSet и DataTable)
System.Data.Common Содержит типы для общего использования всеми
поставщиками ADO.NET, в том числе и общие абстрактные базовые классы
Systern.Data. Sql Содержит типы, позволяющие обнаружить экземпляры Microsoft
SQL Server, которые установлены в текущей локальной сети
Sys tern. Data. SqlTypes Содержит типы данных, используемые Microsoft SQLServer.
Можно применять и соответствующие типы CLR, но
пространство SqlTypes оптимизировано для работы с SQL Server
(например, если база данных SQL Server содержит целое
значение, его можно представить либо как int, либо как
SqlTypes.Sqllnt32)
Мы не собираемся изучать каждый тип в каждом пространстве имен ADO.NET (эта
задача потребовала бы отдельной книги), однако важно ознакомиться с типами из
пространства имен System.Data.
Типы из пространства имен System.Data
System.Data является "наименьшим общим знаменателем" всех пространств имен
ADO.NET. Если нужен доступ к данным, то приложения ADO.NET невозможно создать
без указания этого пространства имен. Оно содержит общие типы, используемые всеми
поставщиками данных ADO.NET, независимо от непосредственного хранилища данных.
Кроме ряда исключений, специфичных для баз данных (NoNullAllowedException,
RowNotlnTableException и MissingPrimaryKeyException), System.Data содержит
типы, представляющие различные примитивы баз данных (например, таблицы,
строки, столбцы и ограничения), а также общие интерфейсы, реализованные объектами
поставщиков данных. Некоторые из основных типов перечислены в табл. 21.4.
Таблица 21.4. Основные члены пространства имен System.Data
Тип Назначение
Constraint Ограничение для данного объекта DataColumn
DataColumn Один столбец в объекте DataTable
DataRelation Отношение "родительский-дочерний" между двумя объектами DataTable
DataRow Одна строка в объекте DataTable
DataSet Находящийся в памяти кэш данных, который состоит из любого
количества взаимосвязанных объектов DataTable
DataTable Табличный блок данных, находящихся в памяти
DataTableReader Позволяет обращаться с DataTable как с примитивным курсором
(доступ к данным только для чтения и только в прямом направлении)
Глава 21. ADO.NET, часть I: подключенный уровень 761
Окончание табл. 21.4
Тип Назначение
DataView Специализированное представление DataTable для сортировки,
фильтрации, поиска, редактирования и навигации
iDataAdapter Определяет основное поведение объекта адаптера данных
IDataParameter Определяет основное поведение объекта параметра
IDataReader Определяет основное поведение объекта чтения данных
IDbCommand Определяет основное поведение объекта команды
IDbDataAdapter Расширяет IDataAdapter для получения дополнительных
возможностей объекта адаптера данных
iDbTransaction Определяет основное поведение объекта транзакции
Подавляющее большинство классов из System.Data применяется при
программировании для автономного уровня ADO.NET. В следующей главе вы ближе
познакомитесь с DataSet и связанными с ним объектами (например, DataTable, DataRelation и
Data Row), а также научитесь применять их (и соответствующие адаптеры данных) для
представления копий удаленных данных на стороне клиента и работы с ними.
Однако следующей задачей будет знакомство с основными интерфейсами
System.Data на высоком уровне: это поможет лучше разобраться в общей
функциональности любого поставщика данных. Конкретные детали вы будете узнавать на
протяжении данной главы, а пока просто рассмотрим общее поведение произвольного типа
интерфейса.
Роль интерфейса IDbConnection
Тип IDbConnection реализован объектом подключения (connection object)
поставщика данных. Этот интерфейс определяет набор членов, применяемых для настройки
подключения к конкретному хранилищу данных, а, кроме того, позволяет получить
объект транзакции поставщика данных. Вот формальное определение IDbConnection:
public interface IDbConnection : IDisposable
{
string ConnectionString { get; set; }
int ConnectionTimeout { get; }
string Database { get; }
ConnectionState State { get; }
IDbTransaction BeginTransaction () ;
IDbTransaction BeginTransaction(IsolationLevel ll);
void ChangeDatabase(string databaseName);
void Close ();
IDbCommand CreateCommand();
void Open();
}
На заметку! Как и многие другие типы в библиотеках базовых классов .NET, метод Close ()
функционально эквивалентен непосредственному или косвенному вызову метода Dispose() с
помощью области видимости С# (см. главу 8).
762 Часть V. Введение в библиотеки базовых классов .NET
Роль интерфейса IDbTransaction
Перегруженный метод BeginTransactionO, определенный в IDbConnection,
предоставляет доступ к объекту транзакции (transaction object) поставщика. Члены,
определенные в IDbTransaction, позволяют программным образом взаимодействовать с
сеансом транзакций и соответствующим хранилищем данных:
public interface IDbTransaction : IDisposable
{
IDbConnection Connection { get; }
IsolationLevel IsolationLevel { get; }
void Commit();
void Rollback();
}
Роль интерфейса IDbCommand
Интерфейс IDbCommand реализуется объектом команды (command object)
поставщика данных. Как и другие объектные модели доступа к данным, объекты команды
позволяют программно работать с операторами SQL, хранимыми процедурами и
параметризованными запросами. Кроме того, объекты команды обеспечивают доступ к типу чтения
данных поставщика данных с помощью перегруженного метода ExecuteReader():
public interface IDbCommand : IDisposable
{
string CommandText { get; set; }
int CommandTimeout { get; set; }
CommandType CommandType { get; set; }
IDbConnection Connection { get; set; }
IDataParameterCollection Parameters { get; }
IDbTransaction Transaction { get; set; }
UpdateRowSource UpdatedRowSource { get; set; }
void Cancel ();
IDbDataParameter CreateParameter();
int ExecuteNonQuery();
IDataReader ExecuteReader();
IDataReader ExecuteReader(CommandBehavior behavior);
object ExecuteScalar ();
void Prepare ();
Роль интерфейсов IDbDataParameter и IDataParameter
Свойство Parameters интерфейса IDbCommand возвращает строго
типизированную коллекцию, которая реализует IDataParameterCollection. Этот интерфейс
предоставляет доступ к набору классов, совместимых с IDbDataParameter (объекты
параметров):
public interface IDbDataParameter : IDataParameter
{
byte Precision { get; set; }
byte Scale { get; set; }
int Size { get; set; }
}
Интерфейс IDbDataParameter расширяет интерфейс IDataParameter с целью
получения дополнительных возможностей:
Глава 21. ADO.NET, часть I: подключенный уровень 763
public interface IDataParameter
{
DbType DbType { get; set; }
ParameterDirection Direction { get; set; }
bool IsNullable { get; }
string ParameterName { get; set; }
string SourceColumn { get; set; }
DataRowVersion SourceVersion { get; set; }
object Value { get; set; }
}
Как видите, функциональность интерфейсов IDbDataParameter и IDataParameter
позволяет использовать параметры в командах SQL (в том числе и в хранимых
процедурах) с помощью особых объектов параметров ADO.NET вместо жестко закодированных
строковых литералов.
Роль интерфейсов IDbDataAdapter и IDataAdapter
Адаптеры данных (data adapter) используются для выборки и занесения наборов
данных DataSet в конкретное хранилище данных. Поэтому интерфейс IDbDataAdapter
определяет набор свойств, предназначенных для поддержки операторов SQL для
соответствующих операций выборки, вставки, изменения и удаления:
public interface IDbDataAdapter : IDataAdapter
{
IDbCommand DeleteCommand { get; set; }
IDbCommand InsertCommand { get; set; }
IDbCommand SelectCommand { get; set; }
IDbCommand UpdateCommand { get; set; }
}
Кроме этих четырех свойств, адаптер данных ADO.NET заодно получает поведение,
определенное в базовом интерфейсе IDataAdapter. Этот интерфейс определяет
основную функцию типа адаптера данных: возможность пересылать объекты DataSet между
вызывающим процессом и непосредственным хранилищем данных с помощью
методов Fill() и Update(). Кроме того, посредством свойства TableMappings интерфейс
IDataAdapter позволяет отобразить имена столбцов из базы данных на более
понятные отображаемые имена:
public interface IDataAdapter
{
MissingMappingAction MissingMappingAction { get; set; }
MissingSchemaAction MissingSchemaAction { get; set; }
ITableMappingCollection TableMappings { get; }
int Fill(System.Data.DataSet dataSet);
DataTable[] FillSchema(DataSet dataSet, SchemaType schemaType);
IDataParameter[] GetFillParameters ();
int Update(DataSet dataSet);
}
Роль интерфейсов IDataReader и IDataRecord
Еще один интерфейс, о котором важно знать — это IDataReader, который
представляет общие функции, поддерживаемые конкретным объектом чтения данных.
Получив от поставщика данных ADO.NET тип, совместимый с IDataReader, вы
сможете просматривать результирующий набор, но только в прямом направлении и только
просматривать.
764 Часть V. Введение в библиотеки базовых классов .NET
public interface IDataReader : IDisposable, IDataRecord
{
int Depth { get; }
bool IsClosed { get; }
int RecordsAffected { get; }
void Close () ;
DataTable GetSchemaTable ();
bool NextResult () ;
bool Pead () ;
}
И, наконец, IDataReader расширяет интерфейс IDataRecord, в котором определено
значительное количество членов, позволяющих сразу извлекать из потока строго
типизированные значения, а не приводить к нужному типу обобщенный тип System.Object,
который получен от перегруженного метода индексатора из объекта чтения данных.
Ниже приведена часть листинга различных методов GetXXXO, определенных в
интерфейсе IDataRecord (полный листинг см. в документации по .NET Framework 4.0 SDK):
public interface IDataRecord
{
int FieldCount { get; }
object this [ string name ] { get; }
object this [ int i ] { get; }
bool GetBoolean(int l) ;
byte GetByte(int l) ;
char GetChar(int l) ;
DateTime GetDateTime(int l) ;
Decimal GetDecimal (int l) ;
float GetFloatdnt l) ;
short Getlntl6(int l) ;
int Getlnt32(int l) ;
long Getlnt64(int l) ;
bool IsDBNull(int l) ;
1
На заметку! Метод IDataReader.IsDBNull () позволяет программным способом узнать,
установлено ли в конкретном поле значение null, прежде чем выбрать значение из объекта чтения
данных (чтобы не возникло исключение времени выполнения). Вспомните, что С#
поддерживает типы данных nullable (см. главу 4), идеально подходящие для взаимодействия со
столбцами данных, которые могут иметь пустые значения в таблице базы данных.
Абстрагирование поставщиков данных
с помощью интерфейсов
Теперь вы уже должны лучше разбираться в общей функциональности всех
поставщиков данных .NET. И хотя точные имена реализованных типов отличаются в
различных поставщиках данных, в программах они используются однотипно — в этом вся
прелесть полиморфизма на основе интерфейсов. К примеру, если определить метод,
принимающий параметр IDbConnection, то в него можно передать любой объект
подключения ADO.NET:
public static void OpenConnection(IDbConnection en)
{
// Открытие входящего подключения для вызывающего процесса.
en.Open();
}
Глава 21. ADO.NET, часть I: подключенный уровень 765
На заметку! Использование интерфейсов не обязательно. Тот же результат можно получить,
применяя в качестве параметров или возвращаемых значений абстрактные базовые классы
(наподобие DbConnection).
То же относится и к возвращаемым значениям. Рассмотрим, например, следующий
простой проект консольного приложения (с именем MyConnectionFactory), в котором
конкретный объект подключения выбирается в зависимости от одного из значений
специального перечисления. Для диагностики мы просто выводим соответствующий
объект подключения с помощью службы отображения:
using System;
using System.Collections .Generic-
using System.Linq;
using System.Text;
// Необходимо для того, чтобы иметь определения общих интерфейсов
//и различные объекты подключения для тестирования.
using System.Data;
using System.Data.SqlClient;
using System.Data.Odbc;
using System.Data.OleDb;
namespace MyConnectionFactory
{
// Список возможных поставщиков.
enum DataProvider
{ SqlServer, OleDb, Odbc, Oracle, None }
class Program
{
static void Main(string [ ] args)
{
Console.WriteLine ("**** Очень простой генератор подключений *****\пм);
// Получение конкретного подключения.
IDbConnection myCn = GetConnection (DataProvider.SqlServer);
Console.WriteLine("Ваше подключение— {0}", myCn.GetType().Name);
// Открытие, использование и закрытие подключения...
Console.ReadLine();
}
// Этот метод возвращает конкретный объект подключения
//на основе значения перечисления DataProvider.
static IDbConnection GetConnection(DataProvider dp)
{
IDbConnection conn = null;
switch (dp)
{
case DataProvider.SqlServer:
conn = new SqlConnection () ;
break;
case DataProvider.OleDb:
conn = new OleDbConnection ();
break;
case DataProvider.Odbc:
conn = new OdbcConnection ();
break;
}
return conn;
}
}
}
766 Часть V. Введение в библиотеки базовых классов .NET
Преимущество работы с обобщенными интерфейсами из System.Data (т.е. с
абстрактными базовыми классами из System.Data.Common) состоит в том, что при этом
имеется гораздо больше возможностей для создания гибкой кодовой базы, которую
со временем можно развивать. Допустим, что сегодня вы создаете приложение для
Microsoft SQL Server, но что, если спустя несколько месяцев ваша компания перейдет на
Oracle? Если в своем решении вы жестко закодировали типы System.Data.SqlClient,
ориентированные конкретно на MS SQL Server, то понятно, что при изменении СУБД
на сервере придется снова выполнять редактирование, компиляцию и развертывание
сборки.
Повышение гибкости с помощью
конфигурационных файлов приложения
Для повышения гибкости приложений ADO.NET можно использовать клиентский
файл *.conf ig, элемент <appSettings> которого может содержать произвольные пары
ключ/значение. Вспомните: в главе 14 было сказано, что пользовательские данные,
хранящиеся в файле *. con fig, можно получить программным образом с помощью типов
из пространства имен System.Configuration. Предположим, например, что в каком-
нибудь конфигурационном файле задано следующее значение поставщика данных:
<configuration>
<appSettings>
<•-- Ключ и значение для соответствия одному из значений перечисления —>
<add key="provider11 value=llSqlServer"/>
</appSettings>
</configuration>
Теперь можно изменить метод Main(), чтобы программно получить
соответствующий поставщик данных. По сути, при этом создается генератор объектов
подключения, который позволяет изменить поставщик, но без необходимости перекомпиляции
кодовой базы (нужно лишь изменить файл *. с on fig). Ниже приведены необходимые
изменения в Main():
static void Main(string[] args)
{
Console.WriteLine ("**** Очень простой генератор подключений *****\n");
// Чтение ключа поставщика.
string dataProvString = ConfigurationManager.AppSettings["provider"];
// Преобразование строки в перечисление.
DataProvider dp = DataProvider.None;
if (Enum.IsDefined(typeof(DataProvider) , dataProvString) )
dp = (DataProvider)Enum.Parse(typeof(DataProvider), dataProvString);
else
Console.WriteLine ("К сожалению, поставщик отсутствует.");
// Получение конкретного подключения.
IDbConnection myCn = GetConnection(dp);
if(myCn != null)
Console .WriteLine ("Ваше подключение — {О}11, myCn.GetType () .Name) ;
// Открытие, использование и закрытие подключения . . .
Console.ReadLine();
}
Глава 21. ADO.NET, часть I: подключенный уровень 767
На заметку! Для использования типа Conf igurationManager необходимо поместить
ссылку на сборку System.Configuration.dll и импортировать пространство имен System.
Configuration.
Теперь у нас имеется код ADO.NET, позволяющий динамически задать нужное
подключение. Очевидно, здесь есть одна проблема: эта абстракция используется только в
приложении MyConnectionFactory.exe. Но если этот код оформить в библиотеку кода
.NET (например, MyConnectionFactory.dll), то можно будет создавать любое
количество клиентов, которые смогут получать различные объекты подключения с помощью
уровней абстракции.
Однако получение объекта подключения — лишь один аспект работы с ADO.NET
Чтобы разработать хоть чего-то стоящую библиотеку генераторов поставщиков данных,
необходимо учитывать объекты команд, объекты чтения данных, адаптеры данных,
объекты транзакций и другие типы, ориентированные на работу с данными. Создание
такой библиотеки кода не обязательно будет трудным, но все же потребует
существенного объема кодирования и времени.
Начиная с выпуска .NET 2.0, эта возможность встроена непосредственно в
библиотеки базовых классов .NET. Ниже мы рассмотрим этот формальный API-интерфейс, но
вначале понадобится создать собственную базу данных для работы на протяжении как
этой главы, так и многих последующих глав.
Исходный код. Проект MyConnectionFactory доступен в подкаталоге Chapter 21.
Создание базы данных AutoLot
Ниже в этой главе мы будем выполнять запросы к простой тестовой базе данных SQL
Server с именем AutoLot. В продолжение постоянно используемой автомобильной темы,
эта база данных будет содержать три взаимосвязанных таблицы (Inventory, Orders
и Customers), содержащих различные данные о заказах гипотетической компании по
продаже автомобилей.
Ниже предполагается, что у вас имеется копия Microsoft SQL Server G.0 или выше)
или копия Microsoft SQL Server 2008 Express Edition (http://msdn.microsoft.com/
vstudio/express/sql). Этот облегченный сервер баз данных отлично подходит для
наших потребностей: он бесплатен, предоставляет графический интерфейс (SQL Server
Management Tool) для создания и администрирования баз данных и интегрирован с
Visual Studio 2010/Visual C# 2010 Express Edition.
Для демонстрации последнего пункта остаток этого раздела будет посвящен
созданию базы данных AutoLot с помощью Visual Studio 2010. Если вы пользуетесь Visual
С# Express, то сможете выполнить аналогичные действия в окне проводника баз
данных (Database Explorer, открывается с помощью пункта меню ViewO Other Windows
(Вид^Другие окна)).
На заметку! База данных AutoLot будет использоваться до конца книги.
Создание таблицы Inventory
Чтобы приступить к созданию тестовой базы данных, запустите Visual Studio 2010
и откройте Server Explorer через меню View (Просмотр). Затем щелкните правой
кнопкой мыши на узле Data Connections (Подключения к данным) и выберите в контекстном
меню пункт Create New SQL Server Database (Создать новую базу данных SQL Server).
768 Часть V. Введение в библиотеки базовых классов .NET
В открывшемся диалоговом окне подключитесь к SQL Server, установленному на вашей
локальной машине (с именем (local)), и укажите в поле имени базы данных AutoLot
(рис. 21.3). Для наших целей можно оставить утентификацию Windows.
Create New SQL Server Database
! 0 №l*J
Enter information to connect to a SQL Server, then specify the
name of a database to create
Server name
(!ocal)\SQL EXPRESS
fiefresh J
Log on to the server
• Use Windows Authentication
Use SQL Server Authentication
Save my password
New database name
Рис. 21.3. Создание новой базы данных SQL Server 2008 Express с помощью Visual Studio 2010
На заметку! При использовании SQL Server Express можно просто ввести в текстовом поле Server
name (Имя сервера) имя (local)\SQLEXPRESS.
Сейчас база данных AutoLot совершенно пуста и не содержит никаких объектов
(таблиц, хранимых процедур и т.п.). Для добавления новой таблицы щелкните правой
кнопкой мыши на узле Tables (Таблицы) и выберите в контекстном меню пункт Add New
Table (рис. 21.4).
Server Explorer
л ,jj Data Connections
a ijL? andrewpc\sqlexpress.AutoLot.dbo
j_3 Database Diagrams
_i Tables;-
:._J Views' Add New Table
_i Stored Compare Data
_J Functi. fj^ Que|y
■ СЛ Synonj
С ClTypes|^
2| Assemj &
;> f|| Servers
!> |*0 SharePoint Connections
Рис. 21.4. Добавление таблицы Inventory
С помощью редактора таблиц добавьте в таблицу четыре столбца данных: Car ID
(Идентификатор автомобиля), Make (Модель), Color (Цвет) и PetName (Дружественное
имя). У столбца Car ID должно быть установлено свойство Primary Key (первичный
ключ) — для этого щелкните правой кнопкой мыши на строке Car ID и выберите в
контекстном меню пункт Set Primary Key (Установить первичный ключ). Окончательные
параметры таблицы показаны на рис. 21.5. На панели Column Properties (Свойства
столбца) ничего делать не надо, просто запомните типы данных для каждого столбца.
Глава 21. ADO.NET, часть I: подключенный уровень 769
dbo.Tablel: TableUsqlecpress-AutoLot)* X I
Column Name
►tf| CarlD
Make
Color
PetName
Data Type
varcharE0)
varcharE0)
varcharE0)
Allow Nulls
1 D
m
m
i
Column Properties j
(Name)
Allow Nulls
Data Type
Default Value or Binding
CarlD
No
int
G|
Рис. 21.5. Структура таблицы Inventory
Сохраните и закройте новую таблицу; новый объект базы данных должен иметь имя
Inventory. Теперь таблица Inventory должна быть видна под узлом Tables (Таблицы)
в Server Explorer. Щелкните правой кнопкой мыши на ее значке и выберите в
контекстном меню пункт Show Table Data (Просмотр данных таблицы). Введите информацию
о нескольких новых автомобилях по своему усмотрению (чтобы было интереснее, пусть
у некоторых автомобилей совпадают цвета и модели). Один из возможных вариантов
списка товаров приведен на рис. 21.6.
Inventory. Query(a...sqlexpress.AutoLot)
CarlD Make
1000 BMW
1001 BMW
904 VW
83 Ford
107 Ford
678 Yugo
1992 Saab
* \null null
И i
3 of 7 ► И ►
с
Color
Black
Tan
Black
Rust
Red
Green
Pink
NULL
ш
5Н5ЯЗНЕИ ИЗ
PetName
Bimmer
Daisy
Hank
Rusty
Snake
Clunker
Pinkey
NULL
Рис. 21.6. Заполнение таблицы Inventory
Создание хранимой процедуры GetPetNameQ
Ниже в данной главе будет показано, как вызывать хранимые процедуры в ADO.NET.
Возможно, вы уже знаете, что хранимые процедуры — это подпрограммы, хранимые
непосредственно в базе данных; обычно они работают с данными таблиц и возвращают
какое-то значение. Мы добавим в базу данных одну хранимую процедуру, которая по
идентификатору автомобиля будет возвращать его дружественное имя. Для этого
щелкните правой кнопкой мыши на узле Stored Procedures (Хранимые процедуры) базы
данных AutoLot в Server Explorer и выберите в контекстном меню пункт Add New Stored
Procedure (Добавить новую хранимую процедуру). В появившемся окне редактора
введите следующий текст:
770 Часть V. Введение в библиотеки базовых классов .NET
Server Explorer
(Jjf Data Connections
л У^ andrewpc\sqlexpress.AutoLot.dbo
0 C3 Database Diagrams
л CM Tables
c> 13 Inventory
£> Q| Views
л Cj Stored Procedures
3 ©carlD
J& ©petName
!> Qa Functions
о CM Synonyms
> CM Types
j> CM Assemblies
Щ Servers
jfti SharePomt Connections
Рис. 21.7. Хранимая процедура
GetPetName
CREATE PROCEDURE GetPetName
@carID int,
@petName char A0) output
AS
SELECT @petName = PetName from Inventory-
where CarlD = @carID
При сохранении этой процедуре
автоматически будет присвоено имя GetPetName, взятое из
оператора CREATE PROCEDURE (учтите, что при
первом сохранении Visual Studio 2010
автоматически изменяет имя SQL-сценария на "ALTER
PROCEDURE..."). После этого новая хранимая
процедура будет видна в Server Explorer (рис. 21.7).
На заметку! Хранимые процедуры не обязательно
должны возвращать данные через выходные
параметры, как это сделано здесь; однако это пригодится,
когда речь пойдет о свойстве Direction объектов
SqlParameter ниже в данной главе.
Создание таблиц Customers и Orders
В нашей тестовой базе данных должны быть еще две таблицы: Customers (Клиенты)
и Orders (Заказы). Таблица Customers будет содержать список клиентов и состоять
из трех столбцов: CustID (Идентификатор клиента; должен быть первичным ключом),
FirstName (Имя) и LastName (Фамилия). Повторите шаги, которые были выполнены
для создания таблицы Inventory, и создайте таблицу Customers, пользуясь схемой,
приведенной на рис. 21.8.
dbo.Tablei Table(...sqlexpressAutoLot)*
Column Name Data Type
Щ CustiD int
FirstName varcharE0)
LastName varcharE0)
Allow Nulls
□
m
В
Column Properties
л (General)
(Name)
Allow Nulls
Data Type
Default Value or Binding
ТэЫе Designer
CustID
No
1
Щ
(General)
Рис. 21.8. Структура таблицы Customers
После сохранения этой таблицы добавьте в нее несколько записей (рис. 21.9).
Последняя наша таблица — Orders — предназначена для связи клиентов и
интересующих их автомобилей. Для этого выполняется отображение значений OrderlD на
CarlD/CustID. Ее структура показана на рис. 21.10 (здесь OrderlD также является
первичным ключом).
Глава 21. ADO.NET, часть I: подключенный уровень 771
Customers Query(
CustID
> '1
2
3
4
* \null
И < |l
i...qle
cf 4
xpress-AutoLot)
FirstName
Dave
Matt
Steve
Pat
NULL
► и ►
X
»'
лшш^^шв
LastName
Brenner
Walton
Hagen
Walton
NULL
Рис. 21.9. Заполнение таблицы Customers
аЬо.ТаЫеЗ: TableUsqlexpressAutoLot)* X |
Column Name Data Type
Wj OrdedD int
CustID int
CarlD int
В
П
о
Column Properties
.|Sl>t;jj.,
л (General)
(Name)
Allow Nulls
Data Type
(General)
I
OrderlD
No
int
'
Рис. 21.10. Структура таблицы Orders
Теперь добавьте в таблицу Orders данные. Выберите для каждого значения CustID
уникальное значение CarlD (предположим, что значения OrderlD начинаются с 1000
(рис. 21.11)).
Orders: Query(andr...sqlexpress.AutoLot) X
OrderlD
► 1000
1001
jl002
|l003
* \NULL
и < ■ i
of 4
CustID
1
2
3
4
NULL
► N ► ®
CarlD
1000
678
904
1992
NULL
:
!
Рис. 21.11. Заполнение таблицы Orders
Например, в соответствии с информацией, приведенной на рисунках, видно, что
Дэйв Бреннер (Dave Brenner, CustID = 1) мечтает о черном BMW (CarlD = 1000), а Пэт
Уолтон (Pat Walton, CustID = 4) приглянулся розовый Saab (CarlD = 1992).
772 Часть V. Введение в библиотеки базовых классов .NET
Визуальное создание отношений между таблицами
И, наконец, между таблицами Customers, Orders и Inventory нужно установить
отношения "родительский-дочерний". В Visual Studio 2010 это выполняется очень
просто, т.к. она позволяет вставить новую диаграмму базы данных на этапе
проектирования. Для этого откройте Server Explorer, щелкните правой кнопкой мыши на узле
Database Diagrams базы AutoLot и выберите пункт контекстного меню Add New Diagram
(Добавить новую диаграмму). Откроется диалоговое окно, в котором можно выбирать
таблицы и добавлять их в диаграмму. Выберите все таблицы из базы данных AutoLot
(рис. 21.12).
Рис. 21.12. Выбор таблиц для диаграммы
Чтобы начать устанавливать отношения между таблицами, щелкните на ключе
CardID таблицы Inventory, а затем (не отпуская кнопку мыши) перетащите его на поле
CardID таблицы Orders. Когда вы отпустите кнопку, появится диалоговое окно;
согласитесь со всеми предложенными в нем значениями по умолчанию.
Теперь повторите те же действия для отображения ключа CustID таблицы Customers
на поле CustID таблицы Orders. После этого вы увидите диалоговое окно классов,
показанное на рис. 21.13 (было включено отображение отношений между таблицами за счет
щелчка правой кнопкой мыши на конструкторе и выбора в контекстном меню пункта
Show Relationship Labels (Показывать метки взаимосвязи)).
Вот и все, база AutoLot полностью готова. Конечно, это лишь бледное подобие
реальных корпоративных баз данных, но она отлично послужит нам до конца книги. И
теперь, имея тестовую базу, можно начать подробно разбираться в модели генератора
поставщиков данных ADO.NET.
Модель генератора поставщиков данных AD0.NET
Генератор поставщиков данных .NET позволяет создать единую кодовую базу с
помощью обобщенных типов доступа к данным. Более того, посредством конфигурационных
файлов приложения (и подэлемента <connectionStrings>) можно получить
поставщики и строки подключения без необходимости в повторной компиляции или
развертывания сборки, в которой используются API ADO.NET.
Глава 21. ADO.NET, часть I: подключенный уровень 773
Inventory *
FK_Orders_InventoTY
ksQ ~ OCJ
^ СагЮ
Make
Color
PetName
Orders *
9 OrderiD
CustID
CarlD
FK_Orders_CustDmers
Customers *
9 CustID
FirstName
LastName
UJ
j№
Рис. 21.13. Отношения между таблицами Orders, Inventory и Customers
Чтобы разобраться в реализации генератора поставщиков данных, вспомните из
табл. 21.1, что все классы в поставщике данных порождены от одних и тех же базовых
классов, определенных в пространстве имен System.Data.Common:
• DbCommand — абстрактный базовый класс для всех объектов команд;
• Disconnection — абстрактный базовый класс для всех объектов.подключений;
• DbDataAdapter — абстрактный базовый класс для всех объектов адаптеров
данных;
• DbDataReader — абстрактный базовый класс для всех объектов чтения данных;
• DbParameter — абстрактный базовый класс для всех объектов параметров;
• Db Transaction — абстрактный базовый класс для всех объектов транзакций,
Все поставщики данных, разработанные Microsoft, содержат класс, порожденный от
System.Data.Common.DbProviderFactory. В этом базовом классе определен ряд
методов, которые выбирают объекты данных, характерные для конкретных поставщиков.
Вот фрагмент с членами DbProviderFactory:
DbProviderFactory
public virtual DbCommand CreateCommand();
public virtual DbCommandBuilder CreateCommandBuilder();
public virtual DbConnection CreateConnection();
public virtual DbConnectionStringBuilder CreateConnectionStringBuilder();
public virtual DbDataAdapter CreateDataAdapter();
public virtual DbDataSourceEnumerator CreateDataSourceEnumerator();
public virtual DbParameter CreateParameter() ;
}
Для получения типа, порожденного от DbProviderFactory, непосредственно для
вашего поставщика данных в пространстве имен System.Data.Common имеется класс
DbProviderFactories. С помощью метода GetFactoryO можно получить конкретный
объект DbProviderFactory для указанного поставщика данных. Для этого нужно
указать строковое имя, которое представляет пространство имен .NET, содержащее
функциональность поставщика:
774 Часть V. Введение в библиотеки базовых классов .NET
static void Main(string [ ] args)
{
// Получение генератора для поставщика данных SQL.
DbProviderFactory sqlFactory =
DbProviderFactories.GetFactory("System.Data.SqlClient");
}
Разумеется, генератор можно получить не с помощью жестко закодированного
строкового литерала, а, например, прочитать эту информацию из клиентского файла
*. con fig (приблизительно также, как в предыдущем примере с MyConnectionFactory).
Вскоре вы увидите, как это сделать, а пока после получения генератора для поставщика
данных можно получить связанные с ним объекты данных (например, подключения,
команды и объекты чтения данных).
На заметку! Для всех практических целей можно рассматривать аргумент, передаваемый в
DbProviderFactories.GetFactory (), как имя пространства имен .NET для поставщика
данных. В реальности это строковое значение используется в значении machine.config для
динамической загрузки нужной библиотеки из глобального кэша сборок.
Полный пример генератора поставщиков данных
Для примера мы создадим новое консольное С#-приложение с именем
DataProviderFactory, которое выводит список всех автомобилей из базы данных
AutoLot. Поскольку это первый пример, мы жестко закодируем логику доступа к
данным непосредственно в сборке DataProviderFactory.exe (чтобы пока не усложнять
программирование). Но когда мы начнем разбираться в деталях модели
программирования ADO.NET, мы изолируем логику данных в специальную кодовую библиотеку .NET,
которая будет использоваться на протяжении всего оставшегося текста.
Вначале добавьте ссылку на сборку System.Configuration.dll и импортируйте
пространство имен System.Configuration. Затем добавьте файл Арр.config в
текущий проект и определите пустой элемент <appSettings>. Добавьте новый ключ по
имени provider, который отображает в пространство имен нужное имя поставщика
данных (System.Data.SqlClient). Кроме того, определите строку подключения,
которая описывает подключение к базе данных AutoLot (с помощью локального экземпляра
SQL Server Express):
<?xml version="l.0м encoding=Mutf-8M ?>
<configuration>
<appSettings>
<!-- Поставщик -->
<add key="provider11 value="System. Data. SqlClient" />
<!-- Строка подключения -->
<add key=,,cnStr" value="Data Source= (local)\SQLEXPRESS;
Initial Catalog=AutoLot; Integrated Secunty=True"/>
</appSettings>
</configuration>
На заметку! Чуть ниже мы рассмотрим строки подключения подробнее. А пока учтите, что если
выбрать в Server Explorer значок базы данных AutoLot, то можно скопировать и вставить
правильную строку подключения из свойства Connection String (Строка подключения) в окне
Properties (Свойства) Visual Studio 2010.
Глава 21. ADO.NET, часть I: подключенный уровень 775
Теперь у вас есть корректный файл *.config, и вы можете прочитать значения
provider и cnStr с помощью индексатора ConfigurationManager.AppSettings.
Значение provider нужно передать в метод DbProviderFactories.GetFactoryO,
чтобы получить тип генератора для необходимого поставщика данных. Значение cnStr
будет использовано для установки свойства ConnectionString в типе, порожденном
от DbConnection.
Если вы импортировали пространства имен System.Data и System.Data.Common,
метод Main () можно изменить следующим образом:
static void Main(string [ ] args)
{
Console.WriteLine
("***** Работа с генераторами поставщиков данных *****\п");
// Получение строки подключения и поставщика из *.config.
string dp =
ConfigurationManager.AppSettings["provider"];
string cnStr =
ConfigurationManager.AppSettings["cnStr"];
// Получение генератора поставщика.
DbProviderFactory df = DbProviderFactories.GetFactory(dp);
// Получение объекта подключения.
using (DbConnection en = df.CreateConnection())
{
Console.WriteLine("Ваш объект подключения: {0}", en.GetType().Name);
en.ConnectionString = cnStr;
en.Open () ;
// Создание объекта команды.
DbCommand cmd = df.CreateCommand();
Console.WriteLine ("Ваш объект команды: {0}", cmd.GetType() .Name);
cmd.Connection = en;
cmd.CommandText = "Select * From Inventory";
// Вывод данных с помощью объекта чтения данных.
using (DbDataReader dr = cmd.ExecuteReader ())
{
Console.WriteLine("Ваш объект чтения данных: {0}", dr.GetType () .Name);
Console.WriteLine (M\n***** Текущее содержимое Inventory *****");
while (dr.Read())
Console.WriteLine ("-> Автомобиль N'{0} — {1} . " ,
dr["CarlD"], dr["Make"] .ToString ());
}
}
Console.ReadLine();
}
Здесь в целях диагностики с помощью службы рефлексии выводятся полностью
определенные имена соответствующих объектов подключения, команды и чтения данных.
Если запустить это приложение, то на консоль будут выведены текущие данные из
таблицы Inventory базы данных AutoLot:
***** Работа с генераторами поставщиков данных *****
Ваш объект подключения: SqlConnection
Ваш объект команды: SqlCommand
Ваш объект чтения данных: SqlDataReader
776 Часть V. Введение в библиотеки базовых классов .NET
***** Текущее содержимое Inventory *****
-> Автомобиль N'83 — Ford.
-> Автомобиль N'107 — Ford.
-> Автомобиль N'678 — Yugo.
-> Автомобиль N'904 - VW.
-> Автомобиль N'1000 - BMW.
-> Автомобиль N'1001 - BMW.
-> Автомобиль N'1992 - Saab.
Теперь укажите в файле *.config в качестве поставщика данных System.Data.
OleDb (и измените строку подключения и сегмент Provider):
<configuration>
<appSettings>
<!-- Поставщик -->
<add key="provider11 value="System. Data .OleDb" />
<!-- Строка подключения -->
<add key="cnStr11 value=
"Provider=SQLOLEDB;Data Source=(local)\SQLEXPRESS;
Integrated Security=SSPI;Initial Catalog=AutoLot"/>
</appSettings>
</configuration>
Вы обнаружите, что неявно были задействованы типы System.Data.OleDb:
***** Работа с генераторами поставщиков данных *****
Ваш объект подключения: OleDbConnection
Ваш объект команды: OleDbCommand
Ваш объект чтения данных: OleDbDataReader
***** Текущее содержимое Inventory *****
-> Автомобиль N'83 — Ford.
-> Автомобиль N'107 — Ford.
-> Автомобиль N'678 — Yugo.
-> Автомобиль N'904 — VW.
-> Автомобиль N'1000 - BMW.
-> Автомобиль N'1001 - BMW.
-> Автомобиль N'1992 - Saab.
Конечно, при недостатке опыта работы с ADO.NET вы можете не быть уверенными,
что именно делают объекты подключения, команды и чтения данных. Пока не
вдавайтесь в детали (ведь в этой главе еще много страниц!) и просто уясните, что модель
генератора поставщиков данных ADO.NET позволяет создать единую кодовую базу, которая
может использовать различные поставщики данных в декларативной манере.
Возможные трудности с моделью генератора поставщиков
Это действительно очень мощная модель, но все же нужно проверить, что в кодовой
базе используются только типы и методы, общие для всех поставщиков как потомков
абстрактных базовых классов. Поэтому при разработке кодовой базы следует
ограничиться членами из DbConnection, DbCommand и других типов из пространства имен
System. Data. Common.
Но такой обобщенный подход не позволит непосредственно задействовать
дополнительные возможности конкретной СУБД. Если все же потребуются вызовы
специфических членов конкретного поставщика (например, SqlConnection), то это можно сделать
с помощью явного преобразования типа, как в данном примере:
Глава 21. ADO.NET, часть I: подключенный уровень 777
using (DbConnection en = df.CreateConnection())
{
Console.WriteLine("Ваш объект подключения: {0}", en.GetType () .Name);
en.ConnectionString = cnStr;
en.Open();
if (en is SqlConnection)
{
// Вывод используемой версии SQL Server.
Console.WriteLine(((SqlConnection)en).ServerVersion);
}
}
Однако при этом сопровождение кодовой базы несколько затруднится (а гибкость
снизится), поскольку придется еще добавить ряд проверок времени выполнения. И все же если
понадобится создать библиотеки доступа к данным наиболее гибким способом, то модель
генератора поставщиков данных предоставляет для этого замечательный механизм.
Элемент <connectionStrings>
Пока наша строка подключения находится в элементе <appSettings>
файла *.config. В конфигурационных файлах приложения может быть определен
элемент <connectionStrings>. В этом элементе можно задать любое количество пар
имя/значение, которые программа может прочитать в память с помощью
индексатора ConfigurationManager.ConnectionStrings. Одним из преимуществ
данного подхода (по сравнению с использованием элемента <appSettings> и индексатора
ConfigurationManager.AppSettings) является то, что при этом можно однотипным
образом определить несколько строк подключения для одного приложения.
Чтобы увидеть все это в действии, модифицируйте текущий файл App.conf ig
следующим образом (обратите внимание, что каждая строка подключения описывается с помощью
атрибутов name и ConnectionString, а не атрибутов key и value, как в <appSettings>):
<configuration>
<appSettings>
<!— Поставщик —>
<add key=MproviderM value="System.Data.SqlClient" />
</appSettings>
<!— Строки подключения -->
<connectionStrings>
<add name =llAutoLotSqlProviderM ConnectionString =
"Data Source=(local)\SQLEXPRESS;
Integrated Security=SSPI;Initial Catalog=AutoLot"/>
<add name ="AutoLot01eDbProvider11 ConnectionString =
"Provider=SQLOLEDB;Data Source=(local)\SQLEXPRESS;
Integrated Security=SSPI;Initial Catalog=AutoLot"/>
</connectionStrings>
</configuration>
Теперь можно изменить метод Main():
static void Main(string[] args)
{
Console.WriteLine("***** Работа с генераторами поставщиков данных *****\п");
string dp = ConfigurationManager.AppSettings["provider"];
string cnStr =
ConfigurationManager.ConnectionStrings["AutoLotSqlProvider"].ConnectionString;
}
778 Часть V. Введение в библиотеки базовых классов .NET
Мы получили приложение, которое может выводить содержимое таблицы Inventory
базы данных AutoLot, используя нейтральную кодовую базу. Вынесение имени
поставщика и строки подключения во внешний файл *. с on fig позволяет модели
генератора поставщиков данных самостоятельно динамически загружать нужный поставщик.
Итак, первый пример закончен, и теперь можно углубиться в детали работы с
подключенным уровнем ADO.NET.
На заметку! Теперь вы уже оценили роль генераторов поставщиков данных в ADO.NET, и в
последующих примерах данной книги мы будем явно использовать типы из пространства имен System.
Data.SqlClient, чтобы не отвлекаться от текущих задач. Если вы пользуетесь другой СУБД
(например, Oracle), то вам надо будет соответствующим образом изменить кодовую базу.
Исходный код. Проект DataProviderFactory доступен в подкаталоге Chapter 21.
Подключенный уровень ADO.NET
Вспомните, что подключенный уровень ADO.NET позволяет взаимодействовать с
базой данных с помощью объектов подключения, чтения данных и команд конкретного
поставщика данных. Вы уже использовали эти объекты в предыдущем приложении
DataProviderFactory, но все же мы рассмотрим весь процесс еще раз на
расширенном примере. При необходимости подключиться к базе данных и прочитать записи с
помощью объект чтения данных нужно выполнить следующие шаги.
1. Создайте, настройте и откройте объект подключения.
2. Создайте и настройте объект команды, указав объект подключения в аргументе
конструктора или через свойство Connection.
3. Вызовите метод ExecuteReader () настроенного объекта команды.
4. Обрабатывайте каждую запись с помощью метода Read() объекта чтения данных.
А теперь к делу. Создайте новое консольное приложение по имени AutoLotDataReader
и импортируйте пространства имен System.Data и System.Data.SqlClient. Вот
полный код метода Main(), который будет проанализирован ниже:
class Program
{
static void Main(string [] args)
{
Console.WriteLine ("***** Работа с объектами чтения данных *****\п");
// Создание открытого подключения.
using(SqlConnection en = new SqlConnection())
{
en.ConnectionString =
@"Data Source=(local)\SQLEXPRESS;Integrated Security=SSPI;" +
"Initial Catalog=AutoLot";
en.Open();
// Создание объекта команды SQL.
string strSQL = "Select * From Inventory";
SqlCommand myCommand = new SqlCommand(strSQL, en) ;
// Получение объекта чтения данных с помощью ExecuteReader().
using(SqlDataReader myDataReader = myCommand.ExecuteReader())
{
Глава 21. ADO.NET, часть I: подключенный уровень 779
// Просмотр всех результатов.
while (myDataReader.Read())
{
Console.WriteLine("-> Make: {0}, PetName: {1}, Color: {2}.",
myDataReader ["Make"] .ToStringO .Trim() ,
myDataReader["PetName"].ToString().Trim(),
myDataReader["Color"].ToString().Trim());
}
}
}
Console.ReadLine();
}
}
Работа с объектами подключения
Первое, что нужно сделать при работе с поставщиком данных — это установить
сеанс с источником данных с помощью объекта подключения (порожденного, как вы
помните, от DbConnection). У объектов подключения .NET имеется форматированная
строка подключения, которая содержит ряд пар имя/значение, разделенных точками
с запятой. Эта информация содержит имя машины, к которой нужно подключиться,
необходимые параметры безопасности, имя базы данных на этой машине и другую
информацию, зависящую от поставщика.
Из приведенного выше кода можно понять, что имя Initial Catalog относится
к базе данных, с которой нужно установить сеанс. Имя Data Source определяет имя
машины, на которой расположена база данных. Элемент (local) позволяет указать
текущую локальную машину (независимо от конкретного имени этой машины), а элемент
\SQLEXPRESS сообщает поставщику SQL Server, что вы подключаетесь к стандартной
инсталляции SQL Server Express (если вы создали AutoLot с помощью полной версии
SQL Server 2005 или более ранней, укажите Data Source= (local)).
Кроме того, можно указать любое количество элементов, которые задают
полномочия безопасности. В нашем примере имени Integrated Security присвоено значение
SSPI (что эквивалентно true), которое использует для аутентификации пользователя
текущие полномочия учетной записи Windows.
На заметку! Назначение каждой пары имя/значение для вашей СУБД можно узнать в документации
по .NET Framework 4.0 SDK, в описании свойства ConnectionString объекта подключения
для вашего поставщика данных.
При наличии строки подключения вызов Ореп() устанавливает соединение с СУБД.
В дополнение к членам ConnectionString, Open() и Close () объект подключения
содержит ряд членов, которые позволяют настроить дополнительные параметры
подключения, например, время тайм-аута и информацию, относящуюся к транзакциям.
В табл. 21.5 приведены некоторые члены базового класса DbConnection.
Таблица 21.5. Члены типа DbConnection
Член Назначение
BeginTransaction () Используется для начала транзакции базы данных
ChangeDatabaseO Изменяет базу данных для открытого подключения
ConnectionTimeout Свойство только для чтения. Возвращает время ожидания при
установке подключения, после которого ожидание прекращается и
выдается сообщение об ошибке (по умолчанию 15 секунд). Для
изменения этого времени нужно изменить в строке подключения сегмент
Connect Timeout (например, Connect Timeout=30)
780 Часть V. Введение в библиотеки базовых классов .NET
Окончание табл. 21.5
Член Назначение
Database Свойство только для чтения. Содержит имя базы данных, с которой
связан объект подключения
DataSource Свойство только для чтения. Содержит местоположение базы данных,
с которой связан объект подключения
GetSchema () Этот метод возвращает объект DataTable, содержащий
информацию схемы из источника данных
State Свойство только для чтения. Содержит текущее состояние
подключения в виде одного из значений перечисления ConnectionState
Свойства типа DbConnection предназначены в основном только для чтения и
поэтому нужны, если требуется получить характеристики подключения во время выполнения.
Если понадобится изменить стандартные значения, необходимо будет изменить саму
строку подключения. Например, можно изменить время тайм-аута с 15 на 30 секунд:
static void Main(string[] args)
{
Console.WriteLine ("***** Работа с объектами чтения данных *****\п");
using(SqlConnection en = new SqlConnection ())
{
en.ConnectionString =
@"Data Source=(local)\SQLEXPRESS;" +
"Integrated Security=SSPI;Initial Catalog=AutoLot;Connect Timeout=30";
en.Open();
// Новая вспомогательная функция (см. ниже).
ShowConnectionStatus (en);
}
}
В этом коде объект подключения передается в качестве параметра новому
статическому вспомогательному методу ShowConnectionStatus () из класса Program, который
реализован следующим образом:
static void ShowConnectionStatus(DbConnection en)
{
// Вывод различных сведений о текущем объекте подключения.
Console.WriteLine("***** Информация о подключении *****");
Console.WriteLine("Местоположение базы данных: {0}", en.DataSource);
Console.WriteLine("Имя базы данных: {0}", en.Database);
Console.WriteLine("Тайм-аут: {0}", en.ConnectionTimeout);
Console.WriteLine("Состояние подключения: {0}\n", en.State.ToString());
}
Все эти свойства понятны без объяснений, за исключением разве что свойства State.
В принципе ему можно присвоить любое значение из перечисления ConnectionState:
public enum ConnectionState
{
Broken, Closed,
Connecting, Executing,
Fetching, Open
}
Глава 21. ADO.NET, часть I: подключенный уровень 781
Тем не менее, допустимыми считаются только значения ConnectionState.Open и
ConnectionState.Closed (остальные значения зарезервированы для будущего
использования). И учтите, что подключение можно закрыть без проблем, если его состояние
равно ConnectionState.Closed.
Работа с объектами ConnectionStringBuilder
Программная работа со строками подключения может оказаться несколько
затруднительной, поскольку они часто представлены в виде строковых литералов, которые
трудно обрабатывать и контролировать на наличие ошибок. Поставщики данных ADO.NET,
разработанные Microsoft, поддерживают объекты построителей строк подключения
(connection string builder object), которые позволяют устанавливать пары имя/значение
с помощью строго типизированных свойств. Рассмотрим следующую модификацию
нашего метода Main ():
static void Main(string [ ] args)
{
Console.WriteLine ("***** Работа с объектами чтения данных *****\п");
// Создание строки подключения с помощью объекта построителя.
SqlConnectionStringBuilder cnStrBuilder =
new SqlConnectionStringBuilder();
cnStrBuilder.InitialCatalog = "AutoLot";
cnStrBuilder.DataSource = @"(local)\SQLEXPRESS";
cnStrBuilder.ConnectTimeout = 30;
cnStrBuilder.IntegratedSecurity = true;
using(SqlConnection en = new SqlConnection ())
{
cn.ConnectionString = cnStrBuilder.ConnectionString;
en.Open ();
ShowConnectionStatus(en);
}
Console.ReadLine();
}
В этом варианте создается экземпляр SqlConnectionStringBuilder,
устанавливаются его свойства, и выбирается внутренняя строка из свойства ConnectionString.
Здесь использован стандартный конструктор типа. При этом можно также создать
экземпляр объекта построителя для строки подключения поставщика данных, передав в
качестве отправной точки существующую строку подключения (это может оказаться
удобным при динамическом чтении значений из файла App.config). После
наполнения объекта начальными строковыми данными можно изменить отдельные пары имя/
значение с помощью соответствующих свойств, как в следующем примере:
static void Main(string[] args)
{
Console.WriteLine ("***** Работа с объектами чтения данных *****\п");
// Допустим, мы получили значение cnStr из файла *.config.
string cnStr = @"Data Source=(local)\SQLEXPRESS;" +
"Integrated Security=SSPI;Initial Catalog=AutoLot";
SqlConnectionStringBuilder cnStrBuilder =
new SqlConnectionStringBuilder(cnStr);
// Изменение значения тайм-аута.
cnStrBuilder.ConnectTimeout = 5;
782 Часть V. Введение в библиотеки базовых классов .NET
Работа с объектами команд
Теперь, когда мы разобрались с ролью объекта подключения, можно рассмотреть, как
отправлять SQL-запросы в базу данных. Тип SqlCommand (порожденный от DbCommand)
представляет собой объектно-ориентированное представление SQL-запроса, имени
таблицы или хранимой процедуры. Тип команды указывается свойством CommandType,
которое принимает значения из перечисления CommandType:
public enum CommandType
{
StoredProcedure,
TableDirect,
Text // Значение по умолчанию.
}
При создании объекта команды можно сразу задать SQL-запрос, передав его с
помощью параметра конструктора или непосредственно свойства CommandText. Кроме того,
при создании объекта команды необходимо указать подключение, которое будет в нем
применяться. Это тоже можно сделать либо через параметр конструктора, либо с
помощью свойства Connection, как в следующем фрагменте кода:
// Создание объекта команды с помощью аргументов конструктора.
string strSQL = "Select * From Inventory";
SqlCommand myCommand = new SqlCommand(strSQL, en);
// Создание еще одного объекта команды с помощью свойств.
SqlCommand testCommand = new SqlCommand();
testCommand.Connection = en;
testCommand.CommandText = strSQL;
Учтите, что на этом этапе SQL-запрос еще не отправляется в базу данных AutoLot,
здесь лишь подготавливается состояние объекта команды для дальнейшего
использования. В табл. 21.6 описаны некоторые дополнительные член типа DbCommand.
Таблица 21.6. Члены типа DbCommand
Член Назначение
CommandTimeout Выдает или устанавливает время ожидания при выполнении команды до
прекращения попытки и генерации ошибки. По умолчанию равно 30 секунд
Connection Выдает или устанавливает объект DbConnection, используемый
данным DbCommand
Parameters Выдает коллекцию типов DbParameter, используемых для
параметризованного запроса
Cancel () Отменяет выполнение команды
ExecuteReader () Выполняет SQL-запрос и возвращает объект DbDataReader
поставщика данных, позволяющий доступ к результату запроса только для
чтения в прямом направлении
ExecuteNonQuery () Выполняет не запросный SQL (например, вставка, обновление,
удаление или создание таблицы)
ExecuteScalarO Облегченная версия метода ExecuteReader (), созданная специально
для одноэлементных запросов (наподобие получения количества записей)
Prepare () Создает подготовленную (или скомпилированную) версию команды в
источнике данных. Подготовленные запросы выполняются несколько быстрее,
и их имеет смысл использовать, если требуется многократное выполнение
одного и того же запроса (обычно каждый раз с различными параметрами)
Глава 21. ADO.NET, часть I: подключенный уровень 783
Работа с объектами чтения данных
После установки подключения и создания объекта команды можно отправлять
запросы к источнику данных. Это делается несколькими способами. Самым простым
и быстрым способом получения информации из хранилища данных является тип
DbDataReader (реализующий интерфейс IDataReader). Вспомните, что объекты
чтения данных представляют поток данных, допускающий только чтение в прямом
направлении, и возвращают каждый раз по одной записи. Поэтому объекты чтения данных
применяются только для выдачи SQL-запросов на выборку информации из хранилища
данных.
Объекты чтения данных удобны, если нужно быстро просмотреть большой объем
данных без необходимости представлять их в памяти. Например, если запросить из
таблицы 20 000 записей, чтобы сохранить их в текстовом файле, то хранение этой
информации BDataSet будет излишней затратой памяти (ведь DataSet полностью хранит
результат запроса в памяти).
Гораздо лучше создать объект чтения данных, который будет максимально быстро
выдавать по одной записи. Учтите, что объекты чтения данных (в отличие от объектов
адаптеров данных, которые мы рассмотрим ниже) поддерживают открытое
подключение к источнику данных, пока сеанс не будет явно закрыт.
Объект чтения данных можно получить из объекта команды с помощью вызова
ExecuteReader(). Объект чтения данных представляет текущую запись, прочитанную
из базы данных. В нем имеется метод индексатора (например, синтаксис [] в С#),
который обеспечивает доступ к столбцам текущей записи. Доступ к конкретному столбцу
возможен либо по имени, либо по целочисленному индексу, начиная с нуля.
В приведенном ниже примере использования объекта чтения данных применяется
метод Read() — чтобы определить, когда достигнут конец данных (при этом он
возвращает значение false). Для каждой прочитанной из базы данных записи с помощью
индексатора типа выводится модель, дружественное имя и цвет каждого автомобиля.
Обратите внимание, что сразу после завершения обработки записей вызывается метод
Close(), чтобы освободить объект подключения:
static void Main(string[] args)
{
// Получение объекта чтения данных с помощью ExecuteReader () .
using (SqlDataReader myDataReader = myCommand.ExecuteReader())
{
// Цикл по результатам.
while (myDataReader.Read())
{
Console.WriteLine ("-> Make: {0}, PetName: {1}, Color: {2}.",
myDataReader["Make"] .ToString () .Trim(),
myDataReader["PetName"].ToString().Trim(),
myDataReader["Color"] .ToString () .Trim());
}
}
Console.PeadLine();
}
В этом фрагменте индексатор объекта чтения данных перегружен, чтобы он мог
принимать либо string (имя столбца), либо int (порядковый номер столбца). Это
позволяет прояснить логику объекта чтения (и избежать применения жестко
закодированных строковых имен) с помощью следующего изменения (обратите внимание на
использование свойства FieldCount):
784 Часть V. Введение в библиотеки базовых классов .NET
while (myDataReader.Read())
{
Console.WriteLine ("***** Запись *****");
for (int i = 0; i < myDataReader.FieldCount; i++)
{
Console.WriteLine ("{0} = {1} ",
myDataReader.GetName(l),
myDataReader.GetValue(i) .ToString () .Trim());
}
Console.WriteLine();
}
Если скомпилировать и запустить этот проект, то будет выдан список всех
автомобилей из таблицы Inventory базы данных AutoLot. Следующие выходные данные
содержат несколько первых записей из моей версии AutoLot:
***** Работа с объектами чтения данных *****
***** Информация о подключении *****
Местоположение : (local)\SQLEXPRESS
Имя базы данных: AutoLot
Тайм-аут: 30
Состояние подключения: Open
***** Запись *****
CarlD = 83
Make = Ford
Color = Rust
PetName = Rusty
***** запись *****
CarlD = 107
Make = Ford
Color = Red
PetName = Snake
Получение множественных результатов
с помощью объекта чтения данных
Объекты чтения данных могут получать множественные результирующие наборы с
помощью одного объекта команды. Например, если нужно получить все строки из
таблицы Inventory, а также все строки из таблицы Customers, то можно указать сразу
оба оператора SQL, разделив их точкой с запятой:
string strSQL = "Select * From Inventory;Select * from Customers";
После получения объекта чтения данных можно просмотреть все записи
результирующего набора с помощью метода NextResult(). Учтите, что автоматически
возвращается всегда первый результирующий набор. И если требуется просмотреть все строки
каждой таблицы, нужно будет создать следующую конструкцию перебора:
do
{
while (myDataReader.Read())
{
Console.WriteLine (••***** Запись *****");
for (int i=0; l < myDataReader.FieldCount; i++)
{
Console.WriteLine ("{0} = {1}",
myDataReader.GetName(l),
myDataReader.GetValue(i) .ToString ());
}
Console.WriteLine();
}
} while (myDataReader.NextResult());
Глава 21. ADO.NET, часть I: подключенный уровень 785
Теперь вы лучше ознакомились с возможностями, которые имеют таблицы
благодаря объектам чтения данных. Не забывайте, что объект чтения данных может
обрабатывать только SQL-операторы Select и не может использоваться для изменения
существующей таблицы базы данных с помощью запросов Insert, Update или Delete. Чтобы
понять, как изменять существующую базу данных, потребуется дальнейшее изучение
объектов команд.
Исходный код. Проект AutoLotDataReader доступен в подкаталоге Chapter 21.
Создание повторно используемой
библиотеки доступа к данным
Метод ExecuteReader () извлекает объект чтения данных, который позволяет
просматривать результаты SQL-оператора Select с помощью потока информации,
доступного только для чтения в прямом направлении. Однако если требуется
выполнить операторы SQL, модифицирующие таблицу данных, то нужен вызов
метода ExecuteNonQueryO данного объекта команды. Этот единый метод предназначен
для выполнения вставок, изменений и удалений, в зависимости от формата текста
команды.
На заметку! Понятие не запросный (nonquery) означает оператор SQL, который не возвращает
результирующий набор. Следовательно, операторы Select представляют собой запросы, а
операторы Insert, Update и Delete — нет. Соответственно, метод ExecuteNonQueryO
возвращает значение int, содержащее количество строк, на которые повлияли эти операторы,
а не новое множество записей.
Чтобы показать, как модифицировать содержимое существующей базы данных с
помощью только запроса ExecuteNonQueryO, следующим шагом будет создание
собственной библиотеки доступа к данным, в которой инкапсулируется процесс работы с
базой данных AutoLot. В реальной производственной среде ваша логика ADO.NET почти
наверняка будет изолирована в .dll-сборке .NET по одной простой причине —
повторное использование кода! В первых примерах данной главы это не было сделано, чтобы
не отвлекать вас от решаемых задач. Но было бы лишними затратами времени
разрабатывать ту же самую логику подключения, ту же самую логику чтения данных и ту
же самую логику выполнения команд для каждого приложения, которому понадобится
работать с базой данных AutoLot.
В результате изоляции логики доступа к данным в кодовой библиотеке .NET
различные приложения с любыми пользовательскими интерфейсами (консольный, в стиле
рабочего стола, в веб-стиле и т.д.) могут обращаться к существующей библиотеке даже
независимо от языка. И если разработать библиотеку доступа к данным на С#, то другие
программисты в .NET смогут создавать свои пользовательские интерфейсы на любом
языке (например, VB или C++/CLI).
В данной главе наша библиотека доступа к данным (AutoLot DAL. dl 1) будет содержать
единое пространство имен (AutoLotConnectedLayer), которое будет взаимодействовать
с базой AutoLot с помощью подключенных типов ADO.NET. В следующей главе в эту же
библиотеку будет добавлено новое пространство имен (AutoLotDisconnectionLayer),
которое будет содержать типы для взаимодействия с базой AutoLot с помощью
автономного уровня. После этого данная библиотека будет применяться в различных
приложениях, которые будут разработаны в последующем тексте.
786 Часть V. Введение в библиотеки базовых классов .NET
Начните с создания нового проекта библиотеки классов (С# Class Library) по имени
AutoLotDAL (сокращенно от 'Au to Lot Data Access Layer" — "Уровень доступа к данным
AutoLot"), а затем смените первоначальное имя файла С#-кода на AutoLotConnDAL.cs.
Потом переименуйте область действия пространства имен в AutoLotConnectedLayer
и измените имя первоначального класса на InventoryDAL, т.к. этот класс будет
определять различные члены, предназначенные для взаимодействия с таблицей Inventory
базы данных AutoLot. И, наконец, импортируйте следующие пространства имен .NET:
using System;
using System.Collections.Generic;
using System.Text;
//Мы будем применять поставщик SQL Server;
//но для большей гибкости можно воспользоваться
//и генератором поставщиков ADO.NET.
using System.Data;
using System.Data.SqlClient;
namespace AutoLotConnectedLayer
{
public class InventoryDAL
{
}
}
На заметку! Вспомните, о чем говорилось в главе 8: когда в объектах используются типы,
управляющие "сырыми" ресурсами (наподобие подключения к базе данных), рекомендуется
реализовать интерфейс IDisposable и написать нужный финализатор В производственной среде
то же самое делают классы, подобные InventoryDAL, но здесь мы это делать не будем,
чтобы не отвлекаться от деталей ADO.NET.
Добавление логики подключения
Первая наша задача — определить методы, позволяющие вызывающему процессу
подключаться к источнику данных с помощью допустимой строки подключения и
отключаться от него. Поскольку в нашей сборке AutoLotDAL.dll будет жестко закодировано
использование типов класса System.Data.SqlClient, определите приватную
переменную SqlConnection, которая будет выделяться при создании объекта InventoryDAL.
Кроме того, определите метод OpenConnectionO, а затем еще CloseConnectionO,
которые будут взаимодействовать с этой переменной:
public class InventoryDAL
{
// Этот член будет использоваться всеми методами.
private SqlConnection sqlCn = null;
public void OpenConnection(string connectionString)
{
sqlCn = new SqlConnection () ;
sqlCn.ConnectionString = connectionString;
sqlCn.Open ();
}
public void CloseConnectionO
{
sqlCn.Close ();
}
}
Глава 21. ADO.NET, часть I: подключенный уровень 787
Для краткости тип InventoryDAL не будет проверять все возможные исключения,
и не будет генерировать пользовательские исключения при возникновении различных
ситуаций (например, когда строка подключения неверно сформирована). Однако при
создании производственной библиотеки доступа к данным вам наверняка пришлось бы
задействовать технику структурированной обработки исключений, чтобы учитывать
все аномалии, которые могут возникнуть во время выполнения.
Добавление логики вставки
Вставка новой записи в таблицу Inventory сводится к форматированию SQL-
оператора Insert (в зависимости от введенных пользователем данных) и вызову
метода ExecuteNonQueryO с помощью объекта команды. Для этого добавьте в класс
InventoryDAL общедоступный метод InsertAuto(), принимающий четыре параметра,
которые соответствуют четырем столбцам таблицы Inventory (CarlD, Color, Make и
PetName). На основании этих аргументов сформируйте строку для добавления новой
записи. И, наконец, выполните SQL-оператор с помощью объекта SqlConnection:
public void InsertAuto(int id, string color, string make, string petName)
{
// Формирование и выполнение оператора SQL.
string sql = string.Format("Insert Into Inventory" +
" (CarlD, Make, Color, PetName) Values" +
" ('{0}', '{1}'/ 42}', ' {3}')", id, make, color, petName);
// Выполнение с помощью нашего подключения.
using(SqlCommand cmd = new SqlCommand(sql, this.sqlCn))
{
cmd.ExecuteNonQuery() ;
}
}
Синтаксически этот метод вполне корректен, но можно предложить перегруженную
версию, которая позволяет вызывающему методу передать объект строго
типизированного класса, представляющий данные для новой строки. Для этого определите новый
класс NewCar, который соответствует новой строке в таблице Inventory:
public class NewCar
(
public int CarlD { get; set; }
public string Color { get; set; }
public string Make { get; set; }
public string PetName { get; set; }
}
Теперь добавьте в класс InventoryDAL следующий вариант InsertAuto ():
public void InsertAuto(NewCar car)
{
// Формирование и выполнение оператора SQL.
string sql = string.Format("Insert Into Inventory" +
"(CarlD, Make, Color, PetName) Values" +
"(•{0}', '{1}'/ '{2}', '{3}')", car.CarlD, car.Make, car.Color, car.PetName);
// Выполнение с помощью нашего подключения.
using (SqlCommand cmd = new SqlCommand(sql, this.sqlCn))
{
cmd.ExecuteNonQuery();
}
}
788 Часть V. Введение в библиотеки базовых классов .NET
Определение классов, представляющих записи в реляционной базе данных —
распространенный способ создания библиотеки доступа к данным. Вообще-то, как будет
сказано в главе 23, ADO.NET Entity Framework автоматически генерирует строго
типизированные классы, которые позволяют взаимодействовать с данными базы. Кстати,
автономный уровень ADO.NET (см. главу 22) генерирует строго типизированные
объекты DataSet для представления данных из заданной таблицы в реляционной базе
данных.
На заметку! Создание оператора SQL с помощью конкатенации строк может оказаться опасным с
точки зрения безопасности (вспомните атаки вставкой в SQL). Текст команды лучше создавать
с помощью параметризованного запроса, который будет описан чуть ниже.
Добавление логики удаления
Удаление существующей записи не сложнее вставки новой записи. В отличие от кода
Insert Auto (), будет показана одна важная область try/catch, которая обрабатывает
возможную ситуацию, когда выполняется попытка удаления автомобиля, уже
заказанного кем-то из таблицы Customers. Добавьте в класс InventoryDAL следующий метод:
public void DeleteCar(int id)
{
// Получение идентификатора автомобиля перед его удалением.
string sql = string.Format("Delete from Inventory where CarlD = ' { 0}'", id);
using (SqlCommand cmgl = new SqlCommand (sql, this.sqlCn))
{
try
{
cmd.ExecuteNonQuery() ;
}
catch(SqlException ex)
{
Exception error = new Exception("К сожалению, эта машина заказана!", ex);
throw error;
}
}
}
Добавление логики изменения
Когда дело доходит до обновления существующей записи в таблице Inventory, то
сразу же возникает очевидный вопрос: что именно можно позволить изменять
вызывающему процессу: цвет автомобиля, дружественное имя, модель или все сразу? Один
из способов максимального повышения гибкости — определение метода,
принимающего параметр типа string, который может содержать любой оператор SQL, но это, по
меньшей мере, рискованно.
В идеале лучше иметь набор методов, которые позволяют вызывающему процессу
изменять записи различными способами. Однако для нашей простой библиотеки
доступа к данным мы определим единый метод, который позволяет вызывающему процессу
изменить дружественное имя указанного автомобиля:
public void UpdateCarPetName (int id, string newPetName)
{
// Получение идентификатора автомобиля и нового дружественного имени для него.
string sql =
string.Format("Update Inventory Set PetName = '{O}1 Where CarlD = ' {1} '",
newPetName, id) ;
Глава 21. ADO.NET, часть I: подключенный уровень 789
using(SqlCommand cmd = new SqlCommand(sql, this.sqlCn))
1
cmd.ExecuteNonQuery() ;
}
}
Добавление логики выборки
Теперь необходимо добавить метод для выборки записей. Как было показано ранее
в этой главе, объект чтения данных конкретного поставщика данных позволяет
выбирать записи с помощью курсора, допускающего только чтение в прямом направлении.
Посредством вызова метода Read() можно обработать каждую запись поочередно. Все
это замечательно, но теперь необходимо разобраться, как возвратить эти записи
вызывающему уровню приложения.
Одним из подходов может быть получение данных с помощью метода Read() с
последующим заполнением и возвратом многомерного массива (или другого объекта вроде
обобщенного List<T>). Вот другой подход получения данных из таблицы Inventory:
public List<NewCar> GetAllInventoryAsList ()
{
// Здесь будут находиться записи.
List<NewCar> inv = new List<NewCar> ();
// Подготовка объекта команды.
string sql = "Select * From Inventory";
using(SqlCommand cmd = new SqlCommand(sql, this.sqlCn))
{
SqlDataReader dr = cmd.ExecuteReader();
while (dr.Read())
{
inv.Add(new NewCar
{
CarlD = (int)dr["CarID"],
Color = (string)dr["Color"],
Make = (string)dr["Make"],
PetName = (string)dr["PetName"]
});
}
dr.Close () ;
}
return inv;
}
Еще один способ — возврат объекта System.Data.DataTable, который вообще-то
принадлежит автономному уровню ADO.NET. Полное описание автономного уровня
будет приведено в следующей главе, а пока достаточно уяснить, что DataTable — это
класс, представляющий табличный блок данных (наподобие бумажной или
электронной таблицы).
Класс DataTable содержит данные в виде коллекции строк и столбцов. Эти
коллекции можно заполнять программным образом, но в типе DataTable имеется метод
Load(), который может автоматически заполнять их с помощью объекта чтения
данных! Вот пример, где данные из таблицы Inventory возвращаются в виде DataTable:
public DataTable GetAllInventoryAsDataTable ()
{
// Здесь будут находиться записи.
DataTable inv = new DataTable ();
790 Часть V. Введение в библиотеки базовых классов .NET
// Подготовка объекта команды.
string sql = "Select * From Inventory";
using(SqlCommand cmd = new SqlCommand(sql, this.sqlCn))
{
SqlDataReader dr = cmd.ExecuteReader ();
// Заполнение DataTable данными из объекта чтения и зачистка.
inv.Load (dr);
dr.Close();
}
return inv;
}
Работа с параметризованными объектами команд
Пока в логике вставки, изменения и удаления для типа Invent or у DAL мы
использовали жестко закодированные строковые литералы для каждого SQL-запроса. Вы,
видимо, знаете о существовании параметризованных запросов, которые позволяют
рассматривать параметры SQL как объекты, а не просто кусок текста. Работа с SQL-
запросами в более объектно-ориентированной манере не только помогает сократить
количество опечаток (при наличии строго типизированных свойств), ведь
параметризованные запросы обычно выполняются значительно быстрее запросов в виде
строковых литералов, поскольку они анализируются только один раз (а не каждый раз, как
это происходит, если свойству CommandText присваивается SQL-строка). Кроме того,
параметризованные запросы защищают от атак внедрением в SQL (широко известная
проблема безопасности доступа к данным).
Для поддержки параметризованных запросов объекты команд ADO.NET
поддерживают коллекцию отдельных объектов параметров. По умолчанию эта коллекция пуста,
но в нее можно занести любое количество объектов параметров, которые соответствуют
параметрам-заполнителям (placeholder parameter) в SQL-запросе. Если нужно связать
параметр SQL-запроса с членом коллекции параметров некоторого объекта команды,
поставьте перед параметром SQL символ @ (по крайней мере, при работе с Microsoft SQL
Server, хотя не все СУБД поддерживают это обозначение).
Задание параметров с помощью типа DbParameter
Прежде чем приступить к созданию параметризованных запросов, ознакомимся с
типом DbParameter (базовый класс для объектов параметров поставщиков). У этого
класса есть ряд свойств, которые позволяют задать имя, размер и тип параметра, а
также другие характеристики, например, направление просмотра параметра. Некоторые
важные свойства типа DbParameter приведены в табл. 21.7.
Таблица 21.7. Основные члены типа DbParameter
Свойство Назначение
DbType Выдает или устанавливает тип данных из параметра, представляемый в
виде типа CLR
Direction Выдает или устанавливает вид параметра: только для ввода, только для
вывода, для ввода и для вывода или параметр для возврата значения
IsNullable Выдает или устанавливает, может ли параметр принимать пустые значения
ParameterName Выдает или устанавливает имя DbParameter
Size Выдает или устанавливает максимальный размер данных для параметра
(полезно только для текстовых данных)
Value Выдает или устанавливает значение параметра
Глава 21. ADO.NET, часть I: подключенный уровень 791
Для демонстрации заполнения коллекции объектов команд совместимыми с
DBParameter объектами переделаем метод InsertAutoO так, что он будет
использовать объекты параметров (аналогично можно переделать и все остальные методы, но
нам будет достаточно и настоящего примера):
public void InsertAuto(int id, string color, string make, string petName)
{
// "Заполнители" в SQL-запросе.
string sql = string.Format("Insert Into Inventory" +
11 (CarlD, Make, Color, PetName) Values" +
"(@CarID, @Make, @Color, @PetName)");
// У этой команды будут внутренние параметры.
using(SqlCommand cmd = new SqlCommand(sql, this.sqlCn))
{
// Заполнение коллекции параметров.
SqlParameter param = new SqlParameter();
param.ParameterName = "@CarID";
param.Value = id;
param.SqlDbType = SqlDbType.Int;
cmd.Parameters.Add(param);
param = new SqlParameter () ;
param.ParameterName = "@Make";
param. Value = make;
param.SqlDbType = SqlDbType.Char;
param.Size = 10;
cmd.Parameters.Add(param);
param = new SqlParameter () ;
param.ParameterName = "@Color";
param.Value = color;
param.SqlDbType = SqlDbType.Char;
param.Size = 10;
cmd.Parameters.Add(param);
param = new SqlParameter () ;
param.ParameterName = "@PetName";
param.Value = petName;
param.SqlDbType = SqlDbType.Char;
param.Size = 10;
cmd.Parameters.Add(param);
cmd.ExecuteNonQuery();
}
Обратите внимание, что здесь SQL-запрос также содержит четыре
символа-заполнителя, перед каждым из которых находится символ @. С помощью свойства
ParameterName в типе SqlParameter можно описать каждый из этих заполнителей и
задать различную информацию (значение, тип данных, размер и т.д.), причем строго
типизированным образом. После подготовки всех объектов параметров они
добавляются в коллекцию объекта команды с помощью вызова Add().
На заметку! Для оформления объектов параметров здесь используются различные свойства.
Однако учтите, что объекты параметров поддерживают ряд перегруженных конструкторов,
которые позволяют задавать значения различных свойств (что дает более компактную кодовую
базу). Учтите также, что в Visual Studio 2010 имеются различные графические конструкторы,
которые автоматически создадут за вас большой объем этого утомительного кода работы с
параметрами (см. главы 22 и 23).
792 Часть V. Введение в библиотеки базовых классов .NET
Создание параметризованного запроса часто приводит к большему объему кода,
но в результате получается более удобный способ для программной настройки SQL-
операторов, а также более высокая производительность. Эту технику можно применять
для любых SQL-запросов, хотя параметризованные запросы наиболее удобны, если
нужно запускать хранимые процедуры.
Выполнение хранимой процедуры
Вспомните, что хранимая процедура (stored procedure) — это именованный блок
SQL-кода, хранимый в базе данных. Хранимые процедуры можно создавать для
возврата набора строк или скалярных типов данных, а также выполнения других нужных
действий (например, вставки, обновления или удаления); они могут принимать любое
количество необязательных параметров. В результате получается рабочая единица,
которая ведет себя как типичная функция, только она находится в хранилище данных,
а не в двоичном бизнес-объекте. На данном этапе в базе данных AutoLot определена
одна хранимая процедура с именем GetPetName, имеющая следующий формат:
GetPetName
@carID int,
@petName char A0) output
AS
SELECT @petName = PetName from Inventory where CarlD = @carID
Теперь рассмотрим следующий финальный метод типа InventoryDAL, который
вызывает эту хранимую процедуру:
public string LookUpPetName(int carlD)
{
string carPetName = string.Empty;
// Задание имени хранимой процедуры.
using (SqlCommand cmd = new SqlCommand("GetPetName", this.sqlCn))
{
cmd.CommandType = CommandType.StoredProcedure;
// Входной параметр.
SqlParameter param = new SqlParameter();
param.ParameterName = "@carID";
param.SqlDbType = SqlDbType.Int;
param.Value = carlD;
param.Direction = ParameterDirection.Input;
cmd.Parameters.Add(param);
//По умолчанию параметры считаются входными, но все же для ясности:
param.Direction = ParameterDirection.Input;
cmd.Parameters.Add(param);
// Выходной параметр.
param = new SqlParameter () ;
param.ParameterName = "@petName";
param.SqlDbType = SqlDbType.Char;
param.Size = 10;
param.Direction = ParameterDirection.Output;
cmd.Parameters.Add(param);
// Выполнение хранимой процедуры.
cmd.ExecuteNonQuery();
// Возврат выходного параметра.
carPetName = ((string)cmd.Parameters["@petName"].Value).Trim();
}
return carPetName;
Глава 21. ADO.NET, часть I: подключенный уровень 793
Один важный аспект, касающийся вызова хранимых процедур: вспомните, что
объект команды может представлять оператор SQL (по умолчанию) или имя хранимой
процедуры. Если необходимо сообщить объекту команды, что он должен вызывать
хранимую процедуру, то нужно передать имя этой процедуры (через аргумент конструктора
или с помощью свойства CommandText) и установить в свойстве CommandType значение
CommandType.StoredProcedure (иначе вы получите исключение времени выполнения,
т.к. по умолчанию объект команды ожидает оператор SQL):
SqlCommand cmd = new SqlCommand("GetPetName", this.sqlCn);
cmd.CommandType = CommandType.StoredProcedure;
Далее обратите внимание, что свойство Direction объекта параметра позволяет
указать направление действия каждого параметра, передаваемого хранимой процедуре
(например, входной параметр, выходной параметр, входной/выходной параметр или
возвращаемое значение). Как и ранее, все объекты параметров добавляются в
коллекцию параметров для данного объекта команды:
// Входной параметр.
SqlParameter param = new SqlParameter();
param.ParameterName = "QcarlD"; f
param.SqlDbType = SqlDbType.Int;
param.Value = carlD;
param.Direction = ParameterDirection.Input;
cmd.Parameters.Add(param);
После завершения работы хранимой процедуры с помощью вызова ExecuteNonQuery ()
значение выходного параметра можно получить с помощью просмотра коллекции
параметров для данного объекта команды и соответствующего приведения типа:
// Возврат выходного параметра.
carPetName = (string)cmd.Parameters["QpetName"].Value;
И вот первый вариант библиотеки доступа к данным AutoLotDAL.dll готов! С
помощью этой сборки можно создавать произвольные интерфейсы для вывода и
редактирования данных (консольные приложения, Windows-приложения, приложения на основе
Windows Presentation Foundation или веб-приложения на базе HTML). Вы еще не умеете
создавать графические пользовательские интерфейсы, так что придется
протестировать полученную библиотеку из нового консольного приложения.
Исходный код. Проект AutoLotDAL доступен в подкаталоге Chapter 21.
Создание консольного
пользовательского интерфейса
Создайте новое консольное приложение по имени AutoLotCUIClient. После
создания нового проекта не забудьте добавить в него ссылку на сборку AutoLotDAL.dll,
а также на System.Configuration.dll, и добавьте в код С# следующие операторы
using:
using AutoLotConnectedLayer;
using System.Configuration;
using System.Data;
794 Часть V. Введение в библиотеки базовых классов .NET
После этого вставьте в проект новый файл App.config, содержащий элемент <соп-
nectionString>, который нужен для подключения к вашему экземпляру базы данных
AutoLot, например:
<configuration>
<connectionStrings>
<add name ="AutoLotSglProvider11 connectionString =
"Data Source=(local)\SQLEXPRESS;" +
"Integrated Security=SSPI;Initial Catalog=AutoLot"/>
</connectionStrings>
</configuration>
Реализация метода MainQ
Метод Main () будет отвечать за подсказку пользователю его возможных действий и
выполнение запросов с помощью оператора switch. Эта программа позволит
пользователю вводить следующие команды:
• I — вставка новой записи в таблицу Inventory;
• U — изменение существующей записи в таблице Inventory;
• D — удаление существующей записи в таблице Inventory;
• L — вывод имеющихся в наличии автомобилей с помощью объекта чтения
данных;
• S — вывод списка возможных команд;
• Р — вывод дружественного имени автомобиля по его идентификатору;
• Q — завершение работы программы.
Каждая из этих команд выполняется специальных статическим методом из класса
Program. Ниже приведена полная реализация метода Main(). Все методы, вызываемые
в цикле do while (за исключением метода ShowInstructionsO), принимают в качестве
единственного параметра объект InventoryDAL.
static void Main(string[] args)
{
Console.WriteLine ("***** Консольный пользовательский интерфейс AutoLot *****\n");
// Получение строки подключения из App.config.
string cnStr =
ConfigurationManager.ConnectionStrings["AutoLotSqlProvider"].ConnectionString;
bool userDone = false;
string userCommand = "";
// Создание объекта InventoryDAL.
InventoryDAL invDAL = new InventoryDAL();
invDAL.OpenConnection(cnStr) ;
// Запросы пользовательских команд, пока не будет нажата клавиша <Q>.
try
{
Showlnstructions ();
do
{
Console.Write("ХпВведите команду: " ) ;
userCommand = Console.ReadLine();
Console.WriteLine();
switch (userCommand.ToUpper())
{
Глава 21. ADO.NET, часть I: подключенный уровень 795
case "I":
InsertNewCar(invDAL);
break;
case "U":
UpdateCarPetName(invDAL);
break;
case "D":
DeleteCar(invDAL);
break;
case "L":
Listlnventory(invDAL) ;
break;
case "S":
Showlnstructions ();
break;
case "P":
LookUpPetName(invDAL);
break;
case "Q":
userDone = true;
breaks-
default :
Console.WriteLine("Неверно' Попробуйте еще раз");
break;
}
} while (luserDone);
catch (Exception ex)
Console.WriteLine (ex.Message);
finally
invDAL.CloseConnection();
}
Реализация метода Showlnstructions ()
Метод Showlnstructions () делает то, что понятно из его названия ("Вывод
инструкций"):
private static void Showlnstructions ()
{
Console.WriteLine ("I: Добавление нового автомобиля.");
Console.WriteLine ("U: Изменение существующего автомобиля.");
Console.WriteLine("D: Удаление существующего автомобиля.");
Console.WriteLine ("L: Вывод автомобилей в наличии.");
Console.WriteLine ("S : Вывод этих инструкций.");
Console.WriteLine("Р: Вывод дружественного имени автомобиля.");
Console.WriteLine("Q: Завершение работы.");
}
Реализация метода ListlnventoryO
Метод ListlnventoryO ("Список наличия") можно реализовать двумя способами, в
зависимости от того, как была создана библиотека доступа к данным. Вспомните, что
метод GetAllInventoryAsDataTableO из класса InventoryDAL возвращает объект
DataTable. Этот подход можно реализовать так:
796 Часть V. Введение в библиотеки базовых классов .NET
private static void Listlnventory(InventoryDAL invDAL)
{
// Вывод автомобилей в наличии.
DataTable dt = invDAL.GetAllInventory();
// Передача DataTable вспомогательной функции для вывода.
DisplayTable (dt);
}
Вспомогательный метод DisplayTable () выводит данные таблицы, используя
свойства Rows и Columns входного параметра DataTable (подробно DataTable будет
рассмотрен в следующей главе, а пока не обращайте внимания на детали):
private static void DisplayTable(DataTable dt)
{
// Вывод имен столбцов.
for (int curCol = 0; curCol < dt.Columns.Count; curCol++)
{
Console.Write(dt.Columns[curCol].ColumnName.Trim() + "\t");
}
Console .WriteLine (" \n ") ;
// Вывод содержимого DataTable.
for (int curRow = 0; curRow < dt.Rows.Count; curRow++)
{
for (int curCol = 0; curCol < dt.Columns.Count; curCol++)
{
Console.Write(dt.Rows[curRow] [curCol] .ToString () .Trim() + "\t");
}
Console.WriteLine();
}
}
А если вам больше нравится метод Get AllInventoryAsList () из класса
InventoryDAL, то можно реализовать метод ListInventoryViaList():
private static void ListlnventoryViaList(InventoryDAL invDAL)
{
// Получение списка содержимого.
List<NewCar> record = invDAL.GetAllInventoryAsList ();
foreach (NewCar с in record)
{
Console.WriteLine ("CarlD: {0}, Make: {1}, Color: {2}, PetName: {3}",
c.CarlD, c.Make, c.Color, c.PetName);
Реализация метода DeleteCar ()
Удаление существующего автомобиля сводится к запросу у пользователя
идентификатора этого автомобиля, с последующим обращением к таблице данных и передачей
этого идентификатора методу DeleteCar () типа InventoryDAL:
private static void DeleteCar(InventoryDAL invDAL)
{
// Получение идентификатора удаляемого автомобиля.
Console.Write("Введите ID удаляемого автомобиля: " ) ;
int id = int.Parse(Console.ReadLine());
//На случай нарушения ограничений первичного ключа!
Глава 21. ADO.NET, часть I: подключенный уровень 797
try
{
invDAL.DeleteCar(id);
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Реализация метода InsertNewCar()
Для вставки новой записи в таблицу Inventory нужно запросить у пользователя
информацию о новом автомобиле (с помощью вызовов Console.ReadLineO) и передать
эти данные в метод Insert Auto() библиотеки Inventor у DAL:
private static void InsertNewCar(InventoryDAL invDAL)
{
// Получение данных от пользователя.
int newCarlD;
string newCarColor, newCarMake, newCarPetName;
Console.Write("Введите ID автомобиля: ") ;
newCarlD = int.Parse (Console.ReadLine());
Console.Write("Введите цвет автомобиля: ");
newCarColor = Console.ReadLine();
Console.Write("Введите модели автомобиля: ");
newCarMake = Console.ReadLine();
Console.Write("Введите дружественного имени: ") ;
newCarPetName = Console.ReadLine ();
// Передача информации в библиотеку доступа к данным.
invDAL.InsertAuto (newCarlD, newCarColor, newCarMake, newCarPetName);
}
Вспомните, что у нас имеется перегруженный вариант InsertAuto (), который
принимает новый объект NewCar, а не набор независимых аргументов. Он позволяет
реализовать метод InsertNewCar() так:
private static void InsertNewCar(InventoryDAL invDAL)
{
// Получение данных от пользователя.
// Передача информации в библиотеку доступа к данным.
NewCar с = new NewCar { CarlD = newCarlD, Color = newCarColor,
Make = newCarMake, PetName = newCarPetName };
invDAL.InsertAuto(c);
}
Реализация метода UpdateCarPetNameQ
Реализация метода UpdateCarPetNameO выглядит аналогично:
private static void UpdateCarPetName(InventoryDAL invDAL)
{
// Получение данных от пользователя.
int carlD;
string newCarPetName;
Console.Write("Введите ID автомобиля: ");
car ID = int.Parse(Console.ReadLine());
798 Часть V. Введение в библиотеки базовых классов .NET
Console.Write ("Введите новое дружественное имя: ");
newCarPetName = Console.ReadLine();
// Передача информации в библиотеку доступа к данным.
invDAL.UpdateCarPetName(carlD, newCarPetName);
Реализация метода LookUpPetName()
Получение дружественного имени указанного автомобиля также мало чем
отличается от предыдущих методов, поскольку все низкоуровневые вызовы ADO.NET
инкапсулированы в библиотеке доступа к данным:
private static void LookUpPetName(InventoryDAL invDAL)
{
// Получение идентификатора автомобиля.
Console.Write("Введите ID автомобиля: ") ;
int id = int.Parse(Console.ReadLine());
Console.WriteLine("Имя {0} - {1}.",
id, invDAL.LookUpPetName(id));
}
На этом консольный интерфейс завершен. Можно запустить полученную
программу и проверить работу каждого метода. Вот часть выходных данных, где проверяются
команды L, Р и Q:
***** Консольный пользовательский интерфейс AutoLot *****
I: Добавление нового автомобиля.
U: Изменение существующего автомобиля.
D: Удаление существующего автомобиля.
L: Вывод автомобилей в наличии.
S: Вывод этих инструкций.
Р: Вывод дружественного имени автомобиля.
Q: Завершение работы.
Введите команду: L
CarlD
83
107
678
904
1000
1001
Make
Ford
Ford
Yugo
VW
BMW
BMW
Color
Rust
Red
Green
Black
Black
Tan
PetName
Rusty
Snake
Clunker
Hank
Bimmer
Daisy
1992 Saab Pink Pinkey
Введите команду: Р
Введите ID автомобиля: 904
Имя 904 - Hank.
Введите команду: Q
Press any key to continue . .
Исходный код. Проект AutoLotCUIClient доступен в подкаталоге Chapter 21.
Глава 21. ADO.NET, часть I: подключенный уровень 799
Транзакции баз данных
И в завершение нашего изучения подключенного уровня ADO.NET рассмотрим
концепцию транзакций базы данных. Попросту говоря, транзакция — это набор операций
в базе данных, которые должны быть либо все выполнены, либо все не выполнены.
Транзакции применяются для обеспечения безопасности, верности и
непротиворечивости данных в таблице.
Транзакции очень важны тогда, когда при работе с базой данных требуется
взаимодействие с несколькими таблицами или несколькими хранимыми процедурами (или
с сочетанием неделимых объектов базы данных). Классический пример транзакции —
процесс перевода денежных средств с одного банковского счета на другой. Например,
если вам понадобилось перевести $500 с депозитного счета на текущий счет, то нужно
выполнить в режиме транзакции следующие шаги:
• банк должен снять $500 с вашего депозитного счета;
• затем банк должен добавить $500 на ваш текущий счет.
Вряд ли вам бы понравилось, если бы деньги были сняты с депозитного счета, но
не переведены (из-за какой-то банковской ошибки) на текущий счет. Но если эти шаги
упаковать в транзакцию базы данных, то СУБД гарантирует, что все взаимосвязанные
шаги будут выполнены как единое целое. Если любая часть транзакции выполнится
неудачно, то будет произведен откат (rollback) всей транзакции в исходное
состояние. А если все шаги будут выполнены успешно, то транзакция будет зафиксирована
(committed).
На заметку! Если вы уже читали о транзакциях, то, возможно, вам встречалось сокращение ACID.
Оно означает четыре ключевых свойства классической транзакции: атомарность (Atomic — все
или ничего), целостность (Consistent — на протяжении транзакции данные остаются в
непротиворечивом состоянии), изолированность (Isolated — транзакции не мешают одна другой) и
устойчивость (Durable — транзакции сохраняются и протоколируются).
Оказывается, в платформе .NET есть несколько способов поддержки
транзакций. В данной главе мы рассмотрим объект транзакции для поставщика данных
ADO.NET (SqlTransaction в случае System.Data.SqlClient). Библиотеки базовых
классов ADO.NET также обеспечивают поддержку транзакций в многочисленных API-
интерфейсах, которые перечислены ниже.
• System.EnterpriseServices. Это пространство имен (из сборки System.
EnterpriseServices.dll) содержит типы, позволяющие интеграцию с уровнем
времени выполнения СОМ+, в том числе и поддержку распределенных транзакций.
• System.Transactions. Это пространство имен (из сборки System.Transactions.dll)
содержит классы, позволяющие писать собственные транзакционные
приложения и диспетчеры ресурсов для различных служб (MSMQ, ADO.NET, СОМ+ и т.д.).
• Windows Communication Foundation. WCF API предоставляет службы для работы с
транзакциями с различными классами распределенного связывания.
• Windows Workflow Foundations. WF API предоставляет транзакционную поддержку
для рабочих потоков.
Кроме встроенной поддержки транзакций в библиотеках базовых классов .NET,
можно пользоваться и возможностями языка SQL используемой СУБД. Например, можно
написать хранимую процедуру, в которой задействованы операторы BEGIN TRANSACTION,
ROLLBACK и COMMIT.
800 Часть V. Введение в библиотеки базовых классов .NET
Основные члены объекта транзакции ADO.NET
Типы для работы с транзакциями существуют во всех библиотеках базовых
классов, но мы будем рассматривать объекты транзакции, которые имеются в
поставщиках данных ADO.NET — все они порождены от DBTransaction и реализуют интерфейс
IDbTransaction. Вспомните, в начале этой главы было сказано, что IDbTransaction
определяет ряд членов:
public interface IDbTransaction : IDisposable
i
IDbConnection Connection { get; }
IsolationLevel IsolationLevel { get; }
void Commit ();
void Rollback();
\
Обратите внимание на свойство Connection, которое возвращает ссылку на объект
подключения, инициировавший данную транзакцию (как мы увидим, объект
транзакции можно получить от данного объекта подключения). Метод Commit () вызывается,
если все операции в базе данных завершились успешно. При этом все ожидающие
изменения фиксируются в хранилище данных. А метод Commit () можно вызвать при
возникновении исключения времени выполнения, чтобы сообщить СУБД, что все
ожидающие изменения следует отменить и оставить первоначальные данные без изменений.
На заметку! Свойство IsolationLevel объекта транзакции позволяет указать степень защиты
транзакции от действий параллельных транзакций. По умолчанию транзакции полностью
изолируются до их фиксации. Полную информацию о значениях перечисления IsolationLevel
можно найти в документации по .NET Framework 4.0 SDK.
Кроме членов, определенных интерфейсом IDbTransaction, в типе SqlTransaction
определен дополнительный член Save(), который предназначен для определения
точек сохранения (save point). Эта концепция позволяет откатить неудачную транзакцию
до указанной точки, не выполняя откат всей транзакции. При вызове метода Save() с
помощью объекта SqlTransaction можно задать произвольный строковый псевдоним.
А при вызове Rollback() можно указать этот псевдоним в качестве аргумента, чтобы
выполнить частичный откат (partial rollback). При вызове Rollback() без аргументов
будут отменены все ожидающие изменения.
Добавление таблицы CreditRisks в базу данных AutoLot
А теперь рассмотрим, как можно использовать транзакции в ADO.NET. Вначале
откройте окно Server Explorer из Visual Studio 2010 и добавьте в базу данных AutoLot
новую таблицу с именем CreditRisks, которая содержит точно такие же столбцы,
что и таблица Customers, созданная ранее в этой главе: CustID (первичный ключ),
FirstName и LastName. Эта таблица предназначена для отсеивания нежелательных
клиентов с плохой кредитной историей. После добавления новой таблицы в диаграмму
базы данных AutoLot ее реализация будет такой, как показано на рис. 21.14.
Как и предыдущий пример с пересылкой денег с депозита на текущий счет, этот
пример, где подозрительный клиент перемещается из таблицы Customers в таблицу
CreditRisks, должен работать под недремлющим оком транзакционной области (ведь
вы хотите запомнить идентификаторы и имена некредитоспособных клиентов). А
именно, необходимо гарантировать, что либо будет выполнено успешное удаление текущих
кредитных рисков из таблицы Customers с последующим их добавлением в таблицу
CreditRisks, либо ни одна из этих операций не будет выполнена.
Глава 21. ADO.NET, часть I: подключенный уровень 801
dbo.CarDiagramFilc...qlexpr6S5.AutoLot)* X |
Inventory
FK_Orders_Inventory
9 CarlD'
Make
Color
DotNsmp
« m
a
►
Orders
S OrderlD
CustID
CarlD
FK_Orders_Customers
CreditRisks
S CustID
FirstName
LastName
Customers
9 CustID
FirstName
LastName
Ы
J
2Щ
Рис. 21.14. Взаимосвязанные таблицы Orders, Inventory и Customers
На заметку! В производственной среде вам не понадобится создавать совершенно новую таблицу
базы данных для подозрительных клиентов. Достаточно добавить в таблицу Customers
логический столбец isCreditRisk. Эта новая таблица предназначена просто для опробования
работы с простыми транзакциями.
Добавление метода транзакции в InventoryDAL
А теперь посмотрим, как работать с транзакциями ADO.NET программным
образом. Откройте созданный ранее проект AutoLotDAL Code Library и добавьте в класс
InventoryDAL новый общедоступный метод processCreditRisk(), предназначенный
для работы с кредитными рисками (в данном примере для простоты не используется
параметризованный запрос, но в производственном методе его следует задействовать):
// Новый член класса InventoryDAL.
public void ProcessCreditRisk(bool throwEx, int custID)
{
// Вначале выборка имени по идентификатору клиента.
string fName = string.Empty;
string IName = string.Empty;
SqlCommand cmdSelect = new SqlCommand(
string.Format("Select * from Customers where CustID
using (SqlDataReader dr = cmdSelect.ExecuteReader())
{
if (dr.HasRows)
{
dr. Read () ;
fName = (string)dr["FirstName"] ;
IName = (string)dr["LastName"] ;
}
else
return;
}
custID), sqlCn);
802 Часть V. Введение в библиотеки базовых классов .NET
// Создание объектов команд для каждого шага операции.
SqlCommand cmdRemove = new SqlCommand(
string.Format("Delete from Customers where CustID = {0}", custID), sqlCn) ;
SqlCommand cmdlnsert = new SqlCommand(string.Format("Insert Into CreditRisks" +
"(CustID, FirstName, LastName) Values" +
"({0}, '{1}\ '{2}')", custID, fName, IName), sqlCn) ;
// Получаем из объекта подключения.
SqlTransaction tx = null;
try
{
tx = sqlCn.BeginTransaction ();
// Включение команд в транзакцию.
cmdlnsert.Transaction = tx;
cmdRemove.Transaction = tx;
// Выполнение команд.
cmdlnsert.ExecuteNonQuery();
cmdRemove.ExecuteNonQuery();
// Имитация ошибки.
if (throwEx)
{
throw new ApplicationException
("Ошибка базы данных! Транзакция завершена неудачно.");
}
// Фиксация.
tx.Commit();
}
catch (Exception ex)
{
Console.WriteLine^ex.Message) ;
// При возникновении любой ошибки выполняется откат транзакции.
tx.Rollback();
}
}
Здесь используется входной параметр типа bool, который указывает, нужно ли
генерировать произвольное исключение при попытке обработки нежелательного
клиента. Это позволит имитировать непредвиденные ситуации, которые могут привести к
неудачному завершению транзакции. Понятно, что здесь это сделано лишь в
демонстрационных целях; в реальности метод транзакции не должен позволять вызывающему
процессу разрушать всю логику по своему усмотрению!
Мы используем два объекта SqlCommand, представляющие каждый шаг
предстоящей транзакции. После получения имени и фамилии клиента по входному параметру
custID с помощью метода BeginTransactionO объекта подключения получаем
нужный объект SqlTransaction. Если этого не сделать, логика вставки/удаления не будет
выполняться в транзакционном контексте.
Если (и только если) значение логического параметра равно true, то после вызова
ExecuteNonQuery () для обеих команд генерируется исключение. В этом случае все
ожидающие подтверждения операции базы данных аннулируются. Если исключение не было
сгенерировано, оба шага фиксируются в таблицах базы данных при вызове Commit ().
Скомпилируйте измененный проект AutoLotDAL и проверьте, нет ли ошибок.
Тестирование транзакции в нашей базе данных
Можно было модифицировать существующее приложение AutoLotCUIClient, чтобы
оно содержало метод ProcessCreditRisk(). Но мы для этой цели создадим новое кон-
Глава 21. ADO.NET, часть I: подключенный уровень 803
сольное приложение с именем AdoNetTransaction. Создайте в нем ссылку на сборку
AutoLotDAL.dll и импортируйте пространство имен AutoLotConnectedLayer.
После этого откройте таблицу Customers, щелкнув правой кнопкой мыши на
значке таблицы в Server Explorer и выбрав в контекстном меню пункт Show Table Data
(Просмотр данных таблицы). Создайте нового клиента с плохой кредитной историей,
например:
• CustID: 333
• FirstName: Homer
• LastName: Simpson
Теперь измените метод Main() следующим образом:
static void Main(string [ ] arqs)
{
Console.WriteLine("***™ Простой пример работы с транзакциями *****\п");
// Простой способ разрешить или запретить успешное выполнение транзакции.
bool throwEx = true;
string userAnswer = string.Empty;
Console.Write("Сгенерировать исключение? (Y или N) : ");
userAnswer = Console.ReadLine ();
if (userAnswer.ToLower() == "n")
{
throwEx = false;
}
InventoryDAL dal = new InventoryDAL();
dal.OpenConnection(@"Data Source=(local)\SQLEXPRESS;Integrated Security=SSPI;" +
"Initial Catalog=AutcLDt");
// Обработка клиента 333.
dal.ProcessCreditRisk(throwEx, 333);
Console.WriteLine("Проверьте результаты в таблице CreditRisk");
Console.ReadLine();
}
Если запустить программу и выбрать генерацию исключения, то клиент Homer не
будет удален из таблицы Customers, т.к. вся транзакция будет отменена. Но при
отсутствии исключения окажется, что клиент номер 333 находится уже не в таблице
Customers, а в таблице CreditRisks.
Исходный код. Проект AdoNetTransaction доступен в подкаталоге Chapter 21.
Резюме
ADO.NET — технология доступа к данным, непосредственно встроенная в
платформу .NET. Ее можно использовать тремя различными способами: подключенным,
автономным и с помощью Entity Framework. В данной главе был рассмотрен подключенный
уровень, а также роль поставщиков данных, которые представляют собой конкретные
реализации нескольких абстрактных базовых классов (в пространстве имен System.
Data.Common) и типов интерфейсов (в пространстве имен System.Data). Вы увидели,
как создавать кодовую базу, не зависящую от поставщика, с помощью модели
генератора поставщиков данных ADO.NET.
Вы также узнали, что объекты подключений, объекты транзакций, объекты команд
и объекты чтения данных из подключенного уровня позволяют выбирать, изменять,
вставлять и удалять записи. Не забывайте также, что объекты команд поддерживают
внутреннюю коллекцию параметров, которые повышают безопасность SQL-запросов и
весьма полезны для запуска хранимых процедур.
ГЛАВА 22
ADO.NET, часть II:
автономный уровень
В предыдущей главе мы рассмотрели подключенный уровень ADO.NET, который
позволяет отправлять в базу данных запросы с помощью объектов подключения,
объектов команд и объектов чтения данных соответствующего поставщика данных. А в
данной главе вы познакомитесь с автономным уровнем (disconnected layer) ADO.NET.
Этот аспект ADO.NET позволяет смоделировать в памяти данные из базы данных —
конкретнее, в вызывающем уровне с помощью многочисленных членов пространства имен
System.Data (в основном это DataSet, DataTable, DataRow, DataColumn, DataView и
DataRelation). При этом возникает иллюзия, что вызывающий уровень постоянно
подключен к внешнему источнику данных, хотя на самом деле все операции выполняются
с локальной копией реляционных данных.
Этот автономный аспект ADO.NET можно использовать даже без подключения к
реляционной базе данных, но все-таки чаще всего заполненные объекты DataSet
получают с помощью объекта адаптера данных конкретного поставщика данных. Как будет
показано, объекты адаптеров данных выполняют связующую роль между клиентским
уровнем и реляционной базой данных. С их помощью можно получить объекты DataSet,
поработать с их содержимым и отправить измененные строки обратно для дальнейшей
обработки. В результате получается широко масштабируемое .NET-приложение
обработки данных.
В данной главе будут также продемонстрированы некоторые приемы привязки к
данным с помощью контекста графических приложений Windows Forms GUI и
рассмотрена роль строго типизированного DataSet. Мы добавим в библиотеку AutoLotDAL.dll,
созданную в главе 21, новое пространство имен, в котором используется автономный
уровень ADO.NET. И в завершение мы рассмотрим технологию LINQ to DataSet, которая
позволяет применять LINQ-запросы к находящемуся в памяти кэшу данных.
На заметку! Технологии привязки к данным для приложений на основе Windows Presentation
Foundation и ASP.NET будут описаны ниже в данной книге.
Знакомство с автономным уровнем ADO.NET
Как было показано в предыдущей главе, работа с подключенным уровнем позволяет
взаимодействовать с базой данных с помощью первичных объектов подключения,
команд и чтения данных. Этот небольшой набор типов позволяет выбирать, вставлять,
изменять и удалять записи (а также вызывать хранимые процедуры или выполнять
Глава 22. ADO.NET, часть II: автономный уровень 805
другие операции над данными — например, операторы DDL для создания таблицы и
DCL для назначения полномочий). Но вы увидели лишь половину ADO.NET, поскольку с
помощью объектной модели ADO.NET можно работать и в автономном режиме.
Автономные типы позволяют эмулировать реляционные данные с помощью модели
объектов, находящихся в памяти. Кроме простого моделирования табличных данных,
состоящих из строк и столбцов, типы из System.Data позволяют воспроизводить
отношения между таблицами, ограничения столбцов, первичные ключи, представления и
другие примитивы баз данных. К смоделированным данным можно применять
фильтры, отправлять запросы и сохранять (или загружать) данные в формате XML и
двоичном формате. И все это можно делать, даже не подключаясь к СУБД (откуда и термин
"автономный уровень") — достаточно загрузить данные из локального XML-файла или
программным образом создать объект DataSet.
Автономные типы действительно можно использовать без подключения к базе
данных, но все-таки обычно применяются подключения и объекты команд. Кроме того,
используется и особый объект — адаптер данных (расширяющий абстрактный тип
Db Data Adapter), который как раз поставляет и обновляет данные. Но в отличие от
подключенного уровня, данные, полученные через адаптер данных, не
обрабатываются с помощью объектов чтения данных. Вместо этого объекты адаптеров
пересылают данные между вызывающим процессом и источником данных с помощью объектов
DataSet. Тип DataSet представляет собой контейнер для любого количества объектов
DataTable, каждый из которых содержит коллекцию объектов DataRow и DataColumn.
Объект адаптера данных конкретного поставщика данных автоматически
обслуживает подключение к базе данных. Для повышения масштабируемости адаптеры данных
держат подключение открытым минимально возможное время. Как только
вызывающий процесс получит объект DataSet, вызывающий уровень полностью отключается
от базы данных и остается с локальной копией удаленных данных. Теперь в нем можно
вставлять, удалять или изменять строки различных объектов DataTable, но
физическая база данных не обновляется, пока вызывающий процесс явно не передаст DataSet
адаптеру данных для обновления. По сути, объекты DataSet имитируют постоянное
подключение клиентов, хотя на самом деле они работают с находящейся в памяти
базой данных (рис. 22.1).
Клиентское приложение
Объект
. DataSet
^ч*--—. —"^^
4
^
Адаптер данных
w
Р\
База данных
Рис. 22.1. Объекты адаптеров данных пересылают информацию в клиентский уровень и из него
Поскольку эпицентром автономного уровня является тип DataSet, то
первоочередная задача данной главы — научиться вручную оперировать с ним. После этого у вас
не будет проблем с обработкой содержимого DataSet, полученного от объекта адаптера
данных.
Роль объектов DataSet
Как уже было сказано, объект DataSet является представлением реляционных
данных, находящимся в памяти. Конкретнее, это класс, содержащий внутри себя три
внутренних строго типизированных коллекции (рис, 22.2).
806 Часть V. Введение в библиотеки базовых классов .NET
DataSet
DataTableCollection
DataRelationCollection
PropertyCollection
Рис. 22.2. Внутреннее
устройство класса DataSet
Свойство Tables класса DataSet предоставляет доступ к
коллекции DataTableCollection, которая содержит
отдельные объекты DataTable. В DataSet используется еще
одна важная коллекция — DataRelationCollection.
Поскольку DataSet является автономной версией схемы
базы данных, его можно использовать для программного
представления отношений родительский/дочерний между
ее таблицами. Например, с помощью типа DataRelation
можно создать отношение между двумя таблицами,
имитирующее ограничение внешнего ключа. Затем с
помощью свойства Relations этот объект можно добавить в
коллекцию DataRelationCollection. Теперь при поиске
данных можно перемещаться по взаимосвязанным таблицам. Как это сделать, будет
рассказано далее в главе.
Свойство ExtendedProperties предоставляет доступ к объекту PropertyCollect ion,
который позволяет связать с DataSet любую дополнительную информацию в виде пар
имя/значение. Эта информация может быть совершенно произвольной, даже не
имеющей отношения к самим данным. К примеру, с каким-либо объектом DataSet можно
связать название компании, которое будет играть роль находящихся в памяти
метаданных. Другими примерами расширенных свойств могут служить временные метки,
зашифрованный пароль, который необходим для доступа к содержимому DataSet, число,
означающее частоту обновления данных, и многое другое.
На заметку! Классы DataTable и DataColumn поддерживают также свойство
ExtendedProperties.
Основные свойства класса DataSet
Прежде чем окунуться в разнообразные программные мелочи, рассмотрим
некоторые основные члены класса DataSet. Кроме свойств Tables, Relations и
ExtendedProperties, в табл. 22.1 приведено несколько дополнительных полезных
свойств.
Таблица 22.1. Свойства класса DataSet
Свойство
Назначение
CaseSensitive
DataSetName
EnforceConstraints
HasErrors
RemotingFormat
Указывает, чувствительны ли к регистру букв сравнения строк в
объектах DataTable. По умолчанию равно false (сравнения строк
выполняются без учета регистра букв)
Задает понятное имя для данного DataSet. Обычно это значение
передается через параметр конструктора
Задает или получает значение, определяющее, применяются ли
правила ограничений при выполнении любых обновлений (по
умолчанию равно true)
Получает значение, определяющее, имеются ли ошибки в любой
строке любого из объектов DataTable данного DataSet
Позволяет определить, как DataSet должен сериализовать свое
содержимое (в виде двоичного файла или, по умолчанию, XML)
Глава 22. ADO.NET, часть II: автономный уровень 807
Основные методы класса DataSet
Методы класса DataSet работают в сочетании с некоторыми функциями, которые
обеспечивают описанные выше свойства. Кроме взаимодействия с потоками XML,
DataSet содержит методы, позволяющие копировать содержимое DataSet,
перемещаться между внутренними таблицами и устанавливать начальные и конечные точки
пакетных обновлений. Некоторые основные методы перечислены в табл. 22.2.
Таблица 22.2. Методы класса DataSet
Метод
Назначение
AcceptChanges()
С1еаг()
Clone()
СоруО
GetChangesO
HasChangesO
Merge()
ReadXmlO
RejectChangesO
WriteXmK)
Отправляет все изменения, выполненные в данном DataSet после его
загрузки или последнего вызова AcceptChanges ()
Полностью очищает DataSet, удаляя все строки в каждом DataTable
Клонирует структуру DataSet, в том числе и всех DataTable, а также
все отношения и ограничения
Копирует структуру и данные текущего DataSet
Возвращает копию DataSet, содержащую все изменения, которые были
выполнены в данном DataSet после его загрузки или последнего
вызова AcceptChanges (). У этого метода есть перегруженные варианты,
которые позволяют получить только новые строки, только измененные
строки или только удаленные строки
Выдает, содержит ли DataSet изменения, т.е. новые, удаленные или
измененные строки
Объединяет данный DataSet с указанным DataSet
Позволяет определить структуру объекта DataSet и заполнить его
данными на основе XML-схемы и данных из потока
Отменяет все изменения, которые были выполнены в данном DataSet
после его загрузки или последнего вызова AcceptChanges ()
Позволяет записать содержимое DataSet в поток
Создание DataSet
Теперь, когда вы уже лучше понимаете роль DataSet (и имеете некоторое
представление о его возможностях), создайте новое консольное приложение по имени
SimpleDataSet и импортируйте пространство имен System.Data. В методе Main()
определите новый объект DataSet с тремя расширенными свойствами,
представляющими временную метку, уникальный идентификатор (типа System.Guid) и название
компании:
static void Main(string[] args)
{
Console.WriteLine ("***** Работа с объектами DataSet *****\n");
// Создание объекта DataSet и добавление нескольких свойств.
DataSet carsInventoryDS = new DataSet("Car Inventory");
carsInventoryDS.ExtendedProperties["TimeStamp"] = DateTime.Now;
carsInventoryDS.ExtendedProperties["DataSetID"] = Guid.NewGuid();
carsInventoryDS.ExtendedProperties["Company" ] = "Супер-гипер-магазин Mikko";
Console.ReadLine();
808 Часть V. Введение в библиотеки базовых классов .NET
Если вы не знакомы с концепцией глобально уникальных идентификаторов
(globally unique identifier — GUID), просто считайте, что это статически уникальное
128-битное число. Хотя идентификаторы GUID применяются в среде СОМ для
идентификации различных сущностей СОМ (классы, интерфейсы, приложения и т.д.), тип
System.Guid очень полезен и в .NET, когда нужно быстро сгенерировать уникальный
идентификатор.
В любом случае объект DataSet не очень-то интересен, если он не содержит хоть
немного объектов DataTable. Значит, теперь нужно изучить внутреннее устройство
класса DataTable, начиная с типа DataColumn.
Работа с объектами DataColumn
Тип DataColumn представляет один столбец в объекте DataTable. Вообще-то
множество всех объектов DataColumn, содержащихся в данном объекте DataTable, содержит
всю информацию схемы таблицы. Например, если понадобится создать копию таблицы
Inventory из базы данных AutoLot (см. главу 21), нужно будет создать четыре
объекта DataColumn, по одному для каждого столбца (CarlD, Make, Color и PetName). После
создания объектов DataColumn они обычно добавляются в коллекцию столбцов типа
DataTable (с помощью свойства Columns).
Возможно, вы уже знаете, что любому столбцу в таблице базы данных можно
назначить набор ограничений (в виде первичного ключа, значения по умолчанию,
разрешения только на чтение информации и т.д.). Кроме того, каждый столбец таблицы должен
относиться к одному из разрешенных в СУБД типов данных. К примеру, схема таблицы
Inventory требует, чтобы столбец CarlD содержал целые числа, а столбцы Make, Color
и PetName — массив символов. Класс DataColumn имеет ряд свойств для указания
таких моментов. Список некоторых основных свойств приведен в табл. 22.3.
Таблица 22.3. Свойства класса DataColumn
Свойство Назначение
AllowDBNull Указывает, может ли данный столбец содержать пустые значения.
По умолчанию содержит значение true
Autolncrement Применяются для настройки поведения автоинкремента для данного
AutoIncrementSeed столбца. Это может оказаться удобным, если нужно обеспечить уни-
AutoIncrementStep кальность значений в этом DataColumn (например, если он содержит
первичные ключи). По умолчанию DataColumn не поддерживает
автоинкрементное поведение
Caption Задает или получает заголовок, который должен отображаться для
данного столбца. Это позволяет определить более наглядные варианты
для имен столбцов в базе данных
ColumnMapping Определяет представление DataColumn при сохранении DataSet
в виде XML-документа с помощью метода DataSet .WriteXml ().
Можно указать, что столбец данных должен быть записан как XML-
элемент, XML-атрибут, простое текстовое содержимое, либо его
следует полностью проигнорировать
ColumnName Задает или получает имя столбца из коллекции Columns (т.е его
внутреннее представление в DataTable). Если не занести значение
в ColumnName явно, то по умолчанию там находится слово "Column"
с числовыми суффиксами по формуле л+1 (т.е. Columnl, Column2,
Column3 и т.д.)
Глава 22. ADO.NET, часть II: автономный уровень 809
Окончание табл. 22.3
Свойство Назначение
DataType Определяет тип данных (логический, строковый, с плавающей точкой
и т.д.), хранящихся в данном столбце
Def aultValue Задает или получает значение по умолчанию, заносимое в данный
столбец при вставке новых строк
Expression Задает или получает выражение для фильтрации строк, вычисления
значения столбца или создания агрегированного столбца
Ordinal Задает или получает числовое положение столбца в коллекции
Columns, содержащейся в DataTable
Readonly Определяет, предназначен ли данный столбец только для чтения после
добавления строки в таблицу. По умолчанию равно false
Table Получает объект DataTable, содержащий данный DataColumn
Unique Задает или получает значение, указывающее, должны ли быть
уникальными значения во всех строках данного столбца, или допустимы
совпадения. При присвоении столбцу ограничения первичного ключа
свойство Unique должно содержать значение true
Создание объекта DataColumn
Продолжаем создание проекта SimpleDataSet (и демонстрацию применения типа
DataColumn). Предположим, что вам нужно смоделировать столбцы таблицы Inventory.
Поскольку столбец CarlD должен быть первичным ключом таблицы, его необходимо
создать только для чтения, содержащим уникальные значения и не допускающим
пустые значения (с помощью свойств Readonly, Unique и AllowDBNull). Вставьте в класс
Program новый метод FillDataSet(), предназначенный для создания четырех
объектов DataColumn. Он принимает в качестве единственного параметра объект DataSet:
static FillDataSet(DataSet ds)
{
// Создание столбцов данных, соответствующих "реальным"
// столбцам таблицы Inventory из базы данных AutoLot.
DataColumn carlDColumn = new DataColumn ("CarlD", typeof(int));
carlDColumn.Caption = "Car ID";
carlDColumn.Readonly = true;
carlDColumn.AllowDBNull = false;
carlDColumn.Unique = true;
DataColumn carMakeColumn = new DataColumn("Make", typeof(string));
DataColumn carColorColumn = new DataColumn("Color", typeof(string));
DataColumn carPetNameColumn = new DataColumn("PetName", typeof(string));
carPetNameColumn.Caption = "Друж.Имя";
}
Обратите внимание, что при конфигурировании объекта carlDColumn было
присвоено значение свойству Caption. Это позволяет определить строковое значение для
отображения при выводе данных, которое может отличаться от имени столбца (имена
столбцов в таблицах баз данных обычно более удобны для программирования
(например, au_f name), чем для отображения (например, Author First Name)). По той же причине
здесь задано заглавие столбца PetName, т.к. Друж.Имя понятнее конечному
пользователю, чем PetName.
810 Часть V. Введение в библиотеки базовых классов .NET
Включение автоинкрементных полей
Одним из аспектов DataColumn, допускающим настройку, является возможность ав-
ттюинкремента (autoincrement). Если в какую-либо таблицу добавляется новая строка,
то значение автоинкрементного поля устанавливается автоматически, на основании
предыдущего значения и шага автоинкремента. Это удобно, когда нужно, чтобы в
каком-либо столбце не было повторяющихся значений (обычно это первичный ключ).
Такое поведение регулируется свойствами Autoincrement, AutoIncrementSeed и
AutoIncrementStep. Значение AutoIncrementSeed используется для задания
начального значения в столбце, AutoIncrementStep — для задания числа, которое
прибавляется для каждого последующего значения. Рассмотрим следующую модификацию
создания carlDColumn:
static void FillDataSet(DataSet ds)
{
DataColumn carlDColumn = new DataColumn("CarlD" , typeof ( mt));
carlDColumn.Readonly = true;
carlDColumn.Caption = "Car ID";
carlDColumn.AllowDBNull = false;
carlDColumn.Unique = true;
carlDColumn.Autoincrement = true;
carlDColumn.AutoIncrementSeed = 0;
carlDColumn.AutoIncrementStep = 1;
}
Здесь объект carlDColumn настроен так, что при добавлении новых строк его
значения увеличиваются на 1. Поскольку первоначальное значение задано равным 0, в
столбце будут содержаться числа 0, 1, 2, 3 и т.д.
Добавление объектов DataColumn в DataTable
Обычно тип DataColumn не применяется обособленно, а вставляется в нужный
объект DataTable. Для демонстрации создайте новый объект DataTable (который вскоре
будет подробно рассмотрен) и вставьте каждый объект DataColumn в коллекцию
столбцов с помощью свойства Columns:
static void FillDataSet(DataSet ds)
{
// Добавление объектов DataColumn в DataTable.
DataTable inventoryTable = new DataTable ("In /ontor_, ") ;
inventoryTable.Columns.AddRange(new DataColumn[]
{ carlDColumn, carMakeColumn, carColorColumn, carPetNameColumn });
}
Теперь объект DataTable содержит четыре объекта DataColumn, которые
представляют схему находящейся в памяти таблицы Inventory. Но пока эта таблица еще
не содержит данных и не входит в коллекцию таблиц, принадлежащих конкретному
DataSet. Мы сделаем и то, и то, а начнем с заполнения таблицы данными с помощью
объектов DataRow.
Работа с объектами DataRow
Мы видели, что коллекция объектов DataColumn представляет схему объекта
DataTable. А коллекция объектов DataRow представляет конкретные данные в таблице.
И если на складе имеются 20 автомобилей, то для хранения информации о них нуж-
Глава 22. ADO.NET, часть II: автономный уровень 811
но 20 объектов Data Row. Некоторые (но не все) члены класса Data Row перечислены в
табл. 22.4.
Таблица 22.4. Основные члены типа DataRow
Член
Назначение
HasErrors
GetColumnsInError()
GetColumnError ()
ClearErrors()
RowError
ItemArray
RowState
Table
AcceptChanges()
RejectChangesO
BeginEdit()
EndEditO
CancelEditO
DeleteO
IsNullO
Свойство HasErrors возвращает логическое
значение, означающее наличие ошибок. Если они есть, то метод
GetColumnsInError () позволяет получить ошибочные
столбцы, а метод GetColumnError () — получить описание ошибки.
Аналогично, метод ClearErrorsO удаляет из строки всю
информацию об ошибках. Свойство RowError позволяет создать
текстовое описание ошибки для данной строки
Свойство, задающее или получающее все значения столбцов строки
в виде массива объектов
Свойство, позволяющее зафиксировать текущее состояние объекта
DataRow в содержащем его DataTable с помощью значений
перечисления RowState (новый, измененный, не измененный или удаленный)
Свойство, позволяющее получить ссылку на объект DataTable,
содержащий данный объект DataRow
Методы для фиксации или отмены всех изменений, выполненных в
данной строке с момента последнего вызова AcceptChanges ()
Методы, начинающие, заканчивающие или отменяющие операцию
редактирования для объекта DataRow
Метод, помечающий данную строку для удаления при вызове метода
AcceptChanges()
Метод, получающий значение, которое указывает, содержит ли
заданный столбец пустое значение
Работа с объектами DataRow несколько отличается от работы с DataColumn:
невозможно напрямую создать экземпляр данного типа, т.к. у него нет общедоступного
конструктора:
// Ошибка! Нет общедоступного конструктора!
DataRow г = new DataRow();
Вместо этого новый объект DataRow можно получить из конкретного DataTable.
Предположим, например, что в таблицу Inventory нужно вставить две строки. Метод
DataTable.NewRow() позволяет получить очередное место в таблице, после чего можно
заполнить каждый столбец с помощью индексатора типа. При этом можно указать либо
строковое имя, присвоенное объекту DataColumn, либо номер его позиции (начиная с
нуля):
static void FillDataSet(DataSet ds)
// Добавление нескольких строк в таблицу Inventory.
DataRow carRow = inventoryTable.NewRow ();
carRow["Make"] = "BMW";
carRow["Color"] = "Black";
carRow["PetName"] = "Hamlet";
inventoryTable.Rows.Add(carRow);
carRow = inventoryTable.NewRow() ;
812 Часть V. Введение в библиотеки базовых классов .NET
// Столбец 0 содержит автоинкрементное
// поле, поэтому начинаем с первого.
carRow[l] = "Saab";
carRow[2] = "Red";
carRow[3] = "Sea Breeze";
inventoryTable.Rows.Add(carRow);
}
На заметку! Если передать методу индексатора типа DataRow неверное имя столбца или
позицию, будет сгенерировано исключение времени выполнения.
Теперь у вас есть один объект DataTable, содержащий две строки. Конечно, этот
общий процесс можно повторить, чтобы создать ряд объектов DataTable, определить их
схемы и заполнить данными. Но прежде чем вставить объект inventoryTable в объект
DataSet, необходимо разобраться с очень важным свойством RowState.
Свойство RowState
Свойство RowState применяется для программной идентификации множества всех
строк таблицы, которые изменили свое первоначальное значение, были вставлены
и т.п. Это свойство может принимать любое значение из перечисления DataRowState.
Возможные значения приведены в табл. 22.5.
Таблица 22.5. Значения перечисления DataRowState
Значение Назначение
Added Строка была добавлена в DataRowCollection, a AcceptChanges () еще
не был вызван
Deleted Строка была помечена для удаления с помощью метода Delete () класса
DataRow, a AcceptChanges () еще не был вызван
Detached Строка была создана, но не включена ни в какой DataRowCollection.
Объект DataRow находится в этом состоянии после его создания, но до
занесения в какую-либо коллекцию, либо после исключения из коллекции
Modified Строка была изменена, a AcceptChanges () еще не был вызван
Unchanged Строка не была изменена после последнего вызова AcceptChanges ()
При программной работе со строками объекта DataTable значения в свойство
RowState заносятся автоматически. Для примера добавьте в свой класс Program новый
метод, который обрабатывает локальный объект DataRow, заодно и выводя состояние
его строк:
private static void ManipulateDataRowState ()
{
// Создание нового DataTable для демонстрационных целей.
DataTable temp = new DataTable("Temp")/
temp.Columns.Add(new DataColumn("TempColumn", typeof (int)));
// RowState = Detached (т.е. еще не принадлежит никакому DataTable).
DataRow row = temp.NewRow();
Console.WriteLine("После вызова NewRow(): {0}", row.RowState);
// RowState = Added.
temp.Rows.Add(row);
Console.WriteLine("После вызова Rows.Add(): {0}", row.RowState);
Глава 22. ADO.NET, часть II: автономный уровень 813
// RowState = Added.
row["TempColumn"] = 10;
Console.WriteLine("После первого присваивания: {0}", row.RowState)/
// RowState = Unchanged.
temp.AcceptChanges() ;
Console.WriteLine("После вызова AcceptChanges(): {0}", row.RowState);
// RowState = Modified.
row["TempColumn"] =11;
Console.WriteLine("После второго присваивания: @}"r row.RowState);
// RowState = Deleted.
temp.Rows[0].Delete();
Console.WriteLine("После вызова Delete(): @}"r row.RowState);
}
Объект ADO.NET DataRow вполне разумно отслеживает свое состояние. Поэтому
владеющий этим объектом объект DataTable может определить добавленные, измененные
или удаленные строки. Это очень важная возможность DataSet, потому что когда
наступит время послать информацию в хранилище данных, будут отправлены только
измененные данные.
Свойство DataRowVersion
Кроме отслеживания текущего состояния строк с помощью свойства RowState,
объект DataRow отслеживает три возможные версии содержащихся в нем данных с
помощью свойства DataRowVersion. При первоначальном создании объект DataRow
содержит лишь одну копию данных, которая считается "текущей версией". Но при
программной работе с объектом DataRow (с помощью вызовов различных методов)
появляются дополнительные версии данных. Конкретнее, свойство DataRowVersion
может содержать любое значение соответствующего перечисления DataRowVersion (см.
табл. 22.6).
Таблица 22.6. Значения перечисления DataRowVersion
Значение Назначение
Current Представляет текущее значение строки, даже после выполнения изменений
Default Стандартный вариант DataRowState. Если значение DataRowState равно
Added, Modified или Deleted, то стандартной версией является Current.
Для значения DataRowState, равного Detached, стандартной версией
является Proposed
Original Представляет значение, первоначально вставленное в DataRow, или значение
при последнем вызове AcceptChanges ()
Proposed Значение строки, редактируемой в настоящий момент с помощью вызова
BeginEditO
Как показано в табл. 22.6, значение свойства DataRowVersion в большинстве
случаев зависит от значения свойства DataRowState. А как было сказано ранее, значение
свойства DataRowVersion автоматически изменяется при вызовах различных методов
объекта DataRow (а в некоторых случаях DataTable). Ниже представлена схема влияния
этих методов на значение свойства DataRowVersion произвольной строки.
• При изменении значения строки поле вызова метода DataRow.BeginEditO
становятся доступны значения Current и Proposed.
• При вызове метода DataRow.CancelEditO значение Proposed удаляется.
814 Часть V. Введение в библиотеки базовых классов .NET
• После вызова DataRow.EndEditO значение Proposed меняется на Current.
• После вызова метода DataRow.AcceptChangesO значение Original становится
равным значению Current. To же самое происходит и при вызове DataTable.
AcceptChanges().
• После вызова DataRow.RejectChanges () значение Proposed отбрасывается, и
версия становится равной Current.
Да, все это несколько запутанно — особенно из-за того, что в любой данный момент
времени DataRow может иметь, а может и не иметь все версии (при попытке
получения версии строки, которая в данный момент не отслеживается, возникнут
исключения времени выполнения). Но поскольку DataRow отслеживает три копии данных, то,
несмотря на эту сложность, можно без особого труда создать пользовательский
интерфейс, который позволяет конечному пользователю изменить значения, потом
передумать и отказаться от этих изменений или зафиксировать их, чтобы они хранились
постоянно. В остальной части этой главы вы увидите различные примеры использования
этих методов.
Работа с объектами DataTable
Тип DataTable определяет значительное количество членов, многие из которых
совпадают по именам и функциям с аналогичными членами DataSet. В табл. 22.7
приведены некоторые основные члены типа DataTable, кроме Rows и Columns.
Таблица 22.7. Основные члены типа DataTable
Член Назначение
CaseSensitive Указывает, чувствительны ли к регистру символов строковые
сравнения в таблице. По умолчанию равно false
ChildRelations Возвращает коллекцию дочерних отношений для данного DataTable
(если они есть)
Constraints Получает коллекцию ограничений, поддерживаемых данной таблицей
Сору () Метод, копирующий схему и дату DataTable в новый экземпляр
DataSet Получает DataSet, содержащий данную таблицу (если он есть)
Def aultview Получает специализированное представление таблицы, которое может
содержать отфильтрованное представление или позицию курсора
ParentRelations Получает коллекцию родительских отношений для данного DataTable
PrimaryKey Получает или задает массив столбцов, которые выступают в качестве
первичных ключей для таблицы данных
RemotingFormat Позволяет определить формат сериализации объектом DataSet его
содержимого (двоичный или XML) для уровня .NET Remoting
TableName Получает или задает имя таблицы. Значение этого свойства можно
также задать через параметр конструктора
В продолжение нашего примера занесем в свойство PrimaryKey объекта DataTable
объект DataColumn по имени carlDColumn. Учтите, что свойству PrimaryKey
соответствует коллекция объектов DataColumn, чтобы можно было обрабатывать ключи из
нескольких столбцов. Но в нашем случае нужно указать только столбец CarlD (который
находится в таблице в самой первой позиции):
Глава 22. ADO.NET, часть II: автономный уровень 815
static void FillDataSet(DataSet ds)
{
// Указание первичного ключа для данной таблицы.
inventoryTable.PrimaryKey = new DataColumn[] { inventoryTable.Columns[0] };
}
Вставка объектов DataTable в DataSet
Наш объект DataTable завершен. Осталось вставить его в объект carsInventoryDS
типа DataSet с помощью коллекции Tables:
static void FillDataSet(DataSet ds)
{
// После чего добавление таблицы в DataSet.
ds.Tables.Add(inventoryTable);
Теперь нужно вставить в метод Main() вызов FillDataSet () и передать ему в
качестве аргумента локальный объект DataSet. Затем передадим этот объект новому (еще
не написанному) вспомогательному методу PrintDataSetO:
static void Main(string[] args)
{
Console.WriteLine ("***** Работа с объектами DataSet *****\n");
FillDataSet(carsInventoryDS);
PrintDataSet(carsInventoryDS);
Console.ReadLine();
}
Получение данных из объекта DataSet
Метод PrintDataSetO просто перебирает все метаданные DataSet (используя
коллекцию ExtendedProperties) и все DataTable в этом DataSet, выводя имена столбцов
и значения строк с помощью индексаторов:
static void PrintDataSet(DataSet ds)
{
// Вывод имени и расширенных свойств.
Console.WriteLine("Имя DataSet: {0}", ds.DataSetName);
foreach (System.Collections.DictionaryEntry de in ds.ExtendedProperties)
{
Console.WriteLine("Ключ = {0}, Значение = {1}", de.Key, de.Value);
}
Console.WriteLine();
// Вывод каждой таблицы.
foreach (DataTable dt in ds.Tables)
{
Console.WriteLine("=> Таблица {0}:", dt .TableName);
// Вывод имен столбцов.
for (int curCol = 0; curCol < dt.Columns.Count/ curCol++)
{
Console.Write(dt.Columns[curCol].ColumnName + "\t");
}
Console.WriteLine ("\n ") ;
// Вывод содержимого DataTable.
for (int curRow = 0; curRow < dt.Rows.Count; curRow++)
816 Часть V. Введение в библиотеки базовых классов .NET
for (int curCol = 0; curCol < dt.Columns.Count/ curCol++)
{
Console.Write(dt.Rows[curRow][curCol].ToStringO + "\t");
}
Console.WriteLine ();
}
}
}
Если теперь запустить эту программу, буде получен следующий результат (конечно,
с другими отметками времени и значением GUID):
***** Работа с объектами DataSet *****
Имя DataSet: Car Inventory
Ключ = TimeStamp, Значение = 1/22/2010 6:41:09 AM
Ключ = DataSetID, Значение = Ilc533ed-dlaa-4c82-96d4-b0f88893ab21
Ключ = Company, Значение = Супер-гипер-магазин Mikko
=> Таблица Inventory:
CarlD Make Color Др.Имя
0 BMW Black Hamlet
1 Saab Red Sea Breeze
Обработка данных из DataTable с помощью
объектов DataTableReader
Вспомните вашу работу в предыдущем примере, и вы поймете, что способы
обработки данных с помощью подключенного уровня (т.е. с помощью объектов чтения данных)
и автономного уровня (т.е. с помощью объектов DataSet) весьма различаются. При
работе с объектом чтения данных обычно пишется цикл while, вызывается метод Read(),
и с помощью индексатора выбираются пары имя/значение. А при обработке DataSet
нужен целый ряд циклических конструкций, чтобы добраться до данных,
находящихся в таблицах, строках и столбцах (не забывайте, что для DataReader нужно открытое
подключение к базе данных, чтобы он мог прочитать данные из реальной базы).
Объекты DataTable поддерживают метод CreateDataReader(). Этот метод
позволяет получать данные из DataTable с помощью схемы навигации, похожей на тип
чтения данных (объект чтения данных читает данные из находящейся в памяти таблицы
DataTable, а не из реальной базы данных, поэтому здесь подключение к базе данных
не требуется). Основное преимущество такого подхода состоит в том, что теперь при
обработке данных используется единая модель, независимо от уровня ADO.NET,
применяемого для получения этих данных. Допустим, в класс Program добавлена следующая
вспомогательная функция по имени PrintTable():
static void PrintTable(DataTable dt)
{
// Создание объекта DataTableReader.
DataTableReader dtReader = dt.CreateDataReader();
// DataTableReader работает так же, как и DataReader.
while (dtReader.Read())
{
for (int 1=0; l < dtReader.FieldCount; i++)
{
Console.Write ("{0}\t", dtReader.GetValue(l) .ToStringO .Trim());
}
Console.WriteLine () ;
}
dtReader.Close();
}
Глава 22. ADO.NET, часть II: автономный уровень 817
Тип DataTableReader работает точно так же, как и объект чтения данных
конкретного поставщика данных. Он очень удобен, если нужно быстро загрузить
данными объект DataTable, не путаясь во внутренних коллекциях строк и столбцов. А
теперь изменим предыдущий метод PrintDataSet(), чтобы в нем применялись вызовы
PrintTableO, а не перебор коллекций Rows и Columns:
static void PrintDataSet(DataSet ds)
(
// Вывод имени и всех расширенных свойств.
Console.WriteLine("Имя DataSet: {0}", ds.DataSetName);
foreach (System.Collections.DictionaryEntry de in ds.ExtendedProperties)
{
Console.WriteLine("Ключ = {0}, Значение = {1}", de.Key, de.Value);
}
Console.WriteLine();
foreach (DataTable dt in ds.Tables)
{
Console.WriteLine ("=> Таблица {0}:", dt.TableName) ;
// Вывод имен столбцов.
for (int curCol = 0; curCol < dt.Columns.Count; curCol++)
{
Console.Write(dt.Columns[curCol] .ColumnName.Trim () + "\t");
}
Console .WriteLine ("\n ") ;
// Вызов нового вспомогательного метода.
PrintTable(dt);
После запуска этого приложения вы увидите выходные данные, полностью
совпадающие с приведенными выше. Единственным отличием будет внутренний доступ к
содержимому DataTable.
Сериализация объектов DataTable и DataSet в формате XML
И тип DataSets, и тип DataTables поддерживают методы WriteXmlO и ReadXml().
Метод WriteXmlO позволяет сохранить содержимое объекта в локальном файле (а
также в любом типе, производном от System. 10.Stream) в виде XML-документа. Метод
ReadXmlO позволяет получить состояние DataSet (или DataTable) из XML-документа.
Кроме этого, типы DataSet и DataTable поддерживают методы WriteXmlSchema ()
и ReadXmlSchemaO, которые сохраняют в файле *.xsd только схему и загружают ее
оттуда.
Вы можете проверить их работу самостоятельно. Для этого измените метод Main(),
чтобы он вызывал следующую финальную вспомогательную функцию (которой
передается единственный параметр типа DataSet):
static void DataSetAsXml(DataSet carsInventoryDS)
I
// Сохранение данного DataSet в виде XML.
carsInventoryDS.WriteXml("carsDataSet.xml");
carsInventoryDS.WriteXmlSchema("carsDataSet.xsd");
// Очистка DataSet.
carsInventoryDS.Clear() ;
// Загрузка DataSet из XML-файла.
carsInventoryDS.ReadXml("carsDataSet.xml")/
818 Часть V. Введение в библиотеки базовых классов .NET
Если открыть файл carsDataSet.xml (который находится в папке \bin\Debug
вашего проекта), то можно увидеть, что каждый столбец таблицы закодирован в виде
XML- элемента:
<?xml version="l.0м standalone="yes"?>
<Car_x0020_Inventory>
<Inventory>
<CarID>0</CarID>
<Make>BMW</Make>
<Color>Black</Color>
<PetName>Hamlet</PetName>
</Inventory>
<Inventory>
<CarID>K/CarID>
<Make>Saab</Make>
<Color>Red</Color>
<PetName>Sea Breeze</PetName>
</Inventory>
</Car_x0020_Inventory>
Если в Visual Studio дважды щелкнуть на сгенерированном .xsd-файле (который
также находится в папке \bin\Debug), то откроется встроенный редактор схемы XML
(рис. 22.3).
carsDataSetJtsd
%& Inventory
* CarlD
Make
Color
PetName
Рис. 22.3. Редактор XSD из Visual Studio 2010
На заметку! В главе 24 будет описан LINQ to XML API, который сейчас рекомендуется для
обработки XML-данных в платформе .NET.
Сериализация объектов DataTable
и DataSet в двоичном формате
Содержимое объекта DataSet (или отдельного DataTable) можно также сохранить
в компактном двоичном формате. Это особенно удобно при необходимости переслать
объект DataSet за пределы компьютера (в случае распределенного приложения), ведь
одним из недостатков представления данных в виде XML является его многословность,
что приводит к большому объему данных.
Чтобы сохранить объект DataTable или DataSet в двоичном формате, просто
занесите в свойство RemotingFormat значение SerializationFormat.Binary. После
этого, как нетрудно догадаться, можно воспользоваться типом BinaryFormatter (см.
Глава 22. ADO.NET, часть II: автономный уровень 819
главу 20). Рассмотрим следующий финальный метод проекта SimpleDataSet (не
забудьте импортировать пространства имен System.10 и System.Runtime.Serialization.
Formatters. Binary):
static void DataSetAsBinary(DatiSet carsInventoryDS)
{
// Установка признака двоичной сериализации.
carsInventoryDS. RemotingFormat = SenalizationFormat .Binary ;
// Сохранение данного DataSet в двоичном виде.
FileStream fs = new FileStream("BinaryCars.bin", FileMode.Create);
BinaryFormatter bFormat = new BinaryFormatter();
bFormat.Serialize(fs, carsInventoryDS);
fs.Close();
// Очистка DataSet.
carsInventoryDS.Clear ();
// Загрузка DataSet из двоичного файла.
fs = new FileStream("BinaryCars.bin", FileMode.Open);
DataSet data = (DataSet)bFormat.Deserialize(fs);
}
После выполнения этого метода из Main() файл *.bin будет находиться в папке bin\
Debug. На рис. 22.4 показано содержимое файла BinaryCars.bin.
BinaryCars.bin X
carsDataSetjesd Prograrn.cs Object Brovw
00002890
000028а0
000028Ь0
000028с0
000028d0
000028е0
000028f0
00002900
00002910
00002920
00002930
00002940
00002950
00002960
00002970
00002980
00002990
000029а0
000029Ь0
00 00 00
00 00 00
65 6D 2Е
62 02 5F
02 5F 68
00 00 00
02 02 АЕ
21 29 01
05 00 00
00 08 00
00 00 00
100 04 53
2D 00 00
52 65 64
log о$ 48
00 10 1А
00 00 00
47 75 69
63 02 5F
02 5F 69
00 00 00
A3 DB 7D
21 00 00
00 05 00
00 00 00
06 2В 00
61 20 42
00 09 31
00 00 00
02 00 00
61 61 62
00 05 42
11 25 00
61 6D 6С
00 00
04 IF
64 0В
64 02
02 5F
00 08
39 9С
00 09
00 00
01 00
00 00
72 65 65
00 00 00
ОС 00 00
00 01 28
11 24
6С 61
00 00
65 74
7А 65
02 00
00 09
00 00
00 00
00 00
00 00
5F 65
6А 02
07 07
D5 4F
00 00
0F 22
00 00
03 D2
00 00
63 6В
02 00
_0бГ30~
01 26
00 00
32 00
00 ОС
00 00 00 10 1В 00
00 0В 53 79 73 74 Syst
.Guid.
b._c._d._e
•_h._i._j.
!).
.}9..0.
00 02 5F 61 02 5F
02 5F 66 02 5F 67
5F 6B 00 00 00 00
02 02 02 02 02 02
93 DB 74 BB FF 72
00 09 2A 00 00 00
00 00 00 02 00 00
11 23 00 00 00 02 #
4D 57 06 2C 00 00|
00 02 00 00 00 06
06 2E 00 00 00 03
00 00 06 2F 00 QQl
00 00 00 0A 53 65
00 00 00 0C 00 00
02 00 00 00 01 27
00 00 02 00 00 00
00 00 00 09 33 00
Рис. 22.4. Сериапизация объекта DataSet в двоичном формате
Исходный код. Проект SimpleDataSet доступен в подкаталоге Chapter 22.
Привязка объектов DataTable к графическим
интерфейсам Windows Forms
К данному моменту вы уже научились вручную создавать, заполнять и
просматривать содержимое объекта DataSet с помощью внутренней объектной модели ADO.NET.
Все это довольно важно, но с платформой .NET поставляются многочисленные API-
интерфейсы, которые автоматически могут связывать данные с элементами
пользовательского интерфейса.
Например, в Windows Forms — "родном" инструментальном наборе GUI .NET —
имеется элемент управления D at aG r id View, который может отображать содержимое
объекта DataSet или DataTable с помощью всего нескольких строк кода. ASP.NET (API-
820 Часть V. Введение в библиотеки базовых классов .NET
интерфейс для веб-разработки в составе .NET) и Windows Presentation Foundation API
(новый, гораздо более мощный API-интерфейс для построения графических
пользовательских интерфейсов, появившийся в .NET 3.0) тоже поддерживают привязку данных.
Вы научитесь привязывать данные к графическим элементам WPF и ADO.NET немного
позже; но в данной главе вы уже начнете применять Windows Forms, т.к. это довольно
простая и понятная модель программирования.
На заметку! В следующем примере предполагается, что у вас есть некоторый опыт использования
Windows Forms для создания графических пользовательских интерфейсов. Если это не так,
можете просто открыть готовое решение и проследить за изложением, либо вернуться к данному
разделу после прочтения приложения А.
Теперь мы создадим приложение Windows Forms, которое будет выводить в
пользовательском интерфейсе содержимое объекта DataTable. Заодно вы научитесь
фильтровать и изменять данные в таблице, а также познакомитесь с ролью объекта DataView.
Сначала создайте новое рабочее пространство проекта Windows Forms по имени
WindowsFormsDataBinding. Смените в Solution Explorer имя первоначального файла
Forml.cs на более понятное MainForm.cs. Затем в Visual Studio 2010 Toolbox
перетащите элемент DataGridView (переименованный в carlnventoryGridView с помощью
свойства Name в окне Properties (Свойства)) на поверхность конструктора. При этом
активизируется контекстное меню, которое позволяет выполнить подключение к
физическому источнику данных. Пока не обращайте на это внимание, поскольку привязка
объекта DataTable будет выполнена программно. И, наконец, добавьте метку с понятным
текстом. Один из возможных вариантов приведен на рис. 22.5.
Рис. 22.5. Первоначальный пользовательский интерфейс приложения Windows Forms
Заполнение DataTable из обобщенного List<T>
Аналогично предыдущему примеру SimpleDataSet, в приложении WindowsForms
DataBmding будет создан объект DataTable, содержащий несколько DataColumn,
которые будут представлять различные столбцы и строки данных. Но на этот раз строки
будут заполняться с помощью переменной-члена List<T>. Сначала вставьте в проект
новый класс С# с именем Саг, определенный следующим образом:
Глава 22. ADO.NET, часть II: автономный уровень 821
public class Car
{
public int ID { get; set; }
public string PetName { get; set; }
public string Make { get; set; }
public string Color { get; set; }
}
Теперь добавьте в стандартный конструктор главной формы заполнение
переменной-члена List<T> (с именем listCars) набором новых объектов Саг:
public partial class MainForm : Form
{
// Коллекия объектов Car.
List<Car> listCars = null;
public MainForm()
{
InitializeComponent () ;
// Добавление в список нескольких автомобилей.
listCars = new List<Car>()
{
new Car { ID = 100, PetName = "Списку", Make = "BMW", Color = "Green" },
new Car { ID = 101, PetName = "Tiny", Make = "Yugo", Color = "White" },
new Car { ID = 102, PetName = "Ami", Make = "Jeep", Color = "Tan" },
new Car { ID = 103, PetName = "Pain Inducer", Make = "Caravan", Color = "Pink" },
new Car { ID = 104, PetName = "Fred", Make = "BMW", Color = "Green" },
new Car { ID = 105, PetName = "Sidd", Make = "BMW", Color = "Black" },
new Car { ID = 106, PetName = "Mel", Make = "Firebird", Color = "Red" },
new Car { ID = 107, PetName = "Sarah", Make = "Colt", Color = "Black" },
};
}
}
Добавьте в класс MainForm новую переменную-член типа DataTable по имени
inventoryTable:
public partial class MainForm : Form
{
// Коллекция объектов Car.
List<Car> listCars = null;
// Информация об автомобилях на складе.
DataTable inventoryTable = new DataTable ();
Теперь добавьте в этот же класс новую вспомогательную функцию CreateDataTable (),
а ее вызов — в стандартный конструктор класса MainForm:
private void CreateDataTable ()
{
// Создание схемы таблицы.
DataColumn carlDColumn = new DataColumn("ID", typeof(int)) ;
DataColumn carMakeColumn = new DataColumn("Make", typeof(string));
DataColumn carColorColumn = new DataColumn("Color", typeof(string));
DataColumn carPetNameColumn = new DataColumn("PetName", typeof(string));
carPetNameColumn.Caption = "Pet Name";
inventoryTable.Columns.AddRange (new DataColumn[] { carlDColumn,
carMakeColumn, carColorColumn, carPetNameColumn });
822 Часть V. Введение в библиотеки базовых классов .NET
// Последовательное создание строк из элементов списка.
foreach (Car с in listCars)
{
DataRow newRow = inventoryTable.NewRow();
newRow["Make"] = c.carMake;
newRow["Color"] = c.carColor/
newRow["PetName"] = с.carPetName;
inventoryTable.Rows.Add(newRow)/
}
// Привязка DataTable к carlnventoryGridView.
carlnventoryGridView.DataSource = inventoryTable;
}
Реализация метода начинается с создания схемы DataTable; для этого создаются
три объекта DataColumn (для простоты автоинкрементное поле CarlD не добавлялось),
а потом они добавляются в переменную-член DataTable. Содержимое строк
переносится из поля List<T> в DataTable с помощью конструкции foreach и объектной модели
ADO.NET.
В последнем операторе кода метода CreateDataTableO таблица inventoryTable
присваивается свойству DataSource объекта DataGridView. Данное свойство —
единственное, что нужно для привязки DataTable к объекту DataGridView. Этот
графический элемент выполняет внутреннее чтение коллекций строк и столбцов, примерно
так же, как это делал метод PrintDataSetO в примере SimpleDataSet. Теперь
можно запустить приложение и увидеть содержимое DataTable в элементе DataGridView
(рис. 22.6).
ndows Forms Data Bmdin
Here is what we have in stock
HDBB
►
ID
101
102
103
104
105
106
1П7
Make
BMW
Yugo
Jeep
Caravan
BMW
BMW
Firebird
n*
Color
Green
White
Tan
P«*
Green
Black
Red
ffcrt
PetName Ш *|
Cbucky
Tiny
a™ W-\
Pain Inducer
Fred
Skid ILJ
Mel
S*ah iH
Рис. 22.6. Привязка объекта DataTable к элементу DataGridView из Windows Forms
Удаление строк из DataTable
Теперь добавим к графическому интерфейсу возможность удалять строки из
находящегося в памяти объекта DataTable, который привязан к элементу DataGridView.
Один из возможных подходов — вызов метода Delete () объекта DataRow, который
содержит удаляемую строку. Для этого нужно просто указать индекс (или объект
DataRow), представляющий эту строку. Чтобы пользователь мог указать, какую
строку необходимо удалить, добавьте в конструктор элементы TextBox (txtRowToRemove) и
Button (btnRemoveRow). На рис. 22.7 показан один из возможных вариантов изменения
интерфейса (обратите внимание на группирование двух элементов с помощью элемента
GroupBox, которое подчеркивает их связь).
Глава 22. ADO.NET, часть II: автономный уровень 823
°&<
:*• ID of Car to Delete
Рис. 22.7. Измененный интерфейс для удаления строк из DataTable
Ниже приведен код обработчика события Click новой кнопки, который удаляет
указанную пользователем строку (по идентификатору автомобиля) из находящегося в
памяти объекта DataTable. Метод Select () из класса DataTable позволяет указать
критерий поиска, который похож на обычный синтаксис SQL. Метод возвращает массив
объектов, удовлетворяющих критерию поиска:
// Удаление данной строки из DataRowCollection.
private void btnP«=moveRow_Click (object sender, EventArgs e)
{
try
{
// Поиск строки, которую нужно удалить.
DataRow[] rowToDelete = inventoryTable.Select(
string.Formиt("ID={0}", int.Parse(txtCarToRemove.Text)));
// Удаление.
rowToDelete [0] .Delete() ;
inventoryTable.AcceptChanges();
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
Теперь можно запустить приложение и указать идентификатор удаляемого
автомобиля. При удалении объектов DataRow из DataTable графическая таблица реагирует
моментально, т.к. она привязана к состоянию объекта.
Выборка строк с помощью фильтра
Во многих приложениях обработки данных бывает необходимо просматривать
небольшое подмножество данных из DataTable на основании какого-то критерия
фильтрации. Например, может понадобиться просмотреть только автомобили BMW, которые
хранятся в памяти в объекте DataTable. Вы уже видели, как метод Select () класса
DataTable позволяет найти строгсу для удаления, но его можно использовать и для
выборки подмножества записей для их отображения.
824 Часть V. Введение в библиотеки базовых классов .NET
В качестве иллюстрации еще раз изменим наш пользовательский интерфейс. Теперь
пользователи получат возможность задавать модель автомобиля, которую они хотели бы
просмотреть (рис. 22.8) с помощью новых элементов Text Box (с именем txtMakeToView)
и Button (с именем btnDisplayMakes).
MatnForm.cs [Design]* X I
"'j Windows Forms Data Binding
Here is what we have in stock
hi ШЫ
Enter ID of Car to Delete
°ШегМ
Рис. 22.8. Добавление возможности фильтрации строк
Метод Select () имеет несколько перегруженных вариантов, предоставляющих
различные возможности выборки. На самом простом уровне передаваемый в Select ()
параметр является строкой, которая содержит какое-то условное выражение. Для
начала рассмотрим следующую логику обработки события Click только что добавленной
кнопки:
private void btnDisplayMakes_Click(object sender, EventArgs e)
{
// Создание фильтра на основании введенных пользователем данных.
string filterStr = string.Format("Make= '{О}'", txtMakeToView.Text);
// Поиск всех строк, удовлетворяющих фильтру.
DataRow[] makes = inventoryTable.Select(filterStr);
if (makes.Length == 0)
MessageBox.Show("Sorry, no cars...", "Selection error!"); // ничего не найдено
else
{
string strMake = null;
for (int i=0; i < makes.Length; i++)
{
// Получение значения PetName из текущей строки.
strMake += temp["PetName"] + "\n";
}
// Вывод названий всех найденных автомобилей указанной марки.
MessageBox.Show(strMake,
string.Format("We have {0}s named:", txtMakeToView.Text));
Здесь сначала создается простой фильтр на основе значения из соответствующего
поля Text Box. Если в этом поле указать BMW, то получится следующий фильтр:
Make = 'BMW
Глава 22. ADO.NET, часть II: автономный уровень 825
Если передать этот фильтр методу Select (), он возвратит массив объектов DataRow
со строками, удовлетворяющими заданному критерию (рис. 22.9).
%* Windows Forms Data Binding
Here is what we have in stock
Cdor
Green
Pet Name
Tchucky
-eBMWsn^I
Enter ID erf Car to Delete
Списку
Fred
81} Sidd
Рис. 22.9. Вывод фильтрованных данных
Логика фильтрации основана на стандартом синтаксисе SQL. Для примера
предположим, что результаты, полученные от предыдущего вызова Select(), нужно выдать
упорядоченными по столбцу PetName. К счастью, имеется перегруженный вариант
метода Select (), позволяющий указать критерий сортировки:
// Сортировка по PetName.
makes = mventoryTable.Select(filterStr, "PetName");
При необходимости вывести результаты упорядоченными по убыванию вызов
SelectO выглядит так:
// Вывод результатов в порядке убывания.
makes = mventoryTable.Select(filterStr, "PetName DESC");
В общем случае строка с критерием сортировки должна содержать имя столбца, за
которым идет слово ASC ("ascending" — по возрастанию, подразумевается по
умолчанию) или DESC ("descending" — по убыванию). При необходимости можно указать
несколько столбцов, разделенных запятыми. И, наконец, строку с критерием фильтрации
можно составить из любого количества реляционных операторов. Например, вот
вспомогательная функция, которая выполняет поиск всех автомобилей с идентификатором,
большим 5:
private void ShowCarsWithldGreaterThanFive()
{
// Вывод дружественных имен всех автомобилей с ID, большим 5.
DataRow[] properlDs;
string newFilterStr = "ID > 5";
properlDs = mventoryTable.Select(newFilterStr);
string strlDs = null;
for(int i=0; l < properlDs.Length; i++)
{
DataRow temp = properlDs[1];
strlDs += temp ["PetName"] + " is ID " + temp ["ID"] + "\n";
MessageBox.Show(strlDs, "Pet names of cars where ID > 5");
826 Часть V. Введение в библиотеки базовых классов .NET
Изменение строк в DataTable
Последний аспект DataTable, с которым следует ознакомиться — это процесс
изменения существующих строк, т.е. занесения в них новых значений. Один из способов
сделать это состоит в получении строк (или строки), удовлетворяющих заданному
критерию, с помощью метода Select (). После получения нужных DataRow остается
изменить их содержимое. Допустим, например, что на поверхности, производной от формы,
находится новый элемент Button по имени btnChangeMakes, при щелчке на котором
выполняет поиск в DataTable всех строк, где столбец Make содержит значение BMW.
После получения этих элементов нужно занести в Make значение Yugo:
// Поиск с помощью фильтра всех строк, которые нужно изменить.
private void btnChangeMakes_Click(object sender, EventArgs e)
{
// Проверка выбора пользователя.
if (DialogResult.Yes ==
MessageBox.Show("Are you sure?? BMWs are much nicer than Yugos'",
"Please Confirm!", MessageBoxButtons . YesIIn) )
{
// Создание фильтра.
string filterStr = "Make='BMW1";
string strMake = string.Empty;
// Поиск всех строк, удовлетворяющих фильтру.
DataRow[] makes = inventoryTable.Select(filterStr);
// Замена всех Бумеров на Yugo.
for (int i = 0; i < makes.Length; i++)
{
makes[i]["Make"] = "Yugo";
}
}'
}
Работа с типом DataView
Объект представления (view object) — это альтернативное представление таблицы
(или набора таблиц). Например, с помощью Microsoft SQL Server можно создать
представление для таблицы Inventory, которое возвращает новую таблицу, содержащую
автомобили только определенного цвета. В ADO.NET тип DataView позволяет
программным образом извлекать подмножество данных из DataTable в отдельный объект.
Серьезным преимуществом наличия нескольких представлений одной и той же
таблицы является то, что все эти представления можно привязать к различным
графическим элементам (наподобие DataGridView). Например, один DataGridView может быть
привязан к DataView, показывающему все автомобили из таблицы Inventory, а другой
можно настроить на вывод лишь автомобилей зеленого цвета.
Для примера добавьте в текущий графический интерфейс еще один элемент
DataGridView по имени dataGridColtsView и элемент Label с пояснением. Потом
определите переменную-член типа DataView по имени coltsOnlyView:
public partial class MainForm : Form
{
// Отображение содержимого DataTable.
DataView coltsOnlyView;
Глава 22. ADO.NET, часть II: автономный уровень 827
Затем создайте новую вспомогательную функцию CreateDataView () и поместите ее
вызов в конструктор по умолчанию формы сразу за завершением создания DataTable:
public MainFormO
{
// Создание таблицы данных.
CreateDataTable();
// Создание представления.
CreateDataView();
}
Ниже показана реализация этой новой вспомогательной функции. Конструктору
каждого DataView передается объект DataTable, который будет использован для
создания специального набора строк данных.
private void CreateDataView ()
{
// Указание таблицы для создания данного представления.
coltsOnlyView = new DataView(inventoryTable);
// Настройка представлений с помощью фильтра.
coltsOnlyView.RowFilter = "Make = ' Yugo'";
// Привязка к новой графической таблице.
dataGridColtsView.DataSource = yugosOnlyView;
}
Как видно, класс DataView содержит свойство по имени RowFilter,
содержащее строку с критерием фильтрации, который используется для извлечения искомых
строк. После создания представления измените соответствующим образом свойство
DataSource графической таблицы. Завершенное приложение в действии показано на
рис. 22.10.
Рис. 22.10. Вывод уникального представления данных
828 Часть V. Введение в библиотеки базовых классов .NET
Исходный код. Проект WindowsFormsDataTableViewer доступен в подкаталоге Chapter 22.
Работа с адаптерами данных
Мы уже разобрались со всеми нюансами ручной работы с объектами DataSet в
ADO.NET, и теперь можно перейти к рассмотрению объектов адаптеров данных. Класс
адаптеров данных применяется для заполнения наборов данных DataSet с помощью
объектов DataTable; кроме того, они могут отправлять измененные DataTable назад в
базу данных для обработки. В табл. 22.8 перечислены основные члены базового класса
DbDataAdapter, от которого порождаются все объекты адаптеров данных (например,
SqlDataAdapter и OdbcDataAdapter).
Таблица 22.8. Основные члены класса DbDataAdapter
Член Назначение
Fill 0 Выполняет команду SQL SELECT (указанную в свойстве SelectCommand)
для запроса к базе данных и загрузки этих данных в объект DataTable
SelectCommand Содержат SQL-команды, отправляемые в хранилище данных при вызовах
InsertCommand методов Fill () и Update ()
UpdateCommand
DeleteCommand
Update () Выполняет команды SQL INSERT, UPDATE и DELETE (указанных
свойствами InsertCommand, UpdateCommand и DeleteCommand) для
сохранения в базе данных изменений, выполненных в DataTable
Адаптер данных определяет четыре свойства: SelectCommand, InsertCommand,
UpdateCommand и DeleteCommand. При создании объекта адаптера данных для
конкретного поставщика данных (например, SqlDataAdapter) можно передать строку с
текстом команды, используемом объектом команды SelectCommand.
После должной настройки каждого из четырех объектов команд можно вызвать
метод Fill() и получить объект DataSet (или, при желании, отдельный DataTable). Для
этого объект команды выполняет оператор SQL SELECT, заданный с помощью свойства
SelectCommand.
Аналогично, при необходимости сохранить измененный объект DataSet (или
DataTable) в базе данных для обработки можно вызвать метод Update (), который
использует какой-то из оставшихся объектов команд в зависимости от состояния каждой
строки в DataTable (подробнее чуть ниже).
Один из самых странных аспектов работы с объектом адаптера данных состоит в
том, что при этом не нужно открывать или закрывать подключение к базе данных. Все
это делается автоматически. Однако адаптеру данных нужно передать объект
подключения или строку подключения (на основании которой все равно будет создан объект
подключения), чтобы сообщить адаптеру данных, с какой базой данных вы хотите
взаимодействовать.
На заметку! Адаптер данных безразличен по своей природе. Вы можете подключать на ходу
разные объекты подключения и объекты команд и выбирать данные из самых различных баз
данных. Например, один объект DataSet может содержать табличные данные, полученные от
поставщиков данных SQL Server, Oracle и MySQL.
Глава 22. ADO.NET, часть II: автономный уровень 829
Простой пример адаптера данных
Теперь добавим новые возможности в библиотеку доступа к данным AutoLotDAL.dll,
созданную в главе 21. Вначале рассмотрим простой пример, в котором объект Data Set
заполняется одной таблицей с помощью объекта адаптера данных ADO.NET.
Создайте новое консольное приложение с именем FillDataSetUsingSqlDataAdapter
и импортируйте в первоначальный код С# пространства имен System.Data и System.
Data.SqlClient. Теперь измените метод Main() следующим образом (в зависимости от
того, как вы создали базу данных AutoLot в предыдущей главе, может понадобиться
изменить строку подключения):
static void Main(string [ ] args)
{
Console.WriteLine ("***** Работа с адаптерами данных *~***\п");
// Жестко закодированная строка подключения.
string cnStr = "Integrated Security = SSPI;Initial Catalog=AutoLot;" +
@"Data Source=(local)\SQLEXPRESS";
// Создание объекта DataSet вызывающим процессом.
DataSet ds = new DataSet ("AutoLot");
// Передача адаптеру текста команды Select и подключения.
SqlDataAdapter clAdapt =
new SqlDataAdapter ("Select * From Inventory", cnStr);
// Заполнение DataSet новой таблицей с именем Inventory.
dAdapt.Fill(ds, "Inventory");
// Вывод содержимого DataSet.
PrintDataSet(ds);
Console.ReadLine();
}
Для создания адаптера данных используется строковый литерал, который
преобразуется в SQL-оператор Select. Из этого значения адаптер создает объект команды,
который потом можно получить с помощью свойства SelectCommand.
Обратите внимание, что экземпляр класса DataSet должен быть создан вызывающим
процессом, и уже затем передан в метод Fill(). В этот метод можно передать второй,
не обязательный, аргумент — строку, с помощью которой будет сформировано свойство
TableName нового объекта DataTable (если не указать имя таблицы, то адаптер данных
назовет таблицу просто Table). Обычно имя, присвоенное DataTable, совпадает с
именем физической таблицы в реляционной базе данных, но это не обязательно.
На заметку! Метод Fill() возвращает целое число, равное количеству строк, возвращенных
SQL-запросом.
И, наконец, обратите внимание, что в методе Main () нигде нет явного открытия или
закрытия подключения к базе данных. В методе Fill() любого адаптера данных с
самого начала заложена возможность открытия и закрытия подключения перед вызовом
метода Fill(). Поэтому при передаче объекта DataSet методу PrintDataSet ()
(реализованному выше в данной главе) вы оперируете с локальной копией автономных
данных, для которой не нужны дополнительные выборки данных.
Замена имен из базы данных более понятными названиями
Как уже было сказано, администраторы баз данных обычно создают такие имена
таблиц и столбцов, что они редко бывают удобны для конечных пользователей
(например, auid, aufname, aulname и т.д.). Но в жизни есть место и хорошему: объ-
830 Часть V. Введение в библиотеки базовых классов .NET
екты адаптеров данных содержат внутреннюю строго типизированную коллекцию
DataTableMappingCollection объектов типа System.Data.Common.DataTableMapping.
Доступ к этой коллекции возможен через свойство TableMappings объекта адаптера
данных.
При желании можно поработать с этой коллекцией, чтобы сообщить объекту
DataTable, какие отображаемые имена следует использовать при выводе его
содержимого. Предположим, например, что при выводе информации вместо имени таблицы
Inventory лучше использовать заголовок "В наличии". Кроме того, допустим, мы хотим
назвать столбец Car ID как "Номер", a PetName — как "Название". Для этого перед
вызовом метода Fill () объекта адаптера данных нужно добавить следующий код (и еще
надо импортировать пространство имен System.Data.Common, чтобы иметь
определение типа DataTableMapping):
static void Main(string [ ] args)
{
// Соответствие имен столбцов базы данных и понятных названий.
DataTableMapping custMap =
dAdapt.TableMappings.Add("Inventory", "В наличии");
custMap.ColumnMappings.Add("CarlD", "Номер");
custMap.ColumnMappings.Add("PetName", "Название");
dAdapt.Fill(myDS, "Inventory");
}
Если еще раз запустить эту программу, то теперь метод PrintDataSetO будет
выводить дружественные имена объектов DataTable и Data Row, а не имена из схемы в
базе данных:
***** работа с адаптерами данных *****
Имя DataSet: AutoLot
=> Таблица В наличии:
Номер
83
107
678
904
1000
1001
1992
2003
Make
Ford
Ford
Yugo
VW
BMW
BMW
Saab
Yugo
Color
Rust
Red
Green
Black
Black
Tan
Pink
Rust
Название
Rusty
Snake
Clunker
Hank
Bimmer
Daisy
Pinkey
Mel
Исходный код. Проект FillDataSetUsingSqlDataAdapter доступен в подкаталоге Chapter 22.
Добавление BAutoLotDAL.dll
возможности отключения
Чтобы продемонстрировать применение адаптера данных для внесения
модификаций из DataTable обратно в базу данных, мы сейчас изменим сборку
AutoLotDAL.dll, созданную в главе 21, чтобы она содержала новое пространство имен
(по имени AutoLotDisconnectedLayer). Это пространство имен будет содержать новый
класс InventoryDALDisLayer, который применяет адаптер данных для взаимодействия
с объектом DataTable.
Глава 22. ADO.NET, часть II: автономный уровень 831
Для начала лучше скопировать всю папку проекта AutoLot, который был создан в
главе 21, в новое место на жестком диске и переименовать ее в AutoLot (Version Two).
Теперь запустите Visual Studio 2010, выберите пункт меню File^Open Project/Solution...
(Файл1^ Открыть проект/решение...) и откройте файл AutoLotDAL. sin из папки
AutoLot (Version Two).
Определение начального класса
С помощью пункта меню Projects Add Class (Проекте Добавить класс) добавьте в
новый кодовый файл новый класс InventoryDALDisLayer. Этот новый класс должен
иметь тип public. Измените имя пространства имен, в которое упакован этот класс,
на AutoLotDisconnectedLayer и импортируйте пространства имен System.Data и
System.Data.SqlClient.
В отличие от типа InventoryDAL, ориентированного на работу с подключением,
этому новому классу не нужны специальные методы открытия и закрытия, т.к. адаптер
данных выполнит всю необходимую обработку автоматически.
Сначала добавьте собственный конструктор, который заносит значение строки
подключения в переменную string. Кроме того, определите приватную переменную-член
SqlDataAdapter, которая будет настраиваться с помощью (пока еще не созданного)
вспомогательного метода ConfigureAdapter(), который принимает выходной
параметр SqlDataAdapter:
namespace AutoLotDisconnectedLayer
{
public class InventoryDALDisLayer
{
// Значения полей.
private string cnString = string.Empty;
private SqlDataAdapter dAdapt = null;
public InventoryDALDisLayer(string connectionString)
{
cnString = connectionString;
// Настройка SqlDataAdapter.
ConfigureAdapter(out dAdapt);
}
}
}
Настройка адаптера данных с помощью SqlCommandBuilder
При использовании адаптера данных для модификации таблиц в наборе
данных DataSet вначале нужно указать в свойствах UpdateCommand, DeleteCommand и
InsertCommand допустимые объекты команд (до этого они содержат значения null).
Для ручного конфигурирования объектов команд для свойств InsertCommand,
UpdateCommand и DeleteCommand может понадобиться серьезный объем кода,
особенно если применяются параметризованные запросы. Вспомните: в главе 21 было
сказано, что параметризованные запросы позволяют создавать SQL-операторы с помощью
объектов параметров. И если мы готовимся к серьезной работе, то можем реализовать
метод ConfigureAdapter() для ручного создания трех новых объектов SqlCommand,
каждый из которых содержит набор объектов SqlParameter. Потом эти объекты можно
указать в свойствах адаптера UpdateCommand, DeleteCommand и InsertCommand.
В Visual Studio 2010 имеется целый ряд средств проектирования, которые берут на
себя хлопоты по составлению этого утомительного участка кода. Эти средства немного
отличаются в зависимости от используемого API (Windows Forms, WPF или ASP.NET), но
832 Часть V. Введение в библиотеки базовых классов .NET
их общие возможности очень похожи. Действие некоторых из них неоднократно будет
продемонстрировано в этой книге, в том числе и на примере конструкторов Windows
Forms ниже в данной главе.
Вам не понадобится писать многочисленные операторы кода, чтобы полностью
сконфигурировать адаптер данных; вместо этого мы существенно снизим объем
работы, реализовав метод ConfigureAdapter () следующим образом:
private void ConfigureAdapter (out SqlDataAdapter dAdapt)
{
// Создание адаптера и заполнение SelectCommand.
dAdapt = new SqlDataAdapter("Select * From Inventory", cnString);
// Динамическое получение остальных объектов команд
//во время выполнения с помощью SqlCommandBuilder.
SqlCommandBuilder builder = new SqlCommandBuilder(dAdapt);
}
Для упрощения создания объектов адаптеров данных в каждом поставщике
данных ADO.NET, разработанном Microsoft, имеется тип построителя команд (command
builder) — SqlCommandBuilder. Он автоматически генерирует значения в свойствах
InsertCommand, UpdateCommand и DeleteCommand объекта SqlDataAdapter на основе
первоначального объекта SelectCommand. Очень удобно, что не нужно создавать
вручную все типы SqlCommand и SqlParameter.
Возникает очевидный вопрос: как может построитель команд создать все эти
объекты команд на ходу? Краткий ответ на этот вопрос — метаданные. Когда во время
выполнения вызывается метод Update () адаптера данных, соответствующий построитель
команд читает информацию схемы из базы данных, чтобы автоматически генерировать
нужные объекты команд вставки, удаления и изменения.
Конечно, для этого нужны дополнительные обращения к удаленной базе данных,
а при неоднократном использовании SqlCommandBuilder в одном приложении его
производительность снизится. Здесь мы постараемся минимизировать негативный
эффект с помощью вызова метода Configure Ad ар ter() во время создания объекта
InventoryDALDisLayer и сохранения настроенного SqlDataAdapter для
использования на протяжении всего времени жизни объекта.
В приведенном выше коде объект построителя команд (SqlCommandBuilder) служил
только для передачи в объект адаптера данных в качестве параметра конструктора. Как
ни странно, это все, что нам нужно с ним сделать (в минимальном варианте). "За
кулисами" этот тип выполняет конфигурирование адаптера данных остальными объектами
команд.
Каждому нравится получить что-то, не прикладывая усилий, но надо учитывать,
что построители команд накладывают серьезные ограничения. А именно, построитель
команд может автоматически генерировать команды SQL для использования их
адаптером данных, если выполнены все следующие условия:
• SQL-команда Select работает только с одной таблицей (т.е. без объединений):
• у этой единственной таблицы имеется первичный ключ;
• в таблице должен быть столбец (или столбцы), который представляет первичный
ключ, включенный в SQL-оператор Select.
Учитывая способ построения базы данных AutoLot, эти ограничения не доставляют
никаких проблем. Но в более реальных базах данных нужно подумать, может ли
оказаться полезным этот тип вообще (а если нет, учтите, что Visual Studio 2010
автоматически генерирует большую часть необходимого кода — это будет показано в конце
настоящей главы).
Глава 22. ADO.NET, часть II: автономный уровень 833
Реализация метода GetAllInventoryQ
Теперь наш адаптер данных готов к применению. Первый метод нового класса
будет просто вызывать метод Fill() объекта SqlDataAdapter для получения DataTable,
представляющего все записи из таблицы Inventory базы данных AutoLot:
public DataTable GetAllInventory ()
{
DataTable inv = new DataTable("Inventory");
dAdapu.Fill(inv);
return inv;
}
Реализация метода UpdatelnventoryQ
Метод UpdatelnventoryO очень прост:
public void Updatelnventory(DataTable modifledTable)
{
dAdapt.Update(modifledTable);
1
Здесь объект адаптера данных проверяет значение RowState у каждой строки
входной таблицы. В зависимости от его значения (RowState.Added, RowState.Deleted или
RowState.Modified) автоматически вызывается нужный объект команды.
Установка номера версии
Замечательно! Логика второй версии нашей библиотеки доступа к данным
завершена. Хотя это и необязательно, все же для порядка установите для этой библиотеки номер
версии 2.0.0.0. Как описано в главе 14, чтобы изменить версию сборки .NET, дважды
щелкните на узле Properties (Свойства) в Solution Explorer, а затем щелкните на кнопке
Assembly Information... (Информация о сборке), которая находится на вкладке Application
(Приложение). В открывшемся диалоговом окне укажите старший номер версии сборки
равным 2 (подробнее см. в главе 14). После этого перекомпилируйте приложение, чтобы
обновить информацию о сборке.
Исходный код. Проект AutoLot DAL (Version 2) доступен в подкаталоге Chapter 22.
Тестирование автономной функциональности
Теперь у нас есть все для создания клиентского интерфейса, который позволит
проверить работу нового класса Invent or yDALDisLayer. Здесь мы опять воспользуемся
Windows Forms API для вывода данных в графическом пользовательском интерфейсе.
Создайте новое приложение Windows Forms с именем InventoryDALDisconnectedGUI и
измените в Solution Explorer первоначальное имя файла Forml.csHaMainForm.cs. После
создания нового проекта вставьте в него ссылку на измененную сборку AutoLotDAL.dll
(не забудьте выбрать версию 2.0.0.0!) и импортируйте следующее пространство имен:
using AutoLotDisconnectedLayer;
Форма приложения содержит элементы Label, DataGridView (inventoryGrid) и
Button (btnUpdatelnventory), причем для кнопки должен быть создан обработчик
события Click. Ниже приведено определение формы:
public partial class MainForm : Form
{
InventoryDALDisLayer dal = null;
834 Часть V. Введение в библиотеки базовых классов .NET
public MainForm()
{
InitializeComponent();
string cnStr =
@"Data Source=(local)\SQLEXPRESS;Initial Catalog=AutoLot;" +
"Integrated Security=True;Pooling=False";
// Создание объекта доступа к данным.
dal = new InventoryDALDisLayer(cnStr);
// Заполнение графической таблицы!
inventoryGrid.DataSource = dal.GetAllInventory();
}
private void btnUpdateInventory_Click(object sender, EventArgs e)
{
// Получение измененных данных из графической таблицы.
DataTable changedDT = (DataTable)inventoryGrid.DataSource;
try
{
// Внесение изменений.
dal.Updatelnventory(changedDT);
}
catch(Exception ex)
{
MessageBox.Show(ex.Message) ;
}
}
}
После создания объекта InventoryDALDisLayer можно выполнить привязку
DataTable, возвращенного вызовом GetAllInventory(), к объекту DataGridView.
Когда конечный пользователь щелкнет на кнопке Update (Обновить), из графической
таблицы выбирается модифицированный DataTable (с помощью свойства DataSource)
и передается в метод Updatelnventory ().
Вот и все! Запустите это приложение, добавьте в графическую таблицу несколько
новых строк и измените и/или удалите несколько других. После щелчка на кнопке Update
все изменения будут сохранены в базе данных AutoLot.
Исходный код. Проект WindowsFormsInventoryUI доступен в подкаталоге Chapter 22.
Объекты DataSet для нескольких
таблиц и взаимосвязь данных
Пока все примеры данной главы оперировали с одним объектом DataTable. Но вся
мощь автономного уровня проявляется тогда, когда объект DataSet содержит
несколько взаимосвязанных DataTable. В этом случае в коллекцию DataRelation данного
DataSet можно вставить любое количество объектов DataRelation, которые
описывают все взаимосвязи таблиц. Эти объекты позволяют клиентскому уровню выполнять
навигацию между данными таблиц без обращения к сети.
На заметку! Вместо еще одного изменения сборки AutoLotDAL.dll, позволяющего работать с
таблицами Customers и Orders, в этом примере вся логика доступа к данным
изолируется в новом проекте Windows Forms. Разумеется, в производственном приложении смешивание
пользовательского интерфейса и логики обработки данных не рекомендуется. В последних
примерах данной главы применяются различные средства проектирования баз данных,
которые позволяют отделить код пользовательского интерфейса от логики обработки данных.
Глава 22. ADO.NET, часть II: автономный уровень 835
Начните этот пример с создания нового приложения Windows Forms по
имени MultitabledDataSetApp. Графический интерфейс приложения максимально
прост. На рис. 22.11 показаны три элемента DataGridView (dataGridViewInventory,
dataGridViewCustomers и dataGridViewOrders) для данных из таблиц Inventory,
Orders и Customers базы данных AutoLot. Кроме того, имеется кнопка Button
(btnUpdateDatabase) для отправки всех изменений, проведенных в графических
таблицах, обратно в базу данных для обработки с помощью объектов адаптеров данных.
MewvForm с* [Design]4 X
Cinvn! ".-:: wi
jmm* 0 ta
—
Рис. 22.11. В первоначальном пользовательском интерфейсе
выводятся данные из всех таблиц базы данных AutoLot
Подготовка адаптеров данных
Для максимального упрощения кода доступа к данным в типе Main Form будут
использоваться объекты построителей команд, которые будут автоматически
генерировать команды SQL для каждого из трех объектов SqlDataAdapter (по одному для
каждой таблицы). Вот первая итерация нашего типа, производного от Form (не забудьте
импортировать пространство имен System.Data.SqlClient):
public partial class HainForm : Form
// Формирование широкого DataSet.
private DataSet autoLotDS = new DataSet("AutoLot");
// Использование построителей команд для упрощения настройки адаптера данных.
private SqlCommandBuilder sqlCBInventory;
private SqlCommandBuilder sqlCBCustomers;
private SqlCommandBuilder sqlCBOrders;
// Адаптеры данных (для каждой таблицы).
private SqlDataAdapter invTableAdapter;
private SqlDataAdapter custTableAdapter;
private SqlDataAdapter ordersTableAdapter;
// Формирование строки подключения.
private string cnStr = string.Empty;
836 Часть V. Введение в библиотеки базовых классов .NET
Конструктор выполняет всю утомительную работу по созданию переменных-членов,
относящихся к обработке данных, и заполнению DataSet. Здесь предполагается, что
вы уже создали файл App.config, содержащий соответствующую строку подключения
(и, следовательно, вставили ссылку на System.Configuration.dll и импортировали
пространство имен System.Configuration):
<configuration>
<connectionStrings>
<add name ="AutoLotSglProvider11 connectionString =
"Data Source=(local)\SQLEXPRESS;
Integrated Security=SSPI;Initial Catalog=AutoLot"
/>
</connectionStrings>
</configuration>
Кроме того, добавляется вызов приватной вспомогательной функции BuildTable
Relationship():
public MainFormO
{
InitializeComponent();
/ / Получение строки подключения из файла *.config.
cnStr =
ConfiguratlonManager.ConnectlonStrings
["AutoLotSglProvider"].ConnectionString;
// Создание адаптеров.
invTableAdapter = new SglDataAdapter("Select * from Inventory", cnStr);
custTableAdapter = new SglDataAdapter("Select * from Customers", cnStr);
ordersTableAdapter = new SglDataAdapter("Select * from Orders", cnStr);
// Генерация команд.
sglCBInventory = new SqlCommandBuilder(invTableAdapter);
sglCBOrders = new SqlCommandBuilder(ordersTableAdapter);
sglCBCustomers = new SqlCommandBuilder(custTableAdapter) ;
// Добавление таблиц в DataSet.
invTableAdapter.Fill(autoLotDS, "Inventory");
custTableAdapter.Fill(autoLotDS, "Customers");
ordersTableAdapter.Fill(autoLotDS, "Orders");
// Создание отношений между таблицами.
BuildTableRelationship();
// Привязка к графическим элементам
dataGridViewInventory.DataSource = autoLotDS.Tables["Inventory"];
dataGridViewCustomers.DataSource = autoLotDS.Tables["Customers" ] ;
dataGridViewOrders.DataSource = autoLotDS.Tables["Orders" ] ;
Создание отношений между таблицами
Вспомогательная функция BuildTableRelationship () берет на себя рутинные
действия по добавлению в объект autoLotDS двух объектов DataRelation. В главе 21 было
сказано, что в базе данных AutoLot имеется ряд отношений "родительский-дочерний",
и они учтены в следующем коде:
private void BuildTableRelationship ()
{
// Создание объекта отношения между данными CustomerOrder.
DataRelation dr = new DataRelation("CustomerOrder",
Глава 22. ADO.NET, часть II: автономный уровень 837
autoLotDS.Tables["Customers"].Columns["CustlD"],
autoLotDS.Tables["Orders"].Columns["CustlD"]);
autoLotDS.Relations.Add(dr);
// Создание объекта отношения между данными InventoryOrder.
dr = new DataRelation("InventoryOrder",
autoLotDS.Tables["Inventory"].Columns["CarID"],
autoLotDS.Tables["Orders"].Columns["CarlD"]);
autoLotDS.Relations.Add(dr);
}
Здесь при создании объекта DataRelation в первом параметре указывается более
понятный строковый идентификатор (вскоре мы покажем, как это можно использовать).
Кроме того, задаются ключи для создания самого отношения. Обратите внимание, что
сначала указывается родительская таблица (второй параметр конструктора), а затем
дочерняя (третий параметр конструктора).
Изменение таблиц базы данных
После заполнения объекта DataSet и отключения его от источника данных
можно локально работать с каждым DataTable. Для этого нужно запустить приложение
и вставлять, изменять или удалять значения в любых элементах DataGridView. Когда
понадобится отправить данные в базу, щелкните на кнопке Update Database (Обновить
базу данных). Теперь уже должен быть понятен код обработки события Click:
private void btnUpdateDatabase_Click (object sender, EventArgs e)
{
try
{
invTableAdapter.Update(carsDS, "Inventory");
custTableAdapter.Update(carsDS, "Customers");
ordersTableAdapter.Update(carsDS, "Orders");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
Запустите приложение и внесите различные изменения в данные. При следующем
запуске приложения вы увидите, что графические таблицы содержат все последние
изменения.
Переходы между взаимосвязанными таблицами
Теперь посмотрим, как объекты DataRelation позволяют программно переходить
между взаимосвязанными таблицами. Добавьте в графический интерфейс новый
элемент Button (btnGetOrderlnfo), соответствующий TextBox (txtCustID) и Label с
подходящим текстом (для большей понятности эти элементы можно сгруппировать в
GroupBox). На рис. 22.12 показан один из возможных вариантов графического
интерфейса рассматриваемого приложения.
Этот измененный интерфейс позволяет пользователю ввести идентификатор
клиента и получить всю информацию о заказе этого клиента (имя, номер заказа,
автомобиль). Эта информация форматируется в виде строки string, а затем выводится в окне
сообщения.
838 Часть V. Введение в библиотеки базовых классов .NET
Рис. 22.12. Измененный интерфейс позволяет пользователю
выполнять поиск информации о заказах клиента
Вот код обработчика события Click только что добавленной кнопки:
private void btnGetOrderlnfo_Click(object sender, System.EventArgs e)
{
string strOrderlnfo = string.Empty;
DataRow[] drsCust = null;
DataRow[] drsOrder = null;
// Получение идентификатора клиента из текстового поля.
int custID = int.Parse(this.txtCustlD.Text);
// Поиск строки в таблице Customers для введенного идентификатора клиента.
drsCust = autoLotDS.Tables["Customers"] .Select (
string.Format("CustID = {0}", custID));
strOrderlnfo += string.Format("Customer {0}: {1} {2}\n",
drsCust[0] ["CustID"] .ToStringO ,
drsCust [0] ["FirstName"] .ToString (),
drsCust [0] ["LastName"] .ToString ());
// Переход из таблицы Customers к таблице Orders.
drsOrder = drsCust[0].GetChildRows(autoLotDS.Relations["CustomerOrder"]);
// Получение номера заказа.
foreach (DataRow r in drsOrder)
strOrderlnfo += string.Format("Order Number: {0}\n", r["OrderlD"]);
//А теперь переход из таблицы Orders к таблице Inventory.
DataRow[] drslnv =
drsOrder[0] .GetParentRows(autoLotDS.Relations["InventoryOrder"]) ;
// Перебор всех заказов данного клиента.
foreach (DataRow order in drslnv)
{
strOrderlnfo += string.Format(" \nOrder Number:
{0}\n", order["OrderlD"]);
// Выборка автомобиля, упоминаемого в данном заказе.
DataRow[] drslnv = order.GetParentRows(autoLotDS.Relations[
"InventoryOrder"]);
// Получение информации для этого (ОДНОГО) автомобиля.
DataRow car = drslnv[0];
strOrderlnfo += string.Format("Make: {0}\n", r["Make"]);
// Марка
Глава 22. ADO.NET, часть II: автономный уровень 839
strOrderlnfо += string.Format("Color : {0}\n", r["Color"]); //Цвет
strOrderlnfo += string.Format("Pet Name: {0}\n", r["PetName"]);
/ / Дружественное имя
}
MessageBox.Show(strOrderlnfo, "Order Details");
// Информация о заказе
На рис. 22.13 показан один из возможных результатов работы, когда указан
идентификатор клиента 3 (в моей базе данных AutoLot это Стив Хаген, у которого имеются
два ожидающих обработки заказа).
£
Current invent,;I,
' CariO
[107
ИУУ>
Current Customers
j CmtID
\2
C
Current Orders
j OrderlD
'jiooi
11002
Lookup Customer Order
Customer ID 3
( GetO
pubitoi
Make
I Ford
i Ford
IW7
RrstNdroe
■ Dave
Matt
| Steve
CustiD
|l
\2
j3
aer Details
Color
Rust
Red
P. .ml.
Last Name
Brenner
Walton
Hagen
CarlD
1000
678
.
PetName
] Rusty
Customer 3: Steve Hagen j»
Order f'4umben 1002 ■
MakeVW
Color. Black
Pet Name: Hank
[ OK ] I
J
1
]
Рис. 22.13. Навигация с помощью отношений между данными
Надеюсь, последний пример убедил вас в пользе типа DataSet. При полном
отсутствии связи DataSet с первоначальным источником данных вы можете работать с
находящейся в памяти копией данных, переходить от таблицы к таблице и вносить
необходимые изменения, удаления или вставки. После этого можно отправить измененные
данные обратно в хранилище. В результате получается очень масштабируемое и
надежное приложение.
Исходный код. Проект MultitabledDataSetApp доступен в подкаталоге Chapter 22.
Средства конструктора баз данных в Windows Forms
До сих пор все приведенные примеры содержали ощутимый объем "грязной работы":
всю логику доступа к данным мы писали вручную. И хотя значительный объем этого
кода был вынесен в библиотеку .NET (AutoLotDAL.dll) для использования в
последующих главах книги, нам все равно приходится вручную создавать различные объекты
поставщика данных, чтобы можно было взаимодействовать с реляционной базой данных.
Теперь мы научимся использовать средства конструктора баз данных Windows Forms,
которые могут создать за вас значительный объем кода доступа к данным.
840 Часть V. Введение в библиотеки базовых классов .NET
На заметку! Для создания веб-проектов с помощью Windows Presentation Foundation и ASP.NET
имеются аналогичные средства, с которыми вы познакомитесь ниже в данной главе.
Одним из способов применения этих интегрированных средств является
использование конструктор, поддерживаемых элементом DataGridView из Windows Forms. Но
тогда средства конструктора баз данных вставят весь код доступа к данным
непосредственно в кодовую базу GUI! Лучше всего изолировать весь этот код, сгенерированный
конструктором, в специальную кодовую библиотеку .NET, чтобы повторно использовать
полученную логику доступа к данным в различных проектах.
Однако полезно начать с рассмотрения, как можно с помощью элемента
DataGridView сгенерировать код доступа к данным, т.к. такой подход все-таки может
быть полезен в небольших проектах и прототипах приложений. А затем мы научимся
изолировать этот сгенерированный код в третьей версии библиотеки AutoLot.dll.
Визуальное проектирование элементов DataGridView
У элемента DataGridView имеется связанный с ним мастер, который может
генерировать код доступа к данным. Для начала создайте новый проект приложения Windows
Forms по имени Da taGridViewDat a Designer. Переименуйте с помощью Solution
Explorer первоначальную форму в MainForm.cs и добавьте на нее экземпляр элемента
DataGridView (с именем inventoryDataGridView). При этом справа от элемента
открывается текстовый редактор. В раскрывающемся списке Choose Data Source (Выберите
источник данных) выберите пункт Add Project Data Source (Добавить источник данных
для проекта), как показано на рис. 22.14.
MatnForm.es [Design}" X
■кг* Windows Forms Data Wizards
Рис. 22.14. Редактор DataGridView
После этого запустится мастер конфигурирования источников данных (Data Source
Configuration Wizard). Он проведет вас через последовательность шагов, позволяющих
выбрать и настроить источник данных, который затем будет привязан к DataGridView.
На первом шаге мастер просто спрашивает тип источника данных, с которым вы
хотели бы взаимодействовать. Выберите вариант Database (База данных) (рис. 22.15) и
щелкните на кнопке Next (Далее).
На следующем шаге (который зависит от выбора, сделанного на первом шаге) мастер
спрашивает, нужно ли использовать модель базы данных DataSet или модель данных
Entity. Выберите модель базы данных DataSet (рис. 22.16), т.к. мы еще не знакомы с
технологией Entity Framework (о ней будет рассказано в следующей главе).
Глава 22. ADO.NET, часть II: автономный уровень 841
Choose a Data Source Type
.Where will the application get data from?
Database j Service Object SharePoint
Lets ycu connect tc a database and choose the database objects for your application.
Рис. 22.15. Выбор типа источника данных
Data Source Configuration Wizard
Choose a Database Model
What type of database model do you want to use?
u
k
_^ Entity Data
"EJT Model
The database model you choose determines the types of data objects your application code uses. A dataset file will
be added to your project,
Рис. 22.16. Выбор модели базы данных
Следующий шаг позволяет настроить подключение к базе данных. Если эта база
данных уже добавлена в Server Explorer, то она будет присутствовать в
раскрывающемся списке. А если ее там нет (например, если она еще не добавлена в Server Explorer),
щелкните на кнопке New Connection (Новое подключение). Результат выбора локального
экземпляра базы данных AutoLot показан на рис. 22.17 (обратите внимание, что при
этом генерируется нужная строка подключения).
Последний шаг мастера позволяет выбрать объекты базы данных, которые будут
фигурировать в автоматически сгенерированном DataSet и соответствующих адаптерах
данных. В принципе можно выбрать все объекты данных из базы AutoLot, но нам здесь
понадобится только таблица Inventory. Поэтому измените предлагаемое имя DataSet
на InventoryDataSet (рис. 22.18), отметьте таблицу Inventory и щелкните на кнопке
Finish (ГЪтово).
842 Часть V. Введение в библиотеки базовых классов .NET
^JgD
Choose Your Data Connection
Which data connection should your application use to connect to the database?
| win-bфЫehfvOv\^qlexpreи.ДutoLotdbo
T Jr.. Connection...
Gl Connection string
Data Sources local) \SQLEXPRESS;Jnitial Cetalog=AutoLot;lntegrated Security=True.Pocling=Faise
.: p,e^c
Caned
Рис. 22.17. Выбор базы данных
Data Source Configuration Wizard i Щ2щ
Щ Choose Your Database Objects
■ЩР
Which database objects do you want in your dataset?
f ' ■>£ T*le,
_J CreditRisks
. _3 Customers
' У _] ln\ enter.-
Й23 CarlD [^
BH Make
, Й1Э Color
SHU PetName
f ИЗ Orders
Etfjj Vievw
| •* L *!> Stored Procedures
□ 3 GetPetName
1 Ej14 Functions
Enable local database caching
DataSet name:
InventoryDataSet
1 < Previous i Finish J j Cancel
Рис. 22.18. Выбор таблицы Inventory
После этого визуальный конструктор изменится сразу во многих отношениях.
Наиболее заметно то, что в элементе DataGridVieu отображается схема таблицы
Inventory, т.е. появились соответствующие заголовки столбцов. Кроме того, в нижней
части конструктора формы (который называется "component tray" — "лоток с
компонентами") имеются три компонента: DataSet, BindingSource и TableAdapter (рис. 22.19).
Сейчас уже можно запустить приложение — и вы увидите, что графическая таблица
сразу заполнена записями из таблицы Inventory. Конечно, тут нет никакого
волшебства. Среда разработки создала за вас значительный объем кода и настроила элемент
графической таблицы для его использования. Давайте рассмотрим этот
сгенерированный код.
Глава 22. ADO.NET, часть II: автономный уровень 843
MatnForm.cs [Design]"
Щ Windows Forms Data Wizards
Шш&Г[
•ntoryOataSet ™ inventoryBindingSource Щ inventoryTableAdapter
Tasks
Choose Data Source
Edit Columns-
Add Column...
H Enable Adding
H Enable Editing
V; Enable Deleting
П Enable Column Reordering
Dock in Parent Container
Add Queiy...
Preview Data...
■'?
Рис. 22.19. Проект Windows Forms после отработки мастера конфигурирования источников данных
Сгенерированный файл app.config
Если посмотреть на проект в Solution Explorer, легко заметить, что он уже содержит
файл app.config. В нем имеется элемент <connectionStrings> с несколько
необычным именем:
<?xml version="l .0" encoding="utf-811 ?>
<configuration>
<configSections>
</configSections>
<connectionStrings>
<add name="DataGridViewDataDesigner. Properties. Settings . AutoLotConnectionString"
connectionString=
"Data Source=(local)\SQLEXPRESS;
Initial Catalog=AutoLot; Integrated Security=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
</configuration>
В качестве имени объекта адаптера данных (о котором будет рассказано ниже)
взято длинное значение "DataGridViewDataDesigner.Properties.Settings.
AutoLotConnectionString".
Анализ строго типизированного DataSet
Кроме конфигурационного файла, IDE генерирует так называемый строго
типизированный DataSet. Этим термином обозначается пользовательский класс, который
расширяет базовый DataSet и содержит ряд членов, позволяющих взаимодействовать
с базой данных с помощью более наглядной объектной модели. Строго типизированные
объекты DataSet содержат свойства, непосредственно отображаемые на имена таблиц
из базы данных. Например, можно использовать свойство Inventory для прямого
обращения к строкам и столбцам базы, не путаясь в коллекции таблиц Tables.
Вставьте в проект новый файл диаграммы классов, выбрав в Solution Explorer
значок проекта и щелкнув на кнопке View Class Diagram (Просмотр диаграммы классов).
Обратите внимание, что на основе введенной информации мастер создал новый тип
DataSet по имени InventoryDataSet. В этом классе определен ряд членов, важнейшим
из которых является свойство Inventory (рис. 22.20).
844 Часть V. Введение в библиотеки базовых классов .NET
Если в Solution Explorer дважды щелкнуть на файле
InventoryDataSet.xsd, то загрузится конструктор
наборов данных (Dataset Designer) Visual Studio 2010
(подробнее он рассматривается чуть ниже). Если щелкнуть правой
кнопкой мыши в любом месте этого конструктора и
выбрать в контекстном меню пункт View Code (Просмотреть
код), то вы увидите совершенно пустое определение
частичного класса:
public partial class InventoryDataSet {
}
При необходимости в это определение частичного
класса можно добавить дополнительные члены. Однако все
действие происходит в сопровождаемом конструктором
файле InventoryDataSet.Designer.cs. Если открыть
этот файл в Solution Explorer, то можно увидеть, что тип
InventoryDataSet расширяет класс DataSet. Рассмотрим следующий фрагмент кода
с добавленными для ясности комментариями:
// Весь этот код сгенерирован конструктором!
public partial class InventoryDataSet : global::System.Data.DataSet
{
// Переменная-член типа InventoryDataTable.
private InventoryDataTable tablelnventory;
// Каждый конструктор вызывает вспомогательный метод InitClass().
public InventoryDataSet()
{
this.InitClass ();
}
// InitClass() подготавливает DataSet и добавляет
// InventoryDataTable в коллекцию Tables.
private void InitClass()
{
this.DataSetName = "InventoryDataSet";
this.Prefix = "";
this.Namespace = "http://tempuri.org/InventoryDataSet.xsd";
this.EnforceConstraints = true;
this.SchemaSerializationMode =
global::System.Data.SchemaSerializationMode.IncludeSchema;
this.tablelnventory = new InventoryDataTable();
base.Tables.Add(this.tablelnventory);
}
// Свойство Inventory (только для чтения) возвращает переменную-член InventoryDataTable.
public InventoryDataTable Inventory
{
get { return this.tablelnventory; }
}
}
В этом строго типизированном DataSet имеется переменная-член, которая является
строго типизированным DataTable — в данном случае это класс InventoryDataTable.
Конструктор строго типизированного класса DataSet вызывает приватный метод
инициализации InitClass (), который добавляет экземпляр этого строго типизированного
DataTable в коллекцию Tables объекта DataSet. И еще один важный момент:
реализация свойства Inventory возвращает переменную-член InventoryDataTable.
| InventoryDataSet ®
I Class
| -fc DataSet
* Fields
s Properties
Wr SchemaSerializationMode
Ш Tables
Я Methods
Э Nested Types
Рис. 22.20. Строго
типизированный DataSet, созданный
мастером конфигурирования
источников данных
Глава 22. ADO.NET, часть II: автономный уровень 845
Анализ строго типизированного DataTable
Теперь вернитесь к файлу диаграммы классов и откройте узел
Nested Types (Вложенные типы) у значка InventoryDataSet. Здесь
имеются строго типизированный класс InventoryDataTable
и строго типизированный класс DataRow.
В классе InventoryDataTable (имеющем тот же тип, что
и только что рассмотренная переменная-член строго
типизированного DataSet) определен набор свойств,
основанных на именах столбцов физической таблицы Inventory
(CarlDColumn, ColorColumn, MakeColumn и PetNameColumn),
а также специальный индексатор и свойство Count для
получения текущего количества записей.
Но более интересно то, что в этом строго
типизированном классе DataTable определен набор методов, которые
позволяют вставлять, находить и удалять строки в таблице
с помощью строго типизированных членов (удобная
альтернатива ручной навигации по индексаторам Rows и Columns).
К примеру, метод AddlnventoryRowO предназначен для
добавления новой строки в находящуюся в памяти таблицу,
FindByCarlDO — для поиска в таблице по первичному
ключу, a RemoveInventoryRow() позволяет удалить строку из
строго типизированной таблицы (рис. 22.21).
Анализ строго типизированного DataRow
Строго типизированный класс DataRow, также вложенный
в строго типизированный DataSet, расширяет класс DataRow
и содержит свойства, непосредственно соотносимые со
схемой таблицы Inventory. Кроме того, конструктор данных
создал метод IsPetNameNullO, который проверяет, содержит
ли данный столбец значение (рис. 22.22).
Анализ строго типизированного
адаптера данных
InventoryDataTable 1*1
Class
•*■ TypedTableBase<Inventorj«ow>
a Fields
Э Properties
CarlDColumn
3^ ColorColumn
j? Count
^ MakeColumn
Ж PetNameColumn
"^ this
Q Methods
ИИШ^^!|||ЯЯ1!В'1Я111ЯР,|'<Р.1»
* Clone
f* Createlnstance
■-♦ FindByCarlD
$♦ GetRow/Type
-♦ GetTypedTableSchema
$Ь InitClass
a* InitVars
-♦ InventoryDataTable !> 2 overlo... j
"♦ Ne/vInventoryRow
f^ NewRowFromBuilder
f* OnRowChanged
f* OnRowChangmg
f* On Rov* Deleted
4^ On Row Deleting
^ RemovelnventoryRow
Я Events
4, J
Рис. 22.21. Строго
типизированный DataTable,
вложенный в строго
типизированный DataSet
InventoryRow
Class
* DataRow
И Fields
|^ tablelnventory
a Properties
*j* CarlD
*§? Color
Й? Make
Э1 PetName
8 Methods
ai* InventoryRow
:* IsPetNameNull
-'• SetPetNameNull
Получение строгой типизации для автономных типов —
серьезный аргумент в пользу использования мастера
конфигурирования источников данных, поскольку создание этих
классов вручную — утомительное (хотя и вполне посильное)
занятие. Этот мастер даже генерирует объект специального
адаптера данных, который может строго типизированным
образом заполнять и обновлять объекты InventoryDataSet и
InventoryDataTable. Найдите в окне визуального
конструктора классов класс InventoryTableAdapter и просмотрите
сгенерированные для него члены (рис. 22.23).
Автоматически сгенерированный тип InventoryTableAdapter содержит коллекцию
объектов SqlCommand (доступ к ним возможен с помощью свойства CommandCollection),
у каждого из которых имеется полностью заполненный набор объектов SqlParameter.
Кроме того, этот специальный адаптер данных предоставляет набор свойств для
выборки соответствующих объектов подключения, транзакций и адаптеров данных, а также
свойство для получения массива, представляющего все типы команд.
Рис. 22.22. Строго
типизированный DataRow
846 Часть V. Введение в библиотеки базовых классов .NET
Inventory Table Adapter
Class
■+ Component
H Fields
& .adapter
ф _clearBeforefill
jjj^ jrcmmandCollection
& .connection
$& .transaction
■~ Properties
Щ Adapter
Ш ClearBeroreFill
jfjj| ComrnandCollection
щ1 Connection
j|* Transaction
В Methods
* Delete
* Fill
• GetData
J* InitAdapter
Д* InitCommandCollection
£* InitConnection
•♦ Insert
'♦ InventoryTableAdapter
Ф Update I* 5 overloads)
Рис. 22.23. Специальный адаптер данных, работающий
со строго типизированными DataSet и DataTable
Завершение приложения Windows Forms
Если внимательно рассмотреть обработчик события Load в типе, порожденном от
формы (то есть если рассмотреть код для MainForm.cs и найти метод MainFormLoadO),
то можно увидеть, что метод Fill () специализированного адаптера данных вызывается
в самом начале, и специальный DataSet передает ему специальный объект DataTable:
private void MainForm_Load(object Gender, EventArgs e)
{
this.inventoryTableAdapter.Fill(this.mventoryDat ?Set.Inventory) ;
Этот объект адаптера данных позволяет отрабатывать изменения, выполненные в
графической таблице. Добавьте в пользовательский интерфейс вашей формы еще один
элемент Button (по имени btnUpdatelnventory). Затем создайте обработчик события
Click и впишите в него следующий код:
private void btnUpdateInventory_Click(object sender, EventArgs e)
{
try
{
// Отправка всех изменений, выполненных в таблице
// Inventory, на обработку в базу данных.
this.inventoryTableAdapter.Update(this.inventor/DataSet.Inventory);
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
// Получение свежей копии для графической таблицы.
this.inventoryTableAdapter.Fill(this.inventoryDataSet.Inventory) ;
Глава 22. ADO.NET, часть II: автономный уровень 847
Снова запустите приложение; добавляйте, удаляйте или изменяйте записи,
отображаемые в графической таблице, а затем щелкните на кнопке Update (Обновить).
При следующем запуске программы вы увидите, что все изменения присутствуют и
учитываются.
Данный пример позволяет увидеть, насколько полезным может быть конструктор
элемента DataGridView. Он позволяет работать со строго типизированными данными
и генерирует за вас большую часть логики работы с базами данных. Очевидной
проблемой является то, что полученный код тесно связан с окном, которое его использует.
Конечно, лучше бы такой код находился в сборке AutoLotDAL.dll (или в какой-то
другой библиотеке доступа к данным). Можно подумать и о перенесении кода,
сгенерированного мастером элемента DataGridView, в проект библиотеки классов, т.к. по
умолчанию конструктор форм в нем отсутствует.
Исходный код. Проект DataGridViewDataDesigner доступен в подкаталоге Chapter 22.
Выделение строго типизированного кода работы
с базами данных в библиотеку классов
К счастью, активизировать средства проектирования данных Visual Studio 2010
можно в любом проекте (как с пользовательским интерфейсом, так и без), без
необходимости копировать и вставлять большие фрагменты кода из одного проекта в другой.
Для иллюстрации добавим в библиотеку AutoLot.dll новые возможности.
Скопируйте всю папку AutoLot (Version two), созданную ранее в этой главе, в новое
место на жестком диске и переименуйте ее в AutoLot (Version Three). Затем выберите
в Visual Studio 2010 пункт меню File^Open Project/Solution... (Файл^Открыть
проект/решение...) и откройте файл AutoLotDAL.sln из новой папки AutoLot (Version Three).
Теперь вставьте в проект новый строго типизированный класс DataSet по имени
AutoLotDataSet .xsd с помощью пункта меню Project^Add New Item (Проект^Добавить
новый элемент), как показано на рис. 22.24.
Add New Item - AutoLotOAL ]
| Instated Тел.рЫел
| л Visual С* Items
Code
Web
Window Formj
WPF
Reporting
Workflow
Name: AutoLotDataSet.
Sort by. Default «f. '•
ф-. Database Unit Test
gJH XML Schema
Щ*~* ь
•<*} XML File
r-^l XSLTFile
Ц Local Database Cache
JL ADO.NtT Entity Data Model
U Local Database
j*'jL L1NQ to SQL Classes
1 Service-based Database
<sd
m
Visual C* Items
Visual C» Items
Visual C» hems
Visual C* Herns
Visual C» Items
Visual C« Hems
Visual C* Hems
Visual C» Hems
Visual C# Hems
Visual C» Hems
Typer. Visual C* Hems
A DataSet for using data
application
| Add
fr»TM'
/- •
\
Д Cancel^J
Рис. 22.24. Вставка нового строго типизированного DataSet
848 Часть V. Введение в библиотеки базовых классов .NET
AutoLotDataSetxsd X
V& QueriesTabteAdaptei
Ш GetPetName (@carID, ©petNa...
Рис. 22.25. Специальные строго типизированные объекты — на сей раз в проекте
библиотеки классов
При этом откроется пустая поверхность конструктора наборов данных. Теперь
воспользуйтесь Server Explorer для подключения к нужной базе данных (у вас уже должно
быть подключение к Auto Lot) и перетащите на поверхность все таблицы и хранимые
процедуры, которые необходимо сгенерировать. На рис. 22.25 показаны все аспекты
базы AutoLot, с которыми будет продолжаться работа (таблица Credit Risk не нужна),
а также автоматически показанные взаимоотношения между ними.
Просмотр сгенерированного кода
Конструктор DataSet создал в точности
такой же код, как и мастер DataGridView в
предыдущем примере Windows Forms. Но на
этот раз задействованы таблицы Inventory,
Customers и Orders, а также хранимая
процедура GetPetName, поэтому получилось
значительно больше сгенерированных
классов. Для каждой таблицы базы данных,
перетащенной на поверхность конструктора,
появились строго типизированные классы
DataSet, DataTable, DataRow и адаптера
данных.
Строго типизированные классы DataSet,
DataTable, DataRow помещаются в
корневое пространство имен проекта AutoLot.
Специализированные адаптеры таблиц
находятся во вложенном пространстве имен.
Есть более легкий способ просмотреть все
сгенерированные типы — средство Class
View (Просмотр классов), которое
открывается из меню View (Просмотр) Visual Studio
(рис. 22.26).
Чтобы все было по правилам, запус-
Рис. 22.26. Строго типизированные классы, сге- тите редактор свойств Visual Studio 2010
нерированные для базы данных AutoLot
1 Class View » П X 1
1 U —
[<Scarch> ~~ L^^^l
1 л i^j AutoLotDAL *|
C> 03 Project References
t> {} AutoLotConnectedLayer
' {>CJ
\> *J AutoLotDataSet
> ^ AutoLotDataSet.CustomersDataTable
!• ^ AutoLotDataSet.CustomersRow
t» ^ AutoLotDataSet.CustomersRowChangeEvent
t> Л AutoLotDataSet.CustomersRcwChangeEventHandler
!> *t$ AutoLotDataSetJnventoryDataTable
t> ^ AutoLotDataSet.lnventoryRow
l> "tj AutoLotDataSetinventoryRowChangeEvent
!> ;jj|| AutoLotDataSetlnventoryRowChangeEventHandler
t> fy AutoLotDataSet.OrdersDataTable
:> *ty AutoLotDataSet.OrdersRow
l> ^$ AutoLotDataSet.OrdersRowChangeEvent
i> Л AutoLotDataSet.OrdersRowChangeEventHandler
л {) AutoLotDAL AutoLotDataSetTableAdapters
t> Щ% CustomersTableAdapter
i> ^J InventoryTableAdapter
t> 4$ OrdersTableAdapter
:> ^ QueriesTableAdapter
It!
> -AX TableAdapterManager
'-■ $t TableAdapterManager.SerfReferenceComparer
■ мг1 TableAdapterManager.UpdateOrderOption „ 1
Ш Class View КИ
Глава 22. ADO.NET, часть II: автономный уровень 849
(подробнее см. главу 14) и измените версию этой последней инкарнации AutoLot.dll
на 3.0.0.0.
Исходный код. Проект AutoLotDAL (Version 3) доступен в подкаталоге Chapter 22.
Выборка данных с помощью сгенерированного кода
Теперь полученные строго типизированные данные можно использовать в любом
приложении .NET, которому необходимо взаимодействовать с базой данных AutoLot.
Чтобы проверить, понимаете ли вы все основные механизмы, создайте консольное
приложение с именем StronglyTypedDataSetConsoleClient. Добавьте в него ссылку
на самую последнюю (и самую замечательную) версию AutoLot.dll и импортируйте в
первоначальный файл с кодом на С# пространства имен AutoLotDAL и AutoLotDAL.
AutoLotDataSetTableAdapters.
Ниже приведен метод Main(), который использует объект InventoryTableAdapter
для выборки всех данных и таблицы Inventory. В нем нет необходимости указывать
строку подключения, т.к. эта информация теперь входит в состав модели строго
типизированных объектов. После заполнения таблицы можно вывести результаты с
помощью вспомогательного метода PrintlnventoryO. При этом со строго типизированным
DataTable можно обращаться так же, как и с "обычным" DataTable — с помощью
коллекций Rows и Columns:
class Program
{
static void Main(string [ ] args)
{
Console.WriteLine (••***** Работа со строго типизированными DataSet *****\n");
AutoLotDataSet.InventoryDataTable table =
new AutoLotDataSet.InventoryDataTable();
InventoryTableAdapter dAdapt = new InventoryTableAdapter ();
dAdapt.Fill(table);
Printlnventory(table);
Console.ReadLine() ;
}
static void Printlnventory(AutoLotDataSet.InventoryDataTable dt)
{
// Вывод имен столбцов.
for (int curCol = 0; curCol < dt.Columns.Count; curCol++)
{
Console.Write(dt.Columns[curCol].ColumnName + "\t");
}
Console.WriteLine ("\n " ) ;
// Вывод данных.
for (int curRow = 0; curRow < dt.Rows.Count; curRow++)
{
for (int curCol = 0; curCol < dt.Columns.Count; curCol++)
{
Console.Write(dt.Rows[curRow] [curCol] .ToString () + "\t");
}
Console.WriteLine();
}
}
}
850 Часть V. Введение в библиотеки базовых классов .NET
Вставка данных с помощью сгенерированного кода
Допустим, что теперь нужно вставить новые записи с помощью модели строго
типизированных объектов. Приведенная ниже вспомогательная функция добавляет две
новых строки в текущую таблицу InventoryDataTable, а затем обновляет содержимое
базы данных с помощью адаптера данных. Первая строка добавляется вручную с
помощью конфигурирования строго типизированного DataRow, а вторая — с помощью
передачи содержимого столбцов, что позволяет создать DataRow автоматически и незаметно
для программиста:
public static void AddRecords(AutoLotDataSet.InventoryDataTable tb,
InventoryTableAdapter dAdapt)
{
// Получение из таблицы новой строго типизированной строки.
AutoLotDataSet.InventoryRow newRow = tb.NewInventoryRow() ;
// Заполнение строки данными.
newRow.CarlD = 999;
newRow.Color = "Purple";
newRow.Make = "BMW";
newRow.PetName = "Saku";
// Вставка новой строки.
tb.AddlnventoryRow(newRow);
// Добавление еще одной строки с помощью перегруженного метода Add.
tb.AddInventoryRow(888, "Yugo", "Green", "Zippy");
// Обновление базы данных.
dAdapt.Update(tb) ;
}
Этот метод можно вызвать из метода Main (), ив таблице базы данных появятся
новые записи:
static void Main(string [ ] args)
{
// Добавление строк, обновление и повторный вывод.
AddRecords(table, dAdapt);
table.Clear ();
dAdapt.Fill(table);
Printlnventory(table);
Console.ReadLine();
}
Удаление данных с помощью сгенерированного кода
Удаление записей с помощью модели строго типизированных объектов также не
представляет трудностей. Сгенерированный метод FindByXXXXO (где ХХХХ — имя
столбца с первичным ключом) строго типизированного DataTable возвращает по
первичному ключу нужный (строго типизированный) DataRow. Вот еще один
вспомогательный метод, который удаляет две только что созданные записи:
private static void RemoveRecords(AutoLotDataSet.InventoryDataTable tb,
InventoryTableAdapter dAdapt)
{
AutoLotDataSet.InventoryRow rowToDelete = tb.FindByCarlD(999);
dAdapt.Delete(rowToDelete.CarID, rowToDelete.Make,
rowToDelete.Color, rowToDelete.PetName);
rowToDelete = tb.FindByCarlD(888);
dAdapt.Delete(rowToDelete.CarlD, rowToDelete.Make,
rowToDelete.Color, rowToDelete.PetName);
}
Глава 22. ADO.NET, часть II: автономный уровень 851
Если вызвать его из метода Main () и снова вывести содержимое таблицы, то вы
увидите, что две новые тестовые записи уже отсутствуют.
На заметку! Если запустить это приложение второй раз с тем же методом AddRecordO, то
возникнет ошибка VIOLATION CONSTRAINT ERROR, т.к. метод AddRecordO оба раза
пытается вставить одно и то же значение первичного ключа CardlD. Если нужна большая гибкость,
понадобится запрашивать данные у пользователя
Вызов хранимой процедуры с помощью сгенерированного кода
Рассмотрим еще один пример применения модели строго типизированных
объектов. Создадим последний метод, который вызывает хранимую процедуру GetPetName.
При создании адаптеров данных для базы данных AutoLot был создан специальный
класс QueriesTableAdapter, который как раз и содержит вызов процедур, хранимых в
реляционной базе данных. Вот последняя вспомогательная функция, которая выводит
название указанного автомобиля при вызове из Main():
public static void CallStoredProc ()
{
QueriesTableAdapter q = new QueriesTableAdapter();
Console.Write("Введите идентификатор автомобиля: ");
string carlD = Console.ReadLine();
string carName = "";
q.GetPetName(int.Parse(carlD), ref carName);
Console.WriteLine("Имя CarlD {0} - {1}", carlD, carName);
Теперь вы умеете использовать строго типизированные объекты базы данных и
упаковывать их в специальную библиотеку классов. В этой объектной модели есть и
другие аспекты, с которыми можно поэкспериментировать, но вы уже знаете достаточно,
чтобы самостоятельно разобраться в них. И в завершение данной главы вы научитесь
применять LINQ-запросы к объекту ADO.NET DataSet.
Исходный код. Проект StronglyTypedDataSetConsoleClient доступен в подкаталоге
Chapter 22.
Программирование с помощью LINQ to DataSet
В данной главе было показано, что данные, находящиеся в объекте DataSet, можно
обрабатывать тремя различными способами:
• С помощью коллекций Tables, Rows и Columns
• С помощью объектов чтения из таблиц данных
• С помощью строго типизированных классов данных
Различные индексаторы типов DataSet и DataTable позволяют взаимодействовать с
содержащимися данными наглядным, но слабо типизированным способом. Вспомните,
что этот запрос требует рассматривать данные как табличный набор ячеек, например:
static void PrintDataWithlndxers(DataTable dt)
{
// Вывод содержимого DataTable.
for (int curRow = 0; curRow ^ dt.Rows.Count; curRow++)
{
for (int curCol = 0; curCol < dt.Columns.Count; curCol++)
{
Console.Write(dt.Rows[curRow] [curCol] .ToString () + "\t");
}
852 Часть V. Введение в библиотеки базовых классов .NET
Console.WriteLine();
}
}
Метод CreateDataReaderO типа DataTable предлагает другой подход, где данные,
содержащиеся в DataSet, трактуются как линейный набор строк, которые можно
обрабатывать последовательным образом. Это позволяет применять модель
программирования для подключенных объектов чтения данных к автономному DataSet:
static void PrintDataWithDataTableReader(DataTable dt)
{
// Получение объекта DataTableReader.
DataTableReader dtReader = dt.CreateDataReader();
while (dtReader.Read())
{
for (int i=0; l < dtReader.FieldCount; i++)
{
Console.Write(M{0}\t", dtReader.GetValue(i));
}
Console.WriteLine();
}
dtReader.Close();
}
И, наконец, можно использовать строго типизированный DataSet для получения
кодовой базы, которая позволяет взаимодействовать с данными, содержащимися в объекте,
с помощью свойств, которые отображаются на имена столбцов в реляционной базе
данных. Строго типизированные объекты позволяют написать, к примеру, следующий код:
static void AddRowWithTypedDataSet ()
{
InventoryTableAdapter invDA = new InventoryTableAdapter ();
AutoLotDataSet.InventoryDataTable inv = invDA.GetData();
inv.AddInventoryRow(999, "Ford", "Yellow", "Sal");
invDA.Update(inv);
}
Все эти подходы вполне работоспособны, но есть еще один — LINQ to DataSet API,
который предназначен для обработки данных из DataSet с помощью выражений LINQ-
запросов.
На заметку! LINQ to DataSet используется только для применения LINQ-запросов к объектам
DataSet, которые возвращаются адаптерами данных, и это никак не связано с передачей
LINQ-запросов непосредственно в механизм базы данных. В главе 23 вы познакомитесь с
технологиями LINQ to Entities и ADO.NET Entity Framework, которые предоставляют способ
представления SQL-запросов в виде LINQ-запросов.
В исходном состоянии объект ADO.NET DataSet (и соответствующие типы
наподобие DataTable и DataView) не содержат необходимую инфраструктуру для
непосредственного использования в LINQ-запросах. Например, следующий метод (в котором
задействовано пространство имен AutoLotDisconnectedLayer) приведет к появлению
ошибки времени компиляции:
static void LinqOverDataTable ()
{
// Получение DataTable, содержащего данные.
InventoryDALDisLayer dal = new InventoryDALDisLayer(
@"Data Source=(local)\SQLEXPRESS;" +
"Initial Catalog=AutoLot;Integrated Security=True");
DataTable data = dal.GetAllInventory();
Глава 22. ADO.NET, часть II: автономный уровень 853
// Применение LINQ-запроса к DataSet?
var moreData = from с in data where (int) с [ "CarlD" ] > 5 select c;
}
При компиляции метода LinqOverDataTableO компилятор сообщит, что тип
DataTable предоставляет реализацию шаблонов запросов. Аналогично применению
LINQ-запросов к объектам, которые не реализуют интерфейс IEnumerable<T>, объекты
ADO.NET необходимо преобразовать в совместимые типы. Чтобы понять, как это
сделать, нужно рассмотреть типы из библиотеки System.Data.DataSetExtensions.dll.
Библиотека расширений DataSet
Сборка System.Data.DataSetExtensions.dll, ссылка на которую по
умолчанию имеется во всех проектах Visual Studio 2010, добавляет в пространство имен
System.Data ряд новых типов (рис. 22.27).
Browse All Components ■'Щ
rsS*"
ПТ!оЯ[
л {) System.Data
> *fy DataRowComparer
> *ty DataRowComparer<TRow>
I> *t} DataRowExtensions
> ^J DataTableExtensions
f> ^ EnumerableRowCollection
1 A% EnumerableRowCollectton<TRow>
t> ^ EnumerableRowCollectionExtensions
t> ^% OrderedEnumerableRcvvCcllecticn<TRow>
t> ^TypedTableBase<T>
> ^ TypedTableBaseExtensicns
1 - [ г
_)-
I*
^^
□
a
1 Assembly System.Data.DataSetExtensions
Member of .NET Framework 4
C:\Program Files (xS6)\Reference Assemblies
|\Microsoft\Framework\.NETFramework\v4,0
[\System.Data.DataSetExtensions.dll
''
Рис. 22.27. Сборка System.Data.DataSetExtensions.dll
Из содержащихся в библиотеке типов наиболее полезны DataTableExtensions
и DataRowExtensions. Эти классы расширяют возможности классов DataTable и
DataRow с помощью набора методов расширения (см. главу 12). Еще один важный
класс — TypedTableBaseExtensions, определяющий методы расширения, которые
можно применять к строго типизированным объектам DataSet, чтобы внутренние
объекты DataTable могли взаимодействовать с LINQ. Все остальные члены сборки
System.Data.DataSetExtensions.dll просто обеспечивают инфраструктуру и не
предназначены для непосредственного применения в кодовой базе.
Получение DataTable, совместимого с LINQ
А теперь посмотрим, как можно использовать расширения DataSet. Допустим, у
нас имеется новое консольное С#-приложение с именем LinqToDataSetApp. Добавьте
в него ссылку на самую последнюю и самую замечательную версию C.0.0.0) сборки
AutoLotDAL.dll и измените первоначальный кодовый файл, чтобы он содержал
следующую логику:
using System;
using System.Data;
// Местоположение строго типизированных контейнеров данных.
using AutoLotDAL;
854 Часть V. Введение в библиотеки базовых классов .NET
// Местоположение строго типизированных адаптеров данных.
using AutoLotDAL.AutoLotDataSetTableAdapters;
namespace LinqToDataSetApp
{
class Program
{
static void Main(string[ ] args)
{
Console.WriteLine ("***** LINQ через DataSet *****\n");
// Получение строго типизированного DataTable, содержащего
// текущие данные таблицы Inventory из базы данных AutoLot.
AutoLotDataSet dal = new AutoLotDataSet ();
InventoryTableAdapter da = new InventoryTableAdapter();
AutoLotDataSet.InventoryDataTable data = da.GetData();
// Здесь будут вызываться описанные ниже методы!
Console.ReadLine();
}
}
}
Чтобы преобразовать объект ADO.NET DataTable (в том числе и строго
типизированный DataTable) в объект, совместимый с LINQ, необходимо вызвать метод
расширения AsEnumerable(), определенный в классе DataTableExtensions. Метод возвращает
объект Enumerable RowCollection, который содержит коллекцию объектов Data Row.
После этого тип EnumerableRowCollection можно использовать для обработки
каждой строки с помощью основного синтаксиса DataRow (т.е. с использованием
индексатора). Рассмотрим приведенный ниже новый метод из класса Program, который
принимает строго типизированный DataTable, получает перечисляемую копию данных и
выводит все значения Car ID:
static void PrintAllCarlDs(DataTable data)
{
// Получение перечисляемой версии DataTable.
EnumerableRowCollection enumData = data.AsEnumerable();
// Вывод идентификаторов автомобилей.
foreach (DataRow r in enumData)
Console.WriteLine("Car ID = {0}", r ["CarlD"]);
}
LINQ-запросы здесь еще не использовались, а смысл этого фрагмента в том, что
к объекту enumData теперь можно применять выражения LINQ-запросов. Объект
EnumerableRowCollection содержит коллекцию объектов DataRow, т.к. для вывода
значений CarlD индексатор типа применяется к каждому подобъекту.
В большинстве случаев нет необходимости объявлять переменную типа
EnumerableRowCollection для хранения значения, возвращаемого AsEnumerable().
Этот метод можно вызывать из самого выражения запроса. А вот более интересный
метод из класса Program, который получает проекцию CarlD + Makes для всех элементов
из DataTable с красным цветом (если в вашей таблице Inventory нет красных
автомобилей, измените LINQ-запрос):
static void ShowRedCars(DataTable data)
{
// Проекция нового результирующего набора, содержащего
// значения ID/цвет для строк, в которых Color = Red.
var cars = from car in data .AsEnumerable ()
Глава 22. ADO.NET, часть II: автономный уровень 855
where
(string)car["Color"] == "Red"
select new
{
ID = (int)car["CarlD"],
Make = (string)car["Make"]
};
Console.WriteLine ("На складе имеются следующие красные автомобили:");
foreach (var item in cars)
{
Console.WriteLine("-> CarlD = {0} - {1}", item.ID, item.Make);
}
}
Метод расширения Da taRowEx tens ions. Field<T>()
Одним из неприятных аспектов текущего выражения LINQ-запроса является то, что
для получения результирующего набора приходится применять различные операции
приведения и индексаторы DataRow, а это может привести к исключениям времени
выполнения, если будет выполнена попытка приведения для несовместимых типов.
Для внесения в запрос строгой типизации можно использовать метод расширения
Field<T>() типа DataRow. Он позволяет увеличить межтиповую безопасность запроса,
т.к. совместимость типов данных проверяется еще на этапе компиляции. Рассмотрим
следующее изменение:
var cars = from car in data. AsEnumerable ()
where
car.Field<string>("Color") == "Red"
select new
{
ID = car.Field<int>("CarlD"),
Make = car.Field<string>("Make")
};
В этом случае вызывается метод Field<T>() с указанием типа параметра, который
представляет соответствующий тип данных столбца. В качестве аргумента методу
передается имя этого столбца. Из-за наличия этой дополнительной проверки на этапе
компиляции рекомендуется при обработке элементов EnumerableRowCollection
использовать метод Field<T>() (а не индексатор DataRow).
Кроме вызова метода AsEnumerable(), общий формат LINQ-запроса остается в
точности таким же, как и в главе 13. Поэтому мы не будем здесь повторять и объяснять
различные операторы LINQ. При желании дополнительные примеры можно посмотреть
в разделе "LINQ to DataSet Examples" документации по .NET Framework 4.0 SDK.
Заполнение новых объектов DataTable с помощью LINQ-запросов
Заполнение нового DataTable результатами LINQ-запросов выполняется легко, если
не используются проекции. При наличии результирующего набора с типом, который
можно представить как IEnumerable<T>, можно вызвать для результата метод
расширения CopyToDataTable<T>():
static void BuildDataTableFromQuery(DataTable data)
{
var cars = from car in data .AsEnumerable ()
where
car.Field<int>("CarlD") >5
select car;
856 Часть V. Введение в библиотеки базовых классов .NET
// Использование этого результирующего набора для создания нового DataTable.
DataTable newTable = cars.CopyToDataTable ();
// Вывод содержимого DataTable.
for (int curRow = 0; curRow < newTable.Rows.Count; curRow++)
{
for (int curCol = 0; curCol < newTable.Columns.Count; curCol++)
{
Console.Write(newTable.Rows[curRow] [curCol] .ToString () .Trim() + "\t");
}
Console.WriteLine();
}
}
На заметку! Можно также преобразовать LINQ-запрос в тип DataView — с помощью метода
расширения AsDataView<T>().
Эта техника может оказаться удобной, если нужно использовать результат LINQ-
запроса в качестве источника для операции привязки к данным. Вспомните, что у
элемента Windows Forms DataGridView (а также ASP.NET и элемента графической таблицы
в WPF) имеется свойство DataSource. Привязку результата LINQ-запроса к графической
таблице можно выполнить так:
// Здесь myDataGrid - объект графической таблицы.
myDataGrid.DataSource = (from car in data.AsEnumerable()
where
car.Field<int>(,,CarID") >5
select car).CopyToDataTable();
На этом рассмотрение автономного уровня ADO.NET завершено. С помощью этого
аспекта API можно выбирать данные из реляционной базы данных, обрабатывать эти
данные и возвращать в базу, открывая подключение к базе данных лишь на
минимальный промежуток времени.
Исходный код. Проект LinqOverDataSet доступен в подкаталоге Chapter 22.
Резюме
В данной главе был подробно рассмотрен автономный уровень ADO.NET. Как вы
видели, центром всего автономного уровня является тип DataSet — размещенное в
памяти представление любого количества таблиц и (возможно) любого количества
отношений, ограничений и выражений. Отношения между локальными таблицами удобны
тем, что позволяют осуществлять программную навигацию между ними без
подключения к удаленному хранилищу данных.
В этой главе была также рассмотрена роль типа адаптера данных. С помощью этого
типа (и соответствующих свойств SelectCommand, InsertCommand, UpdateCommand и
DeleteCommand) адаптер может приводить изменения в DataSet в соответствие с
исходным хранилищем данных. Кроме того, вы научились переходить по объектной
модели DataSet прямолинейным ручным способом, а также с помощью строго
типизированных объектов, обычно сгенерированных средствами конструктора наборов данных
из Visual Studio 2010.
В конце главы был рассмотрен один аспект технологии LINQ под названием LINQ to
DataSet. Он позволяет получать копию DataSet, к которой можно применять
форматированные LINQ-запросы.
ГЛАВА 23
ADO.NET, часть III:
Entity Framework
В предыдущих двух главах рассматривались фундаментальные программные
модели ADO.NET, а именно — подключенный и автономный уровни. Эти подходы
позволяли программистам .NET работать с реляционными данными (в относительно
прямолинейной манере) с самого первого выпуска платформы. Однако в версии .NET 3.5
Serice Pack 1 был предложен совершенно новый компонент API-интерфейса ADO.NET,
который называется Entity Framework (EF).
Главная цель EF заключалась в том, чтобы предоставить возможность
взаимодействия с реляционными базами данных через объектную модель, которая отображается
непосредственно на бизнес-объекты приложения. Например, вместо трактовки пакета
данных как коллекции строк и столбцов можно оперировать коллекцией строго
типизированных объектов, именуемых сущностями (entities). Эти сущности также
естественным образом сочетаются с LINQ, и к ним можно выполнять запросы с
использованием той же грамматики LINQ, которая была описана в главе 13. Исполняющая среда EF
транслирует запросы LINQ в подходящие запросы SQL.
В этой главе вы ознакомитесь с программной моделью EF Будут подробно
рассматриваться различные части инфраструктуры, включая службы объектов, клиент
сущностей, LINQ to Entities и Entity SQL. Также будет описан формат наиболее важного
файла *.edmx и его роль в API-интерфейсе Entity Framework. Вы научитесь генерировать
файлы *.edmx с помощью Visual Studio 2010 и командной строки, используя утилиту
генератора EDM (EdmGen.exe).
К концу главы вы построите финальную версию сборки AutoLotDal.dll и узнаете,
как привязать сущностные объекты к настольному приложению Windows Forms.
Роль Entity Framework
Подключенный и автономный уровни ADO.NET снабжают фабрикой, которая
позволяет выбирать, вставлять, обновлять и удалять данные с помощью объектов
соединений, команд, чтения данных, адаптеров данных и DataSet. Хотя все это замечательно,
эти аспекты ADO.NET заставляют трактовать полученные данные в манере, которая
тесно связана с физической схемой данных. Вспомните, например, что при
использовании подключенного уровня обычно производится итерация по каждой записи за счет
указания имен столбцов объекту чтения данных. С другой стороны, в случае работы
с автономным уровнем придется иметь дело с коллекциями строк и столбцов объекта
DataTable внутри контейнера DataSet.
858 Часть V. Введение в библиотеки базовых классов .NET
Если используется автономный уровень в сочетании со строго типизированными
классами DataSet или адаптерами данных, то получаете программную абстракцию,
которая предоставляет некоторые важные преимущества. Во-первых, строго
типизированный класс DataSet представляет данные таблицы через свойства класса. Во-вторых,
строго типизированный адаптер таблицы поддерживает методы, которые
инкапсулируют конструирование лежащих в основе операторов SQL. Вспомним метод AddRecords ()
из главы 22:
public static void AddRecords(AutoLotDataSet.InventoryDataTable tb,
InventoryTableAdapter dAdapt)
{
// Получение из таблицы новой строго типизированной строки.
AutoLotDataSet.InventoryRow newRow = tb.NewInventoryRow();
// Заполнение строки данными.
newRow.CarlD = 999;
newRow.Color = "Purple";
newRow.Make = "BMW";
newRow.PetName = "Saku";
// Вставка новой строки.
tb.AddlnventoryRow(newRow) ;
// Добавление еще одной строки с помощью перегруженного метода Add.
tb. AddlnventoryRow (888, "Yugo", "Green", "Zippy");
// Обновление базы данных.
dAdapt.Update(tb);
}
Все становится еще лучше, если скомбинировать автономный уровень с LINQ to
DataSet. В этом случае можно применить запросы LINQ к находящимся в памяти
данным, получить новый результирующий набор и затем дополнительно отобразить его
на автономный объект, такой как DataTable, List<T>, Dictionary<K, V> или массив
данных:
static void BuildDataTableFromQuery(DataTable data)
{
var cars = from car in data.AsEnumerable ()
where car.Field<int>("CarlD") > 5 select car;
// Использовать этот результирующий набор для построения нового объекта DataTable.
DataTable newTable = cars.CopyToDataTable ();
// Работать с объектом DataTable...
}
Несмотря на удобство интерфейса LINQ to DataSet, следует помнить, что целью
запроса LINQ являются данные, возвращенные из базы данных, а не сам механизм базы
данных. В идеале хотелось бы строить запрос LINQ, который отправлялся бы на
обработку непосредственно базе данных, и возвращал строго типизированные данные
(именно это и позволяет достичь ADO.NET Entity Framework).
При использовании подключенного и автономного уровней ADO.NET всегда
приходится помнить о физической структуре лежащей в основе базы данных. Необходимо
знать схему каждой таблицы данных, писать сложные SQL-запросы для
взаимодействия с данными таблиц и т.д. Это вынуждает писать довольно громоздкий код С#,
поскольку С# существенно отличается от языка самой базы данных.
Вдобавок способ конструирования физической базы данных (администратором баз
данных) полностью сосредоточен на таких конструкциях базы, как внешние ключи,
представления и хранимые процедуры. Сложность баз данных, спроектированных
администратором, может еще более возрастать, если администратор при этом заботится
о безопасности и масштабируемости. Это также усложняет код С#, который приходится
писать для взаимодействия с хранилищем данных.
Глава 23. ADO.NET, часть III: Entity Framework 859
Платформа ADO.NET Entity Framework (EF) — это программная модель, которая
пытается заполнить пробел между конструкциями базы данных и
объектно-ориентированными конструкциями. Используя EF, можно взаимодействовать с реляционными
базами данных, не имея дело с кодом SQL (при желании). Исполняющая среда EF
генерирует подходящее операторы SQL, когда вы применяете запросы LINQ к строго
типизированным классам.
На заметку! LINQ to Entities — это термин, описывающий применение запросов LINQ к
сущностным объектам ADO.NET.
Другой возможный подход состоит в том, чтобы вместо обновления базы данных
посредством нахождения строки, обновления строки и отправки строки обратно на
обработку в пакете запросов SQL, просто изменять свойства объекта и сохранять его
состояние. И в этом случае исполняющая среда EF обновляет базу данных автоматически.
В Microsoft считают ADO.NET Entity Framework новым членом семейства технологий
доступа к данным, и не намерены заменять им подключенный и автономный уровни.
Однако после недолгого использования EF часто отдается предпочтение этой развитой
объектной модели перед относительно примитивным миром SQL-запросов и коллекций
строк/столбцов.
Тем не менее, иногда в проектах .NET используются все три подхода, поскольку одна
только модель EF чрезмерно усложняет код. Например, при построении внутреннего
приложения, которому нужно взаимодействовать с единственной таблицей базы
данных, подключенный уровень может применяться для запуска пакета хранимых
процедур. Существенно выиграть от использования EF могут более крупные приложения,
особенно если команда разработчиков уверено работает с LINQ. Как с любой новой
технологией, следует знать, как (и когда) имеет смысл применять ADO.NET EF.
На заметку! Вспомните, что в .NET 3.5 появился API-интерфейс программирования баз данных
под названием LINQ to SQL. Он был построен на основе концепции, близкой (даже очень
близкой в смысле конструкций программирования) к ADO.NET ЕЕ Хотя LINQ to SQL формально еще
существует, официальное мнение в Microsoft состоит в том, что теперь следует обращать
внимание на EF, а не на LINQ to SQL.
Роль сущностей
Строго типизированные классы, упомянутые ранее,
называются сущностями (entities). Сущности — это
концептуальная модель физической базы данных, которая
отображается на предметную область. Формально говоря, эта
модель называется моделью сущностных данных (Entity
Data Model — EDM). Модель EDM представляет собой
набор классов клиентской стороны, которые отображаются на
физическую базу данных. Тем не менее, нужно понимать, р oq i г к
что сущности вовсе не обязаны напрямую отображаться на ,„, " " -. ., пп w
J^ r J r цЫ inventory базы данных
схему базы данных, как может показаться, исходя из на- AUtoLot
звания. Сущностные классы можно реструктурировать для
соответствия существующим потребностям, и исполняющая среда EF отобразит эти
уникальные имена на корректную схему базы данных.
Например, вспомним простую таблицу Inventory из базы данных Auto Lot, схема
которой показана на рис. 23.1.
Inventory
? СагЮ
Make
Color
PetName
860 Часть V. Введение в библиотеки базовых классов .NET
Если сгенерировать EDM для таблицы Inventory базы данных
AutoLot (ниже будет показано, как это делается), то по умолчанию
сущность будет называться Inventory. Тем не менее, сущностный
класс можно переименовать в Саг и определить для него уникально
именованные свойства по своему выбору, которые будут
отображены на столбцы таблицы Inventory. Такая слабая привязка
означает возможность формирования сущностей так, чтобы они наиболее
точно соответствовали предметной области. На рис. 23.2 показан
пример сущностного класса.
На заметку! Во многих случаях сущностный класс клиентской стороны
называется по имени связанной с ним таблицы базы данных. Однако помните,
что вы всегда можете изменить сущность для лучшего соответствия
конкретной ситуации.
Теперь давайте рассмотрим следующий класс Program, в котором используется
сущностный класс Саг (и связанный с ним класс по имени AutoLotEntities) для
добавления новой строки к таблице Inventory базы данных AutoLot. Этот класс называется
контекстом объектов, и его назначение — обеспечивать взаимодействие с физической
базой данных (о деталях речь пойдет ниже).
class Program
{
static void Main(string [ ] args)
{
// Строка соединения автоматически читается
//из сгенерированного конфигурационного файла.
using (AutoLotEntities context = new AutoLotEntities ())
{
// Добавить новую строку в таблицу Inventory, используя сущность Саг.
context.Cars.AddObject(new Car() { AutoIDNumber = 987, CarColor = "Black",
MakeOfCar = "Pinto",
NicknameOfCar = "Pete" });
context.SaveChanges ();
}
}
}
Обязанность исполняющей среды EF — позаботиться о клиентском представлении
таблицы Inventory (класс по имени Саг в рассматриваемом случае) и выполнить
обратное отображение на корректные столбцы таблицы Inventory. Обратите внимание, что
здесь нет никаких следов SQL-оператора INSERT; производится просто добавление
нового объекта Саг в коллекцию, поддерживаемую соответственно названным свойством
Cars в контексте объектов, после чего изменения сохраняются. Если затем заглянуть
в таблицу данных с помощью проводника сервера в Visual Studio 2010, можно увидеть
там добавленную новую строку (рис. 23.3).
В приведенном примере нет никакой магии. "За кулисами" процесса открывается
соединение с базой данных, генерируется подходящий оператор SQL и т.д. Преимущество
EF состоит в том, что эти детали обрабатываются без вашего участия. Теперь давайте
взглянем на базовые службы EF, которые все это делают.
Строительные блоки Entity Framework
API-интерфейс EF находится на вершине существующей инфраструктуры ADO.NET,
которая рассматривалась в предыдущих двух главах.
^ ..... _j^
3 Properties
ff AutoIDNumber
*? MakeOfCar
I^JP CarColor
^ NicknameOfCar
S Navigation Properties
Рис. 23.2.
Сущность Car —
это клиентское
представление
схемы Inventory
Глава 23. ADO.NET, часть III: Entity Framework 861
► 93? Pinto Black Pete
999 BMW Red FocFcc
1000 BMW Black Bimmer
||1яиан11вашнаввманаишишнаи1^1^нв '
I И 4 ;'6 Of 10 ! ► И ► \ф J
Рис. 23.3. Результат сохранения контекста
Подобно любому взаимодействию ADO.NET, сущностная платформа использует
поставщик данных ADO.NET для взаимодействия с хранилищем данных. Однако
поставщик данных должен быть обновлен, чтобы поддерживать новый набор служб, прежде
чем он сможет взаимодействовать с API-интерфейсом EF. И как можно было ожидать,
поставщик данных Microsoft SQL Server уже обновлен соответствующей
инфраструктурой, которая полагается на использование сборки System.Data.Entity.dll.
На заметку! Многие сторонние базы данных (например, Oracle и MySQL) предлагают EF-
совместимые поставщики данных. Детальную информацию можно узнать у поставщика
системы управления базами данных или просмотреть список известных поставщиков данных
ADO.NET по адресу www.sqlsummit.com/dataprov.htm.
В дополнение к добавлению необходимых компонентов к поставщику данных
Microsoft SQLServer, сборка System.Data.Entity.dll содержит различные
пространства имен, которые сами полагаются на службы ЕЕ Две ключевых части API-интерфейса
EF, на которые следует обратить внимание сейчас — это службы объектов (object
services) pi клиент сущности (entity client).
Роль служб объектов
Под службами объектов подразумевается часть EF, которая управляет сущностями
клиентской стороны при работе с ними в коде. Службы объектов отслеживают
изменения, внесенные в сущность (например, смена цвета автомобиля с зеленого на синий),
управляют отношениями между сущностями (скажем, просмотр всех заказов для клиента
с заданным именем), а также обеспечивают возможности сохранения изменений в базе
данных и сохранение состояния сущности с помощью сериализации (XML и двоичной).
С точки зрения программирования, уровень службы объектов управляет любым
классом, расширяющим базовый класс EntityObject. Как и ожидалось, EntityObject
представляет цепочку наследования для любых сущностных классов в программной
модели EF. Например, взглянув на цепочку наследования сущностного класса Саг из
предыдущего примера, можно увидеть, что Саг связан с EntityObject отношением
"является" (рис. 23.4).
Роль клиента сущности
Вторым важным аспектом API-интерфейса EF является уровень клиента сущности.
Эта часть API-интерфейса EF отвечает за работу с поставщиком данных ADO.NET для
установки соединений с базой данных, генерации необходимых SQL-операторов на
основе состояния сущностей и запросов LINQ, отображения извлеченных данных на
корректные формы сущностей, а также управления прочими деталями, которые обычно
приходится делать вручную, если не используется Entity Framework.
862 Часть V. Введение в библиотеки базовых классов .NET
>♦ EntityObjectO
ft ReportPropertyChanged(string)
$♦ ReportPropertyChanging(string)
Я1 EntityKey
Я1 EntityState
Рис. 23.4. Уровень служб объектов EF может управлять
любым классом, расширяющим EntityObject
Функциональность уровня клиента сущности определена в пространстве имен
System.Data.EntityClient. Это пространство имен включает набор классов,
которые отображают концепции EF (такие как запросы LINQ to Entity) на лежащий в
основе поставщик данных ADO.NET. Эти классы (т.е. EntityCommand и EntityConnection)
очень похожи на классы, которые можно найти в составе поставщика данных ADO.NET;
например, на рис. 23.5 показано, что классы уровня клиента сущности расширяют те
же абстрактные базовые классы любого другого поставщика (например, DbCommand и
DbConnection; см. главу 22).
л {) System.Data.EntrtyCttent
л d Base Types
Г> Щ$ DbCommand \
s Щ% EntityConnection
л i_j Base Types
> *\% DbConnection
л <*£ Entit>ConnectionStringBuilder
л Q| Base Types
U *£ DbConnectionStringBuilder
л Щ% EntityDataReader
* Гц Base Types
i> Щ% DbDataReader
-° IDataRecord
> ** JExtendedDataReccrd
л ^ EntttyParameter
л Q| Base Types
t> ■*!$ DbParameter
I
J
. Ш -
:-№
,* CrtateDbPaiameterU
•'• CreateParameterO
♦ EntityCommendfstring, S>stem.Data.EntityClient.Ej
Ф Ent'rtyCommand(stn'ng, System.Data.EntityClient.E|
♦ EntrtyCcmmand(stringj
-• EntityCommandO
j% ExecuteDbDataRead*r(System.Data.CommandBeh
♦ ExecuteNonQueryO
-♦ ExecuteReadertSystem.Deta.CommandBehaviof}
♦ ExecuteReadenj
♦ ExecuteScalarO
H
public sealed class EntityCommand :
Syrtgm Oata.CommonPbCommand
Member of $ystem.BiiftJ^Ut^knJ;
Summary:
-Represents a command to be executed against an
Щ Entity Data Model (EDM).
4
Рис. 23.5. Уровень клиента сущности отображает команды сущности
на лежащий в основе поставщик данных ADO.NET
Уровень клиента сущности обычно работает "за кулисами", но вполне допустимо
взаимодействовать с клиентом сущности напрямую, если нужен полный контроль над
его действиями (прежде всего, над генерацией запросов SQL и обработкой
возвращенных данных из базы).
Если требуется более тонкий контроль над тем, как сущностный клиент строит SQL-
оператор на основе входящего запроса LINQ, можно использовать Entity SQL. Это
независимый от базы данных диалект SQL, который работает непосредственно с
сущностями. Построенный запрос Entity SQL может быть отправлен непосредственно службам
Глава 23. ADO.NET, часть III: Entity Framework 863
клиента сущности (или, при желании, объектным службам), где он будет сформатиро-
ван в правильный SQL-оператор для лежащего в основе поставщика данных. Далее в
этой главе будет приведено несколько примеров применения Entity SQL.
Если требуется более высокая степень контроля над манипуляциями
извлеченными результатами, можно отказаться от автоматического отображения результатов базы
данных на сущностные объекты и вручную обрабатывать записи с помощью класса
EntityDataReader. Как и можно было ожидать, EntityDataReader позволяет
обрабатывать извлеченные данные с использованием однонаправленного, доступного только
для чтения потока данных, как это делает SqlDataReader. Ниже в главе
рассматривается работающий пример этого подхода.
Роль файла *.edmx
Подводя итог сказанному: сущности — это классы клиентской стороны, которые
функционируют, как модель сущностных данных (Entity Data Model). Хотя сущности
клиентской стороны в конечном итоге отображаются на таблицу базы данных, жесткая
связь между именами свойств сущностных классов и именами столбцов таблиц с
данными отсутствует.
В контексте API-интерфейса Entity Framework, чтобы данные сущностных классов
отображались на данные таблиц корректно, требуется правильное определение логики
отображения. В любой системе, управляемой моделью данных, уровни сущностей,
реальной базы данных и отображения разделены на отдельные части: концептуальная
модель, логическая модель и физическая модель.
• Концептуальная модель определяет сущности и отношения между ними (если есть).
• Логическая модель отображает сущности и отношения на таблицы с любыми
необходимыми ограничениями внешних ключей.
• Физическая модель представляет возможности конкретного механизма данных,
указывая детали хранилища, такие как табличная схема, разбиение на разделы
и индексация.
В мире EF каждый из этих трех уровней фиксируется в XML-файле. В
результате использования интегрированных визуальных конструкторов Entity Framework из
Visual Studio 2010 получается файл с расширением *.edmx. Этот файл содержит XML-
описания сущностей, физической базы данных и инструкции относительно того, как
отображать эту информацию между концептуальной и физической моделями. Формат
файла *.edmx рассматривается в первом примере этой главы.
При компиляции основанных на EF проектов в Visual Srudio 2010 файл *.edmx
используется для генерации трех отдельных файлов XML: один для концептуальной модели
данных (*.csdl), один для физической модели (*.ssdl) и один для уровня отображения
(*.msl). Данные из этих трех XML-файлов затем объединяются с приложением в виде
двоичных ресурсов. После компиляции сборка .NET имеет все необходимые данные для
вызовов API-интерфейса EF, имеющихся в коде.
Роль классов ObjectContext и ObjectSet<T>
Последним фрагментом мозаики EF является класс ObjectContext, определенный
в пространстве имен System.Data.Objects. Генерация файла *.edmx дает в результате
сущностные классы, которые отображаются на таблицы базы данных, и класс,
расширяющий ObjectContext. Обычно этот класс используется для непрямого
взаимодействия со службами объектов и функциональностью клиента сущности.
Класс ObjectContext предлагает набор базовых служб для дочерних классов,
включая возможность сохранения всех изменений (которые в конечном итоге превращаются
864 Часть V. Введение в библиотеки базовых классов .NET
в обновление базы данных), настройку строки соединения, удаление объектов, вызов
хранимых процедур, а также обработку других фундаментальных деталей. В табл. 23.1
описаны некоторые основные члены класса ObjectContext (не забывайте, что
большинство этих членов остаются в памяти, пока не будет произведен вызов SaveChanges ()).
Таблица 23.1. Общие члены ObjectContext
Член Назначение
AcceptAllChanges () Принимает все изменения, проведенные в сущностных объектах
внутри контекста объектов
AddObj ect () Добавляет объект к контексту объектов
DeleteOb j ect () Помечает объект для удаления
ExecuteFunction<T> () Выполняет хранимую процедуру в базе данных
ExecuteStoreCommandO Позволяет отправлять команду SQL прямо в хранилище данных
GetObjectByKey () Находит объект внутри контекста объектов по его ключу
SaveChanges () Отправляет все обновления в хранилище данных
CommandTimeout Это свойство получает или устанавливает значение таймаута в
секундах для всех операций контекста объектов
Connection Это свойство возвращает строку соединения, используемую
текущим контекстом объектов
SavingChanges Это событие инициируется, когда контекст объектов сохраняет
изменения в хранилище данных
Класс, унаследованный от ObjectContext, служит контейнером, управляющим
сущностными объектами, которые сохраняются в коллекции типа ObjectSet<T>.
Например, в результате генерации файла *.edmx для таблицы Inventory базы
данных AutoLot получается класс с именем (по умолчанию) AutoLotEntities. Этот класс
поддерживает свойство по имени Inventories (обратите внимание на множественное
число), которое инкапсулирует член данных ObjectSet<Inventory>. В случае
создания EDM для таблицы Orders базы данных AutoLot в классе AutoLotEntities
определено второе свойство по имени Orders, которое инкапсулирует переменную-член
ObjectSet<Order>. В табл. 23.2 определены некоторые общие члены System.Data.
Objects.ObjectSet<T>.
Таблица 23.2. Общие члены ObjectSet<T>
Член
AddObjectO
CreateOb]ect<T>
DeleteObject
Назначение
Позволяет вставить новый сущностный объект в коллекцию
Создает новый экземпляр указанного сущностного типа
Помечает объект для удаления
Добравшись до нужного свойства контекста объектов, можно вызывать любой
член ObjectSet<T>. Еще раз рассмотрим простой код примера, приведенного в начале
этой главы:
using (AutoLotEntities context = new AutoLotEntities ())
{
// Добавить новую строку в таблицу Inventory, используя сущность Саг.
Глава 23. ADO.NET, часть III: Entity Framework 865
context.Cars.AddObject(new Car() { AutoIDNumber = 987, CarColor = "Black",
MakeOfCar = "Pinto",
NicknameOfCar = "Pete" });
context.SaveChanges();
}
Здесь AutoLotEntities "является" ObjectContext. Свойство Cars предоставляет
доступ к переменной ObjectSet<Car>. Эта ссылка используется для вставки нового
сущностного объекта Саг и выполнения ObjectContext операции сохранения всех
изменений в базе данных.
Обычной целью запросов LINQ to Entities является экземпляр класса ObjectSet<T>;
этот класс поддерживает те же расширяющие методы, о которых говорилось в главе 13.
Более того, ObjectSet<T> получает значительную часть своей функциональности от
своего прямого предка ObjectQuery<T> — класса, представляющего строго
типизированный запрос LINQ (или Entity SQL).
Собираем все вместе
Прежде чем построить первое приложение, в котором используется Entity Framework,
взгляните на рис. 23.6, на котором показана организация API-интерфейса EF.
Кодовая база С#
Entity SQL
Entity
SQL
|| Гос|| fh
IEnumerable<T>
Объектные службы
*.edmx
w
F\
\z
Дерево команд
ТГ^
EntityDataReader
Поставщик данных клиента сущности
Дерево команд
ТГ^
DbDataReader
Поставщик данных AD0.NET
И"
Физическая
база данных
Рис. 23.6. Базовые компоненты AD0.NET Entity Framework
Составные части на рис. 23.6 не столь сложны, как могут показаться на первый
взгляд. Например, рассмотрим следующий распространенный сценарий. Вы пишете
код С#, в котором применяется запрос LINQ к сущности, полученной от контекста. Этот
запрос проходит через службы объектов, где преобразует команду LINQ в дерево,
которое может понять клиент сущности. В свою очередь, клиент сущности форматирует это
дерево в правильный оператор SQL для лежащего в основе поставщика ADO.NET. Этот
поставщик вернет читатель данных (т. е. объект-наследник DbDataReader), который
клиентские службы используют для направления данных объектным службам,
используя EntityDataReader. Кодовая база получает обратно перечисление данных
сущностей (IEnumerable<T>).
866 Часть V. Введение в библиотеки базовых классов .NET
Теперь рассмотрим другой сценарий. Кодовая база С# желает получить больший
контроль над тем, как клиентские службы конструируют конечный оператор SQL для
отправки в базу данных. Поэтому код С# пишется с использованием Entity SQL,
который может быть передан непосредственно клиенту сущности или объектным службам.
Конечный результат возвращается в виде IEnumerable<T>.
В любом из этих сценариев XML-данные из файла *.edmx должны быть сделаны
известными клиентским службам; это позволит им понять, как следует отображать
элементы базы данных на сущности. Наконец, помните, что клиент (т.е. кодовая база С#)
также может получить результаты, отправленные сущностным клиентом, применяя
EntityDataReader непосредственно.
Построение и анализ первой модели EDM
Понимая предназначение платформы ADO.NET Entity Framewrok, а также имея
общее представление о ее работе, можно приступать к рассмотрению первого примера.
Чтобы пока не усложнять картину, построим модель EDM, которая обеспечит доступ
только к таблице Inventory базы данных AutoLot. Разобравшись с основами, мы затем
построим новую модель EDM, которая будет рассчитана на всю базу данных AutoLot, и
отобразим данные в графическом интерфейсе пользователя.
Генерация файла *.edmx
Начнем с создания консольного приложения по имени InventoryEDMConsoleApp.
Когда планируется использование Entity Framework, первый шаг состоит в генерации
необходимой концептуальной, логической и физической модели данных, определенной
в файле *.edmx. Один из способов предусматривает применение для этого утилиты
командной строки EdmGen.exe из комплекта .NET 4.0 SDK. Откройте окно командной
строки Visual Studio 2010 и введите следующую команду:
EdmGen.exe -?
На консоль выводится список опций, которые можно указывать утилите для
генерации необходимых файлов, основываясь на существующей базе данных; кроме того,
доступны опции для генерации совершенно новой базы данных на основе имеющихся
сущностных файлов. В табл. 23.3 описаны некоторые общие опции EdmGen.exe.
Таблица 23.3. Часто используемые флаги командной строки утилиты EdmGen.exe
Опция Назначение
/mode:FullGeneration Генерировать файлы *.ssdl, *.msl, *.csdl и клиентские сущности
из указанной базы данных
/proj ect: Базовое имя, которое должно использоваться для сгенерированного
кода и файлов. Обычно это имя базы данных, из которой
извлекается информация (допускается сокращенная форма — /р:)
/connectionstring: Строка соединения, используемая для взаимодействия с базой
данных (допускается сокращенная форма — /с:)
/language: Позволяет указать, какой синтаксис должен использоваться для
сгенерированного кода — С# или VB
/pluralize Позволяет автоматически выбирать множественное или
единственное число для имени набора сущностей, имени типа сущности и
имени навигационного свойства, согласно правилам английского языка
Глава 23. ADO.NET, часть III: Entity Framework 867
Как и платформа .NET 4.0 в целом, программная модель EF поддерживает
программирование в стиле сначала домен, что позволяет создавать свойства (с
применением типичных объектно-ориентированных приемов) и использовать их для
генерации новой базы данных. В этом вводном обзоре ADO.NET EF ни подход
"сначала модель", ни генерация сущностной модели клиентской стороны с помощью
утилиты EdmGen.exe применяться не будут. Вместо этого будут использоваться
визуальные конструкторы EDM из среды Visual Studio 2010.
Выберите пункт меню Project^Add New Item (ПроектеДобавить новый элемент) и
вставьте новый элемент ADO.NET Entity Data Model по имени InventoryEDM.edmx, как
показано на рис. 23.7.
Add Ne* Item - Inventor/ED
InsUlled Templates
л Visual C# Items
Code
Data
General
Web
Windows Forms
WPF
Reporting
Workflow
I K|jL LINQ to SQL Classes
j Service-based Database
Visual C# hems
Per user extensions are currently not allowed to load.
Name InventoryEDM.edmx
Рис. 23.7. Вставка нового элемента проекта AD0.NET EDM
Щелчок на кнопке Add (Добавить) приводит к запуску мастера создания модели
сущностных данных (Entity Data Model Wizard). На первом шаге мастер позволяет выбрать,
нужно генерировать EDM из существующей базы данных либо определить пустую модель
(для разработки в стиле "сначала модель"). Выберите опцию Generate from database
(Генерировать из базы данных) и щелкните на кнопке Next (Далее), как показано на рис. 23.8.
На втором шаге мастера выбирается база данных. Если соединение с базой
данных внутри проводника сервера Visual Studio 2010 уже существует, оно будет
присутствовать в раскрывающемся списке. Если же нет, щелкните на кнопке New Connection
(Создать соединение). В любом случае выберите базу данных AutoLot и отметьте
флажок Save entity connection settings in App.config as (Сохранить настройки соединения в
файле App.config как), как показано на рис. 23.9.
Прежде чем щелкать на кнопке Next, взгляните на формат строки соединения:
metadata=res : //VlnventoryEDM. csdl | res: //VlnventoryEDM.ssdl | res : //VlnventoryEDM.msl;
provider=System. Data. SqlClient;provider connection stnng=
"Data Source=(local)\SQLEXPRESS;
Initial Catalog=AutoLot;Integrated Security=True;Pooling=False"
Основной интерес в ней представляет флаг metadata, который используется для
указания имен встроенных данных XML-ресурсов концептуального, физического и
файла отображений (вспомните, что во время компиляции файл *.edmx будет разделен на
отдельные файлы, и данные этих файлов примут вид двоичных ресурсов, встраиваемых
в сборку).
868 Часть V. Введение в библиотеки базовых классов .NET
Entity Data Model Wizard
Choose Model Contents
Empty model
Generates the model from a database. Classes are generated from the model when the project is compiled.
This wizard also lets you specify the database connection and database objects to include in the model.
J^kJ-
Рис. 23.8. Генерация модели EDM из существующей базы данных
Choose Your Data Connection
Which data connection should your application use to connect to the database?
| andrewpc\sqlexpress.AutoLot.dbo
^J [ New Connection...
This connection string appears to contain sensitr.e data (fw example a pass*oid) that к required 1o
connect to the database. Storing sensitive data in tht connection stnng can be a security nsk. Do you want
to include this sensitive data in the connection rtring7
I eta from the connection rtnng 1 will set it i
Yes, include the sensitive data in the connection string.
Entity connection string:
metadata=res://n*/lnventoryEDM.csdl|res://*/InventoryEDM.ssdl|
res://*/InventoryEDM.msl;provider=System.Data.SqlClient;provider connection string="Data Source=
(locaO\SQLEXPRESS;InitialCatalog=AutoLofIntegratedSecurity=True;Pooling=Farse"
} Save entity connection settings in App.Config as;
AutoLotEntities
previous Next Г
Рис. 23.9. Выбор базы данных для генерации EDM
На последнем шаге мастера можно выбрать элементы из базы данных, для
которой необходимо сгенерировать модель EDM. В рассматриваемом примере ограничимся
только таблицей Inventory (рис. 23.10).
Щелкните на кнопке Finish (Готово) для генерации модели EDM.
Глава 23. ADO.NET, часть III: Entity Framework 869
s gfg Tables
□3 CreditRisks (dbo)
LJ3 Customers (dbo)
ЭЭ Inventory (dboV
B3 Orders (dbo) ■*
!_ J3 sysdiagrams (dbo)
(_Г)*р Views
* (ЛЫ Stored Procedures
!7'"_m fn_diagramobjects (dbo)
ОЭ GetPetName (dbo)
ОПП sp_alterdiagram (dbo)
S ]^~] sp_creatediagram (dbo)
■:
[/] Pluralrze or jingufarize generated object names
у j Include foreign key columns in the model
Model Namespace:
AutoLotModel
<£revious }
N.e*t ■
Finish
Рис. 23.10. Выбор элементов базы данных
Изменение формы сущностных данных
После завершения работы с мастером откроется визуальный конструктор EDM в
IDE-среде с одной сущностью по имени Inventory. Просмотреть композицию любой
сущности в визуальном конструкторе можно в окне Model Browser (Браузер моделей),
которое открывается через пункт меню View1^Other Windows (Вид1^ Другие окна). Теперь
взгляните на формат концептуальной модели для таблицы базы данных Inventory,
представленный в папке Entity Types (Типы сущности), как показано на рис. 23.11.
В узле хранилища, имя которого совпадает с именем базы данных (AutoModel.Store),
находится физическая модель базы данных.
InventoryEDM.edmx xQ
-
,...- , ,-... ,
Inventory E)
_n—
- Properties
*$CarID
!*j*Make
*? Color
_<? PetName
Ь Navigation Properties
s
К
4 '" >.ф
[Model Browser
1 Type here to search
1 л ^ InventoryEDM.edmx
* [£| AutoLotModel
* U Entity Types
л ^Inventory
tiCarfD
. 3* Color
*f Make
Ч/О? PetName
l3 Complex Types
_jl Associations
^ EntityContamer AutoLotEntrhes
л \J AutoLotModel.Store
л d Tables / Views
1 Color
I] Make
H PetName
uj Stored Procedures
kjj Constraints
^ ? X
Рис. 23.11. Визуальный конструктор EDM и окно браузера моделей
870 Часть V. Введение в библиотеки базовых классов .NET
По умолчанию имена сущностей будут основаны на именах исходных объектов баз
данных; однако, вспомните, что имена сущностей в концептуальной модели могут быть
любыми. Чтобы изменить имя сущности либо имена свойств сущности, необходимо
выбрать нужный элемент в визуальном конструкторе и установить соответствующим
образом свойство Name в окне свойств (Properties). Переименуйте сущность Inventory
в Саг и свойство PetName в CarNickname (рис. 23.12).
Концептуальная модель должна выглядеть подобно тому, как показано на рис. 23.13.
Теперь выберите сущность Саг в визуальном конструкторе и снова загляните в окно
Properties. Вы должны увидеть поле Entity Set Name (Имя набора сущностей), также
переименованное из Inventories в Cars (рис. 23.14). Значение Entity Set Name важно,
потому что оно соответствует имени свойства в классе контекста данных, который
используется для модификации базы данных. Вспомните, что это свойство инкапсулирует
переменную-член ObjectSet<T> класса-наследника ObjectContext.
Прежде чем двигаться дальше, скомпилируйте приложение; это приведет к
обновлению кодовой базы и генерации файлов *.csdl, *.msl и *.ssdl на основе данных файла
*.edmx.
Properties
AutoLotModel.Car.CarNickname
:: U\3
t Documentation
Entity Key
Fixed Length
Getter
Max Length
Nullable
Setter
Property
Fake
False
Public
50
j CarNickname
(None)
Public
* *x
The name of the property.
Рис. 23.12. Изменение формы сущностей с помощью окна свойств
** Cat ©
а Properties
ЩСагЮ
fit Make
^jjf Color
^ CarNickname
3 Navigation Properties
Рис. 23.13. Модель
клиентской стороны,
измененная в
соответствии с
предметной областью
Properties
AutoLotModel.Car
Abstract
Access
Base Type
I > Documentation
ЕЕЕЕЕППЕ
Name
EntrtyType
- * x]
False
Public
(None)
| Can
Car
entity Set Name
The name of the EntitySet that contains instances of the entity.
Рис. 23.14. Имя оболочки свойства ObjectSet<T>
Глава 23. ADO.NET, часть III: Entity Framework 871
Просмотр отображений
Имея данные в измененной форме, можно просматривать отображения между
концептуальным уровнем и физическим уровнем в окне Mapping Details (Сведения об
отображениях), которое открывается через пункт меню View=> Other Windows1^ Mapping
Details (Вид1^Другие окна1^ Сведения об отображениях). Взгляните на рис. 23.15 и
обратите внимания, что узлы в левой части дерева представляют имена данных из
физического уровня, в то время как узлы справа представляют имена концептуальной модели.
Mapping Details - Car
Column
* Tables
л Maps to Inventory
3£ <AcldaConditran>
л i_j Column Mappings
$JDCarID:int
33 Make:varehar
ffj Color: varchar
J] jPetName: varchar
Г*3 <Add a Tdbieor'view>
Oper..
♦-►
4~*
«-►
Value / Property
t^ CarID:lnt32
^f Make: String
*f Color: String
■^вСШНЗИЕЖЯЯГИ Hi
■ ши M.appinqD
Рис. 23.15. В окне Mapping Details можно просматривать
отображения концептуальной и физической моделей
Просмотр данных сгенерированного файла *.edmx
Теперь давайте посмотрим, что именно мастер EDM Wizard сгенерировал. Щелкните
правой кнопкой мыши на файле InventoryEDM.edmx в проводнике решения и
выберите в контекстном меню пункт Open With... (Открыть с помощью). В открывшемся
диалоговом окне выберите опцию XML Editor (Редактор XML). Это позволит просмотреть XML-
данные, лежащие в основе представления в визуальном конструкторе EDM. Структура
этого XML-документа разделена на четыре части: все они находятся в корневом
элементе <edms:Edmx>.
Подэлемент <edmx:Runtime> определяет XML-данные для концептуальной,
физической и модели уровня отображения. Ниже показано определение физической таблицы
базы данных Inventory:
<!-- Содержимое SSDL -->
<edmx:StorageModels>
<Schema Namespace="AutoLotModel.Store" Alias="Self"
Provider="System.Data.SqlClient"
ProviderManifestToken= 008"
xmlns:store=
"http://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator"
xmlns="http://schemas.microsoft.com/ado/200 9/02/edm/ssdl">
<EntityContainer IJame="AutoLotModelStoreContainer">
<EntitySet Name="Inventory" EntityType="AutoLotModel.Store.Inventory"
store:Type="Tables" Schema="dbo" />
</EntityContainer>
<EntityType Name="Inventory">
<Key>
<PropertyRef Name="CarID" />
</Key>
<Property Name="CarID" Type="int" Nullable="false" />
<Property Name="Make" Type="varchar" Nullable="false" MaxLength=0" />
872 Часть V. Введение в библиотеки базовых классов .NET
<Property Name="Color" Type="varchar" Nullable="false" MaxLength=0" />
<Property Name="PetName" Type="varchar" MaxLength=0" />
</EntityType>
</Schema>
</edmx:StorageModels>
Обратите внимание, что узел <Schema> определяет имя поставщика данных
ADO.NET, который использует эту информацию при взаимодействии с базой данных
(System.Data.SqlClient). Узлами <EntityType> помечается имя физической таблицы
базы данных, а также каждый столбец в таблице.
Следующая важная часть файла *.edmx — элемент <edmx:ConceptualModels>,
который определяет измененные сущности клиентской стороны. Как видно, сущность Cars
определяет свойство CarNickname, которое изменяется в визуальном конструкторе:
<'-- Содержимое CSDL -->
<edmx:ConceptualModels>
<Schema Namespace="AutoLotModel" Alias="Self"
xmlns:annotation="http://schemas.microsoft.com/ado/200 9/02/edm/annotation"
xmlns="http://schemas.microsoft.com/ado/2 00 8/0 9/edm">
<EntityContainer
Name="AutoLotEntities" annotation:LazyLoadingEnabled="true">
<EntitySet Name="Cars" EntityType="AutoLotModel.Car" />
</EntityContainer>
<EntityType Name="Car">
<Key>
<PropertyRef Name="CarID" />
</Key>
<Property Name="CarID" Type="Int32" Nullable="false" />
<Property Name="Make" Type="String" Nullable="false" MaxLength=0"
Unicode="false" FixedLength="false" />
<Property Name="Color" Type="String" Nullable="false" MaxLength=0"
Unicode="false" FixedLength="false" />
<Property Name="CarNickname" Type="String" MaxLength=0"
Unicode="false" FixedLength="false" />
</EntityType>
</Schema>
</edmx:ConceptualModels>
Это перемещает на уровень отображения, который окно Mapping Details и
исполняющая среда EF используют для подключения имен в концептуальной модели к
физической модели:
<!-- Содержимое отображения C-S -->
<edmx:Mappings>
<Mapping Space="C-S"
xmlns="http://schemas.microsoft.com/ado/2008/0 9/mapping/cs">
<EntityContainerMapping StorageEntityContainer="AutoLotModelStoreContainer"
CdmEntityContainer="AutoLotEntities">
<EntitySetMapping Name="Cars">
<EntityTypeMapping TypeName="AutoLotModel.Car">
<MappingFragment StoreEntitySet="Inventory">
<ScalarProperty Name="CarID" ColumnName="CarID" I>
<ScalarProperty Name="Make" ColumnName="Make" />
<ScalarProperty Name="Color" ColumnName="Color" />
<ScalarProperty Name="CarNickname" ColumnName="PetName" />
</MappingFragmentx/EntityTypeMapping>
</EntitySetMapping>
</EntityContainerMapping>
</Mapping>
</edmx:Mappings>
Глава 23. ADO.NET, часть III: Entity Framework 873
Последней частью файла *.edmx является
элемент <Designer>, который исполняющей средой EF
не используется. Он содержит инструкции,
используемые Visual Studio для отображения сущностей на
поверхности визуального конструктора.
Удостоверьтесь, что проект скомпилирован, по
крайней мере, однажды, и щелкните на кнопке Show
All Files (Показать все файлы) в проводнике решений.
Затем зайдите в папку obj\Debug, а после этого — в
edmxResourcesToEmbed. Здесь находятся три XML-
файла, основанные на содержимом файла *.edmx
(рис. 23.16).
Данные в этих файлах будут встроены в сборку
как двоичные ресурсы. Таким образом, приложение
.NET обладает всей информацией, необходимой для
понимания концептуального, физического и уровня
отображения модели EDM.
Просмотр сгенерированного исходного кода
Теперь вы почти готовы к тому, чтобы написать некоторый код, использующий
построенную модель EDM. Однако прежде чем сделать это, стоит заглянуть в
сгенерированный код С#. Откройте окно Class View (Представление класса) и раскройте
пространство имен по умолчанию. В дополнение к классу Program там будет присутствовать
сгенерированный мастером EDM Wizard сущностный класс (который вы переименовали
в Саг) и другой класс по имени AutoLotEntities.
Зайдя в Solution Explorer и раскрыв узел InventoryEDM.edmx, вы увидите
поддерживаемый IDE-средой файл по имени InventoryEDM.Designer.cs. Как и любой другой
файл подобного рода, его не следует редактировать напрямую, потому что IDE-среда
пересоздает его при каждой компиляции. Тем не менее, этот файл можно открыть для
просмотра двойным щелчком кнопкой мыши,
Класс AutoLotEntities расширяет класс ObjectContext, который (как вы,
возможно, помните), представляет собой входную точку в программную модель EF.
Конструктор предоставляет различные способы заполнения данными строки
соединения. Конструктор по умолчанию сконфигурирован на автоматическое чтение данных
строки соединения из сгенерированного мастером файла App.conf ig:
public partial class AutoLotEntities : ObjectContext
{
public AutoLotEntities () : base("name=AutoLotEntities" , "AutoLotEntities")
{
this.ContextOptions.LazyLoadingEnabled = true;
OnContextCreated () ;
}
}
Затем обратите внимание, что свойство Cars класса AutoLotEntities
инкапсулирует член данных ObjectSet<Car>. Это свойство можно использовать для работы с
моделью EDM с целью непрямой модификации физической базы данных заднего плана:
public partial class AutoLotEntities : ObjectContext
{
Solution Explorer w [
^ Solution 'InventoryEDMConsoleApp' A project)
л .JH InventoryEDMConsoleApp
P iM Properties
13Й1 References
ILJ obj
-- :.J/x86
.._>■ Debug
i edmxResourcesToEmbed
j InventoryEDM.csdl
j InventoryEDM. msl
J InventoryEDM.ssdl
^ Solution Explorer I
Рис. 23.16. Файл *.edmx
используется для генерации трех
отдельных XML-файлов
874 Часть V. Введение в библиотеки базовых классов .NET
public ObjectSet<Car> Cars
{
get
{
if ( (_Cars == null) )
{
_Cars = base.CreateObjectSetKCar^ ("Cars");
}
return _Cars;
}
}
private ObjectSet<Car> _Cars;
}
На заметку! В классе, унаследованном от ObjectContext, доступно множество методов, имена
которых начинаются с AddTo. Хотя их можно использовать для добавления новых сущностей к
переменным-членам ObjectSet<T>, предпочтительнее делать это за счет обращения к члену
ObjectSet<T>, полученному от строго типизированных свойств.
И последним интересным аспектом в файле кода визуального конструктора
является сущностный класс Саг. Значительная часть кода каждого сущностного
класса представляет собой коллекцию, моделирующую форму концептуальной модели.
Каждое из этих свойств реализует свою логику set за счет вызова статического метода
StructuralObject.SetValueO из API-интерфейса EF.
Кроме того, логика set включает код, который информирует исполняющую среду
EF о том, что состояние сущности изменилось; это важно, поскольку ObjectContext
должен знать обо всех этих изменениях, чтобы вытолкнуть изменения в физическую
базу данных.
Вдобавок внутри логики set производятся вызовы двух частичных методов.
Вспомните, что частичный метод С# предоставляет простой способ обращения с
уведомлениями об изменениях в приложениях. Если частичный метод не реализован,
компилятор его игнорирует и полностью отбрасывает. Ниже приведена реализация
свойства CarNickname сущностного класса Саг:
public partial class Car : EntityObject
{
public global::System.String CarNickname
{
get
{
return _CarNickname;
}
set
{
OnCarNicknameChanging(value);
ReportPropertyChanging ("CarNickname");
_CarNickname = StructuralObject.SetValidValue(value, true);
ReportPropertyChanged("CarNickname") ;
OnCarNicknameChanged() ;
}
}
private global::System.String _CarNickname;
partial void OnCarNicknameChanging (global: :System.String value);
partial void OnCarNicknameChanged ();
}
Глава 23. ADO.NET, часть III: Entity Framework 875
Улучшение сгенерированного исходного кода
Все классы, сгенерированные визуальным конструктором, объявлены с ключевым
словом partial, которое позволяет разнести реализацию класса по нескольким файлам
кода С#. Это особенно полезно при работе с программной моделью EF, поскольку
означает возможность добавлять "реальные'' методы к сущностным классам, что помогает
лучше моделировать предметную область.
В этом примере будет переопределен метод ToStringO сущностного класса Саг
для возврата состояния сущности в виде хорошо форматированной строки. Также
будет завершены определения частичных методов OnCarNicknameChangingO и
OnCarNicknameChanged() для обслуживания простых диагностических уведомлений.
Определим следующий частичный класс в новом файле Car.cs:
public partial class Car
{
public override string ToStringO
{
// Поскольку столбец PetName может быть пустым,
// укажем в качестве имени по умолчанию **No Name**,
return string. Format (" {0} is a {1} {2} with ID {3}.",
this.CarNicftname ?? "**No Name**",
this.Color, this.Make, this.CarlD);
partial void OnCarNicknameChanging(global::System.String value)
Console.WriteLine ("\t-> Changing name to: {0}", value);
partial void OnCarNicknameChanged ()
Console.WriteLine ("\t-> Name of car has been changed!");
}
Помните, после предоставления реализации этих частичных методов можно
получать уведомления, когда свойства сущностных: классов изменены или изменяются, но
не при изменении физической базы данных. Если требуется знать, когда изменяется
физическая база данных, можно обработать событие SavingChanges
класса-наследника ObjectContext.
Программирование с использованием
концептуальной модели
Теперь можно написать некоторый код, взаимодействующий с моделью EDM.
Начнем с добавления в метод Main() класса Program вызова одного вспомогательного
метода, который выводит каждый элемент из базы данных Inventory с
использованием концептуальной модели и еще одного, который вставляет новую запись в таблицу
Inventory:
class Program
{
static void Main(string [ ] args)
{
Console.WriteLine("***** Fun with ADO.NET EF *****");
AddNewRecord();
PrintAllInventory();
Console.ReadLine();
}
876 Часть V. Введение в библиотеки базовых классов .NET
private static void AddNewRecord ()
{
// Добавить запись в таблицу Inventory базы данных AutoLot.
using (AutoLotEntities context = new AutoLotEntities())
{
try
{
// Жестко закодировать данные новой записи (для целей тестирования).
context.Cars.AddObject(new Car () { CarID = 2222,
Make = "Yugo", Color = "Brown" });
context.SaveChanges();
}
catch(Exception ex)
{
Console.WriteLine(ex.InnerException.Message);
}
}
}
private static void PnntAllInventory ()
{
// Выбрать все элементы из таблицы Inventory базы AutoLot и вывести данные,
// используя специальный метод ToStringO сущностного класса Саг.
using (AutoLotEntities context = new AutoLotEntities())
{
foreach (Car с in context.Cars)
Console.WriteLine(c);
}
}
}
Подобный показанному выше код уже встречался ранее в этой главе, но сейчас вы
уже должны лучше представлять, как он работает. Каждый вспомогательный метод
создает экземпляр класса-наследника ObjectContext (AutoLotEntities) и использует
строго типизированное свойство Cars для взаимодействия с полем ObjectSet<Car>.
Перечисление каждого элемента, представленного свойством Cars, позволяет передать
неявно SQL-оператор SELECT лежащему в основе поставщику данных ADO.NET. За счет
вставки нового объекта Саг методом AddObjectO класса ObjectSet<Car> и
последующего вызова SaveChanges () на контексте выполняете SQL-оператор INSERT.
Удаление записи
Для того чтобы удалить запись из базы данных, сначала необходимо найти
корректный элемент в ObjectSet<T>. Это можно сделать, передав объект EntityKey (член
пространства имен System.Data) методу GetObjectByKeyO. Предполагая, что это
пространство имен импортировано в файл кода С#, теперь можно написать следующий
вспомогательный метод:
private static void RemoveRecord ()
{
// Найти автомобиль для удаления по первичному ключу.
using (AutoLotEntities context = new AutoLotEntities())
{
// Определить ключ для искомой сущности.
EntityKey key = new EntityKey("AutoLotEntities.Cars", "CarlD", 2222);
// Проверить ее существование, и если да - удалить.
Car carToDelete = (Car) context.GetObjectByKey (key);
Глава 23. ADO.NET, часть III: Entity Framework 877
if (carToDelete != null)
{
context.DeleteObject(carToDelete) ;
context.SaveChanges() ;
}
}
}
На заметку! Хорошо это или плохо, но вызов GetObjectByKey () требует обращения к базе
данных, прежде чем можно будет удалить объект.
Обратите внимание, что при создании объекта EntityKey в первом аргументе
передается объект string, информирующий о том, какой объект ObjectSet<T> должен
обрабатываться в заданном классе-наследнике ObjectContext. Второй аргумент (еще
один объект string) представляет имя свойства сущностного класса, которое служит
ключом, а последний аргумент — это значение первичного ключа. Как только нужный
объект найден, можно вызвать DeleteObjectO контекста и сохранить изменения.
Обновление записи
Обновление записи также делается просто: найдите объект, который хотите
изменить, установите новые значения свойств возвращенной сущности и сохраните
изменения:
private static void UpdateRecord ()
{
// Найти автомобиль для обновления по первичному ключу.
using (AutoLotEntities context = new AutoLotEntities ())
{
// Определить ключ для сущности, которую мы ищем.
EntityKey key = new EntityKey("AutoLotEntities.Cars", "CarlD", 2222);
// Извлечь объект автомобиля, изменить его и сохранить.
Car carToUpdate = (Car)context.GetObjectByKey(key);
if (carToUpdate '= null)
{
carToUpdate.Color = "Blue";
context.SaveChanges ();
}
}
}
Приведенный выше метод может показаться немного странным, по крайней
мере, до тех пор, пока вы не вспомните, что сущностный объект, возвращенный из
GetObjectByKey () — это ссылка на существующий объект в поле Object Set<T>. Таким
образом, установка свойств для изменения состояния приводит к изменению того же
объекта в памяти.
На заметку! Во многом подобно объекту DataRow из AD0.NET (см. главу 22), любой наследник
EntityObject (т.е. все сущностные классы) имеет свойство по имени EntityState,
используемое контекстом объектов для определения того, был ли элемент модифицирован, удален,
отсоединен и т.п. Оно устанавливается без вашего участия при работе с программной моделью;
тем не менее, при необходимости его можно изменять вручную.
878 Часть V. Введение в библиотеки базовых классов .NET
Запросы с помощью LINQ to Entities
До сих пор использовались несколько простых методов на контексте объектов и
сущностные объекты для выполнения выборки, вставки, обновления и удаления. Это
удобно и само по себе; однако гораздо больший эффект от EF можно получить, добавив
запросы LINQ. Чтобы использовать LINQ для обновления или удаления записей,
создавать объект EntityKey вручную не понадобится. Взгляните на следующее изменение в
методе RemoveRecordO, которое пока не работает должным образом:
private static void RemoveRecordO
{
// Найти автомобиль для удаления по первичному ключу,
using (AutoLotEntities context = new AutoLotEntities ())
{
// Проверить его наличие.
var carToDelete = from с in context.Cars where c.CarlD == 2222 select c;
if (carToDelete != null)
{
context.DeleteObject(carToDelete);
context.SaveChanges ();
}
}
}
Этот код скомпилируется, но при попытке вызвать метод Delete Ob ject()
возникнет исключение времени выполнения. Причина в том, что этот конкретный запрос
LINQ возвращает объект Ob]ectQuery<T>, а не объект Саг. Помните, что запрос LINQ
для поиска единственной сущности дает в результате объект ObjectQuery<T>, который
представляет запрос, доставляющий необходимые данные. Чтобы выполнить запрос
(и вернуть сущность Саг), потребуется выполнить такой метод, как FirstOrDefaultO,
на объекте запроса, как показано в следующем примере:
var carToDelete =
(from с in context.Cars where c.CarlD == 2222 select c).FirstOrDefault();
Вызов FirstOrDefaultO на ObjectQuery<T> позволяет искать нужный элемент;
если не окажется экземпляра Саг с идентификатором 2222, будет получено значение по
умолчанию — null. Рассмотрим несколько дополнительных примеров запросов LINQ:
private static void FunWithLINQQueries ()
{
using (AutoLotEntities context = new AutoLotEntities ())
{
// Получить проекцию новых данных.
var colorsMakes = from item in context.Cars select
new { item.Color, item.Make };
foreach (var item in colorsMakes)
{
Console.WriteLine(item);
}
// Получить только элементы с CarID < 1000
var idsLessThanlOOO = from item in context.Cars
where item.CarlD < 1000 select item;
foreach (var item in idsLessThanlOOO)
{
Console.WriteLine(item);
}
}
}
Глава 23. ADO.NET, часть III: Entity Framework 879
Хотя синтаксис этих запросов достаточно прост, помните, что при каждом
применении запроса LINQ к контексту объектов происходит обращение к базе данных! Когда
нужно получить независимую копию данных, которая может быть целью новых
запросов LINQ, пригодятся (среди прочих) расширяющие методы ToList<T>(), ToArray<T>()
или ToDictionary<K, V>(). Ниже показан измененный предыдущий метод, который
выполняет эквивалент SELECT *, кэширует сущности в виде массива и манипулирует
данными массива с помощью LINQ to Objects.
using (AutoLotEntities context = new AutoLotEntities () )
{
// Чтобы получить все данные из таблицы Inventory,
// можно было бы также написать следующий код:
// var allData = (from item in context.Cars select item) .ToArrayO;
var allData = context.Cars.ToArray();
// Получить проекцию новых данных.
var colorsMakes = from item in allData select new { item.Color, item.Make };
// Получить только элементы с CarlD < 1000
var idsLessThanlOOO = from item in allData where
item.CarlD < 1000 select item;
}
Работа с LINQ to Entities намного интереснее, когда модель EDM содержит несколько
взаимосвязанных таблиц. Ниже будут продемонстрированы некоторые примеры,
иллюстрирующие сказанное; а сейчас давайте завершим текущий пример рассмотрением
двух других способов взаимодействия с контекстом объектов.
Запросы с помощью Entity SQL
В большинстве случаев запросы к ObjectSet<T> производятся с помощью LINQ.
Сущностный клиент преобразует запрос LINQ в соответствующий SQL-оператор и
передает его на обработку базе данных. В случаях, когда необходим больший контроль над
формированием запроса, можно воспользоваться Entity SQL.
Entity SQL — это SQL-подобный язык, который может применяться к сущностям.
Хотя формат операторов Entity SQL подобен традиционному SQL, все же они не
идентичны. Entity SQL обладает уникальным синтаксисом, так как имеет дело с запросом,
а не с физической базой данных. Подобно запросу LINQ to Entities, запрос Entity SQL
используется для передачи "реального" SQL-запроса базе данных.
Подробные сведения о командах Entity SQL можно найти в документации .NET
Framework 4.0 SDK, а ниже представлен один пример. Взгляните на следующий метод,
где строится запрос Entity SQL, который находит все черные автомобили в коллекции
ObjectSet<Car>:
private static void FunWithEntitySQL ()
{
using (AutoLotEntities context = new AutoLotEntities())
{
// Построить строку, содержащую синтаксис Entity SQL.
string query = "SELECT VALUE car FROM AutoLotEntities.Cars " +
"AS car WHERE car.Color='black'";
// Теперь построить ObjectQuery<T> на основе строки,
var blackCars = context.CreateQuery<Car>(query);
foreach (var item in blackCars)
{
Console.WriteLine(item);
}
}
}
880 Часть V. Введение в библиотеки базовых классов .NET
С форматированный оператор Entity SQL передается в качестве аргумента методу
CreateQuery<T> контекста объектов.
Работа с объектом EntityDataReader
При использовании LINQ to Entities или Entity SQL извлеченные данные
отображаются на сущностные классы автоматически, благодаря службе клиента сущности.
Обычно именно это и нужно; но при желании можно перехватить результирующий
набор, прежде чем он превратится в сущностные объекты, и вручную обработать с
использованием EntityDataReader.
Ниже приведен последний вспомогательный метод для этого примера, в котором
используются несколько членов пространства имен System.Data.EntityClient для
построения соединения вручную, через объект команды и чтения данных. Этот код
должен показаться знакомым по главе 21; основное отличие состоит в том, что
применяется Entity SQL, а не "нормальный" SQL.
private static void FunWithEntityDataReader ()
{
// Создать объект соединения на основе файла *.config.
using (EntityConnection en = new EntityConnection (llname=AutoLotEntities11) )
{
en.Open ();
// Построить запрос Entity SQL.
string query = "SELECT VALUE car FROM AutoLotEntities.Cars AS car";
// Создать командный объект.
using (EntityCommand cmd = en.CreateCommand())
{
cmd.CommandText = query;
// Получить объект для чтения данных и обработать записи.
using (EntityDataReader dr =
cmd.ExecuteReader(CommandBehavior.SequentialAccess))
{
while (dr.ReadO )
{
Console.WriteLine ("***** RECORD *****");
Console.WriteLine ("ID: {0}", dr["CarlD"]);
Console.WriteLine ("Make: {0}", dr["Make"]);
Console.WriteLine("Color : {0}", dr["Color"]);
Console.WriteLine ("Pet Name: {0}", dr["CarNickname"]);
Console.WriteLine ();
}
}
}
}
}
Этот начальный пример должен открыть вам дорогу к пониманию деталей работы
с Entity Framework. Как упоминалось ранее, все становится намного интереснее, когда
модель EDM содержит взаимосвязанные таблицы, о чем речь пойдет ниже.
Исходный код. Проект InventoryEDMConsoleApp доступен в подкаталоге Chapter 23.
Глава 23. ADO.NET, часть III: Entity Framework 881
Проект AutoLotDAL версии 4.0,
теперь с сущностями
Далее будет показано, как строить модель EDM, которая охватывает значительную
часть базы данных AutoLot, включая хранимую процедуру GetPetName. Рекомендуется
скопировать проект AutoLotDAL (Version 3), созданный в главе 22, и переименовать
копию в AutoLotDAL (Version 4).
Откройте проект AutoLotDAL (Version 4) в Visual Studio 2010 и добавьте новый
элемент ADO.NET Entity Data Model (Модель сущностных данных ADO.NET) по
имени AutoLotDALEF. edmx. На третьем шаге мастера понадобится выбрать таблицы
Inventory, Orders и Customers (таблица CreditRisks пока не нужна), а также
специальную хранимую процедуру (рис. 23.17).
Which database objects do you want to include in your model?
ГТЩр Tables
HG3 CreditRisks (dbo)
SO Customers (dbo)
ШШ1 Inventory (dbo)
ШШ Orders (dbo)
LJZ3 sysdiagrams (dbo)
| jj Views
л 2Ш Stored Procedures
1 \~~\ fn_diagramobjects (dbo)
[7[~1 GetPetName (dbo)
I 1 \l\ sp_alterdiagram (dbo)
1'ТП sp^creatediagram (dbo)
I ■iiff", r u,v—-..
\J\ Pluralize or singulartze generated object names
\/\ Include foreign key columns in the model
Model Namespace: j
AutoLotModel
< Previous J j Next > Finish Cancel
Рис. 23.17. Построение файла *.edmx для большей части базы
данных AutoLot
В отличие от первого примера с EDM, на этот раз переименование сущностных
классов и их свойств не требуется.
Отображение хранимой процедуры
Теперь рассмотрим один не совсем очевидный аспект мастера EDM Wizard: отметкой
имени хранимой процедуры с целью ее включения в концептуальную модель работа не
завершается. В этот момент все, что вы сделали — это сообщили IDE-среде о
существовании физической хранимой процедуры; чтобы она материализовалась в коде,
потребуется импортировать функцию на концептуальный уровень. Откройте браузер модели
(выбрав пункт меню View1^Other Windows1^Entity Data Model Browser (Вид^ Другие
окна1^ Браузер модели сущностных данных)) и обратите внимание, что хранимая
процедура GetPetName видна в физической базе данных (в узле AutoLotModel.Store), но папка
Function Imports (Импорты функций), выделенная на рис. 23.18, пока пуста.
882 Часть V. Введение в библиотеки базовых классов .NET
Model Browser
Type here to search
4 AutoLotDAL_EF.edmx
л [£) AutolotModd
' Ш Entity Types
t "^J Customer
I ^ Inventory
f ^J Order
lJ Complex Types
л LJ Associations
03 FK_Orders_Customers
SO FK Orders Inventory
EntityContainen AutoLotEntrbes
_l Entity Sets
:'.J Association Sets
,_J Function Imports
-< Qj AutoLotModeLStore
^ Si Tables / Views
1*3 Customers
3 Inventory
GJ Orders
^ J Stored Procedures
л ^GetPetName
фсагТО
<§3 petName
Li Constraints
Начните с отображения этого на концептуальный
уровень, щелкнув правой кнопкой мыши на папке Function
Imports и выбрав в контекстном меню пункт Add Function
Import (Добавить импорт функции), как показано на
рис. 23.19.
В открывшемся диалоговом окне необходимо выбрать
имя физической хранимой процедуры (в
раскрывающемся списке Stored Procedure Name (Имя хранимой
процедуры)) и указать имя метода для отображения на
концептуальную модель. В этом случае имена будут отображаться
напрямую. Вспомните, что хранимая процедура на самом
деле не возвращает набор записей, однако у нее есть
выходной параметр. Поэтому отметьте переключатель None
(Нет), как показано на рис. 23.20.
Роль навигационных свойств
I Model Bro..
Если теперь посмотреть на визуальный конструктор
EDM, можно увидеть, что все таблицы учтены,
включая новые сущности в разделе Navigation Properties
(Навигационные свойства) данного сущностного класса
(рис. 23.21).
Как следует из названия, навигационные свойства
позволяют выражать операции JOIN в программной модели
Entity Framework (без необходимости написания
сложных SQL-операторов). Чтобы учесть эти отношения внешних ключей, каждая сущность
в файле *.edmx теперь содержит некоторые новые данные XML, которые показывают,
как сущности соединены между собой через данные ключей. Чтобы просмотреть
разметку, откройте файл *.edmx в редакторе XML; тем не менее, некоторая информация
видна и в папке Associations (Ассоциации) браузера моделей (рис. 23.22).
Рис. 23.18. Хранимые
процедуры должны быть
импортированы, прежде чем их можно
будет вызывать
Model Browser
Type here to search
4s AutolotDAL_EF.edmx
л [£ AutoLotModel
* _J Entity Types
*^J Customer
■$$ Inventory
*% Order
L3 Complex Types
•* LJ Associations
OS FK_Orders_Customers
ЙУ FK_Orders_Inventory
* 33 EntityContainen AutoLotEntrbes
Li Entity Sets
12 Association Sets
LJ Function Imports
л \J AutoLotModeLStore
* Ш Tables /Views
13 Customers - j
■ Ш Inventory
C3 Orders
^ t_J Stored Procedures i
* S^GetPetName \
jjSjcariD
Ф petName
_J Constraints
Add Function Import...
Update Model from Database...
Generate Database from Model...
Add Code Generation hem...
Q
Рис. 23.19. Выбор импорта хранимой процедуры в концептуальную модель
Глава 23. ADO.NET, часть III: Entity Framework 883
Function Import Name:
I GetPetName
] Stored Procedure Name.
GetPetName
Returns a Collection Of
о None
" Scalers:
О Complex:
Stored Procedure Column Information
Get Column Information !
Click on "Get Column Information" above to retrieve the stored
procedure's schema. Once the schema is available, click on "Create Ne
Complex Type" below to create a compatible complex type. You can
also always update an existing complex type to match the returned
schema. The changes will be applied to the model once you click on
OK.
j Uwte Ne* Comple < Tyj <?
Рис. 23.20. Отображение физической хранимой процедуры
на концептуальную модель
AutoLotDALJF.edmx* х|
t§CusHD
2Jf FirstName
!"lf LastName
S Navigation Properties
Щ Orders
|£
^OrderTO
^CustTO
■SPCarTO
a Navigation Properties
Щ Customer
Щ Inventory'
-
a Properties
?§CarID
-fMake
^ Color
■•fPetName
3 Navigation Properties
^Orders
Рис. 23.21. Навигационные свойства
884 Часть V. Введение в библиотеки базовых классов .NET
Model Browser - П X
Type here to search
p 4 AutaLotDAL_EF.edmx
- _5_ AutoLotModet
- L_i Entity Types
* ^J Customer
^Inventory
SJ FK_Orders_Customers
1_Й FK_Ofders_Invefrtory
Э EntityContainer: AutoLotEntities
I {J AutoLotModet.Store
Рис. 23.22. Просмотр отношений между сущностями
При желании номер версии этой новой библиотеки можно изменить на 4.0.0.0
(используя кнопку Assembly Information (Информация о сборке) на вкладке Applications
(Приложения) окна Properties (Свойства)). Прежде чем перейти к созданию первого
клиентского приложения, скомпилируйте модифицированную сборку AutoLot.dll.
Исходный код. Проект AutoLotDAL (Version 4) доступен в подкаталоге Chapter 23.
Использование навигационных свойств
внутри запросов LINQ to Entity
Теперь давайте посмотрим, как использовать навигационные свойства внутри
контекста запросов LINQ to Entities (их можно также использовать и с Entity SQL, однако в
этой книге данная тема не рассматривается). Прежде чем выполнить привязку данных
к графическому интерфейсу Windows Forms, потребуется создать еще одно консольное
приложение по имени AutoLotEDMClient. Создав проект, установите ссылку на System.
Data.Entity.dll и на последнюю версию AutoLotDAL.dll.
Добавьте файл App.Config из проекта AutoLotDAL (используя пункт меню
Project^Add Existing Item (ПроектеДобавить существующий элемент)) и импортируйте
пространство имен AutoLotDAL. Вспомните, что в файле App.Config, сгенерированном
с помощью утилиты EdmGen.exe, определена корректная строка соединения, которой
требуется для Entity Framework.
Давайте обновим физическую таблицу Orders несколькими новыми записями.
В частности, необходимо обеспечить, чтобы один заказчик имел несколько заказов.
Используя проводник сервера Visual Studio 2010, добавьте в таблицу Orders одну или
две новых записи, чтобы гарантировать наличие у одного заказчика двух иди более
заказов. Например, на рис. 23.23 заказчик с CustID, равным 4, имеет два отложенных
заказа на автомобили с CarlD, равными 1992 и 83.
Orders: Query (and
! OrderlD
1 • jiooo
1001
1002
[ 1003
► 1005
1* \NULL
H i 5
...sqlexpressAutoLot)
of 5
CustID
2
3
4
4
NULL
► И ► ■
x И
CadD
1000
678
904
1992
83
NULL
Рис. 23.23. Один заказчик с несколькими заказами
Глава 23. ADO.NET, часть III: Entity Framework 885
Теперь модифицируем класс Program, добавив новый вспомогательный метод
(вызываемый в Main ()). Этот метод использует навигационные свойства для выбора
каждого объекта Inventory по заказу для заданного заказчика:
private static void PrintCustomerOrders(string custID)
{
int id = int.Parse(custID);
using (AutoLotEntities context = new AutoLotEntities ())
{
var carsOnOrder = from о in context .Orders
where o.CustID == id select o.Inventory;
Console.WriteLine("\nCustomer has {0} orders pending:", carsOnOrder.Count()) ;
foreach (var item in carsOnOrder)
{
Console.WriteLine("-> {0} {1} named {2}.",
item.Color, item.Make, item.PetName);
}
}
}
Ниже показан вывод, полученный в результате запуска этого приложения (при
вызове PrintCustomerOrders() в Main() указан идентификатор заказчика, равный 4):
••••• Navigation Properties *****
Please enter customer ID: 4
Customer has 2 orders pending:
-> Pink Saab named Pinky. —
-> Rust Ford named Rusty.
Здесь в контексте обнаруживается единственная сущность Customer с указанным
значением CustID. Найдя заказчика, можно перейти к таблице Inventory для выбора
каждого заказанного им автомобиля. Возвращаемым значением запроса LINQ будет
перечисление объектов Inventory, которые выводятся на консоль с помощью
стандартного цикла foreach.
Вызов хранимой процедуры
Теперь в EMD-модели AutoLotDAL имеется вся необходимая информация для вызова
хранимой процедуры GetPetName. Это можно сделать одним из двух способов:
private static void CallStoredProc ()
{
using (AutoLotEntities context = new AutoLotEntities ())
{
ObjectParameter input = new ObjectParameter("carlD", 83);
ObjectParameter output = new ObjectParameter("petName", typeof(string));
// Вызвать ExecuteFunction на контексте...
context.ExecuteFunction("GetPetName", input, output);
II... либо использовать строго типизированный метод контекста,
context.GetPetName(83, output);
Console.WriteLine("Car #83 is named {0}", output.Value);
}
}
Первый подход предусматривает вызов метода ExecuteFunction() на контексте
объектов. В этом случае хранимая процедура идентифицируется строковым именем, а
каждый параметр представлен объектом типа ObjectParameter, который находится в
пространстве имен System.Data.Objects (не забудьте импортировать его в код С#).
886 Часть V. Введение в библиотеки базовых классов .NET
Второй подход состоит в использовании строго типизированного имени в контексте
объектов. Такой подход значительно проще, потому что входные параметры (такие как
идентификатор автомобиля) можно передавать как типизированные данные, а не как
объект ObjectParameter.
Исходный код. Проект AutoLotEDMClient доступен в подкаталоге Chapter 23.
Привязка данных сущностей к графическим
пользовательским интерфейсам Windows Forms
В завершения знакомства с ADO.NET Entity Framework давайте рассмотрим простой
пример, в котором производится привязка сущностных объектов к графическому
интерфейсу Windows Forms. Как упоминалось ранее в этой главе, операции привязки
данных будут демонстрироваться в проектах WPF и ASP.NET.
Создайте новое приложение Windows Forms по имени AutoLotEDMGUI и
переименуйте его начальную форму на MainForm. После создания проекта (подобно ранее
созданному клиентскому приложению) установите ссылки на сборку System.Data.Entity.dll
и на последнюю версию AutoLotDAL.dll. Добавьте файл App.config из проекта
AutoLotDAL (используя пункт меню Project^Add Existing Item) и импортируйте
пространство имен AutoLotDAL в файл кода главной формы.
В визуальном конструкторе форм добавьте элемент управления DataGridView и
переименуйте его на grid Invent or у. После помещения этого элемента управления на
поверхность визуального конструктора выберите встроенный редактор сетки (щелкнув на
маленькой стрелке в верхнем правом углу элемента DataGridView). В раскрывающемся
списке Choose Data Source (Выберите источник данных) укажите источник данных
проекта (рис. 23.24).
В этом случае необходима привязка не напрямую к базе данных, а к сущностному
классу, поэтому выберите в качестве типа источника данных Object (рис. 23.25).
На завершающем шаге мастера отметьте таблицу Inventory из AutoLotDAL.dll,
как показано на рис. 23.26 (если она не видна, значит, не была установлена ссылка на
эту библиотеку).
MainFarm.cs [Design]* X |
Z ■■ вТ а J |
Click the 'Add Projed
connect to data.
'ate Source...' link to
Рис. 23.24. Элемент управления DataGridView в приложении Windows Forms
Глава 23. ADO.NET, часть III: Entity Framework 887
■ СЬоом a Data Source Type
Where will the application get data from?
Database Service Object SharePcmt
Lets you choose objects that can later be used to generate data-bound controls.
j [ Naa> Г ^_*"*» Сави»
Рис. 23.25. Привязка к строго типизированному объекту
(^М—
Data Objects
Expand the referenced assemblies and namespaces to select your objects. If an object is missing from a referenced
assembly, cancel the wizard and rebuild the project that contains the object.
What objects do you want to bind to?
Ё1Э AutoLotEDM.GUI
i H'OAutoLotDAL
Q{> AutoLotConnectedLayer
л Щ{) AutoLotOAL
O^t AutoLotDataSet
Q*^ AutoLotEntities
И^> Customer
•; Inventory
ПЩ Order "W
И {} AutolotDAL.AutoLotDataSetTableAdapters
И {} AutoLotDisconnectedLayer
[ Add Ref степса J]
\J\ Hide system assemblies
Рис. 23.26. Выбор таблицы Inventory
После щелчка на кнопке Finish (Пэтово) сетка DataGridView отобразит все свойства
сущностного класса Inventory, включая и навигационные. Чтобы удалить их из
сетки, снова активизируйте встроенный редактор и щелкните на ссылке Edit Columns...
(Редактировать столбцы). В диалоговом окне Edit Columns (Редактирование столбцов)
выберите столбец Orders и удалите его из представления (рис. 23.27).
В завершение добавьте на форму элемент управления Button и переименуйте его на
btnUpdate. После этого поверхность визуального конструктора должен выглядеть
примерно так, как показано на рис. 23.28.
888 Часть V. Введение в библиотеки базовых классов .NET
Bound Column Properties
[ЩВВН
CcntextMenuStrip
MaxInputLength
ReadOnfy
Resizable
SortMode
л Data
DataPropertyName
л Design
(Name)
ordersDataGridViewTextBox< ,
DataGndViewTextBoxColum »
J 1 K*""**
(Name)
Indicates the name used in code to identify the object.
Рис. 23.27. Настройка внешнего вида DataGridView
Рис. 23.28. Окончательный вид пользовательского интерфейса
Добавление кода привязки данных
К этому моменту имеется экранная сетка, которая может отображать любое
количество объектов Inventory; однако для этого нужно еще написать соответствующий
код. Благодаря исполняющей среде EF это очень просто. Начнем с обработки событий
FormClosed и Load класса MainForm (используя окно Properties (Свойства)) и события
Click элемента управления Button. Модифицируем файл кода следующим образом:
public partial class MainForm : Form
{
AutoLotEntities context = new AutoLotEntities ();
public MainForm()
InitializeComponent ();
Глава 23. ADO.NET, часть III: Entity Framework 889
private void MainForm_Load(object sender, EventArgs e)
{
// Привязать коллекцию ObjectSet<Inventory> к сетке.
gridlnventory.DataSource = context.Inventories;
}
private void btnUpdate_Click(object sender, EventArgs e)
{
context.SaveChanges ();
MessageBox.Show("Data saved!");
}
private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
{
context.Dispose () ;
}
}
Вот и все, что потребуется сделать. Запустив приложение, можно добавлять новые
записи в сетку, выбирать строки и удалять их, а также модифицировать существующие
строки. Щелчок на кнопке Update (Обновить) приводит к автоматическому обновлению
таблицы Inventory, потому что контекст объектов достаточно интеллектуален, чтобы
генерировать необходимые SQL-операторы для автоматический выборки, обновления,
удаления и вставки. На рис. 23.29 показано готовое приложение Windows Forms.
г
iJ Entity Data Binding A
; CarlD
!°Г ......
■ ^—
[904
1ССЮ
<
Upd-! |
Make
Ford
Ford
Yugo
VW
BMW
III
Gator
Red
Yellow
Green
Back
Black
m 'itUm
Pet Name
Snake
Buzz
Clunker
Hank
Bimmer
i»
=
Рис. 23.29. Готовое приложение Windows Forms
Ниже перечислено несколько важных моментов, связанных с предыдущим
приложением.
• Контекст остается в памяти на протяжении работы приложения.
• Вызов context .Inventories выполняет SQL-код для извлечения всех строк из
таблицы Inventory в память.
• Контекст отслеживает все грязные сущности, так что знает, какой SQL-код
следует выполнить при вызове SaveChanges().
• После вызова SaveChanges () сущности снова считаются чистыми.
На этом рассмотрение ADO.NET Entity Framework завершено. Хотя об этой
программной модели можно рассказать много больше, чем уместилось в настоящей главе,
вы получили достаточное представление о задачах, которые EF пытается решить, и об
общей программной модели. За дополнительной информацией по API-интерфейсу EF
обращайтесь в документацию .NET Framework 4.0 SDK.
890 Часть V. Введение в библиотеки базовых классов .NET
Резюме
В этой главе рассмотрением роли Entity Framerowk завершены формальные
исследования программирования баз данных с использованием ADO.NET. Технология EF
позволяет программировать на уровне концептуальной модели, которая близко отражает
предметную область. Хотя можно изменять форму сущностей как угодно, исполняющая
среда EF гарантирует, что измененные данные модели будут отображены на
корректные данные физической таблицы.
В главе рассказывалось о роли (и составе) файлов *.edmx, а также о том, как их
генерировать с помощью IDE-среды Visual Studio 2010. Кроме того, было показано, как
отображать хранимые процедуры на функции концептуального уровня, как применять
запросы LINQ к объектной модели и как потреблять извлеченные данные на самом
низком уровне, используя EntityDataReader. Рассматривалась также и роль Entity SQL.
Завершил главу простой пример привязки сущностных классов к графическому
пользовательскому интерфейсу с использованием Windows Forms. Когда речь пойдет о
Windows Presentation Foundation и ASP.NET, будут продемонстрированы и другие
примеры привязки сущностей к приложениям с графическим интерфейсом.
ГЛАВА 24
Введение в LINQ to XML
Разработчики .NET сталкиваются с данными в формате XML повсеместно.
Конфигурационные файлы для обычных и веб-приложений хранят информацию
в виде XML. Объекты DataSet из ADO.NET могут легко сохранять (или загружать)
данные в формате XML. Технологии Windows Presentation Foundation, Silverlight и Windows
Workflow Foundation — все они используют основанную на XML грамматику (XAML)
для представления настольных пользовательских интерфейсов, браузерных
пользовательских интерфейсов и рабочих потоков, соответственно. В библиотеке Windows
Communication Foundation (а также в первоначальных API-интерфейсах удаленного
взаимодействия .NET) многочисленные установки хранятся в формате строк XML.
Хотя XML распространен повсюду, исторически сложилось так, что
программирование с использованием XML утомительно, громоздко и очень сложно, если вы не знакомы
с большим количеством XML-технологий (XPath, XQuery, XSLT, DOM, SAX и т.п.). Еще
в самом первом выпуске .NET появилась специфическая сборка System.Xml.dll,
ориентированная на программирование для документов XML. В ней находятся множество
пространств имен и типов для различных технологий программирования XML, а также
несколько специфичных для .NET API-интерфейсов XML, таких как классы XmlReader/
XmlWriter.
В наши дни большинство программистов предпочитают взаимодействовать с XML-
данными посредством API-интерфейса LINQ to XML. В этой главе будет показано, что
программная модель LINQ to XML позволяет выражать структуру XML-данных в коде и
предлагает намного более простой способ создания, манипулирования, загрузки и
сохранения XML-данных. Помимо применения LINQ to XML для простого создания XML-
документов, с помощью выражений запросов LINQ можно также быстро извлекать
информацию из этих документов.
История о двух API-интерфейсах XML
Когда впервые появилась платформа .NET, программисты могли манипулировать
XML-документами, используя типы из сборки System.Xml.dll. Содержащиеся в ней
пространства имен и типы позволяли генерировать XML-данные в памяти и сохранять
их на диске. Кроме того, сборка System.Xml.dll предоставляла типы, с помощью
которых производится загрузка XML-документа в память, поиск в нем специфических узлов,
проверка документа на соответствие заданной схеме и решение других
распространенных задач.
Хотя эта исходная библиотека успешно применялась во многих проектах .NET, работа
с ее типами была несколько запутанной, поскольку программная модель не имела
отношения к структуре самого XML-документа. Например, предположим, что нужно создать
файл XML в памяти и сохранить его в файловой системе. В случае использования типов
892 Часть V. Введение в библиотеки базовых классов .NET
System.Xml.dll можно написать код, подобный показанному ниже (предварительно
понадобится создать новое консольное приложение по имени LinqToXmlFirstLook и
импортировать пространство имен System.Xml):
private static void BuildXmlDocWithDOM()
{
// Создать новый документ XML в памяти.
XmlDocument doc = new XmlDocument ();
// Заполнить документ корневым элементом по имени <Inventory>.
XmlElement inventory = doc.CreateElement("Inventory");
// Теперь создать подэлемент по имени <Саг> с атрибутом ID.
XmlElement car = doc.CreateElement("Car");
car.SetAttribute("ID", 000") ;
// Построить данные внутри элемента <Саг>.
XmlElement name = doc.CreateElement("PetName");
name.InnerText = "Jimbo";
XmlElement color = doc.CreateElement("Color");
color.InnerText = "Red";
XmlElement make = doc.CreateElement("Make");
make.InnerText = "Ford";
// Добавить элементы <PetName>, <Color> и <Make> в элемент <Саг>.
car.AppendChild(name) ;
car.AppendChild(color) ;
car.AppendChild(make);
// Добавить элемент <Саг> к элементу <Inventory>.
inventory.AppendChild(car) ;
// Вставить полный XML в объект XmlDocument и сохранить в файле,
doc.AppendChild(inventory) ;
doc.Save ( "Inventory.xml");
}
После вызова этого метода созданный им файл Inventory.xml (находящийся в
папке bin\Debug) будет содержать следующие данные:
<Inventory>
<Car ID=000">
<PetName>Jimbo</PetName>
<Color>Red</Color>
<Make>Ford</Make>
</Car>
</Inventory>
Хотя этот метод работает, как ожидается, нужно сделать несколько замечаний.
Программная модель System.Xml.dll — это реализация Microsoft спецификации
объектной модели документа W3C Document Object Model (DOM). Согласно этой модели,
документ XML строится "вверх дном". Сначала создается документ, затем подэлементы
и, наконец, элементы добавляются в документ. Чтобы выразить это в коде,
потребуется написать довольно много вызовов функций классов из XmlDocument и XmlElement
(помимо прочих).
В данном примере для построения очень простого XML-документа понадобилось 16
строк кода (без учета комментариев). Создание более сложных документов с помощью
сборки System.Xml.dll требует написания гораздо большего объема кода. Хотя этот
код определенно можно упростить, выполняя построение узлов посредством
циклических и условных конструкций, факт остается фактом — тело кода имеет минимум
визуального отражения финального дерева XML.
Глава 24. Введение в LINQ to XML 893
Интерфейс LINQ to XML как лучшая модель DOM
API-интерфейс LINQ to XML предлагает альтернативный способ построения,
манипулирования и опроса XML-документов, который использует намного более
функциональный подход, чем модель DOM из System.Xml. Вместо построения XML-документа за
счет индивидуальной сборки элементов и обновления дерева XML через набор вызовов
функций, код пишется сверху вниз:
private static void BuildXmlDocWithLINQToXml()
1
// Создание XML-документа в более 'функциональной' манере.
XElement doc =
new XElement("Inventory",
new XElement("Car", new XAttribute ("ID", 000"),
new XElement("PetName", "Jimbo"),
new XElement("Color", "Red"),
new XElement("Make", "Ford")
)
) ;
// Сохранить в файл.
doc.Save("InventoryWithLINQ.xml");
}
Здесь используется новый набор типов из пространства имен System.Xml.Linq, a
именно — XElement и XAttribute.
В результате вызова метода BuildXmlDocWithLINQToXml() получаются те же
данные XML, но на этот раз с гораздо меньшими усилиями. Обратите внимание, что
благодаря тщательно выстроенным отступам, исходный код теперь имеет ту же структуру,
что и результирующий XML-документ. Это очень удобно и само по себе, но к тому же
заметьте, насколько компактнее этот код по сравнению с предыдущим примером
(сэкономлено около 10 строк).
Здесь не применяются какие-либо выражения запросов LINQ, а просто с помощью
типов из пространства имен System.Xml.Linq генерируется находящийся в памяти
XML-документ, который затем сохраняется в файл. Фактически API-интерфейс LINQ to
XML использовался как лучшая модель DOM, Далее в этой главе будет показано, что
классы из System.Xml.Linq поддерживают LINQ и могут быть целью для той же
разновидности запросов LINQ, которые рассматривались в главе 13.
По мере изучения LINQ to XML, скорее всего, вы начнете отдавать предпочтение этому
API-интерфейсу перед начальными библиотеками XML в .NET. Тем не менее, это не
значит, что вы вообще перестанете пользоваться пространством имен из System.Xml.dll.
Просто шансы выбора System.Xml.dll в новых проектах значительно уменьшатся.
Синтаксис литералов Visual Basic как
наилучший интерфейс LINQ to XML
Прежде чем приступить к формальному ознакомлению с LINQ to XML с точки зрения
С#, имеет смысл кратко упомянуть о том, что язык Visual Basic переносит
функциональный подход этого API-интерфейса на следующий уровень. В Visual Basic можно
определять литералы XML, которые позволяют присваивать XElement потоку встроенной
разметки XML прямо в коде. Предполагая наличие проекта VB, можно создать
следующий метод:
Public Class XmlLiteralExample
Public Sub MakeXmlFileUsingLiterals ()
' Обратите внимание на возможность встраивания данных XML в XElement.
894 Часть V. Введение в библиотеки базовых классов .NET
Dim doc As XElement = _
<Inventory>
<Car ID=000">
<PetName>Jimbo</PetName>
<Color>Red</Color>
<Make>Ford</Make>
</Car>
</Inventory>
' Сохранить в файл.
doc.Save("InventoryVBStyle.xml")
End Sub
End Class
Когда компилятор VB обрабатывает литерал XML, он отображает данные XML на
лежащую в основе корректную объектную модель LINQ to XML. Фактически, при
работе с LINQ to XML в проекте VB среда IDE уже понимает, что литеральный синтаксис
XML — это сокращенная нотация соответствующего кода. Обратите внимание, что на
рис. 24.1 применяется операция точки к конечному дескриптору </ Invent or y> и видны
те же самые члены, которые отображаются после применения операции точки к строго
типизированному XElement.
XmlLrteraJExampltvb* X I
;XmlLJtmExampte
■ ^Public Class X*lliteralE*aeple
Public Sub MakeX«lFileUsingLiterals()
* Motice that we can inline XML data
' to an XEleeent.
Di* doc As XFleawnt - _
<Inventory>
<Car ID«'*ieee->
<PetMa»e>3inbo< /PetNawe >
<Color> Red</Color >
<Hake>Ford</Hake>
</Car>
</ Inventory >.j
; ЫЛЛааи .- WUaafUfanfc
End Sub
End Class
SaveClnvento;^^ ф
i«# Add
♦ AddAfterSdf
♦ AddAnnotation
♦ AddBef oreSeif
V AddFirrt
♦ Ancestors
♦ AncestorsAndSelf
♦ Annotation
♦ Annotations
i * Attribute
■
V Att r i b utes
Common All
Рис. 24.1. Литеральный синтаксис VB XML — сокращенная нотация
работы с объектной моделью LINQ to XML
Хотя книга посвящена языку программирования С#, некоторые разработчики
считают, что поддержка XML в VB непревзойденна. Даже если вы из тех, кто не может
представить работу с языком из семейства BASIC, все равно рекомендуется изучить
литеральный синтаксис VB в документации .NET Framework 4.0 SDK. Все операции
манипуляций с данными XML можно вынести в отдельную сборку *.dll, так что вполне
допускается применять для этого VB!
Глава 24.
Введение в UNO to XML 895
Члены пространства имен
System. Xml. Linq
Несколько неожиданно, но в основной сборке LINQ to
XML (System.Xml.Linq.dll) определено весьма
небольшое количество типов в трех разных пространствах имен:
System.Xml.Linq, System.Xml.Schema и System.Xml.
XPath (рис. 24.2).
Основное пространство имен System.Xml.Linq
содержит легко управляемый набор классов, представляющих
различные аспекты документа XML (элементы и атрибуты,
пространства имен XML, комментарии XML, инструкции
обработки и т.п.). В табл. 24.1 описаны избранные члены
System. Xml. Linq.
На рис. 24.3 показана цепочка наследования
ключевых типов классов.
Таблица 24.1. Избранные члены пространства имен
System. Xml. Linq
A jgj * 1
л О System.Xml.Linq
!> ^ Extensions
t> J/1 LcadOptions
. и* ReaderOptions
' лГ* SaveOptions
D -% XAttribute
\> -fJXCData
0 ^% XComment
> Щ$ XContainer
'• ^Ц XDeclaration
> *\t XDccument
> -ij XDocumentType
! *tf XEIement
> ij XName
i- Щ% XNamespace
> ^$ XNode
> *\£ XNcdeDocumentOrderCcmparer
£> ^$ XNodeEqualit) Comparer
t> % XObject
t> a#* XObjectChange
1> *i XObjectChangeEventArgs
l> *ft XProcessinglnstruction
t> ^ XStreamingElement
:• «IjXText
> {} System.Xml.Schema
1
£> {} System.Xml.xPath
Рис. 24.2. Пространства имен
System.Xml.Linq.dll
Член
Назначение
XAttribute
XCData
XComment
XDeclaration
XDocument
XEIement
XName
XNamespace
XNode
XProcessinglnstruction
XStreamingElement
Представляет XML-атрибут конкретного элемента XML
Представляет раздел CDATA в документе XML. Информация
раздела CDATA представляет данные в документе XML,
которые должны быть включены, но не отвечают правилам
грамматики XML (например, код сценария)
Представляет комментарий XML
Представляет открывающее объявление документа XML
Представляет целиком весь документ XML
Представляет определенный элемент внутри документа
XML, включая корневой
Представляет имя XML-элемента или XML-атрибута
Представляет пространство имен XML
Представляет абстрактную концепцию узла (элемент,
комментарий, тип документа, инструкция обработки или
текстовый узел) в дереве XML
Представляет инструкцию обработки XML
Представляет элементы в дереве XML, которые
поддерживают отложенный потоковый вывод
Осевые методы LINQ to XML
В дополнение к классам X* в пространстве имен System.Xml.Linq определен класс
по имени Extensions. В этом классе определен набор расширяющих методов,
которые обычно расширяют IEnumerable<T>, где Т — некоторый наследник XNode или
XContainer. В табл. 24.2 описаны важные расширяющие методы, о которых следует
знать (они очень удобны при работе с запросами LINQ).
896 Часть V. Введение в библиотеки базовых классов .NET
XText
Class
•¥ XNode
XCData
Class
-»XText
J
b
: XOfrjerf
HLinsIn'o
*
i Abstract Class
, с ,
L...
j XNode
i Abstract Class
i -»XC*ject
: -J
' ST
... *
fy
J
XProcessinginst
Class
ruction g
■♦XNode
1-е >
XAttribute
Class
-frXObject
XCommenl
Class
•
-►XNode
®>
^
| JfToflfainer
i Abstract Class
1 -» XNode
T
XDocument (D
| Class
-» XContainer
i 1
XDocument Type
Class
4 XNode
, D-'mlSenaliiable
XEJement A
Class
•♦ XContainer
. .
IV"''
Рис. 24.3. Иерархия базовых классов LINQ to XML
Таблица 24.2. Избранные члены класса LINQ to XML
Член
Назначение
Ancestor<T>() Возвращает отфильтрованную коллекцию элементов, содержащих
наследников каждого узла в исходной коллекции
Attributes () Возвращает отфильтрованную коллекцию атрибутов каждого элемента
исходной коллекции
DescendantNodes<T> Возвращает коллекцию узлов-наследников каждого документа и
элемента исходной коллекции
Descendants<T> Возвращает отфильтрованную коллекцию элементов, которые содержат
элементы-наследники каждого элемента и документа в исходной коллекции
Elements<T> Возвращает коллекцию дочерних элементов каждого элемента и
документа исходной коллекции
Nodes<T> Возвращает коллекцию дочерних узлов каждого документа и элемента
в исходной коллекции
Remove () Удаляет каждый атрибут исходной коллекции из родительского элемента
Remove<Т> () Удаляет все вхождения заданного узла в исходной коллекции
Как можно понять из названий, эти методы позволяют опрашивать загруженное
дерево XML в поисках элементов, атрибутов и их значений. Все вместе такие методы
называются осевыми методами или просто осями. Эти методы можно применить
непосредственно к частям дерева или узлам либо использовать их для построения более
сложных запросов LINQ.
На заметку! Абстрактный класс XContainer поддерживает множество методов, которые
имеют имена, идентичные членам класса Extensions. Класс XContainer является
родительским как для XElement, так и для XDocument, и потому они оба поддерживают общую
функциональность.
Глава 24. Введение в LINQ to XML 897
Примеры использования этих осевых методов будут встречаться на протяжении
оставшейся части главы. А сейчас рассмотрим краткий пример:
private static void DeleteNodeFromDoc()
{
XElement doc =
new XElement("Inventory",
new XElement ("Car", new XAttnbute ("ID", 000"),
new XElement("PetName", "Jimbo"),
new XElement("Color", "Red"),
new XElement("Make", "Ford")
)
) ;
// Удалить элемент PetName из дерева.
doc.Descendants("PetName").Remove();
Console.WriteLine(doc);
}
В результате вызова этого метода получается следующее дерево XML:
<Inventory>
<Car ID=000">
<Color>Red</Color>
<Make>Ford</Make>
</Car->
</Inventory^
Избыточность XName (и XNamespace)
Если посмотреть на сигнатуру осевых методов LINQ to XML (или идентично
именованных членов XContainer), можно заметить, что обычно они требуют указания того,
что определяется как объект XName. Взгляните на сигнатуру метода Descendants (),
определенного в XContainer:
public IEnumerable<XElement> Descendants(XName name)
Метод XName является "избыточным" в том смысле, что он никогда не будет
использоваться в коде. Фактически, поскольку этот класс не имеет общедоступного
конструктора, объект типа XName создавать нельзя:
// Ошибка! Нельзя создавать объекты XName!
doc.Descendants(new Xname("PetName")) .Remove ();
Посмотрев на формальное определение XName, вы обнаружите, что этот класс
определяет специальную операцию неявного преобразования (специальные операции
преобразования рассматриваются в главе 12), которая отобразит простой тип System.String
на объект XName:
// На самом деле "за кулисами" создается XName!
doc.Descendants("PetName").Remove();
На заметку! Класс XName space также поддерживает подобного рода неявное преобразование
строк.
Важно отметить, что при работе с осевыми методами можно применять текстовые
значения для представления имен элементов или атрибутов и позволять API-интерфейсу
LINQ to XML отображать строковые данные на необходимые типы объектов.
Исходный код. Проект LinqToXmlFirstLook доступен в подкаталоге Chapter 24.
898 Часть V. Введение в библиотеки базовых классов .NET
Работа с XElement и XDocument
Продолжим исследования LINQ to XML на примере нового консольного
приложения по имени ConstructingXmlDocs. После его создания импортируйте пространство
имен System.Xml.Linq в начальный файл кода. Как уже было показано, XDocument
представляет полный XML-документ в программной модели LINQ to XML, поскольку он
может использоваться для определения корневого элемента, всех содержащихся в нем
элементов, инструкций обработки и объявлений XML. Ниже показан еще один пример
построения данных XML с применением XDocument:
static void CreateFullXDocument ()
{
XDocument inventoryDoc =
new XDocument (
new XDeclaration (. 0", "utf-8", "yes"),
new XComment("Current Inventory of cars1"),
new XProcessinglnstruction("xml-stylesheet",
"href='MyStyles.ess' title='Compact' type='text/ess'"),
new XElement("Inventory",
new XElement("Car", new XAttribute ( "ID" , "),
new XElement("Color", "Green"),
new XElement("Make", "BMW"),
new XElement("PetName", "Stan")
) ,
new XElement("Car", new XAttribute ( "ID", "),
new XElement("Color", "Pink"),
new XElement("Make", "Yugo"),
new XElement("PetName", "Melvin")
)
)
) ;
// Сохранить на диск.
inventoryDoc.Save("Simplelnventory.xml");
}
Обратите внимание, что конструктор объекта XDocument на самом деле
представляет собой дерево дополнительных объектов LINQ to XML. Вызываемый здесь конструктор
принимает в качестве первого параметра XDeclaration, за которым следует параметр-
массив объектов (вспомните, что параметры-массивы позволяют передавать
разделенные запятыми списки аргументов, которые "за кулисами" упаковываются в массив):
public XDocument(System.Xml.Linq.XDeclaration declaration, params object[] content)
После вызова этого метода в Main() в файл Simplelnventory.xml будут записаны
следующие данные:
<?xml version="l.0" encoding="utf-8" standalone="yes"?>
<!—Current Inventory of cars'-->
<?xml-stylesheet href='MyStyles.ess' title='Compact' type='text/ess'?>
<Inventory>
<Car ID=">
<Color>Green</Color>
<Make>BMW</Make>
<PetName>Stan</PetName>
</Car>
<Car ID=">
<Color>Pink</Color>
<Make>Yugo</Make>
<PetName>Melvin</PetName>
</Car>
</Inventory>
Глава 24. Введение в LINQ to XML 899
Как выясняется, объявление XML по умолчанию для любого XDocument
предусматривает использование кодировки utf-8, версии XML 1.0 и автономного документа. Таким
образом, можно полностью удалить создание объекта XDeclaration и получить те же
данные; учитывая, что почти любой документ требует одного и того же объявления,
применять XDeclaration приходится нечасто.
Если определять инструкции обработки или специальное объявление XML не
нужно, можно вообще избежать использования XDocument и просто применять XElement.
Помните, что XElement может использоваться для представления корневого элемента
документа XML и всех подобъектов. Итак, сгенерировать прокомментированный список
складских запасов можно следующим образом:
static void CreateRootAndChildren()
{
XElement inventoryDoc =
new XElement("Inventory",
new XComment("Current Inventory of cars1"),
new XElement("Car", new XAttribute("ID", "),
new XElement("Color", "Green"),
new XElement("Make", "BMW"),
new XElement("PetName", "Stan")
),
new XElement("Car", new XAttribute ( "ID", "),
new XElement("Color", "Pink"),
new XElement("Make", "Yugo"),
new XElement("PetName", "Melvin")
)
) ;
// Сохранить на диск.
inventoryDoc.Save("Simplelnventory.xml");
}
Вывод будет практически идентичным, исключая инструкцию обработки для
гипотетической таблицы стиля:
<?xml version="l.О" encoding="utf-8"?>
<Inventory>
<'—Current Inventory of cars!-->
<Car ID=">
<Color>Green</Color>
<Make^BMW</Make>
<PetName>Stan</PetName>
</Car>
<Car ID=">
<Color>Pink</Color>
<Make>Yugo</Make>
<PetName>Melvin</PetName>
</Car>
</Inventory>
Генерация документов из массивов и контейнеров
До сих пор XML-документы строились с использованием жестко закодированных
значений для конструктора. Но гораздо чаще придется генерировать XElement (или
XDocument), читая данные из массивов, объектов ADO.NET, файлов данных и т.п. Один
из способов отображения данных из памяти на новый XElement предусматривает
применение стандартных циклов for для перемещения данных в объектную модель LINQ
to XML. Хотя это определенно возможно, проще будет встроить запрос LINQ в
конструкцию XElement непосредственно.
900 Часть V. Введение в библиотеки базовых классов .NET
Предположим, что имеется анонимный массив анонимных классов (просто чтобы
сократить объем кода в этом примере; подойдет также любой массив, List<T> или другой
контейнер). Отобразить эти данные на XElement можно было бы следующим образом:
static void MakeXElementFromArray ()
{
// Создать анонимный массив анонимных типов,
var people = new[] {
new { FirstName = "Mandy", Age =32},
new { FirstName = "Andrew", Age = 40 },
new { FirstName = "Dave", Age = 41 },
new { FirstName = "Sara", Age = 31}
};
XElement peopleDoc =
new Xelement("People",
from с in people select new XElement("Person", new XAttribute("Age", с Age),
new XElement("FirstName", с.FirstName))
) ;
Console.WriteLine(peopleDoc);
}
Здесь объект peopleDoc определяет корневой элемент <Реор1е> с результатами
запроса LINQ. Этот запрос LINQ создает новый XElement на основе каждого элемента
массива people. Если встроенный запрос покажется трудно читабельным, можете
разделить этот процесс на отдельные шаги, как показано ниже:
static void MakeXElementFromArray ()
{
// Создать анонимный массив анонимных типов,
var people = new[] {
new { FirstName = "Mandy", Age =32},
new { FirstName = "Andrew", Age =40 },
new { FirstName = "Dave", Age = 41 },
new { FirstName = "Sara", Age = 31}
};
var arrayDataAsXElements = from с in people
select
new XElement("Person",
new XAttribute("Age", c.Age),
new XElement("FirstName", с.FirstName));
XElement peopleDoc = new XElement("People", arrayDataAsXElements);
Console.WriteLine(peopleDoc);
}
В любом случае вывод будет выглядеть следующим образом:
<Реор1е>
<Person Age=2">
<FirstName>Mandy</FirstName>
</Person>
<Person Age=0">
<FirstName>Andrew</FirstName>
</Person>
<Person Age=1">
<FirstName>Dave</FirstName>
</Person>
<Person Age=1">
<FirstName>Sara</FirstName>
</Person>
</People>
Глава 24. Введение в LINQ to XML 901
Загрузка и разбор XML-содержимого
Оба типа — XElement и XDocument — поддерживают методы Load() и Parse(),
которые позволяют наполнить объект XML содержимым из объекта-строки, содержащего
данные XML или внешние файлы XML. Рассмотрим следующий метод, в котором
иллюстрируются оба подхода:
static void ParseAndLoadExistingXml()
{
// Построить XElement из строки.
string myElement =
@"<Car ID ='3'>
<Color>Yellow</Color>
<Make>Yugo</Make>
</Car>";
XElement newElement = XElement.Parse(myElement);
Console.WriteLine(newElement);
Console.WriteLine();
// Загрузить файл Simplelnventory.xml.
XDocument myDoc = XDocument.Load("Simplelnventory.xml");
Console.WriteLine(myDoc);
}
Исходный код. Проект ConstructingXmlDocs доступен в подкаталоге Chapter 24.
Манипулирование XML-документом в памяти
К настоящему моменту были продемонстрированы разные способы использования
LINQ to XML для создания, сохранения, разора и загрузки данных XML. Следующий
аспект LINQ to XML, с которым нужно познакомиться — это навигация по документу для
нахождения и изменения определенных элементов дерева на основе использования
запросов LINQ и осевых методов LINQ to XML.
Для этого построим приложение Windows Forms, которое отобразит данные из
документа XML, сохраненного на жестком диске. Графический интерфейс позволит
пользователю вводить данные для нового узла, который будет добавлен к тому же XML-
документу. Наконец, пользователю будет предоставлено несколько способов выполнения
поиска в документе посредством нескольких запросов LINQ.
На заметку! Учитывая, что некоторые запросы LINQ уже строились в главе 13, здесь они
повторяться не будут. В разделе "Querying XML Trees" ("Выдача запросов к деревьям XML")
документации .NET Framework 4.0 SDK можно посмотреть ряд дополнительных специфических
примеров LINQ to XML
Построение пользовательского интерфейса
приложения LINQ to XML
Создайте приложение Windows Forms под названием LinqToXmlWinApp и измените
имя начального файла For ml. с s на MainForm.cs (в проводнике решения). Графический
пользовательский интерфейс этого приложения довольно прост. В левой части окна
находится элемент управления Text Box (по имени txt Inventory), у которого
свойство Multiline установлено в true, а свойство ScrollBars — в Both. Вдобавок также
есть группа простых элементов TextBox (txtMake, txtColor и txtPetName) и элемент
Button по имени btnAddNewItem, щелчок на котором приводит к добавлению
нового элемента к документу XML. Наконец, есть еще одна группа элементов управления
902 Часть V. Введение в библиотеки базовых классов .NET
(TextBox по имени txtMakeToLookUp и еще один Button по имени btnLookUpColors),
который позволяет запросить из документа XML подмножество определенных узлов.
Возможная компоновка представлена на рис. 24.4.
■1шш „у М1ш^иммшям м,,пю„п.~ ш^пГ^тшшш^я^^шшшт
Чв FunwithUNQtoXML В бзД
Current Inventory
I * Add inventory Item
Make
Color
Pet Name
Г ш 1
Look up Colors for Make
Make to Look Up BMW
| LookUpCotor»
I I , __ о ! !
Рис. 24.4. Графический пользовательский интерфейс приложения LINQ to XML
Потребуется обработать событие Click каждой кнопки для генерации методов
обработки событий, а также обработать событие Load самой формы. Чуть ниже мы
займемся этим.
Импорт файла Inventory, xml
В состав кода примеров для этой главы включен файл Inventory.xml, в котором
имеется набор элементов внутри корневого элемента <Inventory>. Импортируйте этот
файл в проект, выбрав пункт меню Project^pAdd Existing Item (ПроектерДобавить
существующий элемент). Если вы посмотрите на данные, то увидите, что корневой элемент
определяет набор элементов <Саг>, каждый из которых определен примерно так:
<Car carlD =,,0">
<Make>Ford</Make>
<Color>Blue</Color>
<PetName>Chuck</PetName>
</Car>
Прежде чем продолжить, выберите этот файл в Solution Explorer и затем в окне
Properties (Свойства) установите свойство Copy to Output Directory (Копировать в
выходной каталог) в Copy Always (Копировать всегда). Это гарантирует, что данные будут
размещены в папке bin\Debug после компиляции приложения.
Определение вспомогательного класса LINQ to XML
Чтобы изолировать данные LINQ to XML, добавьте в проект новый класс по имени
LinqToXmlObjectModel. В этом классе будет определен набор статических методов,
инкапсулирующих некоторую логику LINQ to XML. Для начала определите метод,
возвращающий заполненный XDocument на основе содержимого файла Inventory.xml
(не забудьте импортировать пространства имен System.Xml.Linq и System.Windows.
Forms в этот новый файл):
р
Глава 24. Введение в LINQ to XML 903
public static XDocument GetXmllnventory ()
{
try
{
XDocument inventoryDoc = XDocument.Load("Inventory.xml");
return inventoryDoc;
}
catch (System.10.FileNotFoundException ex)
{
MessageBox.Show(ex.Message);
return null;
}
}
Метод InsertNewElementO, показанный ниже, принимает значения элементов
управления TextBox раздела Add Inventory Item (Добавить элемент на склад),
чтобы поместить новый узел внутри элемента < Invent or y>, используя осевой метод
Descendants (). После этого можно сохранить документ.
public static void InsertNewElement(string make, string color, string petName)
{
// Загрузить текущий документ.
XDocument inventoryDoc = XDocument.Load("Inventory.xml");
// Сгенерировать случайное число для идентификатора.
Random r = new Random () ;
// Создать новый XElement из входных параметров.
XElement newElement = new XElement("Car", new XAttribute("ID", r.Next E0000)),
new XElement ("Color", color),
new XElement("Make", make),
new XElement("PetName", petName));
// Добавить в объект в памяти.
inventoryDoc.Descendants("Inventory") .First () .Add(newElement);
// Сохранить на диске.
inventoryDoc.Save("Inventory.xml");
}
Последний метод — LookUpColorsForMakeO — принимает данные из
последнего элемента TextBox для построения строки, которая содержит цвета, используемые
определенным изготовителем, с помощью запроса LINQ. Взгляните на следующую
реализацию:
public static void LookUpColorsForMake(string make)
{
// Загрузить текущий документ.
XDocument inventoryDoc = XDocument.Load("Inventory.xml");
// Найти цвета заданного изготовителя.
var makelnfo = from car in inventoryDoc.Descendants("Car")
where (string)car.Element("Make") == make
select car.Element("Color").Value;
// Построить строку, представляющую каждый цвет,
string data = string.Empty;
foreach (var item in makelnfo.Distinct())
{
data += string.Format("- {0}\n", item);
}
// Показать цвета.
MessageBox.Show(data, string.Format("{0} colors:", make));
}
904 Часть V. Введение в библиотеки базовых классов .NET
Оснащение пользовательского интерфейса
вспомогательными методами
Теперь все, что осталось сделать — наполнить кодом обработчики событий. Задача
решается лишь вызовами статических вспомогательных методов:
public partial class MainForm : Form
{
public MainForm ()
{
InitializeComponent ();
}
private void MainForm_Load(object sender, EventArgs e)
{
// Отобразить текущий XML-документ склада в элементе управления TextBox.
txtlnventory.Text = LinqToXmlObjectModel.GetXmllnventory() .ToString ();
}
private void btnAddNewItem_Click(object sender, EventArgs e)
{
// Добавить новый элемент к документу.
LinqToXmlObjectModel.InsertNewElement(txtMake.Text, txtColor.Text, txtPetName.Text) /
// Отобразить текущий XML-документ склада в элементе управления TextBox.
txtlnventory.Text = LinqToXmlObjectModel.GetXmllnventory().ToString();
}
private void btnLookUpColors_Click(object sender, EventArgs e)
{
LinqToXmlObjectModel.LookUpColorsForMake(txtMakeToLookUp.Text);
}
}
Конечный результат показан на рис. 24.5.
*J Fun with LJNQ to XML
Current Inventory
<CarcarlD»"Q">
< Make > Ford c/Make>
<Cokr>BKj«</Color>
<PetName>Chuck</PetName>
</Car>
<СагсатШ»"Т>
<Make>VW</Make>
cCotor>Slver</C<
cPetName>Maryc/Pi BMW colors;
</Car>
<CarcarlD-">
<Make>Yugoo
<Со1ог>Ртк</Сок)гз||| -Black
- Green
Add Inventory lem
Color Green
PetName Green*
I *"
Look up Colors for Make
Make to Look Up BMW
Look Up Colon
I
1
Рис. 24.5. Готовое приложение LINQ to XML
На этом начальное знакомство с LINQ to XML и изучение LINQ завершено. Впервые
вы встретились с технологией LINQ в главе 13, где шла речь о LINQ to Objects. В главе 19
приводились различные примеры использования PLINQ, а в главе 23 рассматривалось
применение LINQ к объектам ADO.NET Entity. Теперь вы готовы и должны двигаться
дальше. В Microsoft ясно дали понять, что LINQ будет развиваться вместе с развитием
платформы .NET.
Глава 24. Введение в LINQ to XML 905
Исходный код. Проект LinqToXmlWinApp доступен в подкаталоге Chapter 24.
Резюме
В этой главе рассматривалась роль LINQ to XML. Как можно было увидеть, этот API-
интерфейс является альтернативой начальной библиотеке для манипуляций XML,
которая поставлялась в составе платформы .NET, а именно — System.Xml.dll. Используя
System.Xml.Linq.dll, можно генерировать новые документы XML, используя подход
"сверху вниз", при котором структура кода очень напоминает конечные данные XML.
В этом свете LINQ to XML — лучшая объектная модель документа (DOM). Также было
показано, как строить объекты XDocument и XElement различными путями (разбором,
загрузкой из файла, отображением объектов в памяти) и как выполнять навигацию и
манипуляции данными с применением запросов LINQ.
ГЛАВА 25
Введение в Windows
Communication
Foundation
В версии .NET 3.0 был представлен API-интерфейс, специально предназначенный
для построения распределенных систем — Windows Communication Foundation
(WCF). В отличие от других распределенных API-интерфейсов, которые, возможно,
приходилось применять в прошлом (например, DCOM, .NET Remoting, веб-службы XML,
очереди сообщений), WCF предлагает единую, унифицированную и расширяемую
программную объектную модель, которая может использоваться для взаимодействия с
множеством ранее разрозненных технологий.
Эта глава начинается с определения потребностей, которые призван удовлетворить
WCF, и рассмотрения задач, которые данный интерфейс должен решить, с приведением
краткого обзора предшествующих API-интерфейсов распределенных вычислений. После
этого рассматриваются средства, предлагаемые WCF, а также ключевые сборки,
пространства имен и типы, которые представляет эта программная модель. На
протяжении главы будет построено несколько служб, хостов и клиентов WCF с использованием
различных инструментов разработки WCF. Также будет отработан один простой пример
для .NET 4.0, который продемонстрирует, насколько упростилась задача построения
конфигурационных файлов хостинга (множество примеров конфигурационных файлов
представлено далее в главе).
API-интерфейсы распределенных вычислений
Операционная система Windows предоставляет множество API-интерфейсов для
построения распределенных систем. Хотя и верно, что под "распределенными системами"
большинство понимает системы, состоящие минимум из двух сетевых компьютеров,
этот термин в широком смысле может охватывать два исполняемых модуля, которые
нуждаются в обмене данными, даже если они с успехом работают на одной и той же
физической машине. Используя приведенное определение, выбор распределенных API-
интерфейсов для решения конкретной программистской задачи обычно подразумевает
ответ на следующий основополагающий вопрос:
Будет ли эта система использоваться исключительно "дома", или лее внешним
пользователям понадобится доступ к функциональности приложения?
Глава 25. Введение в Windows Communication Foundation 907
Если вы строите распределенную систему для "домашнего" пользования, то есть
шанс гарантировать, что каждый подключенный компьютер будет работать под
управлением одной и той же операционной системы и использовать одну и ту же
программную платформу (.NET, COM или Java). Выполнение "домашних" систем также означает
возможность применения для аутентификации, авторизации и тому подобного
существующей системы безопасности. В такой ситуации можно выбрать конкретный
распределенный API-интерфейс, который, исходя из соображений производительности,
наилучшим образом подходит для существующей операционной системы и программной
платформы.
В отличие от этого, при построении системы, которая должна быть доступна другим
за пределами ваших стен, вы сталкиваетесь с целым букетом проблем, подлежащих
решению. Прежде всего, вряд ли удастся диктовать внешним пользователям, какую
операционную систему они должны использовать, какую программную платформу применять
для построения программного обеспечения и как настраивать параметры безопасности.
Если вы работаете в большой компании или в университете, где используется
множество операционных систем и технологий программирования, то в этом случае даже
"домашние" приложения неожиданно сталкиваются с теми же сложностями, что и
приложения, ориентированные на внешний мир. В любом из этих случаев следует
ограничиться наиболее гибким API-интерфейсом распределенных вычислений, чтобы
обеспечить максимальную доступность результирующего приложения.
В зависимости от ответа на этот ключевой вопрос распределенных вычислений,
следующей задачей будет выбор конкретного API-интерфейса (либо их набора). Ниже
представлен краткий перечень некоторых основных распределенных API-интерфейсов,
исторически используемых разработчиками программного обеспечения Windows. После
этого краткого экскурса вы легко сможете оценить удобство Windows Communication
Foundation.
На заметку! Чтобы исключить недоразумения, следует указать, что WCF (и включаемые
технологии) не имеет ничего общего с построением веб-сайтов на основе HTML. Хотя и верно считать
веб-приложения "распределенными", в том смысле, что в обмене обычно участвуют две
машины, API-интерфейс WCF нацелен на установку соединений с машинами для распределения
функциональности удаленных компонентов, а не для отображения HTML-разметки в
веб-браузере. Построение веб-сайтов на платформе .NET будет рассматриваться в главе 32.
Роль DCOM
До появления платформы .NET основным API-интерфейсом удаленных вычислений
на платформе Microsoft была распределенная модель компонентных объектов (Distributed
Component Object Model — DCOM). С помощью DCOM можно было строить
распределенные системы, используя при этом объекты СОМ, системный реестр и определенную
долю старания. Одно из преимуществ DCOM состояло в том, что она обеспечивала
прозрачность расположения компонентов. Просто говоря, это позволяло разрабатывать
клиентское программное обеспечение так, что физическое расположение удаленных
объектов не должно было указываться жестко. Независимо от того, располагался
удаленный объект на той же или на другой сетевой машине, код мог оставаться нейтральным,
поскольку действительное местоположение записывалось в системный реестр.
Хотя модель DCOM имела определенный успех, для всех практических нужд это был
API-интерфейс, ориентированный на Windows. Сама модель DCOM не представляла
собой основы для построения полноценных решений, включающих различные
операционные системы (Windows, Unix, Mac) или разделяющих данные между разными
архитектурами (т.е. COM, Java или CORBA).
908 Часть V. Введение в библиотеки базовых классов .NET
На заметку! Предпринимались некоторые попытки переноса DCOM для запуска на различных
версиях Unix/Linux, но результат не был блестящим и в конечном итоге стал технологическим
тупиком.
В общем и целом, модель DCOM была наилучшим образом приспособлена для
разработки "домашних" приложений, поскольку передача типов СОМ за пределы
компании влекла за собой дополнительные сложности (брандмауэры и т.п.). С появлением
платформы .NET модель DCOM быстро стала устаревшей, и если вам не приходится
сопровождать унаследованные системы DCOM, то можете смело поставить крест на этой
технологии.
Роль служб COM+/Enterprise Services
Модель DCOM представляла собой лишь немногим более чем способ установки канала
взаимодействия между двумя частями программного обеспечения на основе СОМ. Чтобы
заполнить брешь недостающих компонентов для построения полноценных
распределенных вычислительных решений, в Microsoft в конечном итоге выпустили сервер
транзакций (Microsoft Transaction Server — MTS), который позже был переименован в СОМ+.
Несмотря на название, СОМ+ использовали не только программисты СОМ; эта
технология полностью доступна и профессиональным разработчикам приложений для
.NET. Co времен выхода первого выпуска платформы .NET библиотеки базовых классов
предоставляют пространство имен System.EnterpriseServices. С его помощью
программисты .NET могут строить управляемые библиотеки, которые устанавливаются в
исполняющую среду СОМ+, получая доступ к тому же набору служб, что и
традиционный СОМ-сервер, поддерживающий СОМ+. В любом случае, как только
поддерживающая СОМ+ библиотека устанавливается в исполняющей среде СОМ+, она становится
служебным компонентом.
СОМ+ предлагает множество средств, которые может использовать служебный
компонент, включая управление транзакциями, управление временем жизни объектов,
службу поддержки пулов, систему безопасности на основе ролей, модель слабо
связанных событий и т.д. Это было главным преимуществом на то время, учитывая, что
большинство распределенных систем требовали одинакового набора служб. Вместо того
чтобы заставлять разработчиков кодировать их вручную, технология СОМ+ предложила
готовое решение.
Одним из наиболее неотразимых аспектов СОМ+ был тот факт, что все эти
настройки можно было конфигурировать в декларативной манере, используя
административные инструменты. Таким образом, если вы хотели обеспечить мониторинг объекта в
транзакционном контексте или отнести его к определенной роли безопасности, то
просто должны были правильно отметить нужные флажки.
Хотя службы COM+/Enterprise Services все еще используются и сегодня, данная
технология предлагает решение только для Windows Это решение больше подходит для
разработки "домашних" приложений либо в качестве службы заднего плана,
опосредованно управляемой более совершенными интерфейсными средствами (например,
публичными веб-сайтами, вызывающими служебные компоненты — объекты СОМ+ — в
фоновом режиме).
На заметку! В WCF в настоящее время не предусмотрено способа построения служебных
компонентов. Однако службы WCF имеют возможность взаимодействовать с существующими
объектами СОМ+. Если необходимо строить служебные компоненты на С#, придется
непосредственно использовать пространство имен System.EnterpriseServices. Подробные сведения
ищите в документации .NET Framework 4.0 SDK.
Глава 25. Введение в Windows Communication Foundation 909
Роль MSMQ
API-интерфейс MSMQ (Microsoft Message Queuing — очередь сообщений Microsoft)
позволяет разработчикам строить распределенные системы, которые нуждаются в
надежной доставке сообщений по сети. Хорошо известно, что в любой распределенной
системе существует риск отказа сетевого сервера, отключения базы данных или потери
соединений по каким-то необъяснимым причинам. Более того, множество приложений
должны быть сконструированы так, чтобы данные сообщений, которые невозможно
доставить немедленно, сохранялись для последующей доставки (это называется очере-
дизацией данных).
Изначально в Microsoft представили MSMQ как упакованный набор низкоуровневых
API-интерфейсов и СОМ-объектов на основе С. После выхода платформы .NET
программисты на С# могут использовать пространство имен System.Messaging для привязки к
MSMQ и построения программного обеспечения, взаимодействующего с
периодическими подключающимися приложениями в зависимом режиме.
Кстати, уровень СОМ+, включающий функциональность MSMQ в исполняющую
среду (в упрощенном формате), использует технологию, именуемую Queued Components
(QC). Этот способ взаимодействия с MSMQ был оформлен в пространство имен
System.EnterpriseServices, которое упоминалось в предыдущем разделе.
Независимо от того, какая программная модель используется для взаимодействия с
исполняющей средой MSMQ, в конечном итоге гарантируется надежная и
своевременная доставка сообщений. Подобно СОМ+, интерфейс MSMQ определенно можно считать
частью фабрики по построению распределенного программного обеспечения на
платформе операционной системы Windows.
Роль .NET Remoting
Как ранее упоминалось, с появлением платформы .NET технология DCOM
быстро стала устаревшим API-интерфейсом распределенных систем. Библиотеки базовых
классов, обслуживающие уровень .NET Remoting, представлены пространством имен
System.Runtime.Remoting. Этот API-интерфейс позволяет множеству компьютеров
распределять объекты, если только они работают в составе приложений на платформе
.NET.
API-интерфейсы .NET Remoting предоставили множество очень полезных средств.
Наиболее важным было применение конфигурационных файлов на основе XML для
декларативного определения внутренних механизмов, используемых клиентским и
серверным программным обеспечением. Используя файлы *. con fig, очень просто радикально
изменить функциональность распределенной системы, изменив содержимое
конфигурационных файлов и перезапустив приложение.
К тому же, учитывая факт, что этот API-интерфейс используется только
приложениями .NET, можно получить значительный выигрыш в производительности, если данные
будут закодированы в компактном двоичном формате, и при определении параметров
и возвращаемых значений можно будет использовать систему общих типов (Common
Type System — CTS). Хотя .NET Remoting можно было применять для построения
распределенных систем, охватывающих несколько операционных систем (через
платформу Mono, кратко упомянутую в главе 1 и подробно описанную в приложении Б),
взаимодействие с другими программными архитектурами (такими как Java) еще не была
возможным.
910 Часть V. Введение в библиотеки базовых классов .NET
Роль веб-служб XML
Каждый из предшествующих API-интерфейсов распределенных вычислений
обеспечивал минимальную (или вовсе никакую) поддержку доступа удаленных пользователей
к функциональности в независимой манере. Когда нужно было предоставить услуги
удаленных объектов любой операционной системе и любой программной модели, веб-
службы XML предлагали наиболее простой способ сделать это.
В отличие от традиционных браузерных веб-приложений, веб-служба обеспечивает
способ представления функциональности удаленных компонентов через стандартные
веб-протоколы. С момента появления начального выпуска .NET программистам была
предложена превосходная поддержка построения и использования веб-служб XML
через пространство имен System.Web.Services. Фактически во многих случаях
построение полноценной веб-службы сводится просто к применению атрибута [WebMethod] к
каждому общедоступному методу, к которому планируется открыть доступ. Более того,
Visual Studio 2010 позволяет подключаться к удаленным веб-службам парой щелчков
на кнопках.
Веб-службы позволяют разработчикам строить сборки .NET, включающие типы,
которые могут быть доступны по простому протоколу HTTP. Кроме того, веб-служба
кодирует свои данные в простой XML. Учитывая тот факт, что веб-службы базируются
на открытых промышленных стандартах (таких как HTTP, XML и SOAP), а не на
патентованной системе типов и сетевых форматов (как в случае DC ОМ или .NET Remoting),
они допускают высокую степень взаимодействия и возможности обмена данными. На
рис. 25.1 иллюстрируется независимая природа веб-служб XML.
Приложение .NET
на платформе
Win32
Прокси
веб-
служб ы.
Приложение Java
на платформе
Unix ' прокси '
веб-
службы^
Приложение Java (или .NET)
на платформе
Macintosh /Прокси4
( веб-
V службы.
Веб-браузер
(на любой платформе)
HTTPhXML
HTTPhXML
НИР и XML
Веб-сервер А
Веб-служба
XML
НИР и XML
НПРиХМ1_
Рис. 25.1. Веб-службы XML обеспечивают очень высокую степень взаимодействия
Глава 25. Введение в Windows Communication Foundation 911
Разумеется, ни один распределенный API-интерфейс не является безупречным.
Один из потенциальных недостатков веб-служб состоит в том, что они могут страдать
от некоторых проблем с производительностью (учитывая использование HTTP и XML
для представления данных). Другой недостаток связан с тем, что они могут оказаться не
идеальным решением для "домашних" приложений, где беспрепятственно можно
применять протоколы на основе TCP и двоичное форматирование данных.
Пример веб-службы .NET
В течение многих лет программисты .NET создавали веб-службы, используя шаблон
проекта ASP.NET Web Service в среде Visual Studio, к которому можно добраться через
меню File^New^Web Site (Файл ^ Создать1^Веб-сайт). Этот конкретный шаблон проекта
создает общепринятую структуру каталогов и несколько начальных файлов для
представления самой веб-службы. Хотя упомянутый шаблон проектов очень полезен в
качестве основы, веб-службы XML можно также строить на основе .NET, применяя простой
текстовый редактор, и немедленно тестировать их, используя веб-сервер разработчика
(подробности ищите в главе 31).
Например, предположим, что в новый файл по имени HelloWebService. asmx
(*.asmx — расширение по умолчанию для файла веб-служб .NET XML) помещена
следующая программная логика. Сохраните этот файл в каталоге C:\HelloWebService.
<и@ WebService Language="C#11 Class="HelloWebService11 %>
using System;
using System.Web.Services;
public class HelloWebService
{
[WebMethod]
public string HelloWorld()
{
return "Hello World";
}
}
Хотя эту веб-службу вряд ли можно считать сколько-нибудь полезной, обратите
внимание, что файл открывается директивой WebService, которая предназначена для
указания языка программирования .NET, применяемого в данном файле, и имени типа
класса, представляющего службу. Помимо этого, единственный интересный момент —
это оснащение метода HelloWorld() атрибутом [WebMethod]. Во многих случаях это
и все, что требуется сделать, чтобы предоставить метод внешним потребителям через
HTTP. И, наконец, заметьте, что для кодирования возвращаемого значения в формате
XML не нужно предпринимать никаких специальных действий, поскольку это делается
автоматически во время выполнения.
Если хотите протестировать веб-службу, просто нажмите клавишу <F5> для запуска
отладочного сеанса или <Ctrl+F5> для запуска проекта на выполнение. При этом
должен открыться браузер и отобразить каждый веб-метод, представленный этой
конечной точкой. Здесь же можно щелкнуть на ссылке HelloWorld для вызова метода через
HTTP. После этого браузер отобразит возвращаемое значение, закодированное в XML:
<?xml version="l. О" encoding="utf-8" ?>
<string xmlns="http://tempuri.org/">
Hello World
</string>
Пара пустяков, не так ли? Еще лучше то, что когда необходимо построить реальный
клиент для взаимодействия со службой, можно сгенерировать файл прокси клиентской
стороны, который будет применяться для вызова веб-методов. Как известно, прокси
912 Часть V. Введение в библиотеки базовых классов .NET
(proxy) — это класс, инкапсулирующий низкоуровневые детали взаимодействия с
другим объектом, в данном случае — с самой службой.
Генерация прокси для классических веб-служб XML осуществляется двумя
способами. Во-первых, можно использовать инструмент командной строки wsdl.exe, что
удобно, когда нужен полный контроль над тем, как будет генерироваться прокси. Во-
вторых, можно применить опцию Visual Studio под названием Add Service Reference
(Добавить ссылку на службу), доступную в меню Project (Проект). Оба подхода также
обеспечивают генерацию необходимого файла *.config клиентской стороны, который
содержит различные конфигурационные установки для прокси (такие как
расположение веб-службы).
Предполагая, что прокси для рассматриваемой простой веб-службы сгенерирован
(хотя в данном случае это необязательно), клиентское приложение сможет вызывать
веб-метод следующим образом:
static void Main(string [ ] args)
{
// Тип прокси содержит код для чтения файла *.config,
// который позволяет определить расположение веб-службы.
HelloWebServiceProxy proxy = new HelloWebServiceProxy() ;
Console.WriteLine(proxy.HelloWorId() ) ;
}
Хотя в .NET 4.0 осталась возможность строить такого рода "традиционный" вариант
веб-службы XML, большинство новых проектов служб выиграют от применения вместо
этого шаблонов WCF. Фактически, начиная с .NET 3.0, технология WCF стала
предпочтительным способом построения систем, ориентированных на службы.
Исходный код. Проект HelloWorldWebService доступен в подкаталоге Chapter 25.
Стандарты веб-служб
Главная проблема состоит в том, что реализации веб-служб, с которыми мы
имели дело ранее, созданные крупными компаниями (Microsoft, IBM и Sun Microsystems),
были не на 100% совместимы между собой. Вполне очевидно, что это было проблемой,
учитывая, что основной целью веб-служб является достижение высокой степени
взаимодействия между платформами и операционными системами!
Чтобы гарантировать возможность взаимодействия веб-служб, группа под
названием World Wide Web Consortium (W3C; http://www.w3.org) и организация Web Services
Interoperability Organization (WS-I; http://www.ws-i.org) приступили к разработке
спецификаций. Эти спецификации призваны определить, каким образом
поставщики программного обеспечения (т.е. IBM, Microsoft и Sun Microsystems) должны строить
библиотеки программного обеспечения для разработки веб-служб, чтобы обеспечивать
совместимость.
Все эти спецификации получили общее имя WS-* и охватили вопросы безопасности,
вложений, описание веб-служб (через язык описания веб-служб WSDL (Web Description
Language)), политики, форматы SOAP и массу прочих деталей.
Как известно, реализация Microsoft, учитывающая большую часть этих стандартов
(как для управляемого, так и неуправляемого кода), встроена в набор инструментов Web
Services Enhancements (WSE), который может быть свободно загружен с веб-сайта
поддержки: http://msdn2.microsoft.com/en-us/webservices.
При построении приложения службы WCF не приходится напрямую использовать
сборки, которые являются частью набора инструментов WSE. Вместо этого, если
создается служба WCF, использующая привязку на основе HTTP (рассматривается далее в
Глава 25. Введение в Windows Communication Foundation 913
главе), то все эти спецификации WS-* предоставляются в готовом виде (в точности те,
что основаны на выбранной привязке).
Именованные каналы, сокеты и Р2Р
Если выбора между DCOM, .NET Remoting, веб-службами, СОМ+ и MSMQ
оказывается недостаточно, список распределенных API-интерфейсов можно продолжить.
Программисты могут также использовать дополнительные API-интерфейсы
межпроцессного взаимодействия, такие как именованные каналы (pipe), сокеты и
одноранговые (peer-to-peer — Р2Р) службы. Эти низкоуровневые API-интерфейсы обычно
обеспечивают более высокую производительность (особенно для машин, находящихся в одной
локальной сети); однако применение этих API-интерфейсов намного усложняется (если
вообще возможно) при разработке приложений, открытых внешнему миру.
При построении распределенной системы, которая включает множество
приложений, работающих на одной физической машине, можно применить API-интерфейс
именованных каналов через пространство имен System. 10.Pipes. Этот подход обеспечит
самый быстрый способ пересылки данных между приложениями на одной машине.
Также если строится приложение, которое требует абсолютного контроля над
установкой и поддержанием сетевого соединения, то сокеты и функциональность Р2Р
доступны на платформе .NET с использованием пространств имен System.Net.Sockets и
System.Net.PeerToPeer.
Роль WCF
Широкий массив распределенных технологий весьма затрудняет выбор правильного
инструмента для работы. Ситуация еще более усложняется ввиду того факта, что
некоторые из этих технологий имеют перекрывающуюся функциональность (например, в
области транзакций и безопасности).
Даже когда разработчик .NET выбрал технологию, которая кажется правильной для
текущей задачи, построение, сопровождение и конфигурирование такого приложения
будет, по меньшей мере, сложным. Каждый API-интерфейс имеет собственную
программную модель, собственный уникальный набор инструментов конфигурирования и т.д.
До появления .NET 3.0 было очень трудно подключить распределенные API-
интерфейсы без написания существенного объема специальной инфраструктуры.
Например, если вы строите систему с использованием API-интерфейсов .NET Remoting,
а позднее решите применить веб-службы XML как более подходящее решение, придется
полностью перепроектировать кодовую базу.
WCF — это инструментальный набор распределенных вычислений, появившийся в
.NET 3.0, который интегрирует все эти ранее независимые технологии распределенной
обработки в один стройный API-интерфейс, представленный, прежде всего,
пространством имен System.ServiceModel. С помощью WCF можно предоставлять эти службы
вызывающему коду, применяя для этого широкое разнообразие приемов. Например,
при создании "домашнего" приложения, где все подключенные машины работают под
управлением Windows, можно использовать различные протоколы TCP для достижения
максимально возможной производительности. Те же самые службы также могут быть
представлены с применением протоколов на основе веб-служб XML, чтобы позволить
внешним клиентам пользоваться их функциональностью, независимо от языка
программирования или операционной системы.
Учитывая тот факт, что WCF позволяет выбрать правильный протокол для
выполнения работы (используя общую программную модель), вы обнаружите, что подключить
и запустить низкоуровневые механизмы распределенного приложения достаточно про-
914 Часть V. Введение в библиотеки базовых классов .NET
сто. В большинстве случаев это можно делать без перекомпиляции или повторного
развертывания клиентского программного обеспечения и службы, поскольку
низкоуровневые детали часто задаются в конфигурационных файлах приложения (подобно старым
API-интерфейсам .NET Remoting).
Обзор средств WCF
Возможность взаимодействия и интеграция различных API-интерфейсов — это
только два важных аспекта WCF. В дополнение WCF предлагает развитую фабрику
программного обеспечения, которая дополняет технологии удаленной разработки,
представленные WCF. Ниже приведен список главных средств WCF.
• Поддержка как строго типизированных, так и не типизированных сообщений.
Этот подход позволяет приложениям .NET эффективно разделять типы, в то
время как программное обеспечение, созданное с использованием других платформ
(вроде Java) может потреблять потоки слабо типизированного XML.
• Поддержка нескольких привязок (низкоуровневый HTTP, TCP, MSMQ и
именованные каналы), позволяющая выбирать наиболее подходящий механизм для
транспортировки сообщений туда и обратно.
• Поддержка последних спецификаций веб-служб (WS-*).
• Полностью интегрированная модель безопасности, охватывающая как
встроенные протоколы безопасности Windows/.NET, так и многочисленные нейтральные
технологии защиты, построенные на стандартах веб-служб.
• Поддержка технологий хранения состояния сеансов, а также поддержка
однонаправленных сообщений без состояния.
Каким бы впечатляющим ни был этот список, на самом деле он лишь поверхностно
касается функциональности, предлагаемой WCF Технология WCF также предоставляет
средства трассировки и протоколирования, счетчики производительности, модель
публикации и подписки на события, поддержку транзакций и многое другое.
Обзор архитектуры, ориентированной на службы
Еще одно преимущество WCF состоит в том, что он базируется на принципах
дизайна, установленном архитектурой, ориентированной на службы (service-oriented
architecture — SOA). Чтобы быть точным, SOA стало модным словечком в индустрии,
и подобно многим таким словечкам, может быть определено различными способами.
Попросту говоря, SOA — это способ проектирования распределенных систем, где
несколько автономных служб работают совместно, передавая сообщения через границы
(либо сетевых машин, либо двух процессов на одной и той же машине) с
использованием четко определенных интерфейсов.
В мире WCF эти четко определенные интерфейсы обычно создаются с применением
действительных интерфейсных типов CLR (см. главу 9). Однако в более общем смысле
интерфейс службы просто описывает набор членов, которые могут быть вызваны
внешними клиентами.
Команда разработчиков WCF пользовалась четырьмя принципами
проектирования SOA. Хотя данные принципы реализуются автоматически, просто при построении
приложения WCF. понимание этих четырех кардинальных правил дизайна SOA может
помочь применять WCF в дальнейшей перспективе. В последующих разделах приведен
краткий обзор каждого принципа.
Глава 25. Введение в Windows Communication Foundation 915
Принцип 1: границы установлены явно
Этот принцип подчеркивает тот факт, что функциональность службы WCF
выражается через четко определенные интерфейсы (т.е. описания каждого члена, его
параметров и возвращаемых значений). Единственный способ, которым внешний клиент может
связаться со службой WCF, — через интерфейс, при этом оставаясь в блаженном
неведении о деталях ее внутренней реализации.
Принцип 2: службы автономны
Говоря о службах, как об автономных сущностях, имеется в виду тот факт, что
каждая служба WCF является (насколько возможно) отдельным "островом". Автономная
служба должна быть независимой от проблем с версиями, развертыванием и
установкой. Чтобы помочь в продвижении этого принципа, мы опять возвращаемся к
ключевому аспекту программирования на основе интерфейсов. Как только интерфейс внедрен,
он никогда не должен изменяться (или вы рискуете разрушить существующие клиенты).
Когда требуется расширить функциональность службы WCF, просто напишите новый
интерфейс, который моделирует необходимую функциональность.
Принцип 3: службы взаимодействуют через контракт, а не реализацию
Третий принцип — еще один побочный продукт программирования на основе
интерфейсов — состоит в том, что реализация деталей службы WCF (на каком языке она
написана, как именно выполняет свою работу, и т.п.) не касается вызывающего ее внешнего
клиента. Клиенты WCF взаимодействуют со службами исключительно через их
открытые интерфейсы. Более того, если члены службы представляют сложные специальные
типы, они должны быть полностью детализированы в виде контракта данных,
гарантируя, что клиенты смогут отобразить содержимое на определенную структуру данных.
Принцип 4: совместимость служб базируется на политике
Поскольку интерфейсы CLR предоставляют строго типизированные контракты всем
клиентам WCF (и также могут быть использованы для генерации соответствующего
документа WSDL на основе выбранной привязки), важно понимать на то, что
интерфейсы и WSDL сами по себе недостаточно выразительны, чтобы детализировать аспекты
того, что способна делать служба. Учитывая это, SOA позволяет определять политики,
которые еще более проясняют семантику службы (например, ожидаемые требования
безопасности, применяемые для общения со службой). Используя эти политики, можно
отделять низкоуровневые синтаксические описания службы (предоставляемые
интерфейсы) от семантических деталей их работы и способов их вызова.
WCF: итоги
Этот небольшой экскурс в историю проиллюстрировал, что WCF является
предпочтительным подходом для построения распределенных приложений в .NET 3.0 и
последующих версиях Пытаетесь ли вы построить "домашнее" приложение, применяя протоколы
TCP, перемещаете данные между программами на одной и той же машине с
использованием именованных каналов, или же в основном предоставляете данные внешнему миру
через веб-службы — для всего этого имеет смысл применять API-интерфейс WCF
Это не означает невозможность применения в новых разработках первоначальных
пространств имен, связанных с распределенными вычислениями (т.е. System.Runtime.
Remotmg, System.Messaging, Syster-.Enterprisedervices и System.Web.Services).
Фактически в некоторых случаях (в частности, когда нужно строить объекты СОМ+), это
просто придется делать. Но, так или иначе, если вы использовали эти API-интерфейсы
в прошлых проектах, изучение WCF не представит особой сложности. Подобно техно-
916 Часть V. Введение в библиотеки базовых классов .NET
логиям, которые предшествовали ему, WCF интенсивно использует конфигурационные
файлы на основе XML, атрибуты .NET и утилиты генерации прокси.
Вооружившись всеми этими знаниями, теперь можно сконцентрировать внимание
на построении приложения WCF. Опять-таки, имейте в виду, что полное описание WCF
потребовало бы целой книги, поскольку описание каждой из поддерживающих служб
(те. MSMQ, COM+, Р2Р и именованные каналы) заняло бы отдельную главу. Здесь будет
показан общий процесс построения программ WCF с использованием протоколов TCP и
HTTP (веб-службы). Это должно подготовить почву для дальнейшего углубления знаний
в данной области.
Исследование основных сборок WCF
Как и можно было ожидать, фабрика программного обеспечения WCF
представлена набором сборок .NET, установленных в GAC. В табл. 25.1 описаны основные сборки
WCF, которые понадобится применять почти в любом приложении WCF
Таблица 25.1. Основные сборки WCF
Сборка Назначение
System.Runtime.Serialization.dll Определяет пространства имен и типы,
используемые для сериализации и десериализации
объектов в WCF
System.ServiceModel.dll Основная сборка, содержащая типы! используемые
для построения приложений WCF любого рода
В этих двух сборках определен ряд пространств имен и типов. Детальные
сведения о них доступны в документации .NET Framework 4.0 SDK, а в табл. 25.2
описаны важнейшие пространства имен, о которых следует знать.
Таблица 25.2. Основные пространства имен WCF
Сборка Назначение
System.Runtime.Serialization
System.ServiceModel
System.ServiceModel.Configuration
System.ServiceModel.Description
System.ServiceModel.Msmqlntegration
System.ServiceModel.Security
Определяет многочисленные типы, используемые
для управления сериализацией и десериализаци-
ей данных в WCF
Первичное пространство имен WCF,
определяющее типы привязки и хостинга, а также базовые
типы безопасности и транзакций
Определяет типы, обеспечивающие объектную
модель адресов, привязок и контрактов,
определенных внутри конфигурационных файлов WCF
Определяет типы для интеграции со службами
MSMQ
Определяет многочисленные типы для
управления аспектами уровней безопасности WCF
Определяет многочисленные типы,
предоставляющие программный доступ к
конфигурационным файлам WCF
Глава 25. Введение в Windows Communication Foundation 917
Несколько слов о CardSpace
В дополнение к System.ServiceModel.dll и System.Runtime.Serialization.dll,
в WCF имеется и третья сборка, называемая System.IdentityModel.dll. В ней
определены многочисленные дополнительные пространства имен и типы, которые поддерживают
API-интерфейс CardSpace. Эта технология позволяет устанавливать и управлять цифровыми
идентификаторами внутри приложения WCF. По сути, API-интерфейс CardSpace предлагает
унифицированную программную модель для доступа к различным деталям приложений WCF,
связанным с безопасностью, таким как идентичность вызывающего кода, службы
авторизации/аутентификации и т.д. Полное описание API-интерфейса CardSpace API можно найти в документации
.NET Framework 4.0 SDK.
Шаблоны проектов WCF в Visual Studio
Как будет более подробно объясняться позже в этой главе, приложение WCF обычно
представлено тремя сборками, одной из которых является *.dll, содержащая типы, с
которыми может взаимодействовать внешний клиент (другими словами, сама служба
WCF). Когда вы хотите построить службу WCF, совершенно разумно выбрать
стандартный шаблон проекта Class Library (см. главу 14) в качестве начальной точки и вручную
добавить ссылки на сборки WCF.
В качестве альтернативы новую службу WCF можно создать, выбрав в Visual Studio
2010 шаблон проекта WCF Service Library (Библиотека служб WCF), как показано на
рис. 25.2. Этот тип проекта автоматически устанавливает ссылки на необходимые
сборки WCF, однако он также генерирует и приличный объем начального кода, который,
скорее всего, будет удален.
New Project
^^з^^^я
Imtaled Templates
л Visual С*
Windows
Web
Office
Cloud Service
Reporting
Sirverttght
Test
1Езшашшшш
.NET Fra
Щ
Щ
Щ
mework 4 » ! Sort by: Default
WCF Service Library Visual C«
WCF Workflow Service Applic... Visual C*
Syndication Service Library visual C*
C S
»| : 1 (Tj |
Type: Visual C»
A project for creating a WCF service class
library (.dll)
Per user extensions are currently not allowed to load.
Name: WcfServiceLibraryl
Location: С ■ М/С ode
Solution name: -.-.-,- t
Create directory for solution
Add to source control
Рис. 25.2. Шаблон проекта WCF Service Library в Visual Studio 2010
Одно из преимуществ выбора этого шаблона проекта состоит в том, что он также
снабжает файлом App.config, что может показаться странным, так как строится .NET
*.dll, а не .NET *.exe. Однако этот файл очень полезен тем, что при отладке или
запуске проекта WCF Service Library интегрированная среда разработки Visual Studio 2010
автоматически запустит приложение WCF Test Client (Тестовый клиент WCF). Программа
WcfTestClient.exe читает настройки из файла App.config, поэтому может использо-
918 Часть V. Введение в библиотеки базовых классов .NET
ваться для тестирования службы. Далее в этой главе WCF Test Client рассматривается
более подробно.
На заметку! Файл App.conf ig из проекта WCF Service Library также полезен тем, что
демонстрирует начальные установки для конфигурации хост-приложения WCF. Фактически большую
часть этого кода можно копировать и вставлять в конфигурационный файл реального хост-
приложения.
В дополнение к базовому шаблону WCF Service Library в категории проектов WCF
диалогового окна New Project (Новый проект) определены еще два библиотечных
проекта WCF, которые интегрируют функциональность Windows Workflow Foundation (WF) в
службу WCF, а также шаблон для построения библиотеки RSS (см. рис. 25.2). Технология
Windows Workflow Foundation рассматривается в следующей главе, поэтому данные
шаблоны проектов WCF можно пока проигнорировать.
Шаблон проекта WCF Service
Доступен еще один шаблон проектов Visual Studio 2010, связанный с WCF, который
находится в диалоговом окне New Web Site (Новый веб-сайт), открываемом через пункт
меню File^New^Web Site (рис. 25.3).
New Web Site
Recent Templates
Installed Templates
Visual Basic
Visual C*
1 Online Templates
Per user extensions art
Web location:
сигтепйу not ark
file System
^TlTamework 4 ' ) Swt by: "Default
^ Empty Web Site Visual C#
*W Sirverlight 1.0 Web Site Visual C#
,cO WCF Service Visual C#
jff\ ASP.NET Reports Web Site Visual C*
Яу Dynamic Data Linq to SQL W... Visual C*
I Ш^ Dynamic Data Entities Web Si...Visual C*
ijjfe ASP.NET Crystal Reports We... Visual C*
т*4*ьшл.шъшлт Ышш mi —■■■■!
•j СЛМуСоое
ZZIi С I-
Type: Visual C*
A Web site for cresting WCF services
Browse...
OK Cancel \
Рис. 25.3. Веб-ориентированный шаблон проектов WCF Service в Visual Studio 2010
Шаблон проекта WCF Service (Служба WCF) полезен, когда заранее известно, что
служба WCF будет использовать протоколы веб-служб, а не, например, именованные
каналы. Эта опция позволит автоматически создать новый виртуальный каталог IIS
для хранения программных файлов WCF, создаст корректный файл Web.config для
представления службы через HTTP и сгенерирует необходимый файл *.svc (подробнее
о файлах *.svc рассказывается далее в этой главе). Таким образом,
веб-ориентированный проект WCF Service просто экономит время, поскольку IDE-среда автоматически
настроит всю необходимую инфраструктуру IIS.
Напротив, если новая служба WCF создается с применением опции WCF Service
Library, появляется возможность разместить службу несколькими различными
способами (например, специальный хост, служба Windows или вручную созданный виртуальный
каталог IIS). Эта опция больше подходит, когда планируется построить специальный хост
для службы WCF, которая может работать с любым количеством привязок WCF
Глава 25. Введение в Windows Communication Foundation 919
Базовая композиция приложения WCF
При построении распределенной системы WCF обычно создаются три
взаимосвязанных сборки.
• Сборка службы WCF. Эта библиотека *.dll содержит классы и интерфейсы,
представляющие обитую функциональность, которая предлагается внешним
клиентам.
• Хост службы WCF. Этот программный модуль — сущность, которая принимает в
себе сборку службы WCF.
• Клиент WCF. Это приложение, которое обращается к функциональности службы
через промежуточный прокси.
Как уже упоминалось, сборка службы WCF — это библиотека классов .NET,
содержащая в себе множество контрактов WCF и их реализаций. Ключевое отличие состоит
в том, что контрактные интерфейсы оснащены разнообразными атрибутами, которые
управляют представлением типа данных, тем, как исполняющая среда WCF
взаимодействует с представленными типами, и т.д.
Вторая сборка — хост WCF — может быть любой исполняемой программой .NET. Как
будет показано в этой главе, WCF настраивается таким образом, что службы могут быть
легко представлены приложениями любого типа (т.е. Windows Forms, служба Windows,
приложения WPF). При построении специального хоста применяется тип ServiceHost
и связанный с ним файл *. con fig, который содержит детали, касающиеся механизмов
серверной стороны, которые вы хотите использовать. Однако если в качестве хоста для
службы WCF применяется IIS, то нет необходимости в программном построении
специального хоста, поскольку IIS использует "за кулисами" тип ServiceHost.
На заметку! Развернуть службу WCF также можно с применением службы Windows Activation Service
(WAS). За подробными сведениями обращайтесь в документацию .NET Framework 4.0 SDK.
Последняя сборка представляет клиент, который осуществляет вызовы службы WCF.
Как и можно было ожидать, этим клиентом может быть приложение .NET любого типа.
Подобно хосту, клиентское приложение также обычно использует файл *. con fig
клиентской стороны, определяющий все клиентские механизмы. Следует также помнить,
что если служба WCF строится с использованием привязок, основанных на HTTP, то
клиентское приложение может быть реализовано на другой платформе (например, Java).
На рис. 25.4 показаны высокоуровневые отношения между этими тремя
взаимосвязанными сборками WCF. "За кулисами" скрыто несколько низкоуровневых деталей,
реализующих все необходимые внутренние механизмы (фабрики, каналы, слушатели
и т.п.). Эти низкоуровневые детали чаще всего скрыты от глаз; однако при
необходимости они могут быть расширены или настроены. В большинстве случаев вполне
подходят настройки по умолчанию.
Также следует упомянуть, что применение файла *. con fig серверной или
клиентской стороны не является обязательным. При желании можно жестко закодировать
хост (а также клиент), указав необходимые детали (т.е. конечные точки, привязку,
адреса). Очевидная проблема такого подхода состоит в том, что если нужно изменить
детали настройки, понадобится вносить изменения в код, перекомпилировать и заново
развертывать множество сборок. Использование файла *. con fig делает кодовую базу
намного более гибкой, поскольку все изменения настроек производятся
редактированием конфигурационных файлов и последующим перезапуском. С другой стороны,
программные конфигурации обеспечивают приложению более динамичную гибкость — оно
может выбирать конфигурацию внутренних механизмов, например, в зависимости от
результатов проверки условий.
920 Часть V. Введение в библиотеки базовых классов .NET
( 1
Клиентское приложение
( "^
Прокси
1 ^
4
Ч
*
| Т Конфигурационный файл
XoctWCF
с— "^
1 \\
1
Конфигурационный файл у
Рис. 25.4. Высокоуровневое представление типичного приложения WCF
Понятие ABC в WCF
Хосты и клиенты взаимодействуют друг с другом, согласовывая так называемые
ABC — условное наименование для запоминания основных строительных блоков
приложения WCF, таких как адрес, привязка и контракт (address, binding, contract — ABC).
• Адрес. Описывает местоположение службы. В коде представлен типом System.Uri,
однако значение обычно хранится в файлах *. с on fig.
• Привязка. WCF поставляется с множеством различных привязок, которые
указывают сетевые протоколы, механизмы кодирования и транспортный уровень.
• Контракт. Предоставляет описание каждого метода, представленного службой
WCF.
Имейте в виду, что аббревиатура ABC не подразумевает, что разработчик обязан
определять сначала адрес, за ним привязку и только потом — контракт. Во многих случаях
разработчик WCF начинает с определения контракта службы, за которым следует адрес
и привязки (допускается любой порядок, если только указаны все аспекты). Прежде чем
строить первое приложение WCF, давайте более детально рассмотрим ABC.
Понятие контрактов WCF
Понятие контракта является ключевым при построении службы WCF Хотя это и
не обязательно, подавляющее большинство приложений WCF будут начинаться с
определения типов интерфейсов .NET, используемых для представления набора членов,
которые поддерживаются данной службой WCF В частности, интерфейсы, которые
представляют контракт WCF, называются контрактами служб. Классы (или
структуры), которые реализуют их, носят название типов служб.
Контракты служб WCF оснащаются различными атрибутами, наиболее часто
используемые из которых определены в пространстве имен System.ServiceModel. Когда
члены контракта службы (методы в интерфейсе) содержат только простые типы данных
(такие как числовые, булевские и строковые), полную службу WCF можно построить,
используя одни только атрибуты [ServiceContract] и [OperationContract].
Однако если члены представляют специальные типы, придется обратиться к
пространству имен System.Runtime.Serialization (рис. 25.5) из сборки System.Runtime.
Serialization.dll. Здесь доступны дополнительные атрибуты (вроде [DataMember]
и [DataContract]) для тонкой настройки процесса определения того, как составные
типы будут сериализоваться и десериализоваться из XML при передаче в и из операций
службы.
Строго говоря, использовать интерфейсы CLR для определения контракта WCF не
обязательно. Многие из этих атрибутов могут применяться к общедоступным членам
Глава 25. Введение в Windows Communication Foundation 921
общедоступного класса (или структуры). Однако, учитывая множество преимуществ
программирования на основе интерфейсов (полиморфизм, элегантная поддержка
множества версий и т.п.), применение интерфейсов CLR для описания контракта WCF
лучше считать рекомендуемым приемом.
л
^ S stem Puntime Serialization [4.0 0.0]
' пЕшсшЕзга
Vv CcllecticnDataContr3ct-.ttnbLite
\% ContiactfJamejpacer-.ttnbute
T$ DataCcntract^ttribute
1$ DataContractPeiol er
r$ DataContractSenahzer
-; Dataf lember^ttnbute
L^ EnumMemberMttribute
Ti E»portOptionn
,; E'tenzicnDataObject
"-' IDataCGntractSurrcgate
- IE.ten:ibleDataObjert
^ IgncieDataf.lember-ttnbute
% ImpcrtQptions
Ц In ahdDataCcntractE^ception
X$ Knc .nT^pe-tttribute
tt NetDataCcntractSerializer
Lj mlObjectSenalizer
Ц ^.mlSenalizableSer. ices
l^ 'PathOuer, Generator
{£ :clDataCcntractEjporter
T# 'sdDataContiactlmporter
Рис. 25.5. В сборке System.Runtime.Serialization определен
ряд атрибутов, используемых при построении контрактов данных WCF
Понятие привязок WCF
Как только контракт (или набор контрактов) определен и реализован внутри
библиотеки службы, следующий логический шаг состоит в построении агента хостинга для
самой службы WCF. Как уже упоминалось, на выбор доступно множество возможных
вариантов хостов, и все они должны указывать привязки, используемые удаленными
клиентами для получения доступа к функциональности типа службы.
Выбор из набора привязок — это область, которая отличает разработку WCF от .NET
Remoting и/или разработки веб-служб XML. На выбор в WCF доступно множество
возможных привязок, каждая из которых ориентирована на определенные потребности.
Если ни одна из готовых привязок не удовлетворяет существующим требованиям,
можно создать собственную привязку, расширив тип CustomBinding (это в главе делаться
не будет). Привязка WCF может описывать следующие характеристики:
• транспортный уровень, используемый для передачи данных (HTTP, MSMQ,
именованные каналы и TCP);
• каналы, используемые транспортом (однонаправленные, запрос-ответ и
дуплексные);
• механизм кодирования, используемый для работы с данными (XML и двоичный);
• любые поддерживаемые протоколы веб-служб (если разрешены привязкой), такие
как WS-Security, WS-Transactions, WS-Reliability и т.д.
Давайте рассмотрим возможные варианты.
922 Часть V. Введение в библиотеки базовых классов .NET
Привязки на основе HTTP
Классы BasicHttpBinding, WSHttpBinding, WSDualHttpBinding и WSFederation
HttpBinding предназначены для представления контрактных типов через протоколы
веб-служб XML. Ясно, что если для разрабатываемой службы потребуются
дополнительные возможности (множество операционных систем или множество программных
архитектур), эти привязки подойдут наилучшим образом, потому что все они кодируют
данные на основе представления XML и используют в сети протокол HTTP.
В табл. 25.3 показано, что привязка WCF может быть представлена в коде (с
использованием классов из пространства имен System.ServiceModel) или в виде атрибутов
XML, определенных внутри файлов *. con fig.
Таблица 25.3. Привязки WCF на основе HTTP
Класс привязки
Элемент привязки
Назначение
BasicHttpBinding
<basicHttpBinding""
WSHttpBinding
<wsHttpBinding>
WSDualHttpBinding
CwsDualHttpBinding>
WSFederationHttpBinding <wsFederationHttpBinding>
Используется для построения
службы WCF, совместимой
с профилем WS-Basic Profile
(WS-I Basic Profile 1.1). Эта
привязка использует HTTP в
качестве транспорта и Text/XML
в качестве метода кодирования
сообщений по умолчанию
Подобен классу
BasicHttpBinding, НО
предоставляет больше
средств веб-служб. Эта
привязка добавляет поддержку
транзакций, надежной
доставки сообщений и протокола
WS-Addressing
Подобен классу
WSHttpBinding, но
предназначен для применения с
дуплексными контрактами
(например, когда служба и
клиент могут посылать
сообщения туда и обратно).
Эта привязка поддерживает
только безопасность SOAP и
требует надежного обмена
сообщениями
Безопасная привязка с
возможностью взаимодействия,
которая поддерживает
протокол WS-Federation, позволяя
организациям, объединенным
в федерацию, эффективно
проводить аутентификацию и
авторизацию пользователей
BasicHttpBinding является простейшим из всех протоколов на основе веб-служб.
В частности, эта привязка гарантирует соответствие службы WCF спецификациям WS-I
Basic Profile 1.1, определенным WS-I. Основная причина применения этой привязки со-
Глава 25. Введение в Windows Communication Foundation 923
стоит в поддержке обратной совместимости с приложениями, ранее построенными для
взаимодействия с веб-службами ASP.NET (которые были частью библиотек .NET,
начиная с версии 1.0).
Протокол WSHttpBinding не только включает поддержку подмножества
спецификаций WS-* (транзакции, безопасность и надежные сеансы), но также обеспечивает
возможность обработки двоичного кодирования данных с использованием механизма
МТОМ (Message Transmission Optimization Mechanism — механизм оптимизации
передачи сообщений).
Основное преимущество привязки WSDualHttpBinding в том, что она добавляет
возможность двустороннего (дуплексного) обмена сообщениями между отправителем
и получателем. При выборе WSDualHttpBinding можно применять модель событий
издания/подписки.
И, наконец, WSFederationHttpBinding — это протокол на основе веб-служб,
который стоит применять, когда наиболее важна безопасность. Эта привязка поддерживает
спецификации WS-Trust, WS-Security и WS-SecureConversation, которые представлены
API-интерфейсами WCF CardSpace.
Привязки на основе TCP
При построении распределенного приложения, работающего на машинах, которые
сконфигурированы с библиотеками .NET 4.0 (другими словами, все машины работают
под управлением операционной системы Windows), можно получить выигрыш в
производительности, минуя привязки веб-служб и работая непосредственно с TCP, что
гарантирует кодирование данных в компактном двоичном формате вместо XML. При
использовании привязок, перечисленных в табл. 25.4, клиент и хост должны быть
приложениями .NET.
Таблица 25.4. Привязки WCF на основе TCP
Класс привязки Элемент привязки Назначение
NetNamedPipeBinding <netNamedPipeBinding> Безопасная, надежная,
оптимизированная привязка для коммуникаций между
приложениями .NET на одной и той же
машине
NetPeerTcpBinding <netPeerTcpBinding> Предоставляет безопасную привязку
для сетевых приложений Р2Р
NetTcpBinding <netTcpBinding> Безопасная и оптимизированная
привязка, подходящая для межмашинных
коммуникаций между приложениями .NET
Класс NetTcpBinding использует протокол TCP для перемещения двоичных данных
между клиентом и службой WCF. Как упоминалось ранее, это дает выигрыш в
производительности по сравнению с протоколами веб-служб, но при этом решения ограничены
ОС Windows. Положительной стороной является поддержка в NetTcpBinding
транзакций, надежных сеансов и безопасных коммуникаций.
Подобно NetTcpBinding, класс NetNamedPipeBinding поддерживает транзакции,
надежные сеансы и безопасные коммуникации, но при этом обладает способностью
межмашинных вызовов. Если вы ищете самый быстрый способ передачи данных
между приложениями WCF на одной машине, привязке NetNamedPipeBinding нет равных.
Более подробную информацию о NetPeerTcpBinding можно найти в документации
NET Framework 4.0 SDK.
924 Часть V. Введение в библиотеки базовых классов .NET
Привязки на основе MSMQ
И, наконец, если цель заключается в интеграции с сервером MSMQ, то
непосредственный интерес представляют NetMsmqBinding и MsmqlntegrationBinding. Детали
применения привязок MSMQ в этой главе не рассматриваются, но в табл. 25.5 описано
основное назначение каждой из них.
Таблица 25.5. Привязки WCF на основе MSMQ
Класс привязки Элемент привязки Назначение
MsmqlntegrationBinding <msmqIntegrationBinding> Эта привязка может
применяться для того, чтобы позволить
приложениям WCF отправлять и
принимать сообщения от
существующих приложений MSMQ,
которые используют СОМ,
родной C++ или типы,
определенные в пространстве имен
System.Messaging
NetMsmqBinding <netMsmqBinding> Привязка на основе очередей,
подходящая для межмашинных
коммуникаций между
приложениями .NET. Это
предпочтительный подход среди привязок,
основанных на MSMQ
Понятие адресов WCF
Как только контракты и привязки установлены, финальный элемент мозаики
состоит в указании адреса для службы WCF. Это важно, поскольку удаленные клиенты не
смогут взаимодействовать с удаленными типами, если им не удастся найти их. Подобно
большинству аспектов WCF, адрес может быть жестко закодирован в сборке (с
использованием типа System.Uri) или вынесен в файл *.config.
В любом случае точный формат адреса WCF отличается в зависимости от выбранной
привязки (на основе HTTP, именованных каналов, TCP или MSMQ). На самом высоком
уровне адреса WCF могут указывать перечисленные ниже единицы информации.
• Scheme. Транспортный протокол (HTTP и т.п.).
• MachineName. Полностью квалифицированное доменное имя машины.
• Port. Во многих случаях не обязательный параметр. Например, привязка HTTP по
умолчанию использует порт 80.
• Path. Путь к службе WCF
Эта информация может быть представлена следующим обобщенным шаблоном
(значение Port необязательно, поскольку некоторые привязки его не используют):
Scheme : //<MachineName-> [ : Port] /Path
При использовании привязки на базе веб-службы (basicHttpBinding, wsHttpBinding,
wsDualHttpBinding или wsFederationHttpBinding) адрес разбивается следующим
образом (вспомните, что если номер порта не указан, протоколы на основе HTTP по
умолчанию выбирают порт 80):
http://localhost:8080/MyWCFService
Глава 25. Введение в Windows Communication Foundation 925
Если применяется привязка на основе TCP (такая как NetTcpBinding или
NetPeerTcpBinding), то URI принимает следующий формат:
net.tcp://localhost: 8 080/MyWCFService
Привязки на основе MSMQ (NetMsmqBinding и MsmqlntegrationBinding)
уникальны в части их формата URI, учитывая, что MSMQ может использовать общедоступные
или приватные очереди (доступные только на локальной машине), а номера портов не
имеют смысла в URI, связанных с MSMQ. Взгляните на следующий URI, который
описывает приватную очередь по имени MyPrivate():
net.msmq://localhost/private$/MyPrivateQ
И последнее: формат адреса, используемый привязкой для именованных каналов
NetNamedPipeBinding, выглядит так, как показано ниже (вспомните, что
именованные каналы позволяют межпроцессные взаимодействия приложений на одной и той же
физической машине):
net.pipe://localhost/MyWCFService
Хотя одиночная служба WCF может представлять только один адрес (основанный на
единственной привязке), можно сконфигурировать коллекцию уникальных адресов (с
разными привязками). Это делается внутри файла *.config за счет определения
множества элементов <endpoint>. Для одной и той же службы можно указывать любое
количество ABC. Такой подход полезен, когда необходимо позволить клиентам выбирать
протокол, которые они хотят использовать для взаимодействия со службой.
Построение службы WCF
Теперь, когда вы получили представление о построении блоков приложения WCF,
давайте создадим первое простое приложение, чтобы посмотреть, как ABC можно
описывать в коде и в конфигурационном файле. В первом примере шаблоны проектов WCF
из Visual Studio 2010 использоваться не будут, что позволит сосредоточиться на
специфических шагах по созданию службы WCF. Для начала создайте новый проект С# Class
Library по имени MagicEightBallServiceLib.
Переименуйте начальный файл Classl.cs на MagicEightBallService.cs и
добавьте ссылку на сборку System.ServiceModel.dll. В начальном файле кода укажите
использование пространства имен System.ServiceModel. К этому моменту файл С#
должен выглядеть так:
using System;
using System.Collections .Generic-
using System.Linq;
using System.Text;
// Основное пространство имен WCF.
using System.ServiceModel;
namespace MagicEightBallServiceLib
{
public class MagicEightBallService
{
}
}
В этом классе реализован единственный контракт службы WCF, представленный
строго типизированным интерфейсом CLR по имени IEightBall. Как известно,
магический шар Magic 8-Ball — это игрушка, позволяющая получить шуточное
предсказание будущего. В интерфейсе определен единственный метод, который позволит клиенту
задать вопрос магическому шару, чтобы получить случайный ответ.
926 Часть V. Введение в библиотеки базовых классов .NET
Интерфейсы службы WCF оснащены атрибутом [ServiceContract], причем
каждый член интерфейса оснащен атрибутом [OperationContract] (подробнее об этих
двух атрибутах будет сказано чуть позже). Ниже показано определение интерфейса
IEightBall:
[ServiceContract]
public interface IEightBall
{
// Задайте вопрос, получите ответ!
[OperationContract]
string ObtainAnswerToQuestion(string userQuEGtion);
}
На заметку! Допускается определять интерфейс контракта службы, который содержит методы, не
оснащенные атрибутом [OperationContract]. Однако такие члены не будут видны через
исполняющую среду WCF.
Как известно из главы 9, интерфейс — довольно бесполезная вещь, пока он не
реализован классом или структурой с целью пополнения его функциональности. Подобно
реальному магическому шару, реализация типа MaqicEightBallService будет
возвращать случайно выбранный ответ из массипл стрик Конструктор по умолчанию будет
отображать информационное сообщение, которое будет (в конечном итоге) отображено
в окне консоли окна (для диагностических целей):
public class MagicEightBallService : ILijhtb_ill
{
// Для отображения на хосте.
public MagicEightBallService()
{
Console.WriteLine ("The 8-Ball awaits your question...");
}
public string ObtainAnswerToQuestion (string userQuestion)
{
string[] answers = { "Future Uncertain", "Yes", "No",
"Hazy", "Ask again later", "Definitely" };
// Вернуть случайный ответ.
Random г = new Random () ;
return answers[r.Next(answers.Length)];
}
}
Библиотека службы WCF готова. Однако перед конструированием хоста для этой
службы давайте ознакомимся с некоторыми подробностями атрибутов [ServiceContract] и
[OperationContract].
Атрибут [ServiceContract]
Чтобы интерфейс CLR участвовал в службах, предоставленных WCF, он должен быть
оснащен атрибутом [ServiceContract]. Подобно многим другим атрибутам .NET, тип
ServiceContract At tribute поддерживает набор свойств для дальнейшего
прояснения его назначения. Два свойства — Name uNameSpace — могут быть установлены для
управления именем типа службы и именем пространства имен XML, определяющим тип
службы. Если используется привязка, специфичная для веб-служб, эти значения
применяются для определения элементов <portType> связанного документа WSDL.
Здесь мы не заботимся о присваивании значения Name, учитывая, что имя типа
службы по умолчанию основано на имени класса С#. Однако именем по умолчанию
Глава 25. Введение в Windows Communication Foundation 927
для лежащего в основе пространства имен XML будет просто http://tempuri.org (оно
должно быть изменено для всех создаваемых служб WCF).
При построении службы WCF, которая будет посылать и принимать специальные
типы данных (чего мы пока не делаем), важно установить осмысленное значение для
лежащего в основе пространства имен XML, чтобы гарантировать уникальность
специального типа. Как вам должно быть известно из опыта построения веб-служб XML,
пространства имен XML предоставляют способ помещения типов в уникальный контейнер,
гарантируя отсутствие конфликтов с типами из других организаций.
По этой причине можно обновить определение интерфейса, сделав его более
подходящим — почти так же, как это делается при определении пространства имен XML в
проекте .NET Web Service, когда в качестве пространства имен указывается URI
издателя службы. Например:
[ServiceContract (Namespace = "http://MyCompany.com11)]
public interface IEightBall
{
}
Помимо Namespace и Name, атрибут [ServiceContract] может быть
сконфигурирован с помощью дополнительных свойств, которые перечислены в табл. 25.6. Имейте
в виду, что в зависимости от выбранной привязки, некоторые из этих настроек будут
игнорироваться.
Таблица 25.6. Различные именованные свойства атрибута [ServiceContract]
Свойство Назначение
CallbackContract Устанавливает функциональность обратного вызова для
двустороннего обмена сообщениями
Conf igurationName Это имя используется для нахождения элемента службы в
конфигурационном файле приложения. По умолчанию представляет собой
имя класса, реализующего службу
ProtectionLevel Позволяет указать степень, до которой привязка контракта требует
шифрования, цифровых подписей или того и другого для конечных
точек, представленных контрактом
SessionMode Используется для установки разрешения сеанса, запрета сеанса или
обязательности сеанса для данного контракта службы
Атрибут [OperationContract]
Методы, которые планируется использовать внутри WCF, должны быть
оснащены атрибутом [OperationContract], который также может быть сконфигурирован
с помощью различных именованных свойств. Используя свойства, перечисленные
в табл. 25.7, можно указать, что данный метод предназначен для однонаправленной
работы, поддерживает асинхронные вызовы, требует шифрования данных сообщений
и т.д. (в зависимости от выбранной привязки, многие из этих значений могут быть
проигнорированы).
В этом начальном примере дополнительного конфигурирования метода
ObtainAnswerToQuestionO не требуется, поэтому атрибут [OperationContract]
оставлен в том виде, как он определен сейчас.
928 Часть V. Введение в библиотеки базовых классов .NET
Таблица 25.7. Различные именованные свойства атрибута [OperationContract]
Свойство Назначение
AsyncPattern Указывает, реализована ли операция асинхронно с использованием пары
методов Begin/End службы. Это позволяет службе передавать обработку
другому потоку серверной стороны; это не имеет отношения к
асинхронному вызову метода клиентом!
Islnitiating Указывает, может ли эта операция быть начальной операцией сеанса
IsOneWay Указывает, состоит ли операция только из одного входного сообщения
(и никакого ассоциированного вывода)
IsTerminating Указывает, должна ли исполняющая среда WCF пытаться завершить
текущий сеанс после выполнения операции
Служебные типы как контракты операций
И, наконец, вспомните, что при построении служебных типов WCF
использование интерфейсов не обязательно. Фактически атрибуты [ServiceContract] и
[OperationContract] можно применять только непосредственно к самому служебному
типу:
// Только для целей иллюстрации; в текущем примере не используется.
[ServiceContract (Namespace = "http://MyCompany.com11)]
public class ServiceTypeAsContract
I
[OperationContract]
void SomeMethod() { }
[OperationContract]
void AnotherMethod() { }
}
Хотя такой подход возможен, явное определение интерфейсного типа для
представления контракта службы дает массу преимуществ. Наиболее очевидный выигрыш
состоит в том, что один интерфейс может быть применен к нескольким типам служб
(написанных на разных языках и в разных архитектурах), чтобы достичь высокой степени
полиморфизма. Другое преимущество связано с тем, что интерфейс контракта службы
может использоваться в качестве основы для новых контрактов (через наследование
интерфейсов), без необходимости заботиться о реализации.
В любом случае к данному моменту библиотека службы WCF готова. Скомпилируйте
проект, чтобы убедиться в отсутствии опечаток в коде.
Исходный код. Проект MagicEightBallServiceLib доступен в подкаталоге MagicEight
BallServiceHTTP каталога Chapter 25.
Хостинг службы WCF
Теперь мы готовы определить хост. Хотя служба производственного уровня должна
развертываться в службе Windows или виртуальном каталоге IIS, наш первый хост
будет просто консольным приложением по имени MagicEightBallServiceHost.
После создания нового проекта консольного приложения добавьте ссылку на
сборки System.ServiceModel.dll и MagicEightBallServiceLib.dll и обновите
начальный файл кода, добавив импорт пространств имен System. ServiceModel и
MagicEightBallServiceLib:
Глава 25. Введение в Windows Communication Foundation 929
using System;
using System.Collections .Generic-
using System.Linq;
using System.Text;
using System. ServiceModel;
using MagicEightBallServiceLib;
namespace MagicEightBallServiceHost
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** Console Based WCF Host *****");
Console.ReadLine();
}
}
}
Первый шаг, который потребуется предпринять при построении хоста для
служебного типа WCF — решить, будет ли определяться необходимая логика хостинга
полностью в коде либо переместить несколько низкоуровневых деталей в конфигурационный
файл приложения. Как упоминалось ранее, преимущество файлов *.config состоит в
том, что хост может изменять низкоуровневые механизмы без перекомпиляции и
повторного развертывания исполняемых программ. Однако всегда помните, что это не
обязательно, и можно жестко закодировать логику хостинга с использованием типов из
сборки System.ServiceModel.dll.
Этот консольный хост будет использовать конфигурационный файл приложения,
поэтому добавьте новый элемент Application Configuration File (Конфигурационный
файл приложения) к текущему проекту, выбрав пункт меню Projects Add New Item
(ПроектеДобавить новый элемент).
Установка ABC внутри файла App.config
При построении хоста для служебного типа WCF всегда выполняется один и тот же
набор шагов — некоторые через конфигурацию, а некоторые в коде.
1. Определение конечной точки для службы WCF в конфигурационном файле
хоста.
2. Программное использование типа ServiceHost для представления доступных
служебных типов из этой конечной точки.
3. Обеспечение постоянной работы хоста для обслуживания клиентских запросов.
Очевидно, что этот шаг не обязателен, если для хостинга применяется служба
Windows или IIS.
В мире WCF термин конечная точка (endpoint) представляет адрес, привязку и
контракт, объединенные вместе в один пакет. В XML конечная точка выражается элементом
<endpoint> и элементами address, binding и contract. Модифицируйте файл *.con-
fig, указав в нем конечную точку (доступную через порт 8080), которая представлена
данным хостом:
<?xml version="l.О" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<services>
<service name="MagicEightBallServiceLib.MagicEightBallService">
<endpoint address ="http://localhost:80 80/MagicEightBallService"
930 Часть V. Введение в библиотеки базовых классов .NET
binding="basicHttpBinding"
contract="MagicEightBallServiceLib.IEightBall"/>
</service>
</services>
</system.serviceModel>
</configuration>
Обратите внимание, что элемент <system.serviceModel> находится в корне всех
настроек WCF хоста. Каждая служба, развернутая на хосте, представлена элементом
<service>, который помещен в базовый элемент <services>. Здесь единственный
элемент <service> использует (необязательный) атрибут name для указания
дружественного имени служебного типа.
С помощью вложенного элемента <endpoint> задается адрес, модель привязки
(basicHttpBinding в данном примере) и полностью квалифицированное имя типа
интерфейса, определяющего контракт службы (IEightBall). Поскольку применяется
привязка на основе HTTP, указывается схема http:// с произвольным идентификатором
порта.
Кодирование с использованием типа ServiceHost
При текущем конфигурационном файле действительная логика программирования,
необходимая для завершения хоста, чрезвычайно проста. Когда исполняемая программа
стартует, создается экземпляр типа ServiceHost, которому сообщается служба WCF,
отвечающая за хостинг. Во время выполнения этот объект автоматически читает данные
из контекста элемента <system.serviceModel> файла *.config хоста для определения
правильного адреса, привязки и контракта, и создает все необходимые механизмы:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Console Based WCF Host *****");
using (ServiceHost ServiceHost = new ServiceHost(typeof(MagicEightBallService)))
{
// Открыть хост и начать прослушивание входных сообщений.
ServiceHost.Open ()/
// Оставить службу в действии до тех пор, пока не будет нажата клавиша <Enter>.
Console.WriteLine("The service is ready.");
Console.WriteLine("Press the Enter key to terminate service.");
Console.ReadLine();
}
}
После запуска этого приложения вы обнаружите, что хост находится в памяти и
готов к приему входящих запросов от удаленных клиентов.
Указание базового адреса
В настоящее время ServiceHost создается с использованием конструктора,
который требует только информацию о служебном типе. Однако в качестве аргумента
конструктора также можно передать массив элементов типа System.Uri, чтобы
представить коллекцию адресов, для которых доступна данная служба. В настоящий момент
адрес обнаруживается через файл *. с on fig; однако если обновить контекст using
следующим образом:
using (ServiceHost ServiceHost = new ServiceHost(typeof(MagicEightBallService),
new Uri[]{new Uri("http://localhost:8080/MagicEightBallService")}))
{
}
Глава 25. Введение в Windows Communication Foundation 931
то конечную точку можно определить так:
<endpoint address =""
binding=,,basicHttpBinding"
contract="MagicEightBallServiceLib.IEightBall"/>
Разумеется, слишком большой объем жесткого кодирования внутри кодовой базы
хоста снижает гибкость, поэтому в текущем примере предполагается, что хост службы
создается просто передачей информации о типе, как делалось раньше:
using (ServiceHost serviceHost = new ServiceHost(typeof(MagicEightBallService)))
{
}
Один из несколько обескураживающих аспектов написания файлов *. con fig
связан с тем, что существует несколько способов конструирования дескрипторов XML, в
зависимости от объема жесткого кодирования (как это было в случае с необязательным
массивом Uri). Чтобы продемонстрировать другой способ написания файла *. con fig,
рассмотрим следующее изменение:
<?xml version="l.0" encoding=Mutf-8" ?>
<configuration>
<system.serviceModel>
<services>
<service name="MagicEightBallServiceLib.MagicEightBallService">
<!-- Адрес получен из <baseAddresses> -->
<endpoint address =""
binding="basicHttpBinding"
contract="MagicEightBallServiceLib.IEightBall"/>
<!— Перечислить все базовые адреса в выделенном разделе -->
<host>
<baseAddresses>
<add baseAddress ="http://localhost:8080/MagicEightBallService"/>
</baseAddresses>
</host>
</service>
</services>
</system.serviceModel>
</configuration>
В данном случае атрибут address элемента <endpoint> все еще пуст и, невзирая
на то, что массив Uri при создании ServiceHost в коде не указывался, приложение
работает, как и раньше, поскольку значение извлекается из контекста base Ad dresses.
Преимущество хранения базового адреса в подразделе <baseAddresses> раздела
<host> состоит в том, что другие части файла *.config также должны знать адрес
конечной точки службы. Поэтому вместо копирования и вставки значений адресов в
единственный файл *.config можно изолировать единственное значение, как было
показано выше.
На заметку! В последующем примере будет представлен графический инструмент
конфигурирования, позволяющий создавать конфигурационные файлы менее утомительным образом.
В любом случае, перед тем, как построить клиентское приложение для
взаимодействия со службой, давайте немного углубимся в изучение роли класса ServiceHost и
элемента <service.serviceModel>, а также роли служб обмена метаданными
(metadata exchange — МЕХ).
932 Часть V. Введение в библиотеки базовых классов .NET
Подробный анализ типа ServiceHost
Класс ServiceHost применяется для конфигурирования и представления службы
WCF из приложения-хоста. Однако имейте в виду, что этот тип будет использоваться
напрямую только при построении специальных сборок *.ехе, предназначенных для
хостинга служб. Если же для представления службы применяется IIS (или специфичная
для Vista и Windows 7 служба WAS), то объект ServiceHost создается автоматически.
Как уже было показано, этот тип требует полного описания службы, которое
получается динамически через установки конфигурации файла *. con fig хоста. Хотя это
происходит автоматически при создании объекта, можно вручную сконфигурировать
состояние объекта ServiceHost с помощью ряда его членов. Кроме Ореп() и Close ()
(которые взаимодействуют со службой в синхронной манере), есть и другие члены этого
класса (они перечислены в табл. 25.8).
Таблица 25.8. Избранные члены типа ServiceHost
Член
Назначение
Authorization
AddDefaultEndpoints ()
AddServiceEndpointO
BaseAddresses
BeginOpen()
BeginCloseO
CloseTimeout
Credentials
EndOpen()
EndCloseO
OpenTimeout
State
Это свойство получает уровень авторизации для размещенной
службы
Этот метод появился в .NET 4.0 и применяется для программного
конфигурирования хоста службы WCF, чтобы он использовал любое
количество готовых конечных точек, предоставленных платформой
Этот метод позволяет программно регистрировать конечную
точку для хоста
Это свойство получает список зарегистрированных базовых
адресов для текущей службы
Эти методы позволяют асинхронно открывать и закрывать объект
ServiceHost, используя стандартный синтаксис делегата .NET
Это свойство позволяет устанавливать и получать время,
отведенное службе на закрытие
Это свойство получает мандаты безопасности, используемые
текущей службой
Эти методы представляют собой асинхронные аналоги
BeginOpen() и BeginCloseO
Это свойство позволяет устанавливать и получать время,
отведенное службе на открытие
Это свойство получает значение, которое указывает текущее
состояние объекта коммуникации, представленное перечислением
CommunicationState (т.е. открыт, закрыт и создан)
Чтобы проиллюстрировать некоторые дополнительные аспекты ServiceHost,
модифицируйте класс Program, добавив новый статический метод, который выводит
различные аспекты текущего хоста:
static void DisplayHostlnfo(ServiceHost host)
{
Console.WriteLine();
Console.WriteLine ("***** Host Info *****");
Глава 25. Введение в Windows Communication Foundation 933
foreach (System.ServiceModel.Description.ServiceEndpoint s
in host.Description.Endpoints)
{
Console.WriteLine("Address: {0}", se.Address);
Console.WriteLine("Binding: {0}", se.Binding.Name);
Console.WriteLine("Contract: {0}", se.Contract.Name);
Console.WriteLine ();
}
Console.WriteLine(»**********************»);
Console.WriteLine();
}
Предположим, что этот новый метод вызывается в Main() после открытия хоста:
using (ServiceHost serviceHost = new ServiceHost(typeof(MagicEightBallService)))
{
// Открыть хост и начать прослушивание входящих сообщений.
serviceHost.Open();
DisplayHostlnfо(serviceHost);
В результате выводится статистика следующего вида:
***** Console Based WCF Host *****
***** Host Info *****
Address: http://localhost:8080/MagicEightBallService
Binding: BasicHttpBinding
(Contract: IEightBall
**********************
The service is ready.
Press the Enter key to terminate service.
Подробный анализ элемента <system.serviceModel>
Подобно любому XML-элементу, в <system.serviceModel> может определяться
набор подэлементов, каждый из которых может быть квалифицирован многочисленными
атрибутами. Хотя все детали набора возможных атрибутов описаны в документации
.NET Framework 4.0 SDK, ниже приведен скелет, перечисляющий критически важные
подэлементы:
<system.serviceModel>
<behaviors>
</behaviors>
<client>
</client>
<commonBehaviors>
</commonBehaviors>
<diagnostics>
</diagnostics>
<comContracts>
</comContracts>
<services>
</services>
<bindings>
</bindings>
</system.serviceModel>
На протяжении этой главы будут показаны и более экзотические конфигурационные
файлы; в табл. 25.9 приведены краткие описания подэлементов.
934 Часть V. Введение в библиотеки базовых классов .NET
Таблица 25.9. Подэлементы <service.serviceModel>
Подэлемент Назначение
behaviors WCF поддерживает различные поведения конечных точек и служб. По сути,
поведение позволяет точнее задавать функциональность хоста или клиента
bindings Этот элемент позволяет тонко настраивать каждую привязку WCF
(basicHttpBinding, netMsmqBinding и т.д.), а также указывать
любые специальные привязки, используемые хостом
client Этот элемент содержит список конечных точек, используемых клиентом
для подключения к службе. Очевидно, что это не слишком полезно в
файле *.config хоста
comContracts Этот элемент определяет контракты СОМ, обеспечивающие возможность
взаимодействия WCF и СОМ
commonBehaviors Этот элемент может устанавливаться только внутри файла machine. conf ig.
Он применяется для определения всех поведений, используемых каждой
службой WCF на данной машине
diagnostics Этот элемент содержит установки для средств диагностики WCF
Пользователь может включать или отключать трассировку, счетчики
производительности и поставщика WMI, а также добавлять специальные
фильтры сообщений
services Этот элемент содержит коллекцию служб WCF, представленных хостом
Включение обмена метаданными
Вспомните, что клиентское приложение WCF взаимодействует со службой WCF
через промежуточный прокси. Хотя вполне можно написать код прокси вручную, это было
бы довольно утомительно и чревато ошибками. В идеале должен использоваться
инструмент для генерации необходимого рутинного кода (включая файлы *. con fig
клиентской стороны). К счастью, в .NET Framework 4.0 SDK имеется инструмент
командной строки (svcutil.exe), предназначенный именно для этих целей. К тому же Visual
Studio 2010 предлагает аналогичную функциональность через пункт меню Projects Add
Service Reference (ПроектеДобавить ссылку на службу).
Чтобы эти инструменты генерировали необходимый код прокси и файл *. con fig,
они должны иметь возможность исследовать формат интерфейсов службы WCF и любых
определенных контрактов данных (т.е. имена методов и типы параметров).
Обмен метаданными (metadata exchange — МЕХ) — это поведение службы WCF,
которое может использоваться для тонкой настройки способа обработки службы
исполняющей средой WCF Просто говоря, каждый элемент <behavior> может определять
набор действий, на которые данная служба может подписываться. WCF
предоставляет многочисленные поведения в готовом виде, к тому же можно строить собственные
поведения.
Поведение МЕХ (которое по умолчанию отключено) перехватит любые запросы
метаданных, отправленные через HTTP GET. Чтобы позволить svcutil.exe или Visual
Studio 2010 автоматизировать создание необходимого прокси клиентской стороны и
файла *.config, понадобится включить МЕХ.
Включение МЕХ осуществляется в файле *. с on fig хоста установкой необходимой
настройки (или написанием соответствующего кода С#). Во-первых, необходимо
добавить новый элемент <endpoint> конкретно для МЕХ. Во-вторых, потребуется опреде-
Глава 25. Введение в Windows Communication Foundation 935
лить поведение WCF для разрешения доступа HTTP GET. В-третьих, нужно
ассоциировать это поведение по имени со службой с помощью атрибута behaviorConfiguration
в открывающем элементе <service>. И, наконец, в-четвертых, понадобится добавить
элемент <host> для определения базового класса этой службы (МЕХ будет искать здесь
местоположение описываемых типов).
На заметку! Финальный шаг может быть опущен, если вы передаете объект System.Uri для
представления базового адреса в виде параметра конструктору SeviceHost.
Взгляните на следующий обновленный файл *. con fig хоста, который создает
специальный элемент <behavior> (по имени EightBallServiceMEXBehavior), ассоциируемый
со службой через атрибут behaviorConfiguration внутри определения <service>:
<?xml version="l. 0м encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<services>
<service name="MagicEightBa11ServiceLib.MagicEightBa11Service"
behaviorConfiguration = "EightBallServiceMEXBehavior">
<endpoint address =""
binding="basicHttpBinding"
contract="MagicEightBallServiceLib.IEightBall" />
<!— Включить конечную точку МЕХ -->
<endpoint address="mex"
binding="mexHttpBinding"
contract="IMetadataExchange" />
<!— Это необходимо добавить, чтобы МЕХ знал адрес нашей службы -->
<host>
<baseAddresses>
<add baseAddress ="http://localhost:8080/MagicEightBallService" />
</baseAddresses>
</host>
</service>
</services>
<!— Определение поведения для МЕХ -->
<behaviors>
<serviceBehaviors>
<behavior name="EightBallServiceMEXBehavior" >
<serviceMetadata httpGetEnabled="true" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
Теперь можно перезапустить службу и увидеть описание метаданных в веб-браузере.
Для этого при запущенном хосте просто введите следующий URL в строке адреса:
http://localhost:8080/MagicEightBallService
На домашней странице службы WCF (рис. 25.6) можно получить базовую
информацию о том, как программно взаимодействовать с данной службой. Щелчок на гипер-
ссылке в верхней части страницы позволяет просмотреть контракт WSDL. Вспомните,
что язык описания веб-служб (Web Service Description Language — WSDL) — это
грамматика, описывающая структуру веб-служб в заданной конечной точке.
936 Часть V. Введение в библиотеки базовых классов .NET
£ Magicl
rvice - Windows Inter
та
Uj It l-.ttt localhost .: !»у:Е ■■•? - - ~
-|til*t|x|[b^
gf Favorites £ MagicEightBallService Service
*| " 0 " □ # " Page» Safety* Tools» 0»
MagicEightBallService Service
You have created a service.
To test this service, you will need to create a client and use it to call the service. You can do this using the svcutil.exe tool from the command line with the
following syntax:
svcutil.exe ftttp://localhoat;S0g0/HaqicExgatBall5ervice?wsdr'
This will generate a configuration file and a code file that contains the client class, Add the two files to your client application and use the generated client class
Щ to call the Service. For example:
#
class lest , ■*
i
static void Main О
<
EigbtBallClxenr client - new EightEallClient();
// Dae the 'client' variable to call operations on the iimcc.
// Always close the client.
client.Close(); '
ф Internet | Protected Mode Off
*i * «uoo% "
Рис. 25.6. Просмотр метаданных с использованием МЕХ
Хост теперь представляет две различные конечные точки (одна для службы и одна
для МЕХ), поэтому консольный вывод хоста будет выглядеть следующим образом:
***** Console Based WCF Host *****
***** Host Info *****
Address: http://localhost:8080/MagicEightBallService
Binding: BasicHttpBinding
Contract: IEightBall
Address: http://localhost:8 08 0/MagicEightBallService/mex
Binding: MetadataExchangeHttpBinding
Contract: IMetadataExchange
••••••••••••••••••••••
The service is ready.
Исходный код. Проект MagicEightBallServiceHost доступен в подкаталоге MagicEight
BallServiceHTTP каталога Chapter 25.
Построение клиентского приложения WCF
Теперь, имея готовый хост, последняя задача заключается в построении
фрагмента программного обеспечения для взаимодействия с этим служебным типом WCF. Хотя
можно избрать длинный путь и построить всю необходимую инфраструктуру вручную
(осуществимая, но трудоемкая задача), в .NET Framework 4.0 SDK предлагается
несколько подходов для быстрой генерации прокси клиентской стороны. Для начала создайте
новое консольное приложение по имени MagicEightBallServiceClient.
Глава 25. Введение в Windows Communication Foundation 937
Генерация кода прокси с использованием svcutil.exe
Первый способ построения прокси клиентской стороны предусматривает
использование инструмента командной строки svcutil.exe. С его помощью можно
генерировать новый файл на языке С#, представляющий код прокси, а также
конфигурационный файл клиентской стороны. Для этого укажите в первом параметре конечную точку
службы. Флаг /out: используется для определения имени файла *.cs, содержащего код
прокси, а флаг /conf ig: позволяет указать имя генерируемого файла *.conf ig
клиентской стороны.
Предполагая, что служба запущена, следующий набор параметров, переданный
svcutil.exe, приведет к генерации двух новых файлов в рабочем каталоге (вся команда
с параметрами должна вводиться в одной строке внутри окна командной строки Visual
Studio 2010):
svcutil http://localhost:8080/MagicEightBallService
/out:myProxy.cs /config-.app. config
Открыв файл myProxy.cs, вы найдете там представление интерфейса IEightBall
клиентской стороны, а также новый класс по имени EightBallClient, который и
является классом прокси. Этот класс унаследован от обобщенного класса System.
ServiceModel.ClientBase<T>, где Т — зарегистрированный интерфейс службы.
В дополнение к ряду специальных конструкторов, каждый метод прокси (который
основан на исходных методах интерфейса) будет реализован для использования
унаследованного свойства Channels с целью вызова корректного метода службы. Ниже
показан частичный код типа прокси:
[System. Diagnostics . DebuggerStepThroughAttnbute () ]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", .0.0.0")]
public partial class EightBallClient :
System.ServiceModel.ClientBase<IEightBall>, IEightBall
{
public string ObtainAnswerToQuestion(string userQuestion)
{
return base.Channel.ObtainAnswerToQuestion(userQuestion);
}
}
При создании экземпляра типа прокси в клиентском приложении базовый класс
установит соединение с конечной точкой, используя установки, указанные в
конфигурационном файле приложения клиентской стороны. Во многом подобно конфигурационному
файлу серверной стороны, сгенерированный файл App.conf ig стороны клиента
содержит элемент <endpoint> и детали, которые касаются привязки basicHttpBinding,
используемой для взаимодействия со службой.
Кроме того, там имеется следующий элемент <client>, который устанавливает ABC
с точки зрения клиента:
<client>
<endpoint
address="http://localhost:8080/MagicEightBallService"
binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IEightBall"
contract="ServiceReference.IEightBall" name="BasicHttpBinding_IEightBall" />
</client>
Теперь можно было бы включить эти два файла в проект клиента (вместе со
ссылкой на сборку System.ServiceModel.dll) и применять тип прокси для коммуникаций
с удаленной службой WCF. Однако воспользуемся другим подходом и посмотрим, как
938 Часть V. Введение в библиотеки базовых классов .NET
Visual Studio может помочь в дальнейшей автоматизации создания файлов прокси
клиентской стороны.
Генерация кода прокси с использованием Visual Studio 2010
Подобно любому хорошему инструменту командной строки, в svcutil.exe
предусмотрено огромное количество опций, которые можно использовать для управления
процессом генерации прокси. Если же расширенные опции не нужны, те*же два
файла можно сгенерировать в IDE-среде Visual Studio 2010. Просто выберите пункт Add
Service Reference (Добавить ссылку на службу) в меню Project (Проект).
После выбора этого пункта меню будет предложено ввести URI службы. Щелкните на
кнопке Go (Перейти), чтобы увидеть описание службы (рис. 25.7).
■и
То sec
То see a list of available services on • specific server,
services, click Discover.
http://1ocalhoste080/MagicEightBallService
Services: flperatiorw:
j, 1 services) found at address 'rrttp://localhost80eO/MagicEightBallSer/ice
* ® Ш WagicEightBallService
.>* IEightBall
♦ObtainAnswerToQuestion
Namespace
ServiceReference
Advanced-
PMC. 25.7. Генерация файлов прокси с использованием Visual Studio 2010
Помимо создания и вставки файлов прокси в текущий проект, этот инструмент
автоматически установит ссылки на сборки WCF. В соотве гствии с соглашением об
именовании, класс прокси определен в пространстве имен ServiceReference, вложенном
в клиентское пространство имен (во избежание возможных конфликтов имен). Ниже
приведен полный код клиента:
// Нахождение прокси.
using MagicEightBallServiceClient.ServiceReferencel;
namespace MagicEightBallServiceClient
{
class Program
static void Main(string[] args)
{
Console.WnteLineC***** Ask the Magic 8 Ball *****\n");
using (EightBallClient ball = new EightBallClient ())
{
Console.Write("Your question: " ) ;
string question = Console.ReadLine();
string answer =
ball.ObtainAnswerToQuestion(question);
Глава 25. Введение в Windows Communication Foundation 939
Console.WriteLine("8-Ball says: {0 } ", answer);
}
Console.ReadLine () ;
}
}
}
Предполагая, что хост WCF запущен, можно выполнить программу клиента. Далее
представлен возможный вывод:
***** Ask the Magic 8 Ball *****
Your question: Will I get this book done soon?
8-Ball says: No
Press any key to continue . . .
Исходный код. Проект MagicEightBallServiceClient доступен в подкаталоге
MagicEightBallServiceHTTP каталога Chapter 25.
Конфигурирование привязки на основе TCP
К этому моменту приложения хоста и клиента сконфигурированы на использование
простейшей из привязок на основе HTTP — basicHttpBinding. Вспомните, что
преимущество переноса настроек в конфигурационные файлы связано с возможностью менять
внутренние механизмы в декларативной манере и предоставлять множество привязок
для одной и той же службы.
Для целей демонстрации давайте поставим небольшой эксперимент Создайте новую
папку на диске С: (или где был сохранен код) по имени EightBallTCP и внутри него два
подкаталога с именами Host и Client.
Затем в проводнике Windows перейдите в папку bin\Debug проекта хоста и
скопируйте MagicEightBallServiceHost.exe, MagicEightBallServiceHost.exe.config
и MagicEightBallServiceLib.dll в папку C:\EightBallTCP\Host. Откройте файл
*. con fig в простом текстовом редакторе и модифицируйте его содержимое следующим
образом:
<?xml version="l.О" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<services>
<service name="MagicEightBallServiceLib.MagicEightBallService">
<endpoint address =""
binding="netTcpBinding11
contract="MagicEightBallServiceLib.IEightBall"/>
<host>
<baseAddresses>
<add baseAddress ="net.tcp://localhost:80 90/MagicEightBallService"/>
</baseAddresses>
</host>
</service>
</services>
</system.serviceModel>
</configuration>
По сути, из файла *. con fig удалены все установки МЕХ (поскольку уже построен
прокси) и установлено использование типа NetTcpBinding через уникальный порт.
Теперь запустите приложение двойным щелчком на его файле *.ехе. Если все сделано
правильно, должен появиться вывод, показанный ниже:
940 Часть V. Введение в библиотеки базовых классов .NET
***** Console Based WCF Host *****
***** Host Info *****
Address: net.tcp://localhost:80 90/MagicEightBallService
Binding: NetTcpBinding
Contract: IEightBall
**********************
The service is ready.
Press the Enter key to terminate service.
Для завершения этого теста скопируйте файлы MagicEightBallServiceClient.exe
и MagicEightBallServiceClient.exe.config из папки bin\Debug клиентского
приложения в папку C:\EightBallTCP\Client. Обновите конфигурационный файл
следующим образом:
<?xml version="l. 0" encoding="utf-811 ?>
<configuration>
<system.serviceModel>
<client>
<endpoint address="net.tcp://localhost:8090/MagicEightBallService"
binding="netTcpBinding"
contract="ServiceReferencel.IEightBall"
name="netTcpBinding_IEightBall11 />
</client>
</system. serviceMod-el>
</configuration>
Этот конфигурационный файл клиентской стороны значительно проще файла,
создаваемого генератором прокси Visual Studio. Обратите внимание, что существующий
элемент <bindings> полностью удален. Изначально файл *.config содержал элемент
<bindings> с подэлементом <basicHttpBinding>, который определял множество
деталей установок привязки клиента (таймауты и т.п.).
В действительности для рассматриваемого примера эти детали никогда не
понадобятся, поскольку мы автоматически получим значения по умолчанию лежащего в
основе объекта BasicHttpBinding. При необходимости можно было бы модифицировать
существующий элемент <bindings>, определив детали подэлемента <netTcpBinding>,
но это совершенно не обязательно, если устраивают значения по умолчанию объекта
NetTcpBinding.
В любом случае, теперь вы должны быть готовы запустить клиентское приложение,
и если хост все еще работает в фоновом режиме, вы сможете перемещать данные между
сборками по протоколу TCP.
Исходный код. Проект MagicEightBallTCP доступен в подкаталоге Chapter 25.
Упрощение конфигурационных настроек в WCF 4.0
Работая над первым примером этой главы, вы могли заметить, что логика
конфигурации хостинга довольно громоздка. Например, файл *. config (для начальной
базовой привязки HTTP) должен определять элемент <endpoint> службы, второй элемент
<endpoint> для МЕХ, элемент <baseAddresses> (необязательный) для сокращения
избыточных URI, а затем еще раздел <behaviors> для определения характеристик
обмена метаданными во время выполнения.
По правде говоря, изучение правил написания файлов *.config может оказаться
довольно трудным при построении служб WCF. Чтобы еще более усложнить картину,
значительное количество служб WCF склонны требовать одинаковых базовых установок
в конфигурационном файле хоста. Например, если создается совершенно новая служ-
Глава 25. Введение в Windows Communication Foundation 941
ба WCF и совершенно новый хост, и нужно представить эту службу, используя элемент
<basicHttpBinding> с поддержкой МЕХ, необходимый файл *.conf ig будет выглядеть
практически идентично созданному ранее.
К счастью, в .NET 4.0 API-интерфейс Windows Communication Foundation включает
ряд упрощений, в числе которых установки по умолчанию (и прочие сокращения),
облегчающие процесс построения конфигурации хоста.
Конечные точки по умолчанию в WCF 4.0
Когда в .NET 3.5 вызывался метод Ореп() на объекте ServiceHost, а в
конфигурационном файле еще не было определено ни одного элемента <endpoint>,
исполняющая среда генерировала исключение. Тот же результат получался при вызове метода
AddServiceEndpoint () в коде для указания конечной точки. В версии .NET 4.0 каждая
служба WCF автоматически получает конечные точки по умолчанию, которые
фиксируют общепринятые детали конфигурации для каждого поддерживаемого протокола.
Открыв файл machine.config для .NET 4.0, вы обнаружите в нем новый элемент по
имени <protocolMapping>. Этот элемент документирует, какие привязки WCF следует
использовать по умолчанию, если никаких привязок явно не указано:
<system.serviceModel>
<protocolMapping>
<add scheme="http" binding="basicHttpBinding"/>
<add scheme="net.tcp" binding="netTcpBinding"/>
<add scheme="net.pipe" binding="netNamedPipeBinding"/>
<add scheme="net.msmg" binding="netMsmgBinding"/>
</protocolMapping>
</system.serviceModel>
Все, что нужно для использования этих привязок по умолчанию — указать ^базовый
адрес в конфигурационном файле хоста. Чтобы увидеть это в действии, откройте проект
MagicEightBallServiceHost в Visual Studio. После этого модифицируйте файл *. config
хостинга, полностью удалив элемент <endpoint> для службы WCF и данные, связанные
с МЕХ. В результате конфигурационный файл должен выглядеть примерно так:
<configuration>
<system.serviceModel>
<services>
<service name="MagicEightBallServiceLib.MagicEightBallService" >
<host>
<baseAddresses>
<add baseAddress="http://localhost:8080/MagicEightBallService"/>
</baseAddresses>
</host>
</service>
</services>
</system.serviceModel>
</configuration
Поскольку в <baseAddress> указан действительный HTTP-адрес, хост
автоматически использует basicHttpBinding. Запустив хост снова, можно увидеть тот же вывод:
***** Console Based WCF Host *****
***** Host Info *****
Address: http://localhost:8080/MagicEightBallService
Binding: BasicHttpBinding
Contract: IEightBall
942 Часть V. Введение в библиотеки базовых классов .NET
**********************
The service is ready.
Press the Enter key to terminate service.
Пока еще не включены данные МЕХ; это будет сделано чуть ниже с использованием
другого упрощения .NET 4.0, которое называется конфигурациями поведения по
умолчанию. Однако сначала давайте разберемся, как предоставить одиночную службу WCF
с множеством привязок.
Предоставление одной службы WCF
с использованием множества привязок
Со времен своего первого выпуска WCF позволяет одному хосту предоставлять
службу WCF с несколькими конечными точками. Например, чтобы предоставить
MagicEightBallService с использованием привязок HTTP, TCP и именованного
канала, необходимо просто добавить новые конечные точки в конфигурационный файл.
После перезапуска хоста вся необходимая оснастка будет создана автоматически.
Это огромное преимущество по многим причинам. До WCF было трудно предоставить
одну службу с применением множества привязок, так как каждый тип привязки (т.е.
HTTP и TCP) имел собственную модель программирования. Тем не менее, возможность
разрешить вызывающему коду выбирать наиболее подходящую привязку чрезвычайно
удобна. Внутренние клиенты могут отдать предпочтение привязкам TCP, внешние
клиенты (находящиеся за брандмауэром компании) — использовать для доступа HTTP, в то
время как клиенты, находящиеся на той же машине выберут именованный канал.
До появления .NET 4.0 для этого в конфигурационном файле хоста нужно было
определять несколько элементов <endpoint> вручную. Также для каждого протокола
требовалось определять множество элементов <baseAddress>. Однако теперь можно просто
поместить в конфигурационный файл следующие данные:
<configuration>
<system.serviceModel>
<services>
<service name="MagicEightBallServiceLib.MagicEightBallService">
<host>
<baseAddresses>
<add baseAddress="http://localhost:8080/MagicEightBallService" />
<add baseAddress="net.tcp://localhost:8099/MagicEightBallService" />
</baseAddresses>
</host>
</service>
</services>
</system.serviceModel>
</configuration>
Скомпилировав проект (для обновления развернутого файла *.config) и
перезапустив хост, можно будет увидеть следующие данные конечной точки:
***** Console Based WCF Host *****
***** Host Info *****
Address: http://localhost:8080/MagicEightBallService
Binding: BasicHttpBinding
Contract: IeightBall
Address: net.tcp://localhost:8099/MagicEightBallService
Binding: NetTcpBinding
Contract: IEightBall
**********************
The service is ready.
Press the Enter key to terminate service.
Глава 25. Введение в Windows Communication Foundation 943
Теперь, когда служба WCF достижима из двух конечных точек, возникает вопрос:
как клиент сможет производить выбор между ними? При генерации прокси клиентской
стороны инструмент Add Service Reference назначит каждой представленной конечной
точке строковое имя в клиентском файле *.config. В коде можно передавать
корректное строковое имя конструктору прокси и быть уверенным, что будет выбрана
правильная привязка. Однако прежде чем сделать это, потребуется переустановить МЕХ для
модифицированного конфигурационного файла хоста и научиться настраивать
параметры привязки по умолчанию.
Изменение установок для привязки WCF
В случае указания ABC службы в коде С# (это рассматривается далее в главе) для
изменения параметров по умолчанию привязки WCF необходимо просто
модифицировать значения свойств объекта. Например, чтобы использовать BasicHttpBinding, но
изменить установки таймаута, можно написать следующий код:
void ConfigureBindinglnCode()
{
BasicHttpBinding binding = new BasicHttpBinding();
binding.OpenTimeout = TimeSpan.FromSecondsC0);
}
Настройки привязки всегда можно конфигурировать декларативно. Например, в
.NET 3.5 можно создавать конфигурационный файл хоста, в котором изменяется
свойство OpenTimeout класса BasicHttpBinding:
<configuration>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name = "myCustomHttpBinding"
OpenTimeout = 0:00:30" />
</basicHttpBinding>
</bindings>
<services>
<service name = "WcfMathService.MyCalc">
<endpoint address = "http://localhost:8080/MyCalc"
binding = "basicHttpBinding"
bindingConfiguration = "myCustomHttpBinding"
contract = "WcfMathService.IBasicMath" />
</service>
</services>
</system.serviceModel>
</configuration>
В результате получается конфигурационный файл для службы по имени
WcfMathService.MyCalc, которая поддерживает единственный интерфейс IBasicMath.
Обратите внимание, что раздел <bindings> позволяет определить именованный
элемент <binding>, который изменяет настройки для заданной привязки. Внутри
<endpoint> службы специфические настройки можно подключать с использованием
атрибута bindingConfiguration.
Такая конфигурация хостинга по-прежнему работает и в .NET 4.0; однако в случае
использования конечной точки по умолчанию подключить <binding> к <endpoint>
не удастся. К счастью, параметрами конечной точки по умолчанию можно управлять,
просто опустив атрибут name элемента <binding>. Например, в следующем фрагменте
кода изменяются некоторые свойства объектов BasicHttpBinding и NetTcpBinding,
используемые по умолчанию:
944 Часть V. Введение в библиотеки базовых классов .NET
<configuration>
<system.serviceModel>
<services>
<service name="MagicEightBallServiceLib.MagicEightBallService">
<host>
<baseAddresses>
<add baseAddress="http://localhost:8080/MagicEightBallService" />
<add baseAddress=
"net.tcp://localhost:8099/MagicEightBallService" />
</baseAddresses>
</host>
</service>
</services>
<bindings>
<basicHttpBinding>
<binding openTimeout = 0:00:30" />
</basicHttpBinding>
<netTcpBinding>
<binding closeTimeout=0:00:15"/>
</netTcpBinding>
</bindings>
</system.serviceModel>
</configuration>
Конфигурация поведения МЕХ по умолчанию в WCF 4.0
Инструмент генерации прокси должен обнаружить композицию службы во время
выполнения, прежде чем он сможет продолжить работу. В WCF такое обнаружение во
время выполнения разрешается включением МЕХ. В большинстве конфигурационных
файлов хоста МЕХ должно быть включено (по крайней мере, во время разработки); к
счастью, способ конфигурирования МЕХ редко изменяется, поэтому .NET 4.0
предлагается несколько удобных сокращений.
Наиболее полезное из них — готовая поддержка МЕХ. Не нужно добавлять конечную
точку МЕХ, определять именованное поведение службы МЕХ и затем подключать
именованную привязку к службе (как это делалось в HTTP-версии MagicEightBallServiceHost).
Вместо этого теперь можно просто применить следующий код:
<configuration>
<system.serviceModel>
<services>
<service name="MagicEightBallServiceLib.MagicEightBallService">
<host>
<baseAddresses>
<add baseAddress="http://localhost:8080/MagicEightBallService" />
<add baseAddress=
"net.tcp://localhost:8099/MagicEightBallService" />
</baseAddresses>
</host>
</service>
</services>
<bindings>
<basicHttpBinding>
<binding openTimeout = 0:00:30" />
</basicHttpBinding>
<netTcpBinding>
<binding closeTimpout=0:00:15"/>
</netTcpBinding>
</bindings>
Глава 25. Введение в Windows Communication Foundation 945
<behaviors>
<serviceBehaviors>
<behavior>
<•-- Для получения МЕХ по умолчанию не назначайте
имя элементу <serviceMetadata> -->
<serviceMetadata httpGetEnabled="true" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
Трюк состоит в том, что элемент <serviceMetadata> не имеет атрибута name
(также обратите внимание, что элементу <service> больше не нужен атрибут behavior
Configuration). После этих корректировок получается поддержка МЕХ во время
выполнения. Чтобы удостовериться в этом, запустите хост (после компиляции и обновления
конфигурационного файла) и введите следующий URL в браузере:
http://localhost:808O/MagicEightBallService
После этого можно щелкнуть на ссылке wsdl в верхней части веб-страницы и
просмотреть WSDL-описание службы (см. рис. 25.6). В консольном окне хоста в выводе
будут отсутствовать данные о конечной точке МЕХ, потому что она не определялась явно
для IMetadataExchange в конфигурационном файле. Тем не менее, МЕХ включен и
можно приступать к построению клиентских прокси.
Обновление клиентского прокси и выбор привязки
Предполагая, что обновленный хост скомпилирован и выполняется в фоновом
режиме, теперь необходимо открыть клиентское приложение и обновить текущую ссылку
на службу. Начните с открытия папки Service Refreshes (Ссылки на службу) в Solution
Explorer. Затем щелкните правой кнопкой мыши на элементе ServiceReference и
выберите в контекстном меню пункт Update Service Reference (Обновить ссылку на
службу), как показано на рис. 25.8,
[Solution Explorer
1 £9 Solut'or» 'MagicEightBaltServiceClient' A project)
j 3 MagkEightBalServiceCEefit
r» ,;M Properties
> ^| References
л \^Л Service References
#j ServiceReferenc
jj> app.config
cJj| Program.cs
Update Service Reference
Configure Service Reference...
View in Object Browser
A Cut
-J Copy
Jj Paste
X Delete
Rename
!Ql Properties
Я -'^ Solution Explorer H
6
Ctrl+X
Ctrl+C
Ctri+V
Del
Alt+ Enter
*■ nx]
Рис. 25.8. Обновления прокси и файла *.config клиентской стороны
946 Часть V. Введение в библиотеки базовых классов .NET
После этого в файле *.config появятся на выбор две привязки: одна для HTTP и
другая — для TCP. Каждой привязке назначено подходящее имя. Ниже приведен
частичный листинг обновленного конфигурационного файла:
<configuration>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_IEightBall11 . . . />
</basicHttpBinding>
<netTcpBinding>
<bmding name="NetTcpBinding_IEightBall11 . . . />
</netTcpBinding>
</bindings>
</system.serviceModel>
</configuration>
Клиент может использовать эти имена при создании прокси-объекта для выбора
желаемой привязки. Таким образом, если клиент предпочитает применять TCP, можно
изменить код С# клиентской стороны следующим образом:
static void Main(string [ ] args)
{
Console.WriteLine ("***** Ask the Magic 8 Ball *****\n");
using (EightBallClient ball = new EightBallClient(llNetTcpBinding_IEightBall") )
{
}
Console.ReadLine();
}
Если же клиент вместо этого будет использовать привязку HTTP, можно написать
так:
using (EightBallClient ball = new
EightBallClient (,,BasicHttpBinding_IEightBall" ) )
{
}"
На этом текущий пример, который продемонстрировал ряд полезных средств WCF
4.0, завершен. Данные средства упрощают написание конфигурационных файлов
хостинга. Далее будет показано, как использовать шаблон проекта WCF Service Library
(Библиотека служб WCF).
Исходный код. Проект MagicEightBallServiceHTTPDefaultBindings доступен в
подкаталоге Chapter 25.
Использование шаблона проекта
WCF Service Library
Прежде чем строить более экзотическую службу WCF, которая будет
взаимодействовать с базой данных AutoLot, созданной в главе 21, в следующем примере
иллюстрируется ряд важных тем, включая преимущества от использования шаблона проекта WCF
Service Library, приложения WCF Test Client, редактора конфигурации WCF, хостинга
служб WCF внутри службы Windows и асинхронных клиентских вызовов. Чтобы
сосредоточить все внимание на новых концепциях, эта служба WCF также будет умышленно
простой.
Глава 25. Введение в Windows Communication Foundation 947
Построение простой математической службы
Для начала создайте новый проект WCF Service Library под названием MathServiceLibrary,
выбрав соответствующую опцию в узле WCF диалогового окна New Project (рис. 25.2).
Затем измените имя начального файла IServicel.cs на IBasicMath.cs. После
этого удалите весь код внутри пространства имен MathServiceLibrary и замените его
следующим:
[ServiceContract(Namespace="http://MyCompany.com")]
public interface IBasicMath
{
[OperationContract]
int Add(int x, int y) ;
}
Измените имя файла Servicel.cs на MathService.es, удалите весь код внутри
пространства имен MathServiceLibrary и реализуйте контракт службы, как показано
ниже:
public class MathService : IBasicMath
{
public int Add(int x, int y)
{
// Эмулировать длительный запрос.
System.Threading.Thread.SleepE000);
return x + y;
}
}
Наконец, откройте файл App.config и замените все вхождения IServicel на
IBasicMath, а также все вхождения Servicel на MathService. Обратите внимание, что
этот файл *.conf ig уже включает поддержку МЕХ и по умолчанию использует протокол
WsHttpBindintg.
Тестирование службы WCF с помощью WcfTestClient.exe
Одно из преимуществ применения проекта WCF Service Library состоит в том, что
при отладке или запуске библиотеки он читает установки из файла *.configH
использует их для загрузки приложения WCF Test Client (WcfTestClient.exe). Это
приложение с графическим интерфейсом позволяет протестировать каждый член интерфейса
службы по мере ее построения, вместо того, чтобы вручную строить хост/клиент, как
это делалось ранее, просто для целей тестирования.
На рис. 25.9 показана тестовая среда для MathService. Обратите внимание, что
двойной щелчок на методе интерфейса позволяет указать входные параметры и
вызвать метод.
Эта утилита работает в готовом виде после создания проекта WCF Service Library,
однако имейте в виду, что данный инструмент можно применять для тестирования
любой службы WCF, запустив его в командной строке и указав конечную точку МЕХ.
Например, если запустить приложение MagicEightBallServiceHost.exe, можно
ввести следующую команду в окне командной сроки Visual Studio 2010:
wcftestclient http://localhost:8 080/MagicEightBallService
После этого можно вызвать ObtainAnswerToQuestionO аналогичным образом.
948 Часть V. Введение в библиотеки базовых классов .NET
File Tools Help
В Щь My Service Projects
S $j| nttp:/y1ocelhost:80aD/Ma*hService/m«!
Щ 5° IBascMath (WSHttpBndmgJBeKJ
<rj} Config He
IL_
Value
22
E5
Type
System.N32
System Ы32
О Start a new proxy
Invoke j
Name
fretum)
Type
System.lnt32
Formatted |XML |
Service invocation completed.
Рис. 25.9. Тестирование службы WCF с использованием WcfTestClient.exe
Изменение конфигурационных файлов
С помощью SvcConfigEditor.exe
Другое преимущество применения проекта WCF Service Library состоит в том, что
щелчком правой кнопкой мыши на файле App.config внутри Solution Explorer можно
активизировать графический редактор конфигурирования службы (Service Configuration
Editor), SvcConfigEditor.exe (рис. 25.10). Та же техника может применяться из
клиентского приложения, которое ссылается на службу WCF.
1 Solution Explorer
-j j3 Л
1 Solution MathSer
л «$9 MathServkeL
^1 Properties
^ References
_^ -^pp.confic
jj JBasicMath
£) MathServic
1 ^5 Solution Explorer I
iceLibrary' A project]
brary
J Open
Open With.»
^ Edit WCF Configuration
Exclude From Project
A Cut
-J| Copy
X Delete
Rename
u Properties
- r
Ctrl+X
Ctrl* С
Dei
Aft* Enter
">]
Рис. 25.10. Запуск графического редактора файлов *. con fig
Запустив этот инструмент, можно изменять данные в формате XML, используя
дружественный графический интерфейс. Существует много очевидных преимуществ от
применения подобных инструментов для сопровождения файлов *.config. Первое:
имеется уверенность, что сгенерированная разметка соответствует ожидаемому
формату и свободна от опечаток. Второе: это замечательный способ увидеть правильные
значения, которые могут быть присвоены каждому атрибуту. И третье: больше не
понадобится вручную вводить данные XML.
На рис. 25.11 показан внешний вид редактора Service Configuration Editor. По правде
говоря, описание всех интересных опций SvcConfigEditor.exe заняло бы отдельную
главу (интеграция с СОМ+, создание файлов *.conf ig и т.п.). Найдите время, чтобы изу-
Глава 25. Введение в Windows Communication Foundation 949
чить этот инструмент, пользуясь детальной справочной системой, которая вызывается
нажатием клавиши <F1>.
На заметку! Утилита SvcConfigEditor.exe позволяет редактировать (или создавать)
конфигурационные файлы, даже если не был выбран начальный проект WCF Service Library.
Запустите этот инструмент в окне командной строки Visual Studio 2010 и воспользуйтесь
пунктом меню File^Open (Файл^Открыть) для загрузки существующего файла *. con fig для
редактирования.
Конфигурировать MathService больше не нужно, поэтому можно перейти к задаче
построения специального хоста.
Рис. 25.11. Работа с редактором WCF Service Configuration Editor
Хостинг службы WCF в виде службы Windows
Хостинг службы WCF в консольном приложении (или внутри настольного
приложения с графическим интерфейсом) — не идеальный выбор для сервера
производственного уровня, учитывая, что хост всегда должен оставаться запущенным в фоновом режиме
и готовым к обслуживанию клиентов. Даже если свернуть приложение-хост в панели
задач Windows, все равно останется вероятность нечаянно закрыть его, тем самым
разорвав все соединения с клиентскими приложениями.
На заметку! Хотя и верно, что настольное приложение Windows не обязано отображать главное
окно, типичная программа *.ехе требует взаимодействия с пользователем для загрузки и
запуска. Служба Windows (описанная ниже) может быть сконфигурирована для запуска, даже
если ни один пользователь не зарегистрировался на рабочей станции (не вошел в систему).
I
Если вы строите "домашнее" приложение WCF, то еще одной альтернативой для
хостинга библиотеки службы WCF является развертывание ее в службе Windows.
Преимущество такого решения состоит в том, что служба Windows может быть
сконфигурирована для автоматического запуска вместе с загрузкой системы на целевой
машине. Другое преимущество связано с тем, что служба Windows работает
невидимо, в фоновом режиме (в отличие от консольного приложения), и не требует участия
пользователя.
950 Часть V. Введение в библиотеки базовых классов .NET
Чтобы проиллюстрировать построение такого хоста, начните с создания проекта
службы Windows по имени MathWindowsServiceHost (рис. 25.12). Переименуйте
начальный файл Servicel.cs на MathService.cs, используя Solution Explorer.
ЛеЬ
Office
Cloud Service
Reporting
Sitvertight
kd* WPF Custom Control Library Visual C*
t C**j Empty Project Visual C*
!g#] Windows Service Visual C#
■*crt WPF User Control Library Visual C#
шс0\ Windows Forms Control Libr... Visual C*
Per user extensions are currently not allowed to load. 1п«Ые кал4игд. (' > м ноу extenwom
Name MathWindowsServiceHost
Location: CAMyCode '
Solution- Create new solution *
Solution name
Type: Visual C*
A project for creating Windows Services
Create directory for solution
Add to source control
Рис. 25.12. Создание службы Windows для хостинга службы WCF
Спецификация ABC в коде
Теперь, предполагая, что ссылки на сборки MathServiceLibrary.dll и System.
ServiceModel.dll установлены, все, что осталось сделать — это использовать тип
ServiceHost внутри методов OnStartO и OnStopO типа службы Windows. Откройте
файл кода класса службы-хоста (щелчком правой кнопкой мыши в визуальном
конструкторе и выбором в контекстном меню пункта View Code (Просмотреть код)) и добавьте
следующий код:
//Не забудьте импортировать пространства имен:
using MathServiceLibrary;
using System.ServiceModel;
namespace MathWindowsServiceHost
{
public partial class MathWinService: ServiceBase
{
// Переменная-член типа ServiceHost.
private ServiceHost myHost;
public MathWinService ()
{
InitializeComponent();
}
protected override void OnStart (string[] args)
{
// Проверить для подстраховки.
if (myHost '= null)
{
myHost.Close () ;
myHost = null;
}
// Создать хост.
myHost = new ServiceHost(typeof(MathService) ) ;
Глава 25. Введение в Windows Communication Foundation 951
// Указать ABC в коде.
Uri address = new Uri ("http://localhost:8080/MathServiceLibrary");
WSHttpBinding binding = new WSHttpBinding();
Type contract = typeof (IBasicMath);
// Добавить эту конечную точку.
myHost.AddServiceEndpoint(contract, binding, address);
// Открыть хост.
myHost.Open ();
}
protected override void OnStopO
{
// Остановить хост.
if(myHost '= null)
myHost.Close ();
}
}
}
Хотя ничто не мешает применять конфигурационный файл при построении хоста
для службы WCF на основе службы Windows, здесь (для разнообразия) вместо
использования файла *.conf ig конечная точка устанавливается программно с помощью классов
Uri, WSHttpBinding и Туре. После создания всех аспектов ABC хост программно
информируется о них вызовом AddServiceEndpoint().
Если нужно информировать исполняющую среду о том, что необходимо получить
доступ к каждой из привязок конечных точек по умолчанию, описанных в
конфигурационном файле .NET 4.0 machine.config, можно упростить программную логику,
указав базовый адрес при вызове конструктора ServiceHost. В этом случае не потребуется
специфицировать ABC в коде вручную или вызывать AddServiceEndPointO, а просто
вызвать AddDefaultEndPoints(). Взгляните на следующее изменение кода:
protected override void OnStart(string [ ] args)
{
if (myHost != null)
{
myHost.Close ();
}
// Создать хост и указать URL для привязки HTTP.
myHost = new ServiceHost(typeof(MathService),
new Uri("http://localhost:8080/MathServiceLibrary"));
// Выбрать конечные точки по умолчанию.
myHost.AddDefaultEndpoints();
// Открыть хост.
myHost.Open();
}
Включение МЕХ
Хотя включить МЕХ можно и программно, сделаем это в конфигурационном файле
WCF 4.0. Добавьте новый файл Арр.config к проекту службы Windows, внеся в него
следующие настройки МЕХ:
<?xml version="l .0" encoding="utf-811 ?>
<configuration>
<system.serviceModel>
<services>
<service name=llMathServiceLibrary.MathServicell>
</service>
</services>
952 Часть V. Введение в библиотеки базовых классов .NET
<behaviors>
<serviceBehaviors>
<behavior>
<serviceMetadata httpGetEnabled=lltrue" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
Создание программы установки для службы Windows
Чтобы зарегистрировать службу Windows в операционной системе, нужно добавить
к проекту программу установки, которая будет содержать необходимый код для
регистрации службы. Для этого щелкните правой кнопкой мыши на поверхности
визуального конструктора службы Windows и выберите в контекстном меню пункт Add Installer
(Добавить программу установки), как показано на рис. 25.13.
MsthWtnService.es (Oesion] хЩ
gj View Code
То add components
Properties window to Line Up Icons
У°игс Show Large Icons
Add Installer
J* Properties
РУГИ
F7
CtrUV
ootbox and use the
Tods and events for
iew.
Рис. 25.13. Добавление программы установки для службы Windows
В результате на поверхность визуального конструктора добавятся два новых
компонента. Первый компонент (именуемый по умолчанию serviceProcessInstallerl)
представляет тип, который может установить новую службу Windows на целевой
машине. Выберите этот тип в визуальном конструкторе и воспользуйтесь окном Properties
(Свойства) для установки свойства Account в LocalSystem (рис. 25.14).
Второй компонент (под названием servicelnstaller) представляет тип, который
устанавливает конкретную службу Windows. В окне Properties измените значение свойства
ServiceName на MathService (это дружественное отображаемое имя для
зарегистрированной службы Windows), установите свойство StartType в Automatic и добавьте
дружественное описание зарегистрированной службы Windows в свойстве Description (рис. 25.15).
1 Properties
1 serviceProcessInstallerl System
'Пэте:
GenerateMember
HeJpTert
Modifiers
Parent
.ServicePrccess.ServicePrccessi
servic eProc esslnst aller 1
Ш LocalSystem
True
Private
Projectlnstafler
1 Account
1 Indicates the account type under which the service will run.
» nxl
nstaller » 1
Рис. 25.14. Служба Windows должна запускаться от имени учетной записи LocalSystem
Глава 25. Введение в Windows Communication Foundation 953
[Properties
1 servicelnstallerl System.Serv
: ;i J > U
(Name)
DelayedAutoStart
DisplayName
GenerateMember
HelpText
Modifiers
Parent
ServiceName
1 i> Ser.icesDependedOn
StartType
1 Description
1 Indicates the service's descript
1 service!.
ceProces
» nxl
s.Ser. icelnstaller * 1
servicelnstallerl
False
H This is the math service!
MathService
True
Private
Projecllnstailer
MathService
Stnngfj Array
Automatic
on (a brief comment that explains the purpose of the
Рис. 25.15. Конфигурирование деталей, связанных с программой установки
После этого можно компилировать приложение.
Установка службы Windows
Служба Windows может быть установлена на машине-хосте с помощью
традиционной программы установки (такой как *.msi) или инструмента командной строки
installutil.exe. В окне командной строки Visual Studio 2010 перейдите в папку
bin\Debug проекта MathWindowsServiceHost и введите следующую команду:
installutil MathWindowsServiceHost.exe
Предполагая, что установка прошла успешно, можно щелкнуть на значке Services
(Службы) в папке Administrative Tools (Администрирование) панели управления. В
списке служб, упорядоченном по алфавиту, должно присутствовать дружественное имя
службы MathService. Запустите эту службу на локальной машине, щелкнув на ссылке
Start (Запустить), как показано на рис. 25.16.
Services
F.te нсисп
Scr.ice; lc
View Help
В я | ► .■ ■ I
i
JMathServke
Description:
This is the math seroce!
\ Ertended /. Standard /
Nunc
£fc V Helper
:S|iPs*c Policy Agent
Si KtmRm for Distributed Tra...
-w. Link-Layer Topology Disco..
',. Media Center Extender Serv,
.,, Microsoft .NET Framework.
i* Microsoft .NET Framework .
5» Microsoft .NET Framework .
R| Microsoft .NET Framework.
S4 Microsoft Г5СЯ Initiator Ser..
: J, Microsoft Office Diagnostic
Щ Microsoft Software Shade.
Descnpticn
Provides tunnel connectivity..
Internet Protocol security (IPs.
Coordinates transaction» bet..
Creates a fJetwork Map, consi.
Allocs Media Center Extender.
Microsoft .NET Framework N..
Microsoft .NET Framework N..
Microsoft NET Framework N..
Microsoft .NET Framework N..
Manages Internet SCSI (iSCSD .
Run portions of Microsoft Off.
Manages software-based votu.
*~
Status
Started
H
Startup Type
Automatic
Manual
Manual
Manual
Disabled
Disabled
Disabled
Automatic iD...
Automatic (D...
Manual
Manual
Manual
ra
Log On As * 1
Local Syste...
Network S...
Network S...
Local Service !l| *|
Local Service
Local Syste... Щ
Local Syste... 1
Local S>ste...
Local Syste...
Local Syste...
Local Syste...
Local Syste.,.
Рис. 25.16. Служба Windows, исполняющая роль хоста для службы WCF
Теперь, когда служба установлена и работает, остается последний шаг — создание
клиентского приложения, которое будет ее использовать.
Исходный код. Проект MathWindowsServiceHost доступен в подкаталоге Chapter 25.
954 Часть V. Введение в библиотеки базовых классов .NET
Асинхронный вызов службы на стороне клиента
Создайте новый проект консольного приложения по имени MathClient и установите
ссылку на службу на работающую службу WCF, которая в настоящий момент развернута
в службе Windows, функционирующей в фоновом режиме, выбрав для этого пункт меню
Project^Add Service Reference (ПроектеДобавить ссылку на службу) в Visual Studio
(понадобится ввести URL в поле адреса). Однако не щелкайте пока на кнопке ОК. Обратите
внимание на кнопку Advanced (Дополнительно) в нижнем левом углу диалогового окна
Add Service Reference (Добавление ссылки на службу), как показано на рис. 25.17.
То see a list of available services on a specific server, enter a service URL and click Go. To browse for available
services, click Discover.
Address:
'ЯЯШШШШЯШшт
Services:
Go 1 I [oiscovtr :*1
0 3] MathService
5° IBaskMath
Select a service contract to view its operations.
1 serviced) found at address 'http://tocalbost80eO/MathServiceLibrary'.
Namespace:
iServkeReferencej
CZEZIi
J
Рис. 25.17. Ссылка на службу MathService и возможность
конфигурирования расширенных установок
Щелкните на этой кнопке, чтобы увидеть дополнительные установки конфигурации
прокси (рис. 25.18). Используя это диалоговое окно, можно генерировать код, который
позволит вызывать удаленные методы в асинхронном режиме, если отмечен флажок
Generate Asynchronous Operations (Генерировать асинхронные операции). Попробуйте
сделать это.
После этого код прокси будет содержать дополнительные методы, которые
позволяют вызывать каждый член контракта службы с использованием ожидаемого формата
асинхронного вызова Begin/End, описанного в главе 19. Ниже показана простая
реализация, в которой вместо строго типизированного делегата AsyncCallback применяется
лямбда-выражение.
using System;
using MathClient.ServiceReference;
using System.Threading;
namespace MathClient
{
class Program
static void Main(string[] args)
{
Console.WriteLine ("***** The Async Math Client *****\n");
Глава 25. Введение в Windows Communication Foundation 955
using (BasicMathClient proxy = new BasicMathClient ())
{
proxy.Open ();
// Складывать числа в асинхронном режиме с использованием лямбда-выражения.
IAsyncResult result = proxy.BeginAdd B, 3,
ar =>
{
Console.WriteLine( + 5 = {0}", proxy.EndAdd(ar));
}, null) ;
while (!result.IsCompleted)
{
Thread.Sleep B00);
Console.WriteLine("Client working...");
Console.ReadLine();
Исходный код. Проект MathClient доступен в подкаталоге Chapter 25.
Sen ice Reference Settings
1Д
Access tevel'for generated classes:
SI Generate asynchronous operations
Data Type
О Always generate message contracts
Collection type: ISystemArray
Dictionary collection type System.Collections.Genenc.Dictionary
V Reuse types in referenced assemblies
• Reuse types in all referenced assemblies
Reuse types in specified referenced assemblies:
O-Omscorlib
О-О System
□ 'OSystem.Core
□ ^OSystem.Data
Q *aSystem.Data.DataSetExtension5
О -О System.Runtime.Serialixation
В -Q System.ServiceModel
Compatibility '
Add a Web Reference instead of a Service Reference. This will generate code based on .N£T Framework 2.0
Web Services technology.
Add ЛеЬ Reference...
Рис. 25.18. Дополнительные опции конфигурации прокси клиентской стороны
Проектирование контрактов данных WCF
В последнем примере этой главы рассматривается конструирование контрактов
данных WCF. В ранее созданных службах WCF были определены очень простые методы,
оперирующие примитивными типами данных CLR. При использовании любого из типов
привязок HTTP (basicHttpBinding, wsHttpBinding и т.п.), входящие и исходящие про-
956 Часть V. Введение в библиотеки базовых классов .NET
стые типы данных автоматически форматируются в XML-элементы. Кстати говоря, если
применяется привязка на основе TCP (такая как netTcpBinding), то параметры и
возвращаемые значения простых типов данных передаются в компактном двоичном формате.
На заметку! Исполняющая среда WCF также автоматически кодирует любой тип, помеченный
атрибутом [Serializable]. Однако это не является предпочтительным способом определения
контрактов WCF и предназначено только для обратной совместимости.
Однако при определении контрактов служб, которые используют специальные
классы в качестве параметров или возвращаемых значений, эти типы должны быть
объявлены с применением контракта данных. Просто говоря, контракт данных — это тип,
оснащенный атрибутом [DataContract]. Соответственно, каждое поле, которое
планируется использовать как часть предполагаемого контракта, должно помечаться
атрибутом [DataMember].
На заметку! Поля контракта данных, не помеченные атрибутом [DataMember], не сериализуют-
ся исполняющей средой WCR
Теперь посмотрим, как конструировать контракты данных. Начните с создания новой
службы WCF, которая будет взаимодействовать с базой данных AutoLot, созданной в
главе 21. Эта финальная служба WCF будет создана на основе веб-ориентированного
шаблона проекта WCF Service. Вспомните, что служба WCF такого типа автоматически
помещается в виртуальный каталог IIS и функционирует подобно традиционной веб-службе XML
в .NET. Однажды разобравшись в композиции такой службы WCF, не должно возникать
проблем с переносом существующей службы WCF в новый виртуальный каталог IIS.
На заметку! В этом примере предполагается, что вы разбираетесь в структуре виртуального
каталога IIS (и в самом сервере IIS). Если это не так, обратитесь в главу 32.
Использование веб-ориентированного шаблона проекта WCF Service
Выбрав пункт меню File^New^Web Site (Файл^Создать^Веб-сайт), создайте новую
службу WCF по имени AutoLotWCFService, представленную следующим URI: http://
localhost/AutoLotWCFService (рис. 25.19). Убедитесь, что в раскрывающемся списке
Web location (Веб-расположение) выбран элемент HTTP.
^222222НИИНН
Installed Templates
Visual Basic
Visual С*
1
1
1
.NET Framework. 4 - Sort by. Default
4k ASP.NET Web Service
ty|j| Empty Web Site
*4| Silverlight 1Л Web Site
cjjjjl WCFSerwee N
«^ |.«K7.5mmtJ
Jjci] ASP.NET Reports ЯаГЯГ"*""'
Я||г Dynamic Data Lmq to SQL Web Site
Фг Dynamic Data Entities Web Site
' http localhost/AutoLotWCFServ.ee
Visual C#
Visual C#
Visual C*
Visual C#
Visual C#
Visual C*
'
' Type: Visual C#
A Web site for creating WCF se
- .„,„„.., •
■EZI
p
Cancel |
Рис. 25.19. Создание веб-ориентированной службы WCF
Глава 25. Введение в Windows Communication Foundation 957
После этого установите ссылку на сборку AutoLotDAL.dll, созданную в главе 21
(через пункт меню Website^Add Reference (Веб-сайт1^Добавить ссылку)). Будет
предоставлен некоторый начальный код (расположенный в папке AddCode), который
необходимо удалить. Первым делом, переименуйте исходный файл IService.cs на
IAutoLotService.cs и внутри переименованного файла определите начальный
контракт службы:
[ServiceContract]
public interface IAutoLotService
{
[ Oper at юпСоп tract]
void InsertCar (int id, string make, string color, string petname);
[OperationContract]
void InsertCar(InventoryRecord car) ;
[OperationContract]
InventoryRecord[] Getlnventory();
}
В этом интерфейсе определены три метода, один из которых возвращает массив
объектов (еще не созданного) типа InventoryRecord. Вы можете вспомнить, что метод
Getlnventory () класса InventoryDAL просто возвращает объект DataTable, из-за чего
возникает вопрос: почему бы методу Getlnventory () создаваемой службы не делать то
же самое?
Хотя и можно было бы вернуть DataTable из метода службы WCF, вспомните, что
технология WCF обязана следовать принципам SOA, один из которых —
программирование на основе контрактов, а не реализаций. Поэтому вместо возврата внешнему
клиенту специфичного для .NET типа DataTable, мы вернем специальный контракт
данных (InventoryRecord), который будет корректно выражен в документе WSDL в
независимой манере.
Также обратите внимание, что в показанном ранее интерфейсе определен
перегруженный метод по имени InsertCar(). Первая версия принимает четыре
входных параметра, а вторая — один параметр типа InventoryRecord. Контракт данных
InventoryRecord может быть определен следующим образом:
[DataContract]
public class InventoryRecord
{
[DataMember]
public int ID;
[DataMember]
public string Make;
[DataMember]
public string Color;
[DataMember]
public string PetName;
}
Если реализовать интерфейс в таком виде, построить хост и попытаться вызвать
эти методы на стороне клиента, возникнет исключение времени выполнения. Причина
в том, что одно из требований описания WSDL состоит в то, что каждый метод,
представленный данной конечной точкой, должен быть уникально именован. Таким
образом, хотя перегрузка методов замечательно работает в контексте языка С#,
современные спецификации веб-служб не допускают существования двух методов с одним и тем
же именем InsertCar ().
958 Часть V. Введение в библиотеки базовых классов .NET
К счастью, атрибут [OperationContract] поддерживает свойство Name, которое
позволяет указать, как метод С# будет представлен в описании WSDL. С учетом этого,
обновите вторую версию InsertCarO следующим образом:
public interface IAutoLotService
{
[OperationContract(Nairn* = "InsertCarWithDetails")]
void InsertCar(InventoryRecord car) ;
}
Реализация контракта службы
Теперь переименуйте Service.cs на AutoLotService.cs. Тип AutoLotService
реализует этот интерфейс, как показано ниже (не забудьте импортировать пространства
имен AutoLotConnectedLayer и System.Data в этот файл кода):
using AutoLotConnectedLayer;
using System.Data;
public class AutoLotService : IAutoLotService
{
private const string ConnString =
@"Data Source=(local)\SQLEXPRESS;Initial Catalog=AutoLot"+
";Integrated Secunty=True";
public void InsertCar(int id, string make, string color, string petname)
{
InventoryDAL d = new InventoryDAL();
d.OpenConnection(ConnString);
d.InsertAuto(id, color, make, petname);
d.CloseConnection ();
}
public void InsertCar(InventoryRecord car)
{
InventoryDAL d = new InventoryDAL();
d.OpenConnection(ConnString);
d.InsertAuto(car.ID, car.Color, car.Make, car.PetName);
d.CloseConnection ();
}
public InventoryRecord[] Getlnventory()
{
// Сначала получить DataTable из базы данных.
InventoryDAL d = new InventoryDAL();
d.OpenConnection(ConnString) ;
DataTable dt = d.GetAHInventoryAsDataTable () ;
d.CloseConnection (); »
// Теперь создать List<T> для хранения полученных данных.
List<InventoryRecord> records = new List<InventoryRecord>();
// Скопировать таблицу данных в Listo специальных контрактов.
DataTableReader reader = dt.CreateDataReader();
while (reader.Read())
{
InventoryRecord r = new InventoryRecord();
r.ID= (int)reader["CarlD"];
r.Color = ((string)reader["Color"]);
r.Make = ((string)reader["Make"]);
r.PetName = ( (string)reader["PetName"]);
records.Add(r);
Глава 25. Введение в Windows Communication Foundation 959
// Трансформировать List<T> в массив элементов типа InventoryRecord.
return (InventoryRecord[])records.ToArray();
}
I
Добавить здесь особо нечего. Для простоты мы жестко закодировали значение
строки соединения (которую можно скорректировать согласно существующим настройкам)
вместо сохранения ее в файле Web. con fig. Учитывая, что библиотека доступа к данным
выполняет всю реальную работу по взаимодействию с базой данных AutoLot, все, что
потребуется сделать — это передать входные параметры методу InsertAutoO
класса InventoryDAL. Единственное, что здесь представляет интерес — это отображение
значений объекта DataTable на обобщенный список элементов типа InventoryRecord
(с использованием DataTableReader) с последующей трансформацией List<T> в
массив объектов типа InventoryRecord.
Роль файла *.svc
При создании веб-ориентированной службы WCF обнаруживается, что проект
содержит специфичный файл с расширением *.svc. Этот конкретный файл необходим
любой службе WCF, развернутой в IIS; в нем описано имя и местоположение реализации
службы внутри точки установки. Поскольку имена начального файла и типов WCF были
изменены, потребуется модифицировать содержимое файла Service.cs:
<ь@ ServiceHost Language="C#" Debug="true"
Service="AutoLotService" CodeBehind="~/App_Code/AutoLotService.cs" .>
На заметку! Среда .NET 4.0 позволяет развертывать службу WCF в виртуальном каталоге I IS без
файла *.svc. Однако при этом служба должна представлять собой не более чем коллекцию
файлов кода С#. Служба также будет выглядеть очень похожей на традиционную
веб-службу XML из ASP.NET. За дополнительными сведениями обращайтесь в раздел "What's new in
Windows Communication Foundation" ("Что нового в Windows Communication Foundation")
документации .NET Framework 4.0 SDK.
Содержимое файла Web.conf ig
Файл Web.config службы WCF, созданной под HTTP, использует ряд упрощений
.NET 4.0, рассмотренных ранее в этой главе. Как будет подробно описано далее в этой
книге, при рассмотрении ASP.NET, файл Web.config служит той же цели, что и
файлы *.conf ig исполняемых программ; однако он также управляет рядом специфических
веб-настроек. Например, обратите внимание, что механизм МЕХ включен, и не нужно
указывать специальный элемент <endpoint> вручную:
<configuration>
<system.web>
<compilation debug="false" targetFramework=.0" />
</system.web>
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior>
<i__ Чтобы избежать раскрытия информации метаданных,
установите следующее значение в false и удалите
конечную точку метаданных перед развертыванием -->
<serviceMetadata httpGetEnabled="true"Л>
<'-- Для получения деталей исключений при сбоях б цепч:. отпадт
установите следующее значение в true.
960 Часть V. Введение в библиотеки базовых классов .NET
Перед развертыванием установите его снова в false,
чтобы скрыть информацию об исключении -->
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
<system.webServer>
<modules runAlIManagedModulesForAllRequests="true"/>
</system.webserver>
</configuration>
Тестирование службы
Теперь можно построить клиент любого рода для тестирования службы, включая
передачу конечной точки файла *.svc в приложение WcfTestClient.exe:
WcfTestClient http://localhost/AutoLotWCFServiсе/Service.svc
Чтобы создать специальное клиентское приложение, воспользуйтесь диалоговым
окном Add Service Reference (Добавить ссылку на службу), как это делалось ранее в главе,
в примерах проектов MagicEightBallServiceClient и MathClient.
Исходный код. Проект AutoLotService доступен в подкаталоге Chapter 25.
На этом рассмотрение API-интерфейса Windows Communication Foundation
завершено. Конечно, эта тема слишком обширна, чтобы раскрыть ее полностью в одной
ознакомительной главе, однако, разобравшись с изложенным здесь материалом, можно
продолжить изучение WCF самостоятельно. Исчерпывающие сведения о WCF приведены в
документации .NET Framework 4.0 SDK.
Резюме
В этой главе вы ознакомились с API-интерфейсом Windows Communication Foundation
(WCF), который является частью библиотеки базовых классов, начиная с версии .NET 3.0.
Как объяснялось, основная мотивация, лежащая в основе WCF, состоит в
предоставлении унифицированной объектной модели, которая представляет ряд (ранее
несвязанных) API-интерфейсов распределенных вычислений, собранных воедино. В начале
главы было показано, что служба WCF представляется за счет указания адресов, привязок
и контрактов (обозначаемых аббревиатурой ABC).
Как вы видели, типичное приложение WCF включает использование трех
взаимосвязанных сборок. Первая из них определяет контракты службы и типы службы, которые
представляют ее функциональность. Эта сборка развертывается в отдельной
исполняемой программе-хосте, виртуальном каталоге IIS либо в службе Windows. И, наконец,
клиентская сборка использует сгенерированный файл кода, определяющий тип прокси
(и настройки в конфигурационном файле приложения) для взаимодействия с
удаленным типом.
В главе также рассматривалось применение ряда инструментов программирования
WCF, таких как SvcConfigEditor.exe (который позволяет модифицировать содержимое
файлов *.config), приложение WcfTestClient.exe (для быстрого тестирования служб
WCF) и различные шаблоны проектов Visual Studio 2010. Кроме того, было описано
множество упрощений WCF 4.0, включая конечные точки и поведение по умолчанию.
ГЛАВА 26
Введение в Windows
Workflow Foundation 4.0
Несколько лет назад в составе версии .NET 3.0 появился API-интерфейс под
названием Windows Workflow Foundation (WF). Этот API-интерфейс позволяет
моделировать, конфигурировать, наблюдать и выполнять рабочие потоки (которые служат
для моделирования бизнес-процессов), используемые внутри конкретного приложения
.NET. Готовое решение, предлагаемое WF, дает огромные преимущества при построении
программного обеспечения, поскольку отпадает необходимость в ручной разработке
сложной инфраструктуры для поддержки приложений рабочих потоков.
Будучи новой технологией, API-интерфейс WF в своем первом выпуске имел ряд
шероховатостей. Многие разработчики считали, что его среде проектирования,
предоставленной в Visual Studio 2008, недоставало блеска, а навигация по сложным рабочим
потокам во время разработки была крайне запутанной. К тому же начальный выпуск
WF требовал написания громоздкого кода для создания и запуска рабочего потока. При
этом даже построение самого рабочего потока было несколько неуклюжим, учитывая
тот факт, что кодовая база С# и связанное с рабочим потоком представление в
визуальном конструкторе плохо сочетались друг с другом.
В .NET 4.0 произошла полная переделка всего API-интерфейса WF. Теперь рабочие
потоки моделируются (по умолчанию) с помощью декларативной, основанной на XML
грамматики XAML, где данные, используемые рабочим потоком, являются "почетными
гражданами". Кроме того, визуальные конструкторы WF в Visual Studio были полностью
переписаны с применением технологий Windows Presentation Foundation (WPF). Поэтому
если вы использовали предьщушую версию API-интерфейса WF и были ею недовольны,
настоятельно рекомендуется читать дальше.
Для новичков в мире WF глава начинается с определения роли бизнес-процессов и
описания их связи с API-интерфейсом WF. Кроме того, будет представлена концепция
действия (activity) WF, две основных разновидности рабочих потоков в WF 4.0
(блок-схема и последовательный поток), а также различными шаблонами проектов и
инструментами программирования. После ознакомления с основами будет построено несколько
примеров программ, которые проиллюстрируют использование программной модели
WF для установки бизнес-процессов, которые выполняются под неусыпным надзором
исполняющей среды WF.
На заметку! Полное описание WF 4.0 невозможно уместить в единственную ознакомительную главу.
Более глубокое изложение дается в книге Pro WF: Windows Workflow in .NET 4.0 (Apress, 2010 г.).
962 Часть V. Введение в библиотеки базовых классов .NET
Определение бизнес-процесса
Любое реальное приложение должно иметь возможность моделировать различные
бизнес-процессы. Бизнес-процесс (business process) — это концептуально объединенная
группа задач, которая логически работает как единое целое. Например, предположим,
что строится приложение, позволяющее приобретать автомобили через Интернет Как
только пользователь отправил заказ, приводится в действие большое количество
процессов. Начать можно с проверки кредитоспособности. Если пользователь прошел
такую проверку, можно запустить транзакцию базы данных, чтобы исключить элемент
из таблицы Inventory (склад), добавить новый элемент в таблицу Orders (заказы) и
обновить информацию о счете заказчика. После завершения транзакции базы данных
может понадобиться отправить покупателю электронное письмо с подтверждением, а
затем вызвать удаленную веб-службу, чтобы передать заказ дилеру. Все вместе эти
задачи представляют единый бизнес-процесс.
Моделирование бизнес-процессов — это еще одна деталь, которую должны принимать
во внимание программисты, часто посредством написания специального кода, который
гарантирует не только правильное моделирование бизнес-процесса, но также
корректно выполняется внутри самого приложения. Например, может потребоваться написать
код, учитывающий возможные сбои, выполняющий трассировку и протоколирование
(чтобы видеть текущее состояние бизнес-процесса), поддерживающий постоянство (для
сохранения состояния длительно выполняющегося процесса) и т.д. Понятно, что
создание такой инфраструктуры с нуля потребует массы времени и ручной работы.
Если команда разработчиков фактически построит платформу бизнес-процесса для
своих приложений, то на этом их работа не завершится. Просто говоря, "сырая" кодовая
база С# не может быть легко объяснена членам команды — непрограммистам, которые
также нуждаются в понимании бизнес-процесса. Суровая правда состоит в том, что
эксперты предметной области, менеджеры, продавцы и члены команды графического
дизайна часто не говорят на языке кода. Учитывая это, программистам понадобятся
инструменты моделирования (вроде Microsoft Visio, белой доски и т.п.), чтобы
графически представить процесс в общепонятной терминологии. Очевидная проблема,
возникающая здесь, состоит в том, что придется постоянно поддерживать в согласованном
состоянии две сущности. Если изменяется код, то нужно обновить диаграмму. Если
меняется диаграмма, то придется обновить код.
Более того, при построении изощренных приложений, использующих
стопроцентный кодовый подход, кодовая база очень слабо представляет внутренний "поток"
приложения. Например, типичная программа .NET может состоять из сотен специальных
типов (не говоря уже о многочисленных типах из библиотек базовых классов). Хотя
программисты могут представлять себе, какие объекты вызывают другие объекты, сам код
далеко не в состоянии заменить живую документацию, которая объяснит
последовательность действий простым человеческим языком. Хотя команда разработчиков может
разрабатывать внешнюю документацию и графики рабочих потоков, мы опять
упираемся в проблему нескольких представлений одного и того же процесса.
Роль WF 4.0
По существу, API-интерфейс Windows Workflow Foundation 4.0 позволяет
программистам декларативно проектировать бизнес-процессы, используя лредварительно
разработанный набор действий. Поэтому вместо использования только специального
множества сборок для представления определенных бизнес-действий и всей
необходимой инфраструктуры можно применять визуальные конструкторы WF среды Visual
Studio 2010 для создания бизнес-процессов на этапе проектирования.
Глава 26. Введение в Windows Workflow Foundation 4.0 963
Благодаря этому, WF позволяет построить скелет бизнес-процесса, который затем
может быть наполнен конкретным кодом, где это необходимо.
При программировании с применением API-интерфейса WF для представления
всего бизнес-процесса, а также кода, определяющего его, может использоваться
единственная сущность. В дополнение к дружественному визуальному представлению этого
процесса применяется единственный документ WF, и больше не надо беспокоиться о
возможном рассогласовании нескольких документов. Еще лучше то, что документ WF
наглядно иллюстрирует сам процесс. С минимальной помощью специалиста даже
представители не технического персонала быстро понимают то, что моделирует визуальный
конструктор WF.
Построение простого рабочего потока
При построении приложений на основе рабочих потоков вы, несомненно,
отметите, что процесс выглядит совершенно иначе, чем для типичного приложения .NET.
Например, вплоть до этого момента каждый пример кода начинался с создания нового
рабочего пространства проекта (чаще всего проекта Console Application) и
предусматривал написание кода для представления программы в целом. Приложение WF также
состоит из специального кода; однако, вдобавок к этому, производится встраивание
непосредственно в сборку модели самого бизнес-процесса.
Еще один аспект WF, который существенно отличается от других видов приложений
.NET, заключается в том, что подавляющее большинство рабочих потоков будут
моделироваться в декларативной манере, с использованием грамматики на основе XML,
называемой XAML. По большей части не придется непосредственно писать код разметки,
поскольку IDE-среда Visual Studio 2010 сделает это автоматически, в процессе работы
с инструментами проектирования WE В этом состоит значительное отличие от
предыдущей версии API-интерфейса WF, где в качестве стандартного средства моделирования
рабочего потока применялся код С#.
На заметку! Имейте в виду, что диалект XAML, используемый в WF, не идентичен диалекту XAML,
применяемому для WPR Вы ознакомитесь с синтаксисом и семантикой XAML для WPF в
главе 27, поскольку в отличие от WF для XAML, там довольно часто приходится непосредственно
редактировать сгенерированный визуальным конструктором XAML-код.
Чтобы приступить к работе с рабочими потоками, откройте Visual Studio 2010.
В диалоговом окне New Project (Новый проект) выберите проект Workflow Console
Application (Консольное приложение рабочего потока) и назначьте ему имя Fir stWorkf low
ExampleApp (рис. 26.1).
Теперь взгляните на рис. 26.2, на котором показана начальная диаграмма рабочего
потока, сгенерированная Visual Studio 2010. Как видите, здесь мало что происходит,
помимо появления в визуальном конструкторе сообщения, предлагающего поместить
действия на поверхность проектирования.
Для этого простого тестового рабочего потока откройте панель инструментов Visual
Studio 2010 и найдите действие WriteLine в разделе Primitives (Примитивы), как
показано на рис. 26.3.
Найдя это действие, перетащите его на область с надписью Drop activity here
(Поместите сюда действие) и в поле редактирования Text введите сообщение в двойных
кавычках. На рис. 26.4 показан один возможный рабочий поток.
Часть V. Введение в библиотеки базовых классов .NET
i Visual C#
Windows
Web
Office
Cloud
Repotting
SharePcmt
Sitverlight
Test
WCF
Other Languages
j.NET Framework4
•
№ i Activity Designer Library Visual C#
C^0 Activity Library Visual C*
(АЛ WCF Workflow Service Appli... Visual C#
Ш
FirstWorkflowExampleApp
C:\MyCode
I FrrsflrVorkflow£xatnp!eApp
П
Type: Visual C*
A blank Workflow Console Application
Create directory for solu
Add to source control
Рис. 26.1. Создание нового консольного приложения рабочего потока
Workflowljtaml x|
Workflowl
Expand Ail Collapse Ail
Drop activity here
Рис. 26.2. Визуальный конструктор рабочего потока — это
контейнер для действий, моделирующих бизнес-процесс
Toolbox
l> Control Flow
с> Flowchart
' Messaging
> Runtime
л Primitives
It Pointer
Ar-fJ Assign
■ф Delay
Щ InvokeMethod
Щ WriteUne
> Transaction
:> Collection
> Error Handling
л General
V
WriteLine
Version 4.0,0.0 from Microsoft Corporation
Managed ,NET Component
Writes text to a TextWrrter
There are no usable controls in this group. Drag an item onto
this text to add it to the toolbox.
Рис. 26.3. Панель инструментов отображает все
действия по умолчанию, доступные в WF 4.0
Глава 26. Введение в Windows Workflow Foundation 4.0 965
Workflowlxaml* X|
Worfcflowl
m
Expand All Collapse All
ГЛ Л nteLine
Text "First Workflow!"
Рис. 26.4. Действие WriteLine отображает текст
в TextWriter, в данном случае — на консоли
Просмотр полученного кода XAML
Закройте визуальный конструктор рабочего потока, щелкните правой кнопкой
мыши на Workflow]..xaml в Solution Explorer и выберите в контекстном меню пункт
View code (Просмотреть код). Отобразится лежащее в основе XAML-представление
рабочего потока. Как видите, корневым узлом этого XML-документа является <Activity>.
В открывающем объявлении корневого элемента имеется огромное количество
определений пространств имен XML, и почти в каждом будет присутствовать конструкция
clr-namespace.
Как будет объясняться более подробно в главе 27, когда файл XAML нуждается в
ссылке на определения типов .NET, содержащихся во внешних сборках (или в другом
пространстве имен текущей сборки), необходимо строить отображение .NET на XAML,
используя конструкцию clr-namespace.
<Activity mc : Ignorable=llsapM x:Class = "FirstWorkf lowExampleApp . Workf lowl"
xmlns="http://schemas.microsoft.com/netfx/2009/xaml/activities"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mv="clr-namespace:Microsoft.VisualBasic;assembly=System"
xmlns :mva="clr-namespace:Microsoft.VisualBasic.Activities;assembly=System.Activities"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns : sl = "clr-namespace : System; assembly=System11
xmlns:s2="clr-namespace:System;assembly=System.Xml"
xmlns:s3="clr-namespace:System;assembly=Systern.Core"
xmlns:sa="clr-namespace:System.Activities;assembly=Systern.Activities"
xmlns:sad="clr-namespace:System.Activities.Debugger;assembly=Systern.Activities"
xmlns:sap="http://schemas.microsoft.com/netfx/2009/xaml/activities/presentation"
xmlns:scg="clr-namespace:System.Collections.Generic;assembly=System"
xmlns:scgl="clr-namespace:System.Collections.Generic;assembly=System.ServiceModel"
xmlns:scg2="clr-namespace:System.Collections.Generic;assembly=Systern.Core"
xmlns:scg3="clr-namespace:System.Collections.Generic;assembly=mscorlib"
xmlns:sd="clr-namespace:System.Data;assembly=System.Data"
xmlns:sl="clr-namespace:System.Linq;assembly=Systern.Core"
xmlns:st="clr-namespace:System.Text;assembly=mscorlib"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<x:Members>
<x:Property Name="MessageToShow" Type="InArgument(x:String)" />
</x:Members>
<sap:VirtuallzedContainerService.HintSize>
251,240
</sap:VirtuallzedContainerService.HintSize>
966 Часть V. Введение в библиотеки базовых классов .NET
<mva:VisualBasic.Settings>
Assembly references and imported namespaces for internal implementation
</mva:VisualBasic.Settings>
<WriteLine sadrXamlDebuggerXmlReader.FileName=
"C:\My Code\ FirstWorkflowExampleApp\Workflowl.xaml"
sap:VirtualizedContainerService.HintSize=11,200" Text="[MessageToShow]" />
</Activity>
Элемент <Activity> — это контейнер для всех задач, представляющих рабочий
поток. Здесь имеется только одно поддействие <WriteLine>. Обратите внимание, что
атрибут Text установлен на основе данных, введенных в визуальном конструкторе
рабочего потока.
Теперь запомните, что при построении рабочих потоков с использованием Visual
Studio 2010 обычно не придется вручную модифицировать разметку. Используя этот
визуальный конструктор (и различные связанные с WF инструменты, интегрированные
в Visual Studio 2010), можно моделировать процесс, а разметка будет сгенерирована
IDE-средой. По этой причине в настоящей главе мы не углубляемся в детали XAML для
WF. Однако знайте, что можно просматривать разметку, сгенерированную IDE-средой
при перетаскивании действий на поверхность визуального конструктора.
В любом случае разметка, находящаяся в файле XAML, всегда отображается на
"реальные" типы в сборке .NET. Например, каждый элемент в файле XAML (такой как
<Activity>) представляет собой декларативное определение объекта .NET (данном
случае System.Activities.Activity). Атрибуты, которые появляются в открывающем
определении элемента, отображаются на свойства или события в связанном типе класса.
Во время выполнения разметка воплощается в объектной модели времени выполнения,
где каждое описание элемента будет использовано для установки состояния связанного
объекта .NET.
Возможность определять структуру рабочего потока в декларативной манере с
использованием XAML предоставляет массу преимуществ, самое заметное из которых —
инструментальная поддержка. Например, инструменты проектирования из Visual
Studio 2010 можно включить в специальное приложение с графическим интерфейсом.
После этого можно построить простой инструмент, предназначенный не программистам
из команды, для создания бизнес-процессов, реализацией которых займутся
программисты. Результат может быть сохранен в виде файла *.xaml и импортирован в
разрабатываемые проекты .NET.
Другое замечательное преимущество использования разметки для определения
рабочего потока состоит в том, что становится возможной загрузка внешних файлов
XAML в память на лету, позволяя менять работу бизнес-процессов. Например, можно
написать код, который читает файл *.xaml на лету и заполняет связанную объектную
модель. Поскольку логика рабочего потока не кодируется жестко в сборку, изменение
функциональности бизнес-процесса может заключаться в простом изменении разметки
и перезапуске приложения.
Следует понимать, что WF является чем-то большим, нежели просто симпатичным
визуальным конструктором, который позволяет моделировать действия
бизнес-процесса. При построении диаграммы WF разметка может быть всегда расширена для
представления поведения процесса во время выполнения. Фактически, при желании можно
вообще избегать использования XAML и писать рабочие потоки исключительно на С#.
Однако в этом случае мы вернемся к той же основополагающей проблеме: код, который
не понятен нетехническому персоналу.
В результате запуска приложения в окне консоли отобразится заданное сообщение:
First Workflow1
Press any key to continue . . .
Глава 26. Введение в Windows Workflow Foundation 4.0 967
Выглядит неплохо, но пока непонятно, что же запустило этот рабочий поток? И как
можно гарантировать, что консольное приложение останется в рабочем состоянии
достаточно долго, чтобы рабочий поток мог быть завершен? Ответы на эти вопросы
требуют понимания исполняющего механизма рабочего потока.
Исполняющая среда WF 4.0
Следующее, что нужно понять — API-интерфейс WF также состоит из исполняющего
механизма, который загружает, выполняет, выгружает и иными способами
манипулирует процессом рабочего потока. Исполняющий механизм WF может быть развернут
внутри любого из доменов приложений .NET; однако имейте в виду, что отдельный домен
приложения может иметь только один работающий экземпляр механизма WF.
Вспомните из главы 16, что домен приложения (AppDomain) — это раздел внутри
процесса Windows, который играет роль хоста для приложения .NET и любых
внешних библиотек кода. Как таковой, механизм WF может быть встроен в простую
консольную программу, настольное приложение с графическим интерфейсом (Windows
Forms или Windows Presentation Foundation (WPF)) или же представлен службой Windows
Communication Foundation (WCF).
На заметку! Шаблон проекта WCF Workflow Service Application — замечательная начальная точка,
если необходимо построить службу WCF (см. главу 25), которая внутренне использует рабочие
потоки.
При моделировании бизнес-процесса, который должен использоваться широким
разнообразием систем, также есть возможность написания документа WF внутри
проекта С# Class Library Таким образом, новые приложения смогут просто ссылаться на
библиотеку *.dll, чтобы повторно использовать заранее предопределенную коллекцию
бизнес-процессов. Это очевидно полезно, когда не хочется многократно пересоздавать
одни и те же рабочие потоки.
Хостинг рабочего потока с использованием
класса Workf lowlnvoker
Хост-процесс исполняющей среды WF может взаимодействовать с упомянутой
исполняющей средой посредством различных приемов. Простейший способ
предусматривает использование класса Workf lowlnvoker из пространства имен System.Activities.
Этот класс позволяет запустить рабочий поток с помощью всего одной строки кода.
Открыв файл Program.cs текущего проекта Workflow Console Application, вы увидите
следующий метод Main ():
static void Main(string [ ] args)
{
Workflowlnvoker.Invoke(new Workflowl () ) ;
}
Использование Workf lowlnvoker очень удобно, когда нужно просто запустить
рабочий поток и не следить за ним далее. Метод Invoke () будет выполнять рабочий
поток в синхронизированном блокирующем режиме. Вызывающий поток блокируется до
тех пор, пока весь рабочий поток не завершится либо не будет прерван
принудительно. Поскольку метод Invoke () — синхронный вызов, гарантируется, что весь рабочий
поток на самом деле будет завершен до окончания Main(). Фактически, если добавить
какой-нибудь код после вызова Workf lowlnvoker. Invoke (), он будет выполнен, когда
рабочий поток завершится (или в худшем случае будет принудительно прерван):
968 Часть V. Введение в библиотеки базовых классов .NET
static void Main(string [ ] args)
{
Workflowlnvoker.Invoke (new Workflowl());
Console.WriteLine("Thanks for playing");
}
Передача аргументов рабочему потоку
с использованием класса Workf lowlnvoker
Когда хост-процесс запускает рабочий поток, ему очень часто нужно передать
специальные стартовые аргументы. Например, предположим, что пользователю программы
необходимо дать возможность указывать сообщение, которое нужно отобразить в
действии WriteLine вместо жестко закодированного текстового сообщения. В нормальном
коде С# можно было бы создать специальный конструктор класса для приема таких
аргументов. Однако рабочий поток всегда создается с использованием конструктора по
умолчанию! Более того, большинство рабочих потоков определены только с помощью
XAML, а не процедурного кода.
Оказывается, метод Invoke () имеет несколько перегрузок, одна из которых
позволяет передавать аргументы рабочему протоку при запуске. Эти аргументы представлены
с использованием переменной Dictionary<string, object>, содержащей набор пар
"имя/значение", который будет применяться для установки идентично именованных
(и типизированных) переменных-аргументов в самом рабочем потоке.
Определение аргументов с использованием
визуального конструктора рабочих потоков
Для определения аргументов, которые получат данные входящего словаря,
воспользуемся визуальным конструктором рабочих потоков. В окне Solution Explorer
щелкните правой кнопкой мыши на Workf lowl. xaml и выберите в контекстном меню пункт
View Designer (Просмотреть визуальный конструктор). Обратите внимание на кнопку
Arguments (Агрументы) в нижней части визуального конструктора. Щелкните на ней
и в открывшемся диалоговом окне добавьте входной аргумент типа String по имени
MessageToShow (присваивать значение по умолчанию этому новому аргументу не
нужно). Удалите начальное сообщение из действия WriteLine. На рис. 26.5 показан
конечный результат.
WorkflowLxamr X
Workfiowl
Expand AM ColiapseAU
Name
MessageToShow
Create Argument
ГА WriteLine
Text Enter a VB expression
Direction Argument type Default value
к in String I Enter a VB expression
Arguments I
Рис. 26.5. Аргументы рабочего потока могут использоваться для
приема аргументов от хоста
Глава 26. Введение в Windows Workflow Foundation 4.0 969
Теперь в свойстве Text действия WriteLine можно просто ввести MessageToShow
в качестве вычисляемого выражения. Во время ввода вы заметите помощь со стороны
средства IntelliSense (рис. 26.6).
Workflowl латГ X
Workflowl
Expand All Collapse All
1
Name
MessageToShow
Gwfe Argument
Щ WrrteUne
Text «e| •
*1$ MarlcupExtension
Л% MarkupExtensionReturnTypeAttribute
**$Math
«*JMe
j{) Media
;■**$ MemberDefinition
,j* MessageToShow
IO Microsoft
Common j AH
Arguments
Рис. 26.6. Использование специального аргумента в качестве ввода для действия
После этого имеется вся необходимая инфраструктура, и в метод Main() класса
Program можно вносить показанные ниже изменения. Обратите внимание, что для
объявления переменной Dictionaryo в файл Program.cs понадобится импортировать
пространство имен System.Collections.Generic.
static void Main(string[] args)
{
Console.WriteLine ("***** Welcome to this amazing WF application *****");
// Получить от пользователя данные для передачи рабочему потоку.
Console.Write("Please enter the data to pass the workflow: ");
string wfData = Console.ReadLine();
// Упаковать данные в словарь.
Dictionary<stnng, ob]ect> wfArgs = new Dictionary<stnng, ob]ect> () ;
wfArgs.Add("MessageToShow", wfData);
// Передать словарь рабочему потоку.
Workflowlnvoker.Invoke(new Workflowl(), wfArgs);
Console.WriteLine("Thanks for playing");
}
Важно отметить, что строковые значения каждого члена переменной Dictionaryo
должны быть именованы в соответствие с переменными-аргументами рабочего потока.
Запуск модифицированной программы дает вывод, подобный следующему:
***** Welcome to this amazing WF application *****
Please enter the data to pass the workflow: Hello Mr. Workflow!
Hello Mr. Workflow!
Thanks for playing
Press any key to continue . . .
Помимо метода Invoke () другими действительно интересными членами
Workflowlnvoker являются BeginlnvokeO и EndlnvokeO, которые позволяют
запускать рабочий поток во вторичном потоке выполнения, используя шаблон асинхронного
970 Часть V. Введение в библиотеки базовых классов .NET
делегата .NET (см. главу 19). Если необходим большая степень контроля над тем, как
исполняющая среда WF манипулирует рабочим потоком, можете вместо него
использовать класс Workf lowApplication.
Хостинг рабочего потока с использованием
класса Workf lowApplication
Класс Workf lowApplication (вместо Workf lowlnvoker) необходимо использовать,
когда требуется сохранять или загружать длительно выполняющийся рабочий поток с
использованием служб постоянного хранения WF и получать уведомления о различных
событиях, которые происходят на протяжении жизненного цикла экземпляра рабочего
потока, работать с "закладками" WF и прочими расширенными средствами.
При наличии опыта работы с предыдущей версией API-интерфейса WF
использование Workf lowApplication может показаться похожим на работу с классом
Workf lowRuntime в .NET 3.5, в том плане, что для запуска экземпляра рабочего потока
понадобится вызывать метод Run().
В результате вызова метода Run() новый фоновый поток выполнения извлекается
из пула потоков CLR. Таким образом, если не добавить дополнительную поддержку для
обеспечения ожидания основного потока завершения вторичного потока, то экземпляр
рабочего потока может вообще не получить шанса завершить свою работу!
Один из способов обеспечить достаточное время ожидания для завершения работы
фонового потока предусматривает применение объекта AutoResetEvent из
пространства имен System.Threading (на самом деле именно это и делалось в коде запуска
рабочих потоков .NET 3.5). Ниже показаны изменения в текущем примере, которые
позволяют теперь использовать Workf lowApplication вместо Workf lowlnvoker.
static void Main(string [ ] args)
{
Console.WriteLine ("***** Welcome to this amazing WF application *****");
// Получить данные от пользователя для передачи рабочему потоку.
Console.Write("Please enter the data to pass the workflow: ");
string wfData = Console.ReadLine();
// Упаковать данные в словарь.
Dictionary<string, object> wfArgs = new Dictionary<string,object>() ;
wfArgs.Add("MessageToShow", wfData);
// Использовать для информирования первичного потока о необходимости ожидания.
AutoResetEvent waitHandle = new AutoResetEvent(false) ;
// Передать рабочему потоку.
Workf lowApplication app = new Workf lowApplication (new Workflow]. (), wfArgs);
// Связать событие с данным объектом app.
//О факте завершения уведомить другой поток
//и вывести на консоль соответствующее сообщение.
app.Completed = (completedArgs) => {
waitHandle.Set() ;
Console.WriteLine("The workflow is done!");
};
// Запустить рабочий поток.
app.Run ();
// Подождать, пока не появится уведомление о завершении рабочего потока.
waitHandle.WaitOne();
Console.WriteLine("Thanks for playing");
}
Вывод будет похожим на вывод предыдущей итерации этого проекта:
Глава 26. Введение в Windows Workflow Foundation 4.0 971
***** Welcome to this amazing WF application *****
Please enter the data to pass the workflow: Hey again1
Hey again'
The workflow is done!
Thanks for playing
Press any key to continue . . .
Преимущество использования класса Workf lowApplication связано с
возможностью вмешаться в события (как это было сделано непрямо с использованием свойства
Completed) и подключать более сложные службы (постоянного хранения, закладки и т.п.).
В данном ознакомительном вступлении в WF 4.0 детали этих служб времени
выполнения не рассматриваются. Поведения времени выполнения и службы
исполняющей среды Windows Workflow Foundation 4.0 подробно описаны в документации .NET
Framework 4.0 SDK.
Переделка первого рабочего потока
На этом первый взгляд на WF 4.0 завершен. Хотя этот пример тривиален, вы узнали
несколько очень интересных и полезных вещей. Было показано, как передать объект
Dictionary, содержащий пары "имя/значение", которые затем попадают в виде
идентично именованных аргументов в рабочий поток. Это действительно полезно, когда
необходимо получить пользовательский ввод (такой как идентификационный номер
клиента, его номер карточки социального страхования и т.п.), который будет
использоваться рабочим потоком для выполнения действий.
Также было продемонстрировано, что рабочий поток .NET 4.0 определяется в
декларативной манере (по умолчанию) с использованием основанной на XML
грамматики под названием XAML. С помощью XAML можно указывать действия, которые
содержит рабочий поток. Во время выполнения эти данные применяются для создания
корректной объектной модели в памяти. И последнее: были представлены два
разных подхода к запуску рабочего потока с использованием классов Workf lowlnvoker и
WorkflowApplication.
Исходный код. Проект FirstWorkf lowExampleApp доступен в подкаталоге Chapter 26.
Знакомство с действиями Windows Workflow 4.0
Вспомните, что цель WF — позволить моделировать бизнес-процесс в
декларативной манере, с последующим его выполнением исполняющей средой WF. На жаргоне
WF бизнес-процесс состоит из любого количества действий. Попросту говоря, действие
WF — это атомарный "шаг" в общем процессе. При создании нового приложения
рабочего потока вы обнаружите, что панель инструментов содержит значки для встроенных
действий, сгруппированные по категориям.
Эти готовые действия используются для моделирования бизнес-процесса. Каждое
действие в панели инструментов отображается на реальный класс в сборке System.
Activities.dll (и чаще всего содержится внутри пространства имен System.
Activities.Statements). Несколько таких готовых действий будут использоваться
на протяжении этой главы. Полную информацию можно найти в документации .NET
Framework 4.0 SDK.
Действия потока управления
Первая категория действий в панели инструментов позволяет представлять задачи
организации циклов и принятия решений в более крупном рабочем потоке. Их по ль-
972 Часть V. Введение в библиотеки базовых классов .NET
за должна быть очевидной, учитывая, что похожие задачи часто приходится решать
в коде С#. Действия потока управления перечислены в табл. 26.1; обратите внимание,
что некоторые из них допускают параллельную обработку действий с использованием
Task Parallel Library (см. главу 19).
Таблица 26.1. Действия потока управления в WF 4.0
Действие Назначение
DoWhile Циклическое действие, которое выполняет содержащиеся внутри
действия как минимум один раз и повторяет это, пока логическое условие
истинно
ForEach<T> Выполняет действие по одному разу для каждого значения,
представленного в коллекции ForEach<T>.Values
If Моделирует условие If-Then-Else
Parallel Действие, которое выполняет все дочерние действия одновременно и
асинхронно
ParallelForEach<T> Перечисляет элементы коллекции и выполняет каждый элемент
коллекции параллельно
Pick Представляет моделирование потока управления на основе событий
PickBranch Потенциальный путь выполнения внутри родительского действия Pick
Sequence Выполняет набор дочерних действий последовательно
Switch<T> Выбирает одно из возможных действий для выполнения, в зависимости
от значения заданного выражения типа, указанного в параметре типа
объекта
While Выполняет содержащийся элемент рабочего потока, пока условие истинно
Действия блок-схемы
Рассмотрим действия блок-схемы (flowchart), которые довольно важны, учитывая,
что действие Flowchart часто является первым элементом, который помещается на
поверхность визуального конструктора рабочих потоков. Концепция рабочего потока в
виде блок-схемы появилась в .NET 4.0. Она позволяет строить рабочий поток, используя
известную модель, где выполнение рабочего потока основано на множестве ветвящихся
путей, выбор каждого из которых зависит от истинности или ложности определенного
внутреннего условия. В табл. 26.2 описаны члены этого набора действий.
Таблица 26.2. Действия блок-схемы в WF 4.0
Действие Назначение
Flowchart Моделирует рабочие потоки, используя известную парадигму
блок-схемы. Это — самое первое действие, которое помещается на
поверхность визуального конструктора
FlowDecision Действие, предоставляющее возможность моделировать условный
узел с двумя возможными исходами
FlowSwitch<T> Узел, позволяющий моделировать конструкцию переключения, с одним
выражением и одним исходом для каждого соответствия
Глава 26. Введение в Windows Workflow Foundation 4.0 973
Действия обмена сообщениями
Рабочий поток может легко вызывать члены внешней веб-службы или службы WCF,
а также получать уведомления от внешних служб, используя действия обмена
сообщениями (messaging activities). Поскольку эти действия очень тесно связаны с разработкой
WCF, они собраны в отдельную сборку .NET— System.ServiceModel.Activities.dll.
Внутри этой библиотеки имеется пространство имен Activities, в котором
определены основные действия, перечисленные в табл. 26.3.
Таблица 26.3. Действия обмена сообщениями в WF 4.0
Действие Назначение
CorrelationScope Используется для управления дочерними действиями сообщений
InitializeCorrelation Инициализирует корреляцию без отправки или приема сообщения
Receive Принимает сообщение от службы WCF
Send Отправляет сообщение службе WCF
SendAndReceiveReply Отправляет сообщение службе WCF и захватывает возвращенное
значение
TransactedReceiveScope Действие, которое позволяет направить транзакцию в рабочий
поток или созданные диспетчером транзакции стороны сервера
Наиболее часто используемыми действиями обмена сообщениями являются Send и
Receive, которые позволяют взаимодействовать с внешними веб-службами XML или
службами WCF.
Действия исполняющей среды и действия-примитивы
Следующие две категории действий в панели инструментов — Runtime (Исполняющая
среда) и Primitives (Примитивы) — позволяют строить рабочий поток, который
производит вызовы исполняющей среды WF (в случае Persists и TerminateWorkf low) и
выполняет общие операции, такие как помещение текста в выходной поток или вызов
метода на объекте .NET. Эти действия описаны в табл. 26.4.
Таблица 26.4. Действия исполняющей среды и действия-примитивы в WF 4.0
Действие Назначение
Persist Заставляет рабочий поток сохранить свое состояние в базе данных,
используя службу постоянства WF
TerminateWorkf low Прерывает выполняющийся экземпляр рабочего потока, инициируя на
хосте событие Workf lowApplication.Completed, и выдает
сообщение об ошибке. После такого прерывания рабочий поток не может
быть возобновлен
Assign Позволяет устанавливать свойства действия с использованием
присваиваемых значений, определенных в визуальном конструкторе рабочего потока
Delay Заставляет рабочий поток приостановиться на заданный период времени
InvokeMethod Вызывает метод указанного объекта или типа
WriteLine Записывает заданную строку в указанный экземпляр типа,
унаследованного от TextWriter. По умолчанию это будет стандартный выходной
поток (т.е. консоль); однако можно сконфигурировать и другие потоки,
такие как FileStream
974 Часть V. Введение в библиотеки базовых классов .NET
Пожалуй, наиболее интересным и полезным действием из этого набора является
InvokeMethod, потому что оно позволяет вызывать методы классов .NET в
декларативной манере. Действие InvokeMethod можно также сконфигурировать для
сохранения любого возвращенного значения, которое присылает вызванный метод. Действие
TerminateWorkf low может пригодиться, когда нужно рассчитывать на точку
невозврата. Если экземпляр рабочего потока добирается до этого действия, он инициирует
событие Completed, которое может быть перехвачено на стороне хоста, как это делалось
в первом примере.
Действия транзакций
При построении рабочего потока может понадобиться обеспечить выполнение
группы действий в атомарной манере — в том смысле, что они либо все должны
выполниться успешно, либо все вместе быть отменены. Даже если соответствующие действия не
работают непосредственно с реляционной базой данных, основные действия,
представленные в табл. 26.5, позволяют добавлять их в транзакционный контекст.
Таблица 26.5. Действия транзакций в WF 4.0
Действие Назначение
CancellationScope Ассоциирует логику отмены внутри главного потока выполнения
CompensableActivity Любое действие, поддерживающее компенсацию своих дочерних
действий
TransactionScope Действие, обозначающее границы транзакции
Действия над коллекциями и действия обработки ошибок
Последние две категории, которые осталось рассмотреть в этой вводной главе,
позволяют декларативно манипулировать обобщенными коллекциями и реагировать на
исключения времени выполнения. Действия коллекций незаменимы, когда требуется
манипулировать объектами, представляющими бизнес-данные (такими как заказы на
покупку, объекты медицинской информации или отслеживание заказов) на лету в XAML.
Действия ошибок, с другой стороны, позволяют реализовать логику try/catch/throw
внутри рабочего потока. Этот последний набор действий WF 4.0 описан в табл. 26.6.
Таблица 26.6. Действия над коллекциями и действия обработки ошибок в WF 4.0
Действие Назначение
AddToCollection<T> Добавляет элемент к указанной коллекции
ClearCollection<T> Очищает указанную коллекцию от всех элементов
ExistInCollection<T> Определяет, представлен ли указанный элемент в данной коллекции
RemoveFromCollection<T> Удаляет элемент из указанной коллекции
Rethrow Инициирует ранее перехваченное исключение из действия Catch
Throw Инициирует исключение
TryCatch Содержит элементы рабочего потока, которые должны быть
выполнены исполняющей средой рабочего потока внутри блока
обработки исключений
Глава 26. Введение в Windows Workflow Foundation 4.0 975
Ознакомившись со многими из действий по умолчанию, можно приступать к
построению некоторых интересных рабочих потоков, которые их используют По ходу дела
вы узнаете о двух ключевых действиях, которые обычно функционируют как корень
рабочего потока— Flowchart и Sequence.
Построение рабочего потока в виде блок-схемы
В первом примере было показано, как перетащить действие WriteLine
непосредственно на поверхность визуального конструктора рабочих потоков. Хотя и верно, что
любое действие, представленное в панели инструментов Visual Studio 2010, может быть
первым элементом, помещенным в визуальный конструктор, только несколько из них
способны содержать в себе дочерние поддействия (что, очевидно, очень важно). При
построении нового рабочего потока велика вероятность, что первым элементом, который
будет помещен на поверхность визуального конструктора, будет действие Flowchart
или Sequence.
Упомянутые действия обладают способностью содержать любое количество
внутренних дочерних действий (включая дополнительные Flowchart или Sequence),
представляющих сущность бизнес-процесса. Для начала создайте новое консольное приложение
рабочего потока по имени EnumerateMachineDataWF. Затем переименуйте начальный
файл *.xaml в MachinelnfoWF.xaml.
Из раздела Flowchart (Блок-схема) панели инструментов перетащите на поверхность
визуального конструктора действие Flowchart. В окне Properties (Свойства) измените
значение свойства DisplayName на что-то более осмысленное, такое как Show Machine
Data Flowchart (свойство DisplayName определяет то, как элемент называется в
визуальном конструкторе). Теперь поверхность визуального конструктора рабочего потока
должна выглядеть примерно так, как на рис. 26.7.
MachinelnfoWF-xaml X вд
Woricflowl
IJEj Show Machine Data Flowchart
IIJ ,1..ML 1„LMM—
•
Start
Expand AH Collapse All
a
а^НКЕкЯ?
Рис. 26.7. Начальное действие Flowchart
Скоба захвата, которая появляется в нижнем правом углу действия Flowchart (при
наведении курсора мыши), позволяет увеличивать и уменьшать размер пространства
блок-схемы. Его понадобится увеличить, чтобы добавить внутрь новые действия.
Подключение действий к блок-схеме
Большой значок Start (Пуск) представляет точку входа в действие Flowchart,
которое в данном примере является первым действием всего рабочего потока и будет
инициировано при запуске рабочего потока с помощью классов Workf lowlnvoker или
Workf lowApplication. Этот значок может располагаться в любом месте визуального
976 Часть V. Введение в библиотеки базовых классов .NET
конструктора, но рекомендуется его переместить в левый верхний угол, чтобы
освободить побольше места.
Цель состоит в том, чтобы собрать блок-схему, соединяя любое количество
дополнительных действий вместе, обычно используя в процессе действие FlowDecision. Для
начала перетащите действие WriteLine на поверхность визуального конструктора,
изменив значение его свойства DisplayName на Greet User. Наведя курсор мыши на значок
Start, вы увидите петельки для стыковки с каждой стороны. Щелкните на ближайшей к
действию WriteLine петельке для стыковки и перетащите ее к петельке для стыковки
действия WriteLine. Между этими двумя элементами появится соединение, которое
означает, что первым выполняемым действием рабочего потока будет Greet User.
Теперь, подобно первому примеру, добавьте аргумент рабочего потока (через
кнопку Arguments (Аргументы)) по имени UserName типа string без какого-либо значения
по умолчанию. Чуть позже оно будет передано динамически через специальный
объект Dictionaryo. Наконец, установите в качестве значения свойства Text действия
WriteLine следующий оператор кода:
"Hello" & UserName
Что здесь может показаться непривычным? Если вы привыкли писать код С#, то
заметите здесь ошибку, потому что для конкатенации строк должен использоваться
символ +, а не &. Однако всякий раз, когда встречается код условия в рабочем потоке,
должен применяться синтаксис Visual Basic! Причина в том, что API-интерфейс WF 4.0
использует вспомогательную сборку, которая кодировалась для обработки операторов
VB, а не С# (странно, но это правда).
Добавьте второе действие WriteLine на поверхность визуального конструктора и
соедините с предыдущим. На этот раз для свойства Text определите жестко
закодированное строковое значение "Do you want me to list all machine drives?" и измените
значение свойства DisplayName на Ask User. На рис. 26.8 показано соединение между
текущими действиями рабочего потока.
ТА Greet User
it-
Text "Hello" & UserName
Start
Щ Ask User
Text "Do you want me to list all rn
Рис. 26.8. Рабочие потоки в виде блок-схемы соединяют действия вместе
Работа с действием invokeMethod
Поскольку большая часть рабочего потока определяется в декларативной манере с
помощью XAML, наверняка будет часто использоваться действие InvokeMethod,
которое позволяет вызывать методы реальных объектов в различных точках рабочего
потока. Перетащите один из таких элементов на поверхность визуального конструктора,
измените значение свойства DisplayName на Get Y or N, и установите соединение
между ним и действием Ask User типа WriteLine.
Первым свойством для конфигурирования действия InvokeMethod является
TargetType. Оно представляет имя класса, определяющего статический член,
который требуется вызвать. В раскрывающемся списке свойства TargetType действия
InvokeMethod выберите пункт Browse for Types... (Обзор типов...), как показано на
рис. 26.9.
Глава 26. Введение в Windows Workflow Foundation 4.0 977
Щ GetYorN
TargetType
TargetObject
MethodName
Рис. 26.9. Указание цели для InvokeMethod
В открывшемся диалоговом окне выберите класс System.Console из сборки
mscorlib.dll (если ввести имя типа в поле Type Name (Имя типа), он будет
автоматически найден). После нахождения класса System.Console щелкните на кнопке ОК.
В визуальном конструкторе введите ReadLme в качестве значения свойства
MethodName действия InvokeMethod. Подобным образом действие InvokeMethod
конфигурируется на вызов метода Console.ReadLineO по достижении этого шага
рабочего потока.
Как известно, Console.ReadLineO возвращает строковое значение, которое
содержит символы, введенные с клавиатуры до нажатия клавиши <Enter>; однако нужен
какой-то способ получить его. Этим сейчас и займемся.
Определение переменных уровня рабочего потока
Определение переменной рабочего потока в XAML почти идентично определению
аргумента, в том плане, что это можно делать непосредственно в визуальном
конструкторе (на этот раз через кнопку Variables (Переменные)). Отличие в том, что аргументы
используются для захвата данных, переданных хостом, в то время как переменные
просто указывают на данные в рабочем потоке, которые будут использованы для влияния
на его поведение времени выполнения.
Щелкнув на кнопке Variables визуального конструктора, добавьте новую строковую
переменную по имени YesOrNo. Обратите внимание, что при наличии в рабочем
потоке нескольких родительских контейнеров (например, Flowchart, который содержит
внутри Sequence) можно указать область видимости переменной. В рассматриваемом
примере доступен единственный выбор — корневое действие Flowchart. Выберите в
визуальном конструкторе действие InvokeMethod и в окне Property (Свойства)
установите для свойства Result новую переменную (рис. 26.10).
[Properties
' ПХ]
1 Syste m. Act iv ities S tatementsJnvokeMethod
h: ii Search:
В Misc
DisplayName
GenericTypeArguments
MethodName
1 Parameters
Ru nAsynchronousJy
TargetObject
1 TargetType
Gear
GetYorN
(Collection) ГЛ|
ReadLme
(Collection) [3|
Щ YesOrNo (Tl
a
Enter a VB cxpressi [... д
(SystemXonsole »l|
(null)
Рис. 26.10. Полностью сконфигурированное действие InvokeMethod
978 Часть V. Введение в библиотеки базовых классов .NET
Теперь, получив фрагмент данных от вызова внешнего метода, можно использовать
его для принятия решений во время выполнения внутри блок-схемы, используя
действие FlowDecision.
Работа с действием FlowDecision
Действие FlowDecision служит для выбора одного из двух возможных действий, в
зависимости от истинности или ложности булевской переменной либо оператора,
возвращающего булевское значение. Перетащите одно из этих действий на поверхность
визуального конструктора и соедините с действием InvokeMethod (рис. 26.11).
LXj Show Machine Data Flowchart
• Щ Greet User
T ext Enter a VB exp ness ion
! ijfg GetYorN
J , ,
Щ Ask User TargetType j Systcm.Comote »|
Text "Do you want me to list all m« TargetObject Enter a VB expressior
MethodName ReadLine
b
True False
Decision
Рис. 26.11. Действие FlowDecision позволяет организовать ветвление по двум направлениям
На заметку! Если необходимо оформить условие множественного ветвления внутри блок-схемы,
используйте действие FlowSwitch<T>. Оно позволяет определить любое количество путей, из
которых выбирается один в зависимости от значения определенной переменной рабочего потока.
Установите для свойства Condition (в окне Properties) действия FlowDecision
следующий оператор кода, который можно ввести непосредственно в редакторе:
YesOrNo.ToUpper () = "Y"
Здесь производится проверка значения переменной YesOrNo, представленное в
верхнем регистре, на предмет равенства "Y". Не забывайте, что здесь используется
синтаксис VB, так что должна применяться операция проверки равенства VB (=), а не С# (==).
Работа с действием TerminateWorkf low
Теперь необходимо построить действия, которые происходят с обеих сторон от
действия FlowDecision. На стороне "false" подключитесь к действию WriteLine, выводящему
жестко закодированное сообщение, за которым следует действие TerminateWorkf low
(рис. 26.12).
Строго говоря, использовать действие TerminateWorkf low не обязательно,
поскольку рабочий поток и так завершается по завершении ветви "false". Однако с помощью
данного действия можно сгенерировать исключение для хоста рабочего потока,
информируя его о причине останова. Это исключение может быть сконфигурировано в окне
Properties.
Глава 26. Введение в Windows Workflow Foundation 4.0 979
(Ц User Said No
Text Xbye..."
Decision
t
ф TerminateWorkflow
Рис. 26.12. Ветвь "false"
Выберите действие TerminateWorkflow в визуальном конструкторе и в окне
Properties щелкните на кнопке с многоточием для свойства Exception. Откроется
редактор, который позволит определить исключение, как это бы делалось в коде (рис. 26.13).
Рис. 26.13. Конфигурирование исключения для генерации
по достижении действия TerminateWorkflow
Завершите конфигурирование этого действия, установив для свойства Reason
значение "YesOrNo was false".
Построение условия "true"
Чтобы начать построение условия "true" для действия FlowDecision, подключите
действие WriteLine, которое просто отображает жестко закодированную строку,
сообщающую о согласии пользователя продолжить работу. Соедините его с новым
действием InvokeMethod, которое вызовет метод GetLogicalDrives () класса System.
Environment. Для этого установите свойство TargetType в System.Environment, a
свойство MethodName — в GetLogicalDrives.
Добавьте новую переменную уровня рабочего потока (щелкнув на кнопке Variables в
визуальном конструкторе рабочих потоков) по имени DriveNames типа string[]. Для
указания массива строк выберите пункт Array of [T] в раскрывающемся списке Variable
Туре и укажите string в открывшемся диалоговом окне. На рис. 26.14 показана первая
часть условия "true".
Теперь установите переменную DriveHames в качестве значения свойства Result
этого нового действия InvokeMethod.
Работа с действием ForEach<T>
Следующая часть рабочего потока будет выводить имена всех дисков в окне консоли,
что подразумевает необходимость ъ циклическом проходе по данным, представленным в
переменной DriveNames, которая была сконфигурирована как массив объектов string.
Действие ForEach<T> — это эквивалент WF XAML ключевого слова f oreach в С#, и
настраивается это действие похожим образом (во всяком случае, концептуально).
980 Часть V. Введение в библиотеки базовых классов .NET
Щ User Said Yes!
Text "Wonderful!"
■4 True
Decision
iQ InvokeMethod
TargetType J System.Environm<
TargetObject Enter a VB expressic
MethodName GefcLogicalDrives
Рис. 26.14. Конфигурирование исключения для генерации
по достижении действия TerminateWorkf low
Перетащите одно из этих действий на поверхность визуального конструктора и
соедините с предыдущим действием InvokeMethod. Чуть позже вы сконфигурируете
действие ForEach<T>, а пока, чтобы завершить ветвь "true", поместите еще одно действие
WriteLine на поверхность визуального конструктора. На рис. 26.15 показано, как
теперь должен выглядеть рабочий поток.
X Show Machine Data Flowchart
^Ш^ ГА Greet User
^^ Text "Hello" & UserName
Start
T
Щ Ask User
Text Do you want me to list
P User Said Vest
<* True
Text "Wonderful!"
..?
S^f InvokeMethod
TargetType j System.Environm* ▼ J
TargetObject Enter a VB express tor
MethodName GetLogicalDrives
all m
i
Decision
Щ GetYorN
TargetType jSystem.Console * |
TargetObject Enter a VB expresstor
MethodName ReadLine
rj User Said No
Fake ►
Text "K,bye..."
f
Q TerminateWorkflow
.!£J ForEach<String>
ОшЫе-d
f
Щ WriteLine
ck to view
Text "Thanks for using this workfk
lh.
Рис. 26.15. Завершенный рабочий поток высшего уровня
Чтобы избавиться от присутствующей в визуальном конструкторе ошибки,
понадобится завершить конфигурирование действия ForEach<T>. Сначала в окне Properties
укажите параметр типа для обобщения, которым в данном примере будет string.
Глава 26. Введение в Windows Workflow Foundation 4.0 981
Свойство Values — это место, откуда берутся данные, и в данном случае это
переменная DriveNames (рис. 26.16).
■ Properties
ISystem.Activrties.Statements.For£ach<System5tring>
[j|~]ii Search:
IB Misc
DispfayName
TypeArgument
Values
ForEach<String>
[String
DriveNames
- П x]
Clear
» 1
У
Рис. 26.16. Установка типа для перечисления
Это конкретное действие нуждается в дополнительном редактировании, для чего
нужно дважды щелкнуть в визуальном конструкторе, чтобы открыть новый
визуальный мини-конструктор, предназначенный только для данного действия. Не все
действия WF 4.0 поддерживают двойной щелчок с открытием визуального
мини-конструктора, но это несложно определить по самому действию (оно сообщает "Double-click to
view" ("Дважды щелкните для просмотра")). Выполните двойной щелчок на действии
ForEach<T> и добавьте одиночное свойство WriteLine, которое выведет все значения
s-tring (рис. 26.17).
jfl ForEach<String>
Foreach item in
Body
fA WriteLine
Text «tern
DriveNames
Рис. 26.17. Завершающий шаг конфигурирования
действия ForEach<T>
На заметку! В визуальном мини-конструкторе ForEach<T> можно добавлять любое нужное
количество действий. Все они будут выполняться на каждой итерации цикла.
Покончив с этим, воспользуйтесь ссылками в верхнем левом углу визуального
конструктора, чтобы вернуться на верхний уровень рабочего потока (такие навигационные
цепочки часто используются при углублении в набор действий).
Завершение приложения
Пример почти готов. Все, что осталось сделать — это обновить метод Main() класса
Program, добавив перехват исключения, которое будет инициировано, если
пользователь ответит "N" и тем самым вызовет генерацию объекта Exception. Измените код,
как показано ниже (не забыв импортировать в файл кода пространство имен System.
Collect ions. Generic):
982 Часть V. Введение в библиотеки базовых классов .NET
static void Main(string [ ] args)
{
try
{
Dictionary<stnng, ob]ect> wfArgs = new Dictionary<stnng, ob]ect>();
wfArgs.Add("UserName", "Andrew");
Workf lowlnvoker. Invoke (new Workflow]. () , wfArgs);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.Data["Reason"]);
}
} \
Обратите внимание, что причина ("Reason") исключения может быть получена из
свойства Data класса System. Except ion. Если запустить программу и ввести "Y" в
ответ на вопрос о перечислении дисков, то появится следующий вывод:
Hello Andrew
Do you want me to list all machine drives?
У
Wonderful!
C:\
D:\
E:\
F:\
G:\
H:\
I:\
Thanks for using this workflow
Однако если ввести "N" (или любое другое значение, отличное от "Y" или "у"), вывод
будет таким:
Hello Andrew
Do you want me to list all machine drives?
n
K, bye. . .
YesOrNo was false
Промежуточные итоги
У новичков в работе со средой рабочих потоков может возникнуть вопрос: в чем
состоит преимущество кодирования столь простого процесса с использованием WF XAML
по сравнению с кодом С#? В конце концов, можно было бы вообще обойтись без Windows
Workflow Foundation и просто написать следующий класс С#:
class Program
{
static void Main(string [ ] args)
{
try
{
ExecuteBusinessProcess ();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.Data["Reason"]);
}
}
Глава 26. Введение в Windows Workflow Foundation 4.0 983
private static void ExecuteBut me ^Process ()
{
string UserName = "Andrew";
Console.WriteLine("Hello {0}", UserName);
Console.WriteLine("Do you want me to list all machine drives?");
string YesOrNo = Console.ReadLine () ;
if (YesOrNo. ToUpperO == "Y")
{
Console.WriteLine("Wonderful'");
string[] DriveNames = Environment.GetLogicalDrives();
foreach (string item in DriveNames)
{
Console*.WriteLine (item) ;
}
Console.WriteLine("Thanks for using this workflow");
}
else
{
Console.WriteLine ("F, Bye...");
Exception ex = new Exception("User Said No!");
ex. Data ["Reason" ] = "YesOrNo viols false";
}
}
}
Вывод этой программы был бы абсолютно идентичен предыдущему рабочему
потоку на базе XAML. Так зачем вообще связываться со всеми этими действиями? Прежде
всего, учтите, что не всем удобно читать код С#. Если придется объяснять этот бизнес-
процесс, скажем, продавцам и нетехническим менеджерам, что вы выберете — код С#
или блок-схему? Ответ должен быть очевиден.
Однако более важно то, что API-интерфейс WF имеет множество дополнительных
служб времени выполнения, включая сохранение длительно выполняющихся рабочих
потоков в базе данных, автоматическое отслеживание событий рабочего потока и тому
подобное. Если представить себе объем работы, которую пришлось бы сделать, чтобы
воспроизвести всю эту функциональность в новом проекте, польза от WF станет еще
более очевидной.
Наконец, имея возможность декларативно генерировать рабочие потоки с
использованием визуальных конструкторов и XAML, можно передать конструирование рабочих
потоков экспертам и менеджерам, ограничившись при этом включением XAML в
проекты С#.
С учетом сказанного, API-интерфейс WF — не обязательно правильный выбор для
всех программ .NET. Тем не менее, для наиболее традиционных бизнес-приложений
возможности определения, хостинга, выполнения и мониторинга рабочих потоков
действительно очень полезны. Как с любой новой технологией, понадобится определить, что
будет полезно для текущего проекта. Давайте рассмотрим другой пример работы с API-
интерфейсом WF, на этот раз упаковав рабочий поток в отдельную сборку *.dll.
Исходный код. Проект EnumerateMachinelnfoWF доступен в подкаталоге Chapter 26.
984 Часть V. Введение в библиотеки базовых классов .NET
Изоляция рабочих потоков
в выделенных библиотеках
Хотя создание консольного приложения рабочего потока — замечательный способ
поэкспериментировать с API-интерфейсом WF, готовый для реальной производственной
эксплуатации рабочий поток должен быть упакован в специальную сборку .NET *.dll.
После этого рабочие потоки можно многократно использовать на двоичном уровне в
различных проектах.
Хотя можно было бы начать с использования проекта библиотеки классов С# в
качестве стартовой точки, более простой способ построения библиотеки рабочего
потока состоит в том, чтобы начать с шаблона проекта Activity Library (Библиотека
действий), находящегося в узле Workflow (Рабочий поток) диалогового окна New Project.
Преимущество этого типа проекта состоит в том, что он автоматически устанавливает
все необходимые ссылки на сборки WF и предоставляет файл *.xaml для создания
начального рабочего потока.
Создаваемый рабочий поток будет моделировать процесс запроса к базе данных
AutoLot с целью проверки, имеется ли в таблице Inventory указанный автомобиль
соответствующего цвета и изготовителя. Если запрошенный автомобиль есть на складе,
для хоста будет сформирован ответ в виде выходного параметра. Если нужного
элемента на складе нет, будет сгенерировано сообщение руководителю отдела продаж о том,
чтобы он нашел автомобиль нужного цвета.
Определение начального проекта
Создайте новый проект Activity Library по имени ChecklnventoryWorkf lowLib
(рис. 26.18). Сразу после создания проекта переименуйте начальный файл Activity]..
xaml в Checklnventory.xaml.
Рис. 26.18. Построение библиотеки действий
Прежде чем двигаться дальше, закройте визуальный конструктор рабочего потока
и просмотрите лежащее в основе определение XAML, щелкнув правой кнопкой мыши
на файле XAML и выбрав в контекстном меню пункт View Code (Просмотреть код).
Удостоверьтесь, что атрибут х:Class в корневом элементе <Activity>
соответствующим образом обновлен (если нет, замените Activityl на Checklnventory):
<Activity x:Class="CheckInventoryWorkflowLib.Checklnventory" ... >
Глава 26. Введение в Windows Workflow Foundation 4.0 985
На заметку! При создании библиотеки рабочего потока всегда проверяйте значение х:Class
корневого действия, поскольку клиентские программы используют это имя для создания
экземпляра рабочего потока.
В этом рабочем потоке в качестве первичного действия используется Sequence
вместо Flowchart. Перетащите новое действие Sequence на поверхность визуального
конструктора (оно находится в области Control Flow (Поток управления) панели
инструментов) и измените значение свойства DisplayName на Look Up Product.
Как следует из названия, действие Sequence позволяет создавать последовательные
задачи, которые выполняются одна за другой. Однако это не обязательно означает, что
дочерние действия должны следовать строго линейному порядку. Последовательность
может содержать блок-схемы, другие последовательности, параллельную обработку
данных, ветвление if/else и все, что имеет смысл для проектируемого бизнес-процесса.
Импорт сборок и пространств имен
Поскольку рабочий поток будет взаимодействовать с базой данных AutoLot,
следующий шаг состоит в добавлении ссылки на сборку AutoLot.dll с помощью диалогового
окна Add Reference (Добавить ссылку) среды Visual Studio 2010. В этом примере
используется автономный уровень, поэтому рекомендуется взять финальную версию этой
сборки, созданную в главе 22 (в случае установки ссылки на финальную версию,
созданную в главе 23, также понадобится установить ссылку на System.Data.Entity.dll, как
того требует ADO.NET Entity Framework).
В рабочем потоке для опроса возвращенного объекта DataTable с целью
определения наличия запрошенного товара на складе используется LINQ to DataSet. Поэтому
также потребуется установить ссылку на System.Data.DataSetExtensions.dll.
После добавления ссылок на эти сборки щелкните на кнопке Imports (Импорты) в
нижней части визуального конструктора рабочих потоков. Отобразится список всех
пространств имен .NET, которые можно использовать в визуальном конструкторе рабочих
потоков (воспринимайте эту часть как декларативную версию ключевого слова С# using).
Для добавления пространств имен из всех ссылаемых сборок необходимо просто
ввести их в текстовом поле, находящемся в верхней части редактора Imports. Для удобства
импортируйте AutoLotDisconnectedLayer и System.Data.DataSetExtensions. После
этого можно будет ссылаться на содержащиеся типы без необходимости использования
полностью квалифицированных имен. На рис. 26.19 показано финальное содержимое
области Imports.
Enter or Sel'ecf namespace
Imported namespaces
AutoLotDisconnectedLayer
Microsoft. VisualBasic
Microsoft. VisuaiBasicActivrties
System
System Activities
System Activities .Expressions
System Activrties.Statements
System Activities.Validation
System. Activities ,Xamllntegration
System.Collections.Generic
System .Data
System.Data. DataSetExtensions
System.Li nq
Я Imports _
Рис. 26.19. Подобно ключевому слову С# using, область Imports
позволяет включать пространства имен .NET
986 Часть V. Введение в библиотеки базовых классов .NET
Определение аргументов рабочего потока
Определите два новых входных аргумента уровня рабочего потока с именами
RequestedMake и RequestedColor, оба типа String. Подобно предыдущему примеру,
здесь хост рабочего потока создаст объект Dictionary, который содержит данные,
отображаемые на эти аргументы, так что нет необходимости присваивать этим элементам
значения по умолчанию в редакторе Arguments. Как и можно было предположить,
рабочий поток использует эти значения для выполнения запроса к базе данных.
Тот же самый редактор Arguments применяется и для определения выходного
аргумента по имени FormattedResponse типа String. Когда нужно вернуть данные из
рабочего потока обратно хосту, можно создать любое количество выходных аргументов,
которые могут быть перечислены хостом по завершении рабочего потока. На рис. 26.20
показано текущее состояние визуального конструктора рабочих потоков.
Name
RequestedMake
RequestedColor
FormattedResponse
\Crcate Argument
Direction
In
In
Out
Argument type
String
String
String
Default value
Default value not j
upported
Рис. 26.20. Аргументы рабочего потока предоставляют способ передачи
и возврата данных из рабочего потока
Определение переменных рабочего потока
Теперь в рабочем потоке необходимо объявить переменную-член, которая
соответствует классу InventoryDALDisLayer из AutoLot.dll. Вспомните из главы 22, что
этот класс позволяет получать данные из Inventory, возвращенные в виде DataTable.
Выберите действие Sequence в визуальном конструкторе и, щелкнув на кнопке Variables,
создайте переменную по имени AutoLotlnventory. В раскрывающемся списке Variable
Туре (Тип переменной) выберите пункт Browse for Types... (Обзор типов...) и введите тип
InventoryDALDisLayer (рис. 26.21).
Browse and Select а
Type Name: AutoLotDisconnectedlayer.InventoryDALDisLayer
* Referenced assernblies>
л AutoLotOAL A.0.0.0]
л AutoLotDiscormectedLa
Cancel
Рис. 26.21. Переменные рабочего потока предлагают способ
декларативного их определения внутри контекста
Убедившись, что новая переменная выбрана, в окне Properties щелкните на кнопке
с многоточием для свойства Default. Откроется окно редактора кода, размеры
которого можно изменять произвольным образом. Этот редактор значительно упрощает ввод
сложного кода присваивания переменной. Введите следующий код (VB), который
создаст новую переменную InventoryDALDisLayer:
Глава 26. Введение в Windows Workflow Foundation 4.0 987
New InventoryDALDisLayer("Data Source=(local)\SQLEXPRESS;" +
"Initial Catalog=AutoLot;Integrated Security=True")
Объявите вторую переменную рабочего потока типа System.Data.DataTable по
имени Inventory, опять используя пункт меню Browse for Types... (установите ее значение
по умолчанию в Nothing). Позднее переменной Inventory будет присвоен результат
вызова GetAllInventoryO на переменной InventoryDALDisLayer.
Работа с действием Assign
Действие Assign позволяет устанавливать значение переменной, которое может
быть результатом выполнения любых допустимых операторов кода. Перетащите
действие Assign (находящееся в области Primitives панели инструментов) на действие
Sequence. В поле редактирования То укажите переменную Inventory. В окне Properties
щелкните на кнопке с многоточием для свойства Value и введите следующий код:
AutoLotInventory.GetAllInventory ()
После того как действие Assign встречается в рабочем потоке, получится объект
DataTable, содержащий все записи из таблицы Inventory. Однако проверить наличие
корректного товара на складе необходимо вручную с использованием значений
аргументов RequestedMake и RequestedColor, присланных от хоста. Для определения
существования товара применяется LINQ to DataSet. После этого используется действие
If для выполнения XAML-эквивалента оператора if /else/then.
Работа с действиями If и Switch
Перетащите действие If на узел Sequence непосредственно под действием Assign.
Поскольку это действие представляет собой основанный на XAML способ принятия
решения во время выполнения, прежде всего оно должно быть сконфигурировано для
проверки булевского выражения. В редакторе условий Condition введите следующую
проверку запроса LINQ to DataSet:
(From car In Inventory.AsEnumerable()
Where CType(car("Color") , String) = RequestedColor And
CType(car("Make"), String) = RequestedMake Select car).Any()
Этот запрос использует данные RequestedColor и-RequestedMake, поступившие от
хоста, для извлечения всех записей из DataTable об автомобилях нужного изготовителя
и цвета (обратите внимание, что операция Ctype — это эквивалент операции явного
приведения в С#). Вызов расширяющего метода Апу() вернет true или false, в
зависимости от того, содержит ли результат запроса какие-то данные.
Следующей задачей будет конфигурирование набора действий, которые будут
выполнены, когда определенное условие истинно или ложно. Вспомните, что конечная
цель — отправить пользователю сформатированное сообщение, если запрошенный им
автомобиль есть на складе. Однако чтобы усложнить задачу, мы будем возвращать
уникальное сообщение, зависящее от того, какого производителя автомобиль был
запрошен (BMW, Yugo или что-то еще).
Перетащите действие Switch<T> (находящееся в области Control Flow панели
инструментов) в область Then действия If. Откроется диалоговое окно с запросом
обобщенного типа. Укажите String. В области Expression действия Switch введите
RequestedMake.
Вы увидите, что опция Default (По умолчанию) переключателя уже на месте, и будет
предложено добавить действие, представляющее ответ. В рассматриваемом примере
требуется только одно действие Assign; когда нужно выполнить более сложные
действия, то областью по умолчанию, скорее всего, будет Sequence или Flowchart.
988 Часть V. Введение в библиотеки базовых классов .NET
После добавления действия Assign в область редактирования Default (по щелчку на
ссылке Add Activity (Добавить действие)), присвойте аргументу FormattedResponse
следующий оператор кода:
String. Format ("Yes, we have a {0} {1} you can purchase", -
ReguestedColor, ReguestedMake)
Теперь редактор Switch будет выглядеть, как показано на рис. 26.22.
♦ 3 Swhch<String>
Expression RequestedMake
Default
A* Assign
FormattedRespons .
! Add new case
z String.Format("Yes,
A
Рис. 26.22. Определение задачи по умолчанию для действия Switch
Теперь щелкните на ссылке Add New Case (Добавить новый вариант) и введите BMW
(без двойных кавычек) для первой ветви Case, после чего еще раз щелкните для ввода
финальной ветви со значением Yugo (снова без кавычек). В каждую из этих областей
Case перетащите действие Assign, в обоих случаях для присваивания значения
переменной FormattedResponse. В случае BMW укажите такое значение:
String. Format ("Yes sir! We can send you {0} {1} as soon as {2}!", _
ReguestedColor, ReguestedMake, DateTime.Now)
В случае Yugo используйте следующее выражение:
String. Format ("Please, we will pay you to get this {0} off our lot!", _
ReguestedMake)
После этого действие Switch будет выглядеть, как показано на рис. 26.23.
0 Switch<String>
Expression RequestedMake
Default
*8 Assign
FormattedRespons :
4
Case BMW
Case Yugo
Add new case
String.Format("Yes,
AJ
Assign
Assign
Рис. 26.23. Окончательный вид действия Switch
Построение специального действия кода
Впечатляющим средством визуального конструктора рабочего потока является
его способность встраивать сложные операторы кода (и запросы LINQ) в файл XAML,
поскольку наверняка возникнет необходимость написать код в выделенном классе.
Существуют несколько способов сделать это с помощью API-интерфейса WF, но
наиболее простой из них предусматривает создание класса, расширяющего Code Activity,
Глава 26. Введение в Windows Workflow Foundation 4.0 989
или же, если действие должно возвращать значение — CodeActivity<T> (где Т — тип
возвращаемого значения).
Рассмотрим пример создания специального действия, которое будет выводить
данные в текстовый файл, информируя персонал отдела продаж о запросе автомобиля,
которого нет на складе. Выберите пункт меню Projects Add New Item (Проекте Добавить
новый элемент), укажите в качестве шаблона Code Activity (Действие кода) и назначьте
ему имя CreateSalesMemoActivity.cs (рис. 26.24).
Рис. 26.24. Добавление нового действия кода
Если специальное действие требует какого-то ввода для последующей обработки,
он может быть представлен свойством, инкапсулирующим объект InArgument<T>. Тип
класса InArgument<T> — это специфическая сущность API-интерфейса WF, которая
обеспечивает возможность передачи данных, предоставленных рабочим потоком,
самому специальному классу действия. Создаваемому действию понадобится два таких
свойства, которые будут представлять производителя и цвет искомого товара на складе.
Кроме того, специальное действие кода должно переопределить виртуальный
метод Execute(), который будет вызван исполняющей средой WF, когда до него дойдет
очередь выполнения рабочего потока. Обычно в этом методе будут использоваться
свойства InArgument<T> для получения переданной рабочей нагрузки. Реальное
переданное значение должно получаться непрямо с помощью метода GetValueO входного
CodeActivityContext. Ниже приведен код специального действия, которое будет
генерировать новый файл *.txt, описывающий ситуацию для отдела продаж:
public sealed class CreateSalesMemoActivity : CodeActivity
{
// Два свойства для специального действия.
public InArgument<string> Make { get; set; }
public InArgument<string> Color { get; set; }
// Если действие возвращает значение, унаследовать его от
// CodeActivity<TResult> и вернуть значение из метода Execute().
protected override void Execute(CodeActivityContext context)
{
// Вывод сообщения в локальный текстовый файл.
StringBuilder salesMessage = new StringBuilder();
salesMessage.AppendLine("***** Attention sales team1 *****»);
salesMessage.AppendLine ("Please order the following ASAP1");
990 Часть V. Введение в библиотеки базовых классов .NET
salesMessage.AppendFormat( {0} {1}\п",
context.GetValue(Color), context.GetValue(Make));
salesMessage.AppendLine с*********************************»);
System.IO.File.WriteAllText("SalesMemo.txt", salesMessage.ToString());
}
}
Как должно использоваться специальное действие кода? Для специального действия
кода в WF 4.0 допускается строить специальный визуальный конструктор. Однако это
требует понимания WPF, поскольку специальный визуальный конструктор использует
многие из тех технологий, что применяются для построения объектов WPF Window или
UserControl. Технология WPF начнет рассматриваться в следующей главе, а пока
ограничимся простейшим подходом.
Скомпилируйте сборку рабочего потока. Открыв окно визуального конструктора
рабочих потоков в Visual Studio 2010, взгляните в верхнюю часть панели инструментов.
Там должно появиться специальное действие (рис. 26.25).
IToclbox v П X |
1 л ChecWwentoryWorkflawLfb
^ Pointer
_J GeateSaleslVlemoAttn
1 л Control Flow
llf Pointer
+} DoWhtle
2 .
Version 1.0,0,0
Managed ,NE1 Component
'Щ ForEach<T>
& «
*
-J
Рис. 26.25. Специальное действие кода появляется в панели инструментов Visual Studio 2010
Перетащите действие Sequence в ветвь Else действия If. Затем перетащите
специальное действие в Sequence. Теперь в окне Properties можно присвоить значения каждому
из представленных свойств. Используя переменные RequestedMake и RequestedMake,
установите свойства Make и Color действия, как показано на рис. 26.26.
Рис. 26.26. Установка свойств специального действия кода
Для завершения рабочего потока перетащите финальное действие Assign в
действие Sequence ветви Else и установите в качестве значения FormattedResponse строку
"Sorry, out of stock" (Сожалеем, нет на складе). На рис. 26.27 показан
окончательный вид рабочего потока.
Скомпилируйте проект и переходите к финальной части главы, где будет построен
клиентский хост, который будет использовать этот рабочий поток.
Исходный код. Проект ChecklnventoryWorkf lowLib доступен в подкаталоге Chapter 26.
Глава 26. Введение в Windows Workflow Foundation 4.0 991
^ Look Up Product
jfei
Condition
(From car In Inventory.AsEnumerableQ
§| Swrtch<String>
Expression RequestedMake
Default
Case "BWM"
Case "Yugo"
Add net* case
*S Assign
Inventory
Then
\
= AutoLotInventory.(
*
Assign
Assign
Assign
Л
Else
j|| Sequence Л
4^J| CreateSalesMemoActivity
Агв Assign
FormattedRespons = "Sorry, out of stock
V
Рис. 26.27. Завершенный последовательный рабочий поток
Использование библиотеки рабочего потока
Библиотеку рабочего потока может использовать приложение любого рода; однако
мы здесь предпочтем простоту и построим простое консольное приложение под
названием Workf lowLibraryClient. Создав проект, понадобится установить ссылки не
только на сборки CheckInventoryWorkflowLib.dll и AutoLot.dll, но также и на
ключевую библиотеку WF 4.0 — System.Activities.dll, которая находится на вкладке .NET
диалогового окна Add Reference в Visual Studio 2010.
После этого поместите в файл Program.cs следующий код:
using System;
using System.Linq;
using System.Activities;
using System. Collections .Generic-
using ChecklnventoryWorkflowLib;
namespace WorkflowLibraryClient
{
class Program
static void Main(string [ ] args)
Console.WnteLine ("**** Inventory Look up
// Получить предпочтения пользователя.
Console.Write("Enter Color: ");
string color = Console.ReadLine();
Console.Write("Enter Make: " ) ;
string make = Console.ReadLine();
');
992 Часть V. Введение в библиотеки базовых классов .NET
// Упаковать данные для рабочего потока.
Dictionary<stnng, object> wfArgs = new Dictionary<string, object>()
{"RequestedColor", color},
{"RequestedMake", make}
try
// Отправить данные рабочему потоку.
Workflowlnvoker.Invoke(new Checklnventory(), wfArgs);
catch (Exception ex)
Console.WriteLine(ex.Message);
}
}
}
Как это уже делалось в других примерах, воспользуемся классом Workf lowlnvoker
для запуска рабочего потока в синхронном режиме. Теперь давайте посмотрим, как
получить возвращаемое значение рабочего потока. Вспомните, что как только рабочий
поток завершился, необходимо получить сформатированный результат
Получение выходного аргумента рабочего потока
Метод Workf lowlnvoker. Invoke () вернет объект, реализующий интерфейс
IDictionary<string, objectx Поскольку рабочий поток может возвращать любое
количество выходных аргументов, необходимо указать имя нужного выходного аргумента
как строковое значение для индексатора типа. Модифицируйте логику try/catch
следующим образом:
try
{
// Отпарвить данные рабочему потоку.
IDictionary<string, object> outputArgs =
Workflowlnvoker.Invoke (new Checklnventory(), wfArgs);
// Вывести выходное сообщение на консоль.
Console.WriteLine(outputArgs["FormattedResponse"] ) ;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Теперь можно запустить приложение и ввести изготовителя и цвет автомобиля,
имеющегося в таблице Inventory базы данных AutoLot. Ниже показан
результирующий вывод:
**** Inventory Look up ****
Enter Color: Black
Enter Make: BMW
Yes sir! We can send you Black BMW as soon as 2/17/2010 9:23:01 PM!
Press any key to continue . . .
Если ввести информацию об элементе, которого на складе нет, вывод будет
выглядеть следующим образом:
Глава 26. Введение в Windows Workflow Foundation 4.0 993
**** Inventory Look up ****
Enter Color: Pea Soup Green
Enter Make: Viper
Sorry, out of stock
Press any key to continue . . .
Кроме того, вы обнаружите в папке bin\Debug клиентского приложения новый файл
*.txt, в котором сохранено напоминание для продавцов:
***** Attention sales team1 *****
Please order the following ASAP1
1 Pea Soup Green Viper
На этом знакомство с новым API-интерфейсом WF 4.0 завершено. Как упоминалось
в начале этой главы, если вы работали с предыдущей версией API-интерфейса Windows
Workflow Foundation, то заметите, что вся программная модель была полностью
переделана (и существенно улучшена).
В этой главе рассматривались лишь некоторые ключевые аспекты этого API-
интерфейса .NET 4.0, но был построен достаточный фундамент для дальнейших
самостоятельных исследований.
Исходный код. Проект Workf lowLibraryClientproject доступен в подкаталоге Chapter 26.
Резюме
По существу, WF позволяет моделировать внутренние бизнес-процессы приложения
непосредственно в самом приложении. Помимо простого моделирования общего
рабочего потока, однако, в WF предлагается завершенная исполняющая среда и несколько
служб, которые дополняют общую функциональность этого API-интерфейса (службы
постоянного хранения и отслеживания и т.п.). Хотя в главе эти службы не
рассматривались непосредственно, помните, что приложение WF производственного уровня почти
наверняка будет использовать их.
В .NET 4.0 программная модель WF, изначально представленная в .NET 3.0,
полностью поменялась. Теперь можно проектировать рабочие потоки целиком в
декларативной манере, используя основанную на XML грамматику под названием XAML. Вдобавок
XAML можно применять не только для определения состояния каждого действия в
рабочем потоке, но также неявно встраивать "реальный код" в документ XAML, используя
визуальные конструкторы WF.
В этой вводной главе вы ознакомились с двумя ключевыми действиями верхнего
уровня — Flowchart и Sequence. Хотя каждое из них управляет потоком логики
уникальным способом, оба они могут содержать одинаковые дочерние действия и
выполняться хостом в одинаковой манере (через Workf lowlnvoker или Workf lowApplication).
Кроме того, вы узнали, как передавать аргументы хоста в рабочий поток с применением
обобщенного объекта Dictionary и как получать выходные аргументы из рабочего
потока с помощью обобщенного объекта, совместимого с I Dictionary.
ЧАСТЬ VI
Построение настольных
пользовательских
приложений
с помощью WPF
В этой части...
Глава 27. Введение в Windows Presentation Foundation и XAML
Глава 28. Программирование с использованием элементов управления WPF
Глава 29. Службы визуализации графики WPF
Глава 30. Ресурсы, анимация и стили WPF
Глава 31. Шаблоны элементов управления WPF и пользовательские
элементы управления
ГЛАВА 27
Введение в Windows
Presentation
Foundation и XAML
Когда вышла версия 1.0 платформы .NET, программисты, которым нужно было
строить графические настольные приложения, использовали два API-интерфейса
Windows Forms и GDI+, упакованные в основном в сборки System.Windows.Forms.dll
и System.Drawing.dll. Хотя Windows Forms и GDI+ — блестящие API-интерфейсы для
построения традиционных настольных графических интерфейсов, начиная с версии
.NET 3.0, также предлагается альтернативный API-интерфейс под названием Windows
Presentation Foundation (WPF).
Эта вводная глава о WPF начинается с рассмотрения мотивации, лежащей в
основе этой новой технологии построения пользовательских интерфейсов, что поможет
увидеть разницу между моделями программирования Windows Forms/GDI+ и WPF.
Затем мы рассмотрим различные типы приложений WPF, поддерживаемых этим API-
интерфейсом и изучим роль типов Application, Window, ContentControl. Control,
UIElementHFramewprkElement. Попутно вы научитесь перехватывать действия мыши
и клавиатуры, определять данные на уровне приложения и решать другие
распространенные задачи WPF, не используя ничего, кроме кода С#.
В этой главе вы ознакомитесь с грамматикой на основе XML, которая называется
расширяемым языком разметки приложений (Extensible Application Markup Language —
XAML). Вы изучите синтаксис и семантику XAML, включая синтаксис присоединяемых
свойств, роль преобразователей типов, расширения разметки, а также узнаете, как
происходит генерация, загрузка и разбор XAML во время выполнения. Будет показано, как
интегрировать данные XAML в кодовую базу С# WPF (и выгоды, с этим связанные).
Глава завершается рассмотрением различных специфичных для WPF инструментов,
поставляемых в составе IDE-среды Visual Studio 2010. Будет написан собственный
редактор/анализатор XAML, демонстрирующий манипулирование XAML во время
выполнения для построения динамического пользовательского интерфейса.
На заметку! В остальных главах, посвященных WPF, будет описан Microsoft Expression Blend —
инструмент для генерации XAML-разметки.
Глава 27. Введение в Windows Presentation Foundation и XAML 997
Мотивация, лежащая в основе WPF
С годами в Microsoft разработали многочисленные инструменты для создания
пользовательского интерфейса (С/C++/Windows API, VB6, MFC и т.д.), предназначенные
для построения настольных приложений. Каждый из этих программных инструментов
предлагает кодовую базу для представления основных аспектов приложения с
графическим интерфейсом, включая главные окна, диалоговые окна, элементы управления,
системы меню и т.п. В начальном выпуске платформы .NET API-интерфейс Windows
Forms быстро стал предпочтительной моделью разработки пользовательских
интерфейсов, благодаря его простой, но очень мощной объектной модели.
Хотя с помощью Windows Forms было успешно разработано множество полноценных
настольных приложений, следует признать, что его программная модель довольно сисси-
метрична. Просто говоря, сборки System.Windows.Forms.dll и System.Drawing.dll
не обеспечивают прямой поддержки многих дополнительных технологий для
построения полноценного настольного приложения. Чтобы проиллюстрировать это
утверждение, рассмотрим природу разработки графического интерфейса, предшествующую WPF
(т.е. .NET 2.0; см. табл. 27.1).
Таблица 27.1. Решения .NET 2.0 для обеспечения желаемой функциональности
Желаемая функциональность Решение .NET 2.0
Построение форм с элементами управления Windows Forms
Поддержка двухмерной графики GDI+ (System.Drawing.dll)
Поддержка трехмерной графики API-интерфейсы DirectX
Поддержка потокового видео API-интерфейсы Windows Media Player
Поддержка документов нефиксированного формата Программное манипулирование PDF-файлами
Как видите, разработчик Windows Forms вынужден заимствовать типы из множества
несвязанных API-интерфейсов и объектных моделей. Хотя и верно, что использование
всех этих разнообразных API-интерфейсов синтаксически похоже (в конце концов, это
просто код С#), каждая технология требует радикально иного мышления. Например,
навыки, необходимые для создания трехмерной анимации с использованием DirectX,
совершенно отличаются от тех, что нужны для привязки данных к экранной сетке. Честно
говоря, программисту Windows Forms чрезвычайно трудно в равной мере овладеть
природой каждого из этих API-интерфейсов.
На заметку! В приложении А предлагается введение в разработку приложений Windows Forms и
GDI+.
Унификация различных API-интерфейсов
Технология WPJF (появившаяся в версии .NET 3.0) специально создавалась для того,
чтобы объединить все эти ранее несвязанные программистские задачи в рамках
единой объектной модели. Таким образом, при разработке трехмерной анимации больше
не возникает необходимости в ручном кодировании с использованием API-интерфейсов
DirectX (хотя это можно делать), поскольку нужная функциональность встроена в WPF.
Чтобы увидеть, насколько все проясняется, взгляните на табл. 27.2, в которой
проиллюстрирована модель настольной разработки, предложенная в .NET 3.0.
998 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Таблица 27.2. Решения .NET 3.0 для обеспечения желаемой функциональности
Желаемая функциональность Решение .NET 3.0 и последующих версий
Построение форм с элементами управления WPF
Поддержка двухмерной графики WPF
Поддержка трехмерной графики WPF
Поддержка потокового видео WPF
Поддержка документов нефиксированного формата WPF
Очевидное преимущество здесь в том, что программисты .NET теперь имеют
единый, симметричный API-интерфейс для всех общих нужд построения графических
интерфейсов. Освоив ключевые сборки WPF и грамматику XAML, вы будете поражены,
насколько быстро с их помощью можно создавать очень сложные пользовательские
интерфейсы.
Обеспечение разделения ответственности через XAML
Возможно, одним из наиболее значительных преимуществ WPF стал способ четкого
отделения внешнего вида и поведения приложения Windows от программной логики,
управляющей этим. Используя XAML, можно определить пользовательский интерфейс
приложения через разметку XML. Эта разметка (в идеале генерируемая с помощью
инструментов, таких как Microsoft Expression Blend) может быть затем присоединена к
управляемому коду для обеспечения деталей функциональности программы.
На заметку! Применение XAML не ограничивается приложениями WPF. Любое приложение может
использовать XAML для описания дерева объектов .NET, даже если они не имеют отношения к
визуальному пользовательскому интерфейсу. Например, в API-интерфейсе Windows Workflow
Foundation основанная на XAML грамматика применяется для определения бизнес-процессов
и специальных действий.
По мере погружения в WPF вы удивитесь тому, какую гибкость обеспечивает
"разметка рабочего стола". XAML позволяет определять не только простые элементы
пользовательского интерфейса (кнопки, таблицы, окна списков и т.п.), но также двух- и
трехмерную графику, анимацию, логику привязки данных и функциональность
мультимедиа (вроде воспроизведения видео). Например, определение круглой кнопки, которая
анимирует логотип компании, требует всего нескольких строк разметки.
Как будет показано в главе 31, элементы управления WPF могут быть
модифицированы стилями и шаблонами, что позволяет строить внешний вид приложения с
минимальными усилиями. В отличие от Windows Forms, единственной веской причиной для
построения библиотеки специальных элементов управления WPF может быть
необходимость в изменении поведения элемента управления (т.е. добавление специальных
методов, свойств или событий, переопределение виртуальных методов в подклассах). Если
вы просто хотите изменить внешний вид элемента управления (как в случае с круглой
анимированной кнопкой), это можно сделать полностью через разметку.
Обеспечение оптимизированной модели визуализации
Такие наборы инструментов для построения графических интерфейсов, как Windows
Forms, MFC или VB6, обрабатывают все запросы на визуализацию (включая
визуализацию элементов управления, подобных кнопкам и окнам списков), используя низкоуров-
Глава 27. Введение в Windows Presentation Foundation и XAML 999
невый API-интерфейс, основанный на С (GDI), который является составной частью ОС
Windows в течение многих лет. GDI обеспечивает адекватную производительность
простых графических программ; однако если пользовательскому интерфейсу приложения
нужна была высокопроизводительная графика, приходилось обращаться к DirectX.
Программная модель WPF существенно отличается в том, что при визуализации
графических данных GDI не используется^ Все операции визуализации (т.е. двух- и
трехмерная графика, анимация, визуализация графических интерфейсных
элементов) теперь используют API-интерфейс DirectX. Очевидной выгодой этого является тот
факт, что приложение WPF автоматически использует преимущества аппаратной и
программной оптимизации. К тому же приложения WPF могут задействовать очень
развитые графические службы (эффекты размытия, сглаживания, прозрачности и т.п.) без
сложностей, присущих прямому программированию для API-интерфейса DirectX.
На заметку! Хотя WPF выносит все запросы визуализации на уровень DirectX, нельзя утверждать,
что приложение WPF будет работать столь же быстро, как приложение, построенное на
основе неуправляемого C++ и DirectX. При разработке настольного приложения, которое требует
максимальной скорости выполнения (вроде трехмерной игры), неуправляемый C++ и DirectX
по-прежнему остаются наилучшим подходом.
Упрощение программирования сложных
пользовательских интерфейсов
Чтобы закрепить тему, повторимся еще раз: Windows Presentation Foundation (WPF) —
это новый API-интерфейс, предназначенный для построения настольных приложений,
который интегрирует различные настольные API-интерфейсы в рамках единой
объектной модели, с обеспечением четкого разделения ответственности через XAML. В
дополнение к этим важнейшим моментам приложения WPF также выигрывают от других
дополнительных средств, многие из которых будут рассматриваться в последующих
главах. Ниже кратко перечислены основные службы WPF
• Множество диспетчеров компоновки (намного больше, чем в Windows Forms) для
обеспечения исключительно гибкого контроля над размещением содержимого.
• Использование расширенного механизма привязки данных для связи
содержимого с элементами пользовательского интерфейса разнообразными способами.
• Встроенный механизм стилей, позволяющий определять "темы" для приложения
WPF.
• Использование векторной графики, которая позволяет автоматически изменять
размеры содержимого для соответствия размеру и разрешению экрана,
принимающего приложение.
• Поддержка двух- и трехмерной графики, анимации и воспроизведения видео и
аудио.
• Развитый типографский API-интерфейс, поддерживающий документы XML Paper
Specification (XPS), фиксированные документы (WYSIWYG), документы
нефиксированного формата и аннотации в документах (например, API-интерфейс Sticky Notes).
• Поддержка взаимодействия с унаследованными моделями графического
интерфейса (т.е. Windows Forms, ActiveX и Win32 HWND). Например, можно встраивать
специальные элементы управления Windows Forms в приложение WPF и наоборот.
Теперь, имея представление о вкладе WPF в платформу, рассмотрим различные типы
приложений, которые могут быть созданы с использованием этого API-интерфейса.
1000 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Различные варианты приложений WPF
API-интерфейс WPF может использоваться для построения широкого
разнообразия приложений с графическим интерфейсом, которые в основном отличаются
структурой навигации и моделями развертывания. Ниже будут представлены их краткие
описания.
Традиционные настольные приложения
Первая (и наиболее популярная) форма — это традиционная исполняемая сборка,
которая запускается на локальной машине. Например, WPF можно использовать для
построения текстового редактора, программы рисования или мультимедийной
программы, такой как цифровой музыкальный проигрыватель, средство просмотра фотографий
и т.п. Подобно любому другому настольному приложению, эти файлы *.ехе могут
устанавливаться традиционными средствами (программами установки, пакетами Windows
Installer и т.п.) или же посредством технологии ClickOnce, позволяющей распространять
и устанавливать настольные приложения через удаленный веб-сервер.
Говоря языком программиста, этот тип приложений WPF использует (как минимум)
типы Window и Application в дополнение к ожидаемому набору диалоговых окон,
панелей инструментов, панелей состояния, систем меню и прочих элементов
пользовательского интерфейса.
WPF позволяет строить как базовые, простые бизнес-приложения без каких-либо
излишеств, так и встраивать средства подобного рода. На рис. 27.1 показан пример
настольного приложения WPF для просмотра медицинских карточек пациентов в
учреждении здравоохранения.
к < Patient Monitoring
Рис. 27.1. Настольное приложение WPF с развитым интерфейсом
Глава 27. Введение в Windows Presentation Foundation и XAML 1001
К сожалению, на печатной странице невозможно отразить весь набор средств
данного окна. Обратите внимание, что в правом верхнем углу главного окна отображается
график реального времени, показывающий синусный ритм пациента. Если щелкнуть
на кнопке Patient Details (Информация о пациенте) в нижнем правом углу, произойдут
несколько анимаций, которые трансформируют пользовательский интерфейс к
следующему виду (рис. 27.2).
Рис. 27.2. Трансформации и анимации очень легко реализуются с помощью WPF
Можно ли построить подобное приложение без WPF? Безусловно. Однако объем и
сложность кода будут намного выше.
На заметку! Этот пример приложения доступен для загрузки (вместе с исходным кодом) на
официальном веб-сайте WPF по адресу http://windowsclient.net. Здесь можно найти
множество примеров проектов WPF (и Windows Forms), разборов технологии и форумов.
WPF-приложения на основе навигации
Приложения WPF могут также использовать структуру на основе навигации, которая
позволяет традиционному настольному приложению вести себя подобно приложению
веб-браузера. Применяя эту модель, можно построить настольную программу *.ехе,
которая включает в себя кнопки "вперед" и "назад", позволяющие конечному
пользователю перемещаться вперед и назад по различным экранам пользовательского интерфейса,
именуемым страницами. Само приложение поддерживает список страниц и
обеспечивает необходимую инфраструктуру для навигации по ним, попутно передавая данные
и поддерживая список хронологии. Для примера посмотрите на проводник Windows
(рис. 27.3), в котором используется эта функциональность. Обратите внимание, что
кнопки навигации (и список хронологии) находятся в верхнем левом углу окна.
1002 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Рис. 27.3. Настольная программа на основе навигации
Несмотря на то что настольное приложение WPF может принимать веб-подобную
схему навигации, помните, что это всего лишь вопрос дизайна пользовательского
интерфейса. Само приложение остается в виде той же исполняемой сборки, запускаемой
на настольной машине, и помимо внешнего сходства не имеет никакого отношения к
веб-приложениям. Говоря языком программистов, эта разновидность WPF-приложений
построена с использованием таких типов, как Application, Page, NavigationWindow
и Frame.
Приложения ХВАР
WPF также позволяет строить приложения, которые могут размещаться внутри
веб-браузера. Такая разновидность приложений WPF называется браузерными
приложениями XAML, или ХВАР. Согласно этой модели, конечный пользователь переходит
по заданному URL-адресу, указывающему на приложение ХВАР (которое представляет
собой коллекцию объектов Page), затем прозрачно загружает и устанавливает его на
локальной машине. В отличие от традиционной установки исполняемого приложения
с помощью ClickOnce, программа ХВАР располагается непосредственно в браузере
и принимает встроенную систему навигации браузера. На рис. 27.4 показана ХВАР-
программа в действии (а именно — пример WPF Expenselt, поставляемый в составе .NET
Framework 4.0 SDK).
Преимущество технологии ХВАР состоит в том, она позволяет создавать сложные
пользовательские интерфейсы, которые являются более выразительными, чем
типичная веб-страница, построенная с помощью HTML и JavaScript. Объект Page в WPF
может использовать те же службы, что и настольное приложение WPF, включая анимации,
двух- и трехмерную графику, темы и т. п. По сути, веб-браузер в данном случае —
просто контейнер объектов Page, а не средство отображения веб-страниц ASP.NET
Однако, учитывая, что объекты Page развертываются на удаленном веб-сервере,
приложения ХВАР можно легко сопровождать в разных версиях и обновлять без
необходимости поставки исполняемых сборок на пользовательские настольные машины.
Подобно традиционному веб-приложению, объекты Page можно легко обновлять на
вебсервере, и пользователь всегда будет получать самую актуальную версию, обращаясь по
заданному URL-адресу.
Возможным недостатком этой разновидности программ WPF является то, что ХВАР
могут работать только внутри веб-браузеров Microsoft Explorer или Firefox. При
развертывании такого приложения в корпоративной сети компании совместимость браузеров
не должна быть проблемой, так как системные администраторы могут просто диктовать
Глава 27. Введение в Windows Presentation Foundation и XAML 1003
выбор браузера, обязательного для установки на пользовательских машинах. Однако,
открывая доступ к ХВАР-приложению внешнему миру, невозможно гарантировать, что
каждый пользователь будет работать с браузером Internet Explorer или Firefox, а потому
некоторые из них просто не смогут его просмотреть.
Другая проблема состоит в том, что машина, которая выполняет ХВАР-приложение,
должна иметь локальную установку платформы .NET, поскольку объекты Page
пользуются теми же сборками .NET, что и традиционные приложения. Учитывая это, ХВАР-
приложения ограничены только средами Windows и не могут просматриваться на
системах, работающих под управлением Mac OS или Linux.
Рис. 27.4. Программы ХВАР загружаются на локальную машину и выполняются
внутри веб-браузера
Отношения между WPF и Silverlight
WPF и XAML также предоставляют фундамент для межплатформенной, межбрау-
зерной, основанной на WPF технологии, которая называется Silverlight. На самом
высоком уровне Silverlight можно рассматривать скорее как конкурента Adobe Flash, но
с преимуществами использования С# и XAML, а не как новый набор инструментов и
языков. Silverlight является подмножеством функциональности WPF, используемым для
построения интерактивных подключаемых модулей для более крупных HTML-страниц.
Однако в действительности Silverlight — это совершенно уникальный дистрибутив
платформы .NET, включающий в себя уменьшенные версии среды CLR и библиотек
базовых классов .NET.
В отличие от ХВАР, устанавливать .NET Framework на машине пользователя не
требуется. До тех пор, пока целевая машина имеет установленную исполняющую среду
Silverlight, браузер будет загружать ее и отображать приложения Silverlight
автоматически. А лучше всего то, что дополнения Silverlight не ограничены операционными
системами Windows. В Microsoft также разработали исполняющую среду Silverlight для Mac OS.
На заметку! Проект Mono (см. приложение Б) предлагает версию Silverlight с открытым исходным
кодом под названием Moonlight, которая предназначена для операционных систем Linux. Этот
API-интерфейс в сочетании с Silverlight предоставляет действительно межплатформенную
модель для построения исключительно насыщенных веб-страниц.
1004 Часть VI. Построение настольных пользовательских приложений с помощью WPF
С помощью Silverlight можно строить исключительно многофункциональные (и
интерактивные) веб-приложения. Например, подобно WPF, Silverlight обладает векторной
системой графики, поддержкой анимации и поддержкой мультимедиа. Более того, в
приложения можно включать подмножество библиотеки базовых классов .NET. Это
подмножество содержит набор элементов управления WPF, поддержку LINQ, обобщенные
типы коллекций, поддержку веб-служб и полезное подмножество mscorlib.dll
(файловый ввод-вывод, манипулирование XML и т.п.).
На заметку! В этом издании Silverlight подробно не рассматривается, однако большая часть
знаний WPF пригодится при конструировании подключаемых модулей Silverlight. Дополнительные
сведения об этом API-интерфейсе доступны по адресу http://silverlight.net.
Исследование сборок WPF
Независимо от того, какого типа приложение WPF вы собираетесь строить, в
конечном итоге WPF — это лишь немногим более чем коллекция типов, встраиваемых в
сборки WPF. В табл. 27.3 описаны основные сборки, используемые для построения
приложений WPF, ссылка на каждую из которых должна включаться при создании нового
проекта (как и следовало ожидать, WPF-проекты в Visual Studio 2010 и Expression Blend
автоматически получают ссылки на необходимые сборки).
Таблица 27.3. Основные сборки WPF
Сборка Назначение
PresentationCore.dll Эта сборка определяет многочисленные типы,
составляющие фундамент уровня графического интерфейса в WPR
Например, она включает поддержку интерфейса WPF Ink
API (для программирования перьевого ввода для Pocket PC
и Tablet PC), несколько примитивов анимации (через
пространство имен System.Windows.Media.Animation) и
множество типов визуализации графики
PresentationFoundation.dll Эта сборка содержит большинство элементов управления
WPF, классы Application и Window, а также поддержку
интерактивных двухмерных геометрий. К тому же эта
библиотека предоставляет базовую функциональность для
чтения и записи документов XAML во время выполнения
System.Xaml.dll Эта сборка предоставляет пространства имен, которые
позволяют программно взаимодействовать с документами
XAML во время выполнения. В основном эта сборка нужна
только в том случае, если разрабатываются инструменты
поддержки WPF или нужен абсолютный контроль над XAML
во время выполнения
WindowsBase.dll Эта сборка определяет основные типы, составляющие
инфраструктуру API-интерфейса WPF. Здесь находятся типы потоков
WPF, типы безопасности, различные преобразователи типов и
прочие программные примитивы (описанные в главе 29)
Все вместе эти три сборки определяют ряд новых пространств имен и сотни новых
классов .NET, интерфейсов, структур, перечислений и делегатов. Хотя за полной
информацией следует обращаться к документации по .NET Framework 4.0 SDK, некоторые
основные пространства имен описаны в табл. 27.4.
Глава 27. Введение в Windows Presentation Foundation и XAML 1005
Таблица 27.4. Основные пространства имен WPF
Пространство имен
Назначение
System.Windows
System.Windows.Controls
System. Windows.Data
System. Windows. Documents
System. Windows. In к
System. Windows. Markup
System. Windows. Media
System.Windows.Navigation
System.Windows.Shapes
Это корневое пространство имен WPF. Здесь вы найдете
основные типы (такие как Application и Window), которые
необходимы любому настольному проекту WPF
Здесь вы найдете все ожидаемые графические элементы
(виджеты) WPF, включая типы для построения систем меню,
всплывающих подсказок и многочисленные диспетчеры
компоновки
Содержит типы для работы с механизмом привязки данных
WPF, а также для поддержки шаблонов привязки данных
Содержит типы для работы с API-интерфейсом
документов, что позволяет интегрировать в WPF-приложения
функциональность в стиле PDF через протокол XML Paper
Specification (XPS)
Поддерживает Ink API — интерфейс, позволяющий получать
ввод от пера или мыши, реагировать на жесты (gesture) и
т.п. Этот API-интерфейс полезен для программ Tablet PC,
однако может использоваться и в любых WPF-приложениях
Это пространство имен определяет множество типов,
обеспечивающих программный разбор разметки XAML (вместе с
эквивалентным двоичным форматом BAML)
Это корневое пространство имен для нескольких
пространств имен, связанных с мультимедиа. Внутри этих
пространств имен определены типы для работы с анимацией,
визуализацией трехмерной графики, визуализацией текста
и прочие мультимедийные примитивы
Это пространство имен предоставляет типы для
обеспечения логики навигации, используемой браузерными
приложениями XAML (XBAP), а также настольными приложениями
на основе страничной навигационной модели
Это пространство имен определяет различные типы
двухмерной графики, автоматически реагирующей на ввод мыши
Чтобы начать путешествие по программной модели WPF, давайте рассмотрим два
члена пространства имен System.Windows, которые являются общими для разработки
всех настольных приложений: Application и Window.
На заметку! Новички в разработке настольных приложений на платформе .NET должны иметь в
виду, что сборки System.Windows.Forms.* и System.Drawing.* не связаны с WPF! Эти
библиотеки представляют изначальный инструментальный набор для построения графических
интерфейсов .NET — Windows Forms (см. приложение А).
Роль класса Application
Класс System.Windows.Application представляет глобальный экземпляр
работающего приложения WPF. В этом типе предусмотрен метод Run() (для запуска
приложения), серия событий, которые можно обрабатывать для взаимодействия с приложением
на протяжении его времени жизни (вроде Start up HExit), и ряд членов, специфичных
для браузерных приложений XAML (таких как события, инициируемые при перемеще-
1006 Часть VI. Построение настольных пользовательских приложений с помощью WPF
нии пользователя по страницам). В табл. 27.5 описаны ключевые свойства, о которых
нужно знать.
Таблица 27.5. Ключевые свойства типа Application
Свойство Назначение
Current Это статическое свойство позволяет получить доступ к работающему объекту
Application из любого места кода. Это может быть очень полезно, когда
окну или диалоговому окну нужно получить доступ к объекту Application,
который создал их, обычно для взаимодействия с переменными или
функциональностью уровня приложения
MainWindow Это свойство позволяет программно получать и устанавливать главное окно
приложения
Properties Это свойство позволяет устанавливать и получать данные, доступные через все
аспекты приложения WPF (окна, диалоговые окна и т.п.)
StartupUri Это свойство получает или устанавливает URI, который указывает окно или
страницу для автоматического открытия при запуске приложения
Windows Это свойство возвращает тип WindowCollection, который
обеспечивает доступ ко всем окнам, которые созданы в потоке, создавшем объект
Application. Это может весьма пригодиться, когда необходимо выполнить
итерацию по всем открытым окнам приложения и изменить их состояние
(например, свернуть все окна)
Конструирование класса Application
Любое WPF-приложение нуждается в определении класса, расширяющего
Application. Внутри этого класса определяется точка входа программы (метод Main ()),
которая создает экземпляр данного подкласса и обычно обрабатывает события Startup
и Exit. Чуть ниже мы рассмотрим полный проект, а пока вот быстрый пример:
// Определяет глобальный объект приложения для данной программы WPF.
class MyApp : Application
{
[STAThread]
static void Main(string[] args)
{
// Создать объект приложения.
MyApp app = new MyApp () ;
// Зарегистрировать события Startup/Exit.
app. Star tup += (s, e) => { /* Запуск приложения */ };
app.Exit += (s, e) => { /* Завершение приложения */ };
}
}
Чаще всего в обработчике события Startup будут обрабатываться входные
аргументы командной строки, и запускаться главное окно программы. Обработчик Exit, как и
следовало ожидать — это место, где помещается необходимая логика завершения
программы (сохранение пользовательских предпочтений, запись в реестр Windows и т.п.).
Перечисление элементов коллекции Application. Windows
Другое интересное свойство класса Application — это Windows, предоставляющее
доступ к коллекции, в которой представлены все окна, загруженные в память для
текущего WPF-приложения. Запомните, что создаваемые новые объекты Window автомати-
Глава 27. Введение в Windows Presentation Foundation и XAML 1007
чески добавляются в коллекцию Application.Windows. Ниже приведен пример метода,
который сворачивает все окна приложения (возможно, в ответ на нажатие
определенного сочетания клавиш или выбор пункта меню конечным пользователем):
static void MinimizeAllWindows()
{
foreach (Window wnd in Application.Current.Windows)
{
wnd.WindowState = WindowState.Minimized;
}
}
В следующем примере мы построим полный тип, унаследованный от Application.
А пока давайте рассмотрим основную функциональность типа Window и изучим в
процессе ряд важных базовых классов WPF.
Роль класса Window
Класс System.Windows.Window
представляет одиночное окно, принадлежащее
типу-наследнику Application, включая все
диалоговые окна, отображаемые главным окном. Как и
можно было ожидать, тип Window имеет ряд
родительских классов, каждый из которых
добавляет свою функциональность. На рис. 27.5
показана цепочка наследования (и реализованные
интерфейсы) типа System.Windows .Window,
как она выглядит в браузере объектов Visual
Studio 2010.
По мере чтения этой и последующих глав,
вы начнете понимать функциональность,
предлагаемую многими базовыми классами WPF.
В следующем разделе будет предоставлен
краткий перечень функциональности, предлагаемой Рис> 27.5. Иерархия наследования типа
каждым базовым классом (детали ищите в до- window
кументации по .NET Framework 4.0 SDK).
Роль класса System.Windows.Controls.ContentControl
Непосредственным предком Window является ContentControl — вероятно,
наиболее впечатляющий из всех классов WPF. Этот базовый класс обеспечивает производные
типы способностью размещать в себе содержимое, которое представлено коллекцией
объектов, помещенных на поверхность элемента управления, через свойство Content.
Модель содержимого WPF позволяет очень легко настраивать базовый вид и поведение
элемента управления Content.
Например, когда речь идет о типичном элементе управления "кнопка", то обычно
предполагается, что его содержимым будет базовый строковый литерал (OK, Cancel,
Abort и т.п.). В случае использования XAML для описания элемента управления WPF,
и значение, которое необходимо присвоить свойству Content, может быть выражено в
виде простой строки, можете установить свойство Content внутри открывающего
определения элемента, как показано ниже:
<!-- Явная установка значения Content -->
<Button Height="80" Width=00" Content="ClickMe"/>
Browse My Solution
НШШШшшшяшшя
< Search»
чтжт
*-Q| Base Types
л ^ ContentControl
л •% Control
л ^J FrameworkElement
л ~o IFrameworklnputElement
~° DnputElement
-0 DnputElement
*o IQueryAmbierrt
*4> ISupportlmtialize
л % UlElement
~° lAnimatable
*"° DnputElement
л ij Visual
л I» DependencyObject
л -А% DispatcherObject
*% Object
*o lAddChild ra|
1008 Часть VI. Построение настольных пользовательских приложений с помощью WPF
На заметку! Свойство Content может также устанавливаться в коде С#, что позволяет изменять
внутренности элемента управления во время выполнения.
Содержимое может быть любым. Например, предположим, что нужна "кнопка",
которая содержит в себе нечто более интересное, чем простая строка, возможно,
специальную графику или текст. На других платформах построения пользовательских
интерфейсов, таких как Windows Forms, пришлось бы строить специальный элемент управления,
что потребовало бы написания значительного объема кода и сопровождения нового
класса. С моделью содержимого WPF это не требуется.
Когда в свойстве Content должно быть установлено значение, которое не может
быть выражено простым массивом символов, его нельзя присвоить с использованием
атрибута в открывающем определении элемента управления. Вместо этого понадобится
определить данные содержимого неявно, внутри контекста элемента. Например,
показанный ниже элемент <Button> включает содержимое <StackPanel>, которое само
содержит некоторые уникальные данные (а именно — <Ellipse> и <Label>):
<•-- Неявная установка в свойстве Content сложных данных -->
<Button Height="80" Width=00">
<StackPanel>
<Ellipse Fill = ,,Red" Width=,,25" Height=5,,/>
<Label Content ="OK|,,/>
</StackPanel>
</Button>
Для установки сложного содержимого можно также использовать синтаксис XAML
вида свойство-элемент. Рассмотрим следующий функциональный эквивалент
определения <Button>, который устанавливает свойство Content явно с помощью синтаксиса
"свойство-элемент" (дополнительную информацию о XAML вы найдете далее в главе):
<!— Установка свойства Content с использованием синтаксиса "свойство-элемент" —>
<Button Height="80" Width=00">
<Button.Content>
<StackPanel>
<Ellipse Fill="Red" Width=5" Height=5"/>
<Label Content ="OK!"/>
</StackPanel>
<Button.Content>
</Button>
Имейте в виду, что не каждый элемент WPF унаследован от ConentConrtol и потому
не все элементы поддерживают эту уникальную модель содержимого. К тому же
некоторые элементы управления WPF вносят некоторые дополнения к только что
рассмотренной модели. В главе 28 роль содержимого WPF рассматривается более подробно.
Роль класса System.Windows.Controls.Control
В отличие от ContentControl, все элементы управления WPF разделяют базовый
класс Control в качестве общего предка. Этот базовый класс предоставляет
множество членов, незаменимых для обеспечения функциональности пользовательского
интерфейса. Например, Control определяет свойства для установки размеров элемента
управления, прозрачности, порядка обхода по нажатию клавиши <ТаЬ>, дисплейного
курсора, цвета фона и т.д. Более того, этот родительский класс обеспечивает поддержку
шаблонных служб. Как объясняется в главе 31, элементы управления WPF могут
полностью изменять свой внешний вид, используя шаблоны и стили. В табл. 27.6 описаны
некоторые ключевые члены типа Control, сгруппированные по функциональности.
Глава 27. Введение в Windows Presentation Foundation и XAML 1009
Таблица 27.6. Ключевые члены типа Control
Член Назначение
Background, Foreground, BorderBrush, Эти свойства позволяют устанавливать базовые
BorderThickness, Padding, настройки, касающиеся того, как элемент управ-
HorizontalContentAlignment, ления будет визуализирован и позиционирован
Vert icalContent Alignment
FontFamily, FontSize, FontStretch, Эти свойства управляют настройками шрифтов
FontWeight
IsTabStop, Tablndex Эти свойства используются для установки
порядка обхода элементов управления в окне
(по нажатию <ТаЬ>)
MouseDoubleClick, Эти события обрабатывают двойной щелчок
PreviewMouseDoubleClick навиджете
Template Это свойство позволяет получать и устанавливать
шаблон элемента, который может быть
использован для изменения вывода визуализации виджета
Роль класса System.Windows.FrameworkElement
Этот базовый класс предоставляет множество низкоуровневых членов, которые
используются повсюду в WPF, например, для поддержки раскадровки (для анимации),
привязки данных, а также возможности именования членов (через свойство Name),
получения ресурсов, определенных производным типом, и установки общих измерений
производного типа. Ключевые члены перечислены в табл. 27.7.
Таблица 27.7. Ключевые члены типа FrameworkElement
Член Назначение
ActualHeight, ActualWidth, Управляют размерами производного типа
MaxHeight, MaxWidth, MinHeight,
MinWidth, Height, Width
ContextMenu
Cursor
HorizontalAlignment,
VerticalAlignment
Name
Resources
ToolTip
Получает или устанавливает всплывающее
меню, ассоциированное с производным типом
Получает или устанавливает курсор мыши,
ассоциированный с производным типом
Управляет позиционированием типа внутри
контейнера (такого как панель или окно списка)
Позволяет назначать имя типу, чтобы
обращаться к его функциональности в файле кода
Предоставляет доступ к любому ресурсу,
определенному типом (система ресурсов WPF
объясняется в главе 30)
Получает или устанавливает всплывающую
подсказку, ассоциированную с производным типом
1010 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Роль класса System.Windows.UIElement
Из всех типов в цепочке наследования Window базовый класс UIElement предлагает
максимум функциональности. Его ключевая задача — обеспечивать производный тип
многочисленными событиями, чтобы он мог принимать фокус и обрабатывать
входящие запросы. Например, в этом классе предусмотрены многочисленные события для
обслуживания операций перетаскивания, перемещений курсора мыши, клавиатурного
ввода и ввода с помощью пера (для Pocket PC и Tkblet PC).
Модель событий WPF будет подробно описана в главе 29; однако многие из
основных событий покажутся знакомыми (MouseMove, KeyUp, MouseDown, MouseEnter,
MouseLeave и т.п.). В дополнение к определению десятков событий, этот родительский
класс предоставляет множество свойств для управления фокусом, состоянием
доступности, видимостью и логикой проверки попаданий (табл. 27.8).
Таблица 27.8. Ключевые члены типа UIElement
Член Назначение
Focusable, IsFocused Позволяют устанавливать фокус на определенный производный тип
isEnabled Позволяет управлять доступностью определенного производного типа
IsMouseDirectlyOver, Предлагают простой способ выполнения логики проверки попадания
IsMouseOver
IsVisible, Visibility Позволяют работать с установкой видимости производного типа
RenderTransf orm Позволяет устанавливать трансформацию, которая будет
использована для визуализации производного типа
Роль класса Sys tem. Windows. Media .Visual
Класс Visual предлагает основную поддержку визуализации в WPF, включая
проверку попадания на визуализированные данные, трансформацию координат и вычисления
границ. Фактически для рисования данных на экране класс Visual взаимодействует с
подсистемой DirectX. Как объясняется в главе 29, WPF поддерживает три возможных
способа визуализации графических данных, каждый из которых отличается от других
в отношении функциональности и производительности. Применение типа Visual (и его
потомков вроде DrawingVisual) обеспечивает наиболее легковесный способ
визуализации графических данных, но также подразумевает участие большого объема
управляемого кода для обеспечения работы всех необходимых служб. Более подробно об этом
речь пойдет в главе 29.
Роль класса Sys tem. Windows. DependencyObj ec t
WPF поддерживает специальную разновидность свойств .NET, именуемую
свойствами зависимости (dependency properties). Этот подход позволяет типу вычислять
значение свойства на основе значений других свойств (отсюда и "зависимость"). Чтобы
тип участвовал в этой новой схеме, он должен быть унаследован от базового класса
DependencyObject. Вдобавок DependencyObject позволяет типам-наследникам
поддерживать присоединяемые свойства (attached properties), которые представляют собо^
форму свойств зависимости, очень удобную при программировании в модели привязки
данных WPF, а также при установке элементов пользовательского интерфейса внутри
различных типов панелей WPF.
Глава 27. Введение в Windows Presentation Foundation и XAML 1011
Базовый класс Dependency Object предоставляет два ключевых метода для всех
производных типов: GetValueO и SetValue(). С помощью этих членов можно
устанавливать само свойство. Другие части инфраструктуры позволяют "регистрировать" тех,
кто может использовать свойства зависимости или присоединяемые свойства.
Хотя свойства зависимости — это ключевой аспект разработки WPF, большую часть
времени их детали скрыты от глаз. В главе 29 содержатся дополнительные подробности
об этом "новом" типе свойств.
Роль класса System.Windows.Threading.DispatcherObject
И последний базовый класс типа Window (помимо System.Object, который здесь
не требует дополнительных пояснений) — DispatherObject. Этот тип включает одно
свойство, представляющее интерес — Dispatcher, которое возвращает
ассоциированный объект System.Windows.Threading.Dispatcher. Класс Dispatcher — это точка
входа в очередь событий приложения WPF, предоставляющая базовые конструкции для
работы с параллелизмом и многопоточностью. По большому счету, это низкоуровневый
класс, который в большинстве приложений WPF может быть проигнорирован.
Построение приложения WPF без XAML
Учитывая всю функциональность, предлагаемую родительскими классами типа
Window, представить окно в приложении можно, либо напрямую создав объект Window,
либо указав этот класс в качестве родительского для строго типизированного
наследника. В следующем примере рассматриваются оба подхода. Хотя большинство
приложений WPF используют XAML, так поступать вовсе не обязательно. Все, что может быть
выражено на XAML, также можно выразить в коде и (по большей части) наоборот. При
желании можно построить полный проект WPF, используя лежащую в основе объектную
модель и процедурный код.
Чтобы проиллюстрировать сказанное, давайте построим минимальное, но полное
приложение без применения XAML, работая с классами Application и Window напрямую.
Создайте новое консольное приложение по имени Wpf AppAllCode. Откройте диалоговое
окно Add Reference (Добавление ссылки) и добавьте ссылку на сборки WindowBase.dll,
PresentationCore.dll, System.Xaml.dll и PresentationFramework.dll.
Поместите в начальный файл С# следующий код, который создает главное окно с
минимальной функциональностью:
// Простое приложение WPF, написанное без XAML.
using System;
using System.Windows;
using System.Windows.Controls;
namespace WpfAppAllCode
{
// В этом первом примере определяется один класс для
// представления самого приложения и главного окна.
class Program : Application
{
[STAThread]
static void Hain (string [ ] arqzs)
{
// Обработка событий Startup и Exit с последующим запуском приложения.
Program app = new Program();
арр.Startup += AppStartUp;
app. Exit i-= AppExit;
app.Run(); // Инициирует событие Startup.
}
1012 Часть VI. Построение настольных пользовательских приложений с помощью WPF
static void AppExit(object sender, ExitEventArgs e)
{
MessageBox.Show ("App has exited");
}
static void AppStartUp(object sender, StartupEventArgs e)
{
// Создать объект Window и установить некоторые базовые свойства.
Window mainWindow = new Window();
mainWindow.Title = "My First WPF App!";
mainWindow.Height = 200;
mainWindow.Width = 300;
mainWindow.WindowStartupLocation = WindowStartupLocation.CenterScreen;
mainWindow.Show();
}
}
}
На заметку! Метод Main() в приложении WPF должен быть снабжен атрибутом [STAThread],
что гарантирует безопасность к потокам унаследованных СОМ-объектов, используемых
приложением. Если не аннотировать Main() подобным образом, возникнет исключение времени
выполнения.
Обратите внимание, что класс Program расширяет класс System.Windows.
Application. Внутри метода Main() создается экземпляр объекта приложения, затем
обрабатываются события Startup и Exit с помощью синтаксиса группового
преобразования методов. Вспомните из главы 11, что эта сокращенная нотация исключает
необходимость вручную указывать делегаты, используемые определенным событием.
Разумеется, при желании можно указывать лежащие в основе делегаты прямо по
имени. В следующем модифицированном методе Main() обратите внимание, что
событие Startup работает в сочетании с делегатом StartupEventHandler, который может
указывать только на методы, принимающие Object в качестве первого параметра и
StartupEventArgs — в качестве второго. Событие Exit, с другой стороны, работает с
делегатом ExitEventHandler, который требует, чтобы указанный им метод принимал
тип ExitEventArgs во втором параметре:
[STAThread]
static void Main(string[] args)
{
//На этот раз специфицируем лежащие в основе делегаты.
MyWPFApp app = new MyWPFApp();
app.Startup += new StartupEventHandler(AppStartUp);
app.Exit += new ExitEventHandler(AppExit);
app.Run(); // Инициирует событие Startup.
}
В любом случае метод AppStartUp () сконфигурирован для создания объекта Window,
выполнения некоторых установок базовых свойств и вызова Show() для отображения
окна на экране в немодальном режиме (метод ShowDialogO может применяться для
открытия модального диалогового окна). Метод AppExit() просто использует класс
MessageBox из WPF для отображения диагностического сообщения при завершении
приложения.
После компиляции и запуска проекта вы обнаружите очень простое главное окно,
которое может быть свернуто, развернуто и закрыто. Чтобы немного украсить его,
понадобится добавить некоторые элементы пользовательского интерфейса. Но прежде
чем сделать это, давайте переработаем код с использованием строго типизированного
и хорошо инкапсулированного класса-наследника Window.
Глава 27. Введение в Windows Presentation Foundation и XAML 1013
Создание строго типизированного окна
Сейчас класс-наследник Application при запуске приложения напрямую создает
экземпляр типа Window. В идеале нужно было бы создать класс, унаследованный от
Window, чтобы инкапсулировать его внешний вид и функциональность. Предположим,
что создано следующее определение класса в текущем пространстве имен Wpf AppAllCode
(если этот класс помещен в новый файл С#, необходимо импортировать пространство
имен System.Windows):
class MainWindow : Window
{
public MainWindow(string windowTitle, int height, int width)
{
this.Title = windowTitle;
this.WindowStartupLocation = WindowStartupLocation.CenterScreen;
this.Height = height;
this.Width = width;
}
}
Теперь можно модифицировать обработчик событий Startup для прямого создания
экземпляра MainWindow:
static void AppStartUp(object sender, StartupEventArgs e)
{
// Создать объект MainWindow.
MainWindow wnd = new MainWindow ("My better WPF App! ", 200, 300);
wnd.Show();
}
После компиляции и запуска программы получается вывод, идентичный
предыдущей версии. Очевидное преимущество состоит в том, что теперь есть строго
типизированный класс окна для построения.
На заметку! Когда создается объект window (или производный от window), он автоматически
добавляется к внутренней коллекции окон класса Application (через некоторую логику
конструктора, находящуюся в самом классе Window). С помощью свойства Application.Windows
можно проходить по списку объектов Window, находящихся в данный момент в памяти.
Создание простого пользовательского интерфейса
Добавление элементов пользовательского интерфейса к Window включает
выполнение перечисленных ниже базовых шагов.
1. Определить переменную-член для представления нужного элемента управления.
2. Сконфигурировать внешний вид и поведение элемента управления при
конструировании объекта Window.
3. Присвоить элемент управления унаследованному свойству Content или, в качестве
альтернативы — передать его в параметре унаследованному методу AddChild().
Вспомните, что модель содержимого элемента управления WPF требует, чтобы
свойство Content устанавливалось только один раз. Разумеется, окно с единственным
элементом управления было бы не особенно бесполезным. Поэтому почти в каждом случае
"единственной порцией содержимого", которая присваивается свойству Content, на
самом деле является диспетчер компоновки, такой как DockPanel, Grid, Canvas или
StackPanel. Внутри диспетчера компонозки можно иметь любую комбинацию внутрен-
1014 Часть VI. Построение настольных пользовательских приложений с помощью WPF
них элементов, в том числе другие вложенные диспетчеры компоновки (более подробно
этот аспект разработки WPF описан в главе 28).
Пока что добавим единственный объект типа Button к объекту-наследнику Window.
В результате щелчка на этой кнопке текущее окно будет закрываться, что неявно
прервет приложение, поскольку других окон в памяти не существует. Посмотрите на
следующую модификацию класса MainWindow (не забудьте импортировать System.
Windows.Controls для получения доступа к классу Button):
class MainWindow : Window
{
// Наш элемент пользовательского интерфейса.
private Button btnExitApp = new Button();
public MainWindow(string windowTitle, int height, int width)
{
// Сконфигурировать кнопку и установить как дочерний элемент управления.
btnExitApp.Click += new RoutedEventHandler(btnExitApp_Clicked) ;
btnExitApp.Content = "Exit Application";
btnExitApp.Height = 25;
btnExitApp.Width = 100;
// Установить в качестве содержимого окна единственную кнопку.
this.AddChild(btnExitApp);
// Сконфигурировать окно.
this.Title = windowTitle;
this.WindowStartupLocation = WindowStartupLocation.CenterScreen;
this.Height = height;
this.Width = width;
this.Show ();
}
private void btnExitApp_Clicked(object sender, RoutedEventArgs e)
{
// Закрыть окно.
this.Close ();
}
}
Однако обратите внимание, что событие Click кнопки WPF работает в сочетании с
делегатом по имени RoutedEventHandler. Это вызывает очевидный вопрос: что такое
маршрутизируемое событие? Модели событий WPF подробно рассматриваются в
следующей главе, а пока просто учтите, что цели делегата RoutedEventHandler должны
принимать Object в качестве первого параметра и RoutedEventArgs — в качестве второго.
После компиляции и запуска приложения отображается измененное окно,
показанное на рис. 27.6. Кнопка автоматически помещена в центр клиентской области окна, что
является поведением по умолчанию, когда содержимое не помещено в тип панели WPF.
Рис. 27.6. Простое WPF-приложение, написанное полностью на С#
Глава 27. Введение в Windows Presentation Foundation и XAML 1015
Взаимодействие с данными уровня приложения
Вспомните, что в классе Application имеется свойство по имени Properties,
которое позволяет определить коллекцию пар "имя/значение" через индексатор типа.
Поскольку этот индексатор определен для операций над типом System.Object, в
коллекции можно сохранять элементы любого рода (включая экземпляры пользовательских
классов) для последующего извлечения по дружественному имени. Используя этот
подход, можно очень просто разделить данные среди всех окон в приложении WPF.
Для целей иллюстрации модифицируем текущий обработчик событий, чтобы он
проверял входящие параметры командной строки на присутствие значения /GODMODE
(распространенный "мошеннический" код для многих игр). Если эта лексема найдена,
внутри коллекции свойств значение bool под именем GodMode устанавливается в true
(в противном случае — в false).
Звучит достаточно просто, тем не менее, может возникнуть один вопрос: как
передать обработчику события Startup входной аргумент командной строки (обычно
получаемый методом Main ())? Один из подходов предусматривает вызов статического метода
Environment.GetCommandLineArgs(). Однако те же самые аргументы автоматически
добавляются во входной параметр StartupEventArgs и доступны через свойство Args.
С учетом этого, ниже показано первое обновление текущей кодовой базы:
static void AppStartUp (object sender, StartupEventArgs e)
{
// Проверить входящие аргументы командной строки
//на предмет наличия флага /GODMODE.
Application.Current.Properties["GodMode"] = false;
foreach(string arg in e.Args)
{
if (arg.ToLower() == "/godmode")
{
Application.Current.Properties["GodMode"] = true;
break;
}
}
// Создать объект MainWindow.
MainWindow wnd = new MainWindc vi ("Ily better WPF App! ", 200, 300) ;
}
Данные уровня приложения доступны везде внутри WPF-приложения. Все, что
нужно для этого сделать — получить точку доступа к глобальному объекту приложения
(через Application. Си г rent) и исследовать коллекцию. Например, обработчик событий
Click для Button можно было бы изменить следующим образом:
private void btnExitApp_Clicked(object sender, RoutedEventArgs e)
{
// Включил ли пользователь /godmode?
if ( (bool)Application.Current.Properties["GodMode"])
MessageBox.Show("Cheater'");
this.Close();
}
Если теперь конечный пользователь запустит программу следующим образом:
WpfAppAllCode.exe /godmode
то получит окно сообщения, отображаемое по завершению приложения.
1016 Часть VI. Построение настольных пользовательских приложений с помощью WPF
На заметку! Вспомните, что аргументы командной строки можно указывать и в Visual Studio Для
этого щелкните на значке Properties (Свойства) в окне Solution Explorer, перейдите на вкладку
Debug (Отладка) и введите /godmode в поле Command line arguments (Аргумента
командной строки).
Обработка закрытия объекта Window
Конечные пользователи могут завершить работу окна, применяя для этого
многочисленные встроенные средства уровня системы (например, щелкнув на кнопке закрытия
X на рамке окна) или непосредственно вызвав метод Close () в ответ на некоторое
действие пользователя с интерактивным элементом (например, File^Exit (Файло Выход)).
В любом случае WPF предлагает два события, которые можно перехватить для
определения того, действительно ли пользователь намерен остановить работу окна и удалить
его из памяти. Первое такое событие — это Closing, которое работает в сочетании с
делегатом CancelEventHandler.
Этот делегат ожидает целевой метод, принимающий System.Component Model.
CancelEventArgs во втором параметре. CancelEventArgs предоставляет свойство
Cancel, которое, будучи установленным в true, предотвратит действительное
закрытие окна (это удобно, когда необходимо спросить пользователя, действительно ли он
хочет закрыть окно или, возможно, сначала следует сохранить свою работу).
Если пользователь действительно желает закрыть окно, то CancelEventArgs .Cancel
можно установить в false, что затем приведет к выдаче события Closed (работающего
с делегатом System. Event Handle г), представляющего собой точку, в которой окно
полностью и безвозвратно готово к закрытию.
Давайте модифицируем класс MainWindow для обработки этих двух событий,
добавив следующие операторы кода к текущему конструктору:
public MainWindow(string windowTitle, int height, int width)
{
this.Closing += MainWindow_Closing;
this.Closed += MainWindow_Closed;
}
Теперь реализуем обработчики событий, как показано ниже:
private void MainWindow_Closing(object sender,
System.ComponentModel.CancelEventArgs e)
{
// Проверить, действительно ли пользователь желает закрыть окно.
string msg = "Do you want to close without saving?";
MessageBoxResult result = MessageBox.Show(msg,
"MyApp", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (result == MessageBoxResult.No)
{
// Если не хочет, отменить закрытие. ,
е.Cancel = true;
}
}
private void MainWindow_Closed(object sender, EventArgs e)
{
MessageBox.Show("See ya!");
Глава 27. Введение в Windows Presentation Foundation и XAML 1017
Рис. 27.7. Перехват события
Closing объекта Window
Запустите программу и попытайтесь закрыть
окно, щелкнув либо на значке X в правом верхнем
углу окна, либо на кнопке. Должно открыться
показанное на рис. 27 7 диалоговое окно с запросом
подтверждения.
Щелчок на кнопке Yes (Да) приведет к завершению
приложения, а щелчок на кнопке No (Нет) оставит
окно в памяти.
Перехват событий мыши
API-интерфейс WPF предоставляет множество событий, которые можно
перехватывать для организации взаимодействия с мышью. В частности, в базовом классе
UIElement определены такие события мыши, как MouseMove, MouseUp, MouseDown,
MouseEnter, MouseLeave и т.д.
Рассмотрим, например, обработку события MouseMove. Это событие работает в
сочетании с делегатом System.Windows.Input.MouseEventHandler, который ожидает, что
его целевая функция будет принимать тип System.Windows.Input.MouseEventArgs
во втором параметре. Используя MouseEventArgs (как в приложении Windows Forms),
можно извлечь координаты позиции (х, у) курсора мыши и прочие связанные с ним
детали. Рассмотрим следующее частичное определение:
public class MouseEventArgs : InputEventArgs
public Point GetPosition(IlnputElement relativeTo);
public MouseButtonState LeftButton { get; }
public MouseButtonState MiddleButton { get; }
public MouseDevice MouseDevice { get; }
public MouseButtonState RightButton { get; }
public GtylusDevice StylusDevice { get; }
public MouseButtonState XButtonl { get; }
public MouseButtonState XButton2 { get; }
}
На заметку! Свойства XButtonl и XButton2 позволяют взаимодействовать с "расширенными
кнопками мыши" (такими как кнопки "вперед" и "назад", которые имеются в некоторых
устройствах) Они часто используются для взаимодействия с хронологией навигации в браузере для
переходов между посещенными страницами.
Метод GetPosition() позволяет получать значение (х, у) относительно элемента
пользовательского интерфейса в окне. Если вы заинтересованы в захвате позиции
относительно активного окна, просто передайте this. Обработаем событие MouseMove в
конструкторе класса MainWindow следующим образом:
public MainWindow(string windowTitle, int height, int width)
{
this.MouseMove += MainWindow_MouseMove;
}
Ниже приведен обработчик события MouseMove, который отобразит
местоположение мыши в области заголовка окна (обратите внимание, что возвращенный тип Point
транслируется в строковое значение с помощью ToStnngO):
1018 Часть VI. Построение настольных пользовательских приложений с помощью WPF
protected void MainWindow_MouseMove (object sender,
System.Windows.Input.MouseEventArgs e)
{
// Установить в заголовке окна текущие координаты X,Y мыши.
this.Title = e.GetPosition(this) .ToStringO ;
}
Перехват клавиатурных событий
Обработка клавиатурного ввода также очень проста. UIElement определяет ряд
событий, которые можно перехватывать для отслеживания нажатий клавиш клавиатуры
на активном элементе (например, KeyUp, KeyDown). Оба события — KeyUp и KeyDown —
работают с делегатом System.Windows.Input.KeyEventHandler, который ожидает
второго параметра типа KeyEventArgs, определяющего несколько важных общедоступных
свойств:
public class KeyEventArgs : KeyboardEventArgs
public bool IsDown { get; }
public bool IsRepeat { get; }
public bool IsToggled { get; }
public bpol IsUp { get; }
public Key Key { get; }
public KeyStates KeyStates { get; }
public Key SystemKey { get; }
}
Чтобы проиллюстрировать обработку события KeyDown в конструкторе MainWindow
(как это сделано для предыдущего события) и реализации обработчика события, который
изменяет содержимое кнопки текущей нажатой клавишей, используйте следующий код:
private void MainWindow_KeyDown (
object sender, System.Windows.Input.KeyEventArgs e)
{
// Отобразить на кнопке нажатую клавишу.
btnExitApp.Content = е.Key.ToString ();
В качестве последнего штриха дважды щелкните
на значке Properties (Свойства) в окне Solution Explorer
и на вкладке Application (Приложение) установите для
Output Type (Тип вывода) в Windows Application (Windows-
приложение). На рис. 27.8 показан конечный результат
работы первой WPF-программы.
К этому моменту WPF может показаться не более чем
еще одной платформой для построения графических
Рис 27 8 Пепвая WPF- пользовательских интерфейсов, которая обеспечивает
программа, написанная без (почти) те же самые службы, что и Windows Forms, MFC
использования XAML и VB6. Заодно возникает вопрос: зачем нужен еще один
инструментальный набор для создания пользовательских
интерфейсов. Чтобы оценить уникальность WPF, потребуется освоить основанную на
XML грамматику — XAML.
-В аш
п*
!
UftShift
- i^^^^.p
Исходный код. Проект Wpf AppAllCode доступен в подкаталоге Chapter 27.
Глава 27. Введение в Windows Presentation Foundation и XAML 1019
Построение приложения WPF
с использованием только XAML
Типичное WPF-приложение не состоит исключительно из кода, как в первом
примере. Файлы кода С# дополняются связанным исходным файлом XAML, и вместе они
представляют сущность конкретного Window или Application, а также других типов
классов, которые пока не рассматривались, вроде UserControl и Page.
Это называется подходом файла кода к построению WPF-приложения, и именно
он будет интенсивно использоваться при рассмотрении WPF в остальной части книги.
Однако прежде чем двигаться дальше, в следующем примере мы рассмотрим создание
WPF-приложения на основе только файлов XAML. Хотя этот подход применять не
рекомендуется, он поможет лучше понять, каким образом блоки разметки XAML
трансформируются в кодовую базу С# и, в конечном итоге, в сборку .NET.
На заметку! В следующем примере используются приемы XAML, которые пока еще не
рассматривались. Можете просто загрузить файлы решения в текстовый редактор и проследить код строку
за строкой; однако не используйте для этого среду Visual Studio 2010! Некоторая часть
представленной здесь разметки не будет отображаться в визуальных конструкторах XAML этой среды.
В общем случае файлы XAML будут содержать разметку, описывающую внешний
вид и поведение окна, а связанные файлы кода С# — логику реализации. Например,
файл XAML для объекта Window может описывать общую систему разметки, элементы
управления внутри системы разметки и указывать имена различных обработчиков
событий. Связанный файл С# должен содержать логику реализации этих обработчиков
событий и любой специальный код, необходимый приложению.
Расширяемый язык разметки приложений (Extensible Application Markup Language —
XAML) — это основанная на XML грамматика, позволяющая определять состояние (и до
некоторой степени функциональность) дерева объектов .NET через разметку. Хотя XAML
часто применяется при построении пользовательских интерфейсов с WPF, на самом деле
его можно использовать для описания любого дерева неабстрактных типов .NET
(включая разработанные вами типы, определенные в отдельной сборке .NET), при условии,
что каждый из них имеет конструктор по умолчанию. Как вы вскоре убедитесь,
разметка, находящаяся в файле *.xaml, трансформируется в полноценную объектную модель.
Поскольку грамматика XAML основана на XML, мы получаем вместе с ним все
преимущества и недостатки XML. Положительная сторона XAML заключается в том, что
этому языку присущ самоописательный характер (как любому документу XML). По
большому счету каждый элемент в файле XAML представляет имя типа (такое как Button,
Window или Application) в рамках заданного пространства имен .NET Атрибуты в
пределах контекста открытия элемента отображаются на свойства (Height, Width, и т.п.) и
события (Startup, Click и т.д.) указанного типа.
Учитывая тот факт, что XAML является просто декларативным способом
определения состояния объекта, виджет WPF можно определить через разметку либо в
процедурном коде. Например, следующий код XAML:
<!-- Определение WPF Button в XAML -->
<Button Name = "btnClickMe" Height = 0м Width = 00" Content = "Click Me" />
может быть представлен программно так:
// Определение того же элемента WPF Button в коде С#.
Button btnClickMe = new Button ();
btnClickMe.Height = 40;
btnClickMe.Width = 100;
btnClickMe.Content = "Click Me";
1020 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Отрицательным моментом является то, что XAML может быть довольно
многословным и (как любой документ XML) зависимым от регистра, а потому сложные
определения XAML влекут собой немалую работу с разметкой. Большинству разработчиков
не приходится вручную создавать полные XAML-описания WPF-приложений. Большая
часть задачи возлагается на инструменты разработки, такие как Visual Studio 2010,
Microsoft Expression Blend или другие продукты от независимых поставщиков. Как
только инструмент сгенерировал базовую разметку XAML, при необходимости можно
провести ее тонкую настройку вручную.
Определение MainWindow в XAML
Хотя инструменты могут генерировать приемлемый код XAML, все же важно
понимать основы синтаксиса XAML и то, как разметка в конечном итоге трансформируется
в корректную сборку .NET. Чтобы проиллюстрировать XAML в действии, в следующем
примере мы построим полноценное приложение WPF всего из пары файлов *.xaml.
Первый класс-наследник Window (MainWindow) был определен в С# как тип класса,
расширяющего базовый класс System.Windows.Window. Этот класс содержит
единственный объект Button, который вызывает зарегистрированный обработчик события
по щелчку. Определение того же типа Window с применением грамматики XAML может
выглядеть следующим образом (предполагается, что эта разметка содержится в файле
MainWindow. xaml):
<!-- Определение класса Window -->
<Window x:Class="SimpleXamlApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="A Window built using 100% XAML" Height=00" Width=00"
WindowStartupLocation ="CenterScreen">
<!-- Это окно имеет в качестве содержимого единственную кнопку -->
<Button x:Name="btnExitApp" Width=33" Height=4"
Content = "Close Window" Click ="btnExitApp_Clicked"/>
<!-- Реализация обработчика события кнопки Click -->
<x:Code>
<■[CDATA[
private void btnExitApp_Clicked (object sender, RoutedEventArgs e)
{
this.Close ();
}
]]>
</x:Code>
</Window>
Прежде всего, обратите внимание, что корневой элемент <Window> использует
атрибут Class для указания имени класса, который будет сгенерирован при обработке этого
файла XAML. Кроме того, атрибут Class снабжен префиксом х:. Заглянув в
открывающий элемент <Window>, вы увидите, что этому префиксу дескриптора XML
присваивается строка "http://schemas.microsoft.com/winfx/2006/xaml" для построения
объявления пространства имен XML. Детали этого определения пространства имен XML станут
ясны чуть позже в этой главе, а пока просто имейте в виду, что всякий раз, когда
хотите сослаться на элемент, определенный в пространстве имен XAML http://schemas.
microsoft.com/winfx/2006/xaml, вы должны указывать префикс — лексему х:.
В контексте открывающего дескриптора <Window> заданы значения для атрибутов
Title, Height, Width и WindowStartupLocation, которые напрямую отображаются
на одноименные свойства, поддерживаемые типом System.Windows.Window из сборки
PresentationFramework.dll.
Глава 27. Введение в Windows Presentation Foundation и XAML 1021
Далее обратите внимание, что в контексте определения окна находится разметка,
описывающая внешний вид и поведение экземпляра Button, который будет использован для
неявной установки свойства Content окна. Помимо установки имени переменной (с
применением x:Name) и его общих размеров, мы также обрабатываем событие Click типа
Button, присвоив метод делегату, вызываемому при возникновении события Click.
Финальный аспект файла XAML — элемент <x:Code>, который позволяет
определять обработчики событий и прочие методы этого класса непосредственно внутри
файла *.xaml. В качестве меры безопасности сам код помещен в контекст CDATA, чтобы
предотвратить попытки анализатора XML напрямую интерпретировать данные (хотя в
данном примере это не обязательно).
Важно отметить, что использовать специальную функциональность внутри элемента
<x:Code> не рекомендуется. Хотя подход на основе одного файла изолирует все действия
в одном месте, все же встроенный код не обеспечивает ясного отделения разметки
пользовательского интерфейса от программной логики. В большинстве приложений WPF код
реализации находится в связанном файле С# (как и мы поступим в конечном итоге).
Определение объекта Application в XAML
Вспомните, что XAML может применяться для определения разметки любого
неабстрактного класса .NET, поддерживающего конструктор по умолчанию. Исходя из этого,
можно также определить объект приложения в разметке. Рассмотрим следующее
содержимое нового файла MyApp.xaml:
<!— Похоже, отсутствует метод Main()!
Однако атрибут StartupUri является его Функциональным эквивалентом -->
<Application x:Class=MSimpleXamlApp.MyAppM
xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xamlll>
</Application>
Как видите, здесь отображение между классом-наследником Application и его
описанием XAML не столь очевидно, как в случае с XAML-определением MainWindow. В
частности, не видно никаких следов метода Main(). Учитывая, что каждая исполняемая
программа .NET должна иметь точку входа, вы не ошибетесь, если предположите, что
она будет сгенерирована во время компиляции на основе части свойства StartupUri.
Значение, присвоенное StartupUri, представляет ресурс XAML, отображаемый при
запуске приложения.
Хотя метод Main() автоматически создается во время компиляции, при
желании можно использовать элемент <x:Code> для определения других блоков кода С#.
Например, чтобы вывести сообщение при завершении программы, необходимо
реализовать обработчик события Exit, как показано ниже:
<Application x:Class="SimpleXamlApp.MyApp11
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml11 Exit ="AppExit">
<x:Code>
<! [CDATA[
private void AppExit(object sender, ExitEventArgs e)
{
MessageBox . Show ("App has exited11);
}
]]>
</x:Code>
</Application>
1022 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Обработка файлов XAML с помощью msbuild.exe
Все готово к трансформации разметки в корректную сборку .NET. Однако
использовать для этого напрямую компилятор С# не удастся. На данный момент компилятор С#
не имеет возможность распознавать разметку XAML. Однако утилита командной строки
msbuild.exe знает, как трансформировать XAML в код С#, и компилирует этот код на
лету, когда она информирована о правильных целевых файлах *.targets.
msbuild.exe — это инструмент, который скомпилирует код .NET на основе
инструкций, содержащихся в основанном на XML сценарии сборки. В свою очередь, этот
сценарий сборки содержит те же данные, что находятся в файле *.csproj,
сгенерированном Visual Studio. Поэтому можно компилировать программу .NET в командной
строке с помощью msbuild.exe или же применять саму среду Visual Studio 2010.
На заметку! Полное описание утилиты msbuild.exe выходит за рамки настоящей
главы. Исчерпывающие сведения о ней можно найти в разделе "MSBuild" документации .NET
Framework 4.0 SDK.
Ниже приведен очень простой сценарий SimpleXamlApp.csproj, содержащий
достаточно информации для объяснения msbuild.exe, как следует трансформировать
файлы XAML в соответствующую кодовую базу С#:
<Project DefaultTargets=MBuildM
xmlns="http://schemas.microsoft.com/develeper/msbuild/2003">
<PropertyGroup>
<RootNamespace>SimpleXamlApp</RootNamLLpiCL>
<AssemblyName>SimpleXamlApp</AssemblyMame>
<OutputType>winexe</OutputType>
</PropertyGroup>
<ItemGroup>
<Reference Include=llSystem" />
<Reference Include=llWindowsBaseM />
<Reference Include="PresentationCore11 h
<Reference Include="PresentationFramework11 />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="MyApp.xaml" />
<Page Include=llMainWindow.xaml" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(MSBuildBinPath)\Microsoft.WinFX.targets" />
</Project>
На заметку! Этот файл *.csproj не может быть загружен непосредственно в Visual Studio 2010,
поскольку он содержит минимальный набор инструкций, необходимых для построения
приложения в командной строке.
Элемент <PropertyGroup> используется для спецификации некоторых базовых
аспектов сборки, таких как корневое пространство имен, имя результирующей сборки и
тип вывода (эквивалент опции /targetiwinexe программы csc.exe).
Первый элемент <ItemGroup> указывает набор внешних сборок для ссылки из
текущей сборки, которыми, как видно, являются основные сборки WPF, упомянутые ранее
в этой главе.
Второй элемент <ItemGroup> более интересен. Обратите внимание, что атрибуту
Include элемента <ApplicationDefinition> присвоен файл *.xaml, определяющий
объект приложения. Атрибут Include элемента <Раде> может использоваться для пере-
Глава 27. Введение в Windows Presentation Foundation и XAML 1023
числения всех остальных файлов *.xaml, в которых определены окна (и страницы, что
часто применяется при построении браузерных приложений XAML), обрабатываемые
объектом Application.
Однако "магия" этого сценария сборки кроется в элементах <Import>. Здесь
производится ссылка на два файла *.targets, каждый из которых содержит множество
других инструкций, используемых во время процесса сборки.
В файле Microsoft.WinFX.targets определены необходимые настройки для
трансформации определений XAML в эквивалентные файлы кода С#, а в файле Microsoft.
CSharp.targets содержатся данные для взаимодействия с самим компилятором С#.
Теперь можно открыть окно командной строки Visual Studio 2010 и обработать
данные XAML с помощью msbuild.exe. Для этого перейдите в каталог, в котором находятся
файлы MainWindow.xaml, MyApp.xaml и SimpleXamlApp.csproj, и введите следующую
команду:
msbuild SimpleXamlApp.csproj
После завершения процесса сборки в рабочем каталоге обнаружится подкаталог
\bin\obj (как в проекте Visual Studio). В папке \bin\Debug находится новая сборка
.NET по имени SimpleXamlApp.exe. Открыв эту сборку в ildasm.exe, вы увидите, что
разметка XAML была трансформирована в исполняемое приложение (рис. 27.9.).
/7 H:\My Books\C# Вос*\С# and the .NET Platform 5th ed\F.rst Dr_mChapter_28\Code.
Wrf^yfcMMl
File View Help
► MANIFEST
Щ SirnpleXamlApp
SimpleXaml App. Main Window
► .class public auto ansi beforeheldinit
► extends [PresentationFramework]System. Windows. Window
► implements [ WindowsBase]System. Windows. Markup. IComponentConnector
v _contentLoaded : private bool
V btnExitApp : assembly class [Presertatior^ramewwk]System.Windows.Controls.Button
■ .ctor:void()
■ InitializeComponent: voidO
■ System^Windows.Markup.IComponentConnector.Connect: void(int32,object)
■ btnExitApp_Clicked : void(object,class [PresentationCore]System.Windows.RoutedEventArgs)
SirnpleXamlApp. My App
► .class public auto ansi beforefieldinit
► extends [PresentationFramework]Sy stem. Windows. Application
■ .ctor: void()
■ AppExit: void(object, class [Present _tionfr_mework]System. Windows. ExitE vent Args)
■ InitializeComponent: voidO
Q Main: void()
. . '" .
.assembly SirnpleXamlApp
Рис. 27.9. Трансформация разметки XAML в сборку .NET
Запустив программу двойным щелчком на исполняемом файле, вы увидите на
экране ее главное окно.
Трансформация разметки в сборку .NET
Чтобы полностью понять, каким образом разметка превратилась в сборку .NET,
нужно немного углубиться в процесс msbuild.exe и изучить ряд сгенерированных
компилятором файлов, включая определенный двоичный ресурс, встроенный в сборку во
время компиляции. Первой задачей будет узнать, как файлы *.xaml трансформируются в
кодовую базу С#.
1024 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Отображение XAML-данных окна на код С#
Файлы *.targets, указанные в сценарии для msbuild.exe, содержат множество
инструкций трансляции элементов XAML в код С#. Когда msbuild.exe обрабатывает файл
*.csproj, создаются два файла *.g.cs ("g" означает автоматически сгенерированные),
которые сохраняются в каталоге \obj\Debug. На основе имен файлов *.xaml
полученные файлы С# получают названия MainWindow.g.cs и MyApp.g.cs.
Открыв файл MainWindow.g.cs в текстовом редакторе, вы найдете там класс по
имени MainWindow, расширяющий базовый класс Window. Имя этого класса — прямой
результат действия атрибута х:Class открывающего дескриптора <Window>. Кроме того,
в этом классе определена переменная-член типа System.Windows.Controls.Button с
именем btnExitApp. В данном случае имя элемента управления основано на значении
атрибута x:Name открывающего объявления <Button>. Этот класс также содержит
обработчик события Click кнопки — btnExitApp_Clicked(). Ниже приведена часть
листинга этого сгенерированного компилятором файла:
public partial class MainWindow :
System.Windows.Window, System.Windows.Markup.IComponentConnector
{
internal System.Windows.Controls.Button btnExitApp;
private void btnExitApp_Clicked(object sender, RoutedEventArgs e)
{
this.Close ();
}
}
В классе определена приватная переменная-член типа bool (по имени named_
с on tent Loaded), которая не была напрямую представлена в разметке XAML. Этот член
данных используется для того, чтобы гарантировать присваивание содержимого окна
только один раз:
public partial class MainWindow :
System.Windows.Window, System.Windows.Markup.IComponentConnector
{
internal System.Windows.Controls.Button btnExitApp;
// Эту переменную-член мы поясним ниже.
private bool _contentLoaded;
}
Обратите внимание, что сгенерированный компилятором класс также явно
реализует WPF-интерфейс IComponentConnector, определенный в пространстве имен System.
Windows.Markup. В этом интерфейсе определен единственный метод Connect(),
который реализован для подготовки каждого элемента управления, определенного в
разметке, и обеспечения логики событий, как указано в исходном файле MainWindow.xaml.
Перед завершением этого метода переменная contentLoaded устанавливается в true.
Вот как выглядит этот метод:
void System.Windows.Markup.IComponentConnector.Connect(int connectionld,
object target)
{
switch (connectionld)
{
case 1:
this.btnExitApp = ((System.Windows.Controls.Button)(target));
this.btnExitApp.Click += new
System.Windows.RoutedEventHandler(this.btnExitApp_Clicked);
Глава 27. Введение в Windows Presentation Foundation и XAML 1025
return;
this. contentLoaded = true;
}
И, наконец, в-классе MainWindow также реализован метод InitializeComponent().
Можно было ожидать, что этот метод содержит код, устанавливающий внешний вид
и поведение каждого элемента управления за счет присваивания различных свойств
(Height, Width, Content и т.п.). Однако это не так! Как же эти элементы управления
получают корректный пользовательский интерфейс? Логика InitializeComponentO
определяет местоположение встроенного в сборку ресурса, имя которого совпадает с
именем исходного файла *.xaml:
public void InitializeComponentO {
if (_contentLoaded) {
return;
}
_contentLoaded = true;
System.Uri resourceLocater = new
System.Uri ("/SimpleXamlApp;component/mainwindow.xaml" ,
System.UriKind.Relative);
System.Windows.Application.LoadComponent(this, resourceLocater);
Здесь возникает вопрос: что такое встроенный ресурс?
Роль BAML
Когда утилита msbuild.exe обрабатывает файл *.csproj, она генерирует файл с
расширением *.baml, именованный согласно начальному файлу MainWindow.xaml, так
что в папке \obj\Debug должен появиться файл под названием MainWindow.baml. Как
и можно было предположить, формат Binary Application Markup Language (BAML) — это
компактное двоичное представление исходных данных XAML.
Этот файл *.baml встраивается в виде ресурса (через сгенерированный файл
*.д.resources) в скомпилированную сборку. В этом можно удостовериться, открыв
сборку в reflector.exe (рис. 27.10).
Ресурс BAML содержит все необходимые данные для настройки внешнего вида
виджетов пользовательского интерфейса (т.е. свойств вроде Height и Width). Открыв
файл *.baml в Visual Studio 2010, можно увидеть следы начальных атрибутов XAML
(рис. 27.11).
# Red Gates .NET Reflector
IB
File View Tools Help
c#
ffl чЭ PresentationFramework
3 -СЭ SimpleXamlApp
;+■ l\ SimplcXamlApp.exe
S Cl Resources
// public resource SimpleXamlApp.g.i
Size: 1025 bytes
al
Disassembler
Name
Ш mainwindow.baml
Value
0c 00 00 00 4d 00 53 00 ... G97 bytes)
>
Рис. 27.10 Просмотр встроенного ресурса *.baml в утилите .NET Reflector
1026 Часть VI. Построение настольных пользовательских приложений с помощью WPF
MainWindow.baml X
00000160
00000170
00000180
00000190
000001а0
OOOOOlbO
OOOOOlcO
OOOOOldO
OOOOOleO
OOOOOlfO
00000200
00000210
00000220
00000230
00000240
00000250
00000260
00000270
36 61
70 ЗА
6F 73
32 30
74 61
00 00
ЗА 2F
73 6F
30 30
35 04_
05
64 33
2F 2F
6F 66
30 36
74 69
00 03
2F 73
66 74
36 2F
00 00
36 34
73 63
74 2E
2F 78
6F 6E
00 00
63 68-
2E 63
78 61
00 03
65 33
68 65
63 6F
61 6D
03 00
00 14
65 6D
6F 6D
6D 6C
00 00
03 00 00 00 24 09 Dl FF
00 00 00 03 00 00 00 24
FE 36 10 00 00 00 IF 1С
6E 64 6F 77 53 74 61 72
69 6F 6E 24 12 01 00 ОС
35 14 44 00 39 68 74 74 6ad364e35 D.9htt
6D 61 73 2E 6D 69 63 72 p://schemas.micr
6D 2F 77 69 6E 66 78 2F osoft.com/winfx/
6C 2F 70 72 65 73 65 6E 2006/xaml/presen
01 00 02 00 03 00 35 03 tation 5.
38 01 78 2C 68 74 74 70 e.x.http
61 73 2E 6D 69 63 72 6F ://schemas.micro
2F 77 69 6E 66 78 2F 32 soft com/winfx/2
03 00 01 00 02 00 03 00 006/xaml
00 IF ОС 00 00 ID FD 00 5. _ .
jTitle$$. . A Win] ! =
now built using 1 |
99 FD 35 05 00 00 00 llOO* XAMU. .5. . . . !
03 32 30 30 A4 FE 35 06 ....$....200..5.
09 C7 FF 03 33 30 30 A4 $. . . .300.
01 00 ID FD 00 15 57 69 .6 Wi
74 75 70 4C 6F 63 61 74 ndowStartupLocat
43 65 6E 74 65 72 53 63 ion$....CenterSc
: ш ~~ ~~ ~~ i ►
Рис. 27.11. BAML представляет собой компактную двоичную версию исходных данных XAML
Здесь важно понять, что WPF-приложение содержит внутри себя двоичное
представление (BAML) разметки. Во время выполнения ресурс BAML будет извлечен из
контейнера ресурсов и использован для настройки внешнего вида всех окон и элементов
управления.
Также помните, что имена этих двоичных ресурсов идентичны именам написанных
автономных файлов *.xaml. Однако это вовсе не означает необходимость поставки
файлов *.xaml вместе со скомпилированной программой WPF! Если только не строится
WPF-приложение, которое должно динамически загружать и разбирать файлы *.xaml
во время выполнения, поставлять исходную разметку никогда не придется.
Отображение XAML-данных приложения на код С#
Последняя часть автоматически сгенерированного кода, которую мы рассмотрим,
находится в файле MyApp.g.cs. Здесь имеется производный от Application класс с
соответствующей точкой входа — методом Main(). Реализация Main() вызывает метод
InitializeComponentO на типе-наследнике Application, который, в свою очередь,
устанавливает свойство StartupUri, позволяя каждому объекту делать корректные
установки свойств на основе двоичного представления XAML.
namespace SimpleXamlApp
{
public partial class MyApp : System.Windows.Application
{
void AppExit(object sender, ExitEventArgs e)
{
MessageBox.Show ("App has exited");
}
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
public void InitializeComponentO {
this.Exit += new System.Windows.ExitEventHandler(this.AppExit);
this. StartupUri = new System. Uri ("MainWindow. xaml" , System. UriKind. Relative) ;
[System.STAThreadAttribute()]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
public static void Main() {
SimpleXamlApp.MyApp app = new SimpleXamlApp.MyApp();
app.InitializeComponent();
app.Run();
Глава 27. Введение в Windows Presentation Foundation и XAML 1027
Итоговые замечания о процессе трансформирования XAML в сборку
Итак, к этому моменту получена полноценная сборка .NET с использованием
только двух файлов XAML и связанного сценария сборки для msbuild.exe. Как было
показано, для обработки файлов XAML (и генерации *.baml) утилита msbuild.exe в
процессе сборки полагается на вспомогательные настройки, определенные внутри файла
*.targets. Все эти детали происходят "за кулисами", а на рис. 27.12 показана общая
картина, касающаяся обработки файлов *.xaml во время компиляции.
Файлы
MainWindow. xaml,
MyApp.xmal,
*.csproj
SimpleXamlApp. exe
ч
Встроенный
ресурс BAML
Утилита
msbuild.exe
и
необходимые
цели С# и WPF
Вывод в каталог \Obj\Debug
MainWindow.g.cs
МуАрр.g.cs
MainWindow.baml
SimpleXamlApp.g.resources
Компилятор С#
- Компиляция файлов С#
- Внедрение *. g. resources в виде ресурса
Рис. 27.12. Процесс трансформации XAML в сборку во время компиляции
Теперь вы лучше представляете, как используются данные XAML для построения
приложения .NET. Можно переходить к рассмотрению синтаксиса и семантики самого
языка XAML.
Исходный код. Проект Wpf AppAllXaml доступен в подкаталоге Chapter 27.
Синтаксис XAML для WPF
Приложения WPF производственного уровня используют специальные инструменты
для генерации необходимой XAML-разметки. Как бы ни были хороши эти инструменты,
все же понимание общей структуры XAML не помешает.
Введение в Kaxaml
Когда вы впервые приступаете к изучению грамматики XAML, очень полезно
использовать бесплатный инструмент под названием Kaxaml Вы можете получить Этот
популярный редактор/анализатор XAML доступен для загрузки на веб-сайте http://
www.kaxaml.com.
Редактор Kaxaml хорош тем, что он не имеет никакого понятия об исходном коде С#,
обработчиках ошибок или логике реализации, и предлагает намного более простой
способ тестирования фрагментов XAML, чем применение полноценного шаблона проекта
WPF Visual Studio 2010. К тому же Kaxaml имеет набор интегрированных инструментов,
таких как средство выбора цвета и диспетчер фрагментов XAML, и даже опцию "XAML
scrubber", которая форматирует XAML-разметку на основе заданных настроек.
1028 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Открыв Kaxaml в первый раз, вы найдете там простую разметку для элемента
управления <Раде>:
<Раде
xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<!— Добавляйте XAML-разметку сюда! -->
</Grid>
</Page>
Подобно Window, элемент Page содержит различные диспетчеры компоновки и
элементы управления. Однако, в отличие от Window, объекты Page не могут выполняться
как отдельные сущности. Вместо этого они должны помещаться внутри подходящего
хоста, такого как NavigationWindow, Frame или веб-браузер (и в этом случае просто
делается приложение ХВАР). Очень полезно то, что можно вводить идентичную разметку
в области <Раде> или <Window>.
На заметку! Если в окне разметки Kaxaml заменить элементы <Раде> и </Раде> на <Window>
и </window>, то можно нажать клавишу <F5> для отображения нового окна на экране.
Для выполнения простого теста введите следующую разметку в панели XAML,
расположенной внизу окна Kaxaml:
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<!— Кнопка со специальным содержимым —>
<Button Height=00" Width=,,100">
<Ellipse Fill="Green" Height=0" Width=0"/>
</Button>
</Grid>
</Page>
В верхней части окна Kaxaml появится визуализированная страница (рис. 27.13).
Рис. 27.13. Редактор Kaxaml — очень удобный (и бесплатный) инструмент,
применяемый для изучения грамматики XAML
Глава 27. Введение в Windows Presentation Foundation и XAML 1029
При работе в Kaxaml помните, что этот инструмент не позволяет писать разметку,
которая влечет за собой какую-либо компиляцию кода (однако разрешено использовать
x:Name). Сюда входит определение атрибута х:Class (для указания файла кода), ввод
имен обработчиков событий в разметке или применение любых ключевых слов XAML,
которые также вызывают крмпиляцию кода (вроде FieldModif ier или ClassModif ier).
Попытка сделать это приведет к ошибке разметки.
Пространства имен XAML XML и "ключевые слова" XAML
Корневой элемент XAML-документа WPF (такой как <Window>, <Page>, <UserControl>
или <Application>) почти всегда ссылается на два заранее определенных пространства
имен XML:
<Раде
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
</Grid>
</Page>
Первое пространство имен XML, http://schemas.microsoft.com/winfx/2006/xaml/
presentation, отображает множество связанных с WPF пространств имен .NET для
использования в текущем файле *.xaml (System.Windows, System.Windows.Controls,
System.Windows.Data, System.Windows.Ink, System.Windows.Media, System.Windows.
Navigation).
Это отображение "один ко многим" на самом деле жестко закодировано внутри
сборок WPF (WindowsBase.dll, PresentationCore.dll и PresentationFramework.dll) с
использованием атрибута [XmlnsDefinition] уровня сборки. Ниже показан пример
импортирования System.Windows::
[assembly: XmlnsDefinition (
"http://schemas.microsoft.com/winfx/200 6/xaml/presentation",
"System.Windows")]
Загрузив эти сборки WPF в утилиту reflector.exe, можно увидеть отображения
наглядно. Например, выбор сборки PresentationCore.dll с последующим
нажатием клавиши пробела позволяет просмотреть многочисленные экземпляры атрибута
[XmlnsDefinition] (рис. 27.14).
jf Red Gates NET Reflector
file View Tools
О ©! * a I я / \сГ
!й чЭ System.ServiceModel
if 43 System.Workflow.ComponentModel
■i) -СЭ System.Workflow.Runtime
assembly: XminsPrefix("httf^/schemas.microsoft.com/vvinf)c/2006/xaml/presentat(o*i", °av")]
[assembly: XmlnsDefinitionChttp://schemas.microsoft conVwmfx/2006/xaml/presentation",' System.WindowsAutomation')]
[£i -*3 System .Workflow-Activities I [assembly: XmfnsDefinitionrhttp://schemas.microsoft.com/winfx/'2006/xaml/presentation', ' System.Windows.Media.TextForr
№ чЭ WindowsBase [assembly: XmfnsDefinitior»("http://schemas.microsoft.com/)tps/2005/06", "System.Windows.Media.Imaging"}]
-j [assembly: XmlnsDefinrtion(''httpV/schemas.microsoft.com/i«infx/r200б/xaml/presentation,'. "System.Wmdows.lnlc")]
jgi [assembly: XmlnsDefinitiofi('http://schemas.m»crosoft.com/winfx/2006/xaml/presentation', "System.WindowsJnput")]
IS "^ fjjdrew^^^ II [assembly: XmlnsOefinrttofi(°http://schemas.microsoft.com/fwrnfx/2006/xam^presentation",' System.Windows.Media.Effects'",
* ЧЭ|Щ II [assembly: XmlnsDefinition("http://schemas.microsoft.com/netfx/'2009/xaml/presentatton", "System.Wlndows.Media.TextForr
Щ -i_3 PresentationFramework ~ [assembly: XmlnsDefinitionrhttp://schemas.micrcKoft.cofTi/netfx/2007/xaml/presentation''( "System.WtndowsJnput"}]
"—J [assembly: XmlnsDefinitk>n("httpi//schemas.microsoft.com/xps/2tXM,'/06", "System.Windows.Media.Media3D")]
// Assembly PrcsentationCor* Version 4.0 0 0 * [assembly: XmlnsDefinit(on('Kttp://scr>emas.microsoft.com/netfx/'2007/xaml/presentation', ' System.WindowsAutomation')]
J [assembly: XmlnsDettnit^on('Ъttp://schemas.micfosoft,com/fмtfx/2007/xaml/presentatiorl',, "System.Windows.Media.Aiiimati'i
Location: %SystemRoot% I [aKemo|y: XmlnsDeftortion("http://scherras.microsoft.corTVn€tfx/2fJ07/xaml/presentatiori'', "System.Windows")]
\Microsoft.net\Frameworlc\v4.0.2irj06 [ОТ5етЬ1у: XmlnsPrefixrhttp://schemas.microsoft.com/xps/2005/D6', "xps")]
\WPPiPresentationCore.dll , J [assembly: XmlnsDeflпitton('■http:У/schemas.microsoft.com/xps/2005Л)б■,, "System.Windows.Input")]
PresentationCore, Version=4.0.0.0, [assembly: XmlnsDefinitiorc("http://schemas.microsoft.com/'vwnfx/20067xam!/presentation", "System.Windows")]
Curture= neutral.
Рис. 27.14 Пространство имен http://schemas.microsoft.com/winfx/2006/xaml/
presentation отображается на ключевые пространства имен WPF
1030 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Второе пространство имен XML, http://schemas.microsoft.com/winfx/2006/xaml,
используется для включения специфичных для XAML "ключевых слов" (этот термин
выбран за неимением лучшего) вместе с пространством имен System.Windows.Markup:
[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml",
"System.Windows.Markup")]
Одно из правил любого корректно сформированного XML-документа (вспомните,
что XAML — это грамматика, основанная на XML) состоит в том, что он должен иметь
открывающий корневой элемент, назначающий одно пространство имен XML в
качестве пространства по умолчанию, которым обычно является пространство, содержащее
наиболее часто используемые элементы. Если корневой элемент требует включения
дополнительных вторичных пространств имен (как здесь показано), они должны быть
определены с уникальным префиксом (чтобы разрешить возможные конфликты имен).
В качестве префикса принято применять просто х, тем не менее, это может быть любой
уникальный маркер по вашему выбору, например, XamlSpecif icStuf f:
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:XamlSpecificStuff="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<!-- Кнопка со специальным содержимым -->
<Button XamlSpecificStuff:Name="buttonl" Height=00" Width=00">
<Ellipse Fill="Green" Height=0" Width=0"/>
</Button>
</Grid>
</Page>
Очевидный недостаток определения длинных префиксов пространств имен XML
связан с тем, что XamlSpecificStuff придется набирать всякий раз, когда в файле XAML
нужно сослаться на один из типов, определенных в этом пространстве, связанном с
XAML. Поскольку набирать каждый раз префикс XamlSpecificStuff слишком
утомительно, давайте ограничимся х.
В любом случае, помимо ключевых слов x:Name, x:Class и x:Code, включение
пространства имен XML http://schemas.microsoft.com/winfx/2006/xaml также
предоставит доступ к дополнительным ключевым словам XAML, наиболее часто используемые
из которых перечислены в табл. 27.9.
Таблица 27.9. Ключевые слова XAML
Ключевое слово XAML Назначение
х: Array Представляет тип массива .NET на XAML
x:ClassModif ier Позволяет определять видимость типа класса (internal или public),
обозначенного ключевым словом Class
x:FieldModif ier Позволяет определять видимость члена типа (internal, public,
private или protected) для любого именованного элемента корня
(например, <Button> внутри элемента <Window>). Именованный
элемент определяется с использованием ключевого слова XAML Name
х:Key Позволяет устанавливать значение ключа для элемента XAML, которое
должно быть помещено в элемент словаря
x:Name Позволяет указывать сгенерированное С# имя заданного элемента XAML
x:Null Представляет null-ссылку
х:Static Позволяет ссылаться на статический член типа
Глава 27. Введение в Windows Presentation Foundation и XAML 1031
Окончание табл. 27.9
Ключевое слово XAML Назначение
х:Туре XAML-эквивапент операции С# typeof (выдает System.Type на
основе указанного имени)
x:TypeArguments Позволяет устанавливать элемент как обобщенный тип с определенным
параметром типа (например, List<int> или List<bool>)
В дополнение к этим двум необходимым объявлениям пространств имен XML
можно, а иногда и необходимо, определить дополнительные префиксы дескрипторов в
открывающем элементе XAML-документа. Обычно это делается, когда нужно описать в
XAML класс .NET, определенный во внешней сборке. Например, предположим, что вы
построили несколько специальных элементов управления WPF и упаковали их в
библиотеку под названием MyControls.dll.
Теперь, если необходимо создать новый экземпляр Window, который использует эти
элементы, можно установить специальное пространство имен XML, отображаемое на
библиотеку MyControls.dll, с использованием лексем clr-namespace и assembly.
Ниже приведен пример разметки, создающей префикс дескриптора по имени myCtrls,
который может применяться для доступа к членам этой библиотеки:
<Window x:Class="WpfApplicationl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/200 6/xaml"
xmlns:myCtrls="clr-namespace:MyControls;assembly=MyControls"
Title="MainWindow" Height=,,350" Width=25">
<Grid>
<myCtrls:MyCustomControl />
</Grid>
</Window>
Лексеме clr-namespace присваивается название пространства имен .NET в сборке,
в то время как лексема assembly устанавливается в дружественное имя внешней
сборки *.dll. Такой синтаксис можно использовать для любой внешней библиотеки .NET,
которой необходимо манипулировать внутри разметки.
Управление объявлениями классов и переменных-членов
Многие из этих ключевых полей вы увидите в действии там, где они
понадобятся. Давайте в качестве простого примера рассмотрим следующее определение XAML
<Window>, в котором используются ключевые слова ClassModifier и FieldModifier,
а также x:Name и х:Class (вспомните, что редактор Kaxaml не позволяет использовать
ключевые слова XAML, вызывающие компиляцию, такие как x:Code, x:FieldModifier
или x:ClassModifier):
<!— Этот класс теперь будет объявлен как internal в файле *.g.cs -->
<Window x:Class="MyWPFApp.MainWindow" xrClassModifler ="internal11
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/200 6/xaml">
<!— Эта кнопка будет объявлена как public в файле *.g.cs -->
<Button x:Name ="myButton" x:FieldModifier ="public" Content = K"/>
</Window>
По умолчанию все определения типов C#/XAML являются общедоступными (public),
а члены — внутренними (internal). Однако на основе показанного определения XAML
результирующий автоматически сгенерированный файл содержит тип класса internal
с public-членом Button:
1032 Часть VI. Построение настольных пользовательских приложений с помощью WPF
internal partial class MainWindow : System.Windows.Window,
System.Windows.Markup.IComponentConnector
{
public System.Windows.Controls.Button myButton;
}
Элементы XAML, атрибуты XAML и преобразователи типов
После установки корневого элемента и необходимых пространств имен XML
следующей задачей будет наполнение корня дочерним элементом. Как уже упоминалось, в
реальных WPF-приложениях дочерним элементом будет диспетчер компоновки (вроде
Grid или StackPanel), который, в свою очередь, содержит любое количество
элементов, определяющих пользовательский интерфейс. Эти диспетчеры компоновки
подробно рассматриваются в следующей главе, а пока предположим, что элемент <Window>
будет содержать единственный элемент Button.
Как было показано ранее в главе, элементы XAML отображаются на типы классов или
структур внутри заданного пространства имен .NET, в то время как атрибуты в
открывающем дескрипторе элемента отображаются на свойства и события конкретного типа.
Для целей иллюстрации введите следующее определение <Button> в редакторе
Kaxaml:
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<!-- Настроить внешний вид элемента Button -->
<Button Height=,,50" Width=,,100" Content="OK! "
FontSize=,,20" Background="Green" Foreground="Yellow,,/>
</Grid>
</Page>
Обратите внимание, что значения, присвоенные свойствам, являются строковыми.
Это может показаться полным несоответствием типам данных, поскольку после
создания такого элемента Button в коде С# этим свойствам будут присваиваться не
строковые объекты, а значения специфических типов данных. Например, ниже показано, как
та же кнопка описана в коде:
public void MakeAButton ()
{
Button myBtn = new Button ();
myBtn.Height = 50;
myBtn.Width = 100;
myBtn.FontSize = 20;
myBtn.Content = "OK!";
myBtn.Background = new SolidColorBrush(Colors.Green);
myBtn.Foreground = new SolidColorBrush(Colors.Yellow);
}
Оказывается, WPF поставляется с множеством классов преобразователей типов,
которые применяются для трансформации простых текстовых значений в корректные
типы данных. Этот процесс происходит прозрачно (и автоматически).
Тем не менее, нередко возникает потребность присвоить атрибуту XAML намного
более сложное значение, которое не может быть выражено простой строкой. Например,
предположим, что необходимо построить специализированную кисть для установки
свойства Background элемента Button. При создании этой кисти в коде все достаточно
просто:
Глава 27. Введение в Windows Presentation Foundation и XAML 1033
public void MakeAButton ()
{
// Забавная кисть для фона.
LinearGradientBrush fancyBruch =
new LinearGradientBrush(Colors.DarkGreen, Colors.LightGreen, 45);
myBtn.Background = fancyBruch;
myBtn.Foreground = new SolidColorBrush(Colors.Yellow);
}
Но как представить эту сложную кисть в виде строки? Да никак! К счастью, в XAML
предусмотрен специальный синтаксис, который можно применять всякий раз, когда
нужно присвоить сложный объект в качестве значения свойства, и этот синтаксис
называется "свойство-элемент" (property-element).
Понятие синтаксиса XAML "свойство-элемент"
Синтаксис свойство-элемент позволяет присваивать свойству сложные объекты.
Вот как выглядит XAML-описание элемента Button, свойство Background которого
установлено в кисть LinearGradientBrush:
<Button Height=,,50" Width=00" Content="OK' "
FontSize=011 Foreground="Yellow">
<Button.Background>
<LinearGradientBrush>
<GradientStop Color=,,DarkGreen" Offset=,,0"/>
<GradientStop Color=,,LightGreen" Offset="l,,/>
</LinearGradientBrush>
</Button.Background>
</Button>
Обратите внимание, что внутри контекста дескрипторов <Button> и </Button>
определен вложенный элемент по имени <Button.Background>. Внутри него определен
специальный элемент <LinearGradientBrush>. (Пока не беспокойтесь о коде кисти;
графика WPF будет рассматриваться в главе 30.)
В общем случае, любое свойство может быть установлено с использованием
синтаксиса "свойство-элемент", что всегда сводится к следующему шаблону:
<ОпределяющийКласс>
<ОпределяющийКласс.СвойствоОпределяющегоКласса>
<!-- Значение для СвойствоОпределяющегоКласса -->
</ОпределяющийКласс.СвойствоОпределяющегоКласса>
</ОпределяющийКласс >
Хотя любое свойство может быть установлено с использованием этого синтаксиса,
указание значения в виде простой строки экономит время на ввод. Например, ниже
показано, как мог бы выглядеть более громоздкий способ установки свойства Width
элемента Button:
<Button Height=,,50" Content="OK ' "
FontSize=ll20" Foreground="Yellow">
<Button.Width>
100
</Button.Width>
</Button>
1034 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Понятие присоединяемых свойств XAML
В дополнение к синтаксису "свойство-элемент" в XAML поддерживается
специальный синтаксис, используемый для установки значения присоединяемого свойства. По
сути, присоединяемые свойства позволяют дочернему элементу устанавливать
значение какого-то свойства, которое в действительности определено в родительском
элементе. Общий шаблон, которому нужно следовать, выглядит так:
<РодительскийЭлемент>
<ДочернийЭлемент РодительскийЭлемент.СвойствоРодительскогоЭлемента = "Значение">
</Родитель скийЭлемент>
Наиболее распространенное применение синтаксиса присоединяемых свойств
состоит в позиционировании элементов пользовательского интерфейса внутри одного из
классов диспетчеров компоновки (Grid, DockPanel и т.п.). Эти диспетчеры компоновки
более подробно рассматриваются в следующей главе, а пока введите в редакторе Kaxaml
такую разметку:
<Раде
xmlns="http://schemas.microsoft.com/winfx/2 00 6/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Canvas Height=,,200" Width=00" Background=,,LightBlue">
<Ellipse Canvas.Top=0" Canvas.Left=0" Height=0" Width=0" Fill="DarkBlue"/>
</Canvas>
</Page>
Здесь определен диспетчер компоновки Canvas, содержащий в себе элемент Ellipse.
Обратите внимание, что с помощью синтаксиса присоединяемого свойства Ellipse
может информировать свой родительский элемент (Canvas) о том, где его следует
разместить по отношению к левому верхнему углу.
В отношении присоединяемых свойств следует помнить несколько моментов. Прежде
всего, это не универсальный синтаксис, который может применяться к любому
свойству любого родительского элемента. Например, следующий XAML содержит ошибку:
<!-- Ошибка! Нельзя устанавливать свойство Background в Canvas
через присоединяемое свойство -->
<Canvas Height=,,200" Width=00">
<Ellipse Canvas .Background="LightBlue11
Canvas.Top= 0" Canvas.Left="90"
Height=0" Width=0" Fill="DarkBlue"/>
</Canvas>
В действительности присоединяемые свойства представляют собой
специализированную форму связанной с WPF концепции, которая называется свойством
зависимости Если свойство не было реализовано в очень специфической манере, его значение
не может быть установлено с использованием синтаксиса присоединяемых свойств.
Более подробно свойства зависимости рассматриваются в главе 31.
На заметку! Инструменты Kaxaml, Visual Studio 2010 и Expression Blend оснащены средством
IntelliSense, отображающим действительные присоединяемые свойства, которые могут быть
установлены для каждого элемента.
Понятие расширений разметки XAML
Как уже объяснялось, значения свойств чаще всего представляются в виде простой
строки или через синтаксис "свойство-элемент". Однако есть и другой способ указать
значение атрибута XAML — с использованием расширений разметки Расширения раз-
Глава 27. Введение в Windows Presentation Foundation и XAML 1035
метки позволяют анализатору XAML получать значение свойства из выделенного
внешнего класса. Это может быть очень выгодно, учитывая, что некоторые значения свойств
требуют выполнения множества операторов кода для поиска значения.
Расширения разметки предлагают способ ясного расширения грамматики XAML
новой функциональностью. Расширение разметки внутренне представлено как класс,
унаследованный от Mark up Extension. Следует подчеркнуть: шансы, что когда-либо
придется строить специальное расширение разметки, невелики. Тем не менее,
подмножество ключевых слов XAML (таких как х: Array, x:Null, х:Static и х:Туре)— это
именно расширения разметки.
Расширение разметки заключается в фигурные скобки, как показано ниже:
Олемент УстанавливаемоеСвойство = "{РасширениеРазметки}"/>
Чтобы увидеть расширение разметки в действии, введите следующий код в
редакторе Kaxaml:
<Page
xmlns="http://schemas.microsoft.com/winfx/20 0 6/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:CorLib="clr-namespace:System;assembly=mscorlib">
<StackPanel>
<!-- Расширение разметки Static позволяет получать значение
статического члена класса —>
<Label Content ="{x:Static CorLib:Environment.OSVersion}"/>
<Label Content ="{x:Static CorLib:Environment.ProcessorCount}"/>
<•— Расширение разметки Type — это XAML-версия оператора С# typeof -->
<Label Content ="{х:Туре Button}" />
<Label Content ="{x:Type CorLib:Boolean}" />
<!—Наполнение элемента ListBox массивом строк -->
<ListBox Width=00" Height=0">
<ListBox.ItemsSource>
<x:Array Type="CorLib:String">
<CorLib:String>Sun Kil Moon</CorLib:String>
<CorLib:String>Red House Painters</CorLib:String>
<CorLib:String>Besnard Lakes</CorLib:String>
</x:Array>
</ListBox.ItemsSource>
</ListBox>
</StackPanelx/Page>
Прежде всего, обратите внимание, что определение <Раде> имеет новое объявление
пространства имен XML, что позволяет получать доступ к пространству имен System
сборки mscorlib.dll. Имея это пространство имен, сначала с помощью расширения
разметки x:Static извлекаются значения OSVersion и ProcessorCount класса System.
Environment.
Расширение разметки х:Туре позволяет получить доступ к описанию метаданных
указанного элемента. Здесь просто назначаются полностью квалифицированные имена
типов WPF Button и System.Boolean.
Наиболее интересная часть показанной выше разметки связана с элементом
ListBox. Его свойство ItemSource устанавливается в массив строк, полностью
объявленный в разметке. Обратите внимание, что расширение разметки х: Array позволяет
указывать набор под элементов внутри своего контекста:
<х:Array Type="CorLib:String">
<CorLib:String>Sun Kil Moon</CorLib:String>
<CorLib:String>Red House Painters</CorLib:String>
<CorLib:String>Besnard Lakes</CorLib:String>
</x:Array>
1036 Часть VI. Построение настольных пользовательских приложений с помощью WPF
На заметку! Приведенный выше пример XAML служит только для иллюстрации расширения
разметки в действии. Как будет показано в главе 28, существуют намного более легкие способы
наполнения элементов управления ListBox
На рис. 27.15 показана разметка этого <Раде> в редакторе Kaxaml.
Рис. 27.15. Расширения разметки позволяют устанавливать значения через
функциональность выделенного класса
Итак, вы ознакомились с многочисленными примерами, демонстрирующими
основные аспекты синтаксиса XAML. Наверняка вы согласитесь, что XAML — очень
интересный язык в том плане, что позволяет описать дерево объектов .NET в декларативной
манере. Хотя это исключительно полезно для конфигурирования графических
пользовательских интерфейсов, следует помнить, что XAML может описать любой тип из
любой сборки, если только этот тип не является абстрактным и имеет конструктор по
умолчанию.
Построение приложений WPF с использованием
файлов отделенного кода
В первых двух примерах этой главы демонстрировались крайние случаи построения
приложения WPF с использованием только кода, или только XAML. Рекомендованный
способ построения любого приложения WPF предусматривает применение подхода на
основе файла кода. Согласно этой модели, файлы XAML проекта не содержат ничего
кроме разметки, которая описывает общее состояние классов, в то время как файлы
кода содержат детали реализации.
Добавление файла кода для класса MainWindow
Для иллюстрации сказанного модифицируем пример WpfAppAllXaml,
добавив к нему файль! кода. Скопируйте всю папку предыдущего примера и назовите ее
Wpf AppCodeFiles. Создайте в этой папке новый файл кода С# по имени MainWindow.
xaml.cs (по существующему соглашению имя файла отделенного кода С# имеет форму
*.xaml.cs). Поместите в этот новый файл показанный ниже код:
Глава 27. Введение в Windows Presentation Foundation и XAML 1037
// MainWindow.xaml.es
using System;
using System.Windows;
using System.Windows.Controls;
namespace SimpleXamlApp
{
public partial class MainWindow : Window
{
public MainWindow()
{
// Помните! Этот метод определен внутри
// сгенерированного файла MainWindow. g. cs .
InitializeComponent();
}
private void btnExitApp_Clicked(object sender, RoutedEventArgs e)
{
this.Close();
}
}
}
Здесь определен частичный класс, содержащий логику обработки событий, которая
может быть объединена с определением частичного класса того же типа в файле *.g.cs.
Учитывая, что InitializeComponent () определен внутри файла MainWindow.g.cs,
конструктор окна выполняет вызов для загрузки и обработки встроенного ресурса BAML.
Файл MainWindow. xaml также должен быть обновлен; это означает просто удаление
в нем всех следов прежнего кода С#:
<Window х:CIass="SimpleXamlApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="A Window built using Code Files!"
Height=00" Width=00"
WindowstartupLocation ="CenterScreen">
<'—The event handler is now in your code file -->
<Button x:Name="btnExitApp" Width=33" Height=4"
Content = "Close Window" Click ="btnExitApp_Clicked"/>
</Window>
Добавление файла кода для класса МуАрр
При необходимости можно было бы также построить файл отделенного кода для
типа, унаследованного от Application. Поскольку большая часть действий происходит
в файле MyApp.g.cs, код внутри файла MyApp.xaml.cs выглядит следующим образом:
/ / МуАрр.xaml.сs
using System;
using System.Windows;
using System.Windows.Controls;
namespace SimpleXamlApp
{
public partial class MyApp : Application
{
private void AppExit(object sender, ExitEventArgs e)
{
MessageBox.Show("App has exited");
}
}
}
1038 Часть VI. Построение настольных пользовательских приложений с помощью WPF
В файле MyApp.xaml теперь содержится такая разметка:
<Application x:Class="SimpleXamlApp.MyApp11
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml"
Exit ="AppExit">
</Application>
Обработка файлов кода с помощью msbuild.exe
Прежде чем мы перекомпилировать файлы с использованием msbuild.exe,
понадобится обновить файл *.csproj для учета новых файлов С#, подлежащих включению в
процесс компиляции, с помощью элементов <Compile> (выделены полужирным):
<Project DefaultTargets="Build" xmlns=
"http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<RootNamespace>SimpleXamlApp</RootNamespace>
<AssemblyName>SimpleXamlApp</AssemblyName>
<OutputType>winexe</OutputType>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore11 />
<Reference Include="PresentationFramework11 />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="MyApp.xaml" />
<Compile Include = "MainWindow.xaml.es" />
<Compile Include = "MyApp.xaml.es" />
<Page Include="MainWindow.xaml" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(MSBuildBinPath)\Microsoft.WinFX.targets" />
</Project>
Передав этот сценарий сборки утилите msbuild.exe, вы получите ту же исполняемую
сборку, что и в приложении Wpf AppAllXaml. Однако в том, что касается разработки,
теперь имеется четкое отделение представления (XAML) от программной логики (С#).
Поскольку это — предпочтительный метод разработки WPF, в приложениях WPF,
созданных с использованием Visual Studio 2010, всегда применяется модель отделенного
кода.
Исходный код. Проект Wpf AppCodeFiles доступен в подкаталоге Chapter 27.
Построение приложений WPF
с использованием Visual Studio 2010
На протяжении этой главы примеры создавались с использованием простых
текстовых редакторов, компилятора командной строки и редактора Kaxaml. Тогда важно было
сосредоточить внимание на основном синтаксисе приложений WPF, не замутненном
дополнительными элементами графического конструктора. Однако теперь, зная, как
строятся WPF-приложения с помощью простейших средств, давайте посмотрим, каким
образом Visual Studio 2010 может упростить процесс разработки.
Глава 27. Введение в Windows Presentation Foundation и XAML 1039
На заметку! Хотя Visual Studio 2010 обеспечивает некоторую поддержку написания сложной XAML-
разметки с использованием интегрированных визуальных конструкторов, Expression Blend
представляет собой намного лучшую альтернативу для построения WPF-приложений. Среда
Expression Blend рассматривается в главе 28.
Шаблоны проектов WPF
В диалоговом окне New Project (Новый проект) среды Visual Studio 2010 определен
набор рабочих пространств проектов WPF, и все они расположены в узле Windows корня
Visual C#. На выбор доступны следующие варианты: WPF Application (Приложение WPF),
WPF User Control Library (Библиотека пользовательских элементов управления WPF),
WPF Custom Control Library (Библиотека специальных элементов управления WPF) и WPF
Browser Application (Браузерное приложение WPF, т.е. ХВАР). Для начала создадим новое
приложение WPF по имени MyXamlPad (рис. 27.16).
Рис. 27.16. Шаблоны проектов WPF в Visual Studio 2010
Помимо установки ссылок на все сборки WPF (PresentationCore.dll, Presentation
Foundation.dll и WindowsBase.dll), вы также получаете начальные
классы-наследники Window и Application и возможность использования файлов кода и связанный
XAML-файл. На рис. 27.17 показано окно Solution Explorer для этого нового проекта
WPF.
Знакомство с инструментами визуального конструктора WPF
В Visual Studio 2010 предусмотрена панель инструментов flbolbox), доступная через
пункт меню View (Вид) и содержащая множество элементов управления WPF (рис. 27.18).
Используя стандартное перетаскивание с помощью мыши, можно поместить
любой из этих элементов управления на поверхность визуального конструктора элемента
Window. При этом соответствующий XAML будет сгенерирован автоматически. Тем не
менее, разметку можно также вводить и вручную, с использованием интегрированного
редактора XAML.
1040 Часть VI. Построение настольных пользовательских приложений с помощью WPF
I Solution Explorer
f*3 Solution 'MyXamlPad' A project)
л ip MyXamlPad
t> <fM Properties
d ■ £3t References
4Э Microsoft.CSharp
-■O PresentationCore
-C2 PresentationFramework
*GJ System
НЭ System.Core
•O System.Data
нЭ System.Data.DataSetExtensions
*€3 System.Xaml
>Э SystemJKml
-СЭ System.Xml.Linq
-СЭ WindowsBase
j Й Appjcaml
S§^ App,xaml.cs
4 • MainWindcw.xaml
**) MainWindow.xaml.cs
^ Solution Explorer I
Рис. 27.17. Начальные файлы
проекта типа WPF Application
JToolbo
,-IV~,
bi
A
Ш
»I2
a
1
ss
ею
0
□
m
\ m
i ■
GroupBox
Image
Label
ListBox
LrstView
MedraElement
Menu
PasswordBox
ProgressBar
RadioButton
Rectangle
RichTextBox
ScrollBar
ScrollViewer
* п x]
*
1
-1
Рис. 27.18. Панель инструментов
содержит элементы управления
WPF, которые могут быть
помещены на поверхность визуального
конструктора
Как показано на рис. 27.19, при этом обеспечивается поддержка средства IntelliSense,
что может упростить написание разметки.
На заметку! Расположение панелей визуального конструктора легко изменять с помощью кнопок,
встроенных в разделитель — Swap Panels (Поменять панели), которая помечена стрелками
вверх/вниз, Horizontal Split (Разделить горизонтально) и Vertical Split (Разделить
вертикально) и т.д. Уделите время на удобную для себя организацию панелей.
ЕЗ XAML ;
| x:Cias,s="«yXe»lPad.«ainWindow"
xinlns^http://schemes,microsoft .com/winfx/2006/xarel/preb<
xmlns:xV*http://schemas.microsoft.соя/winfx/20в6/ха»1"
Title«"MainWindow" Height-50" Width«25" (>
id> 1 f Activated
Iff AIlowDrop
■ff AllowsTransparency
0\
JO Binding
; iff BindingGroup
iff BitmapEffect
iff BitmapEffectfnput
jiff BorderBmsh
iff BorderThickness
Iff CacheMode
{) Calendar
Iff a*
Рис. 27.19. Визуальный конструктор элемента Window
Глава 27. Введение в Windows Presentation Foundation и XAML 1041
После помещения элементов управления на поверхность визуального конструктора
можете использовать окно Properties (Свойства) для установки значений свойств
выбранного элемента управления, а также для создания обработчиков событий для
выбранного элемента. Для примера перетащите элемент управления — кнопку на любое
место поверхности визуального конструктора. В результате Visual Studio сгенерирует
XAML-разметку вроде показанной ниже:
<Button Content="Button" Height=3" HorizontalAlignment="Left"
Margin=2,12,0,0" Name="buttonl" VerticalAlignment=,,Top" Width=5" />
Теперь с помощью окна Properties измените цвет фона (Background) элемента Button
с использованием встроенного редактора кистей (рис. 27.20).
На заметку! Редактор кистей в Expression Blend очень похож на редактор кистей в Visual Studio и
будет детально рассматриваться в главе 29.
Рис. 27.20. Окно Properties позволяет конфигурировать
пользовательский интерфейс элемента управления WPF
Завершив упражнение с редактором кистей, взгляните на сгенерированную
разметку. Она может выглядеть примерно так:
<Button Content="Button" Height=3" HorizontalAlignment="Left"
Margin=2,12,0,0" Name="buttonl"
VerticalAlignment="Top" Width= 5">
<Button.Background>
<LinearGradientBrush EndPoint="l,0.5" StartPoint=,0.5">
<GradientStop Color="#FF7488CE" Offset=" />
<GradientStop Color="#FFCllElE" Offset=.837"J>
</LinearGradientBrush>
</Button.Background>
</Button>
Для организации обработки событий, связанных с определенным элементом
управления, также может использоваться окно Properties, но на этот раз нужно перейти на
вкладку Events (События). Удостоверьтесь, что на поверхности визуального
конструктора выбрана кнопка, в окне Properties перейдите на вкладку Events и найдите событие
Click. Среда Visual Studio 2010 автоматически построит обработчик событий со
следующим именем ИмяЭлементаУправления_ИмяСобытия.
Пока кнопка не переименована, в окне Properties отображается сгенерированный
обработчик событий по имени buttonl Click (рис. 27.21).
1042 Часть VI. Построение настольных пользовательских приложений с помощью WPF
I Button buttonl
I _jf Properties 4 Events
I |МШ j Search
Context MenuClos...
Context MenuOpe...
DataContextChan...
DragEnter
DragLeave
DragOver
Drop
FocusabteC ha nged
Ш buttonl_Click
□
□
□
□
□
a
D
□
ДН
—I
"pll
w
ч
Рис. 27.21. Обработка событий с использованием визуального конструктора
К тому же Visual Studio 2010 сгенерирует соответствующий код С# в файле кода для
окна. Здесь можно добавить любой код, который должен выполняться по щелчку на
кнопке:
public partial class MainWindow : Window
{
public MainWindow ()
{
InitializeComponent ();
}
private void buttonl_Click(object sender, RoutedEventArgs e)
{
}
}
Обрабатывать событие можно также непосредственно в редакторе XAML. Для
примера поместите курсор мыши внутрь элемента <Button> и введите имя события
MouseEnter, а за ним — знак равенства. Visual Studio отобразит все совместимые
обработчики из файла кода, а также опцию <New Event Handler> (Новый обработчик
события). Двойной щелчок на <New Event Handler> приводит к тому, что IDE-среда
сгенерирует соответствующий обработчик в файле кода С#.
Теперь, когда вы ознакомились с базовыми инструментами, применяемыми внутри
Visual Studio 2010 для манипуляций приложениями WPF, давайте воспользуемся этой
IDE-средой для построения примера программы, которая проиллюстрирует процесс
разбора XAML во время выполнения.
Прежде, чем начать, полностью удалите только что созданную разметку,
описывающую Button, а также удалите код обработчика события С#.
Проектирование графического интерфейса окна
API-интерфейс WPF поддерживает возможность программной загрузки, разбора и
сохранения XAML-описаний. Это может быть полезно во многих ситуациях. Например,
предположим, что есть пять разных файлов XAML, описывающих внешний вид и
поведение типа Window. До тех пор, пока имена каждого элемента (и всех необходимых
обработчиков событий) идентичны внутри каждого файла, можно динамически
менять "обложки" окна (возможно, на основе аргумента, передаваемого приложению при
запуске).
Глава 27. Введение в Windows Presentation Foundation и XAML 1043
Взаимодействие с XAML во время выполнения вращается вокруг типов X ami Reader
и XamlWriter — оба они определены в пространстве имен System.Windows.Markup.
Чтобы проиллюстрировать, как программно наполнить объект Window из внешнего
файла *.xaml, создадим приложение, который имитирует базовую функциональность
редактора Kaxaml.
Хотя наше приложение определенно не будет столь же развитым, как Kaxaml, все
же оно предоставит возможность вводить разметку XAML, просматривать результаты и
сохранять XAML во внешнем файле. Для начала обновите начальное XAML-определение
элемента <Window>, как показано ниже.
На заметку! В следующей главе мы погрузимся в работу с элементами управления и панелями,
так что пока не беспокойтесь о деталях объявления элементов управления.
<Window x:Class="MyXamlPad.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title=MMy Custom XAML Editor" Height=38" Width=041"
Loaded=llWindow_LoadedM Closed="Window_Closed11
WindowStartupLocation="CenterScreen'"*
<!— Используйте DockPanel, а не Grid —>
<DockPanel LastChildFill="True" >
<!— Эта кнопка запустит окно с определенным XAML -->
<Button DockPanel.Dock=MTopn Name = "btnViewXaml" Width=,,100" Height=0"
Content ="View Xaml" Click="btnViewXaml_Click11 />
<!-- Это будет область для ввода -->
<TextBox AcceptsReturn ="True" Nam = = "t::tXamlData"
FontSize = 4" Background="Black" Foreground="Yellow11
BorderBrush ="Blue" VerticalScrollBarVisibility="Auto11
Accept sTab="True"/4
</DockPanel>
</Window>
Прежде всего, обратите внимание на то, что первоначальный тип <Grid> заменен
типом <DockPanel>, содержащим Button (по имени btnViewXaml) и TextBox (по имени
txtXamlData), а также на то, как обрабатывается событие Click типа Button. Кроме
того, события Loaded и Closed самого типа Window были обработаны внутри
открывающего элемента <Window>. Если вы применяли визуальный конструктор для обработки
событий, то должны найти следующий код в файле MainWindow.Xaml.cs:
public partial class MainWindou : Window
{
public MainWindow()
InitializeComponent();
private void btnViewXaml_Clirk (ob]e -1- sender, PoutedEventArgs e)
private void Window_Closed (rt jf it -r, Ev.i t'rj: e)
private void Window Loaded(object r* nder, FcutedEventArgs p)
}
1044 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Прежде чем продолжить, не забудьте импортировать следующие пространства имен
в файл MainWindiw.xaml.cs:
using System.10;
using System.Windows.Markup;
Реализация события Loaded
Событие Loaded главного окна отвечает за определение наличия файла YourXaml.
xaml в папке, содержащей приложение. Если этот файл есть, вы прочитаете его данные и
поместите в Text В ох главного окна. Если же нет, то заполните Text В ох начальным XAML-
описанием по умолчанию пустого окна (это описание в точности совпадает с разметкой,
полученной при начальном определении окна, за исключением того, что для неявной
установки свойства Content из Window вместо <Grid> используется <StackPanel>).
На заметку! Строку, которая строится для представления ключевых пространств имен XML,
набирать довольно утомительно, учитывая потребность в символах отмены, необходимых для
внутренних кавычек, поэтому будьте внимательны.
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// При загрузке главного окна приложения поместить
// некоторый базовый текст XAML в текстовый блок.
if (File.Exists(System.Environment.CurrentDirectory + "\\YourXaml.xaml") )
{
txtXamlData.Text = File.ReadAllText("YourXaml.xaml");
}
else
{
txtXamlData.Text = '
"<Window xmlns=\"http://schemas.microsoft.com/winfx/200£/xaml/presentation\"\nM
+"xmlns:x=\"http://schemas.microsoft.com/winfx/2 00 6/xaml\""
+" Height =\00\" Width =\00\" WindowStartupLocation=\"CenterScreen\">\n"
+"<StackPanel>\n"
+"</StackPanel>\n"
+ "</Window>";
}
}
Используя этот подход, приложение сможет загружать XAML, введенный в
предыдущем сеансе, или при необходимости предлагать блок разметки по умолчанию. Сейчас
можно запустить программу и увидеть внутри объекта Text В ох окно, показанное на
рис. 27.22.
Рис. 27.22. Первый запуск MyXamlPad.exe
Глава 27. Введение в Windows Presentation Foundation и XAML 1045
Реализация события Click объекта Button
При щелчке на объекте Button сначала сохраняются текущие данные из TextBox
в файле YourXaml.xaml. Сохраненные данные читаются через File.0pen() для
получения типа-наследника Stream. Это необходимо, поскольку метод XamlReader.LoadO
требует типа-наследника Stream (вместо простого System.String) для представления
XAML-разметки, подлежащей разбору.
После загрузки описания XAML в <Window>, который будет конструироваться,
создайте экземпляр System.Windiows.Window на основе находящегося в памяти XAML и
отобразите Window в виде модального диалогового окна:
private void btnViewXaml_Click(object sender, RoutedEventArgs e)
{
// Записать данные из текстового блока в локальный файл *.xaml.
File.WriteAllText("YourXaml.xaml", txtXamlData.Text);
// Это окно, к которому будет динамически применяться XAML-раэметка.
Window myWindow = null;
// Открыть локальный файл *.xaml.
try
{
using (Stream sr = File.Open("YourXaml.xaml", FileMode.Open))
{
// Присоединить XAML-раэметку к объекту Window.
myWindow = (Window)XamlReader.Load(sr);
// Отобразить диалоговое окно и выполнить очистку.
myWindow.ShowDialog();
myWindow.Close();
myWindow = null;
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
Обратите внимание, что большая часть логики помещена в блок try/catch. Таким
образом, если файл YourXaml.xaml содержит неверно сформированную разметку, в
результирующем окне сообщения отобразится сообщение об ошибке. Например,
запустите программу и намеренно внесите ошибку в <StackPanel>, скажем, добавив лишнюю
букву Р в открывающий элемент. После щелчка на кнопке отобразится сообщение об
ошибке вроде показанного на рис. 27.23.
Рис. 27.23. Перехват ошибок разметки
1046 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Реализация события Closed
И, наконец, событие Closed типа Window гарантирует, что данные, введенные в
TextBox, сохранятся в файле YourXaml.xaml:
private void Window_Closed(object sender, EventArgs e)
{
// Записать данные из текстового блока в локальный файл *.xaml.
File.WriteAllText("YourXaml.xaml", txtXamlData.Text);
}
Тестирование приложения
Запустите приложение и ведите некоторую XAML-разметку в текстовой области.
Имейте в виду, что (подобно редактору Kaxaml) это приложение не позволяет указывать
атрибуты XAML, связанные с кодом (такие как Class или обработчики событий). Для
целей тестирования введите следующий код XAML внутри <Window>:
<StackPanel>
<Rectangle Fill = "Green" Height = 0" Width = 00" />
<Button Content = "OK!" Height = 0" Width = 00" />
<Label Content ="{x:Type Label}" />
</StackPanel>
После щелчка на кнопке появится окно, визуализирующее определение XAML (или,
возможно, окно с сообщением об ошибке разбора — будьте внимательны при вводе). На
рис. 27.24 показан возможный вывод.
Рис. 27.24. Приложение MyXamlPad.exe в действии
На этом пример завершен. Наверняка вы сразу же придумаете массу возможных
усовершенствований для этого приложения, однако сначала нужно научиться работать
с элементами управления WPF и диспетчерами компоновки, содержащими их. Этим мы
займемся в следующей главе.
Исходный код. Проект MyXamlPad доступен в подкаталоге Chapter 27.
Глава 27. Введение в Windows Presentation Foundation и XAML 1047
Резюме
Windows Presentation Foundation (WPF) — это набор инструментов для построения
пользовательских интерфейсов, появившийся в версии .NET 3.0. Основная цель WPF
состоит в интеграции и унификации множества ранее разрозненных настольных
технологий (двух- и трехмерная графика, разработка окон и элементов управления и т.п.)
в единую унифицированную программную модель. Помимо этого, в WPF-приложениях
обычно используется расширяемый язык разметки приложений (Extendable Application
Markup Language —<XAML), который позволяет определять внешний вид и поведение
элементов WPF через разметку.
Вспомните, что XAML позволяет описывать деревья объектов .NET с применением
декларативного синтаксиса. Во время исследований XAML в настоящей главе вы
узнали несколько новых частей синтаксиса, включая синтаксис "свойство-элемент" и
присоединяемые свойства, а также роль преобразователей типов и расширений разметки
XAML.
Хотя XAML — ключевой аспект любого профессионального WPF-приложения, в
первом примере этой главы было показано, как построить программу WPF исключительно
с помощью кода С#. Затем вы узнали, как строить программу WPF из одного XAML (что,
однако, не рекомендуется; это был всего лишь учебный пример). Наконец, было
продемонстрировано применение "файлов кода", которые позволяют отделять поведение от
функциональности.
В последнем примере этой главы строилось WPF-приложение, позволяющее
программно взаимодействовать с определениями XAML с использованием классов
XamlReader и XamlWriter. Кроме того, вы ознакомились с визуальными
конструкторами WPF среды Visual Studio 2010.
;
ГЛАВА 28
Программирование
с использованием
элементов
управления WPF
В предыдущей главе был заложен фундамент модели программирования WPF,
включая рассмотрение классов Window и Application, грамматику XAML и
использование файлов кода. Вы также ознакомились с процессом построения WPF-приложений
посредством визуальных конструкторов среды Visual Studio 2010. Здесь мы углубимся
в конструирование более сложных пользовательских интерфейсов с применением
нескольких новых элементов управления и диспетчеров компоновки.
В этой главе также будут рассмотрены некоторые связанные с элементами
управления WPF темы, такие как программная модель привязки данных и использование
команд управления. Вы узнаете, как работать с интерфейсами Ink API и Documents API,
позволяющие получать ввод от пера (или мыши) и создавать RTF-документы, используя
XML Paper Specification.
Наконец, в главе будет показано применение Expression Blend для построения
пользовательских интерфейсов для WPF-приложений. Использование Expression Blend в
проектах WPF не является обязательным, но эта IDE-среда может значительно
упростить разработку, поскольку генерирует необходимый код XAML, используя множество
интегрированных визуальных конструкторов, редакторов и мастеров.
Обзор библиотеки элементов управления WPF
Если только вы не новичок в области построения графических интерфейсов
пользователя (что нормально), общее назначение элементов управления WPF не должно
вызывать вопросы. Независимо от того, какой набор инструментов для построения
графических интерфейсов вы применяли в прошлом (MFC, Java AWT/Swing, Windows Forms,
VB 6.0, Mac OS X (Cocoa) или GTK+/GTK#), распространенные элементы управления,
представленные в табл. 28.1, скорее всего, покажутся знакомыми.
Глава 28. Программирование с использованием элементов управления WPF 1049
Таблица 28.1. Основные элементы управления WPF
Категория элементов
управления WPF
Примеры членов
Назначение
Основные пользовательские
элементы управления
Элементы украшения окон
и элементов управления
Элементы мультимедиа
Элементы управления
компоновкой
Button, RadioButton,
ComboBox, CheckBox,
Calendar, DatePicker,
Expander, DataGrid,
ListBox, ListView,
Slider, ToggleButton,
TreeView, ContextMenu,
ScrollBar, Slider,
TabControl, TextBlock,
TextBox, RepeatButton,
RichTextBox, Label
Menu,
ToolBar,
StatusBar,
ToolTip,
ProgressBar
Image,
MediaElement,
SoundPlayerAction
Border, Canvas, DockPanel,
Grid, GridView, GridSplitter,
GroupBox, Panel, TabControl,
StackPanel, Viewbox,
WrapPanel
WPF предлагает полное
семейство элементов управления,
которые можно использовать для
построения пользовательских
интерфейсов
Эти элементы
пользовательского интерфейса служат для
декорирования рамки объекта
Window компонентами для ввода
(наподобие Menu) и элементами
информирования пользователя
(StatusBar, ToolTip и т.п.)
Эти элементы управления
предоставляют поддержку
воспроизведения аудио/видео и
визуализации изображений
WPF предлагает множество
элементов управления, которые
позволяют группировать и
организовывать другие элементы
для управления компоновкой
Большинство этих стандартных элементов
управления WPF упаковано в пространство имен System.
Windows.Controls сборки PresentationFramework.dll.
При построении приложения WPF в Visual Studio 2010
большинство этих элементов находится в панели
инструментов (ТЪоШох), когда в активном окне открыт
визуальный конструктор WPF (рис. 28.1).
Как и при создании приложений Windows Forms,
эти элементы можно перетаскивать на поверхность
визуального конструктора WPF и конфигурировать
их в окне Properties (Свойства), о котором вы узнали
в предыдущей главе. Хотя Visual Studio 2010
сгенерирует значительный объем XAML автоматически, нет
ничего необычного в ручном редактировании
разметки. Давайте обратимся к основам.
Работа с элементами управления
WPF в Visual Studio 2010
Вы можете вспомнить из главы 27, что после
помещения элемента управления WPF на поверхность
л Common WPF Controls
*
В
(S
в
ш
№
СП
*л
А
S3
©
□
т
и
Ж
|*ы1
Pointer
Border
Button
CheckBox
ComboBox
DataGrid
Grid
Image
Label
ListBox
RadioButton
Rectangle
StackPanel
TabControl
TextBlock
TextBox
л АН WPF Controls
*
D
@
^
S3
Pointer
Border
Button
Calendar
Canvas
Рис. 28.1. Панель инструментов
Visual Studio 2010 представляет
встроенные элементы управления WPF
1050 Часть VI. Построение настольных пользовательских приложений с помощью WPF
визуального конструктора Visual Studio 2010 необходимо установить свойство x:Name в
окне Properties, поскольку оно позволяет обращаться к объекту в связанном файле кода
С#. Кроме того, для генерации обработчиков событий выбранного элемента управления
можно использовать вкладку Events (События) окна Properties. Таким образом, с
помощью Visual Studio можно сгенерировать следующую разметку для простого элемента
управления Button:
Button x:Xame=,,btnMyButton" Content="Click lie1" Height=,,23" Width=,,140"
Click=,,btnMyButton_Click" />
Здесь свойство Content элемента Button устанавливается в простую строку. Однако,
благодаря модели содержимого элементов управления WPF, можно оформить Button,
включающий следующее сложное содержимое:
<Button x:Name=,,btnMyButtOD" Height=,,121" Width=56" Click=,,btnShowDlg_Click">
<Button.Content^
<StackPanel Height=,,95" Width=28" Orientation=,,Vertical">
<Ellipse Fill = "Red" Width=,,52" Height = ,,45" Margin=,7>
<Label Width=9" FontSize=,,20" Content = "Click! " Height=,,36" />
</StackPanel>
</Button.Content>
</Button>
Вы можете также вспомнить, что непосредственный дочерний элемент
унаследованного от ContentControl класса — это неявное содержимое, поэтому при указании
сложного содержимого определять контекст <Button.Context> явно не нужно. В таком
случае достаточно написать следующую разметку:
<Button x:Name=,,btnMyButton" Height=21" Wiath=,,156" Click=MbtnShowDlg_ClickM>
<StackPanel Height=,,95" Width=,,128" Orientarion=,,Vertical">
<Ellipse Fill = ,,Red" Width=,,52" Height=5" Margin="/>
<Label Width=,,59" FontSize=,,20" Content="Clickl " Height=6" />
</StackPanel>
</Button>
В любом случае вы устанавливаете свойство Content кнопки B<StackPanel>
связанных элементов. Создавать такого рода сложное содержимое можно также с
использованием визуального конструктора Visual Studio 2010. После определения диспетчер
компоновки можно выбирать в визуальном конструкторе в качестве целевого
компонента для перетаскивания внутренних элементов управления. Каждый из них можно
конфигурировать в окне Properties.
Вы должны помнить, что окно Document Outline (Эскиз документа) в Visual Studio
2010 (которое можно открыть, выбрав пункт меню View1^ Other Windows (Вид1^Другие
окна)} удобно для проектирования элемента управления WPF со сложным содержимым.
Обратите внимание, как на рис. 28.2 показано логическое дерево XAML для созданного
элемента Window. Для предварительного просмотра выбранного элемента щелкните на
любом из этих узлов.
В любом случае, после определения первоначальной разметки можно открыть
связанный файл кода С# для добавления логики реализации:
private void btnShowDlg_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("You clicked the button1");
}
В первой части этой главы для построения пользовательских интерфейсов в
примерах проектов используется Visual Studio 2010. Здесь мы будем отмечать
дополнительные средства по мере необходимости, однако стоит потратить некоторое время на
исследование опций окна Properties и функциональности визуального конструктора.
Глава 28. Программирование с использованием элементов управления WPF 1051
Document Outline
л Window
л Grid
л 'Button (btnShowDtg)
л L Content
л StackPanel
„ [-Ellipse к
I-Label ^
d Design ti EXAML
<6rid>
<Button Height=21" HorizontalAlign«ent»"Left" Margin»
Naa»e="btnShowOlg" VerticalAlignment»"Top" Width
El <Button.Content>
В <StackPanel Height»"95" Width=28" Orientation^]
<Ellipse Fili-"Red" Width«5" Height»6"
<Label Width-9" FontSize«0" Content«"Cl
</StaekPanel>
</Button-Content>
</Button>
| ! </6rid>
: </Window>
1Q0% , «fij^ ». '.^^Д
I Л.г,
Ihpse
Рис. 28.2. Окно Document Outline в Visual Studio 2010 помогает
в навигации по сложному содержимому
После этого в главе для построения пользовательских интерфейсов будет
применяться Expression Blend. Как будет показано, Expression Blend включает множество
встроенных инструментов, генерирующих большую часть необходимой XAML-разметки
автоматически.
Элементы управления Ink API
В дополнение к обычным элементам управления WPF, перечисленным в табл. 28.1,
в WPF определены дополнительные элементы для работы с API-интерфейсом
"цифровых чернил" (digital Ink API). Этот API-интерфейс полезен при разработке приложений
для планшетных ПК (Tablet PC), поскольку позволяет получать ввод от пера. Однако это
не означает, что стандартные настольные приложения не могут пользоваться Ink API,
так как некоторые определенные в нем элементы управления могут получать ввод от
мыши.
Пространство имен System.Windows.Ink сборки PresentationCore.dll содержит
разнообразные типы, поддерживающие Ink API (например, Stroke и StrokeCollection);
однако большинство элементов управления Ink API (такие как InkCanvas и
InkPresenter) упакованы в общие элементы управления WPF из пространства имен
System.Windows.Controls сборки PresentationFramework.dll. Более подробно Ink
API рассматривается далее в главе.
Элементы управления документами WPF
В WPF предлагаются элементы управления для обработки расширенных
документов, позволяя строить приложения, которые поддерживают функциональность в стиле
Adobe PDF Используя типы из пространства имен System.Windows.Documents (также
из сборки PresentationFramework.dll), можно создавать готовые к печати
документы, поддерживающие масштабирование, поиск, пользовательские аннотации ("клейкие"
заметки) и прочие развитые средства работы с текстом.
1052 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Однако "за кулисами" элементы управления документов не используют API-
интерфейсы Adobe PDF, а вместо этого работают с API-интерфейсом XML Paper
Specification. Конечные пользователи никакой разницы не заметят, поскольку
документы PDF и документы XPS имеют почти идентичный вид и поведение. В
действительности доступно множество бесплатных утилит, которые позволяют преобразовывать эти
форматы друг в друга на лету. В последующих примерах мы будем иметь дело с
некоторыми аспектами элементов управления документами.
Общие диалоговые окна WPF
В WPF также предоставляются несколько диалоговых окон, таких как OpenFileDialog
и SaveFileDialog. Эти диалоговые окна определены внутри пространства имен
Microsoft.Win32 сборки PresentationFramework.dll. Работа с каждым из этих
диалоговых окон сводится к созданию объекта и вызову метода ShowDialogO:
using Microsoft.Win32;
namespace WpfControls
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent ();
}
private void btnShowDlg_Click (object sender, RoutedEventArgs e)
{
// Отобразить диалоговое окно сохранения файла.
SaveFileDialog saveDlg = new SaveFileDialog ();
saveDlg.ShowDialog ();
}
}
}
Как и следовало ожидать, в этих классах определены различные члены,
позволяющие устанавливать фильтры файлов и пути каталогов и получать доступ к выбранным
пользователем файлам. Некоторые диалоговые окна будут использоваться в
последующих примерах; кроме того, будет показано, как строить специальные диалоговые окна
для получения пользовательского ввода.
Подробные сведения находятся в документации
Целью этой главы не является обзор всех членов каждого элемента управления WPF.
Вместо этого будет дан обзор различных элементов управления с упором на лежащую
в основе программную модель и ключевые службы, общие для большинства элементов
управления WPF. Вы также узнаете, как строить пользовательские интерфейсы с
помощью Expression Blend и Visual Studio 2010.
Чтобы получить полное представление о конкретной функциональности
определенного элемента управления, обращайтесь в документацию .NET Framework 4.0 SDK.
В частности, элементы управления описаны в разделе Control Library (Библиотека
элементов управления) справочной системы, ссылка на который находится ниже ссылки
Windows Presentation Foundation Controls (Элементы управления Windows Presentation
Foundation), как показано на рис. 28.3.
Глава 28. Программирование с использованием элементов управления WPF 1053
Library Home
Viiual Studio 2010
.NET Framework 4
Windows Presentation Foundation
Controls
Control Library
Border
BuIletDecorator
Button
Canvas
ChecfcBo*
ComboBox
ContextMenu
DataGiid
Doer. Panel
Document Viewer
Expander
Flow/Document PageViewer
FlowDocumentReader
FlowDocumentScroUVtewer
Frame
Grid
The Windows Presentation Foundation (WPF) control library
contains information on the controls provided by Windows
Presentation Foundation (WPF), listed alphabetically.
In This Section
Border
BuIletDecorator
Button
Canvas
Checkfiox
ComboBox
ContextMenu
DockPanel
DocumentViewer
fr Internet | Protected Mode Off
/i » ^95%
Рис. 28.3. Детальная информация по каждому элементу управления
WPF доступна по нажатию клавиши <F1>
Управление компоновкой содержимого
с использованием панелей
Реальное приложение WPF неизбежно содержит изрядное количество элементов
пользовательского интерфейса (элементов ввода, графического содержимого, систем
меню, строк состояния и т.п.), которые должны быть хорошо организованы внутри
содержащего их окна. Кроме того, виджеты пользовательского интерфейса должны вести
себя адекватно в случае изменения конечным пользователем размеров всего окна или
его части (как в случае окна с разделителем). Для гарантирования того, что элементы
управления WPF сохранят свое положение в принимающем окне, предусмотрено
значительное количество типов панелей.
При объявлении элемента управления непосредственно внутри окна, не имеющего
панелей, он размещается в центре окна. Рассмотрим следующее простое объявление
окна, содержащего единственный элемент типа Button. Независимо от того, как вы
будете изменять размеры окна, виджет пользовательского интерфейса всегда окажется на
равном удалении от всех четырех границ клиентской области.
<•-- Эта кнопка всегда находится в центре окна -->
<Window x:Class="MyWPFApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Fun with Panels'" Height=85" Width=25">
<Button x:Name="btnOK" Height = 00" Width="80" Content="OK"/>
</Window>
Также вспомните, что попытка разместить несколько элементов прямо в
контексте <Window> приводит к ошибке разметки во время компиляции. Причина в том, что
свойству Content окна (или любого наследника ContentControl) может быть присвоен
только один объект:
<!-- Ошибка! Свойство Content неявно устанавливается более одного раза! -->
<Window x:Class="MyWPFApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation"
1054 Часть VI. Построение настольных пользовательских приложений с помощью WPF
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Fun with Panels1" Height=,,285" Width=,,325">
<■-- Два непосредственных дочерних элемента <Window>! -->
<Label x:Name="lblInstructions"
Width=28" Height=7" FontSize=5" Content="Enter Information"/>
<Button x:Name="btnOK" Height = 00" Width="8 0" Content="OK"/>
</Window>
Понятно, что от окна, которое может содержать единственный элемент, толку мало.
Поэтому, когда в окно требуется поместить множество элементов, это делается с
помощью панелей. Панель будет содержать все элементы пользовательского интерфейса,
представляющие окно, после чего сама панель используется как единственный объект,
присваиваемый свойству Content окна.
Пространство имен System.Windows.Controls предлагает многочисленные панели,
каждая из которых по-своему обслуживает внутренние элементы. Используя панели, вы
сможете устанавливать поведение элементов управления при изменении размеров окна
пользователем — будут они оставаться в том же месте, куда были помещены во
время проектирования, будут располагаться свободным потоком слева направо или сверху
вниз, и т.д.
В одних панелях допускается помещать другие панели (вроде DockPanel, которая
содержит StackPanel с другими элементами), чтобы обеспечить еще более высокую
гибкость и степень управления. В табл. 28.2 перечислены некоторые из наиболее часто
используемых панелей WPF.
Таблица 28.2. Основные панели WPF
Панель Назначение
Canvas Предлагает классический режим размещения содержимого. Элементы
остаются в точности там, где были размещены во время проектирования
DockPanel Привязывает содержимое к определенной стороне панели (Тор (верхняя),
Bottom (нижняя), Left (левая) или Right (правая))
Grid Располагает содержимое внутри серии ячеек, расположенных в табличной сетке
StackPanel Складывает содержимое по вертикали или горизонтали, в зависимости от
значения свойства Orientation
WrapPanel Позиционирует содержимое слева направо, перенося на следующую строку
по достижении границы панели. Последовательность размещения происходит
сначала сверху вниз или сначала слева направо, в зависимости от значения
свойства Orientation
В следующих нескольких разделах будет показано, как использовать эти типы
панелей, для чего предопределенные данные XAML будут копироваться в приложение
MyXamlPad.exe, созданное в главе 27 (при желании данные можно также загружать и
в редактор Kaxaml). Необходимые файлы XAML находятся в подкаталоге PanelMarkup
каталога Chapter 28 (рис. 28.4).
Позиционирование содержимого внутри панелей Canvas
Панель Canvas поддерживает абсолютное позиционирование содержимого
пользовательского интерфейса. Если конечный пользователь изменяет размер окна, делая его
меньше, чем компоновка, обслуживаемая панелью Canvas, ее внутреннее содержимое
становится невидимым до тех пор, пока контейнер вновь не увеличится до размера,
равного или больше начального размера области Canvas.
Глава 28. Программирование с использованием элементов управления WPF 1055
Л Favorites
■ Desktop
Ш Recent Places
м* Libraries
[*] Documents
л Musk
Й GridWithSplitterjtam!
*, ScrollViewenxaml
{j*j SimpleCanvasjcaml
•: SimpleDockPanel.xaml
£5 SimpleGrid.xarnl
\шттштШтт il,
9/2Я
9/2Я
9/2s[
9.25
9/25
Рис. 28.4. Для тестирования различных компоновок эти ХАМL-данные
будут загружаться в приложение MyXamlPad.exe
Для добавления содержимого к Canvas определите необходимые элементы
управления внутри области между открывающим (<Canvas>) и закрывающим (</Canvas>)
дескрипторами. Затем для каждого элемента управления укажите левый верхний угол
с использованием свойств Canvas .Top и Canvas .Left; здесь должна начинаться
визуализация. Нижняя правая область для каждом элементе управления может быть задана
неявно, за счет установки свойств Canvas.Height и Canvas.Width, либо явно — через
свойства Canvas.Right и Canvas.Bottom.
Чтобы увидеть Canvas в действии, откройте файл SimpleCanvas.xaml (входящий в
состав примеров кода для этой главы) в текстовом редакторе и скопируйте его
содержимое в приложение MyXamlPad.exe (или Kaxaml). Вы должны увидеть следующее
определение Canvas:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Fun with Panels!" Height=85" Width=25">
<Canvas Background="LightsteelBlue">
<Button x:Name="btnOK" Canvas.Left=12" Canvas.Top=03"
Width="80" Content=,,OK"/>
<Label x:Name="lblInstructions" Canvas.Left=7" Canvas.Top=4"
Width=28" Height=7" FontSize=,,15"
Content="Enter Car Information"/>
<Label x:Name="lblMake" Canvas.Left=7" Canvas.Top=0"
Content="Make"/>
<TextBox x:Name="txtMake" Canvas.Left="94" Canvas.Top=0"
Width=93" Height=5"/>
<Label x:Name="lblColor" Canvas.Left=7" Canvas.Top=09"
Content="Color"/>
<TextBox x:Name="txtColor" Canvas.Left="94" Canvas.Top=07"
Width=93" Height=5"/>
<Label x:Name="lblPetName" Canvas.Left=7" Canvas.Top=55"
Content="Pet Name"/>
<TextBox x:Name="txtPetName" Canvas.Left="94" Canvas.Top=53"
Width=93" Height=5"/>
</Canvas>
</Window> I
Щелчок на кнопке View Xaml (Показать Xaml) приведет к отображению окна,
показанного на рис. 28.5.
1056 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Рис. 28.5и Диспетчер компоновки Canvas обеспечивает
абсолютное позиционирование содержимого
Обратите внимание, что порядок, в котором объявляются элементы содержимого
внутри Canvas, для вычисления местоположения не используется; вместо этого оно
базируется на размерах элемента и свойствах Canvas.Top, Canvas.Bottom, Canvas.Left
и Canvas.Right.
На заметку! Если подэлементы внутри Canvas не определяют специфическое
местоположение с использованием синтаксиса присоединяемых свойств (например, Canvas.Left и
Canvas.Top), они автоматически прикрепляются к верхнему левому углу Canvas.
Хотя применение типа Canvas может показаться предпочтительным способом
расположения содержимого (поскольку выглядит столь знакомым), этому подходу присущи
некоторые ограничения. Во-первых, элементы внутри Canvas не изменяют себя
динамически при применении стилей или шаблонов (например, их шрифты не изменяются).
Во-вторых, Canvas не пытается сохранять элементы видимыми, когда пользователь
изменяет размер окна в меньшую сторону.
■ Возможно, наилучшим применением типа Canvas является позиционирование
графического содержимого. Например, при построении изображения с использованием
XAML определенно понадобится, чтобы все линии, фигуры и текст оставались именно
там, где они были размещены, а не позволять им динамически перемещаться при
изменении размеров окна. Мы еще вернемся к Canvas в следующей главе, когда речь пойдет
о службах визуализации графики WPF.
Позиционирование содержимого внутри панелей WrapPanel
Wrap Panel позволяет определить содержимое, которое обтекает панель при
изменении размеров окна. При позиционировании элементов в WrapPanel вы не
указываете положение относительно верхней, левой, нижней и правой границ, как это обычно
делается с Canvas. Однако для каждого подэлемента могут быть определены значения
Height и Width (наряду с другими значениями свойств) для управления общим
размером контейнера.
Поскольку содержимое внутри WrapPanel не стыкуется к определенной стороне
панели, порядок определения элементов важен (содержимое визуализируется от первого
элемента к последнему). В файле SimpleWrapPanel.xaml имеется следующая разметка
(заключенная в определении <Window>):
<WrapPanel Background=llLightSteelBlue">
<Label x:Name=,,lblInstructionM Width=M328M
Height=,,27" FontSize=,,15" Content="Enter Car Information"^
Глава 28. Программирование с использованием элементов управления WPF 1057
<Label x:Name="lblMake" Content="Make"/>
<TextBox Name="txtMake" Width=93" Height=M25n/>
<Label x:Name=,,lblColor" Content="Color,,/>
<TextBox Name=,,txtColor" Width=93" Height=M25"/>
<Label x:Name=,,lblPetName" Content="Pet Name"/>
<TextBox x,:Name=,,txtPetNameM Width=,,193" Height=M25"/>
<Button x:Name=,,btnOK" Width=n80" Content=MOKn/>
</WrapPanel>
Загрузив эту разметку и попробовав изменить ширину окна, содержимое будет
перетекать внутри окна слева направо (рис. 28.6).
И j Fun with Panels!
Sli&i]
Enter Car Information
Рис. 28.6. Содержимое WrapPanel ведет себя подобно традиционной HTML-странице
По умолчанию содержимое WrapPanel перетекает слева направо. Однако если
изменить значение свойства Orientation на Vertical, то содержимое будет располагаться
сверху вниз:
<WrapPanel Orientation ="Vertical">
Панель WrapPanel (как и некоторые другие типы панелей) может быть объявлена с
указанием значений ItemWidth и ItemHeight, которые управляют размером по
умолчанию каждого элемента. Если подэлемент предоставляет собственные значения Height
и/или Width, он позиционируется относительно размера установленного для него
панелью. Рассмотрим следующую разметку:
<WrapPanel Background="LightSteelBlue" ItemWidth =00" ItemHeight =0">
<Label x:Name="lblInstruction"
FontSize=5" Content="Enter Car Information"/>
<Label x:Name="lblMake" Content="Make"/>
<TextBox x:Name="txtMake"/>
<Label x:Name="lblColor" Content="Color"/>
<TextBox x:Name="txtColor"/>
<Label x:Name="lblPetName" Content="Pet Name"/>
<TextBox x:Name="txtPetName"/>
<Button x:Name="btnOK" Width ="80" Content="OK"/>
</WrapPanel>
После визуализации получается окно, показанное на рис. 28.7 (обратите внимание
на размер и положение элемента управления Button, для которого было задано
уникальное значение Width).
Рис. 28.7. Панель WrapPanel может устанавливать ширину и высоту
каждого заданного элемента
1058 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Взглянув на рис. 28.7, трудно не согласиться с тем, что Wrap Panel — обычно не
лучший выбор для непосредственного расположения содержимого в окне, поскольку его
элементы могут перемешиваться, когда пользователь изменяет размеры окна. В
большинстве случаев WrapPanel будет подэлементом панели другого типа, позволяя
небольшой области окна заворачивать свое содержимое при изменении размера (как,
например, элемент управления Toolbar).
Позиционирование содержимого
внутри панелей StackPanel
Подобно WrapPanel, элемент управления
StackPanel организует содержимое в одну строку,
которая может быть ориентирована
горизонтально или вертикально (по умолчанию), в зависимости
от значения, присвоенного свойству Orientation.
Однако отличие между ними состоит в том, что
StackPanel не пытается переносить
содержимое при изменении размера окна пользователем.
Вместо этого элементы внутри StackPanel просто
растягиваются (в зависимости от ориентации),
приспосабливаясь к размеру самой панели StackPanel.
Например, в файле SimpleStackPanel.xaml содержится следующая разметка, которая
дает вывод, показанный на рис. 28.8:
<StackPanel Background=,,LightSteelBlue">
<Label x:Name="lblInstruction11
FontSize=511 Content="Enter Car Information"/>
<Label x:Name=,,lblMake" Content="Make,,/>
<TextBox x:Name="txtMake,,/>
<Label x:Name=,,lblColor" Content=,:Color"/>
<TextBox Name="txtColor,,/>
<Label x:Name=,,lblPetName" Content="Pet Name"/>
<TextBox x:Name=,,txtPetName"/>
<Button x:Name=,,btnOK" Content="OK,,/>
</StackPanel>
Если присвоить свойству Orientation значение Horizontal, то визуализированный
вывод станет таким, как показано на рис. 28.9:
<StackPanel Orientation ="Horizontalll>
Рис. 28.8. Вертикальное
расположение содержимого
f
\ > Fun with Panels1 л
Enter Car Information
rtake This will resize as I typej Color
Pet Name
_
1
OK
_J
Рис. 28.9. Горизонтальное расположение содержимого
Опять-таки, как и WrapPanel, использовать StackPanel для прямого расположения
содержимого окна редко когда придется. Вместо этого StackPanel больше подходит в
качестве подпанели какой-то главной панели.
Глава 28. Программирование с использованием элементов управления WPF 1059
Позиционирование содержимого внутри панелей Grid
Из всех панелей, предлагаемых API-интерфейсом WPF, наиболее гибкой является
Grid. Подобно HTML-таблице, Grid может состоять из набора ячеек, каждая из которых
имеет свое содержимое. При определении Grid выполняются следующие шаги.
1. Определение и конфигурирование каждого столбца.
2. Определение и конфигурирование каждой строки.
3. Назначение содержимого каждой ячейке сетки с использованием синтаксиса
присоединяемых свойств.
На заметку! Если не определить никаких строк и столбцов, то по умолчанию <Grid> будет
состоять из одной ячейки, занимающей всю поверхность окна. Кроме того, если не указать ячейку
для подэлемента <Grid>, он автоматически разместится в столбце 0 и строке 0.
Первые два шага (определение столбцов и строк) выполняются за счет
использования элементов <Grid.ColumnDef initions> и <Grid.RowDef initions>, которые
содержат коллекции элементов <ColumnDefinition> и <RowDefinition>, соответственно.
Поскольку каждая ячейка внутри сетки является настоящим объектом .NET, можно
должным образом конфигурировать внешний вид и поведение каждого элемента.
Ниже приведено простое определение <Grid> (из файла SimpleGrid.xaml), которое
организует содержимое пользовательского интерфейса, как показано на рис. 28.10:
<Grid ShowGridLines =MTrue" Background ="AliceBluell>
<!— Определить строки/столбцы -->
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<!-- Добавить элементы в ячейки сетки -->
<Label x:Name="lblInstruction11 Grid.Column =" Grid.Row ="
FontSize=,,15" Content=MEnter Car Information"/^
<Button x:Name=MbtnOKM Height =0M Grid.Column =" Grid.Row =" Content=,,0K,7>
<Label x:Name=,,lblMake" Grid.Column =" Grid.Row =" Content=,,Make"/>
<TextBox x:Name=,,txtMake" Grid.Column =" Grid.Row =" Width=93" Height=,,25,7>
<Label x:Name=,,lblColorM Grid.Column =" Grid.Row =" Content="Color,,/>
<TextBox x:Name=,,txtColor" Width=93" Height=5" Grid.Column =" Grid.Row =" />
<!— Чтобы сделать картину интереснее, добавим цвет ячейке имени -->
<Rectangle Fill ="LightGreen" Grid.Column ="l" Grid.Row ="l" />
<Label x:Name=,,lblPetNameM Grid.Column =" Grid.Row =" Content="Pet Name"/>
<TextBox x:Name=MtxtPetNameM Grid.Column ="l" Grid.Row ="l"
Width=93" Height=5,,/>
</Grid>
Обратите внимание, что каждый элемент (включая элемент Rectangle
светло-зеленого цвета) прикрепляется к ячейке сетки, используя присоединяемые свойства
Grid.Row и Grid.Column. Порядок ячеек по умолчанию начинается с левой верхней,
которая указана с помощью Grid.Column=" и Grid.Row=". Учитывая, что сетка
состоит всего из четырех ячеек, нижняя правая ячейка может быть идентифицирована
как Grid.Column=,,l" и Grid.Row="l".
1060 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Рис. 28.10. Панель Grid в действии
Панели Grid с типами GridSplitter
Панели Grid также могут поддерживать разделители (splitters). Как известно,
разделители позволяют конечному пользователю изменять размеры столбцов и строк
сетки. При этом содержимое каждой изменяемой в размере ячейки реорганизует себя на
основе содержащихся в ней элементов. Добавить разделители к Grid довольно просто:
для этого необходимо определить элемент управления <GridSplitter> и с
использованием синтаксиса присоединяемых свойств указать строку или столбец, к которому он
относится.
Имейте в виду, что для обеспечения видимости на экране разделителя потребуется
указать значение Width или Height (в зависимости от вертикального или
горизонтального разделения). Ниже показана простая панель Grid с разделителем на первом
столбце (Grid.Column=") из файла GridWithSplitter.xaml:
<Grid Background =llAliceBlue">
<!-- Определить столбцы -->
<Grid.ColumnDefinitions>
<ColumnDefmition Width =llAuto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<!-- Добавить метку в ячейку 0 -->
<Label x:Name="lblLeft11 Background ="GreenYellow11
Grid.Column=M0M Content =MLeft|,,/>
<!— Определить разделитель —>
<GridSplitter Grid.Column =" Width ="/>
<!— Добавить метку в ячейку 1 -->
<Label x:Name=,,lblRight" Grid. Column =" Content ="Right! "/>
</Grid>
Прежде всего, обратите внимание, что столбец с разделителем имеет свойство Width,
значение которого установлено в Auto. Кроме того, элемент <GridSplitter>
использует синтаксис присоединяемых свойств для установки столбца, с которым он работает.
Если вы взглянете на вывод, то увидите там 5-пиксельный разделитель, который
позволяет изменять размер каждого элемента Label. Поскольку для каждого элемента Label
не было задано свойство Height или Width, они заполняют всю ячейку (рис. 28.11).
Позиционирование содержимого внутри панелей DockPanel
Обычно DockPanel используется в качестве главной панели, содержащей любое
количество дополнительных панелей для группирования их в связанные контейнеры.
Панели DockPanel применяют синтаксис присоединяемых свойств (как было
показано в типах Canvas и Grid) для управления тем, куда будет стыкован каждый элемент
внутри DockPanel.
Глава 28. Программирование с использованием элементов управления WPF 1061
Рис. 28.11. Панель Grid с разделителями
В файле SimpleDockPanel.xaml определена следующая простая панель DockPanel,
которая дает результат, показанный на рис. 28.12:
<DockPanel LastChildFill ="True">
<!-- Прикрепить элементы к панели -->
<Label x:Name="lblInstruction11 DockPanel. Dock ="Top"
FontSize=511 Content="Enter Car Information"/>
<Label x:Name="lblMake11 DockPanel. Dock ="Left" Content="Makell/>
<Label x:Name=MlblColorM DockPanel.Dock ="Right" Content=MColorM/>
<Label x:Name=,,lblPetName" DockPanel. Dock =,,Bottom" Content="Pet Name'7>
<Button x:Name="btnOK" Content="OK"/>
</DockPanel>
lji0 Fun with Panels!
Enter Car Information
Make
Color
Рис. 28.12. Простая панель DockPanel
На заметку! В случае добавления множества элементов к одной стороне DockPanel, они
выстраиваются вдоль указанной грани в порядке объявления.
Преимущество использования типов DockPanel состоит в том, что при изменении
размеров окна пользователем каждый элемент остается прикрепленным к указанной
(в DockPanel.Dock) стороне панели. Также обратите внимание, что в открывающем
дескрипторе <DockPanel> атрибут LastChildFill устанавливается в true. Учитывая,
что элемент Button не задает никакого значения DockPanel.Dock, он будет растянут,
чтобы занять все оставшееся место.
Включение прокрутки в типах панелей
В WPF предусмотрен класс <ScrollViewer>, которые обеспечивает возможность
прокрутки данных внутри объектов панелей. Ниже показан фрагмент разметки из файла
ScrollViewer.xaml:
<ScrollViewer>
<StackPanel>
<Button Content="First11 Background="Green11 Height=ll40"/>
<Button Content=MSecondM Background=,,Red" Height=0"/>
1062 Часть VI. Построение настольных пользовательских приложений с помощью WPF
<Button Content=,,Third" Background=,,Pink" Height=0,,/>
<Button Content=MFourthM Background=,,YellowM Height=0M/>
<Button Content=MFifth" Background=MBlueM Height=M40M/>
</StackPanel>
</ScrollViewer>
Результат приведенного определения XAML можно видеть на рис. 28.13.
Рис. 28.13. Работа с классом ScrollViewer
Как и следовало ожидать, каждая панель предоставляет множество членов,
позволяющих настраивать расположение содержимого. В частности, элементы управления
WPF поддерживают два таких свойства (Padding и Margin), которые позволяют самому
элементу информировать панель о том, как с ним следует обращаться. В частности,
свойство Padding контролирует свободное место, которое должно окружать внутренний
элемент управления, а свойство Margin — пространство, окружающее элемент
управления извне.
На этом экскурс в основные типы панелей WPF и различные способы
позиционирования их содержимого завершен. Далее мы перейдем к рассмотрению примера, в котором
используются вложенные панели для создания системы компоновки главного окна.
Построение главного окна с использованием
вложенных панелей
Как упоминалось ранее, в типичном окне WPF для получения нужной системы
компоновки применяется не одиночный элемент управления типа панели, а одни панели
вкладываются в другие. Ниже будет показано, как вкладывать одни панели в другие, а
также как использовать множество новых элементов управления WPF. Изучение будет
проходить на примере приложения простого текстового процессора. Откройте Visual
Studio 2010 и создайте новое приложение WPF по имени MyWordPad.
Наша цель состоит в том, чтобы получить компоновку, в которой главное окно
имеет расположенную в верхней части систему меню, под ней — панель инструментов и
нижней части окна — строка состояния. Строка состояния будет содержать область для
хранения текстовых сообщений, отображаемых при выборе пункта меню (или кнопки
в панели инструментов), а система меню и панель инструментов предоставят
триггеры пользовательского интерфейса для закрытия приложения и отображения вариантов
правописания в элементе Expander. На рис. 28.14 показана начальная компоновка, в
которой отображаются подсказки по правописанию для "ХАМЕ'.
Обратите внимание, что две кнопки панели инструментов не содержат в себе
ожидаемых изображений, а только текстовые значения. Хотя этого явно не достаточно для
профессионального приложения, установка изображений для кнопок панели
инструментов обычно предполагает использование встроенных ресурсов, и эта тема
рассматривается в главе 30 (пока обойдемся текстом). При наведении на кнопку Check (Проверить)
курсор мыши изменяется, и в единственной панели строки состояния отображается
полезное сообщение пользовательского интерфейса.
Глава 28. Программирование с использованием элементов управления WPF 1063
CALM
AXEL
UXMAL
BALM
PALM
AM
AMYL
EXAM
CAMEL
Show Spelling Suggestions
Яй^Ц, is a very useful tool when building
WPP applications.
Рис. 28.14. Использование вложенных панелей для построения
пользовательского интерфейса окна
Чтобы приступить к построению описанного пользовательского интерфейса,
модифицируйте начальное определение XAML типа Window для использования в качестве
дочернего элемента <DockPanel> вместо <Grid> по умолчанию:
<Window x:Class="MySpellChecker.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MySpellChecker" Height=31" Width=08"
WindowstartupLocation ="CenterScreen">
<!-- Эта панель устанавливает содержимое окна —>
<DockPanel>
</DockPanel>
</Window>
Построение системы меню
Системы меню в WPF представлены классом Menu, который поддерживает
коллекцию объектов Menu Item. При построении системы меню в XAML каждый Menu Item
может обрабатывать различные события, из которых в первую очередь следует упомянуть
Click, происходящее, когда пользователь выбирает подэлемент. В рассматриваемом
примере будут созданы два пункта меню верхнего уровня (File (Файл) и Tools (Сервис);
меню Edit (Правка) строится позже), которые будут содержать в себе подэлементы Exit
(Выход) и Spelling Hints (Подсказки по правописанию), соответственно.
В дополнение к обработке события Click для каждого подэлемента будет также
организована обработка событий MouseEnter и MouseExit, которые используются для
установки текста в строке состояния. Добавьте следующую разметку в контекст элемента
<DockPanel> (используйте окно Properties (Свойства) среды Visual Studio 2010, чтобы
сократить объем ручного ввода):
<'— Закрепить систему меню вверху —>
<Menu DockPanel.Dock ="Top"
HorizontalAlignment="Left" Background="White" BorderBrush ="Black">
<MenuItem Header="_File">
<Separator/>
<MenuItem Header ="_Exit" MouseEnter ="MouseEnterExitArea"
MouseLeave ="MouseLeaveArea" Click ="FileExit_Click"/>
</MenuItem>
<MenuItem Header="_Tools">
<MenuItem Header ="_Spelling Hints" MouseEnter ="MouseEnterToolsHintsArea"
MouseLeave ="MouseLeaveArea" Click ="ToolsSpellingHints_Click"/>
</MenuItem>
</Menu>
1064 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Обратите внимание, что система меню стыкована к верхней части DockPanel. Кроме
того, элемент <Separator> используется для добавления тонкой горизонтальной линии
в систему меню, непосредственно перед пунктом Exit. Значения Header для каждого
Menu Item содержат символ подчеркивания (например, Exit). Это указывает символ,
который станет подчеркнутым, когда пользователь нажмет клавишу <Alt> (для ввода
клавиатурного сокращения).
После построения системы меню необходимо реализовать различные
обработчики событий. Прежде всего, понадобится обработчик пункта меню File^Exit по имени
FileExit_Click(), который просто закрывает окно, что, в свою очередь, приводит к
завершению приложения, поскольку это окно самого высшего уровня. Обработчики
событий MouseEnter и MouseExit для каждого из подэлементов должны обновлять
строку состояния; однако пока мы просто оставим их пустыми. И, наконец, обработчик
ToolsSpellingHintsClickO для пункта меню Tools«=>Spelling Hints также пока
оставим пустым. Ниже показаны текущие обновления файла отделенного кода:
public partial class MainWindow : System.Windows.Window
{
public MainWindow()
{
InitializeComponent ();
}
protected void FileExit_Click(object sender, RoutedEventArgs args)
{
// Закрыть это окно.
this.Close ();
}
protected void ToolsSpellingHints_Click(object sender, RoutedEventArgs args)
{
}
protected void MouseEnterExitArea(object sender, RoutedEventArgs args)
{
}
protected void MouseEnterToolsHintsArea(object sender, RoutedEventArgs args)
{
}
protected void MouseLeaveArea(object sender, RoutedEventArgs args)
{
Построение панели инструментов
Панели инструментов (представленные в WPF классом ToolBar) обычно предлагают
альтернативный способ активизации пунктов меню. Добавьте следующую разметку
непосредственно после закрывающего дескриптора определения <Menu>:
<!-- Разместить панель инструментов ниже области меню -->
<ToolBar DockPanel.Dock ="Top" >
<Button Content ="Exit" MouseEnter ="MouseEnterExitArea"
MouseLeave ="MouseLeaveArea" Click ="FileExit_Click"/>
<Separator/>
<Button Content ="Check" MouseEnter ="MouseEnterToolsHintsArea"
MouseLeave ="MouseLeaveArea" Click ="ToolsSpellingHints_Click"
Cursor="Help" />
</ToolBar>
Элемент управления ToolBar состоит из двух элементов Button, которые
предназначены для обработки тех же самых событий теми же методами из файла кода. С по-
Глава 28. Программирование с использованием элементов управления WPF 1065
мощью этой техники можно дублировать обработчики для обслуживания как пунктов
меню, так и кнопок панели управления. Хотя в этой панели используются типичные
нажимаемые кнопки, имейте в виду, что ToolBar "является" ContentControl, и потому на
его поверхность можно помещать любые типы (раскрывающиеся списки, изображения,
графику и т.п.). Единственное, что еще может интересовать здесь — это то, что кнопка
Check поддерживает специальный курсор мыши через свойство Cursor.
На заметку! Элемент ToolBar может быть дополнительно помещен в элемент <Тоо1ВагТгау>,
который управляет компоновкой, стыковкой и перетаскиванием набора объектов ToolBar. За
подробной информацией обращайтесь в документацию .NET Framework 4.0 SDK.
Построение строки состояния
Элемент управления StatusBar прикрепляется к нижней части <DockPanel> и
содержит единственный элемент управления <TextBlock>, который пока еще в этой главе
не использовался. Элемент TextBlock может применяться для хранения текста с
добавлением форматирования, такого как полужирный текст, подчеркнутый текст, разрывы
строк и т.д. Поместите следующую разметку непосредственно после предшествующего
определения ToolBar:
<!-- Разместить строку состояния внизу -->
<StatusBar DockPanel. Dock ="Bottom11 Background="Beige" >
<StatusBarItem>
<TextBlock Name=,,statBarText" Text=,,Ready"/>
</StatusBarItem>
</StatusBar>
Завершение дизайна пользовательского интерфейса
Финальный аспект дизайна разрабатываемого пользовательского интерфейса
заключается в определении элемента Grid с разделителями и двумя столбцами. Слева будет
элемент управления Expander, который отобразит список подсказок по правописанию,
помещенный в <StackPanel>, а справа — элемент TextBox с поддержкой
многострочного текста, линеек прокрутки и включенной проверкой правописания. Элемент <Grid>
целиком может быть размещен в левой части родительской панели <DockPanel>. Чтобы
завершить определение пользовательского интерфейса окна, добавьте следующую
XAML-разметку непосредственно под разметкой, описывающей StatusBar:
<Grid DockPanel. Dock ="Left" Background ="AliceBlueII>
<i-- Определить строки и столбцы -->
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<GridSplitter Grid.Column =" Width =" Background ="Gray" />
<StackPanel Grid. Column=11 VerticalAlignment ="Stretch11 >
<Label Name=,,lblSpellingInstructions" FontSize=,,14" Margin=0, Ю, 0, 0">
Spelling Hints
</Label>
<Expander Name=IIexpanderSpelling" Header ="Try these!" Margin=0,10,10,10">
<!— Будет заполняться программно -->
<Label Name =,,lblSpellingHints" FontSize =2,,/>
</Expander>
</StackPanel>
<i— это будет областью для ввода -->
<TextBox Grid.Column ="
1066 Часть VI. Построение настольных пользовательских приложений с помощью WPF
SpellCheck.IsEnabled =IITrue"
AcceptsReturn ="True11
Name =IItxtData" FontSize =4"
BorderBrush'="Blue"
VerticalScrollBarVisibility=,,Auto"
HorizontalScrollBarVisibility=llAuto">
</TextBox>
</Grid>
Реализация обработчиков событий MouseEnter/MouseLeave
К этому моменту пользовательский интерфейс окна готов. Осталось предоставить
реализацию остальных обработчиков событий. Начните с обновления файла кода С#
так, чтобы каждый из обработчиков MouseEnter и MouseLeave устанавливал в
текстовой панели строки состояния соответствующий текст сообщения для конечного
пользователя:
public partial class MainWindow : System.Windows.Window
{
protected void MouseEnterExitArea(object sender, RoutedEventArgs args)
statBarText.Text = "Exit the Application";
protected void MouseEnterToolsHintsArea(object sender, RoutedEventArgs args)
statBarText.Text = "Show Spelling Suggestions";
protected void MouseLeaveArea(object sender, RoutedEventArgs args)
statBarText.Text = "Ready";
}
Теперь приложение можно запустить. Обратите внимание, что текст в строке
состояния меняется в зависимости от выбранного пункта меню или кнопки панели
инструментов.
Реализация логики проверки правописания
API-интерфейс WPF имеет встроенную поддержку проверки правописания, которая
не зависит от продуктов Microsoft Office. Это значит, что использовать уровень
взаимодействия с СОМ для обращения к функции проверки правописания Microsoft Word
не придется, а вместо этого данная функциональность добавляется всего несколькими
строками кода.
Вспомните, что при определении элемента управления <TextBox> свойство
SpellCheck.IsEnabled было установлено в true. После этого неправильно написанные
слова будут подчеркиваться красной волнистой линией, как это делается в Microsoft
Office. Более того, лежащая в основе программная модель предоставляет доступ к
механизму проверки правописания, который позволяет получить список предполагаемых
слов, написанных с ошибкой. Добавьте в метод ToolSpellingHints_Click()
следующий код:
protected void ToolsSpellingHints_Click(object sender, RoutedEventArgs args)
{
string spellingHints = string.Empty;
// Попытка получить ошибку правописания в текущем положении курсора ввода.
SpellingError error = txtData.GetSpellingError(txtData.Caretlndex);
Глава 28. Программирование с использованием элементов управления WPF 1067
if (error != null)
{
// Построить строку с подсказками по правописанию.
foreach (string s in error.Suggestions)
{
spellingHints += string.Format("{0}\n", s) ;
}
// Отобразить подсказки и раскрыть элемент Expander.
lblSpellingHints.Content = spellingHints;
expanderSpelling.IsExpanded = true;
}
}
Приведенный код достаточно прост. С помощью свойства Caretlndex определяется
текущее положение курсора ввода в текстовом поле и извлекается объект SpellingError.
Если в указанном месте есть ошибка (т.е. значение не равно null), производится
проход в цикле по списку подсказок с использованием свойства Suggestions. После
получения все подсказки по правописанию помещаются в метку Label внутри элемента
Expander.
Вот и все! С помощью всего нескольких строк процедурного кода (и приличной
порции XAML-разметки) заложен фундамент для будущего текстового процессора. После
освоения управляющих команд можно будет добавить несколько новых возможностей.
Понятие управляющих команд WPF
В Windows Presentation Foundation предлагается поддержка независимых от
элементов управления событий через управляющие команды (control commands). Обычное
событие .NET определяется внутри некоторого базового класса и может быть
использовано этим классом и его наследниками. Таким образом, нормальные события .NET очень
тесно связаны с классом, в котором они определены.
В отличие от этого, управляющие команды WPF представляют собой подобные
событиям сущности, которые независимы от конкретного элемента управления и во многих
отношениях могут успешно применяться к многочисленным (и внешне несвязанным)
типам элементов управления. Приведем несколько примеров: WPF поддерживает
команды Сору (Копировать), Paste (Вставить) и Cut (Вырезать), которые могут
применяться к широкому разнообразию элементов пользовательского интерфейса (пунктам
меню, кнопкам панели инструментов, специальным кнопкам), а также клавиатурные
комбинации (<Ctrl+C>, <Ctrl+V> и т.д.).
Хотя другие наборы инструментов для построения пользовательских интерфейсов
(вроде Windows Forms) предлагают для этих целей стандартные события, в результате
получается избыточный и трудный в сопровождении код. В модели WPF команды могут
использоваться в качестве альтернативы. В результате получается более компактный и
гибкий код.
Внутренние объекты управляющих команд
WPF поставляется с множеством встроенных управляющих команд, каждая из
которых может быть ассоциирована в соответствующей горячей клавишей (или другим
источником ввода). Говоря языком программиста, управляющая команда WPF — это
любой объект, поддерживающий свойство (часто называемое Command), которое
возвращает объект, реализующий интерфейс ICommand:
public interface ICommand
{
// Возникает, когда происходит изменение режима,
1068 Часть VI. Построение настольных пользовательских приложений с помощью WPF
// должна или нет выполняться данная команда.
event EventHandler CanExecuteChanged;
// Задает метод, определяющий, может ли
// команда выполняться в ее данном состоянии.
bool CanExecute (object parameter);
// Определяет метод, вызываемый для выполнения команды.
void Execute(object parameter);
}
Хотя можно предоставить собственную реализацию этого интерфейса для
создания управляющей команды, шансы делать это довольно незначительны, учитывая, что
функциональность пяти классов WPF предлагает около сотни готовых объектов команд.
В этих классах определены многочисленные свойства, представляющие специфические
объекты команд, каждый из которых реализует интерфейс ICommand.
В табл. 28.3 описаны некоторые стандартные объекты команд (более подробную
информацию ищите в документации .NET Framework 4.0 SDK).
Таблица 28.3. Стандартные объекты команд WPF
Класс WPF
Объекты команд
Назначение
ApplicationCommands
ComponentCommands
MediaCommands
NavigationCommands
EditingCommands
Close, Copy, Cut, Delete,
Find, Open, Paste, Save,
SaveAs, Redo, Undo
MoveDown, MoveFocusBack,
MoveLeft, MoveRight,
ScrollToEnd, ScrollToHome
BoostBase, ChannelUp,
ChannelDown, FastForward,
NextTrack, Play, Rewind,
Select,Stop
BrowseBack, BrowseForward,
Favorites, LastPage,
NextPage, Zoom
AlignCenter,
CorrectSpellingError,
DecreaseFontSize,
EnterLineBreak,
EnterParagraphBreak,
MoveDownByLine, MoveRightByWord
Разнообразные команды
уровня приложения.
Разнообразные
команды, которые являются
общими для
компонентов пользовательского
интерфейса.
Разнообразные
команды, связанные с
мультимедиа.
Разнообразные
команды, связанные с
навигационной моделью WPF.
Разнообразные
команды, связанные с API-
интерфейсом
документов WPF.
Подключение команд к свойству Command
Для подключения любой из команд к элементу пользовательского интерфейса,
поддерживающему свойство Command (такому как Button или Menultem), прнадобится
сделать совсем немного. Для примера модифицируем текущую систему меню, добавив
новый пункт верхнего уровня по имени Edit (Правка) с тремя подэлементами,
позволяющими копировать, вставлять и вырезать текстовые данные:
<Menu DockPanel.Dock ="Top"
HorizontalAlignment="Left" Background="White" BorderBrush ="Black">
<MenuItem Header="_File" Click ="FileExit_Click" >
<Separator/>
Глава 28. Программирование с использованием элементов управления WPF 1069
<MenuItem Header ="_Exit" MouseEnter ="MouseEnterExitArea"
MouseLeave ="MouseLeaveArea" Click ="FileExit_Click"/>
</MenuItem>
<!— Новые пункты меню с командами -->
<MenuItem Header="_Edit">
<MenuItem Command ="ApplicationCommands.Copy"/>
<MenuItem Command ="ApplicationCommands.Cut"/>
<MenuItem Command ="ApplicationCommands.Paste"/>
</MenuItem>
<MenuItem Header="_Tools">
<MenuItem Header ="_Spelling Hints" MouseEnter ="MouseEnterToolsHintsArea"
MouseLeave ="MouseLeaveArea" Click ="ToolsSpellingHints_Click"/>
</MenuItem>
</Menu>
Обратите внимание, что каждый подэлемент в меню Edit имеет значение,
присвоенное его свойству Command. За счет этого пункт меню автоматически получает
корректное имя и горячую клавишу (например, <Ctrl+C> для операции вырезания) в
интерфейсе меню, и приложение теперь знает, как копировать, вырезать и вставлять, без
написания процедурного кода.
Запустив приложение и выделив некоторую часть текста, сразу же можно
пользоваться новыми пунктами меню. Вдобавок приложение также сможет реагировать на
стандартную операцию щелчка правой кнопкой мыши, предлагая пользователю те же
самые опции (рис. 28.15).
Рис. 28.15. Объекты команд предлагают полезный набор встроенной функциональности
Подключение команд к произвольным действиям
Если необходимо подключить объект команды к произвольному событию
(специфичному для приложения), то для этого понадобится написать процедурный код. Это
несложно, но требует чуть больше логики, чем можно видеть в XAML. Например,
предположим, что все окно должно реагировать на нажатие клавиши <F1>, активизируя
связанную справочную систему.
Пусть в файле кода для главного окна определен новый метод по имени
SetFlCommandBindingO, который вызывается в конструкторе после вызова
InitializeComponent():
public MainWindow()
{
InitializeComponent ();
SetFlCommandBindingO ;
1070 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Этот новый метод программно создает новый объект CommandBinding, который
можно использовать всякий раз, когда нужно привязать объект команды к заданному
обработчику событий приложения. Сконфигурируем объект CommandBinding для
работы с командой ApplicationCommands.Help, которая автоматически доступна по
нажатию <F1>:
private void SetFlCommandBinding ()
{
CommandBinding helpBinding = new CommandBinding(ApplicationCommands.Help);
helpBinding.CanExecute += CanHelpExecute;
helpBinding.Executed += HelpExecuted;
CommandBindings.Add(helpBinding) ;
}
Большинство объектов CommandBinding будет обрабатывать событие CanExecute
(которое позволяет указать команду на основе операций программы) и событие Executed
(в котором определяется то, что должно произойти в ответ на команду). Добавьте
показанные ниже обработчики событий к типу-наследнику Window (обратите внимание на
формат каждого метода, которого требуют ассоциированные делегаты):
private void CanHelpExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Если нужно предотвратить выполнение команды,
// следует установить CanExecute в false.
е.CanExecute = true;
}
private void HelpExecuted(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("Look, it is not that difficult. Just type something1",
"Help!");
}
В предыдущем фрагменте кода метод CanHelpExecute() реализован так, чтобы
справка по нажатию <F1> всегда работала, для чего просто возвращается true. Если в
каких-то ситуациях справочная система отображаться не должна, тогда нужно вернуть
false. Наша "справочная система", отображаемая внутри HelpExecuteO — это всего
лишь простое окно сообщения. Теперь можно запустить приложение. После нажатия
<F1> отображается не слишком полезная справочная система (рис. 28.16).
Рис. 28.16. Пример специальной справочной системы
Глава 28. Программирование с использованием элементов управления WPF 1071
Работа с командами Open и Save
В завершение текущего примера добавим функциональность сохранения текстовых
данных во внешнем файле и открытия файлов *.txt для редактирования. Можно пойти
длинным путем, вручную добавив программную логику, которая включает и отключает
пункты меню в зависимости от того, есть ли данные внутри TextBox. Однако для
сокращения затрат можно воспользоваться командами.
Начнем с обновления элемента <MenuItem>, который представляет меню File
высшего уровня, добавив два новых подменю, использующих объекты Save и Open класса
ApplicationCommands:
<MenuItem Header="_File">
<MenuItem Command ="ApplicationCommands.Open"/>
<MenuItem Command =llApplicationCommands . Save"/>
<Separator/>
<MenuItem Header ="_Exit"
MouseEnter ="MouseEnterExitArea"
MouseLeave ="MouseLeaveArea" Click ="FileExit_Click"/>
</MenuItem>
Как уже упоминалось, все объекты команд реализуют интерфейс ICommand,
определяющий два события (CanExecute и Executed). Теперь нужно позволить окну
выполнять эти команды.
Это делается наполнением коллекции CommandBindings, поддерживаемой окном.
Чтобы сделать это в XAML, необходимо воспользоваться синтаксисом "свойство-элемент"
для определения области <Window.CommandBindings>, куда будут помещены
определения <CommandBinding>. Модифицируйте определение <Window>, как показано ниже:
<Window х:Class="MyWordPad.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MySpellChecker" Height=31" Width=08"
WmdowStartupLocation ="CenterScreen" >
<!— Это информирует Window, какие обработчики вызывать
при проверке команд Open и Save. -->
<Window.CommandBindings>
<CommandBinding Command="ApplicationCommands.Open"
Executed="OpenCmdExecuted"
CanExecute="OpenCmdCanExecute"/>
<CommandBinding Command="ApplicationCommands.Save"
Executed="SaveCmdExecuted"
CanExecute="SaveCmdCanExecute"/>
</Window.CommandBindings>
< ' — Эта панель устанавливает содержимое окна —>
<DockPanel>
</DockPanel>
</Window>
Теперь щелкните правой кнопкой мыши на каждом из атрибутов Executed и
CanExecute в редакторе XAML и выберите в контекстном меню пункт Navigate to Event
Handler (Перейти к обработчику события). Как было описано в главе 27, это
автоматически сгенерирует заготовку кода для обработчика события. Теперь в файле кода С# для
окна имеются пустые обработчики событий. *
Реализация обработчиков событий CanExecute должна сообщить окну, что можно
инициировать соответствующие события Executed в любой момент, для чего нужно
установить свойство CanExecute входящего объекта CanExecuteRoutedEventArgs:
1072 Часть VI. Построение настольных пользовательских приложений с помощью WPF
private void OpenCmdCanExecute (object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
private void SaveCmdCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
Соответствующие обработчики Executed выполняют действительную работу по
отображению диалоговых окон открытия и сохранения файла; кроме того, они
передают данные из TextBox в файл. Начните с импорта пространства имен System. 10 и
Microsoft.Win32 в файл кода. ГЪтовый код прост и показан ниже:
private void OpenCmdExecuted(object sender, ExecutedRoutedEventArgs e)
{
// Создать диалоговое окно открытия файла, отображающее только текстовые файлы.
OpenFileDialog openDlg = new OpenFileDialog ();
openDlg.Filter = "Text Files |*.txt";
// Был ли щелчок на кнопке OK?
if (true == openDlg.ShowDialog ())
{
// Загрузить содержимое выбранного файла.
string dataFromFile = File.ReadAllText(openDlg.FileName);
// Отобразить строку в TextBox.
txtData.Text = dataFromFile;
}
}
private void SaveCmdExecuted(object sender, ExecutedRoutedEventArgs e)
{
SaveFileDialog saveDlg = new SaveFileDialog();
saveDlg.Filter = "Text Files |*.txt";
// Был ли щелчок на кнопке OK?
if (true == saveDlg.ShowDialog() )
{
// Сохранить данные из TextBox в указанном файле.
File.WriteAllText(saveDlg.FileName, txtData.Text);
}
}
На этом пример и начальное знакомство с работой с элементами управления WPF
завершены. Вы узнали, как работать с системой меню, строкой состояния, панелью
инструментов, вложенными панелями и несколькими базовыми элементами
пользовательского интерфейса, такими как TextBox и Expander. В следующем примере будут
задействованы более экзотические элементы управления, и параллельно вы
ознакомитесь с несколькими важными службами WPF. Кроме того, будет показано, как строить
пользовательские интерфейсы с помощью Expression Blend.
Исходный код. Проект MyWordPad доступен в подкаталоге Chapter 28.
Построение пользовательского интерфейса
WPF с помощью Expression Blend
Среда Visual Studio 2010 предлагает развитые средства редактирования WPF, и при
желании эту IDE-среду можно использовать для решения всех задач по редактированию
XAML. Однако для сложных приложений объем работ можно значительно сократить,
Глава 28. Программирование с использованием элементов управления WPF 1073
если применять Expression Blend для генерации разметки, а затем открывать тот же
проект в Visual Studio, чтобы при необходимости подкорректировать полученную
разметку и написать код.
Кстати говоря, Expression Blend 3.0 поставляется с простым редактором кода; хотя
он все равно не дотягивает до мощи Visual Studio, этот встроенный редактор С# можно
использовать для быстрого добавления кода обработчиков событий пользовательского
интерфейса во время разработки.
Ключевые аспекты IDE-среды Expression Blend
Чтобы приступить к знакомству с Blend, загрузите продукт и щелкните на кнопке
New Project (Создать проект) в диалоговом окне приветствия (если оно не отображается,
выберите пункт меню File^New project (Файл"^Создать проект)). В диалоговом окне New
Project (Новый проект) создайте новое приложение WPF по имени WpfControlsAndAPIs,
как показано на рис. 28.17.
Открыв проект, перейдите на вкладку Project (Проект), находящуюся в верхней
левой части IDE-среды (если вы не видите ее, выберите пункт меню Window^ Projects
(Окна1^Проект)). Как видно на рис. 28.18, приложение WPF, сгенерированное Blend,
идентично проекту WPF, сгенерированному Visual Studio.
Теперь найдите вкладку Properties (Свойства) в правой части IDE-среды Blend.
Подобно Visual Studio 2010, здесь можно присваивать значения свойствам
элемента, выбранного в визуальном конструкторе. Однако есть некоторые отличия от окна
Properties в Visual Studio 2010. Например, обратите внимание, что подобные свойства
сгруппированы вместе в сворачиваемых панелях (рис. 28.19).
Возможно, наиболее очевидной частью IDE-среды Blend является визуальный
конструктор окон, который позволяет выполнить большую часть работы. В верхней правой
области визуального конструктора окон видны три маленькие кнопки; они позволяют
открыть сам визуальный конструктор, лежащую в его основе XAML-разметку или
разделить оба представления. На рис. 28.20 показан визуальный конструктор с разделенным
видом.
Рис. 28.17. Создание нового приложения WPF Рис. 28.18. Проект WPF в
в Expression Blend Expression Blend идентичен проекту
WPF в Visual Studio
1074 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Рис. 28.19. Редактор свойств в Expression Blend
1
2
3
4
5
б
7
8
садштш^си ^^
<Window
xmlns-"http://schemes.microsoft.com/winfx/2006/xaml/presentation"
xmlns:х-"http://schemes.microsoft.com/winfx/2006/xaml"
x:Class-"WpfControlsAndAPIs.MainWindowH
x: Name"MWindowM
Title-"MainWindow"
Width-59" Height-88">
Рис. 28.20. Визуальный конструктор Blend позволяет просматривать
лежащую в основе XAML-разметку
Глава 28. Программирование с использованием элементов управления WPF 1075
Редактор XAML довольно насыщен средствами. Например, когда начинает вводиться
код, активизируется средство IntelliSense, обеспечивающее поддержку авто-завершения,
кроме того, имеется удобная справочная система. В рассматриваемом примере не
придется вводить много XAML-разметки, но на рис. 28.21 показан редактор в действии.
2 xeJns-"http://scheeas.«icrosoft.coe/winfx/2ee6/xa«l/presentBtion"
3, xjilns:x«"http://schemes.microsoft.ссж/winfх/2вв6/ха«1*
4 x:Cldr.s-"UpfControlsAndAPIs.MainWin<W
S' x:NaJne-"Window"
6; TitU«"MeinWindow"
7; Width-59" Height-88" >
i ^ AllowDrop
9! <6rid ч: Name-" LayoutRoot"/»
iej </Window> : «f AHowTranspenency
| {} Application
{) AutomationProperties
i 2§* Background
<> Binding
AHowOrop
boo!
Gets or sets a vaJue indicating whether
this element can be used as the target of
a drag-and-drop operation.
Рис. 28.22. Дерево разметки XAML
в окне Objects and Timeline
Рис. 28.21. Редактор XAML в Expression Blend столь же многофункционален, как и
аналогичный редактор в Visual Studio
Еще одним ключевым аспектом Blend, о
котором следует помнить, является область Objects and
Timeline (Объекты и шкала времени), расположенная
в нижней левой области окна Blend. В главе 30 вы
узнаете, что временная шкала этого инструмента
позволяет фиксировать последовательности анимации.
А пока достаточно будет сказать, что этот аспект IDE-
среды позволяет видеть логическое дерево разметки,
подобно тому, как это делает Visual Studio 2010. В
настоящее время окно Objects and Timeline должно
выглядеть, как показано на рис. 28.22.
Здесь видно, что узлом верхнего уровня является
сам элемент Window, имеющий элемент LayoutRoot
в качестве содержимого. Просмотрев XAML-разметку для начального окна, легко
заметить, что это имя назначено диспетчеру компоновки <Grid> по умолчанию:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="WpfControlsAndAPIs.MainWindow"
x:Name="Window"
Title="MainWindow"
Width=,,559" Height=,,288" >
<Grid x:Name="LayoutRoot"/>
</Window>
Если такой диспетчер компоновки по умолчанию не подходит, можно
отредактировать XAML-разметку напрямую. Кроме того, можно щелкнуть правой кнопкой мыши на
значке LayoutRoot в окне Objects and Timeline и выбрать в контекстном меню пункт
Change Layout Type (Изменить тип компоновки).
На заметку! Чтобы изменить внешний вид элемента пользовательского интерфейса в редакторе
свойств, выберите этот элемент в дереве Objects and Timeline. Это автоматически приведет
к выбору корректного элемента в визуальном конструкторе.
1076 Часть VI. Построение настольных пользовательских приложений с помощью WPF
И заключительный аспект Blend, о котором следует знать на данный момент — это
окно Tools (Инструменты), расположенное в левой части IDE-среды. В нем находятся
элементы управления WPF для выбора и размещения в визуальном конструкторе. На
рис. 28.23 видно, что некоторые из этих кнопок снабжены маленьким треугольником в
нижней правой области. Щелкая и удерживая такую кнопку, можно выбирать
связанные варианты.
На заметку! Если окно Tools смонтировано в верхней части IDE-среды Blend, то элементы
управления располагаются в горизонтальной, а не вертикальной ленте. Любое окно Blend можно
стыковать простым перетаскиванием с помощью мыши. Выбор пункта меню Window^Reset
Current Workplace (Окно^Сбросить текущую рабочую область) позволяет восстановить
стандартное местоположение всех окон Blend.
Рис. 29.23. Окно Tools позволяет выбирать элемент управления WPF
для помещения в визуальный конструктор
Также обратите внимание, что одна из кнопок в окне Tools помечена символом ».
Щелчок ней приводит к открытию окна Assets Library (Библиотека активов),
показанного на рис. 28.24. Здесь доступно множество дополнительных элементов управления WPF,
которые по умолчанию в окне Tools не отображаются. Выбор элементов в Assets Library
вызывает их перемещение в область Tools с целью дальнейшего использования.
Рис. 28.24. Окно Assets Library предоставляет доступ к дополнительным
элементам управления WPF
Глава 28. Программирование с использованием элементов управления WPF 1077
В * *
MainVl Selection (V) |
Рис. 28.25. Кнопка
Selection позволяет
изменять размер
и расположение
выбранного
элемента в визуальном
конструкторе
Другая важная тема, связанная с окном Tools, часто вводит в
заблуждение новичков, впервые имеющих дело с Blend; так,
многие часто не понимают разницы между кнопками Selection (Выбор)
и Direct Selection (Направить выбор). Когда в визуальном
конструкторе необходимо выбрать элемент (вроде Button) для изменения
его размеров или местоположения в контейнере, используется
кнопка Selection, которая помечена черной стрелкой (рис. 28.25).
Кнопка Direct Selection (со стрелкой белого цвета) служит для
углубления в содержимое включающего элемента. Таким образом,
выбрав элемент Button, можно углубиться в его содержимое,
чтобы построить сложное визуальное представление (например,
добавить StackPanel и графические элементы). Использование кнопки Direct Selection
еще будет рассматриваться в главе 31, когда речь пойдет о построении специальных
элементов управления. А пока при построении графических интерфейсов пользователя
выбирайте кнопку Select вместо Direct Select.
Использование элемента TabControl
Проектируемое окно будет содержать элемент TabControl с четырьмя вкладками,
каждая из которых отображает набор связанных элементов управления и/или API-
интерфейсов WPF. Для начала удостоверьтесь, что в окне Objects and TimeLine выбран
узел LayoutRoot. Затем щелкните на кнопке » (Assets Library) и найдите элемент
управления TabControl. Выберите этот элемент управления в окне Tools и перетащите
на поверхность визуального конструктора, чтобы он занял большую ее часть. В окне
появится система вкладок с двумя вкладками по умолчанию.
Выберите новый элемент TabControl в окне Objects and TimeLine и в окне Properties
назначьте ему имя myTabSystem, используя поле редактирования Name (Имя) в
верхней части редактора свойств. Щелкните правой кнопкой мыши на TabControl в окне
Objects and TimeLine и выберите в контекстном меню пункт Add Tabltem (Добавить
вкладку), как показано на рис. 28.26.
Рис. 28.26. Визуальное добавление вкладок
1078 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Добавьте к элементу управления еще одну вкладку и найдите в окне Properties
область Common Properties (Общие свойства). Измените свойство Header каждой вкладки,
указав названия Ink API, Documents, Data Binding и DataGrid (рис. 28.27).
Рис. 28.27. Визуальное редактирование элементов управления Tabltem
Теперь поверхность визуального конструктора окна должна выглядеть, как показано
на рис. 28.28.
Последовательно щелкните на вкладках и в окне Properties назначьте каждой из них
уникальное подходящее имя (вкладки также можно выбирать в окне Object and Timeline).
Имейте в виду, что после выбора вкладки с помощью любого из этих двух подходов, она
становится активной, и на нее можно перетаскивать элементы управления из области
Tools. Перед тем, как заняться проектированием каждой вкладки, взгляните на XAML-
разметку, сгенерированную IDE-средой Blend, щелкнув на кнопке XAML. Разметка будет
выглядеть примерно так, как показано ниже:
<Window
xmlns="http://schemas.microsoft.com/winfx/2 0 0 6/xaml/presentation"
Глава 28. Программирование с использованием элементов управления WPF 1079
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x: Class="WpfControlsAndAPIs .MainWindow" x :Name="Window11
Title="MainWindow" Width=28" Height=,,383" >
<Grid x:Name=llLayoutRoot">
<TabControl x:Name="myTabSystem11 Margin=ll8">
<TabItem x:Name="tabInk" Header="Ink API">
<Grid/>
</TabItem>
<TabItem x:Name="tabDocuments11 Header="Documentsll>
<Grid/>
</TabItem>
<TabItem x:Name="tabDataBinding11 Header="Data Binding">
<Grid/>
</TabItem>
<TabItem x :Name="tabDataGrid11 Header=MDataGrid">
<Grid/>
</TabItem>
</TabControl>
</Grid>
</Window>
Построение вкладки ink API
На этой вкладке будет отображаться общее назначение интерфейса Ink API, который
позволяет встраивать в программу функциональность рисования. Конечно, его
применение не обязательно ограничивается приложениями для рисования; этот API можете
использовать для решения широкого круга задач, включая фиксацию рукописного
ввода посредством пера на Tablet PC.
Начните с поиска в области Objects and Timeline узла, представляющего вкладку
Ink API, и раскройте его. Диспетчером компоновки по умолчанию для этого элемента
Tabltem является <Grid>. Щелкните правой кнопкой мыши на нем и замените
диспетчер компоновки на StackPanel (рис. 28.29).
Рис. 28.29. Замена диспетчера компоновки
1080 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Проектирование элемента ToolBar
Удостоверьтесь, что узел StackPanel в данный момент выбран в окне Objects and
Timeline, и воспользуйтесь Assets Library для вставки нового элемента управления
ToolBar по имени inkToolbar. Выберите узел inkToolbar в Objects and Timeline,
найдите раздел Layout (Компоновка) в окне Properties и установите свойство Height
элемента Toolbar равным 60 (для свойства Width оставьте значение Auto).
Отыщите область Common Properties в окне Properties и щелкните на кнопку с
многоточием возле свойства Items (Collection), как показано на рис. 28.30.
После щелчка по кнопке откроется диалоговое окно, которое позволяет выбрать
элементы управления для добавления к ToolBar. Щелкните на раскрывающемся списке
кнопки Add another item (Добавить другой элемент) и добавьте три элемента управления
RadioButton (рис. 28.31).
Воспользуйтесь встроенным редактором свойств в этом диалоговом окне, чтобы
установить для каждого элемента RadioButton значение свойства Height в 50 и Width —
в 100 (эти свойства находятся в разделе Layout). Также установите свойства Content
(в разделе Common Properties) элементов RadioButton в Ink Mode!, Erase Mode! и
Select Mode!, соответственно (рис. 28.32).
После добавления трех элементов управления RadioButton добавьте элемент
Separator, используя раскрывающийся список Add another item. Теперь остается
добавить финальный элемент управления ComboBox (не ComboBoxItem); однако этот элемент
в раскрывающемся списке Add another item отсутствует. Когда необходимо вставить
нестандартные элементы управления в диалоговом окне Items (Collection) (Элементы
(Коллекция)), просто щелкните на области Add another item, как если бы это была
кнопка. В результате откроется редактор Select Object (Выберите объект), в котором можно
ввести имя нужного элемента управления (рис. 28.33).
Установите свойство Width элемента ComboBox в 100 и добавьте три объекта
ComboBoxItem к ComboBox, используя свойство Items (Collection) в разделе Common
Properties редактора свойств. Установите свойство Content каждого ComboBoxItem в
строки Red, Green и Blue.
Закройте редактор для возврата в визуальный конструктор окна. Последней задачей
настоящего раздела будет использование свойства Name для назначения имен перемен-
Рис. 28.30. Первый шаг в до- Рис. 28.31. Добавление трех элементов RadioButton к
бавлении элементов управле- ToolBar
ния к ToolBar с помощью окна
Properties
Глава 28. Программирование с использованием элементов управления WPF 1081
ных для новых элементов. Назовите три элемента RadioButton следующим образом:
inkRadio, selectRadio и eraseRadio. Элемент ComboBox назовите comboColors.
Рис. 28.32. Конфигурирование элементов RadioButton
Рис. 28.33. Использование редактора Select Object
для добавления уникальных элементов к Toolbar
1082 Часть VI. Построение настольных пользовательских приложений с помощью WPF
На заметку! При построении панели инструментов с помощью Blend вы поймете, насколько
быстрее все можно было бы сделать, если иметь возможность просто вручную редактировать
XAML. Это действительно так! Не забывайте, что инструмент Expression Blend предназначен для
дизайнеров, которые могут и не уметь вводить код разметки вручную. Программисты С# всегда
имеют возможность применить встроенный в Blends редактор XAML; тем не менее, полезно
освоить работу внутри IDE-среды, особенно если впоследствии придется объяснять дизайнерам,
как использовать этот инструмент.
Элемент управления RadioButton
В данном примере требуется, чтобы эти три элемента управления RadioButton
были взаимно исключающими. В других платформах для построения графических
интерфейсов пользователя такие элементы должны были бы помещаться в групповую
рамку. В WPF этого делать не нужно. Взамен им просто назначается одинаковое
групповое имя. Это полезно потому, что связанные элементы не обязательно должны быть
физически объединены в общую область, а могут располагаться в любом месте окна.
Сделайте это, выбрав каждый элемент RadioButton в визуальном конструкторе
(чтобы выбрать все три элемента, щелкайте при нажатой клавише <Shift>) и затем
установив для свойства GroupName (находящегося в разделе Common Properties окна
Properties) значение InkMode.
Когда элемент управления RadioButton не помещен внутрь родительского
элемента-панели, он принимает вид, идентичный элементу управления Button. Тем не менее,
в отличие от Button, класс RadioButton включает в себя свойство IsChecked, которое
переключается между true и false, когда конечный пользователь щелкает на
элементе. Более того, RadioButton поддерживает два события (Checked и Unchecked),
которые можно использовать для перехвата изменения этого состояния.
Для конфигурирования элементов управления RadioButton, чтобы они выглядели,
как типичные переключатели, выберите их в визуальном конструкторе,
последовательно щелкая при нажатой клавише <Shift>, а затем щелкните правой кнопкой мыши и
выберите в контекстном меню пункт Group In to ^Border (Сгруппировать внутри^ Элемент
Border), как показано на рис. 28.34.
Рис. 28.34. Группирование элементов внутри элемента Border
Глава 28. Программирование с использованием элементов управления WPF 1083
Теперь все готово к тестированию программы, для чего нужно нажать клавишу <F5>.
В окне отображаются три связанных переключателя и раскрывающийся список с тремя
элементами (рис. 28.35).
Рис. 28.35. Готовая система панели инструментов
Для вкладки Ink API осталось обработать событие Click каждого элемента
управления RadioButton. Подобно Visual Studio, окно Properties в Blend содержит кнопку с
изображением молнии для ввода имен обработчиков событий (на рис. 28.36 она находится
рядом со свойством Name). Привяжите событие Click каждой кнопки к одному и тому
же обработчику по имени RadioButtonClicked (см. рис. 28.36).
Рис. 28.36. Обработка события Click каждого элемента RadioButton
Откроется редактор кода Blend. Обработав все три события Click, обработайте
событие SelectionChanged элемента управления ComboBox посредством обработчика по
имени ColorChanged. В результате должен получиться следующий код С#:
public partial class MainWindow : Window
{
public MainWindow ()
{
this.InitializeComponent () ;
// Добавьте код, необходимый при создании объекта.
}
private void RadioButtonClicked(object sender, System.Windows.RoutedEventArgs e)
{
// TODO: Сюда добавьте реализацию обработчика событий.
}
1084 Часть VI. Построение настольных пользовательских приложений с помощью WPF
private void ColorChanged(object sender,
System.Windows.Controls.SelectionChangedEventArgs e)
{
// TODO: Сюда добавьте реализацию обработчика событий.
}
}
Эти обработчики будут реализованы позже, а пока оставьте их пустыми.
Элемент управления inkCanvas
Для завершения пользовательского интерфейса этой вкладки необходимо поместить
элемент управления InkCanvas в StackPanel, чтобы он появился под только что
созданным элементом Toolbar. Выберите StackPanel для объекта tablnk в окне Object
and Timeline и затем воспользуйтесь Assets Library для добавления InkCanvas по имени
mylnkCanvas. Затем щелкните на инструменте Selection в окне Tools (можно также
нажать клавишу <V>) и растяните новый элемент InkCanvas, чтобы он занял большую
часть области вкладки.
С помощью редактора Brushes (Кисти) можно задать для InkCanvas уникальный
цвет (более подробно редактор кистей рассматривается в следующей главе). Сделав все
это, запустите программу, нажав <F5>. Вы увидите, что поверхность холста теперь
позволяет рисовать графические данные по нажатию левой кнопки и перемещению мыши
(рис. 28.37).
Рис. 28.37. Элемент InkCanvas в действии
Элемент InkCanvas позволяет делать нечто большее, чем просто рисование линий
мышью (или пером); он также поддерживает множество уникальных режимов
редактирования, управляемых свойством EditingMode. Этому свойству можно присвоить
любое значение из связанного перечисления InkCanvasEditingMode. В данном примере
интересует режим Ink, принятый по умолчанию, который только что использовался;
режим Select, позволяющий пользователю выбирать с помощью мыши область для
последующего перемещения или изменения размера; и режим EraseByStroke, который
удаляет предыдущий след, нарисованный мышью.
На заметку! Штрих (stroke) — это визуализация, которая происходит при одиночной
операции нажатия и отпускания кнопки мыши. InkCanvas сохраняет все штрихи в объекте
StrokeCollection, который доступен через свойство Strokes.
Глава 28. Программирование с использованием элементов управления WPF 1085
Обновите обработчик RadioButtonClickedO следующей логикой, которая
переводит InkCanvas в правильный режим в зависимости от выбранного переключателя
RadioButton:
private void RadioButtonClicked(object sender,
System.Windows.RoutedEventArgs e)
{
// В зависимости от того, какая кнопка отправила событие,
//' переключить InkCanvas в нужный режим работы,
switch((sender as RadioButton) .Content.ToString ())
{
// Эти строки должны совпадать со значениями Content
// каждого элемента RadioButton.
case "Ink Mode1":
this.mylnkCanvas.EditingMode = InkCanvasEditingMode.Ink;
break;
case "Erase Mode1":
this.mylnkCanvas.EditingMode = InkCanvasEditingMode.EraseByStroke;
break;
case "Select Mode1":
this.mylnkCanvas.EditingMode = InkCanvasEditingMode.Select;
break;
}
}
Также установите режим в Ink по умолчанию в конструкторе окна. Там же
установите выбор по умолчанию для ComboBox (более подробно этот элемент рассматривается
в следующем разделе):
public MainWindowO
{
this.InitializeComponent();
// Установить по умолчанию режим Ink.
this.mylnkCanvas.EditingMode = InkCanvasEditingMode.Ink;
this.inkRadio.IsChecked = true;
this.comboColors.SelectedIndex=0;
}
Теперь снова запустите программу нажатием <F5>. Выберите режим Ink и
нарисуйте что-нибудь. Затем выберите режим Erase и сотрите ранее нарисованное (курсор
мыши автоматически примет вид стирающей резинки). Наконец, переключитесь в
режим Select, выберите несколько линий, используя мышь как "лассо". Охватив элемент,
вы сможете перемещать его по поверхности холста и изменять его размеры.
На рис. 28.38 показан результат работы в разных режимах.
Рис. 28.38. Элемент InkCanvas в действии, с разными режимами редактирования.
1086 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Элемент управления ComboBox
После заполнения элемента управления ComboBox (или ListBox) есть три способа
определения выбранного в них элемента. Во-первых, если необходимо найти числовой
индекс выбранного элемента, необходимо использовать свойство Selectedlndex
(отсчет начинается с 0; значение -1 означает отсутствие выбора). Во-вторых, если
требуется получить объект, выбранный внутри списка, подойдет свойство Selectedltem.
В-третьих, SelectedValue позволяет получить значение выбранного объекта (обычно
через вызов его метода ToStringO).
И последний фрагмент кода, который понадобится добавить для данной
вкладки, отвечает за изменение цвета линий, нарисованных в InkCanvas. Свойство
DefaultDrawingAttributes элемента InkCanvas возвращает объект DrawingAttributes,
который позволяет конфигурировать различные аспекты пера, включая его размер и цвет
(помимо прочего). Обновите код С# следующей реализацией метода ColorChanged():
private void ColorChanged(object sender,
System.Windows.Controls.SelectionChangedEventArgs e)
{
// Получить выбранный элемент в раскрывающемся списке.
string colorToUse =
(this.comboColors.Selectedltem as ComboBoxItem) .Content.ToString() ;
// Изменить цвет, используемый для визуализации линий.
this.mylnkCanvas.DefaultDrawingAttributes.Color =
(Color)ColorConverter.ConvertFromString(colorToUse);
}
Вспомните, что ComboBox имеет коллекцию ComboBoxItems. В сгенерированной
XAML-разметке имеется следующее определение:
<ComboBox x:Name="comboColors11 Width=0011 SelectionChanged="ColorChangedll>
<ComboBoxItem Content=llRed"/>
<ComboBoxItem Content=llGreen"/>
<ComboBoxItem Content="Bluell/>
</ComboBox>
В результате вызова Selectedltem получается выбранный ComboBixItem, который
хранится как элемент общего типа Object. После приведения Object к ComboBixItem
получится значение Content, представленное строкой Red, Green или Blue. Эта
строка затем преобразуется в объект Color с помощью удобного служебного класса
ColorConverter. Снова запустите программу. Теперь появилась возможность
переключать цвета при визуализации изображения.
Обратите внимание, что элементы управления ComboBox и ListBox могут также
включать сложное содержимое, а не только списки текстовых данных. Чтобы получить пред-
' ставление о некоторых возможностях, откройте редактор XAML для окна и измените
определение ComboBox, чтобы он имел набор элементов <StackPanel>, каждый из которых
содержит <Ellipse> и <Label> (значение свойства Width элемента ComboBox равно 200):
<ComboBox x:Name="comboColors11 Width=0011 SelectionChanged="ColorChangedll>
<StackPanel Orientation ^'Horizontal" Tag=,,Red">
<Ellipse Fill =,,Red" Height = 0" Width =,,50,7>
<Label FontSize =,,20" HorizontalAlignment="Center"
VerticalAlignment="Center" Content=,,Red,,/>
</StackPanel>
<StackPanel Orientation ^'Horizontal11 Tag=llGreen">
<Ellipse Fill ="Green" Height = 0" Width =0'7>
<Label FontSize =ll20" HorizontalAlignment="Center11
VerticalAlignment=,,Center" Content="Green,7>
</StackPanel>
Глава 28. Программирование с использованием элементов управления WPF 1087
<StackPanel Orientation ^'Horizontal11 Tag=llBlue">
<Ellipse Fill =,,BlueM Height = 0" Width =,,50,,/>
<Label FontSize =0" HorizontalAlignment="Center"
VerticalAlignment="Center" Content="Blue"/>
</StackPanel>
</ComboBox>"
Теперь свойству Tag каждого элемента StackPanel присвоено какое-то значение;
это представляет собой быстрый и удобный способ определения стека элементов,
выбранных пользователем (существуют и лучшие способы делать это, но пока такого
достаточно). С этой поправкой потребуется изменить реализацию метода ColorChangedO,
как показано ниже:
private void ColorChanged(object sender,
System.Windows.Controls.SelectionChangedEventArgs e)
{
// Получить свойство Tag выбранного элемента StackPanel.
string colorToUse = (this.comboColors.Selectedltem
as StackPanel).Tag.ToString();
}
Запустите программу и обратите внимание на уникальный ComboBox (рис. 28.39).
шШШШШШвШШШШШШШШШШШщ
| п L??comem*! ^ta Si™in9_L??*!r_nd.._;
Рис. 28.39. Специальный элемент ComboBox, реализованный благодаря
модели содержимого WPF
Сохранение, загрузка и очистка данных inkCanvas
Последняя часть этой вкладки позволит сохранять и загружать данные полотна, а
также очищать его содержимое. Вы уже должны довольно уверенно чувствовать себя в
проектировании пользовательских интерфейсов с помощью Blend, так что инструкции
будут краткими и по существу.
Начните с импорта пространств имен System. 10 и System.Windows.Ink в файл кода.
Затем добавьте в Tool Bar три новых элемента Button с именами btnSave, btnLoad и
btnClear. После этого обработайте событие Click для каждого элемента управления и
реализуйте обработчики следующим образом:
private void SaveData(object sender, System.Windows.RoutedEventArgs e)
{
// Сохранить все данные InkCanvas в локальном файле.
using (FileStream fs = new FileStream("StrokeData.bin", FileMode.Create))
{
this.mylnkCanvas.Strokes.Save(fs);
1088 Часть VI. Построение настольных пользовательских приложений с помощью WPF
fs.Close ();
}
}
private void LoadData(object sender, System.Windows.RoutedEventArgs e)
{
// Наполнить StrokeCollection из файла.
using(FileStream fs = new FileStream("StrokeData.bin",
FileMode.Open, FileAccess.Read))
{
StrokeCollection strokes = new StrokeCollection(fs);
this.mylnkCanvas.Strokes = strokes;
}
}
private void Clear (object sender, System.Windows.RoutedEventArgs e)
{
// Удалить все штрихи.
this.mylnkCanvas.Strokes.Clear();
I
После этого появляется возможность сохранять данные в файле, загружать из файла
и очищать InkCanvas от всех данных. На этом работа с первой вкладкой TabControl
завершена, равно как и рассмотрение интерфейса Ink API.
По правде говоря, еще много чего можно рассказать об этой технологии,
однако теперь вы должны обладать достаточными знаниями, чтобы продолжить
исследование этой темы самостоятельно. Далее мы приступаем к рассмотрению интерфейса
Documents API в WPF.
Введение в интерфейс Documents API
WPF поставляется с множеством элементов управления, которые позволяют вводить
или отображать тщательно сформатированные текстовые данные, подобные тем, что
можно найти в файлах Adobe PDF Интерфейс Documents API из WPF поддерживает
такую функциональность, однако в нем используется формат XML Paper Specification (XPS)
вместо PDF
Documents API позволяет конструировать готовые для печати документы,
используя несколько классов из пространства имен System.Windows.Documents. В этом
пространстве имен доступно множество типов, представляющих части документа XPS:
List, Paragraph, Section, Table, LineBreak, Figure, Floater и Span.
Блочные элементы и встроенные элементы
Формально элементы, которые добавляются к документу XPS, относятся к одной
из двух категорий: блочные элементы (block elements) и встроенные элементы (inline
elements). Первая категория, блочные элементы, состоит из классов, которые
расширяют базовый класс System.Windows.Documents.Block. Примерами блочных элементов
могут служить List, Paragraph, BlockUIContainer, Section и Table. Классы из этой
категории применяются для группирования вместе другого содержимого (например,
список, содержащий данные абзаца, и абзац, содержащий подабзацы для разного
форматирования текста).
Вторая категория, встроенные элементы, состоит из классов, расширяющих
базовый класс System.Windows.Documents.Inline. Встроенные элементы вкладываются
внутрь блочного элемента (или, возможно, внутрь другого встроенного элемента внутри
блочного элемента). К общим встроенным элементам относятся Run, Span, LineBreak,
Figure и Floater.
Глава 28. Программирование с использованием элементов управления WPF 1089
Эти классы имеют имена, которые можно встретить при построении
форматированного документа в профессиональном редакторе. Как и любой другой элемент
управления WPF, эти классы можно конфигурировать в XAML-разметке или в коде. Таким
образом, можно объявить пустой элемент <Paragraphs который наполняется во время
выполнения (в следующем примере будет показано, как это делается).
Диспетчеры компоновки документа
Может показаться, что встроенные и блочные элементы следует размещать
непосредственно в панели-контейнере, такой как Grid, но на самом деле они должны быть
упакованы в элементы <FlowDocument> или <FixedDocument>.
Помещать элементы в FlowDocument идеально, когда нужно позволить конечному
пользователю изменять способ представления данных. Пользователь может изменять
масштаб текста или способ представления данных (например, в виде одной длинной
страницы или в виде пары столбцов). FixedDocument лучше применять для
представления готовых к печати (WYSIWYG), неизменяемых документов.
В рассматриваемом примере мы будем иметь дело только с контейнером
FlowDocument. После вставки встроенных и блочных элементов в FlowDocument этот
объект будет помещен в один из поддерживающих XPS диспетчеров компоновки,
которые перечислены в табл. 28.4.
Таблица 28.4. Диспетчеры компоновки XPS
Элемент управления типа панели Назначение
FlowDocumentReader Отображает данные в FlowDocument и добавляет
поддержку масштабирования, поиска и компоновки содержимого в
разнообразных формах
FlowDocumentScrollViewer Отображает данные в FlowDocument; однако данные
представлены как единый документ, просматриваемый с
использованием линеек прокрутки. Этот контейнер не поддерживает
масштабирование, поиск или альтернативные режимов компоновки
RichTextBox Отображает данные в FlowDocument и добавляет
поддержку редактирования со стороны пользователей
FlowDocumentPageViewer Отображает документ постранично, т.е. одну страницу за раз.
Данные можно масштабировать, но поиск не предусмотрен
Наиболее богатый средствами способ отображения FlowDocument состоит в
том, чтобы поместить его в диспетчер FlowDocumentReader. В результате
пользователь получает возможность изменять компоновку, искать слова в документе и
масштабировать отображение данных. Одно из ограничений этого контейнера (а также
FlowDocumentScrollViewer и FlowDocumentPageViewer) связано с тем, что
отображаемое содержимое доступно только для чтения. Если же необходимо позволить
конечному пользователю вводить новую информацию в FlowDocument, его можно упаковать
в элемент управления RichTextBox.
Построение вкладки Documents
Щелкните на вкладке Documents элемента Tabltem и откройте ее в редакторе Blend.
Здесь уже имеется элемент управления по умолчанию <Grid>, который является
прямым дочерним элементом Tabltem; с помощью окна Objects and Timeline замените его
StackPanel. На вкладке Documents будет отображаться элемент FlowDocument, кото-
1090 Часть VI. Построение настольных пользовательских приложений с помощью WPF
рый позволит выделять текст и добавлять аннотации, используя API-интерфейс Sticky
Notes ("клейкие" заметки).
Начните с определения следующего элемента управления ToolBar, который
включает в себя три простых (неименованных) элемента управления Button. Позднее к этим
элементам управления будут привязаны команды, поэтому ссылаться на них в коде не
понадобится.
<TabItem x:Name="tabDocuments" Header="Documents" VerticalAlignment="Bottom"
Height=0">
<StackPanel>
<ToolBar>
<Button BorderBrush="Green" Content="Add Sticky Note"/>
<Button BorderBrush="Green" Content="Delete Sticky Notes"/>
<Button BorderBrush="Green" Content="Highlight Text"/>
</ToolBar>
</StackPanel>
</TabItem>
Откройте Assets Library и найдите элемент управления FlowDocumentReader в
категории All (Все) узла Conrtols (Элементы управления). Поместите этот элемент
управления в StackPanel, переименуйте в myDocumentReader и растяните на всю поверхность
StackPanel (удостоверившись, что выбран инструмент Selection). Компоновка должна
выглядеть подобно показанной на рис. 28.40.
Теперь выберите элемент управления FlowDocumentReader в окне Objects and
Timeline и найдите категорию Miscellaneous (Прочие) в окне Properties. Щелкните на
кнопке New (Создать) рядом со свойством Document. В XAML-разметку добавляется
пустой элемент <FlowDocument>:
<FlowDocumentReader x:Name="myDocumentReader" Height=69.4">
<FlowDocument/>
</FlowDocumentReader>
Теперь можно добавлять к элементу классы документов (например, List, Paragraph,
Section, Table, LineBreak, Figure, Floater и Span).
Глава 28. Программирование с использованием элементов управления WPF 1091
Resources Data Properties >
Name < No Name >
< /
Type FtowDocumentReadef
* Document (FtowDocu
AllowDrop
Background
BindmgGroup
Blocks (Collection]
CotumnGap Auto
CoiumnRuleBrush
CokimnRuJeWidtn 0
ColumnWidth Auto
ment)
No Brush
I I
No Brush
П
Ш*
New .
New
^
Рис. 28.41. Элемент FlowDocument
наполняется с использованием
свойства Blocks (Collection)
Наполнение FlowDocument
с использованием Blend
Сразу после добавления нового документа к
контейнеру документов свойство Document в окне
Properties станет раскрываемым, отображая массу
новых свойств, которые позволяют формировать
дизайн документа. В рассматриваемом примере
единственным свойством, о котором следует
позаботиться, будет Blocks (Collection), как
показано на рис. 28.41.
Щелкните на кнопке с многоточием справа от
Blocks (Collection) и в открывшемся
диалоговом окне с помощью кнопки Add another Item
(Добавить новый элемент) вставьте в документ элементы
Section, List и Paragraph. Каждый из этих
элементов можно редактировать дальше, используя
для этого редактор Blocks (Блоки), причем каждый блок может содержать в себе
подблоки. Например, выбрав элемент Section, можно добавить в него подблок Paragraph.
Для примера сконфигурируйте элемент Section с определенным цветом фона,
переднего плана и размером шрифта. Кроме того, вставьте подблок Paragraph.
Элемент Section можно конфигурировать как угодно, однако оставьте элементы
List и исходный Paragraph пустыми, поскольку они будут заполняться в коде. Ниже
показан один из возможных вариантов конфигурации FlowDocument:
<FlowDocumentReader x:Name="myDocumentReader" Height=69.4">
<FlowDocument>
<Section Foreground = "Yellow" Background = "Black">
<Paragraph FontSize = 0">
Here are some fun facts about the WPF Document API!
</Paragraph>
</Section>
<List/>
<Paragraph/>
</FlowDocument>
</FlowDocumentReader>
Если теперь запустить программу (нажав <F5>), можно масштабировать
отображение документа (используя ползунок в нижнем правом углу), искать ключевые слова
(с помощью редактора поиска, расположенного внизу слева) и отображать данные в
одном из трех режимов (используя кнопки компоновки). На рис. 28.42 показан поиск
слова "WPF"; обратите внимание, что содержимое отображается в увеличенном масштабе.
Прежде чем приступить к следующему шагу, отредактируйте XAML-разметку, чтобы
вместо FlowDocumentReader использовать другой контейнер FlowDocument, например,
такой как FlowDocumentScrollViewer или RichTextBox. После этого снова запустите
приложение и обратите внимание на отличия в обработке данных документа. По
завершении этого эксперимента верните тип FlowDocumentReader.
Наполнение FlowDocument с помощью кода
Теперь давайте построим блок List и оставшийся блок Paragraph в коде. В этом
важно разобраться, поскольку часто требуется наполнять документ FlowDocument на
основе пользовательского ввода, внешних файлов, информации из базы данных или
любого другого источника.
1092 Часть VI. Построение настольных пользовательских приложений с помощью WPF
1 !^|Р] Documents Datafcndmg j Ca'.aG-ю
i |Add Sticky Note| Delete Sticky Notes|Highlight Tertj
Here are some fun facts
about the WPF
Document API!
'
ItaiwPF «>-]
Б 1
L
, , 1 Hi - *. 4. l l
Рис. 28.42. Манипулирование FlowDocument с помощью FlowDocumentReader
Воспользуйтесь редактором XAML-разметки в Blend, чтобы назначить элементам
List и Paragraph подходящие имена и тем самым получить возможность обращаться
к ним из кода:
<List x:Name="listOfFunFacts"/>
<Paragraph x:Name="paraBodyText"/>
В файле кода определите новый приватный метод по имени PopulateDocument().
Этот метод сначала добавит набор элементов Listltem к List, каждый из которых
имеет элемент Paragraph с единственным элементом Run. Кроме того, вспомогательный
метод динамически построит сформатированный абзац, используя три отдельных
объекта Run, как показано ниже:
private void PopulateDocument ()
{
// Добавить некоторые данные в элемент List.
this.listOfFunFacts.FontSize = 14;
this.listOfFunFacts.MarkerStyle = TextMarkerStyle.Circle;
this.listOfFunFacts.Listltems.Add(new Listltem( new
Paragraph(new Run("Fixed documents are for WYSIWYG print ready docs1"))));
this.listofFunFacts.Listltems.Add(new Listltem(
new Paragraph(new Run("The API supports tables and embedded figures!"))));
this.listOfFunFacts.Listltems.Add(new Listltem(
new Paragraph(new Run("Flow documents are read only1"))));
this.listOfFunFacts.Listltems.Add(new Listltem(new Paragraph(new Run
("BlockUIContainer allows you to embed WPF controls in the document1")
)));
// Добавить некоторые данные в элемент Paragraph. Первая часть абзаца.
Run prefix = new Run("This paragraph was generated ") ;
// Середина абзаца.
Bold b = new Bold () ;
Run infix = new Run("dynamically") ;
infix.Foreground = Brushes.Red;
infix.FontSize = 30;
b. Inlmes .Add (infix) ;
// Последняя часть абзаца.
Run suffix = new Run(" at runtime1");
// Добавить все части в коллекцию встроенных элементов Paragraph.
this.paraBodyText.Inlines.Add(prefix);
this.paraBodyText.Inlines.Add(infix);
this.paraBodyText.Inlines.Add(suffix) ;
Глава 28. Программирование с использованием элементов управления WPF 1093
Вызовите этот метод в конструкторе окна. После этого запустите приложение и
обратите внимание на новое, динамически сгенерированное содержимое документа.
Включение аннотаций и "клейких" заметок
Теперь можно строить документ с интересными данными, используя XAML-разметку
и код С#, однако нужно еще что-то сделать с тремя кнопками в панели инструментов на
вкладке Documents. В составе WPF доступен набор команд, используемых специально
с интерфейсом Documents API. Эти команды позволяют пользователю выделять часть
документа, а также добавлять "клейкие" заметки — аннотации. Все это делается с
помощью всего нескольких строк кода (и небольшого объема разметки). Командные объекты
для Documents API находятся в пространстве имен System.Windows. Annotations
сборки PresentationFramework.dll. Таким образом, понадобится определить специальное
пространство имен XML в открывающем элементе <Window> для использования таких
объектов в XAML (обратите внимание, что префиксом дескриптора является а):
<Window
xmlns:а=
"clr-namespace:System.Windows.Annotations;assembly=PresentationFramework"
x:Class="WpfControlsAndAPIs.MainWindow"
x:Name="Window"
Title="MainWindow"
Width="856" Height=83" mc:Ignorable="d" WindowStartupLocation="CenterScreen" >
</Window>
Обновите три определения <Button>, установив свойства Command в
соответствующие команды аннотаций:
<Тоо1Ваг>
<Button BorderBrush="Green" Content="Add Sticky Note"
Command="a:AnnotationService.CreateTextStickyNoteCommand"/>
<Button BorderBrush="Green" Content="Delete Sticky Notes"
Command="a:AnnotationService.DeletestickyNotesCommand"/>
<Button BorderBrush="Green" Content="Highlight Text"
Command="a:AnnotationService.CreateHighlightCommand"/>
</ToolBar>
Последнее, что потребуется делать — это включить службы аннотации для объекта
FlowDocumentReader, который был назван myDocumentReader. Добавьте в класс еще
один приватный метод по имени EnableAnnotationsO, который будет вызываться в
конструкторе окна. Затем импортируйте-следующие пространства имен:
using System.Windows.Annotations;
using System.Windows.Annotations.Storage;
Теперь реализуйте этот метод:
private void EnableAnnotationsO
{
// Создать объект AnnotationService, работающий с FlowDocumentReader.
AnnotationService anoService = new AnnotationService(myDocumentReader);
// Создать MemoryStream, который будет содержать аннотации.
MemoryStream anoStream = new MemoryStream();
// Создать основанное на XML хранилище на базе MemoryStream. Этот объект можно
// использовать для программного добавления, удаления или поиска аннотаций.
AnnotationStore store = new XmlStreamStore(anoStream);
// Включить службы аннотаций.
anoService.Enable(store);
}
1094 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Класс AnnotationService позволяет заданному диспетчеру компоновки
документа получить поддержку аннотаций. Прежде чем вызвать метод Enable () этого объекта,
необходимо предоставить местоположение объекта для хранения аннотированных
данных, которые в данном примере являются областью памяти, представленной объектом
MemoryStream. Обратите внимание, что объект AnnotationService соединяется с
объектом Stream, используя AnnotationStore.
Запустите приложение. Выделите некоторый текст, щелкните на кнопке Add Sticky
Note (Добавить "клейкую" заметку) и введите некоторую информацию. Выделенный
текст также можно подсвечивать (по умолчанию желтым цветом). Наконец, можно
удалять заметки, выбирая их и щелкая на кнопке Delete Stricky Note (Удалить "клейкую"
заметку). На рис. 28.43 показан результат тестового запуска.
r^t^il"Documents i Deto Binding j DatoGnd |
{Add Sticky NoteJDetete Sticky Notes] Highfcght Tcxt|
Here are some fan tacts about the WPF Document .API!
О Fixed documents are for WYSIWYG print ready docs!
О The API supports tables and embedded figures!
О Flow documents are read only!
О BlockUIContainer allows you to embed WPF control in the document!
This paragraph was generated dynamically )at runtime!
Рис. 28.43. Использование "клейких" заметок
Сохранение и загрузка потокового документа
Экскурс в интерфейс Documents API завершается рассмотрением простого
процесса сохранения документа в файл, а также чтения документа из файла. Вспомните, что
если объект FlowDocument не упакован в RichTextBox, конечный пользователь не
сможет редактировать документ; кроме того, часть документа создавалась динамически во
время выполнения, так что может понадобиться сохранить его для последующего
использования. Возможность сохранения документа в стиле XPS также может быть
полезна во многих приложениях WPF, так как она позволяет определить пустой документ
и загружать его на лету.
В следующем фрагменте разметки предполагается, что к элементу Toolbar вкладки
Documents были добавлены два новых элемента Button, объявленные следующим
образом (обратите внимание, что в разметке никакие события не обрабатываются):
<Button х:Name="btnSaveDoc" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" Width=5" Content="Save Doc"/>
<Button x:Name="btnLoadDoc" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" Width=5" Content="Load Doc"/>
В конструкторе окна напишите следующие лямбда-выражения для сохранения
и загрузки данных FlowDocument (для получения доступа к классам XamlReader и
XamlWriter понадобится импортировать пространство имен System.Windows.Markup):
public MainWindow()
{
Глава 28. Программирование с использованием элементов управления WPF 1095
// Построить обработчики событий Click для сохранения и загрузки
// документа нефиксированного формата.
btnSaveDoc.Click += (о, s) =>
{
using(FileStream fStream = File.Open(
"documentData.xaml", FileMode.Create))
{
XamlWriter.Save(this.myDocumentReader.Document, fStream);
}
};
btnLoadDoc.Click += (o, s) =>
{
using(FileStream fStream = File.Open("documentData.xaml", FileMode.Open))
{
try
{
FlowDocument doc = XamlReader.Load(fStream) as FlowDocument;
this.myDocumentReader.Document = doc;
}
catch(Exception ex) {MessageBox.Show(ex.Message, "Error Loading Doc1");}
}
};
}
Это все, что потребуется сделать для сохранения документа (следует отметить, что
при этом не сохраняются аннотации; тем не менее, это можно сделать с помощью служб
аннотаций). После щелчка на кнопке Save Doc (Сохранить документ) в папке \bin\
Debug появится новый файл *.xaml, который содержит данные текущего документа.
На этом рассмотрение интерфейса Document API завершено. В этом API-интерфейсе
есть еще много такого, что здесь не было показано, но вы получили достаточное
представление об основах. В завершение главы мы коснемся нескольких тем, связанных с
привязкой данных, и завершим текущее приложение.
Введение в модель привязки данных WPF
Элементы управления часто служат целью для различных операций привязки
данных. Просто говоря, привязка данных (data binding) — это акт подключения свойств
элемента управления к значениям данных, которые могут изменяться на протяжении
жизненного цикла приложения. Это позволяет элементу пользовательского интерфейса
отображать состояние переменной в коде. Например, привязку данных можно
использовать для выполнения следующих действий:
• отмечать флажок элемента управления Checkbox на основе булевского свойства
заданного объекта;
• отображать в элементах Text Box информацию, извлеченную из реляционной
базы данных;
• подключать элемент Label к целому числу, представляющему количество файлов
в папке.
При использовании встроенного в WPF механизма привязки данных следует
помнить о разнице между источником и местом назначения операции привязки. Как и
можно было ожидать, источником операции привязки данных являются сами данные
(булевское свойство, реляционные данные и т.п.), в то время как местом назначения
(или целью) является свойство элемента управления пользовательского интерфейса,
который использует содержимое данных (Checkbox, TextBox и т.д.).
1096 Часть VI. Построение настольных пользовательских приложений с помощью WPF
По правде говоря, использование инфраструктуры привязки данных WPF никогда не
является обязательным. Если разработчик пожелает самостоятельно реализовать
собственную логику привязки данных, то подключение между источником и местом
назначения обычно потребует обработки различных событий и написания процедурного
кода для соединения с источником и целью. Например, если в окне имеется элемент
ScrollBar, который должен отобразить свое значение в Label, можно обработать
событие ValueChanged элемента ScrollBar и соответствующим образом обновить
содержимое Label.
Однако привязку данных WPF можно применять для соединения источника и цели
непосредственно в XAML-разметке (или в файле кода С#) без необходимости в обработке
различных событий или жесткого кодирования соединений между источником и целью.
К тому же, на основе построения логики привязки данных можно обеспечить
постоянную синхронизацию целевого объекта с изменяющимися значениями данных.
Построение вкладки Data Binding
С помощью окна Objects and Timeline замените Grid в третьей вкладке на StackPanel.
Затем воспользуйтесь Assets Library и редактором Properties среды Blend для
построения следующей начальной компоновки:
<TabItem x:Name="tabDataBinding" Header="Data Binding">
<StackPanel Width=50">
<Label Content="Move the scroll bar to see the current value"/> •
<!-- Значение линейки прокрутки является источником привязки данных —>
^ScrollBar x:Name="mySB" Orientation="Horizontal" Height=0"
Minimum = " Maximum = 00" LargeChange="l" SmallChange="l"/>
<■-- Содержимое Label будет привязано к линейке прокрутки —>
<Label x:Name="labelSBThumb" Height=0" BorderBrush="Blue"
BorderThickness=" Content = "/>
</StackPanel>
</TabItem>
Обратите внимание, что объект <ScrollBar> (названный здесь mySB)
сконфигурирован с диапазоном от 1 до 100. Цель заключается в том, чтобы при изменении
положения ползунка линейки прокрутки (или по щелчку на стрелке влево или вправо) элемент
Label автоматически обновлялся текущим значением.
Установка привязки данных с использованием Blend
Механизм, обеспечивающий определение привязки в XAML — это расширение
разметки {Binding}. Установка привязки между элементами управления в Blend
осуществляется легко. В рассматриваемом примере найдите свойство Content объекта
Label (в области Common Properties окна Properties) и щелкните на очень
маленьком белом квадрате рядом со свойством для открытия окна расширенных свойств
(Advanced Properties). Выберите элемент Data Binding (Привязка данных), как показано
на рис. 28.44.
Здесь нас интересует вкладка Element Property (Свойство элемента), потому что на ней
представлен список всех элементов в файле XAML, которые можно выбирать в качестве
источника операции привязки данных. Перейдите на эту вкладку и в окне списка Scene
elements (Элементы сцены) найдите объект ScrollBar (по имени mySB). В окне списка
Properties (Свойства) выберите значение Value (рис. 28.45). Щелкните на кнопке ОК.
Глава 28. Программирование с использованием элементов управления WPF 1097
Рис. 28.44. Конфигурирование Рис. 28.45. Выбор объекта-источника и его свойства
операции привязки данных в Blend
Запустив программу, вы обнаружите, что содержимое метки обновляется при
перемещении ползунка. Теперь взглянем на XAML-разметку, сгенерированную
инструментом привязки:
<Label x:Name="labelSBThumb" Height=0" BorderBrush="Blue" BorderThickness="
Content = "(Binding Value, ElementName=mySB, Mode=Default}"/>
Обратите внимание на значение, присвоенное свойству Content элемента Label.
Значение ElementName представляет источник операции привязки данных (объект
ScrollBar), а первый элемент после ключевого слова Binding (Value) представляет
(в данном случае) свойство элемента, который нужно получить.
Если вам приходилось ранее работать с привязкой данных WPF, то вы можете
ожидать увидеть использование лексемы Path для установки наблюдаемого свойства
объекта. Например, следующая разметка будет также корректно обновлять Label:
<Label x:Name="labelSBThumb" Height=0" BorderBrush="Blue"
BorderThickness=" Content = "{Binding Path=Value,
ElementName=mySB, Mode=Default} "/>
По умолчанию Blend пропускает аспект Path= операции привязки данных, если
только свойство не является подсвойством другого объекта (например, myObject.
MyProper ty. Object 2. Proper ty2).
Свойство DataContext
Для определения операции привязки данных в XAML может применяться
альтернативный формат, при котором допускается разбивать значения, заданные расширением
разметки {Binding}, за счет явной установки свойства DataContext в источник
операции привязки:
1098 Часть VI. Построение настольных пользовательских приложений с помощью WPF
<!-- Разбиение пары "объект/значение" через DataContext —>
<Label x:Name="labelSBThumb" Height=0" BorderBrush="Blue"
BorderThickness="
DataContext = "{Binding ElementName=mySB}"
Content = "{Binding Path=Value}" />
В этом примере вывод будет идентичным. С учетом этого может возникнуть вопрос:
когда необходимо устанавливать свойство DataContext явно? Это полезно в ситуации,
когда подэлементы наследуют свое значение в дереве разметки.
Подобным образом можно легко устанавливать один и тот же источник данных для
семейства элементов управления, вместо того, чтобы повторять избыточные
фрагменты XAML "{Binding ElementName=X, Path=Y}" во множестве элементов управления.
Например, предположим, что в контейнер <S tack Panel > этой вкладки добавлен новый
элемент Button:
<Button Content="Click" Height=40"/>
Для генерации привязок данных к множеству элементов управления можно было бы
использовать Blend, но вместо этого давайте попробуем ввести модифицированную
разметку в редакторе XAML:
<!-- В StackPanel устанавливается свойство DataContext —>
<StackPanel Width=50" DataContext = "{Binding ElementName=mySB}">
<Label Content="Move the scroll bar to see the current value"/>
<ScrollBar Orientation="Horizontal" Height=0" Name="mySB"
Maximum = 00" LargeChange=" SmallChange="l"/>
<!-- Теперь оба элемента пользовательского интерфейса работают
со значением линейки прокрутки уникальным образом -->
<Label x:Name="labelSBThumb" Height=0" BorderBrush="Blue" BorderThickness="
Content = "{Binding Path=Value}"/>
<Button Content="Click" Height=00"
FontSize = "{Binding Path=Value}"/>
</StackPanel>
Здесь свойство DataContext в <StackPanel> устанавливается напрямую. В
результате при перемещении ползунка не только отображается текущее значение в элементе
Label, но также увеличивается размер шрифта элемента Button в соответствие с тем
же значением. На рис. 28.46 показан возможный вывод.
Рис. 28.46. Привязка значения ScrollBar к Label и Button
Глава 28. Программирование с использованием элементов управления WPF 1099
Преобразование данных с использованием iValueConverter
Вместо ожидаемого целого числа для представления положения ползунка тип
ScrollBar использует значение типа double. Поэтому при перемещении ползунка в
элементе Label будут отображаться различные значения с плавающей точкой (вроде
61.0576923076923), которые выглядят не слишком интуитивно понятными для
конечного пользователя, который ожидает целые числа (такие как 61, 62, 63 и т.д.).
Одним из возможных способов преобразования значения из операции привязки
данных в альтернативный формат является создание специального класса, реализующего
интерфейс IValueCVonverter, доступный в пространстве имен System.Windows.Data.
В этом интерфейсе определены два члена, которые позволяют осуществлять
преобразование между источником и целью (в случае двунаправленной привязки). После
определения такой класс можно использовать для последующего уточнения процесса
привязки данных.
Исходя из того, что в элементе управления Label необходимо отображать целые
числа, показанный ниже класс можно построить с помощью Expression Blend. Выберите
пункт меню Project^Add New Item (ПроектеДобавить новый элемент) и добавьте класс
по имени MyDoubleConverter. Затем поместите в него следующий код:
class MyDoubleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
// Преобразовать double в int.
double v = (double)value;
return (int)v;
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
// Поскольку заботиться о "двунаправленной" привязке
/ / не нужно, просто вернуть значение.
return value;
}
}
Метод Convert() вызывается при передаче значения от источника (ScrollBar) к
цели (свойство Content элемента Label). Хотя получается множество входных
аргументов, для этого преобразования понадобится манипулировать только входным object,
который представляет текущее значение double. Этот тип можно использовать для
приведения к целому и возврата нового числа.
Метод ConvertBack() будет вызван, когда значение передается от цели к источнику
(если включен двунаправленный режим). Здесь мы просто возвращаем значение. Это,
например, позволяет вводить в TextBox значение с плавающей точкой (например, 99.9)
и автоматически преобразовать его в целочисленное значение (99), когда производится
выход из элемента управления. Такое "свободное" преобразование происходит потому,
что метод Convert() будет вызван еще раз после вызова ConvertBack(). Если просто
вернуть null из ConvertBackO, то синхронизация привязки будет нарушена, потому
что TextBox все еще будет отображать число с плавающей точкой.
Установка привязок данных в коде
Имея этот класс, можно зарегистрировать специальный преобразователь с любым
элементом управления, который желает использовать его. Это допускается сделать
1100 Часть VI. Построение настольных пользовательских приложений с помощью WPF
исключительно в XAML-разметке, но тогда потребуется определить ряд специальных
объектных ресурсов, которые рассматриваются в следующей главе. А пока класс
преобразователя данных можно зарегистрировать в коде. Начните с очистки текущего
определения элемента управления <Label> на вкладке Data Binding, чтобы больше не
использовалось расширение разметки {Binding}:
<Label x:Name="labelSBThumb" Height=0" BorderBrush="Blue"
BorderThickness=" Content = "/>
В конструкторе окна вызовите новую приватную вспомогательную функцию по
имени SetBindings(). Ниже показан код SetBindings():
private void SetBindings ()
(
// Создать объект Binding.
Binding b = new Binding ();
// Зарегистрировать преобразователь, источник и путь.
b.Converter = new MyDoubleConverter ();
b.Source = this.mySB;
b.Path = new PropertyPath("Value");
// Вызвать метод SetBinding на Label.
this.labelSBThumb.SetBinding (Label.ContentProperty, b) ;
}
Единственный фрагмент этой функции, который выглядит несколько необычно — это
вызов SetBinding(). Обратите внимание, что первый параметр обращается к
статическому, доступному только для чтения полю класса Label по имени ContentProperty.
Как будет показано в главе 31, эта конструкция называется свойством
зависимости. Пока просто знайте, что при установке привязки в коде в первом аргументе
почти всегда требуется указывать имя класса, который нуждается в привязке (в данном
случае Label), за которым следует имя свойства с суффиксом Property (см. главу 31).
Запустив приложение, можно удостовериться, что элемент Label отображает только
целые числа.
Построение вкладки DataGrid
К этому моменту построен достаточно интересный пример с использованием
исключительно Expression Blend. Однако в оставшейся части проекта будет применяться
Visual Studio 2010, что даст некоторое начальное представление относительно
совместной работы Blend и Visual Studio. Сохраните проект и закройте Blend. Затем
откройте этот же проект в Visual Studio 2010, выбрав пункт меню File^Open^Project/Solution
(Файл "=> Открыть1^ Проект/Решение).
В предыдущем примере привязки данных было показано конфигурирование двух (или
более) элементов управления для участия в операции привязки данных. Допускается также
привязывать данные из файлов XML, базы данных и объектов в памяти. Для завершения
рассматриваемого примера спроектируем финальную вкладку DataGrid, которая будет
отображать информацию, извлеченную из таблицы Inventory базы данных AutoLot.
Как и с другими вкладками, начнем с замены текущего контейнера Grid на
StackPanel. Сделайте это непосредственным обновлением XAML-разметки в Visual
Studio. После этого в новом элементе StackPanel определите элемент управления
DataGrid по имени gridlnventory:
<TabItem x:Name="tabDataGrid" Header="DataGrid">
<StackPanel>
<DataGrid x:Name="gridInventory" Height=88"/>
</StackPanel>
</TabItem>
Глава 28. Программирование с использованием элементов управления WPF 1101
Затем сошлитесь на сборку AutoLotDAL.dll, созданную в главе 23 (с применением
Entity Framework), а также на System.Data.Entity.dll. Поскольку используется API-
интерфейс Entity Framework, нужно обеспечить необходимые данные строки
соединения в файле App.config. Начать можно с копирования файла App.conf ig из проекта
AutoLotEDMGUI, рассматриваемого в главе 23, в текущий проект с помощью пункта
меню Projects Add Existing Item (Проекте Добавить существующий элемент).
Откройте файл кода окна и добавьте финальную вспомогательную функцию по
имени ConfigureGridO; вызовите ее в конструкторе. Предполагая, что пространство имен
AutoLotDAL импортировано, все, что остается сделать — это добавить несколько строк
кода:
private void ConfigureGridO
{
using (AutoLotEntitien context = new AutoLotEntities () )
{
// Построить запрос LINQ, который извлечет некоторые данные из таблицы Inventory.
var dataToShow = from c in context.Inventories
select new { c.CarlD, c.Make, c.Color, c.PetName };
this.gridlnventory.ItemsSource = dataToShow;
}
}
Обратите внимание, что привязывать напрямую context.Inventories к коллекции
ItemsSource сетки не нужно; вместо этого строится запрос LINQ, который
запрашивает те же данные в сущностях. Причина такого подхода в том, что объект Inventory
также содержит дополнительные свойства EF, которые будут появляться в сетке, но
которые не отображаются на физическую базу данных.
Запустив этот проект в том виде, как он есть, можно увидеть исключительно
простую плоскую сетку. Чтобы немного улучшить ее, воспользуйтесь окном Properties в
Visual Studio 2010 для редактирования категории Rows элемента DataGrid. Как
минимум, установите свойство AlternationCount равным 2 и задайте специальные цвета в
свойствах AlternatingRowBackground и RowBackground, используя интегрированный
редактор (возможные значения показаны на рис. 28.47).
Properties
DataGrid gndlnventory
^f Properties ф Events
Rows
AlternatingP,o*Background
AlternationCcunt
AreRowDetailsFrozen
CanltserAddRows
CanUserDeleteRows
CanUserResizeRows
MinRowHeight
RowDetaiisTemplete
RowDetailsvisibilityMode
RowHeight
RowStyle
Ro wV alidationErro rT em pl«t«j
RowValidationRules
SelectionMode
SelectionUnrt
Layoot
Background
BordeiBrush
V
Рис. 28.47. Некоторые улучшения внешнего вида сетки
1102 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Внешний вид финальной вкладки для данного примера показан на рис. 28.48.
| Ink API ] Documents [
CariD
83
107
555
678
\щ
1000
1001 l
1992
2222
1
Data Binding : DataGrid |
Make
Ford
Ford
Ford
vugo
BMW
BMW
Saab
Vugo
Color
Rust
Red
Yellow
Green
Black
Tar.
Pink
Blue
PetName
Rusty-
Snake
Buzz
Ctunker
Btmmer
Daisy
Pinkey
Рис. 28.48. Финальная вкладка проекта
Следует отметить, что конфигурировать WPF-элемент DataGrid можно огромным
числом способов, так что ищите подробности в документации .NET Framework 4.0 SDK.
Например, можно построить специальные шаблоны данных для DataGrid с
применением графических типов WPF; в конечном счете, это позволит получить исключительно
развитый потльзовательский интерфейс.
На этом рассматриваемый в главе пример завершен. В последующих главах будут
применяться и другие элементы управления, а к настоящему моменту вы должны
почувствовать себя увереннее при построении пользовательских интерфейсов с помощью
Expression Blend, Visual Studio 2010 и непосредственной работой с XAML-разметкой и
кодом С#.
Исходный код. Проект WpfControlsAndAPIs доступен в подкаталоге Chapter 28.
Резюме
В этой главе рассматривались некоторые аспекты, связанные с элементами
управления WPF, начиная с обзора инструментария для элементов управления и роли
диспетчеров компоновки (панелей). Первый пример был посвящен построению простого
приложения текстового процессора. В нем демонстрировалось использование
интегрированной в WPF функциональности проверки правописания, а также создание главного
окна с системой меню, строкой состояния и панелью инструментов.
Что более важно, вы узнали, как строить команды WPF Эти независимые от
элемента управления события можно присоединять к элементу пользовательского интерфейса
или жесту ввода (input gesture) для автоматического наследования готовой
функциональности (например, операций с буфером обмена).
Кроме того, вы узнали, как работать с инструментом Microsoft Expression Blend.
В частности, с его помощью был построен сложный пользовательский интерфейс.
Попутно вы узнали об интерфейсах Ink API и Document API, доступных в WPF. Вы также
получили представление об операциях привязки данных WPF, включая использование
класса DataGrid из WPF в .NET 4.0 для отображения информации из специальной базы
данных AutoLot.
ГЛАВА 29
Службы визуализации
графики WPF
В этой главе мы рассмотрим средства визуализации графики Windows Presentation
Foundation (WPF). Как вы увидите, WPF предлагает три отдельных пути
визуализации графических данных — фигуры, рисунки и визуальные объекты. Разобравшись
в преимуществах и недостатках каждого подхода, мы приступим к изучению мира
интерактивной двухмерной графики с использованием классов из пространства имен
System.Windows.Shapes. После этого будет показано, как с помощью рисунков и
геометрии визуализировать двухмерные данные в облегченной манере. И, наконец, вы
узнаете, обеспечить наивысший уровень мощи и производительности визуального
уровня.
Попутно рассматривается множество связанных тем, таких как создание
специальных кистей и перьев, применение графических трансформаций к визуализациям и
выполнение операций проверки попадания (hit-testing). Будет показано, как упростить
решение задач кодирования графики с помощью интегрированных инструментов Visual
Studio 2010, Expression Blend и Expression Design.
На заметку! Графика является ключевым аспектом разработки WPF. Даже если задача не связана
с построением приложения с интенсивной графикой (вроде видеоигры или мультимедийного
приложения), темы, представленные в этой главе, критичны для работы с такими службами, как
шаблоны элементов управления, анимация и настройка привязки данных.
Службы графической визуализации WPF
В WPF используется особая разновидность графической визуализации, которая
называется графикой сохраненного режима (retained-mode). Это означает, что в случае
использования XAML-разметки или процедурного кода для генерации графической
визуализации, за сохранение этих визуальных элементов и обеспечение их корректной
перерисовки и обновления в оптимальной манере отвечает WPF. Поэтому визуализируемые
графические данные постоянно присутствуют, даже когда конечный пользователь
скрывает изображение, перемещая или сворачивая окно, перекрывая одно окно другим и т.д.
В отличие от этого, прежние версии API-интерфейсов визуализации графики
(включая GDI+ в Windows Forms) были графическими системами режима интерпретации
(immediate-mode). Согласно этой модели на программиста возлагается ответственность
за то, чтобы визуализируемые элементы корректно запоминались и обновлялись на
протяжении жизни приложения. Например, в приложении Windows Forms визуализа-
1104 Часть VI. Построение настольных пользовательских приложений с помощью WPF
ция фигуры, наподобие прямоугольника, предполагала обработку события Paint (или
переопределение виртуального метода OnPainO), получение объекта Graphics для
рисования прямоугольника и, что наиболее важно, добавление инфраструктуры для
обеспечения сохранения изображения, когда пользователь изменяет размеры окна
(например, за счет создания переменных-членов для представления позиции прямоугольника
и вызова Invalidate () в программе).
Переход от графики режима интерпретации к графике сохраненному режиму —
однозначно хорошее решение, поскольку позволяет программистам писать и
сопровождать меньший объем кода. Однако нельзя утверждать, что графический API-интерфейс
WPF полностью отличается от более ранних инструментариев визуализации. Например,
подобно GDI+, в WPF поддерживаются разнообразные типы объектов кистей и перьев,
техника проверки попадания, области отсечения, графические трансформации и т.д.
Поэтому, если есть опыт работы в GDI+ (или GDI), это значит, что уже имеется хорошее
представление о том, как выполнять базовую визуализацию под WPF.
Опции графической визуализации WPF
Как и с другими аспектами разработки WPF, существует выбор, каким образом
выполнять графическую визуализацию, а также делать это в XAML-разметке или
процедурном коде С# (либо с помощью их комбинации). В частности, в WPF предлагаются три
разных подхода к визуализации графических данных.
• Фигуры. В пространстве имен System.Windows .Shapes определено небольшое
количество классов для визуализации двухмерных геометрических объектов
(прямоугольников, эллипсов, многоугольников и т.п.). Хотя эти типы очень просты в
применении и достаточно мощные, в случае непродуманного использования они
могут привести к значительным накладным расходам памяти.
• Рисунки и геометрии. Второй способ визуализации графических данных
предусматривает работу с наследниками абстрактного класса System.Windows.Media.
Drawing. Применяя такие классы, как GeometryDrawing или ImageDrawing
(в дополнение к различным геометрическим объектам), можно осуществлять
визуализацию графических данных в более легковесной (но с ограниченными
возможностями) манере.
• Визуальные объекты. Самый быстрый и наиболее легкий способ визуализации
графических данных в WPF предполагает использование визуального уровня,
который доступен только в кода С#. С помощью наследников класса System.
Windows.Media.Visual можно взаимодействовать непосредственно с
графической подсистемой WPF
Причина существования разнообразных способов решения одной и той же
задачи (т.е. визуализации графических данных) связана с необходимостью оптимального
использования памяти и обеспечения приемлемой производительности приложения.
Поскольку WPF — система, интенсивно использующая графику, нет ничего необычного
в том, что приложению приходится визуализировать сотни или даже тысячи
различных изображений на поверхности окна, и возможность выбора реализации (фигуры,
рисунки или визуальные объекты) может иметь огромное значение.
Следует понимать, что при построении приложения WPF велика вероятность, что
придется использовать все три варианта выбора. В качестве эмпирического правила
отметим, что если нужен небольшой объем интерактивных графических данных,
которыми должен манипулировать пользователь (принимать ввод от мыши, отображать
всплывающие подсказки и т.п.), то стоит отдать предпочтение классам из пространства
имен System.Windows.Shapes.
Глава 29. Службы визуализации графики WPF 1105
В отличие от этого, рисунки и геометрии лучше подходят, когда необходимо
моделировать сложные, в основном не интерактивные, основанные на векторах графические
данные с использованием XAML или С#. Хотя рисунки и геометрии могут реагировать
на события мыши, позволяют проверять попадание и выполнять операции
перетаскивания, обычно для этого приходится писать больше кода.
И последнее: если требуется самый быстрый способ визуализации значительных
объемов графических данных, то для этого наиболее подходит визуальный уровень
(visual layer). Например, предположим, что WPF используется для построения научного
приложения, которое должно рисовать тысячи показателей данных. За счет
применения визуального уровня эти показатели можно визуализировать самым оптимальным
способом. Как будет показано далее в главе, визуальный уровень доступен только из
кода С#, а не из XAML.
Независимо от выбранного подхода (фигуры, рисунки и геометрии либо визуальные
объекты), всегда будут использоваться общие графические примитивы, такие как кисти
(для заполнения ограниченных областей), перья (для рисования контуров) и объекты
трансформации (которые, разумеется, трансформируют данные). Начнем изучение с
классов из пространства System.Windows.Shapes.
На заметку! WPF поставляется также с полноценным API-интерфейсом для визуализации и
манипулирования трехмерной графикой, которая в этой книге не рассматривается. Подробную
информацию по этому поводу можно найти в документации .NET Framework 4.0 SDK.
Визуализация графических данных
с использованием фигур
Члены пространства имен System.Windows.Shapes предлагают самый прямой
и интерактивный, но и наиболее ресурсоемкий по занимаемой памяти способ
визуализации двумерных изображений. Это пространство имен (определенное в сборке
PresentationFramework.dll), достаточно мало, и состоит всего из шести
запечатанных (sealed) классов, расширяющих абстрактный базовый класс Shape: Ellipse,
Rectangle, Line, Polygon, Polyline и Path.
Создайте новое приложение WPF под названием RenderingWithShapes. Найдите в
браузере объектов Visual Studio 2010 абстрактный класс Shape (рис. 29.1) и раскройте
все родительские узлы. Вы увидите, что каждый наследник Shape получает
значительную часть функциональности по цепочке наследования.
Некоторые из этих родительских классов должны быть знакомы из материала
предыдущих двух глав. Вспомните, например, что в классе UIElement определены
многочисленные методы для получения ввода от мыши и обработки событий
перетаскивания, а в классе FrameworkElement доступны члены для работы с изменением размеров,
всгшывающими подсказками и тому подобным. Имея эту цепочку наследования, имейте
в виду, что при визуализации графических данных с использованием
классов-наследников Shape, объекты получаются почти столь же функциональными, как элементы
управления WPF!
Например, определение факта щелчка на визуализированном изображении
осуществляется не сложнее, чем обработка события MouseDown. Вот простой пример: написав
следующий код XAML Объекта Rectangle:
<Rectangle x:Name="myRect" Height=0" Width=0"
Fill="Green" MouseDown="myRect_MouseDown"/>
1106 Часть VI. Построение настольных пользовательских приложений с помощью WPF
| Object Browser X |
Browse My Solution
О Sys
i> ъ
- ч
■ -п
* ч
Elhpse
Line
Path
Polygon
Polyline
Rectangle
• r^j Base Types
* fy FrameworlcElement
(• 'O IFrameworklnputElement
rJ° IlnputElement
"° IQueryAmbient
*"° ISupportJnrtialize
s *\ UIEIement
-° lAnimatable
*° IlnputElement
л -*$ Visual
л tf$ DependencyObject
л Ц| DispatcherObject
■Ц Object
"* ArrangeOvernde(System.Windows.Size)
?* MeasureOverride(System.Windows.Size)
?% OnRender(System.Windows.Media.DrawingContext}
,♦ ShapeO
"^ DefiningGeometry
ЙГ«
; J3* Geometry! ransform
^? RenderedGeometry
jf Stretch
^ Stroke
ЙГ StrokeOashArray
'*? StrokeOashCap
public abstract ctass Shape :
System. Windows. FrameworlcElement
Member of S^eriL.WjLndjDwsJi
Summary:
Provides a base class for shape elements, such as
System.Windows. Shapes. Ellipse,
System.Wirtdows.Shapes.Potygon, and
System.Windows.Shapes.Rectartgle.
Рис. 29.1. Базовый класс Shape наследует значительную часть
функциональности от своих родительских классов
можно реализовать обработчик события MouseDown, который по щелчку на Rectangle
изменяет его цвет фона:
private void myRect_MouseDown(object sender, MouseButtonEventArgs e)
{
// Изменить цвет Rectangle при щелчке на нем.
myRect.Fill = Brushes.Pink;
В отличие от других графических инструментариев, здесь не придется писать
громоздкий код инфраструктуры, которая вручную отобразит координаты мыши на
геометрию, вручную вычислять попадание в границы, визуализировать в невидимый
буфер и т.п. Члены System.Windows.Shapes просто реагируют на события, на которые вы
зарегистрируетесь, подобно типичному элементу управления WPF (Button и т.д.).
Отрицательная сторона такой готовой функциональности состоит в том, что фигуры
потребляют довольно много памяти. При построении научного приложения, которое
рисует тысячи точек на экране, применение фигур будет неподходящим выбором (по сути,
это будет настолько же расточительным по памяти, как визуализация тысяч объектов
Button). Однако когда нужно сгенерировать интерактивное двумерное векторное
изображение, фигуры оказываются прекрасным выбором.
Помимо функциональности, унаследованной от родительских классов UIEIement и
FrameworkElement, в Shape определено множество собственных членов, наиболее
полезные из которых перечислены в табл. 29.1.
Таблица 29.1. Ключевые свойства базового класса Shape
Свойства
Назначение
DefiningGeometry
Fill
Возвращает объект Geometry, представляющий общие размеры
текущей фигуры. Этот объект содержит только точки,
используемые для визуализации данных, не имея следов функциональности
UIEIement или FrameworkElement
Позволяет указать "объект кисти" для визуализации внутренней
области фигуры
Глава 29. Службы визуализации графики WPF 1107
Окончание табл. 29.1
Свойства Назначение
GeometryTransform Позволяет применять трансформацию к фигуре, прежде чем она
будет визуализирована на экране. Унаследованное свойство
Render-Transform (из UIElement) применяет трансформацию
после ее визуализации на экране
stretch Описывает, как фигура располагается внутри выделенного ей
пространства, например, внутри диспетчера компоновки. Это управляется
соответствующим перечислением System.Windows.Media.Stretch
Stroke Определяет объект кисти или в некоторых случаях — объект пера
(который на самом деле также является кистью), используемый для
рисования границы фигуры
StrokeDashArray, Эти (и прочие) свойства, связанные со штрихами (stroke), управляют
StrokeEndLineCap, тем, как сконфигурированы линии при рисовании границ фигуры.
StrokeStartLineCap, В большинстве случаев с помощью этих свойств будет настраиваться
StrokeThickness кисть, применяемая для рисования границы или линии
На заметку! Если вы забудете установить свойства Fill и stroke, WPF предоставит
"невидимые" кисти, в результате чего фигура не будет видна на экране!
Добавление прямоугольников, эллипсов
и линий на поверхность Canvas
Далее в этой главе будет показано, как использовать инструменты Expression Blend и
Expression Design для генерации XAML-описаний графических данных. А пока давайте
построим WPF-приложение, которое может визуализировать фигуры на основе XAML
или С#, и параллельно посмотрим, что такое процессе проверки попадания. Прежде
всего, добавьте в начальную XAML-разметку элемента <Window> определение
контейнера <DockPanel>, содержащего (пока пустые) элементы <Тоо1Ваг> и <Canvas> (полотно).
Обратите внимание, что каждому содержащемуся элементу назначается подходящее
имя через свойство Name.
<DockPanel LastChildFill = ,,True">
<ToolBar DockPanel.Dock="Top11 Name="mainToolBar11 Height=ll50">
</ToolBar>
<Canvas Background="LightBlue11 Name=llcanvasDrawingArea"/>
</DockPanel>
Теперь наполним <ToolBar> набором объектов <RadioButton>, каждый из
которых содержит специфический класс-наследник Shape. При этом каждому элементу
<RadioButton> назначено одно и то же групповое имя GroupName (чтобы обеспечить
взаимное исключение) и соответствующее имя:
<ToolBar DockPanel.Dock="Top" Name=,,mainToolBar" Height=,,50">
<RadioButton Name="circleOption11 GrpupName=" shapeSeleetion">
<Ellipse Fill = ,,Green" Height=M35M Width=,,35" />
</RadioButton>
<RadioButton Name="rectOption11 GroupName=llshapeSelection">
<Rectangle Fill=,,Red" Height=5"
Width=5" Radiu5Y=,,10" RadiusX=,,10" />
</RadioButton>
1108 Часть VI. Построение настольных пользовательских приложений с помощью WPF
<RadioButtan Name="lineOption11 GroupName=llshapeSelection">
<Line Height=M35M Width=,,35"
StrokeThickness=0" Stroke="Blue11
Xl = 0" Yl = ,,10" Y2 = ,,25" X2 = ,,25"
StrokeStartLineCap="Triangle11 StrokeEndLineCap="Round11 />
</RadioButton>
</ToolBar>
Как видите, объявления объектов Rectangle, Ellipse и Line в XAML довольно
прямолинейны и требуют минимума комментариев. Вспомните, что свойство Fill
позволяет указать кисть для рисования внутренностей фигуры. Когда нужна кисть
сплошного цвета, можете просто задать жестко закодированную строку известных значений, а
соответствующий преобразователь типов (см. главу 28) сгенерирует корректный объект.
Одна интересная характеристика типа Rectangle связана с тем, что в нем определены
свойства RadiusX и RadiusY, позволяющие при желании визуализировать скругленные
углы.
Линия Line представляется начальной и конечной точками с помощью свойств XI,
Х2, Y1 и Y2 (учитывая, что высота и ширина применительно к линии не имеют
смысла). В разметке устанавливаются несколько дополнительных свойств, управляющих
визуализацией начальной и конечной точек Line, а также конфигурирующих настройки
штриха. На рис. 29.2 показана визуализированная панель инструментов в визуальном
конструкторе WPF в Visual Studio 2010.
щ
Е
Fun with Shapes!
•Iv
.
Рис. 29.2 Использование объектов Shape в качестве содержимого для
набора элементов RadioButton
Теперь с помощью окна Properties (Свойства) среды Visual Studio 2010 создайте
обработчики события MouseLef tButtonDown для Canvas и события Click для каждой
RadioButton. В файле С# цель заключается в том, чтобы визуализировать выбранную
фигуру (круг, квадрат или линию), когда пользователь щелкнет на Canvas. Для
начала определите следующее встроенное перечисление (и соответствующую переменную-
член) внутри класса, унаследованного от Window:
public partial class MainWindow : Window
{
private enum SelectedShape
{ Circle, Rectangle, Line }
private SelectedShape currentShape;
}
Внутри каждого обработчика Click установите для переменной-члена currentShape
корректное значение SelectedShape. Например, ниже показан код обработчика
события Click объекта RadioButton по имени circleOption. Реализуйте остальные
обработчики Click аналогичным образом:
Глава 29. Службы визуализации графики WPF 1109
private void circleOption_Click(object sender, RoutedEventArgs e)
{
currentShape = SelectedShape.Circle;
}
С помощью обработчика события MouseLef tButtonDown элемента Canvas будет
визуализироваться корректная фигура (предопределенного размера) в начальной точке,
соответствующей позиции X, Y курсора мыши. Ниже показана полная реализация:
private void
canvasDrawingArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Shape shapeToRender = null;
// Сконфигурировать корректную фигуру для рисования.
switch (currentShape)
{
case SelectedShape.Circle:
shapeToRender = new Ellipse ()
{ Fill = Brushes.Green, Height = 35, Width =35 };
break;
case SelectedShape.Rectangle:
shapeToRender = new Rectangle ()
{ Fill = Brushes.Red, Height = 35, Width = 35, RadiusX = 10, RadiusY = 10 };
break;
case SelectedShape.Line:
shapeToRender = new Line()
{
Stroke = Brushes.Blue,
StrokeThickness = 10,
XI =0, X2 = 50, Yl = 0, Y2 = 50,
StrokeStartLineCap= PenLineCap.Triangle,
StrokeEndLineCap = PenLineCap.Round
};
break;
default:
return;
}
// Установить верхний левый угол для рисования на полотне.
Canvas.SetLeft(shapeToRender, e.GetPosition(canvasDrawingArea).X);
Canvas.SetTop (shapeToRender, e.GetPosition(canvasDrawingArea) .Y);
// Нарисовать фигуру.
canvasDrawingArea.Children.Add(shapeToRender);
}
На заметку! Вы можете заметить, что объекты Ellipse, Rectangle и Line, создаваемые в
методе, имеют те же настройки свойств, что и соответствующие определения XAML Как и
ожидалось, этот код можно упростить, но это требует понимания объектных ресурсов WPF, которые
рассматриваются в главе 30.
В коде производится проверка переменной-члена currentShape для создания
корректного объекта-наследника Shape. После этого устанавливаются значения
координат левой верхней вершины внутри Canvas с использованием входного объекта
MouseButtonEventArgs. И, наконец, в коллекцию объектов UIElement,
поддерживаемых Canvas, добавляется новый объект-наследник Shape. Если теперь запустить
программу, то можно щелкать левой кнопки мыши где угодно на Canvas и при этом на
полотне будут появляться фигуры.
1110 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Удаление прямоугольников, эллипсов
и линий с поверхности Canvas
Имея Canvas с коллекцией объектов, может возникнуть вопрос: как теперь
динамически удалить элемент, возможно, в ответ на щелчок пользователя правой кнопкой
мыши на фигуре? Это делается с помощью класса VisualTreeHelper из пространства
имен System.Windows.Media. В главе 31 вы узнаете о роли "визуальных деревьев" и
"логических деревьев". А пока обработаем событие MouseRightButtonDown класса Canvas
и реализуем обработчик, как показано ниже:
private void
canvasDrawingArea_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
// Сначала получить координаты X,Y щелчка пользователя.
Point pt = e.GetPosition((Canvas)sender);
// Воспользоваться методом HitTest() класса VisualTreeHelper,
// чтобы проверить попадание на элемент в Canvas.
HitTestResult result = VisualTreeHelper.HitTest (canvasDrawingArea, pt) ;
// Если результат не равен null, щелчок произведен на фигуре.
if (result l= null)
{
// Получить фигуру, на которой совершен щелчок, и удалить ее из Canvas.
canvasDrawingArea.Children.Remove(result.VisualHit as Shape);
}
}
Этот метод начинается с получения точного местоположения X, Y, где пользователь
щелкнул в Canvas, и проверяет попадание через статический метод VisualTreeHelper.
HitTest(). Возвращенное значение — объект HitTestResult — будет установлен в
null, если пользователь не щелкнул на UIElement внутри Canvas. Если HitTestResult
не равен null, с помощью свойства VisualHit можно получить элемент UIElement, на
котором совершен щелчок, и привести его к объекту-наследнику Shape (вспомните, что
Canvas может содержать любой UIElement, а не только фигуры). Подробности
устройства "визуального дерева" будут изложены в следующей главе.
На заметку! По умолчанию VisualTreeHelper.HitTest() возвращает UIElement верхнего
уровня и не предоставляет информацию о других объектах, находящихся ниже (те перекрытых
в Z-порядке).
После этих модификаций появляется возможность добавлять фигуру на Canvas
щелчком левой кнопки мыши и удалять ее щелчком правой кнопки мыши. На рис. 29.3
продемонстрирована функциональность текущего примера.
Пока все хорошо. Мы использовали объекты-наследники Shape для визуализации
содержимого элементов RadioButton, используя XAML-разметку и заполняя Canvas кодом
С#. При рассмотрении роли кистей и графических трансформаций к этому примеру
будет добавлена еще некоторая функциональность. Кстати говоря, в другом примере этой
главы иллюстрируется реализация технологии перетаскивания на объектах UIElement.
А пока уделим внимание членам пространства имен System.Windows.Shapes.
Работа с элементами Polyline и Polygon
В текущем примере используются только три класса-наследника Shape. Остальные
дочерние классы (Polyline, Polygon и Path) очень утомительно корректно
визуализировать, не используя инструмента вроде Expression Blend — просто потому, что они
требуют рисования большого количества точек для своего представления.
Глава 29. Службы визуализации графики WPF 1111
[ l» Fun with Shapes! UrlIll-УИНИ
Рис. 29.3. Работа с фигурами
Инструмент Expression Blend будет применяться чуть позже, а сейчас представим
краткий обзор остальных типов Shapes.
Тип Polyline позволяет определить коллекцию координат [х, у) (через свойство Points)
для рисования серий сегментов линий, не требующих замыкания. Тип Polygon похож,
однако он запрограммирован так, что всегда замыкает контур, соединяя начальную
точку с конечной, и заполняет внутреннюю область указанной кистью. Предположим, что
показанный ниже элемент <StackPanel> описан в редакторе Kaxaml или в специальном
редакторе XAML, который был создан в качестве примера в главе 27:
<•-- Элемент Polyline автоматически не замыкает концы -->
<Polyline Stroke ="Red11 StrokeThickness =0" StrokeLineJoin ="Round"
Points =M10,10 40,40 10,90 300,50M/>
<!-- Элемент Polygon всегда замыкает концы -->
<Polygon Fill ="AliceBlue11 StrokeThickness =" Stroke ="Green"
Points =0,10 70,80 10,50" />
На рис. 29.4 показывает визуализированный вывод.
Рис. 29.4. Элементы Polyline и Polygon
Работа с элементом Path
Используя только типы Rectangle, Ellipse, Polygon, Polyline и Line, нарисовать
детализированное двухмерное векторное изображение было бы чрезвычайно сложно,
поскольку эти примитивы не позволяют легко фиксировать графические данные,
подобные кривым, объединениям перекрывающихся данных и т.д. Последний
унаследованный от Shape класс Path предоставляет возможность определять сложные двухмерные
1112 Часть VI. Построение настольных пользовательских приложений с помощью WPF
графические данные как коллекцию независимых геометрий. После определения
коллекцию таких геометрий можно присвоить свойству Data класса Path, где эта
информация будет применяться для визуализации сложного двухмерного изображения.
Свойство Data получает экземпляр класса, унаследованного от System.Windows.
Media.Geometry, который содержит ключевые члены, перечисленные в табл. 29.2.
Таблица 29.2. Избранные члены класса System.Windows.Media.Geometry
Член
Назначение
Bounds
FillContainsO
GetAreaO
GetRenderBounds ()
Transform
Устанавливает текущий ограничивающий прямоугольник, содержащий в
себе геометрию
Определяет, находится ли данный Point (или другой объект Geometry)
в границах определенного класса-наследника Geometry. Это полезно
для вычислений попадания
Возвращает общую область, Занятую объектом-наследником Geometry
Возвращает Rect, содержащий минимальный возможный
прямоугольник, который может быть использован для визуализации объекта класса-
наследника Geometry
Назначает геометрии объект Transform для изменения визуализации
Классы, расширяющие Geometry (табл. 29.3), выглядят очень похоже на свои
аналоги, унаследованные от Shape. Например, EllipseGeometry имеет члены, похожие на
члены Ellipse. Значительное отличие состоит в том, что классы-наследники Geometry
не знают, как визуализировать себя непосредственно, поскольку они не являются
UIElement. Вместо этого классы-наследники Geometry представляют всего лишь
коллекцию данных о точках, которая указывает Path, как визуализировать себя.
На заметку! Path — не единственный класс в WPF, который может работать с коллекциями
геометрий. Например, DoubleAnimationUsingPath, DrawingGroup, GeometryDrawing
и даже UIElement могут использовать геометрии для визуализации, применяя свойства
PathGeometry, ClipGeometry, Geometry и Clip соответственно.
Таблица 29.3. Классы, унаследованные от Geometry
Класс
Назначение
LineGeometry
RectangleGeometry
EllipseGeometry
GeometryGroup
CombinedGeometry
PathGeometry
Представляет прямую линию
Представляет прямоугольник
Представляет эллипс
Позволяет сгруппировать вместе несколько объектов Geometry
Позволяет объединить два разных объекта Geometry в единую фигуру
Представляет фигуру, состоящую из линий и кривых
Ниже приведена определенная в Kaxaml разметка для элемента Path, который
использует несколько производных от Geometry типов. Обратите внимание, что
свойство Data объекта Path устанавливается в объект GeometryGroup, содержащий другие
объекты-наследники Geometry, такие как EllipseGeometry, RectangleGeometry и
LineGeometry. Результат можно видеть на рис. 29.5.
Глава 29. Службы визуализации графики WPF 1113
<|-- Элемент Path содержит набор объектов
Geometry, установленный в свойстве Data -->
<Path Fill = "Orange" Stroke = "Blue" StrokeThickness = ">
<Path.Data>
<GeometryGroup>
<EllipseGeometry Center = 5,70"
RadiusX = 0" RadiusY = 0" />
<RectangleGeometry Rect = 5,55 100 30" />
<LineGeometry StartPoint=,0" EndPoint=0,30" />
<LineGeometry StartPoint=0,30" EndPoint=,30" />
</GeometryGroup>
</Path.Data>
</Path>
©3
Рис. 29.5. Элемент Path, содержащий различные объекты Geometry
Изображение на рис. 29.5 может быть визуализировано с помощью показанных
ранее классов Line, Ellipse и Rectangle. Однако для этого потребуется поместить в
память несколько объектов UIElement. Когда для моделирования того, что нужно
нарисовать, используются геометрии, а затем коллекция геометрий помещается в контейнер,
который может визуализировать данные (в данном случае — Path), тем самым
сокращается расход памяти.
Теперь вспомните, что Path имеет ту же цепочку наследования, что и любой член
System.Windows.Shapes, и потому в состоянии посылать те же уведомления о
событиях, что и другие элементы UIElement. Поэтому, если определить тот же элемент <Path>
в проекте Visual Studio 2010, то можно будет выяснить, когда пользователь щелкнет в
любом месте линии, просто обрабатывая событие мыши (редактор Kaxaml не позволяет
обрабатывать события в написанной разметке).
Мини-язык моделирования путей
Из всех классов, перечисленных в табл. 29.2Т PathGeometry является наиболее
сложным для конфигурирования в терминах XAML и кода. Это связано с тем, что
каждый сегмент PathGeometry состоит из объектов, содержащих различные сегменты и
фигуры (например, ArcSegment, BezierSegment, LineSegment, PolyBezierSegment,
PolyLineSegment, PolyQuadraticBezierSegment и т.д.). Ниже приведен пример
объекта Path, свойство Data которого установлено в <PathGeometry>, состоящий из
различных фигур и сегментов.
•'Path Stroke="Black" StrokeThickness = "l">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigure StartPoint=0,50">
<PathFigure.Segments>
<BezierSegment
Pointl=00,0"
Point2=00,200"
1114 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Point3=00,100"/>
<LineSegment Point=00,100" />
<ArcSegment
Size=0,50м RotationAngle=5M
IsLargeArc="True11 SweepDi re с tion=" Clockwise"
Point=00,100"/>
</PathFigure.Segments>
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
Следует отметить, что очень немногим программистам понадобится когда-либо
вручную строить сложные двухмерные изображения, непосредственно описывая
объекты классов-наследников Geometry или PathSegment. В действительности сложные
пути будут формироваться графически с использованием инструмента Expression Blend
или Expression Design.
Но даже учитывая помощь указанных инструментов, объем XAML-разметки,
требуемый для определения сложных объектов Path, может устрашать, поскольку такие
данные должны включать полные описания разнообразных классов-наследников Geometry
или PathSegment. Для того чтобы создавать более краткую и компактную разметку, в
классе Path поддерживается специализированный "мини-язык".
Например, вместо установки для свойства Data объекта Path коллекции объектов-
наследников Geometry и PathSegment, его можно установить в единственный
строковый литерал, содержащий набор известных символов и различных значений, которые
определяют фигуру, подлежащую визуализации. Фактически, инструменты Expression
Blend и Expression Design для построения объекта Path внутри автоматически
используют мини-язык. Рассмотрим простой пример и его результирующий вывод (рис. 29.6):
<Path Stroke="Black" StrokeThickness="
Data="M 10,75 С 70,15 250,270 300,175 H 240" />
Рис. 29.6. Мини-язык моделирования путей позволяет компактно
описывать модель объекта Geometry/PathSegment
Команда М (от move — двигать) принимает координаты X,Y позиции, представляющей
начальную точку рисования. Команда С принимает серию точек для визуализации
кривой (точнее — кубической кривой Безье), а команда Н рисует горизонтальную линию.
И снова следует отметить, что вручную строить или разбирать строковый литерал,
содержащий инструкции мини-языка, придется очень редко. Но, по крайней мере,
XAML-разметка, сгенерированная Expression Blend, не будет казаться чем-то
непонятным. Если интересуют детали этой конкретной грамматики, обращайтесь в раздел "Path
Markup Syntax" ("Синтаксис разметки путей") документации .NET Framework 4.0 SDK.
Глава 29. Службы визуализации графики WPF 1115
Кисти и перья WPF
Каждый из способов графической визуализации (фигура, рисование и геометрии,
а также визуальные объекты) интенсивно использует кисти, что позволяет управлять
заполнением внутренней области двухмерной фигуры. В WPF предлагаются шесть
разных типов кистей, и все они расширяют класс System.Windows.Media.Brush. Хотя
Brush — абстрактный класс, его наследники, перечисленные в табл. 29.4, могут
применяться для заполнения области содержимым почти любого рода.
Таблица 29.4. Классы, унаследованные от Brush
Класс Назначение
DrawingBrush Заполняет область объектом-наследником Drawing
(GeometryDrawing, ImageDrawing или VideoDrawing)
ImageBrush Заполняет область изображением (представленным объектом
ImageSource)
LinearGradientBrush Заполняет область линейным градиентом
RadialGradientBrush Заполняет область радиальным градиентом
SolidColorBrush Рисует сплошной цвет, установленный свойством Color
VisualBrush Заполняет область объектом-наследником Visual
(DrawingVisual, Viewport3DVisualи ContentVisual)
Классы DrawingBrush и VisualBrush позволяют строить кисти на основе
существующего класса, унаследованного от Drawing или Visual. Эти классы кистей
используются во время работы с двумя другими опциями графики WPF (рисунками и
визуальными объектами) и будут объясняться далее в этой главе.
Класс ImageBrush позволяет строить кисть, отображающую графические данные
йЬ внешнего файла или встроенного ресурса приложения, указанного в его свойстве
ImageSource. Остальные типы кистей (LinearGradientBrush и RadialGradientBrush)
использовать достаточно легко, хотя набор необходимого кода XAML может быть
утомительным. К счастью, в среде Visual Studio 2010 поддерживаются интегрированные
редакторы кистей, которые упрощают задачу генерации стилизованных кистей.
Конфигурирование кистей с использованием Visual Studio 2010
Теперь давайте обновим WPF-приложение рисования RenderingShapes для
использования некоторых интересных кистей. Три фигуры, с которыми мы имели дело до сих
пор при визуализации данных в панели инструментов, используют простые сплошные
цвета, так что их значения можно зафиксировать простыми строковыми литералами.
Чтобы сделать задачу немного более интересной, применим теперь интегрированный
редактор кистей. Убедитесь, что редактор XAML начального окна открыт в IDE-среде и
выберите элемент Ellipse. Теперь найдите свойство Fill в окне Properties и щелкните
на раскрывающемся списке. Откроется редактор кистей, показанный на рис. 29.7.
Как видите, этот редактор содержит четыре ползунка, которые позволяют
устанавливать значение ARGB (цветовые компоненты alpha, red, green и blue (прозрачность,
красный, зеленый и синий)) для текущей кисти. Используя эти ползунки и связанную с ними
область выбора цвета, можно создать любой сплошной цвет. С помощью этого
инструмента измените цвет элемента Ellipse и просмотрите результирующую XAML-разметку.
Вы заметите, что цвет сохраняется в виде шестнадцатеричного значения, например:
<Ellipse Fill = "#FF47CE47" Height=,,35" Width=,,35" />
1116 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Что более интересно, тот же редактор позволяет конфигурировать и градиентные
кисти, которые используются для определения серий цветов и точек перехода. В левом
верхнем углу редактора кистей находятся четыре маленькие кнопки, первая из
которых позволяет установить null-кистъ для визуализированного вывода. Остальные три
предназначены для установки кисти сплошного цвета (это было только что сделано),
градиентной кисти и кисти-изображения.
Щелкните на кнопке градиентной кисти, и редактор отобразит несколько новых
опций (рис. 29.8). Три кнопки в нижнем левом углу позволяют выбрать вертикальный,
горизонтальный или радиальный градиент. Лента внизу покажет текущий цвет
каждого градиентного перехода, и каждый из них будет помечен специальным ползунком.
По мере перетаскивания ползунка по полосе градиента можно управлять смещением
градиента. Более того, щелкнув на соответствующем ползунке, можно изменить цвет
определенного градиентного перехода через селектор цвета. Наконец, щелчок на полосе
градиента позволяет добавить дополнительные градиентные переходы.
Потратьте некоторое время на освоение этого редактора, чтобы построить
радиальную градиентную кисть, содержащую три градиентных перехода, и установите их цвета
по своему выбору. На рис. 29.8 показа пример кисти, использующей три разных
оттенка зеленого.
В результате IDE-среда обновит XAML-разметку набором специальных кистей,
установив их в соответствующих свойствах (в данном примере — свойство Fill элемента
Ellipse) с применением синтаксиса "свойство-элемент". Например:
<Ellipse Height = ,,35" Width=,,35">
<Ellipse.Fill>
<RadialGradientBrush>
<GradientStop Color=,,#FF87E71B" Of fset = . 589" />
<GradientStop Color=,,#FF2BA92B" Of f set= . 013" />
<GradientStop Color="#FF34B71B" Offset="l" />
*" / Radi a 1 Gradient Br ush>
</Ellipse.Fill>
</Ellipse>
■ Properties * П X i
I EBpsc <noname> ^^м j
I ^"Properties -/ Events ^^r |
; i Р| [search
Effect □
♦ 1 Green jT]
FlowDirectij 0)И;|] Q
Focusable j _ fsl
FocusVisua _ ° ,^
ForceCHJ iBB ШШШШшХшшШШ О
Horizontal/ ■ ■ ° j
IsHitTestViJ ^^^^™" ^Ъ
] Ш Green /
IsManipulat
LayoutTrarJ ^
Margin □ 0
MaxHeiqht □ Infinity - j
Рис. 29.7. Любое свойство, требующее кисти,
может быть сконфигурировано с помощью
интегрированного редактора кистей
Рис. 29.8. Редактор кистей Visual Studio
позволяет строить базовые градиентные кисти
Глава 29. Службы визуализации графики WPF 1117
Конфигурирование кистей в коде
Итак, мы построили специальную кисть для XAML-определения элемента Ellipse, но
поскольку соответствующий код С# устарел, все равно получается круг со сплошным
зеленым цветом. Чтобы синхронизировать все, обновим соответствующий оператор case
для использования только что созданной кисти. Ниже приведено необходимое
обновление, которое выглядит несколько сложнее, чем можно было ожидать, поскольку шестна-
дцатеричное значение преобразуется в правильный объект Color через класс System.
Windows.Media.ColorConverter (модифицированный вывод показан на рис. 29.9).
case SelectedShape.Circle:
shapeToRender = new Ellipse () { Height = 35, Width = 35 };
// Создать кисть RadialGradientBrush в коде.
RadialGradientBrush brush = new RadialGradientBrush();
brush.Gradientstops.Add(new Gradientstop (
(Color)ColorConverter.ConvertFromString("#FF87E71B"), 0.589));
brush.GradientStops.Add(new GradientStop (
(Color)ColorConverter.ConvertFromString("#FF2BA92B"), 0.013));
brush.GradientStops.Add(new GradientStop (
(Color) ColorConverter .ConvertFromString (,,#FF34B71B") , 1) ) ;
shapeToRender.Fill = brush;
break;
■ Fun with Shapes!
1 *4 '
■ *
I \
ч
l^| (HI [-Д-^
Рис. 29.9. Рисование более изящных кругов
Кстати, объекты GradientStop можно строить, указывая простой цвет в качестве
первого параметра конструктора с использованием перечисления Colors,
возвращающего сконфигурированный объект Color:
GradientStop g = new GradientStop(Colors.Aquamarine, 1) ;
Если же нужен более тонкий контроль, можно сразу передать сконфигурированный
объект Color, например:
Color myColor = new Color () { R = 200, G = 100, В = 20, A = 40 };
GradientStop g = new GradientStop(myColor, 34);
Разумеется, применение перечисления Colors и класса Color не ограничено
градиентными кистями. Их можно использовать где угодно — там, где нужно представлять
значение цвета в коде.
1118 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Конфигурирование перьев
В отличие от кисти, перо (реп) — это объект для рисования границ геометрий, либо в
случае класса Line или PolyLine — самих геометрий. В частности, класс Реп позволяет
рисовать линию заданной толщины, представленную значением типа double. Вдобавок
Реп может быть сконфигурирован с помощью тех же свойств, что были представлены в
классе Shape, таких как начальный и конечный концы пера, шаблоны точек-тире и т.д.
Например:
<Pen Thickness=0" LineJoin="Round11 EndLineCap="Triangle11 StartLineCap="Round11 />
Во многих случаях непосредственно создавать объект Реп не придется,
поскольку это делается неявно, когда устанавливаются значения для свойств, таких как
StrokeThickness объект типа-наследника Shape (а также других типов UIElement).
Однако построение специального объекта Реп может пригодиться при работе с типами-
наследниками Drawing (описанными далее в этой главе). В Visual Studio 2010 редактор
перьев, как таковой, отсутствует, но в окне Properties можно конфигурировать все
свойства выбранного элемента, связанные со штрихами.
Применение графических трансформаций
Чтобы завершить обсуждение использования фигур, рассмотрим тему
трансформаций. WPF поставляется с многочисленными классами, расширяющими абстрактный
базовый класс System.Winodws. Media. Trans form. В табл. 29.5 перечислены основные
классы, унаследованные от Transform.
Таблица 29.5. Основные классы, унаследованные от System.Windows.Media.Transform
Класс Назначение
MatrixTransform Создает произвольную матричную трансформацию,
используемую для манипулирования объектами или координатными
системами на двухмерной поверхности
RotateTransform Поворачивает объект по часовой стрелке вокруг указанной точки
в двухмерной системе координат (х, у)
ScaleTransform Масштабирует объект в двухмерной системе координат (х, у)
SkewTransform Скашивает объект в двухмерной системе координат (х, у)
TranslateTransform Транслирует (перемещает) объект в двухмерной системе
координат (х, у)
TransformGroup Представляет объект Transform, состоящий из других объектов
Transform
Трансформации могут применяться к любому классу UIElement (те. к наследникам
Shape, а также к таким элементам управления, как Button, TextBox и им подобным).
Используя эти классы трансформаций, можно визуализировать графические данные
под заданным углом, смещать изображения по поверхности, растягивать, сжимать или
поворачивать целевой элемент самыми разными способами.
На заметку! Хотя объекты трансформаций могут применяться повсеместно, вы найдете их
наиболее удобными при работе с анимацией WPF и специальными шаблонами элементов
управления. Как будет показано далее, анимацию WPF можно использовать, чтобы включать для
специального элемента управления визуальные знаки, предназначенные конечным пользователям.
Глава 29. Службы визуализации графики WPF 1119
\
Объекты трансформаций (или целые их множества) могут назначаться целевому
объекту (Button, Path, и т.п.) с помощью двух общих свойств. Свойство LayoutTransform
удобно тем, что трансформация происходит перед визуализацией элементов в
диспетчере компоновки, и потому не влияют на z-упорядочивание (другими словами,
трансформируемые данные изображений не перекрываются).
Свойство RenderTransform, с другой стороны, работает после того, как элементы
оказались в своих контейнерах, и потому вполне возможно, что элементы могут быть
трансформированы таким образом, что могут перекрывать друг друга, в зависимости
от того, как они организованы в контейнере.
Первый взгляд на трансформации
Ниже будет добавлена некоторая логика трансформаций к проекту Rendering
WithShapes. Чтобы увидеть объект трансформации в действии, откройте редактор
Kaxaml (или собственный специальный редактор XML) и определите простой элемент
<StackPanel> в корне <Раде> или <Window>, установив для свойства Orientation
значение Horizontal. Теперь добавьте следующий элемент <Rectangle>, который будет
нарисован под углом в 45 градусов с использованием объекта RotateTransform.
<•-- Элемент Rectangle с трансформацией поворота -->
<Fectangle Height =00" Width =0" Fill ="Red">
<Rectangle.LayoutTransform>
<RotateTransform Angle =5"/>
^-/Rectangle. LayoutTransf orm>
</Rectangle>
Далее элемент <Button> скашивается по поверхности на 20% с помощью
трансформации <SkewTransform>:
<!-- Элемент Button с трансформацией скоса -->
<Button Content ="Click Me1" Width="95" Height=0">
<Button.LayoutTransform>
<SkewTransform AngleX =0" AngleY =0"/>
</Button.LayoutTransform>
</Button>
И для полноты картины ниже приведен элемент <Ellipse>, масштабированный на
20% посредством трансформации ScaleTransform (обратите внимание на значения,
установленные в свойствах Height и Width), а также элемент <TextBox>, к которому
применена групповая трансформация:
<*-- Элемент Ellipse, масштабированный на 20% -->
<Ellipse Fill ="Blue" Width=" Height=">
<Ellipse.LayoutTransform>
<ScaleTransform ScaleX =0" ScaleY =0"/>
</Ellipse.LayoutTransform>
</Ellipse>
<•-- Элемент TextBox, повернутый и скошенный -->
<TextBox Text ="Me Too!" Width=0"' Height=0">
<TextBox.LayoutTransform>
<TransformGroup>
<RotateTransform Angle =5"/>
<SkewTransform AngleX =" AngleY =0"/>
</TransformGroup>
</TextBox.LayoutTransform>
</TextBox>
При применении трансформации не обязательно выполнять какие-либо ручные
вычисления для корректного определения попадания, принятия фокуса ввода и т.п. Графический
1120 Часть VI. Построение настольных пользовательских приложений с помощью WPF
механизм WPF самостоятельно решает такие задачи. Например, на рис. 29.10 можно
видеть, что элемент TextBox по-прежнему реагирует на клавиатурный ввод.
Рис. 29.10. Результат применения трансформаций к графическим элементам
Трансформация данных Canvas
Теперь давайте посмотрим, как включить некоторую логику трансформации в
пример RencleringWithShapes. В дополнение к применению объекта трансформации к
одиночному элементу (Rectangle, TextBox и т.д.), трансформации можно также применять
на уровне диспетчера компоновки, чтобы трансформировать все его внутренние
данные. Например, можно визуализировать всю панель <DockPanel> главного окна,
повернув ее на заданный угол:
<DockPanel LastChilflFill="True">
^DockPanel.LayoutTransform>
<PotateTransform Angle=5"/>
« /DockPanel.LayoutTransform>
</DockPanel>
Это чересчур экстремально для рассматриваемого примера, поэтому давайте
добавим последнее (менее агрессивное) средство, которое позволит пользователю повернуть
весь контейнер Canvas с вложенной в него графикой. Начните с добавления
финального элемента <ToggleButton> к <Тоо1Ваг>, определив его следующим образом:
<ToggleButton Name="flipCanvas" Click="flipCanvas_Click" Content="Flip Canvas!"/>
Внутри обработчика событий Click, который инициируется щелчком на новой
кнопке переключения ToggleButton, создадим объект RotateTransform и подключим
его к объекту Canvas через свойство LayoutTransform. Если ToggleButton не
отмечена, трансформация удаляется путем установки этого же свойства в null.
private void flipCanvas_Click(object sender, RoutedEventArgs e)
{
if (flipCanvas.IsChecked
true)
RotateTransform rotate = new RotateTransform(-180);
canvasDrawingArea.LayoutTransform = rotate;
}
else
{
canvasDrawingArea.LayoutTransform = null;
}
Запустите приложение и добавьте какие-то графические фигуры в область Canvas.
После щелчка на новой кнопке обнаруживается, что данные фигуры выходят за границы
Canvas (рис. 29.11). Причина в том, что не был определен прямоугольник отсечения.
Глава 29. Службы визуализации графики WPF 1121
Рис. 29.11. Фигуры выходят за границы Canvas после трансформации
Исправить это просто. Вместо того чтобы вручную писать сложную логику отсечения,
просто установите свойство ClipToBounds элемента <Canvas> в true, что предотвратит
визуализацию дочерних элементов вне границ родителя. Запустив программу вновь, вы
обнаружите, что теперь графические данные не покидают границ отведенной области.
<Canvas ClipToBounds = "True" . . . >
Последняя небольшая модификация, которую понадобится провести, связана с тем
фактом, что когда вы поворачиваете холст нажатием кнопки переключения, а затем
щелкаете на нем для рисования новой фигуры, точка, где был произведен щелчок, не
является той позицией, куда попадут графические данные. Вместо этого они появятся
под курсором мыши.
Чтобы исправить эту проблему, давайте еще раз взглянем на исходный код
примера, к которому добавляется еще одна переменная-член типа Boolean (isFlipped). Она
обеспечивает применение той же трансформации к ранее нарисованной фигуре перед
выполнением визуализации (через RenderTransform). Ниже показан соответствующий
фрагмент кода.
private void canvasDrawmgArea_MouseLeftButtonDown (object sender,
MouseButtonEventArgs e)
{
Shape shapeToRender = null;
// isFlipped — приватное булевское поле. Его значение
// изменяется при щелчке на кнопке переключения.
if (isFlipped)
{
RotateTransform rotate = new RotateTransform(-180);
shapeToRender.RenderTransform = rotate;
}
// Установить верхнюю левую точку для рисования на холсте.
Canvas .Set Left (shapeToRender, e . Get Posit ion (canvas DrawmgArea) . X) ;
Canvas . SetTop (shapeToRender, e . GetPosition (canvasDrawmgArea) . Y) ;
// Нарисовать фигуру.
сanvasDrawingArea.Children.Add(shapeToRender) ;
На этом рассмотрение фигур (System.Windows.Shapes), кистей и трансформаций
завершено. Прежде чем перейти к теме визуализации графики с помощью рисунков и
геометрий, давайте посмотрим, как инструмент Expression Blend может упростить
работу с примитивными графическими элементами.
1122 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Исходный код. Проект RenderingWithShapes доступен в подкаталоге Chapter 29.
Работа с фигурами в Expression Blend
Хотя в IDE-среде Visual Studio 2010 для работы с фигурами предусмотрено
несколько инструментов, средство Expression Blend предлагает гораздо больше возможностей,
включая редактор графических трансформаций. Чтобы попробовать их, запустите
Expression Blend и создайте новое приложение WPF (назовите проект как вам угодно).
Выбор фигуры для визуализации из палитры инструментов
Палитра инструментов позволяет выбирать инструменты
Ellipse, Rectangle и Line (щелчок и удержание кнопки с
маленьким треугольником справа внизу позволяет открыть все
доступные выборы, как показано на рис. 29.12).
Нарисуйте несколько перекрывающихся фигур. Найдите
область в палитре инструментов Blend, где расположены
инструменты Реп и Pencil (рис. 29.13).
Инструмент Pencil (карандаш) позволяет визуализировать
произвольные многоугольники. Инструмент Реп (перо) больше
подходит для визуализации дуг или прямых линий. Чтобы
рисовать линию с помощью Реп, щелкните на желаемой начальной
точке, а затем — на желаемой конечной точке в визуальном
конструкторе. Имейте в виду, что оба эти средства генерируют
определение Path, свойство Data которого устанавливается с
применением мини-языка описания путей, упомянутого ранее в главе.
Поработайте с этими инструментами и постройте
какую-нибудь графику в визуальном конструкторе. Обратите внимание,
что выбор инструмента Direct Selection на любом Path
позволяет настраивать сегменты соединения (рис. 29.14).
Рис. 29.12
Визуализация базовых
фигур с помощью Blend
Рис. 29.13.
Работа с перьями
и карандашами
Рис. 29.14. Редактирование Path
Глава 29. Службы визуализации графики WPF 1123
Преобразование фигур в пути
Инструменты Ellipse, Rectangle или Line соответствуют в XAML-раз метке
элементам <Ellipse>, <Rectangle> и <Line>. Например:
<Rectangle Stroke="Black" StrokeThickness="l" HorizontalAlignment="Left"
Margin=4,42,0,0" VerticalAlignment="Top" Width=37" Height=5"/>
Щелкните правой кнопкой мыши на фигуре в визуальном конструкторе выберите в
контекстном меню пункт PatlT=>Convert to Path (Путь1^ Преобразовать в путь). В
результате объявление исходной фигуры преобразуется в путь Path, содержащий набор
геометрий, которые представлены с помощью мини-языка описания путей. Вот как
выглядит предыдущий элемент <Rectangle> после преобразования:
<Path Stretch="Fill" Stroke="Elack" StrokeThickness="l" HorizontalAlignment="Left"
Margin=4/ 42, 0, 0" VerTncalAiigiiment=,,Top" Width=37"
Height=5" Data="M0.5,0.5 L136.5,0.5 L136.5,34.5 LO.5,34.5 z"/>
Преобразование одиночных фигур в путь, состоящий из геометрий, требуется редко, но
гораздо чаще его приходится применять для комбинирования множества фигур в путь.
Комбинирование фигур
Выберите две или более фигур в визуальном конструкторе (с помощью операции
щелчка и перетаскивания) и щелкните правой кнопкой мыши на наборе выбранных
элементов. В меню Combine (Комбинировать) доступен набор пунктов, которые
позволяют генерировать объекты Path на основе набора операций выбора и операций
комбинирования (рис. 29.15).
По сути, опции меню Combine представляют собой базовые операции диаграммы Венна.
Опробуйте некоторые из них и затем просмотрите сгенерированную XAML-разметку
(как и в Visual Studio 2010, нажатие <Ctrl+Z> отменяет предыдущую операцию).
Рис. 29.15. Комбинирование фигур для генерации новых путей
Редакторы кистей и трансформаций
Подобно Visual Studio 2010, в Blend поддерживается редактор кистей. После выбора
элемента в визуальном конструкторе (такого как одна из фигур) можно перейти в область
1124 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Brushes (Кисти) окна Properties. В самом верху редактора
кистей находятся все свойства выбранного объекта,
которому может быть назначен тип Brush. Ниже доступны
те же самые базовые средства редактирования, которые
демонстрировались ранее в этой главе. На рис. 29.16
показана одна из возможных конфигураций кисти.
Единственное значительное отличие между
редакторами кистей Blend и Visual Studio 2010 состоит в том,
что в Blend можно удалить градиентный переход,
щелкая на соответствующем ползунке и перетаскивая его за
пределы ленты градиента (буквально в любое место за
пределы ленты градиента; щелчок правой кнопкой здесь
не поддерживается).
Другое замечательное средство редактора кистей
Blend состоит в том, что после выбора элемента,
который использует градиентную кисть, можно нажать
клавишу <G> или щелкнуть на значке Gradient Tool
(Инструмент градиента) в панели инструментов и затем
изменить начало градиента (рис. 29.17).
Наконец, в Blend также поддерживается
интегрированный редактор трансформаций. Выберите фигуру в визуальном конструкторе
(разумеется, редактор трансформаций также работает с любым элементом, выбранным в
окне Objects and Timeline (Объекты и шкала времени)). Теперь в окне Properties найдите
панель Transform (Трансформация), как показано на рис. 29.18.
Экспериментируя с редактором трансформаций, уделите некоторое время изучению
результирующей XAML-разметки. Кроме того, обратите внимание, что многие
трансформации могут быть применены непосредственно в визуальном конструкторе —
наведением курсора мыши на узел изменения размеров выбранного объекта. Например,
поместив курсор мыши рядом с узлом на боковой стороне выбранного элемента, можно
применить скашивание. (Однако проще и предпочтительней для его использовать
редактор трансформаций.)
На заметку! Вспомните, что членами пространства имен System.Windows.Shapes являются
UIElement и наследники, имеющие множество событий, на которые можно реагировать. Это
означает возможность обработки событий в рисунках с использованием Blend, как это
делается для типичного элемента управления (см. главу 29).
Рис. 29.16. Редактор кистей
Blend
Рис. 29.17. Изменение начала градиента
Рис. 29.18. Редактор
трансформаций Blend
Глава 29. Службы визуализации графики WPF 1125
На этом завершается обзор служб графической визуализации WPF и
ознакомительный экскурс в использование Blend для генерации простых графических данных. Теперь
давайте рассмотрим второй способ визуализации графических данных — применение
рисунков и геометрий.
Визуализация графических данных
с использованием рисунков и геометрий
Хотя типы Shape позволяют генерировать любые интерактивные двумерные
поверхности, из-за богатой цепочки наследования они потребляют довольно много памяти.
И хотя класс Path может помочь снизить накладные расходы за счет использования
включенной геометрии (вместо огромной коллекции других фигур), в WPF предлагается
развитый API-интерфейс рисования и геометрии, который позволяет визуализировать
даже более легковесные двухмерные векторные изображения.
Входной точкой в этот API-интерфейс является абстрактный класс System.Windows.
Media.Drawing (из сборки PresentationCore.dll), который сам по себе всего лишь
определяет ограничивающий прямоугольник для хранения визуализаций. Обратите
внимание, что на рис. 29.19 цепочка наследования класса Drawing существенно проще, чем
у Shape, учитывая, что в ней отсутствуют как UIElement, так и FrameworkElement.
л CJ BaseTyp«
л -fy Animatable
л -*f$ Freezeble
л **4 DependencyObject
л *\% DispatcherObject
% Object
^ lAnimatable
V CloneO
ч» CloneCurrentValueO
23* Bounds
public abstract class Drawing : System.Windows.Media.Animation,Animatable
Member of Syytem.Windows.Media
Summary:
Abstract class that describes a 2-D drawing. This class cannot be inherited by your
code.
Рис. 29.19. Класс Drawing более легковесный, чем Shape
WPF предоставляет различные классы, которые расширят Drawing; каждый из них
предлагает определенный способ рисования содержимого, как описано в табл. 29.6.
Таблица 29.6. Классы, унаследованные от Drawing
Класс
Назначение
DrawingGrdoup
GeometryDrawing
GlyphRunDrawing
ImageDrawing
VideoDrawing
Используется для комбинирования коллекции отдельных
объектов-наследников Drawing в единую составную визуализацию
Используется для визуализации двухмерных фигур в очень
легковесной манере
Используется для визуализации текстовых данных с
применением служб графической визуализации WPF
Используется для визуализации файла изображения, или набора
геометрий, внутри ограничивающего прямоугольника
Служит для воспроизведения аудио- или видео-файла. Этот тип
может полностью использоваться только в процедурном коде.
Для воспроизведения видео в XAML-разметке лучше подойдет
класс MediaPlayer
1126 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Будучи более легковесными, типы-наследники Drawing не обладают
встроенной возможностью обработки событий, поскольку они не являются UIElement или
FrameworkElement (хотя допускают программную реализацию логики проверки
попадания). Тем не менее, они поддаются анимации (средства анимации WPF
рассматриваются в главе 30).
Другое ключевое отличие между типами-наследниками Drawing и Shape состоит в
том, что типы-наследники Drawing не умеют визуализировать себя, поскольку не
наследуются от UIElement! Вместо этого производные типы должны помещаться в
какой-то хост-объект (а именно — Drawinglmage, DrawingBrush или DrawingVisual) для
отображения содержимого.
Объект Drawinglmage позволяет помещать рисунки и геометрии внутрь элемента
управления Image из WPF, который обычно используется для отображения данных из
внешнего файла. DrawingBrush позволяет строить кисть на основе рисунков и их гео-
. метрий, чтобы установить свойство, требующее кисти. Наконец, DrawingVisual
применяется только на визуальном уровне графической визуализации, полностью
управляемом из кода С#.
Хотя использовать рисунки немного сложнее, чем простые фигуры, отделение
графической композиции от графической визуализации делает типы-наследники Drawing
намного легче типов-наследников Shape, сохраняя их ключевые службы.
Построение кисти DrawingBrush
с использованием объектов Geometry
Ранее в этой главе элемент Path заполнялся группой геометрий следующим образом:
<Path Fill = "Orange" Stroke = "Blue" StrokeThickness = ">
<Path.Data>
<GeometryGroup>
<EllipseGeometry Center = 5,70"
RadiusX = 0" RadiusY = 0" />
<RectangleGeometry Rect = 5,55 100 30" />
<LineGeometry StartPoint=,0" EndPoint=0,30" />
<LineGeometry StartPoint=0,30" EndPoint=,30" />
</GeometryGroup>
</Path.Data>
</Path>
В этом случае получается интерактивность Path при чрезвычайной легковесности,
присущей геометриям. Однако если необходимо визуализировать аналогичный вывод,
но никакая (готовая) интерактивность не нужна, можете поместить тот же элемент
<GeometryGroup> внутрь DrawingBrush, как показано ниже:
<DrawingBrush>
<DrawingBrush.Drawing>
<GeometryDrawing>
<GeometryDrawing.Geometry>
<GeometryGroup>
<EllipseGeometry Center = 5,70"
RadiusX = 0" RadiusY = 0" />
<RectangleGeometry Rect = 5,55 100 30" />
<LineGeometry StartPoint=,0" EndPoint=0,30" />
<LineGeometry StartPoint=0,30" EndPoint=,30" />
</GeometryGroup>
</GeometryDrawing.Geometry>
<!-- Специальное перо для рисования границ -->
<GeometryDrawing.Pen>
Глава 29. Службы визуализации графики WPF 1127
<Pen Brush="Blue" Thickness="/>
</GeometryDrawing.Pen>
<!-- Специальная кисть для заполнения внутренней области -->
<GeometryDrawing.Brush>
<SolidColorBrush Color="Orange"/>
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingBrush.Drauing>
</DrawingBrush>
При помещении группы геометрий в Draw in gB rush также нужно установить
объект Реп, используемый для рисования границ, поскольку более не наследуется свойство
Stroke от базового класса Shape. Здесь был создан элемент <Реп> с теми же
настройками, которые использовались в значениях Stroke и StrokeThickness из предыдущего
примера Path.
Более того, поскольку свойство Fill класса Shape больше не наследуется, нужно
также применять синтаксис "элемент-свойство" для определения объекта кисти,
предназначенного элементу <T>rawingGeometry^>; в данном случае это кисть сплошного
оранжевого цвета, как в предыдущих установках Path.
Рисование с помощью DrawingBrush
Теперь кисть DrawingBiush можно использовать для установки значения
любого свойства, требующего объекта кисти. Например, подготовив следующую разметку
в Kaxaml, с помощью синтаксиса "элемент-свойство" можно рисовать с применением
изображения по всей поверхности Page:
<Page
xmlns="http://schemas.microsoft.com/winfx/2 00 6/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Page.Background>
<!-- Та же кисть DrawingBrush, что и раньше -->
<DrawingBrush>
</DrawingBrush>
</Page.Background>
</Page>
Или же кисть <DrawingBrush> можно использовать для установки другого
совместимого с кистью свойства, такого как свойство Background элемента Button:
<Page
xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Button Height=00" Width=00">
<Button.Background>
<!-- Та же кисть DrawingBrush, что и раньше -->
<DrawingBrush>
</DrawingBrush>
</Button.Background>
</Button>
</Page>
Независимо от того, какое совместимое с кистью свойство будет установлено в
специальный объект <DrawingBrush>, визуализировать двухмерное графическое
изображение получится с намного меньшими накладными расходами, чем в случае фигур.
1128 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Включение типов Drawing в Drawinglmage
Тип Drawinglmage позволяет включать рисованную геометрию в элемент
управления <Image> из WPF. Рассмотрим следующую разметку:
<Раде
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Image Height=00" Width=00">
<Image.Source>
<DrawingImage>
<DrawingImage. Drawing>
<GeometryDrawing>
<GeometryDrawing. Geometry>
<GeometryGroup>
<EllipseGeometry Center = 5,70"
RadiusX = 0" RadiusY = 0" />
<RectangleGeometry Rect = 5,55 100 30" />
<LineGeometry StartPoint=,0" EndPoint=0,30" />
<LineGeometry StartPoint=0,30" EndPoint=,30" />
</GeometryGroup>
</GeometryDrawing.Geometry>
<!-- Специальное перо для рисования границ -->
<GeometryDrawing.Pen>
<Pen Brush="Blue" Thickness="/>
</GeometryDrawing.Pen>
<•-- Специальная кисть для заполнения внутренней области -->
<GeometrуDrawing.Brushy
<SolidColorBrush Color="Orange"/>
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
<-/Page>
В этом случае элемент <GeometryDrawing> помещен в <DrawingImage>, а не в
<DrawingBrush>. Используя этот <DrawingImage>, можно установить свойство Source
элемента управления Image.
Генерация сложной векторной графики
с использованием Expression Design
Честно говоря, ручное написание кода XAML, представляющего сложный набор
геометрий, сегментов, фигур и путей, может оказаться буквально кошмаром, и никто так
не поступает. Как уже было показано, Expression Blend содержит несколько средств IDE-
среды, которые позволяют работать с базовой графикой; однако, когда нужно
генерировать профессиональную, полноценную векторную графику, то инструменту Expression
Design нет равных.
На заметку! Бесплатная пробная версия Expression Design доступна для загрузки'на официальном
веб-сайте Microsoft по адресу http://www.microsoft.com/expression.
Инструмент Expression Design обладает средствами, подобными тем, что можно
найти в профессиональных инструментах для графического дизайна, таких как Adobe
Глава 29. Службы визуализации графики WPF 1129
Photoshop или Adobe Illustrator. В руках талантливого художника Expression Design
позволит создавать очень сложные двухмерные и трехмерные изображения, которые
могут быть экспортированы в самые разнообразные файловые форматы, включая XAML.
Способность экспортировать графическое изображение в XAML очень полезна,
поскольку это позволяет включать разметку в приложения, назначать объектам имена и
манипулировать ими в коде. Например, художник может с помощью Expression Design
нарисовать изображение трехмерного куба. После его экспорта в XAML программист
может добавить атрибут x:Name к элементам, с которыми нужно взаимодействовать, и
получить доступ к сгенерированному объекту из кода С#.
Экспорт документа Expression Design в XAML
Полностью все возможности инструмента Expression Design здесь описываться не
будут. Вместо этого будут производиться манипуляции некоторыми стандартными
примерами документов, поставляемыми с этим продуктом.
Запустите Expression Design и выберите пункт меню Help1^Samples (Справкам
Примеры). В открывшемся диалоговом окне дважды щелкните на одном из примеров
рисунков. Для данного примера это будет файл bearpaper. design, входящий в состав
Expression Design 3.0.
Выберите пункт меню File^Export (Файл^Экспорт). Откроется диалоговое окно
Export (Экспорт), которое позволяет сохранить данные выбранного изображения в виде
одного из популярных графических форматов, включая .png, .jpeg, .gif, .tif, .bmp
и .wdp (HD Photo). Если Expression Design используется просто для создания
профессиональных статических изображений, эти форматы файлов вполне подойдут Однако
если планируется добавить интерактивность к векторной графике, необходимо
сохранить файл с описанием XAML.
Выберите вариант XAML WPF Canvas (Холст XAML WPF) в раскрывающемся списке
Format (Формат). На рис. 29.20 обратите внимание на другие опции, включающие
возможность назначения каждому элементу имени по умолчанию (через атрибут x:Name) и
сохранения текстовых данных в виде объекта Path. Оставьте установки по умолчанию
и щелкните на кнопке Export All (Экспортировать все).
Рис. 29.20. Файлы Expression Design можно экспортировать как XAML-раз метку
1130 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Сгенерированную XAML-разметку объекта Canvas теперь можно скопировать в
Visual Studio 2010, Expression Blend либо в область <Page> или <Window> внутри
редактора Kaxaml (или в специальном редакторе XAML, который был создан в главе 27).
В сгенерированной XAML-разметке представлены объекты Path в формате по
умолчанию. Здесь можно создать обработчики событий в частях изображения, которым
необходимо манипулировать, написав соответствующий код С#. Например, можно
импортировать данные картинки с изображением плюшевого мишки в новое WPF-приложение
и добавить обработчик события MouseDown для одного из объектов-глаз. В обработчике
события попробуйте применить объект трансформации или измените цвет.
На заметку! Инструмент Expression Design больше в этой книге применяться не будет.
Дополнительную информацию по работе в нем можно получить из его встроенной справочной
системы, которая вызывается нажатием <F1>.
Визуализация графических данных
с использованием визуального уровня
Последний вариант визуализации графических данных с помощью WPF называется
визуальным уровнем (visual layer). Как уже упоминалось, доступ к этому уровню
возможен только из кода (он не поддерживает XAML). Хотя для подавляющего большинства
WPF-приложений вполне достаточно фигур, рисунков и геометрий, визуальный уровень
обеспечивает самый быстрый способ визуализации крупного объема графических
данных. Как ни странно, он также может быть полезен, когда необходимо визуализировать
единственное изображение на очень большой площади. Например, если задача состоит
в заполнении фона окна простым статическим изображением, то визуальный уровень
оказывается самым быстрым способом сделать это. Кроме того, он удобен, когда
требуется очень быстро менять фон окна в зависимости от ввода пользователя или еще
чего-нибудь.
Давайте построим небольшой пример программы, иллюстрирующей основы
использования визуального уровня.
Базовый класс Visual и производные дочерние классы
Абстрактный класс System.Windows.Media.Visual предлагает минимальный набор
служб (визуализацию, проверку попадания, трансформации) для визуализации
графики, но не предусматривает поддержки дополнительных невизуальных служб, что может
привести к разбуханию кода (события ввода, службы компоновки, стили и привязка
данных). Обратите внимание на простую цепочку наследования для типа Visual,
показанную на рис. 29.21.
I Object Browser
Browse- My Solution
I <Search>
"if VideoDrawing
d :jj| Base Types
d -t$ DependencyObject
d 1$ DispatcherObject
^Object
1> 'Щ VtsualBrush
r> % VisualCollection
V AddVisualChild(Sy5tem.Windows.Media.Visua!)
♦ FindCommonVisualAncfcstcr^Syitem.V.'ii-.-iows.DeperdencyObject)
V GetyisualChitd(int)
public abstract class Visual: System. Winduws.D<>pandcrKyObj»Ct
Member of Syrt»m,Wir>drws.M«dia
I Summary:
Provides rendering support in WPF, which includes hrt testing, coordinate
transformation and bounding box calculations.
Рис. 29.21. Класс Visual предоставляет базовую проверку попадания,
трансформации координат и вычисления ограничивающих прямоугольников
Глава 29. Службы визуализации графики WPF 1131
Учитывая, что Visual — это абстрактный базовый класс, для выполнения
действительных операций визуализации должен использоваться один из его производных
классов. В WPF определено несколько подклассов Visual, включая DrawingVisual,
Viewport3DVisual и ContainerVisual.
В рассматриваемом ниже примере внимание сосредоточено на DrawingVisual —
легковесном классе рисования, который используется для визуализации фигур,
изображений или текста.
Первый взгляд на класс DrawingVisual
Чтобы визуализировать данные на поверхности с помощью класса DrawingVisual,
потребуется выполнить следующие базовые шаги:
1. Получить объект DrawingContext от DrawingVisual.
2. Использовать DrawingContext для визуализации графических данных.
Эти два шага представляют абсолютный минимум того, что нужно предпринять для
визуализации некоторых данных на поверхности. Однако если необходимо, чтобы
визуализируемые графические данные сами отвечали за определение проверки
попадания (что может быть важно для взаимодействия с пользователем), тогда понадобится
выполнить следующие дополнительные шаги:
1. Обновить логическое и визуальное деревья, поддерживаемые контейнером, на
поверхности которого производится рисувание.
2. Переопределить два виртуальных метода из класса FrameworkElement, позволив
контейнеру получать созданные визуальные данные.
Давайте немного задержимся на последних двух шагах. Чтобы посмотреть, как
использовать класс DrawingVisual для визуализации двухмерных данных, создайте в
Visual Studio новое приложение WPF по имени RenderingWithVisuals. Наша первая
цель — применение DrawingVisual для динамического присваивания данных элементу
управления Image из WPF. Начните со следующего обновления XAML-разметки окна:
<Window x:Class="RenderingWithVisuals.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/present at ion"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title=" Fun with the Visual Layer" Height=50" Width=25"
Loaded="Window _ Loaded" WindowStartupLocation="CenterScreen">
<StackPanel Background="AliceBlue" Name="myStackPanel">
<Image Name="mylmage" Height="80"/>
</StackPanel>
</Window>
Обратите внимание, что элемент управления <Image> пока не имеет значения для
Source, поскольку оно будет присвоено во время выполнения. Кроме того,
предусмотрен обработчик события Loaded окна, который выполняет работу по построению
графических данных в памяти, используя объект Draw in gB rush. Ниде показана
реализация обработчика события Loaded:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
const int TextFontSize = 30;
// Создать объект System.Windows.Media.FormattedText.
FormattedText text = new FormattedText("Hello Visual Layer1",
new System.Globalization.Culturelnfo("en-us"),
FlowDirection.LeftToRight,
new Typeface(this.FontFamily, FontStyles.Italic,
FontWeights.DemiBold, FontStretches.UltraExpanded),
1132 Часть VI. Построение настольных пользовательских приложений с помощью WPF
TextFontSize,
Brushes.Green);
// Создать DrawingVisual и получить DrawingContext.
DrawingVisual drawingVisual = new DrawingVisual() ;
using(DrawingContext drawingContext = drawingVisual.RenderOpen() )
{
// Вызвать тобой из методов DrawingContext для визуализации данных.
drawingContext.DrawRoundedRectangle(Brushes.Yellow, new Pen(Brushes.Black, 5),
newRectE, 5, 450, 100), 20, 20);
drawingContext.DrawText(text, new PointB0, 20));
}
// Динамически создать битовую карту, используя данные из DrawingVisual.
RenderTargetBitmap bmp = new RenderTargetBitmapE00, 100, 100, 90,
PixelFormats.Pbgra32);
bmp.Render(drawingVisual) ;
// Установить источник для элемента управления Image.
mylmage.Source = bmp;
}
В этом коде представлен ряд новых классов WPF, которые кратко описываются ниже
(более подробные сведения по ним можно получить в документации .NET Framework 4.0
SDK). Метод начинается с создания нового объекта FormattedText, представляющего
текстовую часть изображения в памяти, которое мы конструируем. Как видите,
конструктор позволяет указывать многочисленные атрибуты, вроде размера шрифта,
семейства шрифтов, цвета переднего плана и самого текста.
Затем получается необходимый объект DrawingContext через вызов RenderOpen()
на экземпляре DrawingVisual. Здесь в DrawingVisual визуализирован цветной, со
скругленными углами прямоугольник, за которым следует форматированный текст.
В обоих случаях графические данные помещаются в DrawingVisual с использованием
жестко закодированных значений, что не слишком хорошая идея для реального
приложения, но вполне сойдет для простого теста.
На заметку! Ознакомьтесь с описанием класса DrawingContext в документации .NET Framework
4.0 SDK, чтобы просмотреть все члены, связанные с визуализацией. Если вы работали в
прошлом с объектом Graphics из Windows Forms, то DrawingContext должен выглядеть очень
похожим.
Последние несколько операторов отображают DrawingVisual на объект
RenderTargetBitmap, который является членом пространства имен System.Windows.
Media.Imaging. Этот класс принимает визуальный объект и трансформирует его в
находящееся в памяти битовое изображение. После этого устанавливается свойство
Source элемента управления Image и получается вывод, показанный на рис. 29.22.
f Fun with the Visual Layer
!
r
Hello Visual Layer!
1:«=нвЁиайГ
Рис. 29.22. Использование визуального уровня для
отображения находящейся в памяти битовой карты
Глава 29. Службы визуализации графики WPF 1133
На заметку! Пространство имен System.Windows.Media.Imaging содержит множество
дополнительных классов кодирования, которые позволяют сохранять находящийся в памяти
объект RenderTargetBitmap в физический файл в различных форматах Дополнительную
информацию ищите в JpegBitmapEncoder и связанных с ним классах.
Визуализация графических данных
в специальном диспетчере компоновки
Хотя использование DrawingVisual для рисования в фоне элемента управления
WPF представляет интерес, наверное, чаще придется строить специальный диспетчер
компоновки (Grid, StackPanel, Canvas и т.п.), внутри которого для визуализации
содержимого применяется визуальный уровень. После создания такой специальный
диспетчер компоновки можете включать в нормальный элемент Window (или Page, или
UserControl). В таком случае часть пользовательского интерфейса будет
обрабатываться высоко оптимизированным агентом визуализации, а для визуализации некритичных
аспектов включающего элемента Window будут использоваться фигуры и рисунки.
Если дополнительная функциональность, предлагаемая выделенным диспетчером
компоновки, не требуется, можно просто расширить FrameworkElement, который уже
имеет необходимую инфраструктуру, позволяющую хранить внутри также и
визуальные элементы. Давайте рассмотрим пример. Вставьте в проект новый класс по имени
CustomVisualFrameworkElement. Унаследуйте его от FrameworkElement и
импортируйте пространства имен System.Windows, System.Windows.Input и System.Windows.
Media. Этот класс будет поддерживать переменную-член типа VisualCollection,
содержащую два фиксированных объекта DrawingVisual (конечно, можно было бы
добавить члены в эту коллекцию, но простоты это не делается). Модифицируйте класс
CustomVisualFrameworkElement следующим образом:
class CustomVisualFrameworkElement : FrameworkElement
{
// Коллекция всех визуальных объектов.
VisualCollection theVisuals;
public CustomVisualFrameworkElement ()
{
// Заполнить коллекцию VisualCollection несколькими объектами DrawingVisual.
// Аргумент конструктора представляет владельца визуальных объектов.
theVisuals = new VisualCollection (this);
theVisuals.Add(AddRect());
theVisuals.Add(AddCircle ());
}
private Visual AddCircle ()
{
DrawingVisual drawingVisual = new DrawingVisual ();
// Получить DrawingContext для создания нового содержимого.
using (DrawingContext drawingContext = drawingVisual.RenderOpen ())
{
// Создать круг и нарисовать его в DrawingContext.
Rect rect = new Rect (new PointA60, 100), new SizeC20, 80));
drawingContext.DrawEllipse(Brushes.DarkBlue, null, newPointG0, 90), 40, 50);
}
return drawingVisual;
}
private Visual AddRect ()
{
DrawingVisual drawingVisual = new DrawingVisual () ;
1134 Часть VI. Построение настольных пользовательских приложений с помощью WPF
using (DrawingContext drawingContext = drawingVisual.RenderOpen())
{
Rect rect = new Rect(new Point A60, 100), new Size C20, 80));
drawingContext.DrawRectangle(Brushes.Tomato, null, rect);
}
return drawingVisual;
Перед тем как можно будет использовать специальный элемент FrameworkElement
в Window, потребуется переопределить два виртуальных метода, упомянутых ранее,
которые оба вызываются внутренне WPF во время процесса визуализации. Метод
GetVisualChild() возвращает дочерний элемент по указанному индексу из
коллекции дочерних элементов. Свойство VisualChildrenCount возвращает количество
визуальных дочерних элементов внутри этой визуальной коллекции. Оба метода
легко реализовать, поскольку реальную работу можно делегировать переменной-члену
VisualCollection:
protected override int VisualChildrenCount
{
get { return theVisuals.Count; }
}
protected override Visual GetVisualChild(int index)
{
// Значение должно быть больше нуля, поэтому на всякий случай проверить.
if (index < 0 | | index >= theVisuals. Count)
{
throw new ArgumentOutOfRangeException();
}
return theVisuals[index];
}
Теперь есть достаточно функциональности, чтобы протестировать
специальный класс. Обновите описание XAML элемента Window, добавив один объект
CustomVisualFrameworkElement в существующий контейнер StackPanel. Это
потребует создания специального пространства имен XML, которое отображается на
пространство имен .NET (см. главу 28).
<Window x:Class="RenderingWithVisuals.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:RenderingWithVisuals"
Title="Fun with the Visual Layer" Height=50" Width=25"
Loaded="Window_Loaded" WindowstartupLocation="CenterScreen">
<StackPanel Background="AliceBlue" Name="myStackPanel">
<Image Name="mylmage" Height="80"/>
<custom:CustomVisualFrameworkElement/>
</StackPanel>
</Window>
Результат выполнения программы показан на рис. 29.23.
Реагирование на операции проверки попадания
Поскольку DrawingVisual не поддерживает инфраструктуру UIElement или
FrameworkElement, необходимо программно добавить возможность вычисления
операций проверки попадания. К счастью, на визуальном уровне сделать это очень просто,
благодаря концепции логического и визуального деревьев.
■ Fun with the Visual Layer
Глава 29. Службы визуализации графики WPF 1135
Hello Visual Layer!
•
Рис. 29.23. Использование визуального уровня для визуализации
данных в специальном элементе FrameworkElement
Оказывается, что в результате написания блока XAML, по сути, строится логическое
дерево элементов. Однако за каждым логическим деревом стоит намного более развитое
описание, известное как визуальное дерево, которое содержит низкоуровневые
инструкции визуализации.
Упомянутые деревья подробно рассматриваются в главе 32, а сейчас достаточно
знать, что пока специальные визуальные объекты не будут зарегистрированы в этих
структурах данных, операции проверки попадания выполнять невозможно. К счастью,
контейнер VisualCollection делает это автоматически (что объясняет, почему
необходимо передавать ссылку на специальный элемент FrameworkElement в качестве
аргумента конструктора).
Обновите класс CustomVisualFrameworkElement для обработки события MouseDown
в конструкторе класса, используя стандартный синтаксис С#:
this.MouseDown += MyVisualHost_MouseDown;
Реализация этого обработчика вызовет метод VisualTreeHelper.HitTest(),
чтобы проверить нахождение курсора мыши в границах одного из отображенных
визуальных объектов. Для этого в качестве параметра метода HitTestO указывается
делегат HitTestResultCallback, который выполнит вычисления. Добавьте в класс
CustomVisualFrameworkElement следующие методы:
void MyVisualHost_MouseDown(object sender, MouseButtonEventArgs e)
{
// Найти место, где пользователь выполнил щелчок.
Point pt = e.GetPosition((UIElement)sender);
// Вызвать вспомогательную функцию через делегат.
VisualTreeHelper.HitTest(this, null,
new HitTestResultCallback(myCallback), new PointHitTestParameters (pt) ) ;
}
public HitTestResultBehavior myCallback(HitTestResult result)
{
// Если был совершен щелчок на визуальном объекте,
// переключиться между скошенной и нормальной визуализацией.
if (result.VisualHit.GetType () == typeof(DrawingVisual))
{
if (((DrawingVisual)result.VisualHit).Transform == null)
((DrawingVisual)result.VisualHit).Transform = new SkewTransformG, 7),
1136 Часть VI. Построение настольных пользовательских приложений с помощью WPF
else
{
((DrawingVisual)result.VisualHit).Transform = null;
}
}
// Остановить углубление в визуальное дерево методом HitTest().
return HitTestResultBehavior.Stop;
}
Запустите программу. Теперь должна появиться возможность выполнить щелчок на
любом из отображенных визуальных объектов и увидеть трансформацию в действии.
Хотя это очень простой пример работы с визуальным уровнем WPF, помните, что можно
использовать те же самые кисти, трансформации, перья и диспетчеры компоновки, что
и при работе с XAML. Это значит, что вы уже знаете достаточно много о работе классов-
наследников Visual.
Исходный код. Проект RenderingWithVisuals доступен в подкаталоге Chapter 29.
На этом исследование служб графической визуализации Windows Presentation
Foundation завершено. В действительности мы лишь слегка коснулись обширной
области графических средств WPF. Дальнейшее изучение фигур, рисунков, кистей,
трансформаций и визуальных объектов продолжайте самостоятельно (некоторые
дополнительные детали на эту тему будут приводиться в оставшихся главах, посвященных WPF).
Резюме
Поскольку Windows Presentation Foundation является чрезвычайно насыщенным
графикой API-интерфейсом для построения пользовательских интерфейсов, не
удивительно, что существует несколько способов визуализации графического вывода. В начале
главы были рассмотрены все три способа визуализации (фигуры, рисунки и визуальные
объекты) вместе с разнообразными примитивами визуализации, такими как кисти,
перья и трансформации.
Вспомните, что когда нужно построить интерактивную двухмерную визуализацию,
фигуры делают этот процесс очень простым. Статические, не интерактивные
изображения могут визуализироваться более оптимально с использованием рисунков и
геометрии. Визуальный уровень (доступный только из кода) обеспечивает максимальную
степень контроля и высокую производительность.
ГЛАВА 30
Ресурсы, анимация
и стили WPF
В этой главе будут представлены три важных и взаимосвязанных темы, которые
позволят углубить понимание API-интерфейса Windows Presentation Foundation
(WPF). Первая тема — роль логических ресурсов. Как вы увидите, система логических
ресурсов (также называемых объектными ресурсами) — это способ ссылаться на часто
используемые объекты внутри WPF-приложения. Хотя логические ресурсы часто
пишутся на XAML, они могут быть определены и в процедурном коде.
Затем будет показано, как определять, выполнять и управлять последовательностью
анимации. Вопреки тому, что можно было подумать, применение анимации WPF не
ограничено видеоиграми или мультимедиа-приложениями. В API-интерфейсе WPF
анимация может использоваться, например, для подсветки кнопки, когда она получает фокус,
или увеличения размера выбранной строки в DataGrid. Освоение анимации —
ключевой аспект построения специальных шаблонов элементов управления (о чем пойдет
речь в главе 31).
Завершается глава рассмотрением роли стилей WPF. Подобно веб-странице, которая
использует стили CSS или механизм тем ASP.NET, приложение WPF может определять
общий вид и поведение набора элементов управления. Эти стили определяются в
разметке и сохраняются в качестве объектных ресурсов для последующего использования,
а затем динамически применяются во время выполнения.
Система ресурсов WPF
Нашей первой целью является исследование способов встраивания и доступа к
ресурсам приложения. В WPF поддерживаются два вида ресурсов. Первый — это двоичный
ресурс, который обычно включает элементы, обычно рассматриваемые большинством
программистов как ресурс в его традиционном смысле (встроенные файлы
изображений или звуковых клипов, значки, используемые приложением, и т.д.).
Второй вид, называемый объектным или логическим ресурсом, представляет
именованный объект .NET, который может быть упакован и многократно использован по
всему приложению. Хотя любой объект .NET может быть упакован в виде объектного
ресурса, логические ресурсы особенно полезны при работе с графическими данными
любого рода, учитывая, что можно определять часто используемые графические
примитивы (кисти, перья, анимации и т.п.) и ссылаться на них по мере необходимости.
1138 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Работа с двоичными ресурсами
Прежде чем приступить к теме объектных ресурсов, давайте быстро посмотрим, как
упаковывать такие двоичные ресурсы, как значки и файлы изображений (например,
логотипы компаний или изображения для анимации), в приложения. Создайте в Visual
Studio 2010 новое WPF-приложение по имени BinaryResourcesApp. Обновите разметку
начального окна для использования DockPanel в качестве корня компоновки:
<Window х:Class="BinaryResourcesApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Fun with Binary Resources" Height=00" Width=,,649">
<DockPanel LastChildFill=,,True">
</DockPanel>
</Window>
Теперь предположим, что приложение должно отображать один из трех файлов
изображений внутри части окна, в зависимости от пользовательского ввода. Элемент
управления Image из WPF может применяться не только для отображения типичных файлов
изображений (*.bmp, *.gif, *.ico, *.jpg, *.png, *.wdp и *.tiff), но также данных из
Drawinglmage (как было показано в главе 29). Постройте пользовательский интерфейс
окна на основе диспетчера компоновки DockPanel с простой панелью инструментов,
включающей кнопки Next (Вперед) и Previous (Назад). Ниже панели инструментов
можно расположить элемент управления Image, который пока не имеет значения,
установленного в свойстве Source, так как это будет делаться в коде:
<Window х:Class="BinaryResourcesApp.MainWindow"
xmlns = "http : //schemas .microsoft. com/wmfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Fun with Binary Resources" Height=00" Width=49">
<DockPanel LastChildFill="True">
<ToolBar Height=0" Name="picturePickerToolbar11 DockPanel. Dock="Top">
<Button x:Name="btnPreviousImage11 Height=0" Width=0011 BorderBrush="Black11
Margin=11 Content="Previous" Click=llbtnPreviousImage_Click"/>
<Button x:Name="btnNextImage" Height=M40M Width=00" BorderBrush=MBlackM
Margin=M5M Content=MNextM Click="btnNextImage_Click"/>
</ToolBar>
<!-- Этот элемент Image будет заполняться в коде -->
<Border BorderThickness=" BorderBrush="Green">
<Image x:Name="imageHolder" Stretch="Fill" />
</Border>
</DockPanel>
</Window>
Обратите внимание, что событие Click обрабатывается для каждого объекта Button.
Предполагая, что для обработки этих событий использовалась IDE-среда, в файле кода
С# будут определены два пустых метода. Каким образом реализовать обработчики
событий Click для выполнения цикла по графическим данным? И что более важно:
графические данные будут храниться на пользовательском жестком диске или же будут
встроены в скомпилированную сборку? Рассмотрим оба варианта.
Включение в проект файлов свободных ресурсов
Предположим, что изображения должны поставляться в виде набора файлов внутри
подкаталога пути установки приложения. В окне Solution Explorer среды Visual Studio
щелкните правой кнопкой мыши на узле проекта и выберите в контекстном меню пункт
Add^New Folder (Добавить1^Новая папка) для создания подкаталога под названием
Images.
Глава 30. Ресурсы, анимация и стили WPF 1139
Solution Explorer » □ X
I ]J Solution 'BinaryResourcesApp A project)
л »2Э BinaryResourcesApp
j|| Properties
ai References
л *>\J} Images
jy Deer.jpg
■i Dogs.jpg
\-M Welcome.jpg
i> gwj App.xaml
*'; MarnWindowjcaml
1 Q?5 Solution Explorer К
Properties
:. )i
л
Build Action
Copy to Output Directory
Custom Tool
Custom Tool Namespace
Advanced
Content
Copy always
Рис. 30.1. Новый подкаталог
в проекте WPF содержит данные
изображений
Рис. 30.2. Конфигурирование данных изображений для
копирования в выходной каталог
Solution Explorer
: Solution 'BinaryResourcesApp' A project)
л .jl BinaryResourcesApp
Ш1 Properties
''Л References
s ; _;> bin
л _. Debug
л ., Images
3 Deer.jpg
Теперь щелкните правой кнопкой мыши на папке Images и выберите в контекстном
меню пункт Add1^Existing Item (Добавить1^Существующий элемент), чтобы скопировать
в нее файлы изображений. В исходном коде для этого проекта доступны три файла
изображений с именами Welcome.jpg, Dogs.jpg и Deep.jpg, которые можно включить в
проект, или же можно просто добавить три файла изображений по своему выбору. На
рис. 30.1 показан текущий вид окна Solution Explorer.
Конфигурирование свободных ресурсов
Если необходимо, чтобы IDE-среда Visual Studio
2010 копировала содержимое проекта в выходной
каталог, потребуется скорректировать несколько
настроек, используя окно Properties (Свойства).
Чтобы обеспечить копирование содержимого
папки Images в папку \bm\Debug, начните с выбора
всех изображений в Solution Explorer. Когда все
изображения выбраны, в окне Properties установите
свойство Build Action (Действие сборки) в Content,
а свойство Copy Output Directory (Копировать в
выходной каталог) в Copy always (рис. 30.2).
Перекомпилировав программу, щелкните на
кнопке Show all Files (Показать все файлы) в Solution
Explorer и просмотрите скопированную папку
Images в каталоге \bin\Debug (может
потребоваться также щелкнуть на кнопке обновления). Результат
показан на рис. 30.3.
Программная загрузка изображения
В WPF доступен класс по имени Bitmaplmage, входящий в пространство имен
System.Windows.Media.Imaging. Этот класс позволяет загружать данные из файла
изображения, местоположение которого представлено объектом System.Uri. Обработав
событие Loaded окна, можно заполнить список List<T> элементов Bitmaplmages
следующим образом:
public partial class MainWindou : Window
] Welcome.jpg
j BinaryResourcesApp.exe
ij BinaryResourcesApp.pdb
J BinaryResourcesApp.vshost.exe
f> LU Release
bflr Images
iJJ Deer.jpg
£ Dogs.jpg
pjpj Welcome.jpg
Г~! obj
»*j App.xaml
jwj MainWindow.xaml
-?p Solution Explorer I
Рис. 30.3. Скопированные данные
изображений
// Список файлов Bitmaplmage.
1140 Часть VI. Построение настольных пользовательских приложений с помощью WPF
List<BitmapImage> images = new List<BitmapImage>();
// Текущая позиция в списке.
private int currImage = 0;
private const int MAX_IMAGES = 2;
private void Window_Loaded(object sender, RoutedEventArgs e)
{
try
{
string path = Environment.CurrentDirectory;
// Загрузить эти изображения при загрузке окна.
images.Add(new Bitmaplmage(new Uri(string.Format(@"{0}\Images\Deer.jpg11, path))));
images.Add (new Bitmaplmage(new Uri(string.Format(@M{0}\Images\Dogs.jpg", path))));
images. Add (new Bitmaplmage (new Uri (string. Format ((^"{OjMmagesNWelcome.jpg11, path))));
// Показать первое изображение в List<>.
imageHolder.Source = images[currlmage];
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
1
}
Обратите внимание, что в этом классе также определена переменная-член типа int
(currlmage), которая позволяет обработчикам событий Click проходить циклом по
каждому элементу в List<T> и отображать его в элементе Image, устанавливая свойство
Source. (Здесь обработчик события Loaded устанавливает свойство Source в первое
изображение из List<T>.) Вдобавок константа МАХ_IMAGES дает возможность проверить
верхнюю и нижнюю границы по мере итерации по списку. Ниже показаны обработчики
Click, которые делают это:
private void btnPreviousImage_Click(object sender, RoutedEventArgs e)
{
if (--currlmage < 0)
currlmage = MAX_IMAGES;
imageHolder.Source = images[currlmage];
}
private void btnNextImage_Click(object sender, RoutedEventArgs e)
{
if (++currlmage > MAX_IMAGES)
currlmage = 0;
imageHolder.Source = images[currlmage];
}
Теперь можно запустить программу и переключаться между всеми изображениями.
Встраивание ресурсов приложения
Если вы предпочитаете встроить файлы изображений непосредственно в
сборку .NET в виде двоичных ресурсов, выберите файлы изображений в Solution Explorer
(в папке \Images, а не \bin\Debug\Images). Затем установите свойство Build Action в
Resource, а свойство Copy to Output Directory — в Do not copy (рис. 30.4).
В меню Build (Сборка) Visual Studio 2010 выберите пункт Clean Solution (Очистить
решение) для очистки текущего содержимого \bin\Debug\Images и перестройте проект.
Обновите окно Solution Explorer и обратите внимание, что папки \bin\Debug\Images в
каталоге \bin\Debug больше нет.
Глава 30. Ресурсы, анимация и стили WPF 1141
Properties
Welcome.jpg File Properties
:К: 41 !
Copy to Output Directory
Custom Tool
Custom Tool Namespace
I Resource
Do not copy
a
Buid Action
How the file relates to the build and deployment processes.
Рис. 30.4. Конфигурирование изображений как встроенных ресурсов
При текущих параметрах сборки графические данные больше не копируются в
выходную папку, а встраиваются в саму сборку. Предполагая, что проект
перекомпилирован, откройте сборку в утилите reflector.exe и удостоверьтесь в наличии встроенных
данные изображений (рис. 30.5).
■jf Red Gate; NETR
QOI
Qg чЗ WindowsBase
i+l -uJ Calc
[+1 -CJ PresentationFramework
3 -CJ BinaryResourcesApp
\*> l\ BinaryResourcesApp.exe
Q ,_j Resources
j4 BinaryResourcesApp.g.resources
,^3 BinajyResourcesApp.Properties.Resources.resources !.
Name
_J mainwmdow.baml
Value
0c 00 00 00 4d 00 53 00 ... A324 bytes)
// public resource BinaryResourcesApp.g.resources
Size 3532687 bytes
Рис. 30.5. Встроенные данные изображений
Теперь необходимо модифицировать код для загрузки изображений, чтобы
извлекать их уже из скомпилированной сборки:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
try
{
images.Add(new Bitmaplmage(new Uri(@"/Images/Deer.jpg", UriKind.Relative)));
images.Add(new Bitmaplmage(new Uri(@"/Images/Dogs.jpg", UriKind.Relative)));
images .Add (new Bitmaplmage (new Uri @11/ Images /Welcome . jpg", UriKind. Relative) ) ) ;
imageHolder.Source = images[currlmage];
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
Как видите, в этом случае больше не нужно определять путь установки, а можно
просто просматривать ресурсы по имени, принимающем во внимание имя исходного
подкаталога. Также обратите внимание, что при создании объектов Uri указывается
1142 Часть VI. Построение настольных пользовательских приложений с помощью WPF
значение UriKind.Relative. В любом случае в нынешнем виде исполняемая
программа представляет собой единую сущность, которая может быть запущена из любого
местоположения на машине, поскольку все скомпилированные данные находятся внутри
сборки. На рис. 30.6 показано готовое приложение.
Исходный код. Проект BinaryResourcesApp доступен в подкаталоге Chapter 30.
Работа с объектными (логическими) ресурсами
При построении WPF-приложения очень часто приходится определять большой объем
XAML-разметки для использования во множестве мест окна или, возможно, в нескольких
окнах или проектах. Например, предположим, что с помощью Expression Blend построена
идеальная кисть линейного градиента, описываемая 10 строками кода разметки. Теперь
эту кисть необходимо использовать в качестве фонового цвета для каждого элемента
Button в проекте, состоящем из 8 окон, т.е. всего получается 16 элементов Button.
Худшее, что можно сделать в такой ситуации — это копировать и вставлять одну и
ту же XAML-разметку для каждого элемента управления Button. Ясно, что это может
стать кошмаром при сопровождении, поскольку придется вносить изменения во
множестве мест всякий раз, когда нужно скорректировать внешний вид и поведение кисти.
К счастью, объектные ресурсы позволяют определить фрагмент XAML-разметки,
назначить ему имя и сохранить в подходящем словаре для последующего использования.
Подобно бинарному ресурсу, объектные ресурсы часто компилируются в сборку,
которая нуждается в них. Однако при этом не требуется оперировать свойством Build Action.
Достаточно поместить XAML-разметку в правильное место, а компилятор позаботится
об остальном.
Манипуляции объектными ресурсами составляют значительную часть разработки
WPF. Как будет показано, объектные ресурсы могут быть намного сложнее, чем
специальная кисть. Можно определять анимацию на основе XAML, трехмерную
визуализацию, специальный стиль элемента управления, шаблон данных и многое другое — и все
это в одном многократно используемом ресурсе.
Глава 30. Ресурсы, анимация и стили WPF 1143
Роль свойства Resources
Как упоминалось ранее, объектные ресурсы должны помещаться в подходящий объект
словаря, чтобы их можно было использовать во всем приложении. Как известно, каждый
наследник FrameworkElement поддерживает свойство Resources. Это свойство
инкапсулирует объект ResourceDictionary, содержащий определенные объектные ресурсы.
ResourceDictionary может хранить элементы любого типа, поскольку оперирует
экземплярами System.Object, и допускает маштуляции из XAML или процедурного кода.
В WPF все элементы управления, окна (Window), страницы (Page) (используемые
при построении навигационных приложений или программ ХВАР) и UserControl
расширяют FrameworkElement, поэтому почти все виджеты предоставляют
доступ к ResourceDictionary. Более того, класс Application, хотя и не расширяет
FrameworkElement, поддерживает свойство с идентичным именем Resources для тех
же целей.
Определение ресурсов уровня окна
Чтобы приступить к исследованию роли объектных ресурсов, создайте приложение
WPF по имени ObjectResourcesApp, используя для этого Visual Studio 2010, и
замените начальный контейнер Grid горизонтально выровненным диспетчером компоновки
StackPanel. В этом StackPanel определите два элемента управления Button:
<Window x:Class="ObjectResourcesApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2 00 6/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/200 6/xaml"
Title="Fun with Object Resources" Height=50" Width=25">
<StackPanel Orlentation="Horizontal"^
<Euttcn Ilargin=5" Height=00" Width=00" Content="OK" FontSize=0"/ >
<Button Margin=5" Height=00" Width=00" Content="Cancel" FontHize=0"/>
</StackPanel^
</Windou^
Теперь выберите кнопку OK и установите свойство цвета Background в
специальный тип кисти, используя интегрированный редактор кистей (см. главу 29). После
этого обратите внимание, что кисть встраивается в контекст дескрипторов ^Button"* и
</Button>:
<Eutton Margin=5" Height=00" Width=00" Content="OK" FontSize=0"-
<Button.BacLground>
<PadialGradientBrush>
<GradientStop Color="#FFC44EC4" Offset=" />
^Graiient£tzp Color="#FF829CEB" Offset="l" />
<GradientStop Color="#FF793879" Offset=.669" />
</RadialGradientBrush>
^/Button.Вас kground>
^/Button^
Чтобы позволить кнопке Cancel также использовать эту кисть, необходимо
распространить область <RadialGradientBrush> на ресурсный словарь родительского иле-
мента. Например, если переместить его в <StackPanel>, обе кнопки смогут
использовать одну и ту же кисть, поскольку они являются дочерними элементами диспетчера
компоновки. Более того, можно было бы поместить кисть в ресурсный словарь самого
окна, так что все аспекты содержимого окна (вложенные панели, и т.п.) могли бы
свободно пользоваться ею.
Когда требуется определить ресурс, для установки свойства Resources владельца
применяется синтаксис "свойство-элемент". Кроме того, элементу ресурса задается
значение х:Кеу, которое будет использовано другими частями окна, когда им нужно бу-
1144 Часть VI. Построение настольных пользовательских приложений с помощью WPF
дет обратиться к объектному ресурсу. Имейте в виду, что х:Кеу и x:Name — не одно и
то же! Атрибут x:Name позволяет получить доступ к объекту как к переменной-члену
в файле кода, в то время как атрибут х:Кеу позволяет сослаться на элемент в словаре
ресурсов.
Среды Visual Studio 2010 и Expression Blend позволяют продвинуть ресурс на
более высокий уровень, используя соответствующие окна Properties. В Visual Studio 2010
сначала идентифицируется свойство, имеющее сложный объект, который необходимо
упаковать в виде ресурса (в рассматриваемом примере это свойство Background). Рядом
со свойством находится небольшая ромбовидная кнопка, щелчок на которой приводит к
открытию всплывающего меню. Выберите в этом меню пункт Extract value to Resource...
(Извлечь значение в ресурс), как показано на рис. 30.7.
[properties
Button
1 J3* Properties •* Events
Ё! it 4
1 Padding
MinWidth
1 MinHeight
1 MaxWidth
1 MaxHeight
1 HorizontalContentAlignment
I VerticalCcntentAlignment
1 FlowDirectron
I ZIndex
\л Brashes
E57K9HHHHHH
I BorderBrush
1 Foreground
1 OpacrtyMask
□ ■ e a
т=
□
□
□
□
~
r=
□
□
1
V
- п x]
1 ' 1
0
0
Infinity
Infinity
Center
Center
LeftToRjght
0
■ System .Windows. Media. RadialGr
Reset Value
Apply Data Binding...
Apply Resource...
Extract Value to Resource... к
———fH^—■■ ju'« —-»|
Рис. 30.7. Перемещение сложного объекта в контейнер ресурсов
Теперь будет задан вопрос об имени вашего ресурса (myBrush) и предложено
указать, куда он должен быть помещен. В данном примере оставьте выбор по умолчанию —
MainWindow.xaml (рис. 30.8).
Рис. 30.8. Назначение имени объектному ресурсу
Покончив с этим, вы увидите, что разметка будет реструктурирована следующим
образом:
<Window х:Class="ObjectResourcesApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/200 6/xaml"
Title="Fun with Object Resources" Height=50" Width=25">
<Window.Resources>
Глава 30. Ресурсы, анимация и стили WPF 1145
<RadialGradientBrush x:Key="myBrush">
<GradientStop Color="#FFC44EC4" Offset=" />
<GradientStop Color="#FF829CEB" Offset="l" />
<GradientStop Color="#FF793879" Offset=.669" />
</RadialGradientBrush>
</Window.Resources>
<StackPanel Orientation="Horizontal">
<Button Margin=5" Height=00" Width=M200" Content="OK"
FontSize=0" Background="{StatlcResource myBrush}"></Button>
<Button Margin=5" Height=00" Width=00" Content="Cancel"
FontSize=0"/>
</StackPanel>
</Window>
Обратите внимание на новый контекст <Window.Resources>, который теперь
содержит объект RadialGradientBrush, имеющий ключевое значение myBrush.
Расширение разметки {StaticResource}
Другое изменение, которое имеет место при извлечении объектного ресурса:
свойство, которое было целью извлечения (опять-таки — Background) теперь использует
расширение разметки {StaticResource}. Как видите, имя ключа указано в виде
аргумента. Если теперь кнопке Cancel нужно будет использовать ту же кисть для рисования
фона, она сможет это сделать. Или же, если кнопка Cancel имеет некоторое сложное
содержимое, любой подэлемент этой кнопки может также использовать ресурс уровня
окна, например, свойство Fill элемента Ellipse.
<StackPanel Orientation="Horizontal'^
<Button Margin=5" Height=00" Width=00" Content="OK" FontSize=0"
Background="{StaticResource myBrush}">
</Button>
<Button Margin=5" Height=00" Width=00" FontSize=0">
<StackPanel>
<Label HorizontalAlignment="Center" Content= "No Way!"/>
<Ellipse Height=00" Width=00" Fill="{StaticResource myBrush}"/>
</StackPanel>
</Button>
</StackPanel>
Изменение ресурса после извлечения
После того, как ресурс извлечен в контекст окна, его можно найти его в окне
Document Outline (Схема документа), открываемое через пункт меню View1^Other
Windows^ Document Outline (ВидоДругие окна^Схема документа) и показанное на
рис. 30.9.
[Document Outline
1 .j-Window
1 л
[ л
I л
-Resources
L RadialGradientBrush (myBrush)
-GradientStop
-GradientStop
-GradientStop
StaclcPanel
j-Button
1 л L Button
К л LStackPanel
1- Label
L Ellipse
- nxj
и
Рис. 30.9. Просмотр ресурса в окне Document Outline
1146 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Выбрав любой аспект объектного ресурса в окне Document Outline, можно изменять
значения, используя окно Properties. Таким образом, можно изменить индивидуальные
переходы градиентов, их смещения и прочие аспекты кисти.
Расширение разметки {DynamicResource}
При подключении к ключевому ресурсу свойство также может использовать
расширение разметки {DynamicResource}. Чтобы понять разницу, назначьте кнопке О К имя
btnOK и обработайте ее событие Click. В этом обработчике события воспользуйтесь
свойством Resources для получения специальной кисти, и затем изменения некоторых
ее аспектов:
private void btnOK_Click(object sender, RoutedEventArgs e)
{
// Получить кисть и внести изменение.
RadialGradientBrush b = (RadialGradientBrush)Resources["myBrush"];
b.GrartientStops[1] = new GradientStop(Colors.Black, 0.0);
}
На заметку! Здесь индексатор Resources используется для поиска ресурса по имени. Однако
имейте в виду, что если ресурс не обнаруживается, это приводит к генерации исключения
времени выполнения. Можно также применять метод TryFindResource (), который не генерирует
исключение времени выполнения, а просто возвращает null, если указанный ресурс не найден.
Запустив это приложение и щелкнув на кнопке ОК, вы увидите, что изменение
кисти учтено, и каждая кнопка обновляется для визуализации модифицированной кисти.
А что если вы полностью измените тип кисти, указанной ключом myBrush? Например:
private void btnOK_Click(object sender, RoutedEventArgs e)
{
// Поместить совершенно новую кисть в ячейку myBrush.
Resources["myBrush"] = new SolidColorBrush(Colors.Red);
}
На этот раз после щелчка на кнопке никаких обновлений не ожидается. Причина
в том, что расширение разметки {Static Re source} применяет ресурс только
однажды и остается "подключенным" к исходному объекту на протяжении времени жизни
приложения. Однако если изменить в разметке все вхождения {Static Re source} на
{DynamicResource}, обнаружится, что специальная кисть будет заменена кистью со
сплошным красным цветом.
По существу расширение разметки {DynamicResource} может обнаруживать
замену лежащего в основе ключевого объекта новым. Как и следовало ожидать, это
требует некоторой дополнительной инфраструктуры времени выполнения, так что обычно
стоит придерживаться использования {Static Re source}, если только не планируется
заменять объектный ресурс другим объектом во время выполнения, уведомляя все
элементы, использующие этот ресурс.
Ресурсы уровня-приложения
Когда в словаре ресурсов окна имеются объектные ресурсы, то все элементы
этого окна могут пользоваться ими, но другие окна приложения — нет. Назначьте кнопке
Cancel имя btnCancel и обработайте событие Click. Добавьте в текущий проект новое
окно (по имени TestWindow.xaml), содержащее единственную кнопку Button, по щелчку
на которой окно закрывается:
Глава 30. Ресурсы, анимация и стили WPF 1147
public partial class TestWindow : Window
{
public TestWindow()
{
InitializeComponent ();
}
private void btnClose_Click(object sender, RoutedEventArgs e)
{
this.Close ();
}
}
В обработчике Click кнопки Cancel первого окна загрузите и отобразите это новое
окно, как показано ниже:
private void btnCancel_Click(object sender, RoutedEventArgs e)
{
TestWindow w = new TestWindow();
w.Owner = this;
w.WindowStartupLocation = WindowStartupLocation.CenterOwner;
w. ShowDialog() ;
}
Если новое окно желает использовать myBrush, в данный момент оно не может этого
сделать, поскольку myBrush не находится внутри корректного "контекста". Решение
состоит в определении объектного ресурса на уровне приложения, а не на уровне
конкретного окна. В Visual Studio 2010 не предусмотрено какого-либо способа автоматизировать
это, потому просто вырежьте текущий объект кисти из области <Windows.Resource> и
поместите его в область <Application.Resources> файла App.xaml:
<Application x : Class="Ob;jectResourcesApp . Арр"
xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
<RadialGradientBrush x:Key="myBrush">
<GradientStop Color="#FFC44EC4" Offset=" />
<GradientStop Color="#FF829CEB" Offset="l" />
<GradientStop Color="#FF793879" Offset=.669" />
</RadialGradientBrush>
</Application.Resources>
</Application>
Теперь объект TestWindow может использовать эту же кисть для рисования своего
фона. Найдите свойство Background для этого нового Window, щелкните на маленьком
белом квадрате Advanced properties (Расширенные свойства) и выберите пункт меню
Apply Resource... (Применить ресурс). Затем можно найти кисть уровня приложения,
набирая ее имя в поле поиска (рис. 30.10).
Определение объединенных словарей ресурсов
Ресурсы уровня приложения — хорошая отправная точка, но что, если необходимо
определить набор сложных (или не слишком сложных) ресурсов, которые должны
многократно использоваться во множестве проектов WPF? В данном случае требуется
определить то, что известно как "объединенный словарь ресурсов". Это — всего лишь файл
.xaml, который не содержит ничего помимо коллекции объектных ресурсов. Один
проект может иметь любое необходимое количество таких файлов (один для кистей, один
для анимации, и т.д.), каждый из которых может добавляться в диалоговом окне Add
New Item (Добавить новый элемент), доступном через меню Project (рис. 30.11).
1148 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Properties
Window <n&mnm>
I J5* Properties / Events
: lid
I VerticalContentAlignment
FlowDirecticn
> Brushes
I BorderBrush
I Foreground
I Opac'ityMask
□ ■ □ □
ЖШН^г R ШПШ
/
Text
I УМЙТУ
my
ф myBrush
Key
□ Top
Q LeftToRight
Resouice...
U
1
• П X|
«
X 1
' 1
,,
~^* а§
Static »
-
Рис. 30.10. Применение ресурсов уровня приложения
Installed Templates
л Visual С*
Cede
Data
General
Web
Windows Forms
WPF
Reporting
Workflow
Sort by. j Default
Ш
j ^J Page (WPF)
;«М] User Control (WPF)
Type: Visual О
XAML resource dictionary
Resource Dictionary (WPF) Visual C*
^ Custom Control (WPF)
. Jj Flow Document (WPF)
OO Pe9e Function (WPF)
|j£j Splash Screen (WPF)
MyBrushes.xaml
Рис. 30.11. Вставка в проект нового объединенного словаря ресурсов
В новом файле MyBrushes.xaml понадобится вырезать текущие ресурсы из
контекста Application.Resources и перенести их в словарь, как показано ниже:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<PadialGradientBrush x:Key="myBrush">
<GradientStop Color="#FFC44EC4" Offset=" />
<GradientStop Color="#FF829CEB" Offset="l" />
<GradientStop Color="#FF793879" Offset=.669" />
<7RadialGradientBrush>
</ResourceDictionary>
Теперь, несмотря на то, что этот словарь ресурсов является частью проекта,
возникают ошибки времени выполнения. Причина в том, что все словари ресурсов
Глава 30. Ресурсы, анимация и стили WPF 1149
должны быть объединены (обычно на уровне приложения) в существующий словарь
ресурсов. Для этого воспользуйтесь следующим форматом (обратите внимание, что
множество словарей ресурсов могут быть объединены добавлением нескольких элементов
<ResourceDictionary> в область <ResourceDictionary.MergedDictionaries>).
<Application x:Class="Ob3ectResourcesApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<!— Взять логические ресурсы из файла MyBrushes . xaml —>
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source = "MyBrushes.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
Определение сборки из одних ресурсов
И последнее (по порядку, но не по важности): есть возможность создавать
библиотеки классов, которые не содержат ничего, кроме словарей объектных ресурсов. Это
может быть полезно, если определен набор тем, которые должны применяться на уровне
машины. Объектные ресурсы можно упаковать в выделенную сборку, и тогда
приложения, которые хотят использовать их, смогут загружать их в память.
Самый легкий способ построения сборки из одних ресурсов состоит в том, чтобы
начать с проекта WPF User Control Library (Библиотека пользовательских элементов
управления WPF). Добавьте такой проект (под названием MyBrushesLibrary) к
текущему решению, используя пункт меню Add^New Project (Добавить1^Новый проект) среды
Visual Studio 2010 (рис. 30.12).
Теперь полностью удалите файл UserControll.xaml из проекта (единственное, что
понадобится, это ссылаемые сборки WPF). Перетащите файл MyBrushes.xaml в
проект MyBrushesLibrary и удалите его из проекта ObjectResourcesApp. Окно Solution
Explorer должно теперь выглядеть, как показано на рис. 30.13.
Installed Templates
л Visual C#
Windows
Web
Office
Cloud
Reporting
SharePoint
Sifverlight
Test
'.VCF
Workflow
Other Languages
Name
Location:
Щ WPF Browser Application Visual C*
П Empty Project Visual C#
J] Windows Service Visual C*
I *C0 WPF Custom Control Library Visual C#
WPF User Control Library N Visual C#
la
Type Visual C#
Windows Presentation Foundation user
control library
WPF
Windows Forms Control Libr.T^fWWr
User Control Library i
7
MyBrushesLibrary
C:\MyCode
Рис. 30.12. Добавление проекта WPF User Control Library в качестве начальной
точки для построения библиотеки из одних только ресурсов
1150 Часть VI. Построение настольных пользовательских приложений с помощью WPF
■ Solution Explorer
;^3 Solution 'ObjectResourcesApp' B projects)
л i^3 MyBrushesLibrary
!> Ш Properties
b ~ai References
;£*[_ Myfirushesjcaml
л _JH ObjectResourcesApp
3i Properties
> ^ References
t> j«* Appjcaml
j> fag MBinWindowj(aml
i> jai^ TestWindowjcaml
-^ Solution Explorer I
Рис. 30.13. Перемещение файла MyBrushes.xaml
в новый проект библиотеки
Скомпилируемте проект библиотеки кода. Затем установите ссылку на эту библиотеку
из проектj ObjectPesourcesApp, используя диалоговое окно Add Reference (Добавить
ссылку). Теперь необходимо объединить эти двоичные ресурсы со словарем ресурсов
уровня приложения из проекта ObjectResourcesApp. Это потребует некоторого
довольно забавного синтаксиса, как показано ниже:
<Арр1к. ntion.Pesources>
<ResonrceDictionary>
<.Р< ^"iu г се Dictionary .MergedDictionaries ~-
<! - - Синтаксис: /ИмяСборки; Сощропег^/ИмяФайлаХат1вСборке. xaml - ->
rcsourceDictionary Source = "/MyBrusheoLibrary/Component/MyBrushes.xaml"/>
-/RLSourceDictionary.MergedDictionaries>
</Re ",uurctDictionary>
</Application.Resources>
Имейте в виду, что эта срока чувствительна к
пробелам. Если возле двоеточия или косой черты
будут излишние пробелы, возникнут ошибки
времени выполнения. Первая часть строки — это
дружественное имя внешней библиотеки (без расширения
файла). После двоеточия идет слово Component, за
которым — имя скомпилированного двоичного
ресурса, совпадающее с именем исходного словаря
ресурсов XAML.
Извлечение ресурсов в Expression Blend
Как уже упоминалось, в инструменте Expression
Blend предусмотрены подобные способы
продвижения локальных ресурсов на уровень окна,
уровень приложения и даже уровень словаря ресурсов.
Предположим, что определена специальная кисть
для свойства Background главного окна, требуется
упаковать его как новый ресурс. Используя
редактор кистей, щелкните на (маленьком белом)
прямоугольнике Advanced Properties возле свойства
Background для доступа к опции Convert to New
Resource (Преобразовать в новый ресурс), как
показано на рис. 30.14.
Н&аипеч Data Properties ■
. Name Window ^jj Ff
Type Window
• bushes
—— шшшяяш
| Convert tc New ftwo>ate.., ,^И
/m
ъ m
[«■» #FF5A24S9 j
0 0
Щ «□► 57%
» Аррешлнсе
Opacity 100K
Viubrirty Visible
Alkn»Transp<trency
Рис. 30.14. Извлечение нового
ресурса в Expression Blend
Глава 30. Ресурсы, анимация и стили WPF 1151
В открывшемся диалоговом окне объектному ресурсу необходимо назначить имя и
указать, где Blend должен сохранить его (текущее окно, приложение или новый
объединенный словарь ресурсов). В данном случае myNewBrush помещается на уровень
приложения (рис. 30.15).
После этого файл App.xaml будет обновлен, как и следовало ожидать. С
использованием вкладки Resources (Ресурсы), которая открывается через меню Window, можно
модифицировать существующие ресурсы в соответствующем редакторе (рис. 30.16).
Рис. 30.15. Определение нового ресурса уровня приложения
с помощью Expression Blend
Рис. 30.16. Модификация существующих ресурсов в Expression Blend
На этом знакомство с системой управления ресурсами WPF завершено. В
большинстве приложений нередко придется использовать описанные приемы, и мы еще не раз
обратимся к ним в оставшихся главах, посвященных WPF. Теперь давайте приступим к
исследованию API-интерфейса встроенной анимации Windows Presentation Foundation.
Исходный код. Проект Ob^ectResourceApp доступен в подкаталоге Chapter 30.
1152 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Службы анимации WPF
В дополнение к службам графической визуализации, которые рассматривались в
главе 29, в WPF предлагается API-интерфейс для поддержки служб анимации. Услышав
термин анимация, сразу на ум приходят вращающийся логотип компании,
последовательность сменяемых друг друга изображений (создающих иллюзию движения),
бегущая по экрану строка текста либо программа специфического типа, подобная видеоигре
или мультимедиа-приложению.
Хотя API-интерфейсы анимации WPF определенно могут использоваться для
упомянутых выше целей, анимации могут применяться в любой момент, когда требуется
добавить к приложению дополнительный лоск. Например, можно построить анимацию
для кнопки на экране, которая слегка увеличивается, когда курсор мыши попадает в
ее границы (и уменьшается обратно, когда курсор покидает ее поверхность). Или же
можно анимировать окно, чтобы оно закрывалось, используя определенное визуальное
представление, например, постепенно исчезая до полной прозрачности. Фактически
поддержка анимации WPF может применяться в приложениях любого рода (бизнес-
приложениях, мультимедиа-программах, видеоиграх и т.д.), когда нужно создать более
привлекательное впечатление у пользователя.
Как и многие другие аспекты WPF, понятие анимации не представляет собой здесь
ничего нового. Новостью является то, что в отличие от других API-интерфейсов, которые
вы могли использовать в прошлом (включая Windows Forms), разработчики не обязаны
создавать необходимую инфраструктуру вручную. В среде WPF нет необходимости
создавать заранее фоновые потоки или таймеры, чтобы выполнять анимированные
последовательности, определять специальные типы для представления анимации, очищать
и перерисовывать изображения, либо заниматься утомительными математическими
вычислениями.
Как и другие аспекты WPF, анимации могут быть построены целиком в XAML-
разметке, целиком в коде С#, либо за счет комбинации того и другого. Более того, с
помощью Microsoft Expression Blend можно проектировать анимацию с применением
интегрированных инструментов и мастеров, даже не видя ни единого фрагмента кода
С# или XAML. В следующей главе, когда пойдет речь о шаблонах элементов управления,
будет продемонстрировано использование Expression Blend для создания анимации.
А пока давайте рассмотрим общую роль API-интерфейса анимации.
На заметку! В среде Visual Studio 2010 отсутствует поддержка написания анимации с помощью
графических инструментов. В Visual Studio для создания анимации придется набирать код
XAML вручную.
Роль классов анимации
Чтобы разобраться в поддержке анимации в WPF, необходимо начать с
рассмотрения классов анимации из пространства имен System.Windows.Media.Animation
сборки PresentationCore.dll. Здесь можно найти свыше 100 разных типов классов,
которые содержат в своем имени слово Animation.
Все эти классы могут быть отнесены к одной из трех обширных категорий. Во-первых,
любой класс, который следует соглашению об именовании вида ТилДаннь/xAnimation
(ByteAnimation, ColorAnimation, DoubleAnimation, Int32Animation и т.п.)
позволяет работать с анимацией с линейной интерполяцией. Она позволяет изменять значение
во времени гладко, от начального к конечному.
Глава 30. Ресурсы, анимация и стили WPF 1153
Во-вторых, классы, которые следуют соглашению об именовании вида Тип Да иных
AnimationUsmgKeyFrames (StringAnimationUsingKeyFrames, DoubleAnimation
UsingKeyFrames, PointAnimationUsingKeyFrames и т.п.) представляют анимацию
ключевыми кадрами, которая позволяет проходить циклом по набору определенных
значений за указанный период времени. Например, ключевые кадры можно использовать
для замены надписи на кнопке, проходя циклом по сериям индивидуальных символов.
В-третьих, классы, которые следуют соглашению об именовании вида ТипДанных
AnimationUsingPath (DoubleAnimationUsingPath, PointAnimationUsingPath и т.п.)
представляют анимацию на основе пути, которая позволяет анимировать объекты,
перемещая их по определенному пути. Например, при построении приложения
глобального позиционирования (GPS) можно использовать анимацию на основе пути, чтобы
перемещать элемент по кратчайшему пути к указанному пользователем месту.
Теперь очевидно, что эти классы не позволяют каким-то образом предоставить
последовательность анимации непосредственно переменной определенного типа (в конце
концов, анимировать значение п с помощью Int32Animation невозможно).
Например, рассмотрим свойства Height и Width типа Label. Оба они являются
свойствами зависимости, упаковывающими значение double. Чтобы определить
анимацию, которая будет увеличивать ширину метки с течением времени, можно
подключить объект DoubleAnimation к свойству Height и позволить WPF позаботиться о
деталях выполнения анимации. Другой пример: если требуется изменить цвет кисти с
зеленого на желть»! в течение 5 секунд, это можно сделать с использованием анимации
ColorAnimation.
Следует прояснить, что классы Animation могут подключаться к любому свойству
зависимости определенного объекта, который соответствует лежащему в основе типу.
Как будет показано в главе 31, свойства зависимости — это специальная разновидность
свойств, используемых многими службами WPF, включая анимацию, привязку данных
и стили.
По соглашению свойство зависимости определено как статическое, доступное только
на чтение поле класса, чье имя формируется добавлением слова Property к
нормальному имени свойства. Например, свойство зависимости для свойства Height класса
Button будет доступно в коде как Button.HeightProperty.
Свойства То, From и By
Во всех классах Animation определено несколько ключевых свойств, которые
управляют начальным и конечным значениями, используемыми для выполнения анимации:
• То — представляет конечное значение анимации;
• From — представляет начальное значение анимации;
• By — представляет общую величину, на которую анимация изменяет начальное
значение.
Несмотря на тот факт, что все классы поддерживают свойства То, From и By, они не
получают их через виртуальные члены базового класса. Причина в том, что лежащие
в основе типы, упакованные в эти свойства, варьируются в широких пределах (целые,
цвета, объекты Thickness и т.п.), и представление всех возможностей через
единственный базовый гсласс привело бы к очень сложным конструкциям при кодировании.
В связи с этим может также возникнуть вопрос: почему бы не воспользоваться
обобщениями .NET для определения единственного обобщенного класса анимации с
единственным параметром типа (например, Animate<T>)? Опять-таки, учитывая, что
существует огромное число типов данных (цвета, векторы, целые, строки т т.д.), используемых
для анимации свойств зависимости, это решение не будет столь ясным, как можно было
1154 Часть VI. Построение настольных пользовательских приложений с помощью WPF
бы ожидать (не говоря уже о том, что XAML предлагает лишь ограниченную поддержку
обобщенных типов).
Роль базового класса Timeline
Хотя для определения виртуальных свойств То, From и By не использовался
единственный базовый класс, классы Animation все же разделяют общий базовый класс:
System.Windows .Media. Animation.Timeline. Этот тип предоставляет множество
дополнительных свойств, которые управляют темпом анимации (табл. 30.1).
Таблица 30.1. Основные свойства базового класса Timeline
Свойство Назначение
AccelerationRatio, Эти свойства могут использоваться для управления общим темпом
DecelerationRatio, последовательности анимации
SpeedRatio
AutoReverse Это свойство получает и устанавливает значение, которое
указывает, должна ли временная шкала воспроизводиться в обратном
направлении после завершения прямой итерации (значение по
умолчанию — false)
BeginTime Это свойство получает и устанавливает время запуска временной
шкалы. По умолчанию принято значение 0, что запускает анимацию
немедленно
Duration Это свойство позволяет установить продолжительность времени
воспроизведения временной шкалы
FileBehavior, Эти свойства используются для управления тем, что случится по за-
RepeatBehavior вершении временной шкалы (повторение анимации, ничего, и т.д.)
Написание анимации в коде С#
Довольно высоки шансы, что большинство проектов анимации WPF будут созданы с
использованием редактора анимации Expression Blend, и потому представлены в виде
разметки. Однако при этом первом знакомстве со службами анимации WPF мы не будем
использовать ничего, кроме кода С#, поскольку это более прямолинейный подход.
В частности, мы построим окно, которое содержит элемент Button, обладающий
довольно странным поведением: он вращается вокруг своего левого верхнего угла, когда
на него наводится курсор мыши. Начните с создания нового WPF-приложения по имени
SpinningButtonAnimationApp в Visual Studio 2010. Обновите начальную разметку, как
показано ниже (обратите внимание на обработку события МоиseEvent кнопки):
<Window x:Class="SpinningButtomAnimationApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2 006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2 00 6/xaml"
Title="Animations in C# code" Height=50"
Width=2 5" WindowstartupLocation="CenterScreen">
<Grid>
<Button x:Name="btnSpinner" Height=0" Width=00" Content="I Spin1"
MouseEnter="btnSpinner_MouseEnter"/>
</Grid>
</Window>
Теперь импортируйте пространство имен System.Windows.Animation и добавьте
следующий код в файл С#:
Глава 30. Ресурсы, анимация и стили WPF 1155
public partial class MainWindow : Window
{
private bool isSpinning = false;
private void btnSpinner_MouseEnter(object sender, MouseEventArgs e)
{
if (!isSpinning)
{
isSpinning = true;
// Создать объект анимации double и зарегистрировать
//с событием Completed.
DoubleAnimation dblAnim = new DoubleAnimation();
dblAnim. Completed += (o, s) => { isSpinning = false; };
// Установить начальное и конечное значения.
dblAnim.From = 0;
dblAnim. To = 3 60;
// Создать объект RotateTransform и присвоить
// его свойству RenderTransform кнопки.
RotateTransform rt = new RotateTransform();
btnSpinner.RenderTransform = rt;
// Выполнить анимацию объекта RotateTransform.
rt.BeginAnimation(RotateTransform.AngleProperty, dblAnim);
}
}
}
Первая главная задача этого метода — сконфигурировать объект DoubleAnimation,
который начнется со значения 0 и закончится значением 360. Обратите внимание, что
на этом объекте также обрабатывается событие Completed за счет переключения
булевской переменной уровня класса, которая используется для того, чтобы выполняющаяся
анимация не была сброшена в начало.
Затем создается объект RotateTransform, который подключается к свойству
RenderTransform элемента управления Button (btnSpinner). И последнее: объект
RenderTransform информируется о начале анимации его свойства Angle с
использованием для этого объект£ DoubleAnimation. Описание анимации в коде обычно
осуществляется вызовом метода BeginAnimation () и передачей ему лежащего в основе
свойства зависимости, которое требуется анимировать (вспомните, что по существующему
соглашению это — поле класса), и связанного объекта анимации.
Добавим в программу еще одну анимацию, которая заставит кнопку плавно
становиться невидимой при щелчке. Для начала создадим обработчик события Click
объекта btnSpinner и поместим в него следующий код:
private void btnSpinner_Click(object sender, RoutedEventArgs e)
{
DoubleAnimation dblAnim = new DoubleAnimation();
dblAnim.From = 1.0;
dblAnim.To = 0.0;
btnSpinner.BeginAnimation(Button.OpacityProperty, dblAnim);
}
Здесь изменяется свойство Opacity, чтобы постепенно скрыть кнопку из виду. В
настоящее время, однако, это сделать трудно, поскольку кнопка вращается слишком
быстро. Каким образом управлять ходом анимации? Ответ на этот вопрос ищите ниже.
Управление темпом анимации
По умолчанию анимация занимает примерно одну секунду на переход между
значениями, присвоенными свойствам From и То. Поэтому кнопке требуется одна секунда,
1156 Часть VI. Построение настольных пользовательских приложений с помощью WPF
чтобы повернуться на 360 градусов, и так же за секунду она постепенно скрывается из
виду (после щелчка на ней).
Определить другой период времени на выполнение анимации можно сделать с
помощью свойства Duration объекта анимации, которому присваивается экземпляр объекта
Duration. Обычно период времени устанавливается передачей объекта TimeSpan
конструктору Duration. Рассмотрим следующее изменение, при котором кнопке
выделяется на вращение 4 секунды:
private void btnSpinner_MouseEnter(object sender, MouseEventArgs e)
{
if (!isSpinning)
{
isSpinning = true;
// Создать объект анимации double и зарегистрировать
// событие Completed.
DoubleAmmation dblAnim = new DoubleAnimation ();
dblAnim. Completed += (o, s) => { isSpinning = false; };
//На завершение поворота кнопке отводится 4 секунды.
dblAnim.Duration = new Duration(TimeSpan.FromSecondsD));
}
}
Благодаря этой модификации, появляется возможность щелкнуть на кнопке во
время ее вращения, после чего она плавно исчезает.
На заметку! Свойство BeginTime класса Animation также принимает объект TimeSpan. Вспомните,
что это свойство устанавливается для указания времени до начала процесса анимации.
Запуск в обратном порядке и циклическое выполнение анимации
Объекты Animation можно также заставить запускать анимацию в обратном
порядке по ее завершении, устанавливая свойство AutoReverse в true. Например, если
необходимо, чтобы кнопка снова стала видимой после исчезновения, для этого можно
написать следующий код:
private void btnSpinner_Click(object sender, RoutedEventArgs e)
{
DoubleAnimation dblAnim = new DoubleAnimation() ;
dblAnim.From = 1.0;
dblAnim.To = 0.0;
//По завершении - запуск в обратном порядке.
dblAnim.AutoReverse = true;
btnSpinner.BeginAnimation(Button.OpacityProperty, dblAnim);
}
Если хотите, чтобы анимация повторялась некоторое количество раз (или никогда
не прекращалась), то для этого можно использовать свойство RepeatBehavior, общее
для всех классов Animation. Передача конструктору простого числового значения
позволяет задать жестко закодированное количество повторений.
С другой стороны, если передать конструктору объект TimeSpan, то можно указать
длительность времени повторения анимации. Наконец, чтобы выполнять анимацию
бесконечно, можно просто указать RepeatBehavior.Forever. Рассмотрим следующие
способы изменения поведения повтора любого из двух объектов DoubleAnimation,
использованных в этом примере:
Глава 30. Ресурсы, анимация и стили WPF 1157
// Бесконечный цикл.
dblAmm.RepeatBehavior = RepeatBehavior . Forever ;
// Повторить три раза.
dblAmm.RepeatBehavior = new RepeatBehavior C) ;
// Повторять в течение 30 секунд.
dblAnim.FepeatBphavior = new RepeatBehavior(TimeSpan.FromSeconds C0) ) ;
На этом исследования анимационных аспектов объекта в коде С# и API-интерфейса
анимации WPF завершены. Теперь посмотрим, как сделать то же самое в XAML-
разметке.
Исходный код. Проект SpinningButtonAnimationApp доступен в подкаталоге Chapter 30.
Описание анимации в XAML
Описание анимации в разметке подобно описанию ее в коде, по крайней мере, если
речь идет о простых анимированных последовательностях. Когда нужно создавать
более сложные анимации, которые могут включать одновременные изменения значений
сразу множества свойств, объем разметки может значительно увеличиться. К счастью,
позаботиться обо всех деталях могут редакторы анимации Expression Blend. Но даже
несмотря на это, важно знать основы представления анимации в XAML, поскольку это
облегчит решение задачи модификации и подгонки сгенерированного инструментом
содержимого.
На заметку! В подкаталоге XamlAnimations примеров кода для данной главы имеется
множество файлов XAML. Скопируйте эти файлы разметки в специальный редактор XAML или в
редактор Kaxaml, чтобы просмотреть результаты.
В основном создание анимации подобно всему тому, что вы уже видели: сначала
конфигурируется объект Animation, который затем ассоциируется со свойством
объекта. Однако одно большое отличие состоит в том, что API-интерфейс WPF не слишком
дружественен к вызову функций. В связи с этим вместо вызова BeginAnimation ()
применяется раскадровка (storyboard) в качестве непрямого уровня.
Давайте рассмотрим полный пример анимации, определенный в терминах XAML,
с последующим подробным разбором. Следующее определение XAML отобразит окно,
содержащее единственную метку. Как только объект Label загружается в память, он
начинает последовательность анимации, при которой размер шрифта увеличивается
от 12 до 100 за период в четыре секунды. Анимация будет повторяться столько
времени, сколько объект остается загруженным в память. Эта разметка находится в файле
GrowLabelFont.xaml, поэтому скопируйте его в приложение MyXamlPad.exe и
понаблюдайте за поведением.
Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns: .' = "http://schemes.microsoft.com/winfx/2006/xaml"
Hpight= 0 0" Width="h00" WindouStartupLocation=" Center/Screen"
Title="r,ro\Jinq Label Font1""-»
<StackPanel
^Labrl Content = "InteLesting. . . ">
«■ La Ьр 1. Tr l qq'r r s -
- EventTriqqer PnutedEvent = "Label.Loaded">
E^ entTilqger.Action:
Begj riL4or' t._ ird>
-M-i'i 'bo'-trd TargetPtoperty = "FontSize">
1158 Часть VI. Построение настольных пользовательских приложений с помощью WPF
<DoubleAnimation From = 2" То
RepeatBehavior
</Storyboard>
</Beginstoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Label.Triggers>
</Label>
</StackPanel>
</Window>
А теперь подробно разберем этот пример.
Роль раскадровки
Двигаясь от самого вложенного элемента к внешнему, сначала мы встречаем
элемент <DoubleAnimation>, который использует те же свойства, что были установлены в
процедурном коде (То, From, Duration и RepeatBehavior):
<DoubleAnimation From = 2" To = 00" Duration = :0:4"
RepeatBehavior = "Forever"/>
Как упоминалось ранее, элементы Animation помещаются внутрь элемента
<Storyboard>, который используется для отображения объекта анимации на
родительский тип через свойство TargetProperty — которым в данном случае является
Font Size. Элемент <Storyboard> всегда помещен в родительский элемент по имени
<BeginStoryboard>, который представляет собой всего лишь способ обозначения
местоположения раскадровки:
<BeginStoryboard>
<Storyboard TargetProperty = "FontSize">
<DoubleAnimation From = 2" To = 00" Duration = :0:4"
RepeatBehavior = "Forever"/>
</Storyboard>
</Beginstoryboard>
Роль триггеров событий
Как только элемент <BeginStoryboard> определен, должно быть указано какое-то
действие, которое вызовет запуск анимации. В WPF предусмотрены разные способы
реагирования на условия времени выполнения в разметке, один из которых называется
триггер. В самом общем виде триггер можно рассматривать как способ реагирования
на условия событий в XAML-разметке, минуя процедурный код.
Обычно, когда определяется реакция на событие в С#, пишется специальный код,
который выполняется по наступлению события. Триггер — это всего лишь способ
получить уведомление о том, что некоторое событие произошло (загрузка элемента в
память, наведение курсора мыши на элемент, получение элементом фокуса и т.п.).
Получив уведомление о наступлении события, можно запускать раскадровку. В
рассматриваемом примере мы реагируем на факт загрузки элемента Label в память.
Поскольку нас интересует событие Loaded элемента Label, элемент <EventTrigger>
помещается в коллекцию триггеров элемента Label:
<Label Content = "Interesting. .. ">
<Label.Triggers>
<EventTrigger RoutedEvent = "Label.Loaded">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard TargetProperty = "FontSize">
00" Duration = :0:4"
"Forever"/>
Глава 30. Ресурсы, анимация и стили WPF 1159
<DoubleAnimation From = 2" То = 00" Duration = :0:4"
RepeatBehavior = "Forever"/>
</Storyboard>
</Beginstoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Label.Triggers>
</Label>
Теперь рассмотрим другой пример определения анимации в XAML — с
использованием анимации ключевыми кадрами.
Анимация с использованием дискретных ключевых кадров
В отличие от объектов анимации с линейной интерполяцией, которая обеспечивает
продвижение только между начальной и конечной точками, ключевые кадры позволяют
создавать коллекции определенных значений анимации, которые должны иметь место
в определенные моменты времени.
Чтобы проиллюстрировать использование типа дискретного ключевого кадра,
предположим, что необходимо построить элемент управления Button, который анимирует
свое содержимое таким образом, чтобы на протяжении трех секунд появлялось
значение ОК!, по одному символу за раз. Показанная ниже разметка находится в файле
StringAnimation.xaml. Скопируйте ее в программу MyXamlPad.exe и просмотрите
результат.
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/200 6/xaml"
Height=00" Width=00"
WindowStartupLocation="CenterScreen" Title="Animate String Data!">
<StackPanel>
<Button Name="myButton" Height=0"
FontSize=6pt" FontFamily="Verdana" Width = 00">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Loaded">
<BeginStoryboard>
<Storyboard>
<StringAnimationUsingKeyFrames RepeatBehavior = "Forever"
Storyboard.TargetName="myButton"
Storyboard.TargetProperty="Content"
Duration=:0:3">
<DiscreteStringKeyFrame Value="" KeyTime=:0:0" />
<DiscreteStringKeyFrame Value=" KeyTime=:0:1" />
<DiscreteStringKeyFrame Value="OK" KeyTime=:0:1.5" />
<DiscreteStringKeyFrame Value="OK!" KeyTime=:0 : 2" />
</StringAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
</StackPanel>
</Window>
Прежде всего, обратите внимание, что для кнопки определен триггер
события, который гарантирует запуск раскадровки при загрузке кнопки в память. Класс
StringAnimationUsingKeyFrames отвечает за изменение содержимого кнопки, через
значения Storyboard.TargetName и Storyboard.TargetProperty.
1160 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Внутри контекста элемента <StringAnimationUsingKeyFrames> определены
четыре элемента DiscreteStringKeyFrame, которые изменяют свойство Content на
протяжении двух секунд (длительность, установленная StringAnimationUsingKeyFrames,
составляет в сумме три секунды, поэтому между финальным ! и следующим
появлением О заметна небольшая пауза).
Теперь, когда вы получили некоторое представление о том, как строятся анимации
в коде С# и XAML, давайте уделим внимание стилям WPF, которые интенсивно
используют графику, объектные ресурсы и анимацию.
Исходный код. Свободные файлы XAML доступны в подкаталоге XamlAnimations каталога
Chapter 30.
Роль стилей WPF
При построении пользовательского интерфейса WPF-приложения нередко требуется
обеспечить общий вид и поведение для множества элементов управления. Например,
необходимо, чтобы все типы кнопок имели общую высоту, ширину, цвет и размер шрифта
своего строчного содержимого. Хотя это можно обеспечить за счет установки значений
индивидуальных свойств, такой подход затрудняет внесение изменений, поскольку при
каждом изменении придется переустанавливать один и тот же набор свойств на
множестве объектов.
К счастью, в WPF предлагается простой способ ограничения внешнего вида и
поведения взаимосвязанных элементов управления с использованием стилей. Просто говоря,
стиль WPF — это объект, который поддерживает коллекцию пар "свойство/значение".
С точки зрения программирования индивидуальный стиль представлен классом
System.Windows.Style. Этот класс имеет свойство по имени Setters, которое является
строго типизированной коллекцией объектов Setter. Именно объект Setter позволяет
определять пары "свойство/значение".
Вдобавок к коллекции Setters класс Style также определяет ряд других важных
членов, которые позволяют включать триггеры, ограничивать место применения стиля
и даже создавать новый стиль на основе существующего (воспринимайте это как
"наследование стиля"). Ниже перечислены важные члены класса Style:
• Triggers — представляет коллекцию объектов триггеров, которая позволяет
фиксировать различные условия событий в стиле;
• BasedOn — позволяет строить новый стиль на основе существующего;
• TargetType — позволяет ограничивать места применения стиля.
Определение и применение стиля
Почти в любом случае объект Style упаковывается в виде объектного ресурса.
Подобно любому объектному ресурсу, его можно упаковывать на уровне окна или
уровне приложения, а также внутри выделенного словаря ресурсов (это замечательно,
потому что делает объект Style легко доступным по всему приложению). Теперь вспомните,
что целью является определение объекта Style, который наполняет (как минимум)
коллекцию Setters набором пар "свойство/значение".
Создайте новое приложение WPF по имени Wpf Styles, используя Visual Studio 2010.
Давайте построим стиль, фиксирующий базовые характеристики элемента управления
в нашем приложении. Откройте файл App.xaml и определите следующий именованный
стиль:
Глава 30. Ресурсы, анимация и стили WPF 1161
<Application х:Class="WpfStyles.Арр"
xmlns="http://schemas.microsoft.com/winfx/20 06/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
<Style x:Key ="BasicControlStyleII>
<Setter Property = "Control.FontSize" Value =4"/>
<Setter Property = "Control.Height" Value = 0"/>
<Setter Property = "Control.Cursor" Value = "Hand"/>
</Style>
</Application.Resources>
</Application>
Обратите внимание, что BasicControlStyle добавляет три объекта Setter к
внутренней коллекции. Теперь применим этот стиль к нескольким элементам
управления в главном окне. Поскольку этот стиль является объектным ресурсом,
элементы управления, которые хотят использовать его, нуждаются в расширении разметки
{StackResource} или {DynamicResource} для нахождения стиля. Когда они находят
стиль, то устанавливают элемента ресурса в идентично именованное свойство Style.
Рассмотрим следующее определение <Window>:
<Window x:Class="WpfStyles.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="A Window with Style'" Height=29"
Width=25" WindowStartupLocation="CenterScreen">
<StackPanel>
<Label x:Name="lblInfo" Content="This style is boring..."
Style="{StaticResource BasicControlStyle}" Width=50"/>
<Button x:Name="btnTestButton" Content="Yes, but we are reusing settings'"
Style="{StaticResource BasicControlStyle}" Width=50"/>
</StackPanel>
</Window>
Запустив это приложение, вы обнаружите, что оба элемента управления
поддерживают один и тот же курсор, высоту и размер шрифта.
Переопределение настроек стиля
В примере определены элементы Button и Label, которые подчиняются ограничениям,
накладываемым нашим стилем. Разумеется, если элемент управления желает применить
стиль и затем изменить некоторые из определенных установок, это нормально. Например,
Button теперь использует курсор Help (вместо курсора Hand, определенного в стиле):
<Button x:Name="btnTestButton" Content="Yes, but we are reusing settings'"
Cursor="Help" Style="{StaticResource BasicControlStyle}" Width=50" />
Стили обрабатываются перед индивидуальными установками свойств элемента
управления, использующего стиль, поэтому элементы управления могут
"переопределять" настройки от случая к случаю.
Автоматическое применение стиля с помощью TargetType
^ Сейчас стиль определен таким образом, что его может принять любой элемент
управления (и должен делать это явно, устанавливая свое свойство Style), исходя из
того, что каждое свойство квалифицировано классом Control. Для программы,
определяющей десятки настроек это означало бы значительный объем повторяющегося кода.
Один из способов в некоторой степени улучшить ситуацию состоит в применении ат-
1162 Часть VI. Построение настольных пользовательских приложений с помощью WPF
рибута TargetType. Добавление этого атрибута к открывающемуся элементу Style
позволяет точно указать, когда он может быть применен:
<Style x:Key ="BasicControlStyle11 TargetType="ControlII>
<Setter Property = "FontSize" Value =4,,/>
<Setter Property = "Height" Value = 0,,/>
<Setter Property = "Cursor" Value = IIHand"/>
</Style>
На заметку! При построении стиля, использующего тип базового класса, не нужно беспокоиться
о том, что присваиваемое значение свойству зависимости не поддерживается производными
типами. Если производный тип не поддерживает определенное свойство зависимости, оно
игнорируется.
Это до некоторой степени помогает, но все равно мы имеем стиль, который
может применяться к любому элементу управления. Атрибут TargetType более удобен,
когда необходимо определить стиль, который может быть применен только к
определенному типу элементов управления. Добавьте следующий стиль в словарь ресурсов
приложения:
<Style x:Key ="BigGreenButton" TargetType=,,Button">
<Setter Property = "FontSize" Value =0,,/>
<Setter Property = "Height" Value = 00,,/>
<Setter Property = "Width" Value = ,,100"/>
<Setter Property = "Background" Value = "DarkGreen"/>
<Setter Property = "Foreground" Value = "Yellow"/>
</Style>
Этот стиль будет работать только с элементами управления Button (или его
подклассами), и если применить его к несовместимому элементу, то возникнут ошибки
разметки и времени компиляции. Если Button использует новый стиль так, как
продемонстрировано ниже, то вывод будет выглядеть подобно показанному на рис. 30.17:
<Button x:Name="btnTestButton" Content="This Style ROCKS!"
Cursor=,,Help" Style=" { StaticResource BigGreenButton} " Width=,,250" />
Рис. 30.17. Элементы управления с разными стилями
Создание подклассов существующих стилей
Новые стили можно также строить на основе какого-то существующего стиля, с
помощью свойства BasedOn. Расширяемый стиль должен иметь соответствующий атрибут
х:Кеу в словаре, поскольку производный стиль будет ссылаться на него по имени,
используя расширение разметки {StaticResource}. Ниже показан новый стиль,
основанный на стиле BigGreenButton, который поворачивает элемент-кнопку на 20 градусов:
Глава 30. Ресурсы, анимация и стили WPF 1163
<!-- Стиль основан на BigGreenButton -->
<Style x:Key =,,TiltButton" TargetType=,,Button"
BasedOn = "{StaticResource BigGreenButton} ">
<Setter Property = "Foreground" Value = "White"/>
<Setter Property = "RenderTransform">
<Setter.Value>
<RotateTransform Angle = 0"/>
</Setter.Value>
</Setter>
</Style>
На этот раз вывод будет выглядеть, как на рис. 30.18.
Рис. 30.18. Использование производного стиля
Роль безымянных стилей
Предположим, что нужно гарантировать, чтобы все элементы управления Text В ох
имели одинаковый внешний вид и поведение. Пусть определен стиль в форме ресурса
уровня приложения, доступ которому имеют все окна программы. Хотя это шаг в
правильном направлении, но если есть множество окон с множеством элементов
управления TextBox, то свойство Style придется устанавливать много раз!
Стили WPF могут быть неявно применены ко всем элементам управления внутри
заданного контекста XAML. Чтобы создать такой стиль, необходимо использовать
свойство TargetType, но не присваивать ресурсу Style значение х:Кеу. Такой
"безымянный стиль" теперь применяется ко всем элементам управления корректного типа. Ниже
показан другой стиль уровня приложения, который будет применен автоматически ко
всем элементам управления TextBox текущего приложения:
<!— Стиль по умолчанию для всех текстовых полей -->
<Style TargetType="TextBox">
<Setter Property = "FontSize" Value =4"/>
<Setter Property = "Width" Value = 00'7>
<Setter Property = "Height" Value = 0"/>
<Setter Property = "BorderThickness" Value = "/>
<Setter Property = "BorderBrush" Value = "Red"/>
<Setter Property = "FontStyle" Value = "Italic"/>
</Style>
Можно определить любое количество элементов управления TextBox, и все они
автоматически получат определенный внешний вид. Если конкретному элементу TextBox
не нужен внешний вид и поведение по умолчанию, он может отказаться от него,
установив свойство Style в {x:Null}. Например, элемент txtTest будет иметь безымянный
стиль по умолчанию, а элемент txtTest2 сделает все по-своему:
1164 Часть VI. Построение настольных пользовательских приложений с помощью WPF
«'TextBox x:Name = IItxtTest"/>
^TextBox x:Name = ,,txtTest2" Style=" { x :Null} " BorderBrush="Black"
BorderThickness = ,,5" Height = ,,60" Width=00" Text="Ha'"/ >
Определение стилей с триггерами
Стили WPF могут также содержать триггеры, упаковывая объекты Trigger в
коллекцию Triggers объекта Style. Использование триггеров в стиле позволяет
определить некоторые элементы <Setter> таким образом, что они будут применены только
в случае истинности определенного условия триггера. Например, возможно, требуется
увеличить размер шрифта, когда курсор мыши наведен на кнопку. Или, может быть,
требуется обеспечить, чтобы текстовое поле с текущим фокусом было подсвечено
желтым фоном. Ниже показана соответствующая разметка:
<!-- Стиль по умолчанию для всех текстовых полей -->
Style TargetType=,,TextBox">
• Setter Property = "FontSize" Value = 4"/:-
'Setter Property = "Width" Value = 00"/>
^Setter Property = "Height" Value = 0"/^
Setter Property = "BorderThickness" Value = "/>
<Setter Property = "BorderBrush" Value = "Ped" />
•"Setter Property = "FontStyle" Value = "Italic"/^
<!-- Следующая установка будет применена, только когда
текстовое поле находится в фокусе -->
<Style.Triggers>
<Trigger Property = "IsFocused" Value = "True" •
<Setter Property = "Background" Value = "Yellow"/>
</Trigger>
<!Style.Triggers>
</Style>
Опробовав этот стиль, вы обнаружите, что по мере перехода с помощью клавиши
<ТаЬ> между объектами TextBox текущий выбранный TextBox получает желтый фон
(если только это не отключено присваиванием {x:Null} свойству Style).
Триггеры свойств также весьма интеллектуальны, в том смысле, что когда условие
триггера не истинно, свойство автоматически получает значение, присвоенное по
умолчанию. Поэтому, как только TextBox теряет фокус, он также автоматически принимает
цвет по умолчанию, без какого-либо вашего участия. В отличие от этого, триггеры
событий (которые рассматривались при описании анимации WPF) не возвращаются
автоматически к предыдущему состоянию.
Определение стилей с множеством триггеров
Триггеры также могут быть спроектированы так, что определенные элементы
<Setter> будут применены, когда истинно множество условий (это похоже на
построение оператора if для множества условий). Предположим, что необходимо установить
фон Yellow для элемента TextBox только в том случае, если он имеет активный
фокус и курсор мыши наведен на него. В этом случае можно воспользоваться элементом
<MultiTrigger> для определения каждого условия:
<!— Стиль по умолчанию для всех текстовых полей -->
--Style TargetType = "TextBox">
<Setter Property = "FontSize" Value =4"/>
<Setter Property = "Width" Value = 00"/>
^Setter Property = "Height" Value = 0"/>
<Setter Property = "BorderThickness" Value = "/>
<Setter Property = "BorderBrush" Value = "Red"/>
<Setter Property = "FontStyle" Value = "Italic"/"-
Глава 30. Ресурсы, анимация и стили WPF 1165
<!-- Следующая установка будет применена, только когда
текстовое поле в фокусе И на него наведен курсор мыши —>
<Style.Triggers>
<MultiTrigger>
<MultiTrigger . Conditioris>
<Condition Property = "IsFocused" Value = "True"/->
<Condition Property = "IsMouseOver" Value = "True"/>
</MultiTrigger.Conditions>
<"Setter Property = "Background" Value = "Yellow"/>
</MultiTrigger>
</Style.Triggers>
</Style>
Анимированные стили
Стили также могут включать триггеры, которые запускают последовательность
анимации. Ниже приведен последний стиль, который, будучи примененным к элементам
управления Button, заставит элемент расти и сжиматься в размере, когда курсор мыши
находится внутри области поверхности кнопки:
<!— Стиль растущей кнопки —>
<Style x:Key = "GrowingButtonStyle" TargetType="Button">
<Setter Property = "Height" Value = 0'7>
<Setter Property = "Width" Value = 00"/>
<Style.Triggers>
<Tngger Property = "IsMouseOver" Value = "True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard TargetProperty = "Height">
<DoubleAnimation From = 0" To = 00"
Duration = :0:2" AutoReverse="True"/>
</Storyboard>
</Beginstoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
Здесь коллекция триггеров проверяет истинность свойства IsMouseOver. Если оно
истинно, определяется элемент <Trigger.EnterActions> для запуска простой
раскадровки, которая заставляет кнопку за две секунды увеличиться до значения Height,
равного 200 (и затем вернуться к значению Height, равному 40). Чтобы добавить
другие изменения свойств, можно также определить область <Trigger.ExitActions> для
определения любых специальных действий, которые должны быть выполнены, когда
IsMouseOver равно false.
Программное применение стилей
Вспомните, что стиль может быть применен также во время выполнения. Это
полезно, когда нужно позволить конечным пользователем выбирать, как должен выглядеть их
пользовательский интерфейс, либо если требуется принудительно применить внешний
вид и поведение на основе настроек безопасности (например, стиль DisableAllButton)
или каких-то других условий.
В этом проекте уже определено множество стилей, и многие из них могут быть
применены к элементам управления Button. Теперь давайте переделаем пользовательский
интерфейс главного окна, чтобы позволить пользователю выбирать из некоторых этих
стилей, указывая имя в списке ListBox. На основе такого выбора будет применяться
соответствующий стиль. Ниже показана финальная разметка элемента <Window>:
1166 Часть VI. Построение настольных пользовательских приложений с помощью WPF
<Window x:Class="WpfStyles.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height=,,350" Title="A Window with Style1"
Width=2511 Windows tar tupLocation=" Center Screen ">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Top">
<Label Content="Please Pick a Style for this Button" Height=0"/>
<ListBox x:Name ="lstStyles" Height ="80" Width =50"
Background="LightBlue" SelectionChanged ="comboStyles_Changed" />
</StackPanel>
<Button x:Name="btnStyle" Height=0" Width=00" Content="OK!"/>
</DockPanel>
</Window>
Элемент управления ListBox (по имени lststyles) будет определен динамически,
когда это потребуется конструктору окна:
public MainWindow()
{
InitializeComponent ();
// Наполнить это окно списка всеми стилями Button.
IstStyles.Items.Add("GrowingButtonStyle");
lststyles.Items.Add("TiltButton");
IstStyles.Items.Add("BigGreenButton") ;
lststyles.Items.Add("BasicControlStyle");
}
Последняя задача связана с обработкой события SelectionChanged в
соответствующем файле кода. Обратите внимание, что в следующем коде можно извлекать текущий
ресурс по имени, используя унаследованный метод TryFindResouce():
private void comboStyles_Changed (object sender, SelectionChangedEventArgs e)
{
// Получить стиль, выбранный из списка.
Style currStyle = (Style)
TryFindResource(IstStyles.SelectedValue);
if (currStyle != null)
{
// Установить стиль типа кнопки.
this.btnStyle.Style = currStyle;
Запустив это приложение, можно производить выбор одного из четырех стилей
кнопок на лету. На рис. 30.19 показано готовое приложение.
Исходный код. Проект Wpf Styles доступен в подкаталоге Chapter 30.
Генерация стилей с помощью Expression Blend
В завершение исследования стилей WPF давайте посмотрим, как Expression Blend
может автоматизировать процесс конструирования стиля. Обсуждение редактора
анимации (и связанные с ним темы, такие как триггеры и шаблоны элементов
управления) откладывается до следующей главы. Здесь будут рассматриваться стили из группы
Simple Styles (Простые стили), доступные в окне Assets Library (Библиотека активов).
Глава 30. Ресурсы, анимация и стили WPF 1167
A Window with Si
Please Pick a Styte for this Button
TiltButton
BigGreenButton
BasicControlStyte
Рис. 30.19. Элементы управления с разными стилями
Работа с визуальными стилями по умолчанию
Инструмент Expression Blend поставляется с множеством стилей по умолчанию для
распространенных элементов управления, которые вместе образуют группу под
названием Simple Styles. Открыв окно Assets Library, в древовидном представлении слева
легко обнаружить группу Styles (Стили), как показано на рис. 30.20.
Рис. 30.20. Стили по умолчанию в Expression Blend
При выборе одного из этих простых стилей происходит несколько вещей. Первым
делом, элемент управления объявляется с расширением разметки {DynamicResource}.
Например, со стилем Simple Button Style связана примерно следующая XAML-разметка:
<Button Margin=16,49,220,0" Style="{DynamicResource SimpleButton}"
VerticalAlignment="Top11 Height=311 Content=IIButton"/>
Как ни странно, элемент управления будет выглядеть как нормальный Button.
Однако, заглянув в окно Projects (Проекты), вы увидите, что Expression Blend добавил
новый файл по имени Simple Styles.xaml (рис. 30.21).
Дважды щелкнув на этом элементе и открыв редактор XAML, вы увидите очень
большой раздел <ResourceDictionary> стилей элементов управления по умолчанию. Более
того, открыв окно Resources (Ресурсы), можно обнаружить, что каждый перечисленный
элемент может редактироваться щелчком на заданной позиции (рис. 30.22).
Если щелкнуть на значке SimpleButton в представлении Resources, откроется
новый визуальный конструктор, с помощью которого можно соответствующим образом
изменить стиль, используя стандартные инструменты редактирования Blend (окно
Properties, редактор анимации и т.п.). Все это показано на рис. 30.23.
1168 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Projects ■ Assets Triggers States
Search
Щ Solution "WpfApplkationo' A project^))
* |S Wpf Appbcation*
> Щ Re* ~
Рис. 30.21. Это словарь ресурсов
Properties Resources * Data
в*
DisabledForegroundBrush
Disabled BaclcgroundBrush Щ
Disabled BorderBrush ii
WindowBackgroundBrush Щ
Defaulted BorderBrush
SolidBorderBrush
Light BorderBrush
LightColorBfush |
GlyphBrush
Sim pieBiittonFocusVisual
SimpleButton
rh^_ _. in_„.
SimpteChedcBox
Simp*eRad»oButton
SimpteRepeatButton
Sim pleThumbStyle
Sim pteScrol IRepeatButton Style
SimpleScrollBar
SlmpteSorollViewer
Sim pietist Box
SimpleListBoxItem
ъ
ШШШШШШ*
H '
|-
™™:i
шшшшш-
Щ .
JEdrl resource 1
О
«r
•
«
■
■
•
Рис. 30.22. Выбор стиля по умолчанию для
редактирования
jjTj WpfApplication>6.sln - Microsoft Expression Blend 3
Object Project Tools Window Help
III* Solution ~'
. Ц0 WptAppfcoihor*
m
GO
Appjcaml* MainWindow-x
'■
с
<
1
Properties ' Resources
Type Button
Search
* Brushes
Background |
BorderBrush ^m
Foreground
r ,
1
Data * ■
ЕИ
1°
r resources
R 231
|Ш --1
fsr~l
ESSBfl
Рис. 30.23. Редактирование стиля по умолчанию
Глава 30. Ресурсы, анимация и стили WPF 1169
Изящество этого подхода в том, что файл Simple Styles.xaml объединяется с
ресурсами приложения, и потому все локальные изменения будут автоматически
компилироваться в окончательное приложение. Использование простых стилей по умолчанию
позволяет получить начальную разметку для нормального внешнего вида и поведения
элемента управления и его настройки его на уровне проекта. Ниже показана разметка,
которая должна находиться в файле App.xaml:
<Application
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/200 6/xaml"
x:Class="WpfApplication6.App"
StartupUri="MainWindow.xaml">
<Application.Resources>
<!-- Здесь определяются ресурсы, доступные на уровне приложения —>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Simple Styles.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
На этом глава завершена. В следующей главе ваши знания о WPF будут дополнены
рассмотрением шаблонов элементов управления и специальных классов UserControl.
Попутно также будет проиллюстрировано несколько новых аспектов работы с Expression
Blend.
Резюме
В первой части этой главы рассматривалась система управления ресурсами WPF.
Мы начали с изучения работы с двоичными ресурсами, а затем ознакомились с ролью
объектных ресурсов. Вы узнали, что объектные ресурсы — это именованные фрагменты
XAML-разметки, которые могут быть сохранены в различных местах, позволяя
многократно использовать это содержимое.
После этого был описан API-интерфейс анимации WPF. Рассматривались
некоторые примеры создания анимации в коде С#, а также в XAML-разметке. Для
управления выполнением анимации, определенной в разметке, служат элементы и триггеры
Storyboard.
В завершение главы был рассмотрен механизм стилей WPF, который интенсивно
использует графику, объектные ресурсы и анимации. Попутно вы узнали о том, как
использовать Expression Blend для упрощения создания стилей с применением простых
стилей по умолчанию в качестве отправной точки.
ГЛАВА 31
Шаблоны элементов
управления WPF
и пользовательские
элементы управления
В этой главе исследование модели программирования Windows Presentation
Foundation (WPF) завершается рассмотрением вопросов построения специальных
элементов управления. Хотя модель содержимого и механизм стилей позволяют
добавлять уникальные части к стандартным элементам управления WPF, процесс построения
специальных шаблонов и элементов UserControl дают возможность полностью
определить то, как элемент управления должен визуализировать свой вывод, отвечать на
переходы между состояниями и интегрироваться в API-интерфейсы WPF
Глава начинается с рассмотрения двух тем, которые важны для создания
специального элемента управления, а именно — свойств зависимости (dependency properties) и
маршрутизируемых событий (routed events). После освоения этих тем мы перейдем к
изучению роли шаблона по умолчанию и программному взаимодействию с ним во
время выполнения.
Оставшаяся часть этой главы посвящена построению классов UserControl с
помощью Visual Studio 2010 и Expression Blend. Попутно вы узнаете больше о триггерах WPF
(впервые представленных в главе 30), а также о новом механизме Visual State Manager
(VSM), появившемся в .NET 4.0. Наконец, будет показано, как использовать редактор
анимации Expression Blend для определения переходов между состояниями, и увидите,
как включать специальные элементы управления в более крупные WPF-приложения.
Роль свойств зависимости
Подобно любому API-интерфейсу .NET, внутри реализации WPF используются все
члены системы типов .NET (классы, структуры, интерфейсы, делегаты, перечисления)
и каждый член типа (свойства, методы, события, константные данные, поля только для
чтения и т.п.). Однако WPF также поддерживает уникальную программную концепцию
под названием свойства зависимости (dependency property).
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1171
На заметку! Свойства зависимости — это также часть API-интерфейса Window Workflow Foundation
(WF). Хотя свойство зависимости WF конструируется способом, аналогичным свойству
зависимости WPF, при реализации в WF используется другой стек технологий.
Подобно "нормальному" свойству .NET (которое в литературе, посвященной WPF,
часто называют свойством CLR (CLR property)), свойства зависимости могут
устанавливаться декларативно, с использованием XAML или программно в файле кода. Более
того, свойства зависимости (подобно свойствам CLR) в конечном итоге предназначены
для инкапсуляции полей данных класса и могут быть сконфигурированы как доступные
только для чтения, только для записи или для чтения и записи.
Что более интересно: почти в любом случае вы будете пребывать в счастливом
неведении относительно того, что на самом деле устанавливаете (или читаете) свойство
зависимости, а не свойство CLR! Например, свойства Height и Width, которые элементы
управления WPF наследуют от FrameworkElement, а также член Content,
унаследованный от ControlContent — все это фактически свойства зависимости:
<!— Установить три свойства зависимости -->
<Button x:Name = "btnMyButton" Height = 0" Width = 00" Content = "OK"/>
С учетом всех этих сходств возникает вопрос: зачем нужно было определять в WPF
новый термин для такой знакомой концепции? Причина кроется в способе реализации
свойства зависимости внутри класса. Ниже будет показан пример кодирования; однако
на высшем уровне все свойства зависимости создаются следующим образом.
• Прежде всего, класс, определяющий свойство зависимости, должен иметь в своей
цепочке наследования DependencyObject.
• Единственное свойство зависимости представляется, как общедоступное,
статическое, предназначенное только для чтения поле в классе типа DependencyProperty.
По существующему соглашению имя этого поля формируется добавлением слова
Property к имени оболочки CLR (см. последний пункт этого списка).
• Переменная DependencyProperty зарегистрирована с помощью статического
вызова DependencyProperty.Register (), что обычно происходит в статическом
конструкторе или встроенным образом, при объявлении переменной.
• Наконец, класс определит дружественное к XAML свойство CLR, которое
осуществляет вызовы методов, предоставленных DependencyObject, для получения и
установки значения.
Будучи реализованными, свойства зависимости представляют множество мощных
средств, используемых различными технологиями WPF, включая привязку данных,
службы анимации, стили и т.д. В основе своей мотивация свойств зависимости
заключается в предоставлении способа вычисления значения свойства на основе значений
других источников. Ниже приведен список некоторых основных преимуществ, которые
выходят за рамки простой инкапсуляции данных, имеющейся в свойстве CLR.
• Свойства зависимости могут наследовать свои значения от XAML-определения
родительского элемента. Например, если вы определите значение атрибута
FontSize в открывающем дескрипторе <Window>, то все элементы управления в
этом Window будут по умолчанию иметь этот размер шрифта.
• Свойства зависимости поддерживают возможность иметь значения,
установленные элементами, находящимися в области видимости XAML, такие как
установка Button свойства Dock родительской DockPanel. (Вспомните из главы 28, что
присоединяемые свойства делают именно это, поскольку присоединяемые
свойства — это разновидность свойств зависимости.)
1172 Часть VI. Построение настольных пользовательских приложений с помощью WPF
• Свойства зависимости позволяют WPF вычислять значение на основе нескольких
внешних значений, что может быть очень важно для анимации и служб привязки
данных.
• Свойства зависимости предоставляют инфраструктуру поддержки для триггеров
WPF (также довольно часто используемых при работе с анимацией и привязкой
данных).
Запомните, что во многих случаях вы будете взаимодействовать с существующим
свойством зависимости в манере, идентичной нормальному свойству CLR (благодаря
оболочке XAML). Однако когда в главе 28 шла речь о привязке данных, вы узнали, что
если нужно установить привязку данных в коде, то следует вызвать метод SetBindingO
на целевом объекте операции и указать свойство зависимости, которым он будет
оперировать:
private void SetBindings ()
{
Binding b = new Binding();
b.Converter = new MyDoubleConverter();
b.Source = this.mySB;
b.Path = new PropertyPath("Value");
// Указать свойство зависимости.
this.labelSBThumb.SetBinding(Label.ContentProperty, b) ;
}
Подобный код также применялся, когда в предыдущей главе шла речь о запуске
анимации в коде:
// Указать свойство зависимости.
rt.BeginAnimation(RotateTransform.AngleProperty, dblAnim);
Единственный случай, когда может понадобиться строить собственное специальное
свойство зависимости — во время разработки элемента управления WPF. Например,
при построении класса UserControl с четырьмя специальными свойствами, которые
должны тесно интегрироваться в API-интерфейсом WPF, они должны быть написаны с
использованием логики свойств зависимости.
В частности, если свойства должны быть целью привязки данных или операции
анимации, если свойство должно уведомлять о своем изменении, если оно должно уметь
работать, как Setter в стиле WPF, или если оно должно уметь получать свои значения
от родительского элемента, то обычного свойства CLR для этого недостаточно. В
случае применения обычного свойства другие программисты смогут получать и
устанавливать значение, однако если они попытаются использовать такие свойства в контексте
службы WPF, все будет работать не так, как ожидалось. Поскольку никогда заранее не
известно, как другие пожелают взаимодействовать со свойствами специального класса
UserControl, следует выработать привычку всегда определять свойства зависимости
при построении специальных элементов управления.
Проверка существующего свойства зависимости
Прежде чем вы узнаете, как строить специальное свойство зависимости,
давайте взглянем на внутреннюю реализацию свойства Height класса FrameworkElement.
Соответствующий код показан ниже (с комментариями); тот же код можно просмотреть
самостоятельно с помощью утилиты reflector.exe.
// FrameworkElement "является" DependencyObject.
public class FrameworkElement : UIElement, IFrameworklnputElement,
IlnputElement, ISupportlnitialize, IHaveResources, IQueryAmbient
{
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1173
// Статическое свойство только для чтения Dependency-Property.
public static readonly DependencyProperty HeightProperty;
// Поле DependencyProperty часто регистрируется
//в статическом конструкторе класса.
static FrameworkElement()
HeightProperty = DependencyProperty.Register (
"Height",
typeof(double),
typeof(FrameworkElement) ,
new FrameworkPropertyMetadata((double) 1.0 / (double) 0.0,
FrameworkPropertyMetadataOptions.AffectsMeasure,
new PropertyChangedCallback(FrameworkElement.OnTransformDirty)),
new ValidateValueCallback(FrameworkElement.IsWidthHeightValid));
}
// Оболочка CLR, реализованная унаследованными
// методами GetValue ()/SetValue().
public double Height
{
get { return (double) base.GetValue(HeightProperty); }
set { base.SetValue(HeightProperty, value); }
}
}
Как видите, свойства зависимости требуют порядочного дополнительного кода по
сравнению с нормальным свойством CLR. На самом деле зависимость может оказаться
еще более сложной, чем здесь показано.
Прежде всего, помните, что если класс желает определить свойство зависимости,
он должен иметь DependencyObject в своей цепочке наследования, поскольку в этом
классе определены методы GetValue() и SetValue(), используемые оболочкой CLR.
Поскольку FrameworkElement "является" ("is-a") DependencyObject, это требование
удовлетворено.
Далее вспомните, что сущность, которая будет хранить действительное значение
свойства (в случае Height это double), представлено как общедоступное, статическое;
предназначенное только для чтения поле типа DependencyProperty. В соответствии с
существующим соглашением имя этого свойства всегда должно быть снабжено
суффиксом Property, добавленным к имени связанной оболочки CLR, как показано ниже:
public static readonly DependencyProperty HeightProperty;
Учитывая, что свойства зависимости объявлены как статические поля, они
обычно создаются (и регистрируются) внутри статического конструктора класса. Объект
DependencyProperty создается вызовом статического метода DependencyProperty.
Register(). Этот метод многократно переопределен; однако в случае Height метод
DependencyProperty.Register () вызывается следующим образом:
HeightProperty = DependencyProperty.Register(
"Height",
typeof(double),
typeof(FrameworkElement),
new FrameworkPropertyMetadata((doubleH.0,
FrameworkPropertyMetadataOptions.AffectsMeasure,
new PropertyChangedCallback(FrameworkElement.OnTransformDirty)),
new ValidateValueCallback(FrameworkElement.IsWidthHeightValid));
1174 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Первый аргумент DependencyProperty.Register () — это имя нормального
свойства CLR класса (в данном случае Height), в то время как второй аргумент несет
информацию о лежащем в основе типе инкапсулированных данных (double). Третий аргумент
указывает информацию о типе класса, к которому относится это свойство (в данном
случае — FrameworkElement). Хотя это может показаться избыточным (в конце
концов, поле HeightProperty утке определено внутри класса FrameworkElement), это очень
разумный аспект WPF, который позволяет одному классу регистрировать свойства на
другом (даже если определение класса запечатано (sealed)).
Четвертый аргумент, переданный DependencyProperty.RegisterO в данном
примере — это то, что на самом деле обеспечивает свойствам зависимости их
уникальные характеристики. Здесь передается объект FrameworkPropertyMetadata,
описывающий различные детали о том, как среда WPF должна обрабатывать это свойство
в отношении уведомлений обратного вызова (если свойство должно извещать других
об изменениях своего значения) и различные опции (представленные перечислением
FrameworkPropertyMetadataOptions). Значения FrameworkPropertyMetadataOptions
управляют тем, что именно затрагивается данным свойством (работает ли оно с
привязкой данных, может ли наследоваться, и т.п.). В данном случае аргументы
конструктора FrameworkPropertyMetadata описываются следующим образом:
new FrameworkPropertyMetadata(
// Значение свойства по умолчанию.
(doubleH.0,
// Опции метаданных.
FrameworkPropertyMetadataOptions.AffectsMeasure,
// Делегат, указывающий на свойство, вызываемое при изменении свойства.
new PropertyChangedCallback(FrameworkElement.OnTransformDirty)
)
Поскольку финальный аргумент конструктора FrameworkPropertyMetadata
является делегатом, обратите внимание, что этот параметр конструктора указывает На
статический метод класса FrameworkElement по имени OnTransformDirty(). Хотя
код этого метода подробно рассматриваться не будет, имейте в виду, что всякий раз,
когда строится специальное свойство зависимости, можно специфицировать делегат
PropertyChangeCallback для указания на метод, который будет вызван, когда
изменяется значение свойства.
Это приводит к финальному параметру, переданному в метод DependencyProperty.
Register () — второму делегату типа ValidateVakueCallback, который указывает На
метод класса FrameworkElement, вызываемый для проверки достоверности значения,
присвоенного свойству:
new ValidateValueCallback(FrameworkElement.IsWidthHeightValid)
Этот метод содержит логику, которую можно ожидать найти в блоке set свойства
(подробнее об этом рассказывается в следующем разделе):
private static bool IsWidthHeightValid(object value)
{
double num = (double) value;
return ((!DoubleUtil.IsNaN(num) && (num >= 0.0))
&& !double.IsPositivelnfinity(num));
}
Как только объект DependencyProperty зарегистрирован, остается решить
последнюю задачу — поместить поле в оболочку обычного свойства CLR (в данном
случае — Height). Однако обратите внимание, что блоки get и set не просто возвращают
или устанавливают значение double переменной-члена класса, но делают это непря-
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1175
мо, используя методы GetValueO и SetValueO из базового класса System.Windows.
DependencyObject:
public double Height
{
get { return (double) base.GetValue(HeightProperty); }
set { base.SetValue(HeightProperty, value); }
}
Важные замечания относительно оболочек свойств CLR
Резюмируя сказанное выше, отметим, что свойства зависимости выглядят как
обычные нормальные свойства, когда вы получаете или устанавливаете их значения в XAML-
разметке или в коде, но "за кулисами" они реализованы с применением намного более
изощренной техники кодирования. Помните, что основная причина для прохождения
этого процесса состоит в построении специального элемента управления, имеющего
специальные свойства, которые должны быть интегрированы со службами WPF,
требующими взаимодействия со свойством зависимости (например, анимацией, привязкой
данных и стилями).
Несмотря на то что часть реализации свойства зависимости включает определение
оболочки CLR, вы никогда не должны помещать логику проверки достоверности в блок
set. И потому оболочка CLR свойства зависимости никогда не должна делать ничего
помимо вызовов GetValueO или SetValueO.
Причина в том, что исполняющая среда WPF сконструирована таким образом, что в
случае написания XAML-разметки, устанавливающей свойство, как показано ниже:
<Button x:Name="myButton" Height=00" .../>
исполняющая среда полностью минует блок set свойства Height и
непосредственно вызывает SetValueO! Причина столь странного поведения кроется в оптимизации.
Если бы исполняющей среде WPF пришлось непосредственно вызывать блок set
свойства Height, то ей нужно было бы выполнить рефлексию времени выполнения для
нахождения поля DependencyProperty (указанного в первом аргументе SetValueO),
обращаться к нему в памяти, и т.д.
Но раз так, то зачем вообще строить оболочку CLR? Дело в том, что XAML в WPF
не позволяет вызывать функции в разметке, так что следующий фрагмент был бы
ошибочным:
<!-- Ошибка1 Вызывать методы в XAML-разметке для WPF нельзя! -->
<Button x:Name="myButton" this.SetValue(00" ) .../>
Установка или получение значения в разметке с использованием оболочки CLR
должна восприниматься как способ указать исполняющей среде WPF о
необходимости вызвать GetValue ()/SetValue (), поскольку напрямую это делать в разметке не
разрешается.
Что произойдет, если вызвать оболочку CLR в коде следующим образом?
Button b = new Button () ;
b.Height =10;
В этом случае, если бы блок set свойства Height содержал какой-то код помимо
вызова SetValueO, он должен был бы выполняться, так как оптимизация
анализатора WPF XAML не вызывается. Краткий ответ может быть сформулирован так: когда вы
регистрируете свойство зависимости, применяйте делегат ValidateValueCallback для
указания на метод, выполняющий проверку достоверности данных. Это гарантирует
правильное поведение, независимо от того, используется XAML или код для получения/
установки свойства зависимости.
1176 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Построение специального свойства зависимости
Если к настоящему возникла небольшая путаница, то это совершенно нормально.
Построение свойств зависимости может требовать некоторого времени на привыкание.
Однако, как бы то ни было, это часть процесса построения многих специальных
элементов управления WPF, так что давайте посмотрим, как строится свойство зависимости.
Начните с создания приложения WPF по имени CustomDepPropApp. Выберите пункт
меню Projects Add New Item (Проекте Добавить новый элемент), укажите в качестве
шаблона User Control (WPF) (Пользовательский элемент управления (WPF))), и назначьте
ему имя ShowNumberControl.xaml (рис. 31.1).
Т | , Resource Dictionary (WPF) Visual C#
jT About Box Visual C#
. A, ADO.NET Entity Data Model Visual C#
Рис. 31.1. Вставка нового специального элемента UserControl
На заметку! Дополнительные сведения о классе UserControl из WPF представлены далее в
главе.
Как и окно, типы WPF UserControl имеют ассоциированный с ними файл XAML и
связанный файл кода. Обновим XAML-разметку пользовательского элемента
управления, добавив элемент управления Label в Grid:
<UserControl x:Class="CustomDepPropApp.ShowNumberControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight=,,300" d:DesignWidth=00">
<Grid>
<Label x:Name=,,numberDisplay" Height=0" Width=00"
Васkground="LightBlue"/>
</Grid>
</UserControl>
В файле кода этого специального элемента управления создайте обычное свойство
.NET, которое упаковывает int и устанавливает новое значение для свойства Content
элемента Label:
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1177
public partial class ShowNumberControl : UserControl
{
public ShowNumberControl ()
{
InitializeComponent();
}
// Нормальное свойство .NET
private int currNumber = 0;
public int CurrentNumber
{
get { return currNumber; }
set
{
currNumber = value;
numberDisplay .Content = CurrentNumber .ToStnng () ;
}
}
Теперь обновите определение XAML окна, чтобы объявить экземпляр специального
элемента управления внутри диспетчера компоновки StackPanel. Поскольку
специальный элемент управления — не часть основного стека сборок WPF, потребуется
определить специальное пространство имен XML, которое отобразится на этот элемент (см.
главу 27). Ниже показана необходимая разметка:
<Window х:Class="CustomDepPropApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns : myCtrls="clr-namespace: CustomDepPropApp"
Title="Simple Dependency Property App" Height=50" Width=50"
WindowstartupLocation="CenterScreen">
<StackPanel>
<myCtrls:ShowNumberControl x:Name="myShowNumberCtrl" CurrentNumber=00"/>
</StackPanel>
</Window>
Как видите, визуальный конструктор Visual Studio 2010 вроде бы корректно
отображает значение, установленное в свойстве CurrentNumber (рис. 31.2).
«ПК
Рис. 31.2. Свойство выглядит вполне работоспособным
А что если необходимо применить объект анимации к свойству CurrentNumber,
чтобы значение изменялось от 100 до 200 за период в 10 секунд? Желая сделать это в
разметке, можно было бы обновить раздел <myCtrls:ShowNumberControl> следующим
образом:
<myCtrls : ShowNumberControl х : Name=llmyShowNumberCtrl" CurrentNumber=" 100">
<myCtrls : ShowNumberControl. Tnggers>
1178 Часть VI. Построение настольных пользовательских приложений с помощью WPF
<EventTrigger RoutedEvent = "myCtrls:ShowNumberControl.Loaded">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard TargetProperty = "CurrentNumber">
<Int32Animation From = 00" To = 00" Duration = :0:10"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</myCtrls:ShowNumberControl.Triggers>
</myCtrls:ShowNumberControl>
Если попробовать запустить это приложение, то объект анимации не сможет
найти правильную цель, и потому будет проигнорирован. Причина в том, что свойство
CurrentNumber не зарегистрировано как свойство зависимости! Чтобы исправить это,
вернемся к файлу кода специального элемента управления и полностью
закомментируем текущую логику свойства (включая приватное поле заднего плана). Теперь поместите
курсор внутрь области класса и введите фрагмент кода propdp (рис. 31.3).
ShowNumberControUamkcs* X I
Э
^CustomDepPropApp.ShowNurnberControl
;
InitializeComponent();
; •% DependencyPropertyHelper
; *t$ DependencyPropertyKey
\ И$ FrameworkPropertyMetadata
:-jrf" FrameworkPropertyMetadataOptions
^3 prop
JS| propa
..] propdp
; { ) Properties
:J| PropertyChangedCaltback
m
_
IStringOj
Рис. 31.3. Фрагмент кода progdp предоставляет начальную точку
для построения свойства зависимости
После ввода propdp нажмите два раза клавишу <ТаЬ>. Фрагмент кода развернется,
предоставив базовый скелет свойства зависимости (рис. 31.4).
ShowNurnberControljcamt.cs* X |g|
ш
шт
*t% CustomDepPropApp.ShowNu mberControf
['JfMyProperty
public int MyPropertyj
{
get { return (int)GetValue(HyPrcpertyProperty)j }
set { SetValue(rtyPropertyProp€rty, value); }
}
// Using a DependencyProperty as the backing store for HyProperty. This enables animation, styling, binding,
public static readonly DependencyProperty MyPropertyProperty =
DependencyProperty.Register("MyProperty", typeof(int), typeof(ownerxlpss)., new UIPropertyMetadata@))j
Рис. 31.4. Развернутый фрагмент
Простейшая версия свойства CurrentNumber будет выглядеть так:
public partial class ShowNumberControl : UserControl
Глава 31, Шаблоны элементов управления WPF и пользовательские элементы управления 1179
public int CurrentNumber
{
get { return (int)GetValue(CurrentNumberProperty); }
set { SetValue(CurrentNumberProperty, value); }
}
public static readonly DependencyProperty CurrentNumberProperty =
DependencyProperty.Register("CurrentNumber",
typeof(int) ,
typeof(ShowNumberControl) ,
new UIPropertyMetadata@));
}
Это подобно тому, что вы видели в реализации свойства Height; однако фрагмент
кода регистрирует свойство встроенным способом, а не в статическом конструкторе
(что хорошо). Также обратите внимание, что объект UIPropertyMetadata
используется для определения целого значения по умолчанию @) вместо более сложного объекта
FrameworkPropertyMetadata.
Добавление процедуры проверки достоверности данных
Хотя теперь есть свойство зависимости по имени CurrentNumber, все же анимация
в действии не видна. Следующая необходимая поправка, которая может понадобиться,
состоит в указании функции для выполнения некоторой логики проверки
достоверности. Для целей данного примера предположим, что нужно обеспечить, чтобы значение
свойства CurrentNumber находилось в пределах от 0 до 500.
Для этого добавьте финальный аргумент к методу DependencyProperty. Register () типа
ValidateValueCallback, указывающий на метод по имени ValidateCurrentNumber.
ValidateValueCallback — это делегат, который может только указывать на
методы, возвращающие bool и принимающие object в качестве единственного
аргумента. Этот object представляет новое значение, которое присваивается. Реализуйте
ValidateCurrentNumber для возврата true, если входящее значение находится в
заданном диапазоне, и false — в противном случае:
public static readonly DependencyProperty CurrentNumberProperty =
DependencyProperty.Register("CurrentNumber", typeof(int),
typeof(ShowNumberControl) ,
new UIPropertyMetadataA00),
new ValidateValueCallback(ValidateCurrentNumber));
public static bool ValidateCurrentNumber(object value)
{
// Простое бизнес-правило: значение должно лежать между 0 и 500.
if (Convert.ToInt32(value) >= 0 && Convert.ToInt32(value) <= 500)
return true;
else
return false;
}
Реакция на изменение свойства
Итак, допустимое число уже имеется, но все еще нет анимации. Последнее
изменение, которое потребуется внести — это задать второй аргумент конструктора
UIPropertyMrtadata, которым является PropertyChangedCallback. Этот делегат
может указывать на любой метод, принимающий DependencyObject в качестве первого
параметра и DependencyPropertyChangeEventArgs — в качестве второго. Сначала
обновите код следующим образом:
1180 Часть VI. Построение настольных пользовательских приложений с помощью WPF
// Обратите внимание на второй параметр конструктора UIPropertyMetadata.
public static readonly DependencyProperty CurrentNumberProperty =
DependencyProperty.Register("CurrentNumber", typeof(int),
typeof(ShowNumberControl),
new UIPropertyMetadataA00,
new PropertyChangedCallback(CurrentNumberChanged)),
new ValidateValueCallback(ValidateCurrentNumber));
Конечной целью внутри метода CurrentNumberChamgedO является изменение
свойства Content объекта Label на новое значение, присвоенное свойством CurrentNumber.
Однако здесь присутствует серьезная проблема: метод CurrentNumberChanged()
является статическим, поскольку он должен работать со статическим объектом
DependencyProperty. Тогда как же получить доступ к Label для текущего экземпляра
ShowNumberControl? Эта ссылка находится в первом параметре DependencyObject.
Новое значение можно найти, используя входные аргументы события. Ниже показан
необходимый код, который изменит свойство Content объекта Label:
private static void CurrentNumberChanged(DependencyObject depObj,
DependencyPropertyChangedEventArgs args)
{
// Привести DependencyObject к ShowNumberControl.
ShowNumberControl с = (ShowNumberControl)depObj;
// Получить элемент Label в ShowNumberControl.
Label theLabel = с.numberDisplay;
// Установить в Label новое значение.
theLabel.Content = args.NewValue.ToString();
}
Какой длинный путь пришлось пройти, чтобы всего лишь изменить содержимое
метки! Однако преимущество в том, что свойство зависимости CurrentNumber теперь
может быть целью для стиля WPF, объекта анимации, операций привязки данных и т.д.
На рис. 31.5 показано завершенное приложение (теперь оно действительно меняет
значение во время выполнения).
Рис. 31.5. Наконец-то анимация работает!
На этом обзор свойств зависимости WPF завершен. Имейте в виду, что хотя вы
получили определенное представление о назначении этих конструкций, многие детали не
были раскрыты.
Если вы окажетесь в ситуации, когда нужно строить множество специальных
элементов управления, поддерживающих специальные свойства, загляните в подраздел
"Properties" ("Свойства") узла "WPF Fundamentals" ("Основы WPF") документации .NET
Framework 4.0 SDK. Там вы найдете намного больше примеров построения свойств
зависимости, присоединяемых свойств, различные способы конфигурирования
метаданных и массу других деталей.
Исходный код. Проект CustomDepPropApp доступен в подкаталоге Chapter 31.
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1181
Маршрутизируемые события
Свойства — это не единственная программная конструкция .NET, которая требует
настройки, чтобы хорошо работать с API-интерфейсом WPF. Стандартная модель
событий CLR также претерпела некоторые усовершенствования, чтобы обеспечить
обработку событий в такой манере, которая подходит к описанию XAML дерева объектов.
Предположим, что имеется новый проект приложения WPF по имени WPFRoutedEvents.
Модифицируйте описание XAML начального окна, добавив следующий элемент
управления <Button>, в котором определено некоторое сложное содержимое:
<Button Name="btnClickMe" Height=5" Width = 50"
Click ="btnClickMe_Clicked">
<StackPanel Orientation ="Horizontal">
<Label Height=0" FontSize =0">Fancy Button!</Label>
<Canvas Height =0" Width =00" >
<Ellipse Name = "outerEllipse" Fill ="Green" Height =5"
Width =0" Cursor="Handn Canvas.Left=5" Canvas.Top=2"/>
<Ellipse Name = "innerEllipse" Fill ="Yellow" Height = 5" Width =6"
Canvas.Top=7" Canvas.Left=2"/>
</Canvas>
</StackPanel>
</Button>
Обратите внимание, что в открывающем определении <Button> задается обработка
события Click за счет указания имени метода, который должен быть вызван по
наступлению события. Событие Click работает с делегатом RoutedEventHandler, который
ожидает обработчика события, принимающего object в первом параметре и System.Winodws.
RoutedEventArgs — во втором. Реализуем этот обработчик следующим образом:
public void btnClickMe_Clicked(object sender, RoutedEventArgs e)
{
// Действия по щелчку на кнопке.
MessageBox.Show("Clicked the button");
}
Если вы запустите приложение, то увидите это окно сообщения, независимо от того,
на какой части содержимого кнопки был совершен щелчок (зеленый Ellipse, желтый
Ellipse, Label или поверхность Button). Это хорошо. Представьте, насколько
громоздким была бы обработка событий WPF, если бы пришлось обрабатывать событие Click
для каждого подэлемента. И дело не только в том, что создание отдельных обработчиков
событий для каждого аспекта Button было бы трудоемкой задачей, но был бы получен
сложный и громоздкий код, который требовал бы трудоемкого сопровождения.
К счастью, маршрутизируемые события WPF позволяют устроить все так, чтобы
единый обработчик события Click вызывался автоматически, независимо от того, на
какую часть кнопки пришелся щелчок. Проще говоря, модель маршрутизируемых
событий автоматически распространяет события верх (или вниз) по дереву объектов в
поисках подходящего обработчика.
Точнее говоря, маршрутизируемое событие может использовать три стратегии
маршрутизации. Если событие распространяется от исходной точки во внешние
контексты внутри дерева объектов, говорят, что это пузырьковое событие (bubbling event).
В противоположность этому, если событие распространяется от внешнего элемента
(например, Window) вниз к исходной точке, такое событие называется туннелируемым
(tunneling event). Наконец, если событие инициируется и обрабатывается только
исходным элементом (что может быть описано как нормальное событие CLR), говорят, что это
прямое событие (direct event).
1182 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Роль маршрутизируемых пузырьковых событий
В текущем примере если пользователь щелкает на внутреннем желтом элементе
Ellipse, событие Click распространяется на следующий уровень (Canvas), затем на
StackPanel и, наконец, на Button, где событие Click и обрабатывается. Аналогичным
образом, если пользователь щелкает на Label, событие распространяется в StackPanel
и, наконец, попадает в элемент Button.
Благодаря этому шаблону "пузырькового" распространения маршрутизируемого
события, не нужно беспокоиться о регистрации определенных обработчиков событий
Click для всех членов составного элемента управления. Однако если требуется
организовать специальную логику обработки щелчков для множества элементов внутри одного
дерева объектов, то это можно сделать.
Для целей иллюстрации предположим, что необходимо обработать щелчок на
элементе управления outerEllipse в уникальной манере. Сначала обработайте событие
MouseDown для этого подэлемента (графически визуализированные типы вроде Ellipse
не поддерживают событие щелчка, но могут отслеживать активность кнопки мыши
через MouseDown, MouseUp и т.п.):
<Button Name="btnClickMe" Height=5" Width = 50"
Click ="btnClickMe_Clicked">
<StackPanel Orientation ="Horizontal">
<Label Height=0" FontSize =0">Fancy Button!</Label>
<Canvas Height =0" Width =00" >
<Ellipse Name = "outerEllipse" Fill ="Green"
Height =5" MouseDown ="outerEllipse_MouseDown"
Width =0" Cursor="Hand" Canvas.Left=5" Canvas.Top=2"/>
<Ellipse Name = "innerEllipse" Fill ="Yellow" Height = 5" Width =6"
Canvas.Top=7" Canvas.Left=2"/>
</Canvas>
</StackPanel>
</Button>
Затем реализуйте соответствующий обработчик событий, который в целях
демонстрации просто изменяет свойство Title главного окна:
public void outerEllipse_MouseDown(object sender, MouseButtonEventArgs e)
{
// Изменить заголовок окна.
this.Title = "You clicked the outer ellipse1";
}
Теперь можно выполнять различные действия в зависимости от того, на чем
конкретно щелкнул конечный пользователь (от внешнего эллипса и далее — в любом месте
внутри области кнопки).
На заметку! Маршрутизируемые пузырьковые события всегда движутся от исходной точки
до следующего определяющего контекста. Поэтому в данном примере щелчок на объекте
innerEllipse приводит к распространению события в Canvas, а не в outerEllipse,
поскольку оба они относятся к типу Ellipese внутри области Canvas.
Продолжение или прекращение пузырькового распространения
В настоящее время, если пользователь щелкнет на объекте outerEllipse,
инициируется зарегистрированный обработчик событий MouseDown для данного объекта
Ellipse, и в этот момент событие распространится в событие Click кнопки. Чтобы
прекратить пузырьковое распространение по дереву объектов, для этого нужно
установить свойство Handled параметра RoutedEventArgs в true:
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1183
public void outerEllipse_MouseDown(object sender, MouseButtonEventArgs e)
{
// Изменение заголовка окна.
this.Title = "You clicked the outer ellipse!";
// Прекратить пузырьковое распространение!
e.Handled = true;
}
В этом случае обнаруживается, что заголовок окна изменился, но окно сообщения,
отображаемое вызовом MessageBox в обработчике события Click элемента Button, не
отобразится. По сути, маршрутизируемые пузырьковые события позволяют сложной
группе содержимого действовать как единый логический элемент (например, Button)
или как дискретные элементы (например, Ellipse внутри Button).
Роль маршрутизируемых туннелируемых событий
Строго говоря, маршрутизируемые события могут быть по природе своей
пузырьковыми (как только что было описано) или туннелируемыми. Туннелируемые события
(которые все начинаются с префикса Preview, например, PreviewMouseDown) спускаются
вниз от элемента верхнего уровня во вложенные контексты дерева объектов. В общем
случае, каждому пузырьковому событию в библиотеках базовых классов WPF
соответствует связанное туннелируемое событие, которое инициируется перед его пузырьковым
дополнением. Например, перед инициацией пузырькового события MouseDown сначала
происходит туннелируемое событие PreviewMouseDown.
Обработка туннелируемых событий выглядит как обработка любых других событий;
вы просто назначаете имя обработчика события в XAML (или, если необходимо,
используете соответствующий синтаксис обработки событий С# в файле кода) и реализуете
этот обработчик в файле кода. Чтобы продемонстрировать взаимодействие
туннелируемых и пузырьковых событий, начните с обработки события PreviewMouseDown для
объекта outerEllipse:
<Ellipse Name = "outerEllipse" Fill ="Green" Height =5"
MouseDown ="outerEllipse_MouseDown"
PreviewMouseDown ="outerEllipse_PreviewMouseDown"
Width =0" Cursor="Hand" Canvas.Left=5" Canvas.Top=2"/>
Затем пересмотрите текущее определение класса С#, обновляя каждый обработчик
событий (для всех объектов) добавлением данных о текущем событии в
переменную-член типа string по имени mouseActivity, используя входящий объект аргументов
события. Это позволит наблюдать поток инициации событий в фоновом режиме.
public partial class MainWindow : Window
{
string mouseActivity = string.Empty;
public MainWindow()
{
InitializeComponent();
}
public void btnClickMe_Clicked(object sender, RoutedEventArgs e)
{
AddEventlnfo(sender, e) ;
MessageBox.Show(mouseActivity, "Your Event Info");
// Очистить строку для следующего использования.
mouseActivity = "";
}
private void AddEventlnfo(object sender, RoutedEventArgs e)
{
mouseActivity += string.Format(
1184 Часть VI. Построение настольных пользовательских приложений с помощью WPF
"{0} sent a {1} event named {2}.\n", sender,
e.RoutedEvent.RoutingStrategy,
e.RoutedEvent.Name);
}
private void outerEllipse_MouseDown(object sender, MouseButtonEventArgs e)
{
AddEventlnfo(sender, e) ;
}
private void outerEllipse_PreviewMouseDown(
object sender, MouseButtonEventArgs e)
{
AddEventlnfo(sender, e);
}
}
Обратите внимание, что ни в одном обработчике не прекращается пузырьковое
распространение события. Запустив это приложение, вы увидите уникальное окно
сообщения, зависящее от того, где был произведен щелчок на кнопке. На рис. 31.6 показан
результат щелчка на внешнем объекте Ellipse.
Рис. 31.6. Сначала туннелированное, а потом —
пузырьковое распространение
Итак, почему же события WPF обычно идут парами (одно туннелируемое и одно
пузырьковое)? Ответ заключается в том, что за счет предварительного отслеживания
событий вы получаете возможность выполнять любую специфическую логику
(проверку достоверности данных, отключение пузырьковых действий и т.д.), прежде чем будет
инициирован пузырьковый аналог события. Для примера предположим, что имеется
элемент Text Box, который должен допускать ввод только числовых данных. В
обработчике события PreviewKeyDown, если пользователь ввел какие-то нечисловые данные,
можно отменить пузырьковое событие, установив свойство Handled в true.
При построении специального элемента управления, который содержит специальные
события, событие можно написать таким образом, чтобы оно могло распространяться,
как пузырьковое (или туннелируемое) по дереву XAML. В настоящей главе создание
специальных маршрутизируемых событий подробно не рассматриваются (правда, процесс
не так уж сильно отличается от построения специального свойства зависимости). Если
интересно, загляните в раздел "Routed Events Оггегаеш" ("Обзор маршрутизируемых
событий") документации .NET Framework 4.0 SDK. Там вы найдете множество полезных
подсказок.
Исходный код. Проект WPFRoutedEvents доступен в подкаталоге Chapter 31.
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1185
Логические деревья, визуальные
деревья и шаблоны по умолчанию
Есть еще несколько тем, которые следует рассмотреть, прежде чем приступать к
изучению построения специальных элементов управления. В частности, необходимо
понять разницу между логическим деревом, визуальным деревом и шаблоном по
умолчанию. При вводе XAML-разметки в Visual Studio 2010, Expression Blend либо таком
инструменте, как Kaxaml, разметка становится логическим представлением документа
XAML. Точно также, при написании кода С#, добавляющего новые элементы к элементу
управления StackPanel, новые элементы вставляются в логическое дерево. По сути,
логическое представление показывает, как содержимое будет позиционировано внутри
различных диспетчеров компоновки для главного окна (или другого корневого элемента,
такого как Page или NavigationWindow).
Тем не менее, за каждым логическим деревом стоит более сложное представление,
которое называется визуальным деревом и внутренне использует WPF для корректной
визуализации элементов на экране. Внутри любого визуального дерева находятся все
детали шаблонов и стилей, используемых для визуализации каждого объекта, включая
все необходимые рисунки, фигуры, визуальные элементы и анимации.
Полезно знать разницу между логическим и визуальным деревьями, потому что
когда строится специальный шаблон элемента управления, то, по сути, заменяется все
или часть визуального дерева элемента управления и вставляете свое. Поэтому, если
необходимо визуализировать элемент управления Button в виде звезды, можно
определить новый звездообразный шаблон и включить его в визуальное дерево Button.
Логически Button остается типом Button, и поддерживает все свойства, методы и
события, как и ожидалось. Но визуально элемент получает совершенно новый внешний
вид. Один этот факт делает WPF исключительно удобным API-интерфейсом, учитывая,
что другие инструментарии потребовали бы в такой ситуации построения совершенно
нового класса, представляющего звездообразную кнопку. В WPF достаточно определить
новую разметку.
На заметку! Элементы управления WPF часто описываются как лишенные внешнего вида (lookless).
Это указывает на тот факт, что внешность элемента управления WPF полностью независима от
его поведения и настраиваема.
Программный просмотр логического дерева
Хотя анализ логического дерева окна во время выполнения — не слишком часто
применяемое программное действие в WPF, стоит упомянуть, что в пространстве имен
System.Windows определен класс по имени LogicalTreeHelper, который позволяет
просматривать структуру логического дерева во время выполнения. Для иллюстрации
связи между логическими деревьями, визуальными деревьями и шаблонами элементов
управления создайте новое приложение WPF по имени TreesAndTemplatesApp.
Модифицируйте разметку окна, чтобы она содержала два элемента управления
Button и большой доступный только для чтения элемент Text Box с включенными
линейками прокрутки. Воспользуйтесь IDE-средой для обработки события Click каждой
кнопки. Ниже показана необходимая XAML-разметка.
<Window х:Class="TreesAndTemplatesApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://cchemas.microsoft.com/winfx/2006/xaml"
Title="Fun with Trees and Templates" Height=18"
Width="83 6" WindowstartupLocation="CenterScreen">
1186 Часть VI. Построение настольных пользовательских приложений с помощью WPF
<DockPanel LastChildFill="TrueII>
<Border Height=0" DockPanel. Dock=MTopM BorderBrush=,,Blue">
<StackPanel Qrlentation="Horizontal1^
<Button x:Name="btnShowLogicalTree11 Content="Logical Tree of Window"
Margin=" BorderBrush="Blue" Height=0"
Click="btnShowLogicalTree_Click,,/>
<Button x :Name="btnShowVisualTree11 Content="Visual Tree of Window"
BorderBrush="Blue" Height= 0" Click="btnShowVisualTree_Click"/>
</StackPanel>
</Border>
<TextBox x:Name="txtDisplayArea" Margin=0" Background="AliceBlue"
IsReadOnly="True" BorderBrush="Red" VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto" />
</DockPanel>
</Window>
Внутри файла кода С# определите переменную-член типа string по имени
dataToShow. Теперь в обработчике Click объекта btnShowLogicalTree вызовите
вспомогательную функцию, которая будет вызывать себя рекурсивно для наполнения
строковой переменной логическим деревом Window. Для этого применяется статический
метод GetChildrenO объекта LogicalTreeHelper. Ниже показан код.
private void btnShowLogicalTree_Click (object sender, RoutedEventArgs e)
{
dataToShow = "";
BuildLogicalTree@, this);
this.txtDisplayArea.Text = dataToShow;
}
void BuildLogicalTree(int depth, object obj )
{
// Добавить имя типа к переменной-члену dataToShow.
dataToShow += new string (' ', depth) + obj.GetType () .Name + "\n";
// Если элемент - не DependencyObject, пропустить его.
if ( ' (obj is DependencyObject))
return;
// Выполнить рекурсивный вызов для каждого дочернего элемента логического дерева
foreach (object child in LogicalTreeHelper.GetChildren(
obj as DependencyObject))
BuildLogicalTree(depth + 5, child);
}
Запустив приложение и щелкнув на кнопке Logical Tree of Window (Логическое
дерево окна), вы увидите вывод дерева в текстовой области — почти точную копию
первоначальной XAML-разметки (рис. 31.7).
MainWindow
DockPanef
Border
StackPanel
Button
String
Button
TextBox
String
Рис. 31.7. Просмотр логического дерева во время выполнения
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1187
Программный просмотр визуального дерева
Визуальное дерево Window также можно просмотреть во время выполнения с
использованием класса VisualTreeHelper из пространства имен System.Windows.Media.
Ниже приведена реализация обрабочика события Click для второго элемента
управления Button (btnShowVisualTree), которая выполняет аналогичную рекурсивную логику
для построения текстового представления визуального дерева:
private void btnShowVisualTree_Click(object sender, RoutedEventArgs e)
{
dataToShow = "";
BuildVisualTree@, this);
this.txtDisplayArea.Text = dataToShow;
}
void BuildVisualTree (int depth, DependencyObject obj)
{
// Добавить имя типа к переменной-члену dataToShow.
dataToShow += new string(' ' , depth) + obj.GetType () .Name + "\n";
// Выполнить рекурсивный вызов для каждого дочернего элемента визуального дерева
for (int i=0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
BuildVisualTree(depth + 1, VisualTreeHelper.GetChild(obj, i) ) ;
}
Как показано на рис. 31.8, в визуальном дереве присутствует множество
низкоуровневых агентов визуализации, таких как ContentPresenter, AdornerDecorator,
TextBoxLineDrawingVisual и т.д.
Main Window ,•
Border
AdornerDecorator
ContentPresenter
DockPanel
Border
StackPanel
Button
ButtonChrome
ContentPresenter
TextBlock
Button
ButtonChrome
ContentPresenter
TextBlock
TextBox
UstBoxChrome
ScrollVisewer
Grid
Rectangle
ScrollContentPresenter
TextBoxView
TextBoxLineDrawingVisual
AdornerLayer
ScrollBar
ScrollBar
AdornerLayer
Рис. 31.8. Просмотр визуального дерева во время выполнения
1188 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Программный просмотр шаблона по умолчанию
для элемента управления
Вспомните, что визуальное дерево используется WPF для определения, каким
образом визуализировать Window и все содержащиеся элементы. Каждый элемент
управления WPF сохраняет собственный набор команд внутри своего шаблона по умолчанию.
С программной точки зрения любой шаблон может быть представлен как экземпляр
класса ControlTemplate. Кроме того, элемент управления имеет шаблон по умолчанию,
который можно получить с использованием свойства Template:
// Получить шаблон по умолчанию элемента Button.
Button myBtn = new Button ();
ControlTemplate template = myBtn.Template;
Аналогично, можно создать новый объект ControlTemplate в коде и подключить его
к свойству Template элемента управления:
// Подключить новый шаблон для кнопки.
Button myBtn = new Button ();
ControlTemplate customTemplate = new ControlTemplate();
// Предполагается, что этот метод добавит весь код для шаблона звезды.
MakeStarTemplate(customTemplate);
myBtn.Template = customTemplate;
Хотя можно было бы построить новый шаблон в коде, намного чаще это
делается в XAML-разметке. Фактически, Expression Blend имеет огромное количество
инструментов, которые можно использовать для определения шаблона с минимальными
усилиями.
Однако прежде чем приступить к построению собственных шаблонов, давайте
завершим текущий пример и добавим возможность просмотра шаблона по умолчанию
элемента управления WPF во время выполнения. Это может оказаться действительно
удобным способом увидеть общую композицию шаблона. Для начала обновите
разметку окна добавлением нового диспетчера компоновки StackPanel, стыкованного к левой
стороне главной панели DockPanel, как показано ниже:
<Border DockPanel. Dock="Lef t" Margin=011 BorderBrush="DarkGreen11
BorderThickness=11 Width=58">
<StackPanel>
<Label Content="Enter Full Name of WPF Control"
Width=40" FontWeight="DemiBold" />
<TextBox x:Name=,,txtFullName" Width=40" BorderBrush=,,Green"
Background="BlanchedAlmond11 Height=ll22"
Text="System.Windows.Controls.Button" />
<Button x:Name="btnTemplate" Content=ul5ee Template" BorderBrush="Green"
Height=0" Width=00" Margin="
Click="btnTemplate_Click" HorizontalAlignment="Left" />
<Border BorderBrush="DarkGreen" BorderThickness=" Height=60"
Width=01" Margin=0" Background="LightGreen" >
<StackPanel x:Name="stackTemplatePanel" />
</Border>
</StackPanel>
</Border>
Обратите внимание на пустой элемент StackPanel по имени stackTemplatePanel,
поскольку на него будут осуществляться ссылки в коде. На рис. 31.9 показано, как
примерно должно выглядеть окно.
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1189
See Template
Рис. 31.9. Обновленный пользовательский интерфейс окна
Текстовая область вверху слева позволяет вводить полностью квалифицированное
имя элемента управления WPF, находящегося в сборке PresentationFramework.dll.
После загрузки библиотеки экземпляр объекта динамически загружается и
отображается в большом квадрате внизу слева. И, наконец, шаблон по умолчанию элемента
управления будет отображаться в текстовой области справа. Добавьте в свой класс С# новую
переменную-член типа Control:
private Control ctrlToExamme = null;
Ниже приведен остальной код, который требует импорта пространств имен System.
Reflection, System.Xmlи System.Windows.Markup:
private void btnTemplate_Click(object sender, RoutedEventArgs e)
dataToShow = "";
ShowTemplate ();
this.txtDisplayArea.Text =
}
private void ShowTemplate()
dataToShow;
// Удалить элемент управления, находящийся в области предварительного просмотра.
if (ctrlToExamme '= null)
stackTemplatePanel .Children . Remove (ctrlToExamme) ;
try
{
// Загрузить сборку PresentationFramework и создать экземпляр
// указанного элемента управления. Задать его размер для отображения,
// затем добавить пустую панель StackPanel.
Assembly asm = Assembly.Load("PresentationFramework, Version=4.0.0.0," +
"Culture=neutral, PublicKeyToken=31bf3856ad364e35");
ctrlToExamme = (Control)asm.Createlnstance(txtFullName.Text);
ctrlToExamme. Height = 200;
ctrlToExamme.Width = 200;
ctrlToExamme .Margin = new Thickness E) ;
stackTemplatePanel .Children .Add (ctrlToExamme) ;
1190 Часть VI. Построение настольных пользовательских приложений с помощью WPF
// Определить некоторые настройки XML для сохранения отступов.
XmlWriterSettings xmlSettmgs = new XmlWriterSettings();
xmlSettmgs . Indent = true;
// Создать StringBuilder для хранения XAML-разметки.
StringBuilder strBuilder = new StringBuilder();
// Создать XmlWriter на основе существующих настроек.
XmlWriter xWriter = XmlWriter. Create (strBuilder, xmlSettmgs);
// Сохранить XAML-разметку в объекте XmlWriter на основе ControlTemplate.
XamlWriter. Save (ctrlToExamme. Template, xWriter) ;
// Отобразить XAML-разметку в текстовом поле.
dataToShow = strBuilder . ToStnng () ;
}
catch (Exception ex)
{
dataToShow = ex.Message;
}
}
Большую часть работы занимает манипулирование скомпилированным ресурсом
BAML для отображения его на строку XAML. На рис. 31.10 показано финальное
приложение в действии на примере отображения шаблона по умолчанию элемента
управления DatePicker.
Logical Tree of Window Visual Tree of Window
1 Enter Full Name of WPF Control
1 j System,Windows-Controls. DatePicker
1 See Template
Select л date
4 February, 2010
Su Mo Tu We Th Fr
31 1 2 3 4 S
7 S Э 10 11 12
14 15 16 17 16 19
21 22 23 |3 25 26
28 1 2 3 4 5
7 8 Э 10 11 12
D
С
S»
fl
13
20
27
e
%
L
>
<?xml versions.0" encoding="utf-16"?>
<ControlTemplate TargetType="DatePicker" xrnlns="httpy7schemas.micros; jj
<Border BorderThicJcness="{TemplateBinding Border.BorderThickness)" Pelgj
<VisualStateManager.VisualStateGroups>
<VisuaiStateGroup Name="CommonSta,tes" />
</VisualStateManager.VtsualStateGroups>
<Grid Name="PART_Root" HonzontalAlignment="(TemplateBinding Cor
< Gnd.ColumnDef inttions >
<ColumnDefinition Widths"** l>
<ColumnDefinition Width="Auto* />
</Grid.ColumnDefinitions>
<Grid.Resources>
<ControlTemplate TargetType="Button" х:Кеу="ё">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CommonStates" />
</V»ualStateManager.VisualStateGroups>
<Grid &ackground="#llFFFFFF* Width=9* Height=8" FlowDire<
<Grid.ColumnDefinitions >
<ColumnDefinition Width=0*" />
<ColumnDefinition W»dth=0*" />
<ColumnOefinition Width=0*" />
<ColumnDefinition W»dth=0*" />
</Gnd.ColumnDefinitions >
Рис. 31.10. Исследование ControlTemplate во время выполнения
Теперь вы должны лучше представлять совместную работу логических деревьев,
визуальных деревьев и шаблонов элементов управления. Оставшаяся часть главы
будет посвящена построению специальных шаблонов и пользовательских элементов
управления.
Исходный код. Проект TreesAndTemplatesApp доступен в подкаталоге Chapter 31.
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1191
Построение специального шаблона элемента
управления в Visual Studio 2010
Построение специального шаблона для элемента управления может осуществляться
исключительно в коде С#. При таком подходе можно было бы добавить данные к
объекту ControlTemplate и присвоить его свойству Template элемента управления. Однако
большую часть времени вы будете формировать внешний вид ControlTemplate,
используя XAML, и для упрощения задачи обычно при этом применяется инструмент
Expression Blend. Этот инструмент будет задействован в финальном примере
настоящей главы, а пока построим простой шаблон в Visual Studio 2010. Хотя данная IDE-
среда не имеет столько возможностей визуального конструирования, как Expression
Blend, использование Visual Studio 2010 — хороший способ изучить детали
конструкции шаблона.
Создайте новое приложение WPF по имени ButtonTemplate. В этом проекте
основной интерес представляют механизмы создания и использования шаблонов, так что
разметка для главного окна очень проста:
<Wmdow х: Class="ButtonTemplate . MamWindow"
xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Fun with Templates" Height=50" Width=25">
<StackPanel>
<Button x:Name="myButton" Width=00" Height=00"
Click="myButton_Click"/>
</StackPanel>
</Wmdow>
В обработчике события Click просто отобразите окно сообщения (посредством
MessageBox.ShowO), которое подтверждает факт щелчка на элементе управления. При
построении шаблонов элементов управления помните, что поведение элемента
управления неизменно, но его внешний вид может варьироваться.
В настоящее время этот элемент Button визуализируется с использованием
шаблона по умолчанию, который, как иллюстрирует последний пример, представляет
собой ресурс BAML внутри данной сборки WPF. Когда вы хотите определить собственный
шаблон, то по существу заменяете это визуальное дерево по умолчанию собственным
деревом. Для начала обновите определение элемента <Button>, указав новый шаблон
с помощью синтаксиса "свойство-элемент". Этот шаблон придаст элементу управления
округлый вид:
<Button x:Name="myButton" Width=00" Height=00"
Click="myButton_Click">
<Button.Template>
<ControlTemplate>
<Grid x:Name="controlLayout">
<Ellipse x:Name="buttonSurface" Fill = "LightBlue"/>
<Label x:Name="buttonCaption" VerticalAlignment = "Center"
HorizontalAlignment = "Center"
FontWeight = "Bold" FontSize = 0" Content = K!"/>
</Grid>
</ControlTemplate>
</Button.Template>
</Button>
В приведенной выше разметке определен шаблон, состоящий из именованного
элемента управления Grid, который содержит именованный элемент Ellipse и Label.
1192 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Поскольку в Grid не определены строки и столбцы, каждый дочерний элемент
укладывается поверх предыдущего элемента управления, позволяя центрировать содержимое.
Если теперь запустить приложение, можно заметить, что событие Click инициируется
только в том случае, когда курсор мыши находится в границах Ellipse (и даже не в
углах, окружающих эллипс)! Отсутствие необходимости вычислять попадание курсора
мыши на поверхность элемента либо какие-то иные низкоуровневые детали —
замечательная характеристика архитектуры шаблонов WPF. Поэтому, если шаблон
использовал объект Polygon для визуализации некоторой необычной геометрии, можете быть
уверены, что детали проверки попадания курсора мыши будут соответствовать форме
элемента управления, а не охватывающему прямоугольнику.
Шаблоны как ресурсы
В настоящее время шаблон включен в специфический элемент управления Button,
что ограничивает возможности повторного использования. В идеале шаблон круглой
кнопки стоило бы поместить в словарь ресурсов для многократного использования в
разных проектах или, как минимум, переместить его в контейнер ресурсов для
повторного использования в текущем проекте. Давайте перенесем локальный ресурс Button на
уровень приложения, используя Visual Studio 2010. Сначала найдите свойство Template
элемента Button в окне Properties (Свойства). Теперь щелкните на значке с
изображением маленького черного ромба и выберите Extract Value to Resource... (Извлечь
значение в ресурс), как показано на рис. 31.11.
[Properties
1 Button myButton
1 _5* Properties / Events
1 ; 1 OJ j Search
Tablndex □ 2147483647
Tag □
^^| Resource...
ToolTip Reset Value
Uid
UseLayoutRounding & Apply Data Binding...
VerticalAlignment j 9 . Apply Resource...
VerticalContentAlign...
Visibility Extract Value to Resource...
Width + 1ЙГ""
» □ xj
OKI
A 1
I
Л
i
Рис. 31.11. Извлечение локального ресурса
В результирующем диалоговом окне определите новый шаблон по имени
RoundButtonTemplate и сохраните его в App.xaml (рис. 31.12).
Create Resource
Key: RoundButtonTemplate
Destination: appjtaml
;. ok
l
i Я) 1^Шв^ц^шмш1
i
3Lsm* 1
j
Рис. 31.12. Размещение ресурса в файле App.xaml
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1193
После этого в разметке объекта Application появятся следующие данные:
Application х:Class="ButtonTemplate.Арр"
xmlns="http : //schema s .microsoft. com/wmfx/200 6/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWmdow.xaml">
<Application.Resources>
<ControlTemplate x:Key="RoundButtonTemplate">
<Grid x:Name="controlLayout">
<Ellipse x: Name="buttonSurface" Fill = "LightBlue"/>
<Label x:Name="buttonCaption" VerticalAlignment = "Center"
HorizontalAlignment = "Center"
FontWeight = "Bold" FontSize = 0" Content = "OK|M/>
</Grid>
</ControlTemplate>
</Application.Resources>
</Application>
При желании применение этого шаблона можно ограничить. Подобно стилю WPF, к
любому элементу <ControlTemplate> можно добавить атрибут TargetType. Обновите
открывающий дескриптор <ControlTemplate>, указав, что только элементы
управления Button могут использовать этот шаблон:
<ControlTemplate x:Key="RoundButtonTemplate" TargetType ="Button">
Теперь, поскольку этот ресурс доступен всему приложению, можно определить
любое количество круглых кнопок. Для целей тестирования создайте два дополнительных
элемента управления Button, которые используют этот шаблон (обрабатывать событие
Click для них не обязательно):
<StackPanel>
<Button x:Name="myButton" Width=00" Height=00"
Click="myButton_Click"
Template="{StaticResource RoundButtonTemplate}"></Button>
<Button x:Name="myButton2" Width=00" Height=00"
Template="{StaticResource RoundButtonTemplate}"></Button>
<Button x:Name="myButton3" Width=00" Height=00"
Template="{StaticResource RoundButtonTemplate}"></Button>
</StackPanel>
Включение визуальных подсказок с использованием триггеров
При определении специального шаблона все визуальные подсказки удаляются.
Например, шаблон кнопки по умолчанию содержит разметку, которая информирует
элемент управления о том, как ему выглядеть по наступлению определенных событий
пользовательского интерфейса, таких как получение фокуса, щелчок кнопкой мыши,
включение (или отключение) и т.д. Пользователи достаточно привыкли визуальным
подсказкам подобного рода, поскольку они придают элементу управления некоторую
ощутимую реакцию. Однако в шаблоне RoundButtonTemplate не определено какой-либо
предназначенной для этого разметки, так что вид элемента управления будет одинаков,
независимо от действий мыши. В идеале элемент должен выглядеть несколько иначе,
когда на нем производится щелчок (возможно, его цвет изменяется или появляется
тень); это уведомляет пользователя об изменении визуального состояния.
Сразу после появления WPF единственным способом организации таких визуальных
подсказок было добавление к шаблону любого количества триггеров, которые обычно
изменяют значения объектных свойств либо запускают раскадровку анимации (либо
делают то и другое), когда условие того или иного триггера истинно. Для примера об-
1194 Часть VI. Построение настольных пользовательских приложений с помощью WPF
новите RoundButtonTemplate следующей разметкой, что обеспечит изменение цвета
элемента управления на синий, а цвет Foreground — на желтый, когда курсор мыши
наведен на его поверхность:
<ControlTemplate x:Key="RoundButtonTemplate" TargetType="Button">
<Grid x:Name="controlLayout">
<Ellipse x:Name="buttonSurface" Fill="LightBlue" />
<Label x:Name="buttonCaption" Content="OK>" FontSize=0" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property = "IsMouseOver" Value = "True">
<Setter TargetName = "buttonSurface" Property = "Fill" Value = "Blue"/>
<Setter TargetName = "buttonCaption" Property = "Foreground"
Value = "Yellow"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
Вновь запустив эту программу, вы обнаружите, что цвет переключается в
зависимости от того, находится ли курсор мыши в области Ellipse. Ниже показан другой
триггер, в котором размер Grid (и, следовательно, дочерних элементов) сокращается,
когда кнопка нажата с помощью мыши. Добавьте следующую разметку в коллекцию
<ControlTemplate.Triggers>:
<Trigger Property = "IsPressed" Value="True">
<Setter TargetName="controlLayout"
Property="RenderTransformOrigin" Value=.5,0.5"/>
<Setter ТаrgetName="controlLayout" Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX=.8" ScaleY=.8"/>
</Setter.Value>
</Setter>
</Trigger>
В результате получился специальный шаблон с несколькими визуальными
подсказками, включаемыми с помощью триггеров WPF. Как вы увидите в следующем примере
этой главы, в .NET 4.0 появился альтернативный способ включения визуальных
подсказок с использованием Visual State Manager (Диспетчер визуальных состояний). Прежде
чем заняться им, давайте рассмотрим роли расширения разметки {TempiateBinding}
и класса ContentPresenter.
Роль расширения разметки {TempiateBinding}
Построенный специальный шаблон может быть применен только к элементам
управления Button, и потому имеется причина того, что должен существовать способ
установки свойств элемента <Button> так, чтобы заставить шаблон визуализировать себя
уникальным образом. Например, прямо сейчас свойство Fill элемента Ellipse жестко
закодировано в синий цвет, а свойство Content элемента Label всегда установлено в
значение ОК. Естественно, возникает необходимость иметь кнопки другого цвета и с
другими текстовыми значениями, поэтому давайте попробуем определить такие кнопки
в главном окне:
<StackPanel>
<Button x:Name="myButton" Width=00" Height=00"
Background="Red" Content="Howdy!"
Click="myButton_Click"
Template="{StaticResource RoundButtonTemplate}" />
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1195
<Button x:Name="myButton2" Width=00" Height=00"
Background="LightGreen" Content="Cancel'"
Template="{StaticResource RoundButtonTemplate}" />
<Button x:Name="myButton3" Width=00" Height=00"
Background="Yellow" Content="Format"
Template="{StaticResource RoundButtonTemplate}" />
</StackPanel>
Однако, независимо от того факта, что для каждого элемента Button задаются
уникальные значения в свойствах Background и Content, все равно получаются три синих
кнопки с текстом ОК. Проблема в том, что элемент управления, использующий
шаблон (Button), имеет свойства, которые не соответствуют в точности элементам
шаблона (например, свойство Fill элемента Ellipse). Кроме того, хотя элемент Label имеет
свойство Content, значение, определенное в контексте <Button>, не маршрутизируется
автоматически на внутреннее содержимое шаблона.
Описанную проблему можно решить, применив при построении шаблона
расширение разметки {TemplateBonding}. Это позволит захватывать настройки свойств,
определенные элементом управления, который использует шаблон, и использовать
их для установки свойств в самом шаблоне. Ниже приведена переработанная
версия RoundButtonTemplate, в которой теперь применяется это расширение разметки
для отображения свойства Background элемента Button на свойство Fill элемента
Ellipse; также здесь обеспечивается действительная передача значения Content
элемента Button в свойство Content элемента Label.
<ControlTemplate x:Key="RoundButtonTemplate" TargetType="Button">
<Grid x:Name="controlLayout">
<Ellipse x:Name="buttonSurface" Fill = " {TemplateBindmg Background
<Label x:Name="buttonCaption" Content=" {TemplateBindmg Content}"
FontSize=0" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
<ControlTemplate.Triggers>
}"/>
</ControlTemplate.Triggers>
</ControlTemplate>
После такого обновления можно создавать кнопки разных цветов и с разным
текстом (рис. 31.13).
* Fun with Templates
Howdy!
Cancel!
Format
LlMJ
Рис. 31.13. Привязки шаблона позволяют передавать значения
во внутренние элементы управления
1196 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Роль класса ContentPresenter
При проектировании шаблона для отображения текстового значения
элемента управления использовался элемент Label. Подобно Button, элемент Label
поддерживает свойство Content. Поэтому, если применяется расширение разметки
{TemplateBinding}, то можно определить элемент Button со сложным содержимым, а
не с простой строкой. Например:
<Button x:Name="myButton4" Width=00" Height=00" Background="Yellow"
Template="{StaticResource RoundButtonTemplate}">
<Button.Content>
<ListB6x Height=0" Width=5">
<ListBoxItem>Hello</ListBoxItem>
<ListBoxItem>Hello</ListBoxItem>
<ListBoxItem>Hello</ListBoxItem>
</ListBox>
</Button.Content>
</Button>
Для этого конкретного элемента управления все работает, как ожидалось. Но что
если нужно передать сложное содержимое члену шаблона, который не имеет свойства
Content? Когда требуется определить обобщенную область отображения содержимого
в шаблоне, в противоположность специфическому типу элемента (Label или TextBox)
можно использовать класс ContentPresenter. В данном примере делать это не
нужно, однако ниже показана простая разметка, иллюстрирующая построение
специального шаблона, который использует ContentPresenter для показа значения свойства
Content элемента управления, использующего шаблон:
<!-- Этот шаблон кнопки отобразит то, что установлено
в Content принимающей кнопки -->
<ControlTemplate x:Key="NewRoundButton" TargetType="Button">
<Grid>
<Ellipse Fill="{TemplateBinding Background}"/>
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
</ControlTemplate>
Включение шаблонов в стили
Сейчас шаблон просто определяет базовый вид элемента управления Button. Тем не
менее, за процесс установки базовых свойств элемента управления (содержимое,
размер шрифта, его толщина и т.п.) отвечает сам элемент Button:
<!-- Сейчас Button должен устанавливать базовые значения свойств, а не шаблон —>
<Button x:Name ="myButton" Foreground ="Black" FontSize =0" FontWeight ="Bold"
Template ="{StaticResource RoundButtonTemplate}"
Click ="myButton_Click"/>
При желании можно устанавливать эти значения в шаблоне. Подобным образом, по
сути, создается внешний вид по умолчанию. И как, возможно, уже понятно — это
работа для стилей WPF. Когда строится стиль (включающий базовые установки свойств),
можно определить шаблон внутри стиля! Ниже приведен обновленный ресурс
приложения из App.xaml, который помечен ключом RoundButtonSyle:
<!-- Стиль, содержащий шаблон -->
<Style x:Key ="RoundButtonStyle" TargetType ="Button">
<Setter Property ="Foreground" Value ="Black"/>
<Setter Property ="FontSize" Value =4"/>
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1197
<Setter Property ="FontWeight" Value ="Bold"/>
<Setter Property="Width" Value=00"/>
<Setter Property="Height" Value=00"/>
<!— А это — сам шаблон! —>
<Setter Property ="Template">
<Setter.Value>
<ControlTemplate TargetType ="Button">
<Grid x:Name="controlLayout">
<Ellipse x:Name="buttonSurface" Fill=" {TemplateBindmg Background} "/>
<Label x:Name="buttonCaption" Content =" {TemplateBindmg Content}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property = "IsMouseOver" Value = "True">
<Setter TargetName = "buttonSurface" Property = "Fill" Value = "Blue"/>
<Setter TargetName = "buttonCaption" Property = "Foreground" Value = "Yellow"/>
</Trigger>
<Trigger Property = "IsPressed" Value="True">
<Setter ТаrgetName="controlLayout"
Property="RenderTransformOrigin" Value=.5,0. 5"/>
<Setter TargetName="controlLayout" Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX=.8" ScaleY=.8"/>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
После такой модификации появляется возможность создавать кнопочные элементы
управления, устанавливая свойство Style следующим образом:
<Button х:Name="myButton" Background="Red" Content="Howdy'"
Click="myButton_Click" Style="{StaticResource RoundButtonStyle } "/>
Хотя визуализация и поведение кнопки идентичны, преимущество внедрения
шаблонов в стили состоит в том, что это позволяет предоставить готовый набор значений
для общих свойств.
На этом обзор использования Visual Studio 2010 при построении специальных
шаблонов для элементов управления завершен. Но прежде чем перейти к миру
веб-разработки с помощью ASP.NET, давайте посмотрим, как посредством Expression Blend не
только генерировать шаблоны элементов управления, но также создавать специальные
элементы UserControl.
Исходный код. Проект ButtonTemplate доступен в подкаталоге Chapter 31.
Построение специальных элементов UserControl
с помощью Expression Blend
Во время рассмотрения свойств зависимости была кратко представлена концепция
UserControl. В некоторых отношениях UserControl — это следующий логический шаг
после шаблона элемента управления. Вспомните, что при построении шаблона
элемента управления, по сути, применяется обложка (skin), которая изменяет физический
1198 Часть VI. Построение настольных пользовательских приложений с помощью WPF
внешний вид элемента управления WPF. Специальный элемент UserControl, однако,
позволяет буквально строить новый тип класса, который может иметь уникальные
члены (методы, события, свойства и т.п.). К тому же многие специальные UserControl
используют шаблоны и стили; допустимо также определять дерево элементов управления
непосредственно в контексте <UserControl>.
Вспомните, что элемент проекта UserControl можно добавлять к любому
приложению WPF в Visual Studio 2010, включая создание проекта библиотеки UserControl,
которая представляет собой сборку *.dll, не содержащую ничего, помимо коллекции
UserControl для использования между проектами.
В последнем разделе этой главы с использованием инструмента Expression Blend
будут продемонстрированы некоторые очень интересные приемы разработки, включая
создание нового элемента управления из геометрии, работу с редактором анимации
и включение визуальных подсказок с помощью .NET 4.0 Visual State Manager (VSM).
Изучая эти темы, помните, что аналогичных результатов можно достичь и в Visual
Studio 2010; это просто потребует ручного ввода большего объема разметки.
Создание проекта библиотеки UserControl
Целью финального примера этой главы будет построение приложения WPF, которое
представляет простую игру джек-пот Хотя всю логику вполне можно поместить в новую
исполняемую программу WPF, вместо этого вынесем специальные элементы
управления в отдельный проект библиотеки. Запустите Expression Blend, выберите пункт меню
File^New Project (Файл^Новый проект) и создайте новый проект типа WPF Control
Library по имени MyCustomControl (рис. 31.14).
Рис. 31.14. Создание нового проекта WPF Control Library в Expression Blend
Переименование начального элемента UserControl
Проект этого типа предоставляет начальный элемент UserControl по имени
MainControl. Давайте переименуем этот элемент управления в SpinControl. Для
этого начните с изменения имени файла MainControl.xaml на SpinControl.xaml через
вкладку Projects (Проекты). Затем откройте редактор XAML для начального элемента
управления и измените атрибут х:Class, как показано ниже (кроме того, полностью
Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1199
удалите атрибут x:Name). Установите для свойств Height и Width элемента UserControl
значение 150.
<UserControl
xmlns="http: //schemas .microsoft. com/wmfx/200 6/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/200 8"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2 00 6"
mc:Ignorable="d"
x :Class="MyCustomControl. SpmControl"
Width=50" Height=50">
</UserControl>
Откройте файл кода С#, который соответствует XAML-документу, и измените имя
класса (и конструктор) на SpinControl:
public partial class SpinControl
{
public SpinControl ()
{
this.InitializeComponent();
}
}
Чтобы удостовериться в отсутствии опечаток, полностью пересоберите проект.
Проектирование элемента SpinControl
Целью специального SpinConrtol является циклический перебор трех файлов
графических изображений в случайном порядке при вызове метода Spin(). В состав кода
примеров для этой главы входят три файла (Cherries.png, Jackpot.jpg и Limesopg),
которые представляют возможные изображения. Добавьте их к текущему проекту,
используя пункт меню Projects Add Existing Item (Проекте Добавить существующий
элемент). При добавлении этих изображений к проекту Blend они автоматически
конфигурируются как встроенные в результирующую сборку.
Визуальный дизайн SpinControl достаточно прост. Используя Assets Library, добавьте
элемент управления Border, который использует значение BorderThickness, равное 5,
а в редакторе кистей выберите цвет BorderBrush по своему вкусу. Затем поместите
внутрь Grid элемент управления Image (по имени imgDisplay) и установите для
свойства Stretch значение Fill. После этого Grid должен быть сконфигурирован так:
<Grid x:Name="LayoutRoot">
<Border BorderBrush="#FFD51919" BorderThickness="/>
<Image x:Name="imgDisplay" Margin="8" Stretch="Fill"/>
</Grid>
Наконец, установите для свойства Source элемента управления Image одно из трех
упомянутых выше изображений. Поверхность визуального конструктора теперь должна
выглядеть примерно, как показано на рис. 31.15.
Добавление начального кода С#
Теперь, когда разметка готова, воспользуйтесь вкладкой Events (События) окна
Properties (Свойства) в Expression Blend, чтобы обработать событие Loaded элемента
управления, и укажите метод по имени SpinControl Loaded. В окне кода объявите
массив из трех объектов Bitmaplmage, который будет заполняться встроенными
файлами двоичных изображений при наступлении события Loaded:
1200 Часть VI. Построение настольных пользовательских приложений с помощью WPF
Рис. 31.15. Пользовательский интерфейс элемента SpinControl
public partial class SpinControl
{
// Массив объектов Bitmaplmage.
private Bitmaplmage[] images = new Bitmaplmage[3];
public SpinControl ()
{
this.InitializeComponent();
}
private void SpinControl_Loaded (object sender, System.Windows.RoutedEventArgs e)
{
// Заполнить массив ImageSource изображениями.
images [0] = new Bitmaplmage (new Uri ("Cherries .png" , UnKind. Relative) ) ;
images [1] = new Bitmaplmage (new Uri ( "Jackpot. jpg", UnKind.Relative) ) ;
images [2] = new Bitmaplmage (new Uri ("Limes . jpg" , UnKind.Relative) ) ;
}
}
Определите общедоступный член по имени Spin(), реализованный так, чтобы
отображать в элементе управления Image случайно выбранный один из трех объектов
Bitmaplmage и возвращать значение случайного числа:
public int Spin ()
{
// Поместить в элемент управления Image случайно выбранное изображение.
Random г = new Random(DateTime.Now.Millisecond);
int randomNumber = r.NextC);
this.lmgDisplay.Source = images[randomNumber];
return randomNumber;
}
Определение анимации с помощью Expression Blend
На этом элемент SpinControl почти готов. А теперь