Глава 1. Модель выполения кода CLR
Первая версия языка вышла вместе с релизом Microsoft Visual Studio .NET в феврале 2002 года.
C# — это полноценный язык программирования. Хотя он и предназначен для генерации кода, выполняемого в среде .NET, сам по себе он не является частью .NET. Существует ряд средств, которые поддерживаются .NET, но не поддерживаются C#, и, возможно, вас удивит, что есть также средства, поддерживаемые C# и не поддерживаемые .NET (например, некоторые случаи перегрузки операций).
Платформа .NET является открытой средой. Это значит, что компиляторы для нее могут поставляться и сторонними разработчиками. Для обеспечения переносимости компиляторы, входящие в состав платформы, переводят программу не в машинные коды, а в промежуточный язык (Common Intermediate Language, CIL, или просто IL), который не содержит команд, зависящих от языка, операционной системы и типа компьютера. Программа на этом языке выполняется не самостоятельно, а под управлением системы, которая называется общеязыковой средой выполнения (Common Language Runtime, CLR). Код, выполняемый под управлением CLR, часто называют управляемым кодом.
Управляемый и неуправляемый код
managed code
Управляемый код – это приложение созданое на основе платформы .NET и поэтому управляется средой CLR, которая загружает приложение и при необходимости очищает память.
Неуправляемый код – приложения созданные на языке С++, которые компилируются не в общий язык CIL, а в обычный машинный код. В этом случае .NET не управляет приложением.
В то же время платформа .NET предоставляет возможности для взаимодействия с неуправляемым кодом. Со стандартными классами библиотеки .NET могут также использоваться сборки COM
.
- СLR (common language runtime) - исполняющая среда выполнения управляемого кода.
- IL (intermediate language) - управляемый код для CLR.
- JIT (just in time) - компилятор который внутри CLR преобразовывает IL в машинный код для процессора на котором должен выполнятся.
JIT-компиляция
Среда CLR может быть реализована для любой операционной системы. Однако перед тем как код сможет выполняться CLR, любой исходный текст (на C# или другом языке) должен быть скомпилирован. Компиляция в .NET состоит из двух шагов:
Компиляция исходного кода в Microsoft Intermediate Language (IL)
Компиляция IL в специфичный для платформы код с помощью CLR
Компилятор в качестве результата своего выполнения создает так называемую сборку — файл с расширением exe
или dll
, который содержит код на языке IL и метаданные.
Фактически компилятор создает так называемый управляемый модуль - переносимый исполняемый файл (Portable Executable или PE-файл). Этот файл содержит код на IL и метаданные которые представляют собой сведения об объектах, используемых в программе, а также сведения о самой сборке. Они позволяют организовать межъязыковое взаимодействие, обеспечивают безопасность и облегчают развертывание приложений, то есть установку программ на компьютеры пользователей.
Далее при запуске на выполнение подобного приложения происходит JIT-компиляция (Just-In-Time), переводящая код с языка IL в машинные команды конкретного процессора который затем выполняется. При этом, поскольку наше приложение может быть большим и содержать множество инструкций, в текущий момент времени будет компилироваться лишь та часть приложения, к которой непосредственно идет обращение. Код IL всегда компилируется оперативно. Каждая часть программы компилируется один раз и сохраняется в кэше для дальнейшего использования. Если мы обратимся к другой части кода, то она будет скомпилирована из CIL в машинный код. При том уже скомпилированная часть приложения сохраняется до завершения работы программы. В итоге это повышает производительность. Вместо компиляции всего приложения за один проход (что может привести к задержкам при запуске), JIT-компилятор просто компилирует каждую порцию кода при ее вызове (т.е. оперативно). Если промежуточный код однажды скомпилирован, то результирующий машинный исполняемый код сохраняется до момента завершения работы приложения, поэтому его перекомпиляция при повторных обращениях к нему не требуется.
В Microsoft аргументируют, что такой процесс более эффективен, чем компиляция всего приложения при запуске, поскольку высока вероятность того, что крупные фрагменты кода приложения на самом деле не будут выполняться при каждом запуске. При использовании JIT-компилятора такой код никогда не будет скомпилирован.
Основной механизм CLR физически имеет вид библиотеки под названием mscoree.dll
(и также называется общим механизмом выполнения исполняемого кода объектов — Common Object Runtime Execution Engine
). При добавлении ссылки на сборку для ее использования загрузка библиотеки mscoree.dll
осуществляется автоматически и затем, в свою очередь, приводит к загрузке требуемой сборки в память. Механизм исполняющей среды отвечает за выполнение целого ряда задач. Сначала, что наиболее важно, он отвечает за определение места расположения сборки и обнаружение запрашиваемого типа в двоичном файле за счет считывания содержащихся там метаданных. Затем он размещает тип в памяти, преобразует CIL-код в соответствующие платформе инструкции, производит любые необходимые проверки на предмет безопасности и после этого, наконец, непосредственно выполняет сам запрашиваемый программный код. Во время работы программы среда CLR следит за тем, чтобы выполнялись только разрешенные операции, осуществляет распределение и очистку памяти и обрабатывает возникающие ошибки. Это многократно повышает безопасность и надежность программ.
Метаданные
Переносимый исполняемый PE-файл является самодокументируемым файлом и, как уже говорилось, содержит и код, и метаданные, описывающие код. Файл начинается с манифеста и включает в себя описание всех классов, хранимых в PE-файле, их свойств, методов, всех аргументов этих методов - всю необходимую CLR информацию. Поэтому помимо PE-файла не требуется никаких дополнительных файлов и записей в реестр - вся нужная информация извлекается из самого файла. Среди классов библиотеки FCL имеется класс Reflection
, методы которого позволяют извлекать необходимую информацию. Введение метаданных - не только важная техническая часть CLR, но это также часть новой идеологии разработки программных продуктов. Мы увидим, что и на уровне языка C# самодокументированию уделяется большое внимание.
Сборщик мусора. Управление памятью
Garbage Collector
Еще одной важной особенностью построения CLR является то, что исполнительная среда берет на себя часть функций, традиционно входящих в ведение разработчиков трансляторов, и облегчает тем самым их работу. Один из таких наиболее значимых компонентов CLR - сборщик мусора (Garbage Collector). Под сборкой мусора понимается освобождение памяти, занятой объектами, которые стали бесполезными и не используются в дальнейшей работе приложения. В ряде языков программирования (классическим примером является язык C/C++) память освобождает сам программист, в явной форме отдавая команды как на создание, так и на удаление объекта. Неизбежные ошибки программиста при работе с памятью тяжелы по последствиям, и их крайне тяжело обнаружить. Как правило, объект удаляется в одном модуле, а необходимость в нем обнаруживается в другом, далеком модуле. Обоснование того, что программист не должен заниматься удалением объектов, а сборка мусора должна стать частью исполнительной среды, появилось достаточно давно. Наиболее полно оно обосновано в работах Бертрана Мейера и в его книге "Object-Oriented Construction Software", первое издание которой появилось еще в 1988 году.
В CLR эта идея реализована в полной мере. Задача сборки мусора снята не только с программистов, но и с разработчиков трансляторов, она решается в нужное время и в нужном месте - исполнительной средой, ответственной за выполнение вычислений. Здесь же решаются и многие другие вопросы, связанные с использованием памяти, в частности, проверяются возможные нарушения использования "чужой" памяти и другие нарушения, например, с использованием нетипизированных указателей. Данные, удовлетворяющие требованиям CLR и допускающие сборку мусора, называются управляемыми данными.
Но, как же, спросите вы, быть с языком C++ и другими языками, где есть нетипизированные указатели, адресная арифметика, возможности удаления объектов программистом? Такие возможности сохранены и в языке C#. Ответ следующий - CLR позволяет работать как с управляемыми, так и с неуправляемыми данными. Однако использование неуправляемых данных регламентируется и не поощряется. Так, в C# модуль, использующий неуправляемые данные (указатели, адресную арифметику), должен быть помечен как небезопасный unsafe
, и эти данные должны быть четко зафиксированы. Исполнительная среда, не ограничивая возможности языка и программистов, вводит определенную дисциплину в применении потенциально опасных средств языков программирования.
Платформа .NET содержит огромную библиотеку классов, которые можно использовать при программировании на любом языке .NET. Подробное изучение библиотеки классов .NET — необходимая, но и наиболее трудоемкая задача программиста при освоении этой платформы. Библиотека классов вместе с CLR образуют каркас (framework), то есть основу платформы.
Все .NET-совместимые языки должны отвечать требованиям общеязыковой спецификации (Common Language Specification, CLS), в которой описывается набор общих для всех языков характеристик. Это позволяет использовать для разработки приложения несколько языков программирования и вести полноценную межъязыковую отладку. Все программы независимо от языка используют одни и те же базовые классы библиотеки .NET.
События
У CLR есть свое видение того, что представляет собой тип. Есть формальное описание общей системы типов CTS - Common Type System. В соответствии с этим описанием, каждый тип, помимо полей, методов и свойств, может содержать и события. Помимо загрузки пользовательских сборок и создания пользовательских типов, механизм CLR при необходимости будет взаимодействовать и с типами, содержащимися в библиотеках базовых классов .NET. Хотя вся библиотека базовых классов поделена на ряд отдельных сборок, главной среди них является сборка mscorlib.dll
. В этой сборке содержится большое количество базовых типов, охватывающих широкий спектр типичных задач программирования, а также базовых типов данных, применяемых во всех языках .NET. При построении .NET-решений доступ к этой конкретной сборке будет предоставляться автоматически.При возникновении событий в процессе работы с тем или иным объектом данного типа посылаются сообщения, которые могут получать другие объекты. Механизм обмена сообщениями основан на делегатах - функциональном типе. Надо ли говорить, что в язык C# встроен механизм событий, полностью согласованный с возможностями CLR.
Исполнительная среда CLR обладает мощными динамическими механизмами - сборки мусора, динамического связывания, обработки исключительных ситуаций и событий. Все эти механизмы и их реализация в CLR созданы на основании практики существующих языков программирования. Но уже созданная исполнительная среда, в свою очередь, влияет на языки, ориентированные на использование CLR. Поскольку язык C# создавался одновременно с созданием CLR, то, естественно, он стал языком, наиболее согласованным с исполнительной средой, и средства языка напрямую отображаются в средства исполнительной среды.
Общие спецификации и совместимые модули
Уже говорилось, что каркас Framework .Net облегчает межязыковое взаимодействие. Для того чтобы классы, разработанные на разных языках, мирно уживались в рамках одного приложения, для их бесшовной отладки и возможности построения разноязычных потомков они должны удовлетворять некоторым ограничениям. Эти ограничения задаются набором общеязыковых спецификаций - CLS (Common Language Specification). Класс, удовлетворяющий спецификациям CLS, называется CLS-совместимым. Он доступен для использования в других языках, классы которых могут быть клиентами или наследниками совместимого класса.
Спецификации CLS точно определяют, каким набором встроенных типов можно пользоваться в совместимых модулях. Понятно, что эти типы должны быть общедоступными для всех языков, использующих Framework .Net. В совместимых модулях должны использоваться управляемые данные и выполняться некоторые другие ограничения. Заметьте, ограничения касаются только интерфейсной части класса, его открытых свойств и методов. Закрытая часть класса может и не удовлетворять CLS. Классы, от которых не требуется совместимость, могут использовать специфические особенности языка программирования.
Джеффри Рихтер "Программирование на платформе .Net Framework".