2. Каталог, где находится файл CSC.exe. Этот каталог также содержит DLL- библиотеки CLR.


 

         
Jeffrey Richter
 
4 edition
Microsoft• Press

 

Джеффри Рихтер

 

 

 

 

ПРОГРАММИРОВАНИЕ НА ПЛАТФОРМЕ

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. Части управляемого модуля

Часть Описание
Заголовок РЕ32 или РЕ32+ Стандартный заголовок PE-файла Windows, аналогичный заголовку Common Object File Format (COFF). Файл с заголовком в формате РЕ32 может выполняться в 32- и 64-разрядной версиях Windows, а с заголовком РЕ32+ — только в 64-раз- рядной. Заголовок обозначает тип файла: GUI, CUI или DLL, он также имеет временную метку, показывающую, когда файл был собран. Для модулей, содержащих только I L-код, основной объем информации в заголовке РЕ32(+) игнорируется. В модулях, содержащих машинный код, этот заголовок содержит сведения о машинном коде
Заголовок CLR Содержит информацию (интерпретируемую CLR и утилитами), которая превращает этот модуль в управляемый. Заголовок включает нужную версию CLR, некоторые флаги, метку метаданных MethodDef точки входа в управляемый модуль (метод Main), а также месторасположение/размер метаданных модуля, ресурсов, строгого имени, некоторых флагов и пр.
Метаданные Каждый управляемый модуль содержит таблицы метаданных. Есть два основных вида таблиц — это таблицы, описывающие типы данных и их члены, определенные в исходном коде, и таблицы, описывающие типы данных и их члены, на которые имеются ссылки в исходном коде
Код Intermediate Language(IL) Код, создаваемый компилятором при компиляции исходного кода. Впоследствии CLRкомпилирует IL в машинные команды


 



Каждый компилятор, предназначенный для 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 на получаемый модуль и режим выполнения

Значение параметра/ platform Тип выходного управляемого модуля х86 Windows х64 Windows ARM Windows RT
anycpu (по умолчанию) РЕ32/неза- висимый от платформы Выполняется как 32-раз- рядное приложение Выполняется как 64-разрядное приложение Выполняется как 32-раз- рядное приложение
anycpu32bit- preferred РЕ32/неза- висимый от платформы Выполняется как 32-раз- рядное приложение Выполняется как WoW64- приложение Выполняется как 32-раз- рядное приложение
х86 РЕ32Д86 Выполняется как 32-раз- рядное приложение Выполняется как WoW64- приложение Не выполняется
х64 РЕ32+Д64 Не выполняется Выполняется как 64-разрядное приложение Не выполняется
ARM PE32+/Itanium Не выполняется Не выполняется Выполняется как 32-раз- рядное приложение


 


 

После анализа заголовка ЕХЕ-файла для выяснения того, какой процесс необходимо запустить — 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

Пространство имен Описание содержимого
System Все базовые типы, используемые в приложениях
System. Data Типы для взаимодействия с базами данных и обработки данных
System. IО Типы потокового ввода-вывода, обхода дерева каталогов и файлов
System.Net Типы для низкоуровневых сетевых коммуникаций и использования распространенных протоколов Интернета
System. Runtime. Interop Services Типы, позволяющие управляемому коду работать с неуправляемыми платформенными средствами (компонентами СОМ, функциями Win32 и DLL-библиотек)
System.Security Типы защиты данных и ресурсов
System.Text Типы для работы с разными кодировками (такими, как ANSI и Юникод)
System.Threading Типы асинхронных операций и синхронизации доступа к ресурсам
System.Xml Типы для обработки схем и данных XML


 


 

Эта книга посвящена 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

