Лекция 2 2. Обзор архитектуры .NET Язык С# не может рассматриваться отдельно от платформы .NET. Компилятор С# специально предназначен для .NET. Код, написанный на С#, будет выполняться только на платформе .NET. Поэтому архитектура и методология С# отражают архитектуру и методологию .NET. Следовательно, прежде чем начать программировать на С#, необходимо получить некоторое представление об архитектуре .NET. 2.1. Основная терминология .NET Рассмотрим некоторые термины, связанные с .NET. Общеязыковая среда выполнения (.NET runtime, Common Language Runtime, или CLR) – это набор программных средств Windows, обеспечивающий загрузку пользовательских .NET – программ, их выполнение и предоставление им всех необходимых служб. Управляемый код (managed code). Это любой код, который разработан для выполнения в .NET Framework. Код, который непосредственно работает под управлением Windows, называется неуправляемым. Промежуточный язык (MSIL, или IL). Это язык, на котором должен быть написан загружаемый и выполняемый средой .NET код. При компиляции исходной программы генерируется код на IL, a CLR выполняет заключительную стадию компиляции непосредственно перед исполнением кода. Язык IL разработан таким образом, чтобы обеспечить быструю компиляцию в машинный код, но в то же время он поддерживает все особенности .NET. Для текстового просмотра IL-кода, содержащегося в исполняемом модуле (EXE или DLL), можно использовать утилиту– дизассемблер ILDasm.exe. 2 Общая система типов (common type system, CTS). Для обеспечения совместимости различных языков необходимо иметь согласованный набор основных типов данных, чтобы все языки могли быть стандартизированы. Этим целям и служит CTS. Кроме того, CTS предоставляет правила для определения пользовательских типов. Базовые классы .NET. Это обширная иерархическая библиотека классов, которая содержит код для выполнения разнообразных задач в Windows, начиная от отображения форм и заканчивая базовыми службами Windows, чтением и записью в файлы, доступом к Интернету и доступом к источникам данных. Разработчик может непосредственно использовать эти классы или создавать новые классы, производные от базовых. Общая спецификация языка (common language specification, CLS). Это набор стандартов, который гарантирует совместимость языков .NET. Все компиляторы, предназначенные для .NET, должны поддерживать CLS. Сборка (assembly). Это модуль, в котором хранится скомпилированный управляемый код. Она похожа на классический исполняемый файл EXE или библиотеку DLL, но имеет важное свойство – она полностью себя описывает. Модулей может быть несколько. Сборка содержат метаданные, включающие в себя сведения о сборке и обо всех определенных внутри нее типах, методах и т. п. Метаданные содержат также информацию, которая может быть использована для проверки целостности сборки; информацию о версиях; сведения о том, какие сборки будут вызываться данной сборкой и, возможно, информацию о том, какие привилегии потребуются для выполнения кода сборки. Сборка может быть частной (доступной только одному приложению) и разделяемой (доступной любому приложению). Пакет, внутри которого содержится готовое приложение, состоит из некоторого числа сборок. Основной модуль одной из них является исполняемым и содержит точку входа программы. Другие сборки играют роль библиотек. Самоописываемая структура сборок практически исключает ошибки, которые возникают из-за проблем версий или проблем, связанных с 3 перезаписыванием другими приложениями совместно используемых библиотек (эта проблема ранее называлась DLL Hell). Отражение (reflection). Тот факт, что сборки полностью себя описывают, открывает теоретическую возможность программного доступа к метаданным сборки. Существует несколько базовых классов, разработанных с этой целью. Эта технология известна как отражение (поскольку программа может использовать эту технологию для доступа к собственным метаданным). Манифест. Часть сборки, в которой содержатся метаданные. Кэш сборок. Место на диске, где хранятся разделяемые сборки. Компиляция Just-In-Time (JIT). Этот термин обозначает процесс выполнения заключительной стадии компиляции с промежуточного языка в машинный код. Название определяется тем, что части IL-программы компилируются по мере необходимости. Каждый раз, когда в процессе исполнения встречается не исполнявшийся ранее метод, он компилируется в машинный код. Процесс компиляции происходит единственный раз. Заметим, что компилируются только те участки кода, которые действительно используются для выполнения. Пространства имен. идентификаторов, с Это помощью древовидная которой .NET система символьных Framework избегает дублирования между именами типов данных. Имя каждого типа должно дополняться префиксом – именем его пространства. Например, большинство базовых классов .NET общего назначения расположено в пространстве имен System. Базовый класс Array находится в этом пространстве имен, поэтому его полное имя – System.Array. Если пространство имен не объявлено, то тип помещается в безымянное глобальное пространство имен. Microsoft рекомендует использовать не менее двух вложенных пространств, одно из которых может быть названием компании, а другое – названием технологии или программного пакета, к которому принадлежит данный класс. Выполнение этого условия защитит класс от возможной коллизии имен с 4 классами, написанными в других организациях. В языках программирования пространства имен могут быть объявлены в исходном коде. Область приложения (application domain). Это технология, с помощью которой CLR позволяет различным программам выполняться в пространстве одного процесса Windows. Изоляция модулей достигается использованием безопасности типов IL для проверки того, что каждый сегмент кода ведет себя правильно. Это дает большой выигрыш в производительности при организации взаимодействия различных приложений. Сборка мусора (garbage collection). Во время выполнения кода CLR следит за использованием памяти. На основе этих наблюдений она в определенные моменты останавливает программу на короткий промежуток времени (обычно миллисекунды) и запускает сборщика мусора. Он проверяет переменные программы и выясняет, какие из областей памяти не используются программой, чтобы освободить эти участки. Таким образом, само приложение не несет ответственности за освобождение динамической памяти. Обычно сборщик мусора работает в низкоприоритетном потоке, но при дефиците памяти приоритет этого потока повышается. В результате память освобождается быстрее, пока не исчезнет ее дефицит, тогда приоритет потока сборщика мусора вновь снижается. Подобный подход к освобождению памяти, получивший обеспечивает максимальную название недетерминированного, производительность приложений и предоставляет им окружение, более устойчивое к ошибкам. Однако за эти удобства приходится расплачиваться неопределенностью. Невозможно с уверенностью сказать, когда именно будет уничтожен тот или иной объект. 2.2. Сравнение MSIL с байт-кодом Java Промежуточный язык (IL) и байт-код Java в основе имеют общую идею: это языки низкого уровня с простым синтаксисом, который можно быстро оттранслировать в машинный код. Целью байт-кода Java является 5 обеспечение платформенной независимости. При написании исходного кода Java его можно компилировать в байт-код, который может быть выполнен на различных платформах (UNIX, Linux или Windows). Промежуточный язык использует ту же концепцию, но компилируется в процессе выполнения программы, в то время как байт-код Java интерпретируется. Это означает, что большая часть потерь производительности, связанных с интерпретацией байт-кода Java, не затрагивает IL. Эти факты относятся к моменту выхода первой версии платформы .NET. Позднее фирма Sun выпустила Java-машину (JVM), которая также применяет JIT-компиляцию. MSIL предоставляет дополнительный уровень абстракции, позволяющий справляться с переносом кода с одной платформы на другую, в том числе, и с изменением разрядности платформы. В отличие от байт-кода Java, MSIL не привязан к фиксированной разрядности (32). Существуют версии MSIL для мобильных 16-разрядных устройств (.NET Compact Framework), стандартная 32-разрядная версия и специальная версия для работы с получающими все более широкое распространение 64-разрядными устройствами. Целью промежуточного языка является не только платформенная независимость, но и языковая независимость в объектно-ориентированной среде. Идея в том, что должна существовать возможность компиляции с любого языка, и ее результат должен быть совместим с кодом, откомпилированным с других языков. Эта совместимость достигнута в .NET. Однако платформенная независимость является лишь теоретически возможной, поскольку официально CLR разработана пока только для семейства Windows. Существуют независимые проекты (например, Mono и Portable.NET), позволяющие запускать программы .NET на некоторых других операционных системах. Из-за требований языковой независимости и совместимости промежуточный язык гораздо сложнее байт-кода Java. 6 2.3. О совместимости .NET-языков При работе в .NET Framework программы компилируются в общий промежуточный язык, разработанный на основе традиционного объектноориентированного подхода. Однако этого недостаточно для обеспечения совместимости языков. Так C++ и Java используют сходные объектноориентированные парадигмы, однако не считаются совместимыми. Под совместимостью языков понимается тот факт, что классы, созданные на одном из языков, могут напрямую общаться с классами, созданными в другом языке. В частности: класс, написанный на одном языке, должен иметь возможность наследования от класса, созданного в другом языке; класс должен иметь возможность содержать экземпляр другого класса, не важно, на каком языке он написан. Объект должен иметь возможность напрямую вызывать методы другого объекта независимо от того, на каких языках они написаны; должна существовать возможность передачи объектов (или ссылок на объекты) между методами; при вызове методов между различными языками для отладчика должна обеспечиваться возможность просматривать вызовы в общем сеансе отладки. .NET и промежуточный язык имеют все эти возможности. Остальные аспекты совместимости языков достигаются путем использования общей системы типов. 3. Типы данных С# Как и другие языки для .NET, C# поддерживает общую систему типов (CTS), которая включает не только известные примитивы (int, char, float и др.), но и более сложные типы, например, string для строк или decimal для 7 денежных сумм. Представитель каждого из этих типов данных является настоящим объектом, обладающим методами для форматирования, сериализации, преобразования типов и т.д. С# является строго типизированным языком. Например, тип bool не преобразуется автоматически в целый тип. Для выполнения такого преобразования необходимо использовать явное приведение типа. Более того, С# позволяет задавать, как определенные пользователем типы будут вести себя в контексте неявных и явных преобразований типов. Для использования переменной ее надо объявить. Объявление переменной содержит тип и имя переменной. Поскольку каждый тип даных CLR определяется в некотором пространстве имен, тип указывается с префиксом – пространством имен. Например, System.Windows.Forms.Form myForm; Для сокращения записи компилятору можно сообщать о часто используемых пространствах имен оператором using: using System.Windows.Forms; ………………………………….. Form myForm; Типам разрешается присваивать псевдонимы (alias): using FormAlias = System.Windows.Forms.Form; ……………………………………………………… FormAlias myForm; Для упрощения работы с рядом общеизвестных типов данных (примитивами), компилятор С# поддерживает собственные псевдонимы. Для определения значений переменных в C# используется оператор присваивания: myInteger = 42; 8 3.1 Размерные и ссылочные типы Язык C#, как и CLR, поддерживает две различные категории типов: размерные и ссылочные. Размерные типы (типы по значению, value types) представляют собой значения фиксированной длины, расположенные в стеке (stack). Они не могут быть пустыми (null). Каждая программа во время выполнения имеет свой собственный стек, которым не могут воспользоваться другие программы. Как правило, размерные типы занимают немного памяти. К ним относятся примитивы, перечисления и структуры. Такие данные в качестве параметров могут передаваться в функцию по значению. Ссылочные типы (типы по ссылке, reference types) представляют собой ссылки на объекты, расположенные в куче (heep). Они могут содержать пустое значение null. К ним относятся строки, массивы, интерфейсы, делегаты, классы. При передаче таких данных в качестве параметров в функцию передается адрес (или указатель на объект), а не копия объекта, как в случае размерного типа. Сами ссылки располагаются в стеке. Способ хранения типа данных определяет, как объект ведет себя в контексте оператора присваивания. Присвоение по значению приводит к созданию двух разных копий данных в стеке. Присвоение по ссылке копирует ссылку на одну и ту же позицию в памяти. 3.2. Встроенные типы С# С# имеет ряд предопределенных типов – размерных и ссылочных. 3.2.1. Встроенные размерные типы К ним относятся такие примитивы, как целые числа и числа с плавающей точкой, символы и булевский тип. С# использует лаконичный и гибкий 9 синтаксис определения переменных, аналогичный C/C++. Для объявления переменной простого размерного типа необходимо указать имя типа и далее имя объекта. // Это комментарии: int a; // Объявление переменной а типа integer в стеке а = 100; // Присвоение переменной а значения 100. Для удобства можно одновременно объявить и инициализировать переменную: int а = 100; Разрешается объединять несколько объявлений переменных в одном операторе, однако такой стиль считается неудачным, поскольку код труднее понимать и поддерживать: int a = 100, b, с = 200, d; В целях безопасности компилятор С# требует, чтобы каждая размерная переменная была явно инициализирована начальным значением до того, как ее можно будет использовать в операциях, и выдает ошибку при попытке использования неинициализированной переменной. Приведем в виде таблиц перечень встроенных примитивов C#. Диапазон изменения этих типов определяется их разрядностью. Целые типы. Имя Тип CTS sbyte System.SByte 8-разрядное со знаком short System.Int16 16-разрядное со знаком int System.Int32 32-разрядное со знаком long System.Int64 64-разрядное со знаком byte System.Byte 8-разрядное без знака ushort System.UInt16 16-разрядное без знака uint System.UInt32 32-разрядное без знака ulong System.UInt64 64-разрядное без знака Константы целого Описание типа могут задаваться в десятичной или шестнадцатеричной системе. Последняя требует присутствия префикса 0х: long х = 0х12AB; 10 При неоднозначности использования целых констант по умолчанию берется тип int. Для указания другого типа можно использовать символы U, L (заглавные или строчные) записываемые после числа: uint ui = 1234U; long l = 1234L; ulong ul = 1234UL; Вещественные типы (с плавающей точкой). Имя Тип CTS Описание Кол-во значащих цифр float System.Single 32-разрядное 7 64-разрядное 15/16 одинарной точности double System.Double двойной точности decimal System.Decimal 128-разрядное 28 повышенной точности При неоднозначности использования вещественных констант по умолчанию берется тип double. Для определения констант float в конце числа испольуют символы F или R, decimal – M или T . Логический тип. Имя Тип CTS Значения bool System.Boolean Принимает значения true или false. Тип bool нельзя автоматически преобразовать ни в какой из целых типов или обратно. Если переменная объявлена как bool, она может принимать только значения true и false. При попытке использовать нуль в качестве false или ненулевое значение в качестве true возникнет ошибка. Символьный тип. Имя Тип CTS Значения char System.Char Представляет один символ Unicode (16 бит) Хотя для представления любого символа латинского алфавита и цифр от 0 до 9 хватит восьми битов, этого недостаточно для представления символов в более объемных символьных системах (например, китайский язык). В целях обеспечения универсальности компьютерная индустрия движется от 8- 11 битовых наборов символов ASCII к 16-битовой схеме Unicode, в которой кодировка ASCII является подмножеством. Константы типа char в виде литералов обозначаются одинарными кавычками, например 'А'. Символы char можно представлять также четырехзначными шестнадцатеричными значениями Unicode ('\u0041' или ‘\x0041') или приведенными значениями целого типа ((char)65). Они могут содержать также последовательности специальных символов: Последовательность Значение \' Апостроф \” Двойная кавычка \\ Обратный слэш \0 null \а Внимание \b Возврат назад на один символ \f Подача страницы \n Новая строка \г Возврат каретки \t Символ табуляции \v Вертикальная табуляция 3.2.2. Встроенные ссылочные типы С# поддерживает два предопределенных типа по ссылке: Имя Тип CTS Описание object System.Object Предок всех типов CTS string System.String Строка символов Unicode Тип object является исходным типом-предком, от которого берут начало как все встроенные, так и все определенные пользователем типы. Этот тип тесно связан с концепцией объектно-ориентированного программирования в .NET и будет рассматриваться в последующих разделах. Тип string позволяет хранить символьные строки и выполнять над ними ряд операций. 12 string strl = “Здравствуй, “, str2 = “ мир!”; string str3 = strl + str2; // str3 = “Здравствуй, мир!” Несмотря на то, что присваивание выполняется в стиле размерных типов, тип CTS System.String является ссылочным. Когда одна строковая переменная присваивается другой, в результате получаются две ссылки на одну и ту же строку в памяти. Однако если впоследствии будут сделаны изменения в одной из этих строк, то это создаст новый объект string, в то время как другая строка останется неизменной. Строковые константы заключаются в двойные кавычки. Строки С# могут содержать те же символы Unicode и специальные символы, что и тип char. Однако из-за использования знака ‘\’ для обозначения специальных символов, его нельзя указывать в строке просто так. Необходимо поставить перед ним еще один символ ‘\’: string FilePath = "С: \\ProfessionalCSharp\\First.cs"; Этому правилу предусмотрена альтернатива. Строковую константу можно предварить символом ‘@’, тогда все символы в строке будут обрабатываться как содержательные: string FilePath = @"C:\ProfessionalCSharp\First.cs"; Таким образом можно включать в строковые литералы даже переводы строк.