При появлении языка C# в 1999 Microsoft представлял язык следующим образом.
Идеальное решение для C и C++ программистов, обеспечивающее быструю разработку в сочетании с мощью доступа ко всей функциональности платформы. Программисты получают окружение, полностью синхронизированное с появляющимися Web-стандартами и обеспечивающее простую интеграцию с существующими приложениями. В дополнение появляется возможность при необходимости создавать код на низком уровне.
Язык C# - это современный OO-язык, позволяющий программистам быстро строить широкий круг приложений для новой .NET платформы, предоставляющей полный набор инструментария и сервисов, которые необходимы как для вычислений, так и для коммуникаций.
Элегантно спроектированный OO-язык C# является великолепным выбором при построении архитектуры компонентов - начиная от высокоуровневых бизнес-объектов до приложений системного уровня. Используя простые конструкции языка C#, эти компоненты могут быть конвертированы в XML Web-сервисы, вызваны затем в Интернете из любого языка, выполняемого на любой операционной системе.
Большинство читателей этой книги предпочитают нормальный русский язык - поэтому стоит дать перевод этого пышного представления: "C# - это Java плюс делегаты (объекты в духе агентов) и несколько низкоуровневых механизмов, заимствованных от C++". C# был ответом Microsoft в конкурентной борьбе с компаниями, поддерживающими Java, в частности Sun Microsystems и IBM. Язык чрезвычайно близок к Java.
Эта характеристика остается во многом справедливой и сегодня, хотя C# эволюционировал своим собственным путем и ввел несколько интересных инноваций, не имеющих аналогов в Java. На момент написания этого текста C# (версия 3.0) является мощным языком, и здесь мы рассмотрим только его основы .
Для изучения C# знание Java полезно, но не требуется. Это приложение не предполагает, что вы прочли описание Java, приведенное в предыдущем приложении (как следствие, повторяются некоторые рассуждения, когда рассматриваются разделяемые концепции). Подобно другим приложениям, язык не рассматривается с чистого листа, - обсуждение предполагает знакомство с программистскими концепциями, введенными в этой книге. Описание языка сопровождается сравнением с соответствующими механизмами Eiffel.
C# (произносится "C шарп") тесно связан с окружением Microsoft .NET, платформой для разработки и выполнения ПО, использующей виртуальную машину. В предыдущих обсуждениях отмечалась роль виртуальных машин и их преимущества для реализации языков высокого уровня.
В то время как виртуальная машина Java - JVM - была спроектирована специально для поддержки этого языка (хотя позднее она использовалась для реализации других языков программирования), главная цель проекта платформы .NET состояла с самого начала в поддержке нескольких языков. Это решение отражается как в имени виртуальной машины - Common Language Runtime (CLR) - "Общеязыковая Среда Выполнения", так и в поддержке взаимодействия API, Common Language Infrastructure (CLI) - "Общеязыковой Инфраструктуры", которая теперь является международным стандартом.
Частично причина была в том, что Microsoft еще до .NET обеспечил реализацию нескольких языков, прежде всего, Visual Basic (VB), C++ и JScript для клиентских Web-приложений. VB популярен на массовом рынке и часто используется для разработки приложений по настройке офисных документов. Компания не могла, естественно, предложить соответствующим программистским сообществам бросить свои любимые языки и перейти на новый бренд. Она смогла обеспечить общую базу для взаимодействия и будущей эволюции. .NET и CLR/CLI были способны с самого начала обеспечить реализацию четырех поддерживаемых Microsoft языков (помимо трех упомянутых еще и C#), а также языки, разрабатываемые другими компаниями, включая Eiffel (с самого начала введения .NET в 1999) и Cobol, язык прошлых лет, но все еще важный для многих бизнес-приложений.
Языковая открытость среды означала не только доступность нескольких компиляторов, но и влекла высокую степень взаимодействия между программами, написанными на разных языках. В этом роль Общеязыковой инфраструктуры: обеспечить стандартное множество механизмов, которые могут быть использованы в любом языке. CLI представляет объектную модель, близкую ОО-языку, но без синтаксиса. Эта модель задает множество хорошо определенных механизмов - это ОО механизмы, изучаемые в этой книге: классы, их компоненты, наследование, универсальность, система типов, объекты, политика динамического создания объектов и сборка мусора - для которых CLI-проект предложил несколько специфических проектных решений.
При условии, что .NET-языки не слишком отклоняются от этих решений, они могут достичь степени взаимодействия, неслыханной в дни, предшествующие .NET. В частности, классы, написанные на разных языках, могут взаимодействовать друг с другом, используя как отношения наследования, так и клиентские. Например, Eiffel-класс может наследовать от класса C#, возможно и обратное наследование. Это просто предполагает, что компиляторы следуют единым CLI-правилам для поставщиков и клиентов сборок (целевых модулей, создаваемых .NET-компиляторами).
Эта схема взаимодействия доказала свою успешность (несмотря на существующую тенденцию производителей ПО игнорировать правила CLI-совместимости). Она позволяет каждому языку сохранять свою индивидуальность, пока ее можно отобразить в объектную модель CLI. Например, реализация Eiffel должна моделировать множественное наследование - не поддерживаемое напрямую CLI - через специальное использование CLI-механизмов (множественное наследование интерфейсов, концепцию, обсуждаемую позже в этом приложении).
В сообществе языков, как и у людей, все языки равны, но некоторые равны более других. Язык C# - это любимый сын: его объектная модель наиболее тесно связана с CLI. VB .NET, который схож с предыдущей версией только синтаксически, является еще одним претендентом на звание "любимого". CLI-совместимая версия C++ - "управляемый C++" - существенно отличается от обычного C++. Ограничения необходимы, чтобы язык мог принимать участие в играх .NET-взаимодействия. Фактически, семантика C# определялась семантикой CLI, хотя последующие версии ее существенно расширили. Синтаксис языка соответствует традиции C, C++, Java, включая завершение операторов символом точки с запятой и применением фигурных скобок для окаймления блоков программы.
Базисными элементами C# программы являются классы и структуры, организованные в виде нескольких программных файлов.
C#-классы (ключевое слово class) и структуры (ключевое слово struct) задают описание множества возможных объектов периода выполнения. Объекты обладают свойствами, и к ним применимы методы. Общая форма объявления такова:
class name { … Объявление компонентов … }
При объявлении структуры вместо ключевого слова class используется слово struct. Компоненты могут быть разные. Это может быть:
Структура является упрощенной формой класса без возможности наследования. Остальная часть обсуждения фокусируется на классах, но большинство свойств, не связанных с наследованием, применимо и к структурам1 Главное отличие классов от структур состоит в том, что класс определяет ссылочный тип, а структура - развернутый. Значения ссылочного типа разделяют память - на один и тот же объект может указывать несколько ссылок, имена ссылок являются синонимами. Объект развернутого типа ни с кем свою память не разделяет. Все примитивные типы - арифметический и другие - реализованы как структуры..
Классы и структуры группируются в сборки (понятие, соответствующее кластеру в Eiffel2 Для программиста классы, структуры и другие частные случаи - интерфейсы, делегаты, перечисления - группируются в проекты, преобразуемые в сборки в результате компиляции проекта.).
Каждая выполняемая программа должна иметь по меньшей мере один метод, называемый Main и помеченный как статический (static - понятие, поясняемое в следующем разделе). Выполнение программы начинается с выполнения этого метода. Можно написать классический пример "Hello world" с одним классом и Main-методом:
public class Program { static void Main(string[] arguments) { System.Console.WriteLine("Hello world!"); } }
Main может не иметь аргументов или, если выполнение нуждается в аргументах, предоставляемых пользователем, иметь один аргумент, представленный массивом строк (string[]). Метод может не возвращать результат, или возвращать целочисленное значение, которое обычно рассматривается как статус, сигнализирующий об уровне ошибок, если метод завершается с ошибками.
Многие концепции C# совпадают с теми, что мы видели при изучении этой книги. Но есть некоторые вариации.
Одна из концепций C#, уклоняющаяся от строгого ОО-стиля, который используется в этой книге, состоит в поддержке статических компонентов класса и классов в целом.
Обычно для использования компонента класса необходим целевой объект. Стандартная ОО-нотация доступа к компоненту имеет вид target.member (возможно, с передачей аргументов компоненту), где target обозначает целевой объект. Текущий объект (Current в Eiffel), в C# имеет имя this, которое, как и в Eiffel, можно опускать, когда из контекста ясно, что речь идет о текущем объекте.
В C# разрешается объявлять статические члены, не требующие объекта и вызываемые как C.member, где C - имя класса. Определение статического компонента, например статического метода, может использовать только статические компоненты.
Класс в целом может также быть объявленным как статический, если все его компоненты статические. Тогда невозможно создать экземпляры этого класса. Статический класс мoжет быть удобен, например, для группирования множества объектно-независимых общих свойств, таких как математические функции.
Уже упоминалось, что метод Main должен быть статическим; причина в том, что на старте выполнения не существует объекта, который мог бы вызвать метод. В Eiffel проблема решается за счет того, что выполнение определяется как создание "корневого объекта", к которому применяется "корневая процедура3 По поводу статических компонентов класса в Java и C# смотри мой комментарий в приложении по Java. Статический конструктор C# создает статический объект, который и является целью вызова статических компонентов. В статический конструктор можно добавить свой код, например, для определения специфических констант класса. Выполнение программы C#, как и в Eiffel, можно рассматривать как создание статическим конструктором корневого статического объекта, который и вызывает корневую процедуру - Main.".
Для скрытия информации каждый тип и компонент имеет уровень доступности, определяя права клиентов на доступ. Цель та же, что и в Eiifel: механизм скрытия информации, включая селективный экспорт, но с грубой гранулярностью, поскольку в C# нельзя создать список ВИП-персон - классов, которым будет доступен некий компонент класса. Тремя возможными квалификаторами являются:
Применимы некоторые ограничения: класс может быть только internal (по умолчанию) или public, если он не является внутренним классом (классом, объявленным внутри другого класса), который может быть также и private. Деструкторы не могут иметь модификаторов доступа. Операции, определенные программистом, должны быть static и public. Доступность компонентов не может превосходить доступность класса. Не трудно видеть смысл, стоящий за каждым из этих правил5 Важным ограничением для C# является то, что компоненты интерфейсов объявляются без указания квалификаторов доступа..
C#-поля соответствуют атрибутам. В этой книге (вне приложения) используется другая терминология: под полем (динамическим понятием) понимается составляющая объекта, соответствующая компоненту генерирующего класса - атрибуту (статическое понятие). В C# один термин применяется для обоих понятий.
При объявлении поля задается его тип (перед именем поля, как в T f, вместо f: T в Eiffel). Объявление может включать инициализацию поля, используя для этого символ присваивания =, после которого может идти константное выражение, вычислимое в момент компиляции и не содержащее других полей, отличных от констант и статических полей. Вот пример объявления двух полей:
class A { public string s1 = "ABC"; public readonly string s2 = "DEF"; … Other member declarations … }
Заметьте: точка с запятой завершает все объявления и операторы. Квалификатор readonly защищает поля от присваивания, за исключением инициализации в момент объявления, как здесь, или в конструкторе.
В отличие от Eiffel, экспорт полей без статуса readonly дает клиентам право на чтение и запись. Для приложений, признающих преимущества скрытия информации, это означает, что поля должны иметь статус по умолчанию private и при необходимости снабжаться специальными методами доступа - геттером и сеттером. C# упрощает их написание, введя понятия метода - свойства, изучаемого ниже.
C# обеспечивает несколько встроенных типов:
Тип object является предком всех типов (аналог ANY в Eiffel). Ссылка null (void) записывается как null.
Каждый C# тип является ссылочным или значимым типом. Отличия те же, что и для Eiffel. Переменная значимого типа непосредственно обозначает значение, которое может быть простым значением только что рассмотренного типа (встроенные типы, за исключением string и object, являются значимыми типами) или сложным объектом. Переменная ссылочного типа обозначает ссылку на объект.
Перейти от значения к ссылке можно, используя операцию, называемую боксингом, или упаковкой:
int i; object o; i = 1; o = i;//Boxing: Создает объект, обертывающий значение, присоединяет o к нему
Как показывает этот пример, операция боксинга выполняется автоматически при присваивании ссылке переменной значимого типа. Обратная операция - распаковка - должна выполняться явно, с использованием кастинга - приведения к типу:
i=(int)o;//Распаковка: Получение целого, хранимого в o, и присваивании его i.
Поле может быть объявлено константой, как const, указывающее, что для всех экземпляров класса оно сохраняет значение, заданное при инициализации. Значение может быть литеральной константой (манифестным целым, строкой и так далее) или константным выражением, включающим ранее определенные константы:
public const string s3 = "ABC-"; public const string s4 = s3 + "DEF"; // Значение: "ABC-DEF"
Так как константы относятся к статическим объектам, для доступа к ним используется имя класса A.s4, если приведенное объявление константы появилось в классе A.
Заметьте разницу между const- и readonly-полями. Значения первых должны быть заданы при объявлении, значения вторых - могут быть заданы в конструкторе (например, в статическом конструкторе).
Метод в C# может быть реализован процедурой (возвращающей тип void - результат отсутствует) или функцией, возвращающей результат, тип которого отличен от void. Вот примеры, иллюстрирующие некоторые важные возможности:
class B { public void p(int arg1, ref int arg2) {… arg2 = 0;} // Процедура public string f() {… return "ABC";} // Функция public static string sf() {… return "DEF";} // Статическая функция }
По умолчанию аргументы передаются "по значению" (как в Eiffel), в этом случае формальный аргумент представляет копию фактического аргумента (в зависимости от типа - ссылочного или значимого - копия может быть ссылкой или полным объектом). Аргументы можно передавать "по ссылке", снабдив их описателем ref. В этом случае присваивание аргументу, такому как arg2, в процедуре p, будет модифицировать и фактический аргумент.
Фактический аргумент, соответствующий ref-формальному аргументу, должен также специфицироваться как ref при вызове:
B v = new B(); int x = 1; int y = 1; v.p(x, ref y); //Не изменяет x, но значение y становится равным нулю
Объявление локальных переменных может появляться в теле метода, предваряя их использование. Имена не должны совпадать с именами формальных аргументов и других локальных переменных.
Имена локальных переменных и формальных аргументов могут совпадать с именами полей класса, имея приоритет. Конфликт не возникает, поскольку поле класса можно квалифицировать именем текущего объекта this, как в примере:
int a; // a - имя поля класса r (int a) {this.a = a;} // и формального аргумента
Метод может получить доступ к полю, используя нотацию this.a. Для конструкторов C# типичной практикой является именовать аргумент, служащий для инициализации поля, именем этого поля. Лучше избегать этого и выбирать разные имена для каждой цели.
C# допускает перегрузку методов: несколько методов класса могут иметь одинаковые имена, если их сигнатуры различны (отличаются числом аргументов или их типами, включая и описатель ref, входящий в сигнатуру). Тип результата в сигнатуру не входит.
Предыдущий методологический комментарий применим и здесь. Имена не являются дефицитным ресурсом. Перегрузка, однако, является распространенной практикой в C# и требуется для конструкторов, что будет обсуждаться ниже.
Политика экспорта, как отмечалось, не различает доступ на чтение и на запись. Это значит, что поле никогда не следует экспортировать, так как это позволило бы клиентам выполнять прямые присваивания x.a = v полю с именем a, нарушая все принципы скрытия информации. ОО-решение в этом случае обеспечивает сеттер-процедура и геттер-функция (в которой нет необходимости в Eiffel, так как при экспорте гарантируется статус "только для чтения"). В C# написание геттеров и сеттеров стандартизовано благодаря введению понятия метода-свойства. Вот образец использования метода-свойства для закрытого поля:
class C { private string a; // Закрытое поле public string ap { // Метод-свойство get {return a;} // Геттер set { // Сеттер a = value; // Изменение значения поля … Возможно, другие операторы … }}}
Этот механизм использует три ключевых слова get, set и value. Объявляются два специальных метода с именами get и set для доступа к атрибуту, в случае сеттера - через синтаксис присваивания:
C x = new C(); string b; b = x.ap; // Использование геттера x.ap = "ABC"; // Использование сеттера
Эффект подобен тому, что достигается в Eiffel спецификацией присваивания. Механизм Eiffel не требует геттера, который на практике сводится к чтению поля. Сеттер, чаще всего, - нечто большее, чем присваивание, (например, при изменениях поля может требоваться запись в журнал регистрации), так что нормально записывать его явно как процедуру.
Процедуры создания, используемые для создания и инициализации объектов, называются в C# конструкторами. Конструктор может быть:
Следующий класс содержит пример каждого типа:
class D { public D(string a) { // Конструктор 1: экземпляра … Инициализирует поле, обычно использует аргумент a … } static D() { // Конструктор 2: статический … Инициализирует статические поля … } }
Конструкторы не имеют собственных имен, используя имя класса, основываясь на перегрузке и разрешая конфликты через отличия в сигнатурах, если есть более одного конструктора.
Иногда возникают проблемы, например, классу POINT, описывающему точку на плоскости, полезно иметь два конструктора make_cartesian и make_polar, задающих координаты точки в декартовой и полярной системе координат. Оба конструктора имеют одинаковую сигнатуру - два аргумента типа float. Для разрешения конфликта одному из конструкторов приходится добавлять фиктивный аргумент.
Объявление конструктора не специфицирует возвращаемый тип (void тоже не задается). У статического конструктора не может быть никаких других модификаторов.
Создание нового объекта основано на операции new (create в Eiffel) и вызове конструктора экземпляра. Вот пример:
D x = new D("ABC");
В C# нет различия между объявлениями (статикой) и операторами (динамикой), позволяя в объявлениях создавать и инициализировать объекты, выполняя операцию new с вызовом конструктора, создающего объект - экземпляр класса D в примере.
Это, однако, далеко не полная история о конструкторах экземпляра и создании экземпляра. Детальная спецификация (дается ниже при обсуждении наследования) объясняет, что ваш вызов конструктора может в результате приводить к вызову других конструкторов, создавая цепочку вызовов, которая должна включать вызов конструктора каждого предка класса.
Статический конструктор, существующий в единственном экземпляре, без аргументов, что отражено в примере, выполняется до того, как потребуется экземпляр класса или доступ к статическому элементу класса. Это позволяет инициализировать свойства, связанные с классом в целом, прежде чем появятся специфические экземпляры (как это делается в Eiffel через однократные once-функции). Представьте экземпляр системы, фиксирующей ошибки, где ошибки записываются в специальный журнал (файл). Первое появление ошибки приводит к созданию и открытию этого файла.