Член типа Разновидность Эквивалентная конструкция языка программирования
AnEvent 11оле Событие; имя поля - AnEvent, тип - System. EventHandler
.ctor M (:т( >д Конструктор
Finalize М (:тс >д Финализатор
add AnEvent М (:т( >д Метод добавления обработчика события
ge :t A Property М (:тс >д Get- метод дос ту па свойства
ge :t I tem М (:тс >д Get-метод индексатора
op Addition М (:тс >д Оператор +
i >p 1 (quality М (:тс >д Оператор ==
i >p 1 nequality М (:тс >д Оператор !=
remove AnEvent М (:тс >д Метод удаления < )браб()тчика события
set AProperty М (:т( >д Set-метод доступа свойства
set Item М (:тс >д Set-метод индексатора


 



Взаимодействие с неуправляемым кодом

.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. Основные таблицы определений в метаданных

Имя таблицы определений Описание
ModuleDef Всегда содержит одну запись, идентифицирующую модуль. Запись включает имя файла модуля с расширением (без указания пути к файлу) и идентификатор версии модуля (в виде сгенерированного компилятором кода GUID). Это позволяет переименовывать файл, не теряя сведений о его исходном имени. Однако настоятельно рекомендуется не переименовывать файл, иначе среда CLR может не найти сборку во время выполнения
TypeDef Содержит по одной записи для каждого типа, определенного в модуле. Каждая запись включает имя типа, базовый тип, флаги сборки (public, private и т. д.) и указывает на записи таблиц MethodDef, PropertyDef и EventDef, содержащие соответственно сведения о методах, свойствах и событиях этого типа


 



Имя таблицы определений Описание
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. Общие таблицы ссылок, входящие в метаданные

Имя таблицы ссылок Описание
AssemblyRef Содержит по одной записи для каждой сборки, на которую ссылается модуль. Каждая запись включает сведения, необходимые для привязки к сборке: ее имя (без указания расширения и пути), номер версии, региональные стандарты и маркер открытого ключа (обычно это небольшой хеш-код, созданный на основе открытого ключа издателя и идентифицирующий издателя сборки, на которую ссылается модуль). Каждая запись также содержит несколько флагов и хеш-код, который должен служить контрольной суммой битов сборки. Среда CLR полностью игнорирует этот хеш-код и, вероятно, будет игнорировать его в будущем


 


 

Таблица 2.2 (продолжение)

Имя таблицы ссылок Описание
ModuleRef Содержит по одной записи для каждого PE-модуля, реализующего типы, на которые он ссылается. Каждая запись включает имя файла сборки и его расширение (без указания пути). Эта таблица служит для привязки модуля вызывающей сборки к типам, реализованным в других модулях
TypeRef Содержит по одной записи для каждого типа, на который ссылается модуль. Каждая запись включает имя типа и ссылку, по которой можно его найти. Если этот тип реализован внутри другого типа, запись содержит ссылку на соответствующую запись таблицы TypeRef. Если тип реализован в том же модуле, приводится ссылка на запись таблицы ModuleDef. Если тип реализован в другом модуле вызывающей сборки, приводится ссылка на запись таблицы ModuleRef. Если тип реализован в другой сборке, приводится ссылка на запись в таблице Assembly Ref
Member Ref Содержит по одной записи для каждого члена типа (поля, метода, а также свойства или метода события), на который ссылается модуль. Каждая запись включает имя и сигнатуру члена и указывает на запись таблицы TypeRef, содержащую сведения о типе, определяющим этот член


 


 

На самом деле таблиц метаданных намного больше, чем показано в табл. 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. Таблица метаданных манифеста

Имя таблицы метаданных манифеста Описание
AssemblyDef Состоит из единственной записи, если модуль идентифицирует сборку. Запись включает имя сборки (без расширения и пути), сведения о версии (старший и младший номера версии, номер компоновки и редакции), региональные стандарты, флаги, алгоритм хеширования и открытый ключ издателя (это поле может быть пустым — null)

продолжение &

 


 

Таблица 2.3 (продолжение)

Имя таблицы метаданных манифеста Описание
FileDef Содержит по одной записи для каждого PE-файла и файла ресурсов, входящих в состав сборки (кроме файла, содержащего манифест). В каждой записи содержится имя и расширение файла (без указания пути), хеш-код и флаги. Если сборка состоит из одного файла, таблица FileDef пуста
ManifestResourceDef Содержит по одной записи для каждого ресурса, включенного в сборку. Каждая запись включает имя ресурса, флаги (public или private), а также индекс для таблицы FileDef, указывающий файл или поток с ресурсом. Если ресурс не является отдельным файлом (например, JPEG- или GIF-файлом), он хранится в виде потока в составе PE-файла. В случае встроенного ресурса запись также содержит смещение, указывающее начало потока ресурса в РЕ-файле
ExportedTypesDef Содержит записи для всех открытых типов, экспортируемых всеми PE-модулями сборки. В каждой записи указано имя типа, индекс для таблицы FileDef (указывающий файл сборки, в котором реализован этот тип), а также индекс для таблицы TypeDef. Примечание: для экономии файлового пространства типы, экспортируемые из файла, содержащего манифест, не повторяются в этой таблице, потому что информация типов доступна через таблицы TypeDef метаданных


 


 

Манифест позволяет потребителям сборки абстрагироваться от особенностей распределения ее содержимого и делает сборку самоописываемой. Обратите внимание, что в файле, который содержит манифест, находится также информация о том, какие файлы составляют сборку, но отдельные файлы «не знают», что они

включены в сборку.

ПРИМЕЧАНИЕ

Файл сборки, содержащий манифест, содержит также таблицу 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 Атрибут/комментарий
FILEVERSION /fileversion System. Reflection. AssemblyFileVersionAttribute
PRODUCTVERSION /productversion System. Reflection. Assembly I nformational- VersionAttribute
FILEFLAGSMASK Нет Всегда задается равным VS FFI FIFEFFAGSMASK (определяется в WinVer.h как 0x0000003F)
FILEFLAGS Нет Всегда равен 0
FILEOS Нет В настоящее время всегда равен VOS WINDOWS32
FILETYPE /target Задается равным VFT APP, если задан параметр /target:exe или /target:winexe. При наличии параметра /targetdibrary приравнивается VFT DLL
FILESUBTYPE Нет Всегда задается равным VFT2 UNKNOWN (это поле не имеет значения для VFT_APP и VFTDLL)
Assembly Version /version System. Reflection. Assembly VersionAttribute
Comments /description System. Reflection. Assembly DescriptionAttribute
CompanyName /company System. Reflection. AssemblyCompany Attribute
FileDescription /title System. Reflection. AssemblyTitleAttribute
FileVersion /version System. Reflection. Assembly VersionAttribute
InternalName /out Задается равным заданному имени выходного файла (без расширения)
LegalCopyright /copyright System. Reflection. AssemblyCopyright Attribute
LegalTrademarks /trademark System. Reflection. AssemblyTrademarkAttribute


 



Поле ресурса со сведениями о версии Параметр 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 719 2


 


 

В табл. 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. Примеры тегов, определяющих региональные стандарты сборки

Основной тег Вспомогательный тег Региональные стандарты
De Нет Немецкий
De AT Австрийский немецкий
De CH Швейцарский немецкий
En Нет Английский
En GB Английский
En US Английский


 


 

В общем случае сборкам с кодом не назначают региональные стандарты, так как код обычно не содержит зависящих от них встроенных параметров. Сборку, для которой не определен региональный стандарт, называют сборкой с нейтральными региональными стандартами (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

Открытый метод Описание
Equals Возвращает true, если два объекта имеют одинаковые значения. Подробнее об этом методе рассказывается в разделе «Равенство и тождество объектов» главы 5
GetHashCode Возвращает хеш-код для значения данного объекта. Этот метод следует переопределить, если объекты типа используются в качестве ключа хеш-таблиц. Вообще говоря, класс Object выбран для размещения этого метода неудачно, потому что большинство типов не используется в качестве ключей хеш-таблиц; этот метод уместнее было бы определить в интерфейсе. Подробнее об этом методе рассказывается в разделе «Хеш-коды объектов» главы 5


 



Открытый метод Описание
ToString По умолчанию возвращает полное имя типа (this.GetType(). FullName). На практике этот метод переопределяют, чтобы он возвращал объект String, содержащий состояние объекта в виде строки. Например, переопределенные методы для таких фундаментальных типов, как Boolean и Int32, возвращают значения объектов в строковом виде. Кроме того, к переопределению метода часто прибегают при отладке: вызов такого метода возвращает строку, содержащую значения полей объекта. Предполагается, что ToString учитывает информацию Culturelnfo, связанную с вызывающим потоком. Подробнее о методе ToString рассказывается в главе 14
Get Туре Возвращает экземпляр объекта, производного от Туре, который идентифицирует тип объекта, вызвавшего Get Туре. Возвращаемый объект Туре может использоваться с классами, реализующими отражение для получения информации о типе в виде метаданных. Отражение рассматривается в главе 23. Метод Get Туре невиртуальный, его нельзя переопределить, поэтому классу не удастся исказить сведения о своем типе; таким образом обеспечивается безопасность типов

 


 

Кроме того, типам, производным от System.Object, доступны некоторые защищенные методы (табл. 4.2).

Таблица 4.2. Защищенные методы System.Object

Защищенный метод Описание
Memberwise - Clone Этот невиртуальный метод создает новый экземпляр типа и присваивает полям нового объекта соответствующие значения объекта this. Возвращается ссылка на созданный экземпляр
Finalize Этот виртуальный метод вызывается, когда уборщик мусора определяет, что объект является мусором, но до возвращения занятой объектом памяти в кучу. В типах, требующих очистки при сборке мусора, следует переопределить этот метод. Подробнее о нем см. главу 21


 


 

CLR требует, чтобы все объекты создавались оператором new. Например, объект Employee создается следующим образом:

Employee е = new Employee("ConstructorParaml");

Оператор new выполняет следующие действия:

1. Вычисление количества байтов, необходимых для хранения всех экземпляр- ных полей типа и всех его базовых типов, включая System.Object (в котором отсутствуют собственные экземплярные поля). Кроме того, в каждом объекте кучи должны присутствовать дополнительные члены, называемые указателем на объект-тип (type object pointer) и индексом блока синхронизации (sync block index); они необходимы CLR для управления объектом. Байты этих дополнительных членов добавляются к байтам, необходимым для размещения самого объекта.

2. Выделение памяти для объекта с резервированием необходимого для данного типа количества байтов в управляемой куче. Выделенные байты инициализируются нулями (0).