Без всякого преувеличения можно сказать, что платформа .NET стоит в одном ряду с самыми значительными достижениями корпорации Microsoft. Более того, с точки зрения программиста, работающего в области создания компиляторов и других средств разработки программ, .NET является технологией неизмеримо более привлекательной, чем все продукты, ранее созданные в Microsoft.
Разработка платформы .NET началась в 1998 году. Изначально ей дали рабочее название Project 42, которое затем было изменено на COM Object Runtime (сокращенно, COR). Видимо, аббревиатура COR использовалась достаточно длительное время, так как ее до сих пор можно найти в названиях dll-файлов и именах библиотечных функций. Потом платформа сменила еще несколько названий:
Спецификация основной части платформы .NET стандартизована ассоциацией
В этой книге, рассматривая возможности
Создание любого программного продукта подразумевает знакомство программиста с предметной областью. То есть разработчик бухгалтерской программы должен в какой-то степени разбираться в бухучете, а создатель Интернет-магазина - в принципах ведения торговли. Нетрудно догадаться, что создание новых инструментов для разработки программ требует от программиста знакомства с тем, с чем он и так хорошо знаком - с разработкой программ! Наверное, поэтому это занятие столь увлекательно.
Мы будем называть метаинструментами программы, для которых другие программы выступают в роли данных. Метаинструменты используются для разработки, тестирования, анализа и преобразования программ. Это могут быть компиляторы, средства быстрой разработки приложений (
В структуре практически любого современного компилятора можно выделить, по крайней мере, две части: "front-end" и "back-end". "Front-end" осуществляет лексический и синтаксический анализ программы и переводит программу в некоторое промежуточное представление. А "back-end" на основе этого промежуточного представления генерирует код для целевой аппаратной платформы. Между этими двумя частями может находиться оптимизатор, анализирующий и преобразующий промежуточное представление программы (см. рис. 1.1).
(рис 1.1) Структура современного компилятора
В нашем учебнике мы будем рассматривать представление кода в виде графа потока управления, узлы которого соответствуют инструкциям языка, а ребра обозначают передачу управления между ними. Такое представление можно использовать в качестве промежуточного представления кода в компиляторе.
Резюмируя вышесказанное, можно сказать, что в большинстве учебников и учебных курсов, посвященных разработке компиляторов, основное внимание уделяется алгоритмам лексического и синтаксического анализа, то есть они учат в основном программированию "front-end'ов". В нашем учебнике мы концентрируем внимание на архитектуре и языке
Многие идеи, которые легли в основу платформы .NET, были разработаны задолго до ее появления. В этом разделе мы совершим краткий экскурс в историю и рассмотрим несколько программных систем, которые по праву можно считать предшественниками платформы .NET.
Операционная система UCSD p-System была разработана в 1978 году в Калифорнийском университете для учебных целей. Главное ее достоинство заключалось в том, что она могла работать как на компьютерах
Независимость операционной системы от аппаратной платформы достигалась путем введения понятия виртуальной p-машины (p-Machine), обладавшей собственным набором инструкций, который назывался p-кодом (p-code). Сама операционная система и все работавшие в ней программы были закодированы на p-коде, поэтому для того чтобы запустить их на новой аппаратной платформе, требовалось всего лишь реализовать для этой платформы интерпретатор p-кода.
Виртуальная p-машина была похожа на обычный компьютер и обладала процессором и памятью. Программы хранились в памяти машины вместе с данными, а все вычисления выполнялись через расположенный в памяти стек. Виртуальный процессор содержал пять регистров, один из которых использовался для хранения адреса текущей выполняемой инструкции (
Платформа .NET использует похожую схему обеспечения независимости программ от аппаратной платформы. Все программы, работающие на платформе .NET, закодированы на языке CIL (Common
Технология ANDF (Architectural
Формат ANDF не зависит ни от языков программирования, ни от особенностей аппаратных платформ и операционных систем. Программы, распространяемые в формате ANDF, могут быть установлены на любой платформе, для которой имеется транслятор из ANDF в исполняемый код.
Схема использования технологии ANDF показана на рис. 1.2. Для каждого языка программирования реализован компилятор, который генерирует файл в формате ANDF. Такой компилятор называется генератором ANDF (ANDF producer). Для каждой аппаратной платформы реализован инсталлятор ANDF (ANDF installer), который переводит программу из формата ANDF в формат исполняемых файлов.
(рис 1.2) Схема использования технологии ANDF
Технология ANDF имеет много общего с принципами
Платформа Java по архитектуре и своим возможностям наиболее близка к платформе .NET. Она была разработана в середине 1990-х годов в Sun Microsystems для бытовых приборов, подключаемых к компьютерным сетям. Затем произошло стремительное развитие Internet-технологий, которое способствовало широкому распространению Java. В настоящее время Java является основным конкурентом платформы .NET.
Краеугольным камнем платформы Java является виртуальная машина, которая отвечает за независимость Java-программ от операционных систем и аппаратных платформ. Набор инструкций этой виртуальной машины (так называемый Java byte-code) может выполняться как на специализированных Java-процессорах, так и путем компиляции в исполняемый код конкретной аппаратной платформы.
Платформа .NET состоит из двух основных компонентов. Это
.NET Framework
Разработчику системного программного обеспечения важно понимать, что .NET - всего лишь одна из возможных реализаций так называемой общей инфраструктуры языков (
Можно, руководствуясь этой спецификацией, разработать собственную реализацию CLI (рис. 1.3). В настоящее время ведутся по крайней мере два посвященных этому проекта. Это платформа Mono, создаваемая компанией Xamarin, и разрабатываемый в рамках GNU проект Portable .NET. Кроме того, Microsoft распространяет в исходных текстах еще одну свою реализацию CLI, работающую как в Windows, так и под управлением FreeBSD. Эта реализация называется Shared Source CLI (иногда можно услышать другое название - Rotor).
(рис 1.3) Существующие реализации CLI и поддерживаемые ими операционные системы
Итак, чтобы понять, как работает .NET, необходимо изучить спецификацию CLI. Этим мы и займемся в ближайшее время, а пока перечислим ее составные части:
Программы для платформы .NET распространяются в виде так называемых сборок (assemblies). Каждая сборка представляет собой совокупность метаданных, описывающих типы, и CIL-кода.
Ключевой особенностью выполнения программ в среде .NET является
Такая схема выполнения программ в среднем является более эффективной, чем интерпретация инструкций CIL, так как потеря времени на предварительную компиляцию CIL-кода с лихвой компенсируется высокой скоростью работы откомпилированного кода.
В .NET реализованы два ngen, которая входит в состав .NET Framework SDK.
Программу ngen нельзя воспринимать как простой компилятор, позволяющий превратить сборку .NET в обычное приложение Windows. Дело в том, что откомпилированная сборка не может быть непосредственно запущена пользователем - загрузчик выдает сообщение об ошибке, гласящее, что запускаемая программа не является правильным приложением Windows. Откомпилированная сборка запускается системой только при вызове исходной сборки!
Одни из самых неприятных ошибок, которые портят жизнь программисту, это, безусловно, ошибки, связанные с управлением памятью. В таких языках, как C и C++, в которых управление памятью целиком возложено на программиста, львиная доля времени, затрачиваемого на отладку программы, приходится на борьбу с подобными ошибками.
Давайте перечислим типичные ошибки при управлении памятью (некоторые из них особенно усугубляются в том случае, если в программе существуют несколько указателей на один и тот же блок памяти):
Эта беда случается, если мы пытаемся использовать объект, память для которого была уже освобождена. Указатели на такие объекты называются висящими (
Иногда бывает важно не перестараться и не освободить ненужный объект дважды.
Когда мы постоянно выделяем новые блоки памяти, но забываем освобождать блоки, ставшие ненужными, память в конце концов заканчивается.
При интенсивном выделении и освобождении памяти может возникнуть ситуация, когда непрерывный блок памяти определенного размера не может быть выделен, хотя суммарный объем свободной памяти вполне достаточен. Это происходит, если используемые блоки памяти чередуются со свободными блоками и размер любого из свободных блоков меньше, чем нам нужно. Проблема особенно критична в серверных приложениях, работающих в течение длительного времени.
В программах, работающих в среде .NET, все вышеперечисленные ошибки никогда не возникают, потому что эти программы используют реализованное в CLR
Все эти чудеса, которые творит сборщик мусора, возможны исключительно благодаря тому, что во время выполнения программы известны типы всех используемых в ней объектов. Другими словами, данные, с которыми работает программа, находятся под полным контролем среды выполнения и называются, соответственно,
При разработке платформы .NET было уделено много внимания обеспечению безопасности выполняемого программного кода. С точки зрения обеспечения безопасности можно привести следующую классификацию CIL-кода:
Это код, который не может быть обработан
Это код, который может быть представлен в виде машинного кода. При этом он может содержать вредоносные фрагменты (например, вирусы) или ошибки, способные нарушить работу не только программы, но и среды выполнения и даже операционной системы.
Весь код, который поступает в
Следует понимать, что
Абстрагировавшись от конкретных особенностей .NET, можно сказать, что основная цель системы типов заключается в предотвращении определенного класса ошибок в программе до ее выполнения.
Давайте кратко приведем классификацию ошибок и разберемся, с какими ошибками помогает справиться система типов. При этом мы не будем рассматривать ошибки в алгоритмах, а ограничимся ошибками, возникающими в результате неправильного кодирования алгоритмов.
Все ошибки можно разделить на две категории: перехватываемые и неперехватываемые (рис. 1.4). При возникновении перехватываемой ошибки (
(рис 1.4) Классификация ошибок в программах
В качестве примеров перехватываемых ошибок можно привести деление на ноль и обращение к памяти по нулевому указателю. А неперехватываемые ошибки возникают, например, при передаче управления на неправильный адрес или при выходе за границы массива (если отсутствует динамическая проверка размера массива).
Фрагмент программы, в котором не могут возникнуть неперехватываемые ошибки, называется безопасным (
Для любого языка программирования можно определить класс ошибок, называемых запрещенными (forbidden errors). В этот класс следует включить все неперехватываемые ошибки, а также некоторое подмножество перехватываемых ошибок. Говорят, что фрагмент программы имеет хорошее поведение (well behaved), если в нем не могут возникать запрещенные ошибки. Языки программирования, которые гарантируют хорошее поведение всех написанных на них программ, называются языками со строгой проверкой (strongly checked).
Таким образом, для программы, написанной на языке со строгой проверкой, справедливы следующие утверждения:
Существуют два пути для диагностики запрещенных ошибок: статическая проверка программы до ее выполнения (static checking) и динамическая проверка во время выполнения (dynamic checking). Статическая проверка характерна для языков, имеющих систему типов, а динамическая проверка - для так называемых бестиповых (typeless) языков, в которых либо вообще нет системы типов, либо существует только один универсальный тип данных. Динамическая проверка требует дополнительных ресурсов, поэтому статическая проверка является предпочтительной, так как чем больше ошибок диагностируется статически, тем выше эффективность программы.
Система типов в языке программирования разрабатывается для того чтобы можно было осуществлять статическую проверку программы. Она представляет собой набор правил, определяющих условия, при которых конструкции языка не вызывают запрещенных ошибок.
Так как платформа .NET спроектирована с учетом поддержки разных языков программирования, то ее общая система типов (
Наличие в .NET общей системы типов позволяет осуществлять статическую проверку программы не только на уровне компилятора, но и на уровне системы выполнения. Другими словами, система может проводить верификацию двоичных исполняемых файлов непосредственно перед их запуском. Это гарантирует безопасность кода, выполняемого в среде .NET и тем самым обеспечивает возможность автоматического управления памятью (сборку мусора).
Общая система типов достаточно обширна, так как при ее проектировании учитывалась необходимость поддержки различных языков программирования и, кроме того, уделялось большое внимание эффективности выполнения программ в среде .NET. Поэтому, для ясности изложения материала мы сначала рассмотрим основное подмножество общей системы типов. Будем называть это подмножество ядром системы типов.
Схема ядра общей системы типов .NET приведена на рис. 1.5. На схеме все типы делятся на две категории: это типы-значения (value types) и ссылочные типы (
(рис 1.5) Ядро общей системы типов
Типы-значения представляют собой
Ссылочные типы описывают так называемые объектные ссылки (
Значения любого типа хранятся в ячейках (location). В качестве ячеек могут выступать локальные и глобальные переменные, параметры методов, поля объектов и элементы массивов. Для каждой ячейки известен тип значений, которые она может содержать.
Особо важным является то обстоятельство, что ячейки не могут содержать объекты. Все объекты размещаются в специальной области памяти, называемой кучей (
Встроенные типы-значения делятся на две группы: целые типы и типы с плавающей запятой. Они перечислены в таблицах 1.1 и 1.2, соответственно. В первом столбце каждой из таблиц приведены имена типов, используемые в текстовом представлении CIL (в программах, компилируемых ассемблером ILASM). Во втором столбце перечислены имена, используемые для тех же самых типов в библиотеке классов .NET. В третьем столбце находится краткое описание, в котором указывается знаковость и разрядность типа.
| Тип | Имя в .NET Framework |
Описание |
|---|---|---|
bool |
System.Boolean |
булевский (8 бит) |
char |
System.Char |
символ Unicode (16 бит) |
int8 |
System.SByte |
целое со знаком (8 бит) |
int16 |
System.Int16 |
целое со знаком (16 бит) |
int32 |
System.Int32 |
целое со знаком (32 бит) |
int64 |
System.Int64 |
целое со знаком (64 бит) |
unsigned int8 |
System.Byte |
целое без знака (8 бит) |
unsigned int16 |
System.Uint16 |
целое без знака (16 бит) |
unsigned int32 |
System.Uint32 |
целое без знака (32 бит) |
unsigned int64 |
System.UInt64 |
целое без знака (64 бит) |
native int |
System.IntPtr |
целое со знаком (разрядность процессора) |
native unsigned int |
System.UIntPtr |
целое без знака (разрядность процессора) |
| Тип | Имя в .NET Framework |
Описание |
|---|---|---|
float32 |
System.Single |
вещественное (32 бит) |
float64 |
System.Double |
вещественное (64 бит) |
Для встроенных типов-значений определены правила преобразования значений одного типа в другой тип. Такие преобразования бывают сужающие (
В некоторых объектно-ориентированных языках программирования (например, в C++) объекты могут храниться как в куче (в динамической памяти), так и в переменных: глобальных (в статической памяти) и локальных (на стеке). Поэтому системы типов в таких языках содержат отдельные типы для самого объекта и для объектной ссылки (указателя на объект).
В среде .NET объекты и объектные ссылки хранятся раздельно, а именно: объекты хранятся в куче, а ссылки - в ячейках. Поэтому общая система типов спроектирована таким образом, что один и тот же ссылочный тип может являться как типом объекта, так и типом объектной ссылки.
Каждый объект в куче содержит информацию о своем типе. Поэтому ссылочные типы, представляющие объекты, называются самоописывающими (self-
Два самоописывающих типа являются встроенными - это System.Object (или просто object в текстовом представлении CIL) и System.String (или string ). Тип System.Object является общим базовым классом, от которого непосредственно или транзитивно наследует любой другой класс. Тип System.String используется для представления строковых данных в формате Unicode.
Основу самоописывающих типов составляют классы. Классы могут агрегировать значения других типов, а также наследоваться друг от друга (в .NET поддерживается только одиночное наследование). Классы могут содержать следующие элементы:
Поля являются ячейками, в которых хранятся значения других типов.
Методы представляют собой функции классов. Они бывают статическими (
Свойство представляет собой пару методов, один из которых возвращает некоторое значение, а другой устанавливает это значение.
События используются для асинхронного внесения изменений в объект.
Типы-массивы также относятся к самоописывающим типам, то есть каждый массив представляет собой объект в куче, доступ к которому осуществляется через объектную ссылку. Хотя, строго говоря, типы-массивы не являются классами, считается, что все они наследуют от библиотечного класса System.Array. Типы-массивы интересны тем, что, в отличие от классов, определяемых программистом самостоятельно, они формируются системой автоматически. То есть если мы имеем некоторый тип X, то тип массива, состоящего из элементов типа X, нам уже объявлять не нужно - об этом позаботится система выполнения.
(рис 1.6) Особенности представления массивов
Массивы бывают как одномерными (в этом случае они обрабатываются особенно эффективно благодаря использованию специальных инструкций CIL), так и многомерными. Кроме того, система поддерживает массивы, нижняя граница которых отлична от нуля.
Особого внимания заслуживают особенности представления
Интерфейсы служат для компенсации отсутствия в .NET множественного наследования. Они могут рассматриваться как чисто абстрактные классы, содержащие только перечисленные ниже элементы:
Хотя любой класс может наследоваться только от одного базового класса, он может реализовывать произвольное количество интерфейсов. То есть, интерфейс определяет контракт, которому должен удовлетворять любой класс, реализующий этот интерфейс.
Нужно отметить, что интерфейс может содержать реализации статических методов, но все остальные методы, включая методы свойств и событий, должны оставаться абстрактными.
Значение может иметь сразу несколько типов. Например, если некоторый объект реализует несколько интерфейсов, то каждый из них является его типом. Та же ситуация наблюдается при наследовании: если класс имеет несколько
Как уже говорилось ранее, ячейки, в которых могут храниться значения, также имеют тип. Соответственно, ячейка может содержать только значение, совместимое с ее типом. То есть, общая система типов не допускает присваивание ячейке несовместимого с ее типом значения.
Формулируется следующее условие совместимости значения и ячейки: для того чтобы некоторое значение можно было присвоить заданной ячейке, необходимо и достаточно, чтобы хотя бы один из типов значения совпадал с типом ячейки.
Для объектных ссылок и значений типов-значений вводятся отношения идентичности (identity) и равенства (equality). Эти отношения являются
Отношение идентичности для объектных ссылок вводится следующим образом: две объектных ссылки идентичны тогда и только тогда, когда они содержат адреса одного и того же объекта. На рис. 1.7 изображены три объектных ссылки A, B и C, а также два равных объекта-строки. Так как ссылки A и B содержат адрес одного и того же объекта, то они идентичны между собой, но при этом они не идентичны ссылке C, содержащей адрес другого объекта.
(рис 1.7) Пример, объясняющий отношения идентичности и равенства объектных ссылок
Отношение равенства для объектных ссылок формулируется так: две объектных ссылки равны тогда и только тогда, когда они содержат адреса равных объектов. Все три объектные ссылки, изображенные на рис. 1.7, равны между собой.
Отношение идентичности для типов-значений определяется следующим образом: два значения идентичны тогда и только тогда, когда они принадлежат одному и тому же типу-значению, и представляющие их последовательности битов равны.
Отношение равенства для
Отношения идентичности и равенства играют большую роль при программировании в среде .NET. Любой объект имеет Equals, унаследованный от System.Object и выполняющий сравнение объектов на равенство. В зависимости от реализации этого метода, отношение равенства может существенно меняться. При
Из соображений эффективности выполнения программ разработчики платформы .NET добавили в общую систему типов дополнительные элементы, а именно: пользовательские типы-значения (структуры и перечисления) и указатели. Схема общей системы типов, на которой отражены эти дополнительные элементы, приведена на рис. 1.8.
(рис 1.8) Общая система типов
Как показал опыт платформы Java, которая была разработана задолго до платформы .NET, одной из основных причин ухудшения производительности Java-программ является медленная работа сборщика мусора, вызванная большим количеством мелких объектов в куче. Это явление можно наблюдать в двух случаях:
Разработчиками .NET был подмечен тот факт, что использование типов-значений вместо объектов позволяет избежать описанных выше проблем, потому что:
Поэтому в общую систему типов были добавлены так называемые пользовательские типы-значения. Эти типы могут быть объявлены программистом, но, как и встроенные типы-значения, размещаются не в куче, а в ячейках.
Пользовательские типы-значения делятся на структуры и перечисления.
Структуры являются аналогом классов. Они, как и классы, могут содержать поля, методы, свойства и события. Все структуры неявно наследуют от библиотечного класса System.ValueType, и, более того, встроенные типы-значения также наследуют от этого класса. Тут сразу следует заметить, что система типов не предусматривает никакого наследования структур, кроме данного неявного. Другими словами, структуры не могут наследоваться друг от друга и, тем более, не могут наследоваться от классов (кроме System.ValueType ).
Перечисления представляют собой структуры с одним целочисленным полем Value. Кроме того, перечисления содержат набор констант, определяющих возможные значения поля Value. При этом для каждой константы в перечислении хранится ее имя. Перечисления неявно наследуют от библиотечного класса System.Enum, который, в свою очередь, является наследником все того же класса System.ValueType.
Использование указателей может значительно увеличить производительность, и в некоторых языках программирования указатели применяются исключительно часто (например, в C). Однако считается, что применение указателей чревато появлением в программах большого количества трудноуловимых неперехватываемых ошибок. Поэтому, например, система типов уже упоминавшейся платформы Java обходится без указателей.
Тем не менее, разработчикам .NET удалось добавить указатели в общую систему типов. При этом появилось две категории указателей: управляемые указатели (managed pointers) и неуправляемые указатели (unmanaged pointers).
Для того чтобы программы оставались безопасными, на использование управляемых указателей наложен целый ряд ограничений:
На использование неуправляемых указателей никаких ограничений не накладывается, то есть они могут содержать абсолютно любой адрес. Программа, в которой используются неуправляемые указатели, автоматически считается небезопасной и не может пройти верификацию.
Наличие в общей системе типов структур, которые во многом напоминают классы, но в действительности классами не являются, в некоторых случаях вызывает некоторые неудобства. Например, в библиотеке классов .NET существуют достаточно удобные контейнерные классы (наиболее часто используется класс ArrayList, представляющий массив с динамически меняющимся размером). Эти классы могут хранить ссылки на любые объекты, но не могут работать с типами-значениями.
Для решения этой проблемы в общей системе типов предусмотрены так называемые упакованные типы-значения. Эти типы являются ссылочными и самоописывающими. Объекты этих типов предназначены для хранения значений типов-значений.
Упакованные типы-значения не могут быть объявлены программистом. Система автоматически определяет такой тип для любого типа-значения.
Получение объекта упакованного типа-значения осуществляется путем упаковки (boxing). Упаковка заключается в том, что в куче создается пустой объект нужного размера, а затем значение копируется внутрь этого объекта.
С помощью упаковки мы можем превратить значение любого типа-значения (встроенного примитивного типа, структуры, перечисления) в объект и в дальнейшем работать с этим значением как с настоящим объектом (в том числе, мы можем положить его в ArrayList ).
Если же нам требуется произвести обратное действие, мы можем осуществить распаковку (unboxing). Распаковка заключается в том, что мы получаем управляемый указатель на содержимое объекта упакованного типа-значения.
Без всякого преувеличения можно сказать, что платформа .NET стоит в одном ряду с самыми значительными достижениями корпорации Microsoft. Более того, с точки зрения программиста, работающего в области создания компиляторов и других средств разработки программ, .NET является технологией неизмеримо более привлекательной, чем все продукты, ранее созданные в Microsoft.
Разработка платформы .NET началась в 1998 году. Изначально ей дали рабочее название Project 42, которое затем было изменено на COM Object Runtime (сокращенно, COR). Видимо, аббревиатура COR использовалась достаточно длительное время, так как ее до сих пор можно найти в названиях dll-файлов и именах библиотечных функций. Потом платформа сменила еще несколько названий:
Спецификация основной части платформы .NET стандартизована ассоциацией
В этой книге, рассматривая возможности
Создание любого программного продукта подразумевает знакомство программиста с предметной областью. То есть разработчик бухгалтерской программы должен в какой-то степени разбираться в бухучете, а создатель Интернет-магазина - в принципах ведения торговли. Нетрудно догадаться, что создание новых инструментов для разработки программ требует от программиста знакомства с тем, с чем он и так хорошо знаком - с разработкой программ! Наверное, поэтому это занятие столь увлекательно.
Мы будем называть метаинструментами программы, для которых другие программы выступают в роли данных. Метаинструменты используются для разработки, тестирования, анализа и преобразования программ. Это могут быть компиляторы, средства быстрой разработки приложений (
В структуре практически любого современного компилятора можно выделить, по крайней мере, две части: "front-end" и "back-end". "Front-end" осуществляет лексический и синтаксический анализ программы и переводит программу в некоторое промежуточное представление. А "back-end" на основе этого промежуточного представления генерирует код для целевой аппаратной платформы. Между этими двумя частями может находиться оптимизатор, анализирующий и преобразующий промежуточное представление программы (см. рис. 1.1).
(рис 1.1) Структура современного компилятора
В нашем учебнике мы будем рассматривать представление кода в виде графа потока управления, узлы которого соответствуют инструкциям языка, а ребра обозначают передачу управления между ними. Такое представление можно использовать в качестве промежуточного представления кода в компиляторе.
Резюмируя вышесказанное, можно сказать, что в большинстве учебников и учебных курсов, посвященных разработке компиляторов, основное внимание уделяется алгоритмам лексического и синтаксического анализа, то есть они учат в основном программированию "front-end'ов". В нашем учебнике мы концентрируем внимание на архитектуре и языке
Многие идеи, которые легли в основу платформы .NET, были разработаны задолго до ее появления. В этом разделе мы совершим краткий экскурс в историю и рассмотрим несколько программных систем, которые по праву можно считать предшественниками платформы .NET.
Операционная система UCSD p-System была разработана в 1978 году в Калифорнийском университете для учебных целей. Главное ее достоинство заключалось в том, что она могла работать как на компьютерах
Независимость операционной системы от аппаратной платформы достигалась путем введения понятия виртуальной p-машины (p-Machine), обладавшей собственным набором инструкций, который назывался p-кодом (p-code). Сама операционная система и все работавшие в ней программы были закодированы на p-коде, поэтому для того чтобы запустить их на новой аппаратной платформе, требовалось всего лишь реализовать для этой платформы интерпретатор p-кода.
Виртуальная p-машина была похожа на обычный компьютер и обладала процессором и памятью. Программы хранились в памяти машины вместе с данными, а все вычисления выполнялись через расположенный в памяти стек. Виртуальный процессор содержал пять регистров, один из которых использовался для хранения адреса текущей выполняемой инструкции (
Платформа .NET использует похожую схему обеспечения независимости программ от аппаратной платформы. Все программы, работающие на платформе .NET, закодированы на языке CIL (Common
Технология ANDF (Architectural
Формат ANDF не зависит ни от языков программирования, ни от особенностей аппаратных платформ и операционных систем. Программы, распространяемые в формате ANDF, могут быть установлены на любой платформе, для которой имеется транслятор из ANDF в исполняемый код.
Схема использования технологии ANDF показана на рис. 1.2. Для каждого языка программирования реализован компилятор, который генерирует файл в формате ANDF. Такой компилятор называется генератором ANDF (ANDF producer). Для каждой аппаратной платформы реализован инсталлятор ANDF (ANDF installer), который переводит программу из формата ANDF в формат исполняемых файлов.
(рис 1.2) Схема использования технологии ANDF
Технология ANDF имеет много общего с принципами
Платформа Java по архитектуре и своим возможностям наиболее близка к платформе .NET. Она была разработана в середине 1990-х годов в Sun Microsystems для бытовых приборов, подключаемых к компьютерным сетям. Затем произошло стремительное развитие Internet-технологий, которое способствовало широкому распространению Java. В настоящее время Java является основным конкурентом платформы .NET.
Краеугольным камнем платформы Java является виртуальная машина, которая отвечает за независимость Java-программ от операционных систем и аппаратных платформ. Набор инструкций этой виртуальной машины (так называемый Java byte-code) может выполняться как на специализированных Java-процессорах, так и путем компиляции в исполняемый код конкретной аппаратной платформы.
Платформа .NET состоит из двух основных компонентов. Это
.NET Framework
Разработчику системного программного обеспечения важно понимать, что .NET - всего лишь одна из возможных реализаций так называемой общей инфраструктуры языков (
Можно, руководствуясь этой спецификацией, разработать собственную реализацию CLI (рис. 1.3). В настоящее время ведутся по крайней мере два посвященных этому проекта. Это платформа Mono, создаваемая компанией Xamarin, и разрабатываемый в рамках GNU проект Portable .NET. Кроме того, Microsoft распространяет в исходных текстах еще одну свою реализацию CLI, работающую как в Windows, так и под управлением FreeBSD. Эта реализация называется Shared Source CLI (иногда можно услышать другое название - Rotor).
(рис 1.3) Существующие реализации CLI и поддерживаемые ими операционные системы
Итак, чтобы понять, как работает .NET, необходимо изучить спецификацию CLI. Этим мы и займемся в ближайшее время, а пока перечислим ее составные части:
Программы для платформы .NET распространяются в виде так называемых сборок (assemblies). Каждая сборка представляет собой совокупность метаданных, описывающих типы, и CIL-кода.
Ключевой особенностью выполнения программ в среде .NET является
Такая схема выполнения программ в среднем является более эффективной, чем интерпретация инструкций CIL, так как потеря времени на предварительную компиляцию CIL-кода с лихвой компенсируется высокой скоростью работы откомпилированного кода.
В .NET реализованы два ngen, которая входит в состав .NET Framework SDK.
Программу ngen нельзя воспринимать как простой компилятор, позволяющий превратить сборку .NET в обычное приложение Windows. Дело в том, что откомпилированная сборка не может быть непосредственно запущена пользователем - загрузчик выдает сообщение об ошибке, гласящее, что запускаемая программа не является правильным приложением Windows. Откомпилированная сборка запускается системой только при вызове исходной сборки!
Одни из самых неприятных ошибок, которые портят жизнь программисту, это, безусловно, ошибки, связанные с управлением памятью. В таких языках, как C и C++, в которых управление памятью целиком возложено на программиста, львиная доля времени, затрачиваемого на отладку программы, приходится на борьбу с подобными ошибками.
Давайте перечислим типичные ошибки при управлении памятью (некоторые из них особенно усугубляются в том случае, если в программе существуют несколько указателей на один и тот же блок памяти):
Эта беда случается, если мы пытаемся использовать объект, память для которого была уже освобождена. Указатели на такие объекты называются висящими (
Иногда бывает важно не перестараться и не освободить ненужный объект дважды.
Когда мы постоянно выделяем новые блоки памяти, но забываем освобождать блоки, ставшие ненужными, память в конце концов заканчивается.
При интенсивном выделении и освобождении памяти может возникнуть ситуация, когда непрерывный блок памяти определенного размера не может быть выделен, хотя суммарный объем свободной памяти вполне достаточен. Это происходит, если используемые блоки памяти чередуются со свободными блоками и размер любого из свободных блоков меньше, чем нам нужно. Проблема особенно критична в серверных приложениях, работающих в течение длительного времени.
В программах, работающих в среде .NET, все вышеперечисленные ошибки никогда не возникают, потому что эти программы используют реализованное в CLR
Все эти чудеса, которые творит сборщик мусора, возможны исключительно благодаря тому, что во время выполнения программы известны типы всех используемых в ней объектов. Другими словами, данные, с которыми работает программа, находятся под полным контролем среды выполнения и называются, соответственно,
При разработке платформы .NET было уделено много внимания обеспечению безопасности выполняемого программного кода. С точки зрения обеспечения безопасности можно привести следующую классификацию CIL-кода:
Это код, который не может быть обработан
Это код, который может быть представлен в виде машинного кода. При этом он может содержать вредоносные фрагменты (например, вирусы) или ошибки, способные нарушить работу не только программы, но и среды выполнения и даже операционной системы.
Весь код, который поступает в
Следует понимать, что
Абстрагировавшись от конкретных особенностей .NET, можно сказать, что основная цель системы типов заключается в предотвращении определенного класса ошибок в программе до ее выполнения.
Давайте кратко приведем классификацию ошибок и разберемся, с какими ошибками помогает справиться система типов. При этом мы не будем рассматривать ошибки в алгоритмах, а ограничимся ошибками, возникающими в результате неправильного кодирования алгоритмов.
Все ошибки можно разделить на две категории: перехватываемые и неперехватываемые (рис. 1.4). При возникновении перехватываемой ошибки (
(рис 1.4) Классификация ошибок в программах
В качестве примеров перехватываемых ошибок можно привести деление на ноль и обращение к памяти по нулевому указателю. А неперехватываемые ошибки возникают, например, при передаче управления на неправильный адрес или при выходе за границы массива (если отсутствует динамическая проверка размера массива).
Фрагмент программы, в котором не могут возникнуть неперехватываемые ошибки, называется безопасным (
Для любого языка программирования можно определить класс ошибок, называемых запрещенными (forbidden errors). В этот класс следует включить все неперехватываемые ошибки, а также некоторое подмножество перехватываемых ошибок. Говорят, что фрагмент программы имеет хорошее поведение (well behaved), если в нем не могут возникать запрещенные ошибки. Языки программирования, которые гарантируют хорошее поведение всех написанных на них программ, называются языками со строгой проверкой (strongly checked).
Таким образом, для программы, написанной на языке со строгой проверкой, справедливы следующие утверждения:
Существуют два пути для диагностики запрещенных ошибок: статическая проверка программы до ее выполнения (static checking) и динамическая проверка во время выполнения (dynamic checking). Статическая проверка характерна для языков, имеющих систему типов, а динамическая проверка - для так называемых бестиповых (typeless) языков, в которых либо вообще нет системы типов, либо существует только один универсальный тип данных. Динамическая проверка требует дополнительных ресурсов, поэтому статическая проверка является предпочтительной, так как чем больше ошибок диагностируется статически, тем выше эффективность программы.
Система типов в языке программирования разрабатывается для того чтобы можно было осуществлять статическую проверку программы. Она представляет собой набор правил, определяющих условия, при которых конструкции языка не вызывают запрещенных ошибок.
Так как платформа .NET спроектирована с учетом поддержки разных языков программирования, то ее общая система типов (
Наличие в .NET общей системы типов позволяет осуществлять статическую проверку программы не только на уровне компилятора, но и на уровне системы выполнения. Другими словами, система может проводить верификацию двоичных исполняемых файлов непосредственно перед их запуском. Это гарантирует безопасность кода, выполняемого в среде .NET и тем самым обеспечивает возможность автоматического управления памятью (сборку мусора).
Общая система типов достаточно обширна, так как при ее проектировании учитывалась необходимость поддержки различных языков программирования и, кроме того, уделялось большое внимание эффективности выполнения программ в среде .NET. Поэтому, для ясности изложения материала мы сначала рассмотрим основное подмножество общей системы типов. Будем называть это подмножество ядром системы типов.
Схема ядра общей системы типов .NET приведена на рис. 1.5. На схеме все типы делятся на две категории: это типы-значения (value types) и ссылочные типы (
(рис 1.5) Ядро общей системы типов
Типы-значения представляют собой
Ссылочные типы описывают так называемые объектные ссылки (
Значения любого типа хранятся в ячейках (location). В качестве ячеек могут выступать локальные и глобальные переменные, параметры методов, поля объектов и элементы массивов. Для каждой ячейки известен тип значений, которые она может содержать.
Особо важным является то обстоятельство, что ячейки не могут содержать объекты. Все объекты размещаются в специальной области памяти, называемой кучей (
Встроенные типы-значения делятся на две группы: целые типы и типы с плавающей запятой. Они перечислены в таблицах 1.1 и 1.2, соответственно. В первом столбце каждой из таблиц приведены имена типов, используемые в текстовом представлении CIL (в программах, компилируемых ассемблером ILASM). Во втором столбце перечислены имена, используемые для тех же самых типов в библиотеке классов .NET. В третьем столбце находится краткое описание, в котором указывается знаковость и разрядность типа.
| Тип | Имя в .NET Framework |
Описание |
|---|---|---|
bool |
System.Boolean |
булевский (8 бит) |
char |
System.Char |
символ Unicode (16 бит) |
int8 |
System.SByte |
целое со знаком (8 бит) |
int16 |
System.Int16 |
целое со знаком (16 бит) |
int32 |
System.Int32 |
целое со знаком (32 бит) |
int64 |
System.Int64 |
целое со знаком (64 бит) |
unsigned int8 |
System.Byte |
целое без знака (8 бит) |
unsigned int16 |
System.Uint16 |
целое без знака (16 бит) |
unsigned int32 |
System.Uint32 |
целое без знака (32 бит) |
unsigned int64 |
System.UInt64 |
целое без знака (64 бит) |
native int |
System.IntPtr |
целое со знаком (разрядность процессора) |
native unsigned int |
System.UIntPtr |
целое без знака (разрядность процессора) |
| Тип | Имя в .NET Framework |
Описание |
|---|---|---|
float32 |
System.Single |
вещественное (32 бит) |
float64 |
System.Double |
вещественное (64 бит) |
Для встроенных типов-значений определены правила преобразования значений одного типа в другой тип. Такие преобразования бывают сужающие (
В некоторых объектно-ориентированных языках программирования (например, в C++) объекты могут храниться как в куче (в динамической памяти), так и в переменных: глобальных (в статической памяти) и локальных (на стеке). Поэтому системы типов в таких языках содержат отдельные типы для самого объекта и для объектной ссылки (указателя на объект).
В среде .NET объекты и объектные ссылки хранятся раздельно, а именно: объекты хранятся в куче, а ссылки - в ячейках. Поэтому общая система типов спроектирована таким образом, что один и тот же ссылочный тип может являться как типом объекта, так и типом объектной ссылки.
Каждый объект в куче содержит информацию о своем типе. Поэтому ссылочные типы, представляющие объекты, называются самоописывающими (self-
Два самоописывающих типа являются встроенными - это System.Object (или просто object в текстовом представлении CIL) и System.String (или string ). Тип System.Object является общим базовым классом, от которого непосредственно или транзитивно наследует любой другой класс. Тип System.String используется для представления строковых данных в формате Unicode.
Основу самоописывающих типов составляют классы. Классы могут агрегировать значения других типов, а также наследоваться друг от друга (в .NET поддерживается только одиночное наследование). Классы могут содержать следующие элементы:
Поля являются ячейками, в которых хранятся значения других типов.
Методы представляют собой функции классов. Они бывают статическими (
Свойство представляет собой пару методов, один из которых возвращает некоторое значение, а другой устанавливает это значение.
События используются для асинхронного внесения изменений в объект.
Типы-массивы также относятся к самоописывающим типам, то есть каждый массив представляет собой объект в куче, доступ к которому осуществляется через объектную ссылку. Хотя, строго говоря, типы-массивы не являются классами, считается, что все они наследуют от библиотечного класса System.Array. Типы-массивы интересны тем, что, в отличие от классов, определяемых программистом самостоятельно, они формируются системой автоматически. То есть если мы имеем некоторый тип X, то тип массива, состоящего из элементов типа X, нам уже объявлять не нужно - об этом позаботится система выполнения.
(рис 1.6) Особенности представления массивов
Массивы бывают как одномерными (в этом случае они обрабатываются особенно эффективно благодаря использованию специальных инструкций CIL), так и многомерными. Кроме того, система поддерживает массивы, нижняя граница которых отлична от нуля.
Особого внимания заслуживают особенности представления
Интерфейсы служат для компенсации отсутствия в .NET множественного наследования. Они могут рассматриваться как чисто абстрактные классы, содержащие только перечисленные ниже элементы:
Хотя любой класс может наследоваться только от одного базового класса, он может реализовывать произвольное количество интерфейсов. То есть, интерфейс определяет контракт, которому должен удовлетворять любой класс, реализующий этот интерфейс.
Нужно отметить, что интерфейс может содержать реализации статических методов, но все остальные методы, включая методы свойств и событий, должны оставаться абстрактными.
Значение может иметь сразу несколько типов. Например, если некоторый объект реализует несколько интерфейсов, то каждый из них является его типом. Та же ситуация наблюдается при наследовании: если класс имеет несколько
Как уже говорилось ранее, ячейки, в которых могут храниться значения, также имеют тип. Соответственно, ячейка может содержать только значение, совместимое с ее типом. То есть, общая система типов не допускает присваивание ячейке несовместимого с ее типом значения.
Формулируется следующее условие совместимости значения и ячейки: для того чтобы некоторое значение можно было присвоить заданной ячейке, необходимо и достаточно, чтобы хотя бы один из типов значения совпадал с типом ячейки.
Для объектных ссылок и значений типов-значений вводятся отношения идентичности (identity) и равенства (equality). Эти отношения являются
Отношение идентичности для объектных ссылок вводится следующим образом: две объектных ссылки идентичны тогда и только тогда, когда они содержат адреса одного и того же объекта. На рис. 1.7 изображены три объектных ссылки A, B и C, а также два равных объекта-строки. Так как ссылки A и B содержат адрес одного и того же объекта, то они идентичны между собой, но при этом они не идентичны ссылке C, содержащей адрес другого объекта.
(рис 1.7) Пример, объясняющий отношения идентичности и равенства объектных ссылок
Отношение равенства для объектных ссылок формулируется так: две объектных ссылки равны тогда и только тогда, когда они содержат адреса равных объектов. Все три объектные ссылки, изображенные на рис. 1.7, равны между собой.
Отношение идентичности для типов-значений определяется следующим образом: два значения идентичны тогда и только тогда, когда они принадлежат одному и тому же типу-значению, и представляющие их последовательности битов равны.
Отношение равенства для
Отношения идентичности и равенства играют большую роль при программировании в среде .NET. Любой объект имеет Equals, унаследованный от System.Object и выполняющий сравнение объектов на равенство. В зависимости от реализации этого метода, отношение равенства может существенно меняться. При
Из соображений эффективности выполнения программ разработчики платформы .NET добавили в общую систему типов дополнительные элементы, а именно: пользовательские типы-значения (структуры и перечисления) и указатели. Схема общей системы типов, на которой отражены эти дополнительные элементы, приведена на рис. 1.8.
(рис 1.8) Общая система типов
Как показал опыт платформы Java, которая была разработана задолго до платформы .NET, одной из основных причин ухудшения производительности Java-программ является медленная работа сборщика мусора, вызванная большим количеством мелких объектов в куче. Это явление можно наблюдать в двух случаях:
Разработчиками .NET был подмечен тот факт, что использование типов-значений вместо объектов позволяет избежать описанных выше проблем, потому что:
Поэтому в общую систему типов были добавлены так называемые пользовательские типы-значения. Эти типы могут быть объявлены программистом, но, как и встроенные типы-значения, размещаются не в куче, а в ячейках.
Пользовательские типы-значения делятся на структуры и перечисления.
Структуры являются аналогом классов. Они, как и классы, могут содержать поля, методы, свойства и события. Все структуры неявно наследуют от библиотечного класса System.ValueType, и, более того, встроенные типы-значения также наследуют от этого класса. Тут сразу следует заметить, что система типов не предусматривает никакого наследования структур, кроме данного неявного. Другими словами, структуры не могут наследоваться друг от друга и, тем более, не могут наследоваться от классов (кроме System.ValueType ).
Перечисления представляют собой структуры с одним целочисленным полем Value. Кроме того, перечисления содержат набор констант, определяющих возможные значения поля Value. При этом для каждой константы в перечислении хранится ее имя. Перечисления неявно наследуют от библиотечного класса System.Enum, который, в свою очередь, является наследником все того же класса System.ValueType.
Использование указателей может значительно увеличить производительность, и в некоторых языках программирования указатели применяются исключительно часто (например, в C). Однако считается, что применение указателей чревато появлением в программах большого количества трудноуловимых неперехватываемых ошибок. Поэтому, например, система типов уже упоминавшейся платформы Java обходится без указателей.
Тем не менее, разработчикам .NET удалось добавить указатели в общую систему типов. При этом появилось две категории указателей: управляемые указатели (managed pointers) и неуправляемые указатели (unmanaged pointers).
Для того чтобы программы оставались безопасными, на использование управляемых указателей наложен целый ряд ограничений:
На использование неуправляемых указателей никаких ограничений не накладывается, то есть они могут содержать абсолютно любой адрес. Программа, в которой используются неуправляемые указатели, автоматически считается небезопасной и не может пройти верификацию.
Наличие в общей системе типов структур, которые во многом напоминают классы, но в действительности классами не являются, в некоторых случаях вызывает некоторые неудобства. Например, в библиотеке классов .NET существуют достаточно удобные контейнерные классы (наиболее часто используется класс ArrayList, представляющий массив с динамически меняющимся размером). Эти классы могут хранить ссылки на любые объекты, но не могут работать с типами-значениями.
Для решения этой проблемы в общей системе типов предусмотрены так называемые упакованные типы-значения. Эти типы являются ссылочными и самоописывающими. Объекты этих типов предназначены для хранения значений типов-значений.
Упакованные типы-значения не могут быть объявлены программистом. Система автоматически определяет такой тип для любого типа-значения.
Получение объекта упакованного типа-значения осуществляется путем упаковки (boxing). Упаковка заключается в том, что в куче создается пустой объект нужного размера, а затем значение копируется внутрь этого объекта.
С помощью упаковки мы можем превратить значение любого типа-значения (встроенного примитивного типа, структуры, перечисления) в объект и в дальнейшем работать с этим значением как с настоящим объектом (в том числе, мы можем положить его в ArrayList ).
Если же нам требуется произвести обратное действие, мы можем осуществить распаковку (unboxing). Распаковка заключается в том, что мы получаем управляемый указатель на содержимое объекта упакованного типа-значения.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.