.NET
Архитектура и программирование
на Visual C++
.NET Architecture and Programming
usingVisual C + + PETERTHORSTE N ISON,ROBERTJ.OBERG
n ricitR evie H aN lJ P T R07458 UpperSadP d e l r , www p.hcp o .rtm
.NET
Архитектура и программирование
на Visual C++ ПИТЕР ТОРСТЕИНСОН и РОБЕРТ ОБЕРГ
вильямс
Издательский дом "Вильяме" Москва • Санкт-Петербург • Киев 2002
ББК 32.973.26-018.2.75 013 УДК 681.3.07 Издательский дом "Вильяме" Зав. редакцией ВВ. Александров Перевод с английского В.Н. Заики, Д.Г. Ковальчука, И.В. Константинова, О. В. Котовича и Я-К. Шмидского Под редакцией Я-К. Шмидского По общим вопросам обращайтесь в Издательский дом "Вильяме" по адресу:
[email protected], http://www.williamspublishing.com Оберг, Роберт, Дж., Торстейнсон, Питер. OI3
Архитектура .NET и программирование с помощью Visual C++.: Пер. с англ. — М.: Издательский дом "Вильяме", 2002. — 656 с.: ил. — Парад, тит. англ. ISBN 5-8459-0379-3 (рус.) Эта книга представляет собой практическое руководство по программированию на Visual C++ для платформы .NET. Прочитав книгу, вы научитесь использовать Visual Studio .NET с целью создания самых сложных приложений для новой платформы .NET, которую разработала Microsoft. В начале книги автор объясняет, что такое Microsoft .NET, и излагает основные идеи, лежащие в основе модели программирования, использующей библиотеку классов .NET Framework, а затем вводятся управляемые расширения языка C++ и рассматриваются приемы программирования на управляемом C++. Затем автор переходит ко всестороннему обсуждению вопросов, связанных с развертыванием приложений. После этого рассматриваются метаданные, сериализация (преобразование в последовательную форму), поточная обработка данных, атрибуты, асинхронное программирование, удаленные вычисления, а также управление памятью. Далее автор сосредотачивается на подробном освещении технологии доступа к базам данных ADO.NET, и дает основательное введение в Web-программирование на основе технологии ASP.NET и простого протокола доступа к объектам SOAP (Simple Object Access Protocol). В заключение рассматриваются защита, отладка, и вопросы функциональной совместимости платформы .NET с традиционными СОМ-приложениями, а также приложениями, построенными на платформе Win32. Книга предназначена для подготовленных программистов-практиков. Б Б К 32.973.26-018.2.75
Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая ^ютокопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства Prentice Hall, Inc.. Authorized translation from the English language edition published by Prentice Hall, Inc., Copyright © 2002 All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. Russian language edition published by Williams Publishing House according to the Agreement with R&I Enterprises International. Copyright © 2002 ISBN 5-8459-0379-3 (рус.) ISBN 0-1306-5207-5 (англ.)
© Издательский дом "Вильяме", 2002 © Prentice Hall, Inc., 2002
Оглавление Предисловие Глава 1. Что такое Microsoft .NET? Глава 2. Основы технологии .NET
17 21 31
Глава 3. Программирование на управляемом с++ Глава 4. Объектно-ориентированное программирование на управляемом C++ Глава 5. Управляемый C++ в .NET Framework Глава 6. Создание графических пользовательских интерфейсов Глава 7. Сборки и развертывание Глава 8. Классы каркаса .NET Framework Глава 9. Программирование в ADO.NET Глава 10. ASP.NET и Web-формы Глава 11. Web-службы Глава 12. Web-узлы и Web-службы, работающие на основе ATL Server Глава 13. Защита Глава 14. Трассировка и отладка в .NET Глава 15. Смешивание управляемого и неуправляемого кода Приложение A. Visual Studio.NET Предметный указатель
45 101 123 183 227 265 341 395 443 483 529 573 581 621 637
Содержание предисловие глава 1. Что такое Microsoft .NET? Microsoft и Web Приложения в эпоху Internet Web-службы ASP.NET Открытые стандарты и возможность взаимодействия (функциональная совместимость) Протоколы обмена Windows на рабочем столе Проблемы с Windows Стеклянный дом и тонкие клиенты Устойчивая Windows Новая платформа программирования Каркас NET Framework Общеязыковая среда выполнения CLR (Common Language Runtime} Разработка приложений на разных языках Инструментальные средства разработки Важность инструментальных средств разработки Роль языка XML Факторы, определяющие успех Web-служб Резюме
Глава 2. Основы технологии .NET
17 21 22 22 22 23 23 24 24 24 25 25 25 26 26 28 28 29 29 30 30
31
Проблемы, связанные с разработкой Windows-приложений Приложения будущего
31 32
Обзор платформы .NET Волшебство метаданных Библиотека классов .NET Framework Программирование на основе интерфейсов Объектом является все Общая система типов ILDASM — дисассемблер промежуточного языка Microsoft Возможность взаимодействия языков, или функциональная совместимость Управляемый код Сборки ЛТ-компиляция, или оперативная компиляция Производительность Резюме
32 33 36 36 37 37 38
Содержание
39 40 41 42 43 43
Глава 3. Программирование на управляемом C++
45
Место C++ в мире .NET
45
Использование расширений управляемого C++
46
Ваша первая программа на управляемом C++.NET Программа HelloWorld (Привет, мир) Директива #using и оператор using Стандартный ввод-вывод Класс System::String (Система-Строка) Класс System-Array (Система-Массив) Программа Hotel (Гостиница) Отображение C++ на спецификацию общего (универсального) языка (CLS) и .NET Framework Типы данных C++ и общеязыковая среда выполнения CLR Типы данных C++ и .NET Framework Программирование на C++ для платформы .NET Управляемые и неуправляемые типы Управление сборкой мусора Типовая безопасность Типы значений Абстрактные типы Интерфейсы Упаковка и распаковка примитивных типов данных Делегаты События Свойства Закрепление управляемых объектов Конечные классы Управляемое приведение типов Определение ключевых слов в качестве идентификаторов Обработка исключений
47 47 49 50 52 58 63
Атрибуты C++
93
Резюме
68 68 68 71 71 72 74 75 76 78 80 82 84 86 87 89 89 90 91 100
Глава 4. Объектно-ориентированное программирование на управляемом C++ Обзор основных понятий объектно-ориентированного программирования Объекты Классы Полиморфизм Проект: "Бюро путешествий Acme" Проектирование абстракций Логика базовых классов Проектирование инкапсуляции Содержание
101 101 102 103 105 105 106 109 112
Наследование в управляемом C++ Основные принципы наследования Реализация примера "Бюро путешествий Acme"
113 113 114
Запуск программы примера Класс HotelReservation Класс HotelBroker Класс Customers (Клиенты) Пространство имен Класс TestHotel Резюме
115 116 116 119 120 120 122
Глава 5. Управляемый C++ в .NET Framework Объект системы: S y s t e m : : O b j e c t Общедоступные методы экземпляров класса Object (Объект) Защищенные методы экземпляров класса Object (Объект) Родовые интерфейсы и обычное поведение Использование методов класса Object (Объект) в классе Customer (Клиент) Коллекции Пример класса ArrayList (Список массивов) Интерфейсы Основные сведения об интерфейсах Программирование с использованием интерфейсов Динамическое использование интерфейсов Программа Бюро путешествий Acme (Acme Travel Agency) Явное определение интерфейсов Родовые интерфейсы в .NET Интерфейсы коллекций Копирование объектов и интерфейс ICIoneable Сравнение объектов Что такое каркасы приложений Делегаты Объявление делегата Определение метода Создание экземпляра делегата Вызов делегата Объединение экземпляров делегатов Полный пример Моделирование фондовой биржи События События в управляемом C++ и .NET Описание сервера Описание клиента Комната для дискуссий: пример чат-программы Резюме 8
Содержание
123 124 124 125 125 126 129 129 133 134 136 139 143 146 148 148 154 163 165 165 166 166 167 167 168 168 172 175 175 175 177 178 181
Глава 6. Создание графических пользовательских интерфейсов Иерархия Windows Forms (Формы Windows)
183 184
Создание простых форм с помощью комплекса инструментальных средств разработки программ .NET SDK Шаг 0: Создание простой формы Шаг 1: Отображение текста на форме Обработка событий в Windows Forms (Формы Windows) Документация по обработке событий Событие MouseDown (Кнопка мыши нажата) Шаг 2: Обработка событий мыши Шаг 2М: Несколько обработчиков для события Шаг 3: События MouseDown (Кнопка мыши нажата и Keypress (Нажатие клавиши) Меню Шаг 4: Меню для выхода из программы Код меню Код события Menu (Меню) Управляющие элементы Шаг 5: Использование управляющего элемента TextBox (Поле) Visual Studio.NET и формы Демонстрация Windows Forms (Формы Windows) Окно конструктора (Design window) и окно кода (Code window) Добавление события Код обработчика события Использование управляющего элемента Menu (Меню) Закрытие формы (Выход из формы) Диалоговые окна Документация по диалогам .NET Демонстрация диалогового окна Управляющий элемент L i s t B o x (Список элементов) Начальная загрузка списка элементов Выбор элемента в списке элементов ListBox Пример бюро путешествий Acme (Acme Travel Agency) — шаг 3
194 196 196 196 197 198 198 200 200 207 208 209 210 212 214 215 216 221 221 222 223
Резюме
226
185 185 188 190 191 191 192 194
Глава 7. Сборки и развертывание Сборки
227 227
Содержимое сборки Управление версиями сборки Частное развертывание сборки
228 234 236
Общедоступное развертывание сборки Строгие имена Цифровые сигнатуры (подписи) Цифровая подпись и развертывание общедоступной сборки
237 238 238 240
Содержание
Управление версиями общедоступных компонентов Подписание в цифровой форме после компиляции Конфигурация сборки Проводимая по умолчанию политика управления версиями Файлы конфигурации политики управления версиями Обнаружение физического местоположения сборки
246 246 247 248 248 250
Многомодульные, или мультимодульные сборки Инсталляция примера программной системы Установка и развертывание проектов
251 256 259
CAB Project {Проект CAB) Проект установки {Setup Project) Merge Module Project (Проект модуля слияния) Развертывание по сети
260 261 262 263
Резюме
263
Глава 8. Классы каркаса .NET Framework Метаданные и отражение Класс Туре (Тип) Динамическое связывание Ввод И вывод в .NET Потоковые классы Примитивные типы данных и потоки TextReader и TextWriter Обработка файлов Сериализация, или преобразование в последовательную форму Объекты сериализации ISerializable Модель приложений .NET Потоки Изоляция потоков Синхронизация коллекций Контекст Заместители и заглушки ContextBoundObject Изоляция приложений Прикладная область Прикладные области и сборки Класс AppDomain (Прикладная область) События AppDomain (Прикладная область) Пример AppDomain (Прикладная область) Маршализация, прикладные области и контексты Асинхронное программирование Асинхронные шаблоны проектирования lAsyncResult 10
Содержание
265 266 267 271 272 273 274 274 275 278 280 284 286 287 301 303 304 306 307 307 308 308 309 309 309 312 312 313 313
Использование делегатов в асинхронном программировании Организация поточной обработки с параметрами Удаленный доступ Краткий обзор удаленного доступа Удаленные объекты Активация Пример удаленного объекта Пример программы, реализующей удаленный доступ Метаданные и удаленный доступ Конфигурационные файлы удаленного доступа Программируемые атрибуты Использование самостоятельно созданного атрибута Определение класса атрибута Определение базового класса Сборка мусора Уничтожение объектов Неуправляемые ресурсы и освобождение ранее выделенной области памяти Поколения Завершение и раскручивание стека Управление сборкой мусора с помощью класса сборщика мусора GC Программа-пример .
314 317 319 319 320 321 321 322 324 324 324 325 327 328 329 329
Резюме
340
Глава 9. Программирование в ADO.NET
331 337 337 338 339
341
Источники данных Проводник Visual Studio.NET по серверу: Server Explorer Установление соединения Устройства считывания данных
342 344 344 346
Работа с базой данных в соединенном режиме Выполнение операторов SOL DataReader Множественное результирующее множество Коллекция параметров
349 350 351 352 353
Классы S q l D a t a A d a p t e r И D a t a S e t (Набор данных) Отсоединенный режим Коллекции объектов D a t a S e t (Набор данных)
355 356 356
Основные сведения о наборах данных Обновление источника данных Автоматически генерируемые свойства команд Транзакции и обновление базы данных
358 360 362 363
Объект D a t a S e t (Набор данных) и сравнение пессимистического блокирования с оптимистическим
364
Содержание
11
Использование наборов данных • Множественные таблицы в объекте DataSet (Набор данных) Создание таблицы без обращения к источнику данных Ограничения и связи Получение информации о схеме размещения данных в объекте DataTable (Таблица данных) Изменение объекта DataRow
374 381
Пример приложения Acme Travel Agency (Туристическое агентство Acme) Доступ к данным XML
385 385
Схема и данные XML XmfDataDocument DataSet (Набор данных) и XML База данных AirlineBrokers DataSet (Набор данных) и XML Создание документа XML из объекта DataSet (Набор данных)
386 386 386 387 387 392
Резюме
394
Глава 10. ASP.NET и Web-формы
12
366 368 370 371
395
Что такое ASP.NET?
395
Основные принципы создания Web-приложения Программа на С#: Echo (Эхо) Возможности ASP.NET Архитектура Web-форм - Класс Page (Страница) Время существования страниц с Web-формами Состояние представления (вида) Модель событий Web-форм Обработка страницы Трассировка Программирование запросов и ответов Класс HttpRequest Класс HttpResponse Изучение конкретного примера Web-страница с информацией о гостиницах Привязка данных Приложения ASP.NET Сеансы Clobal.asax Состояния в приложениях ASP.NET Статические элементы данных Объект Application (Приложение) Объект Session (Сеанс) Конфигурация ASP.NET
395 399 401 402 404 405 407 408 409 413 415 415 421 424 424 428 435 435 435 438 438 438 438 439
Содержание
Файлы конфигурации Дополнительная информация об ASP.NET
440 441
Резюме
441
Глава 1 1 . Web-службы
443
Протоколы ЯЗЫК XML Пространства имен XML (XML Namespeces) Схема XML (XML Schema) Протокол SOAP Язык описания Web-служб WSDL Архитектура Web-службы Пример Web-службы Add (Сложение) Просмотр Web-службы Add (Сложение) при помощи броузера Отладка Web-службы Add (Сложение) Клиент для Web-службы Add (Сложение) Язык описания Web-служб (Web Services Description Language — WSDL) Классы-заместители Клиент Web-службы, использующий необработанные данные SOAP и протокол передачи гипертекстовых файлов HTTP Особенности форматирования данных согласно спецификации SOAP
444 444 445 446 447 447 447 448 449 450 451 453 456
Класс WebService Использование шаблона Managed C++ Web Service (Web-службы на управляемом C++) Код, генерируемый шаблоном Managed C++ Web Service (Web-служба на управляемом C++) Арифметическая Служба Сети, или Web-служба A r i t h m e t i c (Арифметика) Использование внутренних объектов Web-служба Hotel Broker (Брокер гостиницы) Web-служба Customer (Клиент) Web-служба Hotel Broker (Брокер гостиницы) Соображения по поводу проектирования Резюме
466
Глава 12. Web-узлы и Web-службы, работающие на основе ATL Server
458 460
466 467 470 471 476 478 480 480 481 483
История технологий, работающих с динамическим содержимым Web
484
Приложения на основе ATL Server ATL Server основан на интерфейсе прикладного программирования Internet-сервера (ISAPI) Архитектура приложения, использующего ATL Server
486
Создание проекта ATL Server Project (Проект на основе ATL Server)
489
Содержание
487 487
13
Динамически подключаемая библиотека (DLL) расширения интерфейса прикладного программирования Internet-сервера (ISAPIJ Динамически подключаемая библиотека (DLL) Web-приложения Создание и запуск проекта на основе ATL Server Добавление в сервер еще одного обработчика Добавление на сервер обработки управляющей структуры if-else-endif Добавление на сервер обработки управляющей структуры while-endwhile Передача параметров серверному обработчику Поддержка состояния сеанса Получение доступа к переменным сервера Обработка форм Службы сеанса Создание проекта Web-службы на основе ATL Server (ATL Server Web Service Project) Код Web-службы на основе ATL Server: ATLServerWebService.h Создание клиентской программы, обращающейся к Web-службе Добавление функций в Web-службу на основе ATL Server Изменение клиентской программы, работающей с Web-службой Передача структур в качестве входных и выходных параметров Резюме Глава 13. Защита
14
493 495 497 499 501 502 504 509 512 513 516 516 520 522 524 525 526 528 529
Защита на основе пользователей
530
Защита доступа к коду Политика безопасности Разрешения Internet-безопасность Информационный сервер Internet: Internet Information Server (IIS) Защита .NET на основе ролей Принципалы и личности Роли .NET в Windows Другие классы личностей Личность в операционной системе и общеязыковой среде выполнения CLR Разрешения коду на доступ Простой запрос разрешения кодом Как работает запрос на разрешение Стратегия запроса разрешений Запрет разрешений Утверждение разрешений Другие методы разрешений Класс SecurityPermission Неуправляемый код Разрешения на основе атрибутов Разрешение принципала
531 531 531 532 532 534 534 538 542
Содержание
543 544 545 546 547 547 548 550 550 551 552 553
Класс PermissionSet Личность кода Классы разрешений для личности Подтверждение Политика безопасности Уровни политики безопасности Кодовые группы Именованные наборы разрешений Изменение политики безопасности
555 557 557 557 560 560 560 561 561
Резюме
572
Глава 14. Трассировка и отладка в .NET
573
Пример TraceDemo Разворачивание TraceDemo.exe.config
573 574
Использование классов Debug (Отладка) и Trace (Трассировка)
575
Использование переключателей для активизации диагностики
576
Активация и деактивация переключателей Установка переключателей в файле конфигурации Установка переключателей программным путем Использование переключателей для управления выводом Класс TraceListener
577 577 577 577 578
Коллекция слушателей
578
Резюме
579
Глава 15. Смешивание управляемого и неуправляемого кода
581
Сравнение управляемого и неуправляемого кода Причины смешивания управляемого и неуправляемого кодов Неуправляемый или опасный? Управляемые и неуправляемые ссылки и типы значений
582 582 583 584
Ограничения на использование управляемых типов в C++
586
Вызов управляемого кода из неуправляемого и обратный вызов
590
Сравнение программирования на C++ с использованием модели компонентных объектов Microsoft (COM) и .NET Доступ из управляемого кода к компонентам, построенным на основе модели компонентных объектов Microsoft (COM) Сервисная программа Tlbimp.exe Унаследованный компонент на основе модели компонентных объектов Microsoft (COM) Действующий клиент на основе модели компонентных объектов Microsoft (COM) Создание клиента на основе модели компонентных объектов Microsoft (СОМ) с помощью управляемого C++ Разработка управляемого клиента на основе модели компонентных объектов Microsoft (COM) с помощью С# Содержание
592 595 596 598 600 601 602 15
Создание с помощью управляемого C++ клиента на основе модели компонентных объектов Microsoft (COM) без метаданных 603 Создание с помощью С# управляемого клиента на основе модели компонентных объектов Microsoft (COM) без метаданных 604 Доступ к управляемым компонентам из клиентов на основе модели компонентных объектов Microsoft (COM) 606 Раннее связывание клиента на основе модели компонентных объектов Microsoft (COM) с компонентами .NET 607 Динамическое связывание клиента на основе модели компонентных объектов Microsoft (COM) с компонентами .NET 612 Явное определение интерфейса 614 Службы обращения к платформе: Pinvoke (Platform Invocation Services) 616 Резюме
620
Приложение A. Visual studio.NET Обзор Visual Studio.NET Панели инструментов Создание консольного приложения
621 624 625
Создание проекта C++ Добавление файла на C++ Использование текстового редактора Visual Studio Компиляция проекта Запуск программы Запуск программы в отладчике Конфигурирование проектов Создание новой конфигурации Установка параметров компоновки приложения в конфигурации Отладка Оперативная отладка Обычная отладка Резюме
626 626 627 628 628 629 629 629 630 631 631 633 636
Предметный указатель
16
621
Содержание
637
предисловие
а протяжении нескольких лет язык программирования Visual C++ от компании Microsoft используется как чрезвычайно мощное средство для создания программного обеспечения, работающего под управлением операционной системы Windows. Несмотря на то, что для приобретения необходимых навыков требуются существенные инвестиции, изучение Visual C++ вполне оправдано, ведь с его помощью можно делать некоторые такие вещи, которые просто немыслимо реализовать на каком-либо другом языке программирования. Сейчас, когда для нас открыт мир .NET, мы с радостью узнаем, что для реализации наших усилий, направленных на достижение максимальной мощности и производительности приложений, мы можем продолжать использовать Microsoft Visual C++ совместно с его новыми расширениями управляемого C++. Управляемый C++ может быть использован для построения сборок на платформе .NET, а также для создания новых удивительных настольных программ, Webприложений и Web-сервисов. Неуправляемый C++ также может использоваться, например, при построении Web-узлов и Web-сервисов на основе сервера ATL Server. Платформа .NET привносит радикальные изменения в технологию разработки приложений, работающих под управлением операционной системы Windows компании Microsoft. Кроме того, приобретение навыков, необходимых для построения приложений на платформе .NET, является серьезной задачей для программистов, пишущих Windowsприложения. В состав новой платформы входят новые расширения языка C++, а также громадная библиотека классов, .NET Framework. Данная книга является практическим руководством, в ней содержится масса примеров. На конкретном примере, который рассматривается во многих главах этой книги, демонстрируются реальные возможности платформы .NET. Цель книги — снабдить вас всеми необходимыми знаниями, владея которыми, вы сможете начать самостоятельно писать серьезные приложения на Visual C++, использующие возможности библиотеки классов .NET Framework. Книга, которую вы держите в руках, одна из серии The Integrated .NETSeries, выпускаемой издательствами Object Innovations и Prentice Hali PTR.
Структура книги Книга состоит из пяти основных глав и организована так, что вы легко можете отыскать необходимый для изучения материал. В первой части книги, включающей главы 1 "Что такое Microsoft .NET?" и 2 "Основы технологии .NET", содержится обзорный материал, который должен быть прочитан каждым. Именно здесь содержится ответ на очень важный вопрос: "Что такое Microsoft .NET?". Тут также изложены основные идеи, лежащие в основе модели программирования, использующей библиотеку классов .NET Framework. Во второй части книги, включающей главы с 3 "Программирование на управляемом C + + " по 5 "Управляемый C++ в .NET Framework", рассматриваются приемы программирования, использующие управляемый C++. Даже если вы уже знакомы с обычным C++, вы все равно захотите прочесть эти главы. В главе 4 "Объектно-ориентированное программирование на управляемом C++" вводятся управляемые расширения языка C++. Рассмотрение конкретного примера, который затем прорабатывается в последующих главах книги, также начинается с главы 4 "Объектно-ориентированное программирование на управляемом C++". В главе 5
"Управляемый C++ в .NET Framework" рассматриваются такие важные понятия как интерфейсы, делегаты и события. В этой главе описываются также важные взаимодействия, возникающие между управляемым C++ и библиотекой классов .NET Framework. В третьей части, включающей главы с 6 "Создание графических пользовательских интерфейсов" по 9 "Программирование в ADO.NET", рассматриваются важные фундаментальные понятия, касающиеся библиотеки классов .NET Framework. В главе 6 "Создание графических пользовательских интерфейсов" рассмотрено программирование пользовательского интерфейса с использованием классов Windows Forms. В главе 7 "Сборки и развертывание" обсуждается понятие сборки, а также вопросы, связанные с развертыванием приложений. Решения в этой области составляют главное достижение, результатом которого является простота и надежность развертывания Windows-приложений. Благодаря указанным достижениям положен коней печальной ситуации, известной как "ад DLL" (DLL hell). В главе 8 "Классы каркаса .NET Framework" вводятся важные классы библиотеки .NET Framework, кроме того, рассматриваются понятия метаданных, сериализации (преобразования в последовательную форму), поточной обработки данных, атрибутов, асинхронного программирования, удаленных вычислений, а также управление памятью. В главе 9 "Программирование в AD0.NET" изучается технология доступа к базам данных ADO.NET, которая предоставляет последовательный набор классов для доступа как к реляционным, так и XML-данным. В четвертой части книги содержится основательное введение в Webпрограммирование на основе технологии ASP.NET и простого протокола доступа к объектам SOAP (Simple Object Access Protocol). Глава 10 "ASP.NET и Web-формы" дает представление об использовании технологии ASP.NET и Web-форм при разработке Webузлов. В главе 11 "Web-службы" рассмотрены простой протокол доступа к объектам (Simple Object Access Protocol — SOAP) и Web-сервисы, при помощи которых реализован мощный и легкий в использовании механизм, обеспечивающий функциональную совместимость неоднородных (гетерогенных) систем. В главе 12 "Web-узлы и Web-службы, работающие на основе ATL Server" приводятся приемы программирования при помощи сервера библиотеки шаблонных классов ATL Server, используемые при разработке как Web-приложений, так и Web-сервисов. В последней части книги рассмотрены важные дополнительные понятия библиотеки классов .NET Framework. В главе 13 "Защита" детально изучены вопросы безопасности, включающие безопасность доступа к коду (Code Access Security) и декларативную безопасность. Глава 14 "Трассировка и отладка в .NET" знакомит нас с классами, которые используются при отладке и трассировке и входят в состав платформы .NET. В главе 15 "Смешивание управляемого и неуправляемого кода" рассматриваются вопросы функциональной совместимости платформы .NET с традиционными СОМ-приложениями, а также приложениями, построенными на платформе Win32.
Примеры программ Единственный способ приобретения надежных базовых знаний состоит в прочтении и написании большого количества программ. Причем некоторые из этих программ должны быть достаточно большого размера. В данной книге содержится много маленьких программ, при помощи которых отдельно иллюстрируется каждый из рассматриваемых аспектов платформы .NET. Такая подача материала облегчает его понимание. Программы четко выделены в тексте. Основной рассматриваемый конкретный пример под названием Acme Travel Agency (Туристическое агентство Acme, или Бюро путешествий Acme), тщательно прорабатывается в большинстве глав (с 4 "Объектно-ориентированное программирование на управляемом C + + " по 12 "Web-узлы и Web-службы, работающие на основе ATL Server"). На этом примере иллюстрируются многие особенности совместной работы управляемого C++ и платформы .NET, имеющие место в реальных приложениях. 18
Предисловие
Примеры программ представлены в виде самораспаковывающихся архивных файлов на Web-узле данной книги. При извлечении файлов из архива создается структура каталогов. По умолчанию корневой каталог находится по адресу с: \OI\NetCpp. Примеры программ, которые начинают появляться с главы 2 "Основы технологии .NET", находятся в каталогах ChapO2, ChapO3, и так далее. Каждый пример, относящийся к определенной главе, находится в отдельной папке, которая хранится в каталоге, относящимся к соответствующей главе. Названия папок четко идентифицируются в тексте. Пиктограмма, размещенная на полях, предупреждает о том, что дальше следует пример кода. В каждом каталоге, который относится к какой-либо главе, где пошагово прорабатываются разные аспекты рассматриваемого конкретного примера, содержится папка, имеющая название CaseStudy. В случае необходимости, в каждом каталоге, который связан с определенной главой, имеется файл r e a d m e . t x t . В нем содержатся инструкции и необходимые объяснения, которые вам понадобятся, чтобы запустить приводимые примеры программ. Книга, которую вы держите в руках— одна из серии The Integrated .NETSeries. Примеры программ, относящиеся к другим книгам данной серии, находятся в их собственных каталогах, расположенных в каталоге \О1. То есть, после инсталляции все папки с примерами программ, относящихся к другим книгам данной серии, будут расположены в одном общем каталоге. Эти программы предназначены исключительно для учебных целей. Их не следует использовать в составе какого-либо законченного программного продукта. Программы (а также инструкции, в которых описываются особенности их использования) поставляются так "как есть" ("as is"). Иными словами, какие-либо гарантии на них не распространяются.
Предостережение При написании книги и относящегося к ней кода использовалась версия Beta 2 библиотеки классов .NET Framework. По утверждению компании Microsoft, данная версия платформы .NET близка к ее окончательному варианту. Не подлежит сомнению, что прежде чем выйдет окончательная версия, платформа .NET еще претерпит некоторые изменения. Было проверено, что код, содержащийся в примерах, нормально функционирует на компьютерах, работающих под управлением операционной системы Windows 2000. Код, описывающий работу с базами данных, был проверен на работоспособность при помощи SQL Server 2000. В нескольких примерах, относящихся к главам, где рассматривается работа с базами данных и вопросы безопасности, в качестве связывающих строк и функциональных имен присутствуют имена машин. При попытке выполнения таких примеров вам следует изменить эти имена на соответствующее имя вашей машины. С целью облегчения инсталляции, в примерах, иллюстрирующих работу с базами данных, используется имя пользователя "sa", а пароль отсутствует. Само собой разумеется, что для входа в реальную систему вы всегда должны использовать учетное имя вместе с паролем. Никогда не следует разрабатывать приложения, предназначенные для работы с базами данных, в которых для входа в базу данных используется имя яд.
Web-узлы Web-узел, относящийся к книгам данной серии, имеет следующий адрес: www.objectinnovations.com/dotnet.htm. На этом Web-узле приводятся ссылки на примеры программ, которые рассматриваются в данной серии. Щелкнув на соответствующей ссылке, можно загрузить интересующий вас пример программы. Дополнительная информация относительно технологии .NET может быть найдена по адресу: www.mantasof t . c o m / d o t n e t . htm.
Предостережение
19
Кроме того, на этом Web-узле содержатся также примеры программ, рассмотренных в данной книге. На Web-узле нашей книги размешены еще и ресурсы, посвященные изучению технологии .NET, которые будут отслеживать последние достижения в развитии указанной технологии.
Благодарности Мы признательны Майку Михану (Mike Meehan) за его помощь в реализации данного проекта. Началом проекта послужила наша встреча на ежегодной конференции Professional Development Conference — PDC, на которой компанией Microsoft была анонсирована технология .NET. Наш разговор привел в движение то, что затем вылилось в большую серию книг, посвященных технологии .NET. Данная книга является третьей в этой серии. Нам хотелось бы также поблагодарить Джилл Гарри (Jill Harry), сотрудницу издательства Prentice Hall, за продолжающуюся до настоящего времени поддержку, которую она оказывает в реализации этого амбициозного книжного проекта. Наш редактор, Ник Радхабер (Nick Radhuber), очень помог нам не только при работе над данной книгой. Он также помогает, координируя работу над всей серией книг. Во время работы над указанной серией книг, многими разными способами нам помогли несколько людей, работающих в компании Microsoft, а именно: Стивен Прачнер (Steven Pratschner), Джим Хогг (Jim Hogg), Майкл Пайззо (Michael Pizzo), Майкл Дей (Michael Day), Крыштоф Квалина (Krzysztof Cwalina), Кит Боллинджер (Keith Ballinger) и Эрик Олсен (Eric Olsen). Мы благодарим их за то, что они смогли выделить для нас время в своем чрезвычайно напряженном рабочем графике, и помогли нам глубже проникнуть в суть проблемы и прояснить некоторые моменты. Конни Салливан (Connie Sullivan) и Стейси Джиард (Stacey Giard) помогли нам получить доступ к ресурсам компании Microsoft, а также координировали технические сессии. Майкл Стифэль (Michael Stiefel), автор другой книги из этой же серии, выступил в роли ценного ресурса при написании многих глав данной книги. Уилл Проноет (Will Provost) помог нам прояснить несколько моментов, связанных с языком XML. Мы хотим также поблагодарить всех авторов книг, объединенных в серию, посвященную платформе .NET. Действительно, в группе, которая работает над близкими по тематике книгами, большое значение имеет успешность совместных усилий. (Хотя нужно признаться, что, войдя в процессе написания книги в азарт, мы не всегда сотрудничали так тесно, как могли бы.) К числу этих трудолюбивых людей принадлежат Эрик Белл (Eric Bell), Говард Фенг (Howard Feng), Майкл Салтзман (Michael Saltzman), Ед Сунг (Ed Soong), Дана Вятт (Dana Wyatt), Дэвид Жанг (David Zhang), а также Сэм Жу (Sam Zhu). Роберту всегда было непросто писать благодарности, поскольку в работе над таким крупным проектом как этот, задействовано очень много людей, которым следует выразить благодарность. Я (Роберт) хотел бы поблагодарить мою жену, Мэрианн (Marianne), за то, что она оказывала мне огромную поддержку и вдохновляла все мои замыслы, направленные на написание книги. Для реализации подобного проекта требовались особые усилия, и, таким образом, ее поддержка является еще более значимой. Благодарю вас всех, — коллег, друзей и студентов — вас так много, что невозможно упомянуть каждого по имени — всех тех, кто помогал мне все эти годы. Питер хотел бы поблагодарить свою жену Элизабет (Elizabeth) и дочь Кэтрин ({Catherine). Его любовь к ним является всепроницаюшим свойством пространственновременного континуума, — свойством, проникающим во всю вселенную, во все времена, в прошлое, настоящее и будущее. 7 Ноября, 2001
20
Предисловие
Глава 1
Что такое Microsoft .NET?
овая технология .NET, предложенная компанией Microsoft, отражает видение этой компанией приложений в эпоху Internet. Технология .NET обладает улучшенной функциональной совместимостью, в основе которой лежит использование открытых стандартов Internet. Кроме того, она повышает устойчивость классического пользовательского интерфейса операционной системы Windows— рабочего стола. Разработчикам программного обеспечения технология .NET предоставляет новую профаммную платформу и великолепные инструментальные средства разработки, в которых основную роль играет язык XML (extensible Markup Language - расширяемый язык разметки). Microsoft .NET— платформа, построенная на верхнем слое операционной системы. Технология .NET явилась главным объектом инвестиций компании Microsoft. С момента начала работ над этой технологией и до момента ее публичного анонсирования прошло три года. Несомненно, на развитие технологии .NET оказали влияние другие технологические достижения, в частности расширяемый язык разметки XML, платформа Java , a также модель компонентных объектов Microsoft (Component Object Model — COM). Платформа Microsoft .NET предоставляет: • • •
•
•
• •
устойчивую обшеязыковую среду выполнения CLR (Common Language Runtime), которая входит в состав данной платформы; средства разработки приложений на любом из многих языков программирования, поддерживаемых платформой .NET; лежащую в основе открытой модели программирования офомную библиотеку классов .NET Framework. Эти классы содержат многократно используемый код. Они доступны в любом языке профаммирования, поддерживаемом платформой .NET; поддержку сетевой инфраструктуры, построенной на верхнем слое стандартов Internet, вследствие чего обеспечивается высокий уровень взаимодействия между приложениями; поддержку нового промышленного стандарта, а именно технологии Webслужб. Технология Web-служб предоставляет новый механизм создания распределенных приложений. По сути, она является распространением технологии создания приложений на базе компонентов и на сферу Internet; модель безопасности, которую программисты могут легко использовать в своих приложениях; мошные инструментальные средства разработки.
Microsoft и Web Всемирная паутина {World Wide Web — WWW) рассматривалась компанией Microsoft как вызов, и он был принят. В самом деле, Web достаточно хорошо сосуществует с персональными компьютерами (ПК), — сегментом рынка, в котором компания Microsoft традиционно сильна. С помощью приложения, работающего на ПК, — броузера, — пользователь получает доступ к огромному миру информации. В основе построения всемирной сети лежит использование стандартов, в частности, языка гипертекстовой разметки HTML (HyperText Markup Language), протокола передачи гипертекста HTTP (HyperText Transfer Protocol) и языка XML (extensible Markup Language). Эти стандарты играют существенную роль при обмене информацией между различными пользователями, работающими на самых разнообразных компьютерных системах и устройствах. Несмотря на всю свою сложность, персональный компьютер, работающий под управлением операционной системы Windows, является устройством достаточно стандартизированным. В основе Web хотя и лежат стандартные протоколы, все же она представляет собой Вавилонскую башню, состоящую из многочисленных языков программирования, баз данных, различных сред разработки и разных устройств, работающих на основе этих протоколов. Такая взрывоопасная сложность технологии еще больше усиливает растущую нехватку профессионалов, которые могут на основе новых технологий строить необходимые системы. Платформа .NET предоставляет инфраструктуру, позволяющую программистам отвлечься от повторного изобретения решений общих проблем программирования и сконцентрироваться на создании необходимых приложений.
Приложения в эпоху internet Первоначально Web представляла собой огромное хранилище данных. Для получения страницы с нужной информацией, броузер делал соответствующий запрос. Затем Webсервер доставлял запрошенную информацию в виде статической HTML-страницы. Даже после появления интерактивных Web-приложений, все еще используется язык HTML. С его помощью форматируется информация, отображаемая на экране. Язык XML предоставляет универсальный способ передачи данных, независимый от формата представления данных. Таким образом, именно язык XML может послужить отправной точкой на пути к достижению договоренности между компаниями относительно стандартов передачи документов и информации, в частности заказов на покупку и счетов. Тогда возникнут предпосылки для автоматизации бизнеса в сети Internet между сотрудничающими компаниями. В последнее время подобный вид электронной коммерции даже получил специальное название — B-to-B (Business-To-Business). Но язык XML всего лишь описывает данные, в нем не предусмотрено выполнение действий над данными. Именно для этой цели и нужны Web-службы.
Web-службы Поддержка платформой .NET Web-служб является одним из наиболее важных ее свойств. Web-службы, построенные на основе промышленного стандартного протокола SOAP (Simple Object Access Protocol — простой протокол доступа к объектам), позволяют использовать функции ваших приложений в любом месте Internet. С точки зрения программиста, работающего в среде .NET, не существует различия между Web-службами и другими типами служб, которые реализуются с помощью классов в языках программирования, соответствующих спецификации .NET. Используемая при этом модель программирования остается неизменной, независимо от того, вызывается ли функция приложением, отдельным компонентом, установленным на этой же машине, или, как в случае с Web-службами, надругой машине. Эта присущая простота используемой модели программирования позволяет компаниям очень легко создавать и устанавливать приложения. При желании все необходимое 22
Глава 1. Что такое Microsoft .NET?
для приложения может извлекаться из внешних источников, да и разработку приложения могут выполнить независимые разработчики. В результате этого удается избежать проблем, связанных с разработкой, развертыванием и сопровождением приложения. Иными словами, вы можете просто воспользоваться Web-службами, которые вам предлагают независимые разработчики. Эти Web-службы могли даже и не существовать в то время, когда вы проектировали свое приложение. ASP.NET Платформа .NET включает также полностью переделанную версию популярной технологии ASP (Active Server Pages), известную теперь под названием ASP.NET. В основе ASP лежит интерпретируемый код сценариев, в который вставлены команды форматирования текста. Код сценариев реализуется на одном из языков с довольно ограниченными возможностями. А технология ASP.NET позволяет писать код на любом языке, поддерживаемом платформой .NET. К таким языкам относится С#, VB.NET, JScript и C++ с управляемыми расширениями. Поскольку полученный при этом код является компилируемым, интерфейсный код может быть отделен от бизнес-логики и помешен в отдельный файл. Технология ASP.NET предоставляет в распоряжение разработчиков Web-формы, которые чрезвычайно упрощают создание пользовательских интерфейсов при программировании в Web. Перетаскивание (drag and drop) позволяет очень легко создавать макеты форм в среде Visual Studio.NET. Затем можно добавить код для обработки события формы, например, щелчка. В технологии ASP.NET реализовано автоматическое определение функциональных возможностей броузера. Если броузер обладает широкими функциональными возможностями, обработка кода может быть выполнена на стороне клиента. В случае использования менее мощного броузера, обработку кода выполняет сервер, который затем генерирует стандартную HTML-страницу. Все эти процессы происходят достаточно прозрачно для разработчиков, использующих технологию ASP.NET. В процессе создания Web-приложений использование Web-служб вместе с полнофункциональными компилируемыми языками программирования, такими как С#, VB.NET и управляемый C++, позволяет широко применять модели объектноориентированного программирования. Достичь этого при помощи языков подготовки сценариев, применяемых в ASP, и компонентов, построенных на основе модели компонентных объектов Microsoft (Component Object Model, COM) было бы невозможно.
Открытые стандарты и возможность взаимодействия (функциональная совместимость) Современная вычислительная среда состоит из множества аппаратных и программных систем. В качестве компьютеров могут использоваться мэйнфреймы и высокопроизводительные серверы, рабочие станции и персональные компьютеры, маленькие мобильные устройства, такие как карманные компьютеры, часто называемые персональными цифровыми помощниками (Personal Digital Assistance, PDA) и даже сотовые телефоны. К числу используемых операционных систем принадлежат традиционные операционные системы, под управлением которых работают мэйнфреймы, различные клоны операционных систем Unix, Linux, несколько версий операционной системы Windows, операционные системы реального времени и специальные операционные системы, наподобие PalmOs, предназначенной для управления мобильными устройствами. На практике используются различные языки программирования, различные базы данных, различные инструментальные средства разработки приложений, а также различное промежуточное программное обеспечение (программное обеспечение, содействующее процессам обмена информацией между клиентом и сервером). В современной вычислительной среде очень немногие приложения являются самодостаточными островами. Даже небольшие обособленные приложения, развернутые на отMicrosoft и Web
23
дельном ПК, могут использовать Internet при регистрации программного продукта или для получения обновлений к нему. Ключом к функциональной совместимости приложений является применение существующих стандартов. Поскольку, как правило, приложения работают в сети, ключевым стандартом является протокол, используемый для обмена данными.
Протоколы обмена Сокеты, используемые протоколом TCP/IP, высокостандартизированы и широкодоступны. Но программирование с применением сокетов рассматривается программистами как слишком низкоуровневое. Именно необходимость программирования на низком уровне препятствует продуктивному написанию устойчивых распределенных приложений. Протокол удаленного вызова процедур RPC (Remote Procedure Call) имеет несколько более высокий уровень. Но протокол удаленного вызова процедур RPC (Remote Procedure Call) является достаточно сложным, и к тому же существует масса его разновидностей. Приобрели популярность такие протоколы высокого уровня, как CORBA (Common Object Request Broker Architecture — архитектура посредника объектных запросов), RMI (Remote Method Invocation — технология удаленного вызова методов), а также распределенная модель компонентных объектов DCOM (Distributed Component Object Model). Эти протоколы все еще сложны и для организации их работы требуется наличие специальной среды как на стороне сервера, так и на стороне клиента. Им присущи также и другие недостатки. Например, в процессе использования данных протоколов возможно возникновение проблем при прохождении пакетов данных через брандмауэры (системы сетевой защиты). Тем не менее, один протокол получил повсеместное распространение. Это протокол передачи гипертекстовых файлов HTTP (Hypertext Transfer Protocol). Именно по причине повсеместного распространения протокола HTTP, компании Microsoft и другим производителям сетевого программного обеспечения пришлось разработать новый протокол, получивший название SOAP (Simple Object Access Protocol— простой протокола доступа к объектам). Для кодирования запросов методов объектов и сопутствующих данных в протоколе SOAP используются тексты на языке XML (extensible Markup Language). Огромным достоинством протокола SOAP является его простота. Вследствие своей простоты этот протокол может быть легко реализован на многих устройствах. Протокол SOAP (Simple Object Access Protocol) может работать на верхнем слое любого стандартного протокола. Но именно возможность его работы на верхнем слое таких стандартных Internet-протоколов, как протокол передачи гипертекстовых файлов HTTP (Hypertext Transfer Protocol) и протокол SMTP (Simple Mail Transfer Protocol— простой протокол пересылки почты, или простой протокол электронной почты), позволяет пакетам данных проходить через системы сетевой защиты (брандмауэры) без каких-либо проблем, связанных с возможностью соединения.
Windows на рабочем столе Microsoft начинала с пользовательского интерфейса, который известен под названием рабочего стола. Современная среда Windows получила повсеместное распространение. Под эту среду написано бесчисленное множество приложений. Большинство пользователей на домашних компьютерах использует операционную систему Windows, по крайней мере отчасти. Microsoft удалось достичь многого. Но, тем не менее, все еще существуют значительные проблемы.
Проблемы с Windows Обслуживание персонального компьютера, работающего под управлением операционной системы Windows, является тяжелой и неприятной задачей, так как имеющиеся приложения достаточно сложны. Они состоят из многих файлов, в процессе инсталляции производятся записи в системном реестре, создаются ярлыки и так далее. Различными приложениями могут использоваться одни и те же динамически подключаемые библиотеки (DLL). При инсталляции нового приложения динамически подключаемая библиотека, уже ис24
Глава 1. Что такое Microsoft .NET?
пользуемая существующим приложением, может быть перезаписана. Вследствие этого старое приложение может быть повреждено (ситуация, известная как "проклятие (ад) динамически подключаемых библиотек (DLL)"). Деинсталляция приложения также является довольно сложной задачей, которая часто выполняется не до конца автоматически. Постепенно персональный компьютер становится все менее стабильным, иногда он требует радикального лечения. При этом приходится переформатировать жесткий диск и начинать установку программного обеспечения с самого начала. Использование персональных компьютеров дает огромную экономическую выгоду. Действительно, стандартные приложения недороги и в то же время достаточно мощны, а аппаратные средства дешевые. Но величина сэкономленных средств уменьшается за счет затрат на сопровождение программного обеспечения. Первоначально операционная система Windows была разработана еще в те времена, когда персональные компьютеры не были связаны в сеть, и вопрос безопасности не стоял так остро. Несмотря на то, что средства безопасности были встроены в Windows NT и Windows 2000, соответствующую им модель профаммирования на практике использовать непросто. Ели не верите, ответьте на вопрос: вы когда-либо передавали что-либо, кроме пустого указателя NULL в качестве аргумента LPSECL)RITY_ATTRIBUTES, используемому в Win32?
Стеклянный дом и тонкие клиенты В последнее время приобрела привлекательность старая модель центральной вычислительной машины, в которой, как в стеклянном доме, под строгим и неусыпным контролем выполняются все необходимые приложения. Результатом явилась идея создания некоторого рода тонких клиентов. Но на самом деле широко разрекламированная идея "сетевого ПК" никогда не была принята до конца. Пользователям слишком дороги стандартные приложения для ПК, к тому же им хочется иметь свой персональный (локальный) компьютер, на котором так привычно хранить свои данные. Ведь без линии связи с очень высокой пропускной способностью не сможет удовлетворительно функционировать даже текстовый процессор, работающий на сервере. Проблема безопасности также является слишком сложной, чтобы ее можно было решить при помощи тонких клиентов. И поэтому не вызывает сомнения, что персональный компьютер еще долго будет занимать очень прочные позиции.
Устойчивая Windows В связи со всей этой шумихой, поднятой вокруг платформы .NET и Internet, важно четко осознавать, что с появлением платформы .NET изменилась модель профаммирования. Следствием этого стала возможность создания намного более устойчивых Windowsприложений. Судьба приложения больше не зависит от обширных конфигурационных данных, хранящихся в хрупком системном реестре Windows. .NET-приложения содержат самоописание. Они содержат метаданные в своих исполняемых файлах. Различные версии компонентов могут быть развернуты и существовать одновременно. Благодаря глобальному кэшу сборки (Global Assembly Cache), разные приложения могут совместно использовать одни и те же компоненты. Управление версиями встроено в модель развертывания приложений. Частью платформы .NET является также простая модель безопасности.
Новая платформа программирования А теперь давайте рассмотрим вопросы, которые мы только что обсудили, с точки зрения технологии .NET как новой платформы профаммирования. •
Платформа .NET позволяет реализовать проверку типовой безопасности и проверку надежности. Следствием этого является более устойчивое функционирование приложений. Новая платформа программирования
25
•
Процесс создания приложений на платформе .NET значительно облегчился по сравнению с созданием приложений на основе интерфейса 32-разрядных Windowsприложений (Win32 API) или модели компонентных объектов Microsoft (COM).
•
Платформа целиком, как и некоторые ее части, может быть реализована на многих различных типах компьютеров (аналогично Java-машине).
•
Имеется единая библиотека классов, используемая всеми языками, которые поддерживает платформа .NET.
•
Приложения, написанные на различных языках программирования платформы .NET, могут быть легко интегрированы друг с другом.
Платформа .NET имеет также несколько важных характерных особенностей, а именно: • • •
каркас .NET Framework; общеязыковую среду выполнения CLR (Common Language Runtime); возможность разработки приложения на многих языках программирования, поддерживаемых платформой .NET; • инструментальные средства разработки приложений.
Каркас NET Framework Современный стиль программирования предполагает многократное использование кода, содержащегося в библиотеках. Объектно-ориентированные языки программирования облегчают создание библиотек классов. Получающиеся в результате библиотеки являются гибкими, им присущ высокий уровень абстракции. Эти библиотеки могут быть расширены путем добавления новых классов, а также путем образования новых классов на основе уже существующих. При этом новые классы наследуют функциональность существующих классов. В каркасе .NET Framework представлено более 2500 классов, содержащих повторно используемый код. Эти классы доступны в любом языке программирования, который поддерживается платформой. Библиотека классов .NET Framework является расширяемой. На основе уже существующих базовых классов можно создать новые производные классы, причем производные классы могут быть реализованы на совершенно другом языке программирования. В состав библиотеки классов .NET Framework, входят классы, которые используются при разработке Windows-приложений, Web-приложений, а также приложений с базами данных. В библиотеке классов .NET Framework имеются также классы, обеспечивающие взаимодействие с языком XML, с моделью компонентных объектов Microsoft (COM) и с любой платформой, поддерживающей интерфейс 32-разрядных Windows-приложений (Win32 API). Библиотека классов .NET Framework обсуждается в следующей главе, а также понемногу в остальных главах данной книги.
Общеязыковая среда выполнения CLR (Common Language Runtime) Среда выполнения предоставляет необходимые службы во время выполнения приложений. Традиционно каждой среде программирования соответствует своя среда выполнения. В качестве примера среды выполнения могут служить стандартная библиотека языка С, библиотека базовых классов Microsoft (MFC), среда выполнения языка Visual Basic, а также виртуальная машина Java (Java Virtual Machine). Среда выполнения платформы .NET получила название общеязыковой среды выполнения CLR (Common Language Runtime).
26
Глава 1. Что такое Microsoft .NET?
Управляемый код и данные Общеязыковая среда выполнения CLR (Common Language Runtime) предоставляет в распоряжение .NET-кода ряд служб (включая и библиотеку классов .NET Framework, которая размешается на верхнем слое CLR). Для того чтобы воспользоваться этими службами, .NET-код должен иметь предсказуемое поведение и, к тому же, быть понятным общеязыковой среде выполнения CLR. Например, для того чтобы среда выполнения могла осуществить проверку границ массивов, все массивы в .NET имеют идентичный формат. Требования типовой безопасности могут налагать на .NET-код и другие ограничения. Ограничения, которые накладываются на .NET-код, определяются общей системой типов (Common Type System, CTS), а также ее реализацией в промежуточном языке IL, разработанном корпорацией Microsoft (Microsoft Intermediate Language— MS IL, или просто IL). Общей системой типов определены типы и операции, которые могут использоваться кодом, работающим в общеязыковой среде выполнения CLR. Так, именно общей системой типов (Common Type System, CTS) на используемые типы накладывается ограничение единичного наследования реализации. Код на промежуточном языке, разработанном корпорацией Microsoft (Microsoft Intermediate Language, MSIL), компилируется во внутренний (собственный) код платформы. .NET-приложения содержат в себе метаданные, т.е. описание кода и данных, используемых приложением. Благодаря использованию метаданных возможно автоматическое преобразование данных в последовательную форму общеязыковой средой выполнения CLR при их сохранении. Код, который может использовать службы, предоставляемые общеязыковой средой выполнения CLR, называется управляемым кодом. Память для управляемых данных распределяется и освобождается автоматически. Такое автоматическое освобождение занимаемой памяти называется сборкой мусора (garbage collection). Сборка мусора решает все проблемы утечки памяти и им подобные.
Microsoft и Европейская Ассоциация производителей ЭВМ1 Корпорация Microsoft передала с целью стандартизации спецификацию языка С# и основные части библиотеки классов .NET Framework на рассмотрение Европейской Ассоциации производителей компьютеров (European Computer Manufacturers' Association — ЕСМА). Техническими требованиями этой независимой международной организации по стандартам определена независимая от платформы инфраструктура универсального языка CLI {Common Language Infrastructure). Общеязыковую среду выполнения CLR можно представить себе как инфраструктуру универсального языка CLI (Common Language Infrastructure), дополненную библиотеками базовых классов BCL (Basic Class libraries). Библиотека базовых классов BGL (Basic Class library) поддерживает фундаментальные типы обшей системы типов CTS (Common Type System), а именно: ввод/вывод файлов, строки и форматирование. Поскольку общеязыковая среда выполнения CLR зависит от используемой платформы, в ней используются модели управления процессами и памятью базовой операционной системы. Спецификацией (техническими требованиями) Европейской Ассоциации производителей компьютеров (European Computer Manufacturers' Association — ЕСМА) определен универсальный промежуточный язык CIL (Common Intermediate Language). Согласно этим требованиям, разрешено интерпретировать код на промежуточном языке CiJL или компилировать его в собственный (внутренний) код.
1 European Computer Manufacturers' Association (ЕСМА) имеет также другие названия: Европейская Ассоциация производителей компьютеров (ЕАПК) и Европейская ассоциация изготовителей ЭВМ. Европейская Ассоциация производителей ЭВМ разрабатывает стандарты, соблюдаемые большинством фирм, выпускающих ЭВМ и программное обеспечение. — Прим. ред.
Новая платформа программирования
27
Верифицируемый код Управляемый код может быть проверен на предмет типовой безопасности. Код, удовлетворяющий требованиям типовой безопасности, разрушить не так легко. Например, структуры данных или другие приложения, которые находятся в памяти, не могут быть повреждены в результате перезаписи буфера. Политику безопасности можно применить к коду, удовлетворяющему требованиям типовой безопасности. Например, доступ к некоторым файлам или средствам пользовательского интерфейса может быть разрешен или запрещен. Выполнение кода, происхождение которого неизвестно, можно запретить. Однако, не все приложения, для работы которых требуется общеязыковая среда выполнения CLR, обязаны удовлетворять требованиям типовой безопасности. В частности, такая ситуация реализуется для приложений, написанных на C++. Управляемый код, написанный на C++, может использовать возможности, предоставляемые общеязыковой средой выполнения CLR, например, сборку мусора. Но так как на C++ может быть создан и неуправляемый код, то нет никаких гарантий относительно того, что приложение, написанное на C++, будет удовлетворять требованиям типовой безопасности. В управляемом коде, написанном на C++, нельзя выполнять арифметические операции над управляемыми указателями, или приводить тип управляемого указателя к неуправляемому. Поэтому управляемый код, написанный на C++, можно проверить на безопасность. Но может случиться так, что в этом же приложении, написанном на C++, будут выполняться арифметические операции над указателями или приведение типов управляемых указателей к неуправляемым. А это, по своей сути, ненадежно.
Разработка приложений на разных языках Как следует из ее названия, общеязыковая среда выполнения CLR поддерживает многие языки программирования. Для каждого такого языка должен быть реализован компилятор, который генерирует "управляемый код". Сама компания Microsoft реализовала компиляторы для управляемого C++, Visual Basic.NET, JScript, а также для совершенно нового языка программирования С#. Компиляторы для более чем дюжины других языков реализуются усилиями независимых разработчиков. К числу этих языков программирования принадлежит язык COBOL (его реализацией занимается компания Fujitsu) и язык Perl (его реализацией занимается компания ActiveState). Представьте себе, что миллиарды строк кода, написанных на языке COBOL, после некоторых усилий, связанных с переносом, станут доступными в среде .NET. Чтобы воспользоваться преимуществами среды .NET, программистам, которые пишут приложения на языке COBOL, не придется переучиваться и с начала изучать совершенно новый язык программирования.
Инструментальные средства разработки Настоящим ключом к успеху в разработке программного обеспечения является наличие набора эффективных инструментальных средств разработки. Компания Microsoft уже давно предлагает замечательные инструментальные средства разработки, к числу которых принадлежат Visual C++ и Visual Basic. Платформа .NET объединяет средства разработки в единую интегрированную среду, которая имеет название Visual Studio.NET. •
Среда VS.NET обладает Широкими функциональными возможностями, которые могут быть использованы при создании приложения на любом языке, поддерживаемом платформой .NET. • Платформа .NET позволяет использовать несколько языков программирования для написания приложений и имеет необходимые средства отладки. • Среда VS.NET предоставляет множество различных конструкторов форм, баз данных и других программных элементов.
28
Глава 1. Что такое Microsoft .NET?
Независимые разработчики могут и в дальнейшем разрабатывать расширения среды Visual Studio.NET, а также предлагать дополнительные языки программирования и соответствующие полноценные среды разработки, поддерживаемые платформой .NET. Программы на предложенных независимыми разработчиками языках программирования смогут взаимодействовать с программами на любых языках, поддерживаемых платформой .NET. Существующий набор инструментальных средств разработки обладает широкими возможностями, которые используются при создании Web-приложений и Web-служб. Обеспечивается также всесторонняя поддержка разработки приложений с базами данных.
Важность инструментальных средств разработки Не следует недооценивать значение инструментальных средств разработки приложений. Хорошей иллюстрацией тому может послужить случай, который произошел при работе над проектом языка Ada. Целью данного проекта было создание очень мощного языка программирования. Частью первоначального замысла было также создание стандартизованной среды программирования на языке Ada (Ada Programming Support Environment— APSE). Разработке языка программирования было уделено огромное внимание. В то же время гораздо меньше внимания было уделено надлежащей разработке среды программирования на языке Ada (APSE). Из-за этого у языка программирования Ada так и не появилась среда разработки, которая могла бы сравниться со средой разработки Visual Studio, Smalltalk, или с многочисленными интегрированными средами разработки, которые имеются для языка Java. Преимущество среды разработки Visual Studio.NET состоит в том, что она является стандартом. Следовательно, она будет тщательно настроена для того, чтобы сделать работу в этой среде продуктивной. Вниманию разработчиков будут предложены многочисленные тренинги, посвященные разработке приложений в данной среде, планируется также множество других акций. Компания Microsoft, по сравнению со многими более мелкими разработчиками, присутствующими на обширном рынке инструментальных средств, располагает гораздо большими ресурсами, которые она в состоянии выделить на поддержку среды Visual Studio.NET. Платформа Java характеризуется высоко стандартизированным языком программирования и интерфейсом прикладного программирования (API). В то же время, инструментальные средства разработки, без которых написание высокопроизводительных приложений немыслимо, не являются в ней стандартизированными.
Роль языка XML Язык XML в технологии .NET используется повсеместно. В глобальном видении развития приложений в эпоху Internet компания Mtcrosoft также отводит ему особое место. Ниже перечислены некоторые применения языка XML в .NET. • • • • • •
Язык XML используется для кодирования запросов к Web-службам и ответов, возвращаемых клиенту. Язык XML может использоваться для моделирования данных в наборах данных, используемых в технологии доступа к данным ADO.NET. Язык XML используется при создании конфигурационных файлов. Для некоторых языков, поддерживаемых платформой .NET, документация на языке XML может быть сгенерирована автоматически. Язык XML— лингва-франка (общепринятый язык) для корпоративных серверов, построенных на платформе .NET. Язык XML используется технологией Web-служб для описания и передачи данных.
Роль языка XML
29
Факторы, определяющие успех Web-служб Перспектива Internet-приложений, как ее видит компания Microsoft, стала достоянием общественности. Окончательный успех инициативы, с которой выступила Microsoft, зависит от двух внешних факторов, которые не связаны со сферой программного обеспечения. А именно, от степени развития инфраструктуры сети Internet и успеха предложенной модели предприятия. Вопрос о том, приобретет ли технология Web-служб широкое распространение, прямо зависит от наличия сетей с высокой пропускной способностью. Такие сети уже сейчас широкодоступны. И пропускная способность их в последующие несколько лет существенно увеличится. А вот что касается перспектив предложенной модели предприятия, то они нам пока еще неизвестны! Важно отдавать себе отчет в том, что технология .NET обладает гораздо более широкими возможностями, чем громко рекламируемые возможности Internet. Более устойчивая платформа, предназначенная для создания Windows-приложений, чрезвычайно мощная библиотека классов .NET Framework, а также инструментальные средства разработки — это именно те особенности технологии .NET, благодаря которым она выдержит испытание временем.
Резюме Microsoft .NET— это новая платформа, построенная на верхнем слое операционной системы. Она обладает многими возможностями, которые позволяют создавать и развертывать как обычные, так и новые Web-ориентированные приложения. Web-службы позволяют использовать функциональные возможности приложений во всей сети Internet. Как правило, для организации взаимодействия с Web-службами задействован протокол SOAP (Simple Object Access Protocol — простой протокол доступа к объектам). Поскольку в основу протокола SOAP положены широко распространенные стандарты, в частности язык разметки гипертекста HTML (Hypertext Markup Language) и язык XML (extensible Markup Language), этот протокол характеризуется высокой степенью функциональной совместимости, а значит, и высокой способностью к взаимодействию. Платформа .NET использует управляемый код, для выполнения которого предназначена общеязыковая среда выполнения CLR. Общеязыковая среда выполнения CLR использует общую систему типов (Common Type System). Библиотека классов .NET Framework содержит огромное количество классов, которые в равной степени доступны в любом языке программирования, поддерживаемом платформой .NET. Ключевая роль в технологии .NET принадлежит языку XML. Все функциональные возможности, которыми обладает платформа .NET, могут использоваться как для создания более устойчивых Windows-приложений, так и для построения Internet-приложений.
30
Глава 1. Что такое Microsoft .NET?
Основы технологии .NET
/7,
латформа .NET решает многие проблемы, которые досаждали программистам в прошлом. К их числу относятся проблемы, связанные с развертыванием приложений, управлением версиями, утечкой памяти, а также проблемы безопасности. Платформа .NET позволяет разрабатывать мощные, независимые от языка программирования, настольные приложения и масштабируемые (расширяемые) Web-службы, построенные на базе новой мошной полнофункциональной библиотеки классов .NET Framework.
Проблемы, связанные с разработкой Windows-приложений Представьте себе симфонический оркестр, в котором группам струнных смычковых и ударных инструментов предстоит исполнить свои партии, используя при этом разные варианты партитуры. В таком случае, чтобы исполнить даже простейшую музыкальную композицию, музыкантам пришлось бы приложить героические усилия. Этот пример достаточно хорошо иллюстрирует деятельность разработчиков Windows-приложений. В процессе работы перед разработчиком возникает целый ряд вопросов. Использовать ли в приложении классы библиотеки базовых классов Microsoft (Microsoft Foundation Classes — MFC)? На каком языке писать приложение: на Visual Basic или на C++? Какой интерфейс для работы с базами данных использовать в приложении: открытый интерфейс взаимодействия с базами данных (Open Database Connectivity Interface — ODBC) или интерфейс OLE для баз данных, OLEDB? Использовать в приложении интерфейс модели компонентных объектов Microsoft (Component Object Model — COM) или интерфейс прикладного программирования (API) в стиле языка С? Если выбор сделан в пользу интерфейса модели компонентных объектов Microsoft (COM), какой тогда интерфейс использовать: IDispatch, дуальный (двойственный) интерфейс или только интерфейс с виртуальной таблицей? Какая роль во всем этом отводится Internet? До тех пор пока не появилась платформа .NET, довольно часто проект приложения искажался используемыми в процессе его реализации технологиями, которыми в тот период времени владели разработчики. Или же разработчику приходилось изучать еще одну технологию, которой было суждено через пару лет быть вытесненной следующей.
Развертывание приложения может оказаться трудной и неприятной задачей. В процессе развертывания приложения должны быть сделаны соответствующие записи в системном реестре, который является достаточно хрупким, а его восстановление — нелегкий труд. К тому же не существует хорошей стратегии управления версиями компонентов. Новые версии приложений могут разрушить уже существующие программы и при этом остается лишь догадываться о том, что же собственно произошло. Чтобы избежать проблем, связанных с хранением сведений о конфигурации системы в системном реестре, в других технологиях для этой цели используются метабазы или сервер SQL Server. Еще одной проблемой в Win32 является безопасность. Существующая модель безопасности тяжела для понимания. Еще более тяжело ее использовать на практике. Многие разработчики просто ее игнорируют. Разработчики, которые были вынуждены использовать существующую систему безопасности, пытались в этой трудной модели программирования делать все от них зависящее. Возрастающее значение безопасности, связанное с развитием Internet, грозит изменить плохую ситуацию на потенциальный кошмар. Даже там, где компания Microsoft попыталась облегчить процесс разработки приложений, проблемы все еще оставались. Многие системные службы приходилось разрабатывать с самого начала, по существу создавая инфраструктуру приложения, которая имела мало общего с бизнес-логикой. Гигантским шагом в сторону создания служб более высокого уровня стали сервер транзакций корпорации Microsoft (Microsoft Transaction Server, MTS) и COM+. Тем не менее, потребовалась еще одна парадигма разработки приложений. Модель компонентных объектов Microsoft (Component Object Model — COM) сделала возможным настоящее программирование с использованием компонентов. При этом приложение можно было создать достаточно просто с помощью языка Visual Basic. Но такие приложения не обладали достаточной гибкостью. Значительно более мощные приложения можно было создать с помощью языка C++, но при этом нужно было приложить значительные усилия. И это не говоря уже о том, что на языке C++ приходилось постоянно писать (постоянно воссоздавать) повторяющийся каркас (инфраструктуру) приложения. Если бы от этого всего можно было избавиться, скучать за IUnknown я бы не стал.
Приложения будущего Даже если бы платформа .NET смогла устранить все проблемы прошлого, этого все равно было бы недостаточно. Постоянный рост требований со стороны клиентов к функциональным возможностям приложений является одним из непреложных законов программирования. Возможность беспрепятственной работы приложений в разных компьютерных сетях, обусловленная развитием Internet, стала императивом. Функциональные возможности компонентов должны быть доступны также и с других машин. При этом никто из программистов не хочет писать базовый каркас; все они жаждут писать приложения, предназначенные для непосредственного решения проблем своих клиентов.
Обзор платформы .NET Платформа .NET содержит общеязыковую среду выполнения (Common Language Runtime — CLR). Общеязыковая среда выполнения CLR поддерживает управляемое выполнение, которое характеризуется рядом преимуществ. Совместно с общей системой типов (Common Type System — CTS) общеязыковая среда выполнения CLR поддерживает возможность взаимодействия языков платформы .NET. Кроме того, платформа .NET предоставляет большую полнофункциональную библиотеку классов .NET Framework. 32
Глава 2. Основы технологии .NET
волшебство метаданных Чтобы решить все проблемы, связанные с разработкой Windows-приложений, платформа .NET должна обладать базовым набором служб, которые в любой момент доступны в любом языке программирования. Чтобы предоставить такие службы, платформа .NET должна иметь достаточно сведений о приложении. Сериализация (преобразование в последовательную форму) объекта может послужить в качестве простого примера. Перед каждым программистом, рано или поздно, возникает проблема сохранения данных. Но зачем каждому программисту вновь изобретать колесо, решая вопрос о том, как следует сохранять вложенные объекты и сложные структуры данных? Зачем каждому программисту понимать, как эти объекты и данные хранятся в разных информационных хранилищах? Платформа .NET позволяет выполнить сериализацию объекта без вмешательства программиста. При желании разработчик может это сделать и самостоятельно. Чтобы понять, как происходит сериализация объектов, мы рассмотрим относящийся к данной главе пример S e r i a l i z e (Сериализация). Не станем ак- KG! ' центировать внимание на применяемых приемах программирования. Они буh дут рассмотрены позже. Сейчас же мы сосредоточимся на используемых в этом примере понятиях. //Serialize.cs #using tusing // using namespace System; // использование пространства имен Система; using namespace System::Collections; // использование пространства имен Система:: Коллекции; using namespace System::10; // использование пространства имен Система:: Ввод-вывод; using namespace System::Runtime:Serialization::Formatters::Soap; // использование пространства имен // Система:: Время выполнения:: Преобразование в последовательную // форму:: Форматеры:: Soap; [Serializable] // [Преобразование в последовательную форму] gc class Customer // класс сборщика мусора Клиент { public: String vpname; // Строка long id; // идентификатор gc class Test // класс сборщика мусора Испытание { public: static void Main() { ArrayList *plist = new ArrayList; Обзор платформы .NET
33
Customer *pcust = new Customer; // новый Клиент pcust->pname = "Charles Darwin"; // Чарльз Дарвин pcust->id = 10; // идентификатор plist->Add(pcust); // Добавить pcust = new Customer; // новый Клиент pcust->pname = "Isaac Newton"; // Исаак Ньютон pcust->id = 20; // идентификатор plist->Add(pcust); // Добавить for (int i=0; i < plist->get_Count(); i++) { Customer *pcust = // Клиент dynaraic_cast // (plist->get_Item(i)) ; Console::WriteLine( "{0}: {1}",. pcust->pname, box(pcust->id)); // идентификатор } Console::WriteLine("Saving Customer List"); // ("Сохранение списка клиентов"); FileStream *ps = new FileStream( "cust.txt", FileMode::Create); // Создать SoapFormatter *pf = new SoapFormatter; pf->Serialize (ps, plist); // Преобразование в последовательную форму; ps->Close (); Console::WriteLine("Restoring to New List"); // "Восстановить в новом списке") ; ps = new FileStreamCcust.txt", FileMode: :Open) ; // Открыть pf = new SoapFormatter(); ArrayList *plist2 = dynamic_cast (pf->Deserialize(ps) ) ; ps->Close();
}
for (int i=0; i < plist->get_Count(); i++) { Customer *pcust = // Клиент dynamic_cast // (plist->get_Item(i)); Console::WriteLine( "{0}: {1}", pcust->pname, box(pcust->id)) ; // идентификатор }
void main(void) { Test::Main(); }
34
Глава 2. Основы технологии .NET
Мы определили класс Customer (Клиент) с двумя полями: pname и id (идентификатор). Сначала программа создает экземпляр коллекции, в котором будут храниться экземпляры класса Customer (Клиент). Мы добавляем в коллекцию два объекта Customer (Клиент), а затем распечатываем содержимое коллекции. Потом коллекция сохраняется на диске. Она восстанавливается в новый экземпляр коллекции и выводится на печать. Распечатанные теперь данные будут идентичны данным, которые были распечатаны перед сохранением коллекции1. Если вы запустите приложение и откроете получившийся в результате файл c u s t . t x t , вы увидите, что он содержит данные в необычном XML-формате, который известен как простой протокол доступа к объектам (Simple Object Access Protocol — SOAP). Этот протокол специально разработан для хранения и передачи объектов. Мы не писали код для того, чтобы указать, как сохраняются или восстанавливаются поля объекта Customer (Клиент). Но мы определили формат (SOAP) и создали среду, в которой затем были сохранены данные. Классы библиотеки .NET Framework сгруппированы таким образом, что каждый выбор — среды, формата и способа загрузки (восстановления) или сохранения объекта — можно сделать независимо друг от друга. Такого типа разделение классов существует в библиотеке .NET Framework повсеместно. Класс Customer (Клиент) имеет атрибут S e r i a l i z a b l e (Преобразуемый в последовательную форму, упорядочиваемый). Аналогично поле имени имеет атрибут p u b l i c (общедоступный). Когда вы не хотите, чтобы объект можно было преобразовывать в последовательную форму, не приписывайте ему соответствующий атрибут. Если будет предпринята попытка сохранения объекта, который не имеет атрибута S e r i a l i z a b l e (Преобразуемый в последовательную форму, упорядочиваемый), возникнет исключительная ситуация и произойдет отказ в работе программы2. При программировании на платформе .NET атрибуты можно применять повсеместно. Использование атрибутов позволяет описать способ обработки кода и данных библиотекой классов .NET Framework. При помощи атрибутов можно также указать используемую модель безопасности. Атрибуты можно использовать для того, чтобы организовать с помощью каркаса синхронизацию многопоточной обработки. Благодаря использованию атрибутов становится очевидной идея удаленного размещения объектов. Чтобы указать, что объект может сохраняться и восстанавливаться библиотекой .NET Framework, компилятор добавляет атрибут S e r i a l i z a b l e (Преобразуемый в последовательную форму, упорядочиваемый) к метаданным класса Customer (Клиент). Метаданные представляют собой дополнительную информацию о программном коде и данных, которая содержится в самом .NET-приложении. Метаданные, являющиеся характерным свойством общеязыковой среды выполнения CLR, могут содержать также и другую информацию о коде, включая: • номер версии и информацию о местной специфике (регион, язык); • все используемые типы; ' В результате инсталляции примеров программ, которыми сопровождается данная книга, должен быть создан пример, готовый к выполнению. Если он отсутствует, щелкните два раза на том файле решения Visual Studio.NET, который имеет расширение .sin. Когда откроется Visual Studio, нажмите комбинацию клавиш Ctrl-F5 для того чтобы построить и выполнить пример. ^ Вьщелите в программе атрибут s e r i a l i z a b l e (Преобразуемый в последовательную форму, упорядочиваемый) как комментарий и посмотрите, что при этом произойдет. Для того чтобы ввести комментарий в программу, вы можете использовать синтаксис языка С и C++, то есть применять пары символов /* и */ в качестве открывающей и закрывающей цепочек комментария. Обзор платформы .NET
35
• подробности о каждом типе, включая его имя, область видимости и т.д.; • подробную информацию о членах каждого типа, в частности, используемые ими методы, сигнатуры методов, и т.д.; • атрибуты. Метаданные хранятся вместе с программным кодом, а не в каком-то центральном хранилище наподобие системного реестра в операционной системе Windows. Способ их хранения не зависит от используемого языка программирования. Благодаря всему этому .NETприложения содержат самоописания. Во время выполнения приложения может быть выдан запрос метаданных с целью получения информации о коде (например, о наличии или отсутствии атрибута S e r i a l ! zable (Преобразуемый в последовательную форму, упорядочиваемый)). Вы можете расширить метаданные, дополнив их своими собственными атрибутами. В нашем примере библиотека .NET Framework может запросить метаданные для того, чтобы получить информацию о структуре объекта Customer (Клиент), которая затем используется для сохранения и восстановления объекта.
Библиотека классов .NET Framework В предыдущем примере S e r i a l i z e (Сериализация) используются классы SoapForm a t t e r и F i l e S t r e a m . Они являются лишь двумя из более чем 2500 классов библиотеки .NET Framework. Классы библиотеки .NET Framework создают каркас (инфраструктуру) приложения и предоставляют системные службы .NET-приложениям. Ниже перечислены лишь некоторые из функциональных возможностей библиотеки классов .NET Framework: • библиотека базовых классов, —- содержит основные функциональные возможности, такие как строки, массивы и элементы форматирования; • передача данных по сети; • система безопасности; • удаленная обработка; • диагностика; • ввод/вывод; • базы данных; • языкХМЬ; • Web-службы, которые позволяют использовать интерфейсы компонентов в любом месте Internet; • Web-программирование; • пользовательский интерфейс операционной системы Windows.
Программирование на основе интерфейсов Предположим, что вы хотите зашифровать ваши данные, и, следовательно, не желаете полагаться на сериализацию (преобразование в последовательную форму), реализованную на основе простого протокола доступа к объектам (Simple Object Access Protocol — SOAP), входящего в состав библиотеки .NET Framework. Ваш класс может наследовать интерфейс I S e r i a l i z a b l e и содержать реализацию соответствующего алгоритма (как это сделать, мы обсудим в следующих главах). Тогда при сохранении и восстановлении данных библиотека .NET Framework будет использовать ваши методы. Но как библиотека .NET Framework узнает о том, что вы реализовали интерфейс I S e r i a l i z a b l e ? Оказывается, она может запросить метаданные соответствующего класса 36
Глава 2. Основы технологии .NET
для того, чтобы узнать, наследует ли он указанный интерфейс! Затем при сериализации объекта или преобразовании его из последовательной формы в "параллельную" библиотека классов .NET Framework может использовать либо ее собственный алгоритм, либо код соответствующего класса. Программирование на основе интерфейсов используется платформой .NET для того, чтобы при помощи разработанных программистом объектов дополнить стандартные функциональные возможности библиотеки классов .NET Framework. Использование интерфейсов позволяет также привести работу с разными объектами к общему знаменателю, не зная точного типа объекта. Например, средства форматирования (скажем, форматер SOAP, который используется в данном примере) наследуют интерфейс I F o r r a a t t e r . Программы могут быть написаны безотносительно к какому бы то ни было конкретному (двоичному, SOAP) форматеру, используемому сейчас, или форматеру, который будет использоваться в будущем, и при этом они будут работать должным образом.
Объектом является все Если тип содержит метаданные, тогда среда выполнения может делать многие замечательные вещи. Но все ли объекты в .NET содержат метаданные? Да! Каждый тип, будь то тип, определенный пользователем (например, Customer (Клиент)) или тип, являющийся частью библиотеки классов .NET Framework (например, F i l e S t r e a m ) , является объектом среды .NET. Все объекты среды .NET являются производными от одного базового класса— системного класса Object (Объект). Поэтому все, что выполняется в среде .NET, имеет тип и, следовательно, содержит метаданные.
^Типьг—i сердце; модели п р о г р а м м и р о в а н и я ^ г о с ^СЩ^Т.ип-аналоги^^^ ф программирования .и сочетает- в себе: и с п о д ь з у е м у ^ ; ;;п6ве|Еёнке. Тип й общеязыковой среде в
^
^
^
^
й
Щ
пол*! (элементы данных ; методы; „свойства;; Имёкггся'; т а к ж е / в с т р ^ /данных, тип чисел с плавающей точкой,;стр6кй,:й fакдалее. У^-ЩмШ^ШШШшМъШ^ В нашем примере код преобразования объектов в последовательную форму может просматривать список (типа A r r a y L i s t ) объектов Customer (Клиент), и сохранять каждый объект, а также весь массив, к которому принадлежит объект. Это возможно благодаря тому, что метаданные содержат информацию как о типе объекта, так и о его размещении. Из дальнейшего станет ясно, что благодаря тому, что все .NET-объекты являются производными от общего базового класса, открываются и некоторые другие возможности.
Общая система типов Типы, передаваемые библиотеке классов .NET Framework, имеют некоторую общую природу. Эти типы определяются обшей системой типов (Common Type System — CTS). Общая система типов CTS определяет правила для типов и действий, которые поддерживает среда выполнения CLR. Именно общая система типов CTS накладывает на классы Обзор платформы .NET
37
.NET ограничение единичного наследования реализации. Хотя общая система типов CTS определена для широкого множества языков программирования, не все эти языки должны поддерживать все свойства типов данных, предусмотренные в общей системе типов CTS. Например, в языке C++ множественное наследование разрешено для неуправляемых классов, но запрещено для управляемых. Промежуточный язык Microsoft (Microsoft Intermediate Language — MSIL, или просто IL) определяет систему команд, которая используется всеми компиляторами, транслирующими на язык платформы .NET. Этот промежуточный язык не зависит от используемой платформы. Код на языке MSIL затем преобразуется во внутренний (собственный) код платформы. Мы можем быть уверены, что классы библиотеки .NET Framework будут работать со всеми языками, поддерживаемыми платформой .NET. Особенности создаваемого приложения больше не накладывают ограничений на выбор языка программирования, а выбор языка программирования больше не ограничивает возможности создаваемого приложения. Промежуточный язык MSIL и общая система типов CTS позволяют многим языкам программирования, компиляторы для которых могут генерировать код на языке MSIL, использовать библиотеку классов .NET Framework. Именно в этом состоит одно из наиболее заметных различий между платформами .NET и Java, которые в значительной степени используют одну и ту же философию.
ILDASM — дисассемблер промежуточного языка Microsoft Дисассемблер промежуточного языка Microsoft ILDASM (Microsoft Intermediate Language Disassembler) может отображать метаданные и инструкции языка MSIL, связанные с соответствующим .NET-кодом. Дисассемблер ILDASM является очень полезной утилитой, которая используется при отладке приложений. Он позволяет более глубоко понять инфраструктуру платформы .NET. Кроме того, дисассемблер промежуточного языка Microsoft ILDASM можно использовать для изучения кода библиотеки классов .NET Framework3. На рис. 2.1 приведен фрагмент кода на языке MSIL, взятый из примера Ser i a l i z e (Сериализация). В данном фрагменте описывается создание двух новых объектов Customer (Клиент) и их добавление всписок 4 . Инструкция newobj создает новую объектную ссылку, используя параметр конструктора 5 . Инструкция s t l o c сохраняет значение в локальной переменной. Инструкция l d l o c загружает значение локальной переменной^. Настоятельно рекомендуем вам поэкспериментировать с дисассемблером ILDASM и изучить его возможности.
3
Дисассемблер ILDASM можно найти в меню Tools (Сервис) Visual Studio.NET. Он находится также в подкаталоге Microsoft.NET\FrameworkSDK\Bin. Дисассемблер можно активизировать, шелкнув два раза на его названии в окне Проводника (Explorer) или с помощью командной строки. Если вы активизируете дисассемблер ILDASM с помощью командной строки (или из среды VS.NET), то используйте ключ /ADV для получения доступа к некоторым его дополнительным возможностям. Откройте пример Seriatize.exe и щелкните на знаке плюс (+) рядом с пунктом Test (Тестирование). Щелкните два раза на элементе Main (Главная), чтобы инициировать в MSIL главную процедуру. Формально он не является параметром. В промежуточном языке IL используется стек; конструктор представляет собой лексему метаданных, записанную в стек. Подробно промежуточный язык Microsoft MSIL описан в документах Европейской Ассоциации производителей ЭВМ (European Computer Manufacturers' Association — ЕСМА). Особенно рекомендуется изучить раздел "Partition 111: CIL Instruction Set", посвященный системе команд.
38
Глава 2. Основы технологии .NET
ЯШШШШШШ IL_eeao; ldnull IL~B001: stloc.1 IL 0082: ldnull IL B 0B3: tloc.O IL BOOJJ: s ldnull U 7 IL 90 OS: stl I;; oc.s IL 0007: ldnul l IL O B 08: •щ stl oc.2 IL ЯВВ9: ldnul l| pF IL OBOa: OBOc: stloc.s IL авва: ldnull p l i s t 2 IL BBBf: stloc.s IL ВОЮ: ldnull U 6 IL B012: stloc.s instance IL 1017: neuobj IL 0818: stloc.1 Instance IL 001(1: neuobj IL ВВ1е: stloc.B IL BB1F: ldloc.B ualuetype IL BO2K: ldsflda instance IL neuobj s t r i n g Custoner::pnane "..,; IL B029: stfld IL BO2e: BBZF: l d l D C . B IB IL_0ВЭ1: ldc.iK.s i n t 3 2 R o d o p t ( [ K i c r o s o f t . U i s u a l C ] H i c r o 5 o F t . U i s u a l C . l s L o n g H o d i F i e r ) Custo '• IL~ВВЭ6: stfld IL BBS 7: ldloc.1 IL HB3B: ldloc.B instance IL BB3d: calluirt IL po IL B03e: в*3: n ep uot.b j instance IL Ввв*И: Is dtlo i oc .O в IL B01S: l d s f l d a ualuetype SfirrayType$№ // Требуется для управляемого кода на C++ u s i n g namespace System; // используется пространство имен Система // Не требуется, но обычно используется Директива препроцессора # u s i n g похожа на директиву #import в прежних версиях Visual C++ тем, что делает доступной для компилятора информацию о типах. В случае директивы # import информация о типах содержалась в библиотеках типов, обычно являвшихся файлами TLB, DLL, OCX или ЕХЕ. В случае директивы # u s i n g информация о типах представлена в форме метаданных, содержащихся в сборке .NET. Сборка mscorl i b . d l l содержит информацию о типах, необходимую всем приложениям .NET, включая информацию о базовом классе, являющемся предком всех управляемых классов, — классе System: : O b j e c t (Система::Объект). Заметим, что в такой записи System (Системное пространство имен) обозначает пространство имен, a Object (Объект) — имя корневого класса иерархии управляемых типов.
Ваша первая программа на управляемом C++.NET Хотя вы, почти наверняка, хорошо знакомы с C++, мы начнем с рассмотрения очень простого, но традиционного примера— программы HelloWorld (Привет, мир). В этом разделе мы расскажем, как написать, скомпилировать и запустить эту и другие программы.
Программа H e l l o W o r l d (Привет, мир) Чуть ниже приведен пример кода из очень простой управляемой программы, которая выводит на консоль одну-единственную строку. Вы можете открыть сопровождающее решение1 или создать свой проект и ввести текст программы самостоятельно. Для того чтобы это сделать, необходимо создать пустой проект HelloWorld (Привет, мир), добавить исходный код, а затем скомпилировать и запустить проект.
Как и для всех других примеров в данной книге, реализация программы HelloWorld доступна читателю в готовом виде. Исходные файлы этого проета находятся в папке C:\Of\NetCpp\Chap3\HelloWorId. Для того чтобы открыть его в Visual Studio, дважды щелкните на файле HelloWorld.sin в Проводнике.
Ваша первая программа на управляемом C++.NET
47
Как создать консольное приложение на управляемом C++ Создайте пустой проект консольного приложения Managed C + + , называющийся H e l l o W o r l d (Привет, мир): 1. Откройте Visual Studio.NET. Выберите пункт меню FileoNewOProject (ФаЙл^Создать^ Проект) для того чтобы открыть диалог New Project (Создание проекта). 2. Выберите пункт "Visual C + + Projects (Проекты Visual C + + ) в списке project Types (Типы проектов). 3. Выберите пункт Managed C + + Empty project (Пустой проект на управляемом C + + ) в списке Templates (Шаблоны). 4. Введите H e l l o W o r l d (Привет, мир) в качестве названия проекта. 5. Задайте папку, в которой будет храниться проект. 6. Щелкните на ОК для того чтобы закрыть диалог New Project (Создание проекта) и завершить создание нового проекта. Добавьте исходный код: 7. Щелкните правой кнопкой на папке Source Files (Исходные файлы) в окне Solution Explorer (Поискрешений). Выберите пушег меню Add-^Add New Item (Добавить^Добавить новый элемент) для того, чтобы открыть диалог Add New Item dialog (Добавить новый элемент). 8. Выберите в списке Templates (Шаблоны) пункт C + + File (Файл C + + ) . 9. Укажите H e l l o W o r l d (Привет, мир) в качестве названия проекта. 10. Не изменяйте значение расположения (Location), принятое по умолчанию. 11. Щелкните на кнопке Open (Открыть) для того, чтобы закрыть диалог Add New Item dialog (Добавить новый элемент) и открыть Source Editor (Редактор текстов программ). 12. Введите код примера H e l l o W o r l d (Привет, мир). Скомпилируйте и запустите проект: 13. Выберите пункт меню Build^Build (Создать^Создать). 14. Используйте сочетание клавиш Ctrl-F5 для запуска программы без отладчика. Директива # u s i n g необходима для всех программ на управляемом C++. Она делает доступными для компилятора стандартные типы (такие, как Console (Консоль) и O b j e c t (Объект)), определенные в библиотеке классов .NET. Класс Console (Консоль) находится в пространстве имен System (Системное пространство имен) и его полное имя *— System: : C o n s o l e (Система::Консоль). Данный класс содержит метод W r i t e L i n e , выводящий на консоль текст и добавляющий к нему символ новой строки. //HelloWorld.срр #using // требуется для кода на управляемом C++ void main(void) { System::Console::WriteLine("Hello World"); // ("Привет, мир");
48
Глава 3. Программирование на управляемом C++
Программа может быть скомпилирована либо в Visual Studio.NET, либо при помощи командной строки с параметром /CLR (Common Language Runtime compilation — компиляция для выполнения в общеязыковой среде). Если вы используете командную строку, вы должны определить соответствующую среду. Простейший способ сделать это — открыть командное окно, выбирая пункты меню Start (Пуск) ^Programs (ПрограмMbi)*>Microsoft Visual Studio.NET 7.0^>Visual Studio.NET ToolsOVisual Studio.NET Command Prompt. В командной строке c l /CLR HelloWorld.cpp исходный файл компилируется, а затем автоматически компонуется так, что результатом является ЕХЕ-файл HelloWorld.exe. Позже мы расскажем, как создать управляемую динамически подключаемую библиотеку (DLL). Полученную управляемую программу можно запустить в Visual Studio.NET или из командной строки, как обычный исполняемый файл. Результатом работы программы будет следующее сообщение: Hello World (Привет, мир)
Директива #using и оператор using Директива # u s i n g делает доступной для компилятора информацию о типах, содержащуюся в сборке. Сборка содержит метаданные (описание информации о типах) и код на промежуточном языке IL. Сборка m s c o r l i b . d l l содержит описания многих полезных стандартных классов, определенных в .NET Framework, в том числе класса Console (Консоль), использовавшегося в предыдущем примере, и класса Object (Объект), который является базовым для всех управляемых классов. Добавим, что директива t u s i n g совершенно не похожа на директиву # i n c l u d e , вставляющую в компилируемый файл некоторый другой исходный файл. Как отмечено выше, директива #using скорее напоминает по совершаемым действиям директиву # import. В предыдущем примере System (Системное пространство имен) представляет пространство имен C++, прямо соответствующее пространству имен .NET, имеющему то же название. Полное название класса состоит из названия пространства имен, за которым следуют два двоеточия и название класса, например, System: : Console (Система::Консоль). Хотя выражение us^ng namespace, в предыдущем примере не используется, оно позволяет использовать короткие имена классов, например, Console (Консоль). Обратим ваше внимание на то, что выражение u s i n g namespace (определенное стандартом ANSI C++) и директива fusing (определенная в Microsoft C++)— совершенно разные вещи. Приведем пример использования выражения using namespace, позволяющего заменить полное имя System: : Console (Система::Консоль) укороченным Console (Консоль): //HelloWorld.cpp #using getlnt("How many temp's? " ) ; // Сколько? for (int i = 0; i < numTemp; i++) { i n t fahr = piw->getlnt("Temp. ( F a h r e n h e i t ) : " ) ; // Фаренгейт i n t c e l s i u s = (fahr - 32) * 5 / 9; // Цельсия Console: .-WriteLine ( " F a h r e n h e i t = {0}", f a h r . T o S t r i n g ( ) ) ; // Фаренгейт Console::WriteLine("Celsius = {0}", b o x ( c e l s i u s ) ) ; // Цельсия Заметим, что первым аргументом метода WriteLine является форматирующая строка. Например, при первом вызове метода WriteLine форматирующая строка имеет вид " F a h r e n h e i t = { 0} ", где { 0} — заглушка, указывающая, что на это место следует вставить второй аргумент WriteLine. Число, помещенное в фигурные скобки, определяет, какой именно из следующих за форматирующей строкой аргументов следует вывести в указанном месте (естественно, нумерация начинается с нуля). В нашем примере это число — 0, так как за форматирующей строкой следует только один аргумент. Подставляемые аргументы могут быть нескольких типов, включая строки или упакованные значения, что и продемонстрировано в примере. Приведем пример работы программы, в котором преобразование температур производится два раза; How many temp's? 2 Temp. ( F a h r e n h e i t ) : 212 F a h r e n h e i t = 212 C e l s i u s = 100 Temp. ( F a h r e n h e i t ) : 32 F a h r e n h e i t = 32 Celsius = 0 Перевод такой2: Сколько температур? 2 Фаренгейта: 212 Фаренгейта = 212 Цельсия = 100 Фаренгейта: 32 Фаренгейта = 32 Цельсия = 0 В следующей программе продемонстрировано, как выводить данные в некоторых форматах с помощью метода WriteLine. Для этого применяются коды форматирования. Чтобы получить более подробную информацию о кодах форматирования, используемых в методе WriteLine (совпадающих, кстати, с кодами для метода s t r i n g : : Format (Строка::Формат)), обратитесь к документации по .NET SDK. //FormatString.cpp #using Rep!ace('H1, 'J'); // Замена Console : :WriteLine(psl) ; Console::WriteLine(ps2); Console::WriteLine("StringBuilder can be modified:"); // ("StringBuilder может изменяться: " ) ; Str:.ngBuilder *psbl = new StringBuilder(S"Hello World"); // Привет, Мир StringBuilder *psb2 = psbl->Replace('H', ' J ' ) ; // Замена Console::WriteLine(psbl); Console::WriteLine(psb2); }
Информация, выведенная на экран программой, показывает, что действительно, содержимое объекта, на который указывает psl, не изменяется, т.е. метод Replace (Замена) не изменяет исходный объект String (Строка). С другой стороны, объект *psbl изменяется методом Replace (Замена). String is immutable: Hello World Jello World StringBuilder can be modified: Jello World Jello World
Перевод такой3: Строка является неизменной: Привет, Мир J e l l o Мир StringBuilder может измениться: J e l l o Мир J e l l o Мир
В приведенном выше фрагменте кода вы можете заметить строковые литералы, определенные с префиксом S и без него. Строковый литерал, определенный с использованием только кавычек, является указателем на char (символ), т.е. указателем на последовательность символов ASCII, заканчивающуюся нулем. Такой указатель не является указателем на объект s t r i n g (Строка). А строковый литерал, определенный с префиксом S, является указателем на управляемый объект String (Строка). Префикс L, не использовавшийся в предыдущем примере, обозначает строку символов Unicode, которая также не является объектом String (Строка). Следующий фрагмент демонстрирует эти три типа строк: char *psl = "ASCII s t r i n g l i t e r a l " ; // неуправляемый // символ *psl = "строковый литерал ASCII "; wchar_t *ps2 = L"Unicode s t r i n g l i t e r a l " ; // неуправляемый // L " строковый литерал Уникода "; String *ps3 = S"String object l i t e r a l " ; // управляемый // Строка *ps3 = S " строковый литерал - объект String ";
3
Добавлен редактором русского перевода. — Прим. ред.
Ваша первая программа на управляемом C++.NET
53
S t r i n g (Строка) содержит много полезных методов. Так, для сравнения объектов можно использовать метод Equals (Равняется), что продемонстрировано в следующем примере. Подробнее о методах объекта S t r i n g (Строка) можно узнать из документации по .NET SDK. //Strings.срр #using using namespace System; // использовать пространство имен Система; void main(void) {
String *pstcl = new S t r i n g ( " h e l l o " ) ; // Строка *pstrl = новая Строка ("привет"); String *pstr2 = new S t r i n g ( " h e l l o " ) ; // Строка *pstr2 = новая Строка ("привет"); if (pstrl->Equals(pstr2)) // если (pstrl-> Равняется (pstr2)) Console::WriteLine("equal"); // равны - выполняется else Console::WriteLine("not equal"); // не равный - не // выполняется if (pstrl==pstr2) // если (pstrl ~= pstr2) Console::WriteLine("equal"); // равны - не выполняется else Console::WriteLine("not equal"); // не равный - выполняется
}
Результат работы программы показывает разницу между сравнением объектов S t r i n g (Строка) с помощью метода Equals (Равняется) и оператора ==. Метод Equals (Равняется) проверяет равенство содержимого объектов, тогда как оператор == проверяет лишь равенство указателей (т.е. равенство адресов объектов в памяти). Equal not equal Вот перевод4: равны не равны Метод T o S t r i n g обеспечивает представление объекта S t r i n g (Строка) для любого управляемого типа данных. Хотя метод T o S t r i n g не является автоматически доступным для неуправляемых классов, он доступен для упакованных значимых и упакованных примитивных типов, таких, как i n t или f l o a t (с плавающей точкой). Упаковка и распаковка, также как значимые типы, управляемые и неуправляемые типы, будут рассмотрены ниже в этой главе.
' КОД
Метод ToString наиболее часто используется для вывода информации, а также при отладке, и создаваемые управляемые классы обычно заменяют T o S t r i n g так > чтобы он возвращал определенную разработчиком, удобочитаемую информацию об объекте. Метод O b j e c t : : ToString просто возвращает полное имя
Добавлен редактором русского перевода. — Прим. ред.
54
Глава 3. Программирование на управляемом C++
класса данного объекта и его реализация (не особо полезная, впрочем) доступна через наследование любому управляемому типу. Следующий пример демонстрирует некоторые аспекты работы метода ToString: //ToString.cpp #using GetType{; Console::WriteLine "box(ui)->GetType Отображение C++ на спецификацию общего языка...
69
Console::WriteLine( box(1)->GetType()); Console::WriteLine( box(ul)->GetType()); Console::WriteLine(intManagedArray->GetType( Программа напечатает9: System .Boolean System Char System .Object System . String System . Single System Double System SByte System Byte System ,Intl6 System ,UIntl6 System , Int32 System .UInt32 System . Int32 System ,UInt32 System .Int32[]
// Система. Булева переменная // Система. Символ // Система. Объект // Система. Строка // Система. Одинарный // Система. Двойной // Система. Байт
f Global Functions::main: int3Z modopt([mscorBbJi method p u b l i c s t a t i c i n t 3 2 modopt([mscorlib]System.Rujb main() c i l managed .utentry 1 : 1 // Code size 320 (0x148) .maxstack 1 .locals ([0] int32[] U_0, [I] int32[] intManagedArray, [2] string pstr, [3] object pobj , [U] unsigned int32 ul, [5] int32 1, [6] unsigned int32 ui, [7] int32 i, [8] unsigned int16 us, [9] int16 s, [10] unsigned int8 uc, [II] int8 c, [12] float64 d, [13] float32 f, [14] unsigned int16 ch, [15] bool b) IL_OflOB: lrinull Jj
I
Рис. 3.3. Использование утилиты Ildasm.exe для просмотра типов данных в программе, реализованной на управляемом C++
у
Комментарии справа добавлены для удобства ориентирования. — Прим. ред.
70
Глава 3. Программирование на управляемом C++
Программирование на C++ для платформы .NET В этом разделе главы мы изучим основные аспекты создания кода на управляемом C++. В частности, будут рассмотрены все ключевые слова расширения управляемости C++, поддерживаемые Visual C++.NET. Заметим, что это далеко не все ключевые слова Visual C++ 7.0, не определенные стандартом ANSI C++, — ведь мы концентрируем ваше внимание именно на расширении управляемости C++. Однако в рассмотрении затрагиваются некоторые аспекты, не относящиеся к управляемому коду. Например, использование ключевого слова i n t e r f a c e (интерфейс) не ограничивается лишь управляемым кодом. И в заключение мы кратко опишем атрибуты, технически не относящиеся к управляемости. Соответствие VC++.NET И ANSI C++ Стоит сказать, что все эти особые ключевые слова, связанные с управляемостью, не Противоречат ANSI C++, так что фактически VC++.NET является . более совместимым с ANSI C++, нежели предыдущие версии VC++. При использовании командной строки следует задавать параметр /CLR (Компиляция для выполнения в общеязыковой среде) компилятора, иначе применение ключевых слов, связанных с управляемостью, не допускается. В Visual Studio корректные установки параметров обеспечиваются при выборе соответствующего шаблона автоматически. Тем не менее, если возникла необходимость установить корректные значения параметров, выполните следующие указания: 1. Щелкните в окне Solution Explorer (Поиск решения) правой кнопкой на узле проекта (но не на узле решения). 2. Выберите пункт меню Properties (Свойства). При этом откроется диалог Project Property Pages (Страницы свойств проекта). 3. Выберите узел General (Общие) под узлом C/C++ и выберите Assembly Support (/clr) для опции Compile As Managed (Компилировать как управляемый). 4. Щелкните на кнопке ОК.
Управляемые и неуправляемые типы Управляемый тип — тип данных, инициализируемый (обычно с помощью оператора new (создать)) в управляемой динамически распределяемой области памяти, но ни в коем случае не в неуправляемой динамически распределяемой области памяти или стеке. Попросту говоря, управляемый тип — тип, для которого сборка мусора осуществляется автоматически, потому для освобождения ресурсов, используемых объектами этого типа, нет необходимости использовать оператор d e l e t e (удалить). Вместо того чтобы явно удалять объект, можно либо сделать так, чтобы на него не указывал ни один указатель, либо явно приравнять этот указатель нулю. Неуправляемый тип — тип, который игнорируется автоматическим сборщиком мусора, вследствие чего программист должен освобождать занимаемую объектом память с помощью оператора d e l e t e (удалить). Объекты неуправляемых типов никогда не создаются в управляемой динамически распределяемой области памяти, а только либо в неуправляемой динамически распределяемой области памяти, либо в каком-нибудь другом месте памяти, как переменные в стеке или элементы данных другого неуправляемого класса. Поэтому именно неуправляемые типы — это то, с чем привыкли иметь дело программисты C++, тогда как управляемые типы больПрограммирование на C++ для платформы .NET
71
ше похожи на ссылочные типы языка Java, для которых применяется автоматическая сборка мусора. Ключевое слово __дс'(сокращение от "garbage collection"— "сборка мусора") используется для объявления управляемых классов, или структур, и может использоваться для указателей и массивов. Ключевое слово поде (сокращение от "по garbage collection" — "без сборки мусора") является антонимом дс (сборщик мусора). Надо иметь в виду, что ключевое слово дс (сборка мусора) можно использовать только в управляемом коде, а значит, при этом следует использовать параметр компилятора /CLR (Компиляция для выполнения в общеязыковой среде), причем прагма #pragma unmanaged должна быть неактивна. Ключевое слово nogc (без сборки мусора) можно использовать как в управляемом, так и в неуправляемом коде. Следующий фрагмент демонстрирует типичное использование дс (сборщик мусора) при определении управляемого класса: gc c l a s s ManagedClass // класс сборщика мусора ManagedClass
Ключевое слово поде (без сборки мусора) просто означает, что класс, структура, массив или объект, на который указывает определенный с этим словом указатель, не управляется сборщиком мусора .NET. Данное ключевое слово используется для явного указания, что объект никогда не создается в управляемой динамически распределяемой области памяти. Недопустимо наследование типа, определенного с ключевым словом дс (сборщик мусора) или __подс (без сборки мусора), от типа, определенного с другим из этих ключевых слов, равно, как не допускается использование дс (сборщик мусора) в неуправляемом коде. nogc c l a s s UnmanagedClass
t }; Заметим, что автоматическая сборка мусора управляемых объектов касается лишь освобождения неиспользуемой управляемой динамически распределяемой области памяти, но не других ресурсов, таких, как дескрипторы файлов или соединения с базами данных.
Управление сборкой мусора В документации по языкам .NET вы могли встречать описание метода F i n a l i z e (Завершить), используемого для освобождения ресурсов, не находящихся в управляемой динамически распределяемой области памяти, но созданных управляемыми объектами. Однако в C++ реализовывать данный метод не надо. Если же все-таки сделать это, компилятор выдаст сообщение об ошибке, указав, что вместо метода F i n a l i z e (Завершить) для управляемого класса требуется определить деструктор. Сборщик мусора автоматически вызовет деструктор (в отдельном потоке) при освобождении памяти, занятой объектом; но момент вызова деструктора не определен. А это значит: не следует рассчитывать на то, что деструктор будет вызван при удалении ссылки на объект. У
72
Если вы реализовали деструктор и удаляете управляемый объект явно, дест* РУКТОР будет вызван сразу, и сборщик мусора уже не будет его вызывать. Можно также, вызвав статический метод GC: : C o l l e c t () (Сборщик мусора::Собрать()), вынудить сборщик мусора попытаться освободить память изпод объекта, а вызов деструктора синхронизировать с завершением работы Глава 3. Программирование на управляемом C++
сборщика мусора при помощи статического метода GC: : W a i t F o r P e n d i n g F i n a l i z e r s . Впрочем, обычно неудобно и неэффективно вызывать сборку мусора принудительно или синхронизовать ее с вызовом деструктора, поэтому, если необходимо выполнять очистку в определенный момент, рекомендуется реализовать это независимо в отдельном методе, а затем вызывать его явным образом. Этот метод рекомендуется называть Dispose (Ликвидировать). Рассмотрим следующую программу. //ManagingGC.cpp #using using namespace System; // использовать пространство имен Система; gc class ManagedClass // класс сборщика мусора ManagedClass { public: ManagedClass() { Console::WriteLine("c1tor") ; } -ManagedClass() { Console::WriteLine("d1tor") ;
void main(void) { Console::WriteLine("start"); // начало ManagedClass *pmc = new ManagedClass; // Раскомментируйте следующую строку // для предотвращения вызова деструктора //GC::SuppressFinalize(pmc); // СБОРЩИК МУСОРА Console::WriteLine("middle"); // середина // Раскомментируйте следующую строку // чтобы вызвать деструктор пораньше //delete pmc; // удалить pmc = 0; // ... или две следующие строки для Toro'j // чтобы вызвать деструктор пораньше '• / / G C : : C o l l e c t О ; // СБОРЩИК МУСОРА:: Собрать //GC: :WaitForPendingFinalizers () ; // СБОРЩК МУСОРА Console::WriteLine("end"); // конец } Приведем результат работы программы. Обратите внимание, что деструктор вызывается после того, как программа напечатает end (конец) 1 0 . start // начало с' tor 10 Комментарии справа добавлены для удобства ориентирования. — Прим. ред. Программирование на C++ для платформы .NET
73
middle // середина end // конец d'tor Однако, если раскомментировать строку, содержащую вызов метода S u p p r e s s F i n a l i z e , деструктор не будет вызван вообще, что доказывается следующей выдачей 11 : start // начало с 't o r middle // середина end // конец Кроме того, если раскомментировать оператор, в котором используется delete (удалить), деструктор будет вызван до того, как программа напечатает end (коней) 12 . start // начало с' tor middle // середина d ' tor end // конец Наконец, если раскомментировать только два оператора, содержащих вызовы методов C o l l e c t (Собрать) и W a i t F o r P e n d i n g F i n a l i z e r s , деструктор опять будет вызван до того, как программа напечатает end (конец). В этом случае вызов метода C o l l e c t (Собрать) приводит к вызову деструктора, а метод W a i t F o r P e n d i n g F i n a l i z e r s приостанавливает выполнение текущего потока до завершения работы деструктора13. start // начало с'tor middle // середина d'tor end // конец
Типовая безопасность Программы, написанные на C++, не обладают свойством типовой безопасности. Программы же на управляемом C++ должны гарантированно обладать указанным свойством. Однако, из-за того, что программы C++ могут содержать неуправляемый код, они не обязательно обладают свойством типовой безопасности. Нельзя производить арифметические операции с управляемыми указателями. Кроме того, нельзя приводить тип управляемого указателя к неуправляемому. Поэтому можно доказать безопасность только тех программ 14 на C++, которые содержат лишь управляемые код и данные . Тем не менее, любая программа на C++, в которой выполняются арифметические действия над неуправляемыми указателями или приводятся типы управляемых указателей к неуправляемым, является потенциально опасной.
1
' Комментарии справа добавлены для удобства ориентирования. — Прим. ред.
1
* Комментарии справа добавлены для удобства ориентирования. — Прим. ред.
'
3
Комментарии справа добавлены для удобства ориентирования. — Прим. ред.
14
Управляемый C + + может генерировать код, гарантированно обладающий свойством типовой безопасности, если избегать использования некоторых особенностей языка, таких, как неуправляемые указатели или приведение типов. Для проверки типовой безопасности сборки можно использовать утилиту Peverify.exe
74
Глава 3. Программирование на управляемом C++
Следующая программа является примером небезопасного кода на C++, в котором выполняется приведение указателя pumc на неуправляемый объект к указателю на переменную типа i n t . В этом случае подобная операция не является опасной, но в общем случае ее выполнение может представлять опасность. Затем выполняется арифметическое действие над указателем на объект, которое уже в этом примере небезопасно, так как получающийся в результате указатель не указывает на какой-либо объект. Еще ниже в этом примере, в закомментированных строках, те же действия совершаются над управляемым указателем рте. Если бы строки были не закомментированы, компилятор выдал бы сообщение об ошибке. // Unmanaged.cpp # using class UnmanagedClass // класс UnmanagedClass { public: int
i;
gc class ManagedClass // класс сборщика мусора ManagedClass { public: int i; void main(void) { UnmanagedClass *pumc = new UnmanagedClass; pumc->i = 10; int * pi = (int *)pumc; // Опасно: приведение указателя pi = (int *)(pumc+1); // Опасность: арифметика над указателями ManagedClass *pmc = new ManagedClass; pmc->i = 10; //pi = (int *)pmc; // Ошибка: приведение _gc //(сборщик мусора) * к * //pi = (int *)(pmc+1); // Ошибка: арифметика над // (сборщик мусора) *
gc
Типы значений Ключевое слово __value (значение) похоже на nogc (без сборки мусора), поскольку оно используется для того, чтобы класс или структура не участвовали в сборке мусора. Это полезно для определения объектов в стеке, а не в управляемой динамически распределяемой области памяти. Основная цель использования таких типов — возможность создания объектов, не требующих затрат на сборку мусора. Использование ключевого слова value (значение) имеет побочный эффект — класс автоматически становится конечным (ключевое слово s e a l e d ) и не может быть абстрактным (ключевое слово abstract к нему неприменимо). Программирование на C++ для платформы .NET
75
value struct ValueStruct int i; Может показаться удивительным, что правила позволяют определять тип ___value (значение) там, где не позволяется определять тип дс (сборщик мусора). В следующем фрагменте кода показан пример этого (вместе с несколькими другими конструкциями). Заметьте, что объекты классов Мапa g e d C l a s s , NonManagedClass и V a l u e C l a s s можно создавать в динамически распределяемой области памяти, тогда как в стек можно поместить объекты только классов NonManagedClass и V a l u e C l a s s . Последний оператор во фрагменте закомментирован, так как иначе компилятор выдал бы сообщение о недопустимости объявления управляемого объекта как переменной, помещаемой в стек. //ValueType.срр #using using namespace System; // использовать пространство имен Система; nogc class NonManagedClass
value class ValueClass // класс значения ValueClass
gc class ManagedClass // класс сборщика мусора ManagedClass { NonManagedClass nmc; // Странно! Но для компилятора это не ошибка! ValueClass vc; // Это не ошибка, здесь допускается тип значения void main(void) { NonManagedClass *pnmc = new NonManagedClass; //Нет ошибки ValueClass *pvc = nogc new ValueClass; //Нет ошибки ManagedClass *prac = new ManagedClass; //Нет ошибки NonManagedClass; //Нет ошибки в стеке ValueClass vc; //Нет ошибки в стеке //ManagedClass гас; // ошибка, не может быть размещен в стеке
Абстрактные типы Значение ключевого слова a b s t r a c t (абстрактный) очень похоже на значение ключевого слова a b s t r a c t (абстрактный) в языке Java. Оно также напоминает о сло76
Глава 3. Программирование на управляемом C++
жившейся традиции рассматривать класс C++, содержащий хотя бы одну чистую (риге) виртуальную функцию, как абстрактный. Ключевое слово abstract (абстрактный) делает это объявление явным. Как и в случае ключевого слова _ _ i n t e r f а с е (интерфейс), ключевое слово a b s t r a c t (абстрактный) используется для обозначения того, что класс определяет некоторые общие обязательные соглашения между кодом, реализующим методы этого абстрактного класса, и кодом клиентов, вызывающих эти методы. Обратите внимание, что, если абстрактный класс определяется как управляемый, в его описании следует использовать также и ключевое слово дс (сборщик мусора). Абстрактный класс подобен интерфейсу в том, что он является лишь средством проявления полиморфизма, а создать экземпляр такого класса непосредственно нельзя. Однако, в отличие от интерфейса, абстрактный класс может содержать реализации нескольких, или даже всех своих методов. Абстрактный класс может быть использован как базовый для других классов, экземпляры которых можно инициализировать, причем переменная абстрактного ссылочного типа (т.е. ссылка или указатель, но не тип значения) может использоваться для обращения к экземплярам классов, производных от абстрактного класса. -/ Обратите внимание на то, что использование ключевого слова abstract SvVfc^-< * (абстрактный) вместе с i n t e r f a c e (интерфейс) (это слово не является ЙК6Д"'£;1! расширением управляемости) является избыточным, так как интерфейсы по определению являются абстрактными. Ключевое слово abstract (абстрактный) нельзя использовать в комбинации с ключевыми словами value (значение) или s e a l e d (конечный). Ключевое слово value (значение) указывает на то, что объект содержит непосредственно данные, а не ссылки на объекты в динамически распределяемой области памяти. Это значит, что можно создавать экземпляры такого класса, а следовательно, он не может быть абстрактным. Ключевое слово s e a l e d (конечный) означает, что класс не может быть базовым для других классов, что, очевидно, противоречит концепции абстрактного класса. В следующем фрагменте приведен пример типичного использования ключевого слова ___abstract (абстрактный). //AbstractExample.cpp Jfusing using namespace System; // использовать пространство имен Система; _ _ a b s t r a c t c l a s s AbstractClass // абстрактный класс AbstractClass {
public: virtual void Methodl() = 0; // виртуальный; не реализован здесь virtual void Method2() // виртуальный; реализован здесь { Console::WriteLine("Method2");
c l a s s DerivedClass
:
public AbstractClass
Программирование на C++ для платформы .NET
77
public: void Methodl() // реализован здесь { Console::WriteLine("Methodl");
void main(void) { //AbstractClass *pac = new AbstractClass; // ошибка AbstractClass *pac = new DerivedClass; // указатель pac->Methodl(); pac->Method2(); AbstractClass &ac = *new DerivedClass; // ссылка ас.Methodl(); ac.Method2(); } Программа напечатает: Methodl Method2 Methodl Method2
Интерфейсы Ключевое слово ___interface (интерфейс) технически не относится к расширению управляемости, так как его можно использовать и в управляемом, и в неуправляемом коде. Однако оно часто используется при создании управляемого кода, поэтому стоит остановиться для его рассмотрения. Интерфейсы используются как обобщенные базовые типы для классов, при реализации которых применяются некоторые общие соглашения (контракты). Эти контракты используются для согласования реализации основной программы и программы-клиента посредством определения общего полиморфного набора методов. Интерфейс можно считать крайней формой абстрактного класса, поскольку цели их существования сходны, но интерфейсы — наименее конкретная его разновидность. Так сложилось, что программисты, работающие с C++, используют термин "интерфейс" для обозначения любого класса, содержащего лишь чистые виртуальные методы. Поэтому новое ключевое слово i n t e r f a c e (интерфейс) лишь делает эту договоренность явной. Класс, определенный с использованием ключевого слова _ i n t e r f асе (интерфейс), может содержать лишь общедоступные (public) чистые виртуальные методы. В частности, ни один из методов класса не должен быть реализован, класс не может содержать статические или нестатические элементы данных, конструкторы, деструкторы, статические методы, и не может перегружать операторы. Интерфейс может быть потомком любого количества базовых интерфейсов, но не потомком какого бы то ни было абстрактного или неабстрактного класса. Обратите внимание, что, хотя интерфейс не может содержать элементы данных, он может содержать свойства (доступ к которым осуществляется методами получения/установки (get/set)). О свойствах будет рассказано ниже. Как и в случае абстрактных классов, создать экземпляр интерфейса нельзя, так что они используются как полиморфные базовые классы.
78
Глава 3. Программирование на управляемом C++
В описании интерфейса можно использовать только спецификатор общего доступа (public); однако его использование не обязательно, поскольку в качестве спецификатора доступа по умолчанию принимается именно public (общедоступный). Исходя из того, что задача интерфейса— определять базовый контракт для производных классов, несложно сделать вывод, что описывать интерфейс с ключевым словом s e a l e d (конечный) бессмысленно. / К управляемым интерфейсам (т.е. определенным с ключевым словом дс . j. ,'(сборщик мусора)) предъявляются некоторые дополнительные требования. и EKnnV.i-- Они не могут быть производными от неуправляемых интерфейсов. Однако fcV ЛЛ • • о н и морур быть непосредственными потомками произвольного количества управляемых интерфейсов. Следующий фрагмент представляет пример типичного использования ключевого слова i n t e r f a c e (интерфейс): //InterfaceExample.срр #using using namespace System; // использовать пространство имен Система; ^ i n t e r f a c e Somelnterface // интерфейс public: virtual void Methodl() = 0 ; // чистый виртуальный явный voic Method2(); // чистый виртуальный подразумеваемый class DerivedClass : public Somelnterface 1 public: void Methodl() // реализован здесь { Console::WriteLine("Methodl"); } void Method2() // реализован здесь { Console::WriteLine("Method2");
void main(void) { //Somelnterface *psi = new Somelnterface; // ошибка Somelnterface *psi = new DerivedClass; // указатель psi->Methodl(); psi->Method2(); Somelnterface &si = *new DerivedClass; // ссылка si.Methodl(); si.Method2();
Программирование на C++ для платформы .NET
79
Программа напечатает: Methodl Method2 Methodl Method2
Упаковка и распаковка примитивных типов данных Упаковка и распаковка — важная концепция программирования в .NET вне зависимости от того, какой именно язык программирования вы используете. Одно из самых важных преимуществ .NET — унифицированная система типов. Каждый тип, в том числе простые упакованные встроенные типы, такие как _ b o x ( i n t ) , является потомком класса S y s t e m . O b j e c t (Система.Объект). В языках, подобных Smalltalk, все типы являются объектами, но это приводит к неэффективности использования простых типов. В стандартном C++ простые встроенные типы данных и объекты обрабатываются поразному, — это повышает эффективность использования типов, но исключает возможность унификации системы типов. Управляемый C++ объединяет преимущества обоих подходов, используя прием, называемый упаковкой (boxing). Упаковка — преобразование типов значений, таких, как i n t или double (с удвоенной точностью), в ссылку на объект, хранимый в динамически распределяемой области памяти. Упаковка производится с помощью ключевого слова box. Распаковка — преобразование упакованного типа (хранимого в динамически распределяемой области памяти) в неупакованное значение (хранимое в стеке). Распаковка выполняется приведением типов. Проиллюстрируем упаковку и распаковку следующим фрагментом кода: i n t х = 5; // простой встроенный тип i n t box int *po = box(x); // упаковка х = *ро; // распаковывание Ключевое слово box создает в управляемой динамически распределяемой области памяти управляемый объект, инкапсулирующий копию выражения, имеющего тип значения. Под выражением, имеющим тип значения, подразумевается примитивный тип данных, такой как i n t , f l o a t (с плавающей точкой), double (с удвоенной точностью), или c h a r (символ), либо тип значения, определенный как класс или структура и описанный с использованием ключевого слова _ v a l u e (значение). Например, предопределенный управляемый тип boxed_System__Int32 инкапсулирует упакованный i n t , a управляемый тип boxed_ValueStruct — упакованный тип значения V a l u e S t r u c t . Эти странные названия типов (__boxed_System_Int32 и b o x e d _ V a l u e S t r u c t ) не обязательно будут встречаться в вашем исходном коде, но они показываются утилитой I l d a s m . e x e . Обратите внимание, что box i n t * — альтернативное имя управляемого типа __boxed__System_Int32, a box V a l u e S t r u c t * — альтернативное имя управляемого типа boxed_ValueStruct. Если ключевое слово box используется для создания управляемого объекта, сборщик мусора .NET будет автоматически освобождать память, используемую данным объектом. Это похоже на концепцию использования для примитивных типов интерфейсных классов, однако упаковка имеет более важное значение в среде .NET, чем в программировании на обычном C++. Это происходит из-за того, что объекты в C++ можно использовать и как значения, и как ссылочные типы, тогда как в среде .NET управляемые объекты всегда являются ссылочными типами {т.е. ссылками или указателями на объекты, хранимые в управляемой динамически распределяемой области памяти). 80
Глава 3. Программирование на управляемом
Доступ к типам значений осуществляется так же, как и доступ к неупакованным типам. В приведенном ниже коде это делается в присваивании *pIntBox = 50. Несмотря на то, что pIntBox указывает на управляемый объект, разыменованный указатель используется так, как будто он является просто указателем на неупакованный тип i n t . //BoxExample.срр #using using namespace System; // использовать пространство имен Система; value struct ValueStruct { public: int i; // функция ожидает получить управляемый указатель на объект void ExpectManagedObjectPointer( box ValueStruct* pManagedObject) { pManagedObject->i = 20; // изменяет упакованную копию Console::WriteLine(pManagedObject->i) ;
// функция ожидает получить управляемый указатель на объект void ExpectBoxedPrimitivePointer(__box int* pIntBox) { *pIntBox = 50; //изменяет упакованную копию примитивного типа Console::WriteLine(*p!ntBox); void main(void) { ValueStruct valueStruct; // объект типа значение в стеке valueStruct.i = 10; // изменяет оригинал распакованной копии Console::WriteLine(valueStruct. i) ; box ValueStruct* pManagedObject = box(valueStruct); // boxed_ValueStruct ExpectManagedObjectPointer(pManagedObject) ; pManagedObject->i = 30; // изменяет упакованную копию Console::WriteLine(pManagedObject->i); int j; // тип значения - примитивный тип данных j = 40; // изменяет первоначальный распакованный // примитивный тип Console::WriteLine(j) ; box int *pIntBox = box(j); // ynaKOBaHHbin_System_Int32 ExpectBoxedPrimitivePointer(pIntBox);
Программирование на C++ для платформы .NET
81
Приведенная программа напечатает: 10 20 30 40 50
Делегаты Ключевое слово d e l e g a t e (делегат) используется для объявления класса-делегата, основанного на описании сигнатуры метода. Делегаты очень сходны с указателями на функции в обычном C++, с той лишь разницей, что делегат может указывать только на метод управляемого класса. Чаше всего в приложениях .NET Framework делегаты используются для реализации функций обратного вызова или обработки событий. Однако они могут найти применение во всех случаях, когда необходимо вызывать методы динамически. В .NET Framework определены (как абстрактные классы) два типа делегатов -— System: : D e l e g a t e (Система::Делегат) и System: : M u l t i c a s t D e l e g a t e . Эти два типа делегатов используются как базовые классы для одноадресных (или делегатов единственного приведения •— single-cast) и многоадресных (или групповых — multicast) делегатов соответственно. Одноадресный делегат связывает указатель на метод с методом одного управляемого объекта, тогда как многоадресный делегат связывает указатель на метод с одним или несколькими методами управляемого объекта. Вызов одноадресного делегата приводит к вызову только одного метода, а при вызове многоадресного делегата может выполняться неограниченное количество методов. В связи с тем, что многоадресный делегат можно использовать и для вызова одного метода, одноадресная форма делегата является излишней. Обычно в программах используются лишь многоадресные делегаты. / Встретив в программе ключевое слово d e l e g a t e (делегат) компилятор •• .• . •«. ^ * создает особый управляемый класс, производный от Sysf-КЬд tern: : M u l t i c a s t D e l e g a t e . Конструктор этого класса имеет два аргумента: указатель на экземпляр управляемого класса (который равен нулю, если делегат связывает статический метод), и сам метод, вызываемый с помощью делегата. Этот класс также содержит метод Invoke (Запустить), сигнатура которого совпадает с сигнатурой метода, вызываемого делегатом. Следующий пример демонстрирует использование делегатов: //DelegateExample.срр #using using namespace System; // использовать пространство имен Система; // определить управляемые классы для использования // в качестве делегатов ___delegate i n t SomeDelegate // делегат (int i , i n t j ) ; delegate // делегат void SomeOtherDelegate{int i ) ; gc class SomeClass 82
Глава З. Программирование на управляемом C++
// класс сборщика мусора SomeClass содержит методы, // вызываемые делегатами public: int SomeMethod(int i, int j) Console::WriteLine( "SomeMethod({0} , {1})", return i+j; s t a t i c i n t SoraeStaticMethod(int i , Console::WriteLine( "SomeStaticMethodUO), r e t u r n i-f j ;
box(i), i n t j)
{1})",
box(j)); / / статический
box(i),
box(j));
void SomeOtherMethod(int i) Console::WriteLine( "SomeOtherMethod({0})",
box(i));
void main () 1 SomeDelegate *pscd; int sum; // сумма // связать делегат с нестатическим методом // требуется экземпляр SomeClass SomeClass * psc = new SomeClass(}; pscd = // создать экземпляр класса делегат sc new SomeDelegate( psc, &SomeClass::SomeMethod); // нестатический sum = pscd->Invoke(3, 4); // вызвать метод через делегат // сумма = pscd->Bbi3BaTb (3, 4); Console::WriteLine(sum); // сумма // связать делегат со статическим методом, - нет нужды // ни в каком экземпляре pscd = // создать другой экземпляр класса делегата sc new SomeDelegate( О, sSomeClass::SomeStaticMethod); // статический sum = pscd->Invoke(3, 4); // вызвать метод через делегата // сумма = pscd->Bbi3BaTb (3, 4); Console::WriteLine(sum); // сумма // объединить два делегата SomeClass * pscl = new SomeClass(); SomeClass * psc2 = new SomeClass(); SomeOtherDelegate *pmcdl = new SomeOtherDelegate( pscl, &SomeClass::SomeOtherMethod); SomeOtherDelegate *pmcd2 = new SomeOtherDelegate( psc2, &SomeClass::SomeOtherMethod); SomeOtherDelegate *pmcd = Программирование на C++ для платформы .NET
83
static_cast(Delegate::Combine( // Объединение делегатов pmcdl, pmcd2)); pmcd->Invoke(1); // Вызвать
Приведенная программа напечатает: SomeMethod(3, 4) 7 SomeStaticMethod(3, 4) 7 SomeOtherMethod(1) SomeOtherMethod(1)
События События представляют собой механизм, посредством которого объект имеет возможность получать информацию о происходящем вне него. Событие может быть вызвано неким действием пользователя, например, нажатием кнопки мыши, или некими изменениями состояния приложений, например, приостановкой или завершением задачи. Объект, генерирующий событие, называется источником или отправителем события; объект, который реагирует на событие, называется приемником или получателем события. В обычном C++ для работы с событиями реализуют функции обратного вызова, для выполнения которых используются указатели на функции. В модели компонентных объектов Microsoft (COM) для работы с событиями используются интерфейсы iConnect i o n P o i n t и i C o n n e c t i o n P o i n t C o n t a i n e r . В .NET используются управляемые события. Все эти подходы по сути одинаковы, так что для их объединения Microsoft предложила Унифицированную модель событий (Unified Event Model). Для поддержки этой новой Унифицированной модели событий в C++ введены новые ключевые слова e v e n t (событие), hook (привязать) и ^ u n h o o k (отцепить), а также атрибуты e v e n t ^ s o u r c e (источник события) и e v e n t _ r e c e i v e r (приемник события). Ключевое слово e v e n t (событие) используется для описания события, которое может быть сгенерировано источником события. Это слово можно использовать не только в управляемых классах; оно может применяться к следующим объявлениям: 1. Описание метода класса обычного C++ (обычный обратный вызов). 2. Описание интерфейса модели компонентных объектов Microsoft (COM) (точка стыковки). 3. Описание метода управляемого класса (управляемое событие). 4. Описание элемента данных управляемого класса (управляемое событие с использованием делегата). Мы рассмотрим только третий случай, т.е. случай, в котором источником собы• " тия является метод управляемого класса. Для того чтобы объявить обработчи. ..".,. ком какого-то события метод класса-получателя этого события, используется ключевое слово hook (привязать). После того, как это сделано, при каждом возникновении события будет вызываться его обработчик. А чтобы такое объявление метода аннулировать, используется ключевое слово unhook (отцепить). В следующем примере демонстрируется использование ключевых /
84
Глава 3. Программирование на управляемом C++
слов e v e n t (событие), _ h o o k (привязать) и unhook (отцепить), а также атрибутов e v e n t _ s o u r c e (источник события) и e v e n t _ r e c e i v e r (приемник события) для реализации механизма обратного вызова: //Event.срр #using HookEvent(pEventSource) ; Программирование на C++ для платформы .NET
85
pEventSource->Fire_ManagedEvent(); // вызывается обработчик pReceiver->UnhookEvent(pEventSource); Программа напечатает: HandleManagedEvent c a l l e d
Свойства Ключевое слово p r o p e r t y (свойство) используется для указания на то, что метод получения и/или установки реализует свойство управляемого класса. В отличие от элемента данных (называемого также полем), который однообразен и негибок, свойство может быть доступно для чтения и записи, либо только для чтения и только для записи, может быть реализовано как обычная переменная или как вычисляемое значение. Например, свойство только для чтения должно быть реализовано методом get_, но не методом set_. Свойство доступно другим программам, написанным на любых языках .NET, посредством использования обычного синтаксиса доступа к элементам данных этого языка, как если бы свойство было обычным элементом данных (точнее псевдоэлементом данных). Это продемонстрировано в следующем фрагменте кода, в котором pmcwp->someProperty используется так, как если бы оно было элементом данных с именем someProperty. Фактически такого элемента данных не существует, но то, что класс содержит методы get_someProperty и set_someProperty, объявленные с ключевым словом p r o p e r t y (свойство), делает возможным такой способ доступа. В действительности класс содержит защищенный (protected) элемент данных m_someProperty, но это уже подробности реализации, инкапсулированные в компоненте. Свойство компонента в .NET похоже на свойства OLE-автоматизации в компонентах ActiveX или свойства компонентов (bean) в JavaBean. Другой интересной чертой управляемых свойств, отсутствующей у элементов данных, является то, что вы можете проверить приемлемость аргумента метода s e t _ и, в случае, если значение аргумента некорректно, вызвать исключение. Это помогает в создании корректно работающих компонентов. //PropertyExample.срр iusing using namespace System; // использовать пространство имен Система; gc c l a s s ManagedClassWithProperty // класс сборщика мусора ManagedClassWithProperty public: ManagedClassWithProperty() : m_someProperty(0) {} property int get__someProperty () // свойство // должно быть "get_" return m_someProperty; ___property void set_someProperty( // свойство int propertyValue) // должно быть "set_" m_someProperty = propertyValue; 86
Глава З. Программирование на управляемом C++
} protected: // защищенный int m_someProperty; // можно реализовать как элемент данных }; void main() { ManagedClassWithProperty* pmcwp = new ManagedClassWithProperty; pmcwp->someProperty = 7; // псевдоэлемент данных Console::WriteLine(pmcwp->someProperty); } Вышеприведенная программа выведет: 7
Закрепление управляемых объектов Ключевое слово p i n (закрепить) указывает на то, что указатель на управляемый объект будет оставаться корректным (т.е. общеязыковая среда выполнения GLR не переместит объект в памяти) на протяжении существования закрепленного указателя. Закрепленный объект остается на своем месте в памяти до тех пор, пока на него указывает закрепленный указатель. Если изменить указатель так, что он будет указывать на другой объект или присвоить ему нулевое значение, объект может быть перемещен сборщиком мусора. Когда при определении указателя не задано ключевое слово p i n (закрепить), общеязыковая среда выполнения CLR может в любой момент переместить объект, на который указывает этот указатель. Перемещение объектов происходит вследствие сборки мусора и уплотнения динамически распределяемой области памяти, выполняемых общеязыковой средой выполнения CLR. Эти перемещения не сказываются на управляемом коде, так как общеязыковая среда выполнения CLR автоматически изменяет значения управляемых указателей при перемещении объектов, но могут повлиять на выполнение неуправляемого кода, в котором используются неуправляемые указатели на управляемые объекты. Ключевое слово __pin (закрепить) следует применять только в тех случаях, когда это крайне необходимо, так как закрепление объектов расстраивает сборку мусора 15 и снижает ее эффективность. Для примера необходимости закрепления можно упомянуть ситуацию, в которой вы передаете неуправляемой функции в качестве аргумента указатель на управляемый объект (или указатель на элемент данных такого объекта). В данном случае проблема состоит в том, что в процессе выполнения программы управляемый объект может быть перемещен сборщиком мусора, но неуправляемая функция будет при этом использовать старый, некорректный указатель. Это приведет к тому, что неуправляемая функция обратится по некорректному адресу и последствия этого могут быть катастрофическими. Ниже приведен фрагмент, иллюстрирующий использование ключевого слова p i n (закрепить) в описанной ситуации. Обратите внимание, что объект pPinnedObject закреплен в памяти, так что передача указателя на него методам S e t G l o b a l P o i n t e r V a l u e и G e t G l o b a l P o i n t e r V a l u e в качестве аргумента является допустимой. Реализация этих методов основана на том, что глобальный указатель дх остается корректным, а это может быть верным
В результате фрагментации памяти. — Прим. ред.
Программирование на C++ для платформы .NET
87
только в случае, когда обшеязыковая среда выполнения CLR не будет перемещать объект класса ManagedClass. Заметим, что компилятор способен предсказать возникновение такой ситуации и выдаст сообщение об ошибке, если из приведенного примера удалить ключевое слово j p i n (закрепить). //PinExample.срр #using using namespace System; // использовать пространство имен Система; gc class ManagedClass // класс сборщика мусора ManagedClass { public: int x; tpragma unmanaged // неуправляемый int *gx; // глобальный указатель void SetGlobalPointer(int* pi) { // установить глобальный указатель, // чтобы указать на управляемый объект gx
=
pi;
void SetGlobalPointerValue(int i) { // установить управляемый объектный элемент данных // через глобальный указатель *gx = i; int GetGlobalPointerValue() { // получить управляемый объектный элемент данных // через глобальный указатель return *gx; #pragma managed // управляемый void main() { ManagedClass ____pin * pPinnedObject = new ManagedClass; // обратите внимание на ошибку, генерируемую компилятором / / в следующей инструкции... // если ключевое слово pin удалить из предыдущей инструкции SetGlobalPointer(&pPinnedObject->x); // неуправляемый SetGlobalPointerValue(1); // неуправляемый int x = GetGlobalPointerValue(>///неуправляемый
88
Глава 3. Программирование на управляемом C++
Конечные классы Ключевое слово s e a l e d (конечный) указывает на то, что класс или структуру нельзя использовать в качестве базового типа. Другими словами, в иерархии наследования этот класс или структура— терминальный тип. Ключевое слово s e a l e d (конечный) можно также применять к отдельному методу класса. Конечный метод не может быть переопределен в производных классах. В стандарте C++ подобная возможность не предусмотрена; однако в Java такая возможность реализована с помощью ключевого слова f i n a l (конечный). Следующий фрагмент кода является некорректным, так как конечный класс не может быть базовым: s e a l e c c l a s s SomeSealedClass { };
class SomeDerivedClass : public SomeSealedClass // ошибка
Одной из причин использования ключевого слова __sealed (конечный) является повышение стабильности работы классов за счет препятствования слишком самоуверенным и/или недостаточно квалифицированным программистам испортить важные и сложные элементы поведения классов в производных от них. Другой аргумент использования s e a l e d (конечный) — предотвращение попыток изменить возможности, обеспечивающие безопасность. Например, предопределенный класс s t r i n g (Строка) объявлен как конечный, а вдобавок к тому — еще и как неизменяемый (он не содержит общедоступных методов или элементов данных, что могло бы позволить изменять его содержимое). Это делает его идеальным для использования в защитных целях, например, для хранения паролей. При попытке скомпилировать следующий код будет выдана ошибка, так как класс S t r i n g (Строка) является конечным: // не дспустимо, потому что Система::Строка - конечный класс c l a s s MyString : p u b l i c S t r i n g // класс MyString: общедоступная Строка
Управляемое приведение типов / Ключевое слово _ _ t r y _ c a s t приводит к возникновению исключения Sys• : •• ' * ^ . - ' ' tem: : I n v a l i d C a s t E x c e p t i o n при попытке выполнить приведение типов, £ не поддерживаемое общеязыковой средой выполнения CLR. Это похоже на возникновение исключения b a d c a s t при выполнении оператора dynamic_cast в C++ и на исключение ClassCastException, возникающее при некорректном приведении типов в Java. Хотя по своему действию оператор try__cast больше похож на оператор d y n a m i c c a s t , чем на оператор static__cast, t r y _ c a s t в действительности задуман как временная замена оператора s t a t i c ^ c a s t , применяемая на стадии разработки приложений. После анализа всех возникающих при выполнении t r y _ c a s t исключений и внесения соответствующих исправлений в программу, операторы t r y _ c a s t обычно заменяются операторами s t a t i c _ c a s t . В следующем примере продемонстрировано использование операторов t r y c a s t для выявления некорректных приведений типов. Программирование на C++ для платформы .NET
89
//TryCastExample.cpp #using using namespace System; // использовать пространство имен Система; gc class Mammal // класс сборщика мусора Млекопитающее
gc class Dog : public Mammal // класс сборщика мусора Собака: общедоступное Млекопитающее
gc struct Cat : public Mammal // сборщик мусора; Кот: общедоступное Млекопитающее
void main() { Mammal *pMammal = new Dog; // Млекопитающее *pMammal = новая Собака; try // пробовать { Dog *pDog = try_cast (pMammal); // хорошо // Собака *pDog = try_cast (pMammal); Console::WriteLine(" try_cast ") ; // Собака // хорошо Cat *pCat = __try_cast (pMammal); // плохо! // Кот *pCat = __ try_cast (pMammal); Console::WriteLine(" try_cast "}; // Кот // пропустить } catch(InvalidCastException *pe) { Console::WriteLine("Ooops: {0}", pe->get_Message()); Приведенная программа напечатает: try_cast Ooops: Exception of type System.InvalidCastException was thrown.
Определение ключевых слов в качестве идентификаторов
:
КОД'
90
Ключевое слово i d e n t i f i e r (идентификатор) позволяет использовать любое слово, включая и ключевое, в качестве идентификатора. Его можно использовать и для слов, не являющихся ключевыми, но это не дает никаких преимуществ, и потому является бессмысленным. На первый взгляд кажется нелепым, что такая черта может вообще понадобиться; однако, из-за того, что Глава 3. Программирование на управляемом C++
платформа .NET допускает использование в разработке приложений одновременно нескольких языков, может оказаться, что имя класса или переменной, определенное в части программы, написанной на другом языке, совпадет с каким-либо ключевым словом C++. Очевидно, что использование в качестве имен ключевых слов значительно усложнит чтение и понимание исходного кода, так что к этому приему следует прибегать только в крайнем случае. Выглядящий несколько странно код, приведенный ниже, демонстрирует этот прием. В нем описывается класс, называющийся if, элемент данных которого называется while (эксцентричное сочетание). Затем создается экземпляр класса if и вызывается метод while. (О, меня уже тошнит!!!) Удивительно, но это компилируется и работает! //IdentifierExaraple.срр #using using namespace System; // использовать пространство имен Система; gc class identifier(if) // класс сборщика мусора идентификатор (если) { public: int identifier(while); // int идентификатор (while); void ma^n(void) { _ _ i d e n t i f i e r ( i f ) * p i f = new // идентификатор (если)
identifier(if);
// ж pif = новый идентификатор (если); pif-> identifier(while)= 1; // pif-> идентификатор (while) = 1; Console::WriteLine(pif-> identifier(while)); // !pif-> _ _ идентификатор (while));
Обработка исключений Без сомнения, вы уже хорошо знакомы с механизмом исключений в стандартном C++, так что хорошо понимаете, как работают управляемые исключения. Напомним, что платформа .NET (точнее, общеязыковая среда выполнения CLR) поддерживает расширения, совместимые с расширением управляемости C++, и управляемые исключения, возникшие при выполнении кода, созданного на одном из языков .NET, могут быть перехвачены и обработаны кодом, написанным на любом другом языке .NET. s'
.• 7 t Кроме обработки предопределенных исключений, таких, как I n v a l i d "•[ C a s t E x c e p t i o n или OverflowException, вы можете определить ваши Л собственные производные от Exception (Исключение) классы, инкапсулирующие некоторую специфичную для приложения информацию. Рассмотрим следующий пример:
Обработка исключений
91
//Exceptions.cpp fusing using namespace System; // использовать пространство имен Система; gc class MyException : public Exception // класс сборщика мусора MyException: общедоступное Исключение
void TemperamentalFunction(int i) // ненавидит нечетные числа { Console::WriteLine( "TemperamentalFunction called with {0}", i.ToStringO ) ; if (i%2 != 0) // если (i%2 != 0], т.е. нечетное throw new MyException; Console::WriteLine("No exception thrown"); // Нет исключения void main() { try { TemperamentalFunction(2); // вызов с четным числом TemperamentalFunction(3); // вызов с нечетным числом } catch (MyException *ре) { Console::WriteLine("Exception thrown!"); // Исключение! Console::WriteLine(pe->get_StackTrace()); Приведем результат работы программы: TemperamentalFunction called with 2 No exception thrown TemperamentalFunction called with 3 Exception thrown! at TemperamentalFunction(Int32 i) in c:\netcppcode\ chapO3\exceptions\exceptions.cpp:line 16 at main() in c:\netcppcode\chap03\exceptions\exceptions cpp:line 2 5 Вот более русифицированная версия этой выдачи1**: TemperamentalFunction вызвана с 2 Нет исключения TemperamentalFunction вызвана с 3 Исключение! в TemperamentalFunction (Int32 i) в с:\netcppcode\ Добавлена редактором русского перевода. — Прим. ред. 92
Глава 3. Программирование на управляемом C++
c h a p 0 3 \ e x c e p t i o n s \ e x c e p t i o n s . c p p : l i n e 16 в главном () в с : \ n e t c p p c o d e \ c h a p 0 3 \ e x c e p t i o n s \ e x c e p t i o n s . c p p : l i n e 25 Обратите внимание на метод S t a c k T r a c e , позволяющий получить текстовую строку, представляющую состояние стека в момент возникновения исключения. Хотя в этом примере ключевое слово f i n a l l y (наконец) и не используется, но следует помнить, что такое расширение стандарта ANSI C++ поддерживается в Visual C++. Ключевое слово f i n a l l y (наконец) позволяет вставлять в программу код, который выполняется вне зависимости от того, возникло или нет исключение в блоке t r y . Следует также упомянуть, что ключевое слово f i n a l l y (наконец) полностью совместимо с механизмом исключений, поддерживаемым другими языками .NET. При желании предыдущий пример можно разбить на две части. Первая часть могла быть реализована на С# (в виде динамически подключаемой библиотеки (DLL)) и содержала бы код, при выполнении которого возникало бы исключение. Вторая часть была бы приложением на C++, вызывающим метод TemperaraentalFunction. Этим способом можно было бы наглядно продемонстрировать, что исключения действительно являются мостом, соединяющим разные языки .NET.
Атрибуты C++ Visual C++.NET поддерживает использование атрибутов, позволяющих создавать обычный неуправляемый код, такой, как компоненты модели компонентных объектов Microsoft (COM), даже с меньшими усилиями, чем раньше. Кроме того, Visual C++.NET поддерживает новые возможности .NET, такие, как Унифицированная модель событий (Unified Event Model). Изначально атрибуты, относящиеся к модели компонентных объектов Microsoft (СОМ), использовались в отдельном файле IDL (Interface Definition Language — язык описания интерфейсов) для описания информации о типе компонентов модели компонентных объектов Microsoft (COM). Теперь же эти атрибуты можно использовать непосредственно в исходном коде C++, поэтому необходимость в отдельном файле IDL отпадает. Компилятор использует эти атрибуты для генерации исполняемого кода и информации о типе. Одно из преимуществ применения атрибутов C++ для программирования с использованием модели компонентных объектов Microsoft (COM) состоит в том, что вам придется возиться только с исходными файлами C++, но не с файлами IDL или RGS (Registry Script — Сценарий системного реестра). Это делает проекты с компонентами на основе модели компонентных объектов Microsoft (COM) более простыми и понятными. Использование атрибутов значительно изменяет язык C++ и расширяет возможности модульности программ. Источники атрибутов, в качестве которых могут выступать независимые динамически подключаемые библиотеки (DLL), реализуют механизм динамического расширения возможностей компилятора C++. Атрибуты предназначены для повышения производительности труда программистов, особенно при разработке компонентов на основе модели компонентных объектов Microsoft (COM). Генерируемый при использовании атрибутов код автоматически вставляется в конечный файл. Атрибуты используются для создания кода таких элементов: •
интерфейсы и компоненты на основе модели компонентных объектов Microsoft (СОМ);
Атрибуты C++
93
• события модели компонентных объектов Microsoft (COM) (точки стыковки); • события унифицированной модели событий (управляемые события); • код ATL Server; •
код пользователя OLE для баз данных;
• код со счетчиками производительности; •
входные точки модулей.
Хотя данная глава называется "Программирование на управляемом C++", а атрибуты используются при создании управляемого кода, в этом разделе мы рассмотрим лишь использование атрибутов для создания неуправляемого кода на основе модели компонентных объектов Microsoft (COM). Информацию об использовании атрибутов при работе с управляемыми событиями можно найти в разделе этой главы "События". Информацию об использовании атрибутов для других целей можно найти в документации по комплексу инструментальных средств разработки программ .NET (.NET SDK). Атрибуты, определяемые разработчиком, рассмотрены в главе 8 "Классы каркаса .NET Framework". Продемонстрируем необходимость использования атрибутов в C++ на примере описания функции AddEmUp, приведенного в следующей строке кода. Заметим, что в рамках ANSI C++ эта функция не может быть описана полностью. Так, невозможно указать, какие из аргументов являются входными, а какие — выходными. Обычно эта дополнительная информация, важная при создании кода на основе модели компонентных объектов Microsoft (COM), использующего маршалинг, описывается с помощью атрибутов языка описания интерфейсов (IDL) в отдельном файле IDL. При этом атрибуты языка описания интерфейсов (IDL) заключаются в квадратные скобки и могут использоваться для описания многих черт компонентов на основе модели компонентных объектов Microsoft (СОМ), в том числе интерфейсов, соклассов и библиотек типов. // нет важной информации маршалинга HRESULT AddEmUp(int i, i n t j , i n t * psum); Приведенную функцию C++ можно описать более подробно в файле IDL, как это показано ниже. Здесь используются атрибуты in (входной), out (выходной) и r e t v a l . HRESULT AddEmUp( [ i n ] i n t i , [ i n ] i n t j , [ o u t , r e t v a l ] i n t *psum); Для синтаксического разбора файла IDL, создания библиотек типов и исходных файлов, использующих маршалинг (для заместителя (proxy) и заглушки (stub)), для методов интерфейса модели компонентных объектов Microsoft (COM) (поддерживаются также интерфейсы удаленного вызова процедур (RPQ) используется компилятор языка описания интерфейсов IDL, разработанный фирмой Microsoft, — Midi. exe. Добавлять атрибуты в исходный код можно вручную. Однако полезно будет увидеть, как атрибуты вставляются в проект, генерируемый Мастером приложений библиотеки шаблонных классов (ATL) на основе модели компонентных объектов Microsoft (COM) (ATL COM Application Wizard), который является высокопроизводительным инструментальным средством, предназначенным для создания компонентов на основе модели компонентных объектов Microsoft (COM). На рис. 3.4 показано, как разрешить использование атрибутов в Мастере приложений библиотеки шаблонных классов (ATL) на основе модели компонентных объектов Microsoft (COM) (ATL COM Application Wizard).
94
Глава 3. Программирование на управляемом C++
Ш
ATL Project Wizard - MyflTLProject
Application Settings Specfyi the appcilato i n type and feature support for the proe j ct. y,,... ReservationId = res~>ReservationId; // результат result->ReservationCost = // результат units[unitid]->cost * numDays; // стоимость result->Rate = units[unitid]->cost; // результат = стоимость; result->Comment = "OK"; // результат-> Комментарий = "хорошо"; return result; } Метод Reserve (Резерв) разработан так, чтобы с его помощью можно было зарезервировать разные типы резервируемых объектов. Таким образом, объект Reservation (Резервирование), который хранится в списке резервируемых объектов, создается в классе, производном от Broker (Брокер), и передается в метод Reserve (Резерв) в качестве параметра. Например, объект HotelBroker резервирует объект HotelReservation и т.д. Потом создается объект ReservationResuit, который и будет возвращен (поля Unitid, Date (Дата) и Number Days наследуются из базового класса Reservation (Резервирование)). ReservationResuit *Reserve(Reservation *res) // Резервирование {
int unitid = res->Unit!d; DateTime dt = res->Date; // Дата int numDays = res->NumberDays; ReservationResuit ^result = new ReservationResuit;
Затем мы проверяем, попадают ли даты резервирования в определенный период времени, который поддерживается системой (чтобы облегчить задачу, возьмем период в один год). Для этой цели используем структуру DateTime из пространства имен System (Система). Если какая-то дата не входит в указанный период, будет выдано сообщение об ошибке. 110
Глава 4. Объектно-ориентированное программирование...
// Проверить, лежат ли даты з пределах поддерживаемого диапазона int day = dt.DayOfYear .- 1; if (day + numDays > MaxDay} // если (день + numDays> MaxDay) { result->ReservationId = - 1 ; // результат result->Corranent = "Dates out of range"; // результат-> Комментарий = "Даты вне диапазона"; return r e s u l t ; // результат
Потом нужно для каждого из запрашиваемых дней проверить, доступен ли этот ресурс, то есть, не будет ли число резервирований превышать объем ресурса (гостиницы). Мы сделаем это с помощью массива numCust. Первый индекс этого двумерного массива определяет конкретную дату, а второй — идентификационный код резервируемого объекта (Обратите внимание на то, что поля и методы названы именами, которые описывают их суть, например, HotelBroker). // Проверить, доступны ли комнаты для всех дат for ( i n t i = day; i < day + numDays; i++) {
if (numCust[i, unitid] >~ units[unitid]->capacity)
// вместимость { result->ReservationId = -1; // результат result->Com.Tient = "Room not available"; // результат-> Комментарий = "Комната не доступна"; return result; // результат
Далее нужно зарезервировать номер на указанный период времени, увеличив количество клиентов в nuraCust на единицу в каждый из дней, на которые клиент делает запрос. // Резервировать комнату для требуемых дат for ( i n t i = day; i < day + numDays; i++) numCust[i, u n i t i d ] += 1; Наконец, мы добавляем резервирование к списку резервирований и возвращаем результат. // Добавить резервирование к списку резервирований // и возвратить результат AddP.eservation (res) ; r e s u l t - > R e s e r v a t i o n ! d = r e s - > R e s e r v a t i o n I d ; // результат r e s u l t - > R e s e r v a t i o n C o s t = // результат u n i t s [ u n i t i d ] - > c o s t * numDays; // стоимость result->Rate = u n i t s [ u n i t i d j - > c o s t ; // результат = стоимость; result->Coronent = "OK"; // результат-> Комментарий = "хорошо"; return
result;
Проект: "Бюро путешествий Acme"
111
Список резервирований и резервируемых объектов Класс Broker. (Брокер) !акже содержит список резервирований и резервируемых обьектов. Для нашей реализации н виде массивов мы реализуем только методы добавления (Acid), а в последующих версиях примера рассмотрим, как удалять элементы из списка. vsj.i d A d d R e s e r v a t i o n ( R o s e r v e r ! e n "* r e.-j) // Резервирование j j
!
r e s e r v a t i o n s [nextRose .- v. ::ior: •*-+• •- r e s ; //' резервирование j
void
AddUnir. ( P c s e r v a b i e urii'cs [
n
i
]
'ljn.t} i
Проектирование инкапсуляции В данной реализации класса Зге к и г (Брокер) все списки представлены в виде массивов. Поскольку такая реализация может не быть (и на самом деле не будет) сохранена в последующих версиях, мы не станем рассматривать функции работы с массивами или обращаться к элементам с помощью индексов. Мы реализуем общедоступные свойства Number U n i t s и Xurri^erRe s e : VPT :/-ns. чтобы обеспечить доступ для просмотра частных переменных nextUn.it и nextK'-'se с v-ir ; •-•п. jpror-erty i n " get_NumLe: "Jn •. '..s ' ) i
return nextUnit;
}
property ir:t ge'__ Nuifiiier Reservations () r G t иrn re x tReзe r v a t i о11 ; } Представление простых no^eii Рези: va t i o i J d . Un: t Id, Date (Дата) и Number Days класса R e s e r v a t i o n (Резервирование) не меняется, следовательно, мы не будем инкапсулировать эти поля. Потом, сети потребуется, мы можем реализовать некоторые из этих полей как свойства, не изменяя при этом код клиента. Однако в данный момент, и в дал[>нешпем, мы просто будем использовать общедоступные поля. p u b l i c _ ос _ab/-1 r,iC-\- •'•]i.-:s Resc ^\т-,г.\ с и /1 ибор!Ц>1К .'лу^ора - аО'':1] р'\!-/"ниР, к пасе: Р е з е р в и р о в а н и е j
public: i n t Rc.se>-vatir-r.j d; U:t Unit Id; DateTine 3ate; // Датг ir, "z. Nu:rJ:ierDays; s t a t i c p r i v a t e i r t nextPeservaMcnld = 1; Reservar.i.on () // Fei-ерЬ'Лрование
112
Глава Д. Объектно-ориентированное программирование...
Reservationld = nextReservationId++;
Наследование в управляемом с++ В управляемом C++ поддерживается модель единичного наследования2. Следовательно, класс может быть порожден не больше чем из одного базового класса. Такая модель проста и дает возможность избежать усложнений и неопределенностей, которые возникают при множественном наследовании3 в неуправляемом C++. Хотя класс в управляемом C++ может быть производным только от одного базового класса, однако он может быть наследником нескольких интерфейсов, — это мы обсудим в следующей главе. В данном разделе мы рассмотрим наследование в связи с примером резервирования гостиничного номера.
Основные принципы наследования Если вы используете механизм наследования, учитывайте все абстракции вашей объектной модели и те из них, которые можно будет повторно использовать, поместите в базовые классы как можно более высокого уровня. Добавлять и изменять свойства можно в более специализированных производных классах, которые наследуют стандартное поведение базовых. Благодаря наследованию облегчается повторное использование кода и его расширяемость. Кроме того, производный класс может содержать более подходящий для членов базового класса интерфейс. Давайте рассмотрим базовый класс Reservable (Резервируемый объект, ресурс) и производный от него класс Hotel (Гостиница). У всех резервируемых объектов есть некоторые обшие свойства, например, идентификатор объекта (ID), объем ( c a p a c i t y ) и стоимость (cost). У каждого отдельного вида резервируемых объектов есть свои уникальные свойства. Например, в классе гостиницы есть атрибуты C i t y (Город) и H o t e l Name (Название гостиницы). Синтаксис наследования в управляемом C++ Наследование в управляемом C++ реализуется таким образом: в операторе class производного класса через двоеточие указывается имя базового класса. В файле HotelBroker.h в папке CaseStudy показано, как из базового класса Reservable (Резервируемый объект, ресурс) порождается класс Hotel (Гостиница). public gc c l a s s Hotel : p u b l i c Reservable // класс сборщика мусора - Гостиница:Reservable
* Единичное наследование — форма наследования функций и свойств классами, при которой производный класс может иметь единственный базовый класс. — Прим. ред. Множественное наследование — форма наследования функций и свойств классами, при которой производный класс может иметь любое число базовых классов. — Прим. ред.
Наследование в управляемом C++
113
Класс Hotel (Гостиница) автоматически наследует все поля класса Reservable (Резервируемый объект, ресурс) и содержит свои собственные поля C i t y (Город) и НоtelName (Название гостиницы). Внесение изменений в интерфейс существующих членов класса Члены базового класса, u n i t id, c a p a c i t y (объем имеющихся ресурсов), c o s t (стоимость), предназначены для внутреннего использования и не должны быть общедоступными. В классе H o t e l (Гостиница) находятся общедоступные члены H o t e l l d , NumberRooms и Rate (Цена), к которым пользователи имеют доступ "только для чтения". Когда мы реализуем свойство'таким образом, можно вместо абстрактного имени, например c a p a c i t y (объем имеющихся ресурсов), которое используется в базовом классе, выбрать имя, более конкретное, например, NumberRooms. Вызов конструкторов базового класса Если в производном классе есть конструктор с параметрами, возможно, вы захотите передать некоторые из этих параметров и конструктор базового класса. В C++ можно вызвать конструктор базового класса, поставив перед ним двоеточие. В управляемом C++, в отличие от обычного неуправляемого C++, можно использовать только один базовый класс и список параметров, так как здесь поддерживается только единичное наследование. Hotel( // Гостиница String * c i t y . S t r i n g *name, i n t number, // число Decimal cost) // Десятичная стоимость : Reservable(number, cost) // число, стоимость { City = c i t y ; // Город = город; HotelName = name; // название }
Обратите внимание на то, что в управляемом C++ можно вызвать только конструктор базового класса, из которого непосредственно был порожден данный класс. Нельзя вызвать конструктор, стоящий в иерархии наследования выше.
Реализация примера "Бюро путешествий Acme" С помощью абстрактных классов R e s e r v a b l e (Резервируемый объект, ресурс), Rese r v a t i o n (Резервирование) и Broker (Брокер) можно легко реализовать систему резервирования конкретного ресурса, например гостиничного номера. На рис. 4.2 показана иерархия наследования: класс Hotel (Гостиница) является производным от класса R e s e r v a b l e (Резервируемый объект, ресурс), класс H o t e l R e s e r v a t i o n — производным от класса R e s e r v a t i o n (Резервирование), класс H o t e l B r o k e r — производным от класса Broker (Брокер). В этом разделе мы рассмотрим основные моменты реализации примера, которая находится в папке CaseStudy для этой главы.
114
Глава 4. Объектно-ориентированное программирование...
Reservable
Reservation (Резервирование)
Broker (Брокер)
Hotel (Гостиница).
Hotel Reservation
Hotel Broker
Рис. 4.2. Иерархия классов для системы резервирования ''Бюро путешествий Acme"
Запуск программы примера Перед тем, как продолжить просмотр кода, неплохо было бы запустить пример. Программа T e s t B r o k e r . exe представляет собой консольное приложение. Если после приглашения на ввод команды вы наберете "help" в командной строке, то будет выведен следующий список команд: E n t e r command, q u i t t o e x i t H> h e l p The following commands are available: hotels shows all hotels in a city all shows all hotels cities shows all cities add adds a hotel book book a reservation bookings show all bookings register register a customer email change email address show show customers quit exit the program H> Вот перевод этой выдачи4: Введите команду, q u i t для выхода Н> помощь Доступны следующие команды: hotels (гостиницы) показывает все гостиницы в городе all (все) показывает все гостиницы cities (города) показывает все города add (добавить) добавляет гостиницу book (заказать) заказывает резервирование bookings (заказы) показывает все заказы r e g i s t e r (регистрировать) регистрирует клиента email (электронная почта) изменяет адрес электронной почты show (показать) показывает клиентов quit выход из программы Н> Поэкспериментируйте с этой программой, пока полностью не изучите ее свойства. Добавлен редактором русского перевода. — Прим. ред.
Реализация примера "Бюро путешествий Acme"
115
Класс HotelReservation Класс HotelReservation— это простой класс, который является производным класса Reservation (Резервирование). Его код находится в файле h o t e l b r o k e r . h . Этот класс включает в себя некоторые дополнительные общедоступные поля и свойство ArrivalDate (Дата прибытия), которое несет больше конкретного смысла, чем поле Date (Дата) базового класса. public gc c l a s s HotelReservation : p u b l i c Reservation // класс сборщика мусора - HotelReservation: общедоступное Резервирование {
public: int Customerld; String *HotelName; String *City; DateTime DepartureDate; property DateTime get_ArrivalDate{) {
return Date; // Дата } property void set_ArrivalDate(DateTime value) // значение { Date = value; // Дата = значение;
Класс HotelBroker Самая важная задача в примере — реализовать класс H o t e l B r o k e r , который является производным от класса Broker (Брокер). Код этого класса находится в файле h o t e l broker .h. public __gc class HotelBroker : public Broker // класс сборщика мусора - HotelBroker: общедоступный Брокер {
private: // частный // статические константы; static const int MAXDAY = 366; static const int MAXUN1T = 10; static const int MAXCITY = 5; static private int nextCity = 0; // статическая частная String *cities[]; public: HotelBroker() : Broker(MAXDAY, MAXUNIT) // Брокер { cities = new String*[MAXCITY]; // города AddHotel("Atlanta", "Dixie", 100, 115.00); // Атланта, // Дикси AddHotel("Atlanta", "Marriott", 500, 70.00); //Атланта, // Мариот AddHotel("Boston", "Sheraton", 250, 95.00); // Бостон, // Шератон 116
Глава 4. Объектно-ориентированное программирование...
Для описания массивов вводятся константы, и создается массив, содержащий названия городов. Конструктор определяет массивы с помощью конструктора базового класса, инициализирует массив c i t i e s (города) и добавляет несколько гостиниц для тестирования. Потом определяется свойство NumberCity и метод добавления гостиницы в список гостиниц. __property i n t get_NumberCity(> {
return nextCity; } String "AddHotel( String *city, String ^name, int number, // число Decimal cost) // Десятичная стоимость {
if (Findld(city, name) != -1) // если (Findld (город, название)! =-1) return "Hotel is already on the list"; // "Гостиница уже находится в списке"; Hotel * hotel = // Гостиница new Hotel(city, name, number, cost); // новая Гостиница (город, название, число, стоимость); AddUnit(hotel); // гостиница AddCity(city); // город return "OK";
Частные вспомогательные функции помогают найти идентификатор гостиницы и добавить город в список. Город можно добавить только тогда, когда его в списке еще нет: список не может содержать два одинаковых города. i n t F i n j l d ( S t r i n g * c i t y , S t r i n g *name) {
for (int i = 0; i < NumberUnits; i++) { Hotel *hotel = dynamic_cast ( u n i t s [ i ] ) ; // Гостиница if ( ( S t r i n g : :Compare (hotel->City, c i t y ) === 0) // сравнить (гостиница-> Город, город) && (String::Compare( // сравнить (гостиница-> HotelName, название) hotel->HotelName, name) == 0)) r e t u r n hotel->Hotel!d; // гостиница return -1;
Реализация примера "Бюро путешествий Acme"
117
void AddCity(String *city) // проверить, есть ли город уже в списке, добавить, если нет if (!Contains(city)) // если (! Содержит (город)) c i t i e s [nextCity++] - city; // города [nextCity ++] = город; bool Contains (String *city) for (int i = 0; i < NumberCity; i++) if (String::Compare(cities[i], city) == 0) // сравниваются (города [ i ] , город) return true; // истина return false; // ложь I Итак, мы реализовали методы вывода списка всех гостиниц, гостиниц определенного города и списка всех городов. Стоит взглянуть на код, чтобы понять, как реализуется простое форматирование текста. Наконец, мы подошли к описанию ключевого в классе H o t e l B r o k e r метода Res e r v e (Резерв), с помощью которого можно зарезервировать номер. ReservationResult ^Reserve( int customerId, String *city, String *name, DateTime dt, int numDays) int id = Findld(city, name); // int идентификатор = Findld (город, название(имя)); if (id == -1) // если (идентификатор ==-1) ReservationResult *result = new ReservationResult; result->ReservationId = -1; // результат result->Comment = "Hotel not found"; // результат-> Комментарий = "Гостиница не найдена"; return result; // результат HotelReservation *res = new HotelReservation; res->UnitId = id; // идентификатор res->CustomerId = customerld; res->HotelName = name; // название res->City = city; // Город = город res-->ArrivalDate = dt ; res->DepartureDate = d..Add(TimeSpan(numDays, 0, 0, 0)); // Добавить на период res-MvumberDays = numDays; return Broker::Reserve(res);
18
Глава 4. Объектно-ориентированное программирование...
Реализовать класс H o t e l B r o k e r оказалось несложно, потому что в его основе лежит логика, реализованная в классе Broker (Брокер). Если гостиницы нет в списке гостиниц, то возвращается сообщение об ошибке. После этого создается объект ноt e l R e s e r v a t i o n , который передается в качестве параметра методу Reserve (Резерв) базового класса. В производном классе мы создаем объект резервирования, так как нам нужны все поля класса H o t e l R e s e r v a t i o n , а не только поля, унаследованные от класса R e s e r v a t i o n (Резервирование). Для того чтобы подсчитать дату отбытия (прибавить количество дней, на которые зарезервирован номер, к дате прибытия), мы используем структуру TimeSpan вместо ранее использовавшейся для этих целей структуры DateTime. Такое вычисление сделать нетрудно, поскольку в структуре DateTime знак операции + перегружен.
Класс Customers (Клиенты) Нельзя реализовать систему резервирования, не смоделировав клиентов, которые ее используют. Класс Customers (Клиенты), который находится в файле customers, h, поддерживает список объектов типа Customer (Клиент). Этот список также представлен в виде массива. Реализация указанного класса очень похожа на реализацию гостиничных классов, поэтому она будет приведена в общих чертах, а точнее, мы приведем лишь структуры данных и объявления общедоступных методов и свойств. //Customer.h using namespace System; // использовать пространство имен Система; namespace 01 { namespace NetCpp { namespace Acme { // пространство имен 01 {пространство имен NetCpp // {пространство имен Acme { public gc class Customer // класс сборщика мусора Клиент { public: int Customerld; String *FirstName; String *LastName; String *EmailAddress; private: // частный static int nextCustld = 1; // статический public: Customer(String * first, String *last, String *email) // Клиент { Customerld = nextCust!d++; FirstName = first; LastName = last; EmailAddress = email; // электронная почта
p u b l i c __gc c l a s s Customers // класс сборщика мусора Клиенты Реализация примера "Бюро путешествий Acme"
119
p r i v a t e : // частный Customer *customers [ ] ; // Клиент s t a t i c i n t nextCust = 0; // статический public: Customers(int MaxCust) // Клиенты {
}
customers = new Customer*[MaxCust]; // клиенты RegisterCustomer( "Rocket", // "Ракета" "Squirrel", "
[email protected]"); // "Белка" RegisterCustomer( "Bullwinkle", "Moose", "
[email protected]"); // "Американский лось" property int get_NumberCustomers()
int RegisterCustomer( String *firstName, String *lastName, String *emailAddress) void Add(Customer *cust) // Добавить (Клиент) void ShowCustomers(int customerld) void ChangeEmailAddress( int id, String *emailAddress) // идентификатор
Пространство имен Код примера полностью находится в пространстве имен 0 1 : : N e t C p p : :Acme. Все файлы с описанием классов начинаются с директивы n a m e s p a c e (пространство имен). В файле T e s t H o t e l . h помещена соответствующая директива u s i n g . Определяется пространство имен 0 1 : : N e t C p p : : Acme следующим образом:
namespace 01 { namespace NetCpp { namespace Acme { // пространство имен 01 {пространство имен NetCpp // {пространство имен Acme {
Класс TestHotel Класс T e s t H o t e l , который находится в файле T e s t H o t e l . h , содержит интерактивную программу для испытания классов, связанных с резервированием гостиницы, и классов клиентов, поддерживающих описанные ранее команды. В этом классе имеется цикл, просматривающий команды, — такой иикл считывает команду и затем выполняет ее. Класс содержит большой блок t r y для всех команд, за которым следует обработчик исключений c a t c h . Обратите внимание, — чтобы получить доступ к пространству имен нужно использовать директиву u s i n g .
120
Глава 4. Объектно-ориентированное программирование...
//TestHotel.h using namespace System; // использовать пространство имен Система; using namespace 01::NetCpp::Acme; // использовать пространство имен 01::NetCpp::Acme; public gc class TestHotel // класс сборщика мусора TestHotel public: static void Main(j const int MAXCUST = 10; // константа HotelBroker *hotelBroker = new HotelBroker; Customers ^customers = new Customers(MAXCUST); // новые Клиенты InputWrapper *iw = new InputWrapper; String *cmd; Console::WriteLine("Enter command, quit to exit"); // ("Введите команду, quit для выхода"); cmd = iw->getString("H> " ) ; while (! cmd->Equals("quit")) try // попытка if (cmd->Equals("hotels")) // если Равняется // ("гостиницы") String *city = iw->getString("city: " ) ; // город notelBroker->ShowHotels(city); // город else if (cmd->Equals("all")) // если Равняется // ("все") hotelBroker->ShowHotels();. else hotelhelpO ; catch (Exception *e) // Исключение Console::WriteLine( "Exception: {0}", e->Message); // "Исключение: {0} ", e-> Сообщение); cmd = iw->getString("H> " ) ; } }
Реализация примера "Бюро путешествий Acme"
121
Резюме В этой главе сделан обзор принципов объектно-ориентированного программирования на управляемом C++, причем много внимания было уделено изучению наследования. Мы обратились к примеру "Бюро путешествий Acme", который продолжим использовать на протяжении всей книги. Мы также рассмотрели абстракции, наиболее подходящие для того, чтобы реализовать системы резервирования разных объектов, и реализовали систему резервирования гостиничных номеров. Описанные нами абстрактные базовые классы можно использовать и для реализации других систем резервирования.
122
Глава 4. Объектно-ориентированное программирование...
Управляемый C++ в .NET Framework
зык C++ — мощный инструмент разработки программ, оказавший огромное влияние на развитие вычислительной науки. Управляемые (managed) расширения от Microsoft добавили в язык C++ целый новый мир— мир .NET. Для того чтобы полностью использовать возможности Visual C++ .NET, необходимо понимать, как он работает с .NET Framework. Мы начнем рассмотрение с базового класса Object (Объект) из пространства имен System. Затем рассмотрим коллекции, а также методы класса Object (Объект), которые следует перегрузить для использования возможностей, предоставляемых .NET Framework. Далее познакомимся с интерфейсами, позволяющими строго определить свойства реализуемых классов. В управляемом C++ класс может реализовывать несколько интерфейсов, даже при том, что он может быть потомком только одного суперкласса. Интерфейсы позволяют применять динамическое программирование; во время выполнения программы можно послать классу запрос для того, чтобы узнать, поддерживает ли он определенный интерфейс. Будут подробно рассмотрены интерфейсы, поддерживающие использование коллекций. Потом остановимся на видах копирования. Вместо применения конструкторов копирования, как это делается в обычном C++, в управляемом C++ для реализации копирования используется интерфейс I C l o n e a b l e . Мы рассмотрим родовые интерфейсы в методологии программирования .NET Framework и сравним использование компонентов .NET и СОМ. Более полно использование родовых интерфейсов иллюстрируется на примерах различных сортировок с помощью интерфейса IComparable. Этот пример позволяет также почувствовать удобство работы с каркасом приложений, определяющим архитектуру программ, а не являющимся просто библиотекой классов, в которой имеются некие полезные функции. При использовании каркаса приложений программа может вызывать методы каркаса, а те могут вызывать методы программы. Поэтому создаваемый код можно уподобить сандвичу. Этот пример помогает понять, для чего необходима платформа .NET. Функции обратного вызова применяются в программировании уже много лет. Управляемый C++ использует эту концепцию в работе с делегатами и событиями. Здесь представлены два простых и понятных примера: моделирование фондовой биржи и диалоговая комната для дискуссий (чат-программа).
Объект системы: System: : O b j e c t Как мы уже Знаем, каждый управляемый (managed) тип (объявленный с префиксом ___дс (сборшик мусора)) в управляемом C++ в конце концов является потомком корневого класса System: :Object (Система::Объект). Даже такие традиционные примитивные типы, как System: : I n t 3 2 , System: : SByte и System: : Double являются потомками System: :ValueType, а тот, в свою очередь— потомок System: :Object (Система::Объект). Кроме упомянутых, в .NET Framework имеется свыше 2500 классов, и все они — потомки Object (Объект)1.
Общедоступные методы экземпляров класса o b j e c t (Объект) Есть четыре обшедоступных метода экземпляров класса Object (Объект), три из которых являются виртуальными и часто подменяются в управляемых классах. Метод Equals (Равняется) p u b l i c : v i r t u a l bool E q u a l s ( O b j e c t * ) ; // виртуальный логический (булев) Равняется (Объект *) ; Этот метод сравнивает объект, указатель на который передается методу в качестве аргумента, с текущим объектом. В случае равенства объектов метод возвращает t r u e (истина). В классе Object (Объект) данный метод сравнивает только указатели на объекты. В классе ValueType этот метод подменен и производит сравнение содержимого объектов. Многие классы, в свою очередь, подменяют этот метод для того, чтобы приспособить условия сравнения объектов к своим нуждам. Существует также и статическая версия метода Equals (Равняется), сравнивающая два объекта, указатели на которые передаются ему в качестве аргументов. Метод T o S t r i n g public:
v i r t u a l String* ToString();
/ / виртуальный
Этот метод возвращает строку, содержащую удобочитаемое для человека описание объекта. Принятая по умолчанию версия, реализованная в объекте Object (Объект), возвращает просто полное имя типа. Производные классы часто перегружают данный метод, чтобы возвращаемое описание объекта было более содержательным. Метод GetHashCode public:
v i r t u a l i n t GetHashCode();
/ / виртуальный
Этот метод возвращает значение хеш-функции объекта, который может использоваться в алгоритмах хэширования и хэш-таблицах. Классы, подменяющие данный метод, должны также подменять метод E q u a l s (Равняется) для того, чтобы равные объекты возвращали одинаковые значения хеш-функции. Если этого не сделать, класс H a s h t a b l e (Хэш-таблица) не сможет корректно работать с объектами используемого класса.
Так как большинство программ в управляемом C++ создаются с директивой using namespace System (использовать пространство имен System), делаюшсй возможным обращение к System: : Object просто как к Object, мы будем использовать именно укороченное обозначение. 124
Глава 5. Управляемый C++ в .NET Framework
Метод CetType public:
Type* G e t T y p e O ;
Этот метод возвращает информацию о типе объекта. Такая информация может быть использована для получения связанных метаданных посредством отражения (reflection), которое мы рассмотрим в главе 8 "Классы каркаса .NET Framework". Это метод не виртуальный, поэтому подменять его обычно не приходится.
Защищенные методы экземпляров класса o b j e c t (Объект) Защищенными являются два метода класса Object (Объект). Эти методы могут использоваться только производными классами. Метод MemberwiseClone p r o t e c t e d : Object* MemberwiseClone(); // защищенный Данный метод создает поверхностную (shallow) копию объекта. Это метод не виртуальный, поэтому подменять его обычно не приходится. Для того чтобы сделать детальную (deep) копию, следует использовать интерфейс I C l o n e a b l e . Разница между поверхностной и детальной копией будет рассмотрена в этой главе несколько позже. Метод F i n a l i z e (Завершить) -Object!};
Этот метод позволяет освободить используемые объектом неуправляемые ресурсы и выполнить другие операции, необходимые при сборке мусора (утилизации неиспользуемой памяти и других ресурсов). В управляемом C++ метод F i n a l i z e (Завершить) имеет такой же синтаксис, как и деструктор в обычном C++. Но при этом семантика данного метода качественно отличается от семантики деструктора в обычном C++. В обычном C++ деструктор вызывается детерминирован но и синхронно. В управляемом C++ для сборщика мусора создается независимый поток.
Родовые интерфейсы и обычное поведение Если вы знакомы с языком Smalltalk или ему подобными, набор возможностей, реализованных непосредственно в классе Object (Объект), может показаться вам весьма ограниченным. В языке Smalltalk, в котором использована концепция иерархии классов, являющихся потомками одного базового класса, набор методов, реализованных в Obj e c t (Объект), весьма широк. Я насчитал 38 методов!2 Эти методы осуществляют различные действия, такие, как сравнение и копирование объектов. Библиотека классов .NET Framework содержит и подобные методы, и еще множество других. Но вместо того, чтобы вводить их в базовый класс, .NET определяет набор стандартных интерфейсов, которые при желании может реализовывать класс. Такая организация, используемая также в технологии COM (Component Object Model — модель компонентных объектов Microsoft) от Microsoft и в языке Java, очень гибка. В этой главе мы рассмотрим некоторые родовые интерфейсы .NET Framework.
2
Описание методов класса O b j e c t (Объект) в Smalltalk можно найти в главах 6 и 14 книги Smaihalk-80: The Language and Us Implementation (Sma]|talk-80: Язык и его реализация), написанной Адель Голдберг (Adele Goldberg) и Дэвидом Робсоном (David Robson).
Объект системы: System-Object
125
Использованиеметодовклассаobject(Объект)вклассе Customer (Клиент) Для иллюстрации описанных методов рассмотрим класс Customer (Клиент) до и после перегрузки методов Equals (Равняется), T o S t r i n g и GetHashCode. Стандартные методы класса o b j e c t (Объект) Если не произвести подмены виртуальных методов объекта Object' (Объект), наш класс будет наследовать стандартное поведение. Это поведение продемонстрировано в C u s t o m e r O b j e c t \ S t e p l . //Customer.срр #using using namespace System; // использование пространства имен Система; #include "Customer.h" //Customer.h gc class Customer // сборщик мусора - класс Клиент { public: int nCustomerld; String *pFirstName; String *pLastName; String *pEmailAddress; Customer(int id, String *pFirst, // Клиент (int идентификатор, // Строка *pFirst, String *pLast, String *eMail) { nCustomerld = id; // идентификатор pFirstName = pFirst; pLastName = pLast; pEmailAddress = eMail; // электронная почта
//TestCustomer.срр #using using namespace System; // использование пространства имен Система #include "Customer.h" #include "TestCustomer.h" void main(void) { TestCustomer::Main(); // Главный } //TestCustomer.h __gc class TestCustomer // класс сборщика мусора TestCustomer 126
Глава 5. Управляемый C++ в .NET Framework
public: static void Main() // статическая Главная { Customer *pCustl, *pCust2; // Клиент pCustl = new Customer( // новый Клиент 99, "John", "Doe", " john(a rocky. com") // "Джон", "Доу pCust2 = new Customer! // новый Клиент 99, "John", "Doe", "
[email protected]") // "Джен", "Доу ShowCustomerObject("pCustl", pCustl); ShowCustomerObject("pCust2", pCust2); CompareCustomerObjects(pCustl, pCust2]; } private: // частный static void ShowCustomerObject( String *pLabel, Customer *pCust) { Console::WriteLine(" {0} ", pLabel) Console::WriteLine( "ToStringO = {0}", pCust->ToString()); Console::WriteLine( "GetHashCodeO = {0}", __box(pCust->GetHashCode())); Console::WriteLine( "GetType() = {0}", pCust->GetType()); } static void CorapareCustomerObjects( Customer *pCustl, Customer *pCust2) // Клиенты I Console::WriteLine( "Equals() = {0}", // Равняется box(pCustl->Equals( // Равняется // сборщик dynamic_cast(pCust2) // мусора
3
Запустите испытательную программу, и вы получите следующий результат3: // custl custl // ToString () = Клиент ToString() = Customer // GetHashCode () = 4 GetHashCodeO = 4 // GetType () = Клиент GetType() = Customer // cust2 cust2 // ToString (} = Клиент ToStringO = Customer / / GetHashCode 0 = 6 GetHashCodeO = 6 GetType0 = Customer // GetType () = Клиент // Равняется () = Ложь Equals() = False Понятно, что реализация, принятая по умолчанию, — это совсем не то, что желательно иметь для объекта Customer (Клиент). Метод T o S t r i n g возвращает имя класса, а не информацию о конкретном клиенте. Метод Equals (Равняется) сравнивает только указатели на объекты, а не сами объекты. В приведенном примере два разных указателя указывают на одинаковые (по содержащейся информации) объекты класса Customer (Клиент), но метод Equals (Равняется) при этом возвращает значение f a l s e (ложь). 3 Правая колонка (в виде комментариев) добавлена редактором русского перевода. — Прим. ред. Объект системы: System "Object
127
подмена методов класса o b j e c t (Объект) Версия проекта, содержащаяся в папке CustomerObject\Step2, демонстрирует подмену виртуальных методов Object (Объект). //Customer.h gc c l a s s Customer // сборщик мусора - класс Клиент {
public:
int nCustomerld; String *pFirstName; String *pLastName; String * pEmai lActdre s s ; Customer(int id, String *pFirst, // Клиент (int идентификатор, // Строка *pFirst, String *pLast, String *eMail) { nCustomerld = id; // идентификатор pFirstName = pFirst; pLastName = pLast; pErnailAddress = eMail; // электронная почта } public: bool Equals(Object *pobj) // логический (булев) Равняется (Объект *pobj) { Customer *pCust = // Клиент dynamic_cast(pobj); return (pCust->nCustomerId == nCustomerld); } •int GetHashCode() { return nCustomerld; } String *ToString() { return String::Format( // Формат "{0} {1}", pFirstName, pLastName}; Остальная часть профаммы не изменена. Теперь результат выполнения программы будет таким4: custl // custl ToStringO = John Doe // ToString () = Джон Доу GetHashCode() = 99 // GetHashCode () = 99 GetTypef) = Customer // GetType () = Клиент cust2 // cust2 ToStringO = John Doe // ToString () = Джон Доу GetHashCode() = 99 // GetHashCode () = 99 GetType() = Customer // GetType () = Клиент Equals() = True // Равняется () = Истина Правая колонка (в виде комментариев) добавлена редактором русского перевода. — Прим. ред.
128
Глава 5. Управляемый C++ в .NET Framework
Коллекции Библиотека классов .NET Framework предлагает широкий выбор классов для работы с коллекциями объектов. Все эти классы находятся в пространстве имен System: : C o l l e c t i o n s (Система::Коллекции) и реализуют ряд различного типа коллекций, в том числе списки, очереди, массивы, стеки и хэш-таблицы. В коллекциях содержатся экземпляры класса Object (Объект). Так как все управляемые типы происходят исключительно от Object (Объект), в коллекции может храниться экземпляр любого встроенного или определяемого пользователем типа. В этом разделе мы рассмотрим типичный представитель данного пространства имен — класс A r r a y L i s t (Список массивов), и научимся на практике использовать списки массивов5. В частности, мы используем их для подходящей реализации нашего класса, экземпляры которого предполагается хранить в коллекции. Мы увидим, что метод Equals (Равняется) нашего класса должен быть подменен, так как реализация любого из классов коллекций требует реализации метода Equals (Равняется).
пример класса A r r a y L i s t (Список массивов) Для начала приведем простой Пример использования класса A r r a y L i s t (Список массивов). Как понятно из названия (Array List — Список массивов), A r r a y L i s t — это список объектов, хранимый подобно массиву. Размер списка массивов может динамически изменяться, и может расти при добавлении новых элементов. Классы коллекций содержат экземпляры класса Ob j e c t (Объект). Мы создадим и будем иметь дело с коллекцией объектов Customer (Клиент). Использовать любой другой встроенный или определяемый пользователем управляемый тип ничуть не сложнее. При использовании простого типа, такого, как i n t , экземпляр данного типа для сохранения в коллекции должен быть упакован (boxed), а перед его использованием — распакован обратно в i n t . Взятая для примера программа называется C u s t o m e r C o l l e c t i o n . В ней инициализируется список клиентов, после чего пользователь может просмотреть данный список, зарегистрировать нового клиента, отменить регистрацию клиента или изменить адрес его электронной почты. Вызов простого метода h e l p (помощь) отображает список доступных команд: E n t e r command, q u i t t o e x i t H> h e l p The following commands are available: register register a customer unregister unregister a customer email change email address show show customers quit exit the program
-* Краткости ради термин array list мы переводим, как и принято, — список массивов. На самом деле точнее было бы говорить о списках, построенных на основе массивов, так как список массивов имеет другое значение. Однако, учитывая это замечание, мы надеемся, что по контексту будет видно значение данного Термина и принятое нами сокращение никаких недоразумений не вызовет. — Прим. ред.
Коллекции
129
Вот перевод6: Введите команду, quit для выхода из программы Н> help Доступны следующие команды: r e g i s t e r (регистрировать) регистрирует клиента u n r e g i s t e r (отменить регистрацию) отменяет регистрацию клиента email (электронная почта) изменяет адрес электронной почты show (показать) показывает клиентов quit " выход из программы До того, как ознакомиться с исходным кодом, было бы неплохо запустить программу, зарегистрировать нового клиента, просмотреть список клиентов, изменить адрес электронной почты клиента, отменить регистрацию клиента и снова просмотреть список клиентов. Приведем пример выдачи программы7: Н> show // показать id (-1 for a l l ) : -1 // идентификатор (-1 для в с е х ) : - 1 1 Rocket Squirrel
[email protected] 2 Bullwinkle Moose '
[email protected] H> r e g i s t e r // регистрировать f i r s t name: Bob // имя: Боб l a s t name: Oberg // фамилия: Оберг email address:
[email protected] // адрес электронной // почты: id = 3 // идентификатор = 3 H> email // электронная почта customer id: 1 // идентификатор клиента email address:
[email protected] // адрес электронной // почты Н> unregister id: 2 // идентификатор: 2 Н> show // показать id (-1 for all): -1 // идентификатор (-1 для всех) 1 Rocket Squirrel
[email protected] 3 Bob Oberg
[email protected] Класс Customer (Клиент) Все файлы с исходными кодами программы-примера находятся в папке CustomerCollection. В файле customer . h находится реализация классов Customer (Клиент) и Customers (Клиенты). Исходный код для класса Customer (Клиент) почти такой же, как приведенный ранее. Единственное добавление — специальный конструктор, инициализирующий объект Customer (Клиент) заданным идентификатором. Этот конструктор используется классом Customers (Клиенты) при удалении элемента (UnregisterCustomer) и при проверке того, присутствует ли в коллекции некоторый элемент (Checkld). gc c l a s s Customer // сборщик мусора - класс Клиент
Добавлен редактором русского перевода. — Прим. ред. ' Никаких комментариев в выдаче, естественно, не булет, они добавлены редактором русского перевода для облегчения чтения выдачи. — Прим. ред.
130
Глава 5. Управляемый C++ в .NET Framework
public: Customer(int id) // Клиент (int-идентификатор) { nCustomerld = id; // идентификатор pFirstName = ""; pLastName = ""; pEmailAddress = "";
Класс Customers (Клиенты) содержит список клиентов, хранимый в ArrayList (Список массивов). gc class Customers // сборщик мусора - класс Клиенты { private: // частный ArrayList *pCustomers; public: Customers() // Клиенты { pCustomers = new ArrayList; RegisterCustomer( "Rocket", // Ракета "Squirrel", // Белка "
[email protected]"); RegisterCustomer( "Bullwinkle", "Moose", // Американский лось "
[email protected]");
У int RegisterCustomer( String *pFirstName, String *pLastName, String *pEmailAddress) { Customer *pCust = new Customer(
// Клиент *pCust = новый Клиент ( pFirstName, pLastName, pEmailAddress); pCustomers->Add(pCust); // Добавить return pCust->nCustoraerId;
} void UnregisterCustomer(int id) // идентификатор { Customer *pCust = new Customer(id); // Клиент *pCust = новый Клиент (идентификатор); pCustomers->Remove(pCust); } void ChangeEmailAddress(int id, String *pEmailAddress) // (int идентификатор, Строка *pEmailAddress) { lEnumerator *pEnum = pCustomers->GetEnumerator(); while (pEnum->MoveNext())
Коллекции
131
Customer *pCust = // Клиент dynamic_cast(pEnum->Current); // Клиент if (pCust->nCustomer!d == id) // если (pCust-> nCustomerld == идентификатор) pCust->pEmailAddress return;
pEmai1Address;r
String *pStr = String::Format( // Формат "id {0} (!}", _box(id), S"not found"); // "идентификатор {0} {1}", (идентификатор), // " не найден"); throw new Exception(pStr); // новое Исключение void ShowCustomers(int id) // идентификатор if ( !CheckId(id) 5& id != -1) // если (! Checkld (идентификатор) && идентификатор! =-1) return; IEnumerator *pEnum = pCustomers->GetEnumerator(); while (pEnum->MoveNext()) Customer *pCust = // Клиент dynamic_cast(pEnum->Current); // Клиент if (id == -l j| id == pCust->nCustomerId) // если (идентификатор ==-1 || // идентификатор == pCust-> nCustomerld) String *pSid = pCust->nCustomerId.ToString()->PadLeft(4); String *pFirst = pCust->pFirstName->PadRight(12); String *pLast = pCust->pLastName->PadRight(12) ; String *pEmail = pCust~>pEmailAddress->PadRight(20); Console::Write("{0} ", pSid); // Пульт:: Записать pFirst); // Пульт: Записать Console::Write("{0} Console::Write("{0} pLast); // Пульт:: Записать Console::WriteLine("{Of", pEmail); // Пульт:: Записать
bool Checkldfint id) // логический(булев) Checkld
int идентификатор)
Customer *pCust = new Customer(id); // Клиент *pCust = новый Клиент (идентификатор); return pCustomers->Contains(pCust); // Содержит ?
132
Глава 5. Управляемый C++ в .NET Framework
Жирным шрифтом в листинге выделены строки, в которых используются особенности класса коллекции. Для перемещения по коллекции используется интерфейс IEnum e r a t o r . Он может быть использован в данном случае, так как A r r a y L i s t (Список массивов) поддерживает интерфейс lEnumerable. Этот интерфейс обеспечивает поддержку особой семантики С# — семантики f oreach. Ключевое слово f o r e a c h не поддерживается в C++. Однако из приведенного примера видно, что в управляемом C++ для перебора элементов A r r a y L i s t (Список массивов) можно применять интерфейс lEnumerable. Интерфейсы, предназначенные для работы с коллекциями, включая Ienuraerable, будут рассмотрены в этой главе позже. Методы Add (Добавить) и Remove (Удалить), как и следует предположить по их названиям, используются для добавления и удаления элементов коллекции, соответственно. Метод Remove (Удалить) просматривает коллекцию в поисках элемента, равного элементу, переданному методу в качестве аргумента, и удаляет найденный элемент. Равенство элементов устанавливается вызовом метода Equals (Равняется). Методу Remove (Удалить) в качестве аргумента передается элемент, создаваемый реализованным нами специальным конструктором, причем для создания элемента используется идентификатор. Поскольку мы подменили метод Equals (Равняется) так, что равенство элементов устанавливается только по атрибуту CustomerlD, этот конструктор имеет единственный аргумент — идентификатор клиента. Метод C o n t a i n s (Содержит), применяемый нами во вспомогательном методе Checkld, использует подмененный метод Equals (Равняется) аналогичным образом. Благодаря использованию коллекций облегчается добавление и удаление элементов. При использовании с той же целью массивов вместо коллекций требуется написать немало кода, необходимого для вставки и перемещения элементов массива, а также для заполнения пустого места, остающегося после удаления элемента. Кроме того, коллекции не имеют определенного размера, и при необходимости могут быть динамически увеличены.
Интерфейсы Концепция^ интерфейсов — одна из основных в современном программировании. Большие системы неизбежно разделяются на части, и существенным становится вопрос о взаимодействии этих частей друг с другом. Правила такого взаимодействия должны быть строго определены и постоянны, так как их изменение может повлиять на несколько частей системы. Однако сама реализация взаимодействия может быть изменена и это не потребует изменения кода других частей системы. В Visual C++ .NET ключевое слово interface (интерфейс) имеет четко определенное значение. Управляемый (managed) интерфейс — ссылочный тип данных, подобный абстрактному классу, задающий поведение с помощью набора методов с определенными сигнатурами8. Интерфейс — это просто контракт^. Когда класс реализует интерфейс, он, таким образом, должен придерживаться контракта. Использование интерфейсов — удобный способ разделения функциональных возможностей. Сначала определяются интерфейсы, а затем разрабатываются классы, реализующие эти интерфейсы. Методы класса могут быть сгруппированы в разных интерфей° Сигнатура — типовая часть спецификации элемента определения класса; включает тип результата для атрибута и функции, для процедур включает также число и типы их аргументов. — Прим. ред. 9 Контракт — набор четко определенных условий, регулирующих отношения между классом-сервером и его клиентами; включает индивидуальные контракты для всех экспортируемых членов класса, представленные пред- и постусловиями, а также глобальные свойства класса, выраженные в инварианте класса. — Прим. ред.
Интерфейсы
133
сах. Хотя в управляемом C++ класс является непосредственным потомком только одного базового класса, он может реализовывать несколько интерфейсов. Использование интерфейсов помогает в создании динамических, гибких и легко изменяемых программ. CLR и BCL (Base Class Library— библиотека базовых классов) обеспечивают удобную возможность во время выполнения программы послать классу запрос для определения, реализует ли он некоторый интерфейс. Интерфейсы в .NET концептуально очень похожи на интерфейсы в модели компонентных объектов Microsoft (СОМ), но работать с ними намного легче. Далее мы подробно изучим преимущества и использование интерфейсов. Затем мы рассмотрим несколько важных родовых интерфейсов библиотеки .NET, что поможет нам понять, каким образом управляемый C++ и .NET могут использовать друг друга для того, чтобы способствовать разработчикам в создании мощных и полезных программ.
Основные сведения об интерфейсах Объектно-ориентированное программирование — мощная методология, помогающая в проектировании и реализации больших программных систем. Использование классов позволяет применять обобщение (абстракцию) и инкапсуляцию. С помощью классов большая система может быть естественным образом разбита на удобные в обращении части. Наследование является еще одним удобным средством структурирования системы, которое позволяет поместить общие черты разных частей в один базовый класс, благодаря чему достигается возможность повторного использования программного кода. Основной задачей интерфейсов является определить контракт, не зависящий от реализации. Каждый интерфейс имеет набор методов, не реализованных непосредственно. Для каждого метода определена сигнатура, описывающая количество и тип аргументов, а также тип возвращаемого методом значения. Интерфейсы в управляемом C++ В Visual C++ .NET для определения интерфейсов используется ключевое слово i n t e r f a c e (интерфейс). Само же определение подобно определению класса. Так же, как и классы, интерфейсы являются ссылочными типами, а для определения их как управляемых используется ключевое слово __дс (сборщик мусора). Наибольшее отличие интерфейсов от классов (как управляемых, так и неуправляемых) — отсутствие конкретной реализации для интерфейсов; они представляют собой чистые спецификации. Тем не менее, подобно классам, интерфейсы могут иметь свойства (property), индексаторы (indexer) и, конечно же, методы. На примере интерфейса ICustomer продемонстрируем определение методов, используемых клиентами системы Бюро путешествий Acme (Acme Travel Agency). gc i n t e r f a c e Icustomer // сборщик мусора - ICustomer {
public:
int RegisterCustomer( String *pFirstName, String *pLastName, String *pEmailAddress); void UnregisterCustomer(int id); // идентификатор ArrayList *GetCustomer(int id); // идентификатор void ChangeEmailAddress(int id, String *pEmailAddress); // (int идентификатор, Строка *pEmailAddress);
134
Глава 5. Управляемый C++ в .NET Framework
Описания методов RegisterCustomer, UnregisterCustomer и ChangeEmailAdd r e s s полностью совпадают с сигнатурами одноименных методов, реализованных нами в классе C u s t o m e r (Клиент). Метод G e t C u s t o m e r является новым. Ранее мы использовали метод S h o w C u s t o m e r , выводящий на экран список клиентов. Этот метод использовался временно. Однако лучше возвращать по запросу сами данные и предоставить пользователю самому решать, что с ними делать. Метод G e t C u s t o m e r возвращает информацию об одном или нескольких клиентах в списке массивов. Когда в качестве идентификатора клиента задается значение -1, возвращается информация обо всех зарегистрированных клиентах. В противном случае возвращаемый список содержит информацию о клиенте с заданным идентификатором. Если среди клиентов нет клиента с таким идентификатором, возвращаемый список будет пустым.
Наследование для интерфейсов Интерфейс может быть потомком другого интерфейса (однако управляемый интерфейс не может быть потомком неуправляемого, и наоборот). В отличие от классов, для которых допустимо лишь единичное наследование, допускается множественное наследование интерфейсов, т.е. интерфейс может иметь несколько непосредственных родителей. Например, интерфейс ICustomer может быть определен как производный от двух более мелких интерфейсов IBasicCustomer и ICustomerlnfo. Заметим, что в описании указанных трех интерфейсов не задается спецификатор открытого доступа. Это потому, что интерфейсы общедоступны по умолчанию. gc i n t e r f a c e IBasicCustomer // сборщик мусора - I B a s i c C u s t o m e r {
int RegisterCustomer( String *pFirstName, String *pLastName, String * p E m a i l A d d r e s s ) ;
void UnregisterCustomer(int id); // идентификатор void ChangeEmailAddress(int id, String *pEmailAddress); // (int идентификатор, Строка *pEmailAddress); gc interface ICustomerlnfo // сборщик мусора - ICustomerlnfo { ArrayList *GetCustomer(int id); // идентификатор }; gc i n t e r f a c e ICustomer : IBasicCustomer, ICustomerlnfo // сборщик мусора - ICustomer: IBasicCustomer, ICustomerlnfo При таком объявлении интерфейса можно также определить новые методы, как это сделано ниже для интерфейса lCustomer2. gc interface ICustomer2 : IBasicCustomer, ICustomerlnfo // сборщик мусора - ICustomer2: IBasicCustomer, ICustomerlnfo { void NewMethod();
Интерфейсы
135
Программирование с использованием интерфейсов Использование интерфейсов облегчает программирование на управляемом C++. Интерфейсы реализуются через классы, и для получения указателя на интерфейс можно выполнить приведение указателя на класс. Методы интерфейсов можно вызывать, используя и указатели на класс, и указатели на интерфейс; однако для того, чтобы полностью воспользоваться достоинствами полиморфизма, предпочтительно везде, где только возможно, использовать указатели на интерфейсы. Реализация интерфейсов В C++ указание того, что класс реализует интерфейс, осуществляется с помощью двоеточия, используемого также для указания наследования класса. Управляемый класс может наследовать от одного управляемого класса и, кроме этого, от одного или нескольких управляемых интерфейсов. В этом случае базовый класс должен указываться в списке первым, сразу после двоеточия. Заметим, что, в отличие от управляемых интерфейсов, наследование управляемых классов может быть только общедоступным. gc c l a s s HotelBroker : public Broker, public IHotellnfo, // класс сборщика мусора - HotelBroker: общедоступный Брокер, public IHotelAdmin, public IHotelReservation
В этом примере класс HotelBroker является производным от класса Broker (Брокер) и реализует интерфейсы IHotellnfo, IHotelAdmin и IHotelReservation. В HotelBroker должны быть реализованы все методы этих интерфейсов, либо непосредственно, либо используя реализацию, унаследованную от базового класса Broker (Брокер). Подробно пример использования интерфейсов будет рассмотрен в этой главе несколько позже, когда мы возьмемся за реализацию второго шага создаваемой системы. А сейчас в качестве небольшого примера вышеизложенного, рассмотрим программу Smalllnterface. Класс Account (Счет) реализует интерфейс IBasicAccount. В описании этого интерфейса демонстрируется синтаксис объявления свойства интерфейса. //Account.h gc _ _ i n t e r f a c e IBasicAccount // сборщик мусора - IBasicAccount {
};
void Deposit(Decimal amount); // Депозит (Десятичное // количество); void Withdraw(Decimal amount); // Снять (Десятичное // количество); property Decimal get_Balance(); // Десятичное число
gc class Account : public IBasicAccount // сборщик мусора - класс Счет: IBasicAccount { private: // частный Decimal balance; // Десятичный баланс public: 156
Глава 5. Управляемый C++ в .NET Framework
Account(Decimal balance) // Счет (Десятичный баланс) {
this->balance = balance; // баланс } void Deposit(Decimal amount) // Депозит (Десятичное количество) { balance = balance + amount; // баланс = баланс + количество } void Withdraw(Decimal amount) // Снять (Десятичное количество) { balance = balance - amount; // баланс = баланс - количество } property Decimal get__Balance() // Десятичное число { return balance; // баланс ) 1; Использование интерфейсов Когда известно, что некоторый класс поддерживает определенный интерфейс, его методы можно вызывать с помощью указателя на экземпляр класса. Если же неизвестно, реализован ли интерфейс классом, можно попытаться выполнить приведение указателя на класс к указателю на интерфейс. И если класс не поддерживает данный интерфейс, при такой попытке возникнет исключение. В следующем примере, взятом из файла Small I n t e r f a c e . h, демонстрируется именно такой способ проверки. t r y // попытка ( IBasicAccount * p i f c 2 = dynamic__cast (pacc2) ; p i f c 2 - > D e p o s i t ( 2 5 ) ; // Депозит Console::WriteLine( " b a l a n c e = {0}", b o x ( p i f c 2 - > B a l a n c e ) ) ; // Баланс } catch (NullReferenceException *pe) { Console::WriteLine( "IBasicAccount is not s u p p o r t e d " ) ;
// IBasicAccount
// не поддерживается Console : -.WriteLine (pe->Message) ; // Сообщение В программе Smalllnterface используются два почти одинаковых класса. Класс Account (Счет) поддерживает интерфейс IBasicAccount, а второй класс, NoAccount его не поддерживает. Оба класса имеют идентичные наборы методов и свойств. Приведем полностью содержимое файлов Smalllnterface.cpp и Smalllnterface . h. Заметим, что в этой программе делаются попытки привести указатели на экземпляры классов Account (Счет) и NoAccount к указателю на интерфейс IBasicAccount. //Smalllnterface.срр #using using namespace System; // использование пространства имен Система; #include "Account.h" Интерфейсы
137
#include "NoAccount.h" ^include "Smalllnterface.h" void main() // главный { Smalllnterface::Main(); // Главный //Smalllnterface.h gc class Smalllnterface // класс сборщика мусора Smalllnterface { public: static void Main() // Главный { Account *pacc = new Account(100); // новый Счет // Использовать ссылку на класс Console::WriteLine( // Баланс "balance = {0} , __box(pacc->Balance) pacc->Deposit(25); // Депозит Console::WriteLine _ (pacc->Balance)) "balance = {Of
/ / Баланс
// Использовать ссылку на интерфейс IBasicAccount *pifc = dynamic_cast(pacc); pifc->Deposit(25); // Депозит Console::WriteLine( "balance = {0}", box(pifc->Balance)) // Баланс // Теперь попробовать с классом, // не реализующим IBasicAccount NoAccount *pacc2 = new NoAccount(500); // Использовать ссылку на класс Console::WriteLine( "balance = (0}", box(pacc2->Balance) // Баланс pacc2~>Deposit(25); // Депозит Console::WriteLine( "balance = { 0} ", _box(pacc2->Balance)); // Баланс // Испробовать указатель на интерфейс try // попытка IBasicAccount *piba= dyhamic_cast(pacc2); piba->Deposit(25); // Депозит Console::WriteLine( "balance = {0}", box(piba->Balance)); // Баланс catch (NullReferenceException *pe) Console::WriteLine( "IBasicAccount is not supported"); // IBasicAccount // не поддерживается Console::WriteLine(pe->Message); // Сообщение
138
Глава 5. Управляемый C++ в .NET Framework
В приведенном примере сначала мы имеем дело с классом Account (Счет), который поддерживает интерфейс iBasicAccount. В этом случае попытки вызвать методы интерфейса как с помощью указателя на класс, так и указателя на интерфейс, полученного в результате приведения, заканчиваются успешно. Далее мы имеем дело с классом NoAccount. Несмотря на то, что набор методов этого класса идентичен набору методов класса Account (Счет), в его описании не указано, что он реализует интерфейс IBasicAccount. //NoAccount.h gc c l a s s NoAccount // класс сборщика мусора NoAccount I При запуске этой программы возникает исключение N u l l R e f e r e n c e E x c e p t i o n . Это происходит при попытке использовать указатель на интерфейс IBasicAccount, полученный в результате динамического приведения указателя на класс NoAccount. (Иными словами, исключение возникает при попытке приведения типа NoAccount * к данным типа указателя на интерфейс IBasicAccount *.) Если бы мы использовали обычное приведение типа в стиле С, то при подобной попытке возникло бы исключение I n v a l i d C a s t E x c e p t i o n . Однако уже при компиляции такой программы было бы выдано предупреждение, что использование приведения типов в стиле С не рекомендуется. balance = 100 balance - 125 balance ~ 150 balance = 500 balance = 525 IBasicAccount is not supported Value null was found where an instance of an object was required. Вот перевод выдачи 10 : баланс = 100 баланс = 125 баланс = 150 баланс = 500 баланс = 525 IBasicAccount не поддерживается Пустой указатель там, где требуется указатель на объект.
Динамическое использование интерфейсов Полезной особенностью интерфейсов является возможность их использования в динамических сценариях, что позволяет по ходу выполнения программы проверять, поддерживается ли интерфейс определенным классом. Если интерфейс поддерживается, мы можем воспользоваться предоставляемыми им возможностями; в противном же случае программа может проигнорировать интерфейс. Фактически такое динамическое поведение реализуется с помощью обработки возникающих исключений, как это уже было продемонстрировано выше. Хотя подобный подход вполне работоспособен, однако он отличается некоторой неуклюжестью и приводит к появлению трудночитаемых программ. C++ поддерживает ис-
Добавлен, естественно, редактором русского перевола. — Прим. ред.
Интерфейсы
139
пользование операторов dynainic_cast и typeid, а в .NET Framework есть класс Туре (Тип), облегчающий динамическое использование интерфейсов. В качестве примера рассмотрим интерфейс ICustomer2, имеющий, по сравнению с интерфейсом ICustomerl, дополнительный метод ShowCustomer. gc i n t e r f a c e ICustomer2 : ICustomerl // сборщик мусора - ICustomer2: ICustomerl {
public: void ShowCustomers(int i d ) ; / / идентификатор Предположим, что класс Customerl поддерживает интерфейс ICustomerl, a класс Customer2 — интерфейс ICustomer2. Для консольной программыклиента удобнее использовать исходный метод ShowCustomer, а не метод GetCustomer, так как последний создает список массивов и копирует данные в него. Поэтому программа-клиент предпочтет работать с интерфейсом ICustomer2, если это возможно. В папке T e s t l n t e r f a c e B e f o r e C a s t содержится программа, рассмотренная в следующем разделе. Проверка поддержки интерфейса перед приведением типов Проверку поддержки интерфейса можно производить, выполняя динамическое приведение типа указателя и обрабатывая исключение, которое может при этом возникнуть. Однако более изящным решением будет выполнять проверку до приведения типа, избегая при этом возникновения исключений. Если объект поддерживает необходимый интерфейс, можно выполнять приведение типа для получения доступа к интерфейсу. С# поддерживает использование удобного оператора i s для проверки того, поддерживает ли объект определенный интерфейс. К сожалению, в C++ с этой целью приходится использовать метод отражения, реализуемый посредством методов GetType и Get I n t e r f a c e . В связи с тем, что это приводит к появлению несколько громоздкого выражения, в следующем примере с помощью директивы i d e f i n e определяется макрос IS (THIS, THAT INTERFACE), используемый далее в двух условных операторах if. //TestlnterfaceBeforeCast.cpp //MACRO: pObj->GetType()->GetInterface("Somelnterface"}!=0 // МАКРОС #define IS(THIS, THAT_INTERFACE) (THIS->GetType()->GetInterface( THAT_INTERFACE)!=0) #using using namespace System; // использование пространства имен Система; _gc interface ICustomerl {}; // сборщик мусора - ICustomerl; _gc __interface ICustomer2 : ICustomerl // сборщик мусора - ICustomer2: ICustomerl { public: void ShowCustomers(int id); // идентификатор 140
Глава 5. Управляемый C++ в .NET Framework
gc class Customerl : public ICustomerl {}; // класс сборщика мусора Customerl: ICustomerl {}; gc class Customer2 : public ICustomer2 // класс сборщика мусора Custoraer2: ICustomer2 { public: void ShowCustomers(int id) // идентификатор ( Console : -.WriteLine ( "Customer2 : : ShowCustomers : succeeded") ;
void main(void) // главный { Customerl *pCustl = new Customerl; // не к ICustomer2 Console::WriteLine(pCustl->GetType()); // проверить, относится ли к типу ICustomer2 перед приведением if (IS (pCustl, "ICustomer2")) { ICustomer2 *plcust2 = dynamic_cast(pCustl); pIcust2->ShowCustomers(-1); } else Console::WriteLine ("pCustl does not support ICustomer2 interface"); // ("pCustl не поддерживает интерфейс ICustomer2"); Customer2 **pCust2 = new Customer2; // да, на ICustomer2 Console::WriteLine[pCust2->GetType()); // проверить, относится ли к типу ICustomer2 перед приведением if (IS(pCust2, "ICustomer2")) { ICustomer2 *plcust2 = dynamic_cast(pCust2); pIcust2->ShowCustomers(-1); } else Console::WriteLine ("pCust2 does not support ICustomer2 interface"); // ("pCust2 не поддерживает интерфейс ICustomer2"); } В этом примере продемонстрировано далеко не оптимальное решение, так как проверка типа производится дважды. Первый раз — при использовании отражения для проверки поддержки интерфейса в макросе IS. А еще раз проверка производится автоматически — при выполнении динамического приведения типа, в этом случае, если интерфейс не поддерживается, возникает исключение. Результат работы программы приведен ниже. Обратите внимание, что при выполнении программы действительно не возникает исключения. Customerl pCustl does not support ICustomer2 interface Интерфейсы
141
Customer2 Customer2::ShowCustomers: 11 А вот и перевод :
succeeded
Customerl pCustl не поддерживает интерфейс ICustomer2 Customer2 Customer2:: ShowCustomers: успешно Оператор d y n a m i c _ _ c a s t Результатом выполнения оператора d y n a m i c _ c a s t является непосредственно указатель на интерфейс. В случае, если интерфейс не поддерживается, значение указателя устанавливается равным нулю. Используем этот факт для создания программы, в которой проверка поддержки интерфейса производится один раз 1 2 . Приведенный ниже фрагмент взят из примера C a s t T h e n T e s t F o r N u l l , отличающегося от предыдущего, T e s t l n t e r f aceBef o r e C a s t , тем, что в нем производится проверка равенства нулю результата динамического приведения типа. v o i d m a i n ( v o i d ) / / главный {
Customerl *pCustl = new Customerl; // нет ICustomer2 Console::WriteLine(pCustl->GetType()); // Использовать оператор С ++ dynamic_cast, чтобы проверить // наличие ICustomer2 ICustomer2 *plcust2 = dynamic_cast{pCustl); if (plcust2 != 0) pIcust2->ShowCustoiners (-1) ; else Console::WriteLine ("pCustl does not support ICustomer2 i n t e r f a c e " ) ; // ("pCustl не поддерживает интерфейс ICustomer2"); Customer2 *pCust2 = new Customer2; // да, есть ICustomer2 Console::WriteLine(pCust2->GetType()); // Использовать оператор С ++ dynamic_cast, чтобы проверить // наличие ICustomer2 plcust2 = dynamic_cast(pCust2); if (plcust2 != 0) pIcust2->ShowCustomers(-1); else Console::WriteLine ("pCust2 does not support ICustomer2 i n t e r f a c e " ) ; // ("pCust2 не поддерживает интерфейс ICustomer2"); } Результат выполнения программы C a s t T h e n T e s t F o r N u l l показывает, что действительно, исключения не возникает, но проверка поддержки интерфейса при этом производится всего один раз для каждого из объектов.
Добавлен, естественно, редактором русского перевода. — Прим. ред. ^ Оператор языка C++ dynamic_cast подобен оператору as в языке С#. 142
Глава 5. Управляемый C++ в .NET Framework
Customerl pCustl does not support ICustomer2 interface Customer2
ICustomer2::ShowCustoraers: succeeded
Вот перевод этой выдачи1-': Customerl
pCustl не поддерживает интерфейс ICustomer2 Customer2 I C u s t o m e r 2 : : ShowCustomers: успешно Если вы знакомы с моделью компонентных объектов Microsoft (COM), проверка поддержки классом интерфейса вам должна быть хорошо знакома.
Программа Бюро путешествий Acme (Acme Travel Agency) Попытаемся применить полученные знания об интерфейсах для небольшой переделки программы Бюро путешествий Acme (Acme Travel Agency). Одним из наибольших достоинств интерфейсов является то, что благодаря им повышается уровень абстракции, — это позволяет понять и ошутить систему на уровне взаимодействия ее отдельных частей, абстрагируясь от конкретной их реализации. Файлы с исходным кодом находятся в папке CaseStudy.
Интерфейсы в управляемом C++ и .NET .NET и модель компонентных объектов Microsoft (COM) имеют много сходного. В обеих этих технологиях фундаментальную роль играет концепция интерфейсов. Их удобно использовать для определения контрактов. Интерфейсы обеспечивают очень динамичный стиль программирования. В модели компонентных объектов Microsoft (COM) разработчику приходится самому заботиться о тщательном создании инфраструктуры, необходимой для реализации СОМ-компонентов. Для создания СОМ-объектов требуется реализовать фабрику класса (class factory). Для динамической проверки поддержки интерфейса разработчик должен реализовать метод Query/Interface интерфейса iUnknown. Кроме того, для соответствующего управления памятью следует реализовать методы AddRef и Rel e a s e (Освободить). При Использовании же языков .NET все эти действия осуществляются автоматически виртуальной машиной, реализующей общий язык времени выполнения CLR (Common Language Runtime). Для создания объекта достаточно воспользоваться оператором new (создать). Проверку поддержки классом интерфейса и получение указателя на интерфейс несложно провести с помощью оператора dyn.amic_cast. Управление памятью берет на себя сборщик мусора.
Контракт Ранее мы уже рассмотрели интерфейс ICustomer класса Customers (Клиенты). Теперь обратим внимание на класс HotelBroker. Его методы естественным образом разделяются на три группы. 1. Информация о гостинице, такая, как названия городов, в которых есть гостиницы, и названия гостиниц, которые есть в определенном городе. ' 3 Добавлен, естественно, редактором русского перевода. — Прим. ред.
Интерфейсы
143
2.
Управление информацией о гостиницах, в частности добавление или удаление гостиницы из базы данных либо изменение количества комнат, доступных в некоторой гостинице.
3. Операции резервирования гостиниц, например, бронирование номеров, отмена заказа или просмотр списка резервирования. В свете изложенного логично будет создать для класса HotelBroker три интерфейса. Эти интерфейсы определены в файле AcmeDefinitions . h. ___gc i n t e r f a c e IHotellnfo // сборщик мусора - I H o t e l l n f o {
ArrayList *GetCItiesf}; ArrayList *GetHotels(); ArrayList *GetHotels(String *pCity);
gc interface IHotelAdrain // сборщик мусора - IHotelAdmin { String *AddHotel( String *pCity, String *pName, int numberRooms, Decimal r a t e ) ; // Десятичная цена String *DeleteHotel(String *pCity, String *pName); String *ChangeRooms( String *pCity, String *pName, int numberRooms, Decimal r a t e ) ; // Десятичная цена }; gc interface IHotelReservation // сборщик мусора - IHotelReservation {
ReservationResult MakeReservation( int customerld, String *pCity, String *pHotel, DateTime checkinDate, int numberDays) ; void CancelReservation(int i d ) ; // идентификатор ArrayList *FindReservationsForCustomer( int nCustomerld);
}; Реализация Далее реализуем систему управления гостиницами, используя коллекции вместо массивов. При этом мы будем возвращать программе-клиенту запрошенную ею информацию в методе T e s t H o t e l : : Main вместо того, чтобы делать это непосредственно в классе HotelBroker. Ранее в этой же главе мы рассмотрели новую реализацию класса Customers (Клиенты). Принципы, применявшиеся при тех переделках, будут использованы И для обновления класса HotelBroker. 144
Глава 5. Управляемый C++ в .NET Framework
Структуры Прежде всего следует разобраться со структурой данных, передаваемых клиенту по его запросу. Мы используем класс A r r a y L i s t (Список массивов). А что же будет храниться в указанном списке массивов? В нашем случае это могут быть объекты Customer (Клиент) и Hotel (Гостиница). Проблема применимости такого подхода состоит в том, что кроме данных, которые клиенту могут понадобиться, оба этих класса содержат также данные, необходимые для реализации класса, но не нужные программе-клиенту вовсе. Для того чтобы решить эту проблему, определим несколько структур. В файле Customers . h определим структуру C u s t o m e r L i s t l t e m , предназначенную для возврата информации о клиенте. value
s t r u c t CustomerListltem
{
public: int nCustomerld; String *pFirstName; String *pLastName; String *pEmailAddress; }; В файле Acme D e f i n i t i o n s . h определим структуры для хранения данных о гостиницах и заказах, а также результатов резервирования. value s t r u c t H o t e l L i s t l t e m {
public: String *pCity; String *pHotelName; int nNumberRooms; Decimal decRate; // Десятичное число }; ___value struct ReservationListltem { public: int nCustomerld; int nReservationld; String *pHotelName; String *pCity; DateTime dtArrivalDate; DateTime dtDepartureDate; int nNumberDays; }; value struct ReservationResult { public: int nReservationld; Decimal decReservationCost; // Десятичное число Decimal decRate; // Десятичное число String *pComment;
Интерфейсы
145
R e s e r v a t i o n R e s u l t возвращает значение R e s e r v a t i o n l d или -1 при возникновении проблем (в этом случае в поле pComment содержится более подробное описание возникших проблем; если же никаких проблем нет, там находится строка "ОК"). А теперь вам стоит изучить файлы исходного кода, находящиеся в папке CaseStudy, откомпилировать и скомпоновать приложение, и запустить его.
Явное определение интерфейсов При использовании интерфейсов может возникать неопределенность в случае, если в двух реализованных классом интерфейсах есть методы с одинаковыми именами и сигнатурами. Просмотрим, например, следующие версии интерфейсов IAccount и I S t a t e ment. Обратите внимание, что в каждом из них есть метод Show (Показать). qc interface IAccount // сборщик мусора - IAccount {
void Deposit(Decimal amount); // Депозит (Десятичное // количество) void Withdraw(Decimal amount); // Снять (Десятичное количество) property Decimal get_Balance(); // Десятичное число v o i d S h o w { ) ; // Показать qc interface IStatement // сборщик мусора - IStatement {
property int get_Transactions(); void Show(); // Показать }; •/ Как в подобном случае указать классу нужную реализацию метода? Такая за\щ. ч--^.-.GetEnumerator(); bool more - pIter->MoveNext(); while (more) { String *pStr = dynamic_cast((pIter->Current)); Console::WriteLine(pStr); more = pIter->MoveNext();
} s t a t i c void ShowList(ArrayList *pArray)
// статическая функция
Родовые интерфейсы в .NET
149
lEnumerator *pEnum = pArray->GetEnumerator() ; while (pEnum->MoveNext()) { String *pStr = dynamic__cast(pEnum->Current) Console::WriteLine(pStr);
static void ShowArray(ArrayList *pArray) // статическая функция { for (int i = 0; i < pArray->Count; Console::WriteLine( "pArray->get_Item({0}) = {1}", box(i), pArray->get_Item(i)); static void ShowCount() // статическая функция { Console::WriteLine( "pList->Count = {0}", box(pList->Count)); Console::WriteLine( "pList->Capacity = { 0 } ", box(pList->Capacity)); // Вместимость } static void AddString(String *pStr) // статическая функция { if (pList->Contains(pStr)) // если содержит throw new Exception) // новое Исключение String::Format("list contains {0}", pStr)); // Формат: список содержит pList->AddfpStr); // Добавить } static void RemoveString(String *pStr) // статическая функция { if (pList->Contains(pStr)) // если содержит pList->Remove(pStr); // Удалить else Console::WriteLine( "List does not contain {0}", pStr); // Список // не содержит } static void RemoveAt(int nindex) // статическая функция { try // попытка { pList->RemoveAt(nindex); } catch (ArgumentOutOfRangeException *) { Console::WriteLine( "No element at index {0 } ", box(nindex)); 150 Глава 5. Управляемый C++ в .NET Framework
// Нет элемента с таким индексом
Результат работы программы будет таким14: pList->Count = 0 pList->Capacity = 4 // Вместимость // Эми Amy // Боб Bob Charlie // Чарли pList->Count = 3 pList->Capacity = A // Вместимость Amy// Эми // Боб Bob Charlie // Чарли David // Дэвид Ellen // Эллен pList->Count = 5 pList->Capacity = 8 // Вместимость pArray->get Item(O) = Bob // Боб pArray->get Item(l) = Charlie // Чарли pArray->get Item(2) = Ellen // Эллен pList->Count = 3 pList->Capacity = 8 // Вместимость List does not contain Amy // Список не с No element at index 3 // Нет элемент lEnumerableИIEnumerator Исходным для всех интерфейсов, предназначенных для работы с коллекциями, является интерфейс lEnumerable, имеющий один метод — GetEnumerator. gc i n t e r f a c e lEnumerable // сборщик мусора - lEnumerable IEnumerator*
GetEnumerator();
GetEnumerator возвращает указатель на интерфейс IEnumerator, который используется для последовательного доступа к элементам коллекции. Этот интерфейс имеет свойство Current (Текущая запись) и методы MoveNext и Reset (Сброс). gc i n t e r f a c e IEnumerator // сборщик мусора - IEnumerator property Object* g e t _ C u r r e n t ( ) ; bool MoveNext(); // логический (булев) void R e s e t ( ) ; Сразу после инициализации нумератор указывает на позицию перед первым элементом коллекции и, следовательно, для получения доступа даже к первому ее элементу его следует продвинуть. Использование нумератора для последовательного доступа к элементам списка проиллюстрировано в методе ShowEnum.
'
Колонка справа добавлена для удобства чтения, в реальной выдаче, ее, естественно, не будет. — Прим. ред.
Родовые интерфейсы в .NET
151
static void ShowEnumfArrayList *pArray) // статическая функция { IEnumerator *plter = pArray->GetEnumerator f); bool more = pIter->MoveNext(); // логическое значение while {more) { String *pStr = dynamic_cast( ( p I t e r ~ > C u r r e n t ) ) ; Console::WriteLine(pStr); more = pIter->MoveNext();
Интерфейс I C o l l e c t i o n Интерфейс I C o l l e c t i o n является производным от IEnumerable и в нем добавляются свойство Count (Количество) и метод СоруТо. gc i n t e r f a c e I C o l l e c t i o n : public IEnumerable // сборщик мусора - I C o l l e c t i o n : IEnumerable {
property i n t get_Count(); property bool get_IsSynchronized(); // логический property Object* get_SyncRoot(]; void CopyTo(Array* a r r a y , i n t index); // массив, индекс
};
Кроме того, в данном интерфейсе появляются свойства, предназначенные для обеспечения синхронизации при использовании потоков. "Является ли безопасным использование потоков?" — вот один из часто задаваемых вопросов о любой библиотеке. Что касается среды .NET Framework, то ответ на этот вопрос короток и ясен — нет. Это не значит, что разработчики .NET Framework не задумывались о безопасности использования потоков. Наоборот, они реализовали множество механизмов, которые могут помочь вам при работе с потоками. Причина же, по которой использование потоков при работе с коллекциями не является безопасным автоматически, — в том, что обеспечение безопасности приводит к понижению производительности, а если ее обеспечить автоматически, то и при работе в однопотоковом режиме производительность окажется заниженной. Если же вам необходимо обеспечить безопасность при работе с потоками, вы можете использовать свойства интерфейса, предназначенные для синхронизации. Более подробно механизмы синхронизации потоков в .NET Framework будут рассмотрены в главе 8 "Классы каркаса .NET Framework". Программа S t r i n g L i s t иллюстрирует использование свойства C a p a c i t y (Объем) класса A r r a y L i s t (Список массивов), а также свойства Count (Количество), унаследованного классом A r r a y L i s t (Список массивов) от интерфейса I C o l l e c t i o n . s t a t i c void ShowCount() // статическая функция {
Console::WriteLine(
"pList->Count = {0}", box(pList->Count)); // Счет Console::WriteLine{ "pList->Capacity = {0}", box(pList->Capacity)); // Вместимость
152
Глава 5. Управляемый C++ в .NET Framework
Интерфейс Интерфейс I L i s t является производным от интерфейса i C o l l e c t i o n и в нем введены методы для добавления элемента в список, удаления его из списка и т.д. gc i n t e r f a c e I L i s t : public ICollection // сборшик мусора - I L i s t : ICollection {
property property property property
bool get_IsFixedSize () ; // логический bool get_IsReadOnly(); // логический Object* get_Itera(int index); // индекс void set_Itera(int index, Object*); // индекс, // Объект * int Add(Object* value); // Добавить значение void Clear(); bool Contains(Object* value); // Содержит ли значение int IndexOf(Object* value); // значение void I n s e r t ( i n t index. Object* value); // Вставка (int индекс, // Object* значение); void Remove(Object* value); // Удалить значение void ReraoveAt(int index); // индекс
}; В программе S t r i n g L i s t продемонстрировано использование индексатора g e t _ I t e m и методов C o n t a i n s (Содержит), Add (Добавить), Remove (Удалить) и RemoveAt. static void ShowArray(ArrayList *pArray) { for (int i = 0; i < pArray->Count; i++) { Console::WriteLine( "pArray->get_Item({0}) = {I}", box(i), pArray->get_Item(i));
static void AddString(String *pStr) { if (pList->Contains(pStr)) // если содержит throw new Exception( // новое Исключение String::Format("list contains {0}", pStr)); // Формат:: ("список содержит") pList->AddContains(pStr)) // если содержит pList->Remove(pStr); // Удалить else Console::WriteLine( "List does not contain {0}", pStr); // Список // не содержит } static void RemoveAt(int nlndex) Родовые интерфейсы в .NET
153
try // попытка { pList->RemoveAt(nlndex); } catch (ArgumentOutOfRangeException *) { Console::WriteLine( "No element at index {0}", __box(nlndex)); // Нет элемента / / с индексом }
}
Копирование объектов и интерфейс icioneabie Иногда бывает необходимо сделать копию объекта. Если при этом копируются объекты, которые содержат другие объекты или указатели на них, то для корректной реализации этого процесса необходимо понимать его особенности. Ниже мы сравним копирование указателя, поверхностное почленное копирование и детальное копирование. Мы увидим, что в зависимости от реализации интерфейса I C l o n e a b l e , используемого для копирования объектов, существует возможность как поверхностного, так и детального копирования. Напомним, что в .NET Framework есть значащие типы данных и ссылочные типы 1 5 . Значащие типы данных представляют собой непосредственно данные, а ссылочные типы всего лишь указывают местонахождение данных. Если значение одной переменнойуказателя скопировать в другую переменную того же типа, обе переменные будут указывать на один и тот же объект. Если с помощью первого указателя этот объект будет изменен, то изменится и объект, на который ссылается второй указатель. Иногда требуется именно такое поведение, а иногда другое. Интерфейс I C l o n e a b l e Интерфейс I C l o n e a b l e является исходным и имеет единственный метод— Clone (Клон). Данный метод может быть реализован для выполнения как поверхностного, так и детального копирования, но указатель на Object (Объект), возвращаемый методом, должен указывать на объект того же (или совместимого) типа, для которого реализован интерфейс I C l o n e a b l e . Обычно метод Clone (Клон) реализуют так, чтобы он создавал новый объект. Однако бывает, например в случае класса S t r i n g (Строка), что метод Clone (Клон) просто возвращает указатель на исходный объект. gc i n t e r f a c e ICloneable // сборщик мусора - ICloneable {
Object*
Clone();
//
Клон
};
Поверхностная и детальная копии Неуправляемые структуры и классы в C++ автоматически реализуют почленное копирование содержимого, которое будет актуальным до тех пор, пока не будет произведена подмена конструктора копирования. Почленное копирование называют также поверхДля нашего рассмотрения разница между указателями и ссылками несущественна.
154
Глава 5. Управляемый C++ в .NET Framework
постным копированием. В базовом классе Object (Объект) также есть защищенный (protected) метод, MemberwiseClone, выполняющий почленное копирование управляемых структур или классов. Если среди членов структуры или класса есть указатели, такое почленное копирование может быть не тем, что вам требуется. Результатом этого копирования будет то, что указатели разных объектов будут ссылаться на одни и те же данные, а не на их независимые копии. Для фактического копирования данных следует сделать детальную копию. Детальное копирование может быть обеспечено на уровне языка или на уровне библиотек. В обычном C++ детальное копирование осуществляется на уровне языка с использованием конструктора копирования. В управляемом C++ оно осуществляется .NET Framework с помощью интерфейса I C l o n e a b l e , который можно реализовывать в создаваемых классах специально для того, чтобы эти классы могли выполнять детальное копирование. Следующими тремя вариантами исчерпываются возможные виды копирования, как для управляемых классов и структур, с использованием интерфейса I C l o n e a b l e или без него, так и для неуправляемых классов и структур. •
Присваивание значения одного указателя другому (будь то указатели на управляемые или неуправляемые классы или структуры) приводит к простому копированию указателей. Это не поверхностное, и не детальное копирование, а простое присваивание указателей.
• Преобразование одного нессылочного (неуказательного) типа в другой, причем типы эти могут быть неуправляемыми классами, неуправляемыми структурами или значащими типами данных, является поверхностным копированием, автоматически осуществляемым C++. •
Присваивание новому указателю значения, возвращаемого методом Clone (Клон) (конечно, это значение должно быть также указателем) и являющегося управляемым классом или структурой с реализованным интерфейсом I C l o n e a b l e , представляет собой поверхностное либо детальное копирование, в зависимости от реализации метода Clone (Клон).
Пример программы Проиллюстрируем изложенный материал программой CopyDemo. Эта программа делает копию экземпляра класса Course (Курс). В классе Course (Курс) содержится название курса и список (коллекция) студентов. //Course.h gc class Course : public ICloneable // класс сборщика мусора Курс: ICloneable { public: String *pTitle; ArrayList *pRoster; Course(String *pTitle) // Курс { this->pTitle = pTitle; pRoster = new ArrayList; } void AddStudent(String *pName) Родовые интерфейсы в .NET
155
pRoster->Add(pName); // Добавить } void Show(String *pCaption) // Показать { Console : :WriteLine (" {0} ", pCaption) ; Console::WriteLine( "Course : {0} with {1} students", // Курс: студенты pl'itle, box(pRoster->Count)); // Счет lEnumerator *pEnum = pRoster->GetEnumerator(); while (pEnum->MoveNext() ) { String *pName = dynamic_cast(pEnum->Current); Consoles:WriteLine(pName}; } } Course *ShallowCopy() // Курс - поверхностная копия < return dynamic_cast(MemberwiseClone() ) ; } Object *Clone() { Course *pCourse = new Course(pTitle); // новый Курс pCourse->pRoster = dynamic_cast(pRoster->Clone ()); // Клон return pCourse; Тестовая программа создает экземпляр класса Course (Курс), называющийся pCl, a затем разными способами создает его копии с именем рС2. //CopyDemo.h gc class CopyDemo // класс сборщика мусора CopyDemo { private: // частный static Course *pCl, *pC2; // статический Курс public: static void Main() { Console::WriteLine("Copy is done via pC2 = pCl"); // Копия, сделанная через рС2 = pCl InitializeCourse(); pCl->Show("original"); // Показать ("оригинал"); pC2 = pCl; pC2->Show("copy"); // Показать ("копия"); pC2->pTitle = ".NET Programming"; // Программирование на .NET pC2->AddStudent("Charlie"); // Чарли pC2->Show("copy with changed title and new student"); // Показать ("копия с измененным названием // и новым студентом"); 156
Глава 5. Управляемый C++ в .NET Framework
pCl->Show("original"); // Показать ("оригинал"); Console::WriteLine( "\nCopy is done via pC2 = pCl->ShallowCopy()"); InitializeCourse(); pC2 = pCl->ShallowCopy(); pC2->pTitle = ".NET Programming"; // Программирование на .NET pC2->AddStudent("Charlie"); // Чарли pC2->Show("copy with changed title and new student"); I/ Показать ("копия с измененным названием и новым // студентом"); pCl->Show("original"); // Показать ("оригинал"); Console::WriteLine( "\nCopy is done via pC2 = pCl->Clone()"); InitializeCourse(); pC2 = dynamic_cast(pCl->Clone()); pC2->pTitle = ".NET Programming"; // Программирование //на .NET pC2->AddStudent("Charlie"); // Чарли pC2->Show("copy with changed title and new student"); // Показать ("копия с измененным названием // новым студентом"); pCl->Show("original"); // Показать {"оригинал"); } private: // частный static void InitializeCourse() { pCl = new Course("Intro to Managed C++"); // новый Курс ("Введение в Управляемый С ++ " ) ; pCl->AddStudent("John"); // Джон pCl->AddStudent("Mary"); // Мэри Вот вьщача программы: Copy is done via pC2 = pCl original Course : Intro to Managed C++ with 2 students John Mary copy Course : Intro to Managed C++ with 2 students John Mary copy with changed title and new student Course : .NET Programming with 3 students John Mary Charlie original Course : .NET Programming with 3 students John Mary Charlie
Родовые интерфейсы в .NET
157
Copy is done via pC2 = pCl->ShallowCopy() copy with changed title and new student Course : .NET Programming with 3 students John Mary Charlie original Course : Intro to Managed C++ with 3 students John Mary Charlie Copy is done via pC2 = pCl->Clone() copy with changed title and new student Course : .NET Programming with 3 students John Mary Charlie original Course : Intro to Managed C++ with 2 students John Mary А вот и перевод выдачи 16 : Копия сделана через рС2 = рС1 оригинал Курс: Введение в Управляемый С ++ с 2 студентами Джон Мэри копия Курс: Введение в Управляемый С ++ с 2 студентами Джон Мэри копия с измененным названием и новым студентомКурс: Программирование на .NET с 3 студентами Джон Мэри Чарли оригинал Курс: Программирование на .NET с 3 студентами Джон Мэри Чарли Копия сделана через рС2 = рС1-> ShallowCopy () копия с измененным названием и новым студентомКурс: Программирование на .NET с 3 студентами Джон Мэри Чарли оригинал 16
Добавлен, естественно, редактором русского перевода. — Прим. ред.
158
Глава 5. Управляемый C++ в .NET Framework
Курс: Введение в Управляемый С ++ с 3 студентами Джон Мэри Чарли Копия сделана через рС2 = рС1-> Clone () копия с измененным названием и новым студентом Курс: Программирование на .NET с 3 студентами Джон Мэри Чарли оригинал Курс: Введение в Управляемый С ++ с 2 студентами Джон Мэри Копирование указателей с помощью присваивания Первым способом копирования является просто присваивание рС2=рС1. В этом случае мы получаем два указателя на один и тот же объект, и изменения, произведенные с использованием первого указателя, можно будет обнаружить в данных, адресуемых вторым указателем. gc class CopyDemo // класс сборщика мусора CopyDemo public: static void Main() { Console::WriteLine("Copy is done via pC2 = pCl"); // Копия сделана через рС2 = pCl InitializeCourse(); pCl->Show("original"); // Показать ("оригинал"); pC2 = pCl; pC2->Show("copy"); // Показать ("копия"); pC2->pTitle = ".NET Programming"; // Программирование на .NET pC2->AddStudent("Charlie"); // Чарли pC2->Show("copy with changed title and new student"); // Показать ("копия с измененным названием // и новым студентом"); p C l - > S h o w ( " o r i g i n a l " ) ; // Показать ( " о р и г и н а л " ) ; Экземпляр класса Course (Курс) инициализируется методом I n i t i a l i z e C o u r s e , причем в качестве названия курса выбирается "Intro to Managed C + + " (Введение в управляемый C++) и на курс записаны два студента. Затем выполняется присваивание рС2=рС1, и с помощью указателя рС2 изменяется заголовок и добавляется новый студент. Затем демонстрируется, что произведенные изменения видны с помощью обоих указателей. Приведем результат работы первой части программы: Copy is done via pC2 = pCl original Course : Intro to Managed C++ with 2 students John Родовые интерфейсы в .NET
159
Mary
copy Course : Intro to Managed C++ with 2 students John Mary copy with changed title and new student Course : .NET Programming with 3 students John Mary Charlie original Course : .NET Programming with 3 students John Mary Charlie А вот и перевод выдачи 17 : Копия сделана через рС2 = рС1 оригинал Курс: Введение в Управляемый С ++ с 2 студентами Джон Мэри копия Курс: Введение в Управляемый С ++ с 2 студентами Джон Мэри копия с измененным названием и новым студентом Курс: Программирование на .NET с 3 студентами Джон Мэри Чарли оригинал Курс: Программирование на .NET с 3 студентами Джон Мэри Чарли
Почленное копирование Теперь проиллюстрируем почленное копирование, выполненное с помощью метода MemberwiseClone класса Object (Объект). Так как этот метод защищен (имеет спецификатор доступа p r o t e c t e d ) , он может быть вызван непосредственно только из экземпляра класса Course (Курс). Поэтому в классе Course (Курс) мы определили метод ShallowCopy, реализованный через метод MemberwiseClone. gc c l a s s Course : public ICloneable // класс сборщика мусора Курс: ICloneable Course *ShallowCopy
Добавлен, естественно, редактором русского перевода. — Прим. ред.
160
Глава 5. Управляемый C++ в .NET Framework
r e t u r n dynamic_cast(MemberwiseClone
Приведем вторую часть тестовой программы, в которой происходит вызов метода ShallowCopy. Также, как и раньше, изменим с помощью второго указателя заголовок и добавим в список нового студента. gc c l a s s CopyDemo // класс сборщика мусора CopyDemo public: static void Main Console::WriteLine( "\nCopy is done via pC2 = pCl->ShallowCopy()"); InitializeCourse(); pC2 = pCl->ShallowCopy(); pC2->pTitle = ".NET Programming"; // Программирование на .NET pC2->AddStudent("Charlie"); // Чарли pC2->Show("copy with changed t i t l e and new s t u d e n t " ) ; // Показать ("копия с измененным названием // и новым студентом"); pCl->Show("original"); // Показать ("оригинал"); Ниже приведен результат работы второй части программы. Видно, что поле T i t l e (Название), существует после копирования в двух независимых экземплярах, но коллекция R o s t e r (Список), представляющая собой список студентов, была скопирована через указатель. Поэтому она одна для обеих копий, и изменения, внесенные с использованием одного указателя, видны через другой. Copy is done via pC2 = pCl->ShallowCopy() copy with changed t i t l e and new student Course : .NET Programming with 3 students John Mary Charlie original Course : Intro to Managed C++ with 3 students John Mary Charlie А вот и перевод выдачи 18 : Копия сделана через рС2 = рС1-> ShallowCopy () копия с измененным названием и новым студентом Курс: Программирование на .NET с 3 студентами Джон ' 8 Добавлен, естественно, редактором русского перевода. ^ Прим. ред. Родовые интерфейсы в .NET
161
Мэри Чарли оригинал Курс: Введение в Управляемый С ++ с 3 студентами Джон Мэри Чарли Использование ICloneable Последний способ копирования использует тот факт, что .ласе Course (Курс) поддерживает интерфейс I C l o n e a b l e и реализует метод Clone (Клон). Для копирования коллекции R o s t e r (Список) используется также то, что A r r a y L i s t (Список массивов) реализует интерфейс I C l o n e a b l e , как было указано в этой главе ранее. Заметим, что метод Clone (Клон) возвращает значение типа Object*, так что перед тем, как присвоить его полю p R o s t e r , возвращаемое значение необходимо привести к типу ArrayList*. gc class Course : public ICloneable // класс сборщика мусора Курс: ICloneable { public: Object *Clone() {
Course *pCourse = new Course(pTitle); // новый Курс pCourse->pRoster = dynamic_cast(pRoster->Clone()); return pCourse;
Приведем третью часть программы, в которой вызывается метод Clone (Клон). Здесь мы тоже с помощью второго указателя изменяем название и добавляем в список нового студента. gc c l a s s CopyDemo // класс сборщика мусора CopyDemo public: s t a t i c void Main(} Console::WriteLine ( "\nCopy is done via pC2 = pCl->Clone()"); InitializeCourse(); pC2 = dynamic_cast(pCl->Clone()); pC2->pTitle = ".NET Programming"; // Программирование на .NET pC2->AddStudent("Charlie"); // Чарли pC2->Show("copy with changed t i t l e and new s t u d e n t " ) ; // Показать ("копия с измененным названием // и новым студентом"); 162
Глава 5. Управляемый C++ в .NET Framework
pCl->Show("original"); // Показать ("оригинал"); Приведем выдачу третьей части программы. Теперь видно, что в результате копирования мы получили два независимых экземпляра класса Course (Курс), каждый из которых имеет свой заголовок и список студентов. Copy is done via pC2 = pCl->Clone{) copy with changed t i t l e and new student Course : .NET Programming with 3 students John Mary Charlie original Course : Intro to Managed C++ with 2 students John Mary А вот и перевод выдачи 19 : Копия сделана через рС2 = рС1-> Clone () копия с измененным названием и новым студентом Курс: Программирование на .NET с 3 студентами Джон Мэри Чарли оригинал Курс: Введение в Управляемый С ++ с 2 студентами Джон Мэри Последний подход иллюстрирует природу интерфейсов .NET. Вы можете копировать объекты, не особо задумываясь и не беспокоясь об их типах.
Сравнение объектов Итак, мы подробно рассмотрели копирование объектов. Теперь рассмотрим сравнение объектов. Для сравнения объектов в .NET Framework используется интерфейс ICompar a b l e . В этом разделе мы в качестве примера воспользуемся интерфейсом iComparaЫе для сортировки массива. Сортировка массива / В классе System: : Array (Система::Массив) статический метод S o r t г •••».^- (Сортировка) предназначен для сортировки массива. Программа ArrayName ."'•КОД иллюстрирует п р и м е н е н и е этого метода к с о р т и р о в к е массива объектов Name ( И м я ) , где класс Name ( И м я ) содержит просто строку. Приведем л и с т и н г основной программы: //ArrayName.cpp gc class ArrayName // клас^ сборщика мусора ArrayName
Добавлен, естественно, редактором русского перевода. — Прим. ред.
Родовые интерфейсы в .NET
163
publicstatic void Main Name *array[] = new Name*[5]; // Имя array[0] = new Name("Michael"); // новое Имя ("Майкл") array[l] ~ new Name("Charlie"); // новое Имя ("Чарли") array[2] = new Name("Peter"); // новое Имя ("Питер") array[3] = new Name("Dana"); // новое Имя ("Дана") array[4] = new Name("Bob"); // новое Имя ("Боб") if (dynamic_cast(array[0]) != 0) Array::Sort(array); else Console::WriteLine( "Name does not implement IComparable"); // (" Name (Имя) не реализует IComparable"); IEnumerator *pEnum = array->GetEnumerator(); while (pEnum->MoveNext()) { Name *pName = // Имя dynamic_cast(pEnum->Current); if (pName != 0) Console::WriteLine(pName);
Реализация IComparable Для того чтобы можно было произвести сортировку, необходимо определить правила сравнения сортируемых объектов. Такие правила реализуются в методе СотрагеТо интерфейса IComparable. Поэтому, для того, чтобы иметь возможность сортировать массивы, содержащие данные определенного вами типа, необходимо реализовать интерфейс IComparable для этого типа данных. gc i n t e r f a c e IComparable // сборщик мусора - IComparable {
i n t СотрагеТо(Object* ob j ) ; }; Приведем листинг реализации класса Name (Имя), включая и реализацию интерфейса IComparable: gc c l a s s Name : public IComparable {
private: // частный String *pText; public: Name(String *pText) // Имя { this->pText = pText; } ___property String* get_Item() { return pText; 164
Глава 5. Управляемый C++ в .NET Framework
_property void set_Item(String* pText)
{ this->pText = pText; }
int CompareTo(Object *pObj) // Объект { String *pSl = this->pText; String *pS2 = (dynamic__cast (pObj ) ) ->pText; return String::Compare(pSl, pS2); // сравниваются имена } String *ToString() { return pText; Результатом работы профаммы является упорядоченный по алфавиту список имен 2 0 : Bob Charlie Dana Michael Peter
'
// // // // //
Боб Чарли Дана Майкл Питер
Что такое каркасы приложений Наши примеры предполагают использование некоего каркаса приложений. Такой каркас — это больше, чем просто библиотека. При использовании библиотеки вы можете вызывать в своих программах библиотечные функции. При работе с каркасом приложений не только ваша программа может обращаться к каркасу, но и сам каркас может обращаться к вашей программе. Таким образом, программу можно уподобить среднему слою сандвича: • •
ваша программа обращается к нижнему уровню; верхний уровень обращается к вашей программе.
.NET Framework — отличный пример подобной архитектуры. Этот каркас обеспечивает богатый набор возможностей, которые можно использовать напрямую. Кроме того, широкий выбор интерфейсов, которые можно реализовать в программе, позволяет обеспечить задуманное поведение программы при обращении к ней каркаса, часто выполняемом по заказу других объектов.
Делегаты Интерфейсы облегчают написание программ в том смысле, что ее составляющие могут быть вызваны другими приложениями или системой. Такой стиль программирования известен давно как использование функций обратного вызова (callback function). В этом разделе мы рассмотрим использование делегатов (delegate) в управляемом C++. Делегаты можно рассматривать в качестве объектно-ориентированных функций обратного вызова, обеспечивающих типовую безопасность. Делегаты— основа более сложного протокола
Правая колонка (в виде комментариев) добавлена редактором русского перевода. — Прим. ред.
Делегаты
165
вызова функций обратного вызова, называемого событиями, которые мы рассмотрим в следующем разделе главы. Функции обратного вызова — это функции, которые ваша программа определяет и некоторым образом "регистрирует", после чего они могут быть вызваны другими приложениями. В С и C++ такие функции реализуются с использованием указателей на функции. В управляемом C++ указатель на метод можно инкапсулировать в объекте-делегате. Делегат может указывать как на статический метод, так и на экземпляр метода. Если делегат указывает на метод экземпляра класса, он хранит и сам экземпляр класса, и точку входа в метод. Таким образом, метод экземпляра класса можно вызвать, используя сам экземпляр класса. Если делегат указывает на статический метод, в нем хранится только точка входа в этот метод. Если делегат-объект передается какой-либо части программы, она может вызвать метод, на который указывает делегат. Нередко части программы, использующие делегат, компилируются отдельно от описания самого делегата, так что при их создании даже неизвестно, какие методы они будут использовать в действительности. Делегат в действительности является управляемым классом, потомком класса System: : D e l e g a t e (Система::Делегат). Новый экземпляр делегата создается, как и для любого другого класса, с помощью оператора new (создать). Делегаты являются объектно-ориентированными и безопасными с точки зрения типов; они позволяют полностью использовать все средства безопасности, предусмотренные в среде выполнения управляемого кода.
Объявление делегата В управляемом C++ делегат объявляется с помощью особого обозначения — ключевого слова d e l e g a t e (делегат) — и сигнатуры инкапсулированного метода. В соответствии с соглашением об именовании, имя делегата должно заканчиваться буквосочетанием "Callback". Приведем пример объявления делегата: delegate void NotifyCallback(Decimal b a l a n c e ) ; // делегировать NotifyCallback (Десятичный баланс);
Определение метода После инициализации делегата следует определить метод обратного вызова, сигнатура которого соответствует сигнатуре, описанной в объявлении делегата. Метод может быть как статическим, так и методом экземпляра класса. Приведем несколько примеров методов, которые могут использоваться с объявленным выше делегатом NotifyCallback: s t a t i c void NotifyCustomer(Decimal balance) // Десятичный баланс {
Console::WriteLine("Dear c u s t o m e r , " ) ; // Дорогой клиент Console::WriteLine( " Account overdrawn, balance = {0}", // баланс на счете b o x ( b a l a n c e ) ) ; // баланс }
static void NotifyBank(Decimal balance) // Десятичный баланс { Console::WriteLine("Dear bank,"); // Дорогой банк Console::WriteLine( " Account overdrawn, balance = {0}", // баланс на счете box(balance));
166
Глава 5. Управляемый C++ в .NET Framework
void Notifylnstance(Decimal balance) // Десятичный баланс { Console::WriteLine("Dear instance,"); // Дорогой представитель Console::WriteLine( " Account overdrawn, balance = {0}", // баланс на счете ___box (balance) } ; // баланс }
Создание экземпляра делегата Экземпляр делегата инициализируется с помощью оператора new (создать), так же, как и для любого другого класса. Ниже приведен код, демонстрирующий создание двух экземпляров делегатов. Первый из них связан со статическим методом, второй — с методом экземпляра класса. Второй экземпляр делегата хранит как точку входа в метод, так и экземпляр класса, который используется для вызова метода. // создать делегат для статического метода NotifyCustomer NotifyCallback *pCustDlg = new NotifyCallback( 0, // ноль для статического метода NotifyCustomer NotifyCustomer); // создать делегат для экземпляра метода Notifylnstance NotifyCallback *pInstDlg = new NotifyCallback( pda, // отличный от нуля для экземпляра метода Notifylnstance Notifylnstance);
Вызов делегата Синтаксис "вызова" делегата совпадает с синтаксисом вызова метода. Делегат не является сам по себе методом, но он инкапсулирует метод. Делегат "передает" вызов инкапсулированному методу, потому и называется делегатом (от англ. delegate — поручать, уполномочивать). В приведенном ниже фрагменте кода делегат n o t i f y D l g вызывается в случае, если при выплате со счета получается отрицательный баланс. В этом примере экземпляр n o t i f y D l g инициализируется в методе S e t D e l e g a t e . gc c l a s s Account // класс сборщика мусора Счет {
private: // частный Decimal balance; // Десятичный баланс NotifyCallback *pNotifyDlg; void SetDelegate(NotifyCallback *pDlg) { pNotifyDlg = pDlg; } void Withdraw(Decimal ampunt) // Десятичное количество { balance = balance - amount; // баланс = баланс - количество; if (balance < 0) // если баланс pJoin += new JoinHar.dler (pChat, OnJoinCha't) ; pChat->pQuit += new QuitHandler(pChat, OnQuitChat);
Изначально событие представлено пустым указателем ( n u l l ) , т.е. не связано с какимлибо обработчиком событий, а добавление обработчиков событий происходит в процессе выполнения программы с помощью оператора +=. При вызове делегата, соответствующего событию, будут вызваны все зарегистрированные таким образом обработчики событий. Отменить регистрацию обработчика событий можно оператором -=.
Комната для дискуссий: пример чат-программы Пример чат-программы EventDemo иллюстрирует архитектуру как сервера, так и клиента. В сервере реализованы следующие методы: • JoinChat; • QuitChat; • ShowMembers. Когда в программе регистрируется новый участник или уходит зарегистрированный, сервер посылает сообщение об этом клиенту. Обработчик соответствующего события выводит на экран надлежащее сообщение. Приведем пример выдачи программы: sender = 01 Chat Room, Michael has joined the chat sender = 01 Chat Room, Bob has joined the chat sender = 01 Chat Room, Sam has joined the chat After 3 have joined Michael Bob Sam sender = 01 Chat Room, Bob has quit the chat After 1 has quit Michael Sam А вот и перевод23: отправитель = Комната для дискуссий 01, Майкл присоединился к чату отправитель = Комната для дискуссий 01, Боб присоединился к чату отправитель - Комната для дискуссий 01, Сэм присоединился к чату После того, как 3 присоединились Майкл БоО Сэм
" Д о б а в л е н , естественно, редактором русского перевода. — Прим. ред.
178
Глава 5. Управляемый C++ в .NET Framework
отправитель = Комната для дискуссий 01, Боб оставил чат После того, как 1 покинул Майкл Сэм
Исходный код клиента В клиенте реализованы обработчики событий. Прежде всего, клиент создает экземпляр серверного объекта, а затем ставит в соответствие каждому событию обработчик события. Затем клиент вызывает методы сервера. Эти вызовы приводят к генерации сервером событий, обрабатываемых соответствующими обработчиками событий клиента. //ChatClient .h gc class ChatClient // класс сборщика мусора ChatClient { public: static void OnJoinChat(Object *pSender, ChatEventArg *pe) { Console::WriteLine( "sender = {0}, {1} has joined the chat", // "отправитель = {0}, {1} присоединился к чату ", pSender, pe->pName); static void Or.QuitChat(Object *pSender, ChatEventArg *pe) { Console::WriteLine( "sender = (0}, {1} has quit the chat", // "отправитель = {0}, {1} покинул чат ", pSender, pe->pName); } static void Main() { // создать сервер чата ChatServer *pChat = new ChatServer("01 Chat Room"}; // "Комната для дискуссий 01" // Регистрация обработчиков сообщений от сервера oChat->pJoin += new JoinHandler{pChat, OnJoinChat); pChat->pQuit += new QuitHandler(pChat, OnQuitChat); // вызвать методы сервера pChat->JoinChat("Michael") ; // Майкл pChat->JoinChat("Bob"); // Боб pChat->JoinChat("Sam"); // Сэм pChat->ShowMembers("After 3 have joined"); // "После того, как 3 присоединились" pChat->QuitChat("Bob"); // Боб pChat->ShowMembers("After 1 has quit"); // "После того, как 1 ушел"
События
179
Исходный код сервера Сервер содержит код, обеспечивающий хранение в коллекции имен пользователей, присоединившихся к чату. При уходе участника его имя удаляется из коллекции. Присоединение нового пользователя или уход зарегистрированного приводит к генерации события, обрабатываемого клиентом. Кроме того, в сервере реализованы другие необходимые действия, такие, как объявление делегатов, событий и аргументов событий. В нем также реализованы вспомогательные методы, использующиеся для генерации событий. //ChatServer.h qc class ChatEventArg : public EventArgs // класс сборщика мусора ChatEventArg: EventArgs { public: String *pName; ChatEventArg(String *pName) { this->pName = p N a m e ;
__delegate void JoinHandlerf Object *pSender, ChatEventArg *pe); i delegate void QuitHandler( Object *pSender, ChatEventArg *pe); gc class ChatServer // класс сборщика мусора ChatServer { private: // частный ArrayList *pMembers; String *pChatName; public: __event JoinHandler *pJoin; event QuitHandler *pQuit; ChatServer(String *pChatName) { pMembers = new ArrayList; this->pChatName = pChatName; ) String *ToString() { return pChatName; } protected: // защищенный void OnJoin(ChatEventArg *pe) { if (pJoin != 0) { pJoin(this, ре); // запустить событие void OnQuit(ChatEventArg *pe) 180
Глава 5. Управляемый C++ в .NET Framework
if (pQuit != 0) { pQuitfthis, pe); // запустить событие public: void JoinChat(String *pName) { pMembers->Add(pName); // Добавить OnJoin(new ChatEventArg(pName)); } void QuitChat(String *pName) { pMembers->Remove(pName); // Удалить OnQuit(new ChatEventArg(pName)); } void ShowMemtoers(String *pMsg) { Console::WriteLine(" {0} ", pMsg}; IEnumerator *plter = pMembers->GetEnumerator(); while (p!ter->MoveNext()) { String *pMember = dynamic_cast((pIter->Current) Console::WriteLine(pMember);
Поначалу может показаться, что здесь немалый объем вспомогательного кода, но этот дход намного подход намного про проще, чем прежний — механизм точек стыковки 24 , — реализованный для событий в СОМ.
Резюме В этой главе рассмотрены некоторые важные связи управляемого C++ и .NET Framework, причем начали мы с базового класса O b j e c t (Объект). Мы рассмотрели использование коллекций, в частности, те методы класса O b j e c t (Объект), которые следует переопределять для работы с коллекциями. Очень подробно мы обсудили концепцию интерфейсов, позволяющую разработчику строго определять свойства, которые должны быть реализованы в классе. Хотя класс в управляемом C++ может иметь только один базовый класс, он может реализовывать несколько интерфейсов. Другим достоинством интерфейсов является то, что они значительно облегчают создание динамичных программ. Управляемый C++ обеспечивает возможность во время выполнения программы послать запрос классу для выяснения, поддерживает ли он определенный интерфейс. 2 4
Тонка стыковки (connection point) — механизм OLE, состоящий из объекта, инициирующего интерфейс, и объекта, реализующего функции интерфейса. — Прим. ред.
Резюме
181
Мы подробно рассмотрели интерфейсы, используемые для работы с коллекциями, и виды копирования объектов. В обычном C++ для копирования объектов используются специальные языковые средства — конструкторы копирования, а в управляемом C++ те же возможности обеспечиваются реализацией особого интерфейса I C l o n e a b l e . В итоге мы пришли к изучению роли родовых интерфейсов в методологии программирования .NET Framework и сравнению использования компонентов .NET и СОМ. Использование родовых интерфейсов также проиллюстрировано на примере сортировки коллекций с помощью интерфейса iComparable. Соответствующие примеры позволили полнее ощутить отличие каркаса приложений от простой библиотеки классов. При использовании каркаса приложений программа может вызывать методы каркаса, а те могут вызывать методы программы. Поэтому создаваемый код можно уподобить среднему слою сандвича. Этот пример помогает понять, для чего необходима платформа .NET. А в конце главы рассмотрено использование делегатов и событий. С этой целью были представлены два простых примера: моделирование фондовой биржи и комната для дискуссий (чат-программа).
182
Глава 5. Управляемый C++ в .NET Framework
-•"Глава-в
Создание графических пользовательских интерфейсов
сожалению, конструктор форм (Forms Designer) не поддерживается в C++. Тем не менее, вы можете использовать конструктор форм (Forms Designer) в С#, и потом перенести полученный с помощью С# код графического интерфейса пользователя в программу на C++. Для переноса графического интерфейса пользователя в программу на C++ необходимы дополнительные усилия, и, в большинстве случаев, такой перенос особой пользы не дает. Как правило, C++ не используют для разработки пользовательских графических интерфейсов, вместо этого применяется подход смешения языков, в котором для создания пользовательского интерфейса используется С#, а в других аспектах разработки проекта из разных соображений используется C++. Поскольку вам, скорее всего, придется часто создавать графический интерфейс пользователя для многих приложений, в этой главе мы преследуем две цели. Во-первых, мы хотим научить вас совместно использовать код на С# и C++. Даже если вы в основном программируете на C++, стоит ознакомиться с С# и конструктором форм (Forms Designer), — у вас появится возможность использовать те мощные инструментальные программные средства, которые не поддерживаются в C++. Во-вторых, мы приведем в пример один из немногих случаев, когда перенос кода графического интерфейса пользователя из С# в программу на C++ является целесообразным. В главе представлено несколько пар примеров кода графических пользовательских интерфейсов до и после переноса. Ключевым средством взаимодействия пользователя с компьютером является графический пользовательский интерфейс (Graphical User Interface, GUI). Из этой главы вы узнаете, как создавать графический пользовательский интерфейс с помошью классов Windows Forms (Формы Windows), которые находятся в .NET Framework. На практике программирование Windows-приложений предполагает экстенсивное использование различных инструментальных средств и мастеров, которые намного упрощают этот процесс. Однако все указанные средства автоматизации заслоняют то, что лежит в основе создания графического пользовательского интерфейса. Поэтому сначала мы рассмотрим основы создания графических пользовательских интерфейсов. Иными словами, мы научимся создавать простые приложения Windows с самого начала, пользуясь только комплексом инструментальных средств разработки программ .NET Framework SDK. Это значит, что вначале мы будем создавать простые приложения Windows без применения каких-либо специальных сервисных программ. Будут рассмотрены основы рисования с помощью Windows Forms (Формы Windows) с применением шрифтов и кистей, а также необходимые обработчики событий. Мы объясним принципы обработки событий в Win-
dows Forms (Формы Windows) и реализуем обработчики событий мыши. С помощью Windows Forms (Формы Windows) мы также реализуем меню и соответствующие обработчики событий. Кроме того, мы рассмотрим управляющие элементы, а после этого изучим среду Visual Studio.NET, посредством которой можно без труда создать простой графический пользовательский интерфейс на С#. С помощью конструктора форм (Forms Designer) добавим в форму управляющие элементы, создадим меню, добавим обработчики событий и другие полезные функциональные возможности. При желании полученный в результате проект на С# можно потом перенести на C++. В заключение будут рассмотрены диалоговые окна и такой элемент управления, как сиисок.
Иерархия Windows Forms (Формы Windows) Windows Forms (Формы Windows) — это та часть каркаса .NET Framework, которая поддерживает создание приложений со стандартным графическим пользовательским интерфейсом (GUI) на платформе Windows. Среди классов Windows Forms (Формы Windows) есть обширный набор классов для создания сложных графических пользовательских интерфейсов. Эти классы можно использовать в приложениях, написанных на любом языке .NET. Control
RichControl
ScrollableControl
ContainerControl
Form
UserControl
MyForm Рис. 6.1. Упрощенная схема иерархии классов Windows Forms (Формы Windows)
184
Глава 6. Создание графических пользовательских интерфейсов
Как правило, ваше приложение будет содержать главное окно, которое реализовано с помощью некоторого класса MyForm, производного от класса Form (Форма). На рис. 6.1 изображено, какое место ваш класс MyForm занимает в иерархии классов Windows Forms (Формы Windows).
Создание простых форм с помощью комплекса инструментальных средств разработки программ .NET SDK Для ознакомления с классами Windows Forms (Формы Windows) полезно будет создать простое приложение SimpleForm (Простая форма) в несколько шагов. Ни на одном из этих шагов мы не будем использовать средства проектирования Visual Studio. Используя интерфейс командной строки, необходимо запустить командный файл b u i l d . b a t .
Шаг О: Создание простой формы Приложение SimpleForm (Простая форма) — скелет стандартного приложения Windows. Вот код приложения SimpleForm (Простая форма), созданный на шаге 0: //SimpleForm.срр - Шаг 0 // Эта версия отображает простую форму (simple form) #using #using #using #using
// Система // Система
using namespace System; // использование пространства имен Система; using namespace System::Windows::Forms; // использование пространства имен Система::Windows::Формы; gc class Forml : public Form // класс сборщика мусора Forml: общедоступная Форма { public: Forml() { Size = // Размер *_ nogc new System::Drawing::Size(300,200); // Размер Text = "Simple Form - Step 0"; // Текст = "Простая Форма - Шаг 0"; } static void Main() { Application::Run(new Forml); // Приложение:: Выполнить (новая Forml); int
stdcall WinMain( long hlnstance, // дескриптор текущего экземпляра
Создание простых форм с помощью инструментальных средств...
185
long hPrevInstance, // дескриптор предыдущего экземпляра long lpCmdLine, // командная строка i n t nCmdShow // состояние отображения Forml::Main{); return 0; }
Класс Forml является производным от System: :Windows: : Forms: :Form (Система::Windows::Формы::Форма). В классе System: :Windows: : Forms : a p p l i c a t i o n (Система:^^о\У5::Формы::Приложение) есть статические методы для управления приложением, например Run (Выполнить) и Exit (Выход). Метод WinMain создает новую форму и запускает ее в качестве главного окна приложения. Обратите внимание, что в примерах этой главы, написанных на C++, вместо имени функции main (главная) в качестве точки входа используется WinMain. В принципе можно в функции main (главная) в рамках консольного приложения реализовать все возможности графического интерфейса пользователя. Но при этом подходе придется создать бездействующее консольное окно, что в приложениях с графическим пользовательским интерфейсом совсем ни к чему. Если же использовать WinMain вместо main (главная), то в программе не создаются консольные окна, а сразу создается главное окно. Конструктор формы инициализирует форму. Значение в поле S i z e (Размер) определяет размер формы в пикселях. Поле Text (Текст) определяет заголовок, который отображается в области заголовка окна новой формы. Ключевым классом Windows Forms (Формы Windows) является базовый класс Form (Форма). Этот класс содержит обширный набор функций, которые наследуются разрабатываемыми нами классами форм, производными от класса Form (Форма). Чтобы создать приложение, нужно выполнить из командной строки командный файл b u i l d . b a t . А чтобы запустить командный файл, откройте окно DOS, перейдите в папку SimpleForm\StepO, и введите в командной строке b u i l d (компоновка). Помните, что перед этим необходимо правильно установить значения переменных среды. Для этого достаточно выполнить Visual Studio.NET Command Prompt. c l /CLR SimpleForm.cpp По умолчанию будет откомпилирован исполняемый файл Windows. В исходном коде приложения находятся директивы fusing, в которых указаны используемые библиотеки .NET: S y s t e m . d l l , S y s t e m . D r a w i n g . d l l и System.Windows.Forms.dll. После того, как вы откомпилировали приложение с помощью командного файла, можете запустить его, введя в командной строке SimpleForm (Простая форма). Вы также можете запустить приложение в проводнике Windows, дважды щелкнув на файле SimpleForm.exe. На рис. 6.2 изображен внешний вид этого простого приложения. И хотя приложение SimpleForm (Простая форма) совсем примитивное, в нем заложено множество возможностей, унаследованных созданным нами классом, который является производным от класса Form (Форма). Окно приложения можно перетаскивать по экрану, изменять его размер, свертывать, развертывать, в нем можно открывать системное меню (щелкнув кнопкой мыши в верхнем левом углу окна) и т.д. Сообщения о работе окна В Visual Studio.NET есть инструментальное средство под названием Spy++ (Шпион++). Эта программа "шпионит" за окнами, чтобы иметь представление о том, что происходит 186
Глава 6. Создание графических пользовательских интерфейсов
внутри окна. Чтобы в Visual Studio запустить Spy++ (Шпион++), нужно воспользоваться меню Tools (Сервис). Запустите версию приложения SimpleForm.exe, полученную на шаге 0, а затем запустите Spy++ (Шпион++). Выберите Spy (Шпион)^>Find Window (Найти окно) — появится диалоговое окно Find Window (Найти окно). В этом диалоговом окне установите переключатель Messages (Сообщения), как на рис. 6.3.
Рис. 6.2. Скелет приложения Forms (Формы Windows) (Шаг 0)
Windows
Find Window -Window Finder -
Drag the Finder Tool over a window to select • it, then release the mouse button. Or enter a j window handle (in hexadecimal I Finder Tool:
Г" HkieSpy-M-
OK Cancel Help
Handle; Caption: Class: Rect:
Show I C ; Properties
.;•
(* Messages1
Рис. 6.З. Инструмент Finder Tool (Средство поиска) служит для поиска окна — объекта шпионажа Левой кнопкой мыши перетащите инструмент Finder Tool (Средство поиска) (в диалоговом окне Find window (Найти окно) этот инструмент отображается в виде специальной пиктограммы — перекрестия) на окно приложения SimpleForm (Простая форма), а потом щелкните на кнопке ОК. Теперь в окно программы-шпиона Spy++ будут выводиться сообщения, информирующие обо всех взаимодействиях с окном. Окно программы-шпиона Spy++ показано на рис. 6.4.
Создание простых форм с помощью инструментальных средств...
187
Чтобы обрабатывать события, приложения Windows должны иметь специальную структуру. Операционная система Windows в ответ на действие пользователя, например щелчок кнопкой мыши, выбор меню или ввод символов с клавиатуры, посылает приложению сообщение. Приложения Windows должны иметь такую структуру, которая позволяет реагировать на эти сообщения. Удобство создания Windows-программ с помощью классов .NET Framework состоит в том, что программировать можно на очень высоком уровне абстракции. На шаге 0 вы уже убедились, насколько просто создать приложение. В последующих разделах мы будем добавлять в приложение новые основные свойства графических пользовательских интерфейсов, и таким образом проиллюстрируем основы создания графических пользовательских интерфейсов с помощью классов Windows Forms (Формы Windows).
Шаг 1: Отображение текста на форме В приложении, созданном на первом шаге, будет показано, как отобразить на форме текст. На рис. 6.5 можно увидеть, как выглядит это приложение при выполнении. on Messages (Window ОООЕогаС) ОООЕ020С R WMJJCNITTEST rHktest.HTCLIEfJT 00CE020CSWMJ4CHITTE9TxPos'214yPos-121 O0OEQ20C R WMJJCNITTEST nHiltesfHTCLIENT 00OE020C S WM_SETCURSOR hwnd:000E020C nHit!est:HTCU£NT wM«JseMsg:WMJrfOUSEMOVE 000E02rcRWM_SETCURSORIHaltRocessing:0 000E02GC P WM J^OUSEMOVE lwKeys:0000 xPos:18G yPos.7S 000E020CSWMJJCH!TTESTxPos:234yPos.121 000E02OCRWMJJCHITTESTnHittest:HTCUENT 0Q0E02XS WMJJCNITTEST KPos:234yPos:121 GOOE020C R WMJJCHITTEST nHittestHTCLIENT G00E02OC S WM_SETCURSOR hwnd OOOE020C nHittett.HTCLIENT wMouseMsgWMJ-mUSEMOVE OOGE020C R WM_SETCURSOR fHaltProcessing О 000E020CPWMJH0U9EM0VEfwKeys.0000KPos'206yPos:7B 000EO20CS WMJJCNITTEST иРоя256уРо*:121 OGOE020C R WMJJCHITTEST nH*test:HTCUENT (2S912>000E02CCSWM NCHITTEST KPos:25EyPos:121 OOOEO20C R WMJICHITTEST nHKtestHTCUENT O00E020C S WM_SETCURSOR hwr>J.0Cl0E020C nHhestHTCLIENT wMouseM*g:WMJ^OUSEMDVE COOE020CRWM_SETCURSORIHaltPiocessing:0 OOOE020CPWM_MOUSEMOVEIwf;eysOOOOnPos:228vPo5:76 OOOE020CPWM MOUSELEAVE
Рис. 6.4. Окно Messages (Сообщения) программы Spy++ (Шпион++)
Рис. 6.5. Отображение текста на простой форме (Шаг 1)
188
Глава 6. Создание
пользовательских интерфейсов
/ Вывод данных в программах Windows сильно отличается от вывода данных в *"!>•>. ^ с", аналогичных консольных приложениях, где для этого используется метод i-КОД.'*!1- C o n s o l e : :WriteLine. Вычерчивание результата в окне часто называют закрашиванием или закраской. Закрашивание выполняется в ответ на сообщение "раскрасить", WM_PAINT. Такой способ закрашивания по требованию гарантирует, что если окно будет накрыто каким-либо другим окном, а затем открыто снова, то содержимое окна будет отображено корректно. Еше одно отличие выполнения вывода в Windows-программах от выполнения вывода в консольных приложениях состоит в том, что необходимо определить все детали. Например, нужно указать координаты области рисования, "кисть", которой будет выполняться рисование, шрифт текста, и так далее. Вот код приложения, созданного на шаге 1. //SimpleForm.cpp - Шаг 1 // Эта версия отображает приветствие #using
#using #using #using using namespace System; // использование пространства имен Система; using namespace System::Windows::Forms; // использование пространства имен Система::Windows::Формы; using namespace System::Drawing; // использование пространства имен Система::Рисование; gc class Forml : public Form // класс сборщика мусора Forml: общедоступная Форма { private: // частный float x, у; // с плавающей точкой Brush *pStdBrush; // Кисть public: Forml() { Size = // Размер * nogc new System::Drawing::Size(300,200); // Размер Text = "Simple Form - Step 1"; // Текст = "Простая Форма - Шаг 1"; х = у = 10 ; pStdBrush - new SolidBrush(Color::Black); // Красить:: // Черным } protected: // защищенный virtual void OnPaint(PaintEventArgs * ppea) { ppea->get Graphics{)->Graphics::DrawString // Графика ("Hello, Window Forms", Font, // "Привет, Формы Window ", Шрифт, pStdBrush, x, у ) ; } public: Создание простых форм с помощью инструментальных средств...
189
static void Main() { Application::Run(new Forml); // Приложение:: Выполнить (новая Forml);
i n t _ _ s t d c a l l WinMain( long hlnstance, // дескриптор текущего экземпляра long hPrevInstance, // дескриптор предыдущего экземпляра long ipCradLine, // командная строка i n t nCmdShow // состояние отображения ) f Forml::Main(); r e t u r n 0; } Для того чтобы рисовать с помощью Windows Forms (Формы Windows), нужно переопределить виртуальный метод OnPaint. Класс PaintEventArgs содержит объект Graphics в качестве свойства, доступного только для чтения. Класс Graphics, который принадлежит пространству имен System: : Drawing (Система::Рисунок), содержит методы рисования. Параметры метода Drawstring: • выводимая строка; • шрифт (Font (Шрифт) — свойство класса Form (Форма), которое определяет шрифт, по умолчанию применяемый для вывода текста в форму); • используемая кисть; • координаты в пикселях (числа типа f l o a t (с плавающей точкой)). В качестве стандартной кисти используется черная кисть SolidBrush.
Обработка событий в Windows Forms (Формы windows) Графический пользовательский интерфейс (GUI) управляется событиями: приложение выполняет действия в ответ на события, вызванные пользователем, например, на щелчок кнопкой мыши или выбор пункта меню. Каждая форма или элемент управления имеет заранее определенный набор событий. Например, у каждой формы есть код, обрабатывающий событие MouseDown (Кнопка мыши нажата). 1 В Windows Forms (Формы Windows) применяется модель обработки событий .NET , в которой делегаты используются для того, чтобы связать события с обрабатывающими их методами. В классах Windows Forms (Формы Windows) используются групповые делегаты. Групповой делегат содержит список связанных с ним методов. Когда в приложении происходит событие, управляющий элемент возбуждает событие, вызвав делегат для этого события. Потом делегат вызывает связанные с ним методы.
1
Если хотите, обратитесь к главе 5 "Управляемый C++ в .NET Framework1', где рассмотрены делегаты и события.
190
Глава 6. Создание графических пользовательских интерфейсов
Для того чтобы добавить делегат к событию, в C++ используется перегруженный оператор +=. Мы добавляем метод Forml_MouseDown к событию MouseDown (Кнопка мыши нажата): MouseDown += new MouseEventHandler ( t h i s , Forml^MouseDown); Вскоре мы увидим этот код в программе.
Документация по обработке событий Документацию, касающуюся событий и их обработки, можно найти в справочнике по .NET Framework (.NET Framework Reference). На рис. 6.6 показаны предопределенные события, связанные с классом Form (Форма).
Событие MouseDown (Кнопка мыши нажата) Событие MouseDown (Кнопка мыши нажата) является одним из предопределенных событий класса Control (Элемент управления), от которого порожден класс Form (Форма). public: event MouseEventHandler* MouseDown; А вот и объявление обработчика этого события, MouseEventHandler: public gc delegate void MouseEventHandler( Object* sender, // отправитель MouseEventArgs* e
* ЛВ1 Framework SDK Documentation - Events
m sew 1«ь {».' .fiSTframeteori- CISii Ubmy Farm Svents
Framework SDt Documentato in
• *?] EroP irowder Csss + &j Fesrursrp jpcrt Ca l.. ?" ^j Fe liDa ln lg ca lss •£ Fa lt5tye l Enumerao itn :
Tne events of the Fori conipletE list of Form uliAe!:-: topic. Public Instance Events
^] Fom r Membes r is] Fom r Consrtuco tr '•£ ^) Propere tis >, *. Meh tods
1
Occur; when the f o r n is activated in coda or by the
R.',!-'"- \r.-' ->-.,:,•,: h ahe of (inherited trom Control) t p perty it cl-s o-d
S^j Form.ControC l oe l cto i nCa l ss fj FormeorderStye l Enumersto in •^ Porm5tartPostion Eru i me-ato in j£ FormWn idowState Enumerato in У FrameStyle Eniiniaation Button = MouseButtons::Left) // если левая кнопка x = pmea->X; у = pmea->Y; else if (pmea->Button == MouseButtons::Right) // если правая // кнопка pStr = new StringBuilder(); Invalidate();
Событие Keypress (Нажатие клавиши) На шаге 3 мы научимся обрабатывать событие KeyPress (Нажатие клавиши). Каждый раз, когда пользователь нажмет клавишу, в конец строки приветствия будет добавлен соответствующий символ. Обратите внимание, что вместо класса S t r i n g (Строка) используется класс S t r i n g B u i l d e r , который более эффективен в этой ситуации. Объект S t r i n g (Строка) — стационарный (неизменяемый), то есть, для того, чтобы реализовать добавление символов в конец строки, нужно постоянно удалять одни объекты S t r i n g (Строка) и создавать другие. StringBuilder *pStr;
void Forml_KeyPress (Object *pSender, KeyPressEventArgs *pmea) pStr->Append(pmea->KeyChar); // Добавляем в конец Invalidate(); Так же, как и на шаге 2, необходимо вызвать метод i n v a l i d a t e (Считать недействительным), для того, чтобы принудительно перерисовать окно приложения после сделанных изменений. На рис. 6.8 показано окно приложения SimpleForm (Простая форма), после удаления исходного текста и ввода нового.
Рис. 6.8. Испытываем события мыши и нажатия клавиши на клавиатуре (Шаг 3) Обработка событий в Windows Forms (Формы Windows)
195
Меню Все пользователи Windows-приложений хорошо знакомы с меню, которые представляют собой простой механизм выбора команд. В языках .NET меню реализуется в самой программе. Иными словами, для меню файл ресурсов не нужен.
Шаг 4: Меню для выхода из программы На шаге 4 мы добавим в наше приложение SimpleForm простое меню. Для того чтобы выйти из программы, пользователь должен выбрать (Файл'ФВыход), как на рис. 6.9.
Рис. 6.9. Шаг 4: Добавление в форму меню File&Exit
Код меню //SimpleForm.срр - Шаг 4 gc class Forml : public Form // класс сборщика мусора Forml: общедоступная Форма { private: // частный void InitializeComponent() { pMainMenul = new MainMenu (); pMenuFile = new Menultem (); pMenuExit = new Menultem (); // mainMenul Menultem* pMainMenulItems[] = {pMenuFile}; pMainMenul->get Menulterns() ->AddRange(pMainMenulItems); // Меню File pMenuFile->set_Index{O); Menultem* pMainFileltems[] = {pMenuExit}; pMenuFile->get_MenuItems() ->AddRange(pMainFileltems); pMenuFile->set Text{"File"); // Файл
196
Глава 6. Создание графических пользовательских интерфейсов
// Меню Exit pMenuExit->set Index(0); pMenuExit->set_Text("Exit"); // Выход pMenuExit->Click += new System::EventHandler // Щелчок (this, MenuExit_Click); Menu = pMainMenul; // Меню MouseDown += new MouseEventHandler (this, Forml_MouseDown); KeyPress += new KeyPressEventHandler (this, Forml_KeyPress); } float x, у; // с плавающей точкой Brush *pStdBrush; // Кисть StringBuilder *pStr; Menultem *pMenuExit; Menultem *pMenuFile; MainMenu *pMainMenul; public: private: // частный void MenuExit_Click.{ Object *pSender, EventArgs *pea) { Application::Exit(); // Приложение:: Выход В методе I n i t i a l i z e C o m p o n e n t создается иерархическая структура меню, представленная экземпляром класса MainMenu (Главное меню). Меню состоит из объектов Menultem, каждый из которых является отдельной командой меню. Каждый объект Menultem является командой приложения или командой родительского меню для других пунктов подменю. В нашем приложении мы связываем объект MainMenu (Главное меню) с объектом Form (Форма), присваивая свойству Menu (Меню) объекта Form (Форма) значение MainMenu (Главное меню). Когда в этой главе мы позже обсудим конструктор форм (Forms Designer), вы увидите, что меню можно создать и так: нужно просто перетянуть элемент управления MainMenu (Главное меню) с панели инструментов на форму. Конструктор форм (Forms Designer) позаботится о генерации нужного шаблонного кода.
Код события Menu (Меню) Как и в случае других событий Windows Forms (Формы Windows), с событием связывается его делегат. Щелчок на пункте меню приводит к выполнению соответствующей команды. void InitializeComponent() { pMenuExit->ClicJt += new System::EventHandler // Щелчок (this, MenuExit Click);
Меню
197
void MenuExit_Click( Object *pSender, EventArgs *pea) { Application::Exit(); // Приложение::Выход
Управляющие элементы В программе, которую мы только что рассматривали, объект pMainMenul является управляющим элементом. Данный объект— указатель на экземпляр класса MainMenu (Главное меню). Управляющий элемент — это объект на форме, который придает форме новые функциональные возможности. Управляющий элемент автоматически выполняет много заданий "по поручению" формы. Использование управляющих элементов упрощает программирование, так как программисту не надо беспокоиться о рисовании, о том, следует ли считать представление формы недействительным, не нужно думать о графических элементах и так далее. Для того чтобы реализовать простое меню с самого начала, нам пришлось написать довольно много кода. Управляющие элементы, которые реализуются с помощью объектов, содержат богатый, вполне пригодный для повторного использования код.
Шаг 5: Использование управляющего элемента TextBox (Поле) На шаге 5 создания приложения SimpleForm {Простая форма) мы используем управляющий элемент TextBox (Поле) для отображения строки с приветствием. В более ранних версиях приложения строку можно было переместить щелчком левой кнопки мыши и удалить щелчком правой кнопки мыши. Можно было также ввести свою собственную строку с приветствием. Теперь, применив управляющий элемент TextBox (Поле),'вы получите полноценные возможности редактирования. Управляющий элемент TextBox (Поле) позволяет в любом месте строки вставить символы, вырезать и вставить текст (с помощью комбинаций клавиш Ctrl+X и Ctrl+V соответственно) и так далее. Все возможности редактирования поддерживаются управляющим элементом TextBox (Поле). На рис. 6.10 изображено окно приложения после того, как текст приветствия был перемещен, и мы ввели некий собственный текст. Это новая версия программы. Обратите внимание на то, что она значительно проще предыдущей, хотя и имеет гораздо более богатые функциональные возможности. Нет больше необходимости использовать переменные экземпляра для координат и текста строки приветствия (теперь эта информация хранится в управляющем элементе p T x t G r e e t i n g типа TextBox (Поле)). Не нужен больше метод OnPaint, так как управляющий элемент TextBox (Поле) знает, как нарисовать себя. Можно также избавиться от кисти. Теперь не нужно обрабатывать событие Keypress (Нажатие клавиши), потому что оно автоматически обрабатывается управляющим элементом TextBox (Поле), притом весьма аккуратно.
198
Глава 6. Создание графических пользовательских интерфейсов
If "Щ Simple FormFile
j in texl(
Рис. 6.10. Шаг 4: текст приветствия отображается с помощью управляющего элемента TextBox (Поле) //SimpleForm.cpp - Шаг 5 gc class Forml : public Form // класс сборщика мусора Forml: общедоступная Форма { private: // частный void InitializeComponent() // текст приветствия (text greeting) pTxtGreeting = new TextBox; pTxtGreeting->Location = // Местоположение * nogc new Point(10, 10); // новая точка pTxtGreeting->Size = // Размер (* nogc new struct Size (150, 20)); // новый Размер pTxtGreeting->Text = "Hello, Windows Forms"; // Текст = "Привет, Формы Windows"; Controls->Add(pTxtGreeting); // Добавить } float x, у; // с плавающей точкой Brush *pStdBrush; // Кисть Menultem *pMenuExit; Menultem *pMenuFile; MainMenu *pMainMenul; TextBox *pTxtGreeting; protected: // защищенный void Forml_MouseDown (Object *pSender, MouseEventArgs *pmea) { if (pmea->Button == MouseButtons::Left) // если кнопка левая
Управляющие элементы
199
p T x t G r e e t i n g - > L o c a t i o n = // Местоположение *__nogc new Point(pmea->X, pmea->Y); // новая точка ) e l s e if (pmea->Button == M o u s e B u t t o n s : : R i g h t ) // если кнопка правая { p T x t G r e e t i n g - > T e x t = " " ; // Текст
Управляющий элемент TextBox (Поле) удобен в использовании. В инициализирующей части программы мы создаем объект TextBox (Поле) и определяем значения его свойств L o c a t i o n (Местоположение), S i z e (Размер) и Text (Текст). Мы добавляем новый управляющий элемент к коллекции управляющих элементов C o n t r o l s (Управляющие элементы) этой формы. В обработчике событий мыши мы перемещаем управляющий элемент, изменив значение свойства L o c a t i o n (Местоположение). С помощью свойства Text (Текст) управляющего элемента TextBox (Поле) можно удалить строку с приветствием.
Visual Studio.NET и формы И хотя вполне реально создать приложение Windows Forms (Формы Windows), используя в командной строке только комплекс инструментальных средств разработки программ .NET Framework SDK, на практике подобную работу намного проще выполнить с помощью Visual Studio.NET. К сожалению, в Visual Studio.NET нет средств для генерирования проекта пусковой системы на управляемом C++ на основе Form (Форма), и управляемый C++ не поддерживает конструктор форм (Forms Designer). Однако для начала можно создать проект Windows-приложения на С# (Windows Application). При этом будет сгенерирован код пусковой системы и будут установлены ссылки на необходимые библиотеки .NET. Затем можно в конструкторе форм (Forms Designer) перетащить управляющие элементы с инструментальной панели на вашу форму. Конструктор форм (Forms Designer) вставит необходимый шаблон кода на С#, который поддерживает функционирование этих управляющих элементов в форме. В окне Properties (Свойства) несложно определить свойства управляющего элемента в процессе проектирования. Можно, конечно, определить эти свойства и во время запуска приложения, как мы это сделали для поля p T x t G r e e t i n g в предыдущем примере. После этого можно перенести код С# в программу на управляемом C++, но этого обычно не рекомендуется делать.
Демонстрация Windows Forms (Формы Windows) Лучший способ научиться создавать приложения Windows с помощью Visual Studio.NET— самостоятельно с самого начала создать небольшое приложение на С#. Для примера мы создадим Windows-приложение, которое позволит вносить деньги на счет и снимать деньги со счета в банке. 1. Создайте на С# новый проект Windows Application (Windows-приложение), как на рис. 6.11, и назовите его BankGui.
200
Глава 6. Создание графических пользовательских интерфейсов
mSSrf -г JEf Temnlawtr
Project Тури г i
l
1 v.su^Lasct'rc;:ts : jj WwaK# Project* yg| Visual C++ Projects iU Setup and Depo l yment Projects ••*': fifl Other Projects
— .
—•ч Class Library
*
Windows Control l -Ьэгу
Щ Visual Studio Solutions
ftSP.NET Web ASP.NET Web Web Comci Application Service Lbra-v ; A project For crea№g an appficatton wfth a Windons war rtw : .e Маяе:
| Bankiw
Location: Protect пй be created at C:f
J
_ i I L L
Puc.6.11. Создание проекта Windows Application (Windowsприложение) 2. Раскройте панель инструментов Toolbox, перетянув указатель мыши на вертикальную вкладку Toolbox в левой части главного окна Visual Studio. Если вкладки нет, инструментальную панель Toolbox можно открыть из меню ViewOTooIbox (ВидОПанель инструментов). Чтобы панель инструментов Toolbox оставалась открытой, щелкните на "канцелярской кнопке", которая находится в заголовке панели инструментов Toolbox рядом с X. Если курсор мыши навести на "канцелярскую кнопку', появится подсказка с надписью "Auto Hide" (Автоматическое свертывание). 3. Перетащите из панели инструментов Toolbox две надписи (Label), два поля (TextBox) и две кнопки (Button) на форму (рис. 6.12).
Components^ Vfn l dowsRT jM i 1^ Pon i ter ALAel
:
Д LJr*Labef
label' labeL
•bj Button . [•s TextBox S i ManMenu (7 Chocteox {? RadJoButton
Рис. 6. /2. Перетаскивание управляющих элементов с панели инструментов Toolbox () на форму 4. В конструкторе форм (Forms Designer) щелкните на надписи l a b e l l . Тем самым вы вьщелите этот управляющий элемент в окне Properties (Свойства), которое находится Visual Studio.NET и формы
201
под Solution Explorer (Поиск решения). Окно Properties (Свойства) позволяет изменять свойства управляющих элементов. В поле свойства Text (Текст) объекта l a b e l l введите Amount (Сумма). После того, как вы ввели значение, нажмите возврат каретки. Вы увидите, что текст появится на форме. На рис. 6.13 показано окно Properties (Свойства) после изменения свойства Text (Текст) первой надписи.
I labell System,Window*.Forms.Labs
. Modifiers RightToLeft B;5ize Tablndex Tag
Рис. 6,13. Изменение значений свойств в окне Properties (Свойства) 5. Точно так же измените текст надписи 1аЬе12 на Balance (Баланс). 6. Введите значения свойств полей и кнопок в соответствии с табл. 6.1. 7. С помощью маркеров размера, которые находятся посредине каждой стороны формы, измените ее размер. При желании, перетащите управляющие элементы на выбранные места, и измените их размер. Если внешний вид формы вас удовлетворяет, сохраните изменения, сделанные в проекте. Ваша форма должна выглядеть примерно так, как на рис. 6.14. 8. Добавьте обработчики событий кнопок, дважды щелкнув на каждой кнопке. 9. Добавьте необходимый код к коду, сгенерированному мастером: Таблица 6.1. Значения свойств полей (Textbox) и кнопок (Button) Имя свойства
Текст
txtAmount
(не заполняется)
txtBalance
(не заполняется)
cmdDeposit
Deposit (Вклад)
and Withdraw
Withdraw (Снять)
202
Глава 6. Создание графических пользовательских интерфейсов
• • Amount
. . Balance • • '
Deposit
Wfthdraw
Рис. 6.14. Форма приложения BankGui public class Forml : System.Windows.Forms.Form // общедоступный класс Forml:Система.Windows.Формы.Форма public Forml() // Требуется для поддержки Windows Form Designer InitializeComponent(); // TODO: Добавьте любой код конструктора после // вызова InitializeComponent // txtAmount.Text = "25"; // Текст txtBalance.Text = "100"; // Текст /// The main entry point for the application. /// Основная точка входа для приложения. /// [STAThread] static void Main() { A p p l i c a t i o n . R u n ( n e w Forml ( ) ) ; private void cmdDeposit_Click(object sender, System.EventArgs e) { int amount = Convert.Tolnt32(txtAmcunt.Text); int balance = Convert.Tolnt32 (txtBalance.Text) ; // баланс balance += amount; // баланс + - количество; Visual Studio.NET и формы
203
txtBalance.Text = Convert.ToString(balance); // Текст private void cmdWithdraw_Click(object sender, System.EventArgs e) { int amount = Convert.Tolnt32(txtAmount.Text); int balance = Convert.Tolnt32(txtBalance.Text); // баланс balance -= amount; txtBalance.Text = Convert.ToString(balance); // Текст } 10. Откомпилируйте и выполните приложение. Оно должно вести себя как стандартное приложение Windows. Вы должны без проблем вносить деньги на счет и снимать деньги со счета. На рис. 6.15 показано выполняющееся приложение BankGui.
Рис. 6.15. Windows-приложение BankGui В данный момент проект реализован на С#. И хотя этого, как правило, не делают, мы перенесем этот проект в C++ для того, чтобы показать, как это нужно делать. Сначала с помощью шаблона Managed C++ Empty Project (Пустой проект на управляемом C++) создадим новый проект C++, который назовем BankGuiPort. Теперь создадим исходный файл Forml.cpp в проекте BankGuiPort и перенесем (с помощью команд сору (копировать) и paste (вставить)) код С# из исходного файла Forml. cs проекта BankGui. Перенесите все строки кода из файла Forml. cs проекта BankGui в файл Forml.cpp проекта BankGuiPort. При таком переносе кода могут возникнуть проблемы и непредвиденные ситуации. Эти проблемы не будут рассмотрены в нашей книге и вам придется ознакомиться с ними самостоятельно, если вы и в дальнейшем захотите выполнять подобный перенос кода. Итак, откройте оба проекта — BankGui наС# и BankGuiPort на C++ — в двух копиях Visual Studio.NET и визуально сравните исходные файлы Forml. cs и Forml.cpp, чтобы получить представление о подробностях переноса кода. //Forml.срр #using 204
Глава 6. Создание графических пользовательских интерфейсов
#using #using #using using namespace System; // использование пространства имен Система; namespace BankGui // пространство имен BankGui { gc class Forml : public System::Windows::Forms::Form // класс сборщика мусора Forml: общедоступная Система:: // Windows:: Формы:: Форма
private: // частный System::Windows: Forms System::Windows: Forms System::Windows: Forms System::Windows: Forms System::Windows: Forms System::Windows: Forms System:;ComponentModel public: Forml()
:Label *labell; :Label *label2; :TextBox *txtAmount; : TextBox *txtBalance; :Button *cmdDeposit; // Кнопка :Button *cmdWithdraw; // Кнопка :Container * components; // Контейнер
components = 0 ; // компоненты InitializeComponent(); txtAmount->Text = "25"; // Текст txtBalance->Text = "100"; // Текст private: // частный void InitializeComponent() cmdWithdraw = new System::Windows::Forms::Button; // Кнопка cmdDeposit = new System::Windows::Forms::Button; // Кнопка txtBalance = new System::Windows::Forms::TextBox; txtAmount = new System::Windows::Forms::TextBox; labell = new System::Windows::Forms::Label; // Надпись Iabel2 = new System::Windows::Forms::Label; // Надпись SuspendLayout(); // cmdWithdraw cmdWithdraw->Location = // Местоположение * nogc new System::Drawing::Point(152, 144}; // Точка cmdWithdraw->Name = "cmdWithdraw"; // Имя cmdWithdraw->TabIndex = 2; cmdWithdraw->Text = "Withdraw"; // Текст = "Снять" cmdWithdraw->Click += // Щелчок
Visual Studio.NET и формы
205
new System::EventHandler(this, cmdWithdraw_Click) // Forml // AutoScaleBaseSize = * nogc new System::Drawing::Size(5, 13); // Размер ClientSize = * nogc new System::Drawing::Size(280, 189); // Размер System: :Windows::Forms::Control* pi terns[] = { cmdDeposit, txtAmount, labell, Iabel2, txtBalance, cmdWithdraw} ; Controls->AddRange(plteras); "Name = "Forml"; // Имя Text = "Forml"; // Текст Load +- new System::EventHandler(this, Forml_Load) ResumeLayout(false); // ложь } void Forml_Load( Object *sender, System::EventArgs *e)
void cmdWithdraw_Click( Object *sender, System::EventArgs *e) { int amount = Convert::ToInt32(txtAmount->Text); // преобразование текста int balance = Convert::ToInt32{txtBalance->Text) // преобразование текста balance -= amount; // -количество txtBalance->Text = Convert::ToString(balance); // преобразование в текст public: [STAThread] static void Main() { System: :Windows::Forms: application: :Run(new Forml) // Приложение:: Выполнить (новая Forml);
206
Глава 6. Создание графических пользовательских интерфейсов
Окно конструктора (Design window) и окно кода (Code window) Для работы с проектами Windows Forms (Формы Windows) в Visual Studio очень важно научиться переключаться между окном конструктора (Design window), где вы работаете с управляющими элементами на форме, и окном кода (Code window), где вы работаете с кодом программы. Мы можем показать это на примере двух окон проекта VsForm на С#, код стартовой системы этого проекта находится в папке VsForm\Stepl главной папки данной гла-1 вы. Версия этого проекта, перенесенная из С# на C++, находится в папке VsFormPort\Stepl. Это первая версия проекта стартовой системы, которая отображает одну и ту же строку приветствия. Проекты, отвечающие разным стадиям разработки, последовательно пронумерованы, и каждой версии проекта на С# (они содержатся в папке VsForm), созданной с помошью конструктора форм (Forms Designer), соответствует перенесенная версия проекта на C++, которая содержится в папке VsFormPort. Если дважды щелкнуть на файле Forml. cs проекта VsForml\Stepl в окне Solution Explorer (Поиск решения), то файл будет открыт в окне конструктора (Design window), как на рис. 6.16. Для того чтобы появилось окно кода (Code window), щелкните на кнопке View Code (Просмотреть код), находящейся на инструментальной панели. Таким образом вы откроете исходный код, и вверху главной области окна вы увидите горизонтально расположенные ярлыки, с помощью которых можно выбирать нужные окна. В данный момент для этой формы открыты оба окна, — и окно конструктора (Design window), и окно кода (Code window). Можно без труда вернуться в окно конструктора (Design window), щелкнув на кнопке View Designer (Открыть окно Design), находящейся на инструментальной панели. На рис. 6.17 вы можете увидеть внешний вид окна кода (Code window). Vsf w m - hScrosoft visual C#.NET [ d e s n n l - Forml.свГйезйвД
j»j
Ш _и"° j
-Qutptft
256.20В
Seewip Style
ДиЮ
~? X
"tartFosltion
iVindo'4:Defaul'Lt;c
— ~ \
Tsg
^ . j
_3
Тея
VsForm - Step 1 - J
T'pMost
False
_J
Text The text contained h the osnttol.
Яос. 6.16. Окно конструктора (Design window) в проекте Windows Forms (Формы Windows)
Visual Studio.NET и формы
207
добавление события 1. Скомпонуйте и выполните программы (стартовые системы) на С# и C++, находящиеся в папках VsForm\Stepl и VsFormPort\Stepl, и убедитесь, что они работают одинаково. Это полностью статические приложения, — они просто отображают строку приветствия в фиксированной позиции. 2. Откройте форму проекта VsForm\Stepl в окне конструктора (Design window) и щелкните на кнопке Events (События) в окне Properties (Свойства). 3. Найдите событие MouseDown (Кнопка мыши нажата), как на рис. 6.18. 4. В окне Properties (Свойства) дважды щелкните на событии MouseDcwn (Кнопка мыши нажата). Автоматически будет сгенерирован код, который зарегистрирует делегата для события и образует скелет метода, связанного с делегатом. m - M«roi«ft ViMifll C*J4ET [design] - T a m i l . » L t
*•>
л
- U f J
V c t pre;? dices i t э Eiait-leftchi, •.
.«, :cnut.^r, ViFurr..- (1 i I FJ < P VsForm i ff= ^ References i
аз ng System as S3 System us r.g System из System us System "a «I "* —•
Dtauing; Collections; Comp one tic Mode 1; Hindasia. Fotras;
OUUU,
. 6.77. Окно АГО^Й (Code window) в проекте Windows Forms (Формы Windows)
private void InitializeComponent() this.MouseDown += new System.WinForms.MouseEventHandler ( t h i s . F o r m l MouseDown);
protected void Forml_MouseDown (object sender, System.WinForms.MouseEventArgs e)
208
Глава 6. Создание графических пользовательских интерфейсов
Properties Forml System,Windows.Forms.Form j»J Ai MdiChildActivate MenuComplete MenuStart Minimum5i2eChan( MouseDown MouseEnter MouseHover
MouseDown
Occurs when э mou^p bLtto1". is pressed. |
Рис. 6.18. Добавление события с помощью кнопки Events (События)
Код обработчика события 1. Чтобы установить координаты строки приветствия, добавьте код в обработчик события мыши (нажатие кнопки мыши). Не забудьте после этого вызвать метод I n v a l i d a t e (Считать недействительным)! p r o t e c t e d void Forml_MouseDown ( o b j e c t s e n d e r , System.WinForms.MouseEventArgs e) {
x = e.X; у = e.Y;
Invalidate() ; } 2. Скомпонуйте и выполните проект. Теперь по щелчку мыши (любой кнопкой) приветствие должно перемешаться. Проект сейчас находится на шаге 2 разработки и соответствует проекту, хранящемуся в папке VsForra\Step2. Вместо того, чтобы переносить каждую строчку кода, созданного на С#, в файл Forml.cpp проекта VsForm\Step2, просто сделайте копию проекта VsFormPort\Stepl, который уже получен с помощью переноса кода. Потом перенесите несколько строчек кода, связанных с событием MouseDown (Кнопка мыши нажата) из VsForm\Step2. void InitializeComponent{) MouseDown += new System::Windows::Forms::MouseEventHandler ( t h i s , Forml MouseDown);
Visual Studio.NET и формы
209
void Forml_MouseDown (Object *sender, System::Windows::Forms::MouseEventArgs *e) { x = (float)e->X; // с плавающей точкой у = (float)e->Y; // с плавающей точкой Invalidate();
Использование управляющего элемента Menu (Меню) 3. Откройте панель инструментов Toolbox, если она до сих пор еще не открыта (щелкните на ярлыке панели инструментов Toolbox в вертикальной линейке) и перетащите управляющий элемент MainMenu (Главное меню) на форму приложения. 4. Для создания выпадающего меню F i l e (Файл) с пунктом E x i t (Выход), введите F i l e (Файл) и E x i t (Выход), как на рис. 6.19. ОЛ*_
f ^
1 t X
Л
,_;
filei,.„ij:,^>.:f-J-
Pointer
Д UnHabel '
Д
ЦЧ^.||'
,
MJ,
j£j Button («и TaxtBox ' I
f7 в первоначальном исходном тексте (Stdafx.h). Если бы другой оператор #using добавлялся для другой сборки, например #using , то декларация содержала бы также соответствующую инструкцию зависимости .assembly e x t e r n System.WinForms. f SimpleComponentdft - IL DASM File View Help £ ;rr,pleCcrrpcnen: dl • MANIFEST
Olxl
-Щ SimpleComponent &-Щ: SomeClass • i - • .class public auto ansi ; • Щ .dor. void[|
AddEmUp int32|int32jnt32} inl32|void*,unsigned int32 modopt([mscorlib <
л Рис. 7.1. lldasm. exe показывает содержимое SimpleComponent .dll Инструкция метаданных . p u b l i c k e y t o k e n = (B7 7A 5C 56 19 34 E0 89 ) указывает общедоступную лексему (маркер) открытого ключа, являющуюся хэш-кодом открытого ключа, который ставится в соответствие своему секретному ключу, принадлежащему автору сборки m s c o r l i b . Эта лексема открытого ключа на самом деле не может использоваться непосредственно, чтобы подтвердить подлинность автора m s c o r l i b . Однако первоначальный открытый ключ, указанный в декларации m s c o r l i b может ис230
Глава 7. Сборки и развертывание
пользоваться для того, чтобы математически проверить, что секретный ключ на самом деле совпадает с тем, который действительно применялся при цифровом подписании сборки m s c o r l i b . Поскольку m s c o r l i b . d l l создала Microsoft, лексема открытого ключа, приведенная выше, принадлежит Microsoft. Конечно, соответствующий секретный ключ — тщательно охраняемая корпоративная тайна, и, как полагает большинство экспертов в области зашиты, такой секретный ключ практически очень трудно определить по открытому ключу. Однако нет никакой гарантии, что некий математический гений не найдет когда-нибудь хитроумный способ делать это проще! Как мы вскоре увидим, инструкция . p u b l i c keyto ken присутствует в декларации клиентской сборки только в случае, когда сборка, на которую есть ссылка, имеет цифровую подпись. (На самом деле все сборки, предназначенные для общедоступного развертывания, должны иметь цифровую подпись.) Microsoft подписала в цифровой форме стандартные сборки .NET, такие как rascorlib.dll и System.WinForms.dll принадлежащими ей секретными ключами. Именно поэтому лексема открытого ключа для многих общедоступных сборок, содержащихся в каталоге \WINNT\Assembly, имеет то же самое повторяющееся значение. Создаваемые другими производителями сборки с цифровой подписью подписаны их собственными, отличными от приведенного выше, секретными ключами, и они будут иметь отличную от приведенной выше лексему открытого ключа в декларациях их клиентской сборки. Позже вы научитесь создавать ваши собственные криптографические пары секретного и открытого ключа и сможете подписывать собственные сборки цифровой подписью для их развертывания через глобальный кэш сборок. f MANIFEST
.assembly extern mscorlib -publickeytoken = (B7 7A 5C 56 19 34 EO 89 )
.hash « (09 BB ВС 09 EF 6D 9B F4 F2 CC 1B 55 7 22 88 EF 77 ) -uer 1:0:2411:0 assembly extern Microsoft.UisualC .publickeytoken = (BO 3F 5F 7F 11 D5 0Й 3ft ) .hash = (4A D5 1A 11 0Й 17 DO E3 6D 69 68 80 D ВЙ 79 FA DO ) .uer 7:0:9254:59748 assembly SimpleComponent .custom .custom -custom .custom .custom .custom
instance instance instance instance instance instance
uoid uoid uoid uoid uoid uoid
[mscorlibJSystem [mscorlibJSystem [mscorlib]System [mscorlib]System [mscorlib]System [mscorlibJSystem
Reflect Reflect Reflect Reflect Reflect Reflect
Рис. 7.2. Ildasm. exe показывает декларацию SimpleComponent. dll
Сборки
231
Декларация
.publiekeytbken
Чтобы сэкономить память, декларация . p u b l i c k e y t o k e n содержит только самые младшие 8 байтов хэш-кода открытого ключа производителя (он состоит из 128 байтов), вычисленного с помощью алгоритма SHA1. Однако, несмотря на это, она все же может использоваться для довольно надежной проверки. А вот декларация . p u b l i c k e y содержит полный открытый ключ. Конечно, она занимает больше места, но именно поэтому злодеям труднее найти секретный ключ, который соответствует полному открытому ключу. Важно обратить внимание, что, хотя цифровой ключ уникален, он сам по себе не может идентифицировать фактического автора конкретного модуля. Однако разработчик сборки может использовать утилиту s i g n c o d e , чтобы добавить цифровое свидетельство, которое может идентифицировать издателя сборки. А если зарегистрировать цифровое свидетельство у Certificate Authority (Полномочного свидетеля)1, например у VeriSign, то пользователи смогут установить надежность источника. Инструкция метаданных .hash = (09 ВВ ВС 09 . . . 77 ) обеспечивает фиксированный размер представления хэш-кода двоичного содержимого m s c o r l i b . d l l . Если бы содержимое изменилось, в результате изменился бы и этот хэш-код. Несмотря на то, что хэшкод имеет компактное представление, он с высокой вероятностью характеризует сборку. Поэтому вычисленный на основе первоначальных данных, он может использоваться для многих целей, включая обнаружение ошибок и проверку. Хэш-код для сборки m s c o r l i b , показанной выше, с высокой вероятностью характеризует двоичные данные сборки m s c o r l i b . Это означает, что, если бы содержимое m s c o r l i b . d l l было изменено случайно, преднамеренно, или даже злонамеренно, то, с астрономически высокой вероятностью, новый хэш-код не совпадал бы со старым, и изменение было бы обнаружено по хэш-коду. Как описано дапее в разделе по цифровому подписанию сборок, секретный ключ используется для того, чтобы гарантировать, что только уполномоченный человек может зашифровать хэш-код, и это используется для подтверждения (проверки) подлинности всей сборки. Инструкция метаданных . ver 1:0:2411:0 указывает версию сборки mscorlib. Формат спецификации этой версии— Major:Minor:Build:Revision (Главный:Младший: Компоновка:Пересмотр). Через какое-то время, когда будут выпущены новые версии этой сборки, существующие клиенты, которые были скомпонованы так, чтобы использовать данную версию, продолжат использовать именно данную версию, по крайней мере те версии, у которых совпадают значения главного и младшего номера версий. Более новые клиентские программы, конечно, смогут обратиться к более новым версиям этой сборки, поскольку именно для них станут доступными новые версии. Старые и новые версии могут быть развернуты буквально рядом посредством кэша глобальных сборок, и быть одновременно доступны старым и новым клиентским программам. Обратите внимание, что версия 1:0: 2411: 0, появляющаяся в клиентской декларации, принадлежит текущей версии сборки m s c o r l i b и не связана с атрибутом версии 1.0.*, указанным в исходном тексте SimpleComponent. Вскоре мы более подробно рассмотрим четыре поля, которые составляют номер версии, а также управление версиями сборки.
Certificate Authority (CA, Полномочный свидетель) — доверенная третья сторона, которая проверяет мандаты лица, поставившего цифровую подпись и издает уникальное цифровое свидетельство о тождестве. Свидетельство служит доказательством, что подписывающее лицо действительно является тем, за кого он или она себя выдает. Это полезно в ситуациях, когда две стороны хотели бы заняться коммерцией друг с другом, поскольку в таком случае нет необходимости полностью знать друг друга заранее.
232
Глава 7. Сборки и развертывание
До сих пор мы сосредотачивались на зависимостях, которые определены в декларации сборки SimpleComponent. Теперь давайте подробнее рассмотрим информацию в декларации, описывающую компонент SimpleComponent, содержащийся в сборке. Обратите внимание, что эта сборка не имеет цифровой подписи, и поэтому не содержит информации о ее создателе (т. е., открытый ключ она в себе не содержит). .assembly SimpleComponent
-hash a l g o r i t h m 0x00008004 -ver 1:0:584:39032
Директива . a s s e m b l y Директива .assembly объявляет декларацию и определяет, какой сборке принадлежит текущий модуль. В данном примере директива . assembly определяет SimpleComponent в качестве имени сборки. Именно это имя (вместе с номером версии и, возможно, открытым ключом), а не имя динамически подключаемой библиотеки (DLL) или исполняемого файла, используется во время выполнения для определения принадлежности сборки. Обратите также внимание, что, если сборка подписана, то в директиве .assembly будет определен параметр .publickey. Директива .assembly также указывает, добавлялись ли каюге-либо пользовательские атрибуты к метаданным. Инструкция метаданных . a s s e m b l y SimpleComponent указывает, что имя сборки — SimpleComponent. Имейте в виду, что это — не имя класса компонента в сборке, а само имя сборки. Алгоритмы хэширования Алгоритм хэширования — математическая функция, которая берет первоначальные входные данные произвольной длины и генерирует хэш-код, также известный как профиль сообщения, который представляет собой двоичный результат установленной длины. Эффективная хэш-функция— односторонняя (однонаправленная) функция, при использовании которой коллизии возникают очень редко, а ее результат имеет относительно маленькую установленную длину. Идеальная хэщ-функция также легко вычислима. Односторонняя функция— функция, не имеющая такой обратной, с помощью которой можно было бы фактически быстро вычислить первоначальные данные по значению хэш-кода2. Фраза "коллизии возникают очень редко" означает, что вероятность того, что по двум первоначально различным входным данным будет сгенерирован тот же самый хэш-код, является очень маленькой, и мала вероятность вычисления двух отличающихся входных данных, которые приводят к тому же самому значению хэш-кода. Известные алгоритмы хэширования MD5 и SHA1, как полагают, являются превосходным выбором для использования в цифровом подписании, и оба они поддерживаются в .NET.
2 Коды для одностороннего кодирования используются для того, чтобы сохранять пароли в базе данных паролей. Когда вы входите, вводимый вами пароль шифруется и потом сравнивается с тем, что хранится в базе данных. Если они совпадают, вы можете войти. Пароль не может быть восстановлен по зашифрованному значению, хранящемуся в базе данных паролей.
Сборки
233
Инструкция .ver 1:0:584:39032 указывает окончательную версию сборки SimpleComponent, которая определена частично атрибутом AssemblyVersionAttribute в исходном тексте компонента. Управление версиями более подробно описано в следующем подразделе.
Управление версиями сборки Как мы только что видели, декларация сборки содержит версию сборки, а также версии каждой из сборок, от которых она зависит. Детальный набор правил, используемых общеязыковой средой выполнения CLR для того, чтобы определить зависимости версии, называют политикой управления версиями. Заданная по умолчанию политика управления версиями определена зависимостями, указанными в декларациях сборки, но при необходимости ее можно изменить в файле конфигурации приложения или в общесистемном файле конфигурации. Автоматическая проверка версии выполняется общеязыковой средой выполнения CLR только на сборках со строгими именами (то есть, на сборках с цифровой подписью). Однако, каждой сборке, независимо от того, как она развернута, должен быть назначен номер версии. Номер версии сборки состоит из следующих четырех полей. • Главная версия (Major version): Главные несовместимые изменения. • Младшая версия (Minor version): Менее значительные несовместимые изменения. • Номер компоновки (Build number): Обратно совместимые изменения, сделанные входе разработки. • Пересмотр (Revision): Обратно совместимые изменения, сделанные в ходе текущего быстрого исправления (Quick Fix Engineering, QFE). Вышеупомянутые соглашения относительно назначения каждого поля номера версии не предписаны обшеязыковой средой выполнения CLR. Именно программист устанавливает эти или любые другие соглашения при проверке совместимости сборки и определении политики управления версиями в файле конфигурации, который мы обсудим позже в этой главе. Традиционно изменение значения главного или младшего номера указывает явную несовместимость с предыдущей версией. Это используется при существенных изменениях в новом выпуске сборки, и существующие клиенты не могут использовать новую версию. Изменения номера компоновки подразумевают совместимость вниз, и этот номер обычно изменяется каждый раз при очередной компоновке сборки в ходе разработки. Совместимость вниз между номерами компоновки является намеренной; однако, этого, очевидно, не может гарантировать общеязыковая среда выполнения CLR, и потому данное свойство должно быть проверено. Изменение номера пересмотра относится к изменениям, сделанным в ходе текущего быстрого исправления (Quick Fix Engineering, QFE). Это поле обычно используется для срочного исправления, которое общеязыковой средой выполнения CLR считается обратно совместимым, если в файле конфигурации не указано иное назначение этого поля. И снова, общеязыковая среда выполнения CLR не может гарантировать, что изменение, сделанное в ходе текущего быстрого исправления (Quick Fix Engineering, QFE), на 100 процентов обратно совместимо, но совместимость вниз желательна и должна быть тщательно проверена. Информация, относящаяся к версии, может быть определена в исходном тексте, в атри6yre__assembly: : AssemblyVersionAttribute. Класс AssemblyVersionAttribute определен в пространстве имен System: :Runtime: : C o m p i l e r S e r v i c e s (Система:: Время выполнения::Сотр!1ег5епчсе5). Если ЭТОТ атрибут не используется, в декларации 234
Глава 7. Сборки и развертывание
сборки по умолчанию задается номер версии 0.0.0.0, который, вообще говоря, является признаком небрежности. В проекте, созданном Мастером проектов на управляемом C++ на основе Библиотеки классов (managed C++ Class Library project wizard), исходный файл Assemblylnfo .cpp автоматически генерируется с версией 1.0.*, т.е. главная версия равна 1, младшая версия — 0, причем значения пересмотра и компоновки генерируются автоматически. Если изменить AssemblyVersionAttribute на, например, " 1 . 1 . 0 . 0 " , как показано ниже, то номер версии, отображенный в декларации, изменится, и будет равен 1 : 1 : 0 : 0 . //Assemblylnfo.cpp #using
[__assembly::AssemblyVersionAttribute("1.1.0.0")]; Чтобы не указывать значений пересмотра и компоновки, можно использовать символ звездочка (*). Когда вы вообще определяете какой-либо номер версии, вы должны, как минимум, определить главный номер. Если вы определяете только главный номер, остающиеся значения будут по умолчанию иметь значение нуль. Если вы определяете также младшее значение, то можете опустить оставшиеся поля, которые по умолчанию будут обнулены, или можете указать звездочку, тогда значения будут сгенерированы автоматически. Звездочка в данном случае означает, что значение компоновки будет равняться количеству дней, прошедших с 1 января 2000 года, а значение пересмотра будет установлено равным количеству секунд, прошедших с полуночи, деленному на 2. Если вы определяете значения главного и младшего номеров, а также номера компоновки, причем указываете звездочку для значения пересмотра, то только номер пересмотра будет равен количеству секунд, прошедшему с полуночи, деленному на 2. Когда все четыре поля указаны явно, все четыре значения будут отражены в декларации. Следующие примеры показывают правильные (допустимые) спецификации версии. Определено в исходном тексте
Записано в декларации
Ни одно поле
0:0:0:0
1
1:0:0:0
1.1
1:1:0:0
1.1.*
1:1:464:27461
1.1.43
1:1:43:0
1.1.43.*
1:1:43:29832
1.1.43.52
1:1:43:52
Если указать звездочку, то версия автоматически б.удет изменяться каждый раз при компоновке компонента; однако каждая новая версия считается обратно совместимой, так как главные и младшие номера не изменяются автоматически. Чтобы определить новую обратно несовместимую версию, вы должны явно изменить главный и/или младший номер версии.
Сборки
235
Частное развертывание сборки Частное развертывание сборки просто означает, что конечный пользователь копирует сборку в тот же самый каталог, что и клиентская программа, использующая ее. Не нужна никакая регистрация, и не требуется никакая причудливая инсталляционная программа. Кроме того, не требуется никакая очистка системного реестра, не нужна также и никакая программа деинсталляции для удаления компонента. Чтобы деинсталлировать сборку, просто удалите ее с жесткого диска. Конечно, ни один программист из самоуважения никогда не поставит коммерческий компонент, который конечный пользователь должен вручную копировать или удалять какие-либо файлы подобным способом, даже если сделать это очень просто. Пользователи привыкли использовать формальную инсталляционную программу, так что она должна поставляться, даже если ее работа тривиальна. Однако, ручное копирование и удаление сборки — идеальный способ быстрого и безболезненного управления развертыванием реализаций при разработке, тестировании, отладке и испытании. Как вы помните, развертывание компонентов, построенных на основе модели компонентных объектов Microsoft (COM), никогда не было настолько просто. Ведь для развертывания требовался как минимум файл сценария, чтобы занести в системный реестр информацию о компоненте к клиентским программам и о среде времени выполнения модели компонентных объектов Microsoft (СОМ). С появлением сборок не нужно при инсталляции конфигурировать системный реестр, а, значит, позже, когда вы захотите отказаться от компонента, не нужно заботиться о том, чтобы тщательно стереть информацию из системного реестра. Далее мы рассмотрим, как выполнить частное развертывание сборки для простого .NET-компонента. В следующем разделе будет показано, как развернуть общедоступную сборку в глобальном кэше сборок.
" VЛ
Скомпоновав компонент сборки, можно создать клиентскую программу, которая вызывает общедоступные методы компонента. Следующий код показывает пример такой клиентской программы, вызывающей общедоступный метод AddEmUp, приведенный ранее. Конечно, в случае необходимости, путь к сборке SimpleComponent.dll в инструкции #using должен быть откорректирован, чтобы компилятор смог найти нужные данные. Кроме того, сборку нужно развернуть, чтобы общеязыковая среда выполнения CLR могла определить местонахождение сборки и загрузить ее во время выполнения. В этом подразделе развертывание достигается простым копированием сборки в каталог клиентской программы.
//SimpleComponentClient.срр #include "stdafx.h" #using using namespace System; // использование пространства имен Система; #using #using using namespace SimpleCoragpnent; // использование пространства имен SimpleComponent; void main() { SoraeClass *psc = new SomeClass; int sum = psc->AddEmUp(3, 4); // суммировать Console::WriteLine(sum); // сумма
236
Глава 7. Сборки и развертывание
Как найти SimpleComponent? Обратите внимание, что программа SimpleComponentclient имеет инструкцию # u s i n g , указывающую компилятору, где найти сборку SimpleComponent, метаданные которой компилятор использует для контроля соответствия типов. Однако, если вы попробуете, выполнить клиентскую программу, то возникнет исключение System. 1 0 . F i l e N o t F o u n d E x c e p t i o n . Так получится потому, что загрузчик класса общеязыковой среды выполнения CLR неспособен найти сборку SimpleComponent. Чтобы выполнить клиент, нужно только скопировать сборку SimpleComponent в тот же самый каталог, где находится программа SimpleComponentclient.ехе. Теперь можно рассмотреть и сравнить декларацию сборки этой клиентской программы, чтобы увидеть, как она взаимодействует с декларацией сборки SimpleComponent, приведенной ранее. Чтобы рассмотреть декларацию клиентской программы, используйте команду Ildasra S i m p l e C o m p o n e n t c l i e n t . e x e Можно заметить, что декларация клиентской программы содержит следующую внешнюю зависимость от сборки SimpleComponent. .assembly e x t e r n SimpleComponent {
. h a s h = (2A 1С 2D D7 CA 9E 7E D5 08 5B DO 75 23 D3 50 76 5E 2 8 EA 31 ) .ver 1:0:584:39032 }
Из этого следует, что клиентская программа ожидает использовать сборку SimpleComponent с номером версии 1 : 0 : 5 8 4 : 3 9 0 3 2 . Однако, поскольку сборка развернута частным образом, на самом деле, когда клиент загружает эту сборку, принятая по умолчанию в обшеязыковой среде выполнения CLR политика проверки значения версии проверку номера не выполняет. Просто ответственность за то, что в данном конкретном каталоге развернута нужная версия, возлагается на администратора или инсталляционную программу. Эту заданную по умолчанию политику проверки версии можно отменить, используя файл конфигурации. Хотя в клиентской декларации имеется хэш-код компонента, он фактически не используется общеязыковой средой выполнения CLR для того, чтобы проверить подлинность двоичного кода. И снова это происходит потому, что сборка развернута частным образом. В следующем разделе мы увидим, что номер версии и хэш-код используются автоматически для проверки содержимого кода общедоступных сборок, развернутых в глобальном кэше сборок.
Общедоступное развертывание сборки Кэш сборки — средство параллельного ("бок о бок") развертывания (инсталляции) компонентов на всей машине. Термин "бок о бок" означает, что множество версий того же самого компонента могут постоянно находиться в кэше сборок рядом друг с другом. Глобальный кэш сборок содержит общедоступные сборки, которые являются глобально доступными для всех .NET-приложений на машине. Есть также кэш загружаемых сбо-
Общедоступное развертывание сборки
237
рок, доступный для приложений типа Internet Explorer, которые автоматически загружают сборки по сети. Чтобы развернуть сборку в глобальном кэше сборок, нужно создать для нее строгое имя.
Строгие имена Гарантируется, что строгое имя будет глобально уникальным для любой версии любой сборки. Строгие имена генерируются тогда, когда сборка получает цифровую подпись. Это гарантирует, что строгое имя не только уникально, но и может быть сгенерировано только индивидуумом, который имеет секретный ключ. Строгое имя состоит из простого текстового имени, открытого ключа и хэш-кода, зашифрованного соответствующим секретным ключом. Хэш-код также называется профилем сообщения, а зашифрованный хэш-код— цифровой подписью, электронной подписью и цифровой сигнатурой. Хэш-код фактически эффективно идентифицирует двоичное содержимое сборки, а цифровая сигнатура (подпись) фактически эффективно идентифицирует автора сборки. Все сборки, имеющие одно и то же строгое имя, считаются идентичными (при определении идентичности во внимание принимаются также номера версии). Сборки, строгие имена которых отличаются друг от друга, считаются различными. Полагают, что строгое имя является криптографически стойким, поскольку в противоположность простому текстовому имени, оно однозначно определит сборку на основании ее содержимого и секретного ключа ее автора. Строгое имя имеет следующие полезные свойства: • гарантирует уникальность, основанную на технологии кодирования; • устанавливает уникальное пространство имен, основанное на использовании секретного ключа; • препятствует неправомочному персоналу изменять сборку; • не позволяет неправомочному персоналу управлять версиями сборки; • позволяет общеязыковой среде выполнения CLR выполнять проверку содержимого кода общедоступных сборок.
Цифровые сигнатуры (подписи) Если сборка должна быть развернута в глобальном кэше сборок, то необходимо, чтобы она имела цифровую подпись. Цифровая подпись (сигнатура) не требуется и не особенно полезна для сборки, развернутой частным образом, так как частная сборка развертывается пользователем для того, чтобы работать со своей определенной клиентской программой и потому согласована с ней. Если даже развернутая частным образом сборка имеет цифровую подпись, общеязыковая среда выполнения CLR по умолчанию не проверяет этого, когда загружает сборку для клиентской программы. Поэтому ответственность за предотвращение неправомочной модификации или подтасовки развернутых частным образом сборок полностью возложена на администратора. С другой стороны, очень выгодно использовать общедоступно развернутые (т.е. общедоступные) сборки, так как они должны быть подписаны в цифровой форме, и потому обычно их используют многие клиенты, причем рядом могут существовать несколько версий одной сборки. Цифровые подписи (сигнатуры) основаны на криптографических методах, в которых применяются открытые ключи. В мире криптографии применяется два основных криптографических метода — симметричные шифры (общий ключ) и асимметричные шифры (открытый ключ). Симметричные шифры совместно используют секретный ключ как 238
Глава 7. Сборки и развертывание
для кодирования, так и для расшифровки. Стандарт шифрования данных DES (Data Encryption Standard), Triple DES (Тройной стандарт шифрования данных) и RC2 — примеры симметричных алгоритмов шифрования. Симметричные шифры могут быть очень эффективными и мощными в обеспечении секретности сообщения между двумя доверяющими друг другу сотрудничающими лицами, но они не совсем подходят для ситуаций, где трудно совместно использовать секретный ключ. По этой причине симметричные шифры считаются неподходящими для цифровых подписей (сигнатур). Именно потому цифровые подписи (сигнатуры) используются не для секретности, а лишь для идентификации и опознавания, что является более открытым делом. Если бы вы совместно использовали ваш симметричный ключ с каждым, кто потенциально хотел бы убедиться в подлинности ваших сборок, то по неосторожности могли доверить его людям, которым захотелось исполнить вашу роль. Для использования в цифровых подписях (сигнатурах) намного лучше подходят асимметричные шифры. Асимметричные шифры, которые также называются шифрами с открытым ключом, используют криптографическую пару открытого и секретного ключа. Ключи в паре математически связаны, и генерируются они вместе; однако чрезвычайно трудно вычислить один ключ по другому. Обычно открытый ключ выдается каждому, кто хотел бы убедиться в подлинности владельца сборки. С другой стороны, владелец сохраняет соответствующий секретный ключ подписи в тайне, чтобы никто не мог подделать его подпись. Метод шифрования по схеме открытого ключа RSA (RSA-кодирование) — пример системы шифрования с открытым ключом. Шифрование с открытым ключом основано на очень интересной математической схеме, которая позволяет зашифровать обычный текст одним ключом, а расшифровать, только зная ключ, соответствующий исходному. Например, когда открытый ключ используется, чтобы зашифровать первоначальные данные (называемые открытым текстом), только соответствующий секретный ключ может помочь в расшифровке этого текста. Даже ключ, который использовался для шифрования, не поможет в расшифровке зашифрованного текста! Этот сценарий полезен тогда, когда секретные сообщения посылаются только человеку, который знает секретный ключ. Рассмотрим теперь противоположный сценарий. Человек, который знает секретный ключ, использует его для того, чтобы зашифровать открытый текст. Получающийся зашифрованный текст не является тайной, так как каждый заинтересованный может получить открытый ключ, чтобы расшифровать данный текст. Этот сценарий бесполезен для сохранения тайны, но очень эффективен для опознавательных целей. Нет необходимости шифровать исходные данные полностью, поэтому, чтобы повысить эффективность, вместо них шифруется компактный хэш-код, который с высокой вероятностью характерен для исходных данных. Если вы получаете файл, который содержит зашифрованную версию его собственного хэш-кода, и расшифровываете его с помощью соответствующего открытого ключа, то фактически вы повторно вычислите хэш-код исходных данных. И если теперь вы обнаружите, что он совпадает с хэшкодом, который был зашифрован, можете быть совершенно уверены, что именно владелец секретного ключа приложил цифровую подпись и данные не были изменены другими лицами. Если предположить, что владелец сумел сохранить тайну секретного ключа, тогда совпадение результатов вычисления доказывает, что никто не смог бы исказить файл после того, как он был подписан в цифровой форме. На рис. 7.3 показано, как работает цифровая подпись (сигнатура).
Общедоступное развертывание сборки
239
Подписанная сборка
Неподписанная сборка
Вычисление хэшкода алгоритмом SHA1
Профиль сообщения
Sn.exe
I Секретный ключ
( Открытый ключ
Метод шифрования по схеме открытого ключа RSA
Зашифрованный хэш-код /
Рис, 7.3. Вот так работает цифровая подпись (сигнатура) Цифровое подписание методом шифрования по схеме открытого ключа RSA и SHA1 Чтобы подписать сборку, производитель вычисляет с помощью алгоритма SHA1 ее хэшкод (причем байты, зарезервированные для подписи (сигнатуры), предварительно обнуляются), и затем зашифровывает значение хэш-функции с помощью секретного ключа, используя метод шифрования по схеме открытого ключа RSA (RSA-кодирование). Открытый ключ и зашифрованный хэш-код сохраняются в метаданных сборки.
Цифровая подпись и развертывание общедоступной сборки Чтобы развернуть сборку в глобальном кэше сборок, надо, чтобы она имела цифровую подпись. Разработчики могут разместить сборку в глобальном кэше сборок, используя утилиту G a c u t i l . exe (Global Assembly Cache utility), Проводник Windows (Windows Explorer) с расширением оболочки Windows посредством просмотра кэша сборок, или
240
Глава 7. Сборки и развертывание
Инструмент администрирования .NET (.NET Admin Tool). Инсталляция общедоступных сборок на конкретной машине конечного пользователя должна быть сделана с помощью программы установки компонентов (системы) по выбору пользователя. Процесс цифрового подписания сборки включает генерацию криптографической пары открытого и секретного ключа, вычисление хэш-кода сборки, шифровку хэш-кода с помощью секретного ключа, и запись в сборку зашифрованного хэш-кода вместе с открытым ключом. Зашифрованный хэш-код и открытый ключ вместе составляют полную цифровую сигнатуру (подпись). Обратите внимание, что цифровая подпись (сигнатура) записана в зарезервированную область сборки, не участвующую в вычислении хэш-кода. Когда все это сделано, сборка может быть развернута в глобальном кэше сборок (GAC). Все указанные шаги выполняются тремя простыми инструментальными средствами: утилитой Strong Name (Strong Name utility — Sn. exe), компоновщиком сборок (Assembly Linker — Al. exe) и утилитой Global Assembly Cache (Global Assembly Cache utility — G a c u t i l . e x e ) . Чтобы скомпоновать, подписать в цифровой форме и развертывать общедоступную сборку, необходимо выполнить следующие шаги: 1. Разработать и скомпоновать компонент. 2. Сгенерировать пару открытого и секретного ключей. 3. Вычислить хэш-код содержимого сборки. 4. Зашифровать хэш-код, используя секретный ключ. 5. Поместить зашифрованный хэш-код в декларацию. 6. Поместить открытый ключ в декларацию. 7. Поместить сборку в глобальный кэш сборок. Конечно, шаг 1 обычно выполняется с помощью Visual Studio.NET. Шаги 2 — 6 представляют собой цифровое подписание. Шаг 2 выполняется с помощью утилиты Strong Name— Sn.exe. Шаги 3—6 выполняются с помощью Visual Studio.NET или компоновщика сборок — утилиты Assembly Linking — Al .exe ("1" — это " э л ь " , а не "один"). Чтобы на шаге 7 поместить сборку в глобальный кэш сборок, применяется утилита Global Assembly Cache— G a c u t i l . e x e , Проводник Windows (Windows Explorer), или Инструмент администрирования .NET (.NET Admin Tool). Сейчас мы опишем первый шаг — создание компонента. Для целей демонстрации мы используем пример, подобный предыдущему примеру сборки SimpleComponent, но он называется SharedComponent, и будет развернут в глобальном кэше сборок. Сначала должен быть создан новый проект SharedComponent; для этого используется шаблон Managed C++- Class Library (Управляемый C++ на основе Библиотеки классов), причем нужно добавить следующий код: //SharedComponent.ерр #include "stdafx.h" // имеет #using #include- "SharedComponent. h" //SharedComponent.h using namespace System; Общедоступное развертывание сборки
241
// использование пространства имен Система; namespace SharedComponent // пространство имен SharedComponent {
public
gc class SomeClass
// класс сборщика мусора SomeClass { public: int AddEmUp(int i, int j) { return i+j;
На следующем шаге нужно сгенерировать криптографическую пару. Генерацию криптографической пары можно выполнить с помощью утилиты Sn.exe. Эта утилита известна как утилита Strong Name, но иногда также называется и утилитой Shared Name, однако, этот последний термин теперь осуждается. Данный инструмент генерирует криптографически стойкое имя сборки. Вы генерируете криптографическую пару ключей (открытого и секретного) и размещаете ее в файле KeyPair. snk, как показано в следующей команде. sn -k KeyPair.snk Получающийся двоичный файл KeyPair . snk не предназначен для чтения пользователю. Но если вы любопытны, можете записать эти ключи в текстовый файл, в котором поля разделяются запятыми. Это делается с помощью следующей команды: sn -о KeyPair.snk K e y P a i r . t x t Теперь можете рассмотреть полученный файл с помощью Блокнота (Notepad.exe); однако это не обязательно. На следующем шаге нужно применить секретный ключ к сборке. Это может быть сделано при компиляции, что, конечно, является полезным для разработки и тестирования; однако, когда придет время промышленного выпуска сборки, нужно будет поместить строку с названием компании, да и вообще придется использовать более формальный подход. Для защиты корпоративной цифровой подписи предприятия официальный секретный ключ держится в секрете и потому не может быть сообщен программисту. Вместо него программист может при разработке и проверке (испытании) использовать заменяющую криптографическую пару, которая применяется во время компиляции автоматически. Тогда перед выпуском сборки уполномоченный сотрудник ставит официальную корпоративную цифровую сигнатуру (подпись), используя секретный ключ, строго хранимый в тайне. Это делается после компиляции с помощью инструментального средства А1. ехе. Указанный инструмент будет описан в этом подразделе позже. Однако, чтобы во время компиляции применить цифровую сигнатуру (подпись) автоматически, вы просто используете определенный атрибут C++, как показано в следующем коде. В частности, обратите внимание, что файл KeyPair. snk, сгенерированный предварительно инструментом Sn.exe, определен в атрибуте A s s e m b i y K e y F i l e A t t r i b u t e . //AssemblyInfo.срр ^ i n c l u d e " s t d a f x . h " // имеет #using < m s c o r l i b . d l l > u s i n g namespace S y s t e m : : R e f l e c t i o n ; // использование пространства имен Система: .-Отражение; 242
Глава 7. Сборки и развертывание
u s i n g namespace S y s t e m : : R u n t i m e : : C o m p i l e r S e r v i c e s ; // использование пространства имен // Система::Время выполнения:: C o m p i l e r S e r v i c e s ; [assembly:AssemblyTitieAttribute("")]; [assembly:AssemblyDescriptionAttribute("")]; [assembly:AssemblyConfigurationAttribute("")]; [assembly:AssemblyCompanyAttribute("")]; [assembly:AssemblyProductAttribute("")]; [assembly:AssemblyCopyrightAttribute("")]; [assembly:AssemblyTrademarkAttribute ( " " ) ] ; [assembly:AssemblyCultureAttribute("")]; [assembly:AssemblyVersionAttribute("1.0.*")]; [assembly:AssemblyDelaySignAttribute(false)]; [assembly:AssemblyKeyFileAttribute{"KeyPair.snk")]; [assembly:AssemblyKeyNameAttribute("")]; После добавления файла K e y P a i r . snk к A s s e m b l y K e y F i l e A t t r i b u t e , сборку нужно перетранслировать. Тогда при следующем запуске I l d a s m . exe покажет полученную в результате информацию, которая была включена в декларацию сборки для динамически подключаемой библиотеки (DLL) SharedComponent. Обратите внимание, что новая запись называется . p u b l i c k e y . В этой записи содержится открытый ключ создателя, который находится в файле K e y P a i r . snk. Именно этот открытый ключ может использоваться для расшифровки профиля сообщения, чтобы найти первоначальный хэшкод. Когда сборка развернута в глобальном кэше сборок, этот расшифрованный хэш-код сравнивается с новым, вычисленным по фактическому содержимому сборки, хэш-кодом. Такое сравнение позволяет определить, законна ли сборка (т.е. идентична оригиналу), или незаконна (т.е. разрушена или подделана). Конечно, когда вы используете Sn.exe, эта утилита сгенерирует другую криптографическую пару, и открытый ключ, приведенный ниже, будет отличаться от вашего. . a s s e m b l y SharedComponent {
. p u b l i c k e y = (00 24 00 00 04 80 00 00 94 00 00 00 . . . . . . 56 5А Bl 97 D5 FF 39 5F 42 DF OF 90 7D D4 ) .hash a l g o r i t h m 0x00008004 . v e r 1:0:584:42238
ш
Чтобы проверить разработанную нами сборку SharedComponent, мы должны создать испытательную клиентскую программу, а затем, вместо того, чтобы копировать SharedComponent.dll в каталог клиента, мы применим утилиту G a c u t i l . exe (Global Assembly Cache utility), или Проводник Windows (Windows Explorer), или Инструмент администрирования .NET (.NET Admin Tool), чтобы развернуть сборку в глобальном кэше сборок. Следующий код представляет собой испытательную клиентскую программу. //SharedComponentClient.срр #include "stdafx.h" #using AddEmUp(3, 4 ) ; // суммируем Console : :WriteLine (sum) ; // сумма } Если бы вышеупомянутая испытательная клиентская программа была выполнена до инсталляции компонента-сборки, во время выполнения было бы запушено исключение FileNotFoundException, потому что сборку не удалось бы найти. Но на сей раз мы развертываем сборку в глобальном кэше сборок с помощью утилиты G a c u t i l . e x e (Global Assembly Cache utility). Напомним, это только один из нескольких методов. G a c u t i l - i SharedComponent.dll Затем можно выполнить клиент, причем теперь он должен работать надлежащим образом. I £М C:\WB4NT\Aisembty Ffe
E*
Йюч
Favoritw
Той*
У -"> ТВ-
_J Con.erdon _1] DoCLnentiand S-lt i I - Cj Admn i sitrator Jj Appc ilat™ Da '1 compuls ' i Cooke is •Jj DesHop ^J Favofties 'i FrontPage Ten Jj LocalSetn i gs •^J My Document!
b03f5f7ld50a3a b03f5fTfd l SOa3a bO3f5f7fd l SDa3a bO3f5F7f 1d l 50a3a b03fSf7fd l S0a3a bO3f5f Л1 ld50a3a bO3f5f7flld50aia bO3f 5F7F1 Id50a3a d
Рис. 7.4. Проводник Windows (Windows Explorer), показывает глобальный кэш сборок
После этого вы должны увидеть на консоли сообщение Assembly s u c c e s s f u l l y added to t h e cache (Сборка успешно добавлена в кэш). В результате выполнения приведенной выше команды в каталоге \WlNNT\Assembly был создан новый глобальный узел кэша сборки; имя этого узла — SharedComponent. Как видно на рис. 7.4, номер версии и создатель (т.е. лексема открытого ключа) сборки отображаются в Проводнике Windows (Windows Explorer). Чтобы установить сборку в глобальный кэш сборок (GAC), можно также использовать Проводник Windows (Windows Explorer)— перетащите и опустите компонент в каталог Assembly. Кроме того, чтобы установить сборку в глобальный кэш сборок (GAC), можно ис244
Глава 7. Сборки и развертывание
пользовать Инструмент администрирования .NET (.NET Admin Tool). Инструмент администрирования .NET (.NET Admin Tool) встроен в ММС и расположен в \WINNT\Microsoft. NET\Framework\vl. 0.2 914\mscorcf g .rnsc. Номер версии в каталоге будет отличен в более позднем выпуске .NET Framework. Хотя наличие третьего инструмента может показаться избыточным, эта встроенная в ММС утилита очень полезна, потому что упрощает решение многих задач. На рис. 7.5 показано окно верхнего уровня данного инструмента. Чтобы с его помощью добавить сборку в глобальный кэш сборок (GAC), достаточно всего лишь выбрать Assembly Cache в левой области окна, щелкнуть правой кнопкой мыши, и выбрать Add (Добавить). В появившемся диалоговом окне переместитесь к нужному файлу, выберите сборку, которую хотите добавить, и щелкните на кнопке Open (Открыть). Теперь, когда общедоступная сборка развернута в глобальном кэше сборок, клиентская программа SharedComponentClient может использовать нашу сборку. После того, как вы установили сборку в глобальном кэше сборок (GAC), вы можете переместить клиентскую программу в другой каталог. Вы можете выполнять клиента, причем нет необходимости перемещать сборку, устано1зленную в глобальном кэше сборок (GAC).
Runm ti e Securtiy Poc ily Appc ilato i ns
Tesfts '