ЯЗЫК ПРОГРАММИРОВАНИЯ C# 2010 И ПЛАТФОРМА .NET 4 5-е издание PRO C# 2010 AND THE .NET 4 PLATFORM Fifth Edition Andrew Troelsen Apress ЯЗЫК ПРОГРАММИРОВАНИЯ C# 2010 И ПЛАТФОРМА .NET 4 5-е издание Эндрю Троелсен W Москва • Санкт-Петербург • Киев 2011 ББК 32.973.26-018.2.75 Т70 УДК 681.3.07 И здательский дом “В ильям с” Зав. редакцией С.Н. Тригуб Перевод с английского Я.П. Волковой, А Л . М оргунова, Н.А. Мухина Под редакцией Ю.Н. Арт ем енко По общ им вопросам обращ айтесь в Издательский дом “В и льям с” по адресу: info@ williainspublishing.com , http://www.williamspublishing.com Троелсен, Эндрю. Т70 Язык программирования C# 2010 и платформа .NET 4.0, 5-е изд. : Пер. с англ. — М. : ООО “И.Д. Вильямс”, 2011. — 1392 с. : ил. — Парал. тит. англ. ISBN 978-5-8459-1682-2 (рус.) ББК 32.973.26-018.2.75 Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механиче­ ские, включая фотокопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства APress, Berkeley, СА. 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. Научно-популярное издание Эндрю Троелсен Язык программирования C# 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 (рус.) ISBN 978-1-43-022549-2 (англ.) © Издательский дом “Вильямс”, 2011 © by Andrew Ttoelsen, 2010 Оглавление Часть I. Общие сведения о языке C# и платформе .NET 43 DiaBa 1. Философия .NET DiaBa 2. Создание приложений на языке C# 44 80 Часть II. Главные конструкции программирования на C# 105 DiaBa 3. Пгавные конструкции программирования на С#: часть I DiaBa 4. DiaBHbie конструкции программирования на С#: часть II DiaBa 5. Определение инкапсулированных типов классов 106 150 185 231 265 292 DiaBa 6. Понятия наследования и полиморфизма DiaBa 7. Структурированная обработка исключений DiaBa 8. Время жизни объектов Часть III. Дополнительные конструкции программирования на C# DiaBa 9. Работа с интерфейсами DiaBa 10. Обобщения DiaBa 11. Делегаты, события и лямбда-выражения DiaBa 12. Расширенные средства языка С # DiaBa 13. LINQ to Objects Часть IV. Программирование с использованием сборок .NET DiaBa 14. Конфигурирование сборок .NET 319 320 356 386 421 463 493 494 DiaBa 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов DiaBa 16. Процессы, домены приложений и контексты объектов DiaBa 17. Язык CIL и роль динамических сборок DiaBa 18. Динамические типы и исполняющая среда динамического языка Часть V. Введение в библиотеки базовых классов .NET DiaBa 19. Многопоточность и параллельное программирование DiaBa 20. Файловый ввод-вывод и сериализация объектов DiaBa 21. ADO.NET, часть I: подключенный уровень DiaBa 22. ADO.NET, часть II: автономный уровень DiaBa 23. ADO.NET, часть III: Entity Framework DiaBa 24. Введение в LINQ to XML DiaBa 25. Введение в Windows Communication Foundation DiaBa 26. Введение в Windows Workflow Foundation 4.0 Часть VI. Построение настольных пользовательских приложений с помощью WPF DiaBa 27. Введение в Windows Presentation Fbundation и XAML DiaBa 28. Программирование с использованием элементов управления WPF DiaBa 29. Службы визуализации графики WPF DiaBa 30. Ресурсы, анимация и стили WPF 542 582 607 648 669 670 711 754 804 857 891 906 961 995 996 1048 1103 1137 DiaBa 31. Шаблоны элементов управления WPF и пользовательские элементы управления Часть VII. Построение веб-приложений с использованием ASP.NET DiaBa 32. Построение веб-страниц ASP.NET DiaBa 33. Веб-элементы управления, мастер-страницы и темы ASP.NET DiaBa 34. Управление состоянием в ASP.NET 1170 1213 1214 1257 1294 Часть VIII. Приложения 1327 Приложение А. Программирование с помощью Windows Forms Приложение Б. Независимая от платформы разработка .NET-приложений с помощью Mono 1328 Предметный указатель 1369 1386 Содержание Об авторе О техническом редакторе Благодарности 31 31 31 Введение Автор и читатели — одна команда Краткий обзор содержания Исходный код примеров От издательства 32 33 33 42 42 Часть I. Общие сведения о языке C# и платформе .NET 43 Глава 1. Философия .NET 44 44 45 45 45 46 46 47 48 49 50 50 52 54 54 56 56 58 58 59 60 60 61 61 62 62 63 63 63 64 66 66 67 71 71 73 74 74 74 74 75 Предыдущее состояние дел Подход с применением языка С и API-интерфейса Windows Подход с применением языка C++ и платформы MFC Подход с применением Visual Basic 6.0 Подход с применением Java Подход с применением СОМ Сложность представления типов данных СОМ Решение .NET Главные компоненты платформы .NET (CLR, CTS и CLS) Роль библиотек базовых классов Что привносит язык C# Другие языки программирования с поддержкой .NET Ж изнь в многоязычном окружении Что собой представляют сборки в .NET Однофайловые и многофайловые сборки Роль CIL Преимущества CIL Компиляция CIL-кода в инструкции, ориентированные на конкретную платформу Роль метаданных типов в .NET Роль манифеста сборки Что собой представляет общая система типов (CTS) Типы классов Типы интерфейсов Типы структур Типы перечислений Типы делегатов Члены типов Встроенные типы данных Что собой представляет общеязыковая спецификация (CLS) Забота о соответствии правилам CLS Что собой представляет общеязыковая исполняющая среда (CLR) Различия между сборками, пространствами имен и типами Роль корневого пространства Microsoft Получение доступа к пространствам имен программным образом Добавление ссылок на внешние сборки Изучение сборки с помощью утилиты ildasm.ехе Просмотр CIL-кода Просмотр метаданных типов Просмотр метаданных сборки (манифеста) Изучение сборки с помощью утилиты Reflector Содержание 7 Развертывание исполняющей среды .NET Клиентский профиль исполняющей среды .NET Не зависящая от платформы природа .NET Резюме 76 77 77 79 Глава 2. Создание приложений на языке C# 80 80 81 Роль комплекта .NET Framework 4 .0 SDK Окно командной строки в Visual Studio 2010 Создание приложений на C# с использованием esc. ехе Указание целевых входных и выходных параметров Добавление ссылок на внешние сборки Добавление ссылок на несколько внешних сборок Компиляция нескольких файлов исходного кода Работа с ответными файлами в C# Создание приложений .NET с использованием Notepad++ Создание приложений.NET с помощью SharpDevelop Создание простого тестового проекта Создание приложений .NET с использованием Visual С# 2010 Express Некоторые уникальные функциональные возможности Visual C# 2010 Express Создание приложений .NET с использованием Visual Studio 2010 Некоторые уникальные функциональные возможности Visual Studio 2010 Ориентирование на .NET Framework в диалоговом окне New Project Использование утилиты Solution Explorer Утилита Class View Утилита Obj ect Browser Встроенная поддержка рефакторинга программного кода Возможности для расширения и окружения кода Утилита Class Designer Интегрируемая система документации .NET Framework 4.0 Резюме 89 90 91 92 92 93 93 95 95 96 98 100 102 104 Часть II. Главные конструкции программирования на C# 105 Глава 3. Главные конструкции программирования на С#: часть I 106 106 108 109 110 111 Разбор простой программы на C# Варианты метода Ма iп () Спецификация кода ошибки в приложении Обработка аргументов командной строки Указание аргументов командной строки в Visual Studio 2010 Интересное отклонение от темы: некоторые дополнительные члены класса System.Environment Класс System.Console Базовый ввод-вывод с помощью класса Console Форматирование вывода, отображаемого в окне консоли Форматирование числовых данных г Форматирование числовых данных в приложениях, отличных от консольных Системные типы данных и их сокращенное обозначение в C# Объявление и инициализация переменных Внутренние типы данных и операция new Иерархия классов типов данных Члены числовых типов данных Члены System.Boolean Члены System.Char Синтаксический разбор значений из строковых данных 81 82 84 84 85 85 87 88 112 113 114 115 116 117 117 119 120 121 123 123 124 125 8 Содержание Типы System.DateTime и System.TimeSpan Пространство имен System.Numerics в .NET4.0 Работа со строковыми данными Базовые операции манипулирования строками Конкатенация строк Управляющие последовательности символов Определение дословных строк Строки и равенство Неизменная природа строк Тип System.Text.StringBuilder Сужающие и расширяющие преобразования типов данных Перехват сужающих преобразований данных Настройка проверки на предмет возникновения условий переполнения в масштабах проекта Ключевое слово unchecked Роль класса System.Convert Неявно типизированные локальные переменные Ограничения, связанные с неявно типизированными переменными Неявно типизированные данные являются строго типизированными Польза от неявно типизированных локальных переменных Итерационные конструкции в С # Цикл for Цикл foreach Использование var в конструкциях foreach Конструкции wh i 1е и do /while Конструкции принятия решений и операции сравнения Оператор if/else Оператор switch Резюме 125 126 127 128 129 130 131 131 132 133 135 137 Глава 4. Главные конструкции программирования на С#: часть II 150 150 151 152 153 154 156 157 158 160 161 162 163 163 165 165 167 Методы и модификаторы параметров Стандартное поведение при передаче параметров Модификатор out Модификатор ref Модификатор par ams Определение необязательных параметров Вызов методов с использованием именованных параметров Перегрузка методов Массивы в С # Синтаксис инициализации массивов в С # Неявно типизированные локальные массивы Определение массива объектов Работа с многомерными массивами Использование массивов в качестве аргументов и возвращаемых значений Базовый класс System.Array Тип enum Управление базовым типом, используемым для хранения значений перечисления Объявление переменных типа перечислений Тип System.Enum Динамическое обнаружение пар “имя/значение” перечисления Типы структур Создание переменных типа структур 139 139 140 140 142 143 144 144 145 145 146 146 147 147 148 149 168 168 169 170 172 173 Содержание Типы значения и ссылочные типы Типы значения, ссылочные типы и операция присваивания Типы значения, содержащие ссылочные типы Передача ссылочных типов по значению Передача ссылочных типов по ссылке Заключительные детали относительно типов значения и ссылочных типов Нулевые типы в C# Работа с нулевыми типами Операция?? Резюме Глава 5. Определение инкапсулированных типов классов Знакомство с типом класса C# Размещение объектов с помощью ключевого слова new Понятие конструктора Роль конструктора по умолчанию Определение специальных конструкторов Еще раз о конструкторе по умолчанию Роль ключевого слова t h is Построение цепочки вызовов конструкторов с использованием t h i s Обзор потока конструктора Еще раз об необязательных аргументах Понятие ключевого слова s t a t i c Определение статических методов Определение статических полей данных Определение статических конструкторов Определение статических классов Основы объектно-ориентированного программирования Роль инкапсуляции Роль наследования Роль полиморфизма Модификаторы доступа С# Модификаторы доступа по умолчанию Модификаторы доступа и вложенные типы Первый принцип: службы инкапсуляции C# Инкапсуляция с использованием традиционных методов доступа и изменения Инкапсуляция с использованием свойств .NET Использование свойств внутри определения класса Внутреннее представление свойств Управление уровнями видимости операторов get/set свойств Свойства, доступные только для чтения и только для записи Статические свойства Понятие автоматических свойств Взаимодействие с автоматическими свойствами Замечания относительно автоматических свойств и значений по умолчанию Понятие синтаксиса инициализации объектов Вызов специальных конструкторов с помощью синтаксиса инициализации Инициализация вложенных типов Работа с данными константных полей Понятие полей только для чтения Статические поля только для чтения Понятие частичных типов Резюме 9 174 175 177 178 179 180 181 183 184 184 185 185 187 188 188 189 190 191 193 195 196 197 198 198 201 202 203 204 204 206 207 208 208 209 210 212 214 215 217 218 218 219 221 221 223 224 225 226 228 228 229 230 10 Содержание Глава 6. Понятия наследования и полиморфизма 231 Базовый механизм наследования Указание родительского класса для существующего класса О множественном наследовании Ключевое слово s e a le d Изменение диаграмм классов Visual Studio Второй принцип ООП: подробности о наследовании Управление созданием базового класса с помощью ключевого слова base Хранение фамильных тайн: ключевое слово protected Добавление запечатанного класса Реализация модели включения/делегации Определения вложенных типов Третий принцип ООП: поддержка полиморфизма в C# Ключевые слова virtual и override Переопределение виртуальных членов в Visual Studio 2010 Запечатывание виртуальных членов Абстрактные классы Полиморфный интерфейс Сокрытие членов Правила приведения к базовому и производному классу Ключевое слово as Ключевое слово is Родительский главный класс System.Object Переопределение System.Object.ToStringO Переопределение System.Object .Equals () Переопределение System.Object.GetHashCode() Тестирование модифицированного класса Person Статические члены System.Object Резюме 231 232 234 234 235 236 238 240 241 242 243 245 245 247 248 249 250 253 255 257 257 258 261 261 262 263 264 264 Глава 7. Структурированная обработка исключений 265 265 266 267 268 269 271 272 273 273 274 275 275 277 278 278 280 281 282 284 285 286 287 287 Ода ошибкам и исключениям Роль обработки исключений в .NET Составляющие процесса обработки исключений в .NET Базовый класс System. Exception Простейший пример Генерация общего исключения Перехват исключений Конфигурирование состояния исключения Свойство TargetSite Свойство StackTrace Свойство HelpLink Свойство Data Исключения уровня системы (System. SystemExcept ion) Исключения уровня приложения (System. ApplicationException) Создание специальных исключений, способ первый Создание специальных исключений, способ второй Создание специальных исключений, способ третий Обработка многочисленных исключений Общие операторы catch Передача исключений Внутренние исключения Блок finally Какие исключения могут выдавать методы Содержание 11 К чему приводят необрабатываемые исключения Отладка необработанных исключений с помощью Visual Studio Несколько слов об исключениях, связанных с поврежденным состоянием Резюме 288 289 290 291 Глава 8. Время жизни объектов Классы, объекты и ссылки Базовые сведения о времени жизни объектов CIL-код, генерируемый для ключевого слова new Установка объектных ссылок в nu 11 Роль корневых элементов приложения Поколения объектов Параллельная сборка мусора в версиях .NET 1.0 — .NET 3.5 Фоновая сборка мусора в версии .NET 4.0 Тип System. GC Принудительная активизация сборки мусора Создание финализируемых объектов Переопределение System. Object.Finalize () Описание процесса финализации Создание высвобождаемых объектов Повторное использование ключевого слова using в C# Создание финализируемых и высвобождаемых типов Формализованный шаблон очистки Отложенная инициализация объектов Настройка процесса создания данных Lazy о Резюме 292 292 293 294 296 297 298 299 300 300 302 304 305 307 307 310 311 312 314 317 318 Часть III. Дополнительные конструкции программирования на C# зш Глава 9. Работа с интерфейсами 320 Что собой представляют типы интерфейсов 320 Сравнение интерфейсов и абстрактных базовых классов 321 Определение специальных интерфейсов 324 Реализация интерфейса 326 Вызов членов интерфейса на уровне объектов 328 Получение ссылок на интерфейсы с помощью ключевого слова as 329 Получение ссылок на интерфейсы с помощью ключевого слова is 329 Использование интерфейсов в качестве параметров 330 Использование интерфейсов в качестве возвращаемых значений 332 Массивы типов интерфейсов 332 Реализация интерфейсов с помощью Visual Studio 2010 333 Устранение конфликтов на уровне имен за счет реализации интерфейсов явным образом 334 Проектирование иерархий интерфейсов 337 Множественное наследование в случае типов интерфейсов 338 Создание перечислимых типов (IEnumerable и IE numerator) 340 Создание методов итератора с помощью ключевого слова yield 343 Создание именованного итератора 344 Внутреннее представление метода итератора 345 Создание клонируемых объектов (ICloneable) 346 Более сложный пример клонирования 348 Создание сравнимых объектов (IComparable) 350. Указание множества критериев для сортировки (IComparer) 353 Использование специальных свойств и специальных типов для сортировки 354 Резюме 355 12 Содержание Глава 10. Обобщения Проблемы, связанные с необобщенными коллекциями Проблема производительности Проблемы с безопасностью типов Роль параметров обобщенных типов Указание параметров типа для обобщенных классов и структур Указание параметров типа для обобщенных членов Указание параметров типов для обобщенных интерфейсов Пространство имен System.Collections .Generic Синтаксис инициализации коллекций Работа с классом List<T> Работа с классом Stack<T> Работа с классом Queue<Т> Работа с классом SortedSet<T> Создание специальных обобщенных методов Выведение параметра типа Создание специальных обобщенных структур и классов Ключевое слово de fau It в обобщенном коде Обобщенные базовые классы Ограничение параметров типа Примеры использования ключевого слова where Недостаток ограничений операций Резюме Глава 11. Делегаты, события и лямбда-выражения Понятие типа делегата .NET Определение типа делегата в C# Базовые классы System.MulticastDelegate и System.Delegate Простейший пример делегата Исследование объекта делегата Отправка уведомлений о состоянии объекта с использованием делегатов Включение группового вызова Удаление целей из списка вызовов делегата Синтаксис групповых преобразований методов Понятие ковариантности делегатов Понятие обобщенных делегатов Эмуляция обобщенных делегатов без обобщений Понятие событий C# Ключевое слово event “За кулисами” событий Прослушивание входящих событий Упрощенная регистрация событий с использованием Visual Studio 2010 Создание специальных аргументов событий Обобщенный делегат EventHandler<T> Понятие анонимных методов C# Доступ к локальным переменным Понятие лямбда-выражений Анализ лямбда-выражения Обработка аргументов внутри множества операторов Лямбда-выражения с несколькими параметрами и без параметров Усовершенствование примера PrimAndProperCarEvents за счет использования лямбда-выражений Резюме 356 356 358 362 365 366 367 367 369 370 371 373 374 375 376 378 379 380 381 382 383 384 385 386 386 387 389 391 392 393 396 397 398 400 402 403 404 405 406 407 408 409 410 411 413 413 416 417 418 419 419 Содержание 13 Глава 12. Расширенные средства языка C# 421 Понятие методов-индексаторов Индексация данных с использованием строковых значений Перегрузка методов-индексаторов Многомерные индексаторы Определения индексаторов в интерфейсных типах Понятие перегрузки операций Перегрузка бинарных операций А как насчет операций += и -=? Перегрузка унарных операций Перегрузка операций эквивалентности Перегрузка операций сравнения .внутреннее представление перегруженных операций Финальные соображения относительно перегрузки операций Понятие преобразований пользовательских типов Числовые преобразования Преобразования между связанными типами классов Создание специальных процедур преобразования Дополнительные явные преобразования типа Square Определение процедур неявного преобразования Внутреннее представление процедур пользовательских преобразований Понятие расширяющих методов Понятие частичных методов Понятие анонимных типов Анонимные типы, содержащие другие анонимные типы Работа с типами указателей Ключевое слово u nsafe Работа с операциями * и & Небезопасная и безопасная функция обмена значений Доступ к полям через указатели (операция ->) Ключевое слово s t a c k a llo c Закрепление типа ключевым словом f i x e d Ключевое слово s i z e o f Резюме 421 423 424 425 425 426 427 429 429 430 431 432 433 434 434 434 435 437 438 439 440 448 450 454 455 456 458 459 459 460 460 461 462 Глава 13. LINQ to Objects 463 463 464 464 465 466 466 467 468 468 469 470 471 472 473 474 475 475 476 Программные конструкции, специфичные для LINQ Неявная типизация локальных переменных Синтаксис инициализации объектов и коллекций Лямбда-выражения Расширяющие методы Анонимные типы Роль LINQ Выражения LINQ строго типизированы Основные сборки LINQ Применение запросов LINQ к элементарным массивам Решение без использования LINQ Рефлексия результирующего набора LINQ LINQ и неявно типизированные локальные переменные LINQ и расширяющие методы Роль отложенного выполнения Роль немедленного выполнения Возврат результата запроса LINQ Возврат результатов LINQ через немедленное выполнение 14 Содержание Применение запросов LINQ к объектам коллекций Доступ к содержащимся в контейнере подобъектам Применение запросов LINQ к необобщенным коллекциям Фильтрация данных с использованием OfType<T>() Исследование операций запросов LINQ Базовый синтаксис выборки Получение подмножества данных Проекция новых типов данных Получение счетчиков посредством Enumerable Обращение результирующих наборов Выражения сортировки LINQ как лучшее средство построения диаграмм Исключение дубликатов Агрегатные операции LINQ Внутреннее представление операторов запросов LINQ Построение выражений запросов с использованием операций запросов Построение выражений запросов с использованием типа Enumerable и лямбда-выражений Построение выражений запросов с использованием типа Enumerable и анонимных методов Построение выражений запросов с использованием типа Enumerable и низкоуровневых делегатов Резюме • 477 478 478 479 480 481 482 482 484 484 484 485 486 486 487 488 488 490 490 491 Часть IV. Программирование с использованием сборок .NET 493 Глава 14. Конфигурирование сборок .NET 494 494 Определение специальных пространств имен Устранение конфликтов на уровне имен за счет использования полностью уточненных имен Устранение конфликтов на уровне имен за счет использования псевдонимов Создание вложенных пространств имен Пространство имен, используемое по умолчанию в Visual Studio 2010 Роль сборок .NET Сборки повышают возможность повторного использования кода Сборки определяют границы типов Сборки являются единицами, поддерживающими версии Сборки являются самоописываемыми Сборки поддаются конфигурированию Формат сборки .NET Заголовок файла Windows Заголовок файла CLR CIL-код, метаданные типов и манифест сборки Необязательные ресурсы сборки Однофайловые и многофайловые сборки Создание и использование однофайловой сборки Исследование манифеста Исследование CIL-кода Исследование метаданных типов Создание клиентского приложения на С # Создание клиентского приложения на Visual Basic Межъязыковое наследование в действии Создание и использование многофайловой сборки Исследование файла uf о .netmodule 496 497 498 499 500 500 501 501 501 501 502 502 504 505 505 505 506 510 512 512 513 514 515 516 517 Содержание Исследование файла airvehicles .dll Использование многофайловой сборки Приватные сборки Идентификационные данные приватной сборки Процесс зондирования Конфигурирование приватных сборок Конфигурационные файлы и Visual Studio 2010 Разделяемые сборки Строгие имена Генерирование строгих имен в командной строке Генерирование строгих имен с помощью Visual Studio 2010 Установка сборок со строгими именами в GAC Просмотр содержимого GAC с помощью проводника Windows Использование разделяемой сборки Исследование манифеста SharedCarLibClient Конфигурирование разделяемых сборок Фиксация текущей версии разделяемой сборки Создание разделяемой сборки версии 2.0.0.0 Динамическое перенаправление на конкретную версию разделяемой сборки Сборки политик издателя Отключение политик издателя Элемент <codeBase> Пространство имен System. Configuration Резюме Глава 15. Рефлексия типов, позднее связывание и программирование с использованием атрибутов Необходимость в метаданных типов Просмотр (части) метаданных перечисления EngineState Просмотр (части) метаданных типа Саг Изучение блока ТурeRef Просмотр метаданных самой сборки Просмотр метаданных внешних сборок, на которые имеются ссылки в текущей сборке Просмотр метаданных строковых литералов Рефлексия Класс System.Туре Получение информации о типе с помощью System.Object.GetType () Получение информации о типе с помощью typeof () Получение информации о типе с помощью System.Туре .GetType () Создание специальной программы для просмотра метаданных Рефлексия методов Рефлексия полей и свойств Рефлексия реализуемых интерфейсов Отображение различных дополнительных деталей Реализация метода Main () Рефлексия обобщенных типов Рефлексия параметров и возвращаемых значений методов Динамически загружаемые сборки Рефлексия разделяемых сборок Позднее связывание Класс System.Activator Вызов методов без параметров Вызов методов с параметрами Роль атрибутов .NET 15 518 518 519 520 520 521 522 524 524 526 528 529 530 531 532 533 533 533 536 537 538 538 540 541 542 542 543 544 546 546 546 547 547 548 549 549 549 550 550 551 552 552 552 554 554 556 558 560 560 562 563 564 16 Содержание Потребители атрибутов Применение предопределенных атрибутов в C# Сокращенное обозначение атрибутов в C# Указание параметров конструктора для атрибутов Атрибут [Obsolete] в действии Создание специальных атрибутов Применение специальных атрибутов Синтаксис именованных свойств Ограничение использования атрибутов Атрибуты уровня сборки и модуля Файл Assembly Inf о .cs, генерируемый Visual Studio 2010 Рефлексия атрибутов с использованием раннего связывания Рефлексия атрибутов с использованием позднего связывания Возможное применение на практике рефлексии, позднего связывания и специальных атрибутов Создание расширяемого приложения Создание сборки CommonSnappableTypes .dll Создание оснастки на C# Создание оснастки на Visual Basic Создание расширяемого приложения Windows Forms Резюме 565 565 567 567 567 568 569 569 570 571 572 572 573 Глава 16. Процессы, домены приложений и контексты объектов 582 582 583 585 587 588 588 590 592 592 594 594 596 597 598 599 600 601 602 603 604 604 606 606 Роль процесса Windows Роль потоков Взаимодействие с процессами в рамках платформы .NET Перечисление выполняющихся процессов Изучение конкретного процесса Изучение набора потоков процесса Изучение набора модулей процесса Запуск и останов процессов программным образом Управление запуском процесса с использованием класса ProcessStartlnfo Домены приложений .NET Класс System.AppDomain Взаимодействие с используемым по умолчанию доменом приложения Перечисление загружаемых сборок Получение уведомлений о загрузке сборок Создание новых доменов приложений Загрузка сборок в специальные домены приложений Выгрузка доменов приложений программным образом Границы контекстов объектов Контекстно-свободные и контекстно-зависимые типы Определение контекстно-зависимого объекта Инспектирование контекста объекта Итоговые сведения о процессах, доменах приложений и контекстах Резюме Глава 17. Язык CIL и роль динамических сборок Причины для изучения грамматики языка CIL Директивы, атрибуты и коды операций в CIL Роль директив CIL Роль атрибутов CIL 1 Роль кодов операций CIL Разница между кодами операций и их мнемоническими эквивалентами в CIL 575 576 576 577 577 578 581 607 607 608 609 609 609 609 Содержание Помещение и извлечение данных из стека в CIL Двунаправленное проектирование Роль меток в коде CIL Взаимодействие с CIL: модификация файла * . i l Компиляция CIL-кода с помощью i 1a sm. ехе Создание CIL-кода с помощью SharpDevelop P o л ь p e v e r if у . ехе Использование директив и атрибутов в CIL Добавление ссылок на внешние сборки в CIL Определение текущей сборки в CIL Определение пространств имен в CIL Определение типов классов в CIL Определение и реализация интерфейсов в CIL Определение структур в CIL Определение перечислений в CIL Определение обобщений в CIL Компиляция файла СILTypes . i l Соответствия между типами данных в библиотеке базовых классов .NET, C# и CIL Определение членов типов в CIL Определение полей данных в CIL Определение конструкторов для типов в CIL Определение свойств в CIL Определение параметров членов Изучение кодов операций в CIL Директива .m axstack Объявление локальных переменных в CIL Отображение параметров на локальные переменные в CIL Скрытая ссылка t h is Представление итерационных конструкций в CIL Создание сборки .NET на CIL Создание СIL C a r s . d l l Создание C lL C a r C lie n t . ехе Динамические сборки Пространство имен System. Re f l e e t io n .Em it Роль типа S y s t e m .R e fle c t io n .E m it. IL G e n e ra to r Создание динамической сборки Генерация сборки и набора модулей Роль типа M od u leB u ild er Генерация типа H e llo C la s s n принадлежащей ему строковой переменной Генерация конструкторов Генерация метода S a y H ello () Использование динамически сгенерированной сборки Резюме Глава 18. Динамические типы и исполняющая среда динамического языка Роль ключевого слова C# dynamic Вызов членов на динамически объявленных данных Роль сборки Microsoft.CSharp.dll Область применения ключевого слова dynamic Ограничения ключевого слова dynamic Практическое применение ключевого слова dynamic Роль исполняющей среды динамического языка (DLR) Роль деревьев выражений Роль пространства имен System. Dynamic 17 610 612 615 616 617 618 619 619 619 620 620 621 622 623 623 623 624 625 626 626 627 627 628 628 631 631 632 633 633 634 634 637 638 639 640 641 642 643 644 645 646 646 647 648 648 650 651 652 653 653 654 654 655 18 Содержание Динамический поиск в деревьях выражений во время выполнения Упрощение вызовов позднего связывания с использованием динамических типов Использование ключевого слова dynamic для передачи аргументов Упрощение взаимодействия с СОМ посредством динамических данных Роль первичных сборок взаимодействия Встраивание метаданных взаимодействия Общие сложности взаимодействия с СОМ Взаимодействие с СОМ с использованием средств языка C# 4.0 Взаимодействие с СОМ без использования средств языка C# 4.0 Резюме 655 656 657 659 660 661 661 662 666 667 Часть V. Введение в библиотеки базовых классов .NET 669 Глава 19. Многопоточность и параллельное программирование 670 670 671 672 672 674 675 675 676 676 678 679 680 681 682 683 684 684 685 685 687 688 689 690 692 694 695 696 697 698 700 700 701 702 703 704 705 708 709 709 710 Отношения между процессом, доменом приложения, контекстом и потоком Проблема параллелизма Роль синхронизации потоков Краткий обзор делегатов .NET Асинхронная природа делегатов М етоды Beginlnvoke() HEndlnvokeO Интерфейс System.IAsyncResult Асинхронный вызов метода Синхронизация вызывающего потока Роль делегата AsyncCallback Роль класса AsyncResult Передача и прием специальных данных состояния Пространство имен System.Threading Класс System.Threading.Thread Получение статистики о текущем потоке Свойство Name Свойство Priority Программное создание вторичных потоков Работа с делегатом ThreadStart Работа с делегатом ParametrizedThreadStart Класс AutoResetEvent Потоки переднего плана и фоновые потоки Пример проблемы, связанной с параллелизмом Синхронизация с использованием ключевого слова C# lock Синхронизация с использованием типа System.Threading.Monitor Синхронизация с использованием типа System.Threading. Interlocked Синхронизация с использованием атрибута [Synchronization] Программирование с использованием обратных вызовов Tim er Пул потоков CLR Параллельное программирование на платформе .NET Интерфейс Thsk Parallel Library API Роль класса Parallel Понятие параллелизма данных Класс Task Обработка запроса на отмену Понятие параллелизма задач Запросы параллельного LINQ (PLINQ) Выполнение запроса PLINQ Отмена запроса PLINQ Резюме л Содержание Глава 20. Файловый ввод-вывод и сериализация объектов Исследование пространства имен System.10 Классы Directory (Directorylnfo) и File (Filelnfo) Абстрактный базовый класс FileSystemlnfo Работа с типом Directorylnfo Перечисление файлов с помощью типа Directorylnfo Создание подкаталогов с помощью типа Directorylnfo Работа с типом Directory Работа с типом Drive Info « Работа с классом Filelnfo Метод Filelnfo.Create () Метод Filelnfo.Open() Методы FileOpen.OpenRead() и Filelnfo.OpenWrite () Метод Filelnfo.OpenText () Методы Filelnfo.CreateText () и Filelnfo.AppendText () Работа с типом File Дополнительные члены File Абстрактный класс Stream Работа с классом FileStream Работа с классами StreamWriter и StreamReader Запись в текстовый файл Чтение из текстового файла Прямое создание экземпляров классов StreamWriter/StreamReader Работа с классами StringWriter и StringReader Работа с классами BinaryWriter и BinaryReader Программное отслеживание файлов Понятие сериализации объектов Роль графов объектов Конфигурирование объектов для сериализации Определение сериализуемых типов Общедоступные поля, приватные поля и общедоступные свойства Выбор форматера сериализации Интерфейсы IFormatter и IRemotingFormatter Точность типов среди форматеров Сериализация объектов с использованием BinaryFormatter Десериализация объектов с использованием BinaryFormatter Сериализация объектов с использованием SoapFormatter Сериализация объектов с использованием XmlSerializer Управление генерацией данных XML Сериализация коллекций объектов Настройка процессов сериализации SOAP и двоичной сериализации Углубленный взгляд на сериализацию объектов Настройка сериализации с использованием интерфейса ISerializable Настройка сериализации с использованием атрибутов Резюме Глава 21. AD0.NET, часть I: подключенный уровень Высокоуровневое определение ADO.NET Три стороны ADO.NET Поставщики данных ADO.NET Поставщики данных ADO.NET от Microsoft О сборке System.Data.OracleClient.dll Получение сторонних поставщиков данных ADO.NET Дополнительные пространства имен ADO.NET 19 711 711 712 713 714 715 716 717 717 719 719 720 721 722 722 722 723 724 725 726 727 728 729 730 731 732 734 736 737 737 738 738 739 740 741 742 743 743 744 746 747 748 749 751 752 754 754 755 756 757 759 759 759 20 Содержание Типы из пространства имен System.Data Роль интерфейса IDbConneсt ion Роль интерфейса IDbTra ns act ion Роль интерфейса IDbCommand Роль интерфейсов IDbDataParameter и IDataParameter Роль интерфейсов IDbDataAdapter и IDataAdapter Роль интерфейсов IDataReader и IDataRecord Абстрагирование поставщиков данных с помощью интерфейсов Повышение гибкости с помощью конфигурационных файлов приложения Создание базы данных AutoLot Создание таблицы Inventory Создание хранимой процедуры GetPetName () Создание таблиц Customers и Orders Визуальное создание отношений между таблицами Модель генератора поставщиков данных ADO.NET Полный пример генератора поставщиков данных Возможные трудности с моделью генератора поставщиков Э лем ент <connectionStrings> Подключенный уровень ADO.NET Работа с объектами подключения Работа с объектами ConnectionStringBuilder Работа с объектами команд Работа с объектами чтения данных Получение множественных результатов с помощью объекта чтения данных Создание повторно используемой библиотеки доступа к данным Добавление логики подключения Добавление логики вставки Добавление логики удаления Добавление логики изменения Добавление логики выборки Работа с параметризованными объектами команд Выполнение хранимой процедуры Создание консольного пользовательского интерфейса Реализация метода Main () Реализация метода Showlnstructions () Реализация метода ListInventory () Реализация метода DeleteCar () Реализация метода InsertNewCar () Реализация метода UpdateCarPetName () Реализация метода LookUpPetName () Транзакции баз данных Основные члены объекта транзакции ADO.NET Добавление таблицы CreditRisks в базу данных AutoLot Добавление метода транзакции в InventoryDAL Тестирование транзакции в нашей базе данных Резюме Глава 22. AD0.NET, часть II: автономный уровень Знакомство с автономным уровнем ADO.NET Роль объектов D ataSet Основные свойства класса DataSet Основные методы класса DataSet Создание DataSet Работа с объектами DataColumn 760 761 762 762 762 763 763 764 766 767 767 769 770 772 772 774 776 777 778 779 781 782 783 784 785 786 787 788 788 789 790 792 793 794 795 795 796 797 797 798 799 800 800 801 802 803 804 804 805 806 807 807 808 Содержание Создание объекта DataColumn Включение автоинкрементных полей Добавление объектов DataColumn в DataTable Работа с объектами DataRow Свойство RowState Свойство DataRowVersion Работа с объектами DataTable Вставка объектов DataTable в DataSet Получение данных из объекта DataSet Обработка данных из DataTable с помощью объектов DataTableReader Сериализация объектов DataTable и DataSet в формате XML Сериализация объектов DataTable и DataSet в двоичном формате Привязка объектов DataTable к графическим интерфейсам Windows Forms Заполнение DataTable из обобщенного List<T> Удаление строк из DataTable Выборка строк с помощью фильтра Изменение строк в DataTable Работа с типом DataView Работа с адаптерами данных Простой пример адаптера данных Замена имен из базы данных более понятными названиями Добавление в AutoLotDAL.dll возможности отключения Определение начального класса Настройка адаптера данных с помощью SqlCommandBuilder Реализация метода Get All Invent or у () Реализация метода Updatelnventory () Установка номера версии Тестирование автономной функциональности Объекты DataSet для нескольких таблиц и взаимосвязь данных Подготовка адаптеров данных Создание отношений между таблицами Изменение таблиц базы данных Переходы между взаимосвязанными таблицами Средства конструктора баз данных в Windows Forms Визуальное проектирование элементов DataGridView Сгенерированный файл арр. conf ig Анализ строго типизированного DataSet Анализ строго типизированного DataTable Анализ строго типизированного DataRow Анализ строго типизированного адаптера данных Завершение приложения Windows Forms Выделение строго типизированного кода работы с базами данных в библиотеку классов Просмотр сгенерированного кода Выборка данных с помощью сгенерированного кода Вставка данных с помощью сгенерированного кода Удаление данных с помощью сгенерированного кода Вызов хранимой процедуры с помощью сгенерированного кода Программирование с помощью LINQ to DataSet Библиотека расширений DataSet Получение DataTable, совместимого с LINQ Метод расширения DataRowExtensions .Field<T> () Заполнение новых объектов DataTable с помощью LINQ-запросов Резюме 21 809 810 810 810 812 813 814 815 815 816 817 818 819 820 822 823 826 826 828 829 829 830 831 831 833 833 833 833 834 835 836 837 837 839 840 843 843 845 845 845 846 847 848 849 850 850 851 851 853 853 855 855 856 22 Содержание Глава 23. ADO.NET, часть III: Entity Framework Роль Entity Framework Роль сущностей Строительные блоки Entity Framework Роль служб объектов Роль клиента сущности Роль файла *.edmx Роль классов ObjectContext и ObjectSet<T> Собираем все вместе Построение и анализ первой модели EDM Генерация файла *. е dmх Изменение формы сущностных данных Просмотр отображений Просмотр данных сгенерированного файла *. еdmx Просмотр сгенерированного исходного кода Улучшение сгенерированного исходного кода Программирование с использованием концептуальной модели Удаление записи Обновление записи Запросы с помощью LINQ to Entities Запросы с помощью Entity SQL Работа с объектом EntityDataReader Проект AutoLotDAL версии 4.0, теперь с сущностями Отображение хранимой процедуры Роль навигационных свойств Использование навигационных свойств внутри запросов LINQ to Entity Вызов хранимой процедуры Привязка данных сущностей к графическим пользовательским интерфейсам Windows Forms Добавление кода привязки данных Резюме Глава 24. Введение в LINQ to XML История о двух API-интерфейсах XML Интерфейс LINQ to XML как лучшая модель DOM Синтаксис литералов Visual Basic как наилучший интерфейс LINQ to XML Члены пространства имен System.Xml.Linq Осевые методы LINQ to XML Избыточность XName (и XNamespace) Работа c XElement и XDocument Генерация документов из массивов и контейнеров Загрузка и разбор XML-содержимого Манипулирование XML-документом в памяти Построение пользовательского интерфейса приложения LINQ to XML Импорт файла Inventory, xml Определение вспомогательного класса LINQ to XML Оснащение пользовательского интерфейса вспомогательными методами Резюме Глава 25. Введение в Windows Communication Foundation API-интерфейсы распределенных вычислений Роль DCOM Роль служб СОМ+/Enterprise Services Роль MSMQ 857 857 859 860 861 861 863 863 865 866 866 869 871 871 873 875 875 876 877 878 879 880 881 881 882 884 885 886 888 890 891 891 893 893 895 895 897 898 899 901 901 901 902 902 904 905 906 906 907 908 909 Содержание Роль .NET Remo ting Роль веб-служб XML Именованные каналы, сокеты и P2P Роль WCF Обзор средств WCF Обзор архитектуры, ориентированной на службы WCF: итоги Исследование основных сборок WCF Шаблоны проектов WCF в Visual Studio Шаблон проекта WCF Service Базовая композиция приложения WCF Понятие АВС в WCF Понятие контрактов WCF Понятие привязок WCF Понятие адресов WCF Построение службы WCF Атрибут [ServiceContract] Атрибут [OperationContract] Служебные типы как контракты операций Хостинг службы WCF Установка АВС внутри файла Арр. сon fig Кодирование с использованием типа ServiceHost Указание базового адреса Подробный анализ типа ServiceHost Подробный анализ элемента <system. serviceModel> Включение обмена метаданными Построение клиентского приложения WCF Генерация кода прокси с использованием svcutil.exe Генерация кода прокси с использованием Visual Studio 2010 Конфигурирование привязки на основе TCP Упрощение конфигурационных настроек в WCF 4.0 Конечные точки по умолчанию в WCF 4.0 Предоставление одной службы WCF с использованием множества привязок Изменение установок для привязки WCF Конфигурация поведения МЕХ по умолчанию в WCF 4.0 Обновление клиентского прокси и выбор привязки Использование шаблона проекта WCF Service Library Построение простой математической службы Тестирование службы WCF с помощью WcfTestClient.exe Изменение конфигурационных файлов с помощью SvcConfigEditor.exe Хостинг службы WCF в виде службы Windows Спецификация АВС в коде Включение МЕХ Создание программы установки для службы Windows Установка службы Windows Асинхронный вызов службы на стороне клиента Проектирование контрактов данных WCF Использование веб-ориентированного шаблона проекта WCF Service Реализация контракта службы Роль файла *.svc Содержимое файла Web. сon fig Тестирование службы Резюме 23 909 910 913 913 914 914 915 916 917 918 919 920 920 921 924 925 926 927 928 928 929 930 930 932 933 934 936 937 938 939 940 941 942 943 944 945 946 947 947 948 949 950 951 952 953 954 955 956 958 959 959 960 960 24 Содержание Глава 26. Введение в Windows Workflow Foundation 4.0 961 Определение бизнес-процесса Роль WF 4.0 Построение простого рабочего потока Просмотр полученного кода XAML Исполняющая среда WF 4.0 Хостинг рабочего потока с использованием класса Workf lowlnvoker Хостинг рабочего потока с использованием класса Workf lowApplication Переделка первого рабочего потока Знакомство с действиями Windows Workflow 4.0 Действия потока управления Действия блок-схемы Действия обмена сообщениями Действия исполняющей среды и действия-примитивы Действия транзакций Действия над коллекциями и действия обработки ошибок Построение рабочего потока в виде блок-схемы Подключение действий к блок-схеме Работа с действием InvokeMethod Определение переменных уровня рабочего потока Работа с действием FlowDeсision Работа с действием TerminateWorkf low Построение условия “true” Работа с действием ForEach<T> Завершение приложения Промежуточные итоги Изоляция рабочих потоков в выделенных библиотеках Определение начального проекта Импорт сборок и пространств имен Определение аргументов рабочего потока Определение переменных рабочего потока Работа с действием Assign Работа с действиями If и Switch Построение специального действия кода Использование библиотеки рабочего потока Получение выходного аргумента рабочего потока Резюме 962 962 963 965 967 967 970 971 971 971 972 973 973 974 974 975 975 976 977 978 978 979 979 981 982 984 984 985 986 986 987 987 988 991 992 993 Часть VI. Построение настольных пользовательских П р и лож ен и й С ПОМОЩЬЮ WPF 995 Глава 27. Введение в Windows Presentation Foundation и XAML Мотивация, лежащая в основе WPF Унификация различных API-интерфейсов Обеспечение разделения ответственности через XAML Обеспечение оптимизированной модели визуализации Упрощение программирования сложных пользовательских интерфейсов Различные варианты приложений WPF Традиционные настольные приложения WPF-приложения на основе навигации Приложения ХВАР Отношения между WPF и Silverlight Исследование сборок WPF 996 997 997 998 998 999 1000 1000 1001 1002 1003 1004 Содержание 25 Роль класса Application Роль класса Window Роль класса System.Windows.Controls.ContentControl Роль класса System.Windows .Controls .Control Роль класса System.Windows.FrameworkElement Роль класса System.Windows .UIElement Роль класса System.Windows.Media.Visual Роль класса System.Windows.DependencyObject Роль класса System.Windows.Threading.DispatcherObject Построение приложения WPF без XAML Создание строго типизированного окна Создание простого пользовательского интерфейса Взаимодействие с данными уровня приложения Обработка закрытия объекта Window Перехват событий мыши Перехват клавиатурных событий Построение приложения WPF с использованием только XAML Определение Ма inWindow в XAML Определение объекта Application в XAML Обработка файлов XAML с помощью msbuild.exe Трансформация разметки в сборку .NET Отображение XAML-данных окна на код C# Роль BAML Отображение XAML-данных приложения на код C# Итоговые замечания о процессе трансформирования XAML в сборку Синтаксис XAML для WPF Введение в Kaxaml Пространства имен XAML XML и “ключевые слова” XAML Управление объявлениями классов и переменных-членов Элементы XAML, атрибуты XAML и преобразователи типов Понятие синтаксиса XAML “свойство-элемент” Понятие присоединяемых свойств XAML Понятие расширений разметки XAML Построение приложений WPF с использованием файлов отделенного кода Добавление файла кода для класса MainWindow Добавление файла кода для класса МуАрр Обработка файлов кода с помощью msbuild.exe Построение приложений WPF с использованием Visual Studio 2010 Шаблоны проектов WPF Знакомство с инструментами визуального конструктора WPF Проектирование графического интерфейса окна Реализация события Loaded Реализация события Click объекта Button Реализация события C lo sed Тестирование приложения Резюме 1005 1007 1007 1008 1009 1010 1010 1010 1011 1011 1013 1013 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1027 1027 1029 1031 1032 1033 1034 1034 1036 1036 1037 1038 1038 1039 1039 1042 1044 1045 1046 1046 1047 Глава 28. Программирование с использованием элементов управления WPF 1048 1048 1049 1051 1051 1052 1052 Обзор библиотеки элементов управления WPF Работа с элементами управления WPF в Visual Studio 2010 Элементы управления Ink API Элементы управления документами WPF Общие диалоговые окна WPF Подробные сведения находятся в документации 26 Содержание Управление компоновкой содержимого с использованием панелей Позиционирование содержимого внутри панелей Canvas Позиционирование содержимого внутри панелей WrapPanel Позиционирование содержимого внутри панелей StackPanel Позиционирование содержимого внутри панелей Grid Позиционирование содержимого внутри панелей DockPanel Включение прокрутки в типах панелей Построение главного окна с использованием вложенных панелей Построение системы меню Построение панели инструментов Построение строки состояния Завершение дизайна пользовательского интерфейса Реализация обработчиков событий MouseEnter/MouseLeave Реализация логики проверки правописания Понятие управляющих команд WPF Внутренние объекты управляющих команд Подключение команд к свойству Command Подключение команд к произвольным действиям Работа с командами Open и Save Построение пользовательского интерфейса WPF с помощью Expression Blend Ключевые аспекты IDE-среды Expression Blend Использование элемента TabControl Построение вкладки Ink API Проектирование элемента Tool Bar Элемент управления RadioButton Элемент управления InkCanvas Элемент управления ComboBox Сохранение, загрузка и очистка данных InkCanvas Введение в интерфейс Documents API Блочные элементы и встроенные элементы Диспетчеры компоновки документа Построение вкладки Documents Наполнение FlowDocument с использованием Blend Наполнение FlowDocument с помощью кода Включение аннотаций и “клейких” заметок Сохранение и загрузка потокового документа Введение в модель привязки данных WPF Построение вкладки Data Binding Установка привязки данных с использованием Blend Свойство DataContext Преобразование данных с использованием IValueConverter Установка привязок данных в коде Построение вкладки DataGrid Резюме 1062 1063 1064 1065 1065 1066 1066 1067 1067 1068 1069 1071 1072 1073 1077 1079 1080 1082 1084 1086 1087 1088 1088 1089 1089 1091 1091 1093 1094 1095 1096 1096 1097 1099 1099 1100 1102 Глава 29. Службы визуализации графики WPF 1103 Службы графической визуализации WPF Опции графической визуализации WPF Визуализация графических данных с использованием фигур Добавление прямоугольников, эллипсов и линий на поверхность Canvas Удаление прямоугольников, эллипсов и линий с поверхности Canvas Работа с элементами Polyline и Polygon Работа с элементом Path Кисти и перья WPF 1103 1104 1105 1107 1110 1110 1111 1115 1053 1054 1056 1058 1059 1060 1061 Содержание Конфигурирование кистей с использованием Visual Studio 2010 Конфигурирование кистей в коде Конфигурирование перьев Применение графических трансформаций Первый взгляд на трансформации Трансформация данных С a nva s Работа с фигурами в Expression Blend Выбор фигуры для визуализации из палитры инструментов Преобразование фигур в пути Комбинирование фигур Редакторы кистей и трансформаций Визуализация графических данных с использованием рисунков и геометрий Построение кисти DrawingBrush с использованием объектов Geometry Рисование с помощью DrawingBrush Включение типов Drawing в Drawinglmage Генерация сложной векторной графики с использованием Expression Design Экспорт документа Expression Design в XAML Визуализация графических данных с использованием визуального уровня Базовый класс Visual и производные дочерние классы Первый взгляд на класс DrawingVisual Визуализация графических данных в специальном диспетчере компоновки Реагирование на операции проверки попадания Резюме 1115 1117 1118 1118 1119 1120 1122 1122 1123 1123 1123 1125 1126 1127 1128 1128 1129 1130 1130 1131 1133 1134 1136 Глава 30. Ресурсы, анимация и стили WPF Система ресурсов WPF Работа с двоичными ресурсами Программная загрузка изображения Работа с объектными (логическими) ресурсами Роль свойства Resources Определение ресурсов уровня окна Расширение разметки {StaticResource} Изменение ресурса после извлечения Расширение разметки {DynamicResource} Ресурсы уровня приложения Определение объединенных словарей ресурсов Определение сборки из одних ресурсов Извлечение ресурсов в Expression Blend Службы анимации WPF Роль классов анимации Свойства То, From и By Роль базового класса Timeline Написание анимации в коде C# Управление темпом анимации Запуск в обратном порядке и циклическое выполнение анимации Описание анимации в XAML Роль раскадровки Роль триггеров событий Анимация с использованием дискретных ключевых кадров Роль стилей WPF Определение и применение стиля Переопределение настроек стиля Автоматическое применение стиля с помощью TargetType Создание подклассов существующих стилей 27 4 1137 1137 1138 1139 1142 1143 1143 1145 1145 1146 1146 1147 1149 1150 1152 1152 1153 1154 1154 1155 1156 1157 1158 1158 1159 1160 1160 1161 1161 1162 28 Содержание Роль безымянных стилей Определение стилей с триггерами Определение стилей с множеством триггеров Анимированные стили Программное применение стилей Генерация стилей с помощью Expression Blend Работа с визуальными стилями по умолчанию Резюме Глава 31. Шаблоны элементов управления WPF и пользовательские элементы управления 1163 1164 1164 1165 1165 1166 1167 1169 Роль свойств зависимости Проверка существующего свойства зависимости Важные замечания относительно оболочек свойств CLR Построение специального свойства зависимости Добавление процедуры проверки достоверности данных Реакция на изменение свойства Маршрутизируемые события Роль маршрутизируемых пузырьковых событий Продолжение или прекращение пузырькового распространения Роль маршрутизируемых туннелируемых событий Логические деревья, визуальные деревья и шаблоны по умолчанию Программный просмотр логического дерева Программный просмотр визуального дерева Программный просмотр шаблона по умолчанию для элемента управления Построение специального шаблона элемента управления в Visual Studio 2010 Шаблоны как ресурсы Включение визуальных подсказок с использованием триггеров Роль расширения разметки {TemplateBinding} Роль класса ContentPresenter Включение шаблонов в стили Построение специальных элементов UserControl с помощью Expression Blend Создание проекта библиотеки UserControl Создание WPF-приложения JackpotDeluxe Извлечение UserControl из геометрических объектов Роль визуальных состояний .NET 4.0 Завершение приложения JackpotDeluxe Резюме 1170 1170 1172 1175 1176 1179 1179 1181 1182 1182 1183 1185 1185 1187 1188 1191 1192 1193 1194 1196 1196 1197 1198 1204 1204 1205 1209 1212 Часть VII. Построение веб-приложений с использованием ASP.NET 1213 Глава 32. Построение веб-страниц ASP.NET 1214 1214 1215 Роль протокола HTTP Цикл запрос/ответ HTTP HTTP — протокол без поддержки состояния Веб-приложения и веб-серверы Роль виртуальных каталогов IIS Веб-сервер разработки ASP.NET Роль языка HTML Структура HTML-документа Роль формы HTML Инструменты визуального конструктора HTML в Visual Studio 2010 Построение формы HTML Роль сценариев клиентской стороны Пример сценария клиентской стороны 1215 1216 1216 1217 1217 1218 1219 1219 1220 1221 1223 Содержание 29 Обратная отправка веб-серверу Обратные отправки в ASP.NET Набор средств API-интерфейса ASP.NET Основные средства ASP.NET 1.0-1.1 Основные средства ASP.NET 2.0 Основные средства ASP.NET 3.5 (и .NET 3.5 SP1) Основные средства ASP.NET 4.0 Построение однофайловой веб-страницы ASP.NET Ссылка на сборку AutoLotDAL.dll Проектирование пользовательского интерфейса Добавление логики доступа к данным Роль директив ASP.NET Анализ блока script Анализ объявлений элементов управления ASP.NET Цикл компиляции для однофайловых страниц Построение веб-страницы ASP.NET с использованием файлов кода ^Ссылка на сборку AutoLotDAL.dll Обновление файла кода Цикл компиляции многофайловых страниц Отладка и трассировка страниц ASP. NET Веб-сайты и веб-приложения ASP.NET Структура каталогов веб-сайта ASP.NET Ссылаемые сборки Роль папки App_Code Цепочка наследования типа Раде Взаимодействие с входящим запросом HTTP Получение статистики браузера Доступ к входным данным формы Свойство IsPostBack Взаимодействие с исходящим ответом HTTP Выдача HTML-содержимого Перенаправление пользователей Жизненный цикл веб-страницы ASP.NET Роль атрибута AutoEventWiгеир Событие Error Роль файла Web. сon fig Утилита администрирования веб-сайтов ASP.NET Резюме 1224 1225 1225 1225 1227 1228 1228 1229 1229 1230 1231 1233 1235 1235 1236 1237 1239 1240 1240 1241 1242 1243 1244 1244 1245 1246 1247 1248 1248 1249 1250 1250 1251 1252 1253 1254 1255 1256 Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET 1257 1257 1258 1259 1260 1260 1262 1263 1264 1265 1266 1267 1268 1268 1274 Природа веб-элементов управления Обработка событий серверной стороны Свойство AutoPostBack Базовые классы Control и WebControl Перечисление содержащихся элементов управления Динамическое добавление и удаление элементов управления Взаимодействие с динамически созданными элементами управления Функциональность базового класса WebControl Основные категории веб-элементов управления ASP.NET Краткая информация о System.Web.UI.HtmlControls Документация по веб-элементам управления Построение веб-сайта ASP.NET Cars e Работа с мастер-страницами Определение страницы содержимого Default.aspx 30 Содержание Проектирование страницы содержимого Inventory.aspx Проектирование страницы содержимого BuildCar.aspx Роль элементов управления проверкой достоверности Класс RequiredFieldValidator Класс RegularExpressionValidator Класс RangeValidator Класс CompareValidator Создание итоговой панели проверки достоверности Определение групп проверки достоверности Работа с темами Ф а й л ы * . skin Применение тем ко всему сайту Применение тем на уровне страницы Свойство SkinID Программное назначение тем Резюме 1276 1279 1282 1283 1284 1284 1284 1285 1286 1288 1289 Глава 34. Управление состоянием в ASP.NET 1294 1294 1296 1296 1297 1299 1299 1301 1302 1303 1303 1305 1306 1307 1307 1309 1311 1314 1315 1315 1316 1317 1317 1318 1319 1319 1320 1322 1323 1325 Проблема поддержки состояния Приемы управления состоянием ASP. NET Роль состояния представления ASP NET Демонстрация работы с состоянием представления Добавление специальных данных в состояние представления Роль файла Global.asax Глобальный обработчик исключений “последнего шанса” Базовый класс HttpApplication Различие между свойствами Application и Session Поддержка данных состояния уровня приложения Модификация данных приложения Обработка останова веб-приложения Работа с кэшем приложения Работа с кэшированием данных Модификация файла *. aspx Поддержка данных сеанса Дополнительные члены HttpSessionState Cookie-наборы Создание cookie-наборов Чтение входящих cookie-данных Роль элем ента <sessionState> Хранение данных сеанса на сервере состояния сеансов ASP.NET Хранение информации о сеансах в выделенной базе данных Интерфейс ASP. NET Profile API База данных ASPNETDB.mdf Определение пользовательского профиля в Web. con fig Программный доступ к данным профиля Группирование данных профиля и сохранение специальных объектов Резюме 1291 1291 1291 1292 1293 Часть VIII. Приложения 1327 Приложение А. Программирование с помощью Windows Forms 1328 Приложение Б. Независимая от платформы разработка .NET-приложений с помощью Mono 1369 Предметный указатель 1386 Об авторе Эндрю Троелсен (Andrew Ttolesen) с любовью вспоминает свой самый первый ком­ пьютер Atari 400, оснащенный кассетным устройством хранения и черно-белым телеви­ зионным монитором “(который его родители разрешили ему поставить у себя в спальне, за что им большое спасибо). Еще он благодарен ушедшему в небытие журналу Compute!, степени бакалавра в области математической лингвистики и трем годам формально­ го изучения санскрита. Все это оказало значительное влияние на его сегодняшнюю карьеру. В настоящее время Эндрю работает в центре Intertech, занимающемся консультиро­ ванием и обучением работе с .NET и Java (www.intertech.com). На его счету уже несколько написанных книг, в числе которых Developer s Workshop to COM and ATL 3.0 (Wordware Publishing*, 2000 n), COM and .NETInteroperability (Apress, 2002 r.) и Visual Basic 2008 and the .NET 3.5 Platform: An Advanced Guide (Apress, 2008 r.). 0 техническом редакторе Энди Олсен (Andy Olsen) — независимый консультант и инструктор, проживающий в Великобритании. Энди работает с .NET, начиная с самой первой бета-версии этого продукта, и занимается активным исследованием новых функциональных возможно­ стей, которые появились в .NET 4.0. Он живет у моря в городе Суонси вместе со своей женой Джейн и детьми Эмили и Томом. Любит делать пробежки вдоль побережья (peiyлярно останавливаясь на чашечку кофе по пути), кататься на лыжах и следить за лебе­ дями и орликами. Связаться с ним можно по адресу andyo@olsensof t .com. Благодарности По какой-то непонятной причине (а может и целому ряду причин) настоящее изда­ ние книги оказалось гораздо более трудным в написании, чем ожидалось. Если бы не помощь и поддержка многих хороших людей, скорее всего, оно не вышло бы в свет так скоро. Прежде всего, огромное спасибо техническому редактору Энди Олсену. Помимо ука­ зания на пропущенные точки с запятой он привнес массу замечательных предложе­ ний, позволивших сделать первоначальные примеры кода более понятными и точными. Спасибо тебе, Энди! Далее хочу выразить благодарность всей команде литературных редакторов, а имен­ но — Мэри Бер (Магу Behr), Патрику Мидеру (Patrick Meader), Кэти Стэне (Katie Stence) и Шэрон Тердеман (Sharon Tferdeman), которые выявили и устранили массу граммати­ ческих ошибок. Особая благодарность Дебре Кэлли (Debra Kelly) из Apress. Это наш первый совмест­ ный проект и, несмотря на многочисленные опоздания с предоставлением глав, путани­ цу с электронными письмами и постоянное добавление обновлений, я все равно очень надеюсь, что она согласится работать со мной снова. Спасибо тебе, Дебра! И, наконец, напоследок спасибо моей жене Мэнди. Как всегда, ты поддерживаешь меня в здравом уме во время написания всех моих проектов. Введение ервое издание настоящей книги появилось вместе с выпуском Microsoft второй бета-версии .NET 1.0 (летом 2001 г.) и с тех пор постоянно переиздавалось в том или ином виде. С того самого времени автор с чрезвычайным удовольствием и благо дарностью наблюдал за тем, как данная работа продолжала пользоваться популярно­ стью в прессе и, самое главное, среди читателей. Через некоторое время, в 2002 г., она даже была номинирована на премию Jolt Award (которую, увы, получить так и не уда­ лось, но книга попала в число финалистов), а в 2003 г. — еще и на премию Referenceware Excellence Award, где стала лучшей книгой года по программированию. Даже еще более важно то, что автору стали приходить электронные письма от чи­ тателей со всего мира. Общаться с множеством людей и узнавать, что данная книга как-то помогла им в карьере, просто замечательно. В связи с этим, хотелось бы отме­ тить, что настоящая книга с каждым разом становится все лучше именно благодаря читателям, которые присылают различные предложения по улучшению, указывают на допущенные в тексте опечатки и обращают внимание на прочие промахи. Автор был просто ошеломлен, узнав, что эта книга использовалась и продолжает ис­ пользоваться на занятиях в колледжах и университетах и является обязательной для прочтения на многих предвыпускных и выпускных курсах в области вычислительной техники. Автор благодарит прессу, читателей, преподавателей и всех остальных и желает им успешного программирования! С самого первого выпуска настоящей книги автор прилагал все усилия и обновлял книгу так, чтобы она отражала текущие возможности каждой выходившей версии плат­ формы .NET. В настоящем издании материал был полностью пересмотрен и расширен с целью охвата новых средств языка C# 2010 и платформы .NET 4.0. Вы найдете инфор­ мацию по таким новым компонентам, как Dynamic Language Runtime (DLR), Tksk Parallel Library (TPL), Parallel LINQ (PLINQ) и ADO.NET Entity Framework (EF). Кроме того, в книге описан ряд менее значительных (но очень полезных) обновлений, наподобие именован­ ных и необязательных аргументов в C# 2010, типа класса Lazy<T> и т.д. Помимо описания новых компонентов и возможностей, в книге по-прежнему пре­ доставляется весь необходимый базовый материал по языку C# в целом, основам объ­ ектно-ориентированного программирования (ООП), конфигурированию сборок, получе­ нию доступа к базам данных (через ADO.NET), а также процессу построения настольных приложений с графическим пользовательским интерфейсом, веб-приложений и распре­ деленных систем (и многим другим темам). Как и в предыдущих изданиях, в этом издании весь материал по языку программи­ рования C# и библиотекам базовых классов .NET подается в дружественной и понятной читателю манере. Автор никогда не понимал, зачем другие технические авторы стара­ ются писать свои книги так, чтобы те больше напоминали сложный научный труд, а не легкое для восприятия пособие. В новом издании основное внимание уделяется пре­ доставлению информации, которой необходимо владеть для того, чтобы разрабатывать программные решения прямо сегодня, а не глубокому изучению малоинтересных эзо­ терических деталей. П Введение зз Автор и читатели - одна команда Авторам книг по технологиям приходится писать для очень требовательной группы людей. Всем известно, что детали разработки программных решений с помощью любой платформы (.NET, Java и СОМ) очень сложны и сильно зависят от отдела, компании, клиентской базы и поставленной задачи. Кто-то работает в сфере электронных публи­ каций, кто-то занимается разработкой систем для правительства и региональных орга­ нов власти, а кто-то сотрудничает с НАСА или военными департаментами. Что касается автора настоящей книги, то сам он занимается разработкой детского образовательного ПО (возможно, вам приходилось слышать об Oregon Trail или Amazon Trail), а также раз­ личных многоуровневых систем и проектов в медицинской и финансовой сфере. А это значит, что код, который придется писать читателю, скорее всего, будет иметь мало чего общего с тем, с которым приходится иметь дело автору. Поэтому в настоящей книге автор специально старался избегать приведения приме­ ров, свойственных только какой-то конкретной области производства или программи­ рования. Из-за этого все концепции, связанные с С#, ООП, CLR и библиотеками базовых классов .NET, объясняются на общих примерах. В частности, здесь везде применяется одна и та же тема, которая так или иначе близка каждому — автомобили. Остальное остается за читателем. Задача автора состоит в том, чтобы максимально доступно объяснить читателю язык программирования C# и основные концепции платформы .NET настолько хорошо, а также описать все возможные инструменты и стратегии, которые могут потребовать­ ся для продолжения обучения по прочтении данной книги. Задача читателя состоит в том, чтобы усвоить всю эту информацию и научить­ ся применять ее на практике при разработке своих программных решений. Конечно, скорее всего, проекты, которые понадобится выполнять в будущем, не будут связаны с автомобилями и их дружественными именами, но именно в этом и состоит вся суть применения получаемых знаний на практике! После изучения представленных в этой книге концепций можно будет спокойно создавать решения .NET, удовлетворяющие тре­ бованиям любой среды программирования. Краткий обзор содержания Эта книга логически разделена на восемь частей, в каждой из которых содержит­ ся ряд взаимосвязанных между собой глав. Те, кто читал предыдущие издания данной книги, сразу же отметят ряд отличий. Например, новые средства языка C# больше не описываются в отдельной главе. Вместо этого они рассматриваются в тех главах, в ко­ торых их появление является вполне естественным. Кроме того, по просьбе читателей был значительно расширен материал по технологии Windows Presentation Foundation (WPF). Ниже приведено краткое описание содержимого каждой из частей и глав настоя­ щей книги. Часть I. Общие сведения о языке C# и платформе .NET Назначением первой части этой книги является общее ознакомление читателя с природой платформы .NET и различными средствами разработки, которые могут при­ меняться при построении приложений .NET (многие из которых распространяются с открытым исходным кодом), а также некоторыми основными концепциями языка про­ граммирования C# и системы типов .NET. 34 Введение Глава 1. Ф илософ ия . N ET Первая глава выступает в роли своего рода основы для изучения всего остального излагаемого в данной книге материала. В начале в ней рассказывается о традиционной разработке приложений Windows и недостатках, которые существовали в этой сфере ра­ нее. Главной целью данной главы является ознакомление читателя с набором ключевых составляющих .NET: общеязыковой исполняющей средой (Common Language Runtime — CLR), общей системой типов (Common Type System — CTS), общеязыковой специфика­ цией (Common Language Specification — CLRS) и библиотеками базовых классов. Здесь читатель сможет получить первоначальное впечатление о том, что собой представляет язык программирования С#, и том, как выглядит формат сборок .NET, а также узнать о независимой от платформы природе .NET (о которой более детально рассказывается в приложении Б). Глава 2. Создание приложений на языке С # Целью этой главы является ознакомление читателя с процессом компиляции фай­ лов исходного кода на C# с применением различных средств и методик. Будет показа­ но, как использовать компилятор командной строки C# (c s c .e x e ) и файлы ответов, а также различные редакторы кода и интегрированные среды разработки, в том числе Notepad++, SharpDevelop, Visual C# 2010 Express и Visual Studio 2010. Кроме того, вы узнаете о том, как устанавливать на машину разработки локальную копию документа­ ции .NET Framework 4.0 SDK. Часть II. Главные конструкции программирования на C# Темы, представленные в этой части книги, довольно важны, поскольку подходят для разработки приложений .NET любого типа (т.е. веб-приложений, настольных приложе­ ний с графическим пользовательским интерфейсом, библиотек кода и служб Windows). Здесь читатель ознакомится с основными конструкциями языка C# и некоторыми де­ талями объектно-ориентированного программирования (ООП), обработкой исключений на этапе выполнения, а также автоматическим процессом сборки мусора в .NET. Глава 3. Главные конструкции программирования на С#; часть I В этой главе начинается формальное изучение языка программирования С#. Здесь читатель узнает о роли метода Main () и многочисленных деталях работы с внутрен­ ними типами данных в .NET, в том числе — о манипуляциях текстовыми данными с помощью System. String и System. Text.StringBuilder. Кроме того, будут описаны итерационные конструкции и конструкции принятия решений, операции сужения и расширения, а также ключевое слово unchecked. Глава 4. Главные конструкции программирования на С#; часть II В этой главе завершается рассмотрение ключевых аспектов С#. Будет показано, как создавать перегруженные методы в типах и определять параметры с использованием ключевых слов out, ref и par am s. Также рассматриваются появившиеся в C# 2010 концепции именованных аргументов и необязательных параметры. Кроме того, будут описаны создание и манипулирование массивами данных, определение нулевых типов (с помощью операций ? и ??) и отличия типов значения (включающих перечисления и специальные структуры) от ссылочных типов. Глава 5. Определение инкапсулированных типов классов В этой главе начинается рассмотрение концепций объектно-ориентированного про­ граммирования (ООП) в языке С#. Вначале объясняются базовые понятия ООП (та­ Введение 35 кие как инкапсуляция, наследование и полиморфизм). Затем показано, как создавать надежные типы классов с применением конструкторов, свойств, статических членов, констант и доступных только для чтения полей. Наконец, рассматриваются частичные определения типов, синтаксис инициализации объектов и автоматические свойства. Глава 6. Понятия наследования и полиморфизма Здесь читатель сможет ознакомиться с двумя такими основополагающими концеп­ циями ООП, как наследование и полиморфизм, которые позволяют создавать семейст­ ва взаимосвязанных типов классов. Будет описана роль виртуальных и абстрактных методов (а также абстрактных базовых классов) и полиморфных интерфейсов. И, на­ конец, в главе рассматривается роль одного из главных базовых классов в .NET — System.Object. Глава 7. Структурированная обработка исклю чений В этой главе рассматривается решение проблемы аномалий, возникающих в коде во время выполнения, за счет применения методики структурированной обработки исклю­ чений. Здесь описаны ключевые слова, предусмотренные для этого в C# (try, catch, throw и finally), а также отличия между исключениями уровня приложения и уровня системы. Кроме того, рассматриваются различные инструменты, предлагаемые в Visual Studio 2010, которые предназначены для проведения отладки исключений. Глава 8. Врем я жизни объектов В этой главе рассказывается об управлении памятью CLR-средой с использованием сборщика мусора .NET. Будет описана роль корневых элементов приложений, поколе­ ний объектов и типа System.GC. Будет показано, как создавать самоочшцаемые объ­ екты (с применением интерфейса IDisposable) и обеспечивать процесс финализации (с помощью метода System. Object.Finalize ()). Вы узнаете о появившемся в .NET 4.0 классе Lazy<T>, который позволяет определять данные так, чтобы они не размещались в памяти до тех пор, пока вызывающая сторона не запросит их. Этот класс позволяет не загромождать память такими объектами, которые пока не требуются. Часть III. Дополнительные конструкции программирования на C# В этой части читателю предоставляется возможность углубить знания языка C# за счет изучения других более сложных (но очень важных) концепций. Здесь завершается ознакомление с системой типов .NET описанием типов делегатов и интерфейсов. Кроме того, описана роль обобщений и дано краткое введение в язык LINQ (Language Integrated Query). Также рассматриваются некоторые более сложные средства C# (такие как мето­ ды расширения, частичные методы и приемы манипулирования указателями). Глава 9. Работа с интерфейсами Материал этой главы предполагает наличие понимания концепций объектно-ори­ ентированной разработки и посвящен программированию с использованием интер­ фейсов. Здесь будет показано, как определять классы и структуры, поддерживающие множество поведений, как обнаруживать эти поведения во время выполнения и как выборочно скрывать какие-то из них за счет явной реализации интерфейсов. Помимо создания специальных интерфейсов, рассматриваются вопросы реализации стандарт­ ных интерфейсов из состава .NET и их применения для построения объектов, которые могут сортироваться, копироваться, перечисляться и сравниваться. 36 Введение Глава 10. Обобщения Эта глава посвящена обобщениям. Программирование с использованием обобщений позволяет создавать типы и члены типов, содержащие заполнители, которые заполняют­ ся вызывающим кодом. В целом, обобщения позволяют значительно улучшить произво­ дительность приложений и безопасность в отношении типов. В главе рассматриваются типы обобщений из пространства имен System.Collections .Generic, а также показа­ но, как создавать собственные обобщенные методы и типы (с ограничениями и без). Глава 11. Делегаты, события и лямбда-выражения Благодаря этой главе, станет понятно, что собой представляет тип делегата. Любой делегат в .NET представляет собой объект, который указывает на другие методы в при­ ложении. С помощью делегатов можно создавать системы, позволяющие многочислен­ ным объектам взаимодействовать между собой в обоих направлениях. После изучения способов применения делегатов в .NET, будет показано, как применять в C# ключевое слово event, которое упрощает манипулирование делегатами. Кроме того, рассматри­ вается роль лямбда-операции (=>) и связь между делегатами, анонимными методами и лямбда-выражениями. Глава 12. Расш иренные средства языка C# В этой главе описаны расширенные средства языка С#, в том числе перегрузка опе­ раций, создание специальных процедур преобразования (явного и неявного) для типов, построение и взаимодействие с индексаторами типов, работа с расширяющими мето­ дами, анонимными типами, частичными методами, а также указателями C# с исполь­ зованием в коде контекста unsafe. Глава 13. LINO to O bject В этой главе начинается рассмотрение 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 в C# 2010. Наконец, объясняется, что собой пред­ ставляют контексты объектов, как устроен CIL-код и как создавать сборки в памяти. Глава 14. Конфигурирование сборок .N E T На самом высоком уровне термин сборка применяется для описания любого двоич­ ного файла * . d l l или * . ехе, который создается с помощью компилятора .NET. В дейст­ вительности возможности сборок намного шире. В этой главе будет показано, чем отли­ чаются однофайловые и многофайловые сборки, как создавать и развертывать сборки обеих разновидностей, как делать сборки приватными и разделяемыми с использовали- Введение 37 ем XML-файлов * . conf ig и специальных сборок политик издателя. Кроме того, в главе описан глобальный кэш сборок (Global Assembly Cache — CAG), а также изменения CAG в версии .NET 4.0. Глава 15. Реф лексия типов, позднее связывание и программирование с использованием атрибутов В главе 15 продолжается изучение сборок .NET. В ней показано, как обнаруживать типы во время выполнения с использованием пространства имен System. Ref lection. Типы из этого пространства имен позволяют создавать приложения, способные считы­ вать метаданные сборки на лету. Кроме того, в главе рассматривается динамическая за­ грузка и создание типов во время выполнения с помощью позднего связывания, а также роль атрибутов .NET (стандартных и специальных). Для закрепления материала в конце главы приводится пример построения расширяемого приложения Windows Forms. Глава 16. Процессы , домены приложений и контексты объектов В этой главе рассказывается о создании загруженных исполняемых файлов .NET. Целью главы является иллюстрация отношений между процессами, доменами прило­ жений и контекстными границами. Все эти темы подготавливают базу для изучения процесса создания многопоточных приложений в главе 19. Глава 17. Язык C IL и роль динам ических сборок В этой главе подробно рассматривается синтаксис и семантика языка CIL, а также роль пространства имен System. Re flection. Em it, с помощью типов из которого мож­ но создавать программное обеспечение, способное генерировать сборки .NET в памяти во время выполнения. Формально сборки, которые определяются и выполняются в па­ мяти, называются динамическими сборками. Их не следует путать с динамическими типами, которые являются темой главы 18. Глава 18. Динам ические типы и исполняющая ср ед а динам ического языка В .NET 4.0 появился новый компонент исполняющей среды .NET, который называ­ ется исполняющей средой динамического языка (Dynamic Language Runtime — DLR). С помощью DLR в .NET и ключевого слова dynamic в C# 2010 можно определять дан­ ные, которые в действительности разрешаются во время выполнения. Использование этих средств значительно упрощает решение ряда очень сложных задач по програм­ мированию приложений .NET. В главе рассматриваются некоторые практические спо­ собы применения динамических данных, включая более гладкое использование A PIинтерфейсы рефлексии .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 flhsk Parallel Library — библиотека параллельных задач), появившийся в .NET 4.0. С применением этого API-интерфейса можно создавать .NET-приложения, распределяющие рабочую нагрузку среди доступ­ ных ЦП в исключительно простой манере. В главе также описан API-интерфейс PINQ (Parallel LINQ), который позволяет создавать масштабируемые LINQ-запросы. Глава 20. Файловый ввод-вы вод и сериализация объектов Пространство имен System. 10 позволяет взаимодействовать существующей струк­ турой файлов и каталогов. В главе будет показано, как программно создавать (и удалять) систему каталогов и перемещать данные в различные потоки (файловые, строковые и находящиеся в памяти). Кроме того, рассматриваются службы .NET, предназначенные для сериализации объектов. Сериализация представляет собой процесс, который позво­ ляет сохранять данные о состоянии объекта (или набора взаимосвязанных объектов) в потоке для использования в более позднее время, а десериализация— процесс извлече­ ния данных о состоянии объекта из потока в память для последующего использования в приложении. В главе описана настройка процесса сериализации с применением ин­ терфейса ISerializable и набора атрибутов .NET. Глава 21. A D 0.N ET, часть I: подключенный уровень В этой первой из трех посвященных базам данных главам дано введение в APIинтерфейс ADO.NET. Рассматривается роль поставщиков данных .NET и взаимодейст­ вие с реляционной базой данных с применением так называемого подключенного уровня ADO.NET, который представлен объектами подключения, объектами команд, объектами транзакций и объектами чтения данных. В этой главе также приведен пример создания специальной базы данных и первой версии специальной библиотеки доступа к данным (AutoLotDAL.dll), неоднократно применяемой в остальных примерах книги. Глава 23. ADO.NET, часть II: автономный уровень В этой главе продолжается описание способов работы с базами данных и рассказы­ вается об автономном уровне ADO.NET. Рассматривается роль типа DataSet и объектов адаптеров данных, а также многочисленных средств Visual Studio 2010, которые спо­ собны упростить процесс создания приложений, управляемых данными. Будет показа­ но, как связывать объекты DataTable с элементами пользовательского интерфейса, а также как применять запросы LINQ к находящимся в памяти объектам DataSet с ис­ пользованием API-интерфейса LINQ to DataSet. Глава 23. ADO.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 Comm unication Foundation В этой главе читатель узнает об API-интерфейсе Windows Communication Foundation (WCF), который позволяет создавать распределенные приложения симметричным обра­ зом, какими бы не были лежащие в их основе низкоуровневые детали. Будет показано, как создавать службы, хосты и клиентские приложения WCF. Службы WCF являются чрезвычайно гибкими, поскольку позволяют использовать для клиентов и хостов кон­ фигурационные файлы на основе XML, в которых декларативно задаются необходимые адреса, привязки и контракты. Кроме того, рассматриваются полезные сокращения, которые появились в .NET 4.0. Глава 26. Введение в Windows Workflow Foundation 4 .0 API-интерфейс Windows Workflow Foundation (WF) вызывает больше всего путаницы у разработчиков-новичков. В версии .NET 4.0 первоначальный вариант API-интерфейса WF (появившийся в .NET 3.0) полностью переделан. В этой главе описана роль прило­ жений, поддерживающих рабочие потоки, и способы моделирования бизнес-процессов с применением API-интерфейса WF 4.0. Рассматривается библиотека действий, постав­ ляемая в составе WF 4.0, а также показано, как создавать специальные действия. Часть VI. Построение настольных пользовательских приложений с помощью WPF В .NET 3.0 был предложен замечательный API-интерфейс под названием Windows Presentation Foundation (WPF). Он быстро стал заменой модели программирования на­ стольных приложений Windows Forms. WPF позволяет создавать настольные приложе­ ния с векторной графикой, интерактивной анимацией и операциями привязки данных с использованием декларативной грамматики разметки XAML. Более того, архитектура элементов управления WPF позволяет легко изменять внешний вид и поведение лю бо­ го элемента управления с помощью правильно оформленного XAML-кода. В настоящем издании модели программирования WPF посвящено целых пять глав. Глава 27. Введение в Windows Presentation Foundation и XAM L Технология 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. В первую очередь рассказывается о роли логических ресурсов. Система логических (также называемых объектными) ресурсов позволяет назначать наиболее часто используемым в W PFприложении объектам имена и затем ссылаться на них. Кроме того, будет показано, как определять, выполнять и управлять анимационной последовательностью. И, наконец, в главе рассматривается роль стилей в 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. Построение веб-страниц A S P .N E T В этой главе начинается изучение процесса разработки веб-приложений с помощью ASP.NET. Как будет показано, вместо кода серверных сценариев теперь применяются самые настоящие объектно-ориентированные языки (наподобие C# и VB.NET). В главе Введение 41 рассматривается типовой процесс создания веб-страницы ASP.NET, лежащая в основе модель программирования и другие важные аспекты ASP.NET, вроде того, как выбираеть веб-сервер и работать с файлами Web.conf ig. Глава 33. Веб-элементы управления, мастер-страницы и темы A S P .N E T В отличие от предыдущей главы, посвященной созданию объектов Раде из ASP.NET, в данной главе рассказывается об элементах управления, которые заполняют внутреннее дерево элементов ASP.NET. Здесь описаны основные веб-элементы управления ASP.NET, включая элементы управления проверкой достоверности, элементы управления навига­ цией по сайту и различные операции привязки данных. Кроме того, рассматривается роль мастер-страниц и механизма применения тем ASP.NET, который является сервер­ ным аналогом традиционных таблиц стилей. Глава 34. Управление состоянием в A S P .N E T В этой главе рассматриваются разнообразные способы управления состоянием в .NET Как и в классическом ASP, в ASP.NET можно создавать cookie-наборы и перемен­ ные уровня приложения и уровня сеанса. Кроме того, в ASP.NET есть еще одно средство для управления состоянием, которое называется кэшем приложения. Вдобавок в главе рассказывается о роли базового класса HttpApplication и демонстрируется динами­ ческое переключение поведения веб-приложения с помощью файла Web.conf ig. Часть VIII. Приложения В этой заключительной части книги рассматриваются две темы, которые не совсем вписывались в контекст основного материала. В частности, здесь кратко рассматрива­ ется более ранняя платформа Windows Forms для построения графических интерфейсов настольных приложений, а также использование платформы Mono для создания прило­ жений .NET, функционирующих под управлением операционных систем, отличных от Microsoft Windows. Приложение А. Программирование с помощью Windows Form s Исходный набор инструментов для построения настольных пользовательских интер­ фейсов, который поставляется в рамках платформы .NET с самого начала, называется Windows Forms. В этом приложении описана роль этого каркаса и показано, как с его помощью создавать главные окна, диалоговые окна и системы меню. Кроме того, здесь рассматриваются вопросы наследования форм и визуализации двухмерной графики с помощью пространства имен System. Drawing. В конце приложения приводится при­ мер создания программы для рисования (средней сложности), иллюстрирующий прак­ тическое применение всех описанных концепций. Приложение Б. Независим ая от платформы разработка .N E T -приложений с помощью Mono Приложение Б посвящено использованию распространяемой с открытым исходным кодом реализации платформы .NET под названием Mono. Она позволяет разрабатывать многофункциональные приложения .NET, которые можно создавать, развертывать и выполнять под управлением самых разных операционных систем, включая Mac OS X, Solaris, AIX и многочисленные дистрибутивы Linux. Так как Mono в основном эмули­ рует платформу .NET от Microsoft, ее функциональные возможности вполне очевидны. Поэтому в приложении основное внимание уделено не возможностям платформы Mono, а процессу ее установки, предлагаемым в ее составе инструментам для разработки, а также используемому механизму исполняющей среды. 42 Введение Исходный код примеров Исходный код всех рассматриваемых в настоящей книге примеров доступен для за­ грузки на сайте издательства по адресу: http://www.williamspublishing.com От издательства Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим ваше мнение и хотим знать, что было сделано нами правильно, что можно было сделать луч­ ше и что еще вы хотели бы увидеть изданным нами. Нам интересно услышать и любые другие замечания, которые вам хотелось бы высказать в наш адрес. Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бумаж­ ное или электронное письмо, либо просто посетить наш Web-сервер и оставить свои замечания там. Одним словом, любым удобным для вас способом дайте нам знать, нра­ вится или нет вам эта книга, а также выскажите свое мнение о том, как сделать наши книги более интересными для вас. Посылая письмо или сообщение, не забудьте указать название книги и ее авторов, а также ваш обратный адрес. Мы внимательно ознакомимся с вашим мнением и обяза­ тельно учтем его при отборе и подготовке к изданию последующих книг. Наши координаты: E-mail: inf о@ williamspublishing. com WWW: http://www.williamspublishing.com Информация для писем из: России: Украины: 127055, г. Москва, ул. Лесная, д. 43, стр. 1 03150, Киев, а/я 152 ЧАСТЬ I Общие сведения о языке C# и платформе .NET В этой ча сти ... Глава 1. Философия .NET Глава 2. Создание приложений на языке C# ГЛАВА 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 текущей платформы. Чтобы настроиться надлежащим образом, давайте начнем эту главу с краткого урока истории, чтобы вспомнить об истоках и ограничениях, которые существовали в прошлом (ведь признание существования проблемы является первым шагом на пути к ее решению). После завершения этого краткого экскурса в историю мы обратим наше внимание на те многочисленные преимущества, которые предоставляет язык C# и платформа .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 заключается в представлении “разумного подмножества” исходного A PI-интерфейса Windows в виде набора классов, “магических” макросов и многочисленных средств для автоматической генерации программного кода (обычно называемых мастерами). Несмотря на очевидную пользу данной платформы приложений (и многих других осно­ ванных на C++ наборов средств), процесс программирования на C++ остается трудным и чреватым допущением ошибок занятием из-за его исторической связи с языком С. Подход с применением Visual Basic 6.0 Благодаря искреннему желанию иметь возможность наслаждаться более простой жизнью, многие программисты перешли из “мира платформ” на базе С (C++) в мир менее сложных и более дружественных языков наподобие Visual Basic 6.0 (VB6). Язык VB6 стал популярным благодаря предоставляемой им возможности создавать сложные пользовательские интерфейсы, библиотеки программного кода (вроде COM-серверов) и логику доступа к базам данных с приложением минимального количества усилий. Во многом как и в MFC, в VB6 сложности API-интерфейса Windows скрываются из вида за счет предоставления ряда интегрированных мастеров, внутренних типов данных, клас­ сов и специфических функций VB. 46 Часть I. Общие сведения о языке C# и платформе .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++ могут создавать COM-классы, пригодные для использования в VB6, про­ граммисты на Delphi — применять COM-классы, созданные с помощью С, и т.д. Однако, Глава 1. Философия .NET 47 как не трудно догадаться, подобная независимость СОМ от языка является несколько ограниченной. Например, никакого способа для порождения нового COM-класса с ис­ пользованием уже существующего не имеется (поскольку СОМ не обладает поддержкой классического наследования). Вместо этого для использования типов COM-класса тре­ буется задавать несколько неуклюжее отношение принадлежности (has-a). Еще одним преимуществом СОМ является прозрачность расположения. За счет при­ менения конструкций вроде системного реестра, идентификаторов приложений (АррШ), заглушек, прокси-объектов и исполняющей среды СОМ программисты могут избегать необходимости иметь дело с самими сокетам, RPC-вызовам и другими низкоуровневы­ ми деталями при создании распределенного приложения. Например, рассмотрим сле­ дующий программный код COM-клиента HaVB6: 1 Данный тип MyCOMClass мог бы быть написан на любом 1 поддерживающем СОМ языке и размещаться в любом месте 1 в сети (в том числе и на локальной машине) . Dim obj as MyCOMClass Set obj = New MyCOMClass 1 Местонахождение определяется с помощью AppID. ob;j .DoSomeWork Хотя COM и можно считать очень успешной объектной моделью, ее внутреннее уст­ ройство является чрезвычайно сложным (и требует затрачивания программистами мно­ гих месяцев на его изучение, особенно теми, которые программируют на C++). Для облег­ чения процесса разработки двоичных COM-объектов программисты могут использовать многочисленные платформы, поддерживающие СОМ. Например, в ATL (Active Template Library — библиотека активных шаблонов) для упрощения процесса создания СОМсерверов предоставляется набор специальных классов, шаблонов и макросов на C++. Во многих других языках приличная часть инфраструктуры СОМ тоже скрывается из вида. Поддержки одного только языка, однако, для сокрытия всей сложности СОМ не хватает. Даже при выборе относительно простого поддерживающего СОМ языка вроде VB6, все равно требуется бороться с “хрупкими” записями о регистрации и многочис­ ленными деталями развертывания (в совокупности несколько саркастично называемы­ ми адом DLL). Сложность представления типов данных СОМ Хотя СОМ, несомненно, упрощает процесс создания программных приложений с по­ мощью различных языков программирования, независимая от языка природа СОМ не является настолько простой, насколько возможно хотелось бы. Некоторая доля этой сложности является следствием того факта, что приложения, которые сплетаются вместе с помощью разнообразных языков, получаются совершенно не связанными с синтаксической точки зрения. Например, синтаксис JScript во мно­ гом похож на синтаксис С, а синтаксис VBScript представляет собой подмножество син­ таксиса VB6. COM-серверы, которые создаются для выполнения в исполняющей среде СОМ+ (представляющей собой компонент операционной системы Windows, который предлагает общие службы для библиотек специального кода, такие как транзакции, жизненный цикл объектов, безопасность и т.д.), имеют совершенно не такой вид и по­ ведение, как ориентированные на использование в веб-сети ASP-страницы, в которых они вызываются. В результате получается очень запутанная смесь технологий. Более того, что, пожалуй, даже еще важнее, каждый язык и/или технология облада­ ет собственной системой типов (которая может быть совершенно не похожа на систему типов другого языка или технологии). Помимо того факта, что каждый API-интерфейс поставляется с собственной коллекцией готового кода, даже базовые типы данных мо­ гут не всегда интерпретироваться идентичным образом. Например, тип CComBSTR в ATL 48 Часть I. Общие сведения о языке C# и платформе .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# и т.д.). • Общий исполняющий механизм, используемый всеми поддерживающими .NEH языками. Одним из аспектов этого механизма является наличие хорошо опре­ деленного набора типов, которые способен понимать каждый поддерживающий .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. Общие сведения о языке C# и платформе .NET Роль библиотек базовых классов Помимо среды CLR и спецификаций CTS и CLS, в составе платформы .NET постав­ ляется библиотека базовых классов, которая является доступной для всех языков про­ граммирования .NET. В этой библиотеке не только содержатся определения различных примитивов, таких как потоки, файловый ввод-вывод, системы графической визуали­ зации и механизмы для взаимодействия с различными внешними устройствами, но также предоставляется поддержка для целого ряда служб, требуемых в большинстве реальных приложений. Например, в библиотеке базовых классов содержатся определения типов, которые способны упрощать процесс получения доступа к базам данных, манипулирования XML-документами, обеспечения программной безопасности и создания веб-, а так­ же обычных настольных и консольных интерфейсов. На высоком уровне взаимосвязь между CLR, CTS, CLS и библиотекой базовых классов выглядит так, как показано на рис. 1.1. Библиотека базовых классов Доступ к базе данных Н астольны е граф ические AP I-ин тер ф е й сы О р га ни зац и я п ото ко в ой обра б о тки Файловый ввод-вывод Безопасность API-интерфейсы для работы с веб-содержимым API-интерф ейсы для удаленной работы и другие Общеязыковая исполняющая среда (CLR) Общая система типов (CTS) Общеязыковая спецификация (CLS) Рис. 1.1. Отношения между CLR, CTS, CLS и библиотеками базовых классов Что привносит язык C# Из-за того, что платформа .NET столь радикально отличается от предыдущих тех­ нологий, в Microsoft разработали специально под нее новый язык программирования С#. Синтаксис этого языка программирования очень похож на синтаксис языка Java. Однако сказать, что C# просто переписан с Java, будет неточно. И язык С#, и язык Java просто оба являются членами семейства языков программирования С (в которое также входят языки С, Objective С, C++ и т.д.) и потому имеют схожий синтаксис. Правда состоит в том, что многие синтаксические конструкции в C# моделируются согласно различным особенностям Visual Basic 6.0 и C++. Например, как и в VB6, в C# поддерживается понятие формальных свойств типов (в противоположность традицион­ ным методам get и set) и возможность объявлять методы, принимающие переменное количество аргументов (через массивы параметров). Как и в C++, в C# допускается пе­ регружать операции, а также создавать структуры, перечисления и функции обратного вызова (посредством делегатов). Глава 1. Философия .NET 51 Более того, по мере изучения излагаемого в этой книге материала, можно будет бы­ стро заметить, что в C# поддерживается целый ряд функциональных возможностей, которые традиционно встречаются в различных функциональных языках программи­ рования (например, LISP или Haskell) и к числу которых относятся лямбда-выражения и анонимные типы. Кроме того, с появлением технологии LINQ в C# стали поддержи­ ваться еще и конструкции, которые делают его довольно уникальным в мире програм­ мирования. Несмотря на все это, наибольшее влияние на него все-таки оказали именно языки на базе С. Благодаря тому факту, что C# представляет собой собранный из нескольких языков гибрид, он является таким же “чистым” с синтаксической точки зрения, как и язык Java (а то и “чище” его), почти столь же простым, как язык VB6, и практически таким же мощным и гибким как C++ (только без ассоциируемых с ним громоздких элементов). Ниже приведен неполный список ключевых функциональных возможностей языка С#, которые присутствуют во всех его версиях. • Никаких указателей использовать не требуется! В программах на C# обычно не возникает необходимости в манипулировании указателями напрямую (хотя опус­ титься к этому уровню все-таки можно, как будет показано в главе 12). • Управление памятью осуществляется автоматически посредством сборки мусора. По этой причине ключевое слово delete в C# не поддерживается. • Предлагаются формальные синтаксические конструкции для классов, интерфей­ сов, структур, перечислений и делегатов. • Предоставляется аналогичная C++ возможность перегружать операции для поль­ зовательских типов, но без лишних сложностей (например, заботиться о “возврате *this для обеспечения связывания” не требуется). • Предлагается поддержка для программирования с использованием атрибутов. Такой подход в сфере разработки позволяет снабжать типы и их членов аннота­ циями и тем самым еще больше уточнять их поведение. С выходом версии .NET 2.0 (примерно в 2005 г.), язык программирования C# был обновлен и стал поддерживать многочисленные новые функциональные возможности, наиболее заслуживающие внимания из которых перечислены ниже. • Возможность создавать обобщенные типы и обобщенные элементы-члены. За счет применения обобщений можно создавать очень эффективный и безопасный для типов код с многочисленными метками-заполнителями, подстановка значе­ ний в которые будет происходить в момент непосредственного взаимодействия с данным обобщенным элементом. • Поддержка для анонимных методов, каковые позволяют предоставлять встраи­ ваемую функцию везде, где требуется использовать тип делегата. • Многочисленные упрощения в модели “делегат-событие”, в том числе возмож­ ность применения ковариантности, контравариантности и преобразования групп методов. (Если какие-то из этих терминов пока не знакомы, не стоит пугаться; все они подробно объясняются далее в книге.) • Возможность определять один тип в нескольких файлах кода (или, если необходи­ мо, в виде представления в памяти) с помощью ключевого слова partial. В версии .NET 3.5 (которая вышла примерно в 2008 г.) в язык программирования C# снова были добавлены новые функциональные возможности, наиболее важные из которых описаны ниже. • Поддержка для строго типизированных запросов (также называемых запросами LINQ), которые применяются для взаимодействия с различными видами данных. 52 Часть I. Общие сведения о языке C# и платформе .NET • Поддержка для анонимных типов, которые позволяют моделировать форму типа, а не его поведение. • Возможность расширять функциональные возможности существующего типа с помощью методов расширения. • Возможность использовать лямбда-операцию (=>), которая даже еще больше упро­ щает работу с типами делегатов в .NET. • Новый синтаксис для инициализации объектов, который позволяет устанавли­ вать значения свойств во время создания объектов. В текущем выпуске платформы .NET версии 4.0 язык C# был опять обновлен и до­ полнен рядом новых функциональных возможностей. Хотя приведенный ниже перечень новых конструкций может показаться довольно ограниченным, по ходу прочтения на­ стоящей книги можно будет увидеть, насколько полезными они могут оказаться. • Поддержка н еобязательн ы х параметров в методах, а также именованных аргументов. • Поддержка динамического поиска членов во время выполнения посредством клю­ чевого слова dynamic. Как будет показано в главе 18, эта поддержка предоставля­ ет в распоряжение универсальный подход для осуществления вызова членов “на лету”, с помощью какой бы платформы они не были реализованы (COM, IronRuby, IronPython, HTML DOM или службы рефлексии .NET). , • Вместе с предыдущей возможностью в .NET 4.0 значительно упрощается обеспе­ чение взаимодействия приложений на C# с унаследованными серверами СОМ, благодаря устранению зависимости от сборок взаимодействия (interop assemblies) и предоставлению поддержки необязательных аргументов ref. • Работа с обобщенными типами стала гораздо понятнее, благодаря появлению возможности легко отображать обобщенные данные на и из общих коллекций System.Object с помощью ковариантности и контравариантности. Возможно, наиболее важным моментом, о котором следует знать, программируя на С#, является то, что с помощью этого языка можно создавать только такой код, который будет выполняться в исполняющей среде .NET (использовать C# для построения “клас­ сического” COM-сервера или неуправляемого приложения с вызовами API-интерфейса и кодом на С и C++ нельзя). Официально код, ориентируемый на выполнение в исполняю­ щей среде .NET, называется управляемым кодом (managed code), двоичная единица, в которой содержится такой управляемый код — сборкой (assembly; о сборках будет более подробно рассказываться позже в настоящей главе), а код, который не может обслужи­ ваться непосредственно в исполняющей среде .NET — неуправляемым кодом (unma­ naged code). Другие языки программирования с поддержкой .NET Следует понимать, что C# является не единственным языком, который может приме­ няться для построения .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.сот/fsharp. Помимо управляемых языков, предлагаемых Microsoft, существуют .NET-компиляторы, которые предназначены для таких языков, как Smalltalk, COBOL и Pascal (и это далеко не полный перечень). Хотя в настоящей книге все внимание практически полно­ стью уделяется лишь С#, следующий веб-сайт тоже вызвать интерес: h ttp : //www. dotnetlanguages. net Щелкнув на ссылке Resources (Ресурсы) в самом верху домашней страницы этого сайта, можно получить доступ к списку всех языков программирования .NET и соот­ ветствующих ссылок, по которым для них можно загружать различные компиляторы (рис. 1.2). _ . Т -Т " « i. N E T LML (чaс> n tilл г D e d ic a t e d g u a g es INVESTIG News |FAQI Resources |Contact |RSSFeed |Rec*»nt „omments F»wJ R e so u rce s 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 me. .NET Language Sites APL ASP NET; ASM to IL AsmL ASP (Gotham) Basic O VB NET (Microsoft) О VB .NET (Mono) BETA Boo BlueDragon C О Ice _____ Ф Internet | Protected Mode- On Рис. 1.2. Один из многочисленных сайтов с документацией по известным языкам программирования .NET Хотя настоящая книга ориентирована главным образом на тех, кого интересует раз­ работка программ .NET с использованием С#, все равно рекомендуется посетить ука­ занный сайт, поскольку там наверняка можно будет найти много языков для .NET, за­ служивающих отдельного изучения в свободное время (вроде LISP .NET). 54 Часть I. Общие сведения о языке C# и платформе .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 не выбирался для программирования, важно понимать, что хотя двоичные .NET-единицы имеют такое же файловое расширение, как и двоичные едини­ цы COM-серверов и неуправляемых программ Win32 (* . dll или * . ехе), внутренне они устроены абсолютно по-другому. Например, двоичные .NET-единицы * .dll не экспор­ тируют методы для упрощения взаимодействия с исполняющей средой СОМ (поскольку .NET — это не СОМ). Более того, они не описываются с помощью библиотек СОМ-типов и не регистрируются в системном реестре. Пожалуй, самым важным является то, что они содержат не специфические, а наоборот, не зависящие от платформы инструкции на промежуточном языке (Intermediate Language — IL), а также метаданные типов. На рис. 1.3 показано, как все это выглядит схематически. Глава 1. Философия .NET 55 Рис. 1.3. Все .NET-компиляторы генерируют IL-инструкции и метаданные На заметку! Относительно сокращения “IL” уместно сказать несколько дополнительных слов. В ходе разработки .NET официальным названием для IL было Microsoft Intermediate Language (MSIL). Однако в вышедшей последней версии .NET это название было изменено на OIL (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. Общие сведения о языке C# и платформе .NET Однофайловые и многофайловые сборки В большом количестве случаев между сборками .NET и файлами двоичного кода (* . d l l или * . ехе) соблюдается простое соответствие “один к одному”. Следовательно, получается, что при построении * . d l l -библиотеки .NET, можно спокойно полагать, что файл двоичного кода и сборка представляют собой одно и то же, и что, аналогичным образом, при построении исполняемого приложения для настольной системы на файл * . ехе можно ссылаться как на саму сборку. Однако, как будет показано в главе 14, это не совсем так. С технической точки зрения, сборка, состоящая из одного единственного модуля * . d l l или * . ехе, называется однофайловой сборкой. В однофайловых сборках все необходимые CIL-инструкции, метаданные и манифесты содержатся в одном авто­ номном четко определенном пакете. Многомофайловые сборки, в свою очередь, состоят из множества файлов двоичного кода .NET, каждый из которых называется модулем (module). При построении много­ файловой сборки в одном из ее модулей (называемом первичным или главным (primary) модулем) содержится манифест всей самой сборки (и, возможно, CIL-инструкции и ме­ таданные по различным типам), а во всех остальных — манифест, CIL-инструкции и метаданные типов, охватывающие уровень только соответствующего^ модуля. Как не­ трудно догадаться, в главном модуле содержится описание набора требуемых дополни­ тельных модулей внутри манифеста сборки. На заметку! В главе 14 будет более подробно разъясняться, в чем состоит различие между одно­ файловыми и многофайловыми сборками. Однако следует иметь в виду, что Visual Studio 2010 может применяться только для создания однофайловых сборок. В тех редких случаях возник­ новения необходимости в создании именно многофайловой сборки требуется использовать соответствующие утилиты командной строки. Роль CIL Теперь давайте немного более подробно посмотрим, что же собой представляет CILкод, метаданные типов и манифест сборки. CIL является таким языком, который стоит выше любого конкретного набора ориентированных на определенную платформу ин­ струкций. Например, ниже приведен пример кода на С#, в котором создается модель самого обычного калькулятора. Углубляться в конкретные детали синтаксиса пока не нужно, главное обратить внимание на формат такого метода в этом классе Calc, как A d d () . //Класс C alc.cs using System; namespace CalculatorExample { / / В этом классе содержится точка для входа в приложение. class Program { static void M ain() { Calc c = new Calc () ; int ans = c.Add(10, 84); Console .WnteLine (" 10 + 84 is {0}.", ans) ; // Обеспечение ожидания нажатия пользователем // клавиши <Enter> перед выходом. Console.ReadLine(); // Калькулятор на С#. class Calc Глава 1. Философия .NET 57 { public int Add(int x, int y) { return x + y; } } } После выполнения компиляции файла с этим кодом с помощью компилятора C# (csc.exe) получится однофайловая сборка * .ехе, в которой будет содержаться мани­ фест, CIL-инструкции и метаданные, описывающие каждый из аспектов класса Calc и Program. На заметку! О том, как выполнять компиляцию кода с помощью компилятора С#, а также исполь­ зовать графические IDE-среды, подобные Microsoft Visual Studio 2010, Microsoft Visual C# 2010 Express и SharpDevelop, будет более подробно рассказываться в главе 2. Например, открыв данную сборку в утилите ild a s m .e x e (которая более подробно рассматривается далее в настоящей главе), можно увидеть, что метод Add () был преоб­ разован в CIL так, как показано ниже: .method public hidebysig instance int32 Add(int32 x, int32 y) cil managed { // Code size 9 (0x9) // Размер кода 9 (0x9) .maxstack 2 .locals m i t (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. Главное заметить, что компилятор C# выдает CIL-код, а не ориентированные на определенную платформу ин­ струкции. Теперь напоминаем, что так себя ведут все .NET-компиляторы. Чтобы убе­ диться в этом, давайте попробуем создать то же самое приложение с использованием языка Visual Basic, а не С#. 'Класс Calc.vb Imports System Namespace CalculatorExample ' В VB "модулем" называется класс, в котором ' содержатся только статические члены. Module Program Sub Main () Dim c As New Calc Dim ans As Integer = c. Add (10, 84) Console.WnteLine ("10 + 84 is {0}.", ans) Console.ReadLine() End Sub End Module Class Calc 58 Часть I. Общие сведения о языке C# и платформе .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 (0x8) // Размер кода 8 (0x8) .maxstack 2 .locals m i t (int32 V_0) IL_0000: ldarg.1 IL_0001: ldarg.2 IL_0002: add.ovf IL_0003: stloc.0 I L _ 0 0 0 4 : br.s IL_0006 IL_000 6: ldloc.0 IL 0007: ret } // end of method Calc: :Add // конец метода Calc::Add Исходный код. Файлы с кодом C a lc . cs и C alc . vb доступны в подкаталоге Chapter 1. Преимущества CIL На этом этапе может возникнуть вопрос о том, какую выгоду приносит компиляция исходного кода в CIL, а не напрямую в набор ориентированных на конкретную плат­ форму инструкций. Одним из самых важных преимуществ такого подхода является интеграция языков. Как уже можно было увидеть, все компиляторы .NET генерируют примерно одинаковые CIL-инструкции. Благодаря этому все языки могут взаимодейст­ вовать в рамках четко обозначенной двоичной “арены”. Более того, поскольку CIL не зависит от платформы, .NET Framework тоже получа­ ется не зависящей от платформы, предоставляя те же самые преимущества, к которым привыкли Java-разработчики (например, единую кодовую базу, способную работать во многих операционных системах). На самом деле уже существует международный стан­ дарт языка С#, а также подмножество платформы .NET и реализации для многих опе­ рационных систем, отличных от Windows (более подробно об этом речь пойдет в конце настоящей главы). В отличие от Java, однако, .NET позволяет создавать приложения на предпочитаемом языке. Компиляция C IL-кода в инструкции, ориентированные на конкретную платформу Из-за того, что в сборках содержатся CIL-инструкции, а не инструкции, ориенти­ рованные на конкретную платформу, CIL-код перед использованием должен обяза­ тельно компилироваться на лету. Объект, который отвечает за компиляцию CIL-кода в понятные ЦП инструкции, называется оперативным (just-in-time — JIT) компилято­ ром. Иногда его “по-дружески” называют Jitter. Исполняющая среда .NET использует JIT-компилятор в соответствии с целевым ЦП и оптимизирует его согласно лежащей в основе платформе. Глава 1. Философия .NET 59 Например, в случае создания .NET-приложения, предназначенного для развертыва­ ния на карманном устройству (например, на мобильном устройстве, функционирующем под управлением Windows), соответствующий JIT-компилятор будет оптимизирован под функционирование в среде с ограниченным объемом памяти, а в случае развертыва­ ния сборки на серверной системе (где объем памяти редко представляет проблему), на­ оборот — под функционирование в среде с большим объемом памяти. Это дает разра­ ботчикам возможность писать единственный блок кода, который будет автоматически эффективно компилироваться JIT-компилятором и выполняться на машинах с разной архитектурой. Более того, при компиляции CIL-инструкций в соответствующий машинный код JITкомпилятор будет помещать результаты в кэш в соответствии с тем, как того требует це­ левая операционная система. То есть при вызове, например, метода PrintDocument () в первый раз соответствующие С11>инструкции будут компилироваться в ориентиро­ ванные на конкретную платформу инструкции и сохраняться в памяти для последую­ щего использования, благодаря чему при вызове PrintDocument () в следующий раз компилировать их снова не понадобится. На заметку! Можно также выполнять “ предварительную JIT-компиляцию” при инсталляции прило­ жения с помощью утилиты командной строки ngen.exe, которая поставляется в составе на­ бора .NET Framework 4.0 SDK. Применение такого подхода позволяет улучшить показатели по времени запуска для приложений, насыщенных графикой. Роль метаданных типов в .NET Помимо СIL-инструкций, в сборке .NET содержатся исчерпывающие и точные мета­ данные, которые описывают каждый определенный в двоичном файле тип (например, класс, структуру или перечисление), а также всех его членов (например, свойства, ме­ тоды или события). К счастью, за генерацию новейших и наилучших метаданных по типам всегда отвечает компилятор, а не программист. Из-за того, что метаданные .NET являются настолько детальными, сборки представляют собой полностью самоописываемые (self-describing) сущности. Чтобы увидеть, как выглядит формат метаданных типов в .NET, давайте рассмот­ рим метаданные, которые были сгенерированы для приведенного выше метода Add () из класса Calc на языке C# (метаданные для версии метода Add () на языке Visual Basic будут выглядеть похоже): TypeDef #2 (02000003) TypDefName: CalculatorExample.Calc (02000003) Flags : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldlnit] (00100001) Extends : 01000001 [TypeRef] System.Object Method #1 (06000003) MethodUame : Add (06000003) Flags : [Public] [HideBySig] [ReuseSlot] PVA : 0x00002090 ImplFlagc : [IL] [Managed] (00000000) CallCnvntn : [DEFAULT] hasThis ReturnType: 14 2 Arguments Argument #1: 14 Argument #2: 14 (00000086) 60 Часть I. Общие сведения о языке C# и платформе .NET 2 Parameters (1) ParamToken : (08000001) Name : x flags: [none] (2) ParamToken : (08000002) Name : у flags: [none] (00000000) (00000000) Метаданные используются во многих операциях самой исполняющей среды .NET, а также в различных средствах разработки. Например, функция IntelliSense, предлагае­ мая в таких средствах, как Visual Studio 2010, работает за счет считывания метаданных сборки во время проектирования. Кроме того, метаданные используются в различных утилитах для просмотра объектов, инструментах отладки и в самом компиляторе языка С#. Можно с полной уверенностью утверждать, что метаданные играют ключевую роль во многих .NET-технологиях, в том числе в Windows Communication Foundation (WCF), рефлексии, динамическом связывании и сериализации объектов. Более подробно о роли метаданных .NET будет рассказываться в главе 17. Роль манифеста сборки И, наконец, последним, но не менее важным моментом, о котором осталось вспом­ нить, является наличие в сборке .NET и таких метаданных, которые описывают саму сборку (они формально называются манифестом). Помимо прочих деталей, в манифе­ сте документируются все внешние сборки, которые требуются текущей сборке для кор­ ректного функционирования, версия сборки, информация об авторских правах и т.д. Как и за генерацию метаданных типов, за генерацию манифеста сборки тоже всегда отвечает компилятор. Ниже приведены некоторые наиболее существенные детали ма­ нифеста, сгенерированного в результате компиляции приведенного ранее в этой главе файла двоичного кода C a l c . cs (здесь предполагается, что компилятору было указано назначить сборке имя C alc . ехе): .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 В двух словах, в этом манифесте представлен список требуемых для C a lc .e x e внеш­ них сборок (в директиве . assem bly ex tern ), а также различные характеристики самой сборки (наподобие номера версии, имени модуля и т.д.). О пользе данных манифеста будет гораздо более подробно рассказываться в главе 14. Что собой представляет общая система типов (CTS) В каждой конкретной сборке может содержаться любое количество различающихся типов. В мире .NET “тип” представляет собой просто общий термин, который приме­ няется для обозначения любого элемента из множества |класс, интерфейс, структура, перечисление, делегат). При построении решений с помощью любого языка .NET, ско­ рее всего, придется взаимодействовать со многими из этих типов. Например, в сборке Глава 1. Философия .NET 61 может содержаться один класс, реализующий определенное количество интерфейсов, метод одного из которых может принимать в качестве входного параметра перечисле­ ние, а возвращать структуру. Вспомните, что CTS (общая система типов) представляет собой формальную специ­ фикацию, в которой описано то, как должны быть определены типы для того, чтобы они могли обслуживаться в CLR-среде. Внутренние детали CTS обычно интересуют только тех, кто занимается разработкой инструментов и/или компиляторов для платформы .NET. Абсолютно всем .NET-программистам, однако, важно уметь работать на предпочи­ таемом ими языке с пятью типами из CTS. Краткий обзор этих типов приведен ниже. Типы классов В каждом совместимом с .NET языке поддерживается, как минимум, понятие типа класса (class type), которое играет центральную роль в объектно-ориентированном про­ граммировании (ООП). Каждый класс может включать в себя любое количество членов (таких как конструкторы, свойства, методы и события) и точек данных (полей). В C# классы объявляются с помощью ключевого слова c la s s . // Тип класса C# с одним методом. class Calc { public int Add(int x, int y) { return x + y; } } В главе 5 будет более подробно показываться, как можно создавать типы классов CTS в С#, а пока в табл. 1.1 приведен краткий перечень характеристик, которые свой­ ственны типам классов. Таблица 1.1. Характеристики классов CTS Характеристика классов Описание Запечатанные Запечатанные (sealed), или герметизированные, классы не могут высту­ пать в роли базовых для других классов, т.е. не допускают наследования Реализующие интерфейсы Интерфейсом (interface) называется коллекция абстрактных членов, кото­ рые обеспечивают возможность взаимодействия между объектом и поль­ зователем этого объекта. CTS позволяет реализовать в классе любое количество интерфейсов Абстрактные или конкретные Экземпляры абстрактных (abstract) классов не могут создаваться напря­ мую, и предназначены для определения общих аспектов поведения для производных типов. Экземпляры же конкретных (concrete) классы могут создаваться напрямую Степень видимости Каждый класс должен конфигурироваться с атрибутом видимости (visibility). По сути, этот атрибут указывает, должен ли класс быть доступ­ ным для использования внешним сборкам или только изнутри опреде­ ляющей сборки Типы интерфейсов Интерфейсы представляют собой не более чем просто именованную коллекцию оп­ ределений абстрактных членов, которые могут поддерживаться (т.е. реализоваться) в данном классе или структуре. В C# типы интерфейсов определяются с помощью клю­ чевого слова in t e r fa c e , как, например, показано ниже: 62 Часть I. Общие сведения о языке C# и платформе .NET // Тип интерфейса в C# обычно объявляется // общедоступным, чтобы позволить типам в других // сборках реализовать его поведение. public interface IDraw { void Draw (); } Сами по себе интерфейсы мало чем полезны. Однако когда они реализуются в клас­ сах или структурах уникальным образом, они позволяют получать доступ к дополни­ тельным функциональным возможностям за счет добавления просто ссылки на них в полиморфной форме. Тема программирования с использованием интерфейсов подробно рассматривается в главе 9. Типы структур Понятие структуры тоже сформулировано в CTS. Тем, кому приходилось работать с языком С, будет приятно узнать, что таким пользовательским типам удалось “выжить” в мире .NET (хотя на внутреннем уровне они и ведут себя несколько иначе). Попросту говоря, структура может считаться “облегченным” типом класса с основанной на ис­ пользовании значений семантикой. Более подробно об особенностях структур будет рассказываться в главе 4. Обычно структуры лучше всего подходят для моделирования геометрических и математических данных, и в C# они создаются с помощью ключевого слова struct. / / Тип структуры в C#. struct Point { / / В структурах могут содержаться поля. public int xPos, yPos; / / В структурах могут содержаться параметризованные конструкторы. public Point (int х, int у) { xPos = x; yPos = y; } // В структурах могут определяться методы. public void PnntPosition () { Console .WnteLine (" ({ 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. Multicast Delegate, а не просто указатель на какой-то конкретный адрес в памяти. В C# делегаты объявляют­ ся с помощью ключевого слова delegate. // Этот тип делегата в C# может 'указывать' на любой метод, // возвращающий целое число и принимающий два целых // числа в качестве входных данных. public delegate int BinaryOp(int x, int y); Делегаты очень удобны, когда требуется обеспечить одну сущность возможностью перенаправлять вызов другой сущности и образовывать основу для архитектуры обра­ ботки событий .NET. Как будет показано в главах 11 и 19, делегаты обладают внутрен­ ней поддержкой для групповой адресации (т.е. пересылки запроса сразу множеству по­ лучателей) и асинхронного вызова методов (т.е. вызова методов во вторичном потоке). Члены типов Теперь, когда было приведено краткое описание каждого из сформулированных в CTS типов, пришла пора рассказать о том, что большинство из этих типов способно принимать любое количество членов (member). Формально в роли члена типа может выступать любой элемент из множества (конструктор, финализатор, статический кон­ структор, вложенный тип, операция, метод, свойство, индексатор, поле, поле только для чтения, константа, событие!. В спецификации CTS описываются различные “характеристики”, которые могут быть ассоциированы с любым членом. Например, каждый член может обладать ха­ рактеристикой, отражающей его доступность (т.е. общедоступный, приватный или за­ щищенный). Некоторые члены могут объявляться как абстрактные (для навязывания полиморфного поведения производным типам) или как виртуальные (для определения фиксированной, но допускающей переопределение реализации). Кроме того, почти все члены также могут делаться статическими членами (привязываться на уровне класса) или членами экземпляра (привязываться на уровне объекта). Более подробно о том, как можно создавать членов, будет рассказываться в ходе нескольких следующих глав. На заметку! Как будет описано в главе 10, в языке C# также поддерживается создание обобщен­ ных типов и членов. Встроенные типы данных И, наконец, последним, что следует знать о спецификации CTS, является то, что в ней содержится четко определенный набор фундаментальных типов данных. Хотя в ка­ ждом отдельно взятом языке для объявления того или иного встроенного типа данных 64 Часть I. Общие сведения о языке C# и платформе .NET из CTS обычно предусмотрено свое уникальное ключевое слово, все эти ключевые слова в конечном итоге соответствуют одному и тому же типу в сборке mscorlib.dll. В табл. 1.2 показано, как ключевые типы данных из CTS представляются в различ­ ных .NET-языках. Таблица 1.2. Встроенные типы данных, описанные в CTS Тип данных в CTS Ключевое слово в Visual Basic Ключевое слово в C# Ключевое слово в С++и CLI System.ByteByte Byte byte unsigned char System.SByteSByte SByte sbyte signed char System.Int16 Short short short System.Int32 Integer int int или long System.Int64 Long long System.Ulntl6 UShort ushort unsigned short System.UInt32 UInteger uint unsigned int или unsigned long System.UInt64 ULong ulong unsigned System.SingleSingle Single float float System.DoubleDouble Double double double System.Ob]ectObject Obj ect object objectA System.CharChar Char char wchar t System.Stringstring String String StringA int64 System.DecimalDecimal Decimal decimal Decimal System.BooleanBoolean Boolean bool bool int64 Из-за того факта, что уникальные ключевые слова в любом управляемом языке яв­ ляются просто сокращенными обозначениями реального типа из пространства имен System, больше не нужно беспокоиться ни об условиях переполнения и потери значи­ мости (overflow/underflow) в случае числовых данных, ни о внутреннем представлении строк и булевских значений в различных языках. Рассмотрим следующие фрагменты кода, в которых 32-битные числовые переменные определяются в C# и Visual Basic с использованием соответствующих ключевых слов из самих языков, а также формаль­ ного типа из CTS: // Определение числовых переменных в С#. int 1 = 0 ; System.Int32 j = 0; ' Определение числовых переменных в VB. Dim 1 As Integer = 0 Dim j As System.Int32 = 0 Что собой представляет общеязыковая спецификация (CLS) Как известно, в разных языках программирования одни и те же программные кон­ струкции выражаются своим уникальным, специфическим для конкретного языка об­ разом. Например, в C# конкатенация строк обозначается с помощью знака “плюс” (+), Глава 1. Философия .NET 65 а в VB для этого обычно используется амперсанд (&). Даже в случае выражения в двух отличных языках одной и той же программной идиомы (например, функции, не возвра­ щающей значения), очень высока вероятность того, что с виду синтаксис будет выгля­ деть очень по-разному: //На возвращающий ничего метод в С#. public void MyMethod() { // Какой-нибудь интересный к од ... } 1 Не возвращакяций ничего метод в VB. Public Sub MyMethod () 1 Какой-нибудь интересный к од ... End Sub Как уже показывалось, подобные небольшие вариации в синтаксисе для исполняю­ щей среды .NET являются несущественными благодаря тому, что соответствующие компиляторы (в данном случае — e s c . ехе и v b c . ехе) генерируют схожий набор CILинструкций. Однако языки могут еще отличаться и по общему уровню функциональных возможностей. Например, в каком-то из языков .NET может быть или не быть ключево­ го слова для представления данных без знака, а также поддерживаться или не поддер­ живаться типы указателей. Из-за всех таких вот возможных вариаций было бы просто замечательно иметь в распоряжении какие-то опорные требования, которым должны были бы отвечать все поддерживающие .NET языки. CLS (Common Language Specification — общая спецификация для языков програм­ мирования) как раз и представляет собой набор правил, которые во всех подробностях описывают минимальный и полный комплект функциональных возможностей, которые должен обязательно поддерживать каждый отдельно взятый .NET-компилятор для того, чтобы генерировать такой программный код, который мог бы обслуживаться CLR и к которому в то же время могли бы единообразным образом получать доступ все языки, ориентированные на платформу .NET. Во многих отношениях CLS может считаться просто подмножеством всех функциональных возможностей, определенных в CTS. В конечном итоге CLS является своего рода набором правил, которых должны при­ держиваться создатели компиляторов при желании, чтобы их продукты могли без про­ блем функционировать в мире .NET. Каждое из этих правил имеет простое название (например, “Правило CLS номер 6”) и описывает, каким образом его действие касается тех, кто создает компиляторы, и тех, кто (каким-либо образом) будет взаимодействовать с ними. Самым главным в CLS является правило I, гласящее, что правила CLS касаются только тех частей типа, которые делаются доступными за пределами сборки, в которой они определены. Из этого правила можно (и нужно) сделать вывод о том, что все остальные правила в CLS не распространяются на логику, применяемую для построения внутренних рабочих деталей типа .NET. Единственными аспектами типа, которые должны соответствовать CLS, являются сами определения членов (т.е. соглашения об именовании, параметры и возвращаемые типы). В рамках логики реализации члена может применяться любое количество и не согласованных с CLS приемов, поскольку для внешнего мира это не будет играть никакой роли. Для иллюстрации ниже приведен метод Add () на языке С#, который не отвечает правилам CLS, поскольку в его параметрах и возвращаемых значениях используются данные без знака (что не является требованием CLS): class Calc I // Использование данных без знака внешним образом // не соответствует правилам CLS! 66 Часть I. Общие сведения о языке C# и платформе .NET public ulong A d d (ulong x, ulong y) { return x + y; } Однако если бы мы просто использовали данные без знака внутренним образом, как показано ниже: class Calc { public int Add(int x, int y) { // Поскольку переменная ulong используется здесь // только внутренне, правила CLS не нарушаются. ulong temp = 0; return х + у; } тогда правила CLS были бы соблюдены и все языки .NET могли бы обращаться к дан­ ному методу Add (). Разумеется, помимо правила 1 в CLS содержится и много других правил. Например, в CLS также описано, каким образом в каждом конкретном языке должны представ­ ляться строки текста, оформляться перечисления (подразумевающие использование ба­ зового типа для хранения), определяться статические члены и т.д. К счастью, для того, чтобы быть умелым разработчиком .NET, запоминать все эти правила вовсе не обяза­ тельно. Опять-таки, очень хорошо разбираться в спецификациях CTS и CLS необходимо только создателям инструментов и компиляторов. Забота о соответствии правилам CLS Как можно будет увидеть в ходе прочтения настоящей книги, в C# на самом деле имеется ряд программных конструкций, которые не соответствуют правилам CLS. Хорошая новость, однако, состоит в том, что компилятор C# можно заставить выпол­ нять проверку программного кода на предмет соответствия правилам CLS с помощью всего лишь единственного атрибута. NET: // Указание компилятору C# выполнять проверку / / н а предмет соответствия CLS. [assembly: System.CLSCompliant(true)] Детали программирования с использованием атрибутов более подробно рассматри­ ваются в главе 15. А пока главное понять просто то, что атрибут [CLSCom pliant] за­ ставляет компилятор C# проверять каждую строку кода на предмет соответствия пра­ вилам CLS. В случае обнаружения нарушения каких-нибудь правил CLS компилятор будет выдавать ошибку и описание вызвавшего ее кода. Что собой представляет общеязыковая исполняющая среда (CLR) Помимо спецификаций CTS и CLS, для получения общей картины на данный мо­ мент осталось рассмотреть еще одну аббревиатуру — CLR, которая расшифровывается как Common Language Runtime (общеязыковая исполняющая среда). С точки зрения программирования под термином исполняющая среда может пониматься коллекция внешних служб, которые требуются для выполнения скомпилированной единицы про­ граммного кода. Например, при использовании платформы MFC для создания нового Глава 1. Философия .NET 67 приложения разработчики осознают, что их программе требуется библиотека времени выполнения MFC (те. mfc4 2 . d l l ) . Другие популярные языки тоже имеют свою испол­ няющую среду: программисты, использующие язык VB6, к примеру, вынуждены при­ вязываться к одному или двум модулям исполняющей среды (вроде msvbvm60 . d l l ) , а разработчики на Java — к виртуальной машине Java (JVM). В составе .NET предлагается еще одна исполняющая среда. Отавное отличие между исполняющей средой .NET и упомянутыми выше средами, состоит в том, что исполняю­ щая среда .NET обеспечивает единый четко определенный уровень выполнения, кото­ рый способны использовать все совместимые с .NET языки и платформы. Основной механизм CLR ф изически им еет вид би бли отеки под названием mscoree . d l l (и также называется общим механизмом выполнения исполняемого кода объектов — Common Object Runtime Execution Engine). При добавлении ссылки на сборку для ее использования загрузка библиотеки m scoree . d l l осуществляется авто­ матически и затем, в свою очередь, приводит к загрузке требуемой сборки в память. Механизм исполняющей среды отвечает за выполнение целого ряда задач. Сначала, что наиболее важно, он отвечает за определение места расположения сборки и обна­ ружение запрашиваемого типа в двоичном файле за счет считывания содержащихся там метаданных. Затем он размещает тип в памяти, преобразует CIL-код в соответст­ вующие платформе инструкции, производит любые необходимые проверки на предмет безопасности и после этого, наконец, непосредственно выполняет сам запрашиваемый программный код. Помимо загрузки пользовательских сборок и создания пользовательских типов, ме­ ханизм CLR при необходимости будет взаимодействовать и с типами, содержащимися в библиотеках базовых классов .NET. Хотя вся библиотека базовых классов поделена на ряд отдельных сборок, главной среди них является сборка m s c o r l i b . d l l . В этой сборке содержится большое количество базовых типов, охватывающих широкий спектр типич­ ных задач программирования, а также базовых типов данных, применяемых во всех языках .NET. При построении .NET-решений доступ к этой конкретной сборке будет предоставляться автоматически. На рис. 1.4 схематично показано, как выглядят взаимоотношения между исходным кодом (предусматривающим использование типов из библиотеки базовых классов), ком­ пилятором .NET и механизмом выполнения .NET. Различия между сборками, пространствами имен и типами Каждый из нас понимает важность библиотек программного кода. Главная цель та­ ких библиотек, как MFC, Java Enterprise Edition или ATL, заключается в предоставлении разработчикам набора готовых, правильно оформленных блоков программного кода, чтобы они могли использовать их своих приложениях. Язык С#, однако, не поставля­ ется с какой-либо специфической библиотекой кода. Вместо этого от разработчиков, использующих С#, требуется применять нейтральные к языкам библиотеки, которые поставляются в .NET. Для поддержания всех типов в библиотеках базовых классов в хо­ рошо организованном виде в .NET широко применяется понятие пространства имен (namespace). Под пространством имен понимается группа связанных между собой с семантиче­ ской точки зрения типов, которые содержатся в сборке. Например, в пространстве имен System. 10 содержатся типы, имеющие отношение к операциям ввода-вывода, в про­ странстве имен System. Data — основные типы для работы с базами данных, и т.д. 68 Часть I. Общие сведения о языке C# и платформе .NET Рис. 1.4. Механизм m s c o r e e .d ll в действии Очень важно понимать, что в одной сборке (например, m s c o r l i b . d l l ) может содер­ жаться любое количество пространств имен, каждое из которых, в свою очередь, может иметь любое число типов. Чтобы стало понятнее, на рис. 1.5 показан снимок окна предлагаемой в Visual Studio 2010 утилиты Object Browser. Эта утилита позволяет просматривать сборки, на которые имеются ссылки в текущем проекте, пространства имен, содержащиеся в каждой из этих сборок, типы, определенные в каждом из этих пространств имени, и члены каж­ дого из этих типов. Важно обратить внимание на то, что в m s c o r l i b . d l l содержится очень много самых разных пространств имен (вроде System. 10), и что в каждом из них содержатся свои семантически связанные типы (такие как B in aryR eader) . Глава 1. Философия .NET 69 I Object Bro*vse* > Ц (I My Solut.on 1 С ¥ BinaryReader(SystemJO Stream System Text Encoding) U CSharpAdder jJ Microsoft (TSharp » ♦ BinaiyReader(System IO.Stream) ♦ ♦ CloseO DisposeQ Л - О msccrlib { } Microsoft.Win32 О Microsoft.Win32.SafeHandles { ) System 0 System.Collections { } System CollectionsCcncurrent { } System Collections Generic * Dispose boclj v F.IIBufferfintj ¥ PeeVCharQ ¥ Readfbyte[], inf, int) ¥ Read ,cha>[] int. int) * ReadQ ^ Read7BitEncodedIntO { } System Collections.ObjectModel ♦ { } System-Configuration.Assemblies ♦ ReadByteQ ¥ ReadBytes(int) { } System Deployment Internal { } System Diagnostics 0 System Diagnostics CodeAnalysis * 1 ReadBooleanO ¥ ReadCharO ♦ ReadCbars(mt) { } System Diagnostics Contracts О System-Diagnostics.Eventing ¥ ReadDouhleQ <} System. Diagnostics SymbolStore О System Globalization * { ) System 10 Binary Writer •t$ BufferedStream public class Member B in a ry R e a d e r o f S y s t* rn J Q S u m m a ry . Reads primitive data types as binary values in a specific encoding Рис. 1.5. В одной сборке может содержаться произвольное количество пространств имен Главное отличие между таким подходом и зависящими от конкретного языка библио­ теками вроде MFC состоит в том, что он обеспечивает использование во всех языках, ориентированных на среду выполнения .NETT, одних и тех ж е пространств имен и одних и тех же типов. Например, в трех приведенных ниже программах иллюстрируется соз­ дание постоянно применяемого примера “Hello World” на языках С#, VB и C++/CLI. / / H e ll o w o r ld н а я зы к е C# using System; public class MyApp I static void M a m () Console.WriteLine("Hi from C#"); ' H e ll o w o r ld н а я зы к е VB Imports System Public Module MyApp Sub Main () Console .WnteLine ("Hi from VB") End Sub End Module / / H e llo w o r ld н а язы ке C + + / C L I #include "stdafx.h" using namespace System; int main (array<System: :String /‘> Лагдз) { Console::WnteLine ("Hi from C++/CLI") ; return 0; } Обратите внимание, что в каждом из языков применяется класс Console, определенный в пространстве имен System. Если отбросить незначительные синтаксические отличия, то в целом все три программы выглядят очень похоже, как по форме, так и по логике. 70 Часть I. Общие сведения о языке C# и платформе .NET Очевидно, что главной задачей любого планирующего использовать .NET разработ­ чика является освоение того обилия типов, которые содержатся в (многочисленных) пространствах имен .NET Самым главным пространством имен, с которого следует на­ чинать, является System. В этом пространстве имен содержится набор ключевых ти­ пов, которые любому разработчику .NET нужно будет эксплуатировать снова и снова. Фактически создание функционального приложения на C# невозможно без добавления хотя бы ссылки на пространство имен System, поскольку все главные типы данных (вро­ де System. Int32, System. String и т.д.) содержатся именно здесь. В табл. 1.3 приведен краткий список некоторых (но, конечно же, не всех) предлагаемых в .NET пространств имен, которые были поделены на группы на основе функциональности. Таблица 1.3. Некоторые пространства имен в .NET Пространство имен в .NET Описание System Внутри пространства имен System содержится множе­ ство полезных типов, позволяющих иметь дело с внут­ ренними данными, математическими вычислениями, генерированием случайных чисел, переменными среды и сборкой мусора, а также ряд наиболее часто приме­ няемых исключений и атрибутов System.Collections System.Collections.Generic В этих пространствах имен содержится ряд контейнер­ ных типов, а также несколько базовых типов и интер­ фейсов, которые позволяют создавать специальные коллекции System.Data System.Data.Common System.Data.EntityClient System.Data.SqlClient Эти пространства имен применяются для взаимодейст­ вия с базами данных с помощью AD0.NET System.10 System.10.Compression System.10.Ports В этих пространствах содержится много типов, предна­ значенных для работы с операциями файлового вводавывода, сжатия данных и манипулирования портами System.Reflection System.Reflection.Emit В этих пространствах имен содержатся типы, которые поддерживают обнаружение типов во время выполне­ ния, а также динамическое создание типов System.Runtime.InteropServices В этом пространстве имен содержатся средства, с по­ мощью которых можно позволить типам .NET взаимо­ действовать с “неуправляемым кодом” (например, DLLбиблиотеками на базе С и серверами СОМ) и наоборот System.Drawing System.Windows.Forms В этих пространствах имен содержатся типы, применяе­ мые для построения настольных приложений с исполь­ зованием исходного набора графических инструментов .NET (Windows Forms) System.Windows System.Windows.Controls System.Windows.Shapes Пространство System.Windows является корневым среди этих нескольких пространств имен, которые представляют собой набор графических инструментов Windows Presentation Foundation (WPF) System.Linq System.Xml.Linq System.Data.DataSetExtensions В этих пространствах имен содержатся типы, применяе­ мые при выполнении программирования с использова­ нием API-интерфейса LINQ System.Web Это пространство имен является одним из многих, кото­ рые позволяют создавать веб-приложения 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-данными Роль корневого пространства M i c r o s o f t При изучении перечня, приведенного в табл. 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. Общие сведения о языке C# и платформе .NET содержится внутри пространства имен под названием System. Но с точки зрения ис­ полняющей .NET это не так. Механизм исполняющей среды видит только одну лишь сущность по имени System. Console. В C# ключевое слово using упрощает процесс добавления ссылок на типы, содержащие­ ся в определенном пространстве имен. Вот как оно работает. Предположим, что требуется создать графическое настольное приложение с использованием API-интерфейса Windows Forms. В главном окне этого приложения должна визуализироваться гистограмма с ин­ формацией, получаемой из базы данных, и отображаться логотип компании. Поскольку для изучения типов в каждом пространстве имен требуются время и силы, ниже показаны некоторые из возможных кандидатов на использование в такой программе: // Все пространства имен, которые можно использовать // для создания подобного приложения. using System; // Общие типы из библиотеки базовых классов. using System.Drawing; // Типы для визуализации графики. // Типы для создания элементов пользовательского using System.Windows.Forms; // интерфейса с помощью Windows Forms. using System.Data; // Общие типы для работы с данными. // Типы для доступа к данным MS SQL Server. using System.Data.SqlClient; После указания ряда необходимых пространств имен (и добавления ссылки на сбор­ ки, в которых они находятся), можно свободно создавать экземпляры типов, которые в них содержатся. Например, при желании создать экземпляр класса Bitmap (определен­ ного в пространстве имен System. Drawing), можно написать следующий код: // Перечисляем используемые в данном файле // пространства имен явным образом. using System; using System.Drawing; class Program public void DisplayLogo () { // Создаем растровое изображение // размером 20*20 пикселей. Bitmap companyLogo = new Bitmap(20, 20); } } Благодаря импортированию в этом коде пространства имен System. Drawing, ком­ пилятор сможет определить, что класс Bitmap является членом данного пространства имен. Если пространство имен System. Drawing не указать, компилятор сообщит об ошибке. При желании переменные также можно объявлять с использованием полно­ стью уточненного имени: // Пространство имен System.Drawing здесь не указано! using System; class Program { public void DisplayLogo () { // Используем полностью уточненное имя. System.Drawing.Bitmap companyLogo = new System.Drawing.Bitmap (20, 20); } Глава 1. Философия .NET 73 Хотя определение типа с использованием полностью уточненного имени позволяет делать код более удобным для восприятия, трудно не согласиться с тем, что примене­ ние поддерживаемого в C# ключевого слова using, в свою очередь, позволяет значи­ тельно сократить количество печатаемых знаков. Поэтому в настоящей книге мы будем стараться избегать использования полностью уточненных имен (если только не будет возникать необходимости в устранении какой-то очевидной неоднозначности) и стре­ миться пользоваться более простым подходом, т.е. ключевым словом using. Важно помнить о том, что ключевое слово using является просто сокращенным спо­ собом указания полностью уточненного имени. Поэтому любой из этих подходов приво­ дит к получению одного и ттюго ж е CIL-кода (с учетом того факта, что в CIL-коде всегда применяются полностью уточненные имена) и не сказывается ни на производительно­ сти, ни на размере сборки. Добавление ссылок на внешние сборки Помимо указания пространства имен с помощью поддерживаемого в C# ключевого слова using, компилятору C# необходимо сообщить имя сборки, в которой содержит­ ся само CIL-onpeделение упоминаемого типа. Как уже отмечалось, многие из ключевых пространств имен .NET находятся внутри сборки mscorlib.dll. Класс System.Drawing.Bitmap, однако, содержится в отдельной сборке по имени System. Drawing.dll. Подавляющее большинство сборок в .NET Framework размеще­ но в специально предназначенном для этого каталоге, который называется глобальным кэшем сборок (Global Assembly Cache — GAC). На машине Windows по умолчанию GAC может располагаться внутри каталога %windiг %\Assembly, как показано на рис. 1.6. Favorites В Desktop Recent Pkces УЬ Libraries * Documents Assembly Name Version :ft System Data SqIXml 20.00 -ftSystem Deployment b03f5f7flld50a3a :ft System DnectoryServices 2 0.0 0 2J0.0J0 b03f5f7flld50a3a b03f5f7fl 1d50a3a rft System. Di 'ectoryServices A - countManagement 3.5.00 Ь77а5с561934е069 :ft System DirectoryServ ices. Protocols 2j0j0.0 b03f5f7flld50a3a if t System Design 2.0.0.0 b03f5f7flld50a3a System Drawing-Design 2j0 0 j0 b03f5f7flld50a3a Videos lift System EnterpriseServices aft System EnterpriseServices 2000 2.0 00 b03f5f7flld50a3a b03f5f7flld50a3a 3.00 0 Ь77а5с5б1934е089 .'ft System IdentityMcdel Selectors 3 0 0 j0 Ь77а5с561934е089 tft System JO lo g f t System Management 3.000 200 0 b03f5f7fll d50a3a b03f5f7flld50a3a sft System ManagementAutorrwtion 1-000 31bf3856ad364e35 Computer £ Ь77а5с561934е089 Pictures *4 Hrmegiouf ^ Public Key Token 20 OX) Music В Cut... Mongo D»we (G) -ftSystem IdentityModel CD Drive (F.) Рис. 1.6. Многие библиотеки .NET размещены в GAC В зависимости от того, какое средство применяется для разработки приложений .NET, на выбор может оказываться доступными несколько различных способов для уве­ домления компилятора о том, какие сборки требуется включить в цикл компиляции. Эти способы подробно рассматриваются в следующей главе, а здесь их детали опущены. На заметку! С выходом версии .NET 4.0 в Microsoft решили выделить под сборки .NET 4.0 специ­ альное место, находящееся отдельно от каталога С : \Windows\Assembly. Более подробно об этом будет рассказываться в главе 14. 74 Часть I. Общие сведения о языке C# и платформе .NET Изучение сборки с помощью утилиты ild a s m .e x e Тем, кого начинает беспокоить мысль о необходимости освоения всех пространств имен в .NETT, следует просто вспомнить о том, что уникальным пространство имен делает то, что в нем содержатся типы, которые как-то связаны между собой с семан­ тической точки зрения. Следовательно, если потребность в создании пользователь­ ского интерфейса, более сложного, чем у простого консольного приложения, отсутст­ вует, можно смело забыть о таких пространствах имен, как 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 окно C om m and P rom pt (Командная строка), введя в нем слово ildasm и нажав клавишу <Enter>. После запуска этой утилиты нужно выбрать в меню File (Файл) команду Open (Открыть) и найти сборку, которую требуется изучить. Для целей иллюстрации здесь предполагается, что нужно изучить сборку Calc.ехе, которая была сгенерирована на основе приведенного ранее в этой главе файла Calc, сs (рис. 1.7). Утилита ildasm.exe представляет структуру любой сборки в знакомом древовидном формате. Просмотр CIL-кода Помимо содержащ ихся в сборке про­ странств имен, типов и членов, утилита ет просматривать содержащийся внутри ildasm.ехе также позволяет просматривать и сборки .NET CIL-код, манифест и метадан­ CIL-инструкции, которые лежат в основе каж­ ные типов дого конкретного члена. Например, в результа­ те двойного щелчка на методе Main () в классе Program открывается отдельное окно с CIL-кодом, лежащим в основе этого метода, как показано на рис. 1.8. Рис. 1.7. Утилита ildasm.exe позволя­ Просмотр метаданных типов Для просмотра метаданных типов, которые содержатся в загруженной в текущий момент сборке, необходимо нажать комбинацию клавиш <Ctrl+M>. На рис. 1.9 показаны метаданные метода Calc.Add ( ) . Просмотр метаданных сборки (манифеста) И, наконец, чтобы просмотреть содержимое манифеста сборки, необходимо дважды щелкнуть на значке MANIFEST (рис. 1.10). Глава 1. Философия .NET 75 Рис. 1.8. Просмотр лежащего в основе CIL-кода Рис. 1.9. Просмотр метаданных типов с помощью ild a s m . e x e Рис. 1.10. Просмотр данных манифеста с помощью i ld a s m . e x e Несомненно, утилита ild a s m . e x e обладает большим, чем было показано здесь коли­ чеством функциональных возможностей; все остальные функциональные возможности этой утилиты будут демонстрироваться позже в книге при рассмотрении соответствую­ щих аспектов. Изучение сборки с помощью утилиты Reflector Хотя утилита i ld a s m . е х е и применяется очень часто для просмотра деталей дво­ ичного файла .NET, одним из ее недостатков является то, что она позволяет просмат­ ривать только лежащий в основе CIL-код, но не реализацию сборки с использованием 76 Часть I. Общие сведения о языке C# и платформе .NET предпочитаемого управляемого языка. К счастью, в Интернете для загрузки доступно множество других утилит для просмотра и декомпиляции объектов .NET, в том числе и популярная утилита Reflector. Эта утилита распространяется бесплатно и доступна по адресу http://www. red-gate.com/products/ref lector. После распаковки из ZIP-архива ее можно за­ пускать и подключать к любой представляющей интерес сборке, выбирая в меню File (Файл) команду Open (Открыть). На рис. 1.11 показано ее применение на примере при­ ложения Calc.exe. Рис. 1.11. Утилита Reflector является очень популярной программой для просмотра объектов Важно обратить внимание на то, что в утилите reflector.exe поддерживается окно Dissembler (Дизассемблер), которое можно открыть нажатием клавиши пробела, а также элемент раскрывающегося списка, который позволяет просматривать лежащую в основе кодовую базу на желаемом языке (разумеется, в том числе и на CIL). Остальные интри1ую щ ие функциональные возможности этой утилиты предлагается изучить самостоятельно. На заметку! Следует иметь в виду, что в остальной части настоящей книги для иллюстрации различ­ ных концепций будет применяться как утилита 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.microsof t .com/downloads). После установки пакета dotNetFx40_Full_x86.exe на целевой машине появятся необходимые библиотеки базовых классов .NET, исполняющая среда .NET (mscoree.dll) и дополнительная инфраструктура .NET (такая как GAC). На заметку! Операционные системы Windows Vista и Windows 7 изначально сконфигурированы с необходимой инфраструктурой исполняющей среды .NET. В случае развертывания приложения в среде какой-то другой операционной системы производства Microsoft, например, Windows ХР, нужно будет позаботиться об установке и настройке на целевой машине среды .NET. Клиентский профиль исполняющей среды .NET Установочная программа dotNetFx40_Full_x86.exe имеет объем примерно 77 Мбайт. Если для конечного пользователя обеспечивается возможность развертывать приложение с компакт-диска, это не будет представлять проблемы, поскольку устано­ вочная программа сможет просто запускать исполняемый файл тогда, когда машина не сконфигурирована надлежащим образом. Если пользователь должен будет загружать dotNetFx4 0_Full_x8 6 .ехе по медлен­ ному соединению с Интернетом, ситуация несколько усложняется. Для разрешения по­ добной проблемы в Microsoft разработали альтернативную установочную программу — так называемый клиентский профиль (dotNetFx4 0_Client_x8 6.exe), который тоже доступен для бесплатной загрузки на сайте Microsoft. Эта установочная программа, как не трудно догадаться по ее названию, предусмат­ ривает выполнение установки подмножества библиотек базовых классов .NET в допол­ нение к необходимой инфраструктуре исполняющей среды. Поскольку она имеет го­ раздо меньший размер (примерно 34 Мбайт), установку тех же самых библиотек, что появляются при полной установке .NET, на целевой машине она не обеспечивает. При желании не охватываемые ею сборки могут быть добавлены на целевую машину при выполнении пользователем обновления Windows (с помощью службы Windows Update). На заметку! Как у полного, так и у клиентского профиля исполняющей среды имеются 64-разрядные аналоги, которые называются, соответственно, dotNetFx40_Full_x86_x64.exe и dotNetFx40 Client х86 x64.exe. Не зависящая от платформы природа .NET В завершение настоящей главы хотелось бы сказать несколько слов о не зависящей от платформы природе .NET. К удивлению большинства разработчиков, сборки .NET могут разрабатываться и выполняться в средах операционных систем производства не Microsoft, в частности — в Mac OS X, различных дистрибутивах Linux, Solaris, а так­ же на устройствах типа iPhone производства Apple (через API-интерфейс MonoTbuch). Чтобы понять, что делает подобное возможным, необходимо рассмотреть еще одну ис­ пользуемую в мире .NET аббревиатуру — CLI, которая расшифровывается как Common Language Infrastructure (Общеязыковая инфраструктура). Вместе с языком программирования C# и платформой .NET в Microsoft был также разработан набор официальных документов с описанием синтаксиса и семантики язы­ ков C# и CIL, формата сборок .NET, ключевых пространств имен и технических деталей работы гипотетического механизма исполняющей среды .NET (названного виртуальной системой выполнения — Virtual Execution System (VES)). 78 Часть I. Общие сведения о языке C# и платформе .NET Все эти документы были поданы в организацию Ecma International (http: //www. ecma-international.org) и утверждены в качестве официальных международных стандартов. Среди них наибольший интерес представляют: • документ ЕСМА-334, в котором содержится спецификация языка С#; • документ ЕСМА-335, в котором содержится спецификация общеязыковой инфра­ структуры (CLI). Важность этих документов становится очевидной с пониманием того факта, что они предоставляют третьим сторонам возможность создавать дистрибутивы платформы .NET для любого количества операционных систем и/или процессоров. Среди этих двух спецификаций документ ЕСМА-335 является более “объемным”, причем настолько, что был разбит на шесть разделов, которые перечислены в табл. 1.4. Таблица 1.4. Разделы спецификации CLI Разделы документа ЕСМА-335 Предназначение Раздел 1. Концепции и архитектура В этом разделе описана общая архитектура 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 Fbundation) в 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, распространяемые с открытым исходным кодом Дистрибутив Описание h t t p ://www.mono-project. com Проект Mono представляет собой распространяемый с открытым исходным кодом дистрибутив CLI, который ориентирован на различные версии Linux (например, openSuSE, Fedora и т.п.), а также Windows и устройства Mac OS X и iPhone h ttp : //www,dotgnu. 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 и C# упрощают существующее положение вещей. Главную роль в .NET, по сути, играет механизм выполнения (m scoree . d l l ) и библио­ тека базовых классов ( m s c o r l i b . d l l вместе с сопутствующими файлами). Общеязы­ ковая среда выполнения (CLR) способна обслуживать любой двоичный файл .NET (сбор­ ку), который отвечает правилам управляемого программного кода. Как было показано в этой главе, в каждой сборке (помимо метаданных типов и манифеста) содержатся C1Lинструкции, которые с помощью JIT-компилятора преобразуются в инструкции, ори­ ентированные на конкретную платформу. Помимо этого, здесь рассматривалась роль общеязыковой спецификации (CLS) и общей системы типов (CTS). После этого было рассказано о таких полезных утилитах для просмотра объектов, как ild a s m .e x e и r e f l e c t o r . e x e , а также о том, как сконфигурировать машину для обслуживания приложений .NET с помощью полного и клиентского профилей. И, нако­ нец, напоследок было вкратце упомянуто о преимуществах не зависящей от платформы природы C# и .NET, о чем более подробно пойдет речь в приложении Б. ГЛАВА 2 Создание приложений на языке C# рограммисту, использующему язык С#, для разработки .NET-приложений на выбор доступно много инструментов. Целью этой главы является совершение краткого обзорного тура по различным доступным средствам для разработки .NET приложений, в том числе, конечно же, Visual Studio 2010. DiaBa начинается с рассказа о том, как работать с компилятором командной строки C# (esc . ехе), и самым простей­ шим из всех текстовых редакторов Notepad (Блокнот), который входит в состав опера­ ционной системы Microsoft Windows, а также приложением Notepad++, доступным для бесплатной загрузки. Хотя для изучения приведенного в настоящей книге материала вполне хватило бы компилятора c s c . e x e и простейшего текстового редактора, читателя наверняка за­ интересует использование многофункциональных интегрированных сред разработки (Integrated Development Environment — IDE). В этой главе также описана бесплатная IDE-среда с открытым исходным кодом SharpDevelop, предназначенная для разработ­ ки приложений .NET. Как будет показано, по своим функциональным возможностям эта IDE-среда не уступает многим коммерческим аналогам. Кроме того, в главе кратко рас­ сматривается IDE-среда Visual C# 2010 Express (распространяемая бесплатно), а также ключевая функциональность Visual Studio 2010. П На заметку! В ходе этой главы будут встречаться синтаксические конструкции С#, которые пока еще не рассматривались. Официальное изучение языка C# начнется в главе 3 Роль комплекта .NET Framework 4 .0 SDK Одним из мифов в области разработки .NET-приложений является то, что програм­ мистам якобы обязательно требуется приобретать копию Visual Studio для того, что­ бы разрабатывать программы на С#. На самом деле, создавать .NET-программу любого рода можно с помощью распространяемого бесплатно и доступного для загрузки ком­ плекта инструментов для разработки программного обеспечения .NET Framework 4.0 SDK (Software Development Kit). В этом пакете поставляются многочисленные управляе­ мые компиляторы, утилиты командной строки, примеры кода, библиотеки классов .NET и полная справочная система. На заметку! Программа установки .NET Framework 4.0 SDK (dotnet f х4 O f u ll setu p. ехе) доступ­ на на странице загрузки .NET по адресу h t t p : //msdn . m i c r o s o f t . com/netframework. Глава 2. Создание приложений на языке C# 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 Program s1^ Microsoft Visual Studio 2010*=$Visual Studio Tools (Все программы1^ Microsoft Visual Studio 2010^Инструменты Visual Studio). Рис. 2.1. Окно командной строки в Visual Studio 2010 Преимущество применения именно этого окна командной строки связано с тем, что оно уже сконфи1урировано на предоставление доступа к каждому из инструментов для разработки .NET-приложений. При условии, что на компьютере развернута среда разработки .NET, можно попробовать ввести следующую команду и нажать клавишу <Enter>: C S C - ? Если все в порядке, появится список аргументов командной строки, которые мо­ жет принимать работающий в режиме командной строки компилятор C# (esc означает C-sharp compiler). Создание приложений на C# с использованием e s c . е х е В действительности необходимость в создании крупных приложений с использова­ нием одного лишь компилятора командной строки C# может никогда не возникнуть, тем не менее, важно понимать в общем, как вручную компилировать файлы кода. Существует несколько причин, по которым освоение этого процесса может оказаться полезным. 82 Часть I. Общие сведения о языке C# и платформе .NET • Самой очевидной причиной является отсутствие Visual Studio 2010 или какой-то другой графической ШЕ-среды. • Работа может выполняться в университете, где использование инструментов для генерации кода и IDE-сред обычно запрещено. • Планируется применение автоматизированных средств разработки, таких как m sb u ild .ex e, которые требуют знать опции командной строки для используемых инструментов. • Возникло желание углубить свои познания в С#. В графических IDE-средах в ко­ нечном итоге все заканчивается предоставлением компилятору c s c .e x e инструк­ ций относительно того, что следует делать с входными файлами кода С#. В этом отношении изучение происходящего “за кулисами” позволяет получить необходи­ мые знания. Еще одно преимущество подхода с использованием одного лишь компилятора e s c . ехе состоит в том, что он позволяет обрести навыки и чувствовать себя более уве­ ренно при работе с другими инструментами командной строки, входящими в состав .NET Framework 4.0 SDK. Как будет показано далее, целый ряд важных утилит работает исключительно в режиме командной строки. Чтобы посмотреть, как создавать .NET-приложение без IDE-среды, давайте по­ строим с помощью компилятора C# и текстового редактора Notepad простую ис­ полняемую сборку по имени T e s tA p p .e x e . Сначала необходимо подготовить исход­ ный код. Откройте программу Notepad (Блокнот), выбрав в меню Start (Пуск) пункт All Program s1^ A cce sso rie s1^ Notepad (Все программы1^ Стандартные1^ Блокнот), и введи­ те следующее типичное определение класса на С#: // Простое приложение на языке С#. using System; class TestApp { static void Main() { Console.WriteLine("Testing! 1, 2, 3"); } После окончания ввода сохраните файл (например, в каталоге С: \CscExample) под име­ нем T estA p p . cs. Теперь давайте ознакомимся с ключевыми опциями компилятора С#. На заметку! По соглашению всем файлам с кодом на C# назначается расширение * . cs. Имя фай­ ла не нуждается в специальном отображении на имя какого-либо типа или типов. Указание целевых входных и выходных параметров Первым делом важно разобраться с тем, как указывать имя и тип создаваемой сбор­ ки (т.е., например, консольное приложение по имени M y S h e ll.e x e , библиотека кода по имени M a t h L ib . d ll или приложение Windows Presentation Foundation по имени Halo8 .ех е). Каждый из возможных вариантов имеет соответствующий флаг, который нужно передать компилятору esc . ехе в виде параметра командной строки (табл. 2.1). На заметку! Параметры, передаваемые компилятору командной строки (а также большинству дру­ гих утилит командной строки), могут сопровождаться префиксом в виде символа дефиса (- ) или символа косой черты ( /) . Глава 2. Создание приложений на языке C# 83 Таблица 2.1. Выходные параметры, которые может принимать компилятор C # Параметр Описание /out Этот параметр применяется для указания имени создаваемой сбор­ ки По умолчанию сборке присваивается то же имя, что у входного файла * . с s / t a r g e t : ехе Этот параметр позволяет создавать исполняемое консольное прило­ жение. Сборка такого типа генерируется по умолчанию, потому при создании подобного приложения данный параметр можно опускать / ta rg e t: lib r a r y Этот параметр позволяет создавать однофайловую сборку * . d l l / t a r g e t m odule Этот параметр позволяет создавать модуль. Модули являются эле­ ментами многофайловых сборок (и будут более подробно рассматри­ ваться в главе 14) / t a r g e t : winexe Хотя приложения с графическим пользовательским интерфейсом можно создавать с применением параметра / t a r g e t : ехе, параметр / t a r g e t : winexe позволяет предотвратить открытие окна консоли под остальными окнами Чтобы скомпилировать T es tA p p .cs в консольное приложение T ex tA p p .ex e, перей­ дите в каталог, в котором был сохранен файл исходного кода: cd C:\CscExample Введите следующую команду (обратите внимание, что флаги должны обязательно идти перед именем входных файлов, а не после): esc /target:exe TestApp.es Здесь флаг /out не был указан явным образом, поэтому исполняемый файл получит имя T estA p p . ехе из-за того, что именем входного файла является TestApp. Кроме того, для почти всех принимаемых компилятором C# флагов поддерживаются сокращенные версии написания, наподобие /t вместо / t a r g e t (полный список которых можно уви­ деть, введя в командной строке команду esc -?). esc /t:exe TestApp.es Более того, поскольку флаг I t : ехе используется компилятором как выходной пара­ метр по умолчанию, скомпилировать T e s tA p p .c s также можно с помощью следующей простой команды: esc TestApp.es Теперь можно попробовать запустить приложение T es tA p p . ехе из командной стро­ ки, введя имя его исполняемого файла, как показано на рис. 2.2. Рис. 2.2. Приложение T estA p p в действии 84 Часть I. Общие сведения о языке C# и платформе .NET Добавление ссылок на внешние сборки Давайте посмотрим, как скомпилировать приложение, в котором используются типы, определенные в отдельной сборке .NET. Если осталось неясным, каким образом компилятору C# удалось понять ссылку на тип System. Console, вспомните из главы 1, что во время процесса компиляции происходит автоматическое добавление ссылки на mscorlib.dll (если по какой-то необычной причине нужно отключить эту функцию, следует передать компилятору csc.exe параметр /nostdlib). Модифицируем приложение TestApp так, чтобы в нем открывалось окно сообще­ ния Windows Forms. Для этого откройте файл TestApp. cs и измените его следующим образом: using System; // Добавить эту строку: using System.Windows.Forms; class TestApp { static void Mai n () { Console .WnteLine ("Testing ! 1, // И добавить эту строку: MessageBox.Show("Hello..."); 2, 3"); } } Обратите внимание на импорт пространства имен System. Windows.Forms с помощью поддерживаемого в C# ключевого сло­ ва using (о котором рассказывалось в главе 1). Вспомните, что яв­ Рис. 2.3. Первое приложение Windows Forms ное перечисление пространств имен, которые используются внутри файла * .cs, позволяет избегать необходимости указывать полно­ стью уточненные имена типов. Далее в командной строке нужно проинформировать компилятор csc.exe о том, в какой сборке содержатся используемые простран­ ства имен. Поскольку применялся класс MessageBox из пространст­ ва имен System.Windows .Forms, значит, нужно указать компилято­ ру на сборку System. Windows .Forms .dll, что делается с помощью флага /reference (или его сокращенной версии /г): esc /г:System.Windows.Forms.dll TestApp.es Если теперь снова попробовать запустить приложение, то помимо консольного вы­ вода в нем должно появиться еще и окно с сообщением, как показано на рис. 2.3. Добавление ссылок на несколько внешних сборок Кстати, как поступить, когда необходимо указать csc.exe несколько внешних сбо­ рок? Для этого нужно просто перечислить все сборки через точку с запятой. В рассмат­ риваемом примере ссылаться на несколько сборок не требуется, но ниже приведена ко­ манда, которая иллюстрирует перечисление множества сборок: esc /г:System.Windows.Forms.dll;System.Drawing.dll *.cs На заметку! Как будет показано позже в настоящей главе, компилятор C# автоматически добав­ ляет ссылки на ряд ключевых сборок .NET (таких как System.Windows .Forms .dll), даже если они не указаны с помощью флага / г . Глава 2. Создание приложений на языке C# 85 Компиляция нескольких файлов исходного кода В текущем примере приложение TestApp.ехе создавалось с использованием единст­ венного файла исходного кода * . 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 Main () { Console.WriteLine("Testing 1 1, 2, 3”); // Эта строка тоже больше не нужна: // MessageBox.Show( ”H e llo .. . ”) ; // Используем класс HelloMessage: HelloMessage h = new HelloMessage(); h .Speak(); } } Чтобы скомпилировать файлы исходного кода на C# , необходимо их явно перечис­ лить как входные файлы: esc /г:System.Windows.Forms.dll TestApp.es HelloMsg.es В качестве альтернативного варианта компилятор C# позволяет использовать груп­ повой символ (*) для включения в текущую сборку всех файлов * . cs, которые содержат­ ся в каталоге проекта: esc /г:System.Windows.Forms.dll *.cs Вывод, получаемый после запуска этой программы, идентичен предыдущей про­ грамме. Единственное отличие между этими двумя приложениями связано с разнесе­ нием логики по нескольким файлам. Работа с ответными файлами в C# Как не трудно догадаться, для создания сложного приложения C# из командной строки потребовалось бы вводить утомительное количество входных параметров для уведомления компилятора о том, как он должен обрабатывать исходный код. Для облег­ 86 Часть I. Общие сведения о языке C# и платформе .NET чения этой задачи в компиляторе C# поддерживается использование так называемых ответных файлов (response files). В ответных файлах C# размещаются все инструкции, которые должны использо­ ваться в процессе компиляции текущей сборки. По соглашению эти файлы имеют рас­ ширение * .rsp (сокращение от response — ответ). Чтобы посмотреть на них в действии, давайте создадим ответный файл по имени TestApp.rsp, содержащий следующие аргу­ менты (комментарии в данном случае обозначаются символом #): # Это ответный файл для примера # TestApp.exe ив главы 2. # Ссылки на внешние сборки: /г:System.Windows.Forms.dll # Параметры вывода и подлежащие компиляции файлы # (здесь используется групповой символ): /target:ехе /outrTestApp.exe *.cs Теперь при условии сохранения данного файла в том же каталоге, где находятся под­ лежащие компиляции файлы исходного кода на С#, все приложение можно будет соз­ дать следующим образом (обратите внимание на применение символа @): esc @TestApp.rsp В случае необходимости допускается также указывать и несколько ответных *.rsp файлов в качестве входных параметров (например, esc @FirstFile.rsp @SecondFile .rsp @ThirdFile .rsp). При таком подходе, однако, следует иметь в виду, что компилятор обрабатывает параметры команд по мере их поступления. Следовательно, аргументы командной строки, содержащиеся в поступающем позже файле * . rsp, могут переопределять параметры из предыдущего ответного файла. Еще важно обратить внимание на то, что все флаги, перечисляемые явным образом перед ответным файлом, будут переопределяться настройками, которые содержатся в этом файле. То есть в случае ввода следующей команды: esc /out:MyCoolApp.ехе @TestApp.rsp имя сборки будет по-прежнему выглядеть как TestApp.ехе (а не MyCoolApp.ехе) из-за того, что в ответном файле TestApp. rsp содержится флаг /outiTestApp.exe. В слу­ чае перечисления флагов после ответного файла они будут переопределять настройки, содержащиеся в этом файле. На заметку! Действие флага /reference является кумулятивным. Где бы не указывались внеш­ ние сборки (перед, после или внутри ответного файла), в конечном итоге каждая из них все равно будет добавляться к остальным. Используемы й по умолчанию ответный файл (e s c . r s p ) Последним моментом, связанным с ответными файлами, о котором необходимо упо­ мянуть, является то, что с компилятором C# ассоциирован ответный файл с sc. rsp, который используется по умолчанию и размещен в том же самом каталоге, что и файл esc .ехе (обычно это С :\Windows\Microsof t .NET\Framework\<BepcMH>, где на месте элемента <версия> идет номер конкретной версии платформы). Открыв файл esc.rsp в программе Notepad (Блокнот), можно увидеть, что в нем с помощью флага /г : указано множество сборок .NET, в том числе различные библиотеки для разработки веб-приложений, программирования с использованием технологии LINQ и обеспечения доступа к данным и прочие ключевые библиотеки (помимо, конечно же, самой главной библиоте­ ки mscorlib.dll). Глава 2. Создание приложений на языке C# 87 При создании программ на C# с применением esc . ехе ссылка на этот ответный файл добавляется автоматически, даже когда указан специальный файл * . rsp. Из-за нали­ чия такого ответного файла по умолчанию, рассматриваемое приложение T e s tA p p . ехе можно скомпилировать и помощью следующей команды (поскольку в e s c . rsp уже со­ держится ссылка на System . Windows . Forms . d l l ) : esc /outiTestApp.exe *.cr. Для отключения функции автоматического чтения файла e s c . rsp укажите опцию / n ocon fig: esc @TestApp.rsp /noconfig На заметку! В случае добавления (с помощью опции / г) ссылок на сборки, которые на самом деле не используются, компилятор их проигнорирует Поэтому беспокоиться по поводу “разбухания кода" не нужно. Понятно, что у компилятора командной строки C# имеется множество других пара­ метров, которые можно применять для управления генерацией результирующей сборки .NET. Другие важные возможности будут демонстрироваться по мере необходимости да­ лее в книге, а полные сведения об этих параметрах можно всегда найти в документации .NET Framework 4.0 SDK Исходный код. Код приложения CscExample доступен в подкаталоге Chapter 2. Создание приложений .NET с использованием Notepad++ Еще одним текстовым редактором, о котором следует кратко упомянуть, является распространяемое с открытым исходным кодом бесплатное приложение Notepad++. Загрузить его можно по адресу http://not epad-plus .sou гсе forge .net/. В отличие от простого редактора Notepad (Блокнот), поставляемого в составе Windows, приложе­ ние Notepad++ позволяет создавать код на множестве различных языков и поддержи­ вает установку разнообразных дополнительных подключаемых модулей. Помимо этого, Notepad++ обладает рядом других замечательных достоинств, в том числе: • изначальной поддержкой для использования ключевых слов C# (и их кодирования цветом включительно); • поддержкой для свертывания синтаксиса (syntax folding), позволяющей свора­ чивать и разворачивать группы операторов в коде внутри редактора (и подобной той, что предлагается в Visual Studio 2010/C# 2010 Express); • возможностью увеличивать и уменьшать масштаб отображения текста с помо­ щью колесика мыши (имитирующего действие клавший <Ctrl>); • настраиваемой функцией автоматического завершения (autocompletion) различ­ ных ключевых слов C# и названий пространств имен .NET. Чтобы активизировать поддержку функции автоматического завершения кода на C# (рис. 2.4), необходимо одновременно нажать клавиши <Ctrl> и пробела. На заметку! Список вариантов, предлагаемых для автоматического завершения кода в отображае­ мом окне, можно изменять и расширять. Для этого необходимо открыть файл С : \ Program Files\Notepad+ +\plugins\APIs\cs .xml для редактирования и добавить в него любые дополнительные записи. 88 Часть I. Общие сведения о языке C# и платформе .NET Рис. 2.4. Использование функции автоматического завершения кода в Notepad++ Более подробно о приложении Notepad++ здесь рассказываться не будет. Чтобы узнать о нем больше, воспользуйтесь предлагаемым в его меню ? пунктом Help (Справка). Создание приложений.NET с помощью SharpDevelop Нельзя не согласиться с тем, что написание кода C# в приложении Notepad++, несо­ мненно, является шагом в правильном направлении по сравнению с использованием редактора Notepad (Блокнот) и командной строки. Тем не менее, в Notepad++ отсутству­ ют богатые возможности IntelliSense, визуальные конструкторы для построения графи­ ческих пользовательских интерфейсов, шаблоны проектов, инструменты для работы с базами данных и многое другое. Для удовлетворения перечисленных потребностей боль­ ше подходит такой рассматриваемый далее продукт для разработки .NET-приложений, как SharpDevelop (также называемый #Develop). Продукт SharpDevelop представляет собой распространяемую с открытым исходным кодом многофункциональную IDE-среду, которую можно применять для создания .NETсборок с помощью C# ,VB, CIL, а также Python-образного .NET-языка под названием Воо. Помимо того, что эта IDE-среда предлагается совершенно бесплатно, интересно обратить внимание на тот факт, что она сама реализована полностью на С#. Для ус­ тановки среды SharpDevelop необходимо загрузить и скомпилировать ее файлы * . cs вручную или запустить готовую программу setup.ехе. Оба дистрибутива доступны по адресу http://www.sharpdevelop.сот/. IDE-среда SharpDevelop обладает массой достоинств в плане улучшения продуктив­ ности. Наиболее важными из них являются: • поддержка для множества языков и типов проектов .NET; • функция IntelliSense, завершение кода и возможность использования только оп­ ределенных фрагментов кода; • диалоговое окно Add Reference (Добавление ссылки), позволяющее легко добав­ лять ссылки на внешние сборки, в том числе и те, что находятся в глобальном кэше сборок (Global Assembly Cache — GAC); • визуальный конструктор Windows Forms; • встроенные утилиты для просмотра объектов и определения кода; • визуальные утилиты для проектирования баз данных; • утилита для преобразования кода на C# в код на VB (и наоборот). Глава 2. Создание приложений на языке C# 89 Впечатляюще для бесплатной IDE-среды, не так ли? Ниже кратко рассматриваются некоторые наиболее интересные достоинства из перечисленных выше. На заметку! На момент написания книги в текущей версии SharpDevelop пока не поддерживались средства C# 2010 / .NET 4.0. Периодически заглядывайте на веб-сайт SharpDevelop, чтобы про­ верить, не появились ли следующие выпуски среды. Создание простого тестового проекта После установки SharpDevelop за счет выбора в меню File (Файл) пункта New1^Solution (Создать1 ^ Решение) можно указывать, какой тип проекта требуется сгенерировать (и на каком языке .NETT). Например, предположим, что нужно создать проект по имени MySDWinApp типа Windows Application (Приложение Windows) на языке C# (рис. 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. Общие сведения о языке C# и платформе NET Рис. 2.6. Конструирование приложения типа Windows Forms графическим образом в SharpDevelop MainForm.cs" | 1 ■«’jjMySDWinopp MamForm t h is | rtf AliowTransparerc> ibl c v rtual bool AutoS crol o* sets a value indurating whether the loim enables autovcrofcng | Apply AutoScaimfl 3 J *AotoSca(e -ute Sc at eBase& ze AotoScaleDi mens ion ? j J f AutoScal eF artor J*AjtGScaleMode Soo-ce D e»pi Рис. 2.7. В SharpDevelop поддерживается много утилит для генерации кода Создание приложений .NET с использованием Visual C# 2010 Express Летом 2004 г. компания Microsoft представила совершенно новую линейку ЮЕ-сред по общим названием “Express” (h tt p : //m sdn .m icrosoft.com / express). На сегодняш­ ний день на рынке предлагается несколько членов этого семейства (все они распростра­ няются бесплатно и поддерживаются и обслуживаются компанией Microsoft). • Visual Web Developer 2010 Express. “Облегченный” инструмент для разработки ди­ намических веб-сайтов ASP.NET и служб WCF. • Visual Basic 2010 Express. Упрощенный инструмент для программирования, иде­ ально подходящий для программистов-новичков, которые хотят научиться соз­ давать приложения с применением дружественного к пользователям синтаксиса Visual Basic. Глава 2. Создание приложений на языке C# 91 • Visual C# 2010 Express и Visual C++ 2010 Express. IDE-среды, ориентированные специально на студентов и всех прочих желающих обучиться основам програм­ мирования с использованием предпочитаемого синтаксиса. • SQL Server Express. Система для управления базами данных начального уровня, предназначенная для любителей, энтузиастов и учащихся-разработчиков. Некоторые уникальные функциональные возможности Visual C# 2010 Express В целом продукты линейки Express представляют собой усеченные версии своих полнофункциональных аналогов в линейке Visual Studio 2010 и ориентированы глав­ ным образом на любителей .NET и занимающихся изучением .NET студентов. Как и в SharpDevelop, в Visual C# 2010 Express предлагаются разнообразные инструменты для просмотра объектов, визуальный конструктор Windows Fbrms, диалоговое окно Add R e fe ren ces (Добавление ссылок), возможности IntelliSense и шаблоны для расширения программного кода. Помимо этого в Visual C# 2010 Express доступно несколько (важных) функциональ­ ных возможностей, которые в SharpDevelop в настоящее время отсутствуют. К их числу относятся: • развитая поддержка для создания приложений Window Presentation Foundation (WPF) с помощью XAML; • функция IntelliSense для новых синтаксических конструкций C# 2010, в том числе именованных аргументов и необязательных параметров; • возможность загружать дополнительные шаблоны, позволяющие разрабатывать приложения ХЬох 360, приложения WPF с интеграцией TWitter, и многое другое. На рис. 2.8 показан пример, иллюстрирующий создание с помощью Visual C# Express XAML-разметки для проекта WPF. Common WPF Controls . АО WPF Controls J Solution V/pfApp&cBtionl (1 prorecV; a 3 Wpl Application 1 >lt ad * не There ire no usable controls in this group Drag in item onto this text to add it to the U design U в XAML Indoto • C la s s -“W p fA p p lic B tio n l M Л № *J n s «"h ttp ;//s ch o »a s re ic r t x n b -.: x » " h ttp : //s c h e * a s .i» i; ; « f i itii'.-H a ln W in d o w " Heigh О '-» , cOrld> Properties Rele ences App-xaml M a n W n d o v jam l Calendar Click ClickMode -ц , т - < « u tto n » :*tai»e«=”« y b u tto n " c/uindoM ) 100% - • __ 1 Grid Window,'Ond Рис. 2.8. Visual C# Express обладает встроенной поддержкой API-интерфейсов .NET 4.0 92 Часть I. Общие сведения о языке C# и платформе .NET Поскольку по внешнему виду и поведению IDE-среда Visual C# 2010 Express очень похожа на Visual Studio 2010 (и, в некоторой степени, на SharpDevelop), более подробно она здесь рассматриваться не будет. Ее вполне допустимо использовать для проработ­ ки дальнейшего материала книги, но при этом обязательно следует иметь в виду, что в ней не поддерживаются шаблоны проектов для создания веб-сайтов ASP. NETT. Чтобы иметь возможность строить веб-приложения, необходимо загрузить продукт Visual Web Developer 2010, который также доступен на сайте h t t p : //msdn . m i c r o s o f t . сот/ ex p re ss. Создание приложений .NET с использованием Visual Studio 2010 Профессиональные разработчики программного обеспечения .NET наверняка рас­ полагают самым серьезным в этой сфере продуктом производства Microsoft, который называется Visual Studio 2010 и доступен по адресу http :/ /msdn.microsoft.сот/ 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), дважды щелкая на соответствующем файле решения * . s in . Если на машине не установлено ни Visual Studio 2010, ни С#20010 Express, файлы * . cs потребуется вставлять вручную в рабочую область проекта внутри используемой ЮЕ-среды. Некоторые уникальные функциональные возможности Visual Studio 2010 Как не трудно догадаться, Visual Studio 2010 также поставляется с графическими конструкторами, поддержкой использования отдельных фрагментов кода, средствами для работы с базами данных, утилитами для просмотра объектов и проектов, а так­ же встроенной справочной системой. Но, в отличие от многих из уже рассмотренных IDE-сред, в Visual Studio 2010 предлагается множество дополнительных возможностей, наиболее важные из которых перечислены ниже: • графические редакторы и конструкторы XML; • поддержка разработки программ Windows, ориентированных на мобильные устройства; Глава 2. Создание приложений на языке C# 93 • поддержка разработки программ Microsoft Office; • поддержка разработки проектов Windows Workflow Foundation; • встроенная поддержка рефакторинга кода; • инструменты визуального конструирования классов. По правде говоря, в Visual Studio 2010 предлагается настолько много возможностей, что для их полного описания понадобилась бы отдельная книга. В настоящей книге та­ кие цели не преследуются. Тем не менее, в нескольких следующих разделах наиболее важные функциональные возможности рассматриваются чуть подробнее, а другие по мере необходимости будут описаны далее в книге. Ориентирование на .NET Framework в диалоговом окне New Project Те, кто следует указаниям этой главы, сейчас могут попробовать создать новое кон­ сольное приложение на C# (по имени Vs2010Exam ple), выбрав в меню File (Файл) пункт N e w ^ P ro je c t (Создать1^Проект). Как можно увидеть на рис. 2.9, в Visual Studio 2010 поддерживается возможность выбора версии .NET Framework (2.0, 3.x или 4.0), для ко­ торой должно создаваться приложение, с помощью раскрывающегося списка, отобра­ жаемого в правом верхнем углу диалогового окна N ew P ro je c t (Новый проект). Для всех описываемых в настоящей книге проектов, в этом списке можно оставлять выбранным предлагаемый по умолчанию вариант .NET F ra m e w o rk 4.0. MET Framework 4 * j : • f l i |j Search Installed Tem plate. ^N^^ramew^lTo ■ Visual С * NET Framework 3.0 NET Framework ЗД Д [plication Visual C * Type: Visual C * j A project for creating a com m and-li application W indows Visual C * Web Office Cloud Service Reporting SharePoint jf i WPF Application Visual C * WPF Browser Application Visual C * Console Application Visual C * WPF Custom Control Library Visual C * Empty Project Visual C * |] 1И if SiUerlight Test WCF W oikflow . Other Languages C|l | le cunentfy not aUcwed to load Nam e location Vs2010Example c .user, ar drew troel.en doc u m e n t'1v sual studio lONProjects 1 VivO lOlcomple_______________________________________________ Create tfcrectory For solution Add to source control Рис. 2.9. В Visual Studio 2010 позволяется выбирать определенную целевую версию .NET Framework Использование утилиты Solution Explorer Утилита Solution Explorer (Проводник решений), доступная через меню View (Вид), позволяет просматривать набор всех файлов с содержимым и ссылаемых сборок, кото­ рые входят в состав текущего проекта (рис. 2.10). Обратите внимание, что внутри папки R e fe re n ce s (Ссылки) в окне Solution Explorer отображается список всех сборок, на которые в проекте были добавлены ссылки. В за­ висимости от типа выбираемого проекта и целевой версии .NET Framework, этот список выглядит по-разному. 94 Часть I. Общие сведения о языке C# и платформе .NET - S o lu tio n E x p lo r e r Ck. □ X 3 >> * 3 S o lu tio n 'V s 2 0 1 0 E x a m p le Д p r o je c t ; л ^ V s 2 0 1 0 E x a m p le ^ л P r o p e r tie s R e fe r e n c e s •C J M ic r o s o f t.C S H a r p 4 J S y s te m ■Л S y s t e m .C e r e 4 J S y s t e m .D a t a - О S y s t e m .D a t a D a ta S e tE x te n s io n s -O S y s t e m .X m l -_J S y s t e m .X m l.L m q ^ P r o g r a m .e s Рис. 2.10. Окно утилиты Solution Explorer Д обавление ссы лок на внешние сборки Если необходимо сослаться на дополнительные сборки, щелкните напалке R e fe ren ces правой кнопкой мыши и выберите в контекстном меню пункт A dd R e fe ren ce (Добавить ссылку). После этого откроется диалоговое окно, позволяющее выбрать желаемые сборки (в Visual Studio это аналог параметра /reference в компиляторе командной строки). На вкладке .NET этого окна, показанной на рис. 2.11, отображается список наиболее час­ то используемых сборок .NET; на вкладке B ro w se (Обзор) предоставляется возможность найти сборки .NET, которые находятся на жестком диске; на вкладке R e cen t (Недавние) приводится перечень сборок, на которые часто добавлялись ссылки в других проектах. A d a R e fe re n c e I NET ^CO M J P ro jects f B ro w s * | R ecen t Com ponent Nam e Version R u n tim e P ath S y s te m .ld e n tity M o d e l 4 0 .0 .0 v4.0,21006 C :> P ro g ram S ystem .Id e n tity M o d e l. Sele 4 0 .0 0 Л 021306 C : \P ie g r a m S ystem . M a n a g e m e n t 4.023.0 4*4.0.21006 C A P ro g ra m 4 0 .0 .0 v 4 .0 2 1 006 C A P ro g ra m S y s te m .M e s s a g in g 4 .0 Д 0 v 4 .0.21006 C A P ro g ra m I S v s te m .M a n a g e m e n tln s tr S y s te m .N e t 4.О .0Л Л 0.21 0 0 6 C A P ro g ra m S ystem . N u m e ric s 4.0JD.0 4*4X1.21006 C A P ro g ra m S y s te m .P rin tin g 4.043.0 v 4 .0.21006 C A P ro g ra m S y s te m .R u n tim e 4 0 .0 .0 v 4 .0 2 1 0 0 6 C A P ro g ra m ■ S ystem P u n tim e .R e m c tin g 4 .0 0 .0 v*4.0.2 1 0 0 6 C A P ro g ra m 1 S ystem R u n tim e S e n a liia ti.. 4.0.0.0 Л , 0.2 1 0 0 6 C : \P to g r a m Рис. 2.11. Диалоговое окно A dd R e fe ren ce Просмотр свойств проекта И, наконец, напоследок важно обратить внимание на наличие в окне утилиты Solution Explorer пиктограммы P ro p e rtie s (Свойства). Двойной щелчок на ней приво­ дит к открытию редактора конфигурации проекта, окно которого называется P ro je ct P ro p e rtie s (Свойства проекта) и показано на рис. 2.12. Глава 2. Создание приложений на языке C# 95 Рис. 2.12. Окно P ro je c t P ro p e rtie s Различные возможности, доступные в окне P ro je c t P ro p e rtie s , более подробно рас­ сматриваются по ходу данной книги. В этом окне можно устанавливать различные параметры безопасности, назначать сборке надежное имя, развертывать приложение, вставлять необходимые для приложения ресурсы и конфигурировать события, которые должны происходить перед и после компиляции сборки. Утилита Class View Следующей утилитой, с которой необходимо познакомиться, является Class View (Просмотр классов), доступ к которой тоже можно получать через меню View. Эта утилита позволяет просмат­ - a V s 2 0 1 0 E )u m p te & P ro je c t R eferen ces ривать все типы, которые присутствуют в теку­ i { } V s 2 0 1 0 E x a m p le щем проекте, с объектно-ориентированной точки л P ro g ra m * ;__j Base T y p e s зрения (а не с точки зрения файлов, как это позво­ *1 ляет делать утилита Solution Explorer). В верхней панели утилиты Class View отображается список f * - O b je c tO пространств имен и их типов, а в нижней пане­ ♦ E q u a ls (o b je c t, o b je c t) □ ли — члены выбранного в текущий момент типа, • E q u a ls (o b je c t) 1 как показано на рис. 2.13. ♦ G e tH a s h C o d e O ♦ G e tT y p e O В результате двойного щелчка на типе или члене 4 * M e m b e rv w s e C lo n e O ..................................J типа в окне утилиты Class View в Visual Studio бу­ дет автоматически открываться соответствующий Рис. 2.13. Окно утилиты Class View файл кода С#, с размещением курсора мыши на соответствующем месте. Еще одной замечательной функциональностью утилиты Class View в Visual Studio 2010 является возможность открывать любую ссылаемую сборку и просматривать содержащиеся внутри нее пространства имен, типы и члены (рис. 2.14). Утилита Object Browser В Visual Studio 2010 доступна еще одна утилита для изучения множества сборок, на которые имеются ссылки в текущем проекте. Называется эта утилита Object Browser (Браузер объектов) и получить к ней доступ можно, опять-таки, через меню View. После открытия ее окна останется просто выбрать сборку, которую требуется изучить (рис. 2.15). 96 Часть I. Общие сведения о языке C# и платформе .NET 4 V s20 10 E x a m p le J P ro je c t R eferences •J M ic ro s o ft.C S h a rp * ч э m sc o rlib ( } M ic ro s o ft.W in 3 2 { } M ic ro s o ft.W in 3 2 .S s fe H fln d le s л О > System A c tio n «L A c c e s s V io la tio n E x c e p tio n (S y s te m .R u n tim e .S e rie liia tio n .S e ria ln a tio n In fo , • A ccessV io la tio n E x c e p tio n (s tn n g , S ystem .E xception! V A ccess V io la tio n E x c e p tio n (s tn n g ) • A c c e ssV io latio n E xce p tio n Q Class V ie w J •5? Solutaon Expkiiff Рис. 2.14. Утилита Class View может также применяться для просмотра ссылаемых сборок Рис. 2.15. Окно утилиты Object Browser Встроенная поддержка рефакторинга программного кода Одной из главных функциональных возможностей Visual Studio 2010 является встро­ енная поддержка для проведения рефакторинга существующего кода. Если объяснять упрощенно, то под рефакторингом (refactoring) подразумевается формальный механи­ ческий процесс улучшения существующего кода. В прежние времена рефакторинг тре­ бовал приложения массы ручных усилий. К счастью, теперь в Visual Studio 2010 можно достаточно хорошо автоматизировать этот процесс. За счет использования меню R e fa c to r (Рефакторинг), которое становится доступным при открытом файле кода, а также соответствующих клавиатурных комбинаций быст­ рого вызова, смарт-тегов (smart tags) и/или вызывающих контекстные меню щелчков, можно существенно видоизменять код с минимальным объемом усилий. В табл. 2.2 перечислены некоторые наиболее распространенные приемы рефакторинга, которые распознаются в Visual Studio 2010. Глава 2. Создание приложений на языке C# 97 Таблица 2.2. Приемы рефакторинга, поддерживаемые в Visual Studio 2010 Прием рефакторинга Описание Extract Method (Извлечение метода) Позволяет определять новый метод на основе выбирае­ мых операторов программного кода Encapsulate Field (Инкапсуляция поля) Позволяет превращать общедоступное поле в приватное, инкапсулированное в форму свойство C# Extract Interface (Извлечение интерфейса) Позволяет определять новый тип интерфейса на основе набора существующих членов типа Reorder Parameters (Переупорядочивание параметров) Позволяет изменять порядок следования аргументов в члене Remove Parameters (Удаление параметров) Позволяет удалять определенный аргумент из текущего списка параметров Rename (Переименование) Позволяет переименовывать используемый в коде метод, поле, локальную переменную и т.д. по всему проекту Чтобы увидеть процесс рефакторинга в действии, давайте модифицируем метод М а ш (), добавив в него следующий код: static void Main(string[] args) { // Настройка консольного интерфейса (C U I). Console.Title = "My Rocking App"; Console.ForegroundColor = ConsoleColor.Yellow; Console.BackgroundColor = ConsoleColor.Blue; Console WnteLine ("***************************************") • Console. WnteLine ("***** Welcome to My Rocking App! *******"); Console WriteLine ( " * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * " ) • Console.BackgroundColor = ConsoleColor.Black; // Ожидание нажатия клавиши <Enter>. Console.ReadLine() ; } В таком, как он есть виде, в этом коде нет ничего неправильного, но давайте пред­ ставим, что возникло желание сделать так, чтобы данное приветственное сообщение отображалось в различных местах по всей программе. В идеале вместо того, чтобы за­ ново вводить ту же самую отвечающую за настройку консольного интерфейса логику, было бы неплохо иметь вспомогательную функцию, которую можно было бы вызывать для решения этой задачи. С учетом этого, попробуем применить к существующему коду прием рефакторинга E x tra c t M e th o d (Извлечение метода). Для этого выделите в окне редактора все содержащиеся внутри Main () операто­ ры, кроме последнего вызова Console.ReadLine ( ) , и щелкните на выделенном коде правой кнопкой мыши. Выберите в контекстном меню пункт R e fa c to r ^ E x tra c t M e th o d (Рефакторинг1 ^Извлечь метод), как показано на рис. 2.16. В открывшемся далее окне назначьте новому методу имя ConfigureCUI () . После этого метод Main() станет вызывать новый только что сгенерированный метод ConfigureCUI (), внутри которого будет содержаться выделенный ранее код: class Program { static void Main(string[] args) { ConfigureCUI (); // Ожидание нажатия клавиши <Enter>. 98 Часть I. Общие сведения о языке C# и платформе .NET Console.ReadLine(); } private static void ConfigureCUl() { // Н астрой ка к о н сольн ого и н терф ей са (C U I). Console.Title = "My Rocking App"; Console.Title = "Мое приложение"; Console.ForegroundColor = ConsoleColor.Yellow; Console.BackgroundColor = ConsoleColor.Blue; Console.WriteLine("***************************************" ); Console.WnteLine ("**** * Welcome to My Rocking App! *******") • Console WnteLine ^*********** *****************•»<•************ ««j . Console.BackgroundColor = ConsoleColor.Black; } } Этот лиш ь один простой пример применения предлагаемых в Visual Studio 2010 встроенных возможностей для рефакторинга, и по ходу настоящей книги будет встре­ чаться еще немало подобных примеров. *i ^JVsZOlGExample.Program ^*Wain{string[] args) * static void H ain (vtrin gf] args) { . f t up C o n s o le U I (C U ] ) • . T i t l e - "My R o c k iri . F o re g ro u n d C o lo r - . B a c k g ro u n d C o lo r » C j • * . W r it e L in e ( " * * * •* * “ o l e . W r i t e L i n e ( " ' *•** w o le . W r it e L in e (" * * * " * • * 1 . B a c k g ro u n d C o lo r * J la it f o r t n t f r ke> to b . R e a d L in e O ; Refactor s Organize Usings a </ Renam e.. F2 & Extract Method... C trkR ,M Encapsulate Field- Ctrl+R, E Extract Interface. Ctrl-» R, J Remove Parameters... Ctrl-» R. V Reorder Parameters... O ik ^ O Create Unit Test*... Generate Sequence Diagram... Insert Snippet.. Ctrl-*-ic x Surround With... C trM C S Go To Definition F12 Find All References Ctrl+K, R View Call Hierarchy C trkK. T a.b Breakpoint 41 Run T о Cursor Ctrl+F10 A Cut C txbX Am Copy C tr k C Л Paste Ctrl* У Outlining Рис. 2.16. Активизация рефакторинга кода Возможности для расширения и окружения кода В Visual Studio 2010 (а также в Visual C# 2010 Express) можно вставлять готовые блоки кода C# выбором соответствующих пунктов в меню, вызовом контекстных меню по щелчку правой кнопкой мыши и/или использованием соответствующих клавиатур­ ных комбинаций быстрого вызова. Число доступных шаблонов для расширения кода впечатляет. В целом их можно поделить на две основных группы. • Шаблоны для вставки фрагментов кода (code snippet). Эти шаблоны позволяют вставлять общие блоки кода в месте расположения курсора мыши. • Шаблоны для окружения кода (Surround With). Эти шаблоны позволяют помещать блок избранных операторов в рамки соответствующего контекста. Чтобы посмотреть на эту функциональность в действии, давайте предположим, что требуется обеспечить проход по поступающим в метод Main () параметрам в цикле foreach. Глава 2. Создание приложений на языке C# 99 Вместо того чтобы вводить необходимый код вручную, можно активизировать фраг­ мент кода f oreach. После выполнения этого действия IDE-среда поместит шаблон кода fo rea ch в месте, где в текущий момент находится курсор мыши. Поместим курсор мыши после первой открывающей фигурной скобки в методе Main (). Одним йз способов активизации фрагмента кода является выполнение щелчка правой кнопкой мыши и выбора в контекстном меню пункта Insert Snippet (Вставить фрагмент кода) (или Surround With (Окружить с помощью)). Это приводит к отображению списка всех относящихся к данной категории фрагментов кода (для закрытия контек­ стного меню достаточно нажать клавишу <Esc>). В качестве клавиатурной комбинации быстрого вызова можно просто ввести имя интересующего фрагмента кода, которым в данном случае является fo rea ch . На рис. 2.17 видно, что пиктограмма, представляю­ щая фрагмент кода, внешне немного напоминает клочок бумаги. Рис. 2.17. Активизация фрагмента кода Отыскав фрагмент кода, который требуется активизировать, нажмите два раза кла­ вишу <ТаЬ>. Это приведет к автоматическому завершению всего фрагмента кода и ос­ тавлению ряда меток-заполнителей, в которых останется только ввести необходимые значения, чтобы фрагмент был готов. Нажимая клавишу <ТаЬ>, можно переходить от одной метки-заполнителя к другой и заполнять пробелы (по завершении нажмите кла­ вишу <Esc> для выхода из режима редактирования фрагмента кода). В результате щелчка правой кнопкой мыши и выбора в контекстном меню пункта Surround With (Окружить с помощью) будет тоже появляться список возможных вари­ антов. При использовании средства Surround With обычно сначала выбирается блок операторов кода для представления того, что должно применяться для их окружения (например, блок try / c a tc h ). Обязательно уделите время изучению предопределенных шаблонов расширения кода, поскольку они могут радикально ускорить процесс разра­ ботки программ. На заметку! Все шаблоны для расширения кода представляют собой XML-описания кода, подлежа­ щие генерации в IDE-среде. В Visual Studio 2010 (а также в Visual C# 2010 Express) можно соз­ давать и собственные шаблоны кода. Дополнительные сведения доступны в статье “ Investigating Code Snippet Technology” (“ Исследование технологии применения фрагментов кода” ) на сайте h t t p : // m s d n .m ic ro s o ft. com. 100 Часть I. Общие сведения о языке C# и платформе .NET Утилита Class Designer S o lu tio n Explorer В Visual Studio 2010 имеется возможность кон­ струировать классы визуальным образом (в Visual ft J h P ro p e rtie s C# 2010 Express такой возможности нет). Для этого a o j / R e ferences 4 1 M i c ro s o ft S h jr p в составе Visual Studio 2010 поставляется утилита • J S y s te m под названием Class Designer (Конструктор клас­ 4 3 S y s te m .C o re сов), которая позволяет просматривать и изменять 4 3 S y s te m .D a ta *a3 S y s te m .D a ta .D a ta S e tE x te n s io n s отношения между типами (классами, интерфейса­ 4 3 System JC m l ми, структурами, перечислениями и делегатами) в • O S y s te m .X m l.L in q J j P ro g ra m .e s проекте. С помощью этой утилиты можно визуально добавлять или удалять члены из типа с отражением ^^^Solutior^xploreJ этих изменений в соответствующем файле кода на С#, а также в диаграмме классов. Рис. 2.18 Вставка файла диаграммы Для работы с этой утилитой сначала необходимо классов вставить новый файл диаграммы классов. Делать это можно несколькими способами, одним из которых является щелчок на кнопке View C la ss Diagram (Просмотр диаграммы классов) в правой части окна Solution Explorer, как показано на рис. 2.18 (при этом важно, чтобы в окне был выбран проект, а не решение). После выполнения этого действия появляются пиктограммы, представляющие клас­ сы, которые входят в текущий проект. Щелкая внутри них на значке с изображением стрелки для того или иного типа, можно отображать или скрывать члены этого типа (рис. 2.19). "33 S o lu tio n 'Vs2(J V ie w C lass D ra g ra m I а 5 у»2010Ь* ,Р Г----------1 На заметку! С помощью панели Class Designer (Конструктор классов) можно настраивать парамет­ ры отображения поверхности конструктора желаемым образом. C l a s s D ia q r a m l. c d * V s2 0 1 0 E x a m p le * X И P r o g r a m .e s * P ro gra m Class с В M e th o d s Л * \ * C o n fig u r e C U I M am Ml > Рис. 2.19. Просмотр диаграммы классов Эта утилита работает вместе с двумя другими средствами Visual Studio 2010 — ок­ ном C la ss Details (Детали класса), которое можно открыть путем выбора в меню View (Вид) пункта Other Windows (Другие окна), и панелью C lass Designer Toolbox (Элементы управления конструктора классов), которую можно отобразить выбором в меню View (Вид) пункта Toolbox (Элементы управления). В окне C lass Details не только отображают­ ся детали выбранного в текущий момент элемента в диаграмме, но также можно изме­ нять его существующие члены и вставлять новые на лету, как показано на рис. 2.20. 101 Глава 2. Создание приложений на языке C# Рис. 2.20. Окно C lass D e ta ils Что касается панели Class Designer Toolbox, которую, как уже было сказано, можно активизировать через меню View (Вид), то она позволяет вставлять в проект новые типы (и создавать между ними желаемые отношения) визуальным образом (рис. 2.21). (Следует иметь в виду, что для просмотра этой панели требуется, чтобы окно диаграммы классов было активным.) По мере выполнения этих действий IDE-среда ав­ томатически создает незаметным образом соответствующие новые определения типов на С#. Для примера давайте перетащим из панели C lass D e sig n e r Toolbox в окно C lass D e sig n e r новый элемент C lass (Класс), в отрывшемся окне назначим ему имя Саг, а затем с помощью окна C lass D e ta ils добавим в него общедоступное поле типа string по имени PetName (рис. 2.22). Взглянув на С#-определение класса Саг после этого, мож­ но увидеть, что оно было соответствующим образом обновле­ но (добавленные комментарии не считаются): T o o lb o x Class D e s ig n e r * P o in te r Class E Enum In te rfa c e • A b s tra c t Class си S tru ct о D e le g a te 4- In h e rita n c e A s s o c ia tio n Com m ent G en eral Рис. 2.21. Панель Class D esigner Toolbox public class Car / / И с п о л ь з о в а т ь о б щ ед о сту п н ы е дан н ы е обы чно //н е р е к о м е н д у е т с я , но з д е с ь э т о уп р о щ ает пр и м ер . public string petName; Теперь давайте активизируем утилиту Class Designer еще раз и перетащим на по­ верхность конструктора новый элемент типа C lass, присвоив ему имя SportsCar. Затем выберем в C lass D e s ig n e r Toolbox пиктограмму In h e rita n c e (Наследование) и щелкнем в верхней части пиктограммы SportsCar. Далее, не отпуская левую кнопку мыши, перета­ щим курсор мыши на поверхность пиктограммы класса Саги отпустим ее. Правильное выполнение всех перечисленных выше действий приведет к тому, что класс SportsCar станет наследоваться от класса Саг, как показано на рис. 2.23. ’ Class Details - C a r j V * Nam e ЛУ a ^ Type M o d ifie r S u m m a ry ? x H id e P r o p e r tie s " 5 * - a d d p rope rty * Fields j p e tN a m e * ♦ I a I dd t H H strm q p u b lic Id E v e n ts J -------- Рис. 2.22. Добавление поля с помощью окна C lass D e ta ils ш я ш ш и ж * \ 102 Часть I. Общие сведения о языке C# и платформе .NET КЯ 1Я 1 C la s s D ia q ra m l. cd* X O b je c t Brow ser Vs201OExample* Prog a m .e s " * * P rogram i Class *3 M e th o d s - Fields C o n fig u re C U I l " p e tN a r^ e ' M a in ___________ J gF ) Cm Class 1 L T __ ____ i ___;_____ S portsCar I Class •j -* Car < Г ~ if- ■V ‘ П1^ . . -1 Рис. 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 Program s1^ Microsoft Глава 2. Создание приложений на языке C# 103 Visual S tu d io 2 0 1 0 c=>Visual S tu d io T o o ls '^ Manage Help (Все программы1 ^ Microsoft Visual Studio 2010 ^Утилиты Visual Studio ^Управление настройками справочной системы). Затем можно приступать к добавлению интересующей справочной документации, как показано на рис. 2.24 (если на диске хватает места, имеет смысл добавить всю возмож­ ную документацию). Рис. 2.24. В окне Help Library Manager (Управление библиотекой справочной документации) можно загрузить локальную копию документации .NET Framework 4.0 SDK На заметку! Начиная с версии .NET Framework 4.0, просмотр справочной системы осуществля­ ется через текущий веб-браузер, даже в случае локальной установки документации .NET Framework 4.0 SDK. После локальной установки справочной системы простейшим способом для взаимо­ действия с ней является выделение интересующего ключевого слова С#, имени типа или имени члена в окне представления кода внутри Visual Studio 2010 и нажатие кла­ виши <F1>. Это приводит к открытию окна с документацией, касающейся конкретного выбранного элемента. Например, если выделено ключевое слово s t r i n g в определении класса Саг, после нажатия клавиши <F1> появится страница со справочной информа­ цией об этом ключевом слове. Еще одним полезным компонентом справочной системы является доступное для ре­ дактирования поле Search (Искать), которое отображается в левой верхней части эк­ рана. В этом поле можно вводить имя любого пространства имен, типа или члена и тем самым сразу же переходить в соответствующее место в документации. При попыт­ ке найти подобным образом пространство имен System . R e f l e c t i o n , например, можно будет узнать о деталях этого пространства имен, изучить содержащиеся внутри него типы, просмотреть примеры кода с ним и т.д. (рис. 2.25). В каждом узле внутри дерева описаны типы, которые содержатся в данном простран­ стве имен, их члены и параметры этих членов. Более того, при просмотре страницы справки по тому или иному типу всегда сообщается имя сборки и пространства имен, в котором содержится запрашиваемый тип (соответствующая информация отображается в верхней части данной страницы). В остальной части настоящей книги ожидается, что читатель будет заглядывать в эту очень важную справочную систему и изучать допол­ нительные детали рассматриваемых сущностей. 104 Часть I. Общие сведения о языке C# и платформе .NET System.Reflection Namespace Л V isual S tu d io library Но it * VisualywlvOx'OlO NET Frvmewoif 4 NET FrameworkClass library System. Reflection Namespace AmfcnguousMatchExcepticin I lass Assembly Class Assembly AJqor. I hoTjdAttnbine C lass A sse m b le omparw Allnoute Class AssemMyCoofiguratawiAttnbsJfe Clot1 Assem blyCopyghtAH rAiute Class AsseinMyCuHureAttnbute Class AssemblylMauNAliasAtlribof e C lass Ass errbtyD cla ySrg n Attn b ut e Class AssemblyDescriplioiiAttribute Class Avsembt/filrVcrsicnAttnbute Class Assemb4*n*<l'Attiibijte Class Assembly Inf ormaticnalC ervionAtt rib j AssembMeyFileAttribute Class Aasembl,Kc,N.iroeAttr«buff Class Assembly Name Class Aii*m blyN ainaTia^s Enumeration Send Feedback The Syste m .R e fle ctio n namespace contains types that retrieve information about assemblies, modules members parameters, and other entities m managed code by examining their metadata. These types also can be used to manipulate instances erf loaded types, for example to hook up events or to invoke methods. To йупаггжаНу create types, use the System Reflectrar.Emi’. namespace Classes C la n D esc rip tio n AmbwuousMalchtxcepticn The exception that is thrown when binding to a member results in more tnan one member matching the binding c-itena. This class cannot be inherited. Represents an assembly which is a reusable, versionable and aelf-descnbing building block of a common language runtime application. Рис. 2.25. Поле Search позволяет быстро находить представляющие интерес элементы На заметку! Не лишним будет напомнить еще раз о важности использования документации .NET Framework 4.0 SDK. Ни одна книга, какой бы объемной она ни была, не способна охватить все аспекты платформы .NET. Поэтому необходимо научиться пользоваться справочной системой; впоследствии это окупится с лихвой. Резюме Итак, нетрудно заметить, что у разработчиков появилась масса новых “игрушек”. Целью настоящей главы было проведение краткого экскурса в основные средства, кото­ рые программист на C# может использовать во время разработки. В начале было рас­ сказано о том, как генерировать сборки .NET с применением бесплатного компилятора C# и редактора “Блокнот” (Notepad). Затем было приведено краткое описание прило­ жения Notepad++ и показано, как его использовать для редактирования и компиляции файлов кода * . cs. И, наконец, здесь были рассмотрены три таких многофункциональных ШЕ-среды, как SharpDevelQp (распространяется с открытым исходным кодом), Microsoft Visual C# 2010 Express и Microsoft Visual Studio 2010 Professional. Функциональные возможно­ сти каждого из этих продуктов в настоящей главе были описаны лишь кратко, поэтому каждый волен заняться более детальным изучением избранной IDE-среды в свободное время (многие дополнительные функциональные возможности Visual Studio 2010 будут рассматриваться далее в настоящей книге). ЧАСТЬ DiaBHbie конструкции программирования на C# В этой ч а сти ... Глава 3. Главные конструкции программирования на С#: часть I Глава 4. Главные конструкции программирования на С#: часть II Глава 5. Определение инкапсулированных типов классов Глава 6. Понятия наследования и полиморфизма Глава 7. Структурированная обработка исключений Глава 8. Время жизни объектов ГЛАВА 3 DiaBHbie конструкции программирования на С#: часть I настоящей главе начинается формальное исследование языка программирования С#. Здесь предлагается краткий обзор отдельных тем, в которых необходимо ра­ зобраться, чтобы успешно изучить платформу .NET Framework. В первую очередь пояс­ няется то, как создавать объект приложения и как должна выглядеть структура метода Main ( ), который является входной точкой в любой исполняемой программе. Далее рас­ сматриваются основные типы данных в C# (и их аналоги в пространстве имен System), в том числе типы классов System. String и System. Text.StringBuilder. После представления деталей основных типов данных в .NET рассказывается о ряде методик, которые можно применять для их преобразования, в том числе об операциях сужения (narrowing) и расширения (widening), а также использовании ключевых слов checked и unchecked. Кроме того, в этой главе описана роль ключевого слова v a r в языке С#, которое позволяет неявно определять локальную переменную. И, наконец, в главе приводится краткий обзор ключевых операций, итерационных конструкций и конструкций приня­ тия решений, применяемых для создания рабочего кода на С#. В Разбор простой программы на C# В языке C# вся логика программы должна содержаться внутри определения какогото типа (в главе 1 уже говорилось, что тип представляет собой общий термин, которым обозначается любой элемент из множества (класс, интерфейс, структура, перечисле­ ние, делегат}). В отличие от многих других языков, в C# не допускается создавать ни глобальных функций, ни глобальных элементов данных. Вместо этого требуется, чтобы все члены данных и методы содержались внутри определения типа. Для начала давай­ те создадим новый проект типа C o n so le A p p lic a tio n (Консольное приложение) по имени SimpleCSharpAppB. Как показано ниже, в исходном коде Program, cs ничего особо при­ мечательного нет: using using using using System; System.Collections.Generic; System.Linq; System.Text namespace SimpleCSharpApp Глава 3. Главные конструкции программирования на С#: часть I 107 class Program { static void Main(string[] args) Имея такой код, далее модифицируем метод Маш () в классе Program, добавив в него следующие операторы: class Program { static void Main(string[] args) { // Вывод простого сообщения пользователю. Console.WriteLine ("***** My First C# App *****"); Console .WnteLine ("Hello World! "); .Console.WriteLine(); // Ожидание нажатия клавиши <Enter> // перед завершением работы. Console.ReadLine(); В результате получилось определение типа класса, поддерживающее единственный метод по имени Main (). По умолчанию классу, в котором определяется метод Main (), в Visual Studio 2010 назначается имя Program; при желании это имя легко изменить. Класс, определяющий метод Main ( ), должен обязательно присутствовать в каждом ис­ полняемом приложении на C# (будь то консольная программа, настольная программа для Windows или служба Windows), поскольку он применяется для обозначения точки входа в приложение. Формально класс, в котором определяется метод М а ш (), называется объектом при­ ложения. Хотя в одном исполняемом приложении допускается иметь более одного тако­ го объекта (это может быть удобно при проведении модульного тестирования), при этом обязательно необходимо информировать компилятор о том, какой из методов Main () должен использоваться в качестве входной точки. Для этого нужно либо указать опцию main в командной строке, либо выбрать соответствующий вариант в раскрывающемся списке на вкладке Application (Приложение) окна редактора свойств проекта в Visual Studio 2010 (см. главу 2). Обратите внимание, что в сигнатуре метода Main () присутствует ключевое слово static, которое более подробно рассматривается в главе 5. Пока достаточно знать, что область действия статических (static) членов охватывает уровень всего класса (а не уровень отдельного объекта) и потому они могут вызываться без предварительного соз­ дания нового экземпляра класса. На заметку! C# является чувствительным к регистру языком программирования. Следовательно, Main и main или Readline и ReadLine будут представлять собой далеко не одно и то же. Поэтому необходимо запомнить, что все ключевые слова в C# вводятся в нижнем регистре (например, public, lock, class, dynamic), а названия пространств имен, типов и членов всегда начинаются (по соглашению) с заглавной буквы, равно как и любые вложенные в них слова (как, например, Console .WriteLine, System.Windows .Forms .MessageBox и System. Data.SqlClient). Как правило, при каждом получении от компилятора ошибки, связанной с "неопределенными символами” , требуется проверить регистр символов. 108 Часть II. Главные конструкции программирования на C# Помимо ключевого слова static, данный метод Main () имеет еще один параметр, который представляет собой массив строк (string [ ] args). Хотя в текущий момент этот массив никак не обрабатывается, в данном параметре в принципе может содер­ жаться любое количество аргументов командной строки (процесс получения доступа к которым будет описан чуть ниже). И, наконец, данный метод М а ш () был сконфигури­ рован с возвращаемым значением void, которое свидетельствует о том, что решено не определять возвращаемое значение явным образом с помощью ключевого слова return перед выходом из области действия данного метода. Логика Program содержится внутри самого метода Main ( ). Здесь используется класс Console из пространства имен System. В число его членов входит статический метод WriteLine (), который позволяет отправлять строку текста и символ возврата карет­ ки на стандартное устройство вывода. Кроме того, здесь вызывается метод Console. ReadLine ( ) , чтобы окно командной строки, запускаемое в IDE-среде Visual Studio 2010, оставалось видимым во время сеанса отладки до тех пор, пока не будет нажата клавиша <E nter>. Варианты метода M a in () По умолчанию в Visual Studio 2010 будет генерироваться метод Маш() с возвра­ щаемым значением void и массивом типов string в качестве единственного входного параметра. Однако такой метод Main () является далеко не единственно возможным вариантом. Вполне допускается создавать собственные варианты входной точки в при­ ложение с помощью любой из приведенных ниже сигнатур (главное, чтобы они содер­ жались внутри определения какого-то класса или структуры на С#): // Возвращаемый тип in t и массив строк в качестве параметра. static in t Main(s t r in g [] args) { // Должен обязательно возвращать значение перед выходом! return 0; } //Ни возвращаемого типа, ни параметров. static void Main() // Возвращаемый тип in t , но никаких параметров. static in t Main () { // Должен обязательно возвращать значение перед выходом! return 0; } На заметку! Метод Main () может также определяться как общедоступный (public), а не приват­ ный (private), каковым он считается, если не указан конкретный модификатор доступа. В Visual Studio 2010 метод Main () автоматически определяется как неявно приватный. Это гарантирует отсутствие у приложений возможности напрямую обращаться к точке входа друг друга. Очевидно, что выбор способа создания метода Main () зависит от двух моментов. Во-первых, он зависит от того, нужно ли, чтобы системе после окончания выполнения метода Main () и завершения работы программы возвращалось какое-то значение; в этом случае необходимо возвращать тип данных int, а не void. Во-вторых, он зависит от необходимости обработки предоставляемых пользователем параметров командной строки; в этом случае они должны сохраняться в массиве strings. Рассмотрим все воз­ можные варианты более подробно. Глава 3. Главные конструкции программирования на С#: часть I 109 Спецификация кода ошибки в приложении Хотя в большинстве случаев методы Main () возвращают void в качестве возвра­ щаемого значения, способность возвращать int из Main () позволяет согласовать C# с другими языками на базе С. По соглашению, возврат значения 0 свидетельствует о том, что выполнение программы прошло успешно, а любого другого значения (напри­ мер, -1) — что в ходе выполнения программы произошла ошибка (следует иметь в виду, что значение 0 возвращается автоматически даже в случае, если метод Main () возвра­ щает void). В операционной системе Windows возвращаемое приложением значение сохраня­ ется в переменной среды по имени %ERRORLEVEL%. В случае создания приложения, в коде которого предусмотрен запуск какого-то исполняемого модуля (см. главу 16), по­ лучить значение %ERRORLEVEL% можно с помощью статического свойства System. Diagnostics.Process.ExitCode. Из-за того, что возвращаемое приложением значение в момент завершения его ра­ боты передается системе, приложение не может получать и отображать свой конечный код ошибки во время выполнения. Однако просмотреть код ошибки по завершении вы­ полнения программы все-таки можно. Чтобы увидеть, как это делается, модифицируем метод Main () следующим образом: // Обратите внимание, что теперь возвращается in t, а не void. static int Main(string [] args) { // Вывод сообщения и ожидание нажатия клавиши <Enter>. Console.WriteLine (''***** Му First C# App *****"); Console.WriteLine("Hello World!"); Console.WriteLine(); Console.ReadLine(); // Возврат произвольного кода ошибки. return -1; } Теперь давайте обеспечим перехват возвращаемого Main () значения с помощью ко­ мандного файла. Для этого перейдите в окне проводника Windows в каталог, где хра­ нится скомпилированное приложение (например, в C:\SimpleCSharpApp\bin\Debug), создайте в нем новый текстовый файл (по имени SimpleCSharpApp.bat) и добавьте в него следующие инструкции: 0echo off rem Командный файл для приложения SimpleCSharpApp.exe, геш перехватывающий возвращаемое им значение. SimpleCSharpApp 0if "%ERRORLEVEL%" == "0" goto success :fail echo This application has failed1 rem Выполнение этого приложения не удалось ! 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. Главные конструкции программирования на C# Теперь откройте окно командной строки в Visual Studio 2010 и перейдите в ката­ лог, где находится исполняемый файл приложения и созданный только что файл * .bat. Запустите командный файл, набрав его имя и нажав <Enter>. После этого на экране должен появиться вывод, подобный показанному на рис. 3.1, поскольку метод Main () сейчас возвращает значение -1. Если бы он возвращал значение 0, в окне консоли поя­ вилось бы сообщение This application has succeeded!. Рис. 3.1. Перехват значения, возвращаемого приложением, с помощью командного файла В большинстве приложений на C# (если не во всех) в качестве возвращаемого М а ш () значения будет использоваться void, которое, как уже известно, неявно подразумевает возврат кода ошибки 0. Из-за этого все демонстрируемые далее в настоящей книге ме­ тоды Main () будут возвращать именно void (никакие командные файлы для перехвата кода возврата в последующих проектах не используются). Обработка аргументов командной строки Теперь, когда стало более понятно, что собой представляет возвращаемое значение метода Main ( ) , давайте посмотрим на входной массив данных string. Предположим, что теперь требуется обновить приложение так, чтобы оно могло обрабатывать любые возможные параметры командной строки. Одним из возможных способов для обеспе­ чения такого поведения является использование поддерживаемого в C# цикла for (все итерационные конструкции C# более подробно рассматриваются далее в главе): static int Main(string[] args) { // Обработка любых входящих аргументов. for(int i = 0; i < args.Length; i++) Console .WnteLine ("Arg: {0}", argsfi]); Console.ReadLine() ; return -1; } Здесь с помощью свойства Length типа System.Array производится проверка, со­ держатся ли в массиве string какие-то элементы. Как будет показано в главе 4, все массивы в C# на самом деле относятся к классу System. Array и потому имеют общий набор членов. При проходе по массиву значение каждого элемента выводится в окне консоли. Процесс предоставления аргументов в командной строке сравнительно прост и показан на рис. 3.2. В качестве альтернативы, вместо стандартного цикла for для прохода по входному массиву строк можно также использовать ключевое слово fо reach. Глава 3. Главные конструкции программирования на С#: часть I 111 Рис. 3.2. Предоставление аргументов в командной строке Ниже приведен соответствующий пример: // Обратите внимание, что в случае использования // цикла foreach проверять размер массива //не требуется. static int Main(string[] args) { // Обработка любых входящих аргументов с помощью foreach. foreach (string arg in args) Console .WnteLine ("Arg: {0}", arg); Console.ReadLine(); return -1; И, наконец, получать доступ к аргументам командной строки можно с помощью ста­ тического метода GetCommandLineArgs () , принадлежащего типу System .Environm ent. Возвращаемым значением этого метода является массив строк (s tr in g ). В первом эле­ менте этого массива содержится имя самого приложения, а во всех остальных — отдель­ ные аргументы командной строки. Важно обратить внимание, что при этом определять метод Main () так, чтобы он принимал в качестве входного параметра массив s t r in g , больше не требуется, хотя никакого вреда от этого не будет. public static int Main(string [] args) { // Получение аргументов с использованием System.Environment. string[] theArgs = Environment.GetCommandLineArgs(); foreach(string arg in theArgs) Console .WnteLine ("Arg: {0}", arg); Console.ReadLine(); return -1; } Разумеется, то, на какие аргументы командной строки должна реагировать програм­ ма (если вообще должна), и в каком формате они должны предоставляться (например, с префиксом - или /), можно выбирать самостоятельно. В приведенном выше коде про­ сто передавался набор опций, которые выводились прямо в командной строке. Но что если бы создавалась новая видеоигра, и приложение программировалось на обработку опции, скажем, -godmode? Например, при запуске пользователем приложения с этим флагом можно было бы предпринимать в его отношении соответствующие меры. Указание аргументов командной строки в Visual Studio 2010 В реальном мире конечный пользователь имеет возможность предоставлять аргу­ менты командной строки при запуске программы. Для целей тестирования приложения в процессе разработки также может потребоваться указывать возможные флаги команд­ 112 Часть II. Главные конструкции программирования на C# ной строки. В Visual Studio 2010 для этого необходимо дважды щелкнуть на пиктограм­ ме P ro p e rtie s (Свойства) в окне Solution Explorer, выбрать в левой части окна вкладку D e b u g (Отладка) и указать в текстовом поле C o m m a n d line a rg u m e n ts (Аргументы ко­ мандной строки) желаемые значения (рис. 3.3). Рис. 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.GetLogicalDnves () ) Console.WriteLine("Drive: {0}", drive); // диски Console .WnteLine ("OS : {0 }" , Environment.OSVersion) ; // OC Console.WriteLine("Number of processors: {0}", Environment.ProcessorCount); // количество процессоров Console.WriteLine(".NET Version: {0}", Environment.Version); // версия .NET } На рис. 3.4 показано возможное тестовое выполнение данного метода. Если на вкладке Debug в Visual Studio 2010 аргументы командной строки не указаны, в окне консоли они, соответственно, тоже появляться не будут. g С Wmdo*» ;mrie»e ***** Му First C# App ***** Hello World! Arg: -godmode Arg: -argl Arg: -arg2 Drive: C:\ Drive: D:\ Drive: E:\ Drive: F:\ Drive: H:\ jO S : Microsoft Windows NT 6.1.7600.0 Number of processors: 4 .NFT Version: 4.0.20506.1 Рис. 3.4. Отображение переменных среды Помимо показанных в предыдущем примере, у класса Environment имеются и дру­ гие члены. В табл. 3.1 перечислены некоторые наиболее интересные из них; полный список со всеми деталями можно найти в документации .NET Framework 4.0 SDK. Таблица 3.1. Некоторые свойства S y s te m . E n v ir o n m e n t Свойство Описание ExitCode Позволяет получить или установить код возврата приложения MachineName Позволяет получить имя текущей машины NewLine Позволяет получить символ новой строки, поддерживаемый в текущей среде StackTrace Позволяет получить текущие данные трассировки стека для приложения SystemDirectory Возвращает полный путь к системному каталогу UserName Возвращает имя пользователя, который запустил данное приложение Исходный код. Проект SimpleCSharpApp доступен в подкаталоге Chapter 3. Класс System. Console Почти во всех примерах приложений, создаваемых в начальных главах книги, будет интенсивно использоваться класс System.Console. И хотя в действительности консоль­ ный пользовательский интерфейс (Console User Interface — CUI) не является настолько 114 Часть II. Главные конструкции программирования на C# привлекательным, как графический (Graphical User Interface — GUI) или веб-интерфейс, ограничение первых примеров консольными программами, позволяет уделить больше внимания синтаксису C# и ключевым характеристикам платформы .NET, а не сложным деталям построения графических пользовательских интерфейсов или веб-сайтов. Класс Console инкапсулирует в себе возможности, позволяющие манипулировать вводом, выводом и потоками ошибок в консольных приложениях. В табл. 3.2 перечис­ лены некоторые наиболее интересные его члены. Таблица 3.2. Некоторые члены класса S y s te m .C o n s o le Член Описание Beep () Этот метод вынуждает консоль подавать звуковой сигнал определен­ ной частоты и длительности BackgroundColor ForegroundColor Эти свойства позволяют задавать цвет изображения и фона для теку­ щего вывода. В качестве значения им может присваиваться любой из членов перечисления ConsoleColor BufferHeight BufferWidth Эти свойства отвечают за высоту и ширину буферной области консоли Title Это свойство позволяет устанавливать заголовок для текущей консоли WindowHeight WindowWidth WindowTop WindowLeft Эти свойства позволяют управлять размерами консоли по отношению к установленному буферу Clear() Этот метод позволяет очищать установленный буфер и область изо­ бражения консоли Базовый ввод-вывод с помощью класса C o n s o l e Помимо членов, перечисленных в табл. 3.2, в классе Console имеются методы, ко­ торые позволяют захватывать ввод и вывод; все они являются статическими и пото­ му вызываются за счет добавления к имени метода в качестве префикса имени самого класса (Console). К их числу относится уже показанный ранее метод WriteLine () , ко­ торый позволяет вставлять в поток вывода строку текста (вместе с символом возврата каретки); метод Write (), который позволяет вставлять в поток вывода текст без симво­ ла возврата каретки; метод ReadLine ( ) , который позволяет получать информацию из потока ввода вплоть до нажатия клавиши <Enter>; и метод Read ( ) , который позволяет захватывать из потока ввода одиночный символ. Рассмотрим пример выполнения базовых операций ввода-вывода с использовани­ ем класса Console, для чего создадим новый проект типа C o n so le A p p lic a tio n по имени BasicConsolelO и модифицируем метод Main () внутри него так, чтобы в нем вызывал­ ся вспомогательный метод GetUserData (): class Program { static void Main(string[] args) } Console.WriteLine ("***** Basic Console I/O *****"); GetUserData (); Console.ReadLine(); Глава 3. Главные конструкции программирования на С#: часть I 1 15 Теперь реализуем этот метод в классе 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.WnteLine ("Hello {0}! You are {1} years old.", userName, userAge); // Восстановление предыдущего цвета. Console.ForegroundColor = prevColor; После запуска этого приложения входные данные будут выводиться в окне консоли (с использованием указанного специального цвета). Форматирование вывода, отображаемого в окне консоли В ходе первых нескольких глав можно было заметить, что внутри различных строко­ вых литералов часто встречались обозначения вроде { 0 } и { 1 }. Дело в том, что в .NET для форматирования строк поддерживается стиль, немного напоминающий стиль опе­ ратора printf () в С. Попросту говоря, при определении строкового литерала с сегмен­ тами данных, значения которых остаются неизвестными до этапа времени выполнения, внутри него допускается указывать метку-заполнитель с использованием синтаксиса в виде фигурных скобок. Во время выполнения на месте каждой такой метки-заполните­ ля подставляется передаваемое в Console.WriteLineO значение (или значения). В качестве первого параметра методу W r it e L in e O всегда передается строковый литерал, в котором могут содержаться метки-заполнители вида { 0 } , { 1 } , { 2 } и т.д. Следует запомнить, что отсчет в окружаемых фигурными скобками метках-заполните­ лях всегда начинается с нуля. Остальными передаваемыми W r it e L in e O параметрами являются просто значения, которые должны подставляться на месте соответствующих меток -заполнителей. На заметку! Если количество пронумерованных уникальным образом заполнителей превышает чис­ ло необходимых для их заполнения аргументов, во время выполнения будет генерироваться исключение, связанное с форматом. Метка-заполнитель может повторяться в пределах одной и той же строки. Например, для создания строки " 9, Number 9, Number 9 " можно было бы написать такой код: // Вывод строки "9, Number 9, Number 9" Console.WriteLine("{0}, Number {0}, Number {0}", 9); Также следует знать о том, что каждый заполнитель допускается размещать в любом месте внутри строкового литерала, и вовсе не обязательно, чтобы следующий после него заполнитель имел более высокий номер. Например: // Отображает: 20, 10, 30 Console.WriteLine ("{1}, {0}, {2}", 10, 20, 30); 116 Часть II. Главные конструкции программирования на C# Форматирование числовых данных Если требуется использовать более сложное форматирование для числовых данных, в каждый заполнитель можно включить различные символы форматирования, наибо­ лее полезные из которых перечислены в табл. 3.3. Таблица 3.3. Символы для форматирования числовых данных в .NET Символ форматирования Описание С или с Применяется для форматирования денежных значений. По умолчанию этот флаг идет перед символом локальной культуры (например, знаком доллара [ $ ] , если речь идет о культуре US English) D или d Применяется для форматирования десятичных чисел. Этот флаг может так­ же задавать минимальное количество цифр для представления значения Е или е Применяется для экспоненциального представления. Регистр этого флага указывает, в каком регистре должна представляться экспоненциальная кон­ станта — в верхнем (Е) или в нижнем (е) F или f Применяется для представления числовых данных в формате с фиксирован­ ной точкой. Этот флаг может также задавать минимальное количество цифр для представления значения G или g Расшифровывается как g e n e ra l (общий (формат)). Этот символ может при­ меняться для представления числа в фиксированном или экспоненциальном формате N или п Применяется для базового числового форматирования (с запятыми) X или X Применяется для представления числовых данных в шестнадцатеричном формате. В случае использования символа X в верхнем регистре, в шестна­ дцатеричном представлении будут содержаться символы верхнего регистра Все эти символы форматирования присоединяются к определенной метке-заполни­ телю в виде суффикса после двоеточия (например, { 0 : С}, {1 : d }, {2 : X}.). Для примера изменим метод Main () так, чтобы в нем вызывалась новая вспомогательная функция по имени FormatNumericalData (), и затем реализуем его в классе Program для обеспе­ чения форматирования значения с фиксированной точкой различными способами. // Использование нескольких дескрипторов формата. static void FormatNumericalData() Console .WnteLine Console .WnteLine Console.WriteLine Console.WriteLine Console.WriteLine ("The value 99999 in various formats: ) ; ("c format: {0 :c }" , 99999); ("d9 format: {0:d9}", 99999) ("f3 format: {0:f3}", 99999) ("n format: {0:n }", 99999); } // Обратите внимание, что использование X и х // определяет, будут символы отображаться / / в верхнем или нижнем регистре. Console.WriteLine ("Е format: {0:Е}", 99999); Console.WriteLine ("е format: {0:е}", 99999); Console.WriteLine ("X format: {0:X}", 99999); Console.WriteLine ("x format: {0:x}", 99999); } На рис. 3.5 показан вывод этого приложения. Глава 3. Главные конструкции программирования на С#: часть I 117 Помимо символов, позволяющих управлять 1 С A in d ; * ; ’ форматированием числовых данных, в .NET ***** Basic Console I/O Please enter your name: Saku поддерживается несколько лексем, которые Please enter your age: 1 , Hello Saku! You are 1 years old. можно использовать в строковых литералах и The value 99999 in various formats: которые позволяют управлять позиционирова­ c format: 199,999.00 d9 format: 000099999 нием содержимого и добавлением в него про­ fB format: 99999.000 белов. Более того, лексемы, применяемые для n format: 99,999.00 E format: 9.999900E+004 числовых данных, допускается применять и e format: 9.999900e+004 1869F для форматирования других типов данных (на­ | Xx format: format: 1869f пример, для перечислений или типа DateTime). Press any key to continue . . . _ Вдобавок можно создавать специальный класс (или структуру) и определять в нем специаль­ J ную схему форматирования за счет реализации Рис. 3.5. Базовый консольный ввод-вывод интерфейса ICustomFormatter. (с форматированием строк .NET) По ходу настоящей книги будут встречаться и другие примеры форматирования; тем, кто всерьез заинтересовался темой форма­ тирования строк в .NET, следует обязательно изучить посвященный форматированию строк раздел в документации .NET Framework 4.0. Исходный код. Проект BasicConsolelO доступен в подкаталоге Chapter 3. Форматирование числовых данных в приложениях, отличных от консольных Напоследок хотелось бы отметить, что символы форматирования строка .NET могут ис­ пользоваться не только в консольных приложениях. Тот же синтаксис форматирования можно применять и в вызове статического метода string . Format ( ) . Это может быть удобно при генерации во время выполнения текстовых данных, которые должны исполь­ зоваться в приложении любого типа (например, в настольном приложении с графиче­ ским пользовательским интерфейсом, в веб-приложении ASP.NET или веб-службах XML). Для примера предположим, что требуется создать графическое настольное прило­ жение и применить форматирование к строке, отображаемой в окне сообщения внутри него: static void DisplayMessage () { // Использование string.Form at() для форматирования строкового литерала. string userMessage = string.Format("100000 in hex is {0:x}", 100000); // Для компиляции этой строки кода требуется // ссылка на System.Windows.Forms.dll1 System.Windows.Forms.MessageBox.Show(userMessage); } Обратите внимание, что string.Format () возвращает новый объект string, кото­ рый форматируется в соответствии с предоставляемыми флагами. После этого тексто­ вые данные могут использоваться любым желаемым образом. Системные типы данных и их сокращенное обозначение в C# Как и в любом языке программирования, в C# поставляется собственный набор ос­ новных типов данных, которые должны применяться для представления локальных переменных, переменных экземпляра, возвращаемых значений и входных параметров. 118 Часть II. Главные конструкции программирования на C# Однако в отличие от других языков программирования, в C# эти ключевые слова пред­ ставляют собой нечто большее, чем просто распознаваемые компилятором лексемы. Они, по сути, представляют собой сокращенные варианты обозначения полноценных типов из пространства имен System. В табл. 3.4 перечислены эти системные типы дан­ ных вместе с охватываемыми ими диапазонами значений, соответствующими ключе­ выми словами на C# и сведениями о том, отвечают ли они требованиям общеязыковой спецификации CLS (Common Language Specification). На заметку! Как рассказывалось в главе 1, с koaom .NET, отвечающим требованиям CLS, может ра­ ботать любой управляемый язык программирования. В случае включения в программы данных, которые не соответствуют требованиям CSL, другие языки могут не иметь возможности ис­ пользовать их. Таблица 3.4. Внутренние типы данных C# Сокращенный Отвечает вариант ли требова­ Системный тип обозначения в C# ниям CLS bool Да System.Boolean sbyte Нет System.SByte Диапазон значений Описание true или f^lse Представляет признак истинно­ сти или ложности ДО о т -128 127 8-битное число со знаком byte Да System.Byte от 0 до 255 8-битное число без знака short Да System.Intl6 от -3 2 768 до 32 767 16-битное число со знаком ushort Нет System.UIntl6 от 0 до 65 535 16-битное число без знака int Да System.Int32 от -2 147 483 648 до 2 147 483 647 32-битное число со знаком uint Нет System.UInt32 отО до 4 294 967 295 32-битное число без знака long Да System.Int64 от -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 64-битное число со знаком ulong Нет System.UInt64 от 0 до 18 446 744073 709 551 615 64-битное число без знака char Да System.Char от U+0000 до U+ffff Одиночный 16-битный символ Unicode float Да System.Single о т +1,5x10 -45 до 3,4x1038 32-битное число с плавающей точкой double Да System.Double от 5,0x10-324 до 1,7x10308 64-битное число с плавающей точкой decimal Да System.Decimal от ±1,0x10е-28 до ±7,9x1028 96-битное число со знаком Глава 3. Главные конструкции программирования на С#: часть I 119 Окончание табл. 3.4 Сокращенный Отвечает вариант ли требова- Системный тип обозначения в C# . ниям CLS Диапазон значений Описание string Да System.String Ограничивается объе­ мом системной памяти Представляет ряд символов в фор­ мате Unicode object Да System.Obj ect Позволяет сохранять любой тип в объектной переменной Служит базовым классом для всех типов в мире .NET На заметку! По умолчанию число с плавающей точкой трактуется как относящееся к типу double. Из-за этого для объявления переменной типа float сразу после числового значения должен указываться суффикс f или F (например, 5.3F). Неформатированные целые числа по умолча­ нию трактуются как относящиеся к типу данных int. Поэтому для типа данных long необхо­ димо использовать суффикс 1 или L (например, 4L). Каждый из числовых типов, такой как short или int, отображается на соответст­ вующую структуру в пространстве имен System. Структуры, попросту говоря, пред­ ставляют собой типы значений, которые размещаются в стеке. Типы string и object, с другой стороны, являются ссылочными типами, а это значит, что данные, сохраняе­ мые в переменных такого типа, размещаются в управляемой куче. Более подробно о типах значений и ссылочных типах будет рассказываться в главе 4. Пока что важно просто понять то, что типы-значения могут размещаться в памяти очень быстро и об­ ладают фиксированным и предсказуемым временем жизни. , Объявление и инициализация переменных При объявлении локальной переменой (например, переменной, действующей в пре­ делах какого-то члена) должен быть указан тип данных, за которым следует имя самой переменной. Чтобы посмотреть, как это выглядит, давайте создадим новый проект типа C onsole A p p lic a tio n по имени BasicDataTypes и модифицируем класс Program так, что­ бы в нем использовался следующий вспомогательный метод, вызываемый в Main () : static void LocalVarDeclarations () { Console.WriteLine ("=> Data Declarations:"); // Объявления данных // Локальные переменные объявляются следующим образом: // типДанных имяПеременной; int mylnt; string myStnng; Console.WriteLine() ; ) Следует иметь в виду, что в случае использования локальной переменной до при­ сваивания ей начального значения компилятор сообщит об ошибке. Поэтому рекомен­ дуется всегда присваивать начальные значения локальным элементам данных во время их объявления. Делать это можно как в одной строке, так и в двух, разнося объявление и присваивание на два отдельных оператора кода. static void LocalVarDeclarations() { Console.WriteLine("=> Data Declarations:"); // Объявления данных 120 Часть II. Главные конструкции программирования на C# // Локальные переменные объявляются и инициализируются следующим образом: // тилДанных имяПеременной = начальноеЗначение; int mylnt = 0; // Объявлять локальные переменные и присваивать им начальные // значения можно также в двух отдельных строках. string myString; my S t n n g = "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 в C# является сокращенным вариантом обозначе­ ния такой структуры из пространства имен 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 • значение f a l s e для переменных типа b o o l; • значение 0 для переменных числовых типов (или 0. О для типов с плавающей точкой); • одиночный пустой символ для переменных типа s t r in g ; • значение 0 для переменных типа B ig ln t e g e r ; • значение 1/1/0001 12 : 00 : 00 AM для переменных типа DateTime; • значение n u ll для переменных типа объектных ссылок (включая s t r in g ). На заметку! Упомянутый в предыдущем списке тип данных Biglnteger является нововведением в .NET 4.0 и будет более подробно рассматриваться далее в главе. Хотя код на C# в случае использования ключевого слова 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 DateTimeO; // Установка в 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.Ob ject и System.ValueType, сейчас главное уяснить то, что поскольку любое ключевое слово в C# (например, int) представляет собой сокращенный вариант обозначения соответствующего системного типа (в данном случае System. Int32), п р и в ед еты й ниже синтаксис является вполне допустимым. 122 Часть II. Главные конструкции программирования на C# Рис. 3.6. Иерархия классов системных типов Причина в том, что тип System . In t32 (представляемый как in t в С#) в конечном итоге все равно унаследован от класса S y stem .O b ject и, следовательно, в нем может вызываться любой из его общедоступных членов с помощью такой вспомогательной функции. static void ObjectFunctionality() { Console .WnteLine ("=> System. Object Functionality:"); // Функциональные возможности System.Object // Ключевое слово in t в C# в действительности представляет // собой сокращенный вариант обозначения типа System.Int32, // который наследует от System.Object следующие члены: Console.WriteLine("12.GetHashCode() = {0}", 12.GetHashCode()); Console.WnteLine ("12 .Equals (23) = {0}", 12.Equals(23)) ; Console .WriteLine (" 12 .ToStnng () = {0}", 12 .ToString () ); Console.WriteLine("12.GetType() = {0}", 12.GetType()); Console.WriteLine(); Глава 3. Главные конструкции программирования на С#: часть I 123 На рис. 3.7 показано, как будет выглядеть вывод в случае вызова данного метода в Main () . | С M ncb*i'jyitem 32\cm d,exe ***** Fun with Basic Data Types ***** !=> System.Object Functionality: 12.GetHashCodeO = 12 12.Equals(23) = False 12 .ToStringO = 12 L2.GetType() = System.Int32 Press any key to continue . . . Рис. 3.7. Все типы (даже числовые) расширяют класс System.Object Члены числовых типов данных Продолжая обсуждение встроенных типов данных в С#, нельзя не упомянуть о том, что числовые типы в .NET поддерживают свойства MaxValue и MinValue, которые по­ зволяют получать информацию о диапазоне значений, хранящихся в данном типе. Помимо свойств MinValue/MaxValue каждый числовой тип может иметь и другие по­ лезные члены. Например, тип System.Double позволяет получать значения эпсилон (бесконечно малое) и бесконечность (представляющие интерес для тех, кто занимается решением математических задач). Ниже для иллюстрации приведена соответствующая вспомогательная функция. static void DataTypeFunctionality () { Console .WnteLine ("=> Data type Functionality:"); // Функциональные возможности типов данных // Максимальное значение типа int Console .WnteLine ("Max of int: {0}", int .MaxValue) ; // Минимальное значение типа int Console .WnteLine ("Min of int: {0}", int.MinValue); // Максимальное значение типа double Console.WriteLine("Max of double: {0}", double.MaxValue); // Минимальное значение типа double Console.WriteLine("Min of double: {0}", double.MinValue) ; // Значение эпсилон типа double Console.WriteLine("double.Epsilon: {0}", double.Epsilon); // Значение плюс бесконечность типа double Console.WriteLine("double.Positivelnfinity: {0}", double.Positivelnfinity) ; // Значение минус бесконечность типа double Console.WriteLine("double.NegativeInfinity: {0}", double.Negativelnfinity); Console.WriteLine(); } Члены S y s t e m . B o o l e a n Теперь рассмотрим тип данных System.Boolean. Единственными значениями, ко­ торые могут присваиваться типу bool в С#, являются true и false. Нетрудно догадать­ ся, что свойства MinValue и MaxValue в нем не поддерживаются, но зато поддержива­ ются такие свойства, как TrueString и FalseString (которые выдают, соответственно, 124 Часть II. Главные конструкции программирования на C# строку "True" и "False"). Чтобы стало понятнее, давайте доб^авим во вспомогательный метод DataTypeFunctionality () следующие операторы кода: Console.WnteLine ("bool.Falsest ring: {0 }" , bool.Falsest ring) ; Console .WnteLine ("bool.T r u e S t n n g : {0}"f bool.TrueStnng) ; На рис. 3.8 показано, как будет вы глядеть вывод после вызова DataType Functionality () BMain(). Рис. 3.8. Демонстрация избранных функциональных возможностей различных типов данных Члены S y s t e m . C h a r Текстовые данные в C# представляются с помощью ключевых слов string и char, которые являются сокращенными вариантами обозначения типов System. String и System.Char (оба они основаны на кодировке Unicode). Как известно, string позволя­ ет представлять непрерывный набор символов (например, "Hello"), a char — только конкретный символ в типе string (например, ' Н '). Помимо возможности хранить один элемент символьных данных, тип System. Char обладает массой других функциональных возможностей. В частности, с помощью его статических методов можно определять, является данный символ цифрой, буквой, зна­ ком пунктуации или чем-то еще. Например, рассмотрим приведенный ниже метод. static void CharFunctionality () { Console .WnteLine ("=> char type Functionality:"); // Функциональные возможности типа char char myChar = 'a'; Console .WnteLine ("char .IsDigit (' a ') : {0}", char.IsDigit(myChar)); Console .WnteLine ("char .IsLetter ('a ') : {0}", char.IsLetter(myChar)); Console .WnteLine ("char .IsWhiteSpace (1Hello There 1, 5): {0}", char.IsWhiteSpace("Hello There", 5)); Console .WnteLine ("char .IsWhiteSpace ('Hello There ', 6): {0}", char.IsWhiteSpace("Hello There", 6)); Console .WnteLine ("char.IsPunctuation ('?' ) : {0}", char.IsPunctuation('?')); Console .WnteLine () ; } Как видно в этом фрагменте кода, многие из членов System.Char могут вызывать­ ся двумя способами: с указанием конкретного символа и с указанием целой строки с числовым индексом, ссылающимся на позицию, в которой находится проверяемый символ. Глава 3. Главные конструкции программирования на С#: часть I 125 Синтаксический разбор значений из строковых данных Типы данных .NETT предоставляют возможность генерировать переменную лежаще­ го в их основе типа на основе текстового эквивалента (т.е. выполнять синтаксический разбор). Эта возможность чрезвычайно полезна при преобразовании некоторых из пре­ доставляемых пользователем данных (например, значений, выбираемых в раскрываю­ щемся списке внутри графического пользовательского интерфейса). Ниже приведен пример метода ParseFromStrings (), демонстрирующий выполнение синтаксического разбора. static void ParseFromStrings () { Console.WriteLine ("=> Data type parsing:"); // Синтаксический разбор типов данных 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 c = Char.Parse("w") ; Console.WriteLine("Value of c: {0}", c) ; Console.WriteLine(); } Типы S y s t e m . D a t e T i m e и S y s t e m . T im e S p a n В пространстве имен System имеется несколько полезных типов данных, для кото­ рых в C# ключевых слов не предусмотрено. К этим типам относятся структуры DateTime и TimeSpan (а также показанные на рис. 3.6 типы System. Guid и System. Void, изуче­ нием которых можете заняться самостоятельно). В типе DateTime содержатся данные, представляющие конкретное значение даты (месяц, день, год) и времени, которые могут форматироваться различными способами с применением членов, доступных в этом типе. static void UseDatesAndTimes() { Console.WriteLine ("=> Dates and Times:"); // Отображение значений даты и времени // Этот конструктор принимает в качестве // аргументов сведения о годе, месяце и дне. DateTime dt = new DateTime (2010, 10, 17); // Какой это день месяца? Console.WriteLine("The day of {0} is {1}", dt.Date, d t .DayOfWeek); // Сейчас месяц декабрь. dt = dt .AddMonths(2); Console.WriteLine("Daylight savings: {0}", dt.IsDaylightSavingTime()); // Этот конструктор принимает в качестве аргументов // сведения о часах, минутах и секундах. TimeSpan ts = new TimeSpan (4, 30, 0) ; Console.WriteLine(ts) ; // Вычитаем 15 минут из текущего значения TimeSpan //и отображаем результат. Console.WriteLine(ts.Subtract(new TimeSpan (0, 15, 0) )) ; } 126 Часть II. Главные конструкции программирования на C# Пространство имен S y s t e m . N u m e r i c s в .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 выберите в меню P ro je c t (Проект) пункт A dd R e fe ren ce (Добавить ссылку). 2. В открывшемся после этого окне перейдите на вкладку .NET. 3. Найдите и выделите сборку S y s t e m .Numerics в списке представленных библиотек. 4. Щелкните на кнопке ОК. После этого добавьте следующую директиву в файл, в котором будет использоваться тип данных Biglnteger: // Здесь определен тип B igln teger: using System.Numerics; Теперь можно создать переменную Biglnteger с использованием операции new. Внутри конструктора можно указать числовое значение, в том числе с плавающей точ­ кой. Вспомните, что при определении целочисленный литерал (вроде 500) по умолчанию трактуется исполняющей средой как относящийся к типу int, а литерал с плавающей точкой (такой как 55.333) — как относящийся к типу double. Как же тогда установить для Biglnteger большое значение, не переполняя типы данных, которые используются по умолчанию для неформатированных числовых значений? Простейший подход состоит в определении большого числового значения в виде текстового литерала, который затем может быть преобразован в переменную типа Biglnteger с помощью статического метода Parse ( ) . При необходимости можно так­ же передать байтовый массив непосредственно конструктору класса Biglnteger. На заметку! После присваивания значения переменной Biglnteger изменять ее больше не раз­ решается, поскольку содержащиеся в ней данные являются неизменяемыми. Тем не менее, в классе Biglnteger предусмотрено несколько членов, которые возвращают новые объекты Biglnteger на основе модификаций над данными (вроде статического метода Multiply (), который будет использоваться в следующем примере кода). В любом случае после определения переменной Biglnteger обнаруживается, что в этом классе доступны члены, очень похожие на членов других внутренних типов дан­ ных в C# (например, float и int). Помимо этого в классе Biglnteger еще есть несколь­ ко статических членов, которые позволяют применять базовые математические выра­ жения (такие как сложение и умножение) к переменным Biglnteger. Глава 3. Главные конструкции программирования на С#: часть I 127 Ниже приведен пример работы с классом Biglnteger. static void UseBiglnteger () Console .WnteLine ("=> Use Biglnteger:"); // Использование Biglnteger Biglnteger biggy = Biglnteger.Parse("9999999999999999999999999999999999999999999999"); Console .WnteLine ("Value of biggy is {0}", biggy); // Значение переменной biggy Console .WnteLine (" 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("8888888888888888888888888888888888688888888")) ; Console.WriteLine("Value of reallyBig is {0}", reallyBig); // Значение переменной reallyBig } Важно обратить внимание, что к типу данных Biglnteger применимы внутренние математические операции в С#, такие как +, -, и *. Следовательно, вместо того чтобы вызывать метод Biglnteger .Multiply () для перемножения двух больших чисел, мож­ но использовать такой код: Biglnteger reallyBig2 = biggy * reallyBig; К этому моменту уже должно быть ясно, что ключевые слова, представляющие ба­ зовые типы данных в С#, обладают соответствующим типами в библиотеках базовых классов .NETT, и что каждый из этих типов предоставляет определенные функциональ­ ные возможности. Подробные описания всех членов этих типов данных можно найти в документации .NET Framework 4.0 SDK. Исходный код. Проект BasicDataTypes доступен в подкаталоге Chapter 3. Работа со строковыми данными В System. String предоставляется набор методов для определения длины символь­ ных данных, поиска подстроки в текущей строке, преобразования символов из верхнего регистра в нижний и наоборот, и т.д. В табл. 3.5 перечислены некоторые наиболее ин­ тересные члены этого класса. Таблица 3.5. Избранные члены класса S y ste m . S t r i n g Член Описание Length Свойство, которое возвращает длину текущей строки Compare() Статический метод, который позволяет сравнить две строки Contains () Метод, который позволяет определить, содержится ли в строке определен­ ная подстрока Equals() Метод, который позволяет проверить, содержатся ли в двух строковых объ­ ектах идентичные символьные данные Format() Статический метод, позволяющий сформатировать строку с использованием других элементарных типов данных (например, числовых данных или других строк) и обозначений типа { 0 } , о которых рассказывалось ранее в этой главе 128 Часть II. Главные конструкции программирования на C# Окончание табл. 3.5 Член Описание Insert () Метод, который позволяет вставить строку внутрь другой определенной строки PadLeft() PadRight() Методы, которые позволяют дополнить строку какими-то символами, соответственно, справа или слева Remove() Replace() Методы, которые позволяют получить копию строки с соответствующими изменениями (удалением или заменой символов) Split () Метод, возвращающий массив string с присутствующими в данном экзем­ пляре подстроками внутри, которые отделяются друг от друга элементами из указанного массива char или string Trim() Метод, который позволяет удалять все вхождения определенного набора символов с начала и конца текущей строки ToUpper() ToLower() Методы, которые позволяют создавать копию текущей строки в формате, соответственно, верхнего или нижнего регистра Базовые операции манипулирования строками Работа с членами System. String выглядит так, как и следовало ожидать. Все, что требуется — это просто объявить переменную типа string и воспользоваться предостав­ ляемой этим типом функциональностью через операцию точки. Однако при этом следу­ ет иметь в виду, что некоторые из членов System. String представляют собой статиче­ ские методы и потому должны вызываться на уровне класса (а не объекта). Для примера давайте создадим новый проект типа C o n so le A p p lic a tio n по имени FunWithStrings, до­ бавим в него следующий метод и вызовем его в Маш () : static void BasicStnngFunctionality () { Console.WnteLine ("=> Basic String functionality:"); // Базовые функциональные возможности типа String string firstName = "Freddy"; Console .WnteLine ("Value of firstName: {0}", firstName); // Значение переменной firstName Console.WriteLine("firstName has {0} characters.", firstName.Length); // Длина значения переменной firstname Console .WnteLine ("firstName in uppercase: {0}", {0}", firstName.ToUpper()); // Значение переменной firstName в верхнем регистре Console .WnteLine (" firstName in lowercase: {0}", {0}", firstName .ToLower ()) ; // Значение переменной firstName в нижнем регистре Console .WnteLine (" firstName contains the letter y? : {0}", {0}", // Содержится ли в значении firstName буква у firstName.Contains("у")); Console .WnteLine ("f irstName after replace: {0}", firstName.Peplace ("dy", "") ); // Значение firstName после замены Console .WnteLine () ; } Здесь объяснять особо нечего: в приведенном методе на локальной переменной типа string производится вызов различных членов вроде ToUpper ( ) и Contains () для по­ лучения различных форматов и выполнения различных преобразований. На рис. 3.9 показано, как будет выглядеть вывод. Глава 3. Главные конструкции программирования на С#: часть I 129 |С W<rvck>*j»iyrtem&Z\crr\dе»е ***** Fun with Strings ***** => Basic String functionality: Value of firstName: Freddy firstName has 6 characters, jfirstName in uppercase: FREDDY l firstName in lowercase: freddy (firstName contains the letter y?: True firstName after replace: Fred Рис. 3.9. Базовые операции манипулирования строками Вывод, получаемый в результате вызова метода R e p la c e ( ) , может привести в не­ которое замешательство. Переменная fir s tN a m e на самом деле не изменилась, а вме­ сто этого метод возвращает обратно новую строку в измененном формате. Мы еще вер­ немся к неизменяемой природе строк немного позже, после исследования ряда других моментов. Конкатенация строк Переменные s t r in g могут сцепляться вместе для создания строк большего размера с помощью такой поддерживаемой в C# операции, как +. Как известно, подобный прием называется конкатенацией строк. Для примера рассмотрим следующую вспомогатель­ ную функцию: static void StringConcatenation () Console.WriteLine ("=> String concatenation:"); // Конкатенация строк string si = "Programming the "; string s2 = "PsychoDrill (PTP)"; string s3 = si + s2; Console.WriteLine (s3) ; Console.WriteLine (); } Возможно, будет интересно узнать, что символ + в C# при обработке компилятором приводит к добавлению вызова статического метода S t r i n g . Concat ( ) . После компиля­ ции приведенного выше кода и открытия результирующей сборки в утилите ild a s m . ехе (см. главу 1) можно будет увидеть такой CIL-код, как показан на рис. 3.10. Рис. 3.10. Операция + в C# приводит к добавлению вызова метода String.Concat () По этой причине конкатенацию строк также можно выполнять вызовом метода String.Concat () напрямую (что на самом деле особых преимуществ не дает, а факти­ чески лишь увеличивает количество подлежащих вводу строк кода): 130 Часть II. Главные конструкции программирования на C# static void StringConcatenation () { Console .WnteLine ("=> String concatenation:"); string si = "Programming the "; string s2 = "PsychoDrill (PTP)"; t strin g s3 = S trin g .C o n c a t(s i, s2) ; Console .WnteLine (s3) ; Console.WriteLine(); } Управляющие последовательности символов Как и в других языках на базе С, в C# строковые литералы могут содержать раз­ личные управляющие последовательности символов (escape characters), которые по­ зволяют уточнять то, как символьные данные должны выводиться в выходном потоке. Начинается каждая такая управляющая последовательность с символа обратной косой черты, за которым следует интерпретируемый знак. В табл. 3.6 перечислены некоторые часто применяемые управляющие последовательности. Таблица 3.6. Управляющие последовательности, которые могут применяться в строковых литералах Управляющая последовательность Описание V Вставляет в строковый литерал символ одинарной кавычки V Вставляет в строковый литерал символ двойной кавычки \\ Вставляет в строковый литерал символ обратной косой черты. Может быть полезной при определении путей к файлам и сетевым ресурсам \а Заставляет систему выдавать звуковой сигнал, который в консольных при­ ложениях может служить своего рода звуковой подсказкой пользователю \п Вставляет символ новой строки (на платформах 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 .WnteLine ("C :\\MyApp\\bin\ \Debug\a ") ; // Добавить 4 пустых строки и снова выдать звуковой сигнал. Console.WriteLine("All finished.\n\n\n\a "); Console.WriteLine (); Глава 3. Главные конструкции программирования на С#: часть I 131 Определение дословных строк За счет добавления к строковому литералу префикса @ можно создавать так назы­ ваемые дословные строки (verabtim string). Дословные строки позволяют отключать об­ работку управляющих последовательностей в литералах и выводить объекты string в том виде, в каком они есть. Эта возможность наиболее полезна при работе со строками, представляющими пути к каталогам и сетевым ресурсам. Таким образом, вместо ис­ пользования управляющей последовательности \ \ можно написать следующий код: // Следующая строка будет воспроизводиться дословно, // т .е. с отображением всех управляющих // последовательностей символов. Console.WriteLine(@"С:\MyApp\bin\Debug"); Также важно отметить, что дословные строки могут применяться для сбережения пробелов в строках, разнесенных на несколько строк: // При использовании дословных строк пробелы сохраняются. string myLongStnng = @"This is a very very very long string"; Console.WriteLine(myLongString); С использованием дословных строк можно также напрямую вставлять в литералы символы двойной кавычки, просто дублируя лексему ": Console.WriteLine (@"Cerebus said ""Darrri Pret-ty sun-sets"""); Строки и равенство Как будет подробно объясняться в главе 4, ссылочный тип (reference type) представ­ ляет собой объект, размещаемый в управляемой куче, которая подвергается автоматиче­ скому процессу сборки мусора. По умолчанию при выполнении проверки на предмет ра­ венства ссылочных типов (с помощью таких поддерживаемых в C# операций, как == и ! =) значение true будет возвращаться в том случае, если обе ссылки указывают на один и тот же объект в памяти. Хотя string представляет собой ссылочный тип, операции равенства для него были переопределены так, чтобы давать возможность сравнивать значения объектов string, а не сами объекты в памяти, на которые они ссылаются. static void StnngEquality () { Console.WriteLine("=> // string si = "Hello!"; string s2 = "Yo1"; Console.WriteLine ("si Console.WriteLine ("s2 Console.WriteLine (); String equality:"); Равенство строк = {0}", si); = {0}", s2) ; // Выполнение проверки на предмет равенства данных строк. 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}", "Y o !".Equals(s2)); Console.WriteLine(); } 132 Часть II. Главные конструкции программирования на C# В C# операции равенства предусматривают выполнение в отношении объектов string посимвольной проверки с учетом регистра. Следовательно, строки "Hello! ", "HELLO ! " и "hello ! " не равны между собой. Кроме того, из-за наличия у string связи с System. String, проверку на предмет равенства можно выполнять также с помощью поддерживаемого классом String метода Equals () и других поставляемых в нем опе­ раций. И, наконец, поскольку каждый строковый литерал (например, "Y o ") является самым настоящим экземпляром System.String, доступ к функциональным возможно­ стям для работы со строками можно получать также для фиксированной последователь­ ности символов. Неизменная природа строк Один из интересных аспектов System.String состоит в том, что после присваивания объекту string первоначального значения символьные данные больше изменяться не могут. На первый взгляд это может показаться заблуждением, ведь строкам постоянно присваиваются новые значения, а в типе System. String доступен набор методов, кото­ рые, похоже, только то и делают, что позволяют изменять символьные данные тем или иным образом (например, преобразовывать их в верхний или нижний регистр). Если, однако, присмотреться внимательнее к тому, что происходит “за кулисами”, то можно будет увидеть, что методы типа string на самом деле возвращают совершенно новый объект string в измененном виде: static void StringsArelmmutable () { // Установка первоначального значения для строки. string si = "This is my string."; Console .WnteLine ("si = {0}", si) ; // Преобразование s i в верхний регистр? string upperstring = si.ToUpper(); Console .WnteLine ("upperstring = {0}", upperstring); // Нет! s i по-прежнему остается в том же формате! Console.WriteLine("si = {0}", si); } На рис. 3.11 показано, как будет выглядеть вывод приведенного выше кода, по кото­ рому легко убедиться в том, что исходный объект string (si) не преобразуется в верх­ ний регистр при вызове ToUpper ( ) , а вместо этого возвращается его копия в изменен­ ном соответствующим образом формате. щ С v W t r v d c ^ i ' о»ч1е»е ***** Fun with Strings ***** si = This is my string, upperstring = THIS IS MY STRING, ^sl = This is my string. Press any key to continue . . . e Рис. 3.11. Строки остаются неизменными Тот же самый закон неизменности строк действует и при использовании в C# опе­ рации присваивания. Чтобы удостовериться в этом, давайте закомментируем (или уда­ лим) весь существующий код в StringsArelmmutable () (чтобы уменьшить объем гене­ рируемого CIL-кода) и добавим следующие операторы: Глава 3. Главные конструкции программирования на С#: часть I 133 static void StringArelmmutable() { string s2 = "My other string"; s2 = "New string value"; } Скомпилируем прилож ение и загрузим результирую щ ую сборку в ут и ли т у ildasm.exe (см. главу 1). На рис. 3.12 показан CIL-код, который будет сгенерирован для метода StringsArelmmutable (). Рис. 3.12. Присваивание значения объекту string приводит к созданию нового объекта string Хотя низкоуровневые детали CIL пока подробно не рассматривались, важно обратить внимание на наличие многочисленных вызов кода операции l d s t r (загрузка строки). Этот код операции ldstr в CIL предусматривает выполнение загрузки нового объекта string в управляемую кучу. В результате предыдущий объект, в котором содержалось значение "Му other string", будет в конечном итоге удален сборщиком мусора. Так что же конкретно необходимо вынести из всего этого? Если кратко: класс string может оказываться неэффективным и приводить к “разбуханию” кода в случае непра­ вильного использования, особенно при выполнении конкатенации строк. Когда же не­ обходимо представлять базовые символьные данные, такие как номер карточки соци­ ального страхования, имя и фамилия или простые фрагменты текста, используемые внутри приложения, он является идеальным вариантом. В случае создания приложения, предусматривающего интенсивную работу с тексто­ выми данными, представление обрабатываемых данных с помощью объектов string будет очень плохой идеей, поскольку практически наверняка (и часто не напрямую) будет приводить к созданию ненужных копий данных string. Как тогда должен посту­ пать программист? Ответ на этот вопрос ищите ниже. Тип S y s t e m . T e x t . S t r i n g B u i l d e r Из-за того, что тип string может оказаться неэффективным при необдуманном ис­ пользовании, в библиотеках базовых классов .NET поставляется еще пространство имен System.Text. Внутри этого (достаточно небольшого) пространства имен предлагается класс по имени StringBuilder. Как и в классе System. String, в StringBuilder со­ держатся методы, которые позволяют, например, заменять и форматировать сегменты. Чтобы использовать этот класс в файлах кода на С#, первым делом необходимо позабо­ титься об импорте следующего пространства имен: // Здесь определен класс Strin gB u ilder: using System.Text; 134 Часть II. Главные конструкции программирования на C# Уникальным в StringBuilder является то, что при вызове его членов производит­ ся непосредственное изменение внутренних символьных данных объекта (что, конечно же, более эффективно), а не получение копии этих данных в измененном формате. При создании экземпляра StringBuilder начальные значения для объекта можно задавать с помощью не одного, а нескольких конструкторов. Тем, кто не знаком с понятием кон­ структора, сейчас важно уяснить лишь то, что конструкторы позволяют создавать объ­ ект с определенным начальным состоянием за счет применения ключевого слова new. Ниже приведен пример применения StringBuilder. static void FunWithStringBuilder () { Console .WnteLine ("=> 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.ToStnng () ); sb.Replace ("2", "Invisible War"); Console .WriteLine (sb.ToStnng ()); Console.WriteLine("sb has {0} chars.", sb.Length); . Console.WriteLine(); } Здесь сначала создается объект StringBuilder с первоначальным значением и * * * * Fantastic Games****". Далее можно добавлять символы к внутреннему буфе­ ру, а также заменять (или удалять) каким угодно образом. По умолчанию изначально в StringBuilder может храниться строка длиной не более 16 символов (она автомати­ чески расширяется по мере необходимости), однако это исходное значение легко изме­ нить, передавая конструктору соответствующий дополнительный аргумент: // С о з д а н и е о б ъ е к т а S t s i n g B u i l d e r с и схо дн ы м / / р азм ер ом в 2 5 6 си м во л о в. StringBuilder sb = new StringBuilder("**** Fantastic Games ****", 256); В случае добавления большего количества символов, чем было указано в качестве лимита, объект StringBuilder будет копировать свои данные в новый экземпляр и создавать для него буфер размером, равным указанному лимиту. На рис. 3.13 показан вывод приведенной выше вспомогательной функции. "я С WindOA;\S)-item3?vOTxJ.e« ***** Fun with Strings ***** *j >=> Using the StringBuilder: **** Fantastic Games **** Half Life Beyond Good and Evil Deus Ex 2 System Shock **** Fantastic Games **** Half Life Beyond Good and Evil Deus Ex Invisible War System Shock sb as 96 chars. Рис. 3.13. Класс StringBuilder работает более эффективно, чем string Глава 3. Главные конструкции программирования на С#: часть I 135 Исходный код. Проект FunWithStrings доступен в подкаталоге Chapter 3. Сужающие и расширяющие преобразования типов данных Теперь, когда известно, как взаимодействовать со встроенными типами данных, да­ вайте рассмотрим связанную тему — преобразование типов данных. Создадим новый проект типа C o nso le A p p lic a tio n по имени TypeConversions и определим в нем следую­ щий класс: class Program { static void Main(string[] args) { Console .WnteLine ("***** 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 j ) ему на самом деле передаются две переменных типа short. Хотя это может показаться несоответствием типов, программа будет ком­ пилироваться и выполняться без ошибок и возвращать в результате, как и ожидалось, значение 19. Причина, по которой компилятор будет считать данный код синтаксически коррект­ ным, связана с тем, что потеря данных здесь невозможна. Поскольку максимальное значение (32 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. Главные конструкции программирования на C# // Следующий код вызовет ошибку компиляции! short numbl = 30000, numb2 = 30000; short answer = Add(numbl, numb2); Console.WnteLine ("{0} + {1} = {2}", numbl, numb2, answer); Console.ReadLine (); В таком случае компилятор сообщит об ошибке: Cannot implicitly convert type 'int' to 'short'. An explicit conversion exists (are you missing a cast?) He удается неявным образом преобразоват ь тип ' i n t ' в ’ s h o r t ' . Существует возможность выполнения п реобразова н и я явным образом (не была ли пропущена операция по приведению типов?) Проблема в том, что хотя метод Add () способен возвращать переменную i n t со значением 60000 (поскольку это значение вполне вписывается в диапазон допустимых значений типа S ystem . In t3 2 ), сохранение этого значения в переменной типа sh o rt невозможно, потому что оно выходит за рамки допустимого диапазона для этого типа. Формально это означает, что CLR-среде не удастся применить операцию сужения. Как не трудно догадаться, операция сужения представляет собой логическую противопо­ ложность операции расширения, поскольку предусматривает сохранение большего зна­ чения внутри переменной меньшего типа данных. Важно отметить, что все сужающие преобразования приводят к выдаче компилято­ ром ошибки, даже когда имеются веские основания полагать, что операция сужающе­ го преобразования должна на самом деле пройти успешно. Например, следующий код тоже приведет к генерации компилятором ошибки: // Еще один код, при выполнении которого // компилятор будет сообщать об ошибке! static void NarrowingAttempt() { byte myByte = 0; int mylnt = 200; myByte = mylnt; Console .WnteLine ("Value of myByte: {0}", myByte); } Здесь значение, содержащееся в переменной типа in t (по имени m ylnt), вписыва­ ется в диапазон допустимых значений типа b y te , следовательно, операция сужения по идее не должна приводить к генерации ошибки во время выполнения. Однако из-за того, что язык C# создавался с таким расчетом, чтобы он заботился о безопасности ти­ пов, компилятор все-таки сообщит об ошибке. Если нужно уведомить компилятор о готовности мириться с возможной в результате операции сужения потерей данных, необходимо применить операцию явного приведе­ ния типов, которая в C# обозначается с помощью ( ) . Ниже показан модифицирован­ ный код Program, а на рис. 3.14 — его вывод. class Program { static void Main(string[] args) { Console .WnteLine ("***** Fun with type conversions *****"); short numbl = 30000, numb2 = 30000; // Явное приведение in t к short (с разрешением потери данных) . short answer = (short)Add(numbl, numb2); Console .WnteLine ("{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 ; // Явное приведение in t к byte (без потери данных). myByte = (byte)mylnt; Console .WnteLine ("Value of myByte: {0}", myByte); } Рис. 3.14. При сложении чисел некоторые данные были потеряны Перехват сужающих преобразований данных Как было только что показано, явное указание операции приведения заставляет компилятор производить операцию сужающего преобразования даже тогда, когда это чревато потерей данных. В методе NarrowingAttempt () это не было проблемой, по­ скольку значение 200 вписывается в диапазон допустимых значений типа byte. Однако при сложении двух значений типа short в методе Main () конечный результат оказался совершенно не приемлемым (30 000 + 30 000 = -5536?). Для создания приложений, в которых потеря данных должна быть недопустимой, в C# предлагаются такие ключевые слова, как checked и unchecked, которые позволяют гарантировать, что потеря данных не окажется незамеченной. Чтобы посмотреть, как применяются эти ключевые слова, давайте добавим в Program новый метод, суммирующий две переменных типа byte, каждой из которых присвоено значение, не выходящее за пределы допустимого максимума (255 для данного типа). По идее, после сложения значений этих двух переменных (с приведением результата int к типу byte) должна быть получена точная сумма. static void ProcessBytes () { byte bl = 10 0; byte Ь2 = 250; byte sum = (byte) Add (bl, b2); // В sum должно содержаться значение 350. // Однако там оказывается значение 94! Console .WnteLine ("sum = {0}", sum); } Удивительно, но при изучении вывода данного приложения обнаруживается, что в sum содержится значение 94 (а не 350, как ожидалось). Объясняется это очень просто. Из-за того, что в System.Byte может храниться только значение из диапазона от 0 до 255 включительно, в sum будет помещено значение переполнения (350 - 256 = 94). 138 Часть II. Главные конструкции программирования на C# По умолчанию, в случае, когда не предпринимается никаких соответствующих ис­ правительных мер, условия переполнения (overflow) и потери значимости (underflow) происходят без выдачи ошибки. Обрабатывать условия переполнения и потери значи­ мости в приложении можно двумя способами. Это можно делать вручную, полагаясь на свои знания и навыки в области программирования. Недостаток такого подхода в том, что даже в случае приложения максимальных усилий человек все равно остается человеком, и какие-то ошибки могут ускользнуть от его глаз. К счастью, в C# предусмотрено ключевое слово checked. Если оператор (или блок операторов) заключен в контекст checked, компилятор C# генерирует дополнительные CIL-инструкции, обеспечивающие проверку на предмет условий переполнения, которые могут возникать в результате сложения, умножения, вычитания или деления двух чи­ словых типов данных. В случае возникновения условия переполнения во время выполнения будет генери­ роваться исключение System.OverflowException. Детали обеспечения структуриро­ ванной обработки исключения и использования в связи с этим ключевых слов try и catch будут даны в главе 7. А пока, не вдаваясь особо в детали, можно изучить пока­ занный ниже модифицированный код: static void ProcessBytes () { byte Ы = 100; byte Ъ 2 = 250; // На этот раз компилятору указывается добавлять CIL-код, // необходимый для выдачи исключения в случае возникновения // условий переполнения или потери значимости. try byte sum = checked ((byte) Add (bl, b2)); Console.WnteLine ("sum = {0}", sum); } catch (OverflowException ex) { Console .WnteLine (ex.Message) ; } } Здесь следует обратить внимание на то, что возвращаемое значение метода Add () было заключено в контекст checked. Благодаря этому, в связи с выходом значения sum за пределы диапазона допустимых значений типа byte во время выполнения теперь будет генерироваться исключение, а через свойство Message выводиться сообщение об ошибке, как показано на рис. 3.15. С WindcAj\jystem32\cmd.e*e Fun with type conversions * * * * * 30000 + 30000 = -5536 Value of myByte: 200 Arithmetic 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 (OvertlowException ex) { Console.WriteLine(ex.Message); } И в том и в другом случае интересующий код будет автоматически проверяться на предмет возникновения возможных условий переполнения, и при обнаружении тако­ вых приводить к генерации соответствующего исключения. Настройка проверки на предмет возникновения условий переполнения в масштабах проекта Если создается приложение, в котором переполнение никогда не должно проходить незаметно, может выясниться, что обрамлять ключевым словом checked приходится раздражающе много строк кода. На такой случай в качестве альтернативного вариан­ та в компиляторе C# поддерживается флаг /checked. При активизации этого флага проверке на предмет возможного переполнения будут автоматически подвергаться все имеющиеся в коде арифметические операции, без применения для каждой из них клю­ чевого слова checked. Обнаружение переполнения точно так же приводит к генерации соответствующего исключения во время выполнения. Для активизации этого флага в Visual Studio 2010 необходимо открыть страни­ цу свойств проекта, перейти на вкладку Build (Сборка), щелкнуть на кнопке Advanced (Дополнительно) и в открывшемся диалоговом окне отметить флажок Check for arithmetic overflow/underflow (Выполнять проверку на предмет арифметического переполнения и потери значимости), как показано на рис. 3.16. Рис. 3.16. Включение функции проверки на предмет переполнения и потери значимости в масштабах всего проекта Ключевое слово u n c h e c k e d Теперь давайте посмотрим, что можно сделать, если функция проверки на предмет переполнения и потери значимости в масштабах всего проекта включена, но есть ка- 140 Часть II. Главные конструкции программирования на C# кой-то блок кода, в котором потеря данных является допустимой. Из-за того, что дей­ ствие флага /checked распространяется на всю арифметическую логику, в C# преду­ смотрено ключевое слово unchecked, которое позволяет отключить выдачу связанного с переполнением исключения в отдельных случаях. Применяется это ключевое слово похожим на checked образом, поскольку может быть указано как для одного оператора, так и для целого блока: // При условии, что флаг /checked активизирован, этот // блок не будет приводить к генерации исключения во время выполнения. unchecked { byte sum = (byte) (bl + b2) ; Console.WriteLine ("sum = { 0} ", sum); } Итак, чтобы подвести итог по использованию в C# ключевых слов ch eck ed и unchecked, следует отметить, что по умолчанию арифметическое переполнение в ис­ полняющей среде .NET игнорируется. Если необходимо обработать отдельные операто­ ры, то должно использоваться ключевое слово checked, а если нужно перехватывать все связанные с переполнением ошибки в приложении, то понадобится активизировать флаг /checked. Что касается ключевого слова unchecked, то его можно применять при наличии блока кода, в котором переполнение является допустимым (и, следовательно, не должно приводить к генерации исключения во время выполнения). Роль класса S y s t e m . C o n v e r t В завершении темы преобразования типов данных стоит отметить, что в простран­ стве имен System имеется класс по имени Convert, который тоже может применяться для расширения и сужения данных: static void NarrowWithConvert () { byte myByte = 0; int mylnt = 200; . myByte = Convert.ToByte(m ylnt); Console.WriteLine ("Value of myByte: {0}", myByte); } Одно из преимуществ подхода с применением класса S ystem . C o n vert связано с тем, что он позволяет выполнять преобразования между типами данных нейтральным к языку образом (например, синтаксис приведения типов в Visual Basic полностью от­ личается от предлагаемого для этой цели в С#). Однако, поскольку в C# есть операция явного преобразования, использование класса C onvert для преобразования типов дан­ ных обычно является делом вкуса. Исходный код. Проект T yp eC on version s доступен в подкаталоге Chapter 3. Неявно типизированные локальные переменные Вплоть до этого момента в настоящей главе при определении локальных перемен­ ных тип данных, лежащий в их основе, всегда указывался явно: static void DeclareExplicitVars () { // Явно типизированные локальные переменные // объявляются следующим образом: // dataType vanableName = initialValue; Глава 3. Главные конструкции программирования на С#: часть I int mylnt = 0; bool myBool = true; string myString = "Time, marches o n 141 ; } Хотя указывать явным образом тип данных для каждой переменной всегда считается хорошим стилем, в C# также поддерживается возможность неявной типизации локаль­ ных переменных с помощью ключевого слова va r. Ключевое слово v a r можно исполь­ зовать вместо указания конкретного типа данных (такого как in t , b o o l или s t r in g ). В этом случае компилятор автоматически выводит лежащий в основе тип данных на основе первоначального значения, которое используется для инициализации локаль­ ных данных. Чтобы посмотреть, как это выглядит на практике, давайте создадим новый проект типа C o nso le A p p lic a tio n (Консольное приложение) по имени Im p lic itly T y p e d L o c a lV a rs и объявим в нем те же локальные переменные, что использовались в предыдущем мето­ де, следующим образом: static void DeclarelmplicitVars () { // Неявно типизированные локальные переменные объявляются следующим образом: // var variableName = m it ia lV a lu e ; var mylnt = 0; var myBool = true; var myString = "Time, marches on..." ; } На заметку! На самом деле лексема var ключевым словом в C# не является. С ее помощью можно объявлять переменные, параметры и поля и не получать никаких ошибок на этапе компиляции. При использовании этой лексемы в качестве типа данных, однако, она по контексту восприни­ мается компилятором как ключевое слово. Поэтому ради простоты здесь будет применяться термин “ключевое слово 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 o n ..."; // Вывод имен типов, лежащих в основе этих переменных. Console .WnteLine ("mylnt is а: {0}", mylnt.GetType().Name); Console.WriteLine("myBool is a: {0}", myBool.GetType().Name); Console .WnteLine ("myString is a: {0}", myS tring.GetType () .Name) ; } На заметку! Следует иметь в виду, что такую неявную типизацию можно использовать для любых типов, включая массивы, обобщенные типы (см. главу 10) и пользовательские специальные типы. Далее в книге будут встречаться и другие примеры применения неявной типизации. 142 Часть II. Главные конструкции программирования на C# Если теперь вызвать метод D e c la r e lm p lic it V a r s () в Main ( ) , то получится вывод, показанный на рис. 3.17. Рис. 3.17. Применение рефлексии в отношении неявно типизированных локальных переменных Ограничения, связанные с неявно типизированными переменными Разумеется, с использованием ключевого слова v a r связаны различные ограничения. Самое первое и важное из них состоит в том, что неявная типизация применима толь­ ко для локальных переменных в контексте какого-то метода или свойства. Применять ключевое слово v a r для определения возвращаемых значений, параметров или данных полей специального типа не допускается. Например, ниже показано определение клас­ са, которое приведет к выдаче сообщений об ошибках на этапе компиляции: class ThisWillNeverCompile { // Ошибка! var не может применяться // для определения полей! private var mylnt = 10; // Ошибка! var не может применяться // для определения возвращаемого значения // или типа параметра! public var MyMethod (var x, var y) {} } Кроме того, локальным переменным, объявленным с помощью ключевого слова var, обязательно должно быть присвоено начальное значение в самом объявлении, причем присваивать в качестве начального значения null не допускается! Последние ограни­ чение вполне понятно, поскольку на основании одного лишь значения null компилятор не сможет определить, на какой тип в памяти указывает переменная. // Ошибка! Должно быть присвоено значение! var myData; // Ошибка! Значение должно присваиваться в самом объявлении1 var mylnt; mylnt = 0; // Ошибка! Присваивание n u ll в качестве // начального значения не допускается! var myObj = null; Присваивание значения null локальной переменной с выведенным после началь­ ного присваивания типом вполне допустимо (при условии, переменная отнесена к ссы­ лочному типу): // Все в порядке, поскольку SportsCar // является переменной ссылочного типа! var myCar = new SportsCar (); myCar = null; Глава 3. Главные конструкции программирования на С#: часть I 143 Более того, значение неявно типизированной локальной переменной может быть присвоено другим переменным, причем как неявно, так и явно типизированным: // Здесь тоже все в порядке! var mylnt = 0; var anotherlnt = mylnt; string myStnng = "Wake up!"; var myData = myString; ' Кроме того, неявно типизированную локальную переменную можно возвращать вы­ зывающему методу, при условии, что возвращаемый тип этого метода совпадает с тем, что лежит в основе определенных с помощью var данных: static int GetAnlntO { var retVal = 9; return retVal; } И, наконец, последний, но от того не менее,важный момент: определять неявно ти­ пизированную локальную переменную как допускающую значение null с использова­ нием лексемы ? в C# нельзя (типы данных, допускающие значение null, рассматрива­ ются в главе 4). // Определять неюно типизированные переменные как допускающие значение n u ll // нельзя, поскольку таким переменным изначально не разрешено присваивать n u ll! var? nope = new SportsCar(); var? stillNo = 12; var? noWay = null; Неявно типизированные данные являются строго типизированными Следует иметь в виду, что неявная типизация локальных переменных приводит к получению строго типизированных данных. Таким образом, применение ключевого слова var в C# отличается от техники, используемой в языках сценариев (таких как JavaScript или Perl), а также от применения типа данных Variant в СОМ, где перемен­ ная на протяжении своего существования в программе может хранить значения разных типов (это часто называется динамической типизацией). На заметку! В .NET 4.0 появилась возможность динамической типизации в C# с использованием нового ключевого слова dynamic. Более подробно об этом аспекте языка будет рассказы­ ваться в главе 18. Выведение типа позволяет языку C# оставаться строго типизированным и оказы­ вает влияние только на объявление переменных во время компиляции. После этого данные трактуются как объявленные с выведенным типом; присваивание такой пе­ ременной значения другого типа будет приводить к возникновению ошибок на этапе компиляции. static void ImplicitTypinglsStrongTyping() { // Компилятор знает, что s имеет тип System.String. var s = "This variable can only hold string data!" ; s = "This is fine..."; // Можно вызывать любой член л е ж а щ е г о в основе типа. string upper = s.ToUpperO; // Ошибка! Присваивание числовых данных строке невозможно! s = 44; 144 Часть II. Главные конструкции программирования на C# Польза от неявно типизированных локальных переменных Теперь, когда был показан синтаксис, используемый для объявления неявно типи­ зируемых локальных переменных, наверняка возник вопрос, в каких ситуациях его полезно применять? Самое важное, что необходимо знать — использование v a r для объявления локальных переменных просто так особой пользы не приносит, а в действи­ тельности может даже вызвать путаницу у тех, кто будет изучать данный код, посколь­ ку лишает возможности быстро определить тип данных и, следовательно, понять, для чего предназначена переменная. Поэтому если точно известно, что переменная должна относиться к типу int, лучше сразу объявить ее с указанием этого типа. В наборе технологий LINQ, как будет показано в начале главы 13, применяются так называемые выражения запросов (query expression), которые позволяют получать дина­ мически создаваемые результирующие наборы на основе формата запроса. В таких вы­ ражениях неявная типизация чрезвычайно полезна, так как в некоторых случаях явное указание типа попросту не возможно. Ниже приведен соответствующий пример кода LINQ, в котором предлагается определить базовый тип данных subset: static void QueryOverInts () { 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.WnteLine () ; // К какому типу относится subset? Console .WnteLine ("subset is a: {0}", subset.GetType().Name); Console.WriteLine ("subset is defined in: {0}", subset.GetType().Namespace); } Если интересно, проверьте свои предположения по поводу типа данных subset, вы­ полнив предыдущий код (subset целочисленным массивом не является). В любом слу­ чае должно быть понятно, что неявная типизация занимает свое место в рамках набора технологий LINQ. На самом деле, можно даже утверждать, что единственным случаем, когда применение ключевого слова v a r вполне оправдано, является определение дан­ ных, возвращаемых из запроса LINQ. Нужно всегда помнить о том, что если в точно­ сти известно, что переменная должна представлять собой переменную типа int, лучше всегда сразу же объявлять ее с указанием этого типа. Излишняя неявная типизация (с помощью va r) считается плохим стилем в производственном коде. Исходный код. Проект Im p lic it ly T y p e d L o c a lV a r s доступен в подкаталоге Chapter 3. Итерационные конструкции в C# Все языки программирования предлагают способы для повторения блоков кода до тех пор, пока не будет соблюдено какое-то условие завершения. Какой бы язык не использовался ранее, операторы, предлагаемые для осуществления итераций в С#, не должны вызывать особых недоумений и требовать особых объяснений. В частности, че­ тырьмя поддерживаемыми в C# для выполнения итераций конструкциями являются: Глава 3. Главные конструкции программирования на С#: часть I 145 • цикл for; • цикл foreach/in; • цикл while; • цикл do/while. Давайте рассмотрим каждую из этих конструкций по очереди, предварительно соз­ дав новый проект типа C o n so le A p p lic a tio n по имени IterationsAndDecisions. Цикл f o r Если требуется проходить по блоку кода фиксированное количество раз, приличную гибкость демонстрирует оператор for. Он позволяет указать, сколько раз должен повто­ ряться блок кода, а также задать конечное условие, при котором его выполнение долж­ но быть завершено. Ниже приведен пример применения этого оператора: // База для цикла. static void ForAndForEachLoop () { // Переменная i доступна только в контексте этого цикла fo r. for(int i = 0; 1 < 4; i++ ) { Console.WriteLine("Number is: {0} ", 1 ) ; } // Здесь переменная i уже не доступна. } Все приемы, освоенные в языках С, C++ и Java, применимы также при создании опе­ раторов for в С#. Как и в других языках, в C# можно создавать сложные конечные усло­ вия, определять бесконечные циклы и использовать ключевые слова goto, continue и break. Предполагается, что читатель уже знаком с данной итерационной конструкцией. Дополнительные сведения об использовании ключевого слова for в C# можно найти в документации .NET Framework 4.0 SDK. Цикл f o r e a c h Ключевое слово foreach в C# позволяет проходить в цикле по всем элементам мас­ сива (или коллекции, как будет показано в главе 10) без проверки его верхнего предела. Ниже приведено два примера использования цикла foreach, в одном из которых произ­ водится проход по массиву строк, а в другом — проход по массиву целых чисел. // Проход по элементам массива с помощью foreach. static void ForAndForEachLoop () string[] carTypes = {"Ford", "BMW", "Yugo", "Honda" }; foreach (string c in carTypes) Console.WriteLine(c); int[] mylnts = { 10, 20, 30, 40 }; foreach (int i in mylnts) Console.WriteLine(l); } Помимо прохода по простым массивам, fo r e a c h также позволяет осуществлять итерации по системным и определяемым пользователем коллекциям. Рассмотрение со­ ответствующих деталей откладывается до главы 9, поскольку этот способ применения fo rea ch требует понимания особенностей программирования с использованием интер­ фейсов, таких как IEnumerator и IEnumerable. 146 Часть II. Главные конструкции программирования на C# Использование v a r в конструкциях f o r e a c h В итерационных конструкциях fо reach также можно применять неявную типиза­ цию. Нетрудно догадаться, что компилятор в таких случаях будет корректно выводить соответствующий “тип типа”. Рассмотрим приведенный ниже метод, в котором произ­ водится проход по локальному массиву целых чисел: static void Var IriForeachLoop () { intL] mylnts = { 10, 20, 30, 40 }; // Использование var в стандартном цикле foreach. foreach (var it pm in mylnts) { Console.WiiteLine("Item value: (0}", item); // вывод значения элемента Следует отметить, что в данном примере веской причины для использования ключе­ вого слова var в цикле foreach нет, поскольку четко видно, что итерация осуществляет­ ся по массиву целых чисел. Но, опять-таки, в модели программирования LINQ использо­ вание var в цикле foreach будет очень полезно, а иногда и вообще обязательно. Конструкции w h i l e и d o / w h i l e Конструкцию while удобно применять, когда требуется, чтобы блок операто­ ров выполнялся до тех пор, пока не будет удовлетворено какое-то конечное условие. Естественно, нужно позаботиться о том, чтобы это условие когда-нибудь действитель­ но достигалось, иначе получится бесконечный цикл. Ниже приведен пример, в котором на экран будет выводиться сообщение In while loop (В цикле while) до тех пор, пока пользователь не завершит цикл вводом в командной строке слова yes: static void ExecuteWhileLoop() { string userlsDone = "" ; // Проверка на соответствие строке в нижнем регистре. w h ile (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 (); } w h ile (userlsDone.ToLower() != "yes"); // Обратите внимание на точку с запятой! Глава 3. Главные конструкции программирования на С#: часть I 147 Конструкции принятия решений и операции сравнения Теперь, когда было показано, как обеспечить выполнение блока операторов в цикле, давайте рассмотрим следующую связанную концепцию, а именно — управление выпол­ нением программы. Для изменения хода выполнения программы в C# предлагаются две следующих конструкции: • оператор i f / e l s e ; • оператор sw itch . Оператор i f / e l s e Сначала рассмотрим хорошо знакомый оператор i f / e l s e . В отличие от языков С и C++, в C# этот оператор может работать только с булевскими выражениями, но не с произвольными значениями вроде -1 и 0. С учетом этого, для получения литеральных булевских значений в операторах i f / e l s e обычно применяются операции, перечислен­ ные в табл. 3.7. Таблица 3.7. Операции сравнения в C# Операция сравнения*У Пример использования Описание == i f (age == 30) Возвращает tru e, только если выражения одинаковы 1= i f ( "Foo" != myStr) Возвращает tru e, только если выражения разные < > <= >= if(b o n u s if(b o n u s if(b o n u s if(b o n u s < 2000) > 2000) <= 2000) >= 2000) Возвращает tru e, только если выражение слева (bonus) меньше, больше, меньше или равно либо больше или равно выражению справа (2000) Программисты, ранее работавшие с С и C++, должны иметь в виду, что старые прие­ мы проверки неравенства значения нулю в C# работать не будут. Например, предполо­ жим, что требуется проверить, состоит ли текущая строка из более чем нуля символов. У программистов на С и C++ может возникнуть соблазн написать код следующего вида: static yoid ExecutelfElse () { // Такой код недопустим, поскольку Length возвращает in t , а не bool. string stnngData = "Му textual data"; if (stnngData.Length) { Console .WnteLine ("string is greater than 0 characters") ; } } Если необходимо использовать свойство S t r i n g . Length для определения истинно­ сти или ложности, вычисляемое в условии выражение потребуется изменить так, чтобы оно давало в результате булевское значение: // Такой код является допустимым, поскольку // условие будет возвращать true или fa ls e . if(stringData.Length > 0) { Console.WriteLine("string is greater than 0 characters"); } 148 Часть II. Главные конструкции программирования на C# В операторе i f могут применяться сложные выражения, и он может содержать операторы else, обеспечивая выполнение более сложных проверок. Синтаксис похож на применяемый в аналогичных ситуациях в языках С (C++) и Java. При построении сложных выражений в C# используется вполне ожидаемый набор условных операций, описанный в табл. 3.8. Таблица 3.8. Условные операции в C# Операция Пример Описание && if (аде == 30 && name == "Fred") Условная операция AND (И). Возвращает true, если все выраже­ ния истинны 11 if (age ==30 | | name == "Fred") Условная операция OR (ИЛИ). Возвращает true, если истинно хотя бы одно из выражений 1 i f (ImyBool) Условная операция NOT (НЕ). Возвращает true, если выражение ложно, или false, если истинно На заметку! Операции && и | | поддерживают сокращенный путь выполнения, если это необходи­ мо. То есть сразу же после определения, что некоторое сложное выражение является ложным, оставшиеся подвыражения вычисляться не будут. Оператор s w i t c h Еще одной простой конструкцией, предназначенной в C# для реализации выбора, является оператор switch. Как и в остальных С-подобных языках, в C# этот оператор позволяет организовать выполнение программы на основе заранее определенного на­ бора вариантов. Например, в приведенном ниже методе Main () для каждого из двух возможных вариантов выводится свое сообщение (блок default обрабатывает невер­ ный выбор). // Переход на основе выбранного числового значения. static void ExecuteSwitch () { Console.WriteLine ("1 [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; } } На заметку! В языке C# каждый блок case, в котором содержатся выполняемые операторы (блок default в том числе), должен завершаться оператором break или goto, во избежание сквозного прохода. Глава 3. Главные конструкции программирования на С#: часть I 149 Одна из замечательных особенностей оператора switchBC# заключается в том, что помимо числовых данных он также позволяет производить вычисления и со строковыми данными. Ниже для примера приведена модифицированная версия предыдущего опе­ ратора switch (обратите внимание, что при этом подвергать пользовательские данные синтаксическому разбору и преобразованию их в числовые значения не требуется). static void ExecuteSwitchOnStnng () { Console .WnteLine ("C# or VB"); Console.Write("Please pick your language preference: "); string langChoice = Console.ReadLine (); switch (langChoice) case "C#" : Console .WnteLine ("Good choice, C# is a fine language."); break; case "VB": Console.WnteLine ("VB : OOP, multithreading and more' ") ; break; default: Console .WnteLine ("Well... good luck with that!" );\ break; Исходный код. Проект IterationsAndDecisions доступен в подкаталоге Chapter 3. На этом рассмотрение поддерживаемых в C# ключевых слов для организации цик­ лов и принятия решений, а также общих операций, которые можно использовать при написании сложных операторов, завершено. Здесь предполагалось, что у читателя име­ ется опыт работы с аналогичными ключевыми словами (if, for, switch и т.д.) в других языках программирования. Дополнительные сведения по данной теме можно найти в документации .NET Framework 4.0 SDK. Резюме Задачей настоящей главы было описание многочисленных ключевых аспектов языка программирования С#. Сначала рассматривались типичные конструкции, используе­ мые в любом приложении. Затем была описана роль объекта приложения и рассказано о том, что в каждой исполняемой программе на C# должен обязательно присутствовать тип, определяющий метод Main () , который служит входной точкой в приложении. Внутри метода Main () обычно создается набор объектов, которые, работая вместе, при­ водят приложение в действие. Далее были рассмотрены базовые типы данных в C# и разъяснено, что используе­ мые для их представления ключевые слова (вроде int) на самом деле являются сокра­ щенными обозначениями полноценных типов из пространства имен System (в данном случае System. Int32). Благодаря этому, каждый тип данных в C# имеет набор встроен­ ных членов. Также была описана роль операций расширения и сужения типов и таких ключевых слов, как checked и unchecked. Кроме того, рассматривались особенности неявной типизации с помощью ключевого слова var. Как было отмечено, неявная типизация наиболее полезна в модели програм­ мирования LINQ. И, наконец, в главе кратко рассматривались различные конструкции С#, предназначенные для создания циклов и принятия решений. Теперь, когда базовые характеристики языка C# известны, можно переходить к изучению его ключевой функ­ циональности, а также объектно-ориентированных возможностей. ГЛАВА 4 Главные конструкции программирования на С#: часть II этой главе будет завершен обзор ключевых аспектов языка программирования С#. Сначала рассматриваются различные детали, касающиеся построения мето­ дов в С#, в частности, ключевые слова out, r e f и params. Кроме того, будут описаны две новых функциональных возможности С#, которые появились в .NET 4.0 — необяза­ тельные и именованные параметры. Затем будет рассматриваться перегрузка методов, манипулирование массивами с использованием синтаксиса C# и функциональность связанного с массивами класса В System.Array. Вдобавок в главе показано, как создавать перечисления и структуры в С#, и подроб­ но описаны отличия между типами значений и ссылочными типами. И, наконец, рас­ сматривается роль нулевых (nullable) типов данных и операций ? и ??. После изучения материалов настоящей главы можно переходить к ознакомлению с объектно-ориенти­ рованными возможностями языка С#. Методы и модификаторы параметров Для начала давайте изучим детали, касающиеся определения методов в С#. Как и ме­ тод Main () (см. главу 3), специальные методы могут как принимать, так и не принимать параметров, а также возвращать или не возвращать значения вызывающей стороне. Как будет показано в следующих нескольких главах, методы могут быть реализованы в контексте классов или структур (а также прототипированы внутри типов интерфейсов) и снабжаться различными ключевыми словами (in te r n a l, v i r t u a l , p u b lic , new и т.д.) для уточнения их поведения. До настоящего момента в этой книге формат каждого из демонстрировавшихся методов в целом выглядел так: // Статические методы могут вызываться // напрямую без создания экземпляра класса, class Program • { // static возвращаемыйТип ИмяМетода (параметры) {...} static int Add(±nt х, int у) { return х + у; } } Глава 4. Главные конструкции программирования на С#: часть II 151 Х отя определение метода в C# выглядит довольно понятно, существует несколько ключевых слов, с помощью которых можно управлять способом передачи аргументов интересующему методу. Все эти ключевые слова описаны в табл. 4.1. Таблица 4.1. Модификаторы параметров в C# Модификатор параметра Описание (отсутствует) Если параметр не сопровождается модификатором, предполага­ ется, что он должен передаваться по значению, т.е. вызываемый метод должен получать копию исходных данных out Выходные параметры должны присваиваться вызываемым мето­ дом (и, следовательно, передаваться по ссылке). Если парамет­ рам out в вызываемом методе значения не присвоены, компиля­ тор сообщит об ошибке ref Это значение первоначально присваивается вызывающим кодом и при желании может повторно присваиваться в вызываемом методе (поскольку данные также передаются по ссылке). Если параметрам ref в вызываемом методе значения не присвоены, компилятор никакой ошибки генерировать не будет params Этот модификатор позволяет передавать в виде одного логическо­ го параметра переменное количество аргументов. В каждом мето­ де может присутствовать только один модификатор params и он должен обязательно указываться последним в списке параметров. В реальности необходимость в использовании модификатора params возникает не особо часто, однако он применяется во мно­ гих методах внутри библиотек базовых классов Чтобы посмотреть, как эти ключевые слова используются на практике, давайте соз­ дадим новый проект типа Console Application по имени FunWithMethods и с его по­ мощью изучим роль каждого из этих ключевых слов. Стандартное поведение при передаче параметров По умолчанию параметр передается по значению. Попросту говоря, если аргумент не снабжается каким-то конкретным модификатором параметра, функции передается копия данных. Как будет объясняться в конце настоящей главы, то, как именно выгля­ дит эта копия, зависит от того, к какому типу относится параметр — типу значения или ссылочному типу. Пока что давайте создадим внутри класса Program следующий метод, который оперирует двумя числовыми типами данных, передаваемыми по значению: //По умолчанию аргументы передаются по значению. public static int Add(int x, int y) { int ans = x + y; // Вызывающий метод не увидит эти изменения, поскольку // изменяться в таком случае будет лишь копия исходных данных. X = 10000; у = 88888; return ans; } Числовые данные подпадают под категорию типов значения. Поэтому в случае измене­ ния значений параметров внутри члена вызывающий метод будет оставаться в полном не­ ведении об этом, поскольку значения будут изменяться л и т ь в копии исходных данных. 152 Часть II. Главные конструкции программирования на C# static void Main(string [] args) { Console .WnteLine (" *****Fun with Methods *****\n"); // Передача двух переменных no значению. int x = 9, у = 10; Console .WnteLine ("Before call: X: {0}, Y: {1}", x, y) ; Console .WnteLine ("Answer is: {0}", Add(x, y) ) ; Console.WriteLine("After call: X: {0}, Y: {1}", x, y) ; Console.ReadLine(); // до вызова // ответ // после вызова } Как и следовало ожидать, значения х и у до и после вызова Add ( ) будут выглядеть совершенно идентично: ***** Fun wlth Methods ***** Before call: X: 9 , Y: 10 Answer is: 19 After call: X: 9 , Y: 10 Модификатор o u t Теперь посмотрим, как используются выходные параметры. Методы, которым при определении (с помощью ключевого слова out) указано принимать выходные парамет­ ры, должны перед выходом обязательно присваивать им соответствующие значения (в противном случае компилятор сообщит об ошибке). В целях иллюстрации ниже приведена альтернативная версия метода Add () , кото­ рая предусматривает возврат суммы двух целых чисел с использованием модификато­ ра out (обратите внимание, что физическим возвращаемым значением метода теперь является vo id ). // Выходные параметры должны предоставляться вызываемым методом. 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; A d d (90, 90, out ans); Console.WriteLine("90 + 90 = {0}", ans); Console.ReadLine(); } Этот пример был представлен исключительно для иллюстрации; на самом деле нет совершенно никаких оснований возвращать значение операции суммирования в выход­ Глава 4. Главные конструкции программирования на С#: часть II 153 ном параметре. Но сам модификатор out в C# действительно является очень полезным: он позволяет вызывающему коду получать в результате одного вызова метода сразу не­ сколько значений. // Возврат множества выходных параметров. public static void FillTheseVals (out int a, out string b, out bool c) I a = 9; b = "Enjoy your string."; c = true; } В вызывающем коде в таком случае может находиться обращ ение к методу FillTheseValues (), как показано ниже. Обратите внимание, что модификатор out должен использоваться как при вызове, так и при реализации данного метода. static void Main(string [] args) { Console.WnteLine (''***** Fun with Methods *****"); int i; string str; bool b; FillTheseValues (out i , out str, out b); Console.WnteLine ("Int is: {0}", 1 ); Console.WriteLine ("String is: {0}", str); Console.WriteLine("Boolean is: {0}", b); // целое число // строка // булевское значение Console.ReadLine(); } И, наконец, не забывайте, что в любом методе, в котором определяются выходные параметры, перед выходом им обязательно должны быть присвоены действительные значения. Следовательно, для следующего кода компилятор будет выдавать ошибку, по­ скольку выходному параметру в области действия метода никакого значения присвоено не было: static void ThisWontCompile (out int a) { Console. WriteLine("Error 1 Forgot to assign output arg!"); // Ошибка! Забыли присвоить значение выходному аргументу! Модификатор r e f Теперь посмотрим, как в C# используется модификатор r e f (от “reference” - ссылка). Параметры, сопровождаемые таким модификатором, называются ссылочными и приме­ няются, когда нужно позволить методу выполнять операции и обычно также изменять значения различных элементов данных, объявляемых в вызывающем коде (например, в процедуре сортировки или обмена). Обратите внимание на следующие отличия между ссылочными и выходными параметрами. • Выходные параметры не нужно инициализировать перед передачей методу. Причина в том, что метод сам должен присваивать значения выходным парамет­ рам перед выходом. • Ссылочные параметры нужно обязательно инициализировать перед передачей методу. Причина в том, что они подразумевают передачу ссылки на уже суще­ ствующую переменную. Если первоначальное значение ей не присвоено, это бу­ дет равнозначно выполнению операции над неинициализированной локальной переменной. 154 Часть II. Главные конструкции программирования на C# Давайте рассмотрим применение ключевого слова r e f на примере метода, меняю­ щего две строки местами: // Ссылочные параметры. public static void SwapStnngs (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 (r e f s i, r e f s2 ); Console.WriteLine("After: {0}, [1} ", si, s2); // после Console.ReadLine (); } Здесь в вызывающем коде производится присваивание первоначальных значений локальным строкам (s i и s2). Благодаря этому после выполнения вызова SwapStrings () в строке s i будет содержаться значение "F lo p ", а в строке s2 — значение " F l i p " : Before: Flip, Flop After: Flop, Flip На заметку! Поддерживаемое в C# ключевое слово ref рассматривается в разделе "Типы значе­ ния и ссылочные типы” далее в главе. Как будет показано, поведение этого ключевого слова немного меняется в зависимости от того, является аргумент типом значения (структурой) или ссылочный типом (классом). Модификатор p a ra m s В C# поддерживается использование массивов параметров за счет применения клю­ чевого слова 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.", values.Length); double sum = 0; if(values.Length == 0) return sum; for (int i = 0; l < values.Length; i++) sum += values [l] ; return (sum / values.Length); } Метод определен таким образом, чтобы принимать массив параметров со значения­ ми типа double. По сути, этот метод ожидает произвольное количество (включая ноль) значений double и вычисляет по ним среднее значение. Благодаря этому, он может вызываться любым из показанных ниже способов: static void Main(string[] args) { Console.WriteLine (''***** Fun with Methods *****"); // Передача разделенного запятыми списка значений d o u b le ... double average; average = CalculateAverage(4.0, 3.2, 5.7, 64.22, 87.2); Console.WriteLine("Average of data is: {0}", average); // Среднее значение получается таким: // . . .или массива значений double. double [] data = { 4.0, 3.2, 5.7 }; average = CalculateAverage(data); Console.WriteLine("Average of data is: 10}", average); // Среднее из 0 равно О! Console.WriteLine("Average of data is: (0}", CalculateAverage()); Console.ReadLine(); } Если бы в определении CalculateAverage () не было модификатора params, первый спо­ соб вызова этого метода приводил бы к ошибке на этапе компиляции, поскольку тогда компи­ лятор искал бы версию CalculateAverage (), принимающую пять аргументов double. На заметку! Во избежание какой бы то ни было неоднозначности, в C# требуется, чтобы в любом методе поддерживался только один аргумент 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. Главные конструкции программирования на C# Определение необязательных параметров С выходом версии .NET 4.0 у разработчиков приложений на C# теперь появилась возможность создавать методы, способные принимать так называемые необязательные аргументы (optional arguments). Это позволяет вызывать единственный метод, опуская необязательные аргументы, при условии, что подходят их значения, установленные по умолчанию. На заметку! Как будет показано в главе 18, главным стимулом для добавления необязательных аргументов послужила необходимость в упрощении взаимодействия с объектами СОМ. В не­ скольких объектных моделях Microsoft (например, Microsoft Office) функциональность предос­ тавляется через объекты СОМ, многие из которых были написаны давно и рассчитаны на ис­ пользование необязательных параметров. Чтобы посмотреть, как работать с необязательными аргументами, давайте создадим метод по имени EnterLogData () с одним необязательным параметром: static void EnterLogData(string message, string owner = "Programmer") { Console.Beep (); Console .WnteLine ("Error : {0}", message); Console .WnteLine ("Owner of Error: {0}", owner) ; } Последнему аргументу string был присвоено используемое по умолчанию значение " Programmer" с применением операции присваивания внутри определения парамет­ ров. В результате метод EnterLogData () можно вызывать в Main () двумя способами: static void Main(string[] args) { Console.WnteLine (''***** 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 .WnteLine ("Owner of Error: {0}", owner) ; Console.WriteLine("Time of Error: {0}", timestamp); } Этот код скомпилировать не получится, потому что значение свойства Now класса DateTime вычисляется во время выполнения, а не во время компиляции. Глава 4. Главные конструкции программирования на С#: часть II 157 На заметку! Во избежание возникновения любой неоднозначности, необязательные параметры должны всегда размещаться в конце сигнатуры метода. Если необязательный параметр ока­ жется перед обязательными, компилятор сообщит об ошибке. Вызов методов с использованием именованных параметров Еще одной функциональной возможностью, которая добавилась в C# с выходом вер­ сии .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!", textColor: ConsoleColor.DarkRed, backgroundColor: ConsoleColor.White); DisplayFancyMessage(backgroundColor: ConsoleColor.Green, message: "Testing...", textColor: ConsoleColor.DarkBlue); Console.ReadLine(); } 158 Часть II. Главные конструкции программирования на C# Одной малоприятной особенностью применения именованных аргументов является то, что в вызове метода позиционные параметры должны быть перечислены перед лю ­ быми именованными параметрами. Другими словами, именованные аргументы должны всегда размещаться в конце вызова метода. Ниже показан пример, иллюстрирующий сказанное. // Здесь все в порядке, поскольку позиционные // аргументы идут перед именованными. DisplayFancyMessage(ConsoleColor.Blue, message: "Testing...", 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") { } Из-за того, что каждый аргумент теперь имеет значение по умолчанию, в вызываю­ щем коде с помощью именованных аргументов можно указывать только тот параметр или параметры, для которых не должны применяться значения по умолчанию. То есть, если нужно, чтобы значение " H e l l o ! " появлялось в виде текста голубого цвета на бе­ лом фоне, в вызывающем коде можно использовать следующую строку: DisplayFancyMessage(message: "Hello!") Если же необходимо, чтобы строка "Test Message" выводилась синим цветом на зеленом фоне, то должен применяться такой код: DisplayFancyMessage(backgroundColor: ConsoleColor.Green); Как не трудно заметить, необязательные аргументы и именованные параметры действительно часто работают бок о бок. Чтобы завершить рассмотрение деталей по­ строения методов в С#, необходимо обязательно ознакомиться с понятием перегрузки методов. Исходный код. Приложение FunWithEnums доступно в подкаталоге Chapter 4. Перегрузка методов Как и в других современных объектно-ориентированных языках программирования, в C# можно перегружать (overload) методы. Перегруженными называются методы с на­ бором одинаково именованных параметров, отличающиеся друг от друга количеством (или типом) параметров. Глава 4. Главные конструкции программирования на С#: часть II 159 Чтобы оценить полезность перегрузки методов, давайте представим себя на месте разработчика, использующего Visual Basic 6.0, и предположим, что требуется создать набор методов, возвращающих сумму значений различных типов (Integer, Double и т.д.). Из-за того, что в VB6 перегрузка методов не поддерживается, для решения этой задачи придется определить уникальный набор методов, каждый из которых, по сути, будет делать одно и тоже (возвращать сумму аргументов): ' Примеры кода на VB6. Public Function A d d ln ts(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, в C# возможно создавать обобщенные методы, которые переводят концепцию перегрузки на новый уровень. С использованием обобщений для реали­ заций методов можно определять так называемые “заполнители", которые будут заполняться во время вызова этих методов. Чтобы попрактиковаться с перегруженными методами, создадим новый проект 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 ** * **\n "); // Вызов in t -версии Add() Console.WriteLine(Add(10, 10)); ' // Вызов long-версии Add() Console.WriteLine(Add(900000000000, 900000000000)); 160 Часть II. Главные конструкции программирования на C# // Вызов double-версии Add() Console .WnteLine (Add (4.3, 4.4)); Console.ReadLine(); } ШЕ-среда Visual Studio 2010 обеспечивает помощь при вызове перегруженных ме­ тодов. При вводе имени перегруженного метода (как, например, хорошо знакомого Console .WnteLine ( ) ) в списке IntelliSense предлагаются все его доступные версии. Обратите внимание, что по списку можно легко перемещаться, щелкая на кнопках со стрелками вниз и вверх (рис. 4.1). Рис. 4.1. Окно IntelliSense, отображаемое в Visual Studio 2010 при работе с перегруженными методами На заметку! Приложение MethodOverloading доступно в подкаталоге Chapter 4. На этом обзор основных деталей создания методов с использованием синтаксиса C# завершен. Теперь давайте посмотрим, как в C# создавать и манипулировать массивами, перечислениями и структурами, и завершим главу изучением того, что собой представ­ ляют “нулевые типы данных” и такие поддерживаемые в C# операции, как ? и ??. Массивы в C# Как, скорее всего, уже известно, массивом (array) называется набор элементов дан­ ных, доступ к которым получается по числовому индексу. Если говорить более конкрет­ но, то любой массив, по сути, представляет собой ряд связанных между собой элемен­ тов данных одинакового типа (например, массив int, массив string, массив SportCar и т.п.). Объявление массива в C# осуществляется довольно понятным образом. Чтобы посмотреть, как именно, давайте создадим новый проект типа Console Application (по имени FunWithArrays) и добавим в него вспомогательный метод SimpleArrays ( ) , вы­ зываемый из М а ш (). class Program { static void Main(string[] args) { Console .WnteLine ("**** * Fun with Arrays *****"); SimpleArrays(); Console.ReadLine(); Глава 4. Главные конструкции программирования на С#: часть II 161 static void SimpleArrays () { Console.WriteLine ("=> Simple Array Creation."); // Создание массива in t с тремя элементами {0, 1, 2 }. int[] mylnts = new int[3]; // Инициализация массива с 100 элементами s t r in g , // проиндексированными от 0 до 99. string[] booksOnDotNet = new string[100]; Console.WriteLine (); Внимательно почитайте комментарии в коде. При объявлении массива с помощью такого синтаксиса указываемое в объявлении число обозначает общее количество эле­ ментов, а не верхнюю границу. Кроме того, нижняя граница в массиве всегда начинает­ ся с 0. Следовательно, в результате объявления i n t [ ] m ylnts = new i n t [3] получается массив, содержащий три элемента, проиндексированных по позициям 0, 1,2. После определения переменной массива можно переходить к его заполнению элемен­ тами от индекса к индексу, как показано ниже на примере метода S im pleA rrays () : 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 (1 ); Console.WriteLine (); } На заметку! Следует иметь в виду, что если массив только объявляется, но явно не инициализиру­ ется, каждый его элемент будет установлен в значение, принятое по умолчанию для соответст­ вующего типа данных (например, элементы массива типа bool будут устанавливаться в fa ls e , а элементы массива типа in t — в 0). Синтаксис инициализации массивов в C# Помимо заполнения массива элемент за элементом, можно также заполнять его с использованием специального синтаксиса инициализации массивов. Для этого необхо­ димо перечислить включаемые в массив элемент в фигурных скобках ({ }). Такой син­ таксис удобен при создании массива известного размера, когда нужно быстро задать его начальные значения. Ниже показаны альтернативные версии объявления массива. 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. Главные конструкции программирования на C# // Синтаксис инициализации массива без применения // ключевого слова 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."); // В действительности a - массив типа i n t [ ] . var a = new[] { 1, 10, 100, 1000 }; Console .WriteLine ("a is a: {0}", a .ToStnng () ) ; // В действительности b - массив типа doublet] . var b = new [] { 1, 1.5, 2, 2.5 }; Console.WriteLine("b is a: {0}", b .ToString()); // В действительности с - массив типа s t r in g [ ] . var с = new[] { "hello", null, "world" }; Console.WriteLine("c is a: {0}", c .ToString()); Console.WriteLine(); } Разумеется, как и при создании массива с использованием явного синтаксиса С#, элементы, указываемые в списке инициализации массива, должны обязательно иметь один и тот же базовый тип (т.е. должны все быть intr string или SportsCar). В отли­ чие от возможных ожиданий, неявно типизированный локальный массив не получает по умолчанию тип SystemObject, поэтому приведенный ниже код вызывает ошибку на этапе компиляции: // Ошибка! Используются смешанные типы! var d = new[J { 1, "one", 2, "two", false }; Глава 4. Главные конструкции программирования на С#: часть II 163 Определение массива объектов В большинстве случаев при определении массива тип элемента, содержащегося в массиве, указывается явно. Хотя на первый взгляд это выглядит довольно понятно, су­ ществует одна важная особенность. Как будет объясняться в главе 6, в основе каждого типа в системе типов .NET (в том числе фундаментальных типов данных) в конечном итоге лежит базовый класс S ystem .O b ject. В результате получается, что в случае оп­ ределения массива объектов находящиеся внутри него элементы могут представлять собой что угодно. Например, рассмотрим показанный ниже метод A rr a y O fO b je c ts () (который можно вызвать в Main () для целей тестирования). static void ArrayOfObjects () { Console.WriteLine ("=> Array of Objects."); // В массиве объектов могут содержаться элементы любого типа. object [] myObjects = new object [4]; myObjects[0] = 10 ; myObjects[l] = false; myObjects[2] = new DateTime(1969, 3, 24); myObjects[3] = "Form & Void"; foreach (object obj in myObjects) { // Вывод имени типа и значения каждого элемента массива. Console.WriteLine("Type: {0}, Value: {1}", obj.GetType(), obj); } Console.WriteLine(); } В коде сначала осуществляется проход циклом по содержимому массива m yObjects, а затем с использованием метода GetType () из System . Ob j e c t выводится имя базового типа и значения всех элементов. Вдаваться в детали метода System. Ob j e c t . GetType () на этом этапе пока не требуется; сейчас главное уяснить только то, что с помощью этого метода можно получить полностью уточенное имя элемента (получение информации о типах и службы рефлексии подробно рассматриваются в главе 15). Ниже показано, как будет выглядеть вывод после вызова A rra y O fO b je c ts () . => 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 Работа с многомерными массивами В дополнение к одномерным массивам, которые демонстрировались до сих пор, в C# также поддерживаются две разновидности многомерных массивов. Многомерные массивы первого вида называются прямоугольными массивами и содержат несколько измерений, где все строки имеют одинаковую длину. Объявляются и заполняются такие массивы следующим образом: static void RectMultidimensionalArray() { Console.WriteLine("=> Rectangular multidimensional array."); // Прямоугольный многомерный массив. int[,] myMatrix; myMatrix = new int[6,6]; 164 Часть II. Главные конструкции программирования на C# // Заполнение массива (6 * 6) . for(int i = 0; i < 6; i++) for(int j = 0; j < 6; j++) myMatrix[i, j] = i * j; // Вывод массива (6 * 6) . for(int i = 0; l < 6; i++) { for(int j = 0; j < 6; Ц++) Console.Write(myMatrix[i , j] + "\t"); Console .WnteLine () ; } Console .WnteLine () ; } Многомерные массивы второго вида называются зубчатыми (jagged) массивами и содержат некоторое количество внутренних массивов, каждый из которых может иметь собственный уникальный верхний предел, например: static void JaggedMultidimensionalArray() { Console.WriteLine("=> Jagged multidimensional array."); // Зубчатый многомерный массив (т .е . массив массивов) . // Здесь он будет состоять из 5 других массивов. int [] [] myJagArray = new int [5 ] [] ; // Создание зубчатого массива. for (int i = 0 ; i < myJagArray.Length; i++) myJagArray [i ] = 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 () методов R e c tM u ltid im e n s io n a lA r r a y () и J a g g e d M u ltid im e n s io n a lA rra y ( ). Рис. 4.2. Прямоугольные и зубчатые многомерные массивы Глава 4. Главные конструкции программирования на С#: часть II 165 Использование массивов в качестве аргументов и возвращаемых значений После создания массив можно передавать как аргумент или получать в виде возвра­ щаемого значения члена. Например, ниже приведен код метода PrintArray ( ), который принимает в качестве входного параметра массив значений int и выводит каждое из них в окне консоли, а также метод GetString () , который заполняет массив значения­ ми string и возвращает его вызывающему коду. static void PrintArray (int [] mylnts) { for(int i = 0; i < mylnts.Length; i++) Console.WriteLine("Item {0} is {1}", i, mylnts[1 ]); } static s t r in g [] 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} ; P rin tA rra y (a g e s ); // Получение массива в качестве возвращаемого значения. string[] strs = GetStringArray(); foreach(string s in strs) Console.WriteLine(s); Console.WriteLine() ; } К этому моменту должно уже быть понятно, как определять, заполнять и изучать со­ держимое массивов в С#. В завершение картины рассмотрим роль класса System. Array. Базовый класс S y s t e m .A r r a y Каждый создаваемый массив получает большую часть функциональности от класса System. Array. Общие члены этого класса позволяют работать с массивом с использо­ ванием полноценной объектной модели. В табл. 4.2 приведено краткое описание не­ которых наиболее интересных членов класса System. Array (полное описание этих и других членов данного класса можно найти в документации .NET Framework 4.0 SDK). Таблица 4.2. Некоторые члены класса System. A rray Член класса System. Array Описание Clear () Статический метод, который позволяет устанавливать для всего ряда элементов в массиве пустые значения (0 — для чисел, null — для объектных ссылок и false — для булевских выражений) СоруТо () Метод, который позволяет копировать элементы из исходного массива в целевой Length Свойство, которое возвращает информацию о количестве элементов в массиве 166 Часть II. Главные конструкции программирования на C# Окончание табл. 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. При построении той или иной системы зачастую удобно создавать набор симво­ лических имен, которые отображаются на известные числовые значения. Например, в случае создания системы начисления заработной платы может понадобиться ссы­ латься на сотрудников определенного типа (ЕтрТуре) с помощью таких констант, как VicePresident (вице-президент), Manager (менеджер), Contractor (подрядчик) и Grunt (рядовой сотрудник). Для этой цели в C# поддерживается понятие специальных пере­ числений. Например, ниже показано специальное перечисление по имени ЕтрТуре: // Специальное перечисление, enum ЕтрТуре { Manager, // = 0 Grunt, // = 1 Contractor, // = 2 VicePresident // = 3 } В перечислении ЕтрТуре определены четыре именованных константы, которые со­ ответствуют дискретным числовым значениям. По умолчанию первому элементу при­ сваивается значение 0, а всем остальным элементам значения присваиваются соглас­ но схеме п+1. При желании исходное значение можно изменять как угодно. Например, если в данном примере нумерация членов ЕтрТуре должна идти с 102 до 105, необхо­ димо поступить следующим образом: // Начинаем нумерацию со enum ЕтрТуре { Manager = 102, Grunt, // Contractor, // VicePresident // значения 102. = 103 = 104 = 105 Нумерация в перечислениях вовсе не обязательно должна быть последовательной и содержать только уникальные значения. Например, вполне допустимо (по той или иной причине) сконфигурировать перечисление ЕтрТуре показанным ниже образом: // Значения элементов в перечислении вовсе не обязательно // должны идти в последовательном порядке. enum ЕтрТуре { Manager = 10, Grunt = 1, Contractor = 100, VicePresident = 9 168 Часть II. Главные конструкции программирования на C# Управление базовым типом, используемым для хранения значений перечисления По умолчанию для хранения значений перечисления используется тип System. Int32 (который в C# называется просто int); однако при желании его легко заменить. Перечисления в C# можно определять аналогичным образом для любых других ключе­ вых системных типов (byte, short, int или long). Например, чтобы сделать для пере­ числения ЕтрТуре базовым тип byte, а не int, напишите следующий код: //На этот раз ЕшрТуре отображается на тип byte. enum ЕшрТуре : byte { Manager = 10, Grunt = 1, Contractor = 100, VicePresident = 9 } Изменять базовый тип перечислений удобно в случае создания таких приложений .NET, которые будут развертываться на устройствах с небольшим объемом памяти (та­ ких как поддерживающие .NET сотовые телефоны или устройства PDA), чтобы эконо­ мить память везде, где только возможно. Естественно, если для перечисления в качест­ ве базового типа указан byte, каждое значение в этом перечислении ни в коем случае не должно выходить за рамки диапазона его допустимых значений. Например, приве­ денная ниже версия ЕшрТуре вызовет ошибку на этапе компиляции, поскольку значе­ ние 999 не вписывается в диапазон допустимых значений типа byte: // Компилятор сообщит об ошибке! Значение 999 // является слишком большим для типа b y te 1 enum ЕшрТуре : byte { Manager = 10, Grunt = 1, Contractor = 100, VicePresident = 999 } Объявление переменных типа перечислений После указания диапазона и базового типа перечисление можно использовать вме­ сто так называемых “магических чисел”. Поскольку перечисления представляют собой не более чем просто определяемый пользователем тип данных, их можно применять в качестве возвращаемых функциями значений, параметров методов, локальных пере­ менных и т.д. Для примера давайте создадим метод по имени AskForBonus ( ), прини­ мающий в качестве единственного параметра переменную ЕшрТуре. На основе значе­ ния этого входного параметра в окне консоли будет выводиться соответствующий ответ на запрос о надбавке к зарплате. class Program { static void Main(string [] args) { Console.WnteLine ("**** Fun with Enums *****"); // Создание типа Contractor. ЕшрТуре emp = EmpType.Contractor; AskForBonus(emp); Console.ReadLine (); Глава 4. Главные конструкции программирования на С#: часть II 169 // Использование перечислений в качестве параметра. static void AskForBonus(EmpType e) { switch (e) { case EmpType.Manager: Console .WnteLine ("How about stock options instead?"); //H e желаете ли взамен фондовые опционы? 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 emp = EmpType.SalesManager; // Ошибка! Забыли указать имя перечисления ЕшрТуре перед значением Grunt! emp = Grunt; Тип S y s t e m .E n u m Интересный аспект перечислений в .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. Главные конструкции программирования на C# Заглянув в окно O bject Brow ser (Браузер объектов) в Visual Studio 2010, можно удо­ стовериться, что метод Enum. GetUnderlyingType () требует передачи в качестве пер­ вого параметра System.Туре. Как будет подробно разъясняться в главе 16, Туре пред­ ставляет описание метаданных для заданной сущности .NET. Один из возможных способов получения метаданных (как показывалось ранее) пре­ дусматривает применение метода Get Туре ( ), который является общим для всех типов в библиотеках базовых классов .NET. Другой способ состоит в использовании операции typeof, поддерживаемой в С#. Одно из преимуществ этого способа связано с тем, что он не требует объявления переменной сущности, описание метаданных которой требу­ ется получить: //На этот раз для извлечения информации / / о типе применяется операция typeof. Console .WnteLine ("EmpType uses a {0} for storage", Enum.GetUnderlyingType(typeof(EmpType))); Динамическое обнаружение пар “имя/значение” перечисления Помимо метода Enum.GetUnderlyingType () , все перечисления C# также поддержи­ вают метод по имени ToString ( ) , который возвращает имя текущего значения пере­ числения в виде строки. Ниже приведен соответствующий пример кода: static void Main(string[] args) { Console.WriteLine ("**** Fun with Enums *****"); EmpType emp = EmpType.Contractor; // Выводит строку "emp is a C on tractor". Console.WriteLine("emp is a {0}.", emp.ToString()); Console.ReadLine() ; 1 Чтобы выяснить не имя, а значение определенной переменной перечисления, можно просто привести ее к лежащему в основе типу хранения. Ниже показан пример, как это делается: 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 {0}", e .GetType().Name); Console.WriteLine("Underlying storage type: {0}", Enum.GetUnderlyingType(e.GetType())); // Получение всех nap "имя/значение" // для входн ого п ар ам етр а. Array enumData = Enum.GetValues(е.GetType()); Console.WriteLine("This enum has {0} members.", enumData.Length); // Количество членов: / Л Вы вод с т р о к о в о г о и м ен и и а с с о ц и и р у е м о г о з н а ч е н и я / / с и сп о л ь зо в а н и е м ф л ага D д л я ф орм ати рован и я for(int i = 0 ; (см . гл а в у 3) . i < enumData.Length; i++) { Console.WriteLine ("Name: {0}, Value: {0:D}", enumData.GetValue (i)); } Console.WriteLine(); Чтобы протестировать этот новый метод, давайте обновим Main () так, чтобы в нем создавались переменные нескольких объявленных в пространстве имен System типов перечислений (вместе с перечислением ЕшрТуре): static void Main(string[] args) { Console.WriteLine("**** Fun with Enums *****"); EmpType e2 = EmpType.Contractor; // Эти типы представляют собой перечисления //и з пространства имен System. DayOfWeek day = DayOfWeek.Monday; ConsoleColor cc = ConsoleColor.Gray; EvaluateEnum(e2); EvaluateEnum(day); EvaluateEnum(cc); Console.ReadLine(); } На рис. 4.3 показано, как будет выглядеть вы­ вод в этом случае. Как можно будет убедиться по ходу настоящей книги, перечисления очень широко применяют­ ся во всех библиотеках базовых классов .NET. Например, в ADO.NET множество перечислений используется для обозначения состояния соеди­ нения с базой данных (например, открыто оно или закрыто) и состояния строки в D a ta T a b le (например, является она измененной, новой или отсоединенной). Поэтому в случае применения любых перечислений следует всегда помнить о наличии возможности взаимодействовать с па­ рами “имя/значение” в них с помощью членов System.Enum. Исходный код. Проект FunWithEnums доступен в подкаталоге Chapter 4.________ »_____________________ рис 4 3 Динамическое получеНие пар “имя/значение" типов перечислений 172 Часть II. Главные конструкции программирования на C# Типы структур Теперь, когда роль типов перечислений должна быть ясна, давайте рассмотрим ис­ пользование типов структур (s t r u c t ) в .NET. Типы структур прекрасно подходят для моделирования математических, геометрических и прочих “атомарных” сущностей в приложении. Они (как и перечисления) представляют собой определяемые пользовате­ лем типы, однако (в отличие от перечислений) просто коллекциями пар “имя/значение” не являются. Вместо этого они скорее представляют собой типы, способные содержать любое количество полей данных и членов, выполняющих над ними какие-то операции. На заметку! Если вы ранее занимались объектно-ориентированным программированием, можете считать структуры "облегченными классами” , поскольку они тоже предоставляют возможность определять тип, поддерживающий инкапсуляцию, но не могут применяться для построения се­ мейства взаимосвязанных типов. Когда есть потребность в создании семейства взаимосвязан­ ных типов через наследование, нужно применять типы классов. На первый взгляд процесс определения и использования структур выглядит очень просто, но, как известно, сложности обычно скрываются в деталях. Чтобы приступить к изучению основных аспектов структур, давайте создадим новый проект по имени F u n W ith S tru ctu res. В C# структуры создаются с помощью ключевого слова s tr u c t. Поэтому далее определим в проекте с использованием этого ключевого слова структуру P o in t и добавим в нее две переменных экземпляра типа i n t и несколько методов для взаимодействия с ними. 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.WriteLine("X = {0}, Y = {1}", X, Y) ; } } Здесь определены два целочисленных поля (X и Y) с использованием ключевого слова p u b lic , которое представляет собой один из модификаторов управления доступом (см. главу 5). Объявление данных с использованием ключевого слова p u b lic гарантирует наличие у вызывающего кода возможности напрямую получать к ним доступ из данной переменной P o in t (через операцию точки). На заметку! Обычно определение общедоступных (public) данных внутри класса или структуры считается плохим стилем. Вместо этого рекомендуется определять приватные (private) дан­ ные и получать к ним доступ и изменять их с помощью общ едоступны х свойств. Более под­ робно об этом будет рассказываться в главе 5. Глава 4. Главные конструкции программирования на С#: часть II 173 Ниже приведен код метода Main ( ), с помощью которого можно протестировать тип Point. static void Main(string [] args) { Console.WriteLine ("***** A First Look at Structures *****"); // Создание Point с первоначальными значениями X и Y. Point myPoint; myPoint.X = 349; myPoint.Y = 76; myPoint.Display (); // Настройка значений X и Y. myPoint.Increment (); myPoint.Display (); Console.ReadLine (); } Вывод, как и следовало ожидать, выглядит следующим образом: ***** A First Look at Structures ***** X = 34 9, Y = 76 X = 350, Y = 77 Создание переменных типа структур Для создания переменной типа структуры на выбор доступно несколько вариантов. Ниже просто создается переменная P o in t с присваиванием значений каждому из ее общедоступных элементов данных типа полей перед вызовом ее членов. Если значения общедоступным элементам структуры (в данном случае X и Y) не присвоены перед ее использованием, компилятор сообщит об ошибке. // Ошибка! Полю Y не было присвоено значение. Point pi; pl.X = 10; pi .Display(); // Все в порядке1 Обоим полям были присвоены // значения перед использованием. Point р2; р2 .X = 10; р2 .Y = 10; ' р 2 .Display(); В качестве альтернативного варианта переменные типа структур можно создавать с применением ключевого слова new, поддерживаемого в С#, что предусматривает вызов для структуры конструктора по умолчанию. По определению используемый по умолча­ нию конструктор не принимает никаких аргументов. Преимущество подхода с вызовом для структуры конструктора по умолчанию состоит в том, что в таком случае каждому элементу данных полей автоматически присваивается соответствующее значение по умолчанию: // Установка для всех полей значений по умолчанию // за счет применения конструктора по умолчанию. Point pi = new Point () ; // Выводит Х=0, Y=0 pi.Display(); Создавать структуру можно также с помощью специального конструктора что по­ зволяет указывать значения для полей данных при создании переменной, а не уста­ 174 Часть II. Главные конструкции программирования на C# навливать их для каждого из них по отдельности. В главе 5 специальные конструкторы будут рассматриваться более подробно; здесь же, чтобы в общем посмотреть, как их использовать, изменим структуру Point и добавим в нее следующий код: struct Point { // Поля структуры. public int X; public int Y; // Специальный конструктор. public Point (int XPos, int YPos) { X = XPos; Y = YPos; После этого переменные типа Point можно создавать так, как показано ниже: // Вызов специального конструктора. Point р2 = new Point (50, 60); // Выводит Х=50, Y=60 р 2 .Display (); Как упоминалось ранее, на первый взгляд процесс работы со структурами выглядит довольно понятно. Однако чтобы лучше разобраться в нем, необходимо знать, в чем состоит разница между типами значения и ссылочными типами в .NET. Исходный код. Проект FunWithStructures доступен в подкаталоге Chapter 4. Типы значения и ссылочные типы На заметку! В приведенном далее обсуждении типов значения и ссылочных типов предполагается наличие базовых знаний объектно-ориентированного программирования. Дополнительные све­ дения по этому поводу даны в главах 5 и 6. В отличие от массивов, строк и перечислений, структуры в C# не имеют эквивалент­ ного представления с похожим названием в библиотеке .NET (т.е. класса вроде System. Structure не существует), но зато они все неявно унаследованы от класса System. ValueType. Попросту говоря, роль класса System. ValueType заключается в гарантиро­ вании размещения производного типа (например, любой структуры) в стеке, а не в куче с автоматически производимой сборкой мусора. Данные, размещаемые в стеке, могут создаваться и уничтожаться очень быстро, поскольку срок их жизни зависит только от контекста, в котором они определены. За данными, размещаемыми в куче, наблюдает сборщик мусора .NETT, и время их существования зависит от целого ряда различных факторов, которые более подробно рассматриваются в главе 8. С функциональной точки зрения единственной задачей System.ValueType являет­ ся переопределение виртуальных методов, объявленных в System.Object, так, чтобы в них использовалась семантика, основанная на значениях, а не на ссылках. Как, воз­ можно, уже известно, под переопределением понимается изменение реализации вирту­ ального (или, что тоже возможно, абстрактного) метода, определенного внутри базового класса. Базовым классом для ValueType является System.Object. В действительности Глава 4. Главные конструкции программирования на С#: часть II 175 методы экземпляра, определенные в System. ValueType, идентичны тем, что определе­ ны в System.Object: // Структуры и перечисления неявным образом // расширяют возможности System.ValueType. public abstract class ValueType : object { public public public public virtual bool Equals(object obj ) ; virtual int GetHashCode(); Type GetTypeO ; virtual string ToStringO; } Из-за того, что в типах значения используется семантика, основанная на значени­ ях, время жизни структуры (которая включает все числовые типы данных, наподобие int, float и т.д., а также любое перечисление или специальную структуру) получается очень предсказуемым. При выходе переменной типа структуры за пределы контекста, в котором она определялась, она сразу же удаляется из памяти. // Локальные структуры извлекаются из стека // после завершения метода. static void LocalValueTypes() { //В действительности in t представляет // собой структуру System.Int32. int i = 0; //В действительности Point представляет // собой тип структуры. Point р = 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 (10, 10); Point p2 = pi; // Вывод обеих переменных Point, pi .Display(); p2 .Display(); // Изменение значение pl.X и повторный вывод. // Значение р2.Х не изменяется. pl.X = 100; Console.WriteLine("\n=> Changed pl.X\n"); pi .Display(); p2.Display(); } 176 Часть II. Главные конструкции программирования на C# В коде сначала создается переменная типа P o in t (pi), которая затем присваивается другой переменной типа P o in t (р2). Из-за того, что P o in t представляет собой тип зна­ чения, в стеке размещаются две копии MyPoint, каждая из которых может подвергать­ ся отдельным манипуляциями. Поэтому при изменении значения p i .X значение р2 .X остается неизменным. Ниже показано, как будет выглядеть вывод в случае выполнения этого кода: Assigning value types X = 10, Y = 10 X = 10, Y = 10 => Changed p i .X X = 100, Y = 10 X = 10, Y = 10 В отличие от присваивания одного типа значения другому, в случае применения операции присваивания в отношении ссылочных типов (т.е. всех экземпляров класса) происходит переадресация на то, на что ссылочная переменная указывает в памяти. Чтобы увидеть это на примере, давайте создадим новый тип класса по имени P o in tR e f с точно такими же членами, как у структуры P o in t, только переименуем конструктор в соответствие с именем этого класса: // Классы всегда представляют собой ссылочные типы. class PointRef { // Те же члены, что и в структуре Point. . . // Здесь нужно не забыть изменить имя конструктора на PointRef. public PointRef(int XPos, int YPos) { X = XPos; Y = YPos; } } Теперь воспользуемся этим типом P o in t R e f в показанном ниже новом методе. Обратите внимание, что за исключением применения класса P o in tR e f, а не структу­ ры P o in t, код в целом выглядит точно так же, как и код приведенного ранее метода V a lu eT yp eA ssign m en t( ) . static void ReferenceTypeAssignment () { Console.WriteLine("Assigning reference types\n"); PointRef pi = new PointRef (10, 10); PointRef p2 = pi; // Вывод обеих переменных PointRef. p i .Display(); p 2 .Display(); // Изменение значения pl.X и повторный вывод. pl.X = 100; Console.WriteLine("\n=> Changed pl.X\n"); p i .Display(); p 2 .Display(); } В данном случае создаются две ссылки, указывающие на один и тот же объект в управляемой куче. Поэтому при изменении значения X по ссылке р2 значение p l.X ос­ тается прежним. Ниже показано, как будет выглядеть вывод в случае вызова этого но­ вого метода из Main (). Глава 4. Главные конструкции программирования на С#: часть II 177 Assigning reference types X = 10, Y = 10 X = 10, Y = 10 => Changed p i .X X = 100, Y = ‘10 X = 100, Y = 10 Типы значения, содержащие ссылочные типы Теперь, когда стало более понятно, в чем состоят основные отличия между типами значения и ссылочными типами, давайте рассмотрим более сложный пример. Сначала предположим, что в распоряжении имеется следующий ссылочный тип (класс) с инфор­ мационной строкой (in fo S t r in g ), которая может устанавливаться с помощью специ­ ального конструктора: class Shapelnfo { public string infoString; public Shapelnfo(string info) { infoString = info; } 1 Пусть необходимо, чтобы переменная этого типа класса содержалась внутри типа значения по имени R ecta n gle, и чтобы вызывающий код мог устанавливать значение внутренней переменной экземпляра S h apeln fo, для чего также можно предусмотреть специальный конструктор. Ниже показано полное определение типа R ec ta n gle. 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.inf oStnng, rectTop, rectBottom, rectLeft, rectRight); } } Ссылочный тип содержится внутри типа значения, и отсюда, естественно, вытекает важный вопрос о том, что же произойдет в результате присваивания одной переменной типа R e c ta n g le другой? Исходя из того, что известно о типах значения, можно верно предположить, что целочисленные данные (которые в действительности образуют струк­ туру) должны являться независимой сущностью для каждой переменной R ec ta n g le. Но что будет с внутренним ссылочным типом? Будут ли копироваться все данные о состоя­ нии этого объекта, или же только ссылка на него? Чтобы получить ответ на этот вопрос, определим показанный ниже метод и вызовем его в Main (). 178 Часть II. Главные конструкции программирования на C# static void ValueTypeContainingRefType () { // Создание первой переменной Rectangle. Console .WnteLine ("-> Creating rl"); Rectangle rl = new Rectangle ("First Rect", 10, 10, 50, 50); // Присваивание второй переменной Rectangle ссылки на rl. Console .WnteLine ("-> Assigning r2 to rl"); Rectangle r2 = rl; // Изменение некоторых значений в г2. Console.WnteLine ("-> Changing values of r2"); r2.rectInfо .infoString = "This is new info1"; r2.rectBottom = 4444; // Вывод обеих переменных. 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, значение ссылки r l остается прежним. По умолчанию, когда внут­ ри типа значения содержатся другие ссылочные типы, операция присваивания при­ водит к копированию ссылок. В результате получаются две независимых структуры, в каждой из которых содержится ссылка, указывающая на один и тот же объект в памяти (т.е. поверхностная копия). При желании получить детальную копию, при которой в но­ вый объект копируются полностью все данные состояния внутренних ссылок, в качест­ ве одного из способов можно реализовать интерфейс ICloneable (см. главу 9). Исходный код. Проект ValueAndReferenceTypes доступен В подкаталоге Chapter 4. Передача ссылочных типов по значению Очевидно, что ссылочные типы и типы значения могут передаваться членам в виде параметров. Способ передачи ссылочного типа (например, класса) по ссылке, однако довольно сильно отличается от способа его передачи по значению. Чтобы посмотреть, в чем разница, давайте создадим новый проект типа C o n so le Application по имени RefTypeValTypeParams и определим в нем следующий простой класс 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) { // Изменение значения возраста в р. р.personAge = 99; // Увидит ли вызывающий код это изменение? р = new Person("Nikki", 99); } Важно обратить внимание, что метод SendAPersonByValue () пытается присвоить входному аргументу Person ссылку на новый объект Person, а также изменить некото­ рые данные состояния. Испробуем этот метод, вызвав его в Main ( ): static void Main(string [] args) { // Передача ссылочных типов no значению. 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) { // Изменение некоторых данных в р. р.personAge = 555; 180 Часть II. Главные конструкции программирования на C# // Теперь р указывает на новый объект в куче. р = new Person("Nikki", 222); } Нетрудно догадаться, что такой подход предоставляет вызывающему коду полную свободу в плане манипулирования входным параметром. Вызывающий код может не только изменять состояние объекта, но и переопределять ссылку так, чтобы она указыва­ ла на новый тип Person. Давайте испробуем новый метод SendAPersonByReference ( ) , вызвав его в методе 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: 23 After by ref call, Person is: Name: Nikki, Age: 999 Как не трудно заметить, после вызова объект по имени Mel возвращается как объект по имени Nikki, поскольку методу удалось изменить тип, на который указывала входная ссылка в памяти. Ниже перечислены главные моменты, которые можно вынести из все­ го этого и о которых следует всегда помнить при передаче ссылочных типов. • В случае передачи ссылочного типа по ссылке вызывающий код может изменять значения данных состояния объекта, а также сам объект, на который указывает входная ссылка. • В случае передачи ссылочного типа по значению вызывающий код может изме­ нять только значения данных состояния объекта, но не сам объект, на который указывает входная ссылка. Исходный код. Проект RefTypeValTypeParams доступен В подкаталоге Chapter 4. Заключительные детали относительно типов значения и ссылочных типов В завершение темы ссылочных типов и типов значения ознакомьтесь с табл. 4.3, где приведено краткое описание всех основных отличий между типами значения и ссылоч­ ными типами. Несмотря на все эти отличия, не следует забывать о том, что типы значения и ссылочные типы обладают способностью реализовать интерфейсы и могут поддержи­ вать любое количество полей, методов, перегруженных операций, констант, свойств и событий. Глава 4. Главные конструкции программирования на С#: часть II 181 Таблица 4.3. Отличия между типами значения и ссылочными типами Интересующий вопрос Тип значения Ссылочный тип Где размещается этот тип? В стеке В управляемой куче Как представляется переменная? В виде локальной копии В виде ссылки, указываю­ щей на занимаемое соот­ ветствующим экземпляром место в памяти Какой тип является базовым? Должен обязательно на­ следоваться от System. ValueType Может наследоваться от любого другого типа (кроме System.ValueType), глав­ ное чтобы тот не был запеча­ танным (см. главу 6) Может ли этот тип выступать в роли базового для других типов? Нет. Типы значения всегда являются запечатанными, поэтому наследовать от них нельзя Да. Если тип не является запечатанным, он может вы­ ступать в роли базового типа для других типов Каково по умолчанию поведение при передаче параметров? Переменные этого типа пе­ редаются по значению (т.е. вызываемой функции пере­ дается копия переменной) В случае типов значения объект копируется по значе­ нию, а в случае ссылочных типов ссылка копируется по значению Может ли в этом типе переоп­ ределяться метод System. Нет. Типы значения, никогда не размещаются в куче и потому в финализации не нуждаются Да, неявным образом (см. главу 8) Можно ли для этого типа опреде­ лять конструкторы? Да, но при этом следует помнить, что имеется заре­ зервированный конструктор по умолчанию (это значит, что все специальные конст­ рукторы должны обязатель­ но принимать аргументы) Безусловно! Когда переменные этого типа пре­ кращают свое существование? Когда выходят за рамки того контекста, в котором определялись Когда объект подвергается сборке мусора Object.Finalize()? Нулевые типы в C# В заключение настоящей главы давайте рассмотрим роль нулевых типов дан­ ных (nullable data types) на примере создания консольного приложения по имени NullableTypes. Как уже известно, типы данных CLR обладают фиксированным диа­ пазоном значений и имеют соответствующий представляющий их тип в пространст­ ве имен System. Например, типу данных System.Boolean могут присваиваться только значения из набора {true, false). Вспомните, что все числовые типы данных (а также тип Boolean) представляют собой типы значения. Таким типам значение null никогда не присваивается, поскольку оно служит для установки пустой ссылки на объект: static void Main(string[] args) { // Компилятор сообщит об ошибке! // Типам значения не может присваиваться значение n u ll! 182 Часть II. Главные конструкции программирования на C# bool myBool = null; int mylnt = null; // Здесь все в порядке, потому что строки представляют собой ссылочные типы. string my S t n n g = null; } Возможность создавать нулевые типы появилась еще в версии .NET 2.0. Попросту говоря, нулевой тип может принимать все значения лежащего в его основе типа плюс значение null. Если объявить нулевым, например, тип bool, его допустимыми значе­ ниями будут true, false и null. Это может оказаться чрезвычайно удобным при ра­ боте с реляционными базами данных, поскольку в их таблицах довольно часто встре­ чаются столбцы с неопределенными значениями. Помимо нулевого типа данных в C# больше не существует никакого удобного способа для представления элементов данных, не имеющих значения. Чтобы определить переменную нулевого типа, необходимо присоединить к имени лежащего в основе типа данных знак вопроса (?). Обратите внимание, что примене­ ние такого синтаксиса является допустимым лишь в отношении типов значения. При попытке создать нулевой ссылочный тип (в том числе нулевой тип string) компиля­ тор будет сообщать об ошибке. Как и ненулевым переменным, нулевым локальным пе­ ременным должно быть присвоено начальное значение, прежде чем их можно будет использовать. static void LocalNullableVariables () { // Определение нескольких локальных переменных с нулевыми типами. int? nullablelnt = 10; double? nullableDouble = 3.14; bool? nullableBool = null; char? nullableChar = 'a'; int?[] arrayOfNullablelnts = new int? [10]; // Ошибка! Строки относятся к ссылочным типам! // string? s = "oops"; } Синтаксис с использованием знака ? в качестве суффикса в C# представля­ ет собой сокращенный вариант создания экземпляра обобщенного типа структуры System.Nullable<T>. Хотя об обобщениях будет подробно рассказываться лишь в гла­ ве 10, уже сейчас важно понять, что тип System.Nullable<T> имеет набор членов, ко­ торые могут применяться во всех нулевых типах. Например, выяснить программным образом, было ли нулевой переменной действи­ тельно присвоено значение null, можно с помощью свойства Has Value или операции ! =, а получить присвоенное нулевому типу значение — либо напрямую, либо через свой­ ство 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? numencValue = null; public bool? boolValue = true; // Обратите внимание на использование // нулевого возвращаемого типа. public int? GetlntFromDatabase () { return numencValue; } // Здесь тоже обратите внимание на использование // нулевого возвращаемого типа. public bool? GetBoolFromDatabase () { return boolValue; } } Теперь давайте создадим следующий метод Main (), в котором будет вызываться ка­ ждый из членов класса DatabaseReader и выясняться, какие значения были им при­ своены, с помощью членов HasValue и Value, а также поддерживаемой в C# операции равенства (точнее — операции “не равно”): static void Main(string[] args) { Console.WriteLine (''***** Fun with Nullable Data *****\n"); DatabaseReader dr = new DatabaseReader (); // Получение значения in t из "базы данных". int? i = dr.GetlntFromDatabase(); if (i.HasValue) // вывод значения l Console.WriteLine("Value of 'i' is: {0}", i.Value); else // значение i не определено Console.WriteLine("Value of 'i ' 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. Главные конструкции программирования на C# Операция ‘ ? ? 5 Последним аспектом нулевых типов, о котором следует знать, является использова­ ние с ними операции ??, поддерживаемой в С#. Эта операция позволяет присваивать значение нулевому типу, если извлеченное значение равно n u ll. Для примера предпо­ ложим, что в случае возврата методом GetlntFrom D atabase () значения n u ll (разуме­ ется, этот метод запрограммирован всегда возвращать n u ll, но тут главное уловить об­ щую идею) локальной целочисленной переменной нулевого типа должно присваиваться значение 100: static void Main(string [] args) { Console .WnteLine ("***** Fun with Nullable Data *****\n"); DatabaseReader dr = new DatabaseReader(); // В случае возврата GetlntFromDatabase() // значения n u ll локальной переменной должно // присваиваться значение 100. int myData = d r .GetlntFromDatabase() ?? 100; Console .WnteLine ("Value of myData: {0}", myData); Console.ReadLine(); } Преимущество подхода с применением операции ? ? в том, что он обеспечивает бо­ лее компактную версию кода, чем применение традиционной условной конструкции i f / e ls e . При желании можно написать следующий функционально эквивалентный код, который в случае n u ll устанавливает значение переменной равным 100: // Более длинная версия применения синтаксиса ? : ??. int? moreData = dr .GetlntFromDatabase(); if (!moreData.HasValue) moreData = 100; Console.WriteLine("Value of moreData: {0}", moreData); Исходный код. Приложение NullableType доступно в подкаталоге Chapter 4. Резюме В настоящей главе сначала рассматривался ряд ключевых слов С#, которые позво­ ляют создавать специальные методы. Вспомните, что по умолчанию параметры переда­ ются по значению, однако их можно передавать и по ссылке за счет добавления к ним модификатора r e f или out. Также было рассказано о роли необязательных и именован­ ных параметров и о том, как определять и вызывать методы, принимающие массивы параметров. В главе рассматривалась перегрузка методов, определение массивов, перечислений и структур в C# и их представления в библиотеках базовых классов .NET. Были описаны некоторые детали, касающиеся т ипов значения и ссылочных типов, в том числе то, как они ведут себя при передаче в качестве параметров методам, и то, каким образом взаимодействовать с нулевыми типами данных с помощью операций ? и ? ?. На этом первоначальное знакомство с языком программирования C# заверше­ но. В следующей главе начинается изучение деталей объектно-ориентированной разработки. ГЛАВА 5 Определение инкапсулированных типов классов предыдущих двух главах мы исследовали ряд основных синтаксических конст­ рукций, присущих любому приложению .NET, которое вам придется разрабаты­ вать. Здесь мы приступим к изучению объектно-ориентированных возможностей С#. Первое, что предстоит узнать — это процесс построения четко определенных типов классов, которые поддерживают любое количество конструкторов. После описания ос­ нов определения классов и размещения объектов в остальной части главы рассматри­ вается тема инкапсуляции. По ходу изложения вы узнаете, как определяются свойства класса, а также какова роль статических полей, синтаксиса инициализации объектов, полей, доступных только для чтения, константных данных и частичных классов. В Знакомство с типом класса C# Что касается платформы .NET, то наиболее фундаментальной программной конст­ рукцией является тип класса. Формально класс — это определяемый пользователем тип, который состоит из данных полей (часто именуемых переменными-членами) и чле­ нов, оперирующих этими данными (конструкторов, свойств, методов, событий и т.п.). Все вместе поля данных класса представляют “состояние” экземпляра класса (иначе называемого объектом). Мощь объектных языков, подобных С#, состоит в их способно­ сти группировать данные и связанную с ними функциональность в определении класса, что позволяет моделировать программное обеспечение на основе сущностей реального мира. Для начала создадим новое консольное приложение C# по имени SimpleClassExample. Затем добавим в проект новый файл класса (по имени Car.cs), используя пункт меню Project^Add Class (ПроектаДобавить класс), выберем пиктограмму Class (Класс) в ре­ зультирующем диалоговом окне, как показано на рис. 5.1, и щелкнем на кнопке Add (Добавить). Класс определятся в C# с помощью ключевого слова c la s s . Вот как выглядит про­ стейшее из возможных объявление класса: class Саг { } 186 Часть II. Главные конструкции программирования на C# Рис. 5.1. Добавление нового типа класса C# После определения типа класса нужно определить набор переменных-членов, ко­ торые будут использоваться для представления его состояния. Например, вы можете решить, что объекты Саг (автомобили) должны иметь поле данных типа in t, представ­ ляющее текущую скорость, и поле данных типа s t r in g для представления дружествен­ ного названия автомобиля. С учетом этих начальных положений дизайна класс Саг будет выглядеть следующим образом: class Саг // 'Состояние' объекта Саг. public string petName; public int currSpeed; } Обратите внимание, что эти переменные-члены объявлены с использованием моди­ фикатора доступа p u b lic . Общедоступные (p u b lic ) члены класса доступны непосред­ ственно, как только создается объект данного типа. Как вам, возможно, уже известно, термин “объект” служит для представления экземпляра данного типа класса, созданно­ го с помощью ключевого слова new. На заметку! Поля данных класса редко (если вообще когда-нибудь) должны определяться с моди­ фикатором p u b lic . Чтобы обеспечить целостность данных состояния, намного лучше объявлять данные приватными (p r iv a te ) или, возможно, защищенными (p r o te c te d ) и открывать контро­ лируемый доступ к данным через свойства типа (как будет показано далее в этой главе). Однако чтобы сделать первый пример насколько возможно простым, оставим данные общедоступными. После определения набора переменных-членов, представляющих состояние класса, следующим шагом в проектировании будет создание членов, которые моделируют его поведение. Для данного примера класс Саг определяет один метод по имени Speedup () и еще один — по имени P r in t S t a t e (). Модифицируйте код класса следующим образом: class Саг { // 'Состояние' объекта Саг. public string petName; Глава 5. Определение инкапсулированных типов классов 187 public int currSpeed; // Функциональность Car. public void Pnn t S t a t e O { Console.WriteLine ("{0 } is going {1} MPH.", petName, currSpeed); } public void SpeedUp(int delta) { currSpeed += delta; Метод P r in t S ta t e () — это более или менее диагностическая функция, которая про­ сто выводит текущее состояние объекта Саг в окно командной строки. Метод SpeedUp () повышает скорость Саг, увеличивая ее на величину, переданную во входящем парамет­ ре типа in t. Теперь обновите код метода M ain(), как показано ниже: 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 (5) ; myCar.PrintState (); } Console.ReadLine (); Запустив программу, вы увидите, что переменная Car (myCar) поддерживает свое те­ кущее состояние на протяжении жизни всего приложения, как показано в следующем выводе: ★**** Fun with Class Types Henry Henry Henry Henry Henry Henry Henry Henry Henry Henry Henry is is is is is is is is is is is going going going going going going going going going going going 15 20 25 30 35 40 45 50 55 60 65 MPH. MPH. MPH. MPH. MPH. MPH. MPH. MPH. MPH. MPH. MPH. Размещение объектов с помощью ключевого слова new Как было показано в предыдущем примере кода, объекты должны быть размещены в памяти с использованием ключевого слова new. Если ключевое слово new не указать и попытаться воспользоваться переменной класса в следующем операторе кода, будет получена ошибка компиляции. Например, следующий метод M ain () компилироваться не будет: 188 Часть II. Главные конструкции программирования на C# static void Main(string [] args) { Console.WnteLine ("***** Fun with Class Types *****\n"); // Ошибка1 Забыли использовать new для создания объекта! Car myCar; myCar.petName = "Fred"; } Чтобы корректно создать объект с использованием ключевого слова new, можно оп­ ределить и разместить в памяти объект Саг в одной строке кода: static void Main(string [] args) { Console .WnteLine ("***** Fun with Class Types *****\n"); Car myCar = new Car () ; myCar.petName = "Fred"; } В качестве альтернативы, определение и размещение в памяти экземпляра класса может осуществляться в разных строках кода: static void Main(string[] args) { Console .WnteLine ("***** Fun with Class Types *****\n") ; Car myCar; myCar = new Car() ; myCar.petName = "Fred"; } Здесь первый оператор кода просто объявляет ссылку на еще не созданный объект типа Саг. Только после явного присваивания ссылка будет указывать на действитель­ ный объект в памяти. В любом случае, к этому моменту мы получили простейший тип класса, который определяет несколько элементов данных и некоторые базовые методы. Чтобы рас­ ширить функциональность текущего класса Саг, необходимо разобраться с ролью конструкторов. Понятие конструктора Учитывая, что объект имеет состояние (представленное значениями его перемен­ ных-членов), программист обычно желает присвоить осмысленные значения полям объекта перед тем, как работать с ним. В настоящий момент тип Саг требует присваи­ вания значений полям perName и cu г гSpeed. Для текущего примера это не слишком проблематично, поскольку общедоступных элементов данных всего два. Однако нередко классы состоят из нескольких десятков полей. Ясно, что было бы нежелательно писать 20 операторов инициализации для всех 20 элементов данных такого класса. К счастью, в C# поддерживается механизм конструкторов, которые позволяют ус­ танавливать состояние объекта в момент его создания. Конструктор (constructor) — это специальный метод класса, который вызывается неявно при создании объекта с исполь­ зованием ключевого слова new. Однако в отличие от “нормального” метода, конструктор никогда не имеет возвращаемого значения (даже void) и всегда именуется идентично имени класса, который он конструирует. Роль конструктора по умолчанию Каждый класс C# снабжается конструктором по умолчанию, который при необходи­ мости может быть переопределен. По определению такой конструктор никогда не при­ Глава 5. Определение инкапсулированных типов классов 189 нимает аргументов. После размещения нового объекта в памяти конструктор по ум ол­ чанию гарантирует установку всех полей в соответствующие стандартные значения (значениях по умолчанию для типов данных C# описаны в главе 3). Если вы не удовлетворены такими приеваиваниями по умолчанию, можете переоп­ ределить конструктор по умолчанию в соответствии со своими нуждами. Для иллю ст­ рации модифицируем класс С#, как показано ниже: class Саг { // 'Состояние' объекта Саг. public string petName; public int currSpeed; // Специальный конструктор no умолчанию. public Ca r () { petName = "Chuck"; currSpeed = 10; В данном случае мы заставляем объекты Саг начинать свою жизнь под именем Chuck и скоростью 10 миль в час. При этом создавать объекты со значениями по ум ол­ чанию можно следующим образом: я static void Main(string [] args) { Console .WnteLine ("***** Fun with Class Types *****\n"); // Вызов конструктора no умолчанию. Car chuck = new Car() ; // Печатает "Chuck is going 10 MPH." chuck.PrintState(); } Определение специальных конструкторов Обычно помимо конструкторов по умолчанию в классах определяются дополнитель­ ные конструкторы. При этом пользователь объекта обеспечивается простым и согла­ сованным способом инициализации состояния объекта непосредственно в момент его создания. Взгляните на следующее изменение класса Саг, который теперь поддержива­ ет целых три конструктора: class Саг { // 'Состояние' объекта Саг. public string petName; public int currSpeed; // Специальный конструктор no умолчанию. public Car() { petName = "Chuck"; currSpeed = 10; } // Здесь currSpeed получает значение //no умолчанию типа in t (О) . public Car(string pn) { petName = pn; } 190 Часть II. Главные конструкции программирования на C# // Позволяет вызывающему коду установить полное состояние Саг. public Car(string рп, int cs) { petName = рп; currSpeed = cs; } Имейте в виду, что один конструктор отличается от другого (с точки зрения компи­ лятора С#) количеством и типом аргументов. В главе 4 было показано, что определение методов с одним и тем же именем, но разным количеством и типами аргументов, назы­ вается перегрузкой. Таким образом, класс Саг имеет перегруженный конструктор, что­ бы предоставить несколько способов создания объекта во время объявления. В любом случае, теперь можно создавать объекты Саг, используя любой из его общедоступных, конструкторов. Например: static void Main(string[] args) { Console.WriteLine (''***** Fun with Class Types *****\n"); // Создать объект Car no имени Chuck со скоростью 10 миль в час. Car chuck = new Car() ; chuck.PnntState () ; // Создать объект Car no имени Mary со скоростью 0 миль в час. Саг шагу = new Car ("Магу" ) ; шагу.PrintState(); // Создать объект Саг по имени Daisy со скоростью 75 миль в час. Car daisy = new Car ("Daisy", 75); daisy.PrintState(); Еще раз о конструкторе по умолчанию Как вы только что узнали, все классы “бесплатно” снабжаются конструктором по умолчанию. Таким образом, если добавить в текущий проект новый класс по имени M o to rcycle , определенный следующим образом: class Motorcycle { public void PopAWheelyO { Console.WriteLine("Yeeeeeee Haaaaaeewww1"); } то сразу можно будет создавать экземпляры M o to rc y c le с помощью конструктора по умолчанию: static void Main(string [] args) { Console.WriteLine ("***** Fun with Class Types *****\n"); Motorcycle me = new Motorcycle(); m e .PopAWheely(); Однако, как только определен специальный конструктор, конструктор по умолчанию молча удаляется из класса и становится недоступным! Воспринимайте это так: если вы не определили специального конструктора, компилятор C# снабжает класс конструктором по умолчанию, чтобы позволить пользователю объекта размещать его в памяти с набором Глава 5. Определение инкапсулированных типов классов 191 данных, имеющих значения по умолчанию. В случае же, когда определяется уникальный конструктор, компилятор предполагает, что вы решили взять власть в свои руки. Таким образом, чтобы позволить пользователю объекта создавать экземпляр типа посредством конструктора по умолчанию, а также специального конструктора, пона­ добится явно переопределить конструктор по умолчанию. И, наконец, в подавляющем большинстве случаев реализация конструктора класса по умолчанию намеренно оста­ ется пустой, поскольку все, что требуется — это создание объекта со значениями всех полей по умолчанию. Внесем в класс Motorcycle следующие изменения: class Motorcycle { public int driverlntensity; public void PopAWheelyO { for (int 1 = 0 ; l <= driverlntensity; i++) { Console .WnteLine ("Yeeeeeee Haaaaaeewww!" ) ; } // Вернуть конструктор no умолчанию, который будет устанавливать // для всех членов данных значения по умолчанию. public Motorcycle () {} // Специальный конструктор. public Motorcycle(int intensity) { driverlntensity = intensity; } } Роль ключевого слова t h i s В языке C# имеется ключевое слово 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 (5); с .SetDriverName("Tiny"); с .PopAWheely (); Console.WnteLine ("Rider name is {0}", c.name) ; // Выводит пустое значение name! 192 Часть II. Главные конструкции программирования на C# Проблема в том, что реализация SetDriverName () выполняет присваивание входя­ щему параметру его же значения, поскольку компилятор предполагает, что name здесь ссылается на переменную, существующую в контексте метода, а не на поле паше в контексте класса. Для информирования компилятора, что необходимо установить зна­ чение поля данных текущего объекта, просто используйте t h i s для разрешения этой неоднозначности: public void SetDriverName(string name) { this.name == name; } Имейте в виду, что если неоднозначности нет, то вы не обязаны использовать клю­ чевое слово this, когда классу нужно обращаться к собственным данным или членам. Например, если переименовать член данных паше типа string в driverName (что так­ же потребует обновления метода Main()), то применение this станет не обязательным, поскольку исчезает неоднозначность контекста: class Motorcycle { public int dnverlntensity; public string driverName; public void SetDriverName(string name) { // Эти два оператора функционально эквивалентны. driverName == name; this .driverName == name; } } Помимо небольшого выигрыша от использования this в неоднозначных ситуаци­ ях, это ключевое слово может быть полезно при реализации членов, поскольку такие IDE-среды, как SharpDevelop и Visual Studio 2010, включают средство IntelliSense, когда вводится this. Это может здорово помочь, когда вы забыли название члена класса и хотите быстро вспомнить его определение. Взгляните на рис. 5.2. Рис. 5.2 . Активизация средства IntelliSense для this Глава 5. Определение инкапсулированных типов классов ■ 193 На заметку! Применение ключевого слова this внутри реализации статического члена приво­ дит к ошибке компиляции. Как будет показано, статические члены оперируют на уровне класса (а не объекта), а на этом уровне нет текущего объекта, потому и не существует this! Построение цепочки вызовов конструкторов с использованием t h i s Другое применение ключевого слова this состоит в проектировании класса, исполь­ зующего технику под названием сцепление конструкторов или цепочка конструкто­ ров (constructor chaining). Этот шаблон проектирования полезен, когда имеется класс, определяющий несколько конструкторов. Учитывая тот факт, что конструкторы часто проверяют входящие аргументы на соблюдение различных бизнес-правил, возникает необходимость в избыточной логике проверки достоверности внутри множества конст­ рукторов. Рассмотрим следующее измененное объявление класса Motorcycle: class Motorcycle { public int dnverlntensity; public string dnverName; public Motorcycle () { } // Избыточная логика конструктора! public Motorcycle (int intensity) { if (intensity > 10) { intensity = 10; } dnverlntensity = intensity; } public Motorcycle(int intensity, string name) { if (intensity > 10) { intensity = 10; } dnverlntensity = intensity; dnverName = name; Здесь (возможно, стараясь обеспечить безопасность гонщика) в каждом конструк­ торе предпринимается проверка, что уровень мощности не превышает 10. Хотя все это правильно и хорошо, в двух конструкторах появляется избыточный код. Это далеко от идеала, поскольку придется менять код в нескольких местах в случае изменения правил (например, если предельное значение мощности будет установлено равным 5). Один из способов исправить создавшуюся ситуацию состоит в определении в классе Motorcycle метода, который выполнит проверку входных аргументов. Если поступить так, то каждый конструктор должен будет вызывать этот метод перед присваиванием значений полям. Хотя такой подход позволяет изолировать код, который придется об­ новлять при изменении бизнес-правил, теперь появилась другая избыточность: class Motorcycle { public int dnverlntensity; public string dnverName; 194 Часть II. Главные конструкции программирования на C# // Конструкторы. public Motorcycle () { } public Motorcycle(int intensity) { Setlntensity(intensity); } public Motorcycle(int intensity, string name) { Setlntensity(intensity); dnverName = name; } public void Setlntensity(int intensity) { if (intensity > 10) { intensity = 10; } driverlntensity = intensity; Более ясный подход предусматривает назначение конструктора, который принима­ ет максимальное количество аргументов, в качестве “ведущего конструктора”, с реа­ лизацией внутри него необходимой логики проверки достоверности. Остальные конст­ рукторы смогут использовать ключевое слово t h is , чтобы передать входные аргументы ведущему конструктору и при необходимости предоставить любые дополнительные па­ раметры. В результате беспокоиться придется только о поддержке единственного конст­ руктора для всего класса, в то время как остальные конструкторы остаются в основном пустыми. Ниже приведена финальная реализация класса M o to rc y c le (с дополнительным кон­ структором в целях иллюстрации). При связывании конструкторов в цепочку обратите внимание, что ключевое слово t h i s располагается вне тела конструктора (и отделяется от его имени двоеточием): class Motorcycle { public int driverlntensity; public string dnverName; // Связывание конструкторов в цепочку. public Motorcycle () {} public Motorcycle (int intensity) : this(intensity, "") {} public Motorcycle(string name) : this(0, 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 dnverlntensity; public string driverName; // Связывание конструкторов в цепочку. public Motorcycle() { Console .WnteLine ("In default ctor") ; } public Motorcycle (int intensity) : this(intensity, "") { Console .WnteLine (" In ctor taking an int"); } public Motorcycle(string name) : this(0, name) { Console .WnteLine ("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; Теперь изменим метод M ain(), чтобы он обрабатывал объект M o torcycle , как пока­ зано ниже: static void Main(string [] args) { Console.WriteLine ("***** Fun with Class Types *****\n"); // Создание Motorcycle. Motorcycle c = new Motorcycle(5); c .SetDriverName("Tiny"); c .PopAWheely(); Console.WriteLine("Rider name is {0}", c .driverName); // вывод имени гонщика Console.ReadLine(); 196 Часть II. Главные конструкции программирования на C# Вывод, полученный в результате выполнения предыдущего метода M ain(), выглядит следующим образом: ***** 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 Поток логики конструкторов описан ниже. • Создается объект за счет вызова конструктора, который принимает один аргу­ мент типа in t. • Конструктор передает полученные данные ведущему конструктору, добавляя не­ обходимые дополнительные начальные аргументы, не указанные вызывающим кодом. • Ведущий конструктор присваивает входные данные полям объекта. • Управление возвращается первоначально вызванному конструктору, который вы­ полняет остальные операторы кода. В построении цепочки конструкторов замечательно то, что этот шаблон программи­ рования работает с любой версией языка C# и платформой .NET. Однако если в случае если целевой платформой является .NET 4.0 и выше, можно еще более упростить задачу программирования за счет использования необязательных аргументов в качестве аль­ тернативы традиционным цепочкам конструкторов. Еще раз об необязательных аргументах Вы главе 4 вы узнали об необязательных и именованных аргументах. Вспомните, что необязательные аргументы позволяют определять значения по умолчанию для входных аргументов. Если вызывающий код удовлетворяют эти значения по умолчанию, указы­ вать уникальные значения не обязательно, но это можно делать, когда объект требуется снабдить специальными данными. Рассмотрим следующую версию класса M otorcycle, который теперь предоставляет несколько возможностей конструирования объектов, ис­ пользуя единственное определение конструктора. class Motorcycle { // Единственный конструктор, использующий необязательные аргументы. public Motorcycle(int intensity = 0, string name = "") { if (intensity > 10) { intensity = 10; } dnverlntensity = intensity; dnverName = name; Глава 5. Определение инкапсулированных типов классов 197 С этим единственным конструктором можно создать объект Motorcycle, используя ноль, один или два аргумента. Вспомните, что синтаксис именованных аргументов по­ зволяет, по сути, пропускать приемлемые установки по умолчанию (см. главу 4). static void MakeSomeBikes() { // driverName = "" , d riv e rln te n sity = 0 Motorcycle ml = new Motorcycle(); Console .WnteLine ("Name= {0}, Intensity= {1}", m l .driverName, m l .driverlntensity); // driverName = "Tiny" , d riv e rln te n sity = 0 Motorcycle m2 = new Motorcycle(name:"Tiny"); Console .WnteLine ("Name= {0}, Intensity= {1}", m2.driverName, m2.driverlntensity); // driverName = "" , d riv e rln te n sity = 7 Motorcycle m3 = new Motorcycle(7); Console .WnteLine ("Name= {0}, Intensity= {1}", m 3 .driverName, m 3 .driverlntensity); } Хотя применение необязательных/именованных аргументов — очень удобный путь упрощения определения набора конструкторов, используемых определенным классом, следует всегда помнить, что этот синтаксис привязывает компиляцию приложений к C# 2010, а их выполнение — к .NET 4.0. Если требуется построить классы, которые должны выполняться на платформе .NET любой версии, лучше придерживаться клас­ сической технологии цепочек конструкторов. В любом случае, теперь можно определить класс с данными полей (переменнымичленами) и различными членами, которые могут быть созданы с помощью любого коли­ чества конструкторов. Теперь давайте формализуем роль ключевого слова s t a t i c . Исходный код. Проект SimpleClassExample доступен в подкаталоге Chapter 5. Понятие ключевого слова s t a t i c Класс C# может определять любое количество статических членов с использовани­ ем ключевого слова static. При этом соответствующий член должен вызываться не­ посредственно на уровне класса, а не на объектной ссылке. Чтобы проиллюстрировать разницу, обратимся к System.Console. Как уже было показано, метод W n t e L i n e () не вызывается на уровне объекта: // Ошибка! W riteLin e() не является методом уровня объекта1 Console с = new Console (); с .WriteLine("I can't be printed..."); Вместо этого статический член WriteLine () предваряется именем класса: // Правильно! W riteLin e() - статический метод. Console .WnteLine ("Thanks ..."); Проще говоря, статические члены — это элементы, задуманные (проектировщиком класса) как общие, так что нет нужды создавать экземпляр типа при их вызове. Когда в любом классе определены только статические члены, такой класс можно считать “об­ служивающим классом”. Например, если воспользоваться браузером объектов Visual Studio 2 0 1 0 (выбрав пункт меню V ie w ^ O b je c t B ro w se r (Вид1 ^Браузер объектов)) для про­ смотра пространства имен System сборки mscorlib.dll, можно увидеть, что все члены классов Console, Math, Environment и GC представляют свою функциональность через статические члены. 198 Часть II. Главные конструкции программирования на C# Определение статических методов Предположим, что имеется новый проект консольного приложения по имени 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 () { string[] messages = {"Do I have to?", "He started it!", "I'm too tired...", "I hate school1", "You are sooooooo wrong!"}; return messages[GetRandomNumber(5)]; } } Обратите внимание, что переменная-член System.Random и вспомогательная функция GetRandomNumber () также были объявлены статическими членами класса Teenager, учитывая правило, что статические члены, такие как метод Complain(), мо­ гут оперировать только другими статическими членами. На заметку! Здесь следует повторить: статические члены могут оперировать только статическими данными и вызывать статические методы определяющего их класса. Попытка использования нестатических данных класса или вызова нестатического метода класса внутри реализации статического члена приводит к ошибке времени компиляции. Подобно любому статическому члену, для вызова Complain () нужен префикс — имя определяющего его класса: static void Main(string [] args) { Console.WnteLine ("***** Fun with Class Types *****\n"); for(int l =0; l < 5; i++) Console.WnteLine (Teenager .Complain () ) ; Console.ReadLine(); Исходный код. Проект StaticMethods доступен в подкаталоге Chapter 5. Определение статических полей данных В дополнение к статическим методам, в классе (или структуре) также могут быть определены статические поля, такие как переменная-член Random, представленная в предыдущем классе Teenager. Знайте, что когда класс определяет нестатические дан­ ные (правильно называемые данными экземпляра), то каждый объект этого типа под­ держивает независимую копию поля. Например, представим класс, который моделиру­ ет депозитный счет, определенный в новом проекте консольного приложения по имени StaticData: Глава 5. Определение инкапсулированных типов классов 199 / / П р остой к л а с с д е п о з и т н о г о с ч е т а . class SavingsAccount { public double currBalance; public SavingsAccount(double balance) { currBalance = balance; 1 } } При создании объектов SavingsAccount память под поле currBalance выделяется для каждого объекта. Статические данные, с другой стороны, распределяются однажды и разделяются всеми объектами того же класса. Чтобы проиллюстрировать удобство статических данных, добавьте в класс SavingsAccount статический элемент данных по имени currlnterestRate, принимающий значение по умолчанию 0.04: / / П р остой к л а с с д е п о з и т н о г о с ч е т а . class SavingsAccount { 1 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 (50); SavingsAccount s2 = new SavingsAccount(100); SavingsAccount s3 = new SavingsAccount(10000.75); Console.ReadLine(); } то размещение данных в памяти будет выглядеть примерно так, как показано на рис. 5.3. Рис. 5.3 . Статические данные размещаются один раз и разделяются между всеми экземплярами класса Теперь давайте изменим класс SavingsAccount, добавив к нему два статических ме­ тода для получения и установки значения процентной ставки: 200 Часть II. Главные конструкции программирования на C# // Простой класс депозитного счета. class SavingsAccount { public double currBalance; // Статический элемент данных. public static double currInterestRate = 0.04; public SavingsAccount(double balance) { currBalance = balance; } // Статические члены для установки/получения процентной ставки. public static void SetInterestRate(double newRate ) { currlnterestRate = newRate; } public static double GetInterestRate () { return currlnterestRate; } } Рассмотрим следующее применение класса: static void Main(string [] args) { Console.WnteLine (''***** Fun with Static Data *****\n"); SavingsAccount si = new SavingsAccount(50); SavingsAccount s2 = new SavingsAccount(100); // Вывести текущую процентную ставку. Console .WnteLine ("Interest Rate is: {0}", SavingsAccount.Get InterestRate ()) ; // Создать новый объект; это не 'сбросит' процентную ставку. SavingsAccount s3 = new SavingsAccount(10000.75); Console.WriteLine("Interest Rate is: {0}", SavingsAccount.GetInterestRate()) ; Console.ReadLine(); } Вывод предыдущего метода Main() показан ниже: ***** Fun W1th Static Data ***** In static ctor1 Interest Rate is: 0.04 Interest Rate is: 0.04 Как видите, при создании новых экземпляров класса SavingsAccount значение статических данных не сбрасывается, поскольку CLR выделяет для них место в памяти только один раз. После этого все объекты типа SavingsAccount оперируют одним и тем же значением. При проектировании любого класса C# одна из задач связана с выяснением того, какие части данных должны быть определены как статические члены, а какие — нет. Хотя на этот счет не существует строгих правил, помните, что поле статических данных разделяется между всеми объектами данного класса. Поэтому, если необходимо, чтобы часть данных совместно использовалась всеми объектами, статические члены будут са­ мым подходящим вариантом. Предположим, что переменная currlnterestRate не определена с ключевым сло­ вом static. Это означает, что каждый объект SavingAccount имеет собственную копию currlnterestRate. Пусть создано 100 объектов SavingAccount, и требует­ ся изменить значение процентной ставки. Это потребует стократного вызова методв SetInterestRate()l Ясно, что такой способ моделирования общих для объектов класса данных нельзя считать удобным. Еще раз: статические данные идеальны, когда имеет­ ся значение, которое должно быть общим для всех объектов данной категории. Глава 5. Определение инкапсулированных типов классов 201 Определение статических конструкторов » Вспомните, что конструкторы служат для установки значений поля данных объекта во время его создания. Таким образом, если вы хотите присвоить значение статическо­ му члену внутри конструктора уровня экземпляра, то удивитесь, обнаружив, что это значение будет сбрасываться каждый раз, когда создается новый объект! Например, предположим, что класс SavingsA ccou nt изменен следующим образом: class SavingsAccount { public double currBalance; public static double currlnterestRate; public SavingsAccount(double balance) { currlnterestR ate = 0.04; currBalance = balance; } При выполнении предыдущего метода M a in () обнаруживается, что перемен­ ная c u r r l n t e r e s t R a t e будет сбрасываться при каждом создании нового объекта S avingsAccount, всегда возвращаясь к значению 0.04. Ясно, что установка значений статических данных в нормальном конструкторе экземпляра сводит на нет весь их смысл. Всякий раз, когда создается новый объект, данные уровня класса сбрасывают­ ся! Один из способов правильной установки статического поля состоит в использовании синтаксиса инициализации члена, как это делалось изначально: class SavingsAccount { public double currBalance; // Статические данные. public static double currlnterestRate = 0.04; Этот подход гарантирует, что статическое поле будет установлено только однажды, независимо от того, сколько объектов будет создано. Однако что, если значение стати­ ческих данных нужно получить во время выполнения? Например, в типичном банков­ ском приложении значение переменной — процентной ставки должно быть прочитано из базы данных или внешнего файла. Решение подобных задач требует контекста мето­ да (такого как конструктор), чтобы можно было выполнить операторы кода. Именно по этой причине в C# предусмотрена возможность определения статическо­ го конструктора. Взгляните на следующее изменение в коде: class SavingsAccount { public double currBalance; public static double currlnterestRate; public SavingsAccount(double balance) { currBalance = balance; } // Статический конструктор. static SavingsAccount () { Console.WriteLine("In static ctor'"); currlnterestRate = 0.04; 202 Часть II. Главные конструкции программирования на C# Упрощенно, статический конструктор — это специальный конструктор, который яв­ ляется идеальным местом для инициализации значений статических данных, когда их значение не известно на момент компиляции (например, когда его нужно прочитать из внешнего файла или сгенерировать случайное число). Ниже приведено несколько инте­ ресных моментов, касающихся статических конструкторов. • В отдельном классе может быть определен только один статический конструктор. Другими словами, статический конструктор нельзя перегружать. • Статический конструктор не имеет модификатора доступа и не может принимать параметров. • Статический конструктор выполняется только один раз, независимо от того, сколько объектов отдельного класса создается. • Исполняющая система вызывает статический конструктор, когда создает экземп­ ляр класса или перед первым обращением к статическому члену этого класса. • Статический конструктор выполняется перед любым конструктором уровня экземпляра. Учитывая сказанное, при создании новых объектов S avings Ac count значения стати­ ческих данных сохраняются, поскольку статический член устанавливается только один раз внутри статического конструктора, независимо от количества созданных объектов. Определение статических классов Ключевое слово s t a t i c возможно также применять прямо на уровне класса. Когда класс определен как статический, его нельзя создать с использованием ключевого слова new, и он может включать в себя только статические члены или поля. Если это правило нарушить, возникнет ошибка компиляции. На заметку! Классы или структуры, предоставляющие только статическую функциональность, час­ то называют служебными (utility). При проектировании такого класса рекомендуется применять ключевое слово s t a t i c к определению класса. На первый взгляд это может показаться довольно бесполезным средством, учитывая невозможность создания экземпляров класса. Однако следует учесть, что класс, не со­ держащий ничего кроме статических членов и/или константных данных, прежде всего, не нуждается в выделении памяти. Рассмотрим следующий новый статический тип: // Статические классы могут содержать только статические члены! static class TimeUtilClass { public static void PrintTime() { Console.WriteLine(DateTime.Now.ToShortTimeString()); } public static void PrintDate () { Console.WriteLine (DateTime.Today.ToShortDateString()); } } Учитывая, что этот класс определен с ключевым словом s t a t ic , создавать экземп­ ляры T im e U tilC la s s с помощью ключевого слова 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 единственный способ предотвратить создание экземпляров класса, предлагающего только статическую функциональность, состоял либо в переопределении конструктора по умолчанию с модификатором p r iv a te , либо в объявлении класса как абстрактного с использованием ключевого слова a b s t r a c t (подробности об абстрактных типах ищите в главе 6). Рассмотрим следующие подходы: class TimeUtilClass2 { // Переопределение конструктора по умолчанию как // приватного для предотвращения создания экземпляров. private TimeUtilClass2 (){} public static void PrintTimeO { Console.WriteLine (DateTime.Now.ToShortTimeString()); } public static void PrintDateO { Console.WriteLine(DateTime.Today.ToShortDateString()); } } // Определение типа как абстрактного для предотвращения создания экземпляров. abstract class TimeUtilClass3 { public static void PrintTimeO { Console.WriteLine(DateTime.Now.ToShortTimeString()); } public static void PrintDateO { Console.WriteLine(DateTime.Today.ToShortDateString()); } } Хотя эти конструкции допустимы и сейчас, использование статических классов представляет собой более ясное и более безопасное в отношении типов решение, учи ­ тывая тот факт, что предыдущие два приема допускают объявление нестатических ч ле­ нов внутри класса без ошибок. В результате возникнет большая проблема! При наличии класса, который больше не допускает создания экземпляров, в распоряжении окажется фрагмент функциональности (т.е. все нестатические члены), который не может быть использован. К этому моменту должно быть ясно, как определять простые классы, включающие конструкторы, поля и различные статические (и нестатические) члены. Обладая такими базовыми знаниями, можем приступать к ознакомлению с тремя ос­ новными принципами объектно-ориентированного программирования. Исходный код. Проект S ta tic D a ta доступен в подкаталоге C hapter 5. Основы объектно-ориентированного программирования Все основанные на объектах языки (С#, Java, C++, Smalltalk, Visual Basic и in .) должны отвечать трем основным принципам объектно-ориентированного программи­ рования (ООП), которые перечислены ниже. • Инкапсуляция. Как данный язык скрывает детали внутренней реализации объек­ тов и предохраняет целостность данных? • Наследование. Как данный язык стимулирует многократное использование кода? • Полиморфизм. Как данный язык позволяет трактовать связанные объекты сход­ ным образом? 204 Часть II. Главные конструкции программирования на C# Прежде чем погрузиться в синтаксические детали реализации каждого принципа, важно понять базовую роль каждого из них. Ниже предлагается обзор каждого принци­ па, а в остальной части этой и последующих главах рассматриваются детали. Роль инкапсуляции Первый основной принцип ООП называется инкапсуляцией. Этот принцип касает­ ся способности языка скрывать излишние детали реализации от пользователя объекта. Например, предположим, что используется класс по имени DatabaseReader, который имеет два главных метода: Ореп() и close(). // Этот класс инкапсулирует детали открытия и закрытия базы данных. DatabaseReader dbReader = new DatabaseReader (); dbReader.Open(0"C:\AutoLot.mdf"); // Сделать что-то с файлом данных и закрыть файл. dbReader.Close() ; Фиктивный класс DatabaseReader инкапсулирует внутренние детали нахождения, загрузки, манипуляций и закрытия файла данных. Программистам нравится инкапсу­ ляция, поскольку этот принцип ООП упрощает кодирование. Нет необходимости бес­ покоиться о многочисленных строках кода, которые работают “за кулисами”, чтобы реализовать функционирование класса DatabaseReader. Все, что потребуется — это создать экземпляр и отправлять ему соответствующие сообщения (например, “открыть файл по имени AutoLot.mdf, расположенный на диске С:”). С идеей инкапсуляции программной логики тесно связана идея защиты данных. В идеале данные состояния объекта должны быть специфицированы с использовани­ ем ключевого слова private (или, возможно, protected). Таким образом, внешний мир должен вежливо попросить, если захочет изменить или получить лежащее в основе зна­ чение. Это хороший принцип, поскольку общедоступные элементы данных можно лег­ ко повредить (даже нечаянно, а не преднамеренно). Чуть позже будет дано формальное определение этого аспекта инкапсуляции. Роль наследования Следующий принцип ООП — наследование — касается способности языка позволять строить новые определения классов на основе определений существующих классов. По сути, наследование позволяет расширять поведение базового (или родительского) клас­ са, наследуя основную функциональность в производном подклассе (также именуемом дочерним классом). На рис. 5.4 показан простой пример. Прочесть диаграмму на рис. 5.4 можно так: “шестиугольник является фигурой, кото­ рая является объектом”. При наличии классов, связанных этой формой наследования, между типами устанавливается отношение “является” (“is-a”). Такое отношение называ­ ют классическим наследованием. Здесь можно предположить, что Shape определяет некоторое количество членов, об­ щих для всех наследников (скажем, значение для представления цвета фигуры и другие значения, задающие высоту и ширину). Учитывая, что класс Hexagon расширяет Shape, он наследует основную функциональность, определенную классами Shape и Object, а также определяет дополнительные собственные детали, касающиеся шестиугольников (какими бы они ни были). На заметку! На платформе .NET класс System.Object всегда находится на вершине любой иерар­ хии классов и определяет базовую функциональность, которая подробно описана в главе 6. Глава 5. Определение инкапсулированных типов классов 205 Рис. 5.4 . Отношение “является" (“is-a") Есть и другая форма повторного использования кода в мире ООП: модель включе­ ния/делегации, также известная под названием о т н о ш е н и е “и м е е т " ( “h a s - а " ) или а г р е ­ гация. Эта форма повторного использования н е применяется для установки отношений “родительский-дочерний”.Вместо этого такое отношение позволяет одному классу оп­ ределять переменную-член другого класса и опосредованно представлять его функцио­ нальность (при необходимости) пользователю объекта. Например, предположим, что снова моделируется автомобиль. Может понадобиться выразить идею, что автомобиль “имеет” радиоприемник. Было бы нелогично пытаться наследовать класс Саг от Radio или наоборот (ведь Саг не “является” Radio). Взамен имеются два независимых класса, работающих совместно, причем класс Саг создает и представляет функциональность Radio: class Radio { public void Power(bool turnOn) { Console .WnteLine ("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. Главные конструкции программирования на C# Роль полиморфизма Последний принцип ООП — полиморфизм. Он обозначает способность языка трак­ товать связанные объекты в сходной манере. В частности, этот принцип ООП позво­ ляет базовому классу определять набор членов (формально называемый полиморфным интерфейсом), которые доступны всем наследникам. Полиморфный интерфейс класса конструируется с использованием любого количества виртуальных или абстрактных членов (подробности ищите в главе 6). По сути, виртуальный член— это член базового класса, определяющий реализацию по умолчанию, которая может быть изменена (или, говоря более формально, переоп­ ределена) в производном классе. В отличие от него, абстрактный метод — это член базового класса, который не предусматривает реализации по умолчанию, а предла­ гает только сигнатуру. Когда класс наследуется от базового класса, определяющего аб­ страктный метод, этот метод обязательно должен быть переопределен в производном классе. В любом случае, когда производные классы переопределяют члены, определен­ ные в базовом классе, они по существу переопределяют свою реакцию на один и тот же запрос. Чтобы увидеть полиморфизм в действии, давайте представим некоторые детали иерархии фигур, показанной на рис. 5.4. Предположим, что в классе Shape определен виртуальный метод Draw(), не принимающий параметров. Учитывая тот факт, что ка­ ждая фигура должна визуализировать себя уникальным образом, подклассы (такие как Hexagon и Circle) вольны переопределить этот метод по своему усмотрению (рис. 5.5). 1 © Object T Shape Class Вызов Draw () на объекте Circle приводит к рисованию I C irc le Class J— * Shape двумерного круга. 3 = M e th o d s ♦ D raw ! Hexagon Class Shape ~® ] Вызов 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 (); Глава 5. Определение инкапсулированных типов классов 207 foreach (Shape s in myShapes) { // Используется полиморфный интерфейс! s .Draw(); } Console.ReadLine (); } } На этом краткое знакомство с основными принципами ООП завершено. Оставшаяся часть главы посвящена дальнейшим подробностям инкапсуляции в С#. Детали наследо­ вания и полиморфизма рассматриваются в главе 6. Модификаторы доступа C# При работе с инкапсуляцией всегда следует принимать во внимание то, какие ас­ пекты типа видимы различным частям приложения. В частности, типы (классы, ин­ терфейсы, структуры, перечисления и делегаты), а также их члены (свойства, методы, конструкторы и поля) определяются с использованием определенного ключевого слова, управляющего “видимостью” элемента другим частям приложения. Хотя в C# опреде­ лены многочисленные ключевые слова для управления доступом, их значение может отличаться в зависимости от места применения (к типу или члену). В табл. 5.1 описаны роли и применение модификаторов доступа. Таблица 5.1. Модификаторы доступа C# Модификатор доступа К чему может быть применен Назначение public Типы или члены типов Общедоступные (public) элемен­ ты не имеют ограничений доступа. Общедоступный член может быть доступен как из объекта, так и из любого производ­ ного класса. Общедоступный тип может быть доступен из других внешних сборок private Члены типов или вложенные типы Приватные (private) элементы могут быть доступны только в классе (или струк­ туре), в котором они определены protected Члены типов или вложенные типы Защищенные (protected) элементы могут использоваться классом, который определил их, и любым дочерним классом. Однако за­ щищенные элементы не доступны внешнему миру через операцию точки (.) internal Типы или члены типов Внутренние (internal) элементы дос­ тупны только в пределах текущей сборки. Таким образом, если в библиотеке клас­ сов .NET определен набор внутренних типов, то другие сборки не смогут ими пользоваться protected internal Члены типов или вложенные типы Когда ключевые слова protected и internal комбинируются в объявлении элемента, такой элемент доступен внутри определяющей его сборки, определяюще­ го класса и всех его наследников 208 Часть II. Главные конструкции программирования на C# В этой главе рассматриваются только ключевые слова public и private. В после­ дующих главах будет рассказываться о роли модификаторов internal и protected internal (удобных при построении библиотек кода .NETT) и модификатора 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), вложенное в общедос­ тупный класс (по имени SportsCar): public class SportsCar { // Нормально! Вложенные типы могут быть помечены как private, private enum CarColor { Red, Green, Blue Здесь допускается применять модификатор доступа private к вложенному типу. Однако не вложенные типы (вроде SportsCar) могут определяться только с модифика­ торами public или internal. Поэтому следующее определение класса неверно: // Ошибка1 Не вложенный тип не может быть помечен как private1 private class SportsCar {} После ознакомления с модификаторами доступа можно приступать к формальным исследованиям первого принципа ООП. Глава 5. Определение инкапсулированных типов классов 209 Первый принцип: службы инкапсуляции C# Концепция инкапсуляции вращается вокруг принципа, гласящего, что внутренние данные объекта.не должны быть напрямую доступны через экземпляр объекта. Вместо этого, если вызывающий код желает изменить состояние объекта, то должен делать это через методы доступа (accessor, или метод get) и изменения (mutator, или метод set). В C# инкапсуляция обеспечивается на синтаксическом уровне с использованием ключевых слов public, private, internal и protected. Чтобы проиллюстрировать необходимость в службах инкапсуляции, предположим, что создано следующее определение класса: // Класс с единственным общедоступным полем. class Book { public int numberOfPages; } Проблема с общедоступными данными состоит в том, что сами по себе эти данные не имеют возможности “понять”, является ли присваиваемое значение допустимым в рамках существующих бизнес-правил системы. Как известно, верхний предел значе­ ний для типа int в C# довольно велик (2 147 483 647), поэтому компилятор разрешит следующее присваивание: //Хм Ничего себе — мини-новелла! static void Main(string [] args) { Book miniNovel = new Book(); miniNovel.numberOfPages = 30000000; } Хотя границы типа данных int не превышены, ясно, что мини-новелла на 30 мил­ лионов страниц выглядит несколько неправдоподобно. Как видите, общедоступные поля не дают возможности перехватывать ошибки, связанные с преодолением верхних (или нижних) логических границ. Если в текущей системе установлено бизнес-прави­ ло, гласящее, что книга должна иметь от 1 до 1000 страниц, его придется обеспечить программно. По этой причине общедоступным полям обычно нет места в определении класса производственного уровня. На заметку! Говоря точнее, члены класса, представляющие состояние объекта, не должны поме­ чаться как public. В то же время, как будет показано далее в главе, вполне допускается иметь общедоступные константы и поля только для чтения. Инкапсуляция предоставляет способ предохранения целостности данных о состоя­ нии объекта. Вместо определения общедоступных полей (которые легко приводят к повреждению данных), необходимо выработать привычку определения приватных дан­ ных, управление которыми осуществляется опосредованно, с применением одной из двух техник: • определение пары методов доступа и изменения; • определение свойства .NET. Какая бы техника не была выбрана, идея состоит в том, что хорошо инкапсулиро­ ванный класс должен защищать свои данные и скрывать подробности своего устрой­ ства от любопытных глаз из внешнего мира. Это часто называют программированием черного ящика. Преимущество такого подхода состоит в том, что объект может свобод­ но изменять внутреннюю реализацию любого метода. За счет обеспечения неизменно­ сти сигнатуры метода, работа существующего кода, который использует этот метод, не нарушается. 210 Часть II. Главные конструкции программирования на C# Инкапсуляция с использованием традиционных методов доступа и изменения В оставшейся части этой главы будет построен довольно полный класс, модели­ рующий обычного сотрудника. Для начала создадим новое консольное приложение по имени EmployeeApp и добавим в него новый файл класса (под названием Employee.cs), используя пункт меню P r o je c t^ A d d c la ss (Проект1 ^Добавить класс). Дополним класс 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 .WnteLine (" 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 1 Name must be less than 16 characters!"); else empName = name; Эта техника требует наличия двух уникально именованных методов для управления единственным элементом данных. Для иллюстрации модифицируем метод M ain() сле­ дующим образом: static void Main(string [] args) { Console.WriteLine ("***** Fun with Encapsulation *****\n"); Employee emp = new Employee("Marvin", 456, 30000); emp.GiveBonus(1000); 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() и SetNam e(). Для дальнейшей инкапсуляции данных в клас­ се Employee понадобится добавить ряд дополнительных методов (например, G etlD Q , SetID (), G etC u rren tPay(), S etC u rren tP a y()). Каждый метод, изменяющий данные, мо­ жет иметь в себе несколько строк кода для проверки дополнительных бизнес-правил. 212 Часть II. Главные конструкции программирования на C# Хотя можно поступить именно так, для инкапсуляции данных класса в C# предлагается удобная альтернативная нотация. Инкапсуляция с использованием свойств .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!"); else empName = value; } // Можно было бы добавить дополнительные бизнес-правила для установки // этих свойств, но в данном примере в этом нет необходимости. public int ID { get { return empID; } set { empID = value; } } public float Pay { get { return currPay; } set { currPay = value; } Свойство C# состоит из определений контекстов чтения get (метод доступа) и set (метод изменения), вложенных непосредственно в контекст самого свойства. Обратите внимание, что свойство указывает тип данных, которые оно инкапсулирует, как тип возвращаемого значения. Кроме того, в отличие от метода, в определении свойства не используются скобки (даже пустые). Обратите внимание на комментарий к текущему свойству ID: Глава 5. Определение инкапсулированных типов классов 213 // int представляет тип инкапсулируемых свойством данных. // Тип данных должен быть идентичен связанному полю (empID). public int ID // Обратите внимание на отсутствие скобок. { get { return empID; } set { empID = value; } } В контексте set свойства используется лексема v a lu e, которая представляет вход­ ное значение, присваиваемое свойству вызывающим кодом. Эта лексема не является настоящим ключевым словом С#, а представляет собой то, что называется контексту­ альным ключевым словом. Когда лексема v a lu e находится внутри контекста set, она всегда обозначает значение, присваиваемое вызывающим кодом, и всегда имеет тип, совпадающий с типом самого свойства. Поэтому свойство Name может проверить допус­ тимую длину s t r in g следующим образом: public string Name { get { return empName; } set { // Здесь value имеет тип string, if (value.Length > 15) Console .WnteLine ("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.GiveBonus(1000); 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. Главные конструкции программирования на C# // Обновленные конструкторы. 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; // Обновленный метод D isp la y S ta ts() теперь учитывает возраст. public void DisplayStats() { Console .WnteLine ("Name : {0}", empName); Console.WriteLine ("ID: {0}", empID); Console .WnteLine ("Age : {0}", empAge); Console.WriteLine("Pay: {0}", currPay); Теперь предположим, что создан объект Employee по имени jo e. Необходимо, чтобы в день рождения сотрудника возраст увеличивался на 1 год. Используя традиционные методы set/get, пришлось бы написать код вроде следующего: Employee joe = new Employee(); joe.SetAge(joe.GetAge () + 1); Однако если empAge инкапсулируется через свойство по имени Аде, можно записать проще: Employee joe = new Employee(); joe.Age++; Использование свойств внутри определения класса Свойства, а в особенности их часть set — это общепринятое место для размещения бизнес-правил класса. В настоящее время класс Employee имеет свойство Name, кото­ рое гарантирует длину имени не более 15 символов. Остальные свойства (ID, Рау и Аде) также могут быть обновлены с добавлением соответствующей логики. Хотя все это хорошо, но следует принимать во внимание также и то, что обычно происходит внутри конструктора класса. Конструктор получает входные параметры, проверяет корректность данных и затем выполняет присваивания внутренним приват­ ным полям. В настоящее время ведущий конструктор не проверяет входные строковые данные на допустимый диапазон, поэтому можно было бы изменить его следующим образом: public Employee (string name, int age, int id, float pay) { // Это может оказаться проблемой... if (name.Length > 15) Console.WriteLine ("Error 1 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 .WnteLine (" ID: {0}", ID); Console.WriteLine("Age: {0}", Age); Console.WriteLine("Pay: {0}", Pay); } // Свойства остаются прежними... } Внутреннее представление свойств Многие программисты склонны именовать традиционные методы доступа и измене­ ния с применением префиксов g e t _ и s e t _ (например, g e t _ Name() и s e t _ NameO). 216 Часть II. Главные конструкции программирования на C# В языке C# такое соглашение об именовании само по себе не проблематично. Однако важно понимать, что “за кулисами” свойства представлены в коде CIL с использованием тех же самых префиксов. Например, открыв сборку Em ployeeApp.exe в утилите ild a sm .ex e, можно увидеть, что каждое свойство отображается на скрытые методы g e t XXX () и s e t XXX ( ) , вызы­ ваемые внутри CLR (рис. 5.6). Рис. 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 SocialSecuntyNumber = ssn; } public void DisplayStats () { Console.WriteLine("Name: {0}", Name); Console .WnteLine (" ID : {0}", ID); Console.WriteLine ("Age: {0}", Age); Console.WriteLine("Pay: {0}", Pay); Console .WriteLine ("SSN: {0}", SocialSecuntyNumber); } Если в этом классе также определить два метода с именами get SocialSecurity 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 SocialSecuntyNumber { get { return .empSSN; } set { empSSN = value; } } В некоторых случаях было бы удобно задавать уникальные уровни доступа для л о ­ гики get и set. Для этого просто добавьте префикс — ключевое слово доступа к соответ­ ствующим ключевым словам get или set (контекст без префикса получает видимость объявления свойства): // Пользователи объекта могут только получать значение, однако класс // Employee и производные типы могут также устанавливать значение. public string SocialSecuntyNumber { get { return empSSN; } protected set { empSSN = value; } 218 Часть II. Главные конструкции программирования на C# В этом случае логика 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. Статические свойства В C# также поддерживаются статические свойства. Вспомните из начала этой главы, что статические члены доступны на уровне класса, а не на уровне экземпляра (объекта) этого класса. Например, предположим, что в классе 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.WnteLine ("***** 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, "111-11-1111"); emp.GiveBonus(1000) ; emp.DisplayStats(); Console.ReadLine(); } И, наконец, вспомните, что классы могут поддерживать статические конструкторы. Поэтому если нужно гарантировать, что имя в статическом поле companyName всегда будет установлено в Му Company, понадобится написать следующий код: // Статические конструкторы используются для инициализ ации статических данных. public class Employee { private Static companyName As string static Employee () { companyName = "My Company"; } } Используя такой подход, нет необходимости явно вызывать свойство Company для того, чтобы установить начальное значение: // Автоматическая установка значения "Му Company" через статический конструктор. static void Main(string[] args) { Console.WriteLine("These folks work at {0}", Employee.Company); } В завершение исследований инкапсуляции с использованием свойств C# следует запомнить, что эти синтаксические сущности служат для тех же целей, что и тради­ ционные методы get/set. Преимущество свойств состоит в том, что пользователи объ­ ектов могут манипулировать внутренними данными, применяя единый именованный элемент Исходный код. Проект Em ployeeАрр доступен в подкаталоге C h apter 5. Понятие автоматических свойств С выходом платформы .NET 3.5 в языке C# появился еще один способ определения простых служб инкапсуляции с минимальным кодом, а именно — синтаксис автомати­ ческих свойств. Для целей иллюстрации создадим новый проект консольного приложе­ ния C# по имени AutoProps. Добавим в него новый файл класса C# (Car.cs), в котором определен показанный ниже класс, инкапсулирующий единственную порцию данных с использованием классического синтаксиса свойств. 220 Часть II. Главные конструкции программирования на C# // Тип Саг, использующий стандартный синтаксис свойств, class Саг { private string carName = string.Empty; public string PetName { get { return carName; } set { carName = value; } } Хотя большинство свойств C# содержат в своем контексте бизнес-правила, не так уж редко бывает, что некоторые свойства не делают буквально ничего, помимо простого присваивания и возврата значений, как в приведенном выше коде. В таких случаях было бы слишком громоздко многократно определять приватные поля и некоторые простые определения свойств. Например, при построении класса, которому нужно 15 приватных элементов данных, в конечном итоге получаются 15 связанных с ними свойств, кото­ рые, по сути, представляют собой не более чем тонкие оболочки для инкапсуляции. Чтобы упростить процесс простой инкапсуляции данных полей, можно применять синтаксис автоматических свойств. Как следует из названия, это средство переклады­ вает работу по определению лежащего в основе приватного поля и связанного свойства C# на компилятор, используя небольшое усовершенствование синтаксиса. Рассмотрим переделанный класс Саг, в котором этот синтаксис применяется для быстрого создания трех свойств: class Саг { // Автоматические свойства1 public string PetName { get; set; } public int Speed { get; set; } public string Color { get; set; } } При определении автоматических свойств указывается модификатор доступа, лежа­ щий в основе тип данных, имя свойства и пустые контексты get/set. Во время компи­ ляции тип будет оснащен автоматически сгенерированным полем и соответствующей реализацией логики set/get. На заметку! Имя автоматически сгенерированного лежащего в основе приватного поля в коде C# не доступно. Единственный способ увидеть его — воспользоваться таким инструментом, как ild a sm .ex e. Однако в отличие от традиционных свойств С#, создавать автоматические свойства, предназначенные только для чтения или только для записи, нельзя. Хотя может по­ казаться, что для этого достаточно опустить g e t или s e t в объявлении свойства, как показано ниже: // Свойство только для чтения? Ошибка! public int MyReadOnlyProp { get; } // Свойство только для записи? Ошибка! public int MyWriteOnlyProp { set; } Но на самом деле это приведет к ошибке компиляции. Определяемое автоматическое свойство должно поддерживать функциональность и чтения, и записи. Тем не менее, можно реализовать автоматическое свойство с более ограничивающими контекстами get или set: Глава 5. Определение инкапсулированных типов классов 221 // Контекст get общедоступный, a set — защищенный. // Контекст get/set можно также объявить приватным, public int SomeOtherProperty { get; protected set; } Взаимодействие с автоматическими свойствами Поскольку компилятор будет определять лежащие в основе приватные поля во время компиляции, класс с автоматическими свойствами всегда должен использовать синтак­ сис свойств для установки и чтения лежащих в основе значений. Это важно отметить, потому что многие программисты напрямую используют приватные поля внутри опре­ деления класса, что в данном случае невозможно. Например, если бы класс Саг вклю­ чал метод D is p la y S ta ts (), он должен был бы реализовать этот метод, используя имя свойства: class Саг { // Автоматические свойства! public string PetName { get; set; } public int Speed { get; set; } public string Color { get; set; } public void DisplayStats () { Console.WnteLine ("Car Name: {0}", PetName); Console.WriteLine("Speed: {0}", Speed); Console.WnteLine ("Color: {0}", Color); } При использовании объекта, определенного с автоматическими свойствами, можно присваивать и получать значения, используя ожидаемый синтаксис свойств: static void Main(string[] args) { Console.WriteLine("***** Fun with Automatic Properties *****\n"); Car c = new Car () ; c.PetName = "Frank"; c .Speed = 55; c .Color = "Red"; Console.WriteLine("Your car is named {0}? That's odd...", c .PetName); c .DisplayStats(); Console.ReadLine(); } Замечания относительно автоматических свойств и значений по умолчанию При использовании автоматических свойств для инкапсуляции числовых и булев­ ских данных можно сразу применять автоматически сгенерированные свойства типа прямо в своей кодовой базе, поскольку их скрытым полям будут присвоены безопасные значения по умолчанию, которые могут быть использованы непосредственно. Однако будьте осторожны, если синтаксис автоматического свойства применяется для упаков­ ки переменной другого класса, потому что скрытое поле ссылочного типа также будет установлено в значение по умолчанию, т.е. n u ll. Рассмотрим следующий новый класс по имени Garage, в котором используются два автоматических свойства: 222 Часть II. Главные конструкции программирования на C# class Garage { // Скрытое поле int установлено в О! public int NumberOfCars { get; set; } // Скрытое поле Car установлено в null! public Car MyAuto { get; set; } } Имея установленные C# значения по умолчанию для полей данных, значение NumberOfCars можно вывести в том виде, как оно есть (поскольку ему автоматически присвоено значение 0). Однако если напрямую обратиться к MyAuto, то во время выпол­ нения сгенерируется исключение ссылки на n u ll, потому что лежащей в основе пере­ менной-члену типа Саг не был присвоен новый объект: 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(); } Учитывая, что лежащие в основе приватные поля создаются во время компиляции, в синтаксисе инициализации полей C# нельзя непосредственно размещать экземпляр ссылочного типа с помощью 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; После этой модификации объект Саг можно поместить в объект Garage, как пока­ зано ниже: static void Main(string [] args) { Console.WriteLine (" * ** ** // Создать автомобиль. Car c = new Car () ; Fun with Automatic Properties *****\n"); Глава-5. Определение инкапсулированных типов классов 223 c.PetName = "Frank"; с .Speed = 55; с.Color = "Red"; с .Displaystats (); // Поместить автомобиль в гараж. Garage g = new Garage () ; g.MyAuto = c; // Вывод количества автомобилей в гараже. Console .WnteLine ("Number of Cars in garage: {0}", g .NumberOfCars); // Вывод названия автомобиля. Console.WriteLine("Your car is named: {0}", g .MyAuto.PetName); Console.ReadLine(); Нельзя не согласиться с тем, что это очень полезное свойство языка программиро­ вания С#, поскольку свойства для класса можно определить с использованием просто­ го синтаксиса. Естественно, если свойство помимо получения и установки лежащего в основе приватного поля требует дополнительного кода (такого как логика проверки достоверности, запись в журнал событий, взаимодействие с базой данных), придется определить его как “нормальное” свойство .NET вручную. Автоматические свойства C# никогда не делают ничего кроме предоставления простой инкапсуляции для лежащих в основе приватных данных (сгенерированных компилятором). Исходный код. Проект A u to P ro p s доступен в подкаталоге C h a p te r 5. Понятие синтаксиса инициализации объектов Как можно было видеть на протяжении этой главы, при создании нового объек­ та конструктор позволяет указывать начальные значения. Также было показано, что свойства позволяют получать и устанавливать лежащие в основе данные в безопасной манере. При работе с классами, которые написаны другими, включая классы из биб­ лиотеки базовых классов .NET, нередко можно заметить, что в них есть более одного конструктора, позволяющего устанавливать каждую порцию данных внутреннего со­ стояния. Учитывая это, программист обычно старается выбрать наиболее подходящий конструктор, после чего присваивает недостающие значения, используя доступные в классе свойства. Чтобы облегчить процесс создания и запуска объекта, в C# предлагается синтаксис инициализатора объекта. С помощью этого механизма можно создать новую объект­ ную переменную и присвоить значения множеству свойств и/или общедоступных по­ лей в нескольких строках кода. Синтаксически инициализатор объекта выглядит как список значений, разделенных запятыми, помещенный в фигурные скобки ({}). Каждый элемент в списке инициализации отображается на имя общедоступного поля или свой­ ства инициализируемого объекта. Рассмотрим пример применения этого синтаксиса. Создадим новое консольное при­ ложение по имени O b ject I n i t i a l i z e r s . Ниже показан класс Poin t, в котором исполь­ зуются автоматические свойства (что вообще-то не обязательно в данном примере, но помогает сократить код): class Point { public int X { get; set; } public int Y { get; set; } 224 Часть II, Главные конструкции программирования на C# public Point (int xVal, int yVal) { X = xVal; Y = yVal; } public Point () { } public void DisplayStats () { Console. WnteLine ("[{ 0 }, {1}]", X, Y) ; } Теперь посмотрим, как создавать объекты P oin t: static void Main(string[] args) { Console .WnteLine ("***** Fun with Object Init Syntax *****\n"); // Создать объект Point с установкой каждого свойства вручную. Point firstPoint = new Point (); firstPoint.X = 10; firstPoint.Y = 10; firstPoint.DisplayStats (); // Создать объект Point с использованием специального конструктора. Point anotherPoint = new Point(20, 20); anotherPoint.DisplayStats(); // Создать объект Point с использованием синтаксиса инициализатора объекта. Point finalPoint = new Point { X = 30, Y = 30 }; finalPoint.DisplayStats (); Console.ReadLine(); } При создании последней переменной P o in t не используется специальный конструк­ тор (как это принято делать традиционно), а вместо этого устанавливаются значения общедоступных свойств X и Y. “За кулисами” вызывается конструктор типа по умол­ чанию, за которым следует установка значений указанных свойств. В конечном счете, синтаксис инициализации объектов — это просто сокращенная нотация синтаксиса создания переменной класса с помощью конструктора по умолчанию, с последующей установкой свойств данных состояния. Вызов специальных конструкторов с помощью синтаксиса инициализации В предыдущих примерах типы P o in t инициализировались неявным вызовом конст­ руктора по умолчанию этого типа: // Здесь конструктор по умолчанию вызывается неявно. Point finalPoint = new Point { X = 30, Y = 30 }; При желании конструктор по умолчанию можно вызывать и явно: // Здесь конструктор по умолчанию вызывается явно Point finalPoint = new Point () { X = 30, Y = 30 }; Имейте в виду, что при конструировании типа с использованием нового синтакси­ са инициализации можно вызывать любой конструктор, определенный в классе. В на­ стоящий момент в типе P o in t определен двухаргументный конструктор для установки позиции (х , у). Таким образом, следующее объявление P o in t в результате приведет к установке X равным 100 и Y равным 100, независимо от того факта, что в аргументах конструктора указаны значения 10 и 16: Глава 5. Определение инкапсулированных типов классов 225 // Вызов специального конструктора. Point pt = new Point (10, 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 Colorf 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}]", 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, Главные конструкции программирования на C# public Point TopLeft { get { return topLeft; } set { topLeft = value; } } public Point BottomRight { get { return bottomRight; } set { bottomRight = value; } } public void DisplayStats () { Console .WnteLine (" [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 г = 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. Работа с данными константных полей В C# имеется ключевое слово 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 *****\n"); Console .WnteLine ("The value of PI is: {0}", MyMathClass .PI) ; // Ошибка! Нельзя изменять константу! MyMathClass.PI = 3.1444; Console.ReadLine(); } } Обратите внимание, что обращение к константным данным, определенным в клас­ се MyMathClass, осуществляется с использованием префикса в виде имени класса (т.е. MyMathClass.PI). Это связано с тем, что константные поля класса являются неявно статическими. Однако допустимо определять и обращаться к локальным константным переменным внутри члена типа, например: static void LocalConstStnngVanable () { // Локальные константные данные доступны непосредственно. 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. Главные конструкции программирования на C# Понятие полей только для чтения Близко к понятию константных данных лежит понятие данных полей, доступных только для чтения (которые не следует путать со свойствами только для чтения). Подобно константам, поля только для чтения не могут быть изменены после началь­ ного присваивания. Однако, в отличие от констант, значение, присваиваемое такому полю, может быть определено во время выполнения, и потому может быть на законном основании присвоено в контексте конструктора, но нигде более. Это может быть очень полезно, когда значение поля неизвестно вплоть до момента выполнения (возможно, потому, что для получения значения необходимо прочитать внешний файл), но нужно гарантировать, что оно не будет изменено после первоначального присваивания. Для иллюстрации рассмотрим следующее изменение в классе 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. Понятие частичных типов В этой главе осталось еще разобраться с ролью ключевого слова p a r t i a l . Класс производственного уровня легко может содержать многие сотни строк кода. К тому же, учитывая, что типичный класс определен внутри одного файла *.cs, может получиться очень длинный файл. В процессе создания классов нередко большая часть кода может быть проигнорирована, будучи однажды написанной. Например, данные полей, свой­ ства и конструкторы, как правило, остаются неизменными во время эксплуатации, в то время как методы имеют тенденцию модифицироваться довольно часто. При желании можно разнести единственный класс на несколько файлов С#, что­ бы изолировать рутинный код от более ценных полезных членов. Для примера загру­ зим ранее созданный проект EmployeeАрр в Visual Studio и откроем файл Employee.сs для редактирования. Сейчас этот единственный файл содержит код для всех аспектов класса: class Employee { // // // // Поля данных Конструкторы Методы Свойства } Механизм частичных классов позволяет вынести конструкторы и поля данных в со­ вершенно новый файл по имени Employee.Internal.cs (обратите внимание, что имя файла не имеет значения: здесь оно выбрано в соответствии с назначением класса). Первый шаг состоит в добавлении ключевого слова partial к текущему определению класса и вырезании кода, который должен быть помещен в новый файл: // Employee.cs partial class Employee { // Методы // Свойства } Предполагая, что новый класс добавлен к проекту, можно переместить поля данных и конструкторы в новый файл посредством простой операции вырезания и вставки. Кроме того, необходимо добавить ключевое слово partial к этому аспекту определе­ ния класса. // Employee.Internal.cs partial class Employee { 230 Часть II, Главные конструкции программирования на C# // Поля данных // Конструкторы } На заметку! Помните, что каждый аспект определения частичного класса должен быть помечен ключевым словом p a r t i a l ! Скомпилировав модифицированный проект, вы не должны заметить никакой раз­ ницы. Основная идея, положенная в основу частичного класса, реализуется только во время проектирования. Как только приложение скомпилировано, в сборке оказывается один цельный класс. Единственное требование при определении частичных типов свя­ зано с тем, что разные части должны иметь одно и то же имя и находиться в пределах одного и того же пространства имен .NET. Откровенно говоря, определения частичных классов применяются нечасто. Однако среда Visual Studio постоянно использует их в фоновом режиме. Позже в этой книге, ко­ гда речь пойдет о разработке приложений с графическим пользовательским интерфей­ сом посредством Windows Forms, Windows Presentation Foundation или ASP.NET, будет показано, что Visual Studio изолирует сгенерированный визуальным конструктором код в частичном классе, позволяя сосредоточиться на специфичной программной логике приложения. Исходный код. Проект Em ployeeАрр доступен в подкаталоге C hapter 5. Резюме Цель этой главы заключалась в ознакомлении с ролью типов классов С#. Вы видели, что классы могут иметь любое количество конструкторов, которые позволяют пользо­ вателю объекта устанавливать состояние объекта при его создании. В главе также было проиллюстрировано несколько приемов проектирования классов (и связанных с ними ключевых слов). Ключевое слово t h i s используется для получения доступа к текущему объекту, ключевое слово s t a t i c позволяет определять поля и члены, привязанные к классу (а не объекту), а ключевое слово con st (и модификатор rea d on ly) дает возмож­ ность определять элементы данных, которые никогда не изменяются после первона­ чальной установки. Большая часть главы была посвящена деталям первого принципа ООП: инкапсуля­ ции. Здесь вы узнали о модификаторах доступа C# и роли свойств типа, синтаксиса инициализации объектов и частичных классов. Обладая всеми этими знаниями, теперь вы готовы к тому, чтобы перейти к следующей главе, в которой узнаете о построении се­ мейства взаимосвязанных классов с использованием наследования и полиморфизма. ГЛАВА 6 Понятия наследования и полиморфизма предыдущей главе рассматривался первый принцип ООП — инкапсуляция. Вы узнали, как построить отдельный правильно спроектированный тип класса с конструкторами и различными членами (полями, свойствами, константами и поля­ ми, доступными только для чтения). В настоящей главе мы сосредоточимся на осталь­ ных двух принципах объектно-ориентированного программирования: наследовании и полиморфизме. Прежде всего, вы узнаете, как строить семейства связанных классов с применением наследования. Как будет показано, эта форма повторного использования кода позволя­ ет определять общую функциональность в родительском классе, которая может быть использована и, возможно, изменена в дочерних классах. По пути вы узнаете, как ус­ танавливать полиморфный интерфейс в иерархиях классов, используя виртуальные и абстрактные члены. Завершается глава рассмотрением роли начального родительского класса в библиотеках базовых классов .NET — S ystem .O bject. В Базовый механизм наследования Вспомните из предыдущей главы, что наследование — это аспект ООП, облегчающий повторное использование кода. Строго говоря, повторное использование кода существу­ ет в двух видах: наследование (отношение “является”) и модель включения/делегации (отношение “имеет”). Начнем главу с рассмотрения классической модели наследования типа “является”. При установке между классами отношения “является” строится зависимость между двумя или более типами классов. Базовая идея, лежащая в основе классического на­ следования, заключается в том, что новые классы могут создаваться с использованием существующих классов в качестве отправной точки. Давайте начнем с очень простого примера, создав новый проект консольного приложения по имени B a s ic ln h e r ita n c e . Предположим, что спроектирован класс по имени Саг, моделирующий некоторые базо­ вые детали автомобиля: // Простой базовый класс. class Саг { public readonly int maxSpeed; private int currSpeed; public Car (int max) { maxSpeed = max; } 232 Часть II. Главные конструкции программирования на C# public С а г () { 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 .WnteLine ("***** 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 “является” Саг. Отношение “является” (фор­ мально называемое классическим наследованием) позволяет строить новые определе­ ния классов, расширяющие функциональность существующих классов. Существующий класс, который будет служить основой для нового класса, называет­ ся базовым или родительским классом. Назначение базового класса состоит в опреде­ лении всех общих данных и членов для классов, которые расширяют его. Расширяющие классы формально называются производными или дочерними классами. В C# для уста­ новки между классами отношения “является” используется операция двоеточия в опре­ делении класса: // 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 не добавлены никакие члены, имеет­ ся прямой доступ к p u b lic -свойству Speed родительского класса, и таким образом, его код используется повторно. Это намного лучше, чем создавать класс MiniVan, имеющий в точности те же члены, что и Саг, такие как свойство Speed. В случае дублирования кода в этих двух классах придется сопровождать два фрагмента одинакового кода, что очевидно является непроизводительным расходом времени. Всегда помните, что наследование предохраняет инкапсуляцию, а потому следую­ щий код вызовет ошибку компиляции, поскольку приватные члены никогда не могут быть доступны через ссылку на объект: 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); // Ошибка! Доступ к приватным членам невозможен! myVan.currSpeed = 55; Console.ReadLine(); } Кстати говоря, если в MiniVan будет определен собственный набор членов, он не получит доступа ни к одному приватному члену базового класса Саг. Опять-таки, при­ ватные члены могут быть доступны только в классе, в котором они определены. // MiniVan унаследован от Саг. class MiniVan : Car { public void TestMethod() { // OK! Доступ к p u b lic -членам родителя в производном типе возможен. Speed = 10; // Ошибка! Нельзя осуществлять доступ к p riv a te -членам родителя //из производного типа! currSpeed = 10; } 234 Часть II. Главные конструкции программирования на C# О множественном наследовании ГЬворя о базовых классах, важно иметь в виду, что язык C# требует, чтобы любой конкретный класс имел в точности один непосредственный базовый класс. Невозможно создать тип класса, который напрямую унаследован от двух и более базовых классов (эта техника, поддерживаемая в неуправляемом C++, называется множественным на­ следованием). Попытка создать класс, в котором указано два непосредственных роди­ тельских класса, как показано в следующем коде, приводит к ошибке компиляции: //Не разрешается! Платформа .NET не допускает // множественное наследование классов! class WontWork : BaseClassOne, BaseClassTwo {} Как будет показано в главе 9, платформа .NET позволяет конкретному классу или структуре реализовывать любое количество дискретных интерфейсов. Благодаря этому, тип C# может представлять набор поведений, избегая сложностей, присущих множест­ венному наследованию. Кстати говоря, хотя класс может иметь только один непосред­ ственный базовый класс, допускается наследование одного интерфейса от множества других интерфейсов. Используя эту технику, можно строить изощренные иерархии ин­ терфейсов, моделирующих сложные поведения (см. главу 9). Ключевое слово s e a le d В C# поддерживается еще одно ключевое слово — 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. Понятия наследования и полиморфизма C ares Object Browser X Program <л 235 Start Page ■Ц ♦ CloneQ V Compare(string. hil . unnq, int, mt, System.StringComparisoni я ResolveEventHandler Comparefstnng int, string int, mt. System.Globalization.Cultun RuntimeArgumentKr •$» RuntimeFieldHandle •* ♦ Comparefstnng int string mt mt bool. System.Globalization.f ♦ Comparetstrmg mt string mt int bool) RuntimeMethodHanr W Comparefstnng int string mt int) RuntimeTypeHandle V Compare(strmg. string, bool System Globalization-Culturelnfo) T SByte ^ SerializableAttnbute ^ STAThreadAttribute Single StackOverflowExceptr'^5 • v’ £ StrmgComparer & public sealed class S tring Member of System S um m ary: Represents text as a series of Unicode characters StnngComparison Рис. 6.1. В библиотеках базовых классов определено множество запечатанных типов На заметку! В главе 4 было показано, что структуры C# всегда неявно запечатаны (см. табл. 4.3). Поэтому ни унаследовать одну структуру от другой, ни класс от структуры, ни структуру от класса не получится. Структуры могут использоваться только для моделирования отдельных атомарных, определенных пользователем типов. Для реализации отношения “является" необ­ ходимо применять классы. Как можно догадаться, существует множество других деталей наследования, о кото­ рых вы узнаете в оставшейся части главы. А пока просто имейте в виду, что операция двоеточия позволяет устанавливать между классами отношения “базовый-производный”, а ключевое слово sealed предотвращает наследование. Изменение диаграмм классов Visual Studio В главе 2 кратко упоминалось, что среда Visual Studio 2010 позволяет устанавливать между классами отношения “базовый-производный” визуально во время проектирова­ ния. Чтобы использовать этот аспект IDE-среды, первый шаг состоит во включении нового файла диаграммы классов в текущий проект. Для этого выберите в меню пункт P ro je c t^ A d d N ew Item (ПроектОДобавить новый элемент) и затем пиктограмму C lass D iagram (Диаграмма классов); на рис. 6.2 имя файла ClassDiagraml.cd было изменено на Cars .cd. installed Templates. |r ^ j Settings File Visual C * Items Text File Visual C * Items A is#rrb;y Inform ation File Visual C# Items Class Diagram Type: Visual C * Items л Visual C# Items Code Data General W eb | ||j jjj f^al I Class Diagram W indows r 0 rms W»F ’^ l Reporting Online Templates Application M anifest F>i* tt I Class Diagram j A Wanlc class diagram Visual C# hems Visual C * Items j W indows Script Host Visual C# Items m Debugger Visualize* Visual C * Items Add Рис. 6.2. Вставка в проект новой диаграммы классов J Cancel 236 Часть II. Главные конструкции программирования на C# После щелчка на кнопке Add [Добавить) появится пустая поверхность проектиро­ вания. Для добавления классов к диаграмме просто перетаскивайте каждый файл из окна Solution Explorer на эту поверхность. Также помните, что удаление элемента в ви­ зуальном конструкторе (за счет его выбора и нажатия клавиши <Delete>), не приводит к удалению ассоциированного исходного кода, а просто убирает элемент из поверхности проектирования. Текущая иерархия классов показана на рис. 6.3. Рис. 6.3. Визуальный конструктор Visual Studio На заметку! Итак, если необходимо автоматически добавить все текущие типы проекта на поверх­ ность проектирования, выберите узел P roject (Проект) в Solution Explorer и щелкните на кнопке View Class Diagram (Показать диаграмму классов) в правом верхнем углу окна Solution Explorer. Помимо простого отображения отношений между типами внутри текущего прило­ жения, вспомните из главы 2, что можно также создавать совершенно новые типы и наполнять их членами, используя панель инструментов C lass D e s ig n e r (Конструктор классов) и окно C lass D e ta ils (Детали класса). Если хотите использовать эти визуальные инструменты в процессе дальнейшего чтения книги — пожалуйста. Однако всегда анализируйте сгенерированный код, чтобы четко понимать, что эти инструменты делают за вашей спиной. Исходный код. Проект Basiclnheritance доступен в подкаталоге Chapter 6. Второй принцип ООП: подробности о наследовании Ознакомившись с базовым синтаксисом наследования, давайте рассмотрим более сложный пример и узнаем о многочисленных деталях построения иерархий классов. Для этого воспользуемся классом Employee, спроектированным в главе 5. Для начала создадим новое консольное приложение C# по имени Employees. Выберите пункт меню P ro je ct ^ A d d E xisting Item (Проекта Добавить существующий эле­ мент) и перейдите к месту нахождения файлов Employee.cs и Employee.Internals.cs, которые были созданы в примере EmployeeApp из предыдущей главы. Выберите оба Глава 6. Понятия наследования и полиморфизма 237 файла (щелкая на них при нажатой клавише < C trl> ) и щелкните на кнопке ОК. Среда Visual Studio 2010 отреагирует копированием каждого файла в текущий проект. Прежде чем начать построение производных классов, следует уделить внимание од­ ной детали. Поскольку первоначальный класс Employee был создан в проекте по имени EmployeeApp, этот класс находится в идентично названном пространстве имен .NETT. Пространства имен подробно рассматриваются в главе 14, а пока для простоты просто переименуйте текущее пространство имен (в обоих файлах) на Employees, чтобы оно соответствовало имени нового проекта: // Не забудьте изменить название пространства имен в обоих файлах! namespace Employees { partial class Employee { ••■} ) На заметку! Чтобы подстраховаться, скомпилируйте и запустите новый проект, нажав <C trl+F 5> . Пока программа ничего не делает, однако это позволит убедиться в отсутствии ошибок компиляции. Нашей целью будет создание семейства классов, моделирующих различные типы сотрудников компании. Предположим, что необходимо воспользоваться функциональ­ ностью класса Employee при создании двух новых классов (Salesperson и Manager). Иерархия классов, которую мы вскоре построим, будет выглядеть примерно так, как показано на рис. 6.4 (имейте в виду, что в случае использования синтаксиса автомати­ ческих свойств C# отдельные поля в диаграмме не будут видны). Рис. 6.4. Начальная иерархия классов 238 Часть II. Главные конструкции программирования на C# Как показано на рис. 6.4, класс S a le sp erso n “является” Employee (как и Manager). Вспомните, что в модели классического наследования базовые классы (вроде Employee) используются для определения характеристик, общих для всех наследников. Подклассы (такие как S ale sp erso n и Manager) расширяют общую функциональность, добавляя до­ полнительную специфическую функциональность. Для нашего примера предположим, что класс Manager расширяет Employee, храня количество опционов на акции, в то время как класс S alesperson поддерживает коли­ чество продаж. Добавьте новый файл класса (Manager.сс), определяющий тип Manager следующим образом: // Менеджерам нужно знать количество их опционов на акции. class Manager : Employee { public int StockOptions { get; set; } } Затем добавьте новый файл класса (S a le s P e rs o n .c s ), в котором определен класс S a lesp erson с соответствующим автоматическим свойством: // Продавцам нужно знать количество продаж. class Salesperson : Employee { public int SalesNumber { get; set; } } Теперь, после установки отношения “является”, S alesperson и Manager автоматиче­ ски наследуют все общедоступные члены базового класса Employee. Для иллюстрации обновите метод M ain() следующим образом: // Создание объекта подкласса и доступ к функциональности базового класса. 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(); } Управление созданием базового класса с помощью ключевого слова b a s e Сейчас объекты S a le sp erso n и Manager могут быть созданы только с использова­ нием “бесплатного” конструктора по умолчанию (см. главу 5). Памятуя об этом, пред­ положим, что к типу Manager добавлен новый конструктор, который принимает шесть аргументов и вызывается следующим образом: static void Main(string [] args) { // Предположим, что у Manager есть конструктор со следующей сигнатурой: // (s trin g fullName, in t age, in t empID, // f lo a t currPay, strin g ssn, in t numbOfOpts) Manager chucky = new Manager("Chucky", 50, 92, 100000, "333-23-2322", 9000); Console.ReadLine (); Глава 6. Понятия наследования и полиморфизма 239 Если взглянуть на список параметров, то ясно видно, что большинство из них долж­ но быть сохранено в переменных-членах, определенных в базовом классе Em ployee. В этом случае для класса 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 доступно только для чтения! SocialSecuntyNumber = ssn; } Первая проблема такого подхода состоит в том, что если определить какое-то свойст­ во как доступное только для чтения (например, S ocia lS ecu rityN u m b er), то присвоить значение входного параметра s t r i n g соответствующему полю не удастся, как можно видеть в финальном операторе специального конструктора. Вторая проблема состоит в том, что был неявно создан довольно неэффективный конструктор, учитывая тот факт, что в С#, если не указать иного, конструктор базо­ вого класса вызывается автоматически перед выполнением логики производного кон­ структора. После этого момента текущая реализация имеет доступ к многочисленным p u b lic -свойствам базового класса Employee для установки его состояния. Таким обра­ зом, в действительности во время создания объекта Manager выполняется семь дейст­ вий (обращений к пяти унаследованным свойствам и двум конструкторам). Для оптимизации создания производного класса необходимо хорошо реализовать конструкторы подкласса, чтобы они явно вызывали специальный конструктор базо­ вого класса вместо конструктора по умолчанию. Поступая подобным образом, можно сократить количество вызовов инициализаций унаследованных членов (что уменьшит время обработки). Давайте модифицируем специальный конструктор класса Manager, применив ключевое слово base: public Manager(string fullName, int age, int empID, float currPay, string ssn, int numbOfOpts) : base (fullNam e, age, empID, currPay, ssn) { // Это свойство определено в классе Manager. StockOptions = numbOfOpts; Здесь ключевое слово base ссылается на сигнатуру конструктора (подобно синтак­ сису, используемому для сцепления конструкторов на единственном классе с исполь­ зованием ключевого слова t h is , как было показано в главе 5), что всегда указывает на то, что производный конструктор передает данные конструктору непосредственного родителя. В данной ситуации явно вызывается конструктор с пятью параметрами, оп­ ределенный в Employee, что избавляет от излишних вызовов во время создания экзем­ пляра базового класса. Специальный конструктор S a le s p e rs o n выглядит в основном идентично: 240 Часть II. Главные конструкции программирования на C# / / В качестве общего правила, все подклассы должны юно вызывать // соответствующий конструктор базового класса. 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 () {} Хранение фамильных тайн: ключевое слово p r o t e c t e d Как уже должно быть известно, общедоступные (public) элементы непосредственно доступны отовсюду, в то время как приватные (private) могут быть доступны только в классе, где они определены. Вспомните из главы 5, что C# следует примеру многих дру­ гих современных объектных языков и предлагает дополнительное ключевое слово для определения доступности членов, а именно — protected (защищенный). Когда базовый класс определяет защищенные данные или защищенные члены, он устанавливает набор элементов, которые могут быть доступны непосредственно любо­ му наследнику. Например, чтобы позволить дочерним классам Salesperson и Manager непосредственно обращаться к разделу данных, определенному в Employee, можете из­ менить исходный класс Employee следующим образом: // Защищенные данные состояния. partial class Employee { // Теперь protected protected protected protected protected protected производные классы могут напрямую обращаться к этой информации. string empName; int empID; float currPay; int empAge; string empSSN; static string companyName; } Преимущество определения защищенных членов в базовом классе состоит в том, что производным типам больше не нужно обращаться к данным опосредованно, используя общедоступные методы и свойства. Возможным минусом такого подхода, конечно же, яв­ ляется то, что когда производный тип имеет прямой доступ к внутренним данным своего родителя, возникает вероятность непреднамеренного нарушения существующих бизнесправил, которые реализованы в общедоступных свойствах. При определении защищен­ ных членов создается уровень доверия между родительским и дочерним классами, по­ скольку компилятор не перехватит никаких нарушений существующих бизнес-правил. Глава 6. Понятия наследования и полиморфизма 241 И, наконец, имейте в виду, что с точки зрения пользователя объекта защищенные данные трактуются как приватные (поскольку пользователь находится “вне” семейства). Потому следующий код некорректен: static void Main(string [] args) { // Ошибка! Доступ к защищенным данным через экземпляр объекта невозможен! Employee emp = new Employee(); emp.empName = "Fred"; } На заметку! Хотя p r o t e c t e d -поля данных могут нарушить инкапсуляцию, объявлять p r o t e c t e d методы достаточно безопасно (и полезно). При построении иерархий классов очень часто при­ ходится определять набор методов, которые используются только производными типами. Добавление запечатанного класса Вспомните, что запечатанный (sealed) класс не может быть расширен другими клас­ сами. Как уже упоминалось, эта техника чаще всего применяется при проектировании служебных классов. Тем не менее, при построении иерархий классов можно обнару­ жить, что некоторая ветвь в цепочке наследования нуждается в “отсечении”, поскольку дальнейшее ее расширение не имеет смысла. Например, предположим, что в приложе­ ние добавлен еще один класс (PTSalesPerson), который расширяет существующий тип Salesperson. На рис. 6.5 показано текущее добавление. Рис. 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. Главные конструкции программирования на C# Учитывая, что запечатанные классы не могут быть расширены, может возникнуть вопрос: каким образом повторно использовать функциональность, если класс помечен как sealed? Чтобы построить новый класс, использующий функциональность запеча­ танного класса, единственным вариантом является отказ от классического наследова­ ния в пользу модели включения/делегации (т.е. отношения “имеет”). Реализация модели включения/делегации Вспомните, что повторное использование кода возможно в двух вариантах. Только что было рассмотрено классическое отношение “является”. Перед тем, как мы обратимся к третьему принципу ООП (полиморфизму), давайте поговорим об отношении “имеет” (еще известном под названием модели включения/делегации или агрегации). Предположим, что создан новый класс, который моделирует пакет льгот для сотрудников: // Этот новый тип будет работать как включаемый класс. class BenefitPackage { // Предположим, что есть другие члены, представляющие // медицинские/стоматологические программы и т.д. public double ComputePayDeduction() { return 125.0; Очевидно, что было бы довольно нелепо устанавливать отношение “является” между классом BenefitPackage и типами сотрудников. (Employee “является” BenefitPackage? Вряд ли). Однако должно быть ясно, что какие-то отношения между ними должны быть установлены. Короче говоря, понадобится выразить идею, что каждый сотрудник “име­ ет” 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; } В следующем обновленном методе M ain() обратите внимание на взаимодействие с внутренним типом B en efitsP a ck a ge, который определен в типе Employee: static void Main(string[] args) { Console .WnteLine ("**** * The Employee Class Hierarchy *****\n "); Manager chucky = new Manager("Chucky", 50, 92, 100000, "333-23-2322", 9000); double cost = chucky.GetBenefitCost(); Console.ReadLine(); } Определения вложенных типов В предыдущей главе была кратко упомянута концепция вложенных типов, которая является разновидностью только что рассмотренного отношения “имеет”. В C# (как и в других языках .NET) допускается определять тип (перечисление, класс, интерфейс, структуру или делегат) непосредственно внутри контекста класса или структуры. При этом вложенный (или “внутренний”) тип считается членом охватывающего (или “внеш­ него”) класса, и в глазах исполняющей системы им можно манипулировать как любым другим членом (полем, свойством, методом и событием). Синтаксис, используемый для вложения типа, достаточно прост: public class OuterClass { // Общедоступный вложенный тип может использоваться повсюду. public class PublicInnerClass {} // Приватный вложенный тип может использоваться // только членами включающего класса. private class PnvatelnnerClass {} } Хотя синтаксис ясен, понять, для чего это может потребоваться, не так-то просто. Чтобы разобраться с этой техникой, рассмотрим характерные особенности вложенных типов. • Вложенные типы позволяют получить полный контроль над уровнем доступа внутреннего типа, поскольку они могут быть объявлены как приватные (вспом­ ните, что не вложенные классы не могут быть объявлены с использованием клю­ чевого слова p r iv a te ). • Поскольку вложенный тип является членом включающего класса, он может иметь доступ к приватным членам включающего класса. • Часто вложенные типы удобны в качестве вспомогательных для внешнего класса и не предназначены для использования внешним миром. Когда тип включает в себя другой тип класса, он может создавать переменные-чле­ ны этого типа, как любой другой элемент данных. Однако если вложенный тип нужно использовать вне включающего типа, его понадобится квалифицировать именем вклю­ чающего типа. Взгляните на следующий код: 244 Часть II. Главные конструкции программирования на C# static void Main(string [] args) { // Создать и использовать общедоступный вложенный класс. Верно! OuterClass.PublicInnerClass inner; inner = new OuterClass.PublicInnerClass (); // Ошибка компилятора! Доступ к приватному классу невозможен! OuterClass.PrivatelnnerClass inner2; inner2 = new OuterClass.PrivatelnnerClass (); } Чтобы использовать эту концепцию в рассматриваемом примере с сотрудниками, предположим, что теперь определение BenefitPackage вложено непосредственно в класс 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 Блестяще! К этому моменту вы познакомились с множеством ключевых слов (и кон­ цепций), которые позволяют строить иерархии взаимосвязанных типов через класси­ ческое наследование, включение и вложенные типы. Если пока не все детали ясны, не переживайте. На протяжении оставшейся части книги вы построите еще много допол­ нительных иерархий. А теперь давайте перейдем к рассмотрению последнего принципа ООП: полиморфизма. Третий принцип ООП: поддержка полиморфизма в C# Вспомните, что в базовом классе Employee был определен метод по имени GiveBonusO со следующей первоначальной реализацией: public partial class Employee { public void GiveBonus(float amount) { currPay += amount; } Поскольку этот метод был определен с ключевым словом public, теперь можно раз­ давать бонусы продавцам и менеджерам (а также продавцам с частичной занятостью): static void Main(string[] args) { Console.WnteLine ("**** * The Employee Class Hierarchy *****\n "); // Дать каждому сотруднику бонус? Manager chucky = new Manager("Chucky", 50, 92, 100000, "333-23-2322", 9000); chucky.GiveBonus(300); chucky.DisplayStats(); Console.WnteLine () ; Salesperson fran = new Salesperson ("Fran", 43, 93, 3000, "932-32-3232", 31); fran.GiveBonus(200); fran.DisplayStats(); Console.ReadLine(); } Проблема текущего кода состоит в том, что общедоступно унаследованный метод GiveBonusO работает идентично для всех подклассов. В идеале при подсчете бонуса для штатного продавца и частично занятого продавца должно приниматься во вни­ мание количество продаж. Возможно, менеджеры должны получать дополнительные опционы на акции вместе с денежным вознаграждением. Учитывая это, вы однажды столкнетесь с интересным вопросом: “Как сделать так, чтобы связанные типы по-раз­ ному реагировали на один и тот же запрос?”. Попробуем отыскать на него ответ. Ключевые слова v i r t u a l и o v e r r i d e Полиморфизм предоставляет подклассу способ определения собственной версии ме­ тода, определенного в его базовом классе, с использованием процесса, который назы­ вается переопределением метода (method overriding). Чтобы пересмотреть текущий ди­ зайн, нужно понять значение ключевых слов virtual и override. Если базовый класс желает определить метод, который может быть (но не обязательно) переопределен в подклассе, он должен пометить его ключевым словом virtual: 246 Часть II. Главные конструкции программирования на C# 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 && numberOf Sales <= 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 (500); } } Обратите внимание на использование каждым переопределенным методом поведе­ ния по умолчанию через ключевое слово base. Таким образом, полностью повторять реализацию логики GiveBonus () вовсе не обязательно, а вместо этого можно повторно использовать (и, возможно, расширять) поведение по умолчанию родительского класса. Также предположим, что текущий метод DisplayStatus () класса Employee объявлен виртуальным. При этом каждый подкласс может переопределять этот метод в расчете на отображение количества продаж (для продавцов) и текущих опционов на акции (для менеджеров). Например, рассмотрим версию метода DisplayStatus () в классе Manager (класс Salesperson должен реализовать DisplayStatus () аналогичным образом, чтобы вывести на консоль количество продаж): Глава 6. Понятия наследования и полиморфизма 247 public override void DisplayStats () { base.DisplayStats(); Console.WnteLine ("Number of Stock Options: {0}", numberOf Options); } Теперь, когда каждый подкласс может интерпретировать, что именно эти виртуаль­ ные методы означают для него, каждый экземпляр объекта ведет себя как более неза­ висимая сущность: static void Main(string [] args) { Console.WnteLine ("***** The Employee Class Hierarchy *****\n "); // Лучшая система бонусов! Manager chucky = new Manager("Chucky", 50, 92, 100000, "333-23-2322", 9000); chucky.GiveBonus(300); chucky.DisplayStats(); Console.WriteLine(); Salesperson fran = new Salesperson("Fran", 43, 93, 3000, "932-32-3232", 31); fran.GiveBonus(200); 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: 43 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 реагирует автоматиче­ ским заполнением шаблона метода вместо вас. Обратите внимание, что также добавля­ ется оператор кода, который вызывает родительскую версию виртуального члена (эту строку можно удалить, если она не нужна). Например, при использовании этой техники во время переопределения метода DisplayStatus () добавится следующий автоматиче­ ски сгенерированный код: public override void DisplayStats () { base.DisplayStats () ; 248 Часть II. Главные конструкции программирования на C# Рис. 6.6 . Быстрый просмотр переопределяемых методов в Visual Studio 2010 Запечатывание виртуальных членов Вспомните, что ключевое слово sealed применяется к типу класса для предотвра­ щения расширения другими типами его поведения через наследование. Ранее класс PTSalesPerson был запечатан на основе предположения, что разработчикам не имеет смысла дальше расширять эту линию наследования. Иногда требуется не запечатывать класс целиком, а просто предотвратить пере­ определение некоторых виртуальных методов в производных типах. Например, пред­ положим, что продавцы с частичной занятостью не должны получать определенные бонусы. Чтобы предотвратить переопределение виртуального метода GiveBonus() в классе PTSalesPerson, можно запечатать этот метод в классе Salesperson следующим образом: // S a le s p e r s o n зап е ч ат ал м етод G iv e B o n u s ()1 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() и DisplayStatus()), которые могут быть переопределены наследниками. Хотя все это хорошо и замечательно, у данного дизайна есть один неприятный побоч­ ный эффект: можно непосредственно создавать экземпляры базового класса Employee: // Что это будет означать? Employee X = new Employee() ; В нашем примере базовый класс Employee имеет единственное назначение — опре­ делить общие члены для всех подклассов. По всем признакам вы не намерены позво­ лять кому-либо создавать прямые экземпляры этого класса, поскольку тип Employee слишком общий по своей природе. Например, если кто-то скажет: “Я сотрудник!”, то тут же возникнет вопрос: “Какой конкретно сотрудник?” (консультант, инструктор, админи­ стративный работник, редактор, советник в правительстве и т.п.). Учитывая, что многие базовые классы склонны быть довольно неопределенными сущностями, намного лучший дизайн для рассматриваемого примера не должен разре­ шать непосредственное создание в коде нового объекта Employee. В C# можно добиться этого с использованием ключевого слова abstract в определении класса, создавая, та­ ким образом, абстрактный базовый класс: П р е в р а щ е н и е класса Employee в абстрактный // для предотвращения прямого создания экземпляров. abstract partial class Employee / / } После этого попытка создать экземпляр класса Employee приведет к ошибке во вре­ мя компиляции: // Ошибка! Нельзя создавать экземпляр абстрактного класса! Employee X = new Employee(); На первый взгляд может показаться очень странным, зачем определять класс, экзем­ пляр которого нельзя создать непосредственно. Однако вспомните, что базовые классы (абстрактные или нет) очень полезны тем, что содержат общие данные и общую функ­ циональность унаследованных типов. Используя эту форму абстракции, можно также моделировать общую “идею” сотрудника, а не обязательно конкретную сущность. Также следует понимать, что хотя непосредственно создать абстрактный класс нельзя, он все же присутствует в памяти, когда создан экземпляр его производного класса. Таким об­ разом, совершенно нормально (и принято) для абстрактных классов определять любое количество конструкторов, вызываемых опосредованно при размещении в памяти эк­ земпляров производных классов. Теперь получилась довольно интересная иерархия сотрудников. Позднее в этой гла­ ве, при рассмотрении правил приведения типов С#, мы добавим немного больше функ­ циональности к этому приложению. А пока на рис. 6.7 показан основной дизайн типов на данный момент. Исходный код. Проект Employees доступен в подкаталоге Chapter 6. 250 Часть II. Главные конструкции программирования на C# Полиморфный интерфейс Когда класс определен как абстрактный базовый (с помощью ключевого слова abstract), в нем может определяться любое количество абстрактных членов. Абстрактные члены могут использоваться везде, где необходимо определить член, которые не предла­ гает реализации по умолчанию. За счет этого вы навязываете полиморфный интерфейс каждому наследнику, возлагая на них задачу реализации конкретных деталей абстракт­ ных методов. Полиморфный интерфейс абстрактного базового класса просто ссылается на его набор виртуальных и абстрактных методов. На самом деле это интереснее, чем может показаться на первый взгляд, поскольку данная особенность ООП позволяет строить легко расширяемое и гибкое программное обеспечение. Для иллюстрации реализуем (и слегка модифицируем) иерархию фигур, кратко описанную в главе 5 при обзоре принципов ООП. Для начала создадим новый проект консольного приложения C# по имени Shapes. Обратите внимание на рис. 6.8, что типы Hexagon и Circle расширяют базовый класс Shape. Подобно любому базовому классу, в Shape определен набор членов (в дан­ ном случае свойство PetName и метод Draw()), общих для всех наследников. Подобно иерархии классов сотрудников, нужно запретить непосредственное созда­ ние экземпляров Shape, поскольку этот тип представляет слишком абстрактную кон­ цепцию. Чтобы предотвратить прямое создание экземпляров Shape, можно определить его как абстрактный класс. Также, учитывая, что производные типы должны уникаль­ ным образом реагировать на вызов метода Draw(), давайте пометим его как virtual и определим реализацию по умолчанию. Employer я A b stract Class : в Fields I ^ P ro p e rtie s • ® M e th o d s 13 N e s te d T y pes B e n e fr tP a c k a g e A Class 71 M e th o d s ¥ C o m p u te P a y D e d u c tio n j - N e s te d Ty p e s T m e e D C ir d e Class Circle Рис. 6.7. Иерархия классов E m p l o y e e Рис. 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: // C irc le не переопределяет D ra w (). class Circle : Shape { public Circle () {} public Circle(string name) : base(name){} } // Hexagon переопределяет D ra w (). 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. Главные конструкции программирования на C# Ясно, что это не особо интеллектуальный дизайн для текущей иерархии. Чтобы заставить каждый класс переопределить метод Draw(), можно определить Draw() как абстрактный метод класса Shape, а это означает отсутствие какой-либо реализации по умолчанию. Для пометки метода как абстрактного в C# служит ключевое слово a b s t r a c t . Не забывайте, что абстрактные методы не предусматривают вообще ника­ кой реализации: abstract class Shape { // Вынудить все дочерние классы определить свою визуализацию. public abstract void Draw(); На заметку! Абстрактные методы могут определяться только в абстрактных классах. Попытка по­ ступить иначе приводит к ошибке во время компиляции. Методы, помеченные как abstract, являются чистым протоколом. Они просто оп­ ределяют имя, возвращаемый тип (если есть) и набор параметров (при необходимости). Здесь абстрактный класс Shape информирует типы-наследники о том, что у него есть метод по имени Draw(), который не принимает аргументов и ничего не возвращает. О необходимых деталях должен позаботиться наследник. С учетом этого метод Draw() в классе Circle теперь должен быть обязательно пере­ определен. В противном случае Circle также должен быть абстрактным типом и осна­ щен ключевым словом abstract (что очевидно не подходит в данном примере). Ниже показаны необходимые изменения в коде: // Если не реализовать здесь абстрактный метод D ra w (), то C irc le также // должен считаться абстрактным, и тогда должен быть помечен как abstract! class Circle : Shape { public Circle () {} public Circle(string name) : base(name) {} public override void Draw() { Console .WnteLine ("Drawing {0} the Circle", PetName) ; } Выражаясь кратко, теперь делается предположение о том, что любой унаследован­ ный от Shape класс должен иметь уникальную версию метода Draw(). Для демонстра­ ции полной картины полиморфизма рассмотрим следующий код: static void Main(string [] args) { Console.WriteLine ("***** Fun with Polymorphism *****\n"); // Создать массив совместимых c Shape объектов. Shape[] myShapes = {new Hexagon (), new Circle(), new Hexagon("Mick"), new Circle("Beth"), new Hexagon("Linda")}; // Пройти циклом no всем элементам и взаимодействовать / / с полиморфным интерфейсом. foreach (Shape s in myShapes) { s .Draw (); } Console.ReadLine (); Глава 6. Понятия наследования и полиморфизма 253 Ниже показан вывод этого метода Main(): ***** Fun with Polymorphism ***** Drawing Drawing Drawing Drawing Drawing NoName the Hexagon NoNapie the Circle Mick the Hexagon Beth the Circle Linda the Hexagon Этот метод Main () иллюстрирует использование полиморфизма в чистом виде. Хотя невозможно напрямую создавать экземпляры абстрактного базового класса (Shape), можно свободно сохранять ссылки на объекты любого подкласса в абстрактной базовой переменной. Таким образом, созданный массив объектов Shape может хранить объек­ ты, унаследованные от базового класса Shape (попытка поместить в массив объекты, несовместимые с Shape, приводит к ошибке во время компиляции). Учитывая, что все элементы в массиве my Shapes действительно наследуются от Shape, известно, что все они поддерживают один и тот же полиморфный интерфейс (или, говоря конкретно — все они имеют метод Draw()). Выполняя итерацию по масси­ ву ссылок Shape, исполняющая система сама определяет, какой конкретный тип имеет каждый его элемент. И в этот момент вызывается корректная версия метода Draw(,). Эта техника также делает очень простой и безопасной задачу расширения текущей иерархии. Например, предположим, что от абстрактного базового класса Shape унас­ ледовано еще пять классов (T r ia n g le , Square и т.д.). Благодаря полиморфному интер­ фейсу, код внутри цикла fo re a c h не потребует никаких изменений, если компилятор увидит, что в массив myShapes помещены только Shape-совместимые типы. Сокрытие членов Язык C# предоставляет средство, логически противоположное переопределению ме­ тодов, которое называется сокрытием (shadowing). Выражаясь формально, если про­ изводный класс определяет член, который идентичен члену, определенному в базовом классе, то производный класс скрывает родительскую версию. В реальном мире такая ситуация чаще всего возникает при наследовании от класса, который создавали не вы (и не ваша команда), например, в случае приобретения пакета программного обеспече­ ния .NET у независимого поставщика. Для иллюстрации предположим, что вы получили от коллеги класс по им е­ ни T h r e e D C itc le , в котором определен метод по имени Draw(), не принимающий аргументов: class ThreeDCircle { public void Draw() { Console .WnteLine ("Drawing a 3D Circle"); } } Вы обнаруживаете, что T h re e D C irc le “является” C ir c le , поэтому наследуете его от существующего типа C ir c le : class ThreeDCircle : Circle { public void Draw() { Console .WnteLine ("Drawing a 3D Circle"); } } 254 Часть II. Главные конструкции программирования на C# После компиляции в окне ошибок Visual Studio 2010 появляется предупреждение (рис. 6.9). Рис. 6 .9 . Мы только что скрыли член родительского класса Проблема в том, что в производном классе (ThreeDCircle) присутствует метод, иден­ тичный унаследованному методу. Точное предупреждение компилятора в этом случае будет таким: 'jShapes .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. T h r e e D C ir c le . Draw () ' скрывает унаследованный член 'Shapes. C i r c l e . Draw () '. Чтобы заставить текущий член переопределить эту реализацию , добавьте ключевое сл о в о o v e r r id e . В противном случае добавьте ключевое сл о во 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: // Этот класс расширяет C irc le и скрывает унаследованный метод 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 доступен в подкаталоге C h apter 6. Правила приведения к базовому и производному классу Теперь, когда вы научились строить семейства взаимосвязанных типов классов, сле­ дует познакомиться с правилами, которым подчиняются операции приведения классов. Для этого вернемся к иерархии классов Employee, созданной ранее в главе. На платфор­ ме .NETконечным базовым классом служит System .O bject. Поэтому все, что создается, “является” O b ject и может трактоваться как таковой. Учитывая этот факт, в объектной переменной можно хранить ссылку на экземпляр любого типа: void CastingExamples () { // Manager "является" System.Object, поэтому можно сохранять // ссылку на Manager в переменной типа o bject. object frank = new Manager("Frank Zappa", 9, 3000, 40000, "111-11-1111", 5); } В примере Em ployees типы Manager, S a le s p e rs o n и P T S a lesP e rso n расширяют класс Employee, поэтому можно хранить любой из этих объектов в допустимой ссылке на базовый класс. Это значит, что следующий код также корректен: void CastingExamples () { // Manager "является" System.Object, поэтому можно сохранять // ссылку на Manager в переменной типа object. object frank = new Manager("Frank Zappa", 9, 3000, 40000, "111-11-1111", 5); // Manager также "является" Employee. Employee moonUnit = new Manager("MoonUnit Zappa", 2, 3001, 20000, "101-11-1321", 1); // PTSalesPerson "является" Salesperson. Salesperson Jill = new PTSalesPerson ("Jill", 834, 3002, 100000, "111-12-1119", 90); } 256 Часть II. Главные конструкции программирования на C# Первое правило приведения между типами классов гласит, что когда два класса свя­ заны отношением “является”, всегда можно безопасно сохранить производный тип в ссылке базового класса. Формально это называется неявным приведением, поскольку оно “просто работает” в соответствии с законами наследования. Это делает возможным построение некоторых мощных программных конструкций. Например, предположим, что в текущем классе Program определен новый метод: static void GivePromotion(Employee emp) { // Повысить зарплату... // Предоставить место на парковке компании. .. Console .WnteLine (" {0 } was promoted!", emp. Name); } Поскольку этот метод принимает единственный параметр типа Employee, можно эф­ фективно передавать этому методу любого наследника от класса Employee, учитывая отношение “является”: static void CastingExamples () { // Manager "является" System.Object, поэтому можно сохранять // ссылку на Manager в переменной типа object. object frank = new Manager("Frank Zappa", 9, 3000, 40000, "111-11-1111", 5) ; // Manager также "является" Employee. Employee moonUnit = new Manager("MoonUnit Zappa", 2, 3001, 20000, "101-11-1321", 1); GivePromotion(moonUnit); // PTSalesPerson "является" Salesperson. Salesperson jill = new PTSalesPerson ("Jill", 834, 3002, 100000, "111-12-1119", 90); GivePromotion(jill); Предыдущий код компилируется, благодаря неявному приведению от типа базового класса (Employee) к производному классу. Однако что если нужно также вызвать метод G iveP ro m o tio n () для объекта fra n k (хранимого в данный момент в обобщенной ссылке System .O bject)? Если вы передадите объект fran k непосредственно в G iveProm otion (), как показано ниже, то получите ошибку во время компиляции: // Ошибка! object frank = new Manager("Frank Zappa", 9, 3000, 40000, "111-11-1111", 5); GivePromotion(frank); Проблема в том, что предпринимается попытка передать переменную, которая яв­ ляется не Employee, а более общим объектом S ystem .O bject. Поскольку он находится выше в цепочке наследования, чем Employee, компилятор не допустит неявного приве­ дения, стараясь обеспечить максимально возможную безопасность типов. Несмотря на то что вы можете определить, что объектная ссылка указывает на Em ployee-совместимый класс в памяти, компилятор этого сделать не может, посколь­ ку это не будет известно вплоть до времени выполнения. Чтобы удовлетворить компи­ лятор, понадобится выполнить явное приведение. Второе правило приведения гласит: необходимо явно выполнять приведение “вниз”, используя операцию приведения С#. Базовый шаблон, которому нужно следовать при выполнении явного приведения, вы­ глядит примерно так: ( Класс_к_которому_нужно_привести) существующаяСсылка Таким образом, чтобы передать переменную o b je c t методу G iveP ro m o tio n (), потре­ буется написать следующий код: // Корректно! GivePromotion((Manager)frank); Глава 6. Понятия наследования и полиморфизма 257 Ключевое слово a s Помните, что явное приведение происходит во время выполнения а не во время ком­ пиляции. Поэтому показанный ниже код: // Нет! Приводить frank к типу Hexagon нельзя, хотя код скомпилируется! Hexagon hex = (Hexagon)frank; компилируется нормально, но вызывает ошибку времени выполнения, или, более фор­ мально — исключение времени выполнения В главе 7 будут рассматриваться подробно­ сти структурированной обработки исключений, а пока следует лишь отметить, что при выполнении явного приведения можно перехватывать возможные ошибки приведения, применяя ключевые слова try и catch (см. главу 7): // Перехват возможной ошибки приведения. try { Hexagon hex = (Hexagon)frank; } catch (InvalidCastException ex) { Console .WnteLine (ex.Message) ; } Хотя это хороший пример защитного (defensive) программирования, C# предоставля­ ет ключевое слово as для быстрого определения совместимости одного типа с другим во время выполнения. С помощью ключевого слова as можно определить совместимость, проверив возвращенное значение на равенство null. Взгляните на следующий код: // Использование аз для проверки совместимости. Hexagon hex2 = frank as Hexagon; if (hex2 == null) Console .WnteLine ("Sorry, frank is not a Hexagon..."); Ключевое слово i s Учитывая, что метод GivePromotionO был спроектирован для приема любого воз­ можного типа, производного от Employee, может возникнуть вопрос — как этот метод может определить, какой именно производный тип был ему передан? И, кстати, если входной параметр имеет тип Employee, как получить доступ к специализированным членам типов Salesperson и Manager? В дополнение к ключевому слову as, в C# предлагается ключевое слово is, кото­ рое позволяет определить совместимость двух типов. В отличие от ключевого слова as, если типы не совместимы, ключевое слово is возвращает false, а не n u ll-ссылку. Рассмотрим следующую реализацию метода GivePromotionO: static void GivePromotion(Employee emp) { Console.WnteLine (" {0 } was promoted!", emp.Name); if (emp is Salesperson) { Console .WnteLine ("{ 0 } made {1} sale(s) !", emp.Name, ((Salesperson)emp) .SalesNumber); Console.WnteLine () ; } if (emp is Manager) { Console .WnteLine ("{ 0 } had {1} stock options ...", emp.Name, ((Manager)emp).StockOptions); 258 Часть II. Главные конструкции программирования на C# Console.WriteLine(); } } Здесь во время выполнения производится проверка, на что именно в памяти указы­ вает ссылка типа базового класса. Определив, что примят Salesperson или Manager, можно применить явное приведение и получить доступ к специализированным членам класса. Также обратите внимание, что окружать операции приведения конструкцией try/catch не обязательно! поскольку внутри контекста if, выполнившего проверку ус­ ловия, уже известно, что приведение безопасно. Родительский главный класс S y s te m .O b je c t В завершение этой главы исследуем детали устройства родительского главного клас­ са всей платформы .NET — Object. Возможно, вы уже заметили в предыдущих разде­ лах, что базовые классы всех иерархий (Car, Shape, Employee) никогда явно не указы­ вали свои родительские классы: // Кто родитель Саг? class Саг {...} В мире .NET каждый тип в конечном итоге наследуется от базового класса по имени System.Object (который в C# может быть представлен ключевым словом object). Класс Object определяет набор общих членов для каждого типа в каркасе. Фактически, при построении класса, который явно не указывает своего родителя, компилятор автома­ тически наследует его от Object. Если нужно очень четко прояснить свои намерения, можно определить класс, производный от Object, следующим образом: // Явное наследование класса от System.Object. class Car : object { . .. } Как и в любом другом классе, в System.Object определен набор членов. В следую­ щем формальном определении C# обратите внимание, что некоторые из этих членов определены как 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. Основные методы S y s te m .O b je c t Метод экземпляра Назначение Equals () По умолчанию этот метод возвращает true, только если сравнивае­ мые элементы ссылаются в точности на один и тот же объект в памя­ ти. Таким образом, Equals () используется для сравнения объектных ссылок, а не состояния объекта. Обычно этот метод переопределяет­ ся, чтобы возвращать true, только если сравниваемые объекты име­ ют одинаковые значения внутреннего состояния. Следует отметить, что в случае переопределения Equals () потребу­ ется также переопределить метод GetHashCodeO, потому что эти методы используются внутренне типами Hashtable для извлечения подобъектов из контейнера. Также вспомните из главы 4, что в классе ValueType этот метод пе­ реопределен для всех структур, чтобы он работал для сравнения на базе значений Finalize () На данный момент можно считать, что этот метод (будучи переопре­ деленным) вызывается для освобождения любых размещенных ре­ сурсов перед удалением объекта. Сборка мусора CLR более подробно рассматривается в главе 8 GetHashCodeO Этот метод возвращает значение int, идентифицирующее конкрет­ ный экземпляр объекта ToStringO Этот метод возвращает строковое представление объекта, используя формат Пространство имен>.<имя типа> (так называемое полностью квалифицированное имя). Этот метод часто переопреде­ ляется в подклассе для возврата строки, состоящей из пар “имя/значение” , которая представляет внутреннее состояние объекта, вместо полностью квалифицированного имени GetTypeO Этот метод возвращает объект Туре, полностью описывающий объект, на который в данный момент производится ссылка. Коротко говоря, это метод идентификации типа во время выполнения (Runtime Туре Identification — RTTI), доступный всем объектам (подробно обсуждается в главе 15) MemberwiseClone () Этот метод возвращает полную (почленную) копию текущего объекта и часто используется для клонирования объектов (см. главу 9) Чтобы проиллюстрировать поведение по умолчанию, обеспечиваемое базовым клас­ сом Object, создадим новое консольное приложение C# по имени ObjectOverrides. Добавим в проект новый тип класса С#, содержащий следующее пустое определение типа по имени Person: // Помните: Person расширяет Object. class Person {} Теперь дополним метод Main() взаимодействием с унаследованными членами System.Object, как показано ниже: class Program { static void Main(string[] args) Console.WnteLine ("***** Fun with System.Object *****\n"); Person pi = new Person (); // Использовать унаследованные члены System.Object. Console.WriteLine("ToString: {0}", p i .ToString ()); 260 Часть II. Главные конструкции программирования на C# Console.WriteLine ("Hash code: {0}", p i .GetHashCode()); Console.WnteLine ("Type : {0}", p i .GetType () ); // Создать другую ссылку на p i. Person p2 = pi; object о = p2; // Указывают ли ссылки на один и тот же объект в памяти? if (о.Equals (pi) && р 2 .Equals (о)) { Console.WriteLine("Same instance1"); // один и тот же экземпляр } Console.ReadLine (); } Вывод этого метода Main() выглядит следующим образом: ***** Fun with System.Object ***** ToStnng: ObjectOverndes .Person Hash code: 46104728 Type: ObjectOverndes .Person Same instance! Первым делом, обратите внимание, что реализация T o S t n n g () по умолчанию воз­ вращает полностью квалифицированное имя текущего типа (ObjectOverrides.Person). Как будет показано позже, при рассмотрении построения специальных пространств имен в главе 14, каждый проект C# определяет “корневое пространство имен”, назва­ ние которого совпадает с именем проекта. Здесь мы создали проект под названием 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 public public public string FirstName { get; set; } string LastName { get; set; } int Age { get; set; } Person(string fName, string IName, int personAge) { FirstName = fName; LastName = IName; Age = personAge; } public Person () {} Глава 6. Понятия наследования и полиморфизма 261 Переопределение S y s t e m . O b j e c t . T o S t r i n g O Многие создаваемые классы (и структуры) выигрывают от переопределения T o S trin gO для возврата строки с текстовым представлением текущего состояния эк­ земпляра типа. Помимо прочего, это может быть довольно полезно при отладке. Как вы решите конструировать эту строку — дело персонального вкуса; однако рекомендо­ ванный подход состоит в разделении двоеточиями пар “имя/значение” и взятии всей строки в квадратные скобки (многие типы из библиотек базовых классов .NETT следуют этому принципу). Рассмотрим следующую переопределенную версию T o S tr in g O для нашего класса Person: public override string ToStringO { string myState; myState = string.Format("[First Name: {0}; Last Name: {1}; Age: {2}]", FirstName, LastName, Age); return myState; } Эта реализация T o S tr in g O довольно прямолинейна, учитывая, что класс Person состоит всего их трех фрагментов данных состояния. Однако всегда нужно помнить, что правильное переопределение T o S trin g O должно также учитывать все данные, оп­ ределенные выше в цепочке наследования. Когда вы переопределяете T o S trin g O для класса, расширяющего специальный ба­ зовый класс, первое, что следует сделать — получить возврат T o S trin g O от родитель­ ского класса, используя слово base. Получив строковые данные родителя, можно доба­ вить к ним специальную информацию производного класса. Переопределение S y s t e m .Ob j e c t . E q u a l s () Давайте также переопределим поведение O b j e c t . E q u a l s () для работы с семан­ тикой на основе значений. Вспомните, что по умолчанию E qu als () возвращает tru e , только если два сравниваемых объекта ссылаются на один и тот же экземпляр объекта в памяти. Для класса Person может быть полезно реализовать E qu als () для возврата tru e, когда две сравниваемых переменных содержат одинаковые значения (т.е. фами­ лию, имя и возраст). Прежде всего, обратите внимание, что входной аргумент метода E qu als () — это об­ щий S ystem .O bject. С учетом этого первое, что нужно сделать — удостовериться, что вызывающий код действительно передал тип Person, и для дополнительной подстра­ ховки проверить, что входной параметр не является n u ll-ссылкой. Установив, что передан размещенный Person, один подход состоит в реализации E q u a ls() для выполнения сравнения поле за полем данных входного объекта с соот­ ветствующими данными текущего объекта: public override bool Equals(ob]ect obj) { if (obj is Person && obj != null) { Person temp; temp = (Person)obj; if (temp.FirstName == this.FirstName && temp.LastName == this.LastName && temp.Age == this.Age) { return true; 262 Часть II. Главные конструкции программирования на C# else { return false; return false; } Здесь производится сравнение значения входного объекта с внутренними значения­ ми текущего объекта (обратите внимание на применение ключевого слова this). Если имя, фамилия и возраст, записанные в двух объектах, идентичны, значит, есть два объ­ екта с одинаковыми данными, и потому возвращается true. Любые другие возможные результаты возвратят false. Хотя этот подход действительно работает, представьте, насколько трудоемкой была бы реализация специального метода Equals () для нетривиальных типов, которые могут содержать десятки полей данных. Распространенным сокращением является использо­ вание собственной реализации ToStringO. Если у класса имеется правильная реали­ зация ToStringO, которая учитывает все поля данных вверх по цепочке наследования, можно просто сравнить строковые данные объектов: public override bool Equals(object obj) { // Больше нет необходимости приводить obj к типу Person, // поскольку у всех имеется метод ToStringO . return obj.ToString () == this.ToString(); } Обратите внимание, что в этом случае нет необходимости проверять входной аргу­ мент на принадлежность к корректному типу (в нашем примере — Person), поскольку все классы в .NET поддерживают метод ToStringO . Еще лучше то, что больше не нужно выполнять проверку равенства свойства за свойством, поскольку теперь просто прове­ ряются значения, возвращенные методом ToStringO. Переопределение S y s t e m . O b je c t . G e t H a s h C o d e Q Когда класс переопределяет метод Equals (), вы также обязаны переопределить реа­ лизацию по умолчанию GetHashCode (). ГЬворя упрощенно, хеш-код — это числовое значение, представляющее объект как определенное состояние. Например, если созда­ ны две переменных string, хранящие значение Hello, они должны давать один и тот же хеш-код. Однако если одна переменная string хранит строку в нижнем регистре (hello), должны быть получены разные хеш-коды. По умолчанию System.Object.GetHashCоde() использует текущее местоположение объекта в памяти для порождения хеш-значения. Тем не менее, при построении специ­ ального типа, который нужно хранить в коллекции Hashtable (из пространства имен System.Collect ions), этот член должен быть всегда переопределен, поскольку Hashtable внутри вызывает Equals () HGetHashCodeO, чтобы извлечь правильный объект. На заметку! Точнее говоря, класс System.Collections.Hashtable внутренне вызывает ме­ тод GetHashCode () для получения общего представления местоположения объекта, но по­ следующий вызов Equals () определяет точное соответствие. Хотя мы не собираемся помещать Person в System.Collections .Hashtable, для полноты давайте переопределим GetHashCode (). Существует немало алгоритмов, кото­ рые могут применяться для создания хеш-кода, одни из которых причудливы, а дру­ гие — не очень. В большинстве случаев можно сгенерировать значение хеш-кода, пола­ гаясь на реализацию System.String.GetHashCode(). Глава 6. Понятия наследования и полиморфизма 263 Исходя из того, что класс S t r in g уже имеет солидный алгоритм хеширования, ис­ пользующий символьные данные S tr in g для сравнения хеш-значений, если вы можете идентифицировать часть данных полей класса, которая должна быть уникальной для всех экземпляров (вроде номера карточки социального страхования), просто вызови­ те GetHashCode () на этой части полей данных. Поскольку в классе Person определено свойство SSN, можно написать следующий код: // Вернуть хеш-код на основе уникальных строковых данных. public override int GetHashCode() { return this .ToStnng () .GetHashCode () ; } Если же выбрать уникальный строковый элемент данных затруднительно, но есть переопределенный метод T o S tn n g O , вызовите GetHashCode() на собственном строко­ вом представлении: // Возвращает хеш-код на основе значения ToStringO персоны, public override int GetHashCode () { return this .T o Stnng () .GetHashCode () ; } Тестирование модифицированного класса P e r s o n Теперь, когда виртуальные члены O b jec t переопределены, давайте обновим M ain() для добавления проверки внесенных изменений. static void Main(string[] args) { Console.WriteLine ("***** Fun with System.Object *****\n"); // ПРИМЕЧАНИЕ: эти объекты идентичны для проверки // методов E quals() и GetHashCode( ) . Person pi = new Person ("Homer", "Simpson", 50); Person p2 = new Person("Homer", "Simpson", 50); // Получить строковые версии объектов. Console.WriteLine("pi.ToString () = {0}", p i .ToString ()); Console .WriteLine ("p2 .ToStnng () = {0}", p2 .ToString ()) ; // Проверить переопределенный метод E qu als( ) . Console.WriteLine("pi = p2?: {0}", p i .Equals(p2)); // Проверить хеш-коды. Console.WriteLine ("Same hash codes?: {0}", pi.GetHashCode () == p2.GetHashCode ()) ; Console.WriteLine(); // Изменить возраст p2 и проверить снова. р 2 .Age = 45; Console.WriteLine("pi.ToString () = {0}", p i .ToString()); Console .WriteLine ("p2 .ToString () = {0}", p 2 .ToString()); Console.WriteLine("pi =p2?: {0}", p i .Equals(p2)); Console.WriteLine("Same hash codes?: {0}", pi .GetHashCode () == p 2 .GetHashCode ()) ; Console.ReadLine(); } Ниже показан вывод: ***** Fun with System.Ob]ect ***** 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. Главные конструкции программирования на C# p i .ToString () = [First Name: Homer; Last Name: Simpson; Age: 50] p 2 .T oStnng () = [First Name: Homer; Last Name: Simpson; Age: 45] pi = p2?: False Same hash codes?: False Статические члены S y s t e m .O b je c t В дополнение к только что рассмотренным членам уровня экземпляра, в System. O b je c t также определены два очень полезных статических члена, которые проверяют эквивалентность на основе значений или на основе ссылок. Рассмотрим следующий код: 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.WnteLine ("P3 and P4 are pointing to same object: {0}", object.ReferenceEquals (p 3, p4)); } Здесь можно просто передать два объекта (любого типа) и позволить классу System. O b je c t автоматически определить детали. Эти методы могут быть очень полезны при переопределении эквивалентности для специального типа, когда нужно сохранить воз­ можность быстрого выяснения того, указывают ли две ссылочных переменных на одно и то же местоположение в памяти (через статический метод R e fe re n c e E q u a ls ()). Исходный код. Проект O b je c tO v e r n d e s доступен в подкаталоге C hapter 6. Резюме В этой главе рассматривалась роль и подробности наследования и полиморфизма. Были представлены многочисленные новые ключевые слова и лексемы для поддержки каждой этой техники. Например, вспомните, что двоеточие применяется для установ­ ки родительского класса для заданного типа. Родительские типы могут определять лю­ бое количество виртуальных и/или абстрактных членов для установки полиморфного интерфейса. Производные типы переопределяют эти члены, используя ключевое слово o v e r r id e . В дополнение к построению многочисленных иерархий классов, в главе также рас­ сматривалось явное приведение между базовым и производным типом. Кроме того, было дано описание главного класса среди всех родительских типов библиотеки базо­ вых классов .NET — S ystem .O bject. ГЛАВА 7 Структурированная обработка исключений настоящей главе речь пойдет о способах обработки аномалий, возникающих во время выполнения, в коде на C# с применением методики так называемой струк­ турированной обработки исключений (structured exception handling — SEH). Здесь бу­ дут описаны не только ключевые слова в С#, которые предназначены для этого (t r y , catch , throw, f i n a l l y ) , но и отличия исключений уровня приложения и системы, а также роль базового класса S ystem .E xcep tion . Вдобавок будет показано, как создавать специальные исключения, и рассмотрены инструменты, доступные в Visual Studio 2010 для выполнения отладки. В Ода ошибкам и исключениям Чтобы не нашептывало наше (порой раздутое) эго, ни один программист не идеа­ лен. Написание кода программного обеспечения является сложным делом, и из-за этой сложности довольно часто даже самые лучшие программы поставляются с различными, так сказать, проблемами. В одних случаях причиной этих проблем служит “плохо напи­ санный” код (например, в нем происходит выход за пределы массива), а в других — ввод пользователями неправильных данных, которые не были предусмотрены в коде прило­ жения (например, приводящий к присваиванию полю для ввода телефонного номера значения вроде “Chucky”). Что бы ни служило причиной проблем, в конечном итоге приложение начинает ра­ ботать не так, как ожидается. Прежде чем переходить к рассмотрению структуриро­ ванной обработки исключений, давайте сначала ознакомимся с тремя наиболее часто применяемыми для описания аномалий терминами. • Программные ошибки (bugs). Так обычно называются ошибки, которые допуска­ ет программист. Например, предположим, что приложение создается с помощью неуправляемого языка C++. Если динамически выделяемая память не освобожда­ ется, что чревато утечкой памяти, появляется программная ошибка. • Пользовательские ошибки (user errors). В отличие от программных ошибок, поль­ зовательские ошибки обычно возникают из-за тех, кто запускает приложение, а не тех, кто его создает. Например, ввод конечным пользователем в текстовом поле неправильно оформленной строки может привести к генерации ошибки подобно­ го рода, если в коде не была предусмотрена возможность обработки некорректно­ го ввода. 266 Часть II. Главные конструкции программирования на C# • Исключения (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(); i f (retVal == E_FILENOTFOUND) p n n t f ("Cannot find file..."); / / H e удается найти файл... } Глава 7. Структурированная обработка исключений 267 Такой подход далеко не идеален из-за того факта, что константа E FILENOTFOUND представляет собой не более чем просто числовое значение, но уж точно не агента, спо­ собного помочь в решении проблемы. В идеале хотелось бы, чтобы название ошибки, сообщение с ее описанием и другой полезной информацией подавалось в одном удобном пакете (что как раз и происходит в случае применения структурированной обработки исключений). Помимо приемов, изобретаемых самими разработчиками, в A P I-интерфейсе Windows определены сотни кодов ошибок с помощью # define и НRESULT, а также множество вариаций простых булевских значений (bool, BOOL, VARIANT BOOL и т.д.). Более того, многие разработчики COM-приложений на языке C++ (а также VB 6) явно или неявно применяют небольшой набор стандартных COM-интерфейсов (наподобие ISupportErrorlnf о, IErrorlnfo или ICreateError Inf о) для возврата СОМ-клиенту понятной информации об ошибках. Очевидная проблема со всеми этими более старыми методиками — отсутствие сим­ метрии. Каждая из них более-менее вписывается в рамки какой-то одной технологии, одного языка и, пожалуй, даже одного проекта. Чтобы положить конец всему этому бе­ зумству, в .NET была предложена стандартная методика для генерации и выявления ошибок в исполняющей среде, называемая структурированной обработкой исключений (SEH). Прелесть этой методики состоит в том, что она позволяет разработчикам использо­ вать в области обработки ошибок унифицированный подход, который является общим для всех языков, ориентированных на платформу .NET. Благодаря этому, программист на C# может обрабатывать ошибки почти таким же с синтаксической точки зрения образом, как и программист на VB и программист на C++, использующий C++/CLI. Дополнительное преимущество состоит в том, что синтаксис, который требуется при­ менять для генерации и перехвата исключений за пределами сборок и машин, тоже выглядит идентично. Например, при написании на C# службы Windows Communication Fbundation (WCF) генерировать исключение SOAP для удаленного вызывающего кода можно с использованием тех же ключевых слов, которые применяются для генерации исключения внутри методов в одном и том же приложении. Еще одно преимущество механизма исключений .NET состоит в том, что в отли­ чие от запутанных числовых значений, просто обозначающих текущую проблему, они представляют собой объекты, в которых содержится читабельное описание проблемы, а также детальный снимок стека вызовов на момент, когда изначально возникло исклю­ чение. Более того, конечному пользователю можно предоставлять справочную ссылку, которая указывает на определенный URL-адрес с описанием деталей ошибки, а также специальные данные, определенные программистом. Составляющие процесса обработки исключений в .NET Программирование со структурированной обработкой исключений подразумевает использование четырех следующих связанных между собой сущностей: • тип класса, который представляет детали исключения; • член, способный генерировать (throw) в вызывающем коде экземпляр класса ис­ ключения при соответствующих обстоятельствах; • блок кода на вызывающей стороне, ответственный за обращение к члену, в кото­ ром может произойти исключение; • блок кода на вызывающей стороне, который будет обрабатывать (или перехваты­ вать (catch)) исключение в случае его возникновения. 268 Часть II. Главные конструкции программирования на C# При генерации и обработке исключений в C# используются четыре ключевых слова (t r y , ca tch , throw и f i n a l l y ) . Любой объект, отражающий обрабатываемую пробле­ му, должен обязательно представлять собой класс, унаследованный от базового класса S ystem . E x c e p tio n (или от какого-то его потомка). По этой причине давайте сначала рассмотрим роль этого базового класса в обработке исключений. Базовый класс S y s t e m . E x c e p t i o n Все определяемые на уровне пользователя и системы исключения в конечном ито­ ге всегда наследуются от базового класса System . E xception , который, в свою очередь, наследуется от класса System. O b ject. Ниже показано, как в целом выглядит этот класс (обратите внимание, что некоторые его члены являются виртуальными и, следователь­ но, могут переопределяться в производных классах): 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; } Нетрудно заметить, что многие из содержащихся в S ystem .E x cep tio n свойств яв­ ляются по своей природе доступными только для чтения. Это объясняется тем, что для каждого из них значения, используемые по умолчанию, обычно поставляются в произ­ водных классах. Например, в производном классе IndexO utO fRangeException постав­ ляется сообщение по умолчанию “Index was outside the bounds of the array” (“Индекс вышел за границы массива”). На заметку! В классе E x c e p tio n реализованы два интерфейса .NET. Хотя интерфейсы подробно рассматриваются в главе 9, сейчас главное понять, что интерфейс _ E x c e p tio n позволяет сделать так, чтобы исключение .NET обрабатывалось неуправляемым кодом (таким как прило­ жение СОМ), а интерфейс I S e r i a l i z a b l e — чтобы объект исключения сохранялся за пре­ делами границ (например, границ машины). В табл. 7.1 приведено краткое описание некоторых наиболее важных.свойств класса S ystem . E x cep tio n . Глава 7. Структурированная обработка исключений 269 Таблица 7.1. Ключевые свойства System .Exception Свойство Описание Data Это свойство, доступное только для чтения, позволяет извлекать кол­ лекцию пар "ключ/значение” (представленную объектом, реализующим интерфейс ID ic tio n a r y ), которая предоставляет дополнительную опре­ деляемую программистом информацию об исключении. По умолчанию эта коллекция является пустой HelpLink Это свойство позволяет получать или устанавливать URL-адрес, по которому доступен справочный файл или веб-сайт с детальным описанием ошибки InnerE xception Это свойство, доступное только для чтения, может применяться для полу­ чения информации о предыдущем исключении или исключениях, которые послужили причиной возникновения текущего исключения. Запись преды­ дущих исключений осуществляется путем их передачи конструктору само­ го последнего исключения Message Это свойство, доступное только для чтения, возвращает текстовое опи­ сание соответствующей ошибки. Само сообщение об ошибке задается в передаваемом конструктору параметре Source Это свойство позволяет получать или устанавливать имя сборки или объ­ екта, который привел к выдаче исключения S tackTrace Это свойство, доступное только для чтения, содержит строку с описанием последовательности вызовов, которая привела к возникновению исклю­ чения. Как нетрудно догадаться, это свойство очень полезно во время отладки или для сохранения информации об ошибке во внешнем журнале ошибок T a r g e t S it e Это свойство, доступное только для чтения, возвращает объект MethodBase с описанием многочисленных деталей метода, который привел к выдаче исключения (вызов вместе с ним T o S tn n g () позволяет идентифицировать этот метод по имени) Простейший пример Для иллюстрации пользы от структурированной обработки исключений необходи­ мо создать класс, который будет выдавать исключение при надлежащих (или, мож­ но сказать, и с к л ю ч и т е л ь н ы х ) обстоятельствах. Создадим новый проект типа C o n s o le A p p lica tio n (Консольное приложение) на C# по имени S im p leE xcep tion и определим в нем два класса (Саг (автомобиль) и Radio (радиоприемник)), связав их между собой от­ ношением принадлежности (“has-a”). В классе Radio определим единственный метод, отвечающий за включение и выключение радиоприемника: class Radio { public void TurnOn(bool on) { if(on) Console .WnteLine ("Jamming...") ; else Console .WnteLine ("Quiet time..."); // работает // отключен } В классе Car (показанном ниже) помимо использования класса Radio через принад­ лежность/ делегирование, сделаем так, чтобы в случае превышения объектом Саг пре­ 270 Часть II. Главные конструкции программирования на C# допределенной максимальной скорости (отражаемой с помощью константы экземпляра Max Speed) двигатель выходил из строя, приводя его в нерабочее состояние (отражаемое приватной переменной экземпляра b o o l по имени ca r Is Dead). Кроме того, включим в Саг свойства для представления текущей скорости и указанного пользователем “друже­ ственного названия” автомобиля и различные конструкторы для установки состояния нового объекта Саг. Ниже приведено полное определение Саг вместе с поясняющими комментариями. 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 C a r () { } 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 ( ) , в котором объект Саг будет превышать заданную максимальную скорость (установленную равной 100 в классе Саг), как показано ниже: Глава 7. Структурированная обработка исключений 271 static void Main(string[] args) { Console.WriteLine ("***** Simple Exception Example *****"); Console.WriteLine("=> Creating a car and stepping on i t 1"); Car myCar = new Car ("Zippy" , 20); myCar.CrankTunes(true); for (int i = 0; i < 10; i++) myCar.Accelerate(10) ; Console.ReadLine(); } Вьшод будет выглядеть следующим образом: ***** Simple Exception Example ***** => Creating a car and stepping on it! Jamming... => CurrentSpeed = 30 => CurrentSpeed = 40 => CurrentSpeed = 50 => CurrentSpeed = 60 => CurrentSpeed = 7 0 => CurrentSpeed = 80 => CurrentSpeed = 90 => CurrentSpeed = 100 Zippy has overheated! Zippy is out of order. .. Генерация общего исключения Имея функционирующий класс Саг, давайте рассмотрим простейший способ генера­ ции исключения. Текущая реализация A c c e le r a t e () предусматривает просто отобра­ жение сообщения об ошибке, когда предпринимается попытка разогнать автомобиль (объект Саг) до скорости, превышающей максимальный предел. Для изменения этого метода так, чтобы при попытке разогнать автомобиль до ско­ рости, превышающий установленный в классе Саг предел, генерировалось исключение, потребуется создать и сконфигурировать новый экземпляр класса S y stem .E x cep tio n и установить значение доступного только для чтения свойства Message через конструктор класса. Чтобы объект ошибки отправлялся обратно вызывающей стороне, в C# использует­ ся ключевое слово throw. Ниже показан код модифицированного метода A c c e le r a t e (). // На этот раз, в случае превышения пользователем указанного // в 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. Главные конструкции программирования на C# Прежде чем переходить к рассмотрению перехвата данного исключения в вызы­ вающем коде, необходимо отметить несколько интересных моментов. При генерации исключения то, как будет выглядеть ошибка и когда она должна выдаваться, решает программист. В рассматриваемом примере предполагается, что при попытке увеличить скорость автомобиля (объекта Саг), который уже вышел из строя, должен генерировать­ ся объект S y stem .E x cep tio n для уведомления о том, что метод A c c e le r a t e () не мо­ жет быть продолжен (это предположение может оказаться как подходящим, так и нет, в зависимости от создаваемого приложения). В качестве альтернативы метод A c c e l e r a t e () можно было бы реализовать и так, чтобы он производил автоматическое восстановление, не выдавая перед этим никакого исключения. По большому счету, исключения должны генерироваться только в случае возникновения более критичных условий (например, отсутствии нужного файла, невоз­ можности подключиться к базе данных и т.п.). Принятие решения о том, что должно служить причиной генерации исключения, требует серьезного продумывания и поиска веских оснований на стадии проектирования. Для преследуемых сейчас целей давайте считать, что попытка увеличить скорость неисправного автомобиля является вполне оправданной причиной для выдачи исключения. Перехват исключений Поскольку теперь метод A c c e l e r a t e () способен генерировать исключение, вызы­ вающий код должен быть готов обработать его, если оно вдруг возникнет. При вызо­ ве метода, который может генерировать исключение, должен использоваться блок try / c a tc h . После перехвата объекта исключения можно вызывать различные его чле­ ны и извлекать детальную информацию о проблеме. Что делать с этими деталями дальше по большей части нужно решать самостоя­ тельно. Может возникнуть желание занести их в специальный файл отчета, записать в журнал событий Windows, отправить по электронной почте системному администрато­ ру или отобразить конечному пользователю. Давайте для простоты выведем их в окне консоли. // Обработка сгенерированного исключения. static void Main(string[] args) { Console .WnteLine ("**** * 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.Accelerate(10); } 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.ReadLine(); } Глава 7. Структурированная обработка исключений 273 По сути, блок t r y представляет собой раздел операторов, которые в ходе выполнения могут выдавать исключение. Если обнаруживается исключение, управление переходит к соответствующему блоку catch . С другой стороны, в случае, если код внутри блока t r y не приводит к генерации исключения, блок ca tch полностью пропускается, и все проходит “гладко”. Ниже показано, как будет выглядеть вывод в результате тестового выполнения данной программы. *ж*** Simple Exception Example ***** => Creating a car and stepping on it! Jamming... => CurrentSpeed = 30 => CurrentSpeed = 4 0 => 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), которые могут помочь еще больше уточнить природу проблемы. Чтобы усовер­ шенствовать текущий пример, давайте рассмотрим возможности каждого из этих чле­ нов более подробно. Свойство T a r g e t S i t e Свойство System.Exception.TargetSite позволяет получать различные детали о методе, в котором было сгенерировано данное исключение. Как было показано в пре­ дыдущем методе Main () , вывод значения свойства TargetSite приводит к отобра­ жению возвращаемого значения, имени и параметров выдавшего исключение мето­ да. Вместо простой строки свойство TargetSite возвращает строго типизированный объект System. Ref lection .MethodBase. Объект такого типа может применяться для сбора многочисленных деталей, связанных с проблемным методом, а также классом, в котором он содержится. Для примера изменим предыдущую логику в блоке catch сле­ дующим образом: static void Main(string [] args) 274 Часть II. Главные конструкции программирования на C# // Свойство TargetSite на самом деле // возвращает объект MethodBase. catch (Exception е) { Console .WriteLme ("\n* ** Error1 ***"); Console . W n t e L m e ("Member name: {0}", e .TargetSite) ; // имя члена Console.WriteLme ("Class defining member: {0}", e .TargetSite .DeclanngType) ; // класс, определяющий член 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. Саг), а с помощью свойства MemberType объекта MethodBase выясняется тип члена (свойство или метод), в котором возникло исключение. Ниже показано, как теперь будет выглядеть вывод в результате выполнения логики в блоке catch. *** Error! *** Member name: Void Accelerate(Int32) Class defining member: SimpleException.Car Member type: Method Message: Zippy has overheated! Source: SimpleException Свойство S t a c k T r a c e Свойство System. Exception.StackTrace позволяет определить последовательность вызовов, которая привела к возникновению исключения. Значение этого свойства ни­ когда самостоятельно не устанавливается — это делается автоматически во время соз­ дания исключения. Чтобы проиллюстрировать это, модифицируем логику в блоке catch следующим образом: catch(Exception е) 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 c :\MyApps\SimpleExceptmn\Program. cs :line 21 Строка, возвращаемая из StackTrace, отражает последовательность вызовов, кото­ рая привела к выдаче данного исключения. Обратите внимание, что самый нижний но­ мер в этой строке указывает на место возникновения первого вызова в последователь­ ности, а самый верхний — на место, где точно находится породивший проблему член. Очевидно, что такая информация очень полезна при выполнении отладки или просмот­ ре журналов в конкретном приложении, поскольку позволяет прослеживать весь путь, приведший к возникновению ошибки. Глава 7. Структурированная обработка исключений 275 Свойство H e l p L i n k Хотя свойства TargetSite и StackTrace позволяют программистам понять, почему возникло то или иное исключение, пользователям выдаваемая ими информация мало что дает. Как уже показывалось ранее, для получения удобной для человеческого вос­ приятия и потому пригодной для отображения конечному пользователю информации может применяться свойство System.Exception .Message. Кроме него, также может использоваться свойство HelpLink, которое позволяет направить пользователя на кон­ кретный URL-адрес или стандартный справочный файл Windows, где содержатся более детальные сведения о возникшей проблеме. По умолчанию значением свойства HelpLink является пустая строка. Присваивание этому свойству какого-то более интересного значения должно делаться перед генераци­ ей исключения типа System.Exception. Чтобы посмотреть, как это делается, изменим метод Car .Accelerate () следующим образом: public void Accelerate (int delta) { if (carlsDead) Console .WnteLine ("{ 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 .WnteLine ("= > CurrentSpeed = {0}", CurrentSpeed); Теперь можно модифицировать логику в блоке catch так, чтобы информация из дан­ ного свойства HelpLink выводилась в окне консоли: catch(Exception е) // Ссылка для справки Console.WriteLine("Help Link: {0}", e.HelpLink); } Свойство D a t a Доступное в классе System.Exception свойство Data позволяет заполнять объект исключения соответствующей вспомогательной информацией (например, датой и вре­ менем возникновения исключения). Оно возвращает объект, реализующий интерфейс по имени IDictionary, который определен в пространстве имен System.Collections. В главе 9 более подробно рассматривается программирование с использованием интер­ 276 Часть II. Главные конструкции программирования на C# фейсов, а также пространство имен System.Collections. На данный момент важно понять лишь то, что коллекции типа словарей позволяют создавать наборы значений, извлекаемых по ключу. Модифицируем метод Car .Accelerate (), как показано ниже: public void Accelerate(int delta) { if (carlsDead) Console .WnteLine ("{ 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"; // Вставка специальных дополнительных данных, // имеющих отношение к ошибке. e x .Data.Add("TimeStamp", string.Format("The car exploded at {0}", DateTime.Now)); // дата и время e x .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 е) //По умолчанию поле данных является пустым, поэтому // выполняется проверка на предмет равенства n u ll. Console .WnteLine ("\n-> Custom Data:"); if (e.Data != null) { foreach (DictionaryEntry de in e.Data) Console .WnteLine ("-> {0}: {1}", de.Key, de.Value); } } Ниже показано, как теперь будет выглядеть вывод программы: Глава 7. Структурированная обработка исключений 277 ***** Slmple 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. Главные конструкции программирования на C# public class SystemException : Exception { // Различные конструкторы. } Из-за того, что в System.SystemException никакой дополнительной функциональ­ ности помимо набора специальных конструкторов больше не предлагается, может воз­ никнуть вопрос о том, а зачем он тогда вообще существует Попросту говоря, когда тип исключения наследуется от System. SystemException, это дает возможность понять, что сущностью, которая сгенерировала исключение, является исполняющая среда .NET, а не кодовая база функционирующего приложения. В этом можно довольно легко удо­ стовериться с помощью ключевого слова is: // Действительно1 Исключение NullReferenceException // является исключением типа SystemException. NullReferenceException nullRefEx = new NullReferenceException(); Console.WnteLine ("NullReferenceException is-а SystemException? : {0}", nullRefEx is SystemException); Исключения уровня приложения (System.AppliestionException) Поскольку все исключения .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). Объясняется это тем, что исключения часто пере­ даются за пределы сборки, следовательно, они должны быть доступны вызывающему коду. Чтобы увидеть все на конкретном примере, давайте создадим новый проект типа C o nso le A p p lic a tio n (Консольное приложение) по имени CustomException и скопируем в него приведенные ранее файлы Car.cs и Radio, cs, выбрав в меню P ro je c t (Проект) пункт A dd Existing Item (Добавить существующий элемент) и изменив для ясности назва­ ние пространства имен, в котором определяются типы Саг и Radio, с SimpleException на CustomException. После этого добавим в него следующее определение класса: // Это специальное исключение описывает детали условия // выхода автомобиля из строя. public class CarlsDeadException : ApplicationException U Как и в любой другой класс, в этот класс можно включать любое количество спе­ циальных членов, которые могли бы вызываться в блоке 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. Главные конструкции программирования на C# // Выдача специального исключения CarlsDeadException. public void Accelerate(int delta) CarlsDeadException ex = new CarlsDeadException (string.Format("{0} has overheated!", "You have a lead foot", DateTime.Now); ex.HelpLink = "http://www.CarsRUs.com"; throw ex; PetName), } Для перехвата такого поступающего исключения теперь можно модифицировать блок catch, чтобы в нем перехватывалось именно исключение типа CarlsDeadException (хотя из-за того, что System.CarlsDeadException является потомком System. Exception, перехват в нем исключения типа System.Exception также допустим). static void Main(string [] args) { Console .WnteLine ("**** * Fun with Custom Exceptions *****\n"); Car myCar = new Car ("Rusty" , 90); try { // Отслеживание исключения. myCar.Accelerate(50); } catch (CarlsDeadException e) { Console.WriteLine(e.Message); Console .WnteLine (e .ErrorTimeStamp) ; Console.WriteLine(e.CauseOfError); } Console.ReadLine (); } Теперь, когда известно, как в общем выглядит процесс создания специального ис­ ключения, может возникнуть вопрос о том, когда к нему следует прибегать. Обычно не­ обходимость в создании специальных исключений возникает, только если ошибка тесно связана с генерирующим ее классом (например, специальный файловый класс может выдавать набор специальных ошибок, связанных с файлами, класс Саг — ошибки, свя­ занные с автомобилем, объект доступа к данным — ошибки, связанные с отдельной таблицей в базе данных, и т.д.). Их создание позволяет обеспечить вызывающий код возможностью обрабатывать многочисленные исключения за счет описания каждой ошибки по отдельности. Создание специальных исключений, способ второй В предыдущем примере в специальном типе CarlsDeadException переопределя­ лось свойство System. Exception .Message для настройки специального сообщения об ошибке и поставлялось два специальных свойства для предоставления дополнитель­ ных фрагментов данных. В реальности, однако, переопределять виртуальное свойство Message вовсе не требуется, поскольку можно также просто передавать поступающее сообщение конструктору родителя, как показано ниже: public class CarlsDeadException : ApplicationException { public DateTime ErrorTimeStamp { get; sec; } 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.SerializationInfо info, 282 Часть II. Главные конструкции программирования на C# System.Runtime.Serialization.StreamingContext context) : base ( info, context ) { } // Далее могут идти любые дополнительные специальные // свойства, конструкторы и члены данных. } Поскольку специальные исключения, создаваемые в соответствии с наилучшими практическими рекомендациям .NET, отличаются только именами, не может не радо­ вать тот факт, что в Visual Studio 2010 поставляется специальный шаблон фрагмента кода под названием Exception (рис. 7.1), который позволяет автоматически генериро­ вать новый класс исключения, отвечающий требованиям наилучших практических рекомендаций .NET. (Как рассказывалось в главе 2, для активизации фрагмента кода необходимо ввести его имя, которым в данном случае является exception, и два раза нажать клавишу <ТаЬ>.) Рис. 7.1. Шаблон фрагмента кода под названием exception Исходный код. Проект CustomException находится в подкаталоге Chapter 7. Обработка многочисленных исключений В простейшем варианте блок try сопровождается только одним блоком catch. В реальности, однако, часто требуется, чтобы операторы в блоке try могли приво­ дить к срабатыванию нескольких возможных исключений. Чтобы рассмотреть при­ мер, создадим новый проект типа C o n s o le A p p lic a tio n (Консольное приложение) на C# по имени ProcessMultipleExpceptions. Добавим в него файлы Car.cs, Radio.cs и CarlsDeadException .cs из предыдущего примера CustomException (выбрав в меню P ro je c t пункт A d d E xistin g Item ) и соответствующим образом модифицируем названия пространств имен. Далее изменим в классе Саг метод Accelerate () так, чтобы он выдавал и такое го­ товое исключение из библиотеки базовых классов, как ArgumentOutOfRangeException, в случае передачи недействительного параметра (которым будет считаться любое зна­ чение меньше нуля). Обратите внимание, что конструктор этого класса исключения принимает имя проблемного аргумента в качестве первого параметра string, следом за которым идет сообщение с описанием ошибки: // Выполнение проверки аргумента на предмет действительности перед продолжением. public void Accelerate(int delta) { if(delta < 0) // Скорость должна быть больше нуля! throw new ArgumentOutOfRangeException("delta", "Speed must be greater than zero1"); Глава 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 .WnteLine (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. Главные конструкции программирования на C# типа 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 .WnteLine (e .Message) ; } catch (ArgumentOutOfRangeException e) { Console.WriteLine(e.Message); } // В этом блоке будут перехватываться любые другие исключения // помимо CarlsDeadException и ArgumentOutOfRangeException. catch (Exception е) { Console.WriteLine(e.Message); } Console.ReadLine(); На заметку! Везде, где только возможно, следует отдавать предпочтение перехвату конкретных классов исключений, а не общих исключений типа System.Exception. Хотя поначалу мо­ жет казаться, что это упрощает жизнь (поскольку охватывает все вещи, с которыми не хочется возиться), со временем из-за того, что обработка более серьезной ошибки не была напрямую предусмотрена в коде, могут возникать очень странные сбои во время выполнения. Не сле­ дует забывать о том, что последний блок catch, который отвечает за обработку исключений System.Exception, имеет тенденцию оказываться чрезвычайно общим. Общие операторы c a t c h В C# поддерживается так называемый “общий” (универсальный) блок 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 (); } Очевидно, что это не самый информативный способ обработки исключений, по­ скольку нет никакой возможности для получения более детальных сведений о возник­ шей ошибке (таких как имя метода, стек вызовов или специальное сообщение). Тем не менее, в C# все-таки можно применять конструкцию подобного рода, так как она мо­ жет оказаться полезной, когда требуется обработать все ошибки в чрезвычайно общей манере. Передача исключений При перехвате исключения внутри блока try допускается передавать (rethrow) ис­ ключение вверх по стеку вызовов предшествующему вызывающему коду. Для этого дос­ таточно воспользоваться в блоке catch ключевым словом throw. Это позволит передать исключение вверх по цепочке логики вызовов, что может оказаться полезным, если блок catch способен обрабатывать текущую ошибку только частично. // Передача ответственности. static void Main(string[] args) { try { // Логика, касающаяся увеличения скорости // автомобиля... } catch(CarlsDeadException е) { // Выполнение любой частичной обработки данной ошибки / / и передача дальнейшей ответственности. throw; } } Следует иметь в виду, что в приведенном примере кода конечным получателем ис­ ключения CarlsDeadException будет CLR-среда из-за его передачи в методе Main ( ) . Следовательно, конечному пользователю будет отображаться системное диалоговое окно с информацией об ошибке. Обычно передача частичного обработанного исклю­ чения вызывающему коду осуществляется только в случае, если он способен обрабаты­ вать поступающее исключение более элегантно. Обратите внимание на неявную передачу объекта CarlsDeadException и на приме­ нение ключевого слова throw без аргументов. Никакого нового объекта исключения не создается, а производится просто передача самого исходного объекта исключения (со всей его исходной информацией). Это позволяет сохранить контекст первоначального целевого объекта. 286 Часть II. Главные конструкции программирования на C# Внутренние исключения Нетрудно догадаться, что вполне возможно генерировать исключение во время об­ работки какого-то другого исключения. Например, предположим, что производится об­ работка исключения CarlsDeadException в определенном блоке catch, и в ходе этого процесса обработки предпринимается попытка записать данные трассировки стека в файл carErrors .txt на диске С: (для получения доступа к таким ориентированным на работу с вводом-выводом типам в директиве using должно быть указано пространство имен System. 10). catch(CarlsDeadException е) { // Попытка открыть файл c a rE rro rs.tx t на диске С: FileStream fs = File.Open(@"C:\carErrors.txt", FileMode.Open); } Теперь, если указанный файл на диске С: отсутствует, вызов File.Open () приведет к генерации исключения FileNotFoundException. Позже в книге будет более подробно рассказываться о пространстве имен System. 10 и о том, как программно определить, существует ли файл на жестком диске, перед тем как пытаться открыть его (это позво­ лит вообще избежать генерации исключения). Тем не менее, чтобы не отходить от темы исключений, давайте считать, что такое исключение все-таки генерируется. Если во время обработки исключения возникает какое-то другое исключение, со­ гласно наилучшим практическим рекомендациям, необходимо сохранить новый объект исключения как “внутреннее исключение” в новом объекте того же типа, что у исход­ ного исключения. Причина, по которой необходимо выделять новый объект для обра­ батываемого исключения, связана с тем, что документировать внутреннее исключение допускается только через параметр конструктора. Рассмотрим следующий код: catch (CarlsDeadException е) { try { FileStream fs = File.Open(@"C:\carErrors.txt", FileMode.Open); catch (Exception e2) { // Генерация исключения, записывающего новое // исключение, а также сообщение первого исключения. throw new CarlsDeadException(е.Message, е2) ; } } В данном случае важно обратить внимание, что конструктору CarlsDeadException в качестве второго параметра передается объект FileNotFoundException. После на­ стройки этоп объект передается вверх по стеку вызовов следующему вызывающему коду, которым будет метод Main (). Поскольку после Main () никакого “следующего вызывающего кода”, который мог бы перехватить исключение, не существует, пользователю будет отображаться системное диалоговое окно с сообщением об ошибке. Во многом подобно передаче исключения, запись внутренних исключений обычно осуществляется только тогда, когда вызываю­ щий код способен обрабатывать данное исключение более элегантно. В этом случае в вызывающем коде внутри catch может использоваться свойство InnerException для извлечения деталей объекта внутреннего исключения. Глава 7. Структурированная обработка исключений 287 Блок f i n a l l y В контексте try/catch можно также определять необязательный блок finally. Это гарантирует, что некоторый набор операторов будет выполняться всегда, независимо от того, возникло исключение (любого типа) или нет. Для целей иллюстрации предпо­ ложим, что перед выходом из метода Main () должно всегда производиться выключение радиоприемника в автомобиле, невзирая ни на какие обрабатываемые исключения. static void Main(string[] args) { Console.WnteLine (''***** Handling Multiple Exceptions *****\n"); Car myCar = new Car ("Rusty", 90); try { // Логика, связанная с увеличением скорости автомобиля. } catch(CarlsDeadException е) { // Обработка исключения CarlsDeadException. } catch(ArgumentOutOfRangeException е) { // Обработка исключения 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. Главные конструкции программирования на C# Object Browse» Start Page Radio.cs JfrProcessNfolt»pieExcept»ons.Program CadsDeadExceptionxs Carxs **Mam{string[] args) fin a lly | { / / Th is w i l l always o ccu r. Exception or not. m y C a r.C ra n k T u n es(fa lse); } C o n so le . R e a d L ln e (); > } strirg Console ReadL ref) Reaos t"e •'ext line o' craracteT ^orr the standard nour st-ea^T. L> I' 1 Except ors SystemlQIQExceptor System. OjtOfMe'ro'yExcept on 1 System. Argjnr ertOutOTCangeException Рис. 7.2. Просмотр списка исключений, которые может генерировать определенный метод Тем, кто перешел на .NET с Java, важно понять, что в .NET члены типов не прототипируются с набором исключений, которые они могут выдавать (другими словами, контролируемые исключения в C# не поддерживаются). Хорошо это или плохо, но обра­ батывать каждое исключение, генерируемое определенным членом, не требуется. К чему приводят необрабатываемые исключения К этому моменту наверняка возник вопрос о том, что произойдет, если выданное ис­ ключение обработано не будет? Для примера предположим, что в логике внутри Main () объект Саг разгоняется до скорости, превышающей допустимый максимальный предел, и что логика try / c a tc h отсутствует: static void Main(string[] args) { Console .WnteLine (''***** Handling Multiple Exceptions Car myCar = new Car ("Rusty", 90); myCar.Accelerate(500); Console.ReadLine(); *****\n »); } Игнорирование исключения в таком случае станет серьезным препятствием для ко­ нечного пользователя приложения, поскольку ему будет отображаться диалоговое окно с сообщением о необработанном исключении, показанное на рис. 7.3. Д problem caused the program to stop working correctly. Windows will close the program and notify you if a solution is available. [ Debug j [ Close program j Рис. 7.3. Последствия не обрабатывания исключений Глава 7. Структурированная обработка исключений 289 Отладка необработанных исключений с помощью Visual Studio Среда Visual-Studio 2010 предлагает набор инструментов, которые помогают отла­ живать необработанные специальные исключения. Для примера предположим, что ско­ рость объекта Саг была увеличена до предела, превышающего допустимый максимум. После запуска сеанса отладки в Visual Studio 2010 (выбором в меню Debug (Отладка) пункта Start (Начать)), выполнение программы на месте выдачи не перехватываемого исключения будет автоматически прерываться. Кроме того, откроется окно (рис. 7.4), в котором будет отображаться значение свойства Message. a/iProces»Mulbp*eExcept»cn«.C«r •I •A cceieiaiei*n l ae*ta) t l We n e e d t o c a l l t h e H e l p L i r * p r o p e r t y , t h u s we n e e d / / t o c r e a t e a l o c a l v a r i a b l e b e f o r e t h r o w in g t h e E x c e p t io n o b j e c t . C a r Is O e a d lx re p tlo n e x ________ ___ ____ . n e » a r ID c a < J ( x c e p r _ . - r t C «is D « a d E x c e p tk > n w a s u n h a n d te d " Y o u h a v e a l ^ a * f o o t ^ - T 7~~. . . , , ^ , , Rusty has overheated* e x . H e lp li n J t » h t t p : / / * J t h r o w ex } e ls e Т м и и и ^ к ^ У п д tips. f f a a t t h M O W h c a f d f T ! . v ------------- ------------ ----------------------------------------{H uety has overheated*"} | CauseOlError 4 - "You have a lead foot* lie.wrlu, 3 F . ErrorTvneStamp {10/11/2009 9.-16:00 PM} w| i 4 base I Search fo r m ore H elp Online... Actions: View Detail. C opv exception detail to th e clipboard Рис. 7.4. Отладка необработанных специальных исключений в Visual Studio 210 На заметку! Если обработать исключение, сгенерированное каким-то методом из библиотеки ба­ зовых классов .NET не удается, отладчик Visual Studio 2010 прерывает выполнение программы на операторе, который вызвал этот проблемный метод. Щелкнув в этом окне на ссылке View Detail (Показать подробности), можно увидеть дополнительные сведения о состоянии объекта (рис. 7.5). Рис. 7.5. Просмотр детальной информации об исключении Исходный код. Проект ProcessMultipleExceptions доступен в подкаталоге Chapter 7. 290 Часть II. Главные конструкции программирования на C# Несколько слов об исключениях, связанных с поврежденным состоянием (Corrupted State Exceptions) В завершении изучения предлагаемой в C# поддержки для структурированной обра­ ботки исключений следует упомянуть о появлении в .NET 4.0 совершенно нового про­ странства имен под названием System.Runtime.ExceptionServices (которое постав­ ляется в составе сборки mscorlib.dll). Это довольно небольшое пространство имен включает в себя всего два типа класса, которые могут применяться, когда необходимо снабдить различные методы в приложении (вроде Main ( ) ) возможностью перехвата и обработки “исключений, связанных с поврежденным состоянием” (Corrupted State Exceptions — CSE). Как говорилось в главе 1, платформа .NET всегда размещается в среде обслуживаю­ щей операционной системы (такой как Microsoft Windows). Имея опыт программирова­ ния приложений для Windows, можно вспомнить, что низкоуровневый API-интерфейс Windows обладает очень уникальным набором правил по обработке ошибок времени выполнения, которые немного похожи на предлагаемые в .NET приемы структуриро­ ванной обработки исключений. В A PI-интерфейсе Windows можно перехватывать ошибки чрезвычайно низкого уровня, которые как раз и представляют собой ошибки, связанные с “поврежденным состоянием”. Попросту говоря, если ОС Windows передает такую ошибку, это означает, что с программой что-то серьезно не так, причем настолько, что нет никакой надежды на восстановление, и единственно возможной мерой является завершение ее работы. На заметку! При работе с .NET ошибка CSE может появиться только в случае использования в коде C# служб вызова платформы (для непосредственного взаимодействия с API-интерфейсом Windows) или применения поддерживаемого в C# синтаксиса указателей (см. главу 12). До выхода версии .NET 4.0 подобные низкоуровневые ошибки, специфичные для операционной системы, можно было перехватывать только с помощью блока catch, предусматривающего перехват общих исключений System.Exception. Однако с этим подходом была связана проблема: если каким-то образом возникало исключение CSE, которое перехватывалось в таком блоке catch, в .NET не предлагалось (и до сих пор не предлагается) никакого элегантного кода для восстановления. Теперь, с выходом версии .NET 4.0, среда CLR больше не разрешает автоматический перехват исключений CSE в приложениях .NET. В большинстве случаев это именно то поведение, которое нужно. Если же необходимо получать уведомления о таких ошибках уровня ОС (обычно при использовании унаследованного кода, нуждающегося в таких уведомлениях), понадобится применять атрибут [HandledProcessCorruptedState Exceptions]. Хотя роль атрибутов в .NET рассматривается позже в книге (в главе 15), сейчас важ­ но понять, что данный атрибут может применяться к любому методу в приложении, и в результате его применения соответствующий метод получит возможность иметь дело с подобными низкоуровневыми ошибками, специфическими для операционной системы. Чтобы увидеть хотя бы простой пример, давайте создадим следующий метод Main (), не забыв перед этим импортировать в файл кода C# пространство имен System.Runtime. ExceptionServices: [HandledProcessCorruptedStateExceptions] static int Mai n (string [] args) { Глава 7. Структурированная обработка исключений 291 try { // Предполагаем, что в Ма±п() вызывается метод, // который отвечает за выполнение всей программы. RunMyApplication(); } catch (Exception ex) { // Если мы добрались сюда, значит, что-то произошло. // Поэтому просто отображаем сообщение / / и выходим из программы. Console .WnteLine ("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 типа с применением такого под­ держиваемого в C# ключевого слова, как throw. В вызывающем коде любые поступаю­ щие исключения обрабатываются с помощью ключевого слова catch и необязательного блока finally. Для получения собственных специальных исключений, по сути, требуется создать класс, унаследованный от класса System.ApplicationException. Этот новый класс будет представлять исключения, генерируемые приложением, которое выполняется в настоящий момент. Объекты ошибок, унаследованные от System.SystemException, в свою очередь, позволяют представлять критические (и фатальные) ошибки, которые выдает CLR. Напоследок в главе были продемонстрированы различные инструменты внутри Visual Studio 2010, которые можно применять для создания специальных ис­ ключений (в соответствии с наилучшими практическими рекомендациями .NET), а так­ же для их отладки. ГЛАВА О Время жизни объектов этому моменту было предоставлено немало сведений о создании специальных ти­ пов классов в С#. Теперь речь пойдет о том, как CLR-среда управляет размещен­ ными экземплярами классов (те. объектами) с помощью процесса сборки мусора (gar­ bage collection). Программистам на C# никогда не приходится непосредственно удалять управляемый объект из памяти (в языке C# нет даже ключевого слова вроде delete). Вместо этого объекты .NET размещаются в области памяти, которая называется управ­ ляемой кучей (managed heap), откуда они автоматически удаляются сборщиком мусора, когда наступает “определенный момент в будущем”. После рассмотрения ключевых деталей процесса сборки мусора в настоящей гла­ ве будет показано, как программно взаимодействовать со сборщиком мусора с помо­ щью класса System.GC, и как с применением виртуального метода System.Object. Finalize () и интерфейса IDisposable создавать классы, способные освобождать внутренние неуправляемые ресурсы в определенное время. Кроме того, будут описаны некоторые новые функциональные возможности сборщи­ ка мусора, появившиеся в версии .NET 4.0, включая фоновую сборку мусора и отложен­ ную (ленивую) инициализацию с использованием обобщенного класса System.Lazyo. После изучения материалов настоящей главы должно появиться вполне четкое пред­ ставление об управлении объектами .NET в среде CLR. К Классы, объекты и ссылки Перед изучением тем, излагаемых в настоящей главе, сначала необходимо немного больше прояснить различие между классами, объектами и ссылками. Вспомните, что класс представляет собой ни что иное, как схему, которая описывает то, каким образом экземпляр данного типа должен выглядеть и вести себя в памяти. Определяются классы в файлах кода (которым по соглашению назначается расширение * .cs). Для примера создадим новый проект типа C o n so le Application (Консольное приложение) на C# по имени SimpleGC и определим в нем следующий простой класс Саг: // Содержимое файла Car.cs public class Car { public int CurrentSpeed {get; set;} public string PetIJame {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 Ca r ("Zippy", 50); // Применение к переменной с этой ссылкой С#-операции // точки (.)д л я вызова членов данного объекта. Console .WriteLine (refToMyCar .ToStnng () ) ; Console.ReadLine(); На рис. 8.1 схематично показаны отношения между классами, объектами и ссылка­ ми на них. Рис. 8.1. Ссылки на объекты в управляемой куче На заметку! Вспомните из главы 4, что структуры представляют собой типы значения, которые все­ гда размещаются прямо в стеке и никогда не попадают в управляемую кучу .NET. Размещение в куче происходит только при создании экземпляров классов. Базовые сведения о времени жизни объектов При создании приложений на C# можно смело полагать, что исполняющая среда .NET будет сама заботиться об управляемой куче без непосредственного вмешательства со стороны программиста. На самом деле “золотое правило” по управлению памятью в .NETT звучит просто. 294 Часть II. Главные конструкции программирования на C# Правило. Размещайте объект в управляемой куче с использованием ключевого слова new и забы­ вайте об этом. После создания объект будет автоматически удален сборщиком мусора тогда, когда в нем отпадет необходимость. Разумеется, возникает вопрос о том, каким образом сбор­ щик мусора определяет момент, когда в объекте отпадает необходимость? В двух словах на этот вопрос можно ответить так: сборщик мусора удаляет объект из кучи тогда, когда тот становится недостижимым ни в одной части программного кода. Например, доба­ вим в класс Program метод, который размещает в памяти объект Саг: public static void MakeACar() // Если шуСаг является единственной ссылкой на объект // Саг, тогда при возврате результата данным // методом объект Саг *мояеет* быть уничтожен. Car myCar = new Car(); } Обратите внимание, что ссылка на объект Car (myCar) была создана непосредствен­ но внутри метода MakeACar () и не передавалась за пределы определяющей области действия (ни в виде возвращаемого значения, ни в виде параметров ref/out). Поэтому после завершения вызова данного метода ссылка myCar окажется недостижимой, а объ­ ект Саг, соответственно — кандидатом на удаление сборщиком мусора. Следует, однако, понять, что иметь полную уверенность в немедленном удалении этого объекта из памя­ ти сразу же после выполнения метода MakeACar () нельзя. Все, что в данный момент можно предполагать, так это то, что когда в CLR-среде будет в следующий раз произво­ диться сборка мусора, объект myCar может подпасть под процесс уничтожения. Как станет ясно со временем, программирование в среде с автоматической сборкой мусора значительно облегчает разработку приложений. Программистам на C++ хорошо известно, что если они специально не позаботятся об удалении размещаемых в куче объектов, вскоре обязательно начнут возникать “утечки памяти”. На самом деле отсле­ живание проблем, связанных с утечкой памяти, является одним из самых длительных (и утомительных) аспектов программирования в неуправляемых средах. Благодаря на­ значению ответственным за уничтожение объектов сборщика мусора, обязанности по управлению памятью, по сути, сняты с плеч программиста и возложены на CLR-среду. На заметку! Тем, кто ранее применял для разработки приложений технологию СОМ, следует знать, что в .NET объекты не снабжаются никаким внутренним счетчиком ссылок и потому не поддер­ живают использования методов вроде AddRef () или Release (). C IL-код, генерируемый для ключевого слова new При обнаружении ключевого слова new компилятор C# вставляет в реализацию ме­ тода СIL-инструкцию newob j. Если скомпилировать текущий пример кода и заглянуть в полученную сборку с помощью утилиты ildasm.exe, то можно обнаружить внутри метода MakeACar () следующие CIL-операторы: .method private hidebysig static void MakeACar () cil managed { // Code siz e 8 (0x8) // Размер кода 8 (0x8) .maxstack 1 Глава 8. Время жизни объектов 295 .locals init ([0] class SimpleGC.Car) IL_0000: nop IL_0001: newobj instance void SimpleGC.C a r c t o r () IL_0006: stloc.O IL_0007: ret } // end of method Program::MakeACar // конец метода Program::MakeACar Прежде чем ознакомиться с точными правилами, которые определяют момент, ко­ гда объект должен удаляться из управляемой кучи, давайте более подробно рассмотрим роль CIL-инструкции newobj. Дна начала важно понять, что управляемая куча пред­ ставляет собой нечто большее, чем просто случайный фрагмент памяти, к которому CLR получает доступ. Сборщик мусора .NET “убирает” «у ч у довольно тщательно, причем (при необходимости) даже сжимает пустые блоки памяти с целью оптимизации. Чтобы ему было легче это делать, в управляемой куче поддерживается указатель (обычно назы­ ваемый указателем на следующий объект или указателем на новый объект), который показывает, где точно будет размещаться следующий объект. Таким образом, инструкция newobj заставляет CLR-среду выполнить перечислен­ ные ниже ключевые операции. • Вычислить, сколько всего памяти требуется для размещения объекта (в том числе памяти, необходимой для членов данных и базовых классов). • Проверить, действительно ли в управляемой куче хватает места для обслуживания размещаемого объекта. Если хватает, вызвать указанный конструктор и вернуть вызывающему коду ссылку на новый объект в памяти, адрес которого совпадает с последней позицией указателя на следующий объект. • И, наконец, перед возвратом ссылки вызывающему коду переместить указатель на следующий объект, чтобы он указывал на следующую доступную позицию в управляемой куче. Весь описанный процесс схематично изображен на рис. 8.2. Управляемая куча s t a t i c v o id M a i n ( s t r in g [] a rg s ) { Car c l = new C a r ( ) ; -----------------Car c2 = new C a r( ) ; -----------------} C2 Cl _ t - 1 Указатель на следующий объект Рис. 8.2. Детали размещения объектов в управляемой куче Из-за постоянного размещения объектов приложением пространство в управляемой куче может со временем заполниться. В случае если при обработке следующей инструк­ ции newobj среда CLR обнаруживает, что в управляемой куче не хватает пространства для размещения запрашиваемого типа, она приступает к сборке мусора и тем самым пытается освободить хоть сколько-то памяти. Поэтому следующее правило, касающееся сборки мусора, тоже звучит довольно просто. Правило. В случае нехватки в управляемой куче пространства для размещения запрашиваемого объекта начинает выполняться сборка мусора. 296 Часть II. Главные конструкции программирования на C# Однако то, каким именно образом начнет выполняться сборка мусора, зависит от версии .NET, под управлением которой функционирует приложение. Различия будут описаны позже в настоящей главе. Установка объектных ссылок в n u l l Если ранее приходилось создавать COM-объекты в Visual Basic 6.0, то должно быть известно, что по завершении их использования предпочтительнее устанавливать эти ссылки в Nothing. На внутреннем уровне счетчик ссылок на объект СОМ уменьшалось на единицу, и когда он становился равным нулю, объект можно было удалять из памя­ ти. Аналогичным образом программисты на C/C++ часто предпочитают устанавливать для переменных указателей значение null, гарантируя, что они больше не будут ссы­ латься на неуправляемую память. Из-за упомянутых фактов, вполне естественно, может возникнуть вопрос о том, что же происходит в C# после установки объектных ссылок в null. Для примера изменим метод MakeACar () следующим образом: static void MakeACar () { Car myCar = new Car () ; myCar = null; Когда объектные ссылки устанавливаются в n u ll, компилятор C# генерирует CILкод, который заботится о том, чтобы ссылка (в рассматриваемом примере myCar) боль­ ше не ссылалась ни на какой объект. Если теперь снова воспользоваться утилитой ild a s m . ехе и заглянуть с ее помощью в CIL-код измененного метода MakeACar (), мож­ но обнаружить в нем код операции ld n u ll (который заталкивает значение n u ll в вир­ туальный стек выполнения) со следующим за ним кодом операции s t lo c .O (который присваивает переменной ссылку n u ll): .method private hidebysig static void MakeACar () cil managed { // Code size 10 (Oxa) // Размер кода 10 (Оха) .maxstack 1 .locals m i t ([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 Однако обязательно следует понять, что установка ссылки в n u ll никоим образом не вынуждает сборщик мусора немедленно приступить к делу и удалить объект из кучи, а просто позволяет явно разорвать связь между ссылкой и объектом, на который она ранее указывала. Благодаря этому, присваивание ссылкам значения n u ll в C# имеет гораздо меньше последствий, чем в других языках на базе С (или VB 6.0), и совершенно точно не будет причинять никакого вреда. Глава 8. Время жизни объектов 297 Роль корневых элементов приложения Теперь снова вернемся к вопросу о том, каким образом сборщик мусора определяет момент, когда объект уже более не нужен. Чтобы разобраться в стоящих за этим дета­ лях, необходимо знать, что собой представляет корневые элементы приложения (appli­ cation roots). Попросту говоря, корневым элементом (root) называется ячейка в памяти, в которой содержится ссылка на размещающийся в куче объект. Строго говоря, корне­ выми могут называться элементы любой из перечисленных ниже категорий. • Ссылки на глобальные объекты (хотя в C# они не разрешены, CIL-код позволяет размещать глобальные объекты). • Ссылки на любые статические объекты или статические поля. • Ссылки на локальные объекты в пределах кодовой базы приложения. • Ссылки на передаваемые методу параметры объектов. • Ссылки на объекты, ожидающие финализации (об этом подробно рассказываться далее в главе). • Любые регистры центрального процессора, которые ссылаются на объект. Во время процесса сборки мусора исполняющая среда будет исследовать объекты в управляемой куче, чтобы определить, являются ли они по-прежнему достижимыми (т.е. корневыми) для приложения. Для этого среда CLR будет создавать графы объек­ тов, представляющие все достижимые для приложения объекты в куче. Более подробно объектные графы будут описаны при рассмотрении процесса сериализации объектов в главе 20. Пока главное усвоить то, что графы применяются для документирования всех достижимых объектов. Кроме того, следует иметь в виду, что сборщик мусора никогда не будет создавать граф для одного и того же объекта дважды, избегая необходимости выполнения подсчета циклических ссылок, который характерен для программирования в среде СОМ. Чтобы увидеть все это на примере, предположим, что в управляемой куче содержится набор объектов с именами А, В, С, D, Е, F и G. Во время сборки мусора эти объекты (а так­ же любые внутренние объектные ссылки, которые они могут содержать) будут исследо­ ваны на предмет наличия у них активных корневых элементов. После построения графа все недостижимые объекты (которыми в примере пусть будут объекты С и F) помечаются как являющиеся мусором. На рис. 8.3 показано, как примерно выглядит граф объектов в только что описанном сценарии (линии со стрелками следует воспринимать как “зависит от” или “требует”; например, “Е зависит от G и В”, ‘А не зависит ни от чего” и т.д.). После того как объект помечен для уничтожения (в данном случае это объекты С и F, поскольку в графе объектов они во внимание не принимаются), они будут удалены из памяти. Оставшееся пространство в куче будет после этого сжиматься до компакт­ ного состояния, что, в свою очередь, вынудит CLR изменить набор активных корневых элементов приложения (и лежащих в их основе указателей) так, чтобы они ссылались на правильное место в памяти (это делается автоматически и прозрачно). И, наконец, указатель на следующий объект тоже будет подстраиваться так, чтобы указывать на следующий доступный участок памяти. На рис. 8.4 показано, как выглядит конечный результат этих изменений в рассматриваемом сценарии. На заметку! Собственно говоря, сборщик мусора использует две отдельных кучи, одна из которых предназначена специально для хранения очень больших объектов. Доступ к этой куче во время сборки мусора получается реже из-за возможных последствий в плане производительности, в которые может выливаться изменение места размещения больших объектов. Невзирая на этот факт, управляемая куча все равно может спокойно считаться единой областью памяти. 298 Часть II. Главные конструкции программирования на C# Управляемая куча А В С D Е F С У казатель на следую!дий объект Рис. 8.3. Графы объектов создаются для определения объектов, достижимых для корневых элементов приложения Управляемая куча А В D Е G Указатель на следую!щий объект Рис. 8.4. Очищенная и сжатая до компактного состояния куча Поколения объектов При попытке обнаружить недостижимые объекты CLR-среда не проверяет буквально каждый находящийся в куче объект. Очевидно, что на это уходила бы масса времени, особенно в более крупных (реальных) приложениях. Для оптимизации процесса каждый объект в куче относится к определенному “поко­ лению”. Смысл в применении поколений выглядит довольно просто: чем дольше объект находится в куче, тем выше вероятность того, что он там и будет оставаться. Например, класс, определенный в главном окне настольного приложения, будет оставаться в памя­ ти вплоть до завершения выполнения программы. С другой стороны, объекты, которые были размещены в куче лишь недавно (как, например, те, что находятся в пределах области действия метода), вероятнее всего будут становиться недостижимым довольно быстро. Исходя из этих предположений, каждый объект в куче относится к одному из перечисленных ниже поколений. • Поколение О. Идентифицирует новый только что размещенный объект, который еще никогда не помечался как подлежащий удалению в процессе сборки мусора. • Поколение 1. Идентифицирует объект, который уже “пережил” один процесс сбор­ ки мусора (был помечен как подлежащий удалению в процессе сборки мусора, но не был удален из-за наличия достаточного места в куче). • Поколение 2. Идентифицирует объект, которому удалось пережить более одного прогона сборщика мусора. Глава 8. Время жизни объектов 299 На заметку! Поколения 0 и 1 называются эфемерными (недолговечными). В следующем разделе будет показано, что в ходе процесса сборки мусора эфемерные поколения действительно об­ рабатываются по-другому. Сборщик мусора сначала анализирует все объекты, которые относятся к поколению 0. Если после их удаления остается достаточное количество памяти, статус всех осталь­ ных (уцелевших) объектов повышается до поколения 1. Чтобы увидеть, как поколение, к которому относится объект, влияет на процесс сборки мусора, обратите внимание на рис. 8.5, где схематически показано, как набору уцелевших объектов поколения О (А, В и Е) назначается статус объектов следующего поколения после освобождения требуе­ мого объема памяти. Поколение О А В С D Е F G Поколение 1 А В Е Рис. 8.5. Объектам поколения 0, которые уцелели после сборки мусора, назначается статус объектов поколения 1 Если все объекты поколения 0 уже были проверены, но все равно требуется допол­ нительное пространство, проверяться на предмет достижимости и подвергаться про­ цессу сборки мусора начинают объекты поколения 1. Объектам поколения 1, которым удалось уцелеть после этого процесса, затем назначается статус объектов поколения 2. Если же сборщику мусора все равно требуется дополнительная память, тогда на пред­ мет достижимости начинают проверяться и объекты поколения 2. Объектам, которым удается пережить сборку мусора на этом этапе, оставляется статус объектов поколения 2, поскольку более высокие поколения просто не поддерживаются. Из всего вышесказанного важно сделать следующий вывод: из-за отнесения объектов в куче к определенному поколению, более новые объекты (вроде локальных переменных) будут удаляться быстрее, а более старые (такие как объекты приложений) — реже. Параллельная сборка мусора в версиях .NET 1.0 — .NET 3.5 До выхода версии .NET 4.0 очистка неиспользуемых объектов в исполняющей сре­ де производилась с применением техники параллельной сборки мусора. В этой модели при выполнении сборки мусора для любых объектов поколения 0 или 1 (т.е. эфемерных поколений) сборщик мусора временно приостанавливал все активные потоки внутри текущего процесса, чтобы приложение не могло получить доступ к управляемой куче вплоть до завершения процесса сборки мусора. Потоки более подробно рассматриваются в главе 19, а пока можно считать поток просто одним из путей выполнения внутри функционирующей программы. По заверше­ 300 Часть II. Главные конструкции программирования на C# нии цикла сборки мусора приостановленным потокам разрешалось снова продолжать работу. К счастью, в .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 Член Описание AddMemoryPressure(), RemoveMemoryPressure() Позволяют указывать числовое значение, отражающее "уро­ вень срочности” , который вызывающий объект применяет в отношении к сборке мусора. Следует иметь в виду, что эти методы должны изменять уровень давления в тандеме и, сле­ довательно, никогда не устранять больше давления, чем было добавлено Collect() Заставляет сборщик мусора провести сборку мусора. Должен быть перегружен так, чтобы указывать, объекты какого поко­ ления подлежат сборке, а также какой режим сборки исполь­ зовать (с помощью перечисления GCCollectionMode) CollectionCount() Возвращает числовое значение, показывающее, сколько раз объектам данного поколения удалось переживать процесс сборки мусора GetGeneration() Возвращает информацию о том, к какому поколению в на­ стоящий момент относится объект GetTotalMemory() Возвращает информацию о том, какой объем памяти (в бай­ тах) в настоящий момент занят в управляемой куче. Булевский параметр указывает, должен ли вызов сначала дождаться вы­ полнения сборки мусора, прежде чем возвращать результат MaxGeneration Возвращает информацию о том, сколько максимум поколе­ ний поддерживается в целевой системе. В .NET 4.0 поддер­ живается всего три поколения: 0, 1 и 2 SuppressFinalize() Позволяет устанавливать флаг, указывающий, что для данно­ го объекта не должен вызываться его метод Finalize () WaitForPendingFmalizers () Позволяет приостанавливать выполнение текущего потока до тех пор, пока не будут финализированы все объекты, преду­ сматривающие финализацию. Обычно вызывается сразу же после вызова метода GC.Collect () Рассмотрим применение System. GC для получения касающихся сборки мусора де­ талей на примере следующего метода Main (), в котором используются сразу несколько членов System. GC: static void Main(string[] args) { Console.WnteLine (”***** Fun with System.GC *****”); // Вывод подсчитанного количества байтов в куче. Console.WriteLine("Estimates bytes on heap: {0}”, G C .GetTotalMemory(false)); // Отсчет для MaxGeneration начинается с нуля, // поэтому для удобства добавляется 1. 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 (’’Generation of refToMyCar is: {0 1”, GC .GetGeneration(refToMyCar)); Console.FeadLine (); 302 Часть II. Главные конструкции программирования на C# Принудительная активизация сборки мусора Сборщик мусора .NET предназначен в основном для того, чтобы управлять памя­ тью вместо разработчиков. Однако в очень редких случаях требуется принудительно запустить сборку мусора с помощью метода GC.Collect ( ) . Примеры таких ситуаций приведены ниже. • Приложение приступает к выполнению блока кода, прерывание которого возмож­ ным процессом сборки мусора является недопустимым. • Приложение только что закончило размещать чрезвычайно большое количество объ­ ектов и нуждается в как можно скорейшем освобождении большого объема памяти. Если выяснилось, что выполнение сборщиком мусора проверки на предмет наличия недостижимых объектов может быть выгодным, можно инициировать процесс сборки мусора явным образом, как показано ниже: static void Main(string[] args) { // Принудительная активизация процесса сборки мусора и // ожидание завершения финализации каждого из объектов. GC.Collect (); G C .WaitForPendingFinalizers (); В случае принудительной активизации сборки мусора не забывайте вызвать метод GC . WaitForPendingFinalizers (). Это дает возможность всем финализируемым объек­ там (рассматриваются в следующем разделе) произвести любую необходимую очистку перед продолжением работы программы. Метод GC .WaitForPendingFinalizers () не­ заметно приостанавливает выполнение вызывающего “потока” во время процесса сбор­ ки мусора, что очень хорошо, поскольку исключает вероятность вызова в коде какихлибо методов на объекте, который в текущий момент уничтожается. Методу GC .Collect () можно передать числовое значение, отражающее старейшее поколение объектов, в отношении которого должен проводиться процесс сборки мусора. Например, чтобы CLR-среда анализировала только объекты поколения 0, необходимо использовать следующий код: static void Main(string[] args) { // Исследование только объектов поколения 0. GC.Collect (0); G C .WaitForPendingFinalizers (); } Вдобавок методу Collect () во втором параметре может передаваться значение пе­ речисления GCCollectionMode, которое позволяет более точно указать, каким образом исполняющая среда должна принудительно инициировать сборку мусора. Ниже пока­ заны значения, доступные в этом перечислении: public enum { Default, Forced, Optimized GCCollectionMode // Текущим значением по умолчанию является Forced. // Указывает исполняющей среде начать сборку мусора немедленно1 // Позволяет исполняющей среде выяснить, оптимален / / л и настоящий момент для удаления объектов. Глава 8. Время жизни объектов 303 Как и при любой сборке мусора, в случае вызова G C .C o lle c tO уцелевшим объектам назначается статус объектов более высокого поколения. Чтобы удостовериться в этом, модифицируем метод Main () следующим образом: static void Main(string[] args) { Console.WnteLine (''***** 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. ToStnng () ) ; // Вывод информации о поколении, к которому // относится 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 (0, GCCollectionMode.Forced); GC.WaitForPendingFinalizers(); // Вывод информации о поколении, к которому // относится refToMyCar. Console.WriteLine("\nGeneration of refToMyCar is: {0}", GC .GetGeneration(refToMyCar)); // // // if Выполнение проверки, удалось ли tonsOfObjects[9000] уцелеть после сборки мусора. (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", G C .CollectionCount (0)); Console.WriteLine ("Gen 1 has been swept {0} times", GC.CollectionCount (1)); Console.WriteLine("Gen 2 has been swept {0} times", GC.CollectionCount(2)); Console.ReadLine (); 304 Часть II. Главные конструкции программирования на C# Дя целей тестирования был создан очень большой массив типа 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.CollectO. Помимо Глава 8. Время жизни объектов 305 этого, финализатор типа будет автоматически вызываться и при выгрузке из памяти домена, который отвечает за обслуживание приложения. Некоторым по опыту работы с .NETT уже может быть известно, что домены приложений (AppDomain) применяются для обслуживания исполняемой сборки и любых необходимых внешних библиотек кода. Те, кто еще не знаком с этим понятием .NET, узнают все необходимое после прочтения главы 16. Пока что необходимо обратить внимание лишь на то, что при выгрузке до­ мена приложения из памяти CLR-среда будет автоматически вызывать финализаторы для каждого финализируемого объекта, который был создан во время существования AppDomain. Что бы не подсказывали инстинкты разработчика, в подавляющем большинстве классов C# необходимость в создании явной логики по очистке или специального финализатора возникать не будет. Объясняется это очень просто: если в классах использу­ ются лишь другие управляемые объекты, все они рано или поздно все равно будут под­ вергаться сборке мусора. Единственным случаем, когда может возникать потребность в создании класса, способного выполнять после себя процедуру очистки, является рабо­ та с неуправляемыми ресурсами (такими как низкоуровневые файловые дескрипторы, низкоуровневые неуправляемые соединения с базами данных, фрагменты неуправляе­ мой памяти и т.п.). Внутри .NETT неуправляемые ресурсы появляются в результате непо­ средственного вызова API-интерфейса операционной системы с помощью служб PInvoke (Platform Invocation Services — службы вызова платформы) или применения очень слож­ ных сценариев взаимодействия с СОМ. Ознакомьтесь со следующим правилом сборки мусора. Правило. Единственная причина переопределения Finalize () связана с использованием в клас­ се C# каких-то неуправляемых ресурсов через PInvoke или сложных процедур взаимодействия с СОМ (обычно посредством членов типа System.Runtime .InteropServices .Marshal). Переопределение S y s t e m . O b j e c t . F i n a l i z e ( ) В тех редких случаях, когда создается класс С#, в котором используются неуправ­ ляемые ресурсы, очевидно, понадобится обеспечить предсказуемое освобождение соот­ ветствующей памяти. Для примера создадим новый проект типа Console Application на C# по имени SimpleFinalize, вставим в него класс MyResourceWrapper, в котором будет использоваться какой-то неуправляемый ресурс. Теперь необходимо переопреде­ лить метод Finalize (). Как ни странно, применять для этого ключевое слово override в C# не допускается: class MyResourceWrapper { // Ошибка на этапе компиляции' protected override void Finalize () { } } Вместо этого для достижения того же эффекта должен применяться синтаксис дест­ руктора (подобно C++). Объясняется это тем, что при обработке синтаксиса финализатора компилятор автоматически добавляет в неявно переопределяемый метод Finalize () приличное количество требуемых элементов инфраструктуры. Финализаторы в C# очень похожи на конструкторы тем, что именуются идентично классу, внутри которого определены. Помимо этого, они сопровождаются префиксом в виде тильды (~). В отличие от конструкторов, однако, они никогда не снабжаются моди­ фикатором доступа (поскольку всегда являются неявно защищенными), не принимают никаких параметров и не могут быть перегружены (в каждом классе может присутство­ вать только один финализатор). 306 Часть II. Главные конструкции программирования на C# Ниже приведен пример написания спец иального ф инализатора для класса MyResourceWrapper, который при вызове заставляет систему выдавать звуковой сиг­ нал. Очевидно, что данный пример предназначен только для демонстрационных целей. В реальном приложении финализатор будет только освобождать любые неуправляемые ресурсы и не будет взаимодействовать ни с какими другими управляемыми объекта­ ми, даже теми, на которые ссылается текущий объект, поскольку рассчитывать на то, что они все еще существуют на момент вызова данного метода F in a liz e () сборщиком мусора нельзя. // Переопределение System .O bject.F in alize() / / с использованием синтаксиса финализатора. class MyResourceWrapper { ~MyResourceWrapper() { // Здесь производится очистка неуправляемых ресурсов. // Обеспечение подачи звукового сигнала при // уничтожении объекта (только в целях тестирования). Console.Веер(); } Если теперь просмотреть CIL-код данного деструктора с помощью утилиты ildasm . ехе, обнаружится, что компилятор добавляет некоторый код для проверки ошибок. Вопервых, он помещает операторы из области действия метода F in a liz e () в блок t r y (см. главу 7), и, во-вторых, добавляет соответствующий блок f i n a l l y , чтобы гарантировать выполнение метода F in a l i z e () в базовых классах, какие бы исключения не возникали в контексте tr y . .method family hidebysig virtual instance void F in a liz e () cil managed { // Code siz e 13 (Oxd) // Размер кода 1 3 (Oxd) .maxstack 1 .try { IL_0000: ldc.i4 0x4e20 IL_0005: ldc.i4 0x3e8 IL_000a: call void [m scorlib]System .Console: : Beep(int32, int32) IL_000f: nop IL_0010: nop IL_0011: leave.s IL_001b } // end .try f i n a lly { IL_0013: ldarg.O IL_0014: c a l l instance void [m scorlib]System .O bject: : F in a liz e () IL_0019: 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 .WnteLine ("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 мог освобождать неуправляемые ресурсы во время сборки мусора. Следовательно, при создании типа, в котором никакие неуправляемые сущности не используются (так бывает чаще всего), от финализации оказывается мало толку. На самом деле, всегда, когда возможно, следует стараться про­ ектировать типы так, чтобы в них не поддерживался метод Finalize () по той очень простой причине, что выполнение финализации отнимает время. При размещении объекта в управляемой куче исполняющая среда автоматически определяет, поддерживается ли в нем какой-нибудь специальный метод Finalize (). Если да, тогда она помечает его как финализируемый (finalizable) и сохраняет указа­ тель на него во внутренней очереди, называемой очередью финализации (finalization queue). Эта очередь финализации представляет собой просматриваемую сборщиком мусора таблицу, где перечислены объекты, которые перед удалением из кучи должны быть обязательно финализированы. Когда сборщик мусора определяет, что наступило время удалить объект из памяти, он проверяет каждую запись в очереди финализации и копирует объект из кучи в еще одну управляемую структуру, называемую таблицей объектов, доступных для финали­ зации (finalization reachable table). После этого он создает отдельный поток для вызова метода FinalizeO в отношении каждого из упоминаемых в этой таблице объектов при следующей сборке мусора. В результате получается, что для окончательной финализа­ ции объекта требуется как минимум два процесса сборки мусора. Из всего вышесказанного следует, что хотя финализация объекта действительно по­ зволяет гарантировать способность объекта освобождать неуправляемые ресурсы, она все равно остается недетерминированной по своей природе, и по причине дополнитель­ ной выполняемой незаметным образом обработки протекает гораздо медленнее. Создание высвобождаемых объектов Как было показано, методы финализации могут применяться для освобождения не­ управляемых ресурсов при активизации процесса сборки мусора. Однако многие не­ управляемые объекты являются “ценными элементами” (например, низкоуровневые 308 Часть II. Главные конструкции программирования на C# соединения с базой данных или файловые дескрипторы) и часто выгоднее освобождать их как можно раньше, еще до наступления момента сборки мусора. Поэтому вместо пе­ реопределения Finalize () в качестве альтернативного варианта также можно реали­ зовать в классе интерфейс IDisposable, который имеет единственный метод по имени 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 .WnteLine ("**** * In Dispose1 *****"); } Обратите внимание, что метод Dispose () отвечает не только за освобождение не­ управляемых ресурсов типа, но и за вызов аналогичного метода в отношении любых других содержащихся в нем высвобождаемых объектов. В отличие от Finalize (), в нем вполне допустимо взаимодействовать с другими управляемыми объектами. Объясняется это очень просто: сборщик мусора не имеет понятия об интерфейсе IDisposable и по­ тому никогда не будет вызывать метод Dispose (). Следовательно, при вызове данного метода пользователем объект будет все еще существовать в управляемой куче и иметь доступ ко всем остальным находящимся там объектам. Логика вызова этого метода вы­ глядит довольно просто: Глава 8. Время жизни объектов 309 class Program { static void Main(string [] args) { Console .WnteLine ("***** Fun with Dispose *****\n"); // Создание высвобождаемого объекта и вызов метода // Dispose() для освобождения любых внутренних ресурсов. MyResourceWrapper rw = new MyResourceWrapper() ; rw.Dispose(); Console.ReadLine (); } } Разумеется, перед тем как пытаться вызывать метод Dispose () на объекте, нужно проверить, поддерживает ли соответствующий тип интерфейс IDisposable. И хотя в документации .NET Framework 4.0 SDK всегда доступна информация о том, какие типы в библиотеке базовых классов реализуют IDisposable, такую проверку удобнее выпол­ нять программно с применением ключевого слова is или as (см. главу 6). class Program { static void Main(string[] args) { Console.WnteLine (''***** 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.1 0 ... static void DisposeFileStream() { FileStream fs = new FileStream("myFile.txt", FileMode.OpenOrCreate); // Мягко говоря, сбивает с толку! // Вызовы этих методов делают одно и то же! fs.Close(); fs.Dispose() ; 310 Часть II. Главные конструкции программирования на C# И хотя “закрытие” (close) файла действительно звучит более естественно, чем его “освобождение” (dispose), нельзя не согласиться с тем, что подобное дублирование от­ вечающих за одно и то же методов вносит путаницу. Поэтому при использовании этих нескольких типов, в которых применяются псевдонимы, просто помните о том, что если тип реализует интерфейс IDisposable, то вызов метода Dispose () всегда является правильным образом действия. Повторное использование ключевого слова using в C# При работе с управляемым объектом, который реализует интерфейс IDisposable, довольно часто требуется применять структурированную обработку исключений, га­ рантируя, что метод Dispose () типа будет вызываться даже в случае возникновения какого-то исключения: static void Main(string [] args) { Console.WriteLine ("***** Fun with Dispose *****\n"); MyResourceWrapper rw = new MyResourceWrapper(); try // Использование членов rw. } finally { // Обеспечение вызова метод D ispose() в любом случае, / / в том числе при возникновении ошибки. rw.Dispose(); } } Хотя это является замечательными примером “безопасного программирования”, ис­ тина состоит в том, что очень немногих разработчиков прельщает перспектива заклю­ чать каждый очищаемый тип в блок try/finally лишь для того, чтобы гарантировать вызов метода Dispose (). Для достижения аналогичного результата, но гораздо менее громоздким образом, в C# поддерживается специальный фрагмент синтаксиса, кото­ рый выглядит следующим образом: static void Main(string [] args) { Console.WriteLine ("***** Fun with Dispose *****\n"); // Метод D ispose() вызывается автоматически // при выходе за пределы области действия 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 Sim pleFinalize.MyResourceWrapper: : D ispose() } // end handler } // end of method Program::Main На заметку! При попытке применить using к объекту, который не реализует интерфейс IDisposable, на этапе компиляции возникнет ошибка. Хотя применение такого синтаксиса действительно избавляет от необходимости вручную помещать высвобождаемые объекты в рамки try/f inally, в настоящее время, к сожалению, ключевое слово using в C# имеет двойное значение (поскольку служит и для добавления ссылки на пространства имен, и для вызова метода Dispose ()). Тем не менее, при работе с типами.NET, которые поддерживают интерфейс IDisposable, дан­ ная синтаксическая конструкция будет гарантировать автоматический вызов метода Dispose () в отношении соответствующего объекта при выходе из блока using. Кроме того, в контексте using допускается объявлять несколько объектов одного и того ж е типа. Как не трудно догадаться, в таком случае компилятор будет вставлять код с вызовом Dispose () для каждого объявляемого объекта. static void Main(string[] args) { Console.WriteLine ("***** Fun with Dispose *****\n"); // Использование разделенного запятыми списка для // объявления нескольких подлежащих освобождению объектов. using(MyResourceWrapper rw = new MyResourceWrapper(), rw2 = new MyResourceWrapper()) { // Использование объектов rw и rw2. } } Исходный код. Проект SimpleDispose доступен в подкаталоге Chapter 8. Создание финализируемых и высвобождаемых типов К этому моменту были рассмотрены два различных подхода, которые можно при­ менять для создания класса, способного производить очистку и освобождать внутрен­ ние неуправляемые ресурсы. Первый подход заключается в переопределении метода System. Object. Finalize () и позволяет гарантировать то, что объект будет очищать себя сам во время процесса сборки мусора (когда бы тот не запускался) без вмешатель­ ства со стороны пользователя. Второй подход предусматривает реализацию интерфей­ са IDisposable и позволяет обеспечить пользователя объекта возможностью очищать объект сразу же по окончании работы с ним. Однако если пользователь забудет вызвать метод Dispose ( ) , неуправляемые ресурсы могут оставаться в памяти на неопределен­ ный срок. Как не трудно догадаться, оба подхода можно комбинировать и применять вместе в определении одного класса, получая преимущества от обеих моделей. Если пользо­ 312 Часть II. Главные конструкции программирования на C# ватель объекта не забыл вызвать метод Dispose () , можно проинформировать сбор­ щик мусора о пропуске финализации, вызвав метод G C .SuppressFinalize () . Если же пользователь забыл вызвать этот метод, объект рано или поздно будет подвергнут финализации и получит возможность освободить внутренние ресурсы. Преимущество такого подхода в том, что при этом внутренние ресурсы будут так или иначе, но всегда освобождаться. Ниже приведена очередная версия класса MyResourceWrapper, которая теперь пре­ дусматривает выполнение и финализации и освобождения и содержится внутри проек­ та типа Console Application по имени FinalizableDisposableClass. // Сложный упаковщик ресурсов, public class MyResourceWrapper : IDisposable { // Сборщик мусора будет вызывать этот метод, если // пользователь объекта забыл вызвать метод Dispose () . ~MyResourceWrapper() { // Освобождение любых внутренних неуправляемых // ресурсов. Метод Dispose () НЕ должен вызываться / / н и для каких управляемых объектов. // Пользователь объекта будет вызывать этот метод для // того, чтобы освободить ресурсы как можно быстрее, public void Dispose () { // Здесь осуществляется освобождение неуправляемых ресурсов / / и вызов Dispose()для остальных высвобождаемых объектов. // Если пользователь вызвал Dispose (), то финализация не нужна, // поэтому далее она подавляется. G C .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. Главные конструкции программирования на C# Чтобы протестировать последнюю версию класса MyResourceW rapper, добавим в метод финализации вызов C o n s o le . Веер ( ): -MyResourceWrapper () { Console.Веер (); // Вызов вспомогательного метода. // Указание значения fa ls e свидетельствует о том, // что очистка была инициирована сборщиков мусора. Cleanup(false); } Давайте обновим метод Main () следующим образом: static void Main(string [] args) { Console.WriteLine (''***** Dispose () / Destructor Combo Platter *****"); // Вызов метода D ispose() вручную; метод финализации / / в таком случае вызываться не будет. MyResourceWrapper rw = new MyResourceWrapper(); rw.Dispose (); // Пропуск вызова метода D ispose( ) ; в таком случае будет // вызываться метод финализации и выдаваться звуковой сигнал. MyResourceWrapper rw2 = new MyResourceWrapper(); Обратите внимание, что в первом случае производится явный вызов метода D ispose () на объекте rw и потому вызов деструктора подавляется. Однако во втором случае мы “забываем” вызвать метод D ispose () на объекте rw2 и потому по окончании выполнения приложения услышим однократный звуковой сигнал. Если закомментиро­ вать вызов метода D isp o seO на объекте rw, звуковых сигналов будет два. Исходный код. Проект F in a liz a b le D is p o s a b le C la s s доступен в подкаталоге Chapter 8. На этом тема управления объектами CLR-средой посредством сборки мусора за­ вершена. И хотя некоторые дополнительные (и довольно экзотические) детали, касаю­ щиеся сборки мусора (вроде слабых ссылок и восстановления объектов), остались не рассмотренными, полученных базовых сведений должно оказаться достаточно, чтобы продолжить изучение самостоятельно. Напоследок в главе предлагается исследовать совершенно новую функциональную возможность, появившуюся в .NET 4.0, которая называется отложенной (ленивой) инициализацией. Отложенная инициализация объектов На заметку! В настоящем разделе предполагается наличие у читателя знаний о том, что собой представляют обобщения и делегаты в .NET. Исчерпывающие сведения о делегатах и обобще­ ниях можно найти в главах 10 и 11. При создании классов иногда возникает необходимость предусмотреть в коде опреде­ ленную переменную-член, которая на самом деле может никогда не понадобиться из-за того, что пользователь объекта не будет обращаться к методу (или свойству), в котором она используется. Вполне разумное решение, однако, на практике его реализация мо­ жет оказываться очень проблематичной в случае, если инициализация интересующей переменной экземпляра требует большого объема памяти. Глава 8. Время жизни объектов 315 Для примера представим, что требуется создать класс, инкапсулирующий операции цифрового музыкального проигрывателя, и помимо ожидаемых методов вроде P la y ( ) , Pause () и Stop () его нужно также обеспечить способностью возврата коллекции объ­ ектов Song (через класс по имени A llT r a c k s ), которые представляют каждый из имею­ щихся в устройстве цифровых музыкальных файлов. Чтобы получить такой класс, создадим новый проект типа Console Application по имени L a z y O b je c t I n s t a n t ia t io n и добавим в него следующие определения типов классов: // Представляет одну композицию. 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 .WnteLine ("Filling up the songs!"); } } // Класс MediaPlayer включает объект AllTracks. class MediaPlayer { // Предполагаем, что эти методы делают нечто полезное. public void Play() { /* Воспроизведение композиции */ } public void Pause() { /* Приостановка воспроизведения композиции */ } public void Stop() { /* Останов воспроизведения композиции */ } private AllTracks allSongs = new AllTracks (); public AllTracks GetAllTracks () { // Возвращаем все композиции. return allSongs; В текущей реализации M ed ia P la y er делается предположение о том, что пользовате­ лю объекта понадобится получать список объектов с помощью метода G e tA llT ra c k s ( ). А что если пользователю объекта не нужен этот список? Так или иначе, но переменная экземпляра A llT ra c k s будет приводить к созданию 10 000 объектов Song в памяти: static void Main(string [] args) { //В этом вызывающем коде получение всех композиций не // производится, но косвенно все равно создаются // 10 000 объектов! MediaPlayer myPlayer = new MediaPlayer(); myPlayer.Play(); Console.ReadLine (); 316 Часть II. Главные конструкции программирования на C# Понятно, что создания 10 000 объектов, которыми никто не будет пользоваться, луч­ ше избежать, так как это изрядно прибавит работы сборщику мусора .NET. Хотя можно вручную добавить код, который обеспечит создание объекта all Songs только в случае его использования (например, за счет применения шаблона с методом фабрики), суще­ ствует и более простой путь. С выходом .NET 4.0 в библиотеках базовых классов появился очень интересный обобщенный класс по имени Lazyo, который находится в пространстве имен 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; } Помимо того, что переменная экземпляра A l l Track теперь имеет тип L a z y o , важно отметить, что реализация предыдущего метода G etA llT ra c k s () тоже изменилась. В ча­ стности, теперь требуется использовать доступное только для чтения свойство Value класса L a z y o для получения фактических хранимых данных (в этом случае — объект A llT r a c k s , обслуживающий 10 000 объектов Song). Кроме того, обратите внимание, как благодаря этому простому изменению, пока­ занный ниже модифицированный метод Ma i n( ) будет незаметно размещать объек­ ты Song в памяти только в случае, когда был действительно выполнен вызов метода G e t A l l T r a c k s (). static void Main(string [] args) { Console .WriteLine (''***** Fun with Lazy Instantiation *****\n "); // Никакого размещения объекта A llTracks! MediaPlayer myPlayer = new MediaPlayer(); myPlayer.Play(); Глава 8. Время жизни объектов 317 // Размещение объекта AllTracks происходит // только в случае вызова метода G etA llT racks( ) . MediaPlayer yourPlayer = new MediaPlayer(); AllTracks yourMusic = yourPlayer.GetAllTracks(); Console.ReadLine(); } Настройка процесса создания данных L a z y o При объявлении переменной L a z y o фактический внутренний тип данных создает­ ся с помощью конструктора по умолчанию: // Конструктор по умолчанию для AllTracks вызывается // при использовании переменной LazyO . private Lazy<AllTracks> allSongs = new Lazy<AllTracks>(); В некоторых случаях подобное поведение может быть вполне подходящим, но что если класс AllTracks имеет дополнительные конструкторы, и нужно позаботиться о том, чтобы вызывался подходящий из них? Более того, а что если необходимо, чтобы при создании переменной Lazy () выполнялась какая-то дополнительная работа (поми­ мо просто создания объекта AllTracks)? К счастью, класс Lazy () позволяет предостав­ лять в качестве необязательного параметра обобщенный делегат, который указывает, какой метод должен вызываться во время создания находящегося внутри него типа. Этим обобщенным делегатом является тип System. Funco, который может указы­ вать на метод, возвращающий тот же тип данных, что создается соответствующей пе­ ременной Lazyo, и способный принимать вплоть до 16 аргументов (которые типизиру­ ются с помощью параметров обобщенного типа). В большинстве случаев необходимость указывать параметры для передачи методу, на который ссылается Funco, возникать не будет Более того, чтобы значительно упростить применение F u n c o , лучше исполь­ зовать лямбда-выражения (отношения между делегатами и лямбда-выражениями под­ робно рассматриваются в главе 11). С учетом всего сказанного, ниже приведена окончательная версия MediaPlayer, в которой теперь при создании внутреннего объекта AllTracks добавляется небольшой специальный код. Следует запомнить, что данный метод должен обязательно возвра­ щать новый экземпляр указанного в L a z y o типа перед выходом, а использовать мож­ но любой конструктор (в коде для AllTracks по-прежнему вызывается конструктор по умолчанию). class MediaPlayer // Использование лямбда-выражения для добавления // дополнительного кода при создании объекта A llT ra c k s. private Lazy<AllTracks> allSongs = new Lazy<AllTracks> ( () => { Console .WnteLine ("Creating AllTracks object!"); return new AllTracks (); public AllTracks GetAllTracks () { // Возврат всех композиций. return allSongs.Value; 318 Часть II. Главные конструкции программирования на C# Хочется надеяться, что удалось продемонстрировать выгоду, которую способен обеспечить класс Lazyo. В сущности, этот новый обобщенный класс позволяет де­ лать так, чтобы дорогостоящие объекты размещались в памяти только тогда, когда они будут действительно нужны пользователю объекта. Если эта тема заинтересовала, можно заглянуть в посвященный классу System.L a z y o раздел в документации .NET Framework 4.0 SDK и найти там дополнительные примеры программирования отложен­ ной инициализации. Исходный код. Проект LazyObjectInstantiation доступен в подкаталоге Chapter 8. Резюме Цель настоящей главы состояла в разъяснении, что собой представляет процесс сборки мусора. Было показано, что сборщик мусора активизируется только тогда, ко­ гда ему не удается получить необходимый объем памяти из управляемой кучи (или ко­ гда происходит выгрузка домена соответствующего приложения из памяти). После его активизации поводов для волнения возникать не должно, поскольку разработанный в Microsoft алгоритм сборки мусора хорошо оптимизирован и предусматривает исполь­ зование поколений объектов, дополнительных потоков для финализации объектов и управляемой кучи для обслуживания больших объектов. В главе также было показано, каким образом программно взаимодействовать со сборщиком мусора, применяя класс System.GC. Как отмечалось, единственным случа­ ем, когда в подобном может возникнуть необходимость, является создание финализируемых или высвобождаемых типов классов, в которых используются неуправляемые ресурсы. Вспомните, что под финализируемыми типами подразумеваются классы, в кото­ рых переопределен виртуальный метод System.Object.Finalize () для обеспечения очистки неуправляемых ресурсов на этапе сборки мусора, а под высвобождаемыми — классы (или структуры), в которых реализован интерфейс IDisposable, вызываемый пользователем объекта по окончании работы с ним. Кроме того, в главе был продемон­ стрирован формальный шаблон “очистки”, позволяющий совмещать оба эти подхода. И, наконец, в главе был описан появившийся в .NET 4.0 обобщенный класс по име­ ни Lazyo. Как здесь было показано, данный класс позволяет отложить создание до­ рогостоящих (в плане потребления памяти) объектов до тех пор, пока у вызывающей стороны действительно не возникнет потребность в их использовании. Это помогает сократить количество объектов, сохраняемых в управляемой куче, и облегчает нагрузку на сборщик мусора. ЧАСТЬ III Дополнительные конструкции программирования на C# В этой ч а сти ... Глава 9. Работа с интерфейсами Глава 10. Обобщения Глава 11. Делегаты, события и лямбда-выражения Глава 12. Расширенные средства языка C# Глава 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 сопровождаются префиксом в виде заглавной буквы Т . При создании собственных специальных интерфейсов рекомендуется тоже следовать этому соглашению. Вдумываться, что именно делают все эти члены, пока не нужно. Сейчас главное про­ сто понять, что в интерфейсе 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. Дополнительные конструкции программирования на C# спокойно определяться любое количество конструкторов, полей, неабстрактных членов (с реализацией) и т.п. Интерфейсы, с другой стороны, могут содержать только опреде­ ления абстрактных членов. Полиморфный интерфейс, предоставляемый абстрактным родительским классом, обладает одним серьезным ограничением: определяемые в абстрактном родительском классе члены поддерживаются только в производных типах. В более крупных про­ граммных системах, однако, довольно часто разрабатываются многочисленные иерар­ хии классов, не имеющие никаких общих родительских классов кроме System.Object. Из-за того, что абстрактные члены в абстрактном базовом классе подходят только для производных типов, получается, что настраивать типы в разных иерархиях так, чтобы они поддерживали один и тот же полиморфный интерфейс, невозможно. Для примера предположим, что определен следующий абстрактный класс: abstract class CloneableType { // Только производные типы могут поддерживать этот // "полиморфный интерфейс". Классы в других иерархиях //н е будут иметь доступа к этому абстрактному члену. public abstract object Clone(); } Из-за такого определения поддерживать метод Clone () могут только члены, которые расширяют CloneableType. В случае создания нового набора классов, которые не рас­ ширяют CloneableType, использовать в них этот полиморфный интерфейс, соответст­ венно, не получится. Кроме того, как уже неоднократно упоминалось, в C# не поддержи­ вается возможность наследования от нескольких классов. Следовательно, создать класс MiniVan, унаследованный и от Саг, и от CLoneableType, тоже не получится: // В C# наследование от нескольких классов не допускается. 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. Дополнительные конструкции программирования на C# Определение специальных интерфейсов Теперь, имея более четкое представление о роли интерфейсов, давайте рассмотрим пример определения и реализации специальных интерфейсов. Создадим новый проект типа Console Application по имени Customlnterface и, выбрав в меню Project (Проект) пункт Add Existing Item (Добавить существующий элемент), вставим в него файл или файлы, содержащие определения типов фигур (файл Shapes.cs в примерах кода), ко­ торые были созданы в главе 6. После этого переименуем пространство имен, в котором содержатся определения отвечающих за фигуры типов, в Customlnterface (просто что­ бы не импортировать их из этого пространства имен в новый проект): namespace Customlnterface { // Здесь должны идти определения // созданных ранее типов фигур... } Теперь вставим в проект новый интерфейс по имени IPointy, выбрав в меню Project пункт Add Existing Item, как показано на рис. 9.1. Рис. 9.1. Интерфейсы, как и классы, могут определяться в любом файле * .cs На синтаксическом уровне любой интерфейс определяется с помощью ключевого слова interface. В отличие от классов, базовый класс (даже System.Object) для ин­ терфейсов никогда не указывается (хотя, как будет показано позже в главе, базовые интерфейсы указываться могут). Более того, модификаторы доступа для членов интер­ фейсов тоже никогда не указываются (поскольку все члены интерфейсов всегда явля­ ются неявно общедоступными и абстрактными). Ниже показано, как должно выглядеть определение специального интерфейса IPointyHaC#: // Этот интерфейс определяет поведение "наличие вершин". public interface IPointy { // Этот член является неюно общедоступным и абстрактным. byte GetNumberOfPoints (); Глава 9. Работа с интерфейсами 325 Вспомните, что при определении членов интерфейсов область их реализации не задается. Интерфейсы являются чистым протоколом, и потому реализация в них ни­ когда не предоставляется (за это отвечает поддерживающий класс или структура). Следовательно, использование показанной ниже версии IPointy привело бы к возник­ новению различных ошибок на этапе компиляции: // Внимание! Полно ошибок! public interface IPointy // Ошибка! Интерфейсы не могут иметь поля! public int numbOfPoints; // Ошибка! Интерфейсы не могут иметь конструкторы! public IPointy() { numbOfPoints = 0;}; // Ошибка! В интерфейсах не может предоставляться // реализация методов! byte GetNumberOfPoints() { return numbOfPoints; } } В любом случае, в начальном интерфейсе IPointy определен только один метод. Однако в .NET допустимо определять в типах интерфейсов любое количество прототи­ пов свойств. Например, можно было бы создать интерфейс IPointy так, чтобы в нем использовалось доступное только для чтения свойство, а не традиционный метода доступа: // Определение в IPointy свойства, доступного только для чтения. public interface IPointy { // Свойство, доступное как для чтения, так и для записи, // в этом интерфейсе может выглядеть так: // retV al PropName { get; se t; } // а свойство, доступное только для записи - так: // retV al PropName { set; } byte Points { get; } На заметку! Типы интерфейсов также могут содержать определения событий (см. главу 11) и ин­ дексаторов (см. главу 12). Сами по себе типы интерфейсов довольно бесполезны, поскольку представляют собой не более чем просто именованную коллекцию абстрактных членов. Например, размещать типы интерфейсов таким же образом, как классы или структуры, не разрешается: // Внимание! Размещать типы интерфейсов не допускается! static void Main(string[] args) { IPointy p = new IPointy(); // Компилятор сообщит об ошибке! } Интерфейсы ничего особого не дают, если не реализуются в каком-то классе или структуре. Здесь IPointy представляет собой интерфейс, который выражает поведение “наличие вершин”. Стоящая за его созданием идея выглядит довольно просто: некото­ рые классы в иерархии фигур (например, Hexagon) должны иметь вершины, а некото­ рые (вроде Circle) — нет. 326 Часть III. Дополнительные конструкции программирования на C# Реализация интерфейса Чтобы расширить функциональность какого-то класса или структуры за счет обес­ печения в нем поддержки интерфейсов, необходимо предоставить в его определении список соответствующих интерфейсов, разделенных запятыми. Следует иметь в виду, что непосредственный базовый класс должен быть обязательно перечислен в этом списке первым, сразу же после двоеточия. Когда тип класса наследуется прямо от S ystem .O b ject, допускается перечислять только лишь поддерживаемый им интерфейс или интерфейсы, поскольку компилятор C# автоматически расширяет типы возможно­ стями S y s te m .O b je c t в случае, если не было указано иначе. Из-за того, что структу­ ры всегда наследуются от класса S ystem .V alueType (см. главу 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 класс по имени T rian gle. class Triangle : Shape, IPointy { public Triangle () { } public Triangle(string name) : base(name) { } public override void Draw() { Console.WnteLine ("Drawing {0} the Triangle", PetName) ; } Глава 9. Работа с интерфейсами // 327 Р еали зац и я и н тер ф ей са I P o in t y . public byte Points { get { return 3; } } } Модифицируем существующий тип Hexagon так, чтобы он тоже поддерживал интер­ фейс IPointy: / / H exagon те п е р ь р е а л и з у е т I P o in t y . class Hexagon : Shape, IPointy { public Hexagon () { } public Hexagon(string name) : base(name){ } public override void Draw() { Console.WriteLine("Drawing {0} the Hexagon", PetName); } } // Р еал и зац и я и н тер ф ей са I P o in t y . public byte Points { get { return 6; } } } Чтобы подвести итог всему изученному к этому моменту, на рис. 9.2 приведена соз­ данная с помощью Visual Studio 2010 диаграмма классов, на которой все совместимые с IPointy классы представлены с помощью обозначения в виде “леденца на палочке”. Обратите внимание на диаграмме, что в Circle и ThreeDCircle интерфейс IPointy не реализован, потому что предоставляемое им поведение для этих классов не имеет смысла. Рис. 9.2. Иерархия фигур, теперь с интерфейсами На заметку! Чтобы скрыть или отобразить имена интерфейсов в визуальном конструкторе классов, щелкните правой кнопкой мыши на значке, представляющем интерфейс, и выберите в контек­ стном меню пункт C o lla p se (Свернуть) или E xpand (Развернуть). 328 Часть III. Дополнительные конструкции программирования на C# Вызов членов интерфейса на уровне объектов Теперь, когда уже есть несколько классов, поддерживающих интерфейс IPointy, да­ вайте посмотрим, как взаимодействовать с новой функциональностью. Самый простой способ взаимодействия с функциональными возможностями, предлагаемыми заданным интерфейсом, предусматривает вызов его членов прямо на уровне объектов (при усло­ вии, что члены этого интерфейса не реализованы явным образом, о чем более подробно рассказывается в разделе “Устранение конфликтов на уровне имен за счет реализации интерфейсов явным образом” далее в главе). Например, рассмотрим следующий метод 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(itfPt.Points); } catch (InvalidCastException e) { Console.WriteLine(e.Message); } Console.ReadLine (); } Разумеется, можно использовать логику try/catch и надеяться на лучшее, но в идеале все-таки правильнее выяснять, какие интерфейсы поддерживаются, перед вы­ зовом их членов. Давайте рассмотрим два способа, которыми это можно делать. Глава 9. Работа с интерфейсами 329 Получение ссылок на интерфейсы с помощью ключевого слова a s Определить, поддерживает ли данный тип тот или иной интерфейс, можно с исполь­ зованием ключевого слова as, которое впервые рассматривалось в главе 6. Если объект удается интерпретировать как указанный интерфейс, то возвращается ссылка на инте­ ресующий интерфейс, а если нет, то ссылка n u ll. Следовательно, перед продолжением в коде необходимо предусмотреть проверку на n u ll: static void Main(string[] args) // Можно ли интерпретировать hex2 как I Pointy? Hexagon hex2 = new Hexagon("Peter"); IPointy itfPt2 = hex2 as IPointy; if (ltfPt2 != null) // Вывод числа вершин. Console.WriteLine ("Points: {0}", itfPt2.Points) ; else // Это не интерфейс IPointy. Console.WriteLine ("OOPS! Not pointy..."); Console.ReadLine (); Обратите внимание, что в случае применения ключевого слова as использовать л о ­ гику t r y / c a tc h нет никакой необходимости, поскольку возврат ссылки, отличной от n u ll, означает, что вызов осуществляется с использованием действительной ссылки на интерфейс. Получение ссылок на интерфейсы с помощью ключевого слова i s Проверить, был ли реализован нужный интерфейс, можно также с помощью клю­ чевого слова i s (которое тоже впервые упоминалось в главе 6). Если запрашиваемый объект не совместим с указанным интерфейсом, возвращается значение f a l s e , а если совместим, то можно спокойно вызывать члены этого интерфейса без применения л о ­ гики try / c a tc h . Для примера предположим, что имеется массив типов Shape, некоторые из членов которого реализуют интерфейс IP o in ty . Ниже показано, как можно определить, какие из элементов в этом массиве поддерживают данный интерфейс с помощью ключевого слова 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. Дополнительные конструкции программирования на C# // Это не интерфейс IPointy. Console.WriteLine ("-> {0}\'s not pointy!", myShapes[i].PetNamd); Console.WriteLine(); } Console.ReadLine(); Ниже приведен вывод, полученный в результате выполнения этого кода: ***** Fun with 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) та­ ким образом, чтобы они поддерживали это новое поведение: // C irc le поддерживает 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 .WnteLine ("-> Drawing IDraw3D compatible type"); itf3d.Qraw3D(); } Shape Abstract Class Рис. 9.3. Обновленная иерархия фигур Теперь можно проверить, поддерживает ли элемент в массиве 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. * * * * * Fun with Interfaces ***** Drawing NoName the Hexagon -> Points: 6 -> Drawing IDraw3D compatible type Drawing Hexagon in 3D! Drawing NoName the Circle -> NoName1s not pointy! Drawing Joe the Triangle -> Points: 3 Drawing JoJo the Circle -> JoJo 1s not pointy! 332 Часть III. Дополнительные конструкции программирования на C# Использование интерфейсов в качестве возвращаемых значений Интерфейсы можно также использовать и в качестве возвращаемых значений ме­ тодов. Для примера создадим метод, который принимает в качестве параметра массив объектов S y s te m .O b je c t и возвращает ссылку на первый элемент, поддерживающий интерфейс I P o in ty : // Этот метод возвращает первый объект в массиве, // который реализует интерфейс IPointy. { foreach (Shape s in shapes) { if (s is IPointy) return s as IPointy; } return null; } Взаимодействовать с этим методом можно следующим образом: static void Main(string[] args) { Console .WnteLine ("***** Fun with Interfaces *****\n"); // Создание массива объектов Shapes. Shape[] myShapes = { new Hexagon(), new Circle (), new Triangle("Joe"), new Circle("JoJo")}; // Получение первого элемента, который имеет вершины. // Ради безопасности не помешает предусмотреть проверку // firstPointyltern на предмет равенства null. IPointy firstPointyltem = FindFirstPointyShape(myShapes); // Вывод числа вершин. Console .WnteLine ("The item has {0} points", firstPointyltem. Points) ; Массивы типов интерфейсов Вспомните, что один и тот же интерфейс может быть реализован во множестве ти­ пов, даже если они находятся не в одной и той же иерархии классов и не имеют ника­ кого общего родительского класса, помимо System.Ob ject. Это позволяет формировать очень мощные программные конструкции. Например, давайте создадим в текущем про­ екте три новых типа класса, два из которых (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.", l.Points); Console.ReadLine() ; Shape Abstract Class ^ -4 IPointy IDrawBD 11' IPointy I Circle < Class ■♦Shape £ 1 Triangle Class ■♦ Shape ( if H exagon Class Shape J IPointy <y IPointy Fork Class (if K n ife Class 'y IPointy I P ftchFork Class Рис. 9.4. Вспомните, что интерфейсы могут “встраиваться" в любой тип внутри любой части иерархии классов Исходный код. Проект Customlnterface доступен в подкаталоге Chapter 9. Реализация интерфейсов с помощью Visual Studio 2010 Хотя программирование с применением интерфейсов и является очень мощной технологией, реализация интерфейсов может сопровождаться довольно приличным объемом ввода. Поскольку интерфейсы представляют собой именованные наборы аб­ страктных членов, для каждого метода интерфейса в каждом типе, который должен поддерживать такое поведение, требуется вводить и определение, и реализацию. К счастью, в Visual Studio 2010 поддерживаются различные инструменты, которые существенно упрощают процесс реализации интерфейсов. Для примера давайте вста­ вим в текущий проект еще один класс по имени PointyTestClass. При реализации для него интерфейса IPointy (или любого другого подходящего интерфейса) можно будет заметить, как по окончании ввода имени интерфейса (или при размещении на нем курсора мыши в окне кода) первая буква будет выделена подчеркиванием (или, согласно формальной терминологии, снабжена так называемой контекстной меткой — смарт-тегом (smart tag)). В результате щелчка на этом смарт-теге появится раскрываю­ щийся список с различными возможными вариантами реализации этого интерфейса (рис. 9.5). 334 Часть III. Дополнительные конструкции программирования на C# Рис. 9.5. Реализация интерфейсов в Visual Studio 2010 Обратите внимание, что в этом списке предлагаются два варианта, причем второй из них (реализация интерфейса явным образом) подробно рассматривается в следую­ щем разделе. Пока что выберем первый вариант. В этом случае Visual Studio 2010 сге­ нерирует (внутри именованного раздела кода) показанный ниже удобный для дальней­ шего обновления код-заглушку (обратите внимание, что в реализации по умолчанию предусмотрена генерация исключения S ystem .N otlm plem en tedE xception, что вполне можно удалить). namespace Customlnterfасе { class PointyTestClass : IPointy { #region IPointy Members public byte Points { get { throw new NotlmplementedException() ; } } #endregion } } На заметку! В Visual Studio 2010 также поддерживается опция рефакторинга типа выделения ин­ терфейса (Extract Interface), которая доступа в меню Refactoring (Рефакторинг). Она позволя­ ет извлекать определение нового интерфейса из существующего определения класса. Устранение конфликтов на уровне имен за счет реализации интерфейсов явным образом Как было показано ранее в главе, единственный класс или структура может реали­ зовать любое количество интерфейсов. Из-за этого всегда существует вероятность реа­ лизации интерфейсов с членами, имеющими идентичные имена, и, следовательно, воз­ никает необходимость в устранении конфликтов на уровне имен. Чтобы ознакомиться с различными способами решения этой проблемы, давайте создадим новый проект типа C o n so le A p p lic a tio n по имени In terfa c eN a m e C la sh и добавим в него три специальных интерфейса, представляющих различные места, в которых реализующий их тип может визуализировать свой вывод: Глава 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 .WnteLine ("Drawing the Octagon..."); } } Хотя компиляция этого кода пройдет гладко, одна возможная проблема в нем всетаки присутствует. Попросту говоря, предоставление единой реализации метода Draw () не позволяет предпринимать уникальные действия на основе того, какой интерфейс по­ лучен от объекта Octagon. Например, следующий код будет приводить к вызову одного и того же метода Draw (), какой бы интерфейс не был получен: static void Main(string[] args) { Console.WriteLine ("***** Fun with Interface Name Clashes *****\n"); //Во всех этих случаях будет вызываться один и тот же метод D ra w ()! 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. Дополнительные конструкции программирования на C# class Octagon : IDrawToForm, IDrawToMemory, IDrawToPnnter { // Явная привязка реализаций Draw() к конкретному интерфейсу. 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). Рис. 9.6. Реализуемые явным образом члены интерфейсов перестают быть доступными на уровне объектов Как и следовало ожидать, для получения доступа к необходимым функциональным возможностям в таком случае потребуется явное приведение, например: Глава 9. Работа с интерфейсами 337 static void Main(string [] args) { Console.WnteLine (''***** Fun with Interface Name Clashes *****\n"); Octagon oct = new Octagon (); // Теперь для получения доступа к членам Draw() // должно использоваться приведение. IDrawToForm itfForm = (IDrawToForm)oct; itfForm.Draw(); // Сокращенная версия на случай, если переменную интерфейса //н е планируется использовать позже. ((IDrawToPrinter)oct) .Draw(); // Можно было бы также использовать ключевое слово аз. if (oct is IDrawToMemory) ((IDrawToMemory)oct).Draw(); Console.ReadLine (); } Хотя такой синтаксис довольно полезен, когда необходимо устранить конфликты на уровне имен, приемом явной реализации интерфейсов можно пользоваться и просто для сокрытия более “сложных” членов на уровне объектов. В таком случае при примене­ нии операции точки пользователь объекта будет видеть только некоторую часть общей функциональности типа. Те же пользователи, которым необходим доступ к более слож­ ным поведениям, все равно смогут получать их из желаемого интерфейса через явное приведение. Исходный код. Проект InterfaceNameClash доступен в подкаталоге Chapter 9. Проектирование иерархий интерфейсов Интерфейсы могут быть организованы в иерархии. Как и в иерархии классов, в ие­ рархии интерфейсов, когда какой-то интерфейс расширяет существующий, он насле­ дует все абстрактные члены своего родителя (или родителей). Конечно, в отличие от классов, производные интерфейсы никогда не наследуют саму реализацию. Вместо это­ го они просто расширяют собственное определение за счет добавления дополнительных абстрактных членов. Использовать иерархию интерфейсов может быть удобно, когда нужно расширить функциональность определенного интерфейса без нарушения уже существующих ко­ довых баз. Для примера создадим новый проект типа C o n s o le A p p lic a tio n по имени InterfaceHierarchy и добавим в него новый набор отвечающих за визуализацию ин­ терфейсов так, чтобы 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. Дополнительные конструкции программирования на C# При таком дизайне для реализации интерфейса IAdvancedDraw в классе потребует­ ся реализовать каждый из определенных в цепочке наследования членов (те. Draw (), DrawInBoundingBox () и DrawUpsideDown()): public class Bitmaplmage : 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 down1"); } } Теперь при использовании Bitmaplmage можно вызывать каждый метод на уровне объекта (поскольку все они являются общедоступными), а также извлекать ссылку на каждый поддерживаемый интерфейс явным образом с помощью приведения: static void Main(string [] args) { Console.WriteLine ("*****Simple Interface Hierarchy *****"); // Выполнение вызова на уровне объекта. Bitmaplmage myBitmap = new Bitmaplmage(); myBitmap.Draw(); myBitmap.DrawInBoundingBox(10, 10, 100, 150); myBitmap.DrawUpsideDown () ; // Получение IAdvancedDraw юным образом. IAdvancedDraw iAdvDraw; iAdvDraw = (IAdvancedDraw)myBitmap; iAdvDraw.DrawUpsideDown(); Console.ReadLine(); } Исходный код. Проект InterfaceHierarchy доступен в подкаталоге Chapter 9. Множественное наследование в случае типов интерфейсов В отличие от классов, один интерфейс может расширять сразу несколько базовых ин­ терфейсов, что позволяет проектировать очень мощные и гибкие абстракции. Для при­ мера создадим новый проект типа C o n so le A p p lic a tio n по имени MI InterfaceHierarchy и добавим в него еще одну коллекцию интерфейсов, моделирующих различные свя­ занные с визуализацией и фигурами абстракции. Обратите внимание, что интерфейс IShape в этой коллекции расширяет оба интерфейса IDrawable и IPrintable. // Множественное наследование в случае типов // интерфейсов является вполне допустимым. interface IDrawable { void Draw(); Глава 9. Работа с интерфейсами 339 interface IPrintable { void Print(); void DrawO; // <-- Здесь возможен конфликт имен! } // Множественное наследование интерфейса. Все в порядке! public interface IShape : IDrawable, IPrintable { int GetNumberOfSides (); } На рис. 9.7 показано, как теперь выглядит иерархия интерфейсов. Рис. 9.7. В отличие от классов, интерфейсы могут расширять • сразу несколько базовых интерфейсов Теперь главный вопрос состоит в том, сколько методов потребуется реализовать при создании класса, поддерживающего IShape? Ответ будет несколько. Если нужно пре­ доставить простую реализацию метода Draw ( ) , понадобится только реализовать три его члена, как показано ниже на примере типа R ec ta n g le: 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. Дополнительные конструкции программирования на C# public void Print () { // Печать ... } public int GetNumberOfSides () { return 4; } К этому моменту процесс определения и реализации специальных интерфейсов на C# должен стать более понятным. По правде говоря, на привыкание к программирова­ нию с применением интерфейсов может уйти некоторое время. Однако уже сейчас важно уяснить, что интерфейсы являются фундаментальным компонентом .NET Framework. Какого бы типа приложение не разрабатывалось (веб­ приложение, приложение с настольным графическим интерфейсом, библиотека досту­ па к данными и т.п.), работа с интерфейсами будет обязательной частью этого процесса. Подводя итог всему изложенному, отметим, что интерфейсы могут приносить чрезвы­ чайную пользу в следующих случаях. • При наличии единой иерархии, в которой только какой-то набор производных ти­ пов поддерживает общее поведение. • При необходимости моделировать общее поведение, которое должно встречать­ ся в нескольких иерархиях, не имеющих общего родительского класса помимо S ystem .O b ject. Теперь, когда мы разобрались со специфическими деталями построения и реализа­ ции специальных интерфейсов, необходимо посмотреть, какие стандартные интерфей­ сы предлагаются в библиотеках базовых классов .NET. Исходный код. Проект MI I n t e r fa c e H ie r a r c h y доступен в подкаталоге Chapter 9. Создание перечислимых типов (IEnumerable и IEnumerator) Прежде чем приступать к исследованию процесса реализации существующих ин­ терфейсов .NET, давайте сначала рассмотрим роль типов IEnumerable и IEnumerator. Вспомните, что в C# поддерживается ключевое слово f o r each, которое позволяет осу­ ществлять проход по содержимому массива любого типа: // Итерация по массиву элементов. int [] myArrayOf Ints = {10, 20, 30, 40}; foreach(int i in myArrayOfInts) { Console.WnteLine (i) ; } Хотя может показаться, что данная конструкция подходит только для массивов, на самом деле с ее помощью можно анализировать любой тип, который поддерживает ме­ тод GetEnumerator (). Для примера создадим новый проект типа C o n so le A p p lic a tio n по имени CustomEnumerator и добавим в него файлы C a r.c s и R a d io .c s , которые были определены в примере S im pleE xception в главе 7 (выбрав в меню P ro je ct (Проект) пункт A d d E xistin g Item (Добавить существующий элемент)). На заметку! Может возникнуть желание переименовать содержащее типы Саг и Radio простран­ ство имен в CustomEnumerator, чтобы не импортировать в новый проект пространство имен Custom Exception. Глава 9. Работа с интерфейсами 341 Вставим новый класс Garage (гараж), обеспечивающий сохранение ряда объектов Саг (автомобиль) внутри S ystem .A rray: // Garage содержит набор объектов Саг. public class Garage { private Car [] carArray = new Car [4]; // Заполнение какими-то объектами Car при запуске. public Garage() { carArray[0] carArray[l] carArray[2] carArray[3] = = = = new new new new Car("Rusty", 30); Car ("Clunker", 55); Car("Zippy", 30); Car("Fred", 30); В идеале удобно было бы осуществлять проход по элементам объекта Garage как по массиву значений данных с применением конструкции fo rea ch : // Такой вариант кажется целесообразным. . . public class Program { static void Main(string [] args) { Console .WnteLine ("***** Fun with IEnumerable / IEnumerator *****\n"); Garage carLot = new Garage (); // Проход по всем объектам Car в коллекции? foreach (Car c in carLot) { Console .WnteLine ("{ 0 } is going {1} MPH", c.PetName, c. Speed); } Console.ReadLine (); } К сожалению, в этом случае компилятор сообщит, что в классе Garage не реализован метод GetEnumerator (). Этот метод формально определен в интерфейсе IEnumerable, который находится глубоко внутри пространства имен S y s te m .C o lle c tio n s . На заметку! В следующей главе будет рассказываться о роли обобщений и пространства имен System. C o l l e c t i o n s . G en eric. В этом пространстве имен содержатся обобщенные вер­ сии Enumerable и IEnum erator, которые предоставляют более безопасный в отношении типов способ для реализации прохода по подобъектам. Классы или структуры, которые поддерживают такое поведение, позиционируются как способные предоставлять содержащиеся внутри них подэлементы вызывающему коду (в данном примере это само ключевое слово fo rea ch ): // Этот интерфейс информирует вызывающий код о том, // что подэлементы объекта могут перечисляться. public interface IEnumerable { IEnumerator GetEnumerator(); } Метод GetEnumerator () возвращает ссылку на еще один интерфейс под названи­ ем System. C o lle c t io n s . IEnumerator. Этот интерфейс обеспечивает инфраструктуру, 342 Часть III. Дополнительные конструкции программирования на C# позволяющую вызывающему коду проходить по внутренним объектам, которые содер­ жатся в совместимом с I Enumerable контейнере: // Этот интерфейс позволяет вызывающему коду получать // содержащиеся в контейнере внутренние подэлементы. public interface IEnumerator { bool MoveNextO; object Current { get;} void FesetO; // // // // Перемещение внутренней позиции курсора, Извлечение текущего элемента (свойство, доступное только для чтения). Сброс курсора перед первым членом. При модификации типа 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]; carArray[0] = new C a r ("FeeFee", 200, 0) ; carArray[1] = new Car ("Clunker", 90, 0) ; carArray[2] = new Car ("Zippy", 30, 0) ; carArray[3] = new Ca r ("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 После этого обычный пользователь объекта не будет видеть метода Get Enumerator () в Garage, хотя конструкция foreach будет все равно получать интерфейс незаметным образом, когда это необходимо. Исходный код. Проект CustomEnumerator доступен в подкаталоге Chapter 9. Создание методов итератора с помощью ключевого слова y i e l d Раньше, когда требовалось создать специальную коллекцию (вроде Garage), способ­ ную поддерживать перечисление элементов посредством конструкции foreach, реали­ зация интерфейса IEnumerable (и возможно IEnumerator) была единственным доступ­ ным вариантом. Потом, однако, появился альтернативный способ для создания типов, способных работать с циклами foreach, который предусматривает использование так называемых итераторов (iterator). Попросту говоря, итератором называется такой член, который указывает, каким об­ разом должны возвращаться внутренние элементы контейнера при обработке в цикле foreach. Хотя метод итератора должен все равно носить имя GetEnumerator (), а его возвращаемое значение относиться к типу IEnumerator, необходимость в реализации каких-либо ожидаемых интерфейсов в специальном классе при таком подходе отпадает. Для примера давайте создадим новый проект типа C o n s o le A p p lic a tio n по имени CustomEnumeratorWithYield и вставим в него типы Car, Radio и Garage из предыду­ щего примера (при желании переименовав пространство имен в соответствии с теку­ щим проектом), после чего модифицируем тип Garage показанным ниже образом: public class Garage { private C ar[] carArray = new Car[4]; // Метод итератора. public IEnumerator GetEnumerator () { foreach (Car c in carArray) { yield return c; } Обратите внимание, что в данной реализации GetEnumerator () проход по подэле­ ментам осуществляется с использованием внутренней логики foreach, а каждый объ­ ект Саг возвращается вызывающему коду с применением синтаксиса yield return. Ключевое слово yield служит для указания значения или значений, которые должны возвращаться конструкции foreach в вызывающем коде. При достижении оператора yield return производится сохранение текущего местоположении в контейнере, и при следующем вызове итератора выполнение начинается уже с этого места (детали будут описаны позже). Использовать ключевое слово foreach в методах итераторов для возврата содержи­ мого не требуется. Метод итератора можно также определять следующим образом: public IEnumerator GetEnumerator () { yield yield yield yield } return return return return carArray[0]; carArray[1]; carArray[2]; carArray[3]; 344 Часть III. Дополнительные конструкции программирования на C# В этой реализации важно обратить внимание, что метод GetEnum erator () явным образом возвращает вызывающему коду новое значение после каждого прогона. В дан­ ном примере подобный подход не особо удобен, поскольку в случае добавления больше­ го числа объектов в переменную экземпляра с а гА гга у метод GetEnumerator () вышел бы из-под контроля. Тем не менее, такой синтаксис все-таки может быть довольно по­ лезным, когда необходимо возвращать из метода локальные данные для последующей обработки внутри fo re a c h . Создание именованного итератора Еще интересен тот факт, что ключевое слово y i e l d формально может применяться внутри любого метода, как бы ни выглядело его имя. Такие методы (называемые име­ нованными итераторами) уникальны тем, что могут принимать любое количество ар­ гументов. При создании именованного итератора необходимо очень хорошо понимать, что метод будет возвращать интерфейс I Enumerable, а не ожидаемый совместимый с IEnum erator тип. Для примера добавим к типу 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 с; } Обратите внимание, что добавленный новый метод позволяет вызывающему коду получать подэлементы как в прямом, так и в обратном порядке, если во входном пара­ метре передается значение tru e . Теперь с ним можно взаимодействовать следующим образом: static void Main(string [] args) { Console .WnteLine ("***** Fun with the Yield Keyword *****\n") ; Garage carLot = new Garage(); // Получение элементов с помощью GetEnumerator() . foreach (Car c in carLot) { Console.WriteLine("{0} is going {1} MPH", c.PetName, c .CurrentSpeed); Console.WriteLine (); // Получение элементов (в обратном порядке) / / с помощью именованного итератора. Глава 9. Работа с интерфейсами 345 foreach (Car с in carLot.GetTheCars(true)) { Console .WnteLine ("{ 0 } is going {1} MPH", c.PetName, c .CurrentSpeed) ; } Console.ReadLine(); } Нельзя не согласиться с тем, что именованные итераторы представляют собой очень полезные конструкции, поскольку позволяют определять в единственном специальном контейнере сразу несколько способов для запрашивания возвращаемого набора. Внутреннее представление метода итератора Столкнувшись с методом итератора, компилятор C# динамически генерирует внут­ ри соответствующего типа (в данном случае Garage) определение вложенного класса. В этом сгенерированном вложенном классе, в свою очередь, автоматически реализуют­ ся такие члены, как GetEnumerator () ,MoveNext () и Current (но, как ни странно, не метод Reset ( ) , и при попытке его вызвать возникает исключение времени выполнения). Если загрузить текущее приложение в утилиту ildasm.exe, можно обнаружить в нем два вложенных типа, в каждом из которых будет содержаться логика, необходимая для конкретного метода итератора. На рис. 9.8 видно, что эти сгенерированные автомати­ чески компилятором типы имеют имена <GetEnumerator>d__0 и <GetTheCars>d__6. / / Н Л М у Books'уС * B o c k \C * and th e NET P latfo rm 5th e d First D ra ft\C h a ~ Eile у lew H elp H:\My Books\C# Book\C# and the NET Platform 5th ed\Frst Draft\Chapter _09\Code\CustomEnur ► MANIFEST W CustomEnumeratorWithYield * 0 £ CustomEnumeratcr WthYierfd.C a CustomEnumeratorWithYield. Garage ► .class public auto ansi beforefleldinit ► implements [mscorlb]System Colections IEnumerable * M .ДЖ М Е it £ <GetTheCars>d__6 . ■ car Array : private class CustomEnumeratorWithYield. Car[] ■ ctor : void() ■ GetEnumerator class [msccrlib]5ystem. Colections. IEnumeratcr() ■ GetTheCars class [mscorib]5ystem. Colections IEnumerabte(bool) * B : CustomEnumeratorWithYield.Program * B : CustomEnumeratorWithYield.Radio assembly CustomEnumerato'WithYield i Рис. 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(int 32) } // end of method Garage::GetEnumerator 346 Часть III. Дополнительные конструкции программирования на C# В завершение темы построения перечислимых объектов запомните, что для того, чтобы специальные типы могли работать с ключевым словом foreach, в контейнере должен обязательно присутствовать метод по имени GetEnumeratorO , который уже был формально определен типом интерфейса IEnumerable. Реализация данного метода обычно осуществляется за счет делегирования внутреннему члену, который отвечает за хранение подобъектов; однако можно также использовать синтаксис yield return и предоставлять с его помощью множество методов “именованных итераторов”. Исходный код. Проект CustomEnumeratorWithYield доступен в подкаталоге Chapter 9. Создание клонируемых объектов ( ic io n e a b le ) Как уже рассказывалось в главе 6, в System.Ob j ect имеется член по имени MemberwiseClone ( ) . Он представляет собой метод и позволяет получить поверхност­ ную копию (shallow сору) текущего объекта. Пользователи объекта не могут вызывать этот метод напрямую, поскольку он является защищенным, но сам объект вполне мо­ жет это делать во время так называемого процесса клонирования. Для примера давайте создадим новый проект типа 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 () {} // Переопределение O bject.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(50, 50); Point р2 = pi; р2 .X = 0; Console.WriteLine(pi); Console.WriteLine(p2); Console.ReadLine(); } Глава 9. Работа с интерфейсами 347 Чтобы обеспечить специальный тип способностью возвращать идентичную ко­ пию самого себя вызывающему коду, можно реализовать стандартный интерфейс IC lo n e a b le . Как уже показывалось в начале настоящей главы, этот интерфейс имеет единственный метод по имени Clone () : public interface ICloneable { object Clone (); На заметку! По поводу полезности интерфейса IC lo n e a b le в сообществе .NET ведутся горячие споры. Проблема связана с тем, что в формальной спецификации никак явно не говорится о том, что объекты, реализующие данный интерфейс, должны обязательно возвращать деталь­ ную копию (deep сору) объекта (т.е. внутренние ссылочные типы объекта должны приводить к созданию совершенно новых объектов с идентичным состоянием). Из-за этого с технической точки зрения возможно, что объекты, реализующие IC lo n e a b le , на самом деле будут воз­ вращать поверхностную копию интерфейса (т.е. внутренние ссылки будут указывать на один и тот же объект в куче), что вызывает приличную путаницу. В рассматриваемом примере пред­ полагается, что метод Clone () должен быть реализован так, чтобы возвращать полную, де­ тальную копию объекта. Разумеется, реализация метода Clone () в различных объектах может выглядеть поразному. Однако базовая функциональность обычно остается неизменной и заключает­ ся в копировании переменных экземпляра в новый экземпляр объекта того же типа и в возврате его пользователю. Для примера изменим класс P o in t следующим образом: // Класс 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 () { } // Переопределение Obj ect.ToStnng () . public override string ToStnng () { return string.Format("X = {0}; Y = {1}", X, Y ) ; } // Возврат копии текущего объекта, public object Clone () { return new Point(this.X, this.Y); } } Теперь можно создавать точные автономные копии типа P o in t так, как показано ниже: static void Main(string[] args) { Console.WriteLine ("***** Fun with Object Cloning *****\n"); // Обратите внимание, что C lone() возвращает // простой тип объекта. Для получения производного // типа требуется явное приведение. Point рЗ = new Point (100, 100) ; Point р4 = (Point)р З .Clone() ; // Изменение р4.Х (которое не приводит к изменению рЗ.х) . р 4 .X = 0; 348 Часть III. Дополнительные конструкции программирования на C# // Вывод объектов на консоль. Console.WriteLine (рЗ); Console.WriteLine (р 4); Console.ReadLine (); } Хотя текущая реализация Point отвечает всем требованиям, ее все равно можно немного улучшить. В частности, поскольку Point не содержит никаких внутренних пе­ ременных ссылочного типа, реализацию метода Clone () можно упростить следующим образом: p u b lic object C lone() { // Копируем каждое поле 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.NewGuid (); ' Как здесь видно, для начала был модифицирован сам класс Point, чтобы его метод ToString () принимал во внимание подобные новые фрагменты данных о состоянии. Кроме того, был определен и создан ссылочный тип 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 Point () { } // Переопределение Object.ToStnng () . public override string ToStnng () { 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(100, 100, "Jane") ; Point p4 = (Point)p3.Clone () ; //До изменения. Console.WriteLine("Before modification:"); Console.WriteLine("p3: {0}", p3) ; Console.WriteLine ("p4: {0}", p4); p 4 .desc.PetName = "My new Point; p 4 .X = 9; // После изменения. Console.WriteLine("\nChanged p 4 .desc.petName and p4.X"); Console.WriteLine("After modification:"); Console.WriteLine("p3: {0}", p3) ; Console.WriteLine ("p4: {0}", p4); Console.ReadLine(); } sa Ниже показано, как будет выглядеть вывод в таком случае. Обратите внимание, что хотя типы значения на самом деле изменились, у внутренних ссылочных типов остались же самые значения, поскольку они “указывают” на одинаковые объекты в памяти, частности, дружественное имя у обоих объектов сейчас выглядит как Му new Point.). ***** Fun with Object Cloning ***** Cloned p3 and stored new Point in p4 Before modification: рЗ: X = 100; Y = 100; Name = Jane; ID = 133d66a7-0 837-4bd7-95c6-b22ab0 434 50 9 p4 : X = 100; Y = 100; Name = Jane; ID = 133d66a7-0 837-4bd7-95c6-b22ab0 434 50 9 Changed p 4 .desc.petName and p4.X After modification: рЗ: 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. Дополнительные конструкции программирования на C# Для того чтобы метод Clone () создавал полную детальную копию внутренних ссы­ лочных типов, необходимо настроить возвращаемый методом Memberwise Сlon e () объ­ ект, чтобы он принимал во внимание имя текущего объекта P o in t (тип System. Guid на самом деле представляет собой структуру, поэтому в действительности числовые дан­ ные будут копироваться). Ниже показан один из возможных вариантов реализации. // Теперь необходимо подстроить код таким образом, чтобы // в нем принимался во внимание член P ointD escription. 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 теперь стали уникальными). ***** Fun with Object Cloning ***** Cloned рЗ and stored new Point in p4 Before modification; рЗ: X = 10 0; Y = 100; Name = Jane; ID = 5 If 64 f25-4b0e-4 7ac-ba35-37d2634 964 0 6 p4: X = 100; Y = 100; Name = Jane; ID = 0d377 6b3-bl5 9-4 90d-b022-7 f3f 607 88e8a Changed p 4 .desc.petName and p4.X After modification: р З : X = 100; Y = 100; Name = Jane; ID = 51f64f25-4b0e-47ac-ba35-37d2 634 9640 6 p4: X = 9; Y = 100; Name = My new Point; ID = 0d377 6b3-bl5 9-4 90d-b022-7 f3f 607 88e8a Подведем итог по процессу клонирования. При наличии класса или структуры, в которой не содержится ничего кроме типов значения, достаточно реализовать метод Clone () с использованием MemberwiseClone ( ) . Однако если есть специальный тип, поддерживающий другие ссылочные типы, необходимо создать новый объект, прини­ мающий во внимание каждую из переменных экземпляра ссылочного типа. Исходный код. Проект CloneablePoint доступен в подкаталоге Chapter 9. Создание сравнимых объектов (iComparable) Интерфейс System. IComparable обеспечивает поведение, которое позволяет сорти­ ровать объект на основе какого-то указанного ключа. Формально его определение вы­ глядит так: // Этот интерфейс позволяет объекту указывать // его отношения с другими подобными объектами, public interface IComparable { int CompareTo(object o) ; Глава 9. Работа с интерфейсами 351 На заметку! В обобщенной версии этого интерфейса (1СошрагаЫе<т>) предлагается более безопасный в отношении типов способ для обработки сравнений объектов. Обобщения будут более подробно рассматриваться в главе 10. Д ля примера создадим новый проект типа C o n s o le A p p lic a tio n по имени ComparableCar и вставим в него следующую обновленную версию класса Саг (обратите внимание, что здесь просто добавлено новое свойство для представления уникального идентификатора каждого автомобиля и модифицированный конструктор): public class Car public int CarlD {get; set;} public Car(string name, int currSp, int id) { CurrentSpeed = currSp; PetName = name; CarID = id; } Теперь создадим массив объектов Саг, как показано ниже: 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 [1] = new Car ("Mary", 40, 234); myAutos[2] = new Ca r ("Viper", 40, 34); myAutos [3] = new Car ("Mel", 40, 4); myAutos[4] = new Ca r ("Chucky", 40, 5); Console.ReadLine(); } В классе System. A rray определен статический метод S ort () . При вызове этого ме­ тода на массиве внутренних типов (in t, sh ort, s t r in g и т.д.) элементы массива могут сортироваться в числовом или алфавитном порядке, поскольку эти внутренние типы данных реализуют интерфейс I Comparable. Однако что будет происходить в случае пе­ редачи методу S ort () массива типов Саг, как показано ниже? // Будет ли выполняться сортировка автомобилей? Array.Sort(myAutos) ; В случае выполнения этого тестового кода в исполняющей среде будет возникать исключение, потому что в классе Саг необходимый интерфейс не поддерживается. При создании специальных типов для обеспечения возможности сортировки массивов, которые содержат элементы этих типов, можно реализовать интерфейс IComparable. При реализации деталей CompareTo () решение о том, что должно брать­ ся за основу в операции упорядочивания, необходимо принимать самостоятельно. Для рассматриваемого типа Саг с логической точки зрения наиболее подходящим на эту роль “кандидатом” является внутренняя переменная CarlD: // Упорядочивание элементов при итерации Саг // может производиться на основе CarlD. public class Car : IComparable { 352 Часть III. Дополнительные конструкции программирования на C# // Реализация 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!"); // Параметр не является объектом типа Саг! Как здесь показано, логика CompareTo () состоит в сравнении входного объекта с текущим экземпляром по конкретному элементу данных. Возвращаемое значение CompareTo () служит для выяснения того, является данный объект меньше, больше или равным объекту, с которым он сравнивается (табл. 9.1). Таблица 9.1. Значения, которые может возвращать CompareTo () Возвращаемое значение Описание Любое число меньше нуля Обозначает, что данный экземпляр находится перед указанным объектом в порядке сортировки Нуль Обозначает,что данный экземпляр равен указанному объекту Любое число больше нуля Обозначает, что данный экземпляр находится после указанного объекта в порядке сортировки Предыдущую реализацию CompareTo () можно упростить, благодаря тому, что в C# тип данных m t (который представляет собой сокращенный вариант обозначения типа System. Int32 в CLR) реализует интерфейс IComparable. Реализовать CompareTo () в Саг можно следующим образом: int IComparable.CompareTo (object obj) Car temp = obj as Car; if (temp != null) return this.CarlD.CompareTo(temp.CarID); else throw new ArgumentException("Parameter is not a Car!"); // Параметр не является объектом типа Саг' } Далее в обоих случаях, чтобы тип Саг понимал, каким образом сравнивать себя с подобными объектами, можно написать следующий код: // Использование интерфейса IComparable. static void Main (string [] args) { // Создание массива объектов Car. Глава 9. Работа с интерфейсами 353 // Отображение текущего массива. Console.WnteLine ("Here is the unordered set of cars:") ; foreach(Car c in myAutos) Console.WriteLine("{0} {1}", c.CarlD, c.PetName); // Сортировка массива с применением интерфейса ХСохорагаЫе. Array.Sort(myAutos); // Отображение отсортированного массива. Console.WnteLine ("Here is the ordered set of cars:") ; foreach(Car c 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 Chucky Hete is the ordered set of cars: 1 Rusty 4 Mel 5 Chucky 34 Viper 234 Mary Указание множества критериев для сортировки ( iC o m p a r e r ) В предыдущей версии класса Саг в качестве основы для порядка сортировки ис­ пользовался идентификатор автомобиля (ca rlD ). В другой версии для этого могло бы применяться дружественное название автомобиля (для перечисления автомобилей в а л­ фавитном порядке). А что если возникнет желание создать класс Саг, способный про­ изводить сортировку и по идентификатору, и по дружественному названию? Для этого должен использоваться другой стандартный интерфейс по имени IComparer, который поставляется в пространстве имен System. C o lle c t io n s и определение которого выгля­ дит следующим образом: // Общий способ для сравнения двух объектов, interface IComparer { int Compare(object ol, object o2) ; На заметку! В обобщенной версии этого интерфейса (IC om p a rere< T > ) предлагается более безопасный в отношении типов способ для обработки сравнений между объектами. Обобщения более подробно рассматриваются в главе 10. В отличие от ICom parable, интерфейс ICom parer обычно реализуется не в самом подлежащем сортировке типе (Саг в рассматриваемом случае), а в наборе соответст­ вующих вспомогательных классов, по одному для каждого порядка сортировки (по дружественному названию, идентификатору и т.д.). В настоящее время типу Саг (ав­ томобиль) уже “известно”, как ему следует сравнивать себя с другими автомобилями по 354 Часть III. Дополнительные конструкции программирования на C# внутреннему идентификатору. Следовательно, чтобы позволить пользователю объекта производить сортировку массива объектов Саг еще и по значению petName, понадо­ бится создать дополнительный вспомогательный класс, реализующий IComparer. Ниже показан весь необходимый для этого код (перед его использованием важно не забыть импортировать в файл кода пространство имен S y s te m .C o lle c tio n s ). // Этот вспомогательный класс предназначен для // обеспечения возможности сортировки массива // объектов Саг по дружественному названию. public class PetNameComparer : IComparer { // Проверка дружественного названия каждого объекта. 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!"); // Параметр не является объектом типа Саг1 } } Теперь можно использовать этот вспомогательный класс в коде. В S y stem .A rra y имеется набор перегруженных версий метода S o rt ( ) , в одной из которых принимается в качестве параметра объект, реализующий интерфейс IComparer. static void Main(string[] args) { // Теперь выполнение сортировки по дружественному названию. Array.Sort(myAutos, new PetNameComparer()); // Вывод отсортированного массива в окне консоли. Console.WnteLine ("Ordering by pet name:"); foreach(Car c in myAutos) Console .WnteLine (" {0 } {!}", 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, C ar. SortByPetName); Исходный код. Проект ComparableCar доступен в подкаталоге Chapter 9. К этому моменту должны быть понятны не только способы определения и реали­ зации собственных интерфейсов, но и то, какую пользу они могут приносить. Следует отметить, что интерфейсы встречаются в каждом крупном пространстве имен .NET, и в остальной части книги придется неоднократно иметь дело с различными стандартны­ ми интерфейсами. Резюме Интерфейс может быть определен как именованная коллекция абстрактных членов. Из-за того, что никаких касающихся реализации деталей в интерфейсе не предоставля­ ется, интерфейс часто рассматривается как поведение, которое может поддерживаться тем или иным типом. В случае, когда один и тот же интерфейс реализуют два или более класса, каждый из типов может обрабатываться одинаково (что называется обеспече­ нием полиморфизма на основе интерфейса), даже если эти типы расположены в разных иерархиях классов. Для определения новых интерфейсов в C# предусмотрено ключевое слово interface. Как было показано в этой главе, в любом типе может обеспечиваться поддержка для лю ­ бого количества интерфейсов за счет предоставления соответствующего разделенного запятыми списка их имен. Более того, также допускается создавать интерфейсы, унас­ ледованные от нескольких базовых интерфейсов. Помимо возможности создания специальных интерфейсов, в библиотеках .NET пред­ лагается набор стандартных (поставляемых вместе с платформой) интерфейсов. Как было показано в этой главе, можно создавать специальные типы, реализующие пре­ допределенные интерфейсы, и получать доступ к желаемым возможностям, таким как клонирование, сортировка и перечисление. ГЛАВА 10 Обобщения амым элементарным контейнером на платформе .NET является тип System . A rray. Как было показано в главе 4, массивы C# позволяют определять наборы типизированных элементов (включая массив объектов типа S y stem .O b ject, по сути представляющий собой массив любых типов) с фиксированным верхним пределом. Хотя базовые массивы могут быть удобны для управления небольшими объемами известных данных, бывает ташке немало случаев, когда требуются более гибкие структуры дан­ ных, такие как динамически растущие и сокращающиеся контейнеры или контейне­ ры, которые хранят только элементы, отвечающие определенному критерию (например, элементы, унаследованные от заданного базового класса, реализующие определенный интерфейс или что-то подобное). После появления первого выпуска платформы .NET программисты часто исполь­ зовали пространство имен S y s te m .C o lle c tio n s для получения более гибкого способа управления данными в приложениях. Однако, начиная с версии .NET 2.0, язык про­ граммирования C# был расширен поддержкой средства, которое называется обобщени­ ем (generic). Вместе с ним библиотеки базовых классов пополнились совершенно новым пространством имен, связанным с коллекциями — S y s te m .C o lle c tio n s .G en eric. Как будет показано в настоящей главе, обобщенные контейнеры во многих отноше­ ниях превосходят свои необобщенные аналоги, обеспечивая высочайшую безопасность типов и выигрыш в производительности. После общего знакомства с обобщениями бу­ дут описаны часто используемые классы и интерфейсы из пространства имен System. C o lle c t io n s .G e n e r ic . В оставшейся части этой главы будет показано, как строить соб­ ственные обобщенные типы. Вы также узнаете о роли ограничений (constraint), которые позволяют строить контейнеры, исключительно безопасные в отношении типов. С На заметку! Можно также создавать обобщенные типы делегатов; об этом пойдет речь в следую­ щей главе. Проблемы, связанные с необобщенными коллекциями С момента появления платформы .NET программисты часто использовали простран­ ство имен S y s te m .C o lle c ito n s из сборки m s c o r lib .d ll. Здесь разработчики платфор­ мы предоставили набор классов, позволявших управлять и организовывать большие объемы данных. В табл. 10.1 документированы некоторые наиболее часто используе­ мые классы коллекций, а также основные интерфейсы, которые они реализуют. Глава 10. Обобщения 357 Таблица 10.1. Часто используемые классы из S y s t e m . C o l l e c t i o n s Основные реализуемые интерфейсы Класс Назначение ArrayList Представляет коллекцию динамически из­ меняемого размера, содержащую объекты в определенном порядке IList, ICollection, IEnumerable и ICloneable Hashtable Представляет коллекцию пар “ключ/значение” , организованных на основе хеш-кода ключа IDictionary, ICollection, IEnumerable и ICloneable Queue Представляет стандартную очередь, рабо­ тающую по алгоритму FIFO (“первый во­ шел — первый вышел” ) и ICloneable SortedList Представляет коллекцию пар “ключ/значение” , отсортированных по ключу и доступных по ключу и по индексу Stack Представляет стек LIFO (“последний во­ шел — первый вышел” ), поддерживающий функциональность заталкивания (push) и вы­ талкивания (pop), а также считывания (реек) ICollection, IEnumerable IDictionary, Icollection, IEnumerable и ICloneable ICollection, IEnumerable и ICloneable Интерфейсы, реализованные этими базовыми классами коллекций, представля­ ют огромное “окно” в их общую функциональность. В табл. 10.2 представлено опи­ сание общей природы этих основных интерфейсов, часть из которых поверхностно рассматривалась в главе 9. Таблица 10.2. Основные интерфейсы, поддерживаемые классами S y s t e m .C o lle c t io n s Интерфейс Назначение ICollection Определяет общие характеристики (т.е. размер, перечисление и безопас­ ность к потокам) всех необобщенных типов коллекций ICloneable Позволяет реализующему объекту возвращать копию самого себя вызываю­ щему коду IDictionary Позволяет объекту необобщенной коллекции представлять свое содержимое в виде пар “имя/значение” IEnumerable Возвращает объект, реализующий интерфейс IEnumerator (см. следующую строку в таблице) IEnumerator Позволяет итерацию в стиле foreach по элементам коллекции IList Обеспечивает поведение добавления, удаления и индексирования элементов в списке объектов В дополнение к этим базовым классам (и интерфейсам) добавляю тся н еск оль­ ко специализированных типов коллекций, таких как BitVector32, ListDictionary, StringDictionary и StringCollection, определенных в пространстве имен System. Collections.Specialized из сборки System.dll. Это пространство имен также со­ держит множество дополнительных интерфейсов и абстрактных классов, которые можно использовать в качестве отправной точки при создании специальных классов коллекций. 358 Часть III. Дополнительные конструкции программирования на C# Хотя за последние годы с применением этих “классических” классов коллекций (и интерфейсов) было построено немало успешных приложений .NET, опыт показал, что применение этих типов может быть сопряжено с множеством проблем. Первая проблема состоит в том, что использование классов коллекций System. Collections и System.Collections.Specialized приводит к созданию низкопроизво­ дительного кода, особенно если осуществляются манипуляции со структурами данных (т.е. типами значения). Как вскоре будет показано, при сохранении структур в класси­ ческих классах коллекций среде CLR приходится выполнять массу операций перемеще­ ния данных в памяти, что может значительно снизить скорость выполнения. Вторая проблема связана с тем, что эти классические классы коллекций не явля­ ются безопасными к типам, так как они (по большей части) были созданы для работы с System.Object и потому могут содержать в себе все что угодно. Если разработчику .NET требовалось создать безопасную в отношении типов коллекцию (т.е. контейнер, который может содержать только объекты, реализующие определенный интерфейс), то единственным реальным вариантом было создание совершенно нового класса коллек­ ции собственноручно. Это не слишком трудоемкая задача, но довольно утомительная. Учитывая эти (и другие) проблемы, разработчики .NET 2.0 добавили новый набор классов коллекций, собранных в пространстве ийен System.Collections .Generic. В любом новом проекте, который создается с помощью платформы .NET 2.0 и последую­ щих версий, предпочтение должно отдаваться соответствующим обобщенным классам перед унаследованными необобщенными. На заметку! Следует повториться: любое приложение, которое строится на платформе версии .NET 2.0 и выше, должно игнорировать классы из пространства имен System.Collect ions, а использовать вместо них классы из пространства имен System.Collections.Generic. Прежде чем будет показано, как использовать обобщения в своих программах, стоит глубже рассмотреть недостатки необобщенных коллекций; это поможет лучше понять проблемы, которые был призван решить механизм обобщений. Давайте создадим новое консольное приложение по имени IssuesWithNongenericCollections и затем импор­ тируем пространство имен System.Collections в начале кода С#: using System.Collections; Проблема производительности Как уже должно быть известно из главы 4, платформа .NETT поддерживает две обшир­ ные категории данных: типы значения и ссылочные типы. Поскольку в .NET определе­ ны две основных категории типов, однажды может возникнуть необходимость предста­ вить переменную одной категории в виде переменной другой категории. Для этого в C# предлагается простой механизм, называемый упаковкой (boxing), который служит для сохранения данных типа значения в ссылочной переменной. Предположим, что в мето­ де по имени SimpleBoxUnboxOperationO создана локальная переменная типа int: static void SimpleBoxUnboxOperationO { // Создать переменную ValueType (int). int mylnt = 25; } Если далее в приложении понадобится представить этот тип значения в виде ссы­ лочного типа, значение следует упаковать, как показано ниже: private static void SimpleBoxUnboxOperationO { Глава 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; } Когда компилятор C# встречает синтаксис упаковки/распаковки, он генерирует CILкод, содержащий коды операций box/unbox. Заглянув в сборку с помощью утилиты ildasm.exe, можно найти там следующий CIL-код: .method private hidebysig static void SimpleBoxUnboxOperation () cil managed // Code size 19 (0x13) .maxstack 1 .locals init ([0] int32 mylnt, [1] object boxedlnt, IL_0000 nop IL_0001 ldc.i4.s 25 stloc.0 IL_0003 IL_0004 ldloc.O box [m scorlib] System. Int32 IL_0005 IL_000a stloc.1 IL_000b ldloc.l unbox.any [mscorlib]System.Int32 IL_000c IL_0011 stloc.2 ret IL 0012 } // end of method Program::SimpleBoxUnboxOperation [2] int32 unboxedlnt) Помните, что в отличие от обычного приведения распаковка должна производиться только в соответствующий тип данных. Попытка распаковать порцию данных в некор­ ректную переменную приводит к генерации исключения InvalidCastException. Для полной безопасности следовало бы поместить каждую операцию распаковки в конст­ рукцию try / ca tc h , однако делать это для абсолютно каждой операции распаковки в 360 Часть III. Дополнительные конструкции программирования на C# приложении может оказаться довольно трудоемкой задачей. Взгляните на следующий измененный код, который выдаст ошибку, поскольку предпринята попытка распако­ вать упакованный 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.Collections.ArrayList для хранения множества числовых (расположенных в стеке) данных. Члены ArrayList прототипированы для ра­ боты с данными System.Object. Теперь рассмотрим методы Add(), Insert (), Remove(), а также индексатор класса: public class ArrayList : object, IList, ICollection, IEnumerable, ICloneable public public public public virtual virtual virtual virtual int A d d (o bject value); void Insert(int index, o bject value); void Remove(o bject obj); object this[int index] {get; set; } } Класс A r r a y L is t ориентирован на работу с экземплярами o b je c t , которые пред­ ставляют данные, расположенные в куче, поэтому может показаться странным, что следующий код компилируется и выполняется без ошибок: static void WorkWithArrayList () { // Типы значений упаковываются автоматически // при передаче методу, запросившему объект. ArrayList mylnts = new ArrayList (); mylnts.Add(10); mylnts.A d d (20); mylnts.Add(35); Console.ReadLine(); } Глава 10. Обобщения 361 Несмотря на непосредственную передачу числовых данных в методы, которые при­ нимают тип object, исполняющая среда автоматически упаковывает их в данные, распол оженные в стеке. При последующем извлечении элемента из ArrayList с использованием индексато­ ра типа потребуется распаковать операцией приведения объект, находящийся в куче, в целочисленное значение, расположенное в стеке. Помните, что индексатор ArrayList возвращает System.Object, а не System.Int32: static void WorkWithArrayList () { // Типы значений автоматически упаковываются, когда // передаются члену, принимающему объект. ArrayList mylnts = new ArrayList(); mylnts.Add(10); mylnts.Add(20); mylnts.Add(35); // Распаковка происходит, когда объект преобразуется // обратно в расположенные в стеке данные. int i = (int)mylnts[0]; // Теперь значение вновь упаковывается, // так как W riteL in e() требует объектных типов1 Console.WnteLine ("Value of your int: {0}", l) ; Console.ReadLine(); } Обратите внимание, что расположенные в стеке значения System.Int32 упаковы­ ваются перед вызовом ArrayList .Add (), чтобы их можно было передать в требуемом виде System.Object. Кроме того, объекты System.Object распаковываются обрат­ но в System.Int32 после их извлечения из ArrayList с использованием индексатора типа только для того, чтобы вновь быть упакованными для передачи в метод Console. WriteLineO, поскольку этот метод оперирует переменными System.Object. Хотя упаковка и распаковка очень удобна с точки зрения программиста, этот уп­ рощенный подход к передаче данных между стеком и кучей влечет за собой проблемы производительности (это касается как скорости выполнения, так и размера кода), а так­ же недостаток безопасности в отношении типов. Чтобы понять, в чем состоят проблемы с производительностью, взгляните на перечень действий, которые должны быть выпол­ нены при упаковке и распаковке простого целого числа. 1. Новый объект должен быть размещен в управляемой куче. 2. Значение данных, находящихся в стеке, должно быть передано в выделенное ме­ сто в памяти. 3. При распаковке значение, которое хранится в объекте, находящемся в куче, долж­ но быть передано обратно в стек. 4. Неиспользуемый больше объект в куче будет (в конечном) итоге удален сборщи­ ком мусора. Хотя существующий метод Main() не является основным узким местом в смысле производительности, вы определенно это почувствуете, если ArrayList будет содер­ жать тысячи целочисленных значений, к которым программа обращается на регуляр­ ной основе. В идеальном случае хотелось бы манипулировать расположенными в стеке данными внутри контейнера, не имея проблем с производительностью. Было бы хо­ рошо иметь возможность извлекать данные из контейнера, обходясь без конструкций try/catch (именно это обеспечивают обобщения). 362 Часть III. Дополнительные конструкции программирования на C# Проблемы с безопасностью типов Проблема безопасности типов уже затрагивалась, когда речь шла об операции рас­ паковки. Вспомните, что данные должны быть распакованы в тот же тип, который был для них объявлен перед упаковкой. Однако есть и другой аспект безопасности типов, который следует иметь в виду в мире без обобщений: тот факт, что классы из System. Col lections могут хранить все что угодно, поскольку их члены прототипированы для работы с System.Object. Например, в следующем методе контейнер ArrayList хранит произвольные фрагменты несвязанных данных: static void ArrayListOfRandomObjects () { // A rra y L ist может хранить все что угодно. ArrayList allMyObject = new ArrayList (); allMyObjects.Add(true) ; allMyObjects.Add(new OperatingSystem(PlatformID.MacOSX, new Version(10, 0))); allMyObjects.A d d (66) ; allMyObjects.Add(3.14) ; } В некоторых случаях действительно необходим исключительно гибкий контейнер, который может хранить буквально все. Однако в большинстве ситуаций понадобится безопасный в отношении типов контейнер, который может оперировать только опре­ деленным типом данных, например, контейнер, который хранит только подключения к базе данных, битовые образы или объекты, совместимые с IPointy. До появления обобщений единственным способом решения этой проблемы было соз­ дание вручную строго типизированных коллекций. Предположим, что создана специ­ альная коллекция, которая может содержать только объекты типа 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 по всем содержащимся в коллек­ ции элементам. Кроме того, методы GetPerson () и AddPerson () прототипированы на работу только с объектами Person, а не битовыми образами, строками, подключениями к базе данных или другими элементами. За счет создания таких классов обеспечивается безопасность типов; при этом компилятору C# позволяется определять любую попытку вставки элемента неподходящего типа: 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]; } // Вставка только объектов Car. public void AddCar(Car c) { arCars.Add(c); } 364 Часть III. Дополнительные конструкции программирования на C# public void ClearCars () { arCars.Clear (); } public int Count { get { return arCars.Count; } } // Поддержка перечисления с помощью foreach. IEnumerator IEnumerable.GetEnumerator() { return arCars.GetEnumerator (); } } Однако эти специальные контейнеры мало помогают в решении проблем упаковки/ распаковки. Даже если создать специальную коллекцию по имени In t C o lle c t io n , пред­ назначенную для работы только с элементами S y stem .In t32, все равно придется выде­ лить некоторый тип объекта для хранения данных (т.е. S ystem .A rray и A rra y L is t): 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 arInts.Count; } } IEnumerator IEnumerable.GetEnumerator() { return arInts.GetEnumerator (); } } Независимо от того, какой тип выбран для хранения целых чисел, дилеммы упаков­ ки нельзя избежать, применяя необобщенные контейнеры. В случае использования классов обобщенных коллекций исчезают все описанные выше проблемы, включая затраты на упаковку/распаковку и недостаток безопасно­ сти типов. Кроме того, необходимость в построении собственного специального класса обобщенной коллекции возникает редко. Вместо построения специальных коллекций, которые могут хранить людей, автомобили и целые числа, можно обратиться к обоб­ щенному классу коллекции и указать тип хранимых элементов. В показанном ниже методе класс L i s t o (из пространства имен S y s te m .C o lle c tio n .G e n e ric ) используется для хранения различных типов данных в строго типизированной манере (пока не обра­ щайте внимания на детали синтаксиса обобщений): static void UseGenericList () { Console.WriteLine ("***** Fun with Generics *****\n"); // Этот L i s t O может хранить только объекты Person. List<Person> morePeople = new List<Person> (); morePeople.Add(new Person ("Frank", "Black", 50)); Console.WriteLine(morePeople [0]); // Этот L i s t o может хранить только целые числа. List<int> morelnts = new List<int>(); morelnts.A d d (10); morelnts.A d d (2); int sum = morelnts [0] + morelnts[1]; // Ошибка компиляции! Объект Person не может быть добавлен к списку целых! // morelnts.Add(new Person ()); } Глава 10. Обобщения 365 Первый объект Lis t o может хранить только объекты Person. Поэтому выполнять приведение при извлечении элементов из контейнера не требуется, что делает этот под­ ход более безопасным в отношении типов. Второй Lis t o может хранить только целые, и все они размещены в стеке; другими словами, здесь не происходит никакой скрытой упаковки/распаковки, как это имеет место в необобщенном ArrayList. Ниже приведен краткий список преимуществ обобщенных контейнеров перед их не­ обобщенными аналогами. • Обобщения обеспечивают более высокую производительность, поскольку не стра­ дают от проблем упаковки/распаковки. • Обобщения более безопасны в отношении типов, так как могут содержать только объекты указанного типа. • Обобщения значительно сокращают потребность в специальных типах кол­ лекций, потому что библиотека базовых классов предлагает несколько готовых контейнеров. Исходный код. Проект IssuesWithNonGenericCollections доступен в подкаталоге Chapter 10. Роль параметров обобщенных типов Обобщенные классы, интерфейсы, структуры и делегаты буквально разбросаны по всей базовой библиотеке классов .NET, и они могут быть частью любого пространства имен .NET. На заметку! Обобщенными могут быть только классы, структуры, интерфейсы и делегаты, но не перечисления. Отличить обобщенный элемент в документации .NET Framework 4.0 SDK или брау­ зере объектов Visual Studio 2010 от других элементов очень легко по наличию пары угловых скобок с буквой или другой лексемой. На рис. 10.1 показано множество обоб­ щенных элементов в пространстве имен System.Collections .Generic, включая выде­ ленный класс List<T>. Рис. 10.1. Обобщенные элементы, поддерживающие параметры типа 366 Часть III. Дополнительные конструкции программирования на C# Формально эти лексемы можно называть п а р а м е т р а м и т ипа, однако в более дру­ жественных к пользователю терминах их можно считать просто м е с т а м и п одст а н ов ­ ки (placeholder). Конструкцию <Т> можно воспринимать как т и п а Т. Таким образом, IEnumerable<T> можно читать как I E n u m e r a b l e т и п а Т, или, говоря иначе, п еречи с­ л е н и е т и п а Т. На заметку! Имя параметра типа (места подстановки) не важно, и это — дело вкуса того, кто созда­ ет обобщенный элемент. Тем не менее, обычно для представления типов используется т, ТКеу или к — для представления ключей, а также TValue или V — для представления значений. При создании обобщенного объекта, реализации обобщенного интерфейса или вы­ зове обобщенного члена должно быть указано значение для параметра типа. Как в этой главе, так и в остальной части книги будет показано немало примеров. Однако для начала следует ознакомиться с основами взаимодействия с обобщенными типами и членами. Указание параметров типа для обобщенных классов и структур При создании экземпляра обобщенного класса или структуры параметр типа указы­ вается, когда объявляется переменная и когда вызывается конструктор. В предыдущем фрагменте кода было показано, что UseGenericList () определяет два объекта Listo: // Этот L i s t o может хранить только объекты Person. List<Person> morePeople = new List<Person> (); Этот фрагмент можно трактовать как L i s t o о б ъ е к т о в Т, г д е Т — т и п P e r s o n , или более просто — сп исок о б ъ е к т о в персон. Однажды указав параметр типа обобщенно­ го элемента, его нельзя изменить (помните: обобщения предназначены для поддерж­ ки безопасности типов). После указания параметра типа для обобщенного класса или структуры все вхождения заполнителей заменяются указанным значением. Просмотрев полное объявление обобщенного класса 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 public public public public public public public public public public public void Add(T item); ReadOnlyCollection<T> AsReadOnly (); int BinarySearch (T item); bool Contains(T item); void CopyTo(T[] array); int Findlndex(System.Predicate<T> match); T FindLast(System.Predicate<T> match); bool Remove(T item); int RemoveAll(System.Predicate<T> match); T [] ToArrayO; bool TrueForAll(System.Predicate<T> match); 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 public public public public public public public public public public public void Add(Person item); ReadOnlyCollection<Person> AsReadOnly(); int BinarySearch(Person item); bool Contains(Person item); void CopyTo(Person[] array); int Findlndex(System.Predicate<Person> match); Person FindLast(System.Predicate<Person> match); bool Remove(Person item); int RemoveAll(System.Predicate<Person> match); Person [] ToArrayO; bool TrueForAll(System.Predicate<Person> match); 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 }; // Указание заполнителя для обобщенного метода S o r t < > (). Array.Sort<int>(mylnts); foreach (int i in mylnts) { Console.WriteLine(i); } Указание параметров типов для обобщенных интерфейсов Обобщенные интерфейсы обычно реализуются при построении классов или струк­ тур, которые должны поддерживать различные поведения платформы (т.е. клонирова­ ние, сортировку и перечисление). В главе 9 рассматривалось множество необобщен­ ных интерфейсов, таких как IComparable, IEnumerable, IEnumerator и IComparer. Вспомните, как определен необобщенный интерфейс IComparable: 368 Часть III. Дополнительные конструкции программирования на C# public interface IComparable { int CompareTo(object obj); } В той же главе 9 этот интерфейс был реализован в классе Саг для обеспечения сортировки в стандартном массиве. Однако код требовал нескольких проверок вре­ мени выполнения и операций приведения, потому что параметром был общий тип System .O bject: 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 > obj.CarlD) return 1; if (this.CarlD < obj.CarlD) return -1; else return 0; } Здесь уже не нужно проверять, относится ли входной параметр к типу Саг, потому что он может быть только Саг! В случае передачи несовместимого типа данных возни­ кает ошибка времени компиляции. Получив начальные сведения о том, как взаимодействовать с обобщенными элементами, а также ознакомившись с ролью параметров тала (т.е. заполнителей), можно приступать к изучению классов и интерфейсов из пространства имен S ystem .C ollection s.G en eric. Глава 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>, которые более подробно рассматриваются далее в этой главе. Таблица 1 0 .3 . Основные интерфейсы, поддерживаемые классами из пространства имен S y s t e m . C o l l e c t i o n s . G e n e r i c Интерфейс S y s t e m . C o lle c t io n s Назначение ICollection<T> Определяет общие характеристики (например, размер, перечисление и безопасность к потокам) для всех типов обобщенных коллекций IComparer<T> Определяет способ сравнения объектов IDictionary<TKey, TValue> Позволяет объекту обобщенной коллекции представлять свое со­ держимое посредством пар "ключ/значение” IEnumerable<T> Возвращает интерфейс IEnumerator<T> для заданного объекта IEnumerator<T> Позволяет выполнять итерацию в стиле foreach по элементам коллекции 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. Дополнительные конструкции программирования на C# Таблица 10.4. Классы из пространства имен S y s t e m . C o l l e c t i o n s . G e n e r i c Обобщенный класс Поддерживаемые основные интерфейсы Dictionary<TKey, TValue> ICollection<T>, IDictionary<TKey, TValue>, Назначение Представляет обобщен­ ную коллекцию ключей и значений IEnumerable<T> List<T> ICollect ion<T>, IEnumerable<T>, IList<T> LinkedList<T> ICollection<T>, Динамически изменяе­ мый последовательный список элементов Представляет двуна­ правленный список IEnumerable<T> Queue<T> ICollection (Это не опечатка! Это интерфейс необобщенной кол­ лекции!), IEnumerable<T> Обобщенная реализация очереди — списка, ра­ ботающего по алгоритму “первые вошел — пер­ вый вышел” (FIFO) SortedDictionary<TKey, TValue> ICollection<T>, IDictionary<TKey, TValue>, Обобщенная реализа­ ция сортированного множества пар “ключ/ значение” IEnumerable<T> SortedSet<T> ICollect ion<T>, IEnumerable<T>, ISet<T> Stack<T> ICollection (Это не опечатка! Это интерфейс необобщенной кол­ лекции!), IEnumerable<T> Представляет коллекцию объектов, поддерживае­ мых в сортированном по­ рядке без дублирования Обобщенная реализация стека — списка, рабо­ тающего по алгоритму “последний вошел — первый вышел” (LIFO) В любом случае следующей задачей будет научиться использовать некоторые из этих обобщенных классов коллекций. Но прежде давайте рассмотрим языковые средства C# (впервые появившиеся в .NET 3.5), которые упрощают наполнение данными обобщен­ ных (и необобщенных) коллекций. Синтаксис инициализации коллекций В главе 4 был представлен синтаксис инициализации объектов, который позволя­ ет устанавливать свойства для новой переменной во время ее конструирования. Тесно связан с этим синтаксис инициализации коллекций. Это средство языка C# позволя­ ет наполнять множество контейнеров (таких как ArrayList или List<T>) элемента­ ми, используя синтаксис, похожий на тот, что применяется для наполнения базового массива. На заметку! Синтаксис инициализации коллекций может применяться только к классам, ко­ торые поддерживают метод A d d (), формализованный интерфейсами ICollection<T>/ ICollection. Глава 10. Обобщения 371 Рассмотрим следующие примеры: // Инициализация стандартного массива. int [] myArrayOf Ints = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // Инициализация обобщенного списка L i s t o элементов int. List<int> myGenericList = new List<int> { 0, 1, 2 , 3 , 4, 5 , // Инициализация ArrayList числовыми данными. ArrayList myList = new ArrayList { 0, 1, 2 , 3 , 4, 5, 6, 1, 6, 1, 8, 9 }; 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); Работа с классом L i s t < T > Д ля начала создадим новый проект к он сольн ого п р и лож ен и я по им ени FunWithGenericCollections. Проекты этого типа автоматически ссылаются на сбор­ ки mscorlib.dll и System.dll, что обеспечивает доступ к большинству обобщенных классов коллекций. Обратите внимание, что в первоначальном файле кода C# уже им­ портируется пространство имен System.Collections.Generic. Первый обобщенный класс, который мы рассмотрим — это List<T>, который уже использовался ранее в этой главе. Из всех классов пространства имен System. Col lections. Generic класс List<T> будет применяться наиболее часто, потому что он позволяет динамически изменять размер своего содержимого. Чтобы проиллюстри­ 372 Часть III. Дополнительные конструкции программирования на C# ровать основы этого типа, добавьте в класс Program метод UseGenericList(), в кото­ ром List<T> используется для манипуляций множеством объектов Person; вы должны помнить, что в классе Person определены три свойства (Age, FirstName и LastName) и специальная реализация метода ToStringO. private static void UseGenericList () { // Создать список объектов Person и заполнить его с помощью // синтаксиса инициализации объектов/коллекций. List<Person> people = new List<Person> () { new new new new Person Person Person Person {FirstName= {FirstName= {FirstName= {FirstName= "Homer", LastName="Simpson", Age=47}, "Marge", LastName="Simpson", Age=45}, "Lisa", LastName="Simpson", Age=9}, "Bart", LastName="Simpson", Age=8} }; // Вывести на консоль количество элементов в списке. Console .WnteLine ("Items in list: {0}", people.Count); // Перечислить список, foreach (Person p in people) Console .WnteLine (p) ; // Вставить новую персону. Console .WnteLine (" \n->Inserting new person.") ; people.Insert(2, new Person { FirstName = "Maggie", LastName = "Simpson", Age = 2 }); Console .WnteLine ("Items in list: {0}", people.Count); // Скопировать данные в новый массив. Person[] arrayOfPeople = people.ToArray() ; for (int i = 0 ; i < arrayOfPeople.Length; i++) { Console .WnteLine ("First Names: {0}", arrayOfPeople[l].FirstName); Здесь вы используете синтаксис инициализации для наполнения вашего L ist< T > объектами, как сокращенную нотацию вызовов A dd() п раз. Выведя количество эле­ ментов в коллекции (а также пройдясь по каждому элементу), вы вызываете In s e r t (). Как можно видеть, I n s e r t () позволяет вам вставить новый элемент в L ist< T > по ука­ занному индексу. И наконец, обратите внимание на вызов метода T o A r r a y (), который возвращает массив объектов Person, основанный на содержимом исходного List<T>. Затем вы вы­ полняете проход по всем элементам этого массива, используя синтаксис индекса масси­ ва. Если вы вызовете этот метод из M ain(), то получите следующий вывод: ***** Fun with Generic Collections ***** Items Name: Name: Name: Name: in list: 4 Homer Simpson, Age: 47 Marge Simpson, Age: 45 Lisa Simpson, Age: 9 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>. Это даст более полное понимание базовых вари­ антов хранения данных в приложении. Работа с классом S t a c k < 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 = 4 5 }); stackOfPeople.Push(new Person { FirstName = "Lisa", LastName = "Simpson", Age = 9 }); // Просмотреть верхний элемент, вытолкнуть его и просмотреть снова. Console.WriteLine("First person is: {0}", stackOfPeople.Peek()); Console .WnteLine ("Popped of f {0}", stackOf People .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. После серии вызовов Рор() и Реек() стек, наконец, опустошается, после чего вызовы Реек () и Рор() приводят к генерации системного исключения. Вывод этого примера показан ниже: ***** 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. Дополнительные конструкции программирования на C# Работа с классом Q u e u e < T > Очереди — это контейнеры, гарантирующие доступ к элементам в стиле “первый вошел — первый вышел” (FIFO). К сожалению, людям приходится сталкиваться с очере­ дями каждый день: очереди в банк, очереди в кинотеатр, очереди в кафе. Когда нужно смоделировать сценарий, в котором элементы обрабатываются в режиме FIFO, класс Queue<T> подходит наилучшим образом. В дополнение к функциональности, предос­ тавляемой поддерживаемыми интерфейсами, Queue определяет основные члены, которые перечислены в табл. 10.5. Таблица 10.5. Члены типа Q u eu e<T> Член Назначение Dequeue () Удаляет и возвращает объект из начала Queue<T> Enqueue () Добавляет объект в конец Queue<T> Peek() Возвращает объект из начала Queue<T>, не удаляя его Теперь давайте посмотрим на эти методы в работе. Можно снова вернуться к классу Person и построить объект Queue<Т>, эмулирующий очередь людей, которые ожидают заказа кофе. Для начала представим, что имеется следующий статический метод: static void GetCoffee(Person p) { Console.WriteLine("{0} got coffee!", p .FirstName) ; } Кроме того, есть также дополнительный вспомогательный метод, который вызывает GetCof fee () внутренне: static void UseGenencQueue () { // Создать очередь из трех человек. Queue<Person> peopleQ = nev; 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="Simpson", Age=9}); // Кто первый в очереди? Console.WriteLine ("{0} is first in line!", peopleQ.Peek().FirstName); // Удалить всех из очереди. GetCoffee(peopleQ.Dequeue ()); GetCof fee(peopleQ.Dequeue()); GetCof fee(peopleQ.Dequeue()); . // Попробовать извлечь кого-то из очереди снова? try { GetCoffee(peopleQ.Dequeue()); } catch(InvalidOperationException e) { Console.WriteLine ("Error 1 {0}", e.Message); // очередь пуста } Глава 10. Обобщения 375 Здесь вы вставляете три элемента в класс Queue<T>, используя метод Enqueue (). Вызов Реек () позволяет вам просматривать (но не удалять) первый элемент, находя­ щийся в данный момент в Queue. Наконец, вызов Dequeue () удаляет элемент из очере­ ди и посылает его вспомогательной функции GetCoffeeO для обработки. Заметьте, что если вы пытаетесь удалять элементы из пустой очереди, генерируется исключение вре­ мени выполнения. Приведем вывод, который вы получаете при вызове этого метода: ***** Fun with Generic Collections ***** Homer is first in line1 Homer got coffee! Marge got coffee! Lisa got coffee! Error! Queue empty. Работа с классом S o r t e d S e t < 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 new new new Person Person Person Person {FirstName= {FirstName= {FirstName= {FirstName= "Homer", LastName="Simpson", Age=47}, "Marge", LastName="Simpson", Age=45}, "Lisa", LastName="Simpson", Age=9}, "Bart", LastName="Simpson", Age=8} }; // Обратите внимание, что элементы отсортированы по возрасту, foreach (Person р in setOfPeople) { Console.WriteLine(р); 376 Часть III. Дополнительные конструкции программирования на C# Console.WnteLine () ; // Добавить еще несколько людей разного возраста. setOfPeople.Add(new Person { FirstName = "Saku", LastName = "Jones", Age = 1 }); setOfPeople.Add(new Person { FirstName = "Mikko", LastName = "Jones", Age = 32 }); // Элементы по-прежнему отсортированы по возрасту, foreach (Person р in setOfPeople) { Console .WnteLine (p) ; После запуска приложения видно, что список объектов будет всегда упорядочен по значению свойства Аде, независимо от порядка вставки и удаления объектов в коллекцию: ★ ★ ★ ★ ★ Fun with Generic Collections Name : Name: Name : Name : Bart Simpson, Age: 8 Lisa Simpson, Age: 9 Marge Simpson, Age: 45 Homer Simpson, Age: 47 Name : Name : Name : Name : Name : Name : Saku Jones, Age: 1 Bart Simpson, Age: 8 Lisa Simpson, Age: 9 Mikko Jones, Age: 32 Marge Simpson, Age: 45 Homer Simpson, Age: 47 Великолепно! Теперь вы должны почувствовать себя увереннее, причем не только в отношении преимуществ обобщенного программирования вообще, но также в исполь­ зовании обобщенных типов из библиотеки базовых классов .NET. В завершении этой главы будет также показано, как и для чего строить собственные обобщенные типы и обобщенные методы. Исходный код. Проект F u n W ith G e n e ric C o lle c tio n s доступен в подкаталоге Chapter 10. Создание специальных обобщенных методов Хотя большинство разработчиков обычно используют существующие обобщенные типы из библиотек базовых классов, можно также строить собственные обобщенные методы и специальные обобщенные типы. Чтобы понять, как включать обобщения в собственные проекты, начнем с построения обобщенного метода обмена, предваритель­ но создав новое консольное приложение по имени G enericM ethods. Построение специальных обобщенных методов представляет собой более развитую версию традиционной перегрузки методов. В главе 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 Т a, ref Т b) { Console.WnteLine ("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.WnteLine (''***** Fun with Custom Generic Methods *****\n"); // Обмен двух значений int. int a = 10, b = 90; Console. WnteLine ("Before swap: {0}, {1}", a, b) ; Swap<int>(ref a, ref b); Console.WnteLine ("After swap: {0}, {1}", a, b) ; Console.WnteLine () ; 378 Часть III. Дополнительные конструкции программирования на C# // Обмен двух строк. string si = "Hello", s2 = "There"; Console.WriteLine("Before swap: {0} {1}!", si, s2) ; Swap<stnng> (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 Therel You sent the SwapO method a System.String After swap: There Hello 1 Основное преимущество этого подхода в том, что нужно будет сопровождать только одну версию Swap<T>(), хотя она может оперировать любыми двумя элементами опре­ деленного типа, причем в безопасной к типам манере. Еще лучше то, что находящиеся в стеке элементы остаются в стеке, а расположенные в куче — соответственно, в куче. Выведение параметра типа При вызове таких обобщенных методов, как Swap<T>, можно опускать параметр тип, если (и только если) обобщенный метод требует аргументов, поскольку компилятор мо­ жет вывести параметр типа из параметров членов. Например, добавив к Main() следую­ щий код, можно обменивать значения System.Boole ап: // Компилятор самостоятельно выведет тип System.Boolean. bool Ы = true, Ь2 = false; Console .WriteLine ("Before swap: {0}, {1}", bl, b2) ; Swap (ref bl, ref b2); Console.WriteLine("After swap: {0}, {1}", bl, b2) ; Несмотря на то что компилятор может определить параметр типа на основе типа данных, использованного в объявлении Ы и Ь2, стоит выработать привычку всегда ука­ зывать параметр типа явно: Swap<bool> (ref bl, 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<stnng> () ; // Ошибка на этапе компиляции! Нет параметров? // Значит, необходимо указать тип для подстановки! // 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.WnteLine ("You sent the SwapO method a {0}", typeof(T) ); T temp; temp = a; a = b; b = temp; } public static void DisplayBaseClass<T> () { Console.WnteLine ("Base class of {0} is: {1}.", typeof(T), typeof(T).BaseType); } Статические методы Swap<T> и DisplayBaseClass<T> находятся в контексте нового статического типа класса, поэтому потребуется указать имя типа при вызове каждого члена, например: MyGenericMethods.Swap<int>(ref a, ref b) ; Разумеется, методы не обязательно должны быть статическими. Если бы Swap<T> и DisplayBaseClass<T> были методами уровня экземпляра (определенными в нестатиче­ ском классе), нужно было бы просто создать экземпляр MyGenericMethods и вызывать их с использованием объектной переменной: MyGenericMethods с = new MyGenericMethods(); с .Swap<int>(ref a, ref b) ; Исходный код. Проект CustomGenericMethods доступен в подкаталоге Chapter 10. Создание специальных обобщенных структур и классов Теперь, когда известно, как определяются и вызываются обобщенные методы, да­ вайте посмотрим, как конструировать обобщенную структуру (процесс построения обобщенного класса идентичен) в новом проекте консольного приложения по имени GenericPoint. Предположим, что строится обобщенная структура Point, которая под­ держивает единственный параметр типа, определяющий внутреннее представление ко­ ординат (х, у). Вызывающий код должен иметь возможность создавать типы Point<T> следующим образом: 380 Часть III. Дополнительные конструкции программирования на C# // Точка с координатами in t. Point<int> р = new Point<int> (10, 10); // Точка с координатами double. Point<double> р2 = new Point<double>(5.4, 3.3); А вот полное определение Point<T> с последующим анализом: // Обобщенная структура Point. public struct Point<T> { // Обобщенные данные состояния. private Т xPos; private Т yPos; // Обобщенный конструктор. public Point(Т xVal, Т 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 ResetPoint () { xPos = default (Т); yPos = default(T); Ключевое слово d e f a u l t в обобщенном коде Как видите, Point<T> использует параметр типа в определении данных полей, ар­ гументов конструктора и определении свойств. Обратите внимание, что в дополнение к переопределению T o S trin g O , в Point<T> определен метод по имени R e s e tP o in t(), в котором используется некоторый новый синтаксис: // Ключевое слово d e fa u lt перегружено в С#. // При использовании с обобщениями оно представляет // значение по умолчанию для параметра типа. public void ResetPoint () { X = d e fa u lt (Т ); Y = d e fa u lt (Т ); } Глава 10. Обобщения 381 С появлением обобщений ключевое слово d e fa u lt обрело второй смысл. В дополне­ ние к использованию с конструкцией sw itch оно теперь может применяться для уста­ новки значения по умолчанию для параметра типа. Это очень удобно, учитывая, что обобщенный тип не знает заранее, что будет подставлено вместо заполнителя в угловых скобках, и потому не может безопасно строить предположений о значениях по умолча­ нию. Умолчания для параметров типа следующие: • значение по умолчанию числовых величин равно 0; • ссылочные типы имеют значение по умолчанию n u ll; • поля структур устанавливаются в 0 (для типов значений) или в n u ll (для ссылоч­ ных типов). Для Point<T> можно было установить значение X и Y в 0 напрямую, исходя из пред­ положения, что вызывающий код будет применять только числовые значения. Однако за счет использования синтаксиса d e fa u lt (Т) повышается общая гибкость обобщен­ ного типа. В любом случае теперь можно использовать методы Poin t<T> следующим образом: static void Main(string [] args) { Console. WnteLine ("***** Fun with Generic Structures *****\n"); // Объект Point, в котором используются in t. Point<int> p = new Point<int>(10, 10); Console .WnteLine ("p.ToStnng () = {0 }", p .ToString () ) ; p .ResetPoint(); Console.WriteLine("p.ToString()={0}", p .ToString()); Console.WnteLine () ; // Объект Point, в котором используются double. Point<double> p2 = new Point<double>(5.4, 3.3); Console.WriteLine ("p2.ToString()={0}", p 2 .ToString()); p 2 .ResetPoint(); Console.WnteLine ("p2 .ToString () = {0 }", p2 .ToString () ) ; Console.ReadLine (); } Ниже показан вывод этого примера: ***** Fun with Generic Structures ***** p .ToString()=[10, 10] p .ToString () = [0, 0] p2 .ToString() = [5.4, 3.3] p2 .ToString () = [0, 0] Исходный код. Проект G e n e ric P o in t доступен в подкаталоге Chapter 10. Обобщенные базовые классы Обобщенные классы могут служить базовыми для других классов, и потому опре­ делять любое количество виртуальных или абстрактных методов. Однако типы-нас­ ледники должны подчиняться нескольким правилам, чтобы гарантировать сохранение природы обобщенной абстракции. Во-первых, если необобщенный класс расширяет обобщенный класс, то класс-наследник должен указывать параметр типа: 382 Часть III. Дополнительные конструкции программирования на C# // Предположим, что создан специальный класс обобщенного списка. public class MyList<T> { private List<T> listOfData = new List<T>(); } // Heобобщенные типы должны указывать параметр типа // при наследовании от обобщенного базового класса. public class MyStnngList : MyList<string> {} Во-вторых, если обобщенный базовый класс определяет обобщенные или абстракт­ ные методы, то тип-наследник должен переопределять обобщенные методы, используя указанный параметр типа: // Обобщенный класс с виртуальным методом. public class MyList<T> { private List<T> listOfData = new List<T>(); public virtual void PnntList(T data) { } } public class MyStnngList : MyList<string> { // Должен подставлять параметр типа, использованный / / в родительском классе, в унаследованные методы. public override void PnntList (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 PnntList (T data) { } } При решении повседневных программистских задач вряд ли часто придется созда­ вать иерархии специальных обобщенных классов. Тем не менее, это вполне возможно (до тех пор, пока вы придерживаетесь правил). Ограничение параметров типа Как показано в этой главе, любой обобщенный элемент имеет, по крайней мере, один параметр типа, который должен быть указан при взаимодействии с обобщенным ти­ пом или членом. Одно это позволит строить безопасный в отношении типов код; одна­ ко платформа .NET позволяет использовать ключевое слово where для*указания особых требований к определенному параметру типа. С помощью ключевого слова t h i s можно добавлять набор ограничений к конкрет­ ному параметру типа, которые компилятор C# проверит во время компиляции. В част­ ности, параметр типа можно ограничить, как описано в табл. 10.6. Глава 10. Обобщения 383 Таблица 10.6. Возможные ограничения параметров типа для обобщений Ограничение обобщения Назначение where Т : struct Параметр типа <Т> должен иметь в своей цепочке насле­ дования System.ValueType. Другими словами, <Т> должен быть структурой where Т : class Параметр типа <Т> должен не иметь System.ValueType в своей цепочке наследования (т.е. <т> должен быть ссылочным типом) where Т : new() Параметр типа <т> должен иметь конструктор по умол­ чанию. Это очень полезно, если обобщенный тип должен создавать экземпляры параметра типа, поскольку не уда­ ется предположить формат специальных конструкторов. Обратите внимание, что в типе со многими ограничения­ ми это ограничение должно указываться последним where T : И м я Б а зо в о го К л а с са Параметр типа <т> должен быть наследником класса, указанного в И м я Б а з о в о го К л а с с а where Т : ИмяИнтерфейса Параметр типа <т> должен реализовать интерфейс, ука­ занный в ИмяИнтерфейса. Можно задавать несколько интерфейсов, разделяя их запятыми Если только не требуется строить какие-то исключительно безопасные к типам специальные коллекции, возможно, никогда не придется использовать ключевое слово where в проектах С#. Так или иначе, но в следующих нескольких примерах (частичного) кода демонстрируется работа с ключевым словом where. Примеры использования ключевого слова w h e r e Будем исходить из того, что создан специальный обобщенный класс, и необходимо гарантировать наличие в параметре типа конструктора по умолчанию. Это может быть полезно, когда специальный обобщенный класс должен создавать экземпляры Т, потому что конструктор по умолчанию — это единственный конструктор, потенциально общий для всех типов. Также подобного рода ограничение Т позволит производить проверку во время компиляции; если Т — ссылочный тип, то компилятор напомнит программисту о необходимости переопределения конструктора по умолчанию в объявлении класса (если помните, конструкторы по умолчанию удаляются из классов, в которых определяются собственные конструкторы). // Класс MyGenencClass унаследован от object, примем содержащиеся / / в нем элементы должны иметь конструктор по умолчанию, public class MyGenencClass<T> where T : new() { } Обратите внимание, что конструкция where указывает параметр типа, на который накладывается ограничение, а за ним следует операция двоеточия. После этой опера­ ции перечисляются все возможные ограничения (в данном случае — конструктор по умолчанию). Ниже показан еще один пример: // MyGenencClass унаследован от Object, причем содержащиеся // в нем элементы должны относиться к классу, реализующему IDrawable. / / и поддерживать конструктор по умолчанию. public class MyGenencClass<T> where T : class, IDrawable, new () 384 Часть III. Дополнительные конструкции программирования на C# В данном случае к Т предъявляются три требования. Во-первых, это должен быть ссылочный тип (не структура), что помечено лексемой c la s s . Во-вторых, Т должен реа­ лизовывать интерфейс ID rawable. В-третьих, он также должен иметь конструктор по умолчанию. Множество ограничений перечисляются в списке, разделенном запятыми; однако имейте в виду, что ограничение new() всегда должно идти последним! По этой причине следующий код не скомпилируется: // Ошибка! Ограничение new() должно быть последним в списке! public class MyGenericClass<T> where T : new(), class, IDrawable { } В случае создания обобщенного класса коллекции с несколькими параметрами типа, можно указывать уникальный набор ограничений для каждого параметра с помощью отдельной конструкции where: // <К> должен расширять SomeBaseClass и иметь конструктор по умолчанию, в то время // как <Т> должен быть структурой и реализовывать обобщенный интерфейс IComparable. public class MyGenericClasscK, T> where К : SomeBaseClass, new() where T : IComparable<T> } Необходимость построения полностью нового обобщенного класса коллекции возни­ кает редко; однако ключевое слово where также допускается применять и в обобщенных методах. Например, если необходимо гарантировать, чтобы метод Swap<T>() работал только со структурами, обновите код следующим образом: // Этот метод обменяет местами любые структуры, но не классы, static void Swap<T>(ref Т a, ref Т b) where Т : struct } Обратите внимание, что если ограничить метод Swap О подобным образом, обмени­ вать местами объекты s t r i n g (как это делалось в коде примера) уже не получится, по­ скольку s t r in g является ссылочным типом. Недостаток ограничений операций В конце этой главы следует упомянуть об одном моменте относительно обобщенных методов и ограничений. При создании обобщенных методов может оказаться сюрпри­ зом появление ошибок компиляции во время применения любых операций C# (+, -, *, == и т.д.) к параметрам типа. Например, подумайте о пользе от класса, который может выполнять Add(), S u b stra ctO , M u ltip ly () и D evideO над обобщенными типами: // Ошибка на этапе компиляции' Нельзя применять // арифметические операции к параметрам типа! public class Bas±cMath<T> { public T { return public T { return public T { return public T { return Add(T argl, T arg2) argl + arg2; } Subtract (T argl, T arg2) argl - arg2; } Multiply(T argl, T arg2) argl * arg2; } Divide(T argl, T arg2) argl / arg2; } Глава 10. Обобщения 385 К сожалению, приведенный выше класс BasicMath<T> не скомпилируется. Хотя это может показаться серьезным недостатком, следует снова вспомнить, что обобщения яв­ ляются общими. Естественно, числовые данные работают достаточно хорошо с бинар­ ными операциями С#. Однако если <Т> будет специальным классом или структурой, то компилятор мог бы предположить, что этот класс поддерживает операции +, -, * и /. В идеале язык C# должен был бы позволять ограничивать обобщенные типы поддержи­ ваемыми операциями, например: // Код т о л ь к о д л я и л л ю с т р а ц и и ! public class BasicMath<T> where T : operator + , operator operator *, operator / { public T { return public T { return public T { return public T { return Add(T argl, T arg2) argl + arg2; } Subtract (T argl, T arg2) argl - arg2; } Multiply (T argl, T arg2) argl * arg2; } Divide (T argl, T arg2) argl / arg2; } } К сожалению, ограничения операций в текущей версии C# не поддерживаются. Однако можно (хотя это и потребует дополнительной работы) достичь желаемого эф­ фекта, определив интерфейс, поддерживающий эти операции (интерфейсы C# могут оп­ ределять операции) и затем указать ограничение интерфейса для обобщенного класса. На этом первоначальный обзор построения специальных обобщенных типов завершен. В следующей главе мы вновь обратимся к теме обобщений, когда будем рассматривать тип делегата .NET. Резюме Настоящая глава начиналась с рассмотрения использования классических контей­ неров, включенных в пространство имен S y s t e m .C o lle c t io n s . Хотя эти типы будут и далее поддерживаться для обратной совместимости, новые приложения .NET выиг­ рают от применения вместо них новых обобщенных аналогов из пространства имен S y s te m .C o lle c tio n s .G e n e r ic . Как вы видели, обобщенный элемент позволяет специфицировать заполнители (параметры типа), которые указываются во время создания (или вызова — в случае обобщенных методов). По сути, обобщения предлагают решение проблем упаковки и обеспечения безопасности типов, которые досаждали при разработке программного обеспечения для .NET 1.1. Вдобавок обобщенные типы в значительной мере исключают необходимость в построении специальных типов коллекций. Хотя чаще всего обобщенные типы, представленные в библиотеках базовых классов .NET, просто будут использоваться, можно также создавать и собственные обобщенные типы (и обобщенные методы). При этом есть возможность задавать любое количество ограничений (с помощью ключевого слова where), чтобы повышать уровень безопасно­ сти в отношении типов и гарантировать выполнение операций над типами в “известном количестве”; это обеспечивает предоставление определенных базовых возможностей. ГЛАВА 1 1 Делегаты, события и лямбда-выражения плоть до этого момента большинство разработанных приложений добавляли различные порции кода к методу M a in (), тем или иным способом посылающие запросы заданному объекту. Однако многие приложения требуют, чтобы объект мог об ращаться обратно к сущности, которая создала его — посредством механизма обратно­ го вызова. Хотя механизмы обратного вызова могут применяться в любом приложении, они особенно важны в графических интерфейсах пользователя, где элементы управле­ ния (такие как кнопки) нуждаются в вызове внешних методов при надлежащих условиях (выполнен щелчок на кнопке, курсор мыши находится на поверхности кнопки и т.п.). На платформе .NET тип делегата является предпочтительным средством определе­ ния и реагирования на обратные вызовы в приложении. По сути, тип делегата .NET — это безопасный к типам объект, который “указывает” на метод или список методов, которые могут быть вызваны позднее. Однако в отличие от традиционного указателя на функцию C++, делегаты .NET представляют собой классы, обладающие встроенной поддержкой группового выполнения и асинхронного вызова методов. В этой главе будет показано, как создавать и управлять типами делегатов, а также использовать ключевое слово e v e n t в С#, которое облегчает работу с типами делегатов. По пути вы также изучите несколько языковых средств С#, ориентированных на делега­ ты и события, включая анонимные методы и групповые преобразования методов. Завершается глава исследованием лямбда-выраженияй (lambda expressions). Исполь­ зуя лямбда-операцию C# (=>), теперь можно указывать блок операторов кода (и парамет­ ры для передачи этим операторам) везде, где требуется строго типизированный делегат. Как будет показано, лямбда-выражение — это немногим более чем маскировка аноним­ ного метода, и представляет собой упрощенный подход к работе с делегатами. В Понятие типа делегата .NET Прежде чем приступить к формальному определению делегатов .NET, давайте огля­ немся немного назад. В интерфейсе Windows API часто использовались указатели на функции в стиле С для создания сущностей, именуемых функциями обратного вызова (callback functions) или просто обратными вызовами (callbacks). С помощью обратных вызовов программисты могли конфигурировать одну функцию таким образом, чтобы она осуществляла обратный вызов другой функции в приложении. Применяя такой подход, разработчики Windows смогли обрабатывать щелчки на кнопках, перемещения курсора мыши, выбор пунктов меню и общие двусторонние коммуникации между про­ граммными сущностями в памяти. Глава 11. Делегаты, события и лямбда-выражения 387 Проблема со стандартными функциями обратного вызова в стиле С заключается в том, что они представляют собой лишь немногим более чем простой адрес в памяти. В идеале обратные вызовы могли бы конфигурироваться для включения дополнитель­ ной безопасной к типам информации, такой как количество и типы параметров и воз­ вращаемого значения (если оно есть) метода, на который они указывают. К сожалению, это невозможно с традиционными функциями обратного вызова и это, как следовало ожидать, является постоянным источником ошибок, аварийных завершений и прочих неприятностей во время выполнения. Тем не менее, обратные вызовы — удобная вещь. В .NET Framework обратные вызовы по-прежнему возможны, и их функциональ­ ность обеспечивается в гораздо более безопасной и объектно-ориентированной манере с использованием делегатов. По сути, делегат — это безопасный в отношении типов объект, указывающий на другой метод (или, возможно, список методов) приложения, который может быть вызван позднее. В частности, объект делегата поддерживает три важных фрагмента информации: • адрес метода, на котором он вызывается; • аргументы (если есть) этого метода; • возвращаемое значение (если есть) этого метода. На заметку! Делегаты .NET могут указывать как на статические, так и на методы экземпляра. Как только делегат создан и снабжен необходимой информацией, он может динами­ чески вызывать методы, на которые указывает, во время выполнения. Каждый делегат в .NET Framework (включая специальные делегаты) автоматически снабжается способ­ ностью вызывать свои методы синхронно или асинхронно. Этот факт значительно уп­ рощает задачи программирования, поскольку позволяет вызывать метод во вторичном потоке выполнения без рунного создания и управления объектом Thread. На заметку! Асинхронное поведение типов делегатов будет рассматриваться во время исследо­ вания пространства имен System.Threading в главе 19. Определение типа делегата в C# Для определения делегата в C# используется ключевое слово delegate. Имя делега­ та может быть любым. Однако сигнатура определяемого делегата должна соответство­ вать сигнатуре метода, на который он будет указывать. Например, предположим, что планируется построить тип делегата по имени BinaryOp, который может указывать на любой метод, возвращающий целое число и принимающий два целых числа в качестве входных параметров: / / Э т о т д е л е г а т м ож ет у к а з ы в а т ь н а лю бой м е т о д , котор ы й / / приним ает д в а целы х и в о зв р а щ а е т ц е л о е з н а ч е н и е . public delegate int BinaryOp(int x, int y) ; Когда компилятор C# обрабатывает тип делегата, он автоматически генерирует за­ печатанный (sealed) класс, унаследованный от System.MulticastDelegate. Этот класс (в сочетании с его базовым классом System.Delegate) предоставляет необходимую ин­ фраструктуру для делегата, чтобы хранить список методов, подлежащих вызову в бо­ лее позднее время. Например, если просмотреть делегат BinaryOp с помощью утилиты ildasm.exe, обнаружится класс, показанный на рис. 11.1. 388 Часть III. Дополнительные конструкции программирования на C# l> Н \Му Books\C* BookVC# ana ih* NET fUetfornn 5th e<f\First C-'3#t\Chapter_11 Code,: impieD< . File View Help V - H:\My Books\C# Book\C# and the NET Platform 5th ed\First Draft\Chapter_l l\Code\SimpleDelegate\ob]\x86\Debug\Smple ► MANIFEST 6 Щ SimpleDelegate пВЕИЗЗЕВИ « ► .class public auto ansi sealed ► extends [mscorllb]5ystem.MulticastDelegate ■ .cto r: void(object, native mt) ■ Begnlnvoke : class [mscorlib]5ystem.IAsyncResult(int 32, г^32, class [mscorlib]5ystem. AsyncCallback object) ■ Endlnvoke : int32(dass [mscorlib]5ystem.IAsyncResult) ■ Invoke : mt32(int32,mt32) SlmpleDelegate.Program 4 .assembly Sir.ipleDelegate { Рис. 11.1. Ключевое слово delegate представляет запечатанный класс, унаследованный от System.MulticastDelegate Как видите, сгенерированный компилятором класс BinaryOp определяет три обще­ доступных метода. lnvoke() — возможно, главный из них, поскольку он используется для синхронного вызова каждого из методов, поддерживаемых объектом делегата; это означает, что вызывающий код должен ожидать завершения вызова, прежде чем про­ должить свою работу. Может показаться странным, что синхронный метод Invoke () не должен вызываться явно в коде С#. Как вскоре будет показано. Invoke () вызывается “за кулисами”, когда применяется соответствующий синтаксис С#. Методы Begin Invoke () и Endlnvoke () предлагают возможность вызова текущего метода асинхронным образом, в отдельном потоке выполнения. Имеющим опыт в мно­ гопоточной разработке должно быть известно, что одной из основных причин, вынуж­ дающих разработчиков создавать вторичные потоки выполнения, является необходи­ мость вызова методов, которые требуют определенного времени для завершения. Хотя в библиотеках базовых классов .NET предусмотрено целое пространство имен, посвящен­ ное многопоточному программированию (System.Threading), делегаты предлагают эту функциональность в готовом виде. Каким же образом компилятор знает, как следует определить методы Invoke(), BeginlnvokeO и Endlnvoke ()? Чтобы разобраться в процессе, ниже приведен код сге­ нерированного компилятором типа класса BinaryOp [полужирным курсивом выделены элементы, указанные определенным типом делегата): sealed class B in a r y O p : System.MulticastDelegate { public i n t Invoke ( i n t x, i n t y) ; public IAsyncResult Begmlnvoke (in t x, in t y, AsyncCallback cb, object state); public int Endlnvoke(IAsyncResult result); } Первым делом, обратите внимание, что параметры и возвращаемый тип для метода Invoke () в точности соответствуют определению делегата BinaryOp. Начальные пара­ метры для членов BeginlnvokeO (в данном случае — два целых) также основаны на делегате BinaryOp; однако BeginlnvokeO всегда будет предоставлять два финальных параметра (типа AsyncCallback и object), используемых для облегчения асинхронно­ го вызова методов. И, наконец, возвращаемый тип Endlnvoke () идентичен исходному объявлению делегата и всегда принимает единственный параметр — объект, реализую­ щий интерфейс IAsyncResult. Глава 11. Делегаты, события и лямбда-выражения 389 Давайте рассмотрим другой пример. Предположим, что определен тип делегата, ко­ торый может указывать на любой метод, возвращающий string и принимающий три входных параметра System.Boolean: public delegate string MyDelegate(bool a, bool b, bool c); На этот раз сгенерированный компилятором класс будет выглядеть так: sealed class M y D e l e g a t e : System.MulticastDelegate { public s t r i n g Invoke (bool a, b o o l b, b o o l c) ; public IAsyncResult Begin Invoke {bool a, b o o l b, b o o l c, AsyncCallback cb, object state); public s t r i n g Endlnvoke(IAsyncResult result); Делегаты также могут “указывать” на методы, содержащие любое количество пара­ метров out и ref (как и массивов параметров, помеченных ключевым словом params). Например, предположим, что имеется следующий тип делегата: public delegate string MyOtherDelegate (out bool a, r e f bool b, int c) ; Сигнатуры методов InvokeO и Begin Invoke () выглядят так, как и следовало ожи­ дать; однако взгляните на метод Endlnvoke (), который теперь включает набор аргумен­ тов out/ref, определенных типом делегата: sealed class M y O t h e r D e l e g a t e : System.MulticastDelegate { public s t r i n g Invoke (out b o o l a, r e f b o o l b, i n t c) ; public IAsyncResult Begin Invoke (out b o o l a, r e f b o o l b, i nt c, AsyncCallback cb, object state); public s t r i n g Endlnvoke (out b o o l a, r e f b o o l b, IAsyncResult result) ; } Чтобы подытожить: определение типа делегата C# порождает запечатанный класс с тремя сгенерированными компилятором методами, типы параметров и возвращаемых значений которых основаны на объявлении делегата. Базовый шаблон может быть опи­ сан с помощью следующего псевдокода: // Это только псевдокод! public sealed class ИмяДелегата : System.MulticastDelegate { public возвращаемоеЗначениеДелегата Invoke {всеВходныеЯе£иОи1ПараметрыДелегата) ; public IAsyncResult Beginlnvoke(в c e B x o д н ы e R e f и O u t П a p a м e т p ы Д e л e г a т a r AsyncCallback cb, object state); public возвращаемоеЗначениеДелегата Endlnvoke(всеВходныеЯе£иОи1ПараметрыДелегата, IAsyncResult result); } Базовые классы System.MulticastDelegate и System.Delegate При построении типа, использующего ключевое слово delegate, неявно объявля­ ется тип класса, унаследованного от System.MulticastDelegate. Этот класс обеспе­ чивает своих наследников доступом к списку, который содержит адреса методов, под­ держиваемых типом делегата, а также несколько дополнительных методов (и несколько перегруженных операций), чтобы взаимодействовать со списком вызовов. Ниже показаны некоторые избранные методы System.MulticastDelegate: 390 Часть III. Дополнительные конструкции программирования на C# 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. Основные члены S y s te m .M u ltc a s tD e le g a te / S y s te m .D e le g a te Член Назначение Method Это свойство возвращает объект System.Ref lection.Method, который представляет детали статического метода, поддерживаемо­ го делегатом Target Если метод, подлежащий вызову, определен на уровне объекта (т.е. не является статическим), то Target возвращает объект, представ­ ляющий метод, поддерживаемый делегатом. Если возвращенное Target значение равно null, значит, подлежащий вызову метод является статическим Combine () Этот статический метод добавляет метод в список, поддерживаемый делегатом. В C# этот метод вызывается за счет использования пере­ груженной операции += в качестве сокращенной нотации GetlnvokationListO Этот метод возвращает массив типов System.Delegate, каждый из которых представляет определенный метод, доступный для вызова Remove () RemoveAll () Эти статические методы удаляют метод (или все методы) из списка вызовов делегата. В C# метод 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 .WnteLine ("***** Simple Delegate Example *****\n"); // Создать объект делегата BinaryOp, "указывающий" на SimpleMath.Add(). BinaryOp b = new BinaryOp(SimpleMath.Add); // Вызвать метод Add() непрямо с использованием объекта делегата. Console.WnteLine (" 10 + 10 is {0}", b(10, 10)); Console.ReadLine(); Обратите внимание на формат объявления типа делегата BinaryOp: он определяет, что объекты делегата BinaryOp могут указывать на любой метод, принимающий два целых и возвращающий целое (действительное имя метода, на который он указывает, не существенно). Здесь создан класс по имени SimpleMath, определяющий два статиче­ ских метода, которые соответствуют шаблону, определенному делегатом BinaryOp. Когда нужно вставить целевой метод в заданный объект делегата, просто передайте имя этого метода конструктору делегата: // Создать делегат BinaryOp, который "указывает" на SimpleMath.Add () . BinaryOp b = new BinaryOp(SimpleMath.Add); С этого момента указанный метод можно вызывать с использованием синтаксиса, который выглядит как прямой вызов функции: // Invoke() действительно вызывается здесь! Console .WnteLine ("10 + 10 is {0}", b(10, 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. Дополнительные конструкции программирования на C# Хотя C# и не требует явного вызова Invoke () в коде, это вполне можно сделать. То есть следующий оператор кода является допустимым: Console.WnteLine ("10 + 10 is {0}", b.Invoke(10, 10)); Вспомните, что делегаты .NET безопасны в отношении типов. Поэтому, попытка передать делегату метод, не соответствующий шаблону, приводит к ошибке времени компиляции. Чтобы проиллюстрировать это, предположим, что в классе SimpleMath теперь определен дополнительный метод по имени SquareNumber (), принимающий единственный целочисленный аргумент: public class SimpleMath public static int SquareNumber(int a) { return a * a; } Учитывая, что делегат Binary Op может указывать только на методы, принимаю­ щие два целых и возвращающие целое, следующий код неверен и компилироваться не будет: // Ошибка компиляции! Метод не соответствует шаблону делегата1 BinaryOp Ь2 = new BinaryOp(SimpleMath.SquareNumber); Исследование объекта делегата Давайте услож ним текущ ий пример, создав статический метод (по имени DisplayDelegatelnfoO) в классе Program. Этот метод будет выводить на консоль име­ на методов, поддерживаемых объектом делегата, а также имя класса, определяющего метод. Для этого будет реализована итерация по массиву System.Delegate, возвра­ щенному GetlnvocationList(), с обращением к свойствам Target и Method каждого элемента. static void DisplayDelegatelnfo (Delegate delOb]) { // Вывести на консоль имена каждого члена списка вызовов делегата. foreach (Delegate d in delOb].GetlnvocationList ()) { Console .WnteLine ("Method Name: {0}"f d.Method) ; // имя метода Console .WnteLine ("Type Name: {0}"f d.Target); // имя типа } Исходя из предположения, что в метод Main() добавлен вызов этого вспомогательно­ го метода, вывод приложения будет выглядеть следующим образом: ***** simple Delegate Example **** * Method Name: Int32 Add(Int32, Int32) Type Name: 10 + 10 is 20 Обратите внимание, что имя типа (SimpleMath) в настоящий момент не отображает­ ся при обращении к свойству Target. Причина в том, что делегат BinaryOp указывает на статический метод, и потому просто нет объекта, на который нужно ссылаться! Однако если сделать методы Add() и SubstractO нестатическими (удалив в их объяв­ лениях ключевые слова static), можно будет создавать экземпляр типа SimpleMath и указывать методы для вызова с использованием ссылки на объект: Глава 11. Делегаты, события и лямбда-выражения 393 static void Main(string [] args) { Console.WriteLine("***** Simple Delegate Example *****\n"); // Делегаты .NET могут указывать на методы экземпляра. SimpleMath.m = new SimpleMath (); BinaryOp b = new BinaryOp(m.Add); // Вывести сведения об объекте. DisplayDelegatelnfо (b); Console.WriteLine("10 + 10 is {0}"f b(10, 10)); Console.ReadLine(); } В этом случае вывод будет выглядеть, как показано ниже: ***** Simple Delegate Example ***** Method Name: Int32 Add(Int32, Int32) Type Name: SimpleDelegate.SimpleMath 10 + 10 is 20 Исходный код. Проект S im p leD e leg a te доступен в подкаталоге C hapter 11. Отправка уведомлений о состоянии объекта с использованием делегатов Ясно, что предыдущий пример S im p le D e le g a te был предназначен только для це­ лей иллюстрации, потому что нет особого смысла создавать делегат только для того, чтобы просуммировать два числа. Рассмотрим более реалистичный пример, в котором делегаты используются для определения класса Саг, обладающего способностью инфор­ мировать внешние сущности о текущем состоянии двигателя. Для этого понадобится предпринять перечисленные ниже действия. • Определение нового типа делегата, который будет отправлять уведомления вызы­ вающему коду. • Объявление переменной-члена этого делегата в классе Саг. • Создание вспомогательной функции в Саг, которая позволяет вызывающему коду указывать метод для обратного вызова. • Реализация метода A c c e le r a t e () для обращения к списку вызовов делегата при нужных условиях. Для начала создадим проект консольного приложения по имени C arD elegate. Затем определим новый класс Саг, который изначально выглядит следующим образом: 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) 1 CurrentSpeed = currSp; 394 Часть III. Дополнительные конструкции программирования на C# MaxSpeed = maxSp; PetName = name; } Ниже показаны обновления, связанные с реализацией трех первых пунктов: public class Car { // 1. Определить тип делегата. public delegate void CarEngineHandler(string msgForCaller); / / 2 . Определить переменную-член типа этого делегата. 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 (10 == (MaxSpeed - CurrentSpeed) && listOfHandlers != null) { listOfHandlers("Careful buddy! Gonna blow!"); Глава 11. Делегаты, события и лямбда-выражения 395 if (CurrentSpeed >= MaxSpeed) carlsDead = true; else Console.WnteLine ("CurrentSpeed = {0}", CurrentSpeed); } } Обратите внимание, что прежде чем вызывать методы, поддерживаемые переменнойчленом listOfHandlers, она проверяется на равенство null. Причина в том, что разме­ щать эти объекты вызовом вспомогательного метода RegisterWithCarEngine() — зада­ ча вызывающего кода. Если вызывающий код не вызовет этот метод, а мы попытаемся обратиться к списку вызовов делегата, то получим исключение NullReferenceException и нарушим работу исполняющей системы (что очевидно нехорошо). Теперь, имея всю инфраструктуру делегатов, рассмотрим обновления класса Program: class Program { static void Main(string [] args) { Console .WnteLine (••***** Delegates as event enablers *****\n "); // Сначала создадим объект Car. Car cl = new Car ("SlugBug", 100, 10); // Теперь сообщим ему, какой метод вызывать, // когда он захочет отправить сообщение. cl.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent)); // Ускорим (это инициирует события). Console .WnteLine ("***** Speeding up *****"); for (int l = 0; l < 6; i++) cl.Accelerate(20); Console.ReadLine() ; } // Это цель для входящих сообщений. public static void OnCarEngineEvent(string msg) { Console .WnteLine ("\n***** Message From Car Object *****"); Console .WnteLine ("=> {0}", msg); Console WnteLine ("***********************************\n") ; Метод Main() начинается с создания нового объекта Car. Поскольку мы заинтере­ сованы в событиях, связанных с двигателем, следующий шаг заключается в вызове специальной регистрационной функции RegisterWithCarEngine (). Вспомните, что этот метод ожидает получения экземпляра вложенного делегата CarEngineHandler, и как с любым делегатом, метод, на который он должен указывать, задается в параметре конструктора. Трюк в этом примере состоит в том, что интересующий метод находится в классе Program! Обратите внимание, что метод OnCarEngineEvent () полностью соответствует связанному делегату в том, что принимает string и возвращает void. Ниже показан вывод этого примера: ***** Delegates as event enablers ***** ***** Speeding CurrentSpeed = CurrentSpeed = CurrentSpeed = up ***** 30 50 70 396 Часть III. Дополнительные конструкции программирования на C# ***** Message From Car Object ***** => Careful buddy! Gonna blow! *********************************** CurrentSpeed = 90 ***** Message From Car Object ***** => Sorry, this car is dead... ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ Включение группового вызова Вспомните, что делегаты .NET обладают встроенной возможностью группового вы­ зова. Другими словами, объект делегата может поддерживать целый список методов для вызова, а не просто единственный метод. Для добавления нескольких методов к объекту делегата используется перегруженная операция +=, а не прямое присваи­ вание. Чтобы включить групповой вызов в типе Саг, можно модифицировать метод R egisterW ith C a rE n gin eO следующим образом: 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.Accelerate(2 0 ); Console.ReadLine(); // Теперь есть ДВА метода, которые будут вызваны Саг // при отправке уведомлений. public static void OnCarEngineEvent (string msg) { Console.WriteLine ("\n***** Message From Car Object *****"); Console.WriteLine ("=> {0}", msg); Console ^£j_^0 Lj_j^0 ^,,*^*^*^*^*^*^*'*'*'*'*'*''**'*'*'*'*'*'*'*'*'*'*'*'*'*'*'*'\j"i,,j • } Глава 11. Делегаты, события и лямбда-выражения 397 public static void OnCarEngineEvent2(string msg) { Console.WriteLine("=> {0}"f msg.ToUpper()); } В терминах кода CIL операция += разрешает вызывать статический метод Delegate. Combine () (фактически, его можно вызывать и напрямую, но операция += предлагает бо­ лее простую альтернативу). Ниже показан CIL-код метода RegisterWithCarEngine(). .method public hidebysig instance void RegisterWithCarEngine() (class CarDelegate.Car/AboutToBlow clientMethod) cil managed { // Code size 25 (0x19) .maxstack 8 IL_0000: nop IL_0001: ldarg.O IL_0002: dup IL_0003: ldfld class CarDelegate.Car/CarEngineHandler CarDelegate. Car::listOfHandlers IL_0008: ldarg.l 1 L 0 0 0 9 :c a ll cla ss [m scorlib]System .Delegate [m scorlib] System.Delegate: : Combine(class [m scorlib]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 (), разработчики C# могут использовать также перегруженную операцию -= в качестве сокращения. Давайте добавим в класс Саг новый метод, который позволяет вызывающему коду исключать метод из списка вызовов: public class Car public void UnRegisterWithCarEngine(CarEngineHandler methodToCall) { listOfHandlers -= methodToCall; } Опять-таки, синтаксис -= представляет собой просто сокращенную нотацию для рунного вызова метода Delegate .Remove (), что иллюстрирует следующий код CLI для метода UnRegisterWithCarEvent () класса Саг: .method public hidebysig instance void UnRegisterWithCarEvent(class CarDelegate.Car/AboutToBlow clientMethod) cil managed { // Code size 25 (0x19) .maxstack 8 IL_0000: nop 398 Часть III. Дополнительные конструкции программирования на C# IL_0001: ldarg.O IL_0002: dap IL_0003: ldfld class CarDelegate.Car/CarEngineHandler CarDelegate.Car::listOfHandlers 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::listOfHandlers IL_0018 : ret } // end of method Car::UnRegisterWithCarEngine В текущей версии класса Саг прекратить получение уведомлений от второго обра­ ботчика можно за счет изменения метода Main() следующим образом: static void Main (string [] args) { Console .WnteLine ("***** Delegates as event enablers *****\n"); // Сначала создадим объект Car. Car cl = new Ca r ("SlugBug", 100, 10); cl .RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent)); // Сохраним объект делегата для последупцей отмены регистрации. Car.CarEngineHandler handler2 = new Car.CarEngineHandler(OnCarEngineEvent2); cl .RegisterWithCarEngine(handler2); // Ускорим (это инициирует события). Console .WnteLine ("***** Speeding up *****"); for (int l = 0; l < 6; i++) cl.Accelerate(20); // Отменим регистрацию второго обработчика, cl .UnRegisterWithCarEngine(handler2) ; // Сообщения в верхнем регистре больше не выводятся. Console .WnteLine ("***** Speeding up *****"); for (int l = 0; l < 6; i++) cl.Accelerate(20); Console.ReadLine(); } Одно отличие Main() состоит в том, что на этот раз создается объект Саг. CarEngineHandler, который сохраняется в локальной переменной, чтобы иметь воз­ можность позднее отменить подписку на получение уведомлений. Тогда при следующем ускорении Саг уже больше не будет выводиться версия входящего сообщения в верхнем регистре, поскольку эта цель исключена из списка вызовов делегата. Исходный код. Проект CarDelegate доступен в подкаталоге Chapter 11. Синтаксис групповых преобразований методов В предыдущем примере CarDelegate явно создавались экземпляры объекта делега­ та Car.CarEngineHandler, чтобы регистрировать и отменять регистрацию на получе­ ние уведомлений: static void Main(string[] args) { Console.WnteLine (" ***** Delegates as event enablers *****\n"); Car cl = new Car ("SlugBug", 100, 10); cl .RegisterWithCarEngine(new C a r.CarEngineHandler(OnCarEngineEvent)); Глава 11. Делегаты, события и лямбда-выражения 399 Саг.CarEngineHandler handler2 = new Car.CarEngineHandler(OnCarEngineEvent2); cl.RegisterWithCarEngine(handler2); } Если нужно вызывать любые унаследованные члены MulticastDelegate или Delegate, то наиболее простым способом сделать это будет ручное создание перемен­ ной делегата. Однако в большинстве случаев обращаться к внутренностям объекта де­ легата не понадобится. Объект делегата будет нужен только для того, чтобы передать имя метода как параметр конструктора. Для простоты в C# предлагается сокращение, которое называется групповое преоб­ разование методов (method group conversion). Это средство позволяет указывать прямое имя метода, а не объект делегата, когда вызываются методы, принимающие делегаты в качестве аргументов. На заметку! Как будет показано далее в этой главе, синтаксис группового преобразования мето­ дов можно также использовать для упрощения регистрации событий С#. Для целей иллю страц и и создадим новое консольное прилож ение по имени CarDelegateMethodGroupConversion и добавим в него файл, содержащий класс Саг, который был определен в проекте CarDelegate. В показанном ниже коде класса Program используется групповое преобразование методов для регистрации и отмены регистрации подписки на уведомления. class Program { static void Main (string[] args) { Console .WnteLine ("***** Method Group Conversion *****\n "); Car cl = new Car(); // Зарегистрировать простое имя метода. cl.RegisterWithCarEngine(CallMeHere); Console .WnteLine ("***** Speeding up *****"); for (int i = 0; l < 6; i++) cl.Accelerate(20); // Отменить регистрацию простого имени метода. cl.UnRegisterWithCarEngine(CallMeHere); // Уведомления больше не поступают! for (int 1 = 0; i < 6; i++) cl.Accelerate(20); Console.ReadLine() ; } static void CallMeHere(string msg) { Console .WnteLine ("=> Message from Car: {0}", msg); Обратите внимание, что мы не создаем непосредственно объект делегата, а просто указываем метод, который соответствует ожидаемой сигнатуре делегата (в данном слу­ чае — метод, возвращающий void и принимающий единственную строку). Имейте в виду, что компилятор C# по-прежнему обеспечивает безопасность типов. Поэтому, если метод CallMeHere () не принимает string и не возвращает void, компилятор сообщит об ошибке. 400 Часть III. Дополнительные конструкции программирования на C# Исходный код. Проект CarDelegateMethodGroupConversion доступен в подкаталоге Chapter 11. Понятие ковариантности делегатов Как вы могли заметить, каждый из делегатов, созданных до сих пор, указывал на ме­ тоды, возвращающие простые числовые типы данных (или void). Однако предположим, что имеется новое консольное приложение по имени D eleg a teC o va ria n ce, определяю­ щее тип делегата, который может указывать на методы, возвращающие объект пользо­ вательского класса (не забудьте включить в новый проект определение класса Саг): // Определение типа делегата, указывающего на методы, которые возвращают объект Саг. public delegate Car ObtainCarDelegate(); Разумеется, цель для делегата можно определить следующим образом: namespace DelegateCovariance { class Program { // Определение типа делегата, указывающего на методы, // которые возвращают объект Саг. public delegate Car ObtainCarDelegate(); static void Main(string[] args) { Console .WnteLine ("***** Delegate Covariance *****\n"); ObtainCarDelegate targetA = new ObtainCarDelegate(GetBasicCar); Car c = targetA(); Console. WnteLine ("Obtained a {0}", c) ; Console.ReadLine (); } public static Car GetBasicCar () { return new C a r ("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 C a r ( ) ; } 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 c = 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 { // Определение единственного типа делегата, указывающего на методы, // которые возвращают объект Саг или 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 c = 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. Дополнительные конструкции программирования на C# Понятие обобщенных делегатов Вспомните из предыдущей главы, что язык C# позволяет определять обобщенные типы делегатов. Например, предположим, что необходимо определить делегат, который может вызывать любой метод, возвращающий void и принимающий единственный па­ раметр. Если передаваемый аргумент может изменяться, это моделируется через пара­ метр типа. Для иллюстрации рассмотрим следующий код нового консольного приложе­ ния по имени GenericDelegate: namespace GenericDelegate { // Этот обобщенный делегат может вызывать любой метод, который // возвращает void и принимает единственный параметр типа. public delegate void MyGenericDelegate<T> (Т arg); class Program { static void Main(string [] args) { Console.WriteLine ("***** Generic Delegates *****\n"); // Регистрация целей. MyGenencDelegate<string> strTarget = new MyGenericDelegate<string>(StringTarget); strTarget("Some string data"); MyGenencDelegate<int> intTarget = new MyGenericDelegate<int>(IntTarget); intTarget (9); Console.ReadLine (); } static void StringTarget(string arg) { Console.WriteLine("arg in uppercase is: {0}"f arg.ToUpper()); } static void IntTarget(int arg) { Console.WriteLine("++arg is: {0}", ++arg); } } } Обратите внимание, что в MyGenericDelegate<T> определен единственный пара­ метр, представляющий аргумент для передачи цели делегата. При создании экземпляра этого типа необходимо указать значение параметра типа вместе с именем метода, кото­ рый может вызывать делегат. Таким образом, если указать тип string, целевому методу будет отправлено строковое значение: // Создать экземпляр MyGenencDelegate<T> // со string в качестве параметра типа. MyGenericDelegate<stnng> strTarget = new MyGenericDelegate<string>(StringTarget); strTarget("Some string data"); Имея формат объекта strTarget, метод StringTarget теперь должен принимать в качестве параметра единственную строку: static void StringTarget(s trin g arg) { Console.WriteLine("arg in uppercase is: {0}", arg.ToUpper()); } Глава 11. Делегаты, события и лямбда-выражения 403 Эмуляция обобщенных делегатов без обобщений Обобщенные делегаты предоставляют более гибкий способ спецификации ме­ тода, подлежащего вызову в безопасной к типам манере. До появления обобщений (в .NET 2.0) тога же конечного результата можно было достичь с использованием пара­ метра S ystem .O bject: public delegate void MyDelegate(object arg); Хотя это позволяет посылать любой аргумент цели делегата, это не обеспечивает безопасность типов и не избавляет от бремени упаковки/распаковки. Для примера предположим, что созданы два экземпляра M yDelegate, и оба они указывают на один и тот же метод 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()); } } } Когда целевому методу посылается тип значения, это значение упаковывается и сно­ ва распаковывается при получении методом. Также, учитывая, что входящий параметр может быть чем угодно, перед приведением должна производиться динамическая про­ верка лежащего в основе типа. С помощью обобщенных делегатов необходимую гиб­ кость можно получить без упомянутых проблем. Исходный код. Проект G e n e ric D e le g a te доступен в подкаталоге C hapter 11. На этом первоначальный экскурс в тип делегата .NET завершен. Мы еще вернемся к некоторым дополнительным деталям работы с делегатами в конце этой главы и еще раз — в главе 19, когда будем рассматриваться многопоточность. А теперь переходим к связанной теме — ключевому слову ev e n t в С#. 404 Часть III. Дополнительные конструкции программирования на C# Понятие событий C# Делегаты — весьма интересные конструкции в том смысле, что позволяют объектам, находящимся в памяти, участвовать в двустороннем общении. Однако работа с делега­ тами напрямую может порождать довольно однообразный код (определение делегата, определение необходимых переменных-членов и создание специальных методов реги­ страции и отмены регистрации для предохранения инкапсуляции). Более того, если делегаты используются в качестве механизма обратного вызова в приложениях напрямую, существует еще одна проблема: если не определить делегат — переменную-член класса как приватную, то вызывающий код получит прямой доступ к объектам делегатов. В этом случае вызывающий код может присвоить переменной новый объект-делегат (фактически удалив текущий список функций, подлежащих вызо­ ву), и что еще хуже — вызывающий код сможет напрямую обращаться к списку вызовов делегата. Чтобы проиллюстрировать проблему, рассмотрим следующую переделанную (и упрощенную) версию предыдущего примера CarDelegate: public class Car { public delegate void CarEngineHandler(string msgForCaller); // Теперь этот член p u b lic ! 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 ("***** A gh1 No Encapsulation1 *****\n"); // Создать Car. Car myCar = new Car(); // Есть прямой доступ к делегату! myCar.listOfHandlers = new Car.CarEngineHandler(CallWhenExploded) ; myCar.Accelerate (10); // Назначаем ему совершенно новый объект... / / В лучшем случае получается путаница. myCar.listOfHandlers = new Car.CarEngineHandler(CallHereToo); myCar.Accelerate (10); // Вызывающий код может также напрямую вызвать делегат! myCar.listOfHandlers.Invoke ("hee, hee, hee..."); Console.ReadLine (); } static void CallWhenExploded(string msg) { Console.WriteLine(msg); } static void CallHereToo(string msg) { Console.WriteLine (msg); } Глава 11. Делегаты, события и лямбда-выражения 405 Общедоступные члены-делегаты нарушают инкапсуляцию, что не только затруднит сопровождение кода (и отладку), но также сделает приложение уязвимым в смысле безо­ пасности. Ниже показан вывод текущего примера: ***** Agh [ No Encapsulation1 ***** Sorry, this car is dead. .. Sorry, this car is dead. .. hee, hee, hee. .. Очевидно, что вряд ли имеет смысл предоставлять другим приложениям право и з­ менять то, на что указывает делегат, или вызывать его члены напрямую. Исходный код. Проект PublicDelegateProblem доступен в подкаталоге Chapter 11. Ключевое слово e v e n t В качестве сокращения, избавляющего от необходимости строить специальные ме­ тоды для добавления и удаления методов в списке вызовов делегата, в C# предусмотре­ но ключевое слово event. Обработка компилятором ключевого слова event приводит к автоматическому получению методов регистрации и отмены регистрации наряду со всеми необходимыми переменными-членами для типов делегатов. Эта переменнаячлен делегата всегда объявляется приватной, и потому не доступна напрямую объекту, инициировавшему событие. Точнее говоря, ключевое слово event — это не более чем синтаксическое украшение, позволяющее экономить на наборе кода. Определение события представляет собой двухэтапный процесс. Во-первых, нужно определить делегат, который будет хранить список методов, подлежащих вызову при возникновении события. Во-вторых, необходимо объявить событие (используя ключе­ вое слово event) в терминах связанного типа делегата. Чтобы проиллюстрировать ключевое слово event, создадим новое консольное при­ ложение по имени CarEvents. В классе Саг будут определены два события под назва­ ниями 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. Дополнительные конструкции программирования на C# Exploded("Sorry, this car is dead..."); } else { CurrentSpeed += delta; // Почти сломан? if (10 == MaxSpeed - CurrentSpeed && AboutToBlow != null) { AboutToBlow("Careful buddy! Gonna blow!"); // Все в порядке! if (CurrentSpeed >= maxSpeed) carlsDead = true; else Console.WriteLine ("CurrentSpeed = {0}", CurrentSpeed); } } Таким образом, объект Car сконфигурирован для отправки двух специальных со­ бытий, без необходимости определения специальных функций регистрации или объ­ явления переменных-членов. Чуть ниже будет продемонстрировано использование этого нового объекта, но сначала давайте рассмотрим архитектуру событий немного подробнее. “За кулисами” событий Событие C# в действительности развертывается в два скрытых метода, один из которых имеет префикс 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 'value') cil managed synchronized call class [mscorlib]System.Delegate [m scorlib]System .D elegate: : Combine( cla ss [m scorlib]System .D elegate, class [m scorlib]System .Delegate) Как и следовало ожидать, remove AboutToBlow () неявно вызывает Delegate. Remove (): .method public hidebysig specialname instance void remove_AboutToBlow (class CarEvents.Car/CarEngineHandler 'value') cil managed synchronized { call class [mscorlib]System.Delegate [m scorlib] System. D elegate: : Remove ( cla ss [m scorlib]System .Delegate, cla ss [m scorlib]System .Delegate) Глава 11. Делегаты, события и лямбда-выражения 407 И, наконец, в CIL-коде, представляющем само событие, используются директи­ вы .addon и .removeon для отображения имен корректных методов add XXX () и 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) } Теперь, когда вы разобрались, как строить класс, способный отправлять события C# (и уже знаете, что события — это лишь способ сэкономить время на наборе кода), сле­ дующий большой вопрос связан с организацией прослушивания входящих событий на стороне вызывающего кода. Прослушивание входящих событий События C# также упрощают акт регистрации обработчиков событий на стороне вызывающего кода. Вместо того чтобы специфицировать специальные вспомогатель­ ные методы, вызывающий код просто использует операции += и -= непосредственно (что приводит к внутренним вызовам методов add XXX () или remove XXX ()). Для реги­ страции события руководствуйтесь следующим шаблоном: // ИмяОбъекта.ИмяСобытия += 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(CarIsAlmostDoomed); 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.Accelerate(20); // Удалить метод CarExploded из списка вызовов. cl.Exploded -= d; Console.WriteLine("\n***** Speeding up *****"); for (int i = 0; i < 6; i++) cl.Accelerate (20) ; Console.ReadLine (); 408 Часть III. Дополнительные конструкции программирования на C# 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); // Регистрация обработчиков событий, c l .AboutToBlow += CarlsAlmostDoomed; cl .AboutToBlow += CarAboutToBlow; cl.Exploded += CarExploded; Console.WriteLine ("***** Speeding up *****"); for (int i = 0; l < 6; i++) cl.Accelerate(20) ; cl.Exploded -= CarExploded;' Console.WriteLine ("\n***** Speeding up *****"),for (int l = 0; l < 6; i++ ) cl.Accelerate(20); Console.ReadLine() ; } Упрощенная регистрация событий с использованием Visual Studio 2010 Среда Visual Studio 2010 предоставляет помощь в процессе регистрации обработ­ чиков событий. В случае применения синтаксиса += во время регистрации событий открывается окно IntelliSense, приглашающее нажать клавишу <ТаЬ> для автоматиче­ ского заполнения экземпляра делегата (рис. 11.2). ^{C arE vents.Program *1 V H o okln to E ven tiQ * p u b lic S ta tic v o i d H o o k In t o E v e n ts ( ) { newCar = new • (); n e w C a r.A b o u tT o B lo w + } | new C a r.C a r E n g in e H a n d le r( n e w C a r _ A b o u tT o B lo w ) ; iP r e s s TAB t o in s e r t ) j ) 100 % - ‘ Рис. 11.2. Выбор делегата с помощью IntelliSense После нажатия клавиши <ТаЬ> появляется возможность ввести имя обработчика событий, который нужно сгенерировать (или просто принять имя по умолчанию), как показано на рис. 11.3. Снова нажав <ТаЬ>, вы получите заготовку кода цели делегата в корректном форма­ те (обратите внимание, что этот метод объявлен статическим, потому что событие было зарегистрировано внутри статического метода Main()): static void newCar_AboutToBlow (string msg) { // Add your code1 } Глава 11. Делегаты, события и лямбда-выражения 409 Рис. 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. Дополнительные конструкции программирования на C# 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 .WnteLine ("{ 0 } says: {1}", sender, e.msg); } Если получатель желает взаимодействовать с объектом, отправившим событие, можно выполнить явное приведение System.Object. С помощью такой ссылки можно вызвать любой общедоступный метод объекта, который отправил событие: public static void CarAboutToBlow (object sender, CarEventArgs e) { // Чтобы подстраховаться, произведем проверку во время выполнения перед приведением. if (sender is Car) { Car c = (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.WnteLine ("***** Prim and Proper Events *****\n "); // Создать Car обычным образом. Car cl = new Car ("SlugBug", 100, 10); // Зарегистрировать обработчики событий. cl.AboutToBlow += CarIsAlmostDoomed; cl.AboutToBlow += CarAboutToBlow; EventHandler<CarEventArgs> d = new EventHandler<CarEventArgs>(CarExploded); cl.Exploded += d; Итак, вы ознакомились с основными аспектами работы с делегатами и событиями на языке С#. Хотя этого вполне достаточно для решения практически любых задач, свя­ занных с обратными вызовами, в завершение главы рассмотрим ряд финальных упро­ щений, а именно — анонимные методы и лямбда-выражения. Исходный код. Проект PrimAndProperCarEvents (Generic) доступен в подкаталоге Chapter 11. Понятие анонимных методов C# Как было показано выше, когда вызывающий код желает прослушивать входящие события, он должен определить специальный метод в классе (или структуре), соответ­ ствующий сигнатуре ассоциированного делегата. Ниже приведен пример: 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. Дополнительные конструкции программирования на C# class Program { static void Main(string[] args) { Console .WnteLine ("**** * Anonymous Methods *****\n "); Car cl = new Car ("SlugBug", 100, 10); // Зарегистрировать обработчики событий в виде анонимных методов. cl.AboutToBlow += delegate { Console .WnteLine ("Eek ! Going too fast!"); }; cl .AboutToBlow += delegate(object sender, CarEventArgs e) { Console .WnteLine ("Message from Car: {0}", e.msg); cl.Exploded += delegate(object sender, CarEventArgs e) { Console .WnteLine ("Fatal Message from Car: {0}", e.msg); // Это в конечном итоге инициирует события, for (int i = 0; i < 6; i++ ) cl.Accelerate (20); Console.ReadLine (); На заметку! Последняя фигурная скобка анонимного метода должна завершаться точкой с запя­ той. Если забыть об этом, во время компиляции возникнет ошибка. Обратите внимание, что в классе Program теперь не определяются специальные ста­ тические обработчики событий вроде CarAboutToBlowO или CarExplodedO . Вместо этого с помощью синтаксиса += определяются встроенные неименованные (анонимные) методы, к которым вызывающий код будет обращаться во время обработки события. Базовый синтаксис анонимного метода соответствует следующему псевдокоду: class Program { static void Main(string [] args) { SomeType t = new SomeType(); t.SomeEvent += delegate (optionallySpecifledDelegateArgs) { /* операторы */ }; } } Обратите внимание, что при обработке первого события AboutToBlow внутри пре­ дыдущего метода Main() аргументы, передаваемые из делегата, не указываются: c l .AboutToBlow += delegate { Console. WnteLine ("Eek! Going too fast!"); }; Строго говоря, вы не обязаны принимать входные аргументы, посланные определен­ ным событием. Однако если планируется использовать эти входные аргументы, нужно указать параметры, прототипированные типом делегата (как показано во второй обра­ ботке событий AboutToBlow и Exploded). Например: cl.AboutToBlow += delegate(o bject 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.WnteLine ("***** 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 .WnteLine ("Critical Message from Car: {0}", msg) ; Console.WriteLine("AboutToBlow event was fired {0} times.", aboutToBlowCounter); Console.ReadLine(); } Запустив этот модифицированный метод M ain (), вы обнаружите, что финальный вывод Console.WriteLine() сообщит о двукратном вызове AboutToBlow. Исходный код. Проект AnonymousMethods доступен в подкаталоге Chapter 11. Понятие лямбда-выражений Чтобы завершить знакомство с архитектурой событий .NET, рассмотрим лямбда-вы­ ражения. Как объяснялось ранее в этой главе, C# поддерживает способность обрабаты­ вать события “встроенным образом”, назначая блок операторов кода непосредственно событию с использованием анонимных методов вместо построения отдельного мето­ 414 Часть III. Дополнительные конструкции программирования на C# да, подлежащего вызову лежащим в основе делегатом. Лямбда-выражения — это всего лишь лаконичный способ записи анонимных методов, в конечном итоге упрощающий работу с типами делегатов .NET. Чтобы подготовить фундамент для изучения лямбда-выражений, создадим новое консольное приложение по имени SimpleLambdaExpressions. Теперь займемся мето­ дом FindAllO обобщенного типа List<T>. Этот метод может быть вызван, когда нужно извлечь подмножество элементов из коллекции, и он имеет следующий прототип: // Метод класса System.Collections.Generic.List<T>. public List<T> FindAll(Predicate<T> match) Как видите, этот метод возвращает объект List<T>, представляющий подмножество данных. Также обратите внимание, что единственный параметр FindAllO — обобщен­ ный делегат типа System. Predicate<T>. Этот делегат может указывать на любой метод, возвращающий bool и принимающий единственный параметр: // Этот делегат используется методом FindAllO для извлечения подмножества. public delegate bool Predicate<T>(Т obj); Когда вызывается FindAllO, каждый элемент в List<T> передается методу, указанно­ му объектом Predicate<T>. Реализация этого метода будет производить некоторые вычис­ ления для проверки соответствия элемента данных указанному критерию, возвращая true или false. Если метод вернет true, то текущий элемент будет добавлен в List<T>, пред­ ставляющий искомое подмножество. Прежде чем посмотреть, как лямбда-выражения упро­ щают работу с FindAll (), давайте решим эту задачу в длинной нотации, используя объек­ ты делегатов непосредственно. Добавим метод (по имени TraditionalDelegateSyntaxO) к типу Program, который взаимодействует с System.Predicate<T> для обнаружения чет­ ных чисел в списке List<T> целочисленных значений: class Program { static void Mai n (string [] args) { Console.WriteLine ("***** Fun with Lambdas *****\n"); TraditionalDelegateSyntax (); Console.ReadLine (); } static void TraditionalDelegateSyntaxO { // Создать список целых чисел. List<int> list = new List<int>(); list.AddRange (new int [] { 20, 1, 4, 8, 9 , 44 }); // Вызов F in d A llO с использованием традиционного синтаксиса делегатов. 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 (); ) // Цель для делегата P re d ic a te O . static bool IsEvenNumber(int i) { // Это четное число? return (i % 2) == 0; Глава 11. Делегаты, события и лямбда-выражения 415 Здесь имеется метод (IsEvenNumber ()), отвечающий за проверку входного целочис­ ленного параметра на предмет четности или нечетности через операцию C# взятия мо­ дуля % (получения остатка от деления). В результате запуска приложения на консоль выводятся числа .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 (l d 2) == 0; } ); // Вывод четных чисел. Console.WnteLine ("Here are your even numbers:"); foreach (int evenNumber in evenNumbers) { Console.Write("{0}\t", evenNumber); } Console.WnteLine () ; } В этом случае вместо прямого создания типа делегата 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 .WnteLine () ; 416 Часть III. Дополнительные конструкции программирования на C# Здесь обратите внимание на довольно странный оператор кода, передаваемый ме­ тоду FindAllO, который в действительности и является лямбда-выражением. В этой модификации примера вообще нет никаких следов делегата Predicate<T> (как и клю­ чевого слова delegate). Все, что специфицировано вместо них — это лямбда-выраже­ ние: i ==> (i ь 2) == 0. Прежде чем разбирать синтаксис дальше, пока просто усвойте, что лямбда-выра­ жения могут применяться везде, где использовался анонимный метод или строго ти­ пизированный делегат (обычно в более лаконичном виде). “За кулисами” компилятор C# транслирует выражение в стандартный анонимный метод, использующий тип де­ легата 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 и 2) == 0; }) ; Анализ лямбда-выражения Лямбда-выражение начинается со списка параметров, за которым следует лексема => (лексема C# для лямбда-выражения позаимствована из лямбда-вычислений), а за ней — набор операторов (или единственный оператор), который будет обрабатывать параметры. На самом высоком уровне лямбда-выражение можно представить следую­ щим образом: АргументыДляОбработки => ОбрабатывающиеОператоры То, что находится внутри метода LambdaExpressionSyntaxO, следует понимать так: // i — список параметров. // ( i % 2) = 0 - набор операторов для обработки i . List<int> evenNumbers = list.FindAll(i => (l 1 2) == 0); Параметры лямбда-выражения могут быть типизированы явно или неявно. В на­ стоящий момент тип данных, представляющий параметр i (целое), определяется неяв­ но. Компилятор способен понять, что i — целое, на основе контекста всего лямбда-вы­ ражения, поместив тип данных и имя переменной в пару скобок, как показано ниже: // Теперь установим тип параметров явно. List<int> evenNumbers = list.FindAll ((int i) => (i % 2) == 0) ; Как было показано, если лямбда-выражение имеет одиночный неявно типизирован­ ный параметр, то скобки в списке параметров могут быть опущены. При желании быть последовательным в использовании параметров лямбда-выражений, можно всегда за­ ключать список параметров в скобки, чтобы выражение выглядело так: List<int> evenNumbers = list.FindAll(( i ) => (l 1 2) == 0); И, наконец, обратите внимание, что сейчас выражение не заключено в скобки (разу­ меется, вычисление остатка от деления помещается в скобки, чтобы гарантировать его выполнение перед проверкой равенства). Лямбда-выражение с оператором, заключен­ ным в скобки, выглядит следующим образом: // Теперь заключим в скобки и выражение. List<int> evenNumbers = list.FindAll ((i) => ((l % 2) == 0)); Глава 11. Делегаты, события и лямбда-выражения 417 Теперь, когда известны разные способы построения лямбда-выражения, как его представить в понятных человеку терминах? Оставив чистую математику в стороне, можно привести следующее объяснение: // Список параметров (в данном случае — единственное целое по имени ±) // будет обработан выражением ( i % 2) = 0 . List<int> evenNumbers = list.FindAll((i) => ( ( 1 % 2) == 0)); Обработка аргументов внутри множества операторов Наше первое лямбда-выражение состояло из единственного оператора, который в результате вычисления дает булевское значение. Однако, как должно быть хорошо из­ вестно, многие цели делегатов должны выполнять множество операторов кода. По этой причине C# позволяет строить лямбда-выражения, состоящие из нескольких блоков операторов. Когда выражение должно обрабатывать параметры в нескольких строках кода, понадобится выделить контекст этих операторов с помощью фигурных скобок. Рассмотрим следующую модификацию метода 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.WriteLine(), оператор вычисления остатка от деления разбит на два оператора для повышения читабельно­ сти. Предположим, что каждый из рассмотренных выше методов вызывается в Main(): static void Main(string [] args) { Console.WriteLine ("***** Fun with Lambdas *****\n"); TraditionalDelegateSyntax (); AnonymousMethodSyntax(); Console.WriteLine(); LambdaExpressionSyntax(); Console.ReadLine(); } Запуск этого приложения даст следующий вывод: ***** pun with Lambdas ***** Here are your even numbers: 20 4 8 44 418 Часть III. Дополнительные конструкции программирования на C# Here are 20 4 value of value of value of value of value of value of Here are 20 4 your even numbers : 8 44 l is currently: 20 l is currently: 1 l is currently: 4 l is currently: 8 l is currently: 9 l is currently: 44 your even numbers: 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() может быть реализован так: s ta tic void M ain(string [ ] args) { // Регистрация делегата как лямбда-выражения. SimpleMath m = new SimpleMath(); m. SetMathHandler( (msg, re su lt) => {C on sole.W riteLin e( "Message: {0 }, Result: {1 }" , msg, r e s u l t ) ; } ) ; // Это приведет к выполнению лямбда-выражения. m.Add(10, 10); Console. ReadLine( ) ; } Здесь используется выведение типа компилятором, поскольку для простоты два па­ раметра не типизированы строго. Однако можно было бы вызвать SetMathHandler () следующим образом: m. SetMathHandler( (s trin g msg, in t re su lt) => { Console.W riteL in e( "Message: { 0} , Result: { 1} " , msg, r e s u l t ) ; } ) ; И, наконец, если лямбда-выражение используется для взаимодействия с делегатом, вообще не принимающим параметров, то это можно сделать, указав в качестве пара­ метра пару пустых скобок. Таким образом, предполагая, что определен следующий тип делегата: Глава 11. Делегаты, события и лямбда-выражения 419 public delegate string VerySimpleDelegate (); вот как можно обработать результат вызова: // Вывод на консоль строки "Enjoy your s t r in g !" . VerySimpleDelegate d = new VerySimpleDelegate ( () => {return "Enjoy your string!";} ); Console.WriteLine(d.Invoke () ); Исходный код. Проект LambdaExpressionsMultipleParams доступен в подкаталоге Chapter 11. Усовершенствование примера P r im A n d P r o p e r C a r E v e n t s за счет использования лямбда-выражений Учитывая то, что главное предназначение лямбда-выражений состоит в обеспече­ нии возможности в чистой, сжатой манере определить анонимный метод (и тем самым упростить работу с делегатами), давайте переделаем проект 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.Accelerate(20); Console.ReadLine(); } } Теперь общая роль лямбда-выражений должна проясниться, и становится понятно, что они обеспечивают “функциональную манеру” работы с анонимными методами и ти­ пами делегатов. К новой лямбда-операции (=>) необходимо привыкнуть, однако помни­ те, что любые лямбда-выражения сводятся к следующему простому уравнению: АргументыДляОбработки => ОбрабатывающиеИхОператоры Исходный код. Проект CarEventsWithLambdas доступен в подкаталоге Chapter 11. Резюме В этой главе вы ознакомились с несколькими способами двустороннего взаимодейст­ вия множества объектов. Во-первых, было рассмотрено ключевое слово delegate, исполь­ зуемое для неявного конструирования класса-наследника System.MulticastDelegate. 420 Часть III. Дополнительные конструкции программирования на C# Как было показано, объект делегата поддерживает список методов для вызова тогда, когда ему об этом укажут. Такие вызовы могут выполняться синхронно (с использова­ нием метода Invoke ()) или асинхронно (через методы Be gin Invoke () и Endlnvoke ()). Асинхронная природа типов делегатов .NET будет рассмотрена в главе 19. Во-вторых, вы ознакомились с ключевым словом event, которое в сочетании с ти­ пом делегата может упростить процесс отправки уведомлений о событиях ожидающим объектам. Как видно в результирующем коде CIL, модель событий .NET отображается на скрытые обращения к типам System.Delegate/System.MulticastDelegate. В этом свете ключевое слово event является необязательным и просто позволяет сэкономить на наборе кода. В главе также рассматривалось средство языка С#, которое называется анонимными методами. С помощью этой синтаксической конструкции можно явно ассоциировать блок операторов кода с заданным событием. Как было показано, анонимные методы могут игнорировать параметры, переданные событием, и имеют доступ к “внешним пе­ ременным” определяющего их метода. Вы также ознакомились с упрощенным способом регистрации событий с применением групповых преобразований методов. И, наконец, в завершение главы было дано описание лямбда-операции => в С#. Как было показано, этот синтаксис значительно сокращает нотацию написания анонимных методов, когда набор аргументов может быть передан на обработку группе операторов. ГЛАВА 1 2 Расширенные средства языка C# этой главе рассматриваются некоторые более сложные синтаксические конст­ рукции языка программирования С#. Сначала будет показано, как реализуется и используется метод-индексатор. Этот механизм C# позволяет строить специальные типы, обеспечивающие доступ к внутренним подтипам с применением синтаксиса, по­ хожего на синтаксис массивов. Затем вы узнаете о том, как перегружать различные операции (+, -, <, > и т.д.) и как создавать специальные процедуры явного и неявного преобразования типов (а также причины, по которым это может понадобиться). Далее рассматриваются три темы, которые особенно полезны при работе с APIинтерфейсами LINQ (хотя это применимо и вне контекста LINQ), а именно: расширяю­ щие методы, частичные методы и анонимные типы. И в завершение вы узнаете, как создавать контекст “небезопасного” кода, чтобы напрямую манипулировать неуправляемыми указателями. Хотя использовать указа­ тели в приложениях C# приходится исключительно редко, понимание того, как это делается, может пригодиться в определенных ситуациях со сложными сценариями взаимодействия. В Понятие методов-индексаторов Программисты хорошо знакомы с процессом доступа к индивидуальным элементам, содержащимся в стандартных массивах, через операцию индекса ([ ]). Например: static void Main(string [] args) { // Цикл no аргументам командной строки с использованием операции индекса. for(int i = 0 ; i < args.Length; i++) Console.WnteLine ("Args : {0}", args [l] ) ; // Объявление массива локальных целых. int [] mylnts = { 10, 9, 100, 432, 9874}; // Использование операции индекса для доступа к элементам. for (int з = 0 ; j < mylnts.Length; j++) Console .WnteLine (" Index {0} = {1} ", 3 , mylnts [j]); Console.ReadLine(); } Приведенный код не должен быть для вас чем-то новым. В C# имеется возможность проектировать специальные классы и структуры, которые могут быть индексированы подобно стандартному массиву, посредством определения метода-индексатора. Это 422 Часть III. Дополнительные конструкции программирования на C# конкретное языковое средство наиболее полезно при создании специальных типов кол­ лекций (обобщенных и необобщенных). Прежде чем ознакомиться с реализацией специального индексатора, начнем с рас­ смотрения его в действии. Предположим, что вы добавили поддержку метода-индекса­ тора к пользовательскому типу PeopleCollection, разработанному в главе 10 (в про­ екте CustomNonGenericCollection). Рассмотрим следующее его применение в новом консольном приложении по имени Simple Indexer: // Индексаторы позволяют обращаться к элементам в стиле массива. class Program { static void Main(string[] args) { Console.WriteLine ("***** Fun with Indexers *****\n"); PeopleCollection myPeople = new PeopleCollection (); // Добавление myPeople[0] = myPeople[1] = myPeople[2] = myPeople[3] = myPeople [4] = объектов с помощью синтаксиса индексатора. new Person("Homer", "Simpson", 40); new Person("Marge", "Simpson", 38); new Person("Lisa", "Simpson", 9); new Person("Bart", "Simpson", 7); 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 и IEnumerable (либо их обобщенные версии). Главное отличие, конечно, в том, что вместо доступа к содержимому с использованием конструкции fоreach можно манипулировать внутренней коллекцией подобъектов подобно стандартному массиву. Но тут возникает серьезный вопрос: как сконфигурировать класс PeopleCollection (или любой другой класс либо структуру) для поддержки этой функциональности? Индексатор представляет собой несколько видоизмененное определение свойства. В его простейшей форме индексатор создается с использованием синтаксиса this [ ]. Ниже показано необходимое изменение класса PeopleCollection из главы 10: // Добавим индексатор к существующему определению класса. public class PeopleCollection : IEnumerable { private ArrayList arPeople = new ArrayListO ; // Специальный индексатор для этого класса. public Person this[int index] { get { return (Person)arPeople[index]; } set { arPeople.Insert(index, value); } Глава 12. Расширенные средства языка C# 423 Помимо использования ключевого слова this, индексатор выглядит как объявление любого другого свойства С#. Например, роль конструкции get состоит в возврате кор­ ректного объекта вызывающему коду. Здесь мы фактически и делаем это, делегируя за­ прос к индексатору объекта ArrayList. В противоположность этому, конструкция set отвечает за размещение входящего объекта в контейнере по определенному индексу; в данном примере это достигается вызовом метода Insert () объекта ArrayList. Как видите, индексаторы — это просто еще одна форма синтаксиса, учитывая, что та же функциональность может быть обеспечена с использованием “нормальных” обще­ доступных методов вроде AddPerson () или Get Ре г son ( ) . Тем не менее, поддержка методов-индексаторов в специальных типах коллекций позволяет их легко интегрировать с библиотеками базовых классов .NET. Хотя создание методов-индексаторов — обычное дело при построении специальных коллекций, следует помнить, что обобщенные типы предлагают эту функциональность в готовом виде. В следующем методе используется обобщенный список List<T> объ­ ектов Person. Обратите внимание, что индексатор List<T> можно просто применять непосредственно. static void UseGenencListOf People () { 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. Дополнительные конструкции программирования на C# // Этот индексатор возвращает персону по строковому индексу, 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 .WnteLine (''***** 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 для дос­ тупа к базам данных), то вспомните, что тип DataSet поддерживает свойство по имени Tables, которое возвращает строго типизированную коллекцию DataTableCollection. В свою очередь, в DataTableCollection определены три индексатора для получения объектов DataTable — по порядковому номеру, по дружественным строковым именам и необязательному пространству имен: public sealed class DataTableCollection : InternalDataCollectionBase { // Перегруженные public DataTable public DataTable public DataTable индексаторы. this[string name] { get; } this[string name, string tableNamespace] { get; } this[int index] { get; } Глава 12. Расширенные средства языка C# 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.Ad d ("Mel", "Appleby", 60); // Использовать многомерный индексатор для вывода деталей первой строки. Console.WriteLine("First Name: {0]", myTable.Rows[0][0]); Console .WnteLine ("Last Name : {0]", myTable.Rows[0][1]); Console.WriteLine("Age : {0]", myTable.Rows[0][2]); } Начиная с главы 21, мы продолжим рассмотрение ADO.NET, так что не пугайтесь, если что что-то в приведенном выше коде покажется незнакомым. Этот пример просто иллюстрирует, что методы-индексаторы могут поддерживать множество измерений, а при правильном использовании могут упростить взаимодействие с подобъектами, со­ держащимися в специальных коллекциях. Определения индексаторов в интерфейсных типах Индексаторы могут определяться в типе интерфейса, позволяя поддерживающим типам предоставлять их специальные реализации. 426 Часть III. Дополнительные конструкции программирования на C# Ниже показан пример такого интерфейса, который определяет протокол для получе­ ния строковых объектов с использованием числового индексатора: public interface IStringContainer { // Этот интерфейс определяет индексатор, возвращающий // строки по числовому индексу, string this[int index] { get; set; } } При таком определении интерфейса любой класс или структура, реализующие его, должны поддерживать индексатор чтения/записи, манипулирующий подэлементами через числовое значение. На этом первая главная тема настоящей главы завершена. Хотя понимание синтак­ сиса индексаторов C# важно, как объяснялось в главе 10, обычно единственным слу­ чаем, когда программисту нужно строить специальный обобщенный класс коллекции, является ситуация, когда необходимо добавить ограничения к параметрам-типам. Если придется строить такой класс, добавление специального индексатора может заставить класс коллекции выглядеть и вести себя подобно стандартному классу коллекции из библиотеки базовых классов .NET. А теперь давайте рассмотрим языковое средство, позволяющее строить специальные классы и структуры, которые уникальным образом реагируют на встроенные операции C# — перегрузку операций. Понятие перегрузки операций В С#, подобно любому языку программирования, имеется готовый набор лексем, ис­ пользуемых для выполнения базовых операций над встроенными типами. Например, известно, что операция + может применяться к двум целым, чтобы дать их сумму: // Операция + с целыми. int а = 10 0 ; int Ь = 240; int с = а + b; / / с теперь равно 340 Здесь нет ничего нового, но задумывались ли вы когда-нибудь о том, что одна и та же операция + может применяться к большинству встроенных типов данных С#? Например, рассмотрим такой код: // Операция string si = string s2 = string s3 = + со строками. "Hello"; " world!"; si + s2; // s3 теперь содержит "Hello world!" По сути, функциональность операции + уникальным образом базируются на пред­ ставленных типах данных (строках или целых в данном случае). Когда операция + при­ меняется к числовым типам, мы получаем арифметическую сумму операндов. Однако когда та же операция применяется к строковым типам, получается конкатенация строк. Язык C# предоставляет возможность строить специальные классы и структуры, ко­ торые также уникально реагируют на один и тот же набор базовых лексем (вроде опе­ рации +). Имейте в виду, что абсолютно каждую встроенную операцию C# перегружать нельзя. В табл. 12.1 описаны возможности перегрузки основных операций. Глава 12. Расширенные средства языка C# 427 Таблица 12.1. Возможности перегрузки операций C# Возможность перегрузки Операция C# +, , ~, ! ++, Этот набор унарных операций может быть перегружен — , tru e, fa ls e +, * , /, %, &, I , A , « , >> Эти бинарные операции могут быть перегружены и A if V a" if v" if II Эти операции сравнения могут быть перегружены. C# требует совместной перегрузки “подобных” операций (т.е. < и >, < = и > = , = = и != ) [] Операция [ ] не может быть перегружена. Как было показано ра­ нее в этой главе, однако, аналогичную функциональность пред­ лагают индексаторы 0 Операция ( ) не может быть перегружена. Однако, как будет по­ казано далее в этой главе, ту же функциональность предоставля­ ют специальные методы преобразования + = , - = , * = , / = , %=, &= , A=, « = , >>= 1= , Сокращенные операции присваивания не могут перегружаться; однако вы получаете их автоматически, перегружая соответст­ вующую бинарную операцию Перегрузка бинарных операций Чтобы проиллюстрировать процесс перегрузки бинарных операций, представим сле­ дующий простой класс Point, определенный в новом консольном приложении по имени OverloadedOps: // Простой класс C# для повседневного пользования, 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); Теперь, логически рассуждая, имеет смысл складывать экземпляры P o in t вместе. Например, если сложить вместе две переменных Poin t, получится новая P o in t с суммар­ ными значениями х и у. Кстати, также может быть полезно иметь возможность вычитать одну P o in t из другой. В идеале пригодилась бы возможность написать такой код: // Сложение и вычитание двух точек? static void Main(string[] args) { Console.WriteLine("***** Fun with Overloaded Operators *****\n"); // Создать две точки. Point ptOne = new Point (100, 100); Point ptTwo = new Point (40, 40); Console.WriteLine("ptOne = {0}", ptOne); Console.WriteLine("ptTwo = {0}", ptTwo); 428 Часть III. Дополнительные конструкции программирования на C# // Сложить две точки, чтобы получить большую? Console.WriteLine("ptOne + ptTwo: {0} ", ptOne + ptTwo); // Вычесть одну точку из другой, чтобы получить меньшую? Console.WriteLine("ptOne - ptTwo: {0} ", ptOne - ptTwo); Console.ReadLine(); } Однако в том виде, как он есть, класс P o in t приведет к ошибкам этапа компиляции, поскольку типу P o in t не известно, как реагировать на операции + и Чтобы оснастить специальный тип возможностью уникально реагировать на встро­ енные операции, в C# служит ключевое слово o p e ra to r, которое может использовать­ ся только в сочетании со статическими методами. При перегрузке бинарной операции (вроде + и -) чаще всего передаются два аргумента того же типа, что и определяющий их класс (в данном примере — P oin t); это иллюстрируется в следующей модификации кода: // Более интеллектуальный тип 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); } } Логика, положенная в основу операции +, состоит просто в возврате нового экзем­ пляра P o in t на основе сложения соответствующих полей входных параметров P o in t. Поэтому, когда вы напишете p t l + pt2, “за кулисами” произойдет следующий скрытый вызов статического метода o p era to r+ : // Псевдокод: Point рЗ = Point.operator+ (pi, р2) Point рЗ = pi + р2; Аналогично, p i - р2 отображается на следующее: // Псевдокод: Point р4 = Point.operator- (pi, р2) Point р4 = pi - р2; После этого дополнения программа скомпилируется, и мы получим возможность складывать и вычитать объекты P o in t: ptOne ptTwo ptOne ptOne = = + - [100, 100] [40, 40] ptTwo: [140, 140] ptTwo: [60, 60] При перегрузке бинарной операции вы не обязаны передавать ей два параметра оди­ накового типа. Если это имеет смысл, один из аргументов может отличаться. Например, ниже показана перегруженная операция +, которая позволяет вызывающему коду полу­ чить новый объект P o in t на основе числового смещения: public class Point public static Point operator + (Point pi, int change) { return new Point(pi.X + change, pl.Y + change); } Глава 12. Расширенные средства языка C# 429 public static Point operator + (int change, Point pi) { return new Point(pl.X + change, pl.Y + change); } } Обратите внимание, что если нужно передавать аргументы в любом порядке, по­ требуются обе версии метода (т.е. нельзя просто определить один из методов и рассчи­ тывать, что компилятор автоматически будет поддерживать другой). Теперь можно ис­ пользовать эти новые версии операции + следующим образом: // Выводит [110, 110] Point biggerPoint = ptOne + 10; Console .WnteLine ("ptOne + 10 = {0}", biggerPoint); // Выводит [120, 120] Console. WnteLine ("10 + biggerPoint = {0]", 10 + biggerPoint); Console.WriteLine (); А как насчет операций += и -= ? Перешедших на C# с языка C++ может удивить отсутствие возможности перегрузки операций сокращенного присваивания (+=, -+ и т.д.). Не беспокойтесь. В терминах C# операции сокращенного присваивания автоматически эмулируются при перегрузке со­ ответствующих бинарных операций. Таким образом, если в классе P o in t уже перегру­ жены операции + и -, можно написать следующий код: // Перегрузка бинарных операций автоматически обеспечивает // перегрузку сокращенных операций. static void Main (string[] args) // Операция += автоматически перегружена Point ptThree = new Point (90, 5) ; Console .WnteLine ("ptThree = {0}", ptThree); Console.WriteLine("ptThree += ptTwo: {0}", ptThree += ptTwo); // Операция -= автоматически перегружена Point ptFour = new Point (0, 500); Console.WriteLine("ptFour = {0}", ptFour); Console.WriteLine ("ptFour -= ptThree: {0}", ptFour -= ptThree); Console.ReadLine(); } Перегрузка унарных операций В C# также допускается перегружать и унарные операции, такие как ++ и — . При перегрузке унарной операции также определяется статический метод через ключевое слово o p e ra to r, однако в этом случае просто передается единственный параметр того же типа, что и определяющий его класс/структура. Например, дополните P o in t сле­ дующими перегруженными операциями: 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. Дополнительные конструкции программирования на C# В результате появляется возможность выполнять инкремент и декремент значений X и Y класса Point, как показано ниже: static void Main(string [] args) // Применение унарных операций ++ и — к Point. Point ptFive = new Point (1, 1); Console.WriteLine("++ptFive = {0}", ++ptFive); // [2, 2] Console.WriteLine("--ptFive = {0}", — ptFive); // [1, 1] // Применение тех же операций для постфиксного инкремента/декремента. Point ptSix = new Point (20, 20); Console.WriteLine("ptSix++ = {0}", ptSix++); // [20, 20] Console .WriteLine ("ptSix— = {0}", ptSix— ); // [21, 21] Console.ReadLine(); } В предыдущем примере кода обратите внимание, что специальные операции ++ и — применяются двумя разными способами. В C++ допускается перегружать операции префиксного и постфиксного инкремента/декремента по отдельности. В C# это невоз­ можно; тем не менее, возвращаемое значение инкремента/декремента автоматически обрабатывается правильно (т.е., для перегруженной операции ++ выражение pt++ имеет значение ^модифицированного объекта, в то время как ++pt имеет новое значение, примененное перед использованием выражения). Перегрузка операций эквивалентности Как вы должны помнить из главы 6, метод System.Object .Equals () может быть перегружен для выполнения сравнений объектов на основе значений (а не ссы­ лок). Если вы решите переопределить Equals () (часто вместе со связанным методом System. Object.GetHashCode ()), это позволит легко переопределить и операции про­ верки эквивалентности (== и ! =). Для иллюстрации рассмотрим модифицированное оп­ ределение типа Point: // Этот вариант Point также перегружает операции == и '=. public class Point public override bool Equals(object o) { return o . T o S t n n g O == this.ToString(); } public override int GetHashCode () { return this.ToString().GetHashCode(); } // Теперь перегрузим операции == и !=. public static bool operator == (Point pi, Point p2) { return p i .Equals(p2 ); } public static bool operator != (Point pi, Point p2) { return 1p i .Equals(p2 ); } } Обратите внимание, что для выполнения нужной работы реализации операций == и ! = просто вызывают перегруженный метод Equals () . Теперь класс Point можно ис­ пользовать следующим образом: // Использование перегруженных операций эквивалентности, static void Main(string [] args) Console.WriteLine("ptOne == ptTwo : {0}", ptOne == ptTwo); Глава 12. Расширенные средства языка C# 431 Console.WriteLine("ptOne != ptTwo : {0}", ptOne != ptTwo); Console.ReadLine(); } Как видите, сравнение двух объектов с применением хорошо знакомых операций == и != выглядит намного интуитивно понятней, чем вызов Object .Equals ( ) . При пере­ грузке операций эквивалентности для определенного класса помните, что C# требует, чтобы в случае перегрузки операции == обязательно перегружалась также и операция ! = (компилятор напомнит, если вы забудете это сделать). Перегрузка операций сравнения В главе 9 было показано, как реализовать интерфейс I Comparable для выполнения сравнений двух сходных объектов. Вдобавок для того же класса можно перегрузить опе­ рации сравнения (<, >, <= и >=). Подобно операциям эквивалентности, C# и здесь требует, чтобы в случае перегрузки операции < обязательно перегружалась также и операция >. После перегрузки в классе P o in t этих операций сравнения пользователь объекта смо­ жет сравнивать объекты P o in t следующим образом: // Использование перегруженных операций < и >. static void Main(string [] args) Console.WriteLine ("ptOne < ptTwo : {0}", ptOne < ptTwo); Console.WriteLine ("ptOne > ptTwo : {0}", ptOne > ptTwo); Console.ReadLine(); } Предполагая, что интерфейс IComp a rable уже реализован, перегрузка операций сравнения становится тривиальной. Ниже показано модифицированное определение класса. // Объекты 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. Дополнительные конструкции программирования на C# Внутреннее представление перегруженных операций Подобно любому программному элементу С#, перегруженные операции имеют спе­ циальное представление в синтаксисе CIL. Чтобы начать исследование того, что проис­ ходит “за кулисами”, откройте сборку OverloadedOps .ехе в утилите ildasm.exe. Как видно на рис. 12.1, перегруженные операции внутренне представлены в виде скрытых методов (op_Addition (), op_Subtraction (), op_Equality () и т.д.). Рис. 12.1. В терминах CIL перегруженные операции отображаются на скрытые методы Если посмотреть на инструкции CIl[ для метода op Addition (того, что принимает два параметра 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. Отображение операций C# на специально именованные методы СИ Встроенная операция C# Представление CIL — op Decrement() ++ op Increment() + op Addition() * op Subtraction() / == op Division() > op GreaterThan() < op LessThan() op Multiply() op Equality() Глава 12. Расширенные средства языка C# 433 Окончание табл. 12.2 Встроенная операция C# Представление CIL i— op Inequality() >= op GreaterThanOrEqual() <= op LessThanOrEqual() -= op SubtractionAssignment() += op AdditionAssignment() Финальные соображения относительно перегрузки операций Как уже было показано, C# предлагает возможность строить типы, которые могут уникальным образом реагировать на различные встроенные, хорошо известные опера­ ции. Теперь перед добавлением поддержки этого поведения в классы необходимо убе­ диться в том, что операции, которые вы собираетесь перегружать, имеют хоть какой-то смысл в реальном мире. Например, предположим, что перегружена операция умножения для класса MiniVan (минивэн). Что вообще должно означать перемножение двух объектов MiniVan? Не слишком много. Фактически, если коллеги по команде увидят следующее использова­ ние объектов MiniVan, то будут весьма озадачены: // Это не слишком понятно. .. MiniVan newVan = myVan * yourVan; Перегрузка операций обычно полезна только при построении служебных типов. Строки, точки, прямоугольники, функции и шестиугольники — подходящие кандидаты на перегрузку операций. Люди, менеджеры, автомобили, подключения к базе данных и веб-страницы — нет. В качестве эмпирического правила: если перегруженная опера­ ция затрудняет пользователю понимание функциональности типа, не делайте этого. Используйте это средство с умом. Также имейте в виду, что даже если вы не хотите перегружать операции в специаль­ ных классах, это уже сделано в многочисленных типах из библиотек базовых классов. Например, сборка System.Drawing.dll предлагает определение Point, применяемое в Windows Forms, в котором перегружено множество операций. Обратите внимание на значок операции в браузере объектов Visual Studio 2010 (рис. 12.2). B row s e .NET Fram ew ork 4 ;<Search> ■ = 3 ® ' ,, Im a g e G e tT h u m b n aillm ag eA b o rl • Im a g e A m m a to r K now nC olor + Г X Derived Types R ectangle ==(System Draw ing.Poin t, S ystem .D raw ing.P ointj public static S y s te m -D ra w in g ,P o in t o p e r a to r (S ystem . D ra w in g ,P o in t p r, S y s te m ,D ra w in g ,S iz e sz) Member o f S ys te m -D ra w in g .P o in t Point ’[^|Base Types P om tC onverter [~j k E m p ty Pens Pom tF ш im p lic it operator(System D raw ing Point) Г 5 o p erato r Pen ^ !> m o p erato r -(S ystem Draw ing Point, S yste m D ra w in g S iz e ) Im a g e F o rm atC o n verter > m , exp Ik rt oper ator(Systcm . D raw in g Point) o p erato r l=(S ys tem .D raw in g .P o in t System D raw in g .P o in t) Im ageC orv. erter Л - ‘s e t . .. Truncate(S ys tem .D raw ing Pom tF) ^ 1- * .1 1 S u m m a ry : Translates a System Drawing.Point by a given System.Drawing.Size Рис. 12.2. Множество типов в библиотеках базовых классов включают уже перегруженные операции 434 Часть III. Дополнительные конструкции программирования на C# Исходный код. Проект O verloadedO ps доступен в подкаталоге Chapter 12. Понятие преобразований пользовательских типов Теперь обратимся к теме, близкой к перегрузке операций — преобразованию поль­ зовательских типов. Чтобы заложить фундамент для последующей дискуссии, давайте кратко опишем нотацию явного и неявного преобразования между числовыми данными и связанными с ними типами классов. Числовые преобразования В терминах встроенных числовых типов (s b y te , in t , f l o a t и т.п.) явное преобра­ зование требуется при попытке сохранить большее значение в меньшем контейнере, поскольку это может привести к потере данных. По сути, это означает, что вы говорите компилятору: “Я знаю, что делаю”. В противоположность этому неявное преобразование происходит автоматически, когда вы пытаетесь поместить меньший тип в целевой тип, и в результате этой операции не происходит потеря данных: static void Main() { int a = 123; long b = a; int c = (int) b; // Неявное преобразование int в long // Явное преобразование long в int Преобразования между связанными типами классов Как было показано в главе 6, типы классов могут быть связаны классическим на­ следованием (отношение “является” (“is а”)). В этом случае процесс преобразования C# позволяет выполнять приведение вверх и вниз по иерархии классов. Например, класснаследник всегда может быть неявно приведен к базовому типу. Однако если необхо­ димо хранить тип базового класса в переменной типа класса-наследника, понадобится явное приведение: // Два связанных типа классов, class Base{} class Derived : Base{} class Program { static void Main(string [] args) { // Неявное приведение наследника к предку. Base myBaseType; myBaseType = new Derived (); // Для хранения базовой ссылке в ссылке //на наследника нужно юное преобразование. Derived myDerivedType = (Derived)myBaseType; } } Это явное приведение работает благодаря тому факту, что классы Base и D erived связаны отношением классического наследования. Однако что если есть два типа клас­ сов из разных иерархий без общего предка (кроме S ystem .O b ject), которые требуют преобразования друг в друга? Если они не связаны классическим наследованием, явное приведение здесь не поможет. Глава 12. Расширенные средства языка C# 435 Рассмотрим типы значений, такие как структуры. Предположим, что определены две структуры .NET с именами Square и Rectangle. Учитывая, что они не могут пола­ гаться на классическое наследование (поскольку всегда запечатаны), нет естественного способа выполнить приведение между этими, на первый взгляд, связанными типами. Наряду с возможностью создания в структурах вспомогательных методов (вроде Rectangle.ToSquare О ), язык C# позволяет строить специальные процедуры преоб­ разования, которые позволят типам реагировать на операцию приведения ( ) . Таким образом, если корректно сконфигурировать эти структуры, можно будет использовать следующий синтаксис для явного преобразования между ними: // Преобразовать Rectangle в Square! Rectangle rect; rect.Width = 3; rect.Height = 1 0 ; Square sq = (Square)rect; Создание специальных процедур преобразования Начнем с создания нового консольного приложения по имени CustomC on vers ions. В C# предусмотрены два ключевых слова — 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. Дополнительные конструкции программирования на C# 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 *****\n"); // Создать Rectangle. Rectangle r = new Rectangle(15, 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. Расширенные средства языка C# 437 Имея операцию явного преобразования в тип Square, можно передавать типы Rectangle для обработки этому методу, используя явное приведение: static void Main(string[] args) { // Преобразовать Rectangle в Square для вызова метода. Rectangle rect = new Rectangle(10, 5); DrawSquare((Square)rect); Console.ReadLine(); } Рис. 12.3. Преобразование Rectangle в Square Дополнительные явные преобразования типа S q u a r e Теперь, когда можно явно преобразовывать объекты Rectangle в объекты Square, давайте рассмотрим несколько дополнительных явных преобразований. Учитывая, что квадрат симметричен по всем сторонам, может быть полезно предусмотреть процедуру преобразования, которая позволит вызывающему коду привести целочисленный тип к типу Square (который, разумеется, будет иметь длину стороны, равную переданному целому). Аналогично, что если вы захотите модифицировать Square так, чтобы вызы­ вающий код мог выполнять приведение из Square в System. Int32? Логика вызова вы­ глядит следующим образом. static void Main(string[] args) { // Преобразование int в Square. Square sq2 = (Square)90; Console.WriteLine("sq2 = {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. Дополнительные конструкции программирования на C# 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 х 2) г.Width = s .Length * 2 ; return г; Глава 12. Расширенные средства языка C# 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(); • } Внутреннее представление процедур пользовательских преобразований Подобно перегруженным операциям, методы, квалифицированные ключевыми сло­ вами i m p l i c i t или e x p l i c i t , имеют специальные имена в терминах CIL: op l m p l i c i t и op E x p l i c i t , соответственно (рис. 12.4). Рис. 12.4. Представление CIL определяемых пользователем процедур преобразования На заметку! В браузере объектов Visual Studio 2010 операции пользовательских преобразований отображаются с использованием значков “ явная операция" и “ неявная операция". На этом рассмотрение определений операций пользовательского преобразования завершено. Как и с перегруженными операциями, здесь следует помнить, что данный фрагмент синтаксиса представляет собой просто сокращенное обозначение “нормаль­ ных” функций-членов, и в этом смысле является необязательным. Однако в случае пра­ вильного применения пользовательские структуры могут использоваться более естест­ венно, поскольку трактуются как настоящие типы классов, связанные наследованием. Исходный код. Проект Custom Conversions доступен в подкаталоге C h apter 12. 440 Часть III. Дополнительные конструкции программирования на C# Понятие расширяющих методов В .NET 3.5 появилась концепция расширяющих методов (extension method), которая позволила добавлять новую функциональность к предварительно скомпилированным типам “на лету”. Известно, что как только тип определен и скомпилирован в сборку .NET, его определение становится более-менее окончательным. Единственный способ добавления новых членов, обновления или удаления членов состоит в перекодирова­ нии и перекомпиляции кодовой базы в обновленную сборку (или же можно прибегнуть к более радикальным мерам, таким как использование пространства имен System. Ref lection.Emit для динамического изменения скомпилированного типа в памяти). Теперь в C# можно определять расширяющие методы. Суть расширяющих методов в том, что они позволяют существующим скомпилированным типам (а именно — клас­ сам, структурам или реализациям интерфейсов), а также типам, которые в данный мо­ мент компилируются (такие как типы в проекте, содержащем расширяющие методы), получать новую функциональность без необходимости в непосредственном изменении расширяемого типа. Эта техника может оказаться полезной, когда нужно внедрить новую функциональ­ ность в типы, исходный код которых не доступен. Также она может пригодиться, когда необходимо заставить тип поддерживать набор членов (в интересах полиморфизма), но вы не можете модифицировать его исходное объявление. Механизм расширяющих методов позволяет добавлять функциональность к предварительно скомпилированным типам, создавая иллюзию, что она была у него всегда. На заметку! Имейте в виду, что расширяющие методы на самом деле не изменяют скомпили­ рованную кодовую базу! Эта техника лишь добавляет члены к типу в контексте текущего приложения. При определении расширяющих методов первое ограничение состоит в том, что они должны быть определены внутри статического класса (см. главу 5), и потому каж­ дый расширяющий метод должен быть объявлен с ключевым словом static. Второй момент состоит в том, что все расширяющие методы помечаются таковыми посредст­ вом ключевого слова this в виде модификатора первого (и только первого) параметра данного метода. Третий момент — каждый расширяющий метод может быть вызван либо от текущего экземпляра в памяти, либо статически, через определенный стати­ ческий класс! Звучит странно? Давайте рассмотрим полный пример, чтобы прояснить картину. О пределение расш иряю щ их методов Создадим новое консольное приложение по имени ExtensionMethods. Теперь пред­ положим, что строится новый служебный класс по имени MyExtensions, в котором оп­ ределены два расширяющих метода. Первый позволяет любому объекту из библиотек базовых классов .NET получить новый метод по имени DisplayDef iningAssembly (), который использует типы из пространства имен System. Ref lection для отображения сборки указанного типа. На заметку! API-интерфейс рефлексии формально рассматривается в главе 15. Если эта тема яв­ ляется новой, просто знайте, что рефлексия позволяет исследовать структуру сборок, типов и членов типов во время выполнения. Второй расширяющий метод по имени ReverseDigits () позволяет любому экземп­ ляру System. Int32 получить новую версию себя, но с обратным порядком следования Глава 12. Расширенные средства языка C# 441 цифр. Например, если на целом значении 1234 вызвать ReverseDigits () , возвращен­ ное целое значение будет равно 4321. Взгляните на следующую реализацию класса (не забудьте импортировать пространство имен System.Reflection): static class MyExtensions { // Этот метод позволяет любому объекту отобразить // сборку, в которой он определен. public static void DisplayDefiningAssembly(this object obj) { Console.WnteLine ("{ 0 } lives here: => {l}\n", obj.GetType().Name, Assembly.GetAssembly(obj.GetType()).GetName().Name); // Этот метод позволяет любому целому изменить порядок следования // десятичных цифр на обратный. Например, 56 превратится в 65. public static int ReverseDigits(this int i) { // Транслировать int в string и затем получить все его символы. char[] digits = i .ToStnng () .ToCharArray () ; // Изменить порядок элементов массива. Array.Reverse(digits); // Вставить обратно в строку. string newDigits = new string(digits); // Вернуть модифицированную строку как int. return int.Parse(newDigits); Обратите внимание, что первый параметр каждого расширяющего метода квали­ фицирован ключевым словом this, перед определением типа параметра. Первый па­ раметр расширяющего метода всегда представляет расширяемый тип. Учитывая, что DisplayDef iningAssembly () прототипирован расширять System. Object, любой тип в любой сборке теперь получает этот новый член. Однако ReverseDigits () прототипиро­ ван только для расширения целочисленных типов, и потому если что-то другое попыта­ ется вызвать этот метод, возникнет ошибка времени компиляции. Знайте, что каждый расширяющий метод может иметь множество параметров, но только первый параметр может быть квалифицирован как this. Например, вот как вы­ глядит перегруженный расширяющий метод, определенный в другом служебном классе по имени TestUtilClass: static class TesterUtilClass { // Каждый Int32 теперь имеет метод Foo()... public static void Foo(this int i) { Console.WriteLine ("{0 } called the Foo() method.", l); } // ...который перегружен для приема параметра string! public static void Foo(this int i, string msg) { Console.WriteLine ("{0 } called Foo() and told me: {1}", l, msg); } } Вызов расш иряю щ их методов на уровне экземпляра После определения этих расширяющих методов теперь все объекты (в том числе, конечно же, все содержимое библиотек базовых классов .NET) имеют метод по имени DisplayDef iningAssembly () , в то время как типы System. Int 32 (и только целые) — методы ReverseDigits () и Foo (): 442 Часть III. Дополнительные конструкции программирования на C# 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; // Ошибка! Booleans не имеет метода Foo()! // Ь2 .Foo (); Console.ReadLine(); Ниже показан вывод этой программы: ***** Fun with Extension Methods ***** Int32 lives here: => mscorlib DataSet lives here: => System.Data SoundPlayer lives here: => System Value of Reversed 12345678 12345678 mylnt: digits called called 12345678 of mylnt: 87654321 the Foo () method. Foo () and told me: Ints that Foo? Who would have thought it! Вызов расш иряю щ их методов статически Вспомните, что первый параметр расширяющего метода помечен ключевым словом th is , а за ним следует тип элемента, к которому метод применяется. Если вы посмотри­ те, что происходит “за кулисами” (с помощью инструмента вроде ild a sm .ex e), то обна­ ружите, что компилятор просто вызывает “нормальный” статический метод, передавая переменную, на которой вызывается метод, в первом параметре (т.е. в качестве значе­ ния th is ). Ниже показаны примерные подстановки кода. private static void Main(string [] args) { Console .WriteLine (''***** Fun with Extension Methods *****\n"); int mylnt = 12345678; MyExtensions.DisplayDefiningAssembly(mylnt); System. Data.DataSet d = new DataSetO; 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. Расширенные средства языка C# 443 Учитывая, что вызов расширяющего метода из объекта (что похоже на вызов метода уровня экземпляра) — это просто эффект “дымовой завесы”, создаваемый компилято­ ром, расширяющие методы всегда можно вызвать как нормальные статические методы, используя привычный синтаксис C# (как показано выше). Контекст расширяющ его метода Как только что объяснялось, расширяющие методы — это, по сути, статические методы, которые могут быть вызваны от экземпляра расширяемого типа. Поскольку это — разновидность синтаксического “украшения”, важно понимать, что в отличие от “нормального” метода, расширяющий метод не имеет прямого доступа к членам типа, который он расширяет. Иначе говоря, расширение — это не наследование. Взгляните на следующий простой тип Саг: public class Car { public int Speed; public int SpeedUp () { return ++Speed; Построив расширяющий метод для типа Саг по имени 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 --c.Speed; Теперь можно создать объект Саг и вызывать методы SpeedUp () и SlowDown (), как показано ниже: static void UseCarO { Car с = new Car () ; Console.WnteLine ("Speed: {0}", c .SpeedUp ()) ; Console.WriteLine("Speed: {0}", c .SlowDown()); 444 Часть III. Дополнительные конструкции программирования на C# Импорт типов, которые определяют расш иряю щ ие методы В случае выделения набора статических классов, содержащих расширяющие мето­ ды, в уникальное пространство имен другие пространства имен в этой сборке использу­ ют стандартное ключевое слово 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. Расширенные средства языка C# 445 К счастью, средство IntelliSense в Visual Studio маркирует все расширяющие методы уникальным значком с изображением синей стрелки вниз (рис. 12.5). Рис. 12 .5. Отображение расширяющих методов в IntelliSense Если метод помечен этим значком, это означает, что он определен вне исходного определения класса, через механизм расширяющих методов. Исходный код. Проект ExtensionMethods доступен в подкаталоге Chapter 12. Построение и использование библиотек расш ирений В предыдущем примере производилось расширение функциональности различных типов (таких как System. Int32) для использования в текущем консольном приложе­ нии. Представьте, насколько было бы полезно построить библиотеку кода .NET, опреде­ ляющую расширения, на которые могли бы ссылаться многие приложения. К счастью, сделать это очень легко. Подробности создания и конфшурирования специальных библиотек будут рассмат­ риваться в главе 14; а пока, если хотите реализовать самостоятельно приведенный здесь пример, создайте проект библиотеки классов по имени MyExtensionLibrary. Затем пе­ реименуйте начальный файл кода C# на MyExtensions .cs и скопируйте определение класса MyExtensions в новое пространство имен: namespace MyExtensionsLibrary { // Не забудьте импортировать System.Reflection! public static class MyExtensions { // Та же реализация, что и раньше. public static void DisplayDefiningAssembly(this object obj) {...} // Та же реализация, что и раньше. public static int ReverseDigits(this int l) { ...} } } На заметку! Чтобы можно было экспортировать расширяющие методы из библиотеки кода .NET, определяющий их тип должен быть объявлен с ключевым словом public (вспомните, что по умолчанию действует модификатор доступа internal). 446 Часть III. Дополнительные конструкции программирования на C# П осле этого можно ском п и ли ровать б и бли отек у и ссы латься на сборку 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; myInt.DisplayDefiningAssembly() ; Console.WriteLine("{0} is reversed to {1}", mylnt, mylnt.ReverseDigits() ); Console.ReadLine(); } } В Microsoft рекомендуют размещать типы, которые имеют расширяющие методы, в отдельной сборке (внутри выделенного пространства имен). Причина проста — сокра­ щение сложности программной среды. Например, если вы напишете базовую библиоте­ ку для внутреннего использования в компании, причем в корневом пространстве имен этой библиотеки определено 30 расширяющих методов, то в конечном итоге все прило­ жения будут видеть эти методы в списках IntelliSense (даже если они и не нужны). Исходный код. Проекты MyExtensionsLibrary и MyExtensionsLibraryClient доступны в подкаталоге Chapter 12. Расш ирение интерфейсных типов через расш иряю щ ие методы Итак, было показано, каким образом расширять классы (а также структуры, которые следуют тому же синтаксису) новой функциональностью через расширяющие методы. Чтобы завершить исследование расширяющих методов С#, следует отметить, что новы­ ми методами можно также расширять и интерфейсные типы; однако семантика такого действия определенно несколько отличается от того, что можно было бы ожидать. Создадим новое консольное приложение по имени InterfaceExtensions, а в нем — простой интерфейсный тип (IBasicMath), включающий единственный метод по име­ ни Add ( ) . Затем подходящим образом реализуем этот интерфейс в каком-нибудь типе класса (Муса 1с). Например: // Определение обычного интерфейса на С#. interface IBasicMath Глава 12. Расширенные средства языка C# 447 int Add(int x f 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 itf, 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.WnteLine ("***** Extending an interface *****\n "); // Вызов членов IBasicMath из объекта MyCalc. MyCalc c = new MyCalc (); Console.WnteLine ("1 + 2 = {0}", c.Add(l, 2)); Console.WriteLine("1 - 2 = {0}", c .Subtract(1, 2)); // Для вызова расширения можно выполнить приведение к IBasicMath. Console. WnteLine ("30 - 9 = {0}", ((IBasicMath) с ). Subtract (30, 9)); // Это не будет работать! // IBasicMath ltfBM = new IBasicMath (); // itfBM.Subtract (10, 10); Console.ReadLine (); } 448 Часть III. Дополнительные конструкции программирования на C# На этом исследование расширяющих методов C# завершено. Помните, что это кон­ кретное языковое средство может быть очень полезным, когда нужно расширить функ­ циональность типа, даже если нет доступа к первоначальному исходному коду (или если тип запечатан), в целях поддержания полиморфизма. Во многом подобно неявно типизированным локальным переменным, расширяющие методы являются ключевым элементом работы с A PI-интерфейсом LINQ. Как будет показано в следующей главе, множество существующих типов в библиотеках базовых классов расширены новой функциональностью через расширяющие методы, что позволяет им интегрироваться в программную модель LINQ. Исходный код. Проект InterfaceExtension доступен в подкаталоге Chapter 12. Понятие частичных методов Начиная с версии .NET 2.0, строить определения частичных классов стало возмож­ но с использованием ключевого слова p a r t i a l (см. главу 5). Вспомните, что эта деталь синтаксиса позволяет разбивать полную реализацию типа на несколько файлов кода (или других мест, таких как память). До тех пор, пока каждый аспект частичного типа имеет одно полностью квалифицированное имя, конечным результатом будет “нормаль­ ный” скомпилированный класс, находящийся в созданной компилятором сборке. Язык C# расширяет роль ключевого слова p a r t i a l , позволяя его применять на уров­ не метода. По сути, это дает возможность прототипировать метод в одном файле, а реа­ лизовать в другом. При наличии опыта работы в C++, это может напомнить отношения между файлами заголовков и реализаций C++. Тем не менее, частичные методы C# об­ ладают рядом важных ограничений. • Частичные методы могут определяться только внутри частичного класса. • Частичные методы должны возвращать void. • Частичные методы могут быть статическими или методами экземпляра. • Частичные метода могут иметь аргументы (включая параметры с модификатора­ ми this, ref или params, но не с out). • Частичные метода всегда неявно приватные (private). Еще более странным является тот факт, что частичный метод может быть как поме­ щен, так и не помещен в скомпилированную сборку! Для прояснения картины давайте рассмотрим пример. Первый взгляд на частичные методы Для оценки влияния определения частичного метода создадим проект консольно­ го приложения по имени PartialMethods. Затем определим новый класс CarLocator внутри файла C# по имени CarLocator.cs: // CarLocator.cs partial class CarLocator { // Этот член всегда будет частью класса CarLocator. public bool CarAvailablelnZipCode (string zipCode) { // Этот вызов *может* быть частью реализации данного метода. VenfyDuplicates (zipCode) ; // Некоторая интересная логика взаимодействия с базой данных... return true; } Глава 12. Расширенные средства языка C# 449 // Этот член *может* быть частью класса CarLocator! partial void V e n f yDuplicates (string make); } Обратите внимание, что метод Verif yDuplicates () определен с модифика­ тором partial и не имеет определения тела внутри этого файла. Кроме того, ме­ тод CarAvailablelnZipCode () содержит вызов VerifyDuplicates () внутри своей реализации. Скомпилировав это приложение в таком, как оно есть виде, и открыв скомпилиро­ ванную сборку в утилите 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 1 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, то компилятор принимает решение о том, нужно ли его включить в сборку, в зависимости от нали­ чия у этого метода тела или же просто пустой сигнатуры. Если у метода нет тела, все его упоминания (вызовы, описания метаданных, прототипы) на этапе компиляции отбрасываются. В некоторых отношениях частичные методы C# — это строго типизированная вер­ сия условной компиляции кода (через директивы препроцессора #if, #elif и #endif). Однако основное отличие состоит в том, что частичный метод будет полностью проиг­ 450 Часть III. Дополнительные конструкции программирования на C# норирован во время компиляции (независимо от настроек сборки), если отсутствует его соответствующая реализация. Использование частичных методов Учитывая ограничения, присущие частичным методам, самое важное из которых связано с тем, что они должны быть неявно private и всегда возвращать void, сразу представить множество полезных применений этого средства языка может быть труд­ но. По правде говоря, из всех языковых средств C# частичные методы кажутся наиме­ нее востребованными. В текущем примере метод VerifyDuplicates () помечен как частичный в демонст­ рационных целях. Однако предположим, что этот метод, будучи реализованным, выпол­ няет некоторые очень интенсивные вычисления. Снабжение этого метода модификатором 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. Понятие анонимных типов Как объектно-ориентированный программист, вы знаете преимущества классов в отношении представления состояния и функциональности заданной программной сущности. То есть, когда нужно определить класс, который предполагает многократное использование и предоставляет обширную функциональность через набор методов, со­ бытий, свойств и специальных конструкто