2. Каталог, где находится файл CSC.exe. Этот каталог также содержит DLL- библиотеки CLR.
|
| ||||||
| ||||||
|
Джеффри Рихтер
ПРОГРАММИРОВАНИЕ НА ПЛАТФОРМЕ
miciosift.NET FRAMEWORK 4.5
МЯЗЫКЕ C#
С^ППТЕР’
Москва ■ Санкт-Петербург ■ Нижний Новгород ■ Воронеж
Ростов-на-Дону ■ Екатеринбург ■ Самара ■ Новосибирск
Киев ■ Харьков ■ Минск
2013
УДК 004.43 Р55
Рихтер Дж.
Р55 CLR via С#. Программирование на платформе Microsoft .NET Framework 4.5 на языке С#. 4-е изд. — СПб.: Питер, 2013. — 896 с.: ил. — (Серия «Мастер-класс»),
ISBN 978-5-496-00433-6
Эта книга, выходящая в четвертом издании и уже ставшая классическим учебником по программированию, подробно описывает внутреннее устройство и функционирование общеязыковой исполняющей среды (CLR) Microsoft .NET Framework версии 4.5. Написанная признанным экспертом в области программирования Джеффри Рихтером, много лет являющимся консультантом команды разработчиков .NET Framework компании Microsoft, книга научит вас создавать по-настоящему надежные приложения любого вида, в том числе с использованием Microsoft Silverlight, ASP.NET, Windows Presentation Foundation и т. д.
Четвертое издание полностью обновлено в соответствии со спецификацией платформы .NET Framework 4.5, а также среды Visual Studio 2012 и C# 5.0.
12+ (Для детей старше 12 лет. В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-ФЗ.)
ББК 32.973.2-018.1 УДК 004.43
Права на издание получены по соглашению с Microsoft Press. Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книги.
ISBN 978-0735667457 англ. © Authorized Russian translation of the English edition of titled CLR via C#,
4 edition © 2012 Jeffrey Richter (ISBN 9780735667457). This translation is published and sold by permission of O’Reilly Media, Inc., which owns or controls all rights to publish and sell the same.
ISBN 978-5-496-00433-6 © Перевод на русский язык ООО Издательство «Питер», 2013
© Издание на русском языке, оформление ООО Издательство «Питер», 2013
Краткое содержание
Предисловие ............................................................................................... 22
Введение....................................................................................................... 24
ЧАСТЬ I. ОСНОВЫ CLR
Глава 1. Модель выполнения кода в среде CLR......................................... 28
Глава 2. Компоновка, упаковка, развертывание и администрирование приложений и типов 58
Глава 3. Совместно используемые сборки и сборки
со строгим именем....................................................................................... 94
ЧАСТЬ II. ПРОЕКТИРОВАНИЕ ТИПОВ
Глава 4. Основы типов............................................................................... 122
Глава 5. Примитивные, ссылочные и значимые типы ............................ 142
Глава 6. Основные сведения о членах и типах......................................... 186
Глава 7. Константы и поля......................................................................... 210
Глава 8. Методы ......................................................................................... 215
Глава 9. Параметры.................................................................................... 245
Глава 10. Свойства..................................................................................... 263
Глава 11. События...................................................................................... 286
Глава 12. Обобщения.................................................................................. 302
Глава 13. Интерфейсы ............................................................................... 333
ЧАСТЬ III. ОСНОВНЫЕ ТИПЫ ДАННЫХ
Глава 14. Символы, строки и обработка текста........................................ 356
Глава 15. Перечислимые типы и битовые флаги...................................... 403
Глава 16. Массивы...................................................................................... 416
Глава 17. Делегаты ..................................................................................... 434
Глава 18. Настраиваемые атрибуты........................................................... 464
Глава 19. Null-совместимые значимые типы............................................ 485
ЧАСТЬ IV. КЛЮЧЕВЫЕ МЕХАНИЗМЫ
Глава 20. Исключения и управление состоянием..................................... 496
Глава 21. Автоматическое управление памятью
(уборка мусора).......................................................................................... 554
Глава 22. Хостинг CLR и домены приложений.......................................... 606
Глава 23. Загрузка сборок и отражение..................................................... 636
Глава 24. Сериализация............................................................................. 666
Глава 25. Взаимодействие с компонентами WinRT................................... 698
ЧАСТЬ V. МНОГОПОТОЧНОСТЬ________________________
Глава 26. Потоки исполнения..................................................................... 724
Глава 27. Асинхронные вычислительные операции................................. 747
Глава 28. Асинхронные операции ввода-вывода...................................... 787
Глава 29. Примитивные конструкции
синхронизации потоков ............................................................................ 820
Глава 30. Гибридные конструкции синхронизации потоков .. .854 Словарь соответствия русскоязычных
и англоязычных терминов......................................................................... 893
Содержание
Предисловие................................................................................................. 22
Введение....................................................................................................... 24
От издателя перевода.............................................................................. 26
ЧАСТЫ. ОСНОВЫ CLR
Глава 1. Модель выполнения кода в среде CLR....................................... 28
Компиляция исходного кода в управляемые модули .............................. 28
Объединение управляемых модулей в сборку......................................... 32
Загрузка CLR............................................................................................ 34
Исполнение кода сборки.......................................................................... 37
IL-код и верификация............................................................................ 44
Небезопасный код................................................................................ 45
IL и защита интеллектуальной собственности...................................... 46
NGen.exe.................................................................................................. 47
Библиотека FCL ...................................................................................... 47
CTS.......................................................................................................... 49
CLS.......................................................................................................... 52
Взаимодействие с неуправляемым кодом................................................ 57
Глава 2. Компоновка, упаковка, развертывание и администрирование приложений и типов 58
Задачи развертывания в .NET Framework................................................. 58
Компоновка типов в модуль.................................................................... 60
Файл параметров................................................................................. 61
Несколько слов о метаданных................................................................. 64
Объединение модулей для создания сборки........................................... 71
Добавление сборок в проект в среде Visual Studio.............................. 78
Использование утилиты Assembly Linker.............................................. 79
Включение в сборку файлов ресурсов................................................ 81
Ресурсы со сведениями о версии сборки ............................................... 82
Номера версии..................................................................................... 86
Региональные стандарты......................................................................... 87
Развертывание простых приложений (закрытое
развертывание сборок)........................................................................ 88
Простое средство администрирования (конфигурационный файл) .... 90 Алгоритм поиска файлов сборки 92
Глава 3. Совместно используемые сборки и сборки
со строгим именем....................................................................................... 94
Два вида сборок — два вида развертывания........................................... 95
Назначение сборке строгого имени.......................................................... 96
Глобальный кэш сборок......................................................................... 102
Построение сборки, ссылающейся на сборку со строгим именем........ 104
Устойчивость сборок со строгими именами к несанкционированной модификации 106
Отложенное подписание........................................................................ 107
Закрытое развертывание сборок со строгими именами ........................ 110
Как исполняющая среда разрешает ссылки на типы ............................. 111
Дополнительные административные средства
(конфигурационные файлы)................................................................ 115
Управление версиями при помощи политики издателя....................... 117
ЧАСТЬ II. ПРОЕКТИРОВАНИЕ ТИПОВ
Глава 4. Основы типов............................................................................... 122
Все типы — производные от System.Object .......................................... 122
Приведение типов.................................................................................. 124
Приведение типов в C# с помощью операторов is и as..................... 126
Пространства имен и сборки.................................................................. 128
Связь между сборками и пространством имен.................................. 132
Как разные компоненты взаимодействуют во время выполнения.......... 133
Глава 5. Примитивные, ссылочные и значимые типы ................................... 142
Примитивные типы в языках программирования .................................... 142
Проверяемые и непроверяемые операции для примитивных типов.... 146
Ссылочные и значимые типы.................................................................. 150
Как CLR управляет размещением полей для типа.............................. 155
Упаковка и распаковка значимых типов.................................................. 156
Изменение полей в упакованных значимых типах посредством
интерфейсов (и почему этого лучше не делать).............................. 169
Равенство и тождество объектов....................................................... 172
Хеш-коды объектов ............................................................................... 175
Примитивный тип данных dynamic.......................................................... 177
Глава 6. Основные сведения о членах и типах............................................. 186
Члены типа............................................................................................. 186
Видимость типа ..................................................................................... 189
Дружественные сборки...................................................................... 189
Доступ к членам типов........................................................................... 191
Статические классы................................................................................ 193
Частичные классы, структуры и интерфейсы......................................... 194
Компоненты, полиморфизм и версии..................................................... 196
Вызов виртуальных методов, свойств и событий в CLR..................... 198
Разумное использование видимости типов
и модификаторов доступа к членам................................................ 202
Работа с виртуальными методами при управлении версиями типов.. 205
Глава 7. Константы и поля........................................................................... 210
Константы.............................................................................................. 210
Поля....................................................................................................... 212
Глава 8. Методы .......................................................................................... 215
Конструкторы экземпляров и классы (ссылочные типы) ....................... 215
Конструкторы экземпляров и структуры (значимые типы) ..................... 219
Конструкторы типов............................................................................... 222
Методы перегруженных операторов...................................................... 226
Операторы и взаимодействие языков программирования.................. 229
Особое мнение автора о правилах Microsoft, связанных с именами методов операторов 229
Методы операторов преобразования.................................................... 230
Методы расширения............................................................................... 234
Правила и рекомендации.................................................................... 237
Расширение разных типов методами расширения.............................. 238
Атрибут расширения ......................................................................... 240
Частичные методы................................................................................. 241
Правила и рекомендации.................................................................... 244
Глава 9. Параметры..................................................................................... 245
Необязательные и именованные параметры........................................... 245
Правила использования параметров.................................................. 246
Атрибут DefaultParameterValue и необязательные атрибуты............... 248
Неявно типизированные локальные переменные.................................... 248
Передача параметров в метод по ссылке ............................................. 251
Передача переменного количества аргументов..................................... 257
Типы параметров и возвращаемых значений ......................................... 259
Константность........................................................................................ 261
Глава 10. Свойства....................................................................................... 263
Свойства без параметров...................................................................... 263
Автоматически реализуемые свойства .............................................. 267
Осторожный подход к определению свойств..................................... 268
Свойства и отладчик Visual Studio ..................................................... 270
Инициализаторы объектов и коллекций.............................................. 271
Анонимные типы................................................................................. 273
Тип System.Tuple................................................................................ 276
Свойства с параметрами ....................................................................... 279
Выбор главного свойства с параметрами.......................................... 283
Производительность при вызове методов доступа................................ 284
Доступность методов доступа свойств.................................................. 285
Обобщенные методы доступа свойств................................................... 285
Глава 11. События........................................................................................ 286
Разработка типа, поддерживающего событие........................................ 287
Этап 1. Определение типа для хранения всей дополнительной информации, передаваемой получателям уведомления о событии ................................................................. 288
Этап 2. Определение члена-события.................................................. 289
Этап 3. Определение метода, ответственного за уведомление
зарегистрированных объектов о событии....................................... 290
Этап 4. Определение метода, преобразующего входную информацию в желаемое событие 292
Реализация событий компилятором....................................................... 293
Создание типа, отслеживающего событие............................................. 295
Явное управление регистрацией событий.............................................. 298
Глава 12. Обобщения................................................................................... 302
Обобщения в библиотеке FCL................................................................ 307
Инфраструктура обобщений ................................................................. 308
Открытые и закрытые типы................................................................. 309
Обобщенные типы и наследование..................................................... 311
Идентификация обобщенных типов.................................................... 313
Разрастание кода............................................................................... 314
Обобщенные интерфейсы...................................................................... 315
Обобщенные делегаты........................................................................... 316
Контравариантные и ковариантные аргументы-типы в делегатах и интерфейсах 317
Обобщенные методы............................................................................. 319
Обобщенные методы и выведение типов............................................ 320
Обобщения и другие члены.................................................................... 322
Верификация и ограничения .................................................................. 322
Основные ограничения....................................................................... 325
Дополнительные ограничения............................................................ 327
Ограничения конструктора................................................................. 328
Другие проблемы верификации.......................................................... 329
Глава 13. Интерфейсы ................................................................................. 333
Наследование в классах и интерфейсах................................................ 333
Определение интерфейсов.................................................................... 334
Наследование интерфейсов................................................................... 335
Подробнее о вызовах интерфейсных методов....................................... 338
Явные и неявные реализации интерфейсных методов
(что происходит за кулисами)............................................................. 339
Обобщенные интерфейсы...................................................................... 341
Обобщения и ограничения интерфейса ................................................. 344
Реализация нескольких интерфейсов с одинаковыми сигнатурами и именами методов 345
Совершенствование безопасности типов за счет явной
реализации интерфейсных методов................................................... 346
Опасности явной реализации интерфейсных методов............................ 348
Дилемма разработчика: базовый класс или интерфейс?........................ 351
ЧАСТЬ III. ОСНОВНЫЕ ТИПЫ ДАННЫХ
Глава 14. Символы, строки и обработка текста......................................... 356
Символы ............................................................................................... 356
Тип System.String................................................................................... 359
Создание строк.................................................................................. 359
Неизменяемые строки ........................................................................ 362
Сравнение строк................................................................................. 362
Интернирование строк........................................................................ 369
Создание пулов строк........................................................................ 372
Работа с символами и текстовыми элементами в строке................... 372
Прочие операции со строками............................................................ 375
Эффективное создание строк................................................................ 375
Создание объекта StringBuilder........................................................... 376
Члены типа StringBuilder .................................................................... 377
Получение строкового представления объекта..................................... 379
Форматы и региональные стандарты ................................................. 380
Форматирование нескольких объектов в одну строку ....................... 384
Создание собственного средства форматирования........................... 386
Получение объекта посредством разбора строки................................. 389
Кодировки: преобразования между символами и байтами..................... 391
Кодирование и декодирование потоков символов и байтов............... 397
Кодирование и декодирование строк в кодировке Base-64 ............... 398
Защищенные строки............................................................................... 399
Глава 15. Перечислимые типы и битовые флаги........................................... 403
Перечислимые типы............................................................................... 403
Битовые флаги....................................................................................... 409
Добавление методов к перечислимым типам......................................... 413
Глава 16. Массивы....................................................................................... 416
Инициализация элементов массива........................................................ 418
Приведение типов в массивах................................................................ 421
Базовый класс System.Array................................................................... 423
Реализация интерфейсов lEnumerable, ICollection и IList......................... 424
Передача и возврат массивов................................................................ 425
Массивы с ненулевой нижней границей.................................................. 426
Внутренняя реализация массивов.......................................................... 427
Небезопасный доступ к массивам и массивы фиксированного размера 432
Глава 17. Делегаты ...................................................................................... 434
Знакомство с делегатами....................................................................... 434
Обратный вызов статических методов................................................... 437
Обратный вызов экземплярных методов................................................ 438
Тонкости использования делегатов........................................................ 439
Обратный вызов нескольких методов (цепочки делегатов).................... 443
Поддержка цепочек делегатов в C#.................................................... 448
Дополнительные средства управления цепочками делегатов............ 448
Обобщенные делегаты........................................................................... 451
Упрощенный синтаксис работы с делегатами......................................... 452
Упрощение 1: не создаем объект делегата......................................... 452
Упрощение 2: не определяем метод обратного вызова..................... 453
Упрощение 3: не создаем обертку для локальных переменных для передачи их методу обратного вызова 457
Делегаты и отражение............................................................................ 460
Глава 18. Настраиваемые атрибуты............................................................. 464
Сфера применения настраиваемых атрибутов....................................... 464
Определение класса атрибутов............................................................. 468
Конструктор атрибута и типы данных полей и свойств ......................... 471
Выявление настраиваемых атрибутов.................................................... 473
Сравнение экземпляров атрибута ......................................................... 477
Выявление настраиваемых атрибутов без создания объектов,
производных от Attribute.................................................................... 480
Условные атрибуты................................................................................ 484
Глава 19. Null-совместимые значимые типы............................................. 485
Поддержка в C# null-совместимых значимых типов................................ 487
Оператор объединения null-совместимых значений ............................... 490
Поддержка в CLR null-совместимых значимых типов.............................. 491
Упаковка null-совместимых значимых типов........................................ 491
Распаковка null-совместимых значимых типов.................................... 492
Вызов метода GetType через null-совместимый значимый тип........... 492
Вызов интерфейсных методов через null-совместимый значимый тип 493
ЧАСТЬ IV. КЛЮЧЕВЫЕ МЕХАНИЗМЫ
Глава 20. Исключения и управление состоянием...................................... 496
Определение «исключения»................................................................... 496
Механика обработки исключений........................................................... 498
Блок try.............................................................................................. 499
Блок catch.......................................................................................... 499
БлокЬпаПу......................................................................................... 501
CLS-совместимые и CLS-несовместимые исключения........................ 503
Класс System.Exception......................................................................... 505
Классы исключений, определенные в FCL.............................................. 509
Генерирование исключений.................................................................... 511
Создание классов исключений............................................................... 513
Продуктивность вместо надежности...................................................... 515
Приемы работы с исключениями............................................................ 524
Активно используйте блоки finally ..................................................... 525
Не надо перехватывать все исключения............................................. 526
Корректное восстановление после исключения................................. 528
Отмена незавершенных операций при невосстановимых
исключениях.................................................................................... 529
Сокрытие деталей реализации для сохранения контракта.................. 530
Необработанные исключения................................................................. 533
Отладка исключений.............................................................................. 537
Скорость обработки исключений........................................................... 540
Области ограниченного выполнения...................................................... 543
Контракты кода...................................................................................... 546
Глава 21. Автоматическое управление памятью
(уборка мусора)........................................................................................... 554
Управляемая куча ................................................................................. 554
Выделение ресурсов из управляемой кучи........................................ 555
Алгоритм уборки мусора................................................................... 557
Уборка мусора и отладка................................................................... 560
Поколения ............................................................................................. 562
Запуск уборки мусора........................................................................ 568
Большие объекты............................................................................... 569
Режимы уборки мусора...................................................................... 570
Программное управление уборщиком мусора................................... 573
Мониторинг использования памяти приложением.............................. 574
Освобождение ресурсов при помощи механизма финализации............ 576
Типы, использующие системные ресурсы........................................... 583
Интересные аспекты зависимостей..................................................... 588
Другие возможности уборщика мусора для работы
с системными ресурсами................................................................. 590
Внутренняя реализация финализации................................................. 594
Мониторинг и контроль времени жизни объектов.................................. 597
Глава 22. Хостинг CLR и домены приложений.............................................. 606
Хостинг CLR........................................................................................... 606
Домены приложений............................................................................... 609
Доступ к объектам из других доменов............................................... 612
Выгрузка доменов.................................................................................. 624
Мониторинг доменов ............................................................................. 626
Уведомление о первом управляемом исключении домена..................... 627
Использование хостами доменов приложений....................................... 628
Исполняемые приложения.................................................................. 628
Полнофункциональные интернет-приложения Silverlight..................... 629
MicrosoftASP.NET и веб-службы ХМL................................................. 629
Microsoft SQL Server........................................................................... 630
Будущее и мечты ............................................................................... 630
Нетривиальное управление хостингом................................................... 631
Применение управляемого кода......................................................... 631
Разработка надежных хост-приложений............................................. 631
Возвращение потока в хост................................................................ 633
Глава 23. Загрузка сборок и отражение....................................................... 636
Загрузка сборок..................................................................................... 637
Использование отражения для создания
динамически расширяемых приложений............................................. 641
Производительность отражения ........................................................... 642
Нахождение типов, определенных в сборке....................................... 644
Объект Туре ...................................................................................... 644
Создание иерархии типов, производных от Exception ....................... 646
Создание экземпляра типа................................................................. 648
Создание приложений с поддержкой подключаемых компонентов .. .650
Нахождение членов типа путем отражения............................................ 653
Нахождение членов типа.................................................................... 654
Обращение к членам типов................................................................ 658
Использование дескрипторов привязки для снижения потребления памяти процессом 663
Глава 24. Сериализация............................................................................... 666
Практический пример сериализации/десериализации............................. 667
Сериализуемые типы ............................................................................. 672
Управление сериализацией и десериализацией...................................... 673
Сериализация экземпляров типа............................................................ 677
Управление сериализованными и десериализованными данными... .679 Определение типа, реализующего интерфейс ISerializable, не реализуемый базовым классом.............. 684
Контексты потока ввода-вывода ........................................................... 686
Сериализация в другой тип и десериализация в другой объект............ 688
Суррогаты сериализации........................................................................ 691
Цепочка селекторов суррогатов........................................................ 694
Переопределение сборки и/или типа при десериализации объекта . .695
Глава 25. Взаимодействие с компонентами WinRT.................................. 698
Проекции уровня CLR и правила системы типов
компонентов WinRT............................................................................ 700
Основные концепции системы типов WinRT........................................ 700
Проекции уровня .NET Framework.......................................................... 705
Асинхронные вызовы WinRT API из кода .NET................................... 705
Взаимодействия между потоками WinRT и потоками .NET.................. 710
Передача блоков данных между CLR и WinRT.................................... 712
Определение компонентов WinRT в коде C#.......................................... 715
ЧАСТЬ V. МНОГОПОТОЧНОСТЬ
Глава 26. Потоки исполнения..................................................................... 724
Для чего Windows поддерживает потоки? ............................................. 724
Ресурсоемкость потоков....................................................................... 725
Так дальше не пойдет!............................................................................ 729
Тенденции развития процессоров ......................................................... 732
CLR- и Windows-потоки .......................................................................... 733
Потоки для асинхронных вычислительных операций............................. 734
Причины использования потоков........................................................... 736
Планирование и приоритеты потоков..................................................... 739
Фоновые и активные потоки................................................................... 744
Что дальше? ......................................................................................... 746
Глава 27. Асинхронные вычислительные операции................................. 747
Пул потоков в CLR................................................................................. 747
Простые вычислительные операции....................................................... 748
Контексты исполнения............................................................................ 750
Скоординированная отмена................................................................... 752
Задания.................................................................................................. 757
Завершение задания и получение результата..................................... 758
Отмена задания.................................................................................. 760
Автоматический запуск задания по завершении предыдущего........... 762
Дочерние задания............................................................................... 764
Структура задания.............................................................................. 765
Фабрики заданий................................................................................ 767
Планировщики заданий....................................................................... 769
Методы For, ForEach и Invoke класса Parallel......................................... 771
Встроенный язык параллельных запросов............................................. 775
Периодические вычислительные операции............................................. 779
Разновидности таймеров.................................................................... 782
Как пул управляет потоками.................................................................. 783
Ограничение количества потоков в пуле............................................ 783
Управление рабочими потоками......................................................... 784
Глава 28. Асинхронные операции ввода-вывода.......................................... 787
Операции ввода-вывода в Windows........................................................ 787
Асинхронные функции C#....................................................................... 792
Преобразование асинхронной функции в конечный автомат.................. 795
Расширяемость асинхронных функций................................................... 799
Асинхронные функции и обработчики событий ..................................... 803
Асинхронные функции в FCL.................................................................. 804
Асинхронные функции и исключения...................................................... 806
Другие возможности асинхронных функций........................................... 807
Потоковые модели приложений............................................................. 810
Асинхронная реализация сервера ......................................................... 813
Отмена операций ввода-вывода............................................................. 814
Некоторые операции ввода-вывода
должны выполняться синхронно...................................................... 815
Проблемы FileStream.......................................................................... 816
Приоритеты запросов ввода-вывода...................................................... 817
Глава 29. Примитивные конструкции синхронизации
потоков...................................................................................................... 820
Библиотеки классов и безопасность потоков........................................ 822
Примитивные конструкции пользовательского режима
и режима ядра.................................................................................... 824
Конструкции пользовательского режима............................................... 825
Volatile-конструкции............................................................................ 826
Interlocked-конструкции....................................................................... 832
Реализация простой циклической блокировки.................................... 837
Универсальный Interlocked-паттерн..................................................... 841
Конструкции режима ядра...................................................................... 843
События............................................................................................. 847
Семафоры.......................................................................................... 850
Мьютексы .......................................................................................... 851
Глава 30. Гибридные конструкции синхронизации потоков .. .854
Простая гибридная блокировка............................................................. 854
Зацикливание, владение потоком и рекурсия......................................... 857
Гибридные конструкции в FCL................................................................ 859
Классы ManualResetEventSlim и SemaphoreSlim................................. 859
Класс Monitor и блоки синхронизации................................................ 860
Класс ReaderWriterLockSlim................................................................ 866
Класс OneManyLock........................................................................... 868
Класс CountdownEvent ...................................................................... 871
Класс Barrier....................................................................................... 872
Выводы по гибридным конструкциям................................................. 873
Блокировка с двойной проверкой.......................................................... 875
Паттерн условной переменной............................................................... 880
Асинхронная синхронизация ................................................................. 882
Классы коллекций для параллельного доступа .................................... 888
Словарь соответствия русскоязычных
и англоязычных терминов......................................................................... 893
Посвящение
Кристин. Словами не выразить то, что я думаю о нашей совместной жизни. Я с нежностью отношусь к нашей семье и ко всем нашим семейным радостям. Каждый день моей жизни наполнен любовью к тебе.
Адэн (9 лет) и Грант (5 лет). Вы оба вдохновляли меня, учили меня играть и весело проводить время. Наблюдения за тем, как вы росли, доставляли мне радость. Мне повезло стать частью вашей жизни. Я люблю и ценю вас больше всех на свете.
Предисловие
Вот мы и снова встретились. Кто бы мог подумать? Ага, знаю — я должна была это
предвидеть!
Жизнь в браке — один сплошной «День сурка». Если вы не видели этот фильм, посмотрите; вы внезапно поймете, почему одни и те же ошибки приходится повторять снова и снова. Когда Джефф сказал мне, что он не будет писать следующую книгу, я сразу поняла, что это пустое обещание. Джефф не может не написать следующую книгу. Только сегодня мы с ним обсуждали еще одну книгу, которую он тоже совершенно не собирается писать (только почему-то уже написал целую главу). Это у него в крови. Породистая лошадь рождается для скачек, а Джефф рожден для того, чтобы писать книги.
Джефф предсказуем, как смена времен года. Он не может держаться подальше от ноликов и единичек на своем жестком диске. И когда нормальные люди мирно спят в своих постелях, внутренний будильник Джеффа начинает звонить где-то в 3 часа ночи (когда наш 5-летний сын залезает в нашу постель — еще одно явление, с которым, похоже, ничего не поделаешь). Какая-то таинственная сила направляет Джеффа в кабинет и заставляет его мозг решать всевозможные маленькие и большие проблемы. Другим остается лишь перевернуться на другой бок и снова заснуть — зная, что Джефф где-то рядом решает эти проблемы за нас — словно некий киберсупергерой, спасающий программные потоки от преждевременной гибели.
Однако Джеффу недостаточно копить всю эту информацию в своем личном уголке Вселенной. Его начинают терзать угрызения совести — он должен поделиться своими мыслями с другими; записать их на бумаге. Они словно радиоволны, которые передаются в пространстве, чтобы их кто-то где-то принял. И все это делается для вас, дорогой читатель; перед вами свидетельство его страсти к технологиям Microsoft.
В этой книге добавилось авторской мудрости. Валяясь на солнышке, Джефф становится старше, и с течением лет он начинает оглядываться назад. Размышляя о вещах с более зрелых позиций, он переписал главу, посвященную отражению (reflection). Возможно, вы оцените его поэтический подход к теме. В этой главе рассказано о том, как заставить программный код задавать вопросы о другом коде, а также приводятся более глубокие рассуждения о том, почему отражение работает именно так, а не иначе. Наденьте халат, сядьте в кожаное кресло и предайтесь высоким размышлениям о своем коде и смысле жизни.
Также в книге добавился материал о async/await. Очевидно, это развитие темы AsyncEnumenaton, о которой мой любимый говорил какое-то время назад. Тогда мне порой казалось, что он ни о чем другом говорить уже не способен! Правда, после всех разговоров я так толком и не запомнила, что это такое. Джефф работал с группой в «большом М», доводя до ума механизм async 'await, и теперь этот материал вошел в книгу, чтобы порадовать читателя.
Еще одно крупное дополнение к книге вызывает у меня больше всего эмоций. Надеюсь, все читатели ознакомятся с главой о WinRT и возьмут этот материал на вооружение. WinRT — такой термин для технарей, которое каким-то образом означает: «Сделайте Мне Крутое Приложение Для Моего Планшета - ПРЯМО СЕЙЧАС!» Да, все верно; это новая исполнительная среда Windows для сенсорных экранов. Мои дети обожают кидаться птичками в свиней. Мне нравятся приложения с цветочками, и определенно планшеты полезны для образовательных целей. Дайте волю фантазии! II пожалуйста, сделайте так, чтобы эта глава принесла мне какую-нибудь пользу. Иначе мое терпение с Джеффом и его вечной работой над книгами иссякнет, и я запру его в комнате с вязальными спицами и без электричества. Выбирайте, программисты: либо пишем классные приложения с Win RT, либо новых книг от Джеффа уже не будет!
Короче говоря. Джефф (при вашем неустанном содействии) выдал очередной шедевр, и наша семья может вернуться к своему нормальному состоянию. Хотя что считать нормой? Возможно, состояние работы над очередной книгой уже стало нормальным.
В терпеливом ожидании следующей книги, Кристин Трейс (жена Джеффа) октябрь 2012 г.
На помощь! Спасите Джеффа от вязания! |
Введение
В октябре 1999 года люди из Microsoft впервые продемонстрировали мне платформу .NET Framework, общеязыковую среду выполнения (Common Language Runtime, CLR) и язык программирования С#. Все это произвело на меня сильное впечатление: я сразу понял, что мой подход к написанию программного обеспечения претерпит существенные изменения. Меня попросили проконсультировать команду разработчиков, и я немедленно согласился. Поначалу я думал, что платформа .NET Framework представляет собой абстрактную прослойку между Win32 API (Application Program Interface, интерфейс прикладного программирования) и моделью COM (Component Object Model, объектная модель компонентов). Но чем больше я изучал платформу .NET Framework, тем яснее видел, насколько я ее недооценивал. В некотором смысле это операционная система. У нее собственные диспетчер виртуальной памяти, система безопасности, файловый загрузчик, механизм обработки ошибок, модель изоляции приложений (домены приложений), модель многопоточности и многое другое. В этой книге все эти (и многие другие) темы освещаются таким образом, чтобы вы могли эффективно проектировать и реализовывать программное обеспечение на этой платформе.
Я написал текст этой книги в октябре 2012 года, и уже прошло 13 лет с тех пор, как я работаю с платформой .NET Framework и языком программирования С#. За эти 13 лет я создал разнообразные приложения и как консультант компании Microsoft внес значительный вклад в разработку платформы .NET Framework. В качестве сотрудника своей компании Wintellect (http://Wintellect.com) я работал со многими заказчиками, помогая им проектировать программное обеспечение, отлаживать и оптимизировать программы, решать их проблемы, связанные с .NET Framework. Весь этот опыт помог мне реально узнать, какие именно проблемы возникают у людей при работе с платформой .NET Framework и как эти проблемы решать. Я попытался сопроводить этими знаниями все темы, представленные в этой книге.
Для кого эта книга
Цель этой книги — объяснить, как разрабатывать приложения и многократно используемые классы для .NET Framework. В частности, это означает, что я собираюсь рассказать, как работает среда CLR и какие возможности она предоставляет разработчику. В этой книге также описываются различные классы стандартной библиотеки классов (Framework Class Library, FCL). Ни в одной книге невозможно описать FCL полностью — она содержит тысячи типов, и их число продолжает расти ударными темпами. Так что я остановлюсь на основных типах, с которыми
должен быть знаком каждый разработчик. И хотя в этой книге не рассматриваются отдельно подсистема графического интерфейса пользователя Windows Forms, система для построения клиентских приложений Windows Presentation Foundation (WPF), Microsoft Silverlight, веб-службы XML, веб-формы, Microsoft ASP.NET MVC, Windows Store Apps и т. д., технологии, описанные в ней, применимы ко всем этим видам приложений.
В книге используются системы Microsoft Visual Studio 2012, .NET Framework 4.5 и компилятор C# версии 5.0. Компания Microsoft старается по возможности обеспечивать обратную совместимость при выпуске новой версии этих программных продуктов, поэтому многое из того, что обсуждается в данной книге, применимо и к ранним версиям. Все примеры в книге написаны на языке С#, но так как в среде CLR можно использовать различные языки программирования, книга пригодится также и тем, кто пишет на других языках программирования.
ПРИМЕЧАНИЕ
Примеры кода, приведенные в книге, можно загрузить с сайта компании Wintellect (http://Wintellect.com/Books).
Я и мои редакторы усердно потрудились, чтобы дать вам наиболее точную, свежую, исчерпывающую, удобную, понятную и безошибочную информацию. Однако даже такая фантастическая команда не может предусмотреть все. Если вы обнаружите в этой книге ошибки (особенно ошибки в программном коде) или же у вас появится конструктивное предложение, то я буду вам очень признателен за сообщение об этом по моему адресуJeffreyR@Wintellect.com.
Благодарности
Я бы не написал эту книгу без помощи и технической поддержки многих людей. В частности, я хотел бы поблагодарить мою семью. Очень сложно измерить время и усилия, которые требуются для написания хорошей книги, но я знаю точно, что без поддержки моей жены Кристин и двух сыновей, Адэна и Гранта, я бы не смог написать эту книгу. Очень часто наши планы совместного времяпровождения срывались из-за жесткого графика работы. Теперь, по завершении работы над книгой, я наконец предвкушаю радость общения с семьей.
В написании книги мне помогали многие замечательные люди. Участники группы .NET Framework (многих из которых я считаю своими друзьями) занимались рецензированием материалов книги и беседовали со мной, давая информацию для размышлений. Кристоф Насарр (Christophe Nasarre), с которым я работал уже в нескольких проектах, выполнил титанический труд по проверке моей работы и позаботился о точности и корректности формулировок. Он внес очень большой вклад в повышение качества книги. Как всегда, было очень приятно работать с редикторской командой Microsoft Press. Отдельную благодарность я хочу выразить Бену Райану (Ben Ryan), Дэвону Масгрейву (Devon Musgrave) и Кэрол Диллингем (Carol Dillingham). Спасибо также Сьюзи Карр (Susie Carr) и Кандейс Синклер (Candace Sinclair) за редактуру и помощь при выпуске книги.
Поддержка
Мы приложили максимум усилий, чтобы эта книга и сопроводительные материалы содержали как можно более точную информацию. Все исправления и изменения публикуются в нашем разделе Microsoft Press на сайте oreilly.com по адресу http:// go.microsoft.com/FWLink/?Linkid=266601.
При обнаружении ошибки, отсутствующей в списке, вы сможете сообщить о ней на той же странице.
Если вам понадобится дополнительная поддержка, обращайтесь в службу поддержки Microsoft Press Book Support по адресу mspinput@microsoft.com.
Пожалуйста, обратите внимание, что по указанным адресам техническая поддержка программных продуктов Microsoft не предоставляется.
Мы хотим знать ваше мнение
Мы приветствуем любые отзывы на эту книгу. Пожалуйста, сообщите свое мнение о книге в опросе, который находится по адресу http://microsoft.com/learning/booksurvey.
Опрос займет совсем немного времени, а мы внимательно прислушиваемся ко всем замечаниям и предложениям. Заранее спасибо за ваше участие!
Будьте в курсе
Общение продолжается! Мы в Твиттере: http://twitter.com/MicrosoftPress.
От издателя перевода
Для удобства читателей мы добавили в книгу краткий словарь соответствия русскоязычных и англоязычных терминов. Ваши замечания, предложения и вопросы отправляйте по адресу электронной почты comp@piter.com (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
Подробную информацию о наших книгах вы найдете на веб-сайте издательства http://www.piter.com.
ЧАСТЬ I
Основы CLR
Глава 1. Модель выполнения кода в среде CLR.................... 28
Глава 2. Компоновка, упаковка, развертывание и администрирование приложений и типов 58
Глава 3. совместно используемые сборки и сборки со строгим именем 94
Глава 1. Модель выполнения кода в среде CLR
В Microsoft .NET Framework появилось много новых концепций, технологий и терминов. Цель этой главы — дать обзор архитектуры .NET Framework, познакомить с новыми технологиями этой платформы и определить термины, с которыми вы столкнетесь при работе с ней. Также в этой главе изложен процесс построения приложения или набора распространяемых компонентов (файлов), которые содержат типы (классы, структуры и т. п.), и затем объяснено, как выполняется приложение.
Компиляция исходного кода в управляемые модули
Итак, вы решили использовать .NET Framework как платформу разработки. Отлично! Ваш первый шаг — определить вид создаваемого приложения или компонента. Предположим, что этот вопрос уже решен, все спроектировано, спецификации написаны и все готово для начала разработки.
Теперь надо выбрать язык программирования. И это непростая задача — ведь у разных языков имеются разные возможности. Например, с одной стороны, «неуправляемый код» C/C++ дает доступ к системе на низком уровне. Вы вправе распоряжаться памятью по своему усмотрению, при необходимости создавать программные потоки и т. д. С другой стороны, Microsoft Visual Basic 6.0 позволяет очень быстро строить пользовательские интерфейсы и легко управлять СОМ-объектами и базами данных.
Название среды — общеязыковая среда выполнения (Common Language Runtime, CLR) — говорит само за себя: это среда выполнения, которая подходит для разных языков программирования. Основные возможности CLR (управление памятью, загрузка сборок, безопасность, обработка исключений, синхронизация) доступны в любых языках программирования, использующих эту среду. Например, при обработке ошибок среда выполнения опирается на исключения, а значит, во всех языках программирования, использующих эту среду выполнения, сообщения об ошибках передаются при помощи механизма исключений. Или, например, среда выполнения позволяет создавать программные потоки, а значит, во всех языках программирования, использующих эту среду тоже могут создаваться потоки.
Фактически во время выполнения программы в среде CLR неизвестно, на каком языке программирования разработчик написал исходный код. А это значит, что можно выбрать любой язык программирования, который позволяет проще всего решить конкретную задачу. Разрабатывать программное обеспечение можно на любом языке программирования, если только используемый компилятор этого языка поддерживает CLR.
Так в чем же тогда преимущество одного языка программирования перед другим? Я рассматриваю компиляторы как средства контроля синтаксиса и анализа «правильности кода». Компиляторы проверяют исходный код, убеждаются, что все написанное имеет некий смысл, и затем генерируют код, описывающий решение данной задачи. Разные языки программирования позволяют разрабатывать программное обеспечение, используя различный синтаксис. Не стоит недооценивать значение выбора синтаксиса языка программирования. Например, для математических или финансовых приложений выражение мысли программиста на языке APL может сохранить много дней работы по сравнению с применением в данной ситуации языка Perl.
Компания Microsoft разработала компиляторы для следующих языков программирования, используемых на этой платформе: C++/CLI, C# (произносится «си шарп»), Visual Basic, F# (произносится «эф шарп»), Iron Python, Iron Ruby и ассемблер Intermediate Language (IL). Кроме Microsoft, еще несколько компаний и университетов создали компиляторы, предназначенные для среды выполнения CLR. Мне известны компиляторы для Ada, APL, Caml, COBOL, Eiffel, Forth, Fortran, Haskell, Lexico, LISP, LOGO, Lua, Mercury, ML, Mondrian, Oberon, Pascal, Perl, Php, Prolog, RPG, Scheme, Smalltalk и Tcl/Tk.
Рисунок 1.1 иллюстрирует процесс компиляции файлов с исходным кодом. Как видно из рисунка, исходный код программы может быть написан на любом языке, поддерживающем среду выполнения CLR. Затем соответствующий компилятор проверяет синтаксис и анализирует исходный код программы. Вне зависимости от типа используемого компилятора результатом компиляции будет являться управляемый модуль (managed module) — стандартный переносимый исполняемый (portable executable, РЕ) файл 32-разрядной (РЕ32) или 64-разрядной Windows (РЕ32+), который требует для своего выполнения CLR. Кстати, управляемые сборки всегда используют преимущества функции безопасности «предотвращения выполнения данных» (DEP, Data Execution Prevention) и технологию ASLR (Address Space Layout Optimization), применение этих технологий повышает информационную безопасность всей системы.
Компиляторы машинного кода производят код, ориентированный на конкретную процессорную архитектуру, например х86, х64 или ARM. В отличие от этого, все CLR-совместимые компиляторы генерируют IL-код. (Подробнее об IL-коде рассказано далее в этой главе.) IL-код иногда называют управляемым (managed code), потому что CLR управляет его выполнением.
|
Рис. 1.1. Компиляция исходного кода в управляемые модули
В табл. 1.1 описаны составные части управляемого модуля.
Таблица 1.1. Части управляемого модуля
|
|
Каждый компилятор, предназначенный для CLR, помимо генерирования IL-кода, должен также создавать полные метаданные (metadata) для каждого управляемого модуля. Проще говоря, метаданные — это набор таблиц данных, описывающих то, что определено в модуле, например типы и их члены. В метаданных также есть таблицы, указывающие, на что ссылается управляемый модуль, например на импортируемые типы и их члены. Метаданные расширяют возможности таких старых технологий, как библиотеки типов СОМ и файлы IDL (Interface Definition Language, язык описания интерфейсов). Важно отметить, что метаданные CLR содержат куда более полную информацию. И, в отличие от библиотек типов и IDL-файлов, они всегда связаны с файлом, содержащим IL-код. Фактически метаданные всегда встроены в тот же EXE- или DLL-файл, что и код, так что их нельзя разделить. А поскольку компилятор генерирует метаданные и код одновременно и привязывает их к конечному управляемому модулю, возможность рассинхронизации метаданных и описываемого ими IL-кода исключена.
Метаданные находят много применений. Перечислим лишь некоторые из них.
□ Метаданные устраняют необходимость в заголовочных и библиотечных файлах при компиляции, так как все сведения об упоминаемых типах/членах содержатся в файле с реализующим их IL-кодом. Компиляторы могут читать метаданные прямо из управляемых модулей.
□ Среда Microsoft Visual Studio использует метаданные для облегчения написания кода. Ее функция IntelliSense анализирует метаданные и сообщает, какие методы, свойства, события и поля предпочтительны в данном случае и какие именно параметры требуются конкретным методам.
□ В процессе верификации кода CLR использует метаданные, чтобы убедиться, что код совершает только «безопасные по отношению к типам» операции. (Проверка кода обсуждается далее.)
□ Метаданные позволяют сериализовать поля объекта, а затем передать эти данные по сети на удаленный компьютер и там провести процесс десериализации, восстановив объект и его состояние на удаленном компьютере.
□ Метаданные позволяют сборщику мусора отслеживать жизненный цикл объектов. При помощи метаданных сборщик мусора может определить тип объектов и узнать, какие именно поля в них ссылаются на другие объекты.
В главе 2 метаданные описаны более подробно.
Языки программирования С#, Visual Basic, F# и IL-ассемблер всегда создают модули, содержащие управляемый код (IL) и управляемые данные (данные, поддерживающие сборку мусора). Для выполнения любого управляемого модуля на машине конечного пользователя должна быть установлена среда CLR (в составе .NET Framework) — так же, как для выполнения приложений MFC или Visual Basic 6.0 должны быть установлены библиотека классов Microsoft Foundation Class (MFC) или DLL-библиотеки Visual Basic.
По умолчанию компилятор Microsoft C++ создает EXE- и DLL-файлы, которые содержат неуправляемый код и неуправляемые данные. Для их выполнения CLR не требуется. Однако если вызвать компилятор C++ с параметром /CLR в командной строке, он создаст управляемые модули (и конечно, для работы этих модулей должна быть установлена среда CLR). Компилятор C++ стоит особняком среди всех упомянутых компиляторов производства Microsoft — только он позволяет разработчикам писать как управляемый, так и неуправляемый код и встраивать его в единый модуль. Это также единственный компилятор Microsoft, разрешающий программистам определять в исходном коде как управляемые, так и неуправляемые типы данных. Компилятор Microsoft предоставляет разработчику непревзойденную гибкость, позволяя использовать существующий неуправляемый код на С/ C++ из управляемого кода и постепенно, по мере необходимости, переходить на управляемые типы.
Объединение управляемых модулей в сборку
На самом деле среда CLR работает не с модулями, а со сборками. Сборка (assembly) — это абстрактное понятие, понять смысл которого на первых порах бывает нелегко. Во-первых, сборка обеспечивает логическую группировку одного или нескольких управляемых модулей или файлов ресурсов. Во-вторых, это наименьшая единица многократного использования, безопасности и управления версиями. Сборка может состоять из одного или нескольких файлов — все зависит от выбранных средств и компиляторов. В контексте среды CLR сборкой называется то, что мы обычно называем компонентом.
О сборках довольно подробно рассказано в главе 2, а здесь достаточно подчеркнуть, что это концептуальное понятие обозначает способ объединения группы файлов в единую сущность.
Рисунок 1.2 поможет понять суть сборки. На этом рисунке изображены некоторые управляемые модули и файлы ресурсов (или данных), с которыми работает некоторая программа. Эта программа создает единственный файл РЕ32(+), который обеспечивает логическую группировку файлов. При этом в файл РЕ32(+) включа- етсяч блок данных, называемый манифестом (manifest). Манифест представляет собой обычный набор таблиц метаданных. Эти таблицы описывают файлы, которые входят в сборку, общедоступные экспортируемые типы, реализованные в файлах сборки, а также относящиеся к сборке файлы ресурсов или данных.
По умолчанию компиляторы сами выполняют работу по преобразованию созданного управляемого модуля в сборку, то есть компилятор C# создает управляемый модуль с манифестом, указывающим, что сборка состоит только из одного файла. Таким образом, в проектах, где есть только один управляемый модуль и нет файлов
![]() | ![]() | ![]() | |||
ресурсов (или файлов данных), сборка и является управляемым модулем, поэтому выполнять дополнительные действия по компоновке приложения не нужно. В случае если необходимо сгруппировать несколько файлов в сборку, потребуются дополнительные инструменты (например, компоновщик c6opoKAL.exe) со своими параметрами командной строки. О них подробно рассказано в главе 2.
Рис. 1.2. Объединение управляемых модулей в сборку
Сборка позволяет разделить логическое и физическое представления компонента, поддерживающего многократное использование, безопасность и управление версиями. Разбиение программного кода и ресурсов на разные файлы полностью определяется желаниями разработчика. Например, редко используемые типы и ресурсы можно вынести в отдельные файлы сборки. Отдельные файлы могут загружаться по запросу из Интернета по мере необходимости в процессе выполнения программы. Если некоторые файлы не потребуются, то они не будут загружаться, что сохранит место на жестком диске и сократит время установки программы. Сборки позволяют разбить на части процесс развертывания файлов, при этом все файлы будут рассматриваться как единый набор.
Модули сборки также содержат сведения о других сборках, на которые они ссылаются (в том числе номера их версий). Эти данные делают сборку самоописы- ваемой (self-describing). Другими словами, среда CLR может определить все прямые зависимости данной сборки, необходимые для ее выполнения. Не нужно размещать никакой дополнительной информации ни в системном реестре, ни в доменной службе AD DS (Active Directory Domain Services). Вследствие этого развертывать сборки гораздо проще, чем неуправляемые компоненты.
Загрузка CLR
Каждая создаваемая сборка представляет собой либо исполняемое приложение, либо библиотеку DLL, содержащую набор типов для использования в исполняемом приложении. Разумеется, среда CLR отвечает за управление исполнением кода. Это значит, что на компьютере, выполняющем данное приложение, должна быть установлена платформа .NET Framework. В компании Microsoft был создан дистрибутивный пакет .NET Framework для свободного распространения, который вы можете бесплатно поставлять своим клиентам. Некоторые версии операционной системы семейства Windows поставляются с уже установленной платформой .NET Framework.
Для того чтобы понять, установлена ли платформа .NET Framework на компьютере, попробуйте найти файл MSCorEE.dll в каталоге %SystemRoot%\system32. Если он есть, то платформа .NET Framework установлена. Однако на одном компьютере может быть установлено одновременно несколько версий .NET Framework. Чтобы определить, какие именно версии установлены, проверьте содержимое следующих подкаталогов:
2»SystemRoot%\Microsoft.NET\Framework %SystemRoot%\Microsoft.NET\Framework64
Компания Microsoft включила в .NET Framework SDK утилиту командной строки CLRVer.exe, которая выводит список всех версий CLR, установленных на машине, а также сообщает, какая именно версия среды CLR используется текущими процессами. Для этого нужно указать параметр -all или идентификатор
интересующего процесса.
Прежде чем переходить к загрузке среды CLR, поговорим поподробнее об особенностях 32- и 64-разрядных версий операционной системы Windows. Если сборка содержит только управляемый код с контролем типов, она должна одинаково хорошо работать на обеих версиях системы. Дополнительной модификации исходного кода не требуется. Более того, созданный компилятором готовый EXE- или DLL-файл будет правильно выполняться в Windows версий х86 и х64, а библиотеки классов и приложения Windows Store будут работать на машинах с Windows RT (использующих процессор ARM). Другими словами, один и тот же файл будет работать на любом компьютере с установленной платформой .NET Framework.
В исключительно редких случаях разработчикам приходится писать код, совместимый только с какой-то конкретной версией Windows. Обычно это требуется при работе с небезопасным кодом (unsafe code) или для взаимодействия с неуправляемым кодом, ориентированным на конкретную процессорную архитектуру. Для таких случаев у компилятора C# предусмотрен параметр командной строки /platform. Этот параметр позволяет указать конкретную версию целевой платформы, на которой планируется работа данной сборки: архитектуру х86, использующую только 32-раз- рядную систему Windows, архитектуру х64, использующую только 64-разрядную операционную систему Windows, или архитектуру ARM, на которой работает только
![]() | ![]() | ![]() | |||
32-разрядная Windows RT. Если платформа не указана, компилятор задействует значение по умолчанию anycpu, которое означает, что сборка может выполняться в любой версии Windows. Пользователи Visual Studio могут указать целевую платформу в списке Platform Target на вкладке Build окна свойств проекта (рис. 1.3).
frmn and warnings [1] [2]
Wjrnirg level: |a________________ vj
juppmt mmingc | |
Trtatwamings emirs ------------------------------------------------------------------------------------------------------
® Цопе
О a*
О ffififir wi/ninqi:
Output -■
Qutputpithi |bm\Debug\ | [ O[0wse... |
При запуске исполняемого файла Windows анализирует заголовок ЕХЕ-файла для определения того, какое именно адресное пространство необходимо для его работы — 32- или 64-разрядное. Файл с заголовком РЕ32 может выполняться в адресном пространстве любого из указанных двух типов, а файлу с заголовком РЕ32+ требуется 64-разрядное пространство. Windows также проверяет информацию о процессорной архитектуре на совместимость с заданной конфигурацией. Наконец, 64-разрядные версии Windows поддерживают технологию выполнения 32-разрядных приложений в 64-разрядной среде, которая называется WoW64 (Windows on Windows64).
Таблица 1.2 иллюстрирует две важные вещи. Во-первых, в ней показан тип получаемого управляемого модуля для разных значений параметра /platform командной строки компилятора С#. Во-вторых, в ней представлены режимы выполнения приложений в различных версиях Windows.
Таблица 1.2. Влияние значения /platform на получаемый модуль и режим выполнения
|
|
После анализа заголовка ЕХЕ-файла для выяснения того, какой процесс необходимо запустить — 32- или 64-разрядный, — Windows загружает в адресное пространство процесса соответствующую версию библиотеки MSCorEE.dll (х86, х64 или ARM). В системах Windows семейств х86 и ARM 32-разрядная версия MSCorEE.dll хранится в каталоге %SystemRoot%\System32. В системах х64 версия х86 библиотеки находится в каталоге %SystemRoot%\SysWow64, а 64-разрядная версия MSCorEE.dll размещается в каталоге %SystemRoot%\System32 (это сделано из соображений обратной совместимости). Далее основной поток вызывает определенный в библиотеке MSCorEE.dll метод, который инициализирует CLR, загружает сборку EXE, а затем вызывает ее метод Main, в котором содержится точка входа. На этом процедура запуска управляемого приложения считается завершенной[3].
ПРИМЕЧАНИЕ
Сборки, созданные при помощи версий 7.0 и 7.1 компилятора C# от Microsoft, содержат заголовок РЕ32 и не зависят от архитектуры процессора. Тем не менее во время выполнения среда CLR считает их совместимыми только с архитектурой х86. Это повышает вероятность максимально корректной работы в 64-разрядной среде, так как исполняемый файл загружается в режиме WoW64, который обеспечивает процессу среду, максимально приближенную к существующей в 32-разрядной версии х86 Windows.
Когда неуправляемое приложение вызывает функцию Win32 Load Library для загрузки управляемой сборки, Windows автоматически загружает и инициализирует CLR (если это еще не сделано) для обработки содержащегося в сборке кода. Ясно, что в такой ситуации предполагается, что процесс запущен и работает, и это сокращает область применимости сборки. В частности, управляемая сборка, скомпилированная с параметром / platform: х8б, не сможет загрузиться в 64-разрядном процессе, а исполняемый файл с таким же параметром загрузится в режиме WoW64 на компьютере с 64-разрядной Windows.
Исполнение кода сборки
Как говорилось ранее, управляемые модули содержат метаданные и программный код IL. Это не зависящий от процессора машинный язык, разработанный компанией Microsoft после консультаций с несколькими коммерческими и академическими организациями, специализирующимися на разработке языков и компиляторов. IL — язык более высокого уровня по сравнению с большинством других машинных языков. Он позволяет работать с объектами и имеет команды для создания и инициализации объектов, вызова виртуальных методов и непосредственного манипулирования элементами массивов. В нем даже есть команды инициирования и перехвата исключений для обработки ошибок. IL можно рассматривать как объектно-ориентированный машинный язык'.
Обычно разработчики программируют на высокоуровневых языках, таких как С#, Visual Basic или F#. Компиляторы этих языков генерируют IL-код. Однако такой код может быть написан и на языке ассемблера, так, М icrosoft предоставляет ассемблер IL (ILAsm.exe), а также дизассемблер IL (ILDasm.exe).
Имейте в виду, что любой язык высокого уровня, скорее всего, использует лишь часть возможностей, предоставляемых CLR. При этом язык ассемблера IL открывает доступ ко всем возможностям CLR. Если выбранный вами язык программирования не дает доступа именно к тем функциям CLR, которые необходимы, можно написать часть программного кода на ассемблере IL или на другом языке программирования, позволяющем их задействовать.
Узнать о возможностях CLR, доступных при использовании конкретного языка, можно только при изучении соответствующей документации. В этой книге сделан акцент на возможностях среды CLR и на том, какие из этих возможностей доступны при программировании на С#. Подозреваю, что в других книгах и статьях среда CLR рассматривается с точки зрения других языков и разработчики получают представление лишь о тех ее функциях, которые доступны при использовании описанных там языков. Впрочем, если выбранный язык решает поставленные задачи, такой подход не так уж плох.
ВНИМАНИЕ
Я думаю, что возможность легко переключаться между языками при их тесной интеграции — чудесное качество CLR. К сожалению, я также практически уверен, что разработчики часто будут проходить мимо нее. Такие языки, как C# и Visual Basic, прекрасно подходят для программирования ввода-вывода. Язык APL (A Programming Language) — замечательный язык для инженерных и финансовых расчетов. Среда CLR позволяет написать на C# часть приложения, отвечающую за ввод-вывод, а инженерные расчеты — на языке APL. Среда CLR предлагает беспрецедентный уровень интеграции этих языков, и во многих проектах стоит серьезно задуматься об использовании одновременно нескольких языков.
Для выполнения какого-либо метода его IL-код должен быть преобразован в машинные команды. Этим занимается JIT-компилятор (Just-In-Time) среды CLR.
На рис. 1.4 показано, что происходит при первом вызове метода.
Непосредственно перед исполнением метода Main среда CLR находит все типы данных, на которые ссылается программный код метода Main. При этом CLR выделяет внутренние структуры данных, используемые для управления доступом к типам, на которые есть ссылки. На рис. 1.4 метод Main ссылается на единственный тип — Console, и среда CLR выделяет единственную внутреннюю структуру. Эта внутренняя структура данных содержит по одной записи для каждого метода, определенного в типе Console. Каждая запись содержит адрес, по которому можно найти реализацию метода. При инициализации этой структуры CLR заносит в каждую запись адрес внутренней недокументированной функции, содержащейся в самой среде CLR. Я обозначаю эту функцию HITCompiler.
Рис. 1.4. Первый вызов метода |
Когда метод Main первый раз обращается к методу WriteLine, вызывается функция HITCompiler. Она отвечает за компиляцию IL-кода вызываемого метода в собственные команды процессора. Поскольку IL-код компилируется непосредственно перед выполнением («just in time»), этот компонент CLR часто называют JIT-компилятором.
ПРИМЕЧАНИЕ
Если приложение исполняется в х86 версии Windows или в режиме WoW64, ЛТ- компилятор генерирует команды для архитектуры х86. Для приложений, выполняемых как 64-разрядные в версии х64 Windows, JIT-компилятор генерирует команды х64. Наконец, если приложение выполняется в ARM-версии Windows, ЛТ-компилятор генерирует инструкции ARM.
Функции JITCompiler известен вызываемый метод и тип, в котором он определен. JITCompiler ищет в метаданных соответствующей сборки IL-код вызываемого метода. Затем JITCompiler проверяет и компилирует IL-код в машинные команды, которые сохраняются в динамически выделенном блоке памяти. После этого JITCompiler возвращается к структуре внутренних данных типа, созданной средой CLR, и заменяет адрес вызываемого метода адресом блока памяти, содержащего готовые машинные команды. В завершение JITCompiler передает управление коду в этом блоке памяти. Этот программный код является реализацией метода WriteLine (вариант этого метода с параметром String). Из этого метода управление возвращается в метод Main, который продолжает выполнение в обычном порядке.
Рассмотрим повторное обращение метода Main к методу WriteLine. К этому моменту код метода WriteLine уже проверен и скомпилирован, так что обращение к блоку памяти производится напрямую, без вызова JITCompiler. Отработав, метод WriteLine возвращает управление методу Main. На рис. 1.5 показано, как выглядит ситуация при повторном обращении к методу WriteLine.
|
Рис. 1.5. Повторный вызов метода |
Снижение производительности наблюдается только при первом вызове метода. Все последующие обращения выполняются «на максимальной скорости», потому что повторная верификация и компиляция не производятся.
JIT-компилятор хранит машинные команды в динамической памяти. Это значит, что скомпилированный код уничтожается по завершении работы приложения. Для повторного вызова приложения или для параллельного запуска его второго экземпляра (в другом процессе операционной системы) JIT-компилятору придется заново скомпилировать IL-код в машинные команды. В зависимости от приложения это может привести к существенному повышению затрат памяти по сравнению с низкоуровневыми приложением, у которых находящийся в памяти код доступен только для чтения и совместно используется всеми экземплярами приложения.
Для большинства приложений снижение производительности, связанное с работой JIT-компилятора, незначительно. Большинство приложений раз за разом обращается к одним и тем же методам. На производительности это сказывается только один раз во время выполнения приложения. К тому же выполнение самого метода обычно занимает больше времени, чем обращение к нему.
Также следует учесть, что JIT-компилятор среды CLR оптимизирует машинный код аналогично компилятору неуправляемого кода C++. И опять же: создание оптимизированного кода занимает больше времени, но при выполнении он гораздо производительнее, чем неоптимизированный.
Есть два параметра компилятора С#, влияющих на оптимизацию кода, — /optimize и /debug. В следующей таблице показано их влияние на качество IL-кода, созданного компилятором С#, и машинного кода, сгенерированного JIT- компилятором.
Параметры компилятора | Качество IL-кода компилятора | Качество машинного ЛТ-кода |
/optimize-/debug- (по умолчанию) | Неоптимизированный | Оптимизированный |
/optimize-/debug(+/full/ pdbonly) | Неоптимизированный | Неоптимизированный |
/optimizer/debug(-/+/full/ pbdonly) | Оптимизированный | Оптимизированный |
|
С параметром /optimize- компилятор C# генерирует неоптимизированный IL- код, содержащий множество пустых команд (no-operation, NOP). Эти команды предназначены для поддержки функции «Изменить и продолжить» (edit-and-continue) в Visual Studio во время процесса отладки. Они также упрощают процесс отладки, позволяя расставлять точки останова (breakpoints) на управляющих командах, таких как for, while, do, if, else, а также блоках try, catch и finally. Во время оптимизации IL-кода компилятор C# удаляет эти посторонние команды, усложняя процесс отладки кода, но зато оптимизируя поток управления программой. Кроме того, возможно, некоторые оценочные функции не выполняются во время отладки. Однако IL-код меньше по размерам, и это уменьшает результирующий размер ЕХЕ- или DLL-файлов; кроме того, IL-код легче читать тем, кто обожает исследовать IL-код, пытаясь понять, что именно породил компилятор (например, мне).
Кроме того, компилятор строит файл PDB (Program Database) только при задании параметра /debug(+/full/pdbonly). Файл PDB помогает отладчику находить локальные переменные и связывать команды IL с исходным кодом. Параметр /debug: full сообщает JIT-компилятору о том, что вы намерены заняться отладкой сборки; JIT-компилятор сохраняет информацию о том, какой машинный код был сгенерирован для каждой команды IL. Это позволяет использовать функцию JIT-отладки Visual Studio для связывания отладчика с уже работающим процессом и упрощения отладки кода. Без параметра /debug: full компилятор по умолчанию не сохраняет информацию о соответствии между IL и машинным кодом; это несколько ускоряет компиляцию и сокращает затраты памяти. Если запустить процесс в отладчике Visual Studio, то JIT-компилятор будет отслеживать информацию о соответствии IL и машинного кода (независимо от состояния параметра / debug), если только вы не снимете флажок Suppress JIT Optimization On Module Load (Managed Only) в Visual Studio.
При создании нового проекта C# в Visual Studio в отладочной конфигурации проекта устанавливаются параметры /optimize и /debug: full, а в конфигурации выпуска - параметры /optimize+ и /debug:pdbonly.
Разработчиков с опытом написания неуправляемого кода С или C++ обычно беспокоит, как все это сказывается на быстродействии. Ведь неуправляемый код компилируется для конкретного процессора и при вызове может просто выполняться. В управляемой среде компиляция кода состоит из двух фаз. Сначала компилятор проходит по исходному коду, выполняя максимально возможную работу по генерированию IL-кода. Но для выполенния кода сам IL-код должен быть откомпилирован в машинные команды во время выполнения, что требует выделения дополнительной памяти, которая не может использоваться совместно, и дополнительных затрат процессорного времени.
Я и сам пришел к CLR с опытом программирования на C/C++ и меня сильно беспокоили дополнительные затраты. Действительно, вторая стадия компиляции, происходящая во время выполнения, замедляет выполнение и требует выделения динамической памяти. Однако компания Microsoft основательно потрудилась над оптимизацией, чтобы свести эти дополнительные затраты к минимуму.
Если вы тоже скептически относитесь к двухфазной компиляции, обязательно попробуйте построить приложения и измерьте их быстродействие. Затем проделайте то же самое с какими-нибудь нетривиальными управляемыми приложениями, созданными Microsoft или другими компаниями. Вас удивит, насколько эффективно они выполняются.
Трудно поверить, но многие специалисты (включая меня) считают, что управляемые приложения способны даже превзойти по производительности неуправляемые приложения. Это объясняется многими причинами. Например, в тот момент, когда JIT-компилятор компилирует IL-код в машинный код во время выполнения, он знает о среде выполнения больше, чем может знать неуправляемый компилятор. Перечислим некоторые возможности повышения производительности управляемого кода по сравнению с неуправляемым:
□ JIT-компилятор может определить, что приложение выполняется на процессоре Intel Pentium 4, и сгенерировать машинный код со специальными командами, поддерживаемыми Pentium 4. Обычно неуправляемые приложения компилируются с самым общим набором команд и не используют специальные команды, способные повысить эффективность приложения.
□ JIT-компилятор может определить, что некоторое условие на том компьютере, на котором он выполняется, всегда оказывается ложным. Допустим, метод содержит следующий фрагмент:
if (numberOfCPUs > 1) {
1
Если компьютер оснащен всего одним процессором, то JIT-компилятор не будет генерировать машинные команды для указанного фрагмента. В этом случае машинный код оптимизируется для конкретной машины, а следовательно, занимает меньше места и быстрее выполняется.
□ CLR может профилировать выполняемую программу и перекомпилировать IL в машинный код в процессе выполнения. Перекомпилированный код реорганизуется для сокращения ошибочного прогнозирования переходов на основании наблюдаемых закономерностей выполнения. Текущие версии CLR такую возможность не поддерживают, но возможно, она появится в будущих версиях.
Это лишь некоторые из причин, по которым в будущем управляемый код может превзойти по производительности неуправляемый код. Как я уже сказал, в большинстве приложений достигается вполне неплохая производительность и в будущем стоит ожидать ее улучшения.
Если ваши эксперименты показывают, что JIT-компилятор не обеспечивает вашему приложению необходимого уровня производительности, возможно, вам стоит воспользоваться утилитой NGen.exe из пакета .NET Framework SDK. Эта утилита компилирует весь IL-код сборки в машинный код и сохраняет его в файле на диске. В момент выполнения при загрузке сборки CLR автоматически проверяет, существует ли заранее откомпилированная версия сборки, и если существует — загружает ее, так что компиляция во время выполнения уже не требуется. Учтите, что NGen.exe приходится осторожно строить предположения относительно фактической среды выполнения, поэтому код, генерируемый NGen.exe, будет менее оптимизированным, чем код JIT-компилятора.
Также при анализе производительности может пригодиться класс System. Runtime.ProfileOptimization. Он заставляет CLR сохранить (в файле) информацию о том, какие методы проходят JIT-компиляцию во время выполнения приложения. Если машина, на которой работает приложение, оснащена несколькими процессорами, при будущих запусках приложения JIT-компилятор параллельно компилирует эти методы в других программных потоках. В результате приложение работает быстрее, потому что несколько методов компилируются параллельно, причем это происходит во время инициализации приложения (вместо JIT-компиляции).
IL-код и верификация
IL является стековым языком; это означает, что все его инструкции заносят операнды в исполнительный стек и извлекают результаты из стека. IL не содержит инструкций для работы с регистрами, и это упрощает создание новых языков и компиляторов, генерирующих код для CLR.
Инструкции IL также являются нетипизованными. Например, в IL имеется инструкция для сложения двух последних операндов, занесенных в стек. У инструкции сложения нет двух раздельных версий (32-разрядной и 64-разрядной). При выполнении инструкция сложения определяет типы операндов, хранящихся в стеке, и выполняет соответствующую операцию.
Однако, на мой взгляд, самое большое преимущество IL-кода состоит даже не в том, что он абстрагирует разработчика от конкретного процессора. IL-код обеспечивает безопасность приложения и его устойчивость перед ошибками. В процессе компиляции IL в машинные инструкции CLR выполняется процедура, называемая верификацией — анализ высокоуровневого кода IL и проверка безопасности всех операций. Например, верификация убеждается в том, что каждый метод вызывается с правильным количеством параметров, что все передаваемые параметры имеют правильный тип, что возвращаемое значение каждого метода используется правильно, что каждый метод содержит инструкцию return и т. д. Вся информация о методах и типах, используемая в процессе верификации, хранится в метаданных управляемого модуля.
В системе Windows каждый процесс обладает собственным виртуальным адресным пространством. Необходимость разделения адресных пространств объясняется тем, что код приложения в принципе ненадежен. Ничто не мешает приложению выполнить операцию чтения или записи по недопустимому адресу памяти (и к сожалению, это часто происходит на практике). Размещение процессов Windows в изолированных адресных пространствах обеспечивает защищенность и стабильность системы; один процесс не может повредить другому процессу.
Однако верификация управляемого кода гарантирует, что код не будет некорректно обращаться к памяти и не сможет повредить выполнению кода другого приложения. Это означает, что вы можете запустить несколько управляемых приложений в одном виртуальном адресном пространстве Windows.
Так как процессы Windows требуют значительных затрат ресурсов операционной системы, избыток их в системе снижает производительность и ограничивает доступные ресурсы. Сокращение количества процессов за счет запуска нескольких приложений в одном процессе операционной системы улучшает производительность, снижает затраты ресурсов и обеспечивает такой же уровень защиты, как если бы каждое приложение располагало собственным процессом. Это еще одно преимущество управляемого кода по сравнению с неуправляемым.
Итак, CLR предоставляет возможность выполнения нескольких управляемых приложений в одном процессе операционной системы. Каждое управляемое приложение выполняется в домене приложений (AppDomain). По умолчанию каждый управляемый ЕХЕ-файл работает в отдельном адресном пространстве, состоящем из одного домена. Тем не менее процесс, обеспечивающий размещение (хостинг) CLR — например, IIS (Internet Information Services) или Microsoft SQL Server, — может запустить несколько доменов приложений в одном процессе операционной системы. Обсуждению доменов приложений посвящена одна из частей главы 22.
Небезопасный код
По умолчанию компилятор C# компании Microsoft генерирует безопасный код. Под этим термином понимается код, безопасность которого подтверждается в процессе верификации. Тем не менее компилятор Microsoft C# также позволяет разработчикам писать небезопасный код, способный напрямую работать с адресами памяти и манипулировать с байтами по этим адресам. Как правило, эти чрезвычайно мощные средства применяются для взаимодействия с неуправляемым кодом или для оптимизации алгоритмов, критичных по времени.
Однако использование небезопасного кода создает значительный риск: небезопасный код может повредить структуры данных и использовать (или даже создавать) уязвимости в системе безопасности. По этой причине компилятор C# требует, чтобы все методы, содержащие небезопасный код, помечались ключевым словом unsafe, а при компиляции исходного кода использовался параметр компилятора /unsafe.
Когда JIT-компилятор пытается откомпилировать небезопасный метод, он сначала убеждается в том, что сборке, содержащей метод, были предоставлены разрешения System. Security .Permissions. SecurityPermission с установленным флагом SkipVerification из перечисления System.Security.Permissions. SecurityPermissionFlag. Если флаг установлен, JIT-компилятор компилирует небезопасный код и разрешает его выполнение. CLR доверяет этому коду и надеется, что прямой доступ к памяти и манипуляции с байтами не причинят вреда. Если флаг не установлен, JIT-компилятор выдает исключение System. InvalidProgramException или System.Security.VerificationException,предотвращая выполнение метода. Скорее всего, в этот момент приложение аварийно завершится, но по крайней мере без причинения вреда.
Компания Microsoft предоставляет утилиту PEVerify.exe, которая проверяет все методы сборки и сообщает обо всех методах, содержащих небезопасный код. Возможно, вам стоит запустить PEVerify.exe для всех сборок, на которые вы ссылаетесь; это позволит узнать о возможных проблемах с запуском ваших приложений по интрасети или Интернету.
ПРИМЕЧАНИЕ
По умолчанию сборки, загружаемые с локальной машины или по сети, обладают полным доверием; это значит, что им разрешено выполнение чего угодно, включая небезопасный код. Однако по умолчанию сборки, выполняемые по Интернету, не получают разрешений на выполнение небезопасного кода. Если они содержат небезопасный код, выдается одно из упомянутых исключений. Администратор или конечный пользователь может изменить эти настройки по умолчанию, однако в этом случае он несет полную ответственность за поведение этого кода.
Следует учитывать, что верификация требует доступа к метаданным, содержащимся во всех зависимых сборках. Таким образом, когда вы используете РЕ Verify для проверки сборки, программа должна быть способна найти и загрузить все упоминаемые сборки. Так как PEVerify использует CLR для поиска зависимых сборок, при этом используются те же правила привязки и поиска, которые обычно применяются при исполнении сборок. Эти правила будут рассмотрены в главах 2 и 3.
IL и защита интеллектуальной собственности
Некоторых разработчиков беспокоит, что IL не обеспечивает достаточного уровня защиты интеллектуальной собственности для их алгоритмов. Иначе говоря, они полагают, что кто-то другой может воспользоваться дизассемблером IL, взять построенный ими управляемый модуль и легко восстановить логику кода приложения.
Да, IL-код работает на более высоком уровне, чем большинство других ассемблеров, и в общем случае дизассемблирование IL-кода выполняется относительно просто. Однако при реализации кода, работающего на стороне сервера (веб-служба, веб-форма или хранимая процедура), сборка находится на сервере. Поскольку посторонний не сможет обратиться к сборке, он не сможет и воспользоваться любыми программами для просмотра IL — ваша интеллектуальная собственность в полной безопасности.
Если вас беспокоят распространяемые сборки, используйте «маскировочные» утилиты от независимых разработчиков. Такие программы шифруют все закрытые символические имена в метаданных сборки. Постороннему будет трудно расшифровать такое имя и понять назначение каждого метода. Учтите, что маскировка предоставляет лишь относительную защиту, потому что среда CLR должна в какой- то момент получить доступ к IL-коду для его JIT-компиляции.
Если вы не считаете, что маскировка обеспечивает желаемый уровень защиты интеллектуальной собственности, рассмотрите возможность реализации более секретных алгоритмов в неуправляемом модуле, содержащем машинные команды вместо IL и метаданных. После этого вы сможете использовать средства взаимодействия CLR (при наличии достаточных разрешений) для работы с неуправляемыми частями ваших приложений. Конечно, такое решение предполагает, что вас не беспокоит возможность дизассемблирования машинных команд неуправляемого кода.
Библиотека FCL 47
NGen.exe
Программа NGen.exe, входящая в поставку .NET Framework, может использоваться для компиляции IL-кода в машинный код при установке приложения на машине пользователя. Так как код компилируется на стадии установки, JIT-компилятору CLR не приходится компилировать его во время выполнения, что может улучшить быстродействие приложения. nporpaMMaNGen.exe полезна в двух ситуациях.
Ускорение запуска приложения. Запуск NGen.exe ускоряет запуск, потому что код уже откомпилирован в машинную форму, и компиляцию не нужно выполнять на стадии выполнения.
Сокращение рабочего набора приложения. Если вы ожидаете, что сборка будет загружаться в нескольких процессах одновременно, обработка ее программой NGen.exe может сократить рабочий набор приложения. Дело в том, что NGen.exe преобразует IL в машинный код и сохраняет результат в отдельном файле. Этот файл может отображаться на память в нескольких адресных пространствах одновременно, а код будет использоваться совместно, без использования каждым процессом собственного экземпляра кода.
Библиотека FCL
Одним из компонентов .NET Framework является FCL (Framework Class Library) — набор сборок в формате DLL, содержащих несколько тысяч определений типов, каждый из которых предоставляет некоторую функциональность. Компания Microsoft разрабатывает дополнительные библиотеки — такие, как Windows Azure SDK и DirectX SDK. Эти библиотеки содержат еще больше типов, предоставляя в ваше распоряжение еще больше функциональности. Сейчас, когда Microsoft с феноменальной скоростью выпускает огромное количество библиотек, разработчикам стало как никогда легко использовать технологии Microsoft.
Ниже перечислены некоторые разновидности приложений, которые могут создаваться разработчиками при помощи этих сборок:
Веб-службы. Технологии Microsoft ASP.NET XML Web Service и Windows Communication Foundation (WCF) позволяют очень легко создавать методы для обработки сообщений, передаваемых по Интернету.
Приложения Web Forms/приложения MVC на базе HTML. Как правило, приложения ASP.NET обращаются с запросами к базам данных и вызовами к вебслужбам, объединяют и фильтруют полученную информацию, а затем представляют ее в браузере с использованием расширенного пользовательского интерфейса на базе HTML.
Приложения Windows с расширенным графическим интерфейсом. Вместо реализации пользовательского интерфейса приложений в виде веб-страниц можно использовать более мощную и высокопроизводительную функциональность,
предоставляемую технологиями Windows Store, WPF (Windows Presentation Foundation) и Windows Forms. Такие приложения могут использовать события элементов управления, меню, сенсорного экрана, мыши, пера и клавиатуры, а также могут обмениваться информацией с операционной системой, выдавать запросы к базам данных и пользоваться веб-службами.
Консольные приложения Windows. Консольные приложения простой и быстрый вариант для создания приложений с минимальными потребностями в пользовательском интерфейсе. Компиляторы, утилиты и вспомогательные инструменты часто реализуются в виде консольных приложений.
Службы Windows. Да, теперь стало возможным построение служб (services), управляемых через Windows SCM (Service Control Manager) с использованием .NET Framework.
Хранимые процедуры баз данных. Серверы баз данных Microsoft SQL Server, IBM DB2 и Oracle дают возможность разработчикам писать свои хранимые процедуры с использованием .NET Framework.
Библиотеки компонентов. .NET Framework позволяет создавать автономные сборки (компоненты) с типами, легко встраиваемыми в приложения всех упоминавшихся разновидностей.
ВНИМАНИЕ
В Visual Studio также предусмотрен тип проекта Portable Class Library для создания сборок библиотек классов, работающих с разными видами приложений, включая классические приложения .NET Framework, приложений Silverlight, Windows Phone, Windows Store и Xbox 360.
Так как FCL содержит буквально тысячи типов, взаимосвязанные типы объединяются в одно пространство имен. Например, пространство имен System (которое вам стоит изучить как можно лучше) содержит базовый тип Object — «предок» всех остальных типов в системе. Кроме того, пространство имен System содержит типы для целых чисел, символов, строк, обработки исключений и консольного ввода-вывода, а также набор вспомогательных типов, осуществляющих безопасные преобразования между типами данных, форматирование, генерирование случайных чисел и выполняющих математические функции. Все приложения используют типы из пространства имен System.
Чтобы использовать возможности FCL, необходимо знать, какое пространство имен содержит типы, предоставляющие нужную функциональность. Многие типы поддерживают настройку своего поведения; для этого тип просто объявляется производным от нужного типа FCL. Объектно-ориентированная природа платформы проявляется в том, как .NET Framework предоставляет разработчикам единую парадигму программирования. Кроме того, разработчик может легко создавать собственные пространства имен, содержащие его типы. Эти пространства и типы легко интегрируются в парадигму программирования. По сравнению с парадигмой программирования Win32 новый подход значительно упрощает процесс разработки.
Большинство пространств имен в FCL содержит типы, которые могут использоваться в приложениях любых видов. В табл. 1.3 перечислены некоторые общие пространства имен и основные области применения типов этих пространств. Это очень маленькая выборка доступных пространств — чтобы больше узнать о постоянно расширяющемся множестве пространств имен, создаваемых компанией Microsoft, обращайтесь к документации различных пакетов Microsoft SDK.
Таблица 1.3. Некоторые пространства имен FCL
|
|
Эта книга посвящена CLR и типам общего назначения, тесно взаимодействующим с CLR. Таким образом, ее содержимое актуально для всех программистов, занимающихся разработкой приложений и компонентов для CLR. О конкретных разновидностях приложений — веб-служб, приложений Web Forms/MVC, WPF и т. д. — написано много замечательных книг, которые станут хорошей отправной точкой для разработки ваших собственных приложений. В этой книге я предоставляю информацию, которая относится не к конкретному типу приложений, а к платформе разработки. Прочитав эту книгу вместе с другой книгой, посвященной конкретным приложениям, вы сможете легко и эффективно создать приложение нужного типа.
CTS
Вероятно, вы уже поняли, что самое важное в CLR — типы, предоставляющие функциональность вашим приложениям и другим типам. Механизм типов позволяет коду, написанному на одном языке программирования, взаимодействовать с кодом, написанным на другом языке. Поскольку типы занимают центральное место в CLR, компания Microsoft разработала формальную спецификацию CTS (Common Туре System), которая описывает способ определения и поведение типов.
ПРИМЕЧАНИЕ
Компания Microsoft предоставляет CTS вместе с другими частями .NET Framework (форматы файлов, метаданные, IL, механизм вызова P/Invoke и т. д.) в органкоми- тет ЕСМА с целью стандартизации. Стандарт называется CLI (Common Language Infrastructure) и определяется спецификацией ЕСМА-335. Кроме того, компания Microsoft предоставила отдельные части FCL, язык программирования C# (ЕСМА- 334) и язык программирования C++/CLI. Информация об этих отраслевых стандартах доступна на сайте ЕСМА по адресу http://www.ecma-international.org. Вы также можете обратиться на сайт Microsoft: http://msdn.microsoft.com/en-us/netframework/ aa569283.aspx.
Согласно спецификации CTS, тип может содержать нуль и более членов. Подробные описания всех возможных членов типов приведены в части II книги, а пока я ограничусь краткими вводными описаниями:
□ Поле — переменная, являющаяся частью состояния объекта. Поля идентифицируются именем и типом.
□ Метод — функция, выполняющая операцию с объектом, часто с изменением его состояния. Метод обладает именем, сигнатурой и модификаторами. Сигнатура определяет количество параметров (и порядок их следования), типы параметров, наличие возвращаемого значения, и если оно имеется — тип значения, возвращаемого методом.
□ Свойство — с точки зрения вызывающей стороны выглядит как поле, но в реализации типа представляет собой метод (или два). Свойства позволяют организовать проверку параметров или состояния объекта перед обращением к значению и/или вычислять его значение только при необходимости. Кроме того, они упрощают синтаксис работы с данными и позволяют создавать «поля», доступные только для чтения или записи.
□ Событие — используется для создания механизма оповещения между объектом и другими заинтересованными объектами. Например, кнопка может поддерживать событие, оповещающее другие объекты о щелчке на ней.
CTS также задает правила видимости типов и доступа к членам типа. Например, помечая тип как открытый (ключевое слово public), вы тем самым экспортируете этот тип, делая его видимым и доступным для любой сборки. С другой стороны, пометка типа на уровне сборки (ключевое слово internal в С#) делает его видимым и доступным для кода той же сборки. Таким образом, CTS устанавливает правила, по которым сборки формируют границу видимости типа, a CLR обеспечивает выполнение правил видимости.
Тип, видимый для вызывающей стороны, может установить дополнительные ограничения на возможность обращения к своим членам. Ниже перечислены варианты ограничения доступа к членам типа:
□ Закрытый (приватный) доступ — член типа доступен только для других членов того же типа.
□ Доступ в семействе — член типа доступен для производных типов независимо от того, принадлежат ли они той же сборке или нет. Обратите внимание: во многих языках (таких, как C# и C++) доступ в семействе обозначается ключевым словом protected.
□ Доступ в семействе и сборке — член типа доступен для производных типов, но
только в том случае, если они определяются в той же сборке. Многие языки (например, C# и Visual Basic) не поддерживают этот уровень доступа. Разумеется, в IL-коде он поддерживается.
□ Доступ в сборке — член типа доступен для любого кода, входящего в ту же сборку. Во многих языках доступ в сборке обозначается ключевым словом internal.
□ Доступ в семействе или сборке — член типа доступен для производных типов из любой сборки, а также для любых типов в той же сборке. В C# этот вариант доступа обозначается ключевыми словами protected internal.
□ Открытый доступ — член типа доступен для любого кода в любой сборке.
Кроме того, CTS определяет правила, управляющие наследованием, работой виртуальных методов, сроком жизни объектов и т. д. Эти правила разрабатывались для выражения семантики, выражаемой средствами современных языков программирования. Собственно, вам вообще не придется изучать правила CTS как таковые, потому что выбранный вами язык предоставляет собственный синтаксис и правила работы с типами. Синтаксис конкретного языка преобразуется в IL, «язык» CLR, в процессе генерирования сборки на стадии компиляции.
Когда я только начал работать с CLR, довольно быстро выяснилось, что язык и поведение кода лучше рассматривать как две разные сущности. Используя C++/ CLI, вы можете определять собственные типы с нужным набором членов. Конечно, для определения того же типа с теми же членами можно также использовать C# или Visual Basic. Конечно, синтаксис определения типа зависит от выбранного языка, но поведение типа остается неизменным, потому что оно определяется спецификацией CTS.
Чтобы сказанное стало более понятным, я приведу пример. CTS позволяет типу быть производным только от одного базового класса. И хотя язык C++ поддерживает возможность наследования от нескольких базовых типов, CTS не примет такие классы и не будет работать с ними. Обнаружив попытку создания управляемого кода с типом, производным от нескольких базовых типов, компилятор Microsoft C++/CLI выдает сообщение об ошибке.
А вот еще одно правило CTS: все типы должны быть производными (прямо или опосредованно) от предопределенного типа System.Object (то есть от типа Object из пространства имен System). Тип Object является корнем иерархии типов, а следовательно, гарантирует, что каждый экземпляр типа обладает минимальным набором аспектов поведения. А если говорить конкретнее, тип System.Object позволяет сделать следующее:
□ сравнить два экземпляра на равенство;
□ получить хеш-код экземпляра;
□ запросить фактический тип экземпляра;
□ выполнить поверхностное (поразрядное) копирование экземпляра;
□ получить строковое представление текущего состояния экземпляра.
CLS
Модель СОМ позволяет объектам, написанным на разных языках, взаимодействовать друг с другом. С другой стороны, среда CLR интегрирует все языки и обеспечивает возможность равноправного использования объектов, написанных на одном языке, в коде на совершенно другом языке. Такая интеграция стала возможной благодаря стандартному набору типов CLR, метаданным (самодокументирующей информацией о типах) и общей исполнительной среде.
Хотя языковая интеграция — совершенно замечательная цель, по правде говоря, языки программирования очень сильно отличаются друг от друга. Например, некоторые языки не учитывают регистр символов в именах, другие не поддерживают целые числа без знака, перегрузку операторов или методы с поддержкой переменного количества аргументов.
Если вы намереваетесь создавать типы, с которыми можно легко работать из других языков программирования, вам придется использовать только те возможности вашего языка, которые заведомо доступны во всех остальных языках. Для упрощения этой задачи компания Microsoft определила спецификацию CLS (Common Language Speciication); в ней перечислен минимальный набор возможностей, которые должны поддерживаться компилятором для генерирования типов, совместимых с другими компонентами, написанными на других CLS-совместимых языках на базе CLR.
Возможности CLR/CTS выходят далеко за рамки подмножества, определяемого CLS. Если вас не беспокоит межъязыковая совместимость, вы можете разрабатывать типы с широкой функциональностью, ограничиваемой только возможностями языка. А если говорить конкретнее, CLS определяет правила, которым должны соответствовать типы и методы с внешней видимостью, для того чтобы они могли использоваться в любом CLS-совместимом языке программирования. Обратите внимание: правила CLS не распространяются на код, доступный только в опре-
деляющей сборке. На рис. 1.6 наглядно представлены концепции, выраженные в этом абзаце.
Как видно из рис. 1.6, CLR/CTS определяет набор функциональных возможностей. Некоторые языки реализуют более широкое подмножество CLR/CTS. Например, программист, пожелавший работать на языке ассемблера IL, сможет использовать все возможности CLR/CTS. Большинство других языков (С#, Visual Basic, Fortran и т. д.) предоставляют в распоряжение программиста подмножество возможностей CLR/CTS. CLS определяет минимальный набор возможностей, которые должны поддерживаться всеми языками. Если вы проектируете тип на одном языке и собираетесь использовать его в другом языке, не размещайте никакие возможности, выходящие за пределы CLS, в его открытых и защищенных членах. В этом случае члены вашего типа могут стать недоступными для программистов, пишущих код на других языках программирования.
В следующем коде CLS-совместимый тип определяется в коде С#. Однако при этом тип содержит несколько CLS-несовместимых конструкций, из-за которых компилятор C# выдает предупреждения.
using System;
// Приказываем компилятору проверять код // на совместимость с CLS [assembly: CLSCompliant(true)]
namespace SomeLibrary {
// Предупреждения выводятся, потому что класс является открытым public sealed class SomeLibraryType {
// Предупреждение: возвращаемый тип 'SomeLibrary.SomeLibraryType.Abc()'
// не является CLS-совместимым public UInt32 Abc() { return 0; }
// Предупреждение: идентификаторы 'SomeLibrary.SomeLibraryType.abc()',
// отличающиеся только регистром символов, не являются // CLS-совместимыми public void abc() { }
// Предупреждения нет: закрытый метод private UInt32 АВС() { return 0; }
}
}
В этом коде атрибут [assembly:CLSCompliant(true)] применяется к сборке. Этот атрибут приказывает компилятору следить за тем, чтобы тип с открытым уровнем доступа не содержал конструкций, препятствующих его использованию в другом языке программирования. При компиляции этого кода компилятор C# выдает два предупреждения. Первое выдается из-за того, что метод Abe возвращает целое без знака; некоторые языки программирования не умеют работать с беззнаковыми целыми числами. Второе предупреждение выдается из-за того, что тип содержит два открытых метода, различающихся только регистром и типом возвращаемого значения: Abe и abc. В Visual Basic и некоторых других языках вызов обоих методов невозможен.
Если удалить ключевое слово public перед sealed class SomeLibraryType
и перекомпилировать код, оба предупреждения пропадают. Дело в том, что тип SomeLibraryType по умолчанию рассматривается как internal, а следовательно, становится недоступным за пределами сборки. Полный список правил CLS приведен в разделе «Cross-Language Interoperability» документации .NET Framework SDK (http://msdn.microsoft.com/en-us/library/730f1wy3.aspx).
Позвольте мне изложить правила CLS в предельно упрощенном виде. В CLR каждый член типа является либо полем (данные), либо методом (поведение). Это означает, что каждый язык программирования должен уметь обращаться к полям и вызывать методы. Некоторые поля и некоторые методы используются специальным образом. Для упрощения программирования языки обычно предоставляют дополнительные абстракции, упрощающие реализацию этих стандартных паттернов — перечисления, массивы, свойства, индексаторы, делегаты, события, конструкторы, финализаторы, перегрузки операторов, операторы преобразования и т. д. Когда компилятор встречает эти абстракции в исходном коде, он должен преобразовать их в поля и методы, чтобы сделать их доступными для CLR и любых других языков программирования.
Следующее определение типа содержит конструктор, финализатор, перегруженные операторы, свойство, индексатор и событие. Учтите, что приведенный код написан всего лишь для того, чтобы он компилировался, и не демонстрирует правильного способа реализации типа.
using System;
internal sealed class Test {
// Конструктор public Test() {}
// Финализатор -Test() {}
// Перегрузка оператора
public static Boolean operator == (Test tl, Test t2) { return true;
>
public static Boolean operator != (Test tl. Test t2) { return false;
// Перегрузка оператора
public static Test operator + (Test tl, Test t2) { return null; } // Свойство
public String AProperty { get { return null; } set { >
// Индексатор
public String this[Int32 x] { get { return null; }
set { }
// Событие
public event EventHandler AnEvent;
Результатом компиляции этого кода является тип, содержащий набор полей и методов. В этом можно легко убедиться, просмотрев полученный управляемый модуль в программе IL Disassembler (ILDasm.exe), входящей в пакет .NET Framework SDK (рис. 1.7).
В табл. 1.4 продемонстрировано соответствие между конструкциями языка программирования и эквивалентными полями/методами CLR.
Дополнительные узлы типа Test, не упомянутые в табл. 1.4— .class, .custom, AnEvent, AProperty и Item, — содержат дополнительные метаданные типа. Они не отображаются на поля или методы, а только предоставляют дополнительную информацию о типе, которая может использоваться CLR, языками программирования или инструментами. Например, программа может узнать, что тип Test поддерживает событие AnEvent, для работы с которым используются два метода (add_AnEvent и remove_AnEvent).
Рис. 1.7. Программа ILDasm с полями и методами типа Test (информация получена из метаданных) |
Таблица 1.4. Поля и методы типа Test
|
|
Взаимодействие с неуправляемым кодом
.NET Framework обладает множеством преимуществ перед другими платформами разработки. Впрочем, лишь немногие компании могут позволить себе заново спроектировать и реализовать весь существующий код. Компания Microsoft понимает это, поэтому среда CLR была спроектирована так, чтобы приложения могли состоять как из управляемых, так и из неуправляемых компонентов. А если говорить конкретнее, CLR поддерживает три сценария взаимодействий:
Управляемый код может вызывать неуправляемые функции из DLL с использованием механизма P/Invoke (сокращение от «Platform Invoke»). В конце концов, многие типы, определяемые в FCL, во внутренней реализации вызывают функции, экспортируемые из Kernel32.dll, User32.dll и т. д. Многие языки программирования предоставляют средства, упрощающие вызов неуправляемых функций из DLL в управляемом коде. Например, приложение C# может вызвать функцию CreateSemaphore, экспортируемую библиотекой Kernel32.dll.
Управляемый код может использовать готовые компоненты СОМ. Многие компании уже реализовали большое количество неуправляемых компонентов СОМ. На основе библиотек типов из этих компонентов можно создать управляемую сборку с описанием компонента СОМ. Управляемый код обращается к типу из управляемой сборки точно так же, как к любому другому управляемому типу. За дополнительной информацией обращайтесь к описанию программы Tlblmp.exe, входящей в поставку .NET Framework SDK.
Неуправляемый код может использовать управляемый тип. Большая часть существующего неуправляемого кода требует наличия компонента СОМ. Такие компоненты гораздо проще реализуются с управляемым кодом, что позволяет избежать служебного кода, связанного с подсчетом ссылок и интерфейсами. Например, на C# можно написать элемент управления ActiveX или расширение командного процессора. За дополнительной информацией обращайтесь к описанию программ TlbExp.exe и RegAsm.exe, входящих в поставку .NET Framework SDK.
ПРИМЕЧАНИЕ
Чтобы помочь разработчикам в написании программ, взаимодействующих с машинным кодом, компания Microsoft опубликовала исходный код программ Type Library Importer и P/Invoke Interop Assistant. Эти программы и их исходный код можно загрузить по адресу http://CLRInterop.CodePlex.com/.
В Windows 8 компания Microsoft ввела новый интерфейс прикладного программирования, называемый Windows Runtime (WinRT). Его внутренняя реализация базируется на компонентах СОМ, но вместо библиотеки типов компоненты СОМ описывают свой API в стандарте метаданных ЕСМА, созданном рабочей группой .NET Framework. Элегантность решения заключается в том, что код, написанный на языке .NET, может (в основном) легко взаимодействовать с WinRT API. CLR обеспечивает все взаимодействие с СОМ во внутренней реализации, вам вообще не придется использовать дополнительные средства — все просто работает! За подробностями обращайтесь к главе 25.
Глава 2. Компоновка, упаковка, развертывание и администрирование приложений и типов
Прежде чем перейти к главам, описывающим разработку программ для Microsoft .NET Framework, давайте обсудим вопросы создания, упаковки и развертывания приложений и их типов. В этой главе акцент сделан на основах создания компонентов, предназначенных исключительно для ваших приложений. В главе 3 рассказано о ряде более сложных, но очень важных концепций, в том числе способах создания и применения сборок, содержащих компоненты, предназначенные для использования совместно с другими приложениями. В этой и следующей главах также показано, как администратор может влиять на исполнение приложения и его типов.
Современные приложения состоят из типов, которые создаются самими разработчиками или компанией Microsoft. Помимо этого, процветает целая отрасль поставщиков компонентов, которые используются другими компаниями для ускорения разработки проектов. Типы, реализованные при помощи языка, ориентированного на общеязыковую исполняющую среду (CLR), способны легко работать друг с другом; при этом базовый класс такого типа может быть написан на другом языке программирования.
В этой главе объясняется, как эти типы создаются и упаковываются в файлы, предназначенные для развертывания. В процессе изложения дается краткий исторический обзор некоторых проблем, решенных с приходом .NET Framework.
Задачи развертывания в .NET Framework
Все годы своего существования операционная система Windows «славилась» нестабильностью и чрезмерной сложностью. Такая репутация, заслуженная или нет, сложилась по ряду причин. Во-первых, все приложения используют динамически подключаемые библиотеки (Dynamic Link Library, DLL), созданные Microsoft и другими производителями. Поскольку приложение исполняет код, написанный разными производителями, ни один разработчик какой-либо части программы не может быть на 100 % уверен в том, что точно знает, как другие собираются применять созданный им код. В теории такая ситуация чревата любыми неполадками, но на
практике взаимодействие кодов от разных производителей редко создает проблемы, так как перед развертыванием приложения тестируют и отлаживают.
Однако пользователи часто сталкиваются с проблемами, когда производитель решает обновить поставленную им программу и предоставляет новые файлы. Предполагается, что новые файлы обеспечивают «обратную совместимость» с прежним программным обеспечением, но кто за это поручится? Одному производителю, выпускающему обновление своей программы, фактически не под силу заново протестировать и отладить все существующие приложения, чтобы убедиться, что изменения при обновлении не влекут за собой нежелательных последствий.
Уверен, что каждый читающий эту книгу сталкивался с той или иной разновидностью проблемы, когда после установки нового приложения нарушалась работа одной (или нескольких) из установленных ранее программ. Эта проблема, наводящая ужас на рядовых пользователей компьютеров, получила название «кошмар DLL». В конечном итоге пользователи должны как следует обдумать, стоит ли устанавливать новое программное обеспечение на их компьютеры. Лично я вообще отказался от установки некоторых приложений из опасения, что они нанесут вред наиболее важным для меня программам.
Второй фактор, повлиявший на репутацию Windows, — сложности при установке приложений. Большинство приложений при установке умудряются «просочиться» во все части операционной системы. Например, при установке приложения происходит копирование файлов в разные каталоги, модификация параметров реестра, установка ярлыков и ссылок на рабочий стол (Desktop), в меню Пуск (Start) и на панель быстрого запуска. Проблема в том, что приложение — это не одиночная изолированная сущность. Нельзя легко и просто создать резервную копию приложения, поскольку, кроме файлов приложения, придется скопировать соответствующие части реестра. Вдобавок, нельзя просто взять и переместить приложение с одной машины на другую — для этого нужно запустить программу установки еще раз, чтобы корректно скопировать все файлы и параметры реестра. Наконец, приложение не всегда просто удалить — нередко выясняется, что какая-то его часть притаилась где-то внутри компьютера.
Третий фактор — безопасность. При установке приложений записывается множество файлов, созданных самыми разными компаниями. Вдобавок, многие веб-приложения (например, ActiveX) зачастую содержат программный код, который сам загружается из Интернета, о чем пользователи даже не подозревают. На современном уровне технологий такой код может выполнять любые действия, включая удаление файлов и рассылку электронной почты. Пользователи справедливо опасаются устанавливать новые приложения из-за угрозы потенциального вреда, который может быть нанесен их компьютерам. Для того чтобы пользователи чувствовали себя спокойнее, в системе должны быть встроенные функции защиты, позволяющие явно разрешать или запрещать доступ к системным ресурсам коду, созданному теми или иными компаниями.
Как показано в этой и следующей главах, платформа .NET Framework в значительной мере устраняет «кошмар DLL» и делает существенный шаг вперед к решению проблемы, связанной с распределением данных приложения по всей операционной системе. Например, в отличие от модели СОМ информацию о компонентах уже не нужно сохранять в реестре. К сожалению, приложениям пока еще требуются ссылки и ярлыки. Совершенствование системы защиты связано с новой моделью безопасности платформы .NET Framework — безопасностью доступа на уровне кода (code access security). Если безопасность системы Windows основана на идентификации пользователя, то безопасность доступа на уровне кода основана на правах, которые контролируются хостом приложений, загружающим компоненты. Сетевое приложение (такое, как Microsoft SQL Server) может предоставить коду минимальные полномочия, в то время как локально установленное приложение во время своего выполнения может иметь уровень полного доверия (со всеми полномочиями). Как видите, платформа .NET Framework предоставляет пользователям намного больше возможностей по контролю над тем, что устанавливается и выполняется на их машинах, чем когда-либо давала им система Windows.
Компоновка типов в модуль
В этом разделе рассказывается, как превратить файл, содержащий исходный код с разными типами, в файл, пригодный для развертывания. Для начала рассмотрим следующее простое приложение:
public sealed class Program { public static void Main() {
System.Console.WriteLine("Hi");
>
>
Здесь определен тип Program с единственным статическим открытым методом Main. Внутри метода Main находится ссылка на другой тип — System.Console. Этот тип разработан в компании Microsoft, и его программный код на языке IL, реализующий его методы, находится в файле MSCorLib.dll. Таким образом, данное приложение определяет собственный тип, а также использует тип, созданный другой компанией.
Для того чтобы построить это приложение, сохраните этот код в файле (допустим, Program.cs, а затем наберите в командной строке следующее:
csc.exe /out:Program.exe /t:exe /r:MSCorLib.dll Program.es
Эта команда приказывает компилятору C# создать исполняемый файл Program, ехе (имя задано параметром /out:Program.exe). Тип создаваемого файла — консольное приложение Win32 (тип задан параметром /t[arget] :ехе).
При обработке файла с исходным кодом компилятор C# обнаруживает ссылку на метод WriteLine типа System.Console. На этом этапе компилятор должен убедиться, что этот тип существует и у него есть метод WriteLine. Компилятор также проверяет, чтобы типы аргументов, предоставляемых программой, совпадали с ожидаемыми типами метода Write Line. Поскольку тип не определен в исходном коде на С#, компилятору C# необходимо передать набор сборок, которые позволят ему разрешить все ссылки на внешние типы. В показанной команде параметр /reference]:MSCorLib.dll приказывает компилятору вести поиск внешних типов в сборке, идентифицируемой файлом MSCorLib.dll,
MSCorLib.dll — специальный файл, в котором находятся все основные типы: Byte, Char, String, Int32 и т. д. В действительности, эти типы используются так часто, что компилятор C# обращается к этой сборке (MSCorLib.dll) автоматически. Другими словами, следующая команда (в ней опущен параметр /г) даст тот же результат, что и предыдущая:
csc.exe /out:Program.exe /t:exe Program.es
Более того, поскольку значения, заданные параметрами командной строки /out: Program. ехе и /t: ехе, совпадают со значениями по умолчанию, следующая команда даст аналогичный результат:
csc.exe Program.cs
Если по какой-то причине вы не хотите, чтобы компилятор C# обращался к сборке MSCorLib.dll, используйте параметр /nostdlib. В компании Microsoft этот параметр используется при построении сборки MSCorLib.dll. Например, во время исполнения следующей команды при компиляции файла Program.cs генерируется ошибка, поскольку тип System. Con sole определен в сборке MSCorLib.dll:
csc.exe /out:Program.ехе /t:exe /nostdlib Program.es
А теперь присмотримся поближе к файлу Program.exe, созданному компилятором С#. Что он из себя представляет? Для начала это стандартный файл в формате РЕ (portable executable). Это значит, что машина, работающая под управлением 32- или 64-разрядной версии Windows, способна загрузить этот файл и что-нибудь с ним сделать. Система Windows поддерживает два типа приложений: с консольными (Console User Interface, CUI) и графическими пользовательскими интерфейсами (Graphical User Interface, GUI). Параметр /t :exe указывает компилятору C# создать консольное приложение. Для создания приложения с графическим интерфейсом необходимо указать параметр /t:winexe, а для создания приложения Windows Store - параметр /t:appcontainerexe.
Файл параметров
В завершение рассказа о параметрах компилятора хотелось бы сказать несколько слов о файлах параметров (response files) — текстовых файлах, содержащих набор параметров командной строки для компилятора. При выполнении компилятора CSC. ехе открывается файл параметров и используются все указанные в нем параметры, как если бы они были переданы в составе командной строки. Файл параметров
передается компилятору путем указания его в командной строке с префиксом Например, пусть есть файл параметров MyProject.rsp со следующим текстом:
/out:МуProject.exe /target:winexe
Для того чтобы компилятор (CSC.exe) использовал эти параметры, необходимо вызвать файл следующим образом:
csc.exe (SMyProject. rsp CodeFilel.es CodeFile2.cs
Эта строка сообщает компилятору C# имя выходного файла и тип скомпилированной программы. Очевидно, что файлы параметров исключительно полезны, так как избавляют от необходимости вручную вводить все аргументы командной строки каждый раз при компиляции проекта.
Компилятор C# допускает использование нескольких файлов параметров. Помимо явно указанных в командной строке файлов, компилятор автоматически ищет файл с именем CSC.rsp в текущем каталоге. Компилятор также проверяет каталог с файлом CSC.exe на наличие глобального файла параметров CSC.rsp, в котором следует указывать параметры, относящиеся ко всем проектам. В процессе своей работы компилятор объединяет параметры из всех файлов и использует их. В случае конфликта параметров в глобальных и локальных файлах предпочтение отдается последним. Кроме того, любые явно заданные в командной строке параметры имеют более высокий приоритет, чем указанные в локальных файлах параметров.
При установке платформы .NET Framework по умолчанию глобальный файл CSC.rsp устанавливается в каталог%SystemRoot%\Microsoft.NET\Framework(64)\ чХ.Х.Х (где Х.Х.Х — версия устанавливаемой платформы .NET Framework). ТГовейшая версия этого файла содержит следующие параметры:
# Этот файл содержит параметры командной строки,
# которые компилятор C# командной строки (CSC)
# будет обрабатывать в каждом сеансе компиляции,
# если только не задан параметр "/noconfig".
# Ссылки на стандартные библиотеки Framework /г:Accessibility.dll
/г:Microsoft.CSharp.dll
/г:System.Configuration.dll
/г:System.Configuration.Install.dll
/г:System.Core.dll
/r:System.Data.dll
/r:System.Data.DataSetExtensions.dll
/r:System.Data.Linq.dll
/r:System.Data.OracleClient.dll
/r:System.Deployment.dll
/r:System.Design.dll
/r:System.DirectoryServices.dll
/r:System.dll
/r:System.Drawing.Design.dll
/г:System.Drawing.dll
/г:System.EnterpriseServices.dll
/г :System.Management. dll
/г :System.Messaging.dll
/г :System.Runtime.Remoting.dll
/г:System.Runtime.Senlallzation.dll
/г:System.Runtime.Serialization.Formatters.Soap.dll
/r :System.Security.dll
/r:System.ServiceModel.dll
/r :System.ServiceModel.Web.dll
/r :System.ServiceProcess.dll
/r:System.Transactions.dll
/r :System.Web.dll
/r :System.Web.Extensions.Design.dll
/r:System.Web.Extensions, dll
/r :System.Web.Mobile, dll
/r :System.Web.RegularExpressions, dll
/r:System.Web.Services.dll
/r :System.Windows.Forms.Dll
/r :System.Workflow.Activities.dll
/r:System.Workflow.ComponentModel. dll
/r :System.Workflow.Runtime.dll
/r :System.Xml.dll
/r:System.Xml.Linq.dll
В глобальном файле CSC.rsp есть ссылки на все перечисленные сборки, поэтому нет необходимости указывать их явно с помощью параметра /reference. Этот файл параметров исключительно удобен для разработчиков, так как позволяет использовать все типы и пространства имен, определенные в различных опубликованных компанией Microsoft сборках, не указывая их все явно с применением параметра /reference.
Ссылки на все эти сборки могут немного замедлить работу компилятора, но если в исходном коде нет ссылок на типы или члены этих сборок, это никак не сказывается ни на результирующем файле сборки, ни на производительности его выполнения.
ПРИМЕЧАНИЕ
При использовании параметра /reference для ссылки на какую-либо сборку можно указать полный путь к конкретному файлу. Однако если такой путь не указать, компилятор будет искать нужный файл в следующих местах (в указанном порядке).
Рабочий каталог.
Каталог, содержащий файл самого компилятора (CSC.exe). Библиотека MSCorLib. dll всегда извлекается из этого каталога. Путь к нему имеет примерно следующий вид:
- %SystemRoot%\Microsoft.NET\Framework\v4.0.#####.
- Все каталоги, указанные с использованием параметра/МЬ компилятора.
- Все каталоги, указанные в переменной окружения LIB.
Конечно, вы вправе добавлять собственные параметры в глобальный файл CSC. rsp, но это сильно усложняет репликацию среды компоновки на разных машинах — приходится помнить про обновление файла CSC. rsp на всех машинах, используемых для сборки приложений. Можно также дать компилятору команду игнорировать как локальный, так и глобальный файлы CSC.rsp, указав в командной строке параметр /noconfig.
Несколько слов о метаданных
Что же именно находится в файле Program.exe? Управляемый PE-файл состоит из 4-х частей: заголовка РЕ32(+), заголовка CLR, метаданных и кода на промежуточном языке (intermediate language, IL). Заголовок РЕ32(+) хранит стандартную информацию, ожидаемую Windows. Заголовок CLR — это небольшой блок информации, специфичной для модулей, требующих CLR (управляемых модулей). В него входит старший и младший номера версии CLR, для которой скомпонован модуль, ряд флагов и маркер MethodDef (о нем — чуть позже), указывающий метод точки входа в модуль, если это исполняемый файл СШ, GUI или Windows Store, а также необязательную сигнатуру строгого имени (она рассмотрена в главе 3). Наконец, заголовок содержит размер и смещение некоторых таблиц метаданных, расположенных в модуле. Для того чтобы узнать точный формат заголовка CLR, изучите структуру IMAGE_COR20_HEADER, определенную в файле CorHdr.h.
Метаданные — это блок двоичных данных, состоящий из нескольких таблиц. Существуют три категории таблиц: определений, ссылок и манифестов. В табл. 2.1 приводится описание некоторых наиболее распространенных таблиц определений, существующих в блоке метаданных модуля.
Таблица 2.1. Основные таблицы определений в метаданных
|
|
Имя таблицы определений | Описание |
MethodDef | Содержит по одной записи для каждого метода, определенного в модуле. Каждая строка включает имя метода, флаги (private, public, virtual, abstract, static, final и т. д.), сигнатуру и смещение в модуле, по которому находится соответствующий IL-код. Каждая запись также может ссылаться на запись в таблице ParamDef, где хранятся дополнительные сведения о параметрах метода |
FieldDef | Содержит по одной записи для каждого поля, определенного в модуле. Каждая запись состоит из флагов (например, private, public и т. д.) и типа поля |
ParamDef | Содержит по одной записи для каждого параметра, определенного в модуле. Каждая запись состоит из флагов (in, out, retval и т. д.), типа и имени |
Property Def | Содержит по одной записи для каждого свойства, определенного в модуле. Каждая запись включает имя, флаги, тип и вспомогательное поле (оно может быть пустым) |
EventDef | Содержит по одной записи для каждого события, определенного в модуле. Каждая запись включает имя и флаги |
|
Для каждой сущности, определяемой в компилируемом исходном тексте, компилятор генерирует строку в одной из таблиц, перечисленных в табл. 2.1. В ходе компиляции исходного текста компилятор также обнаруживает типы, поля, методы, свойства и события, на которые имеются ссылки в исходном тексте. Все сведения о найденных сущностях регистрируются в нескольких таблицах ссылок, составляющих метаданные. В табл. 2.2 показаны некоторые наиболее распространенные таблицы ссылок, которые входят в состав метаданных.
Таблица 2.2. Общие таблицы ссылок, входящие в метаданные
|
|
Таблица 2.2 (продолжение)
|
|
На самом деле таблиц метаданных намного больше, чем показано в табл. 2.1 и 2.2; я просто хотел дать общее представление об информации, используемой компилятором для создания метаданных. Ранее уже упоминалось о том, что в состав метаданных входят также таблицы манифестов. О них мы поговорим чуть позже.
Метаданные управляемого PE-файла можно изучать при помощи различных инструментов. Лично я предпочитаю ILDasm.exe — дизассемблер языка IL. Для того чтобы увидеть содержимое таблиц метаданных, выполните следующую команду:
ILDasm Program.exe
Запустится файл ILDasm.exe и загрузится сборка Program.exe. Для того чтобы вывести метаданные в удобочитаемом виде, выберите в меню команду View ► Metalnfo ► Show! (или нажмите клавиши Ctrl+M). В результате появится следующая информация:
ScopeName : Program.exe
MVID : {CA73FFE80D424610A8D39276195C35AA}
Global functions
Global fields
Global MemberRefs
TypeDef #1 (02000002)
TypDefName: Program (02000002)
Flags : [Public] [AutoLayout] [Class] [Sealed] [AnsiClass] [BeforeFieldlnit] (00100101)
Extends : 01000001 [TypeRef] System.Object Method #1 (06000001) [ENTRYPOINT]
MethodName: Main (06000001)
Flags : [Public] [Static] [HideBySig] [ReuseSlot] (00000096)
RVA : 0x00002050
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
ReturnType: Void No arguments.
Method #2 (06000002)
MethodName: .ctor (06000002)
Flags : [Public] [HideBySig] [ReuseSlot] [SpecialName]
[RTSpecialName] [.ctor] (00001886)
RVA : 0x0000205c
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT] hasThis
ReturnType: Void No arguments.
TypeRef #1 (01000001)
Token: 0x01000001
ResolutionScope: 0x23000001
TypeRefName: System.Object
MemberRef #1 (0a000004)
Member: (0a000004) .ctor:
CallCnvntn: [DEFAULT] hasThis
ReturnType: Void No arguments.
TypeRef #2 (01000002)
Token: 0x01000002
ResolutionScope: 0x23000001
TypeRefName: System.Runtime.CompilerServices.CompilationRelaxationsAttribute
MemberRef #1 (0a000001)
Member: (0a000001) .ctor:
CallCnvntn: [DEFAULT] hasThis
ReturnType: Void 1 Arguments
Argument #1: I4
TypeRef #3 (01000003)
Token: 0x01000003
ResolutionScope: 0x23000001
TypeRefName: System.Runtime.CompilerServices.RuntimeCompatibilityAttribute
MemberRef #1 (0a000002)
Member: (0a000002) .ctor:
CallCnvntn: [DEFAULT] hasThis
ReturnType: Void No arguments.
TypeRef #4 (01000004)
Token: 0x01000004
ResolutionScope: 0x23000001
TypeRefName: System.Console
MemberRef #1 (0a000003)
Member: (0a000003) WriteLine:
CallCnvntn: [DEFAULT]
ReturnType: Void 1 Arguments
Argument #1: String
Assembly
Token: 0x20000001 Name : Program Public Key :
Hash Algorithm : 0x00008004 Version: 0.0.0.0 Major Version: 0x00000000 Minor Version: 0x00000000 Build Number: 0x00000000 Revision Number: 0x00000000 Locale: <null>
Flags : [none] (00000000) CustomAttribute #1 (0c000001)
CustomAttribute Type: 0a000001 CustomAttributeName:
System.Runtime.CompilerServices.CompilationRelaxationsAttribute :: instance void .ctor(int32)
Length: 8
Value : 01 00 08 00 00 00 00 00 > <
ctor args: (8)
CustomAttribute #2 (0C000002)
CustomAttribute Type: 0Э000002 CustomAttributeName:
System.Runtime.CompilerServices.RuntimeCompatibilityAttribute :: instance void .ctor()
Length: 30
Value : 01 00 01 00 54 02 16 57 72 61 70 4e 6f 6e 45 78 > T WrapNonExc
: 63 65 70 74 69 6f 6e 54 68 72 6f 77 73 01 >ceptionThrows <
ctor args: ()
AssemblyRef #1 (23000001)
Token: 0x23000001
Public Key or Token: b7 7a 5c 56 19 34 e0 89
Name: mscorlib
Version: 4.0.0.0
Major Version: 0x00000004
Minor Version: 0x00000000
Build Number: 0x00000000
Revision Number: 0x00000000
Locale: <null>
HashValue Blob:
Flags: [none] (00000000)
User Strings
70000001 : ( 2) LnHi“
Coff symbol name overhead: 0
К счастью, ILDasm самостоятельно обрабатывает таблицы метаданных и комбинирует информацию, поэтому пользователю не приходится заниматься синтаксическим разбором низкоуровневых табличных данных. Например, в приведенном фрагменте видно, что, показывая строку таблицы TypeDef, ILDasm выводит перед первой записью таблицы TypeRef определение соответствующего члена.
Не обязательно понимать, что означает каждая строка этого дампа — важно запомнить, что Program.exe содержит в таблице TypeDef описание типа Program. Этот тип идентифицирует открытый запечатанный (sealed) класс, производный от System. Object (то есть это ссылка на тип из другой сборки). Тип Program также определяет два метода: Main и . ctor (конструктор).
Метод Main — это статический открытый метод, чей программный код представлен на языке IL (а не в машинных кодах процессора, например х86). Main возвращает void и не получает аргументов. Метод-конструктор (всегда отображаемый под именем .cton) является открытым, его код также записан на языке IL. Тип возвращаемого значения конструктора — void, у него нет аргументов, но есть указатель this, ссылающийся на область памяти, в которой должен создаваться экземпляр объекта при вызове конструктора.
Я настоятельно рекомендую вам поэкспериментировать с дизассемблером ILDasm. Он предоставляет массу полезных сведений, и чем лучше вы в них разберетесь, тем быстрее изучите общеязыковую исполняющую среду CLR и ее возможности. В этой книге еще не раз будет использоваться дизассемблер ILDasm.
![]() | ![]() | ||
Просто для интереса посмотрим на некоторую статистику сборки Program.exe. Выбрав в меню программы ILDasm команду View ► Statistics, увидим следующее:
File size | 3584 | |
PE header size | 512 (496 used) | (14.29%) |
PE additional info | 1411 | (39.37%) |
Num.of PE sections | 3 | |
CLR header size | 72 | ( 2.01%) |
CLR metadata size | 612 | (17.08%) |
CLR additional info | 0 | ( 0.00%) |
CLR method headers | 2 | ( 0.06%) |
Managed code | 20 | ( 0.56%) |
Data | 2048 | (57.14%) |
Unaccounted | 1093 | (30.50%) |
Num.of PE sections .text 1024 .rsrc 1536 .reloc 512 | 3 |
CLR metadata size : 612
|
Num.of tiny headers 2
Managed code : 20 Ave method size 10
Здесь приводятся как размеры самого файла (в байтах), так и размеры его составляющих частей (в байтах и процентах от размера файла). Приложение Program.cs очень маленькое, поэтому большая часть его файла занята заголовком РЕ и метаданными. Фактически IL-код занимает всего 20 байт. Конечно, чем больше размер приложения, тем чаще типы и ссылки на другие типы и сборки используются повторно, поэтому размеры метаданных и данных заголовка существенно уменьшаются по отношению к общему размеру файла.
ПРИМЕЧАНИЕ
В ILDasm.exe есть ошибка, искажающая отображаемую информацию о размере файла. В частности, нельзя доверять сведениям в строке Unaccounted.
Объединение модулей для создания сборки
Файл Program.exe — это не просто PE-файл с метаданными, а еще и сборка (assembly), то есть совокупность одного или нескольких файлов с определениями типов и файлов ресурсов. Один из файлов сборки выбирается для хранения ее манифеста. Манифест (manifest) — это еще один набор таблиц метаданных, которые в основном содержат имена файлов, составляющих сборку. Кроме того, эти таблицы описывают версию и региональные стандарты сборки, ее издателя, общедоступные экспортируемые типы, а также все составляющие сборку файлы.
CLR работает со сборками, то есть сначала CLR всегда загружает файл с таблицами метаданных манифеста, а затем получает из манифеста имена остальных файлов сборки. Некоторые характеристики сборки стоит запомнить:
□ в сборке определены многократно используемые типы;
□ сборке назначается номер версии;
□ со сборкой может быть связана информация безопасности.
У отдельных файлов сборки, кроме файла с таблицами метаданных манифеста, таких атрибутов нет.
Чтобы упаковать типы, а также обеспечить безопасность типов и управление их версиями, нужно поместить типы в модули, объединенные в сборку. Чаще всего сборка состоит из одного файла, как приложение Program.exe в рассмотренном примере, но могут быть и сборки из нескольких файлов: PE-файлов с метаданными и файлов ресурсов, например GIF- или JPG-файлов. Р1аверное, проще представлять себе сборку как «логический» EXE- или DLL-файл.
Уверен, многим читателям интересно, зачем компании Microsoft понадобилось вводить новое понятие — «сборка». Дело в том, что сборка позволяет разграничить логическое и физическое понятия многократно используемых типов. Допустим, сборка состоит из нескольких типов. При этом типы, применяемые чаще всех, можно поместить в один файл, а применяемые реже — в другой. Если сборка развертывается путем загрузки через Интернет, клиент может вовсе не загружать файл с редко используемыми типами, если он никогда их не задействует. Например, независимый поставщик ПО (independent software vendor, ISV), специализирующийся на разработке элементов управления пользовательского интерфейса, может реализовать в отдельном модуле типы Active Accessibility (необходимые для соответствия требованиям логотипа Microsoft). Загружать этот модуль достаточно лишь тем, кому нужны специальные возможности.
Можно настроить приложение так, чтобы оно загружало файлы сборки, определив в его конфигурационном файле элемент CodeBase (см. подробнее главу 3). Этот элемент идентифицирует URL-адрес, по которому можно найти все файлы сборки. При попытке загрузить файл сборки CLR получает URL из элемента CodeBase и проверяет наличие нужного файла в локальном кэше загруженных файлов. Если файл там присутствует, то он загружается, если нет — CLR использует для загрузки файла в кэш URL-адрес. Если найти нужный файл не удается, CLR генерирует исключение FileNotFoundException.
У меня есть три аргумента в пользу применения многофайловых сборок.
□ Можно распределять типы по нескольким файлам, допуская избирательную загрузку необходимых файлов из Интернета, а также частично упаковывать и развертывать типы, варьируя функциональность приложения.
□ Можно добавлять к сборке файлы с ресурсами и данными. Допустим, имеется тип для расчета некоторой страховой суммы. Ему может потребоваться доступ к актуарным таблицам. Вместо встраивания актуарных таблиц в исходный код можно включить соответствующий файл с данными в состав сборки (например, с помощью компоновщика сборок AL.exe, который рассмотрен далее). В сборки можно включать данные в любом формате: в текстовом, в виде таблиц Microsoft Excel или Microsoft Word, а также в любом другом при условии, что приложение способно разобрать данные в этом формате.
□ Сборки могут состоять из типов, написанных на разных языках программирования. Одна часть типов может быть написана на С#, другая — на Visual Basic, остальные — на других языках программирования. При компиляции исходного текста на языке C# компилятор создает один модуль, а при компиляции исходного текста на Visual Basic — другой. Затем при помощи соответствующего инструмента все эти модули объединяются в одну сборку. Использующие такую сборку разработчики увидят в ней лишь набор типов. Разработчики даже не заметят, что применялись разные языки программирования. Кстати, при желании с помощью ILDasm.exe можно получить файлы с исходным текстом всех модулей на языке IL. После этого можно запустить утилиту ILAsm.exe и передать ей полученные файлы, и утилита выдаст файл, содержащий все типы. Для этого компилятор исходного текста должен генерировать только IL-код.
ВНИМАНИЕ
Подводя итог, можно сказать, что сборка — это единица многократного использования, управления версиями и безопасности типов. Она позволяет распределять типы и ресурсы по отдельным файлам, чтобы ее пользователи могли решить, какие файлы упаковывать и развертывать вместе. Загрузив файл с манифестом, среда CLR может определить, какие файлы сборки содержат типы и ресурсы, на которые ссылается приложение. Любому потребителю сборки достаточно знать лишь имя файла, содержащего манифест, после чего он сможет, не нарушая работы приложения, абстрагироваться от особенностей распределения содержимого сборки по файлам, которое со временем может меняться.
При работе со многими типами, совместно использующими одну версию и набор параметров безопасности, по соображениям производительности рекомендуется размещать все типы в одном файле, не распределяя их по нескольким файлам, не говоря уже о разных сборках. На загрузку каждого файла или сборки CLR и Windows тратят значительное время: на поиск сборки, ее загрузку и инициализацию. Чем меньше файлов и сборок, тем быстрее загрузка, потому уменьшение числа сборок способствует сокращению рабочего пространства и степени фрагментации адресного пространства процесса. Ну, и наконец, nGen.exe лучше оптимизирует код, если обрабатываемые файлы больше по размеру.
Чтобы скомпоновать сборку, нужно выбрать один из PE-файлов, который станет хранителем манифеста. Можно также создать отдельный PE-файл, в котором не будет ничего, кроме манифеста. В табл. 2.3 перечислены таблицы метаданных манифеста, наличие которых превращает управляемый модуль в сборку.
Таблица 2.3. Таблица метаданных манифеста
продолжение &
|
Таблица 2.3 (продолжение)
|
|
Манифест позволяет потребителям сборки абстрагироваться от особенностей распределения ее содержимого и делает сборку самоописываемой. Обратите внимание, что в файле, который содержит манифест, находится также информация о том, какие файлы составляют сборку, но отдельные файлы «не знают», что они
включены в сборку.
ПРИМЕЧАНИЕ
Файл сборки, содержащий манифест, содержит также таблицу AssemblyRef. В ней хранятся записи с описанием всех сборок, на которые ссылаются файлы данной сборки. Это позволяет инструментам, открыв манифест сборки, сразу увидеть весь набор сборок, на которые ссылается эта сборка, не открывая другие файлы сборки. И в этом случае данные AssemblyRef призваны сделать сборку самоописываемой.
Компилятор C# создает сборку, если указан любой из параметров командной строки /t[arget]:ехе, /t[arget]:winexe, /t[arget]: appcontainerexe, /t[arget] :library или /t[arget] iwinmdobj[4]. Каждый из этих параметров заставляет компилятор генерировать единый PE-файл с таблицами метаданных манифеста. В итоге генерируется соответственно консольное приложение, приложение с графическим интерфейсом, исполняемый файл Windows Store, библиотека классов или библиотека WINMD.
Кроме этих параметров компилятор C# поддерживает параметр /t[arget]: module, который заставляет компилятор создать PE-файл без таблиц метаданных манифеста. При использовании этого параметра всегда получается DLL-файл в формате РЕ. Для того чтобы получить доступ к типам такого файла, его необходимо поместить в сборку. При указании параметра /t :module компилятор C# по умолчанию присваивает выходному файлу расширение .netmodule.
ВНИМАНИЕ
К сожалению, в интегрированной среде разработки (Integrated Development Environment, IDE) Microsoft Visual Studio нет встроенной поддержки создания многофайловых сборок — для этого приходится использовать инструменты командной строки.
Существует несколько способов добавления модуля в сборку. Если РЕ-файл с манифестом строится при помощи компилятора С#, можно применить параметр /addmodule. Для того чтобы понять, как создают многофайловые сборки, рассмотрим пример. Допустим, есть два файла с исходным текстом:
□ файл RUT.cs содержит редко используемые типы;
□ файл FUT.cs содержит часто используемые типы.
Скомпилируем редко используемые типы в отдельный модуль, чтобы пользователи сборки могли отказаться от развертывания этого модуля, если содержащиеся в нем типы им не нужны:
esc /t:module RUT.cs
Команда заставляет компилятор C# создать файл RUT. netmodule, который представляет собой стандартную PE-библиотеку DLL, но среда CLR не сможет просто загрузить ее.
Теперь скомпилируем в отдельном модуле часто используемые типы и сделаем его хранителем манифеста сборки, так как к расположенным в нем типам обращаются довольно часто. Фактически теперь этот модуль представляет собой целую сборку, поэтому я изменил имя выходного файла с FUT.dll на MultiFileLibrary.dll:
esc /out :MultiFileLibrary. dll /t:library /addmodule: RUT. netmodule FUT.cs
Эта команда приказывает компилятору C# при компиляции файла FUT.cs создать файл MultiFilel_ibrary.dll. Поскольку указан параметр /t: library, результирующий PE-файл DLL с таблицами метаданных манифеста называется MultiFileLibrary.dll. Параметр /addmodule: RUT. netmodule указывает компилятору, что файл RUT. net- module должен быть частью сборки. В частности, параметр /addmodule заставляет компилятор добавить к таблице FileDef в метаданных манифеста сведения об этом файле, а также занести в таблицу ExportedTypesDef сведения об открытых экспортируемых типах этого файла.
Завершив работу, компилятор создаст несколько файлов (рис. 2.1). Модуль справа содержит манифест.
Файл RUT. netmodule содержит IL-код, сгенерированный при компиляции RUT.cs. Кроме того, этот файл содержит таблицы метаданных, описывающие типы, методы, поля, свойства, события и т. п., определенные в RUT.cs, а также типы, методы и др., на которые ссылается RUT.cs. MultiFilel_ibrary.dll — это отдельный файл. Подобно RUT. netmodule, он включает IL-код, сгенерированный при компиляции FUT.cs, а также аналогичные метаданные в виде таблиц определений и ссылок. Однако MultiFileLibrary.dll также содержит дополнительные таблицы метаданных, которые и делают его сборкой. Эти дополнительные таблицы описывают все файлы, составляющие сборку (сам файл MultiFileLibrary.dll и RUT.netmodule). Таблицы метаданных манифеста также включают описание всех открытых типов, экспортируемых файлами MultiFileLibrary.dll и RUT.netmodule.
ПРИМЕЧАНИЕ
На самом деле в таблицах метаданных манифеста не описаны типы, экспортируемые PE-файлом, в котором находится манифест. Цель этой оптимизации — уменьшить число байт, необходимое для хранения данных манифеста в PE-файле. Таким образом, утверждения вроде «таблицы метаданных манифеста включают все открытые типы, экспортируемые MultiFileLibrary.dll и RUT.netmodule», верны лишь отчасти. Однако это утверждение вполне точно отражает логический набор экспортируемых типов.
Построив сборку MultiFileLibrary.dll, можно изучить ее таблицы метаданных манифеста при помощи ILDasm.exe, чтобы убедиться, что файл сборки действительно содержит ссылки на типы из файла RUT.netmodule. Таблицы метаданных FileDef и ExportedTypesDef выглядят следующим образом:
File #1 (26000001)
Token: 0x26000001 Name : RUT.netmodule
HashValue Blob : еб e6 df 62 2c al 2c 59 97 65 0f 21 44 10 15 96 f2 7e db c2
Flags : [ContainsMetaData] (00000000)
ExportedType #1 (27000001)
Token: 0x27000001 Name: ARarelyUsedType
Implementation token: 0x26000001
TypeDef token: 0x02000002
Flags : [Public] [AutoLayout] [Class] [Sealed] [AnsiClass]
[BefoneFleldlnlt](00100101)
Из этих сведений видно, что RUT.netmodule это файл, который считается частью сборки с маркером 0x26000001. Таблица ExpontedType показывает наличие открытого экспортируемого типа ARarelyUsedType. Этот тип помечен маркером реализации (implementation token) 0x26000001, означающим, что IL-код этого типа находится в файле RUT.netmodule.
![]() | ![]() | ||
MuHiFileUbrary.dll
Рис. 2.1. Многофайловая сборка из двух управляемых модулей и манифеста
ПРИМЕЧАНИЕ
Для любопытных: размер маркеров метаданных — 4 байта. Старший байт указывает тип маркера (0x01 =TypeRef, 0x02=TypeDef, 0x26=FileRef, 0x27=ExportedType). Полный списоктипов маркеров см. в перечислимом типе CorTokenType в заголовочном файле CorHdr.h из .NET Framework SDK. Три младших байта маркера просто идентифицируют запись в соответствующей таблице метаданных. Ь1апример, маркер реализации 0x26000001 ссылается на первую строку таблицы FileRef (в большинстве таблиц нумерация строк начинается с 1, а не с 0). Кстати, в TypeDef нумерация строк начинается с 2.
Любой клиентский код, использующий типы сборки MultiFileLibrary.dll, должен компоноваться с указанием параметракомпилятора/rteference] :MultiFileLibnany. dll, который заставляет компилятор загрузить сборку MultiFileLibrary.dll и все файлы, перечисленные в ее таблице FileDef. Компилятору необходимо, чтобы все файлы сборки были установлены и доступны. Если удалить файл RUT.netmodule, компилятор C# выдаст следующее сообщение об ошибке:
fatal error CS0009: Metadata file 'C:\MultiFilel_ibrary.dll' could not be opened-'Error importing module 'rut.netmodule' of assembly 'C:\MultlFlleLlbrary.dll'- The system cannot find the file specified
Это означает, что при построении новой сборки должны присутствовать все файлы, на которые она ссылается.
Во время исполнения клиентский код вызывает разные методы. При первом вызове нею>т<>рог< > мете>да среда CLR определяет, на какие типы он ссылается как на параметр, возвращаемое значение или локальную переменную. Далее CLR пытается загрузить из сборки, на которую ссылается код, файл с манифестом. Если этот файл описывает типы, к которым обращается вызванный метод, срабатывают внутренние механизмы CLR, и нужные типы становятся доступными. Если в манифесте указано, что нужный тип находится в другом файле, CLR загружает этот файл, п внутренние механизмы CLR обеспечивают доступ к данному типу. CLR загружает файл сборки только при вызове метода, ссылающегося на расположенный в этом файле тип. Это значит, что наличие всех файлов сборки, на которую ссылается приложение, для его работы не обязательно.
Добавление сборок в проект в среде Visual Studio
Если проект создается в среде Visual Studio, необходимо добавить в проект все сборки, на которые он ссылается. Для этого откройте окно Solution Explorer, щелкните правой кнопкой мыши на проекте, на который нужно добавить ссылку, и выберите команду Add Reference. Откроется диалоговое окно Reference Manager (рис. 2.2).
Рис. 2.2. Диалоговое окно Reference Manager в Visual Studio |
Для того чтобы добавить в проект ссылку на сборку, выберите ее в списке. Если в списке нет нужной сборки, то для того чтобы ее найти (файл с манифестом), щелкните на кнопке Browse. Вкладка Solution служит для добавления в текущий проект ссылки на сборки, созданные в другом проекте этого же решения. Раздел СОМ в диалоговом окне Reference Manager позволяет получить доступ к неуправляемому COM-серверу из управляемого кода через класс-представитель, автоматически генерируемый Visual Studio. Вкладка Browse позволяет выбрать сборку, недавно добавленную в другой проект.
Чтобы сборки отображались в списке на вкладке .NET, выполните инструкции по адресу:
http: //msdn.microsoft. com/en-us/library/wkze6zky(v=vs.H0) .aspx
Использование утилиты Assembly Linker
Вместо компилятора C# для создания сборки можно задействовать компоновщик сборок (assembly linker) AL.exe. Эта утилита оказывается кстати, если нужно создавать сборки из модулей, скомпонованных разными компиляторами (если компилятор языка не поддерживает параметр, эквивалентный параметру /addmodule из С#), а также в случае, когда требования к упаковке сборки на момент компоновки просто не известны. Утилита AL.exe пригодна и для компоновки сборок, состоящих исключительно из ресурсов (или сопутствующих сборок — к ним мы еще вернемся), которые обычно используются для локализации ПО.
Утилита AL.exe может генерировать файлы формата EXE или DLL РЕ, которые не содержат ничего, кроме манифеста, описывающего типы из других модулей. Чтобы понять, как работает AL.exe, скомпонуем сборку MultiFileLibrary.dll по-другому:
esc /t:module RUT.cs esc /t:module FUT.cs
al /out: MultiFileLibrary.dll /t:library FUT.netmodule RUT.netmodule
Файлы, генерируемые в результате исполнения этих команд, показаны на рис. 2.3.
В этом примере создаются два отдельных модуля, RUT. netmodule и FUT.netmodule. Оба модуля не являются сборками, так как не содержат таблиц метаданных манифеста. Третий же — MultiFileLibrary.dll — это небольшая библиотека РЕ DLL (поскольку она скомпонована с параметром /t[anget]: library), в которой нет IL-кода, а только таблицы метаданных манифеста, указывающие, что файлы RUT.netmodule и FUT. netmodule входят в состав сборки. Результирующая сборка состоит из трех файлов: MultiFileLibrary.dll, RUT.netmodule и FUT.netmodule, так как компоновщик сборок не «умеет» объединять несколько файлов в один.
Утилита AL.exe может генерировать PE-файлы с консольным и графическим интерфейсом, а также файлы приложений Windows Store с помощью параметров /t [arget] :ехе, /t [arget] :winexe или /t [arget]: appcontainerexe). Однако это довольно необычно, поскольку означает, что будет сгенерирован исполняемый PE-файл, содержащий не больше IL-кода, чем нужно для вызова метода из другого
модуля. Чтобы указать, какой метод должен использоваться в качестве входной точки, задайте при вызове компоновщика сборок параметр командной строки /main. Приведем пример вызова AL.exe с этим параметром:
esc /t:module /г:MultiFileLibrary.dll Program.es
![]() |
al /out:Program.exe /t:exe /main:Program.Main Program.netmodule
Рис. 2.3. Многофайловая сборка из трех управляемых модулей и манифеста
Первая строка компонует Program.cs в модуль, а вторая генерирует небольшой РЕ-файл Program.exe с таблицами метаданных манифеста. В нем также находится небольшая глобальная функция, сгенерированная AL.exe благодаря параметру /main: Program.Main. Эта функция,__________ EntryPoint, содержит следующий IL-код:
.method privatescope static void _______ EntryPoint$PST06000001() cil managed
{
.entrypoint
// Code size 8 (0x8)
.maxstack 8 IL_0000: tail.
IL_0002: call void [.module "Program.netmodule"JProgram::Main()
IL_0007: ret
} // end of method 'Global Functions'::________ EntryPoint
Как видите, этот код просто вызывает метод Main, содержащийся в типе Program, который определен в файле Program.netmodule. Параметр /main, указанный при вызове AL.exe, здесь не слишком полезен, так как вряд ли вы когда-либо будете создавать приложение, у которого точка входа расположена не в PE-файле с таблицами метаданных манифеста. Здесь этот параметр упомянут лишь для того, чтобы вы знали о его существовании.
В программном коде для данной книги имеется файл Ch02-3-BuildMultiFileLibrary. bat, в котором инкапсулированы последовательно все шаги построения многофайловой сборки. Проект Ch02-4-AppUsingMultiFileLibrary в Visual Studio выполняет данный файл на этапе предварительного построения. Изучение этого примера поможет вам понять, как интегрировать многофайловую сборку из Visual Studio.
Включение в сборку файлов ресурсов
Если сборка создается с AL.exe, параметр /embed[resource] позволяет добавить в сборку файлы ресурсов (файлы в формате, отличном от РЕ). Параметр принимает любой файл и включает его содержимое в результирующий PE-файл. Таблица ManifestResourceDef в манифесте обновляется, отражая наличие нового ресурса.
Утилита AL.exe поддерживает также параметр /link[resource], который принимает файл с ресурсами. Однако параметр только обновляет таблицы манифеста ManifestResourceDef и FileDef сведениями о ресурсе и о том, в каком файле сборки он находится. Сам файл с ресурсами не внедряется в PE-файл сборки, а хранится отдельно и подлежит упаковке и развертыванию вместе с остальными файлами сборки.
ПРИМЕЧАНИЕ
В файлахуправляемой сборки содержится также файл манифеста Win32. По умолчанию компилятор C# автоматически создает файл манифеста, однако ему можно запретить это делать при помощи параметра /nowin32manifest. Программный код манифеста, генерируемого компилятором C# по умолчанию, выглядит следующим образом:
<?xml version="1.0" encoding="UTF8" standalone="yes"?>
<assembly xmlns="urn:schemasmicrosoftcom:asm.vl" manifestVersion="l.0"> <assemblyldentity version="l.0.0.0" name="MyApplication.app" />
<trustInfo xmlns="urn:schemasmicrosoftcom:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemasmicrosoftcom:asm.v3">
<requestedExecution Level level="asInvoker" uiAccess="false"/> </requestedPrivileges>
</security>
</trustInfo>
</assembly>
Подобно AL.exe, CSC.exe позволяет объединять ресурсы со сборкой, генерируемой компилятором С#. Параметр /resource компилятора C# включает указанный файл с ресурсами в результирующий PE-файл сборки и обновляет таблицу ManifestResounceDef. Параметр компилятора /linkresource добавляет в таблицы ManifestResounceDef и FileDef записи со ссылке)й на <)тдельный файл с ресурсами.
И последнее: в сборку можно включить стандартные ресурсы Win32. Это легко сделать, указав при вызове AL.exe или CSC.exe путь к RES-файлу и параметр /win32nes. Кроме того, можно легко включить стандартный ресурс значка Win32 в файл сборки, указав при вызове AL.exe или CSC.exe путь к ICO-файлу и параметр /win32icon. В Visual Studio файл ресурсов добавляют в сборку на вкладке Application в диалоговом окне свойств проекта. Обычно значки включают, чтобы Проводник Windows (Windc >ws Explc>rer) мог отображать значок для управляемого исполняемого файла.
Ресурсы со сведениями о версии сборки
Когда утилита AL.exe или CSC.exe генерирует сборку в виде PE-файла, она также включает в этот файл стандартный ресурс версии Win32. Пользователи могут увидеть версию, просматривая свойства файла. Для получения этой информации из программы служит статический методGetVersionlnfoтипа System .Diagnostics. FileVersionlnfo. На рис. 2.4 показана вкладка Details диалогового окна свойств файла Ch02-3-MultiFileLibrary.dll.
Рис. 2.4. Вкладка Details диалогового окна свойств файла Ch02-3-MultiFileLibrary.dll |
При построении сборки следует задавать значения полей версии в исходном тексте программы с помощью специализированных атрибутов, применяемых на уровне сборки. Вот как выглядит код, генерирующий информацию о версии, показанную на рис. 2.4.
using System.Reflection;
// Информация версии поля FileDescription:
[assembly: AssemblyTitle("MultiFileLibrary.dll")]
// Информация версии поля Comments:
[assembly: AssemblyDescription("This assembly contains MultiFileLibrary's types")]
// Информация версии поля CompanyName:
[assembly: AssemblyCompany("Wintellect")]
// Информация версии поля ProductName:
[assembly: AssemblyProduct("Wintellect (R) MultiFileLibrary's Type Library")]
// Информация версии поля LegalCopyright:
[assembly: AssemblyCopyright("Copyright (c) Wintellect 2013")]
// Информация версии поля LegalTrademarks:
[assembly:AssemblyTrademark("MultiFileLibrary is a registered trademark
of Wintellect")]
// Информация версии поля AssemblyVersion:
[assembly: AssemblyVersion("3.0.0.0")]
// Информация версии поля FILEVERSION/FileVersion:
[assembly: AssemblyFileVersion("1.0.0.0")]
// Информация версии поля PRODUCTVERSION/ProductVersion:
[assembly: AssemblyInformationalVersion("2.0.0.0")]
// Задать поле Language (см. далее раздел "Региональные стандарты")
[assembly:AssemblyCulture("")]
ВНИМАНИЕ
К сожалению, в диалоговом окне свойств Проводника Windows отсутствуют поля для некоторых атрибутов. В частности, было бы оченьудобно, если бы в нем отображался атрибут AssemblyVersion, потому что среда CLR использует значение этого атрибута при загрузке сборки (об этом рассказано в главе 3).
В табл. 2.4 перечислены поля ресурса со сведениями о версии и соответствующие им атрибуты, определяемые пользователем. Если сборка компонуется утилитой AL.exe, сведения о версии можно задать параметрами командной строки вместо атрибутов. Во втором столбце табл. 2.4 показаны параметры командной строки для каждого поля ресурса со сведениями о версии. Обратите внимание на отсутствие аналогичных параметров у компилятора С#; поэтому сведения о версии обычно задают, применяя специализированные атрибуты.
Таблица 2.4. Поля ресурса со сведениями о версии и соответствующие им параметры AL.exe и пользовательские атрибуты
|
|
Поле ресурса со сведениями о версии | Параметр AL.exe | Атрибут/комментарий |
OriginalFilename | /out | Задается равным заданному имени выходного файла (без пути) |
PrivateBuild | Нет | Всегда остается пустым |
ProductName | /product | System. Reflection. Assembly ProductAttribute |
ProductVersion | /productversion | System. Reflection. Assembly Informational- VersionAttribute |
SpecialBuild | Нет | Всегда остается пустым |
|
ВНИМАНИЕ
При создании нового проекта C# в Visual Studio файл Assemblylnfo.cs генерируется автоматически. Он содержит все атрибуты сборки, описанные в этом разделе, а также несколько дополнительных — о них речь идет в главе 3. Можно просто открыть файл Assemblylnfo.cs и изменить относящиеся к конкретной сборке сведения. Visual Studio также предоставляет диалоговое окно для редактирования информации о версии сборки, которое изображено на рис. 2.5.
Рис. 2.5. Диалоговое окно с информацией о сборке в Visual Studio |
Номера версии
Ранее было показано, что сборка может идентифицироваться по номеру версии. У частей этого номера одинаковый формат: каждая состоит из 4 частей, разделенных точками (табл. 2.5).
Таблица 2.5. Формат номеров версии
|
|
В табл. 2.5 показан пример номера версии 2.5.719.2. Первые две цифры составляют то, что обычно понимают под номером версии: пользователи будут считать номером версии 2.5. Третье число, 719, указывает номер построения. Если в вашей компании сборка строится каждый день, увеличивать этот номер надо ежедневно. Последнее число 2 — номер редакции сборки. Если в компании сборка строится дважды в день (скажем, после исправления критической и обязательной к немедленному исправлению ошибки, тормозившей всю работу над проектом), надо увеличивать номер редакции. Такая схема нумерации версий принята в компании Microsoft, и я настоятельно рекомендую ей следовать.
Обратите внимание: со сборкой связаны три номера версии. Это очень неудачное решение стало источником серьезной путаницы. Попробую объяснить, для чего нужен каждый из этих номеров и как его правильно использовать.
□ AssemblyFileVersion — этот номер версии хранится в ресурсе версии Win32 и предназначен лишь для информации, CLR его полностью игнорирует. Обычно устанавливают старший и младший номера версии, определяющие отображаемый номер версии. Далее при каждой компоновке увеличивают номер компоновки и редакции. Теоретически инструмент от компании Microsoft (например, CSC.exe или AL.exe) должен автоматически обновлять номера компоновки и редакции (в зависимости от даты и времени на момент компоновки), но этого не происходит. Этот номер версии отображается Проводником Windows и служит для определения точного времени компоновки сборки.
□ AssemblylnformationalVersion — этот номер версии также хранится в ресурсе версии Win32 и, как и предыдущий, предназначен лишь для информации; CLR его игнорирует. Этот номер служит для указания версии продукта, в который входит сборка. Например, продукт версии 2.0 может состоять из нескольких сборок. Одна из них может отмечаться как версия 1.0, если это новая сборка, не входившая в комплект поставки продукта версии 1.0. Обычно отображаемый номер версии формируется из старшего и младшего номеров версии. Затем номера компоновки и редакции увеличивают при каждой упаковке всех сборок готового продукта.
□ AssemblyVersion — этот номер версии хранится в манифесте, в таблице метаданных AssemblyDef. CLR использует этот номер версии для привязки к сборкам, имеющим строгие имена (о них рассказано в главе 3). Этот номер версии чрезвычайно важен, так как он однозначно идентифицирует сборку. Начиная разработку сборки, следует задать старший и младший номера версии, а также номера компоновки и редакции; не меняйте их, пока не будете готовы начать работу над следующей версией сборки, пригодной для развертывания. При создании сборки, ссылающейся на другую, этот номер версии включается в нее в виде записи таблицы AssemblyRef. Это значит, что сборка знает, с какой версией она была построена и протестирована. CLR может загрузить другую версию, используя механизм перенаправления привязки, описанный в главе 3.
Региональные стандарты
Помимо номера версии, сборки идентифицируют региональными стандартами (culture). Например, одна сборка может быть исключительно на немецком языке, другая — на швейцарском варианте немецкого, третья — на американском английском и т. д. Региональные стандарты идентифицируются строкой, содержащей основной и вспомогательный теги (как описано в RFC 1766). Несколько примеров приведено в табл. 2.6.
Таблица 2.6. Примеры тегов, определяющих региональные стандарты сборки
|
|
В общем случае сборкам с кодом не назначают региональные стандарты, так как код обычно не содержит зависящих от них встроенных параметров. Сборку, для которой не определен региональный стандарт, называют сборкой с нейтральными региональными стандартами (culture neutral).
При создании приложения, ресурсы которого привязаны к региональным стандартам, компания Microsoft настоятельно рекомендует объединять программный ресурс и ресурсы приложения по умолчанию в одной сборке и не назначать ей региональных стандартов при построении. Другие сборки будут ссылаться на нее при создании и работе с типами, которые она предоставляет для общего доступа.
После этого можно создать одну или несколько отдельных сборок, содержащих только ресурсы, зависящие от региональных стандартов, и никакого программного кода. Сборки, помеченные для применения в определенных региональных стандартах, называют сопутствующими (satellite assemblies). Региональные стандарты, назначенные такой сборке, в точности отражают региональные стандарты размещенного в ней ресурса. Рекомендуется создавать отдельную сопутствующую сборку для каждого регионального стандарта, который вы намерены поддерживать.
Обычно сопутствующие сборки строятся при помощи утилиты AL.exe. Не стоит использовать для этого компилятор — ведь в сопутствующей сборке не должно быть программного кода. Применяя утилиту AL.exe, можно задать желаемые региональные стандарты параметром /с [ultune]: text, где text это строка (например, en-US, представляющая американский вариант английского языка). При развертывании сопутствующие сборки следует помещать в подкаталог, имя которого совпадает с текстовой строкой, идентифицирующей региональные стандарты. Например, если базовым каталогом приложения является C:\MyApp, сопутствующая сборка для американского варианта английского языка должна быть в каталоге C:\MyApp\en-US. Во время выполнения доступ к ресурсам сопутствующей сборки осуществляют через класс System.Resources.ResourceManager.
ПРИМЕЧАНИЕ
Хотя это и не рекомендуется, можно создавать сопутствующие сборки с программным кодом. При желании вместо параметра /culture утилиты AL.exe региональный стандарт можно указать в атрибуте System.Reflection.AssemblyCulture, определяемом пользователем, например, следующим образом:
// Назначить для сборки региональный стандарт Swiss German [assembly:AssemblyCulture("de-CH")]
В общем случае не стоит создавать сборки, ссылающиеся на сопутствующие сборки. Другими словами, все записи таблицы AssemblyRef должны ссылаться на сборки с нейтральными региональными стандартами. Если необходимо получить доступ к типам или членам, расположенным в сопутствующей сборке, следует воспользоваться механизмом отражения (см. главу 23).
Развертывание простых приложений (закрытое развертывание сборок)
Ранее в этой главе было показано, как строить модули и объединять их в сборки. Пора заняться упаковкой и развертыванием сборок, чтобы пользователь мог работать с приложением.
Для приложений Windows Store устанавливаются исключительно жесткие правила упаковки сборок. Visual Studio упаковывает все сборки, необходимые приложению, в один файл .аррх, который либо отправляется в Windows Store, либо загружается на машину Когда пользователь устанавливает файл .аррх, все содержащиеся в нем сборки помещаются в каталог, из которого CLR загружает их, a Windows добавляет плитку приложения на начальный экран пользователя. Если другие пользователи установят тот же файл .аррх, будут использованы ранее установленные сборки, а у нового пользователя на начальном экране просто добавится плитка. Когда пользователь удаляет приложение Windows Store, система удаляет плитку с начального экрана пользователя. Если приложение не установлено у других пользователей, Windows уничтожает каталог вместе со всеми сборками. Е1е забывайте, что разные пользователи могут устанавливать разные версии одного приложения Windows Store. Учитывая такую возможность, Windows устанавливает сборки в разные каталоги, чтобы несколько версий одного приложения могли одновременно существовать на одной машине.
Для настольных приложений (не относящихся к Windows Store) особых средств для упаковки сборки не требуется. Легче всего упаковать набор сборок, просто скопировав все их файлы. Например, можно поместить все файлы сборки на компакт- диск и передать их пользователю вместе с программой установки, написанной в виде пакетного файла. Такая программа просто копирует файлы с компакт-диска в каталог на жестком диске пользователя. Поскольку сборка включает все ссылки и типы, определяющие ее работу, ему достаточно запустить приложение, a CLR найдет в каталоге приложения все сборки, на которые ссылается данная сборка. Так что для запуска приложения не нужно модифицировать реестр, а чтобы удалить приложение, достаточно просто удалить его файлы — и все!
Конечно, можно применять для упаковки и установки сборок другие механизмы, например САВ-файлы (они обычно используются в сценариях с загрузкой из Интернета для сжатия файлов и сокращения времени загрузки). Можно также упаковать файлы сборки в MSI-файл, предназначенный для службы установщика Windows (Windows Installer), MSIExec.exe. MSI позволяет установить сборку по требованию при первой попытке CLR ее загрузить. Эта функция не нова для службы MSI, она также поддерживает аналогичную функцию для неуправляемых ЕХЕ- и DLL-файлов.
ПРИМЕЧАНИЕ
Пакетный файл или подобная простая «установочная программа» скопирует приложение на машину пользователя, однако для создания ярлыков на рабочем столе, в меню Пуск (Start) и на панели быстрого запуска понадобится программа посложнее. Кроме того, скопировать, восстановить или переместить приложение с одной машины на другую легко, но ссылки и ярлыки потребуют специального обращения.
Естественно, в Visual Studio есть встроенные механизмы, которые можно задействовать для публикации приложений, — это делается на вкладке Publish страницы свойств проекта. Можно использовать ее, чтобы заставить Visual Studio создать MSI-файл и скопировать его на веб-сайт, FTP-сайт или в заданную папку на диске. MSI-файл также может установить все необходимые компоненты, такие как .NET Framework или Microsoft SQL Server Express Edition. Наконец, приложение может автоматически проверять наличие обновлений и устанавливать их на пользовательской машине посредством технологии ClickOnce.
Сборки, развертываемые в том же каталоге, что и приложение, называют сборками с закрытым развертыванием (privately deployed assemblies), так как файлы сборки не используются совместно другими приложениями (если только другие приложения не развертывают в том же каталоге). Сборки с закрытым развертыванием — серьезное преимущество для разработчиков, конечных пользователей и администраторов, поскольку достаточно скопировать такие сборки в базовый каталог приложения, и CLR сможет загрузить и исполнить содержащийся в них код. Приложение легко удаляется; для этого достаточно удалить сборки из его каталога. Также упрощаются процедуры резервного копирования и восстановления подобных сборок.
Несложный сценарий установки/перемещения/удаления приложения стал возможным благодаря наличию в каждой сборке метаданных. Метаданные указывают, какую сборку, на которую они ссылаются, нужно загрузить — для этого не нужны параметры реестра. Кроме того, область видимости сборки охватывает все типы. Это значит, что приложение всегда привязывается именно к тому типу, с которым оно было скомпоновано и протестировано. CLR не станет загружать другую сборку просто потому, что она предоставляет тип с тем же именем. Этим CLR отличается от СОМ, где типы регистрируются в системном реестре, что делает их доступными любому приложению, работающему на машине.
В главе 3 рассказано о развертывании совместно используемых сборок, доступных нескольким приложениям.
Простое средство администрирования (конфигурационный файл)
Пользователи и администраторы лучше всех могут определять разные аспекты работы приложения. Например, администратор может решить переместить файлы сборки на жесткий диск пользователя или заменить данные в манифесте сборки. Есть и другие сценарии управления версиями и удаленного администрирования, о некоторых из них рассказано в главе 3.
Для того чтобы предоставить администратору контроль над приложением, можно разместить в каталоге приложения конфигурационный файл. Его может создать и упаковать издатель приложения, после чего программа установки запишет конфигурационный файл в базовый каталог приложения. Кроме того, администратор или конечный пользователь машины может сам создать или модифицировать этот файл. CLR интерпретирует его содержимое для изменения политики поиска и загрузки файлов сборки.
Конфигурационные файлы содержат XML-теги и могут ассоциироваться с приложением или с компьютером. Использование отдельного файла (вместо параметров, хранимых в реестре) позволяет легко создать резервную копию файла, а администратору — без труда копировать файлы с машины на машину: достаточно скопировать нужные файлы — в результате будет также скопирована административная политика.
В главе 3 такой конфигурационный файл рассматривается подробно, а пока вкратце обсудим его. Допустим, издатель хочет развернуть приложение вместе с файлами сборки MultiFileLibrary, но в отдельном каталоге. Желаемая структура каталогов с файлами выглядит следующим образом:
Каталог AppDir (содержит файлы сборки приложения)
Program.exe
Program.exe.config (обсуждается ниже)
Подкаталог AuxFiles (содержит файлы сборки MultiFileLibrary)
MultiFileLibrary.dll FUT.netmodule RUT.netmodule
Поскольку файлы сборки MultiFileLibrary более не находятся в базовом каталоге приложения, CLR не сможет найти и загрузить их, и при запуске приложения будет сгенерировано исключение System.10. FileNotFoundException. Чтобы избежать этого, издатель создает конфигурационный файл в формате XML и размещает его в базовом каталоге приложения. Имя этого файла должно совпадать с именем главного файла сборки и иметь расширение config, в данном случае — Program.exe. config. Содержимое этого конфигурационного файла должно выглядеть примерно следующим образом:
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.vl">
<probing privatePath="AuxFiles" />
</assemblyBinding>
</runtime>
</configuration>
Пытаясь найти файл сборки, CLR всегда сначала ищет в каталоге приложения, и если поиск заканчивается неудачей, продолжает искать в подкаталоге AuxFiles. В атрибуте privatePath элемента, направляющего поиск, можно указать несколько путей, разделенных точкой с запятой. Считается, что все пути заданы относительно базового каталога приложения. Идея заключается в том, что приложение может управлять своим каталогом и его подкаталогами, но не может управлять другими каталогами.
Алгоритм поиска файлов сборки
В поиске сборки среда CLR просматривает несколько подкаталогов. Порядок при поиске сборки с нейтральными региональными стандартами таков (при условии, что параметры firstPrivatePath и secondPrivatePath определены в атрибуте pnivatePath конфигурационного файла):
AppDir\AsmName.dll
AppDir\AsmName\AsmName.dll
AppDir\firstPrivatePath\AsmName.dll
AppDir\firstPrivatePath\AsmName\AsmName.dll
AppDir\secondPrivatePath\AsmName.dll
AppDir\secondPrivatePath\AsmName\AsmName.dll
В этом примере конфигурационный файл не понадобится, если файлы сборки MultiFileLibnany развернуты в подкаталоге MultiFileLibrary, так как CLR автоматически проверяет подкаталог, имя которого совпадает с именем искомой сборки.
Если ни в одном из упомянутых каталогов сборка не найдена, CLR начинает поиск заново, но теперь ищет файл с расширением EXE вместо DLL. Если и на этот раз поиск оканчивается неудачей, генерируется исключение FileNotFoundException.
В отношении сопутствующих сборок действуют те же правила поиска за одним исключением: ожидается, что сборка находится в подкаталоге базового каталога приложения, имя которого совпадает с названием регионального стандарта. Например, если для файла AsmName.dll назначен региональный стандарт «en-US», порядок просмотра каталогов таков:
С:\AppDir\enUS\AsmName.dll
С:\AppDir\enUS\AsmName\AsmName.dll
С:\AppDir\firstPrivatePath\enUS\AsmName.dll
С:\AppDir\firstPrivatePath\enUS\AsmName\AsmName.dll
C:\AppDir\secondPrivatePath\enUS\AsmName.dll
C:\AppDir\secondPrivatePath\enUS\AsmName\AsmName.dll
C:\AppDir\enUS\AsmName.exe
C:\AppDir\enUS\AsmName\AsmName.exe
C: \AppDir\firstPrivatePath\enLIS\AsmName.exe
C:\AppDir\firstPrivatePath\enUS\AsmName\AsmName.exe
C: \AppDir\secondPrivatePath\enLIS\AsmName.exe
C:\AppDir\secondPrivatePath\enUS\AsmName\AsmName.exe
C:\AppDir\en\AsmName.dll
C:\AppDir\en\AsmName\AsmName.dll
C:\AppDir\firstPrivatePath\en\AsmName.dll
C:\AppDir\firstPrivatePath\en\AsmName\AsmName.dll
C:\AppDir\secondPrivatePath\en\AsmName.dll
C:\AppDir\secondPrivatePath\en\AsmName\AsmName.dll
C:\AppDir\en\AsmName.exe C:\AppDir\en\AsmName\AsmName.exe
С:\AppDir\fiгstPrivatePath\en\AsmName.exe С:\AppDir\firstPrivatePath\en\AsmName\AsmName.exe С:\AppDir\secondPrivatePath\en\AsmName.exe С:\AppDir\secondPrivatePath\en\AsmName\AsmName.ехе
Как видите, CLR ищет файлы с расширением ЕХЕ или DLL. Поскольку поиск может занимать значительное время (особенно когда CLR пытается найти файлы в сети), в конфигурационном XML-файле можно указать один или несколько элементов региональных стандартов culture, чтобы ограничить круг проверяемых каталогов при поиске сопутствующих сборок. Microsoft предоставляет программу FusLogVw.exe, при помощи которой можно увидеть, как CLR осуществляет привязку сборок во время выполнения. Дополнительная информация доступна по адресу http://msdn.microsoft.com/en-us/library/e74a18c4(v=vs.110).aspx.
Имя и расположение конфигурационного XML-файла может различаться в зависимости от типа приложения.
□ Для исполняемых приложений (ЕХЕ) конфигурационный файл должен располагаться в базовом каталоге приложения. У него должно быть то же имя, что и у EXE-файла, но с расширением config.
□ Для приложений Microsoft ASRNET Web Form конфигурационный файл всегда должен находиться в виртуальном корневом каталоге веб-приложения и называться Web.config. Кроме того, в каждом вложенном каталоге может быть собственный файл Web.config с унаследованными параметрами конфигурации. Например, веб-приложение, расположенное по адресу http://www.Wintellect.com/ Training, будет использовать параметры из файлов Web.config, расположенных в виртуальном корневом каталоге и в подкаталоге Training.
Как уже было сказано, параметры конфигурации применяются к конкретному приложению и конкретному компьютеру. При установке платформа .NET Framework создает файл Machine.config. Существует по одному файлу Machine.config на каждую версию среды CLR, установленную на данной машине. Файл Machine.config расположен в следующем каталоге:
%SystemRoot%\Microsoft.NET\Framework\6epcofl\C0NFIG
Естественно, %SystemRoot% — это каталог, в котором установлена система Windows (обычно C:\Windows), а версия — номер версии, идентифицирующий определенную версию платформы .NET Framework (например, v4.0.#####).
Параметры файла Machine.config заменяют параметры конфигурационного файла конкретного приложения. Администраторам и пользователям следует избегать модификации файла Machine.config, поскольку в нем хранятся многие параметры, связанные с самыми разными аспектами работы системы, что серьезно затрудняет ориентацию в его содержимом. Кроме того, конфигурационные файлы, специфичные для приложения, упрощают организацию резервного копирования и восстановления конфигурации приложения.
Глава 3. Совместно используемые сборки и сборки со строгим именем
В главе 2 говорилось о построении, упаковке и развертывании сборок. При этом основное внимание уделялось закрытому развертыванию (private deployment), при котором сборки, предназначенные исключительно для одного приложения, помещают в базовый каталог приложения или в его подкаталог. Закрытое развертывание сборок позволяет в значительной мере управлять именами, версиями и поведением сборок.
В этой главе мы займемся созданием сборок, которые могут совместно использоваться несколькими приложениями. Замечательный пример глобально развертываемых сборок — это сборки, поставляемые вместе с Microsoft .NET Framework, поскольку почти все управляемые приложения используют типы, определенные Microsoft в библиотеке классов .NET Framework Class Library (FCL).
Как уже было отмечено в главе 2, операционная система Windows получила репутацию нестабильной главным образом из-за того, что для создания и тестирования приложений приходится использовать чужой код. В конце концов, любое приложение для Windows, которое вы пишете, вызывает код, созданный разработчиками Microsoft. Более того, самые разные компании производят элементы управления, которые разработчики затем встраивают в свои приложения. Фактически такой подход стимулирует сама платформа .NET Framework, а со временем, вероятно, число производителей элементов управления возрастет.
Время не стоит на месте, как и разработчики из Microsoft, как и сторонние производители элементов управления: они устраняют ошибки, добавляют в свой код новые возможности и т. п. В конечном счете, на жесткий диск пользовательского компьютера попадает новый код. В результате в давно установленном и прекрасно работавшем пользовательском приложении начинает использоваться уже не тот код, с которым оно создавалось и тестировалось. Поведение такого приложения становится непредсказуемым, что, в свою очередь, негативно влияет на стабильность Windows.
Решить проблему управления версиями файлов чрезвычайно трудно. Fla самом деле, я готов спорить, что если взять любой файл и изменить в нем значение одного- единственного бита с 0 на 1 или наоборот, то никто не сможет гарантировать, что программы, использовавшие исходную версию этого файла, будут работать с новой версией файла, как ни в чем не бывало. Это утверждение верно хотя бы потому, что многие программы случайно или преднамеренно эксплуатируют ошибки других
программ. Если в более поздней версии кода какая-либо ошибка будет исправлена, то использующее его приложение начинает работать непредсказуемо.
Итак, вопрос в следующем: как, устраняя ошибки и добавляя к программам новые функции, гарантировать, что эти изменения не нарушат работу других приложений? Я долго думал над этим и пришел к выводу — это просто невозможно. Но, очевидно, такой ответ не устроит никого, поскольку в поставляемых файлах всегда будут ошибки, а разработчики всегда будут одержимы желанием добавлять новые функции. Должен все же быть способ распространения новых файлов, позволяющий надеяться, что любое приложение после обновления продолжит замечательно работать, а если нет, то легко вернуть приложение в последнее состояние, в котором оно прекрасно работало.
В этой главе описана инфраструктура .NET Framework, призванная решить проблемы управления версиями. Позвольте сразу предупредить: речь идет о сложных материях. Нам придется рассмотреть массу алгоритмов, правил и политик, встроенных в общеязыковую исполняющую среду (CLR). Помимо этого, упомянуты многие инструменты и утилиты, которыми приходится пользоваться разработчику. Все это достаточно сложно, поскольку, как я уже сказал, проблема управления версиями непроста сама по себе и то же можно сказать о подходах к ее решению.
Два вида сборок — два вида развертывания
Среда CLR поддерживает два вида сборок: с нестрогими именами (weakly named assemblies) и со строгими именами (strongly named assemblies).
ВНИМАНИЕ
Вы никогда не встретите термин «сборка с нестрогим именем» в документации по .NET Framework. Почему? А потому, что я сам его придумал. В действительности в документации нет термина для обозначения сборки, у которой отсутствует строгое имя. Я решил обозначить такие сборки специальным термином, чтобы по тексту было однозначно понятно, о каких сборках идет речь.
Сборки со строгими и нестрогими именами имеют идентичную структуру, то есть в них используется файловый формат РЕ (portable executable), заголовок РЕ32(+), CLR-заголовок, метаданные, таблицы манифеста, а также IL-код, рассмотренный в главах 1 и 2. Оба типа сборок компонуются при помощи одних и тех же инструментов, например компилятора C# или AL.exe. В действительности сборки со строгими и нестрогими именами отличаются тем, что первые подписаны при помощи пары ключей, уникально идентифицирующей издателя сборки. Эта пара ключей позволяет уникально идентифицировать сборку, обеспечивать ее безопасность, управлять ее версиями, а также развертывать в любом месте пользовательского жесткого диска или даже в Интернете. Возможность уникальной идентификации сборки позволяет CLR при попытке привязки приложения к сборке со строгим именем применить определенные политики, которые гарантируют безопасность. Эта глава посвящена разъяснению сущности сборок со строгим именем и политик, применяемых к ним со стороны CLR.
Развертывание сборки может быть закрытым или глобальным. Сборки первого типа развертываются в базовом каталоге приложения или в одном из его подкаталогов. Для сборки с нестрогим именем возможно лишь закрытое развертывание. О сборках с закрытым развертыванием речь шла в главе 2. Сборку с глобальным развертыванием устанавливают в каком-либо общеизвестном каталоге, который CLR проверяет при поиске сборок. Такие сборки можно развертывать как закрыто, так и глобально. В этой главе объяснено, как создают и развертывают сборки со строгим именем. Сведения о типах сборок и способах их развертывания представлены в табл. 3.1.
Таблица 3.1. Возможные способы развертывания сборок со строгими и нестрогими именами
|
|
Назначение сборке строгого имени
Если сборка должна использоваться несколькими приложениями, ее следует поместить в общеизвестный каталог, который среда CLR должна автоматически проверять, обнаружив ссылку на сборку. Однако при этом возникает проблема — две (или больше) компании могут выпустить сборки с одинаковыми именами. Тогда, если обе эти сборки будут скопированы в один общеизвестный каталог, «победит» последняя из них, а работа приложений, использовавших первую, нарушится — ведь первая при копировании заменяется второй (это и является причиной «кошмара DLL» в современных системах Windows — все библиотеки DLL копируются в папку System 32).
Очевидно, одного имени файла мало, чтобы различать две сборки. Среда CLR должна поддерживать некий механизм, позволяющий уникально идентифицировать сборку. Именно для этого и служат строгие имена. У сборки со строгим именем четыре атрибута, уникально ее идентифицирующих: имя файла (без расширения), номер версии, идентификатор регионального стандарта и открытый ключ. Поскольку открытые ключи представляют собой очень большие числа, вместо последнего атрибута используется небольшой хеш-код открытого ключа, который называют
![]() | ![]() |
маркером открытого ключа (public key token). Следующие четыре строки, которые иногда называют отображаемым именем сборки (assembly display name), идентифицируют совершенно разные файлы сборки:
Первая строка идентифицирует файл сборки MyTypes.exe или MyTypes.dll (на самом деле, по строке идентификации нельзя узнать расширение файла сборки). Компания-производитель назначила сборке номер версии 1.0.8123.0, в ней нет компонентов, зависимых от региональных стандартов, так как атрибут Culture определен как neutral. Но сделать сборку MyTypes.dll (или MyTypes.exe) с номером версии 1.0.8123.0 и нейтральными региональными стандартами может любая компания.
Должен быть способ отличить сборку, созданную этой компанией, от сборок других компаний, которым случайно были назначены те же атрибуты. В силу ряда причин компания Microsoft предпочла другим способам идентификации (при помощи GUID, URL и URN) стандартные криптографические технологии, основанные на паре из закрытого и открытого ключей. В частности, криптографические технологии позволяют проверять целостность данных сборки при установке ее на жесткий диск, а также назначать разрешения доступа к сборке в зависимости от ее издателя. Все эти механизмы обсуждаются далее в этой главе. Итак, компания, желающая снабдить свои сборки уникальной меткой, должна получить пару ключей — открытый и закрытый, после чего открытый ключ можно будет связать со сборкой. У всех компаний будут разные пары ключей, поэтому они смогут создавать сборки с одинаковыми именами, версиями и региональными стандартами, не опасаясь возникновения конфликтов.
ПРИМЕЧАНИЕ
Вспомогательный класс System.Reflection.AssemblyName позволяет легко генерировать имя для сборки, а также получать отдельные части имени сборки. Он поддерживает ряд открытых экземплярных свойств: Culturelnfo, FullName, KeyPair, Name и Version — и предоставляет открытые методы экземпляров, такие как GetPublicKey, GetPublicKeyToken, SetPublicKey и SetPublicKeyToken.
В главе 2 я показал, как назначить имя файлу сборки, как задать номера версии и идентификатор регионального стандарта. У сборки с нестрогим именем атрибуты номера версии и региональных стандартов могут быть включены в метаданные манифеста. Однако в этом случае CLR всегда игнорирует номер версии, а при поиске сопутствующих сборок использует лишь идентификатор региональных стандартов. Поскольку сборки с нестрогими именами всегда развертываются в закрытом режиме, для поиска файла сборки в базовом каталоге приложения или в одном из
его подкаталогов, указанном атрибутом privatePath конфигурационного XML- файла, CLR просто берет имя сборки (добавляя к нему расширение DLL или EXE).
Кроме имени файла, у сборки со строгим именем есть номер версии и идентификатор региональных стандартов. Кроме того, она подписана при помощи закрытого ключа издателя.
Первый этап создания такой сборки — получение ключа при помощи утилиты Strong Name (SN.exe), поставляемой в составе .NET Framework SDK и Microsoft Visual Studio. Эта утилита поддерживает множество функций, которыми пользуются, задавая в командной строке соответствующие параметры. Заметьте: все параметры командной строки SN.exe чувствительны к регистру. Чтобы сгенерировать пару ключей, выполните следующую команду:
SN -k MyCompany.snk
Эта команда заставит SN.exe создать файл MyCompany.snk, содержащий открытый и закрытый ключи в двоичном формате.
Числа, образующие открытый ключ, очень велики. При необходимости после создания этого файла можно использовать SN.exe, чтобы увидеть открытый ключ. Для этого нужно выполнить SN.exe дважды: сначала с параметром -р, чтобы создать файл, содержащий только открытый ключ (MyCompany .PublicKey)[5]:
SN -р MyCompany.keys MyCompany.PublicKey
Затем SN.exe выполняется с параметром -tp с указанием файла, содержащего
открытый ключ:
SN -tp MyCompany.PublicKey
На своем компьютере я получил следующий результат:
Microsoft (R) .NET Framework Strong Name Utility Version 4.0.30319.17929 Copyright (c) Microsoft Corporation. All rights reserved.
Public key (hash algorithm: sha256):
00240000048000009400000006020000002400005253413100040000010001003f9d621b702111 850be453b92bd6a58c020eb7b804f75d67ab302047fc786ffa3797b669215afb4d814a6f294010 b233bac0b8c8098ba809855da256d964c0d07fl6463d918d651a4846a62317328cac893626a550 69f21al25bc03193261176dd629eace6c90d36858de3fcb781bfc8b817936a567cad608ae672b6 Ifb80eb0
Public key token is 3db32f38c8b42c9a
При этом невозможно заставить SN.exe аналогичным образом отобразить закрытый ключ.
Большой размер открытых ключей затрудняет работу с ними. Чтобы облегчить жизнь разработчику (и конечному пользователю), были созданы маркеры открытого ключа. Маркер открытого ключа — это 64-разрядный хеш-код открытого ключа. Если вызвать утилиту SN.exe с параметром — tр, то после значения ключа она выводит соответствующий маркер открытого ключа.
Теперь мы знаем, как создать криптографическую пару ключей, и получение сборки со строгим именем не должно вызывать затруднений. При компиляции сборки необходимо задать компилятору параметр /keyfilе:имя_файла\
esc /keyfile:MyCompany.snk Program.es
Обнаружив в исходном тексте этот параметр, компилятор C# открывает заданный файл (MyCompany.snk), подписывает сборку закрытым ключом и встраивает открытый ключ в манифест сборки. Заметьте: подписывается лишь файл сборки, содержащий манифест, другие файлы сборки нельзя подписать явно.
В Visual Studio новая пара ключей создается в окне свойств проекта. Для этого перейдите на вкладку Signing, установите флажок Sign the assembly, а затем в поле со списком Choose a strong name keyfile выберите вариант <New...>.
Слова «подписание файла» означают здесь следующее: при компоновке сборки со строгим именем в таблицу метаданных манифеста FileDef заносится список всех файлов, составляющих эту сборку. Каждый раз, когда к манифесту добавляется имя файла, рассчитывается хеш-код содержимого этого файла, и полученное значение сохраняется вместе с именем файла в таблице FileDef. Можно заменить алгоритм хеширования, используемый по умолчанию, вызвав AL.exe с параметром /algid или задав на уровне сборки следующий атрибут, определяемый пользователем, — System. Ref lection. AssemblyAlgorithmldAttribute. По умолчанию хеш-код вычисляется по алгоритму SHA-1.
После компоновки PE-файла с манифестом рассчитывается хеш-код всего содержимого этого файла (за исключением подписи Authenticode Signature, строгого имени сборки и контрольной суммы заголовка РЕ), как показано на рис. 3.1. Для этой операции применяется алгоритм SE1A-1, здесь его нельзя заменить никаким другим. Значение хеш-кода подписывается закрытым ключом издателя, а полученная в результате цифровая подпись RSA заносится в зарезервированный раздел PE-файла (при хешировании PE-файла этот раздел исключается), и в CLR-заголовок PE-файла записывается адрес, по которому встроенная цифровая подпись находится в файле.
В этот PE-файл также встраивается открытый ключ издателя (он записывается в таблицу AssemblyDef метаданных манифеста). Комбинация имени файла, версии сборки, региональных стандартов и значения открытого ключа составляет строгое имя сборки, которое гарантированно является уникальным. Две разных компании ни при каких обстоятельствах не смогут создать две одинаковые сборки, скажем, с именем Calculus, с той же парой ключей (без явной передачи ключей).
Теперь сборка и все ее файлы готовы к упаковке и распространению.
Calculus.dll Рис. 3.1. Подписание сборки |
Как отмечено в главе 2, при компиляции исходного текста компилятор обнаруживает все типы и члены, на которые ссылается исходный текст. Также компилятору необходимо указать все сборки, на которые ссылается данная сборка. В случае компилятора C# для этого применяется параметр /reference. В задачу компилятора входит внедрение таблицы метаданных AssemblyRef в результирующий управляемый модуль. Каждая запись таблицы метаданных AssemblyRef описывает файл сборки, на которую ссылается данная сборка, и состоит из имени файла сборки (без расширения), номера версии, регионального стандарта и значения открытого ключа.
ВНИМАНИЕ
Поскольку значение открытого ключа велико, в том случае, когда сборка ссылается на множество других сборок, значения открытых ключей могут занять значительную часть результирующего файла. Для экономии места в компании Microsoft хешируют открытый ключ и берут последние 8 байт полученного хеш-кода. В таблице AssemblyRef на самом деле хранятся именно такие усеченные значения открытого ключа — маркеры отрытого ключа. В общем случае разработчики и конечные пользователи намного чаще встречаются с маркерами, чем с полными значениями ключа.
Вместе с тем нужно иметь в виду, что среда CLR никогда не использует маркеры открытого ключа в процессе принятия решений, касающихся безопасности или доверия, потому что одному маркеру может соответствовать несколько открытых ключей.
Далее приведены метаданные таблицы AssemblyRef (полученные средствами ILDasm.exe) для файла MultiFileLibrary.dll, обсуждавшегося в главе 2:
AssemblyRef #1 (23000001)
Token: 0x23000001
Public Key or Token: b7 7a 5c 56 19 34 e0 89
Name: mscorlib
Version: 4.0.0.0
Major Version: 0x00000004
Minor Version: 0x00000000
Build Number: 0x00000000
Revision Number: 0x00000000
Locale: <null>
HashValue Blob:
Flags: [none] (00000000)
Из этих сведений видно, что файл MultiFileLibrary.dll ссылается на тип, расположенный в сборке со следующими атрибутами:
"MSCorLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
К сожалению, в утилите ILDasm.exe используется термин Locale, хотя на самом деле там должно быть слово Culture.
Взглянув на содержимое таблицы метаданных AssemblyDef файла MultiFileLibrary. dll, мы увидим следующее:
Assembly
Token: 0x20000001 Name : MultiFileLibrary Public Key :
Hash Algorithm : 0x00008004 Version: 3.0.0.0 Major Version: 0x00000003 Minor Version: 0x00000000 Build Number: 0x00000000 Revision Number: 0x00000000 Locale: <null>
Flags : [none] (00000000)
Это эквивалентно следующей строке:
"MultiFileLibrary, Version=3.0.0.0, Culture=neutral, PublicKeyToken=null"
Здесь открытый ключ не определен, поскольку сборка MultiFileLibrary.dll, созданная в главе 2, не была подписана открытым ключом и, следовательно, является сборкой с нестрогим именем. Если бы я создал файл с ключами при помощи утилиты SN.exe, а затем скомпилировал сборку с параметром /keyfile, то получилась бы подписанная сборка. Если просмотреть метаданные полученной таким образом сборки при помощи утилиты ILDasm.exe, в соответствующей записи таблицы AssemblyDef обнаружится заполненное поле Public Key, говорящее о том, что это сборка со строгим именем. Кстати, запись таблицы AssemblyDef всегда хранит полное значение открытого ключа, а не его маркер. Полный открытый ключ гарантирует целостность файла. Позже я объясню принцип, лежащий в основе устойчивости к несанкционированной модификации сборок со строгими именами.
Глобальный кэш сборок
Теперь вы знаете, как создаются сборки со строгим именем — пора научиться развертывать такие сборки и узнать, как CLR использует метаданные для поиска и загрузки сборки.
Если сборка предназначена для совместного использования несколькими приложениями, ее нужно поместить в общеизвестный каталог, который среда CLR должна автоматически проверять при обнаружении ссылки на сборку. Место, где располагаются совместно используемые сборки, называют глобальным кэшем сборок (global assembly cache, GAC). Точное местонахождение GAC - подробность реализации, которая может изменяться в будущих версиях .NET Framework. Тем не менее обычно GAC находится в каталоге
%SystemRoot%\Microsoft.NET\Assembly
GAC имеет иерархическое строение и содержит множество вложенных каталогов, имена которых генерируются по определенному алгоритму. Ни в коем случае не следует копировать файлы сборок в GAC вручную — вместо этого надо использовать инструменты, созданные специально для этой цели. Эти инструменты «знают» внутреннюю структуру GAC и умеют генерировать надлежащие имена подкаталогов.
В период разработки и тестирования сборок со строгими именами для установки их в каталог GAC чаще всего применяют инструмент GACUtil.exe. Запущенный без параметров, он отобразит следующие сведения:
Microsoft (R) .NET Global Assembly Cache Utility. Version 4.0.30319.17929 Copyright (c) Microsoft Corporation. All rights reserved.
Usage: Gacutil <command» [ «options» ]
Commands:
/i <assembly_path» [ /r <...» ] [ /f ]
Installs an assembly to the global assembly cache.
/il <assembly_path_list_file» [ /r <...»][ /f ]
Installs one or more assemblies to the global assembly cache.
/u <assembly_display_name» [ /r <...» ]
Uninstalls an assembly from the global assembly cache.
/ul <assembly_display_name_list_file» [ /r <...» ]
Uninstalls one or more assemblies from the global assembly cache.
/1 [ <assembly_name> ]
List the global assembly cache filtered by <assembly_name>
/1г [ <assembly_name> ]
List the global assembly cache with all traced references, /cdl
Deletes the contents of the download cache /ldl
Lists the contents of the download cache
/?
Displays a detailed help screen Options:
/r <reference_scheme> <reference_id> <description>
Specifies a traced reference to install {/i, /il) or uninstall (/u, /ul).
/f
Forces reinstall of an assembly.
/nologo
Suppresses display of the logo banner /silent
Suppresses display of all output
Вызвав утилиту GACUtil.exe с параметром /i, можно установить сборку в каталог GAC, а с параметром /и сборка будет удалена из GAC. Обратите внимание, что сборку с нестрогим именем нельзя поместить в GAC. Если передать GACUtil. ехе файл сборки с нестрогим именем, утилита выдаст следующее сообщение об ошибке (ошибка добавления сборки в кэш: попытка установить сборку без строгого имени):
Failure adding assembly to the cache: Attempt to install an assembly without a strong name
ПРИМЕЧАНИЕ
По умолчанию манипуляции с каталогом GAC могут осуществлять лишь члены группы Windows Administrators. GACUtil.exe не сможет установить или удалить сборку, если вызвавший утилиту пользователь не входит в эту группу.
Параметр /i утилиты GACUtil.exe очень удобен для разработчика во время тестирования. Однако при использовании GACUtil.exe для развертывания сборки в рабочей среде рекомендуется применять параметр /г в дополнение к /i — при установке и /и — при удалении сборки. Параметр /г обеспечивает интеграцию сборки с механизмом установки и удаления программ Windows. По сути вызов утилиты с этим параметром сообщает системе, для какого приложения требуется эта сборка, и связывает их между собой.
104 Глава 3. Совместно используемые сборки и сборки со строгим именем ПРИМЕЧАНИЕ
Если сборка со строгим именем упакована в САВ-файл или сжата иным способом, то, прежде чем устанавливать файл сборки в каталог GAC при помощи утилиты GACUtil.exe, следует распаковать его во временный файл, который нужно удалить после установки сборки.
Утилита GACUtil.exe не входит в состав свободно распространяемого пакета .NET Framework, предназначенного для конечного пользователя. Если в приложении есть сборки, которые должны развертываться в каталоге GAC, используйте программу Windows Installer (MSI), так как это единственный инструмент, способный установить сборки в GAC и гарантированно присутствующий на машине конечного пользователя.
ВНИМАНИЕ
Глобальное развертывание сборки путем размещения ее в каталогGAC —это один из видов регистрации сборки в системе, хотя это никакие затрагивает реестр Windows. Установка сборок в GAC делает невозможными простые установку, копирование, восстановление, перенос и удаление приложения. По этой причине рекомендуется избегать глобального развертывания и использовать закрытое развертывание сборок всюду, где это только возможно.
Зачем «регистрировать» сборку в каталоге GAC? Представьте себе, что две компании сделали каждая свою сборку OurLibrary, состоящую из единственного файла: OurLibrary.dll. Очевидно, эти файлы нельзя записывать в один каталог, поскольку файл, копируемый последним, перезапишет первый и тем самым нарушит работу какого-нибудь приложения. Если для установки в GAC использовать специальный инструмент, он создаст в каталоге %SystemRoot%\Microsoft.NET\Assembly отдельные папки для каждой из этих сборок и скопирует каждую сборку в свою папку.
Обычно пользователи не просматривают структуру каталогов GAC, поэтому для вас она не имеет реального значения. Довольно того, что структура каталогов GAC известна CLR и инструментам, работающим с GAC.
Построение сборки, ссылающейся на сборку со строгим именем
Какую бы сборку вы ни строили, в результате всегда получается сборка, ссылающаяся на другие сборки со строгими именами. Это утверждение верно хотя бы потому, что класс System.Object определен в MSCorLib.dll, сборке со строгим именем. Однако велика вероятность того, что сборка также будет ссылаться на типы из других сборок со строгими именами, изданными Microsoft, сторонними
разработчиками либо созданными в вашей организации. В главе 2 показано, как использовать компилятор CSC.exe с параметром /reference для определения сборки, на которую должна ссылаться создаваемая сборка. Если вместе с именем файла задать полный путь к нему, CSC.exe загрузит указанный файл и использует его метаданные для построения сборки. Как отмечено в главе 2, если задано имя файла без указания пути, CSC.exe пытается найти нужную сборку в следующих каталогах (просматривая их в порядке перечисления).
1. Рабочий каталог.
2. Каталог, где находится файл CSC.exe. Этот каталог также содержит DLL- библиотеки CLR.
3. Каталоги, заданные параметром /НЬ командной строки компилятора.
4. Каталоги, указанные в переменной окружения LIB,
Таким образом, чтобы скомпоновать сборку, ссылающуюся на файл System. Drawing.dll разработки Microsoft, при вызове CSC.exe можно задать параметр /reference: System. Drawing. dll. Компилятор проверит перечисленные каталоги и обнаружит файл System.Drawing.dll в одном каталоге с файлом CSC. ехе — том же, который содержит библиотеки DLL версии CLR, которую сам использует для создания сборки. Однако несмотря на то, что при компиляции сборка находится в этом каталоге, во время выполнения эта сборка загружается из другого каталога.
Дело в том, что во время установки .NET Lramework все файлы сборок, созданных Microsoft, устанавливаются в двух экземплярах. Один набор файлов заносится в один каталог с CLR, а другой — в каталог GAC. Файлы в каталоге CLR облегчают построение пользовательских сборок, а их копии в GAC предназначены для загрузки во время выполнения.
CSC.exe не ищет нужные для компоновки сборки в GAC, потому что для этого вам пришлось бы задавать путь к файлу сборки, а структура GAC не документирована. Также можно было бы задавать сборки при помощи не менее длинной, но чуть более изящной строки вида:
System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken= b03f5f7flld50a3a
Оба способа были настолько неуклюжими, что было решено предпочесть им установку на пользовательский жесткий диск двух копий файлов сборок.
Кроме того, сборки в каталоге CLR не привязаны к машине. Иначе говоря, эти сборки содержат только метаданные. Так как код IL не нужен на стадии построения, в этом каталоге не нужно хранить версии сборки для х86, х64 и ARM. Сборки в GAC содержат метаданные и IL, потому что код нужен только во время выполнения. А поскольку код может оптимизироваться для конкретной архитектуры процессора, в GAC могут храниться несколько версий одной сборки; они находятся в разных подкаталогах, соответствующих разным архитектурам процессоров.
Устойчивость сборок со строгими именами к несанкционированной модификации
Подписание файла закрытым ключом и внедрение подписи и открытого ключа в сборку позволяет CLR убедиться в том, что сборка не была модифицирована или повреждена. При установке сборки в GAC система хеширует содержимое файла с манифестом и сравнивает полученное значение с цифровой подписью RSA, встроенной в PE-файл (после извлечения подписи с помощью открытого ключа). Идентичность значений означает, что содержимое файла не было модифицировано. Кроме того, система хеширует содержимое других файлов сборки и сравнивает полученные значения с таковыми из таблицы манифеста FileDef. Если хоть одно из значений не совпадает, значит хотя бы один из файлов сборки был модифицирован и попытка установки сборки в каталог GAC окончится неудачей.
Когда приложению требуется привязка к сборке, на которую оно ссылается, CLR использует для поиска этой сборки в GAC ее свойства (имя, версию, региональные стандарты и открытый ключ). Если нужная сборка обнаруживается, возвращается путь к каталогу, в котором она находится, и загружается файл с ее манифестом. Такой механизм поиска сборок гарантирует вызывающей стороне, что во время выполнения будет загружена сборка издателя, создавшего ту сборку, с которой компилировалась программа. Такая гарантия возможна благодаря соответствию маркера открытого ключа, хранящегося в таблице Assembly Ref ссылающейся сборки, открытому ключу из таблицы AssemblyDef сборки, на которую ссылаются. Если вызываемой сборки нет в GAC, CLR сначала ищет ее в базовом каталоге приложения, затем проверяет все закрытые пути, указанные в конфигурационном файле приложения; потом, если приложение установлено при помощи MSI, CLR просит MSI найти нужную сборку. Если ни в одном из этих вариантов сборка не находится, привязка заканчивается неудачей и выдается исключение System. 10.FileNotFoundException.
ПРИМЕЧАНИЕ
Когда сборка со строгим именем загружается из каталога GAC, система гарантирует, что файлы, содержащие манифест, не подверглись несанкционированной модификации. Эта проверка происходит только один раз на этапе установки. Для улучшения производительности среда CLR не проверяет, были ли файлы несанкционированно модифицированы, и загружает их в домен приложений с полными правами. В то же время, когда сборка со строгим именем загружается не из каталога GAC, среда CLR проверяет файл манифеста сборки дабы удостовериться в том, что он устойчив к несанкционированной модификации, занимая дополнительное время для проверки каждый раз во время загрузки этого файла.
При загрузке сборки со строгим именем не из GAC, а из другого каталога (базового каталога приложения или каталога, заданного элементом CodeBase в конфигурационном файле) CLR проверяет ее хеш-коды. Иначе говоря, в данном случае хеширование файла выполняется при каждом запуске приложения. При этом быстродействие несколько снижается, но можно гарантировать, что содержимое сборки не подверглось несанкционированной модификации. Обнаружив во время выполнения несоответствие хеш-кодов, CLR выдает исключение System. 10.FileLoadException.
Отложенное подписание
Ранее в этой главе обсуждался способ получения криптографической пары ключей при помощи утилиты SN.exe. Эта утилита генерирует ключи, вызывая функции предоставленного Microsoft криптографического API-интерфейса под названием Crypto. Полученные в результате ключи могут сохраняться в файлах на любых запоминающих устройствах. Например, в крупных организациях (вроде Microsoft) генерируемые закрытые ключи хранятся на аппаратных устройствах в сейфах, и лишь несколько человек из штата компании имеют доступ к закрытым ключам. Эти меры предосторожности предотвращают компрометацию закрытого ключа и обеспечивают его целостность. Ну, а открытый ключ, естественно, общедоступен и распространяется свободно.
Подготовившись к компоновке сборки со строгим именем, надо подписать ее закрытым ключом. Однако при разработке и тестировании сборки очень неудобно то и дело доставать закрытый ключ, который хранится «за семью печатями», поэтому .NET Framework поддерживает отложенное подписание (delayed signing), также иногда называемое частичным (partial signing). Отложенное подписание позволяет построить сборку с открытым ключом компании, не требуя закрытого ключа. Открытый ключ дает возможность встраивать в записи таблицы AssemblyRef сборки, ссылающиеся на вашу сборку, получать правильное значение открытого ключа, а также корректно размещать эти сборки во внутренней структуре GAC. Не подписывая файл закрытым ключом, вы полностью лишаетесь защиты от несанкционированной модификации, так как при этом не хешируются файлы сборки, а цифровая подпись не включается в файл. Однако на данном этапе это не проблема, поскольку подписание сборки откладывается лишь на время ее разработки, а готовая к упаковке и развертыванию сборка подписывается закрытым ключом.
Обычно открытый ключ компании получают в виде файла и передают его утилитам, компонующим сборку. (Как уже отмечалось в этой главе, для извлечения открытого ключа из файла, содержащего пару ключей, можно вызвать утилиту SN.exe с параметром -р.) Следует также указать программе построения сборку, подписание которой будет отложено, то есть ту, что будет скомпонована без закрытого ключа. В компиляторе C# для этого служит параметр /delaysign. В Visual Studio в окне свойств проекта нужно перейти на вкладку Signing и установить флажок Delay sign only. При использовании утилиты AL.exe необходимо задать параметр /delay [sign ].
Обнаружив, что подписание сборки откладывается, компилятор или утилита AL.exe генерирует в таблице метаданных сборки AssemblyDef запись с открытым ключом сборки. Как было сказано ранее, наличие открытого ключа позволяет разместить эту сборку в GAC, а также создавать другие сборки, ссылающиеся на нее, при этом у них в записях таблицы метаданных AssembyRef будет верное значение открытого ключа. При построении сборки в результирующем PE-файле остается место для цифровой подписи RSA. (Программа построения определяет размер необходимого свободного места, исходя из размера открытого ключа.) Кстати, и на этот раз хеширование содержимого файлов не производится.
На этом этапе результирующая сборка не имеет действительной цифровой подписи. Попытка установки такой сборки в GAC окончится неудачей, так как хеш-код содержимого файла не был рассчитан, что создает видимость повреждения файла. Для того чтобы установить такую сборку в GAC, нужно запретить системе проверку целостности файлов сборки, вызвав утилиту SN.exe с параметром командной строки -Vr. Вызов SN.exe с таким параметром также вынуждает CLR пропустить проверку хеш-кода для всех файлов сборки при ее загрузке во время выполнения. С точки зрения внутренней реализации системы параметр -Vr утилиты SN.exe обеспечивает размещение идентификационной информации сборки в разделе реестра HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\StrongName\Verification.
ВНИМАНИЕ
При использовании любой утилиты, имеющей доступ к реестру, необходимо убедиться в том, что для 64-разрядной платформы используется соответствующая 64-разрядная утилита. По умолчанию утилита под 32-разрядную платформу х86 установлена в каталоге C:\Program Files (x86)\Microsoft SDKs\Windows\v8.0A\bin\ NETFX 4.0 Tools, а утилита под 64-разрядную платформу x64 — в каталоге C:\Program Files (x86)\Microsoft SDKs\Windows\v8.0A\bin\NETFX4.0 Tools\x64.
Окончательно протестированную сборку надо официально подписать, чтобы сделать возможными ее упаковку и развертывание. Чтобы подписать сборку, снова вызовите утилиту SN.exe, но на этот раз с параметром -R, указав имя файла, содержащего настоящий закрытый ключ. Параметр -R заставляет SN.exe хешировать содержимое файла, подписать его закрытым ключом и встроить цифровую подпись RSA в зарезервированное свободное место. После этого подписанная по всем правилам сборка готова к развертыванию. Не забудьте снова включить верификацию этой сборки на машинах разработки и тестирования, вызвав SN.exe с параметром -Vu или -Vx.
Полная последовательность действий по созданию сборки с отложенным подписанием выглядит следующим образом.
1. Во время разработки сборки следует получить файл, содержащий лишь открытый ключ компании, и добавить в строку компиляции сборки параметры /key-file и /delaysign:
esc /keyfile:MyCompany.PublicKey /delaysign MyAssembly.es
2. После построения сборки надо выполнить показанную далее команду, чтобы получить возможность тестирования этой сборки, установки ее в каталог GAC и компоновки других сборок, ссылающихся на нее. Эту команду достаточно исполнить лишь раз, не нужно делать это при каждой компоновке сборки.
SN.exe -Vr MyAssembly.dll
3. Подготовившись к упаковке и развертыванию сборки, надо получить закрытый ключ компании и выполнить приведенную далее команду. При желании можно установить новую версию в GAC, но не пытайтесь это сделать до выполнения шага 4.
SN.exe -R MyAssembly.dll MyCompany.PrivateKey
4. Для тестирования сборки в реальных условиях снова включите проверку следующей командой:
SN -Vu MyAssembly.dll
В начале раздела говорилось о хранении ключей организации на аппаратных носителях, например на смарт-картах. Для того чтобы обеспечить безопасность ключей, необходимо следить, чтобы они никогда не записывались на диск в виде файлов. Криптографические провайдеры (Cryptographic Service Providers, CSP) операционной системы предоставляют контейнеры, позволяющие абстрагироваться от физического места хранения ключей. Например, Microsoft использует CSP-провайдера, который при обращении к контейнеру считывает закрытый ключ с устройства.
Если пара ключей хранится в CSP-контейнере, необходимо использовать другие параметры при обращении к утилитам CSC.exe, AL.exe и SN.exe. При компиляции (CSC.exe) вместо /keyfile нужно задействовать параметр /keycontainer, при компоновке (AL.exe) — параметр /keyname вместо /keyfile, а при вызове SN.exe для добавления закрытого ключа к сборке, подписание которой было отложено, параметр -Rc вместо -R. SN.exe поддерживает дополнительные параметры для работы с CSP.
ВНИМАНИЕ
Отложенное подписание удобно, когда необходимо выполнить какие-либо действия над сборкой до ее развертывания. Например, может понадобиться применить к сборке защитные утилиты, модифицирующие до неузнаваемости код. После подписания сборки это сделать уже не удастся, так как хеш-код станет недействительным. Так что если после компоновки сборки нужно ее защитить от декомпиляции или выполнить над ней другие действия, надо применить операцию отложенного подписания. В конце нужно запустить утилиту SN.exe с параметром -R или -Rc, чтобы завершить подписание сборки и рассчитать все необходимые хеш-коды.
Закрытое развертывание сборок со строгими именами
Установка сборок в каталог GAC дает несколько преимуществ. GAC позволяет нескольким приложениям совместно использовать сборки, сокращая в целом количество обращений к физической памяти. Кроме того, при помощи GAC легче развертывать новую версию сборки и заставить все приложения использовать новую версию сборки посредством реализации политики издателя (см. далее). GAC также обеспечивает совместное управление несколькими версиями сборки. Однако GAC обычно находится под защитой механизмов безопасности, поэтому устанавливать сборки в GAC может только администратор. Кроме того, установка сборки в GAC делает невозможным развертывание сборки простым копированием.
Хотя сборки со строгими именами могут устанавливаться в GAC, это вовсе не обязательно. В действительности рекомендуется развертывать сборки в GAC, только если они предназначены для совместного использования несколькими приложениями. Если сборка не предназначена для этого, следует развертывать ее закрыто. Это позволяет сохранить возможность установки путем «простого» копирования и лучше изолирует приложение с его сборками. Кроме того, GAC не задуман как замена каталогу C:\Windows\System32 в качестве «общей помойки» для хранения общих файлов. Это позволяет избежать затирания одних сборок другими путем установки их в разные каталоги, но «отъедает» дополнительное место на диске.
ПРИМЕЧАНИЕ
На самом деле элемент CodeBase конфигурационного файла задает URL-адрес, который может ссылаться на любой каталог пользовательского жесткого диска или на адрес в Web. В случае веб-адреса CLR автоматически загрузит указанный файл и сохранит его в кэше загрузки на пользовательском жестком диске (в подкаталоге C:\Documentsand Settings\<UserName>\Local Settings\ApplicationData\Assembly, где <UserName> — имяучетной записи пользователя, вошедшего в систему). В дальнейшем при ссылке на эту сборку CLR сверит метку времени локального файла и файла по указанному URL-адресу. Если последний новее, CLR загрузит файл только раз (это сделано для повышения производительности). Пример конфигурационного файла с элементом CodeBase будет продемонстрирован позже.
Помимо развертывания в GAC или закрытого развертывания, сборки со строгими именами можно развертывать в произвольном каталоге, известном лишь небольшой группе приложений. Допустим, вы создали три приложения, совместно использующие одну и ту же сборку со строгим именем. После установки можно создать по одному каталогу для каждого приложения и дополнительный каталог для совместно используемой сборки. При установке приложений в их каталоги также записывается конфигурационный XML-файл, а в элемент CodeBase для совместно используемой сборки заносится путь к ней. В результате при выполнении
CLR будет знать, что совместно используемую сборку надо искать в каталоге, содержащем сборку со строгим именем. Учтите, что этот способ применяется довольно редко и в силу ряда причин не рекомендуется. Дело в том, что в таком сценарии ни одно отдельно взятое приложение не в состоянии определить, когда именно нужно удалить файлы совместно используемой сборки.
Как исполняющая среда разрешает ссылки на типы
В начале главы 2 вы видели следующий исходный текст:
public sealed class Program { public static void MainQ {
System.Console.WriteLine("Hi");
}
}
Допустим, в результате компиляции и построения этого кода получилась сборка Program.exe. При запуске приложения происходит загрузка и инициализация CLR. Затем CLR сканирует CLR-заголовок сборки в поисках атрибута MethodDefToken, идентифицирующего метод Main, представляющий точку входа в приложение. CLR находит в таблице метаданных MethodDef смещение, по которому в файле находится IL-код этого метода, и компилирует его в машинный код процессора при помощи JIT-компилятора. Этот процесс включает в себя проверку безопасности типов в компилируемом коде, после чего начинается исполнение полученного машинного кода. Далее показан IL-код метода Main. Чтобы получить его, я запустил ILDasm.exe, выбрал в меню View команду Show Bytes и дважды щелкнул на методе Main в дереве просмотра.
.method public hidebysig static void Main() cil managed // SIG: 00 00 01 {
.entrypoint
// Method begins at RVA 0x2050 // Code size 11 (0xb)
.maxstack 8
IL_0000: /* 72 | (70)000001 */
ldstr "Hi"
IL_0005: /* 28 | (0A)000003 */
call void [mscorlib]System.Console::WriteLine(string)
IL_000a: /* 2A | */
ret
} // end of method Program::Main
Во время JIT-компиляции этого кода CLR обнаруживает все ссылки на типы и члены и загружает сборки, в которых они определены (если они еще не загружены). Как видите, показанный код содержит ссылку на метод System. Con sole. Write- Line: команда Call ссылается на маркер метаданных 0А000003. Этот маркер идентифицирует запись 3 таблицы метаданных MemberRef (таблица 0А). Просматривая эту запись, CLR видит, что одно из ее полей ссылается на элемент таблицы TypeRef (описывающий тип System.Console). Запись таблицы TypeRef направляет CLR к следующей записи в таблице AssemblyRef:
MSCorLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
На этом этапе CLR уже знает, какая сборка нужна, и ей остается лишь найти и загрузить эту сборку.
При разрешении ссылки на тип CLR может найти нужный тип в одном из трех мест.
□ В том же файле. Обращение к типу, расположенному в том же файле, определяется при компиляции (данный процесс иногда пазы паютранним связыванием). Тип загружается прямо из этого файла, и исполнение продолжается.
□ В другом файле той же сборки. Исполняющая среда проверяет, что файл, на который ссылаются, описан в таблице FileRef в манифесте текущей сборки. При этом исполняющая среда ищет его в каталоге, откуда был загружен файл, содержащий манифест сборки. Файл загружается, проверяется его хеш-код, чтобы гарантировать его целостность, затем CLR находит в нем нужный член типа, и исполнение продолжается.
□ В файле другой сборки. Когда тип, на который ссылаются, находится в файле другой сборки, исполняющая среда загружает файл с манифестом этой сборки. Если в файле с манифестом необходимого типа нет, загружается соответствующий файл, CLR находит в нем нужный член типа, и исполнение продолжается.
ПРИМЕЧАНИЕ
Таблицы метаданных ModuleDef, ModuleRef и FileDef ссылаются на файлы по имени и расширению. Однако таблица метаданных AssemblyRef ссылается на сборки только по имени, без расширения. Во время привязки ксборке система автоматически добавляет к имени файла расширение DLL или EXE, пытаясь найти файл путем проверки каталогов по алгоритму, описанному в главе 2.
Если во время разрешения ссылки на тип возникают ошибки (не удается найти или загрузить файл, не совпадает значение хеш-кода и т. п.), выдается соответствующее исключение.
ПРИМЕЧАНИЕ
При желании можно зарегистрировать в вашем коде методы обратного вызова с событиями из System.AppDomain.AssemblyResolve, System.AppDomain. ReflectionOnlyAssemblyRessolve и System.AppDomain. TypeResolve. В методах обратного вызова вы можете выполнить программный код, который решает эту проблему и позволяет приложению выполняться без выбрасывания исключения.
В предыдущем примере среда CLR обнаруживала, что тип System. Console реализован в файле другой сборки. CLR должна найти эту сборку и загрузить РЕ-файл, содержащий ее манифест. После этого манифест сканируется в поисках сведений о PE-файле, в котором реализован искомый тип. Если необходимый тип содержится в том же файле, что и манифест, все замечательно, а если в другом файле, то CLR загружает этот файл и просматривает его метаданные в поисках нужного типа. После этого CLR создает свою внутреннюю структуру данных для представления типа и JIT-компилятор завершает компиляцию метода Main. В завершение процесса начинается исполнение метода Main.
Рисунок 3.2 иллюстрирует процесс привязки к типам.
Рис. 3.2. Блок-схема алгоритма поиска метаданных, используемых CLR, файла сборки, где определен тип или метод, на который ссылается IL-код |
Если какая-либо операция заканчивается неудачей, то выдается соответствующее исключение.
Есть еще один нюанс: CLR идентифицирует все сборки по имени, версии, региональному стандарту и открытому ключу. Однако GAC различает сборки по имени, версии, региональному стандарту, открытому ключу и процессорной архитектуре. При поиске сборки в GAC среда CLR выясняет, в каком процессе выполняется при
ложение — 32-разрядном х86 (возможно, с использованием технологии WoW64), 64-разрядном х64 или 64-разрядном ARM. Сначала выполняется поиск сборки в GAC с учетом процессорной архитектуры. В случае неудачи происходит поиск сборки без учета процессорной архитектуры.
ВНИМАНИЕ
Строго говоря, приведенный пример не является стопроцентно верным. Для ссылок на методы и типы, определенные в сборке, не поставляемой в составе .NET Framework, все сказанное верно. Однако сборки .NET Framework (в том числе MSCorLib.dll) тесно связаны с работающей версией CLR. Любая сборка, ссылающаяся на сборки .NET Framework, всегда привязывается к соответствующей версии CLR. Этот процесс называют унификацией (unification), и Microsoft его поддерживает, потому что в этой компании все сборки .NET Framework тестируются с конкретной версией CLR. Поэтому унификация стека кода гарантирует корректную работу приложений.
В предыдущем примере ссылка на метод WriteLine объекта System.Console привязывается к версии MSCorLib.dll, соответствующей версии CLR, независимо от того, на какую версию MSCorLib.dll ссылается табл nnaAssemblyRef в метаданных сборки.
Из этого раздела мы узнали, как CLR ищет сборки, когда действует политика, предлагаемая по умолчанию. Однако администратор или издатель сборки может заменить эту политику. Способу изменения политики привязки CLR по умолчанию посвящены следующие два раздела.
ПРИМЕЧАНИЕ
CLR поддерживает возможность перемещения типа (класса, структуры, перечислимого типа, интерфейса или делегата) из одной сборки в другую. Например, в .NET 3.5 класс System.TimeZonelnfo определен в сборке System.Core.dll. Но в .NET 4.0 компания Microsoft переместила этот класс в сборку MsCorLib.dll. В стандартной ситуации перемещение типа из одной сборки в другую нарушает работу приложения. Однако CLR предлагает воспользоваться атрибутом System.Runtime.CompilerServices. TypeForwardedToAttribute, который применяется в оригинальной сборке (например, System.Core.dll). Конструктору атрибута передается параметр типа System. Туре. Он обозначает новый тип (который теперь определенный в MSCorLib.dll), который теперь должно использовать приложение. С того момента, как конструктор TypeForwardedToAttribute принимает этот тип, содержащая его сборка будет зависеть от сборки, в которой он определен.
Если вы воспользуетесь этим преимуществом, нужно также применить атрибут System.Runtime.CompilerServices.TypeForwardedToAttribute в новой сборке и указать конструктору атрибута полное имя сборки, которая служит для определения типа. Этот атрибут обычно используется для инструментальных средств, утилит и сериализации. Как только конструктор TypeForwardedToAttribute получает строку с этим именем, сборка, содержащая этот атрибут, становится независимой от сборки, определяющей тип.
Дополнительные административные средства (конфигурационные файлы)
В разделе «Простое средство администрирования (конфигурационный файл)» главы 2 мы кратко познакомились со способами изменения администратором алгоритма поиска и привязки к сборкам, используемого CLR. В том же разделе я показал, как перемещать файлы сборки, на которую ссылаются, в подкаталог базового каталога приложения и как CLR использует конфигурационный XML-файл приложения для поиска перемещенных файлов.
Поскольку в главе 2 обсуждался лишь атрибут privatePath элемента probing, здесь мы рассмотрим остальные элементы конфигурационного XML-файла:
<?xml version=""1.0""?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemasmicrosoftcom:asm.vl">
<probing privatePath="AuxFilesjbin\subdir" />
<dependentAssembly»
<assemblyldentity name="SomeClassLibrary"
publicKeyToken="32ab4ba45e0a69al" culture="neutral"/>
cbindingRedirect
oldVersion="l.0.0.0" newVersion="2.0.0.0" />
<codeBase version="2.0.0.0"
href="http://www.Wlntellect.com/SomeClassLibrary.dll" />
</dependentAssembly»
cdependentAssembly»
<assemblyIdentity name="TypeLib"
publicKeyToken="lf2e74e897abbcfe" culture="neutral"/»
cbindingRedirect
oldVersion="3.0.0.03.5.0.0" newVersion="4.0.0.0" />
cpublisherPolicy apply="no" />
</dependentAssembly»
</assemblyBinding»
</runtime>
</configuration»
XML-файл предоставляет CLR обширную информацию.
□ Элемент probing. Определяет поиск в подкаталогах AuxFiles и bin\subdir, расположенных в базовом каталоге приложения, при попытке найти сборку с нестрогим
именем. Сборки со строгим именем ищутся в GAC или по URL-адресу, указанному элементом CodeBase. CLR ищет сборки со строгим именем в закрытых каталогах приложения только в том случае, если элемент CodeBase не указан.
□ Первый набор элементов dependentAssembly, assemblyldentity и binding- Redirect. При попытке найти сборки SomeClassLibrary с номером версии
1.0. 0.0 и нейтральными региональными стандартами, изданные организацией, владеющей открытым ключом с маркером 32ab4ba45e0a69al, система вместо этого будет искать аналогичную сборку, но с номером версии 2.О.О.О.
□ Элемент codebase. При попытке найти сборку SomeClassLibrary с номером версии 2.0.0.0 и нейтральными региональными стандартами, изданную организацией, владеющей открытым ключом с маркером 32ab4ba45e0a69al, система будет пытаться выполнить привязку по адресу, заданному в URL: http://wwwWintellect. com/SomeClassLibrary.dll. Хотя я и не говорил об этом в главе 2, элемент CodeBase можно применять и к сборкам с нестрогими именами. При этом номер версии сборки игнорируется и его следует опустить при определении элемента CodeBase. URL-адрес, заданный элементом CodeBase, должен ссылаться на подкаталог базового каталога приложения.
□ Второй набор элементов dependentAssembly, assemblyldentity и binding- Redirect. При попытке найти сборку TypeLib с номерами версии от 3.0.0.0 до
3.5.0. 0 включительно и нейтральными региональными стандартами, изданную организацией, владеющей открытым ключом с маркером If2e74e897abbcfe, система будет искать версию 4.0.0.0 той же сборки.
□ Элемент publisherPolicy. Если организация, производитель сборки TypeLib, развернула файл политики издателя (описание этого файла см. в следующем разделе), среда CLR должна игнорировать этот файл.
При компиляции метода CLR определяет типы и члены, на которые он ссылается. Используя эти данные, исполняющая среда определяет (путем просмотра таблицы AssemblyRef вызывающей сборки), на какую сборку исходно ссылалась вызывающая сборка во время компоновки. Затем CLR ищет сведения о сборке в конфигурационном файле приложения и следует любым изменениям номера версии, заданным в этом файле.
Если значение атрибута apply элемента publisherPolicy равно yes или отсутствует, CLR проверяет наличие в GAC новой сборки/версии и применяет все перенаправления, которые счел необходимым указать издатель сборки (о политике издателя рассказывается в следующем разделе); далее CLR ищет именно эту сбор- ку/версию. Наконец CLR просматривает сборку/версию в файле Machine.config и применяет все указанные в нем перенаправления к другим версиям.
На этом этапе среда CLR знает номер версии сборки, которую она должна загрузить, и пытается загрузить соответствующую сборку из GAC. Если сборки в GAC нет, а элемент CodeBase не определен, CLR пытается найти сборку, как описано в главе 2. Если конфигурационный файл, задающий последнее изменение номера версии, содержит элемент CodeBase, CLR пытается загрузить сборку с URL-адреса, заданного этим элементом.
Эти конфигурационные файлы обеспечивают администратору настоящий контроль над решением, принимаемым CLR относительно загрузки той или иной сборки. Если в приложении обнаруживается ошибка, администратор может связаться с издателем сборки, содержащей ошибку, после чего издатель пришлет новую сборку. По умолчанию среда CLR не может загрузить новую сборку, потому что уже существующая сборка не содержит ссылки на новую версию. Однако администратор может заставить CLR загрузить новую сборку, модифицировав конфигурационный XML-файл приложения.
Если администратор хочет, чтобы все сборки, установленные на компьютере, использовали новую версию, то вместо конфигурационного файла приложения он может модифицировать файл Machine.config для данного компьютера, и CLR будет загружать новую версию сборки при каждой ссылке из приложений на старую версию.
Если в новой версии старая ошибка не исправлена, администратор может удалить из конфигурационного файла строки, определяющие использование этой сборки, и приложение станет работать, как раньше. Важно, что система позволяет использовать сборку с версией, отличной от указанной в метаданных. Такая дополнительная гибкость очень удобна.
Управление версиями при помощи политики издателя
В ситуации, описанной в предыдущем разделе, издатель сборки просто присылал новую версию сборки администратору, который устанавливал сборку и вручную редактировал конфигурационные XML-файлы машины или приложения. Вообще говоря, после исправления ошибки в сборке издателю понадобится простой механизм упаковки и распространения новой сборки по всем пользователям. Кроме того, нужно как-то заставить среду CLR каждого пользователя задействовать новую версию сборки вместо старой. Естественно, каждый пользователь может сам изменить конфигурационные XML-файлы на своих машинах, но этот способ крайне неудобен и ненадежен. Издателю нужен механизм, который позволил бы ему определить свою «политику» и установить ее на пользовательский компьютер с новой сборкой. В этом разделе показано, как издатель сборки может создать подобную политику.
Допустим, вы — издатель, только что создавший новую версию своей сборки, в которой исправлено несколько ошибок. Упаковывая новую сборку для рассылки пользователям, надо создать конфигурационный XML-файл. Он очень похож на те, что мы обсуждали раньше. Вот пример конфигурационного файла SomeClassLibrary. config для сборки SomeClassLibrary.dll:
«configuration»
«runtime»
продолжение &
<assemblyBinding xmlns="urn:schemasmicrosoftcom:asm.vl">
<dependentAssembly)
<assemblyIdentity name="SomeClassLibrary"
publlcKeyToken="32ab4ba45e0a69al" cultune="neutral"/)
cblndlngRedlrect
oldVersion="l.0.0.0" newVersion="2.0.0.0" /> ccodeBase version="2.0.0.0"
href="http://www.Wintellect.com/SomeClassLlbrary.dll"/)
</dependentAssembly)
</assemblyBinding)
</runtime>
</configuration)
Конечно, издатель может определять политику только для своих сборок. Кроме того, показанные здесь элементы — единственные, которые можно задать в конфигурационном файле политики издателя. Например, в конфигурационном файле политики нельзя задавать элементы probing и publisherPolicy.
Этот конфигурационный файл заставляет CLR при каждой ссылке на версию 1.0.0.0 сборки SomeClassLibrary загружать вместо нее версию 2.О.О.О. Теперь вы, как издатель, можете создать сборку, содержащую конфигурационный файл политики издателя. Для создания сборки с политикой издателя вызывается утилита AL.exe со следующими параметрами:
AL.exe /out:Policy.1.0.SomeClassLibrary.dll /version:l.0.0.0 /keyfile:MyCompany.snk /linkresource:SomeClassLibrary.config
Ниже приведены краткие описания параметров командной строки AL.exe.
□ Параметр /out приказывает AL.exe создать новый PE-файл с именем Policy. 1.0.SomeClassLibrary.dll, в котором нет ничего, кроме манифеста. Имя этой сборки имеет очень большое значение. Первая часть имени, Policy, сообщает CLR, что сборка содержит информацию политики издателя. Вторая и третья части имени, 1.0, сообщают CLR, что эта политика издателя предназначена для любой версии сборки SomeClassLibrary, у которой старший и младший номера версии равны 1.0. Политики издателя применяются только к старшему и младшему номерам версии сборки; нельзя создать политику издателя для отдельных построений или редакций сборки. Четвертая часть имени, SomeClassLibrary, указывает имя сборки, которой соответствует политика издателя. Пятая и последняя часть имени, dll, — это просто расширение, данное результирующему файлу сборки.
□ Параметр /version идентифицирует версию сборки с политикой издателя, которая не имеет ничего общего с версией самой сборки. Как видите, версиями
сборок, содержащих политику издателя, тоже можно управлять. Сейчас издателю нужно создать политику, перенаправляющую CLR от версии 1.0.0.0 сборки SomeClass Library к версии 2.0.0.0, а в будущем может потребоваться политика, перенаправляющая от версии 1.0.0.0 сборки SomeClass Library к версии 2.5.0.0. CLR использует номер версии, заданный этим параметром, чтобы выбрать самую последнюю версию сборки с политикой издателя.
□ Параметр /keyfile заставляет AL.exe подписать сборку с политикой издателя при помощи пары ключей, принадлежащей издателю. Эта пара ключей также должна совпадать с парой, использованной для подписания всех версий сборки SomeClassLibrary. В конце концов, именно это совпадение позволяет CLR установить, что сборка SomeClassLibrary и файл с политикой издателя для этой сборки созданы одним издателем.
□ Параметр /linkresource заставляет AL.exe считать конфигурационный XML-файл отдельным файлом сборки. При этом в результате компоновки получается сборка из двух файлов. Оба следует упаковывать и развертывать на пользовательских компьютерах с новой версией сборки SomeClassLibrary. Между прочим, конфигурационный XML-файл нельзя встраивать в сборку, вызывая AL.exe с параметром /embedresource, и создавать таким образом сборку из одного файла — CLR требует, чтобы сведения о конфигурации в формате XML размещались в отдельном файле.
Сборку, скомпонованную с политикой издателя, можно упаковать с файлом новой версии сборки SomeClassLibrary.dll и передать пользователям. Сборка с политикой издателя должна устанавливаться в GAC. Саму сборку SomeClassLibrary можно установить в GAC, но это не обязательно. Ее можно развернуть в базовом каталоге приложения или в другом каталоге, заданном в LTRL-адресе из элемента CodeBase.
ВНИМАНИЕ
Издатель должен создавать сборку со своей политикой лишь для развертывания исправленной версии сборки или пакетов исправлений для нее. Установка нового приложения не должна требовать сборки с политикой издателя.
И последнее о политике издателя. Допустим, издатель распространил сборку с политикой издателя, но в новой сборке почему-либо оказалось больше новых ошибок, чем исправлено старых. В этом случае администратору необходимо, чтобы CLR игнорировала сборку с политикой издателя. Для этого он может отредактировать конфигурационный файл приложения, добавив в него элемент publisherPolicy:
<publisherPolicy apply="no"/>
Этот элемент можно разместить в конфигурационном файле приложения как дочерний по отношению к элементу <assemblyBinding> — в этом случае он применяется ко всем его сборкам. Если элемент размещается как дочерний по отношению к <dependantAssembly>, он применяется к отдельной сборке. Обрабатывая конфигурационный файл приложения, CLR «видит», что не следует искать в GAC сборку с политикой издателя, и продолжает работать с более старой версией сборки. Однако замечу, что CLR все равно проверяет и применяет любую политику, заданную в файле Machine.config.
ВНИМАНИЕ
Использование сборки с политикой издателя фактически является заявлением издателя о совместимости разных версий сборки. Если новая версия несовместима с более ранней версией, издатель не должен создавать сборку с политикой издателя. Вообще, следует использовать сборки с политикой издателя, если компонуется новая версия с исправлениями ошибок. Новую версию сборки нужно протестировать на обратную совместимость. В то же время, если к сборке добавляются новые функции, следует подумать о том, чтобы отказаться от связи с прежними сборками и от применения сборки с политикой издателя. Кроме того, в этом случае отпадет необходимость тестирования на обратную совместимость.
ЧАСТЬ II
Проектирование типов
Глава 4. Основы типов........................................................... 122
Глава 5. Примитивные, ссылочные и значимые типы ......... 142
Глава 6. Основные сведения о членах и типах...................... 186
Глава 7. Константы и поля ..................................................... 210
Глава 8. Методы...................................................................... 215
Глава 9. Параметры................................................................. 245
Глава 10. свойства................................................................... 263
Глава 11. события................................................................... 286
Глава 12. Обобщения.............................................................. 302
Глава 13. интерфейсы............................................................. 333
Глава 4. Основы типов
В этой главе представлены основные принципы использования типов и общеязыковой исполняющей среды (Common Language Runtime, CLR). В частности, мы рассмотрим минимальную функциональность, присущую всем типам, и такие вопросы, как контроль типов, пространства имен, сборки и различные способы приведения типов объектов. В конце главы я объясняю, как во время выполнения взаимодействуют друг с другом типы, объекты, стеки потоков и управляемая куча.
Все типы — производные от System.Object
В CLR каждый объект прямо или косвенно является производным от System. Object. Это значит, что следующие определения типов идентичны:
// Тип, неявно производный от Object class Employee {
}
// Тип, явно производный от Object class Employee : System.Object {
}
Благодаря тому, что все типы, в конечном счете, являются производными от System.Object, любой объект любого типа гарантированно имеет минимальный набор методов. Открытые экземплярные методы класса System.Object перечислены в табл. 4.1.
Таблица 4.1. Открытые методы System.Object
|
|
Открытый метод | Описание |
ToString | По умолчанию возвращает полное имя типа (this.GetType(). FullName). На практике этот метод переопределяют, чтобы он возвращал объект String, содержащий состояние объекта в виде строки. Например, переопределенные методы для таких фундаментальных типов, как Boolean и Int32, возвращают значения объектов в строковом виде. Кроме того, к переопределению метода часто прибегают при отладке: вызов такого метода возвращает строку, содержащую значения полей объекта. Предполагается, что ToString учитывает информацию Culturelnfo, связанную с вызывающим потоком. Подробнее о методе ToString рассказывается в главе 14 |
Get Туре | Возвращает экземпляр объекта, производного от Туре, который идентифицирует тип объекта, вызвавшего Get Туре. Возвращаемый объект Туре может использоваться с классами, реализующими отражение для получения информации о типе в виде метаданных. Отражение рассматривается в главе 23. Метод Get Туре невиртуальный, его нельзя переопределить, поэтому классу не удастся исказить сведения о своем типе; таким образом обеспечивается безопасность типов |
|
Кроме того, типам, производным от System.Object, доступны некоторые защищенные методы (табл. 4.2).
Таблица 4.2. Защищенные методы System.Object
|
|
CLR требует, чтобы все объекты создавались оператором new. Например, объект Employee создается следующим образом:
Employee е = new Employee("ConstructorParaml");
Оператор new выполняет следующие действия:
1. Вычисление количества байтов, необходимых для хранения всех экземпляр- ных полей типа и всех его базовых типов, включая System.Object (в котором отсутствуют собственные экземплярные поля). Кроме того, в каждом объекте кучи должны присутствовать дополнительные члены, называемые указателем на объект-тип (type object pointer) и индексом блока синхронизации (sync block index); они необходимы CLR для управления объектом. Байты этих дополнительных членов добавляются к байтам, необходимым для размещения самого объекта.
2. Выделение памяти для объекта с резервированием необходимого для данного типа количества байтов в управляемой куче. Выделенные байты инициализируются нулями (0).