При появлении языка C# в 1999 Microsoft представлял язык следующим образом.
Идеальное решение для C и C++ программистов, обеспечивающее быструю разработку в сочетании с мощью доступа ко всей функциональности платформы. Программисты получают окружение, полностью синхронизированное с появляющимися Web-стандартами и обеспечивающее простую интеграцию с существующими приложениями. В дополнение появляется возможность при необходимости создавать код на низком уровне.
Язык C# - это современный OO-язык, позволяющий программистам быстро строить широкий круг приложений для новой .NET платформы, предоставляющей полный набор инструментария и сервисов, которые необходимы как для вычислений, так и для коммуникаций.
Элегантно спроектированный OO-язык C# является великолепным выбором при построении архитектуры компонентов - начиная от высокоуровневых C#, эти компоненты могут быть конвертированы в XML Web-сервисы, вызваны затем в Интернете из любого языка, выполняемого на любой операционной системе.
(рис 12.1) Андерс Хейлсберг (разработчик C#), 2007
Большинство читателей этой книги предпочитают нормальный русский язык - поэтому стоит дать перевод этого пышного представления: "C# - это Java плюс делегаты (объекты в духе агентов) и несколько низкоуровневых механизмов, заимствованных от C++". C# был ответом Microsoft в конкурентной борьбе с компаниями, поддерживающими Java, в частности Sun Microsystems и IBM. Язык чрезвычайно близок к Java.
Эта характеристика остается во многом справедливой и сегодня, хотя C# эволюционировал своим собственным путем и ввел несколько интересных инноваций, не имеющих аналогов в Java. На момент написания этого текста C# (версия 3.0) является мощным языком, и здесь мы рассмотрим только его основы .
Для изучения C# знание Java полезно, но не требуется. Это приложение не предполагает, что вы прочли описание Java, приведенное в предыдущем приложении (как следствие, повторяются некоторые рассуждения, когда рассматриваются разделяемые концепции). Подобно другим приложениям, язык не рассматривается с чистого листа, - обсуждение предполагает знакомство с программистскими концепциями, введенными в этой книге. Описание языка сопровождается сравнением с соответствующими механизмами .
C# (произносится "C шарп") тесно связан с окружением Microsoft .NET, платформой для разработки и выполнения ПО, использующей виртуальную машину. В предыдущих обсуждениях отмечалась роль виртуальных машин и их преимущества для реализации языков высокого уровня.
В то время как виртуальная машина Java - - была спроектирована специально для поддержки этого языка (хотя позднее она использовалась для реализации других языков программирования), главная цель проекта платформы .NET состояла с самого начала в поддержке нескольких языков. Это решение отражается как в имени виртуальной машины - Common Language Runtime (CLR) - "Общеязыковая Среда Выполнения", так и в поддержке взаимодействия API, - "Общеязыковой Инфраструктуры", которая теперь является международным стандартом.
Частично причина была в том, что Microsoft еще до .NET обеспечил реализацию нескольких языков, прежде всего, Visual Basic (VB), C++ и JScript для клиентских Web-приложений. VB популярен на массовом рынке и часто используется для разработки приложений по настройке офисных документов. Компания не могла, естественно, предложить соответствующим программистским сообществам бросить свои любимые языки и перейти на новый бренд. Она смогла обеспечить общую базу для взаимодействия и будущей эволюции. .NET и CLR/CLI были способны с самого начала обеспечить реализацию четырех поддерживаемых Microsoft языков (помимо трех упомянутых еще и C#), а также языки, разрабатываемые другими компаниями, включая (с самого начала введения .NET в 1999) и Cobol, язык прошлых лет, но все еще важный для многих бизнес-приложений.
Языковая CLI представляет объектную модель, близкую ОО-языку, но без синтаксиса. Эта модель задает множество хорошо определенных механизмов - это ОО механизмы, изучаемые в этой книге: классы, их компоненты, наследование, универсальность, система типов, объекты, политика динамического создания объектов и сборка мусора - для которых CLI-проект предложил несколько специфических проектных решений.
При условии, что .NET-языки не слишком отклоняются от этих решений, они могут достичь степени взаимодействия, неслыханной в дни, предшествующие .NET. В частности, классы, написанные на разных языках, могут взаимодействовать друг с другом, используя как -класс может наследовать от класса C#, возможно и обратное наследование. Это просто предполагает, что компиляторы следуют единым CLI-правилам для поставщиков и клиентов сборок (целевых модулей, создаваемых .NET-компиляторами).
Эта схема взаимодействия доказала свою успешность (несмотря на существующую тенденцию производителей ПО игнорировать правила CLI-совместимости). Она позволяет каждому языку сохранять свою индивидуальность, пока ее можно отобразить в объектную модель CLI. Например, реализация должна моделировать множественное наследование - не поддерживаемое напрямую CLI - через специальное использование CLI-механизмов (множественное наследование интерфейсов, концепцию, обсуждаемую позже в этом приложении).
В сообществе языков, как и у людей, все языки равны, но некоторые равны более других. Язык C# - это любимый сын: его объектная модель наиболее тесно связана с CLI. VB .NET, который схож с предыдущей версией только синтаксически, является еще одним претендентом на звание "любимого". CLI-совместимая версия C++ - "управляемый C++" - существенно отличается от обычного C++. Ограничения необходимы, чтобы язык мог принимать участие в играх .NET-взаимодействия. Фактически, семантика C# определялась семантикой CLI, хотя последующие версии ее существенно расширили. Синтаксис языка соответствует традиции C, C++, Java, включая завершение операторов символом точки с запятой и применением фигурных скобок для окаймления блоков программы.
Базисными элементами C# программы являются классы и структуры, организованные в виде нескольких программных файлов.
C#-классы (ключевое слово class) и структуры (ключевое слово struct) задают описание множества возможных объектов периода выполнения. Объекты обладают свойствами, и к ним применимы методы. Общая форма объявления такова:
class name {
… Объявление компонентов …
}
При объявлении структуры вместо ключевого слова class используется слово struct. Компоненты могут быть разные. Это может быть:
Eiffel ;Структура является упрощенной формой класса без возможности наследования. Остальная часть обсуждения фокусируется на классах, но большинство свойств, не связанных с наследованием, применимо и к
Классы и структуры группируются в сборки (понятие, соответствующее кластеру в
Каждая выполняемая программа должна иметь по меньшей мере один метод, называемый 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 в ), в C# имеет имя this, которое, как и в , можно опускать, когда из контекста ясно, что речь идет о текущем объекте.
В C# разрешается объявлять статические члены, не требующие объекта и вызываемые как C.member, где C - имя класса. Определение статического компонента, например статического метода, может использовать только статические компоненты.
Класс в целом может также быть объявленным как статический, если все его компоненты статические. Тогда невозможно создать экземпляры этого класса. Статический класс мoжет быть удобен, например, для группирования множества объектно-независимых общих свойств, таких как математические функции.
Уже упоминалось, что метод Main должен быть статическим; причина в том, что на старте выполнения не существует объекта, который мог бы вызвать метод. В проблема решается за счет того, что выполнение определяется как создание "корневого объекта", к которому применяется "корневая
Для скрытия информации каждый тип и компонент имеет уровень доступности, определяя права клиентов на доступ. Цель та же, что и в Eiifel: механизм скрытия информации, включая C# нельзя создать список ВИП-персон - классов, которым будет доступен некий компонент класса. Тремя возможными квалификаторами являются:
public: доступен в любом коде;internal: доступен в коде той же сборки;private: доступен коду самого класса или структуры. Это статус по умолчанию для компонентов Применимы некоторые ограничения: класс может быть только internal (по умолчанию) или public, если он не является внутренним классом (классом, объявленным внутри другого класса), который может быть также и private. Деструкторы не могут иметь модификаторов доступа. Операции, определенные программистом, должны быть static и public. Доступность компонентов не может превосходить доступность класса. Не трудно видеть смысл, стоящий за каждым из этих C# является то, что компоненты интерфейсов объявляются без указания
C#-поля соответствуют атрибутам. В этой книге (вне приложения) используется другая терминология: под полем (динамическим понятием) понимается составляющая объекта, соответствующая компоненту генерирующего класса - атрибуту (статическое понятие). В C# один термин применяется для обоих понятий.
При объявлении поля задается его тип (перед именем поля, как в T f, вместо f: T в ). Объявление может включать инициализацию поля, используя для этого символ присваивания =, после которого может идти константное выражение, вычислимое в момент компиляции и не содержащее других полей, отличных от констант и
class A {
public string s1 = "ABC";
public readonly string s2 = "DEF";
… Other member declarations …
}
Заметьте: точка с запятой завершает все объявления и операторы. Квалификатор readonly защищает поля от присваивания, за исключением инициализации в момент объявления, как здесь, или в конструкторе.
В отличие от , экспорт полей без статуса readonly дает клиентам право на чтение и запись. Для приложений, признающих преимущества скрытия информации, это означает, что поля должны иметь статус по умолчанию private и при необходимости снабжаться специальными методами доступа - геттером и сеттером. C# упрощает их написание, введя понятия метода - свойства, изучаемого ниже.
C# обеспечивает несколько встроенных типов:
bool, представляющий булевские значения;char, представляющий 16-бит Unicode-символы. Константа char записывается в одиночных кавычках, как 'A';string, представляющий последовательность из нуля или более символов char. Строковая константа записывается в двойных кавычках, как "ABC";sbyte (знаковый 8-бит), byte (беззнаковый 8-бит), short (знаковый 16-бит), ushort (беззнаковый 16-бит), int (знаковый 32-бит), uint (беззнаковый 32-бит), long (знаковый 64-бит), ulong (беззнаковый 64-бит);float, double и decimal, представляющие 32-бит, 64-бит и 128-бит IEEE-числа с плавающей точкой.Тип object является предком всех типов (аналог ANY в ). Ссылка null (void) записывается как null.
Каждый C# тип является ссылочным или . Переменная 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";} // Статическая функция
}
По умолчанию аргументы передаются "по значению" (как в ), в этом случае 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, нарушая все принципы скрытия информации. ОО-решение в этом случае обеспечивает сеттер-процедура и геттер-функция (в которой нет необходимости в , так как при экспорте гарантируется статус "только для чтения"). В 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"; // Использование сеттера
Эффект подобен тому, что достигается в
Процедуры создания, используемые для создания и инициализации объектов, называются в C# конструкторами. Конструктор может быть:
Следующий класс содержит пример каждого типа:
class D {
public D(string a) { // Конструктор 1: экземпляра
… Инициализирует поле, обычно использует аргумент a …
}
static D() { // Конструктор 2: статический
… Инициализирует статические поля …
}
}
Конструкторы не имеют собственных имен, используя имя класса, основываясь на перегрузке и разрешая конфликты через отличия в сигнатурах, если есть более одного конструктора.
Иногда возникают проблемы, например, классу POINT, описывающему точку на плоскости, полезно иметь два конструктора make_cartesian и make_polar, задающих координаты точки в декартовой и float. Для разрешения конфликта одному из конструкторов приходится добавлять фиктивный аргумент.
Объявление конструктора не специфицирует возвращаемый тип (void тоже не задается). У статического конструктора не может быть никаких других модификаторов.
Создание нового объекта основано на операции new (create в ) и вызове конструктора экземпляра. Вот пример:
D x = new D("ABC");
В C# нет различия между объявлениями (статикой) и операторами (динамикой), позволяя в объявлениях создавать и инициализировать объекты, выполняя операцию new с вызовом конструктора, создающего объект - экземпляр класса D в примере.
Это, однако, далеко не полная история о конструкторах экземпляра и создании экземпляра. Детальная спецификация (дается ниже при обсуждении наследования) объясняет, что ваш вызов конструктора может в результате приводить к вызову других конструкторов, создавая цепочку вызовов, которая должна включать вызов конструктора каждого предка класса.
Статический конструктор, существующий в единственном экземпляре, без аргументов, что отражено в примере, выполняется до того, как потребуется экземпляр класса или доступ к через однократные once-функции). Представьте экземпляр системы, фиксирующей ошибки, где ошибки записываются в специальный журнал (файл). Первое появление ошибки приводит к созданию и открытию этого файла.
В C# предполагается существование сборщика мусора: в то время как создание объектов выполняется явно при задании операции new, освобождение памяти для неиспользуемых более объектов возлагается на автоматический механизм - сборщик мусора (). Иногда можно попросить GC выполнить специфическую операцию, помимо освобождения памяти. Типичным примером является работа с файлами. Всякий раз, когда закончена работа с объектом, представляющим файл, требуется закрыть физический файл, связанный с объектом (в можно для этих целей вызывать процедуру dispose, которую GC будет выполнять при освобождении объекта).
Деструкторы C# отвечают этим потребностям. Имя деструктора ~C, где C - имя класса. Здесь нет перегрузки, так как деструктор существует, если он задан, в единственном экземпляре. Он не имеет аргументов, не возвращает значения, не имеет модификаторов, таких как static:
class File {
… Другие компоненты, включая конструкторы …
~File() {
… Операторы, закрывающие, например, физический файл …
}
}
Операция, определяемая программистом, - это статический метод, где знак операции выступает в качестве имени метода, как в примере:
class E {
public static E operator +(E a, E b) {
… Вычисление результата exp, возвращаемого в операторе return …
return exp;
}
}
Операция может быть вызвана в инфиксном синтаксисе, свойственном операциям в математике, как x + y, где x и y типа E (это аналогично alias-компоненту ). Операции встроены в int и float. Имена (знаки) операций и их приоритеты фиксированы (в отличие от , нельзя определять собственные знаки операций). Главными доступными знаками операций являются:
+ - ~ ! ++
Здесь "!" - отрицание для булевских, "~" - отрицание для целых (взаимное обращение 0 и 1 в бинарном представлении числа). Операции "++" и " " имеют побочный эффект: x++ возвращает значение x, затем увеличивает x на 1; ++x также увеличивает x, возвращая увеличенное значение; (аналогичная семантика у операций x- и ). Как вы знаете, операции с побочным эффектом - не очень хорошая идея, используйте их на собственный страх и риск.
Бинарными операциями являются:
+ - _ / % ^ // Арифметика (% - остаток от деления нацело) < > <= >= // Операции отношения (дают булевский результат) == != // Эквивалентность (равно и не равно) ^ | // Булевские строгие (and, xor, or) << >> // Побитовый сдвиг (влево, вправо) || // Булевские полустрогие (and, or)
Как и в других языках, наследуемых от C, знак равенства означает присваивание, а эквивалентность задается двумя знаками равенства. Некоторые из приведенных выше операций применяются к побитовому представлению целых. Строгие булевские операции применимы не только к булевским значениям, но и к целым, применяя операцию к каждой паре соответствующих битов. m << n эквивалентен умножению m на $$2^n$$, сдвиг вправо - делению.
Можно использовать перечисленные знаки для определения операций в собственных <), то необходимо определить и "больше" (> ). Аналогично и для других операций сравнения.
Кроме того, C# поддерживает следующие неперегружаемые операции:
[…] // Получение элемента массива (…) // Кастинг += -= _= /= %= ^= // Присваивание = |= <<= >>= // Присваивание
Присваивание x += 1 это сокращенная форма записи x = x + 1. Аналогичный смысл и у других операций присваивания с операциями.
Для объявления массива с одной или более размерностью используется нотация с квадратными скобками:
string[] a; // Одномерный массив string[,] b; // Двумерный массив
Для задания многомерных массивов используются запятые, как показано в примере. Число запятых, увеличенное на 1, определяет размерность массива. Нижняя граница индекса по каждому измерению фиксирована и равна 0. Поэтому элемент b [1, 1] задает элемент во второй строке и втором столбце. Такая политика требует внимательности при работе с индексами.
В объявление ). Присваивание:
a = new string[4];
создаст массив из четырех элементов, инициализируемых стандартными значениями по умолчанию. Разрешается инициализировать массив значениями в момент его создания:
a = new string [] {"A", "B", "C", "D"}; //Массив из четырех элементов
При инициализации многомерных массивов используются запятые, разделяющие списки, которые заключены в фигурные скобки:
b = new string[2, 3];
b = new string[,] {{"A", "B"}, {"C", "D"}, {"E", "F"}}; // Размерность [3,2]
Кроме прямоугольных массивов, C# предлагает изрезанные, гребенчатые массивы, которые являются массивами массивов, как в примере:
string[][] c;
Каждая строка может иметь различный размер (отсюда и название - изрезанность, гребенка). Вот пример типичной инициализации:
c = new string[][] {new string[] {"A"},
new string[] {"B", "C", "D"},
new string[] {"E", "F"}};
Число элементов в каждой строке соответственно - 1, 3, 2.
Доступ и модификация использует синтаксис с квадратными скобками:
b[0, 0] = "Z";
c [0] = new string[] {"Y", "Z"}; // Изменяет первую строку с индексом 0
c [0][0] = "Z";
Можно определить нотацию со скобками для доступа к структурам, отличным от массивов, как в следующем примере:
Table t = new Table (); string n; … n = t [1, 1]; // Доступ к первому элементу (смотри реализацию ниже)
Здесь Table - собственный класс, поставляемый с индексатором, выполняющим ту же роль, что и псевдоним (alias) " []" - "квадратные скобки" в . Определение индексатора является обобщением метода-свойства:
class Table {
private string[ , ] rep;// Инициализация rep опущена
public string this [int i, int j] {
get {return rep [i - 1, j - 1];}
set {rep [i - 1, j - 1] = value;}
}
}
Имя индексатора фиксировано и совпадает с именем текущего объекта this, как следствие, у класса может быть только один индексатор. Индексатор определяет два метода - геттер и сеттер с аргументами, задающими индексы элемента контейнера, к которому осуществляется доступ. В реализации класса Table индексация идет по двумерному массиву rep и устроена так, что для клиента начальный индекс по обоим измерениям начинается с единицы.
Концепция универсальности C# знакома по ; родовые параметры заключаются в угловые скобки <…>. Объявим:
class F<G, H> where H: T, new() {
… Объявление класса …
}
Класс F имеет два родовых параметра. Для H задано ограничение (как в классе C [G, H -> T] в ), так что любой фактический родовой параметр должен наследовать от T. Включение в ограничение new() означает (как в , если объявление имеет вид C [G, H -> T create make end] для процедуры создания make из T), что T должен обеспечить public-кон-структор без аргументов; это позволит методам F создавать экземпляры T.
Родовое порождение также использует угловые скобки:
F<V, W> //W должен быть согласован с T и иметь конструктор без аргументов.
Дадим обзор основных операторов языка.
Присваивание, уже появляющееся в примерах, использует знак равенства:
var = e;
Заметьте: точка с запятой является не разделителем операторов, а завершителем, и должна завершать любой оператор.
Вызов метода использует имя метода и список аргументов. В отличие от вызова в , круглые скобки всегда сопровождают вызов метода, даже если список аргументов пуст, как в methodWithNoArgument ().
Оператор return не имеет прямого аналога в . Он обязателен для функций, задавая значение, возвращаемое функцией:
return some_expression;
Для процедур оператор возможен и задает завершение процедуры. Поскольку в методах он может появляться в нескольких местах, это означает, что методы C# не соответствуют правилу "один выход".
Условный оператор удовлетворяет следующему синтаксису:
if (c1) {
…
} else if (c2) {
…
} else {
…
}
Множественный выбор имеет форму:
switch (expression) {
case value: statement; break;
case value: statement; break;
…
default: statement; break
}
Здесь expression задается булевским или целочисленным выражением, а каждое value представляет вычислимую в период компиляции константу. Когда значение выражения не совпадает ни с одной константой, выполняется ветвь default, если она задана, в противном случае ничего не делается (в в отсутствие ветви else в операторе в подобной ситуации в период выполнения генерируется ошибка). Оператор switch не задает конструкцию с одним входом и одним выходом, а представляет многоцелевой goto. Для правильной структурированности следует четко следовать показанной схеме. Принудительный оператор break, завершающий каждую ветвь, позволяет избежать типичной ошибки для C++ и C-версии switch, когда управление проваливается в другую ветвь.
Доступно несколько форм оператора цикла. Наиболее общая идет от С и соотносится с конструкцией from в :
for (initialization; exit; modification) {
… body …
}
Вначале выполняется инициализация цикла - initialization - и работа цикла заканчивается, если условие выхода из цикла - exit - получает в результате значение true. Обычно оно не выполняется после инициализации, и тогда выполняется тело цикла, а затем модификация параметров цикла - , после чего снова проверяется условие выхода. Цикл завершается, когда выполняется условие выхода. Модификация обеспечивает продвижение к следующему шагу, увеличивая индекс, продвигая курсор (в она интегрирована с телом цикла).
Можно использовать циклы в стиле while или until:
while (condition) {statements}
do {statements} while (condition)
Удивительно, C# сохранил оператор goto M, где M - метка. Операторы можно снабжать метками, отделяя метку от оператора двоеточием.
Исключение - это событие периода выполнения программы, появление которого приводит к прерыванию нормального выполнения программы. Причины исключений могут быть разные, такие как деление целого на нуль, отсутствие файла, null-ссылка у цели, вызывающей метод. Возможно также, что исключение "выбрасывается" при выполнении специального оператора языка C#:
throw e;
Здесь e - Exception.
Обработка исключений в C# выполняется в следующем стиле:
try {
… Обычные операторы, во время выполнения которых может возникнуть
… исключение …
} catch (ET1 e) {
… Обработка исключения типа ET1, детали в объекте e.…
} catch (ET2 e) {
Обработка исключения типа ET2, детали в объекте e.…
}… Возможно, другие случаи …
finally {
… Выполняется во всех случаях, было исключение или нет.
}
Если в охраняемом try-блоке включается исключение одного из перечисленных типов ET1, ET2, …, то выполнение в try-блоке прерывается и управление передается соответствующему catch-блоку, который в состоянии захватить исключение данного типа. Блок finally выполняется всегда, если присутствует. Его обычная цель - освобождение ресурсов, закрытие файлов и так далее.
Появление исключения создает объект исключения, доступный программе в соответствующем catch-блоке. Это дает возможность при обработке исключения использовать свойства объекта, такие как имя исключения, понятное человеку, состояние стека вызовов.
Если встретилось исключение, чей тип не совпадает с типами, перечисленными в catch-блоках, или исключение встретилось вне охраняемого try-блока, то оно передается вверх по цепочке вызовов - в вызывающий метод. Здесь, опять-таки рекурсивно, исключение может быть обработано, если предусмотрен catch-блок, или передано наверх. При завершении цепочки вызовов, если обработка исключения не выполнена, то программа завершается стандартным сообщением об ошибке - обрабатывается стандартным catch-обработчиком исключения.
Читатель, знакомый с Java, заметил, что приведенное описание применимо к обоим языкам (получившим этот механизм от С++). Есть отличие от модели Java - метод в C# не специфицирует типы выбрасываемых исключений, как это делает Java, используя спецификацию throws.
В C# предлагается ) для описания методов как объектов. Ассоциированный механизм - события - дополняет делегаты для программирования, управляемого событиями (в типы событий описываются как обычные объекты и, следовательно, нет необходимости в специальных конструкциях). Рассмотрим объявление делегата:
public delegate int DT (string s);
Это объявление типа: оно определяет тип D T, который представляет функции с одним строковым аргументом, возвращающие целое в качестве результата. Для определения экземпляра класса и связывания его с конкретным методом - скажем, int lettercount(string s) - функцией, подсчитывающей число буквенных символов, - можно использовать:
DT d = new DT(lettercount)
Можно обойтись без вызова конструктора, используя явное присваивание:
DT d = lettercount;
Некоторые языки программирования (отличные от функциональных) не позволяют использовать метод как аргумент другого метода. Механизмы агентов, делегатов, указателей функций в С++ спроектированы как раз с целью создания специальных объектов, передаваемых как аргументы и представляющих обертки соответствующих функций. Для создания в C# делегата из метода необходимо передать метод конструктору, как в [5]. Концептуально это единственный случай, допускающий использование имени метода как значения.
На практике C# ослабляет правило, допуская присваивания, такие как [6], или передавая имя метода как аргумент другому методу, но это синтаксический сахар, фактически передаваемое значение является делегатом, здесь new DT ( ) .
Делегат можно вызывать подобно любому другому методу:
n = d ("A");
После присваивания [5] или [6] эффект будет тот же, как и при непосредственном вызове n = lettercount ("A") ;. Конечно, при вызове [7] обычно неизвестно, какую именно функцию представляет d. Часто такой вызов осуществляется в методе r, для которого d -
r (lettercount);
Эквивалент комбинации [4] или [5] задается одним оператором d:= agent lettercount, не требующим объявления типа, такого как [4]. Эта форма не имеет прямого эквивалента в C#, но можно использовать делегаты с "анонимными методами" (эквивалент встроенных агентов), как в примере:
r (delegate (string s) {return lettercount (s);} );
Здесь используется анонимный метод. Заметьте, что анонимный метод объявляет сигнатуру, но не объявляет тип результата. Хотя в C# нет прямого эквивалента -понятия "открытый аргумент", но анонимные методы позволяют достичь того же результата.
Эквивалентом встроенного агента является лямбда-выражение C#, как в
(int x, int y) => x + y
соответствующее математической записи: ?x, y: INTEGER | x + y.
В C# делегат не ограничен представлением ровно одного метода. Если a и b - делегаты одного типа, то a + b обозначает делегата того же типа. Его выполнение приводит к последовательному выполнению методов, связанных с a и b. Операции add и remove позволяют в список методов, связанных с делегатом, добавлять или удалять новые члены. Можно для этого использовать и операцию присваивания += и -=.
Для программирования, управляемого событиями, можно определить типы событий и связать их с делегатами, как в следующем объявлении:
public event DT1 click;
Здесь click определяется как событие, которое будет обрабатываться делегатом типа DT1.
Этот пример использует новый тип делегата DT1 вместо рассматриваемого ранее D T, так как типы делегатов, обрабатывающих события, обычно являются процедурами, в то время как DT задает класс функций. Если мы собираемся рассматривать щелчки кнопки мыши, то надо передать событию аргументы, задающие координаты мыши, тогда DT1 будет объявлен как:
public delegate void DT1 (int x, int y);
Для понимания механизма необходимо знать, что реализация C# представляет каждый тип события, такой как click, как список делегатов, которые соответствуют различным методам, подписанным на события. Это объясняет, как подписаться на события:
click += r;
Здесь r - это метод с подходящей сигнатурой: void r (int x, int y) . Операция += перегружена для списков; она добавляет элемент в список. Заметьте, что можно применить метод r непосредственно, прежде чем явно обернуть его в одежды делегата. Но то, что получается при добавлении в список, является делегатом. Для удаления подписки используется конструкция -=.
Так выполняется подписка. Для публикации события используется схема:
if (click != null) {click (h, v);}
В этом примере h и v задают координаты мыши. Опять-таки, нужно знать о реализации списка, чтобы понимать необходимость теста click != null: если нет делегатов, подписанных на click, то список будет иметь значение null, и вызов click (h, v) станет причиной исключительной ситуации.
Рекомендуемый стиль для обработки аргументов события в .NET состоит в том, чтобы аргумент события был объявлен как потомок библиотечного класса EventArgs. В этом примере можно было бы объявить класс IntPairArgs, наследуемый от EventArgs, и в этом классе объявить два целочисленных поля x и y. Тогда подписываемые методы имели бы форму:
private void rHandler (object sender, IntPairArgs e) {r (e.x, e.y);}
Здесь - как часть того же рекомендуемого стиля - первый аргумент sender представляет целевой объект, второй аргумент e представляет аргументы события. Преимущество в том, что все схемы обработки события выглядят одинаково. Неясно, однако, оправдывает ли это возникающие усложнения: вместо прямого повторного использования методов, существующих в модели, таких как r, необходимо обернуть их в специальный склеивающий код, такой как rHandler.
Модель наследования C#, совпадающая в основном с моделью Java, не поддерживает множественного наследования классов, за исключением специального вида абстракции, называемого интерфейсом.
Вот как класс объявляет другой класс своим родителем:
class L :K
{… Объявления компонентов L …}
Класс L объявил себя наследником K. Следующий пример показывает случай, когда наследуемый класс объявляется универсальным, здесь - с ограниченной универсальностью:
class M<G>:K where G: T
{… Объявления компонентов M …}
Только один класс, здесь - K, может быть объявлен в качестве родителя.
В C# можно объявлять абстрактными как отдельные методы, так и класс в целом (это соответствует отложенным методам и классам , но без контрактов):
abstract class N {
public abstract void r(); // Заметьте, реализация отсутствует
public abstract int s(); // Заметьте, реализация отсутствует {…}
… Другие методы, которые могут быть или не быть абстрактными…
}
Нельзя использовать конструктор для создания экземпляров абстрактного класса (как new N (…)).
Методы-свойства и индексаторы также могут быть абстрактными.
Интерфейс подобен полностью абстрактному классу. Интерфейсы задают абстрактную функциональность, которую каждый потомок должен реализовать. Интерфейсы обеспечивают ограниченную форму множественного наследования, так как каждый класс может наследовать от множества интерфейсов. Это позволяет языку избегать конфликтов, когда несколько наследуемых методов от разных родителей с одинаковыми именами имеют разные реализации (мы знаем, что конфликты разрешимы за счет переименования). Платой является некоторая потеря мощности наследования. Пример типичного интерфейса:
interface IOrdered <T> {
bool lesser (T other);
bool greater (T other);
bool lesserEqual (T other);
bool greaterEqual (T other);
}
По соглашению имена интерфейсов должны начинаться с буквы I. Пример показывает, что интерфейс может быть универсальным. Важно, что интерфейсы объявляются без указания модификатора доступа, - они неявно имеют статус public.
Данный интерфейс задает свойство сравнимости объектов, используя общепринятые имена операций сравнения. Он также иллюстрирует границы понятия интерфейса - поскольку методы не могут иметь реализации, нельзя задать, что a.greater (b) должно быть реализовано как b.lesser (a) (для абстрактного класса такое возможно, но только один родитель может быть классом, не имеет значения - абстрактным или эффективным).
Класс может наследовать от одного или нескольких интерфейсов, обеспечивая реализации методов:
class TennisPlayer: IOrdered<TennisPlayer>, IAnotherInterface {
public int ranking;
public bool lesser (TennisPlayer other) {return (ranking < other.ranking);}
… Аналогично для greater, lesserEqual, greaterEqual …
… Реализация методов IAnotherInterfacei …
… Другие компоненты TennisPlayer …
}
Множественное наследование интерфейсов не исключает конфликта имен. В C# нет явного механизма переименования, аналогичного , но конфликта можно избежать, используя в качестве префикса имя интерфейса и нотацию с точкой, например, IAnotherInterface.clashingname.
В связи с наследованием в дополнение к трем ранее рассмотренным модификаторам доступа (public, internal, private) добавляется новый модификатор и новая комбинация:
protected: доступно для потомков;protected internal: доступно потомкам и классам той же сборки.Когда переопределяется наследуемый компонент, как обсуждается далее, статус доступа изменить нельзя. Этим язык отличается от Java, где разрешается расширить права доступа, но не ограничить их. Кроме того, потомки не могут иметь доступность большую, чем их предки.
Наследуемый метод можно переопределить. Соглашения, однако, отличаются от других современных языков программирования. Как в С++, и в отличие от и Java, связывание в C# - удивительно для языка, первая версия которого появилась в 1999 году, - статическое. Другими словами, версия f, выполняемая при вызове a.f, будет следовать объявлению a, но не virtual:
class P {
public virtual void f (…) {…}
…
}
Потомок переопределяет виртуальный метод, в точности сохраняя сигнатуру и заменяя модификатор virtual на override:
class Q: P {
public override void f (…) {…}
…
}
Оригинальная версия переопределяемого метода остается доступной, как с Precursor в , для этого применяется нотация с точкой, а в качестве префикса используется имя родителя - base. Например, реализация f в Q может пользоваться результатами работы, проделанной родителем:
base.f (n);
Для реализации динамического связывания требуется точное соответствие схеме (оригинал метода специфицирован как virtual, новый - как override1). Если условие не выполняется, то компилятор по-другому интерпретирует текст. Рассмотрим следующий вариант:
class R {public void f (int i) {…}}
class S: R {public void f (int i) {…}}
R r1 ; S s1 = new S() ; int n;
r1 = s1;
Оригинал f не объявлен virtual в R, но все же потомок S дал новую реализацию. Это не является base). В этой ситуации применяется статическое связывание. Вызов s1.f(n) будет использовать новую версию, но вызов r1.f(n) будет использовать версию R независимо от того, с каким объектом связано будет r1 во время выполнения. Это довольно опасно, хотя компилятор отслеживает ситуацию и выдает предупреждающие сообщения. Для защиты от риска ошибки новый метод следует помечать как new:
class S: R {public new void f (int i) {…}
Динамическое связывание не встретится и в случае вызова p1.f (n) , где p1 типа P, и динамически типа Q, если в Q опустить при переопределении модификатор override. Это не будет восприниматься как ошибка, просто означает применение
При переопределении можно задать модификатор sealed, чтобы защитить метод от переопределения у потомков:
class Q1: P {
public sealed override void f (…) {…}
…
}
Этот модификатор допускается только для override-методов, поскольку остальные по определению не являются переопределяемыми. Модификатор sealed может быть у класса (sealed class T …), и тогда такой класс не может иметь потомков.
Объявление классов и методов с модификатором sealed характерно для .NET-библи-отек, вероятно, по той причине, что в условиях отсутствия контрактов это единственный способ не позволить потомкам исказить намерения родителя.
Выбор в языке C# по умолчанию C#-программистов: всегда объявлять методы virtual, удаляя этот модификатор в тех редких случаях, когда метод должен быть защищён от переопределения - sealed.
В C# в присутствии наследования действует специальное правило для конструкторов: любой конструктор должен вызвать конструктор родителя. Как результат, вызов конструктора приводит к цепочке вызовов, включающей вызовы всех родительских конструкторов и заканчивающейся вызовом конструктора по умолчанию прародителя - object. Этот эффект может достигаться явно или неявно.
base (как base (n); ). Так как конструкторы не имеют индивидуального имени, сигнатура аргументов, благодаря правилам перегрузки, однозначно определяет, какой конструктор будет вызван.Один из этих случаев должен быть применим. Если в конструкторе нет явного вызова конструктора родителя и родитель при этом не имеет конструктора по умолчанию, то возникнет ошибка периода компиляции.
Как отмечалось при обсуждении соответствующих механизмов Java, причина этих правил не полностью ясна. Намерением, вероятно, было убедиться, что экземпляр потомка удовлетворяет правилам согласованности, определенными предками. Цепь конструкторов появляется как попытка убедиться в такой согласованности. В отсутствие понятия инварианта класса она позволяет явно выразить ограничения.
Для U выражения exp типа T (как в тесте объектов ) можно использовать два механизма.
(U) exp, не учитывая объявленный тип exp. Если все хорошо и exp в самом деле представляет объект типа U в момент выполнения, то можно использовать это выражение для ссылки на значение приведенного типа. Обратная сторона в том, что если U, попытка вычисления выражения приведет к возникновению исключительной ситуации, обработку которой надо предусмотреть для безопасного программирования.Более приемлемым способом является использование специальной C#-конструкции - exp is U. Во втором случае exp по-прежнему статически принадлежит объявленному типу T, но теперь можно комбинировать два механизма с гарантией, что кастинг будет работать:
if (exp is U) {r ((U) exp)}; // Метод r объявлен как: r(U : x) {…}
В C# введены несколько механизмов ОО-парадигму.
При конструировании ПО, приходящего из разных источников, возможны конфликты имен используемых классов: два провайдера могут поставлять классы с одинаковыми именами. В C# такие конфликты могут разрешаться благодаря введению уровня структуризации более высокого, чем классы, - пространства имен.
По умолчанию имена всех типов (классов) принадлежат глобальному пространству имён. Разрешается определять собственное пространство имен, содержащее объявления классов:
namespace N1 {
… Объявления классов (и других типов по желанию) …
}
В этом случае клиент, которому необходим доступ к нескольким классам с одним именем, скажем, С, может устранить неопределенность, используя нотацию N1.C.
Важным предопределенным пространством имен является пространство System, содержащее базисные библиотеки классов.
Пространства имен могут быть вложенными. Это достигается как прямой вложенностью текста одного пространства в другое, так и с использованием нотации с точкой:
namespace N1.N2 {// N2 - подпространство N1: элементы доступны в нотации
// N1.N2.V.
… Объявления классов, включая V …
}
Клиент, часто использующий классы из некоторого пространства имен, может избежать повторения квалифицированных имен (как в N1.N2.C, N1.N2.D и т. д.) благодаря использованию директивы using:
using N1.N2; using SomeOtherNamespace; … Здесь можно использовать V как сокращенную запись N1.N2.V …
Как показано в примере, можно использовать любое число using-директив. Если соответствующие пространства имен содержат классы с конфликтующими именами, то приходится вернуться к нотации с точкой для разрешения конфликтов.
Предположим, вы хотите расширить концепцию, покрываемую существующим классом X. Обычный ОО-механизм состоит в объявлении нового класса, наследуемого от X. Но наследование может быть неудобным или неприменимым. Например, X может запрещать наследование (как отмечалось, обычная практика для .NET-библиотек). Вообще, определение нового класса означает определение нового типа. Даже если вы только добавляете методы, не добавляя полей, система типов не позволит применять новые операции к существующим объектам исходного типа X, например, к объектам, хранящимся в файле или в базе данных.
Чтобы справиться с этой проблемой, C# обеспечивает интересный механизм расширения методов: методы добавляются извне к существующему классу.
Очевидно, методы расширения представляют просто синтаксическое упрощение, так как проблему можно решить за счет статических методов: в любой класс можно добавить статический метод , который будет вызываться как с x1 типа X. Однако мы хотим, чтобы метод m вызывался в том же стиле, как если бы он был методом X:
x1.m (other_args);
При этом m объявляется не в классе X, а в другом классе. Синтаксический трюк состоит в том, чтобы маркировать первый аргумент m модификатором this:
public static class Y {
static void m (this X x, int arg1, int arg2) {…}
…
}
В результате в классе X появляется расширенный метод и [9] становится правильной конструкцией (с двумя целочисленными аргументами, заменяющими other_args).
По своей природе языки программирования ограничены семантическими механизмами, предусмотренными их создателями. Иногда возникает желание добавить новые свойства, не изменяющие существующую семантику, но полезные, например, для документирования или сериализации.
Вполне разумно для языка программирования обеспечить возможность таких расширений, известных как включение метаданных, - поддерживающей информации, добавляемой в документ, отделенной от основного содержания. В именно для такой цели используются предложения note, связанные с классами и методами. В .NET и C# поддержка метаданных обеспечивается в форме атрибутов (не путайте с ОО-понятием атрибута, используемым в этой книге, для которого в C# применяется термин "поле").
Некоторые атрибуты предопределены, но программисты могут определять собственные атрибуты, известные как
Примером предопределенного атрибута является атрибут сериализации , который можно присоединить к классу, указав тем самым, что экземпляры могут быть конвертированы в некоторое внешнее представление и храниться во внешней памяти:
[Serializable]
public class Z {… Обычное объявление класса …}
Здесь показано добавление атрибута к классу. То же соглашение действует при связывании атрибута с методом. Атрибут заключается в квадратные скобки и используется в качестве префикса класса или метода.
Для задания пользовательского атрибута следует определить класс - потомок класса System.Attribute (то есть класса Attribute из пространства имен System). Предположим, что мы хотим поставлять классы с базисной версией управляющей информации: именем автора, датой модификации, возможно, номером версии, все в строковом формате. Мы используем:
public class ChangeAttribute: System.Attribute {
private string author;
private string last;
public ChangeAttribute (string a, string l)
{author = a ; last = l;}
public string revision;
}
Заметьте, имя класса заканчивается словом Attribute, - это рекомендуемый стиль именования атрибутных классов.
Тогда можно поставлять класс или (здесь) метод с информацией о версии:
[ChangeAttribute ("Caroline", "24 December 2009")] public void r {…}
При задании атрибута мы должны передать аргументы выбранному конструктору. Также возможно задать значения public полей атрибутного класса, так называемых полей, допускающих запись. В данном примере таким является поле :
[ChangeAttribute ("Caroline", "24 December 2009", revision = "2.1" )]
public void r {…}
Атрибут ChangeAttribute, в том виде как он объявлен, применим к любым программным элементам: class, struct, method, field, delegate и некоторым другим. Можно ограничить применимость атрибута, присоединив собственное объявление у атрибута AttributeUsage:
[System.AttributeUsage(System.AttributeTargets.Class)]
public class ChangeAttribute: System.Attribute {… Остальное, как прежде …}
Вместо задания Class (в качестве области действия атрибута) можно использовать другие ключевые слова: All (по умолчанию), Assembly, Delegate, Event, Interface, Field, . Разрешается задавать несколько целей, разделяя их символом вертикальной черты |.
Для элементов, снабженных атрибутами, можно получать значения атрибутов через процесс, называемый отражением. Рассмотрим вызов объекта о:
o.GetType().GetCustomAttributes(true);
Результатом будет массив, содержащий атрибуты, определенные для класса объекта, с их значениями.
В C# отсутствуют несколько ОО-механизмов, интенсивно применяемых в этой книге, в первую очередь - контракты и множественное наследование.
Контракты введены в исследовательскую версию языка C# - SpecC#, разработанную в Microsoft Research. Эта версия языка доступна для свободного использования.
Полезно ознакомиться с несколькими поддерживающими C# конструкциями.
В C# комбинируются строгие требования к безопасности типов с предоставлением программистам возможности работы на низком уровне, характерном для языка С или ассемблера. Конструкция "" поддерживает четкое разделение между небезопасными элементами и нормальными, прошедшими проверку типов.
Объявление метода небезопасным () означает, что область данных расположена вне "кучи" - области, отводимой под объекты. Небезопасный метод может осуществлять в своей области непосредственные манипуляции с указателями и, следовательно, игнорировать нормальные правила работы с данными.
Типы, заданные перечислением, позволяют оперировать значениями из конечного множества предопределенных значений:
enum CardColors {Spades, Hearts, Diamonds, Clubs}
Значения, заданные перечислением, проецируются на целочисленный тип, по умолчанию int, но можно задать и другой тип, как в enum T:long {…}. Значения проецируются на отрезок, начинающийся с 0, но можно задать и другое начало:
enum CardColors1 {Spades = 1, Hearts, Diamonds, Clubs}
Значения можно обозначать, используя нотацию с точкой, как в CardColors.Spades. Допустимы взаимные преобразования между целыми и значениями
В версии языка C# 3.0 появились новые важные механизмы, привлекшие внимание программистов. Механизм, известный как Linq, позволяет языку программирования работать непосредственно с базами данных и Web. Для работы с данными используется типичный язык запросов SQL и XML для Web. Обычно такие возможности поддерживаются с помощью специальных библиотек, обеспечивающих интерфейс к реляционным базам данных или к протоколу HTTP. Оригинальность Linq в том, что он делает все гораздо выразительнее, оставаясь в рамках языка программирования. Например, запрос к базе данных можно выполнить так:
from e in Employees where e.salary > median select e.rank
Здесь идет ссылка на отношение Employees из базы данных. В результате создается спиок всех служащих, ранжированных по зарплате. Стиль тот же, что и у SQL-запросов, но запрос интегрирован в язык. Все используемые объекты - это нормальные объекты программы, такие как списки.
В C# идентификаторы следуют соглашениям, подобным . Они могут, однако, начинаться с подчеркивания, хотя эта возможность редко используется в обычных приложениях. Важная разница в том, что идентификаторы чувствительны к регистру: anIdentifier, AnIdentifier и anidentifier - это все различные идентификаторы. Лежащее в основе множество символов - это Unicode.
Комментарии в C# программах могут быть:
//;/* …*/.Однострочные комментарии, начинающиеся с /// (с добавленным слеш-символом), задают документированный комментарий, записываемый в виде XML-кода. Эти комментарии используются специальным инструментарием для разных целей - документирования, интеллектуальной подсказки.
Judith Bishop, Nigel Horspool, C# Concisely, Addison-Wesley, 2003. Введение в основные механизмы C# (для ранних версий языка).
Онлайн документация на сайте:http://msdn.microsoft.com/en-us/vcsharp/default.aspx
Лучшее место для получения детальной спецификации механизмов языка. Сайт также включает ссылки на несколько учебников.
На русском языке: Биллиг В.А. "Основы объектного программирования на C# 3.0", Интернет-Университет. Питер, Лаборатория знаний,
При появлении языка C# в 1999 Microsoft представлял язык следующим образом.
Идеальное решение для C и C++ программистов, обеспечивающее быструю разработку в сочетании с мощью доступа ко всей функциональности платформы. Программисты получают окружение, полностью синхронизированное с появляющимися Web-стандартами и обеспечивающее простую интеграцию с существующими приложениями. В дополнение появляется возможность при необходимости создавать код на низком уровне.
Язык C# - это современный OO-язык, позволяющий программистам быстро строить широкий круг приложений для новой .NET платформы, предоставляющей полный набор инструментария и сервисов, которые необходимы как для вычислений, так и для коммуникаций.
Элегантно спроектированный OO-язык C# является великолепным выбором при построении архитектуры компонентов - начиная от высокоуровневых C#, эти компоненты могут быть конвертированы в XML Web-сервисы, вызваны затем в Интернете из любого языка, выполняемого на любой операционной системе.
(рис 12.1) Андерс Хейлсберг (разработчик C#), 2007
Большинство читателей этой книги предпочитают нормальный русский язык - поэтому стоит дать перевод этого пышного представления: "C# - это Java плюс делегаты (объекты в духе агентов) и несколько низкоуровневых механизмов, заимствованных от C++". C# был ответом Microsoft в конкурентной борьбе с компаниями, поддерживающими Java, в частности Sun Microsystems и IBM. Язык чрезвычайно близок к Java.
Эта характеристика остается во многом справедливой и сегодня, хотя C# эволюционировал своим собственным путем и ввел несколько интересных инноваций, не имеющих аналогов в Java. На момент написания этого текста C# (версия 3.0) является мощным языком, и здесь мы рассмотрим только его основы .
Для изучения C# знание Java полезно, но не требуется. Это приложение не предполагает, что вы прочли описание Java, приведенное в предыдущем приложении (как следствие, повторяются некоторые рассуждения, когда рассматриваются разделяемые концепции). Подобно другим приложениям, язык не рассматривается с чистого листа, - обсуждение предполагает знакомство с программистскими концепциями, введенными в этой книге. Описание языка сопровождается сравнением с соответствующими механизмами .
C# (произносится "C шарп") тесно связан с окружением Microsoft .NET, платформой для разработки и выполнения ПО, использующей виртуальную машину. В предыдущих обсуждениях отмечалась роль виртуальных машин и их преимущества для реализации языков высокого уровня.
В то время как виртуальная машина Java - - была спроектирована специально для поддержки этого языка (хотя позднее она использовалась для реализации других языков программирования), главная цель проекта платформы .NET состояла с самого начала в поддержке нескольких языков. Это решение отражается как в имени виртуальной машины - Common Language Runtime (CLR) - "Общеязыковая Среда Выполнения", так и в поддержке взаимодействия API, - "Общеязыковой Инфраструктуры", которая теперь является международным стандартом.
Частично причина была в том, что Microsoft еще до .NET обеспечил реализацию нескольких языков, прежде всего, Visual Basic (VB), C++ и JScript для клиентских Web-приложений. VB популярен на массовом рынке и часто используется для разработки приложений по настройке офисных документов. Компания не могла, естественно, предложить соответствующим программистским сообществам бросить свои любимые языки и перейти на новый бренд. Она смогла обеспечить общую базу для взаимодействия и будущей эволюции. .NET и CLR/CLI были способны с самого начала обеспечить реализацию четырех поддерживаемых Microsoft языков (помимо трех упомянутых еще и C#), а также языки, разрабатываемые другими компаниями, включая (с самого начала введения .NET в 1999) и Cobol, язык прошлых лет, но все еще важный для многих бизнес-приложений.
Языковая CLI представляет объектную модель, близкую ОО-языку, но без синтаксиса. Эта модель задает множество хорошо определенных механизмов - это ОО механизмы, изучаемые в этой книге: классы, их компоненты, наследование, универсальность, система типов, объекты, политика динамического создания объектов и сборка мусора - для которых CLI-проект предложил несколько специфических проектных решений.
При условии, что .NET-языки не слишком отклоняются от этих решений, они могут достичь степени взаимодействия, неслыханной в дни, предшествующие .NET. В частности, классы, написанные на разных языках, могут взаимодействовать друг с другом, используя как -класс может наследовать от класса C#, возможно и обратное наследование. Это просто предполагает, что компиляторы следуют единым CLI-правилам для поставщиков и клиентов сборок (целевых модулей, создаваемых .NET-компиляторами).
Эта схема взаимодействия доказала свою успешность (несмотря на существующую тенденцию производителей ПО игнорировать правила CLI-совместимости). Она позволяет каждому языку сохранять свою индивидуальность, пока ее можно отобразить в объектную модель CLI. Например, реализация должна моделировать множественное наследование - не поддерживаемое напрямую CLI - через специальное использование CLI-механизмов (множественное наследование интерфейсов, концепцию, обсуждаемую позже в этом приложении).
В сообществе языков, как и у людей, все языки равны, но некоторые равны более других. Язык C# - это любимый сын: его объектная модель наиболее тесно связана с CLI. VB .NET, который схож с предыдущей версией только синтаксически, является еще одним претендентом на звание "любимого". CLI-совместимая версия C++ - "управляемый C++" - существенно отличается от обычного C++. Ограничения необходимы, чтобы язык мог принимать участие в играх .NET-взаимодействия. Фактически, семантика C# определялась семантикой CLI, хотя последующие версии ее существенно расширили. Синтаксис языка соответствует традиции C, C++, Java, включая завершение операторов символом точки с запятой и применением фигурных скобок для окаймления блоков программы.
Базисными элементами C# программы являются классы и структуры, организованные в виде нескольких программных файлов.
C#-классы (ключевое слово class) и структуры (ключевое слово struct) задают описание множества возможных объектов периода выполнения. Объекты обладают свойствами, и к ним применимы методы. Общая форма объявления такова:
class name {
… Объявление компонентов …
}
При объявлении структуры вместо ключевого слова class используется слово struct. Компоненты могут быть разные. Это может быть:
Eiffel ;Структура является упрощенной формой класса без возможности наследования. Остальная часть обсуждения фокусируется на классах, но большинство свойств, не связанных с наследованием, применимо и к
Классы и структуры группируются в сборки (понятие, соответствующее кластеру в
Каждая выполняемая программа должна иметь по меньшей мере один метод, называемый 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 в ), в C# имеет имя this, которое, как и в , можно опускать, когда из контекста ясно, что речь идет о текущем объекте.
В C# разрешается объявлять статические члены, не требующие объекта и вызываемые как C.member, где C - имя класса. Определение статического компонента, например статического метода, может использовать только статические компоненты.
Класс в целом может также быть объявленным как статический, если все его компоненты статические. Тогда невозможно создать экземпляры этого класса. Статический класс мoжет быть удобен, например, для группирования множества объектно-независимых общих свойств, таких как математические функции.
Уже упоминалось, что метод Main должен быть статическим; причина в том, что на старте выполнения не существует объекта, который мог бы вызвать метод. В проблема решается за счет того, что выполнение определяется как создание "корневого объекта", к которому применяется "корневая
Для скрытия информации каждый тип и компонент имеет уровень доступности, определяя права клиентов на доступ. Цель та же, что и в Eiifel: механизм скрытия информации, включая C# нельзя создать список ВИП-персон - классов, которым будет доступен некий компонент класса. Тремя возможными квалификаторами являются:
public: доступен в любом коде;internal: доступен в коде той же сборки;private: доступен коду самого класса или структуры. Это статус по умолчанию для компонентов Применимы некоторые ограничения: класс может быть только internal (по умолчанию) или public, если он не является внутренним классом (классом, объявленным внутри другого класса), который может быть также и private. Деструкторы не могут иметь модификаторов доступа. Операции, определенные программистом, должны быть static и public. Доступность компонентов не может превосходить доступность класса. Не трудно видеть смысл, стоящий за каждым из этих C# является то, что компоненты интерфейсов объявляются без указания
C#-поля соответствуют атрибутам. В этой книге (вне приложения) используется другая терминология: под полем (динамическим понятием) понимается составляющая объекта, соответствующая компоненту генерирующего класса - атрибуту (статическое понятие). В C# один термин применяется для обоих понятий.
При объявлении поля задается его тип (перед именем поля, как в T f, вместо f: T в ). Объявление может включать инициализацию поля, используя для этого символ присваивания =, после которого может идти константное выражение, вычислимое в момент компиляции и не содержащее других полей, отличных от констант и
class A {
public string s1 = "ABC";
public readonly string s2 = "DEF";
… Other member declarations …
}
Заметьте: точка с запятой завершает все объявления и операторы. Квалификатор readonly защищает поля от присваивания, за исключением инициализации в момент объявления, как здесь, или в конструкторе.
В отличие от , экспорт полей без статуса readonly дает клиентам право на чтение и запись. Для приложений, признающих преимущества скрытия информации, это означает, что поля должны иметь статус по умолчанию private и при необходимости снабжаться специальными методами доступа - геттером и сеттером. C# упрощает их написание, введя понятия метода - свойства, изучаемого ниже.
C# обеспечивает несколько встроенных типов:
bool, представляющий булевские значения;char, представляющий 16-бит Unicode-символы. Константа char записывается в одиночных кавычках, как 'A';string, представляющий последовательность из нуля или более символов char. Строковая константа записывается в двойных кавычках, как "ABC";sbyte (знаковый 8-бит), byte (беззнаковый 8-бит), short (знаковый 16-бит), ushort (беззнаковый 16-бит), int (знаковый 32-бит), uint (беззнаковый 32-бит), long (знаковый 64-бит), ulong (беззнаковый 64-бит);float, double и decimal, представляющие 32-бит, 64-бит и 128-бит IEEE-числа с плавающей точкой.Тип object является предком всех типов (аналог ANY в ). Ссылка null (void) записывается как null.
Каждый C# тип является ссылочным или . Переменная 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";} // Статическая функция
}
По умолчанию аргументы передаются "по значению" (как в ), в этом случае 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, нарушая все принципы скрытия информации. ОО-решение в этом случае обеспечивает сеттер-процедура и геттер-функция (в которой нет необходимости в , так как при экспорте гарантируется статус "только для чтения"). В 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"; // Использование сеттера
Эффект подобен тому, что достигается в
Процедуры создания, используемые для создания и инициализации объектов, называются в C# конструкторами. Конструктор может быть:
Следующий класс содержит пример каждого типа:
class D {
public D(string a) { // Конструктор 1: экземпляра
… Инициализирует поле, обычно использует аргумент a …
}
static D() { // Конструктор 2: статический
… Инициализирует статические поля …
}
}
Конструкторы не имеют собственных имен, используя имя класса, основываясь на перегрузке и разрешая конфликты через отличия в сигнатурах, если есть более одного конструктора.
Иногда возникают проблемы, например, классу POINT, описывающему точку на плоскости, полезно иметь два конструктора make_cartesian и make_polar, задающих координаты точки в декартовой и float. Для разрешения конфликта одному из конструкторов приходится добавлять фиктивный аргумент.
Объявление конструктора не специфицирует возвращаемый тип (void тоже не задается). У статического конструктора не может быть никаких других модификаторов.
Создание нового объекта основано на операции new (create в ) и вызове конструктора экземпляра. Вот пример:
D x = new D("ABC");
В C# нет различия между объявлениями (статикой) и операторами (динамикой), позволяя в объявлениях создавать и инициализировать объекты, выполняя операцию new с вызовом конструктора, создающего объект - экземпляр класса D в примере.
Это, однако, далеко не полная история о конструкторах экземпляра и создании экземпляра. Детальная спецификация (дается ниже при обсуждении наследования) объясняет, что ваш вызов конструктора может в результате приводить к вызову других конструкторов, создавая цепочку вызовов, которая должна включать вызов конструктора каждого предка класса.
Статический конструктор, существующий в единственном экземпляре, без аргументов, что отражено в примере, выполняется до того, как потребуется экземпляр класса или доступ к через однократные once-функции). Представьте экземпляр системы, фиксирующей ошибки, где ошибки записываются в специальный журнал (файл). Первое появление ошибки приводит к созданию и открытию этого файла.
В C# предполагается существование сборщика мусора: в то время как создание объектов выполняется явно при задании операции new, освобождение памяти для неиспользуемых более объектов возлагается на автоматический механизм - сборщик мусора (). Иногда можно попросить GC выполнить специфическую операцию, помимо освобождения памяти. Типичным примером является работа с файлами. Всякий раз, когда закончена работа с объектом, представляющим файл, требуется закрыть физический файл, связанный с объектом (в можно для этих целей вызывать процедуру dispose, которую GC будет выполнять при освобождении объекта).
Деструкторы C# отвечают этим потребностям. Имя деструктора ~C, где C - имя класса. Здесь нет перегрузки, так как деструктор существует, если он задан, в единственном экземпляре. Он не имеет аргументов, не возвращает значения, не имеет модификаторов, таких как static:
class File {
… Другие компоненты, включая конструкторы …
~File() {
… Операторы, закрывающие, например, физический файл …
}
}
Операция, определяемая программистом, - это статический метод, где знак операции выступает в качестве имени метода, как в примере:
class E {
public static E operator +(E a, E b) {
… Вычисление результата exp, возвращаемого в операторе return …
return exp;
}
}
Операция может быть вызвана в инфиксном синтаксисе, свойственном операциям в математике, как x + y, где x и y типа E (это аналогично alias-компоненту ). Операции встроены в int и float. Имена (знаки) операций и их приоритеты фиксированы (в отличие от , нельзя определять собственные знаки операций). Главными доступными знаками операций являются:
+ - ~ ! ++
Здесь "!" - отрицание для булевских, "~" - отрицание для целых (взаимное обращение 0 и 1 в бинарном представлении числа). Операции "++" и " " имеют побочный эффект: x++ возвращает значение x, затем увеличивает x на 1; ++x также увеличивает x, возвращая увеличенное значение; (аналогичная семантика у операций x- и ). Как вы знаете, операции с побочным эффектом - не очень хорошая идея, используйте их на собственный страх и риск.
Бинарными операциями являются:
+ - _ / % ^ // Арифметика (% - остаток от деления нацело) < > <= >= // Операции отношения (дают булевский результат) == != // Эквивалентность (равно и не равно) ^ | // Булевские строгие (and, xor, or) << >> // Побитовый сдвиг (влево, вправо) || // Булевские полустрогие (and, or)
Как и в других языках, наследуемых от C, знак равенства означает присваивание, а эквивалентность задается двумя знаками равенства. Некоторые из приведенных выше операций применяются к побитовому представлению целых. Строгие булевские операции применимы не только к булевским значениям, но и к целым, применяя операцию к каждой паре соответствующих битов. m << n эквивалентен умножению m на $$2^n$$, сдвиг вправо - делению.
Можно использовать перечисленные знаки для определения операций в собственных <), то необходимо определить и "больше" (> ). Аналогично и для других операций сравнения.
Кроме того, C# поддерживает следующие неперегружаемые операции:
[…] // Получение элемента массива (…) // Кастинг += -= _= /= %= ^= // Присваивание = |= <<= >>= // Присваивание
Присваивание x += 1 это сокращенная форма записи x = x + 1. Аналогичный смысл и у других операций присваивания с операциями.
Для объявления массива с одной или более размерностью используется нотация с квадратными скобками:
string[] a; // Одномерный массив string[,] b; // Двумерный массив
Для задания многомерных массивов используются запятые, как показано в примере. Число запятых, увеличенное на 1, определяет размерность массива. Нижняя граница индекса по каждому измерению фиксирована и равна 0. Поэтому элемент b [1, 1] задает элемент во второй строке и втором столбце. Такая политика требует внимательности при работе с индексами.
В объявление ). Присваивание:
a = new string[4];
создаст массив из четырех элементов, инициализируемых стандартными значениями по умолчанию. Разрешается инициализировать массив значениями в момент его создания:
a = new string [] {"A", "B", "C", "D"}; //Массив из четырех элементов
При инициализации многомерных массивов используются запятые, разделяющие списки, которые заключены в фигурные скобки:
b = new string[2, 3];
b = new string[,] {{"A", "B"}, {"C", "D"}, {"E", "F"}}; // Размерность [3,2]
Кроме прямоугольных массивов, C# предлагает изрезанные, гребенчатые массивы, которые являются массивами массивов, как в примере:
string[][] c;
Каждая строка может иметь различный размер (отсюда и название - изрезанность, гребенка). Вот пример типичной инициализации:
c = new string[][] {new string[] {"A"},
new string[] {"B", "C", "D"},
new string[] {"E", "F"}};
Число элементов в каждой строке соответственно - 1, 3, 2.
Доступ и модификация использует синтаксис с квадратными скобками:
b[0, 0] = "Z";
c [0] = new string[] {"Y", "Z"}; // Изменяет первую строку с индексом 0
c [0][0] = "Z";
Можно определить нотацию со скобками для доступа к структурам, отличным от массивов, как в следующем примере:
Table t = new Table (); string n; … n = t [1, 1]; // Доступ к первому элементу (смотри реализацию ниже)
Здесь Table - собственный класс, поставляемый с индексатором, выполняющим ту же роль, что и псевдоним (alias) " []" - "квадратные скобки" в . Определение индексатора является обобщением метода-свойства:
class Table {
private string[ , ] rep;// Инициализация rep опущена
public string this [int i, int j] {
get {return rep [i - 1, j - 1];}
set {rep [i - 1, j - 1] = value;}
}
}
Имя индексатора фиксировано и совпадает с именем текущего объекта this, как следствие, у класса может быть только один индексатор. Индексатор определяет два метода - геттер и сеттер с аргументами, задающими индексы элемента контейнера, к которому осуществляется доступ. В реализации класса Table индексация идет по двумерному массиву rep и устроена так, что для клиента начальный индекс по обоим измерениям начинается с единицы.
Концепция универсальности C# знакома по ; родовые параметры заключаются в угловые скобки <…>. Объявим:
class F<G, H> where H: T, new() {
… Объявление класса …
}
Класс F имеет два родовых параметра. Для H задано ограничение (как в классе C [G, H -> T] в ), так что любой фактический родовой параметр должен наследовать от T. Включение в ограничение new() означает (как в , если объявление имеет вид C [G, H -> T create make end] для процедуры создания make из T), что T должен обеспечить public-кон-структор без аргументов; это позволит методам F создавать экземпляры T.
Родовое порождение также использует угловые скобки:
F<V, W> //W должен быть согласован с T и иметь конструктор без аргументов.
Дадим обзор основных операторов языка.
Присваивание, уже появляющееся в примерах, использует знак равенства:
var = e;
Заметьте: точка с запятой является не разделителем операторов, а завершителем, и должна завершать любой оператор.
Вызов метода использует имя метода и список аргументов. В отличие от вызова в , круглые скобки всегда сопровождают вызов метода, даже если список аргументов пуст, как в methodWithNoArgument ().
Оператор return не имеет прямого аналога в . Он обязателен для функций, задавая значение, возвращаемое функцией:
return some_expression;
Для процедур оператор возможен и задает завершение процедуры. Поскольку в методах он может появляться в нескольких местах, это означает, что методы C# не соответствуют правилу "один выход".
Условный оператор удовлетворяет следующему синтаксису:
if (c1) {
…
} else if (c2) {
…
} else {
…
}
Множественный выбор имеет форму:
switch (expression) {
case value: statement; break;
case value: statement; break;
…
default: statement; break
}
Здесь expression задается булевским или целочисленным выражением, а каждое value представляет вычислимую в период компиляции константу. Когда значение выражения не совпадает ни с одной константой, выполняется ветвь default, если она задана, в противном случае ничего не делается (в в отсутствие ветви else в операторе в подобной ситуации в период выполнения генерируется ошибка). Оператор switch не задает конструкцию с одним входом и одним выходом, а представляет многоцелевой goto. Для правильной структурированности следует четко следовать показанной схеме. Принудительный оператор break, завершающий каждую ветвь, позволяет избежать типичной ошибки для C++ и C-версии switch, когда управление проваливается в другую ветвь.
Доступно несколько форм оператора цикла. Наиболее общая идет от С и соотносится с конструкцией from в :
for (initialization; exit; modification) {
… body …
}
Вначале выполняется инициализация цикла - initialization - и работа цикла заканчивается, если условие выхода из цикла - exit - получает в результате значение true. Обычно оно не выполняется после инициализации, и тогда выполняется тело цикла, а затем модификация параметров цикла - , после чего снова проверяется условие выхода. Цикл завершается, когда выполняется условие выхода. Модификация обеспечивает продвижение к следующему шагу, увеличивая индекс, продвигая курсор (в она интегрирована с телом цикла).
Можно использовать циклы в стиле while или until:
while (condition) {statements}
do {statements} while (condition)
Удивительно, C# сохранил оператор goto M, где M - метка. Операторы можно снабжать метками, отделяя метку от оператора двоеточием.
Исключение - это событие периода выполнения программы, появление которого приводит к прерыванию нормального выполнения программы. Причины исключений могут быть разные, такие как деление целого на нуль, отсутствие файла, null-ссылка у цели, вызывающей метод. Возможно также, что исключение "выбрасывается" при выполнении специального оператора языка C#:
throw e;
Здесь e - Exception.
Обработка исключений в C# выполняется в следующем стиле:
try {
… Обычные операторы, во время выполнения которых может возникнуть
… исключение …
} catch (ET1 e) {
… Обработка исключения типа ET1, детали в объекте e.…
} catch (ET2 e) {
Обработка исключения типа ET2, детали в объекте e.…
}… Возможно, другие случаи …
finally {
… Выполняется во всех случаях, было исключение или нет.
}
Если в охраняемом try-блоке включается исключение одного из перечисленных типов ET1, ET2, …, то выполнение в try-блоке прерывается и управление передается соответствующему catch-блоку, который в состоянии захватить исключение данного типа. Блок finally выполняется всегда, если присутствует. Его обычная цель - освобождение ресурсов, закрытие файлов и так далее.
Появление исключения создает объект исключения, доступный программе в соответствующем catch-блоке. Это дает возможность при обработке исключения использовать свойства объекта, такие как имя исключения, понятное человеку, состояние стека вызовов.
Если встретилось исключение, чей тип не совпадает с типами, перечисленными в catch-блоках, или исключение встретилось вне охраняемого try-блока, то оно передается вверх по цепочке вызовов - в вызывающий метод. Здесь, опять-таки рекурсивно, исключение может быть обработано, если предусмотрен catch-блок, или передано наверх. При завершении цепочки вызовов, если обработка исключения не выполнена, то программа завершается стандартным сообщением об ошибке - обрабатывается стандартным catch-обработчиком исключения.
Читатель, знакомый с Java, заметил, что приведенное описание применимо к обоим языкам (получившим этот механизм от С++). Есть отличие от модели Java - метод в C# не специфицирует типы выбрасываемых исключений, как это делает Java, используя спецификацию throws.
В C# предлагается ) для описания методов как объектов. Ассоциированный механизм - события - дополняет делегаты для программирования, управляемого событиями (в типы событий описываются как обычные объекты и, следовательно, нет необходимости в специальных конструкциях). Рассмотрим объявление делегата:
public delegate int DT (string s);
Это объявление типа: оно определяет тип D T, который представляет функции с одним строковым аргументом, возвращающие целое в качестве результата. Для определения экземпляра класса и связывания его с конкретным методом - скажем, int lettercount(string s) - функцией, подсчитывающей число буквенных символов, - можно использовать:
DT d = new DT(lettercount)
Можно обойтись без вызова конструктора, используя явное присваивание:
DT d = lettercount;
Некоторые языки программирования (отличные от функциональных) не позволяют использовать метод как аргумент другого метода. Механизмы агентов, делегатов, указателей функций в С++ спроектированы как раз с целью создания специальных объектов, передаваемых как аргументы и представляющих обертки соответствующих функций. Для создания в C# делегата из метода необходимо передать метод конструктору, как в [5]. Концептуально это единственный случай, допускающий использование имени метода как значения.
На практике C# ослабляет правило, допуская присваивания, такие как [6], или передавая имя метода как аргумент другому методу, но это синтаксический сахар, фактически передаваемое значение является делегатом, здесь new DT ( ) .
Делегат можно вызывать подобно любому другому методу:
n = d ("A");
После присваивания [5] или [6] эффект будет тот же, как и при непосредственном вызове n = lettercount ("A") ;. Конечно, при вызове [7] обычно неизвестно, какую именно функцию представляет d. Часто такой вызов осуществляется в методе r, для которого d -
r (lettercount);
Эквивалент комбинации [4] или [5] задается одним оператором d:= agent lettercount, не требующим объявления типа, такого как [4]. Эта форма не имеет прямого эквивалента в C#, но можно использовать делегаты с "анонимными методами" (эквивалент встроенных агентов), как в примере:
r (delegate (string s) {return lettercount (s);} );
Здесь используется анонимный метод. Заметьте, что анонимный метод объявляет сигнатуру, но не объявляет тип результата. Хотя в C# нет прямого эквивалента -понятия "открытый аргумент", но анонимные методы позволяют достичь того же результата.
Эквивалентом встроенного агента является лямбда-выражение C#, как в
(int x, int y) => x + y
соответствующее математической записи: ?x, y: INTEGER | x + y.
В C# делегат не ограничен представлением ровно одного метода. Если a и b - делегаты одного типа, то a + b обозначает делегата того же типа. Его выполнение приводит к последовательному выполнению методов, связанных с a и b. Операции add и remove позволяют в список методов, связанных с делегатом, добавлять или удалять новые члены. Можно для этого использовать и операцию присваивания += и -=.
Для программирования, управляемого событиями, можно определить типы событий и связать их с делегатами, как в следующем объявлении:
public event DT1 click;
Здесь click определяется как событие, которое будет обрабатываться делегатом типа DT1.
Этот пример использует новый тип делегата DT1 вместо рассматриваемого ранее D T, так как типы делегатов, обрабатывающих события, обычно являются процедурами, в то время как DT задает класс функций. Если мы собираемся рассматривать щелчки кнопки мыши, то надо передать событию аргументы, задающие координаты мыши, тогда DT1 будет объявлен как:
public delegate void DT1 (int x, int y);
Для понимания механизма необходимо знать, что реализация C# представляет каждый тип события, такой как click, как список делегатов, которые соответствуют различным методам, подписанным на события. Это объясняет, как подписаться на события:
click += r;
Здесь r - это метод с подходящей сигнатурой: void r (int x, int y) . Операция += перегружена для списков; она добавляет элемент в список. Заметьте, что можно применить метод r непосредственно, прежде чем явно обернуть его в одежды делегата. Но то, что получается при добавлении в список, является делегатом. Для удаления подписки используется конструкция -=.
Так выполняется подписка. Для публикации события используется схема:
if (click != null) {click (h, v);}
В этом примере h и v задают координаты мыши. Опять-таки, нужно знать о реализации списка, чтобы понимать необходимость теста click != null: если нет делегатов, подписанных на click, то список будет иметь значение null, и вызов click (h, v) станет причиной исключительной ситуации.
Рекомендуемый стиль для обработки аргументов события в .NET состоит в том, чтобы аргумент события был объявлен как потомок библиотечного класса EventArgs. В этом примере можно было бы объявить класс IntPairArgs, наследуемый от EventArgs, и в этом классе объявить два целочисленных поля x и y. Тогда подписываемые методы имели бы форму:
private void rHandler (object sender, IntPairArgs e) {r (e.x, e.y);}
Здесь - как часть того же рекомендуемого стиля - первый аргумент sender представляет целевой объект, второй аргумент e представляет аргументы события. Преимущество в том, что все схемы обработки события выглядят одинаково. Неясно, однако, оправдывает ли это возникающие усложнения: вместо прямого повторного использования методов, существующих в модели, таких как r, необходимо обернуть их в специальный склеивающий код, такой как rHandler.
Модель наследования C#, совпадающая в основном с моделью Java, не поддерживает множественного наследования классов, за исключением специального вида абстракции, называемого интерфейсом.
Вот как класс объявляет другой класс своим родителем:
class L :K
{… Объявления компонентов L …}
Класс L объявил себя наследником K. Следующий пример показывает случай, когда наследуемый класс объявляется универсальным, здесь - с ограниченной универсальностью:
class M<G>:K where G: T
{… Объявления компонентов M …}
Только один класс, здесь - K, может быть объявлен в качестве родителя.
В C# можно объявлять абстрактными как отдельные методы, так и класс в целом (это соответствует отложенным методам и классам , но без контрактов):
abstract class N {
public abstract void r(); // Заметьте, реализация отсутствует
public abstract int s(); // Заметьте, реализация отсутствует {…}
… Другие методы, которые могут быть или не быть абстрактными…
}
Нельзя использовать конструктор для создания экземпляров абстрактного класса (как new N (…)).
Методы-свойства и индексаторы также могут быть абстрактными.
Интерфейс подобен полностью абстрактному классу. Интерфейсы задают абстрактную функциональность, которую каждый потомок должен реализовать. Интерфейсы обеспечивают ограниченную форму множественного наследования, так как каждый класс может наследовать от множества интерфейсов. Это позволяет языку избегать конфликтов, когда несколько наследуемых методов от разных родителей с одинаковыми именами имеют разные реализации (мы знаем, что конфликты разрешимы за счет переименования). Платой является некоторая потеря мощности наследования. Пример типичного интерфейса:
interface IOrdered <T> {
bool lesser (T other);
bool greater (T other);
bool lesserEqual (T other);
bool greaterEqual (T other);
}
По соглашению имена интерфейсов должны начинаться с буквы I. Пример показывает, что интерфейс может быть универсальным. Важно, что интерфейсы объявляются без указания модификатора доступа, - они неявно имеют статус public.
Данный интерфейс задает свойство сравнимости объектов, используя общепринятые имена операций сравнения. Он также иллюстрирует границы понятия интерфейса - поскольку методы не могут иметь реализации, нельзя задать, что a.greater (b) должно быть реализовано как b.lesser (a) (для абстрактного класса такое возможно, но только один родитель может быть классом, не имеет значения - абстрактным или эффективным).
Класс может наследовать от одного или нескольких интерфейсов, обеспечивая реализации методов:
class TennisPlayer: IOrdered<TennisPlayer>, IAnotherInterface {
public int ranking;
public bool lesser (TennisPlayer other) {return (ranking < other.ranking);}
… Аналогично для greater, lesserEqual, greaterEqual …
… Реализация методов IAnotherInterfacei …
… Другие компоненты TennisPlayer …
}
Множественное наследование интерфейсов не исключает конфликта имен. В C# нет явного механизма переименования, аналогичного , но конфликта можно избежать, используя в качестве префикса имя интерфейса и нотацию с точкой, например, IAnotherInterface.clashingname.
В связи с наследованием в дополнение к трем ранее рассмотренным модификаторам доступа (public, internal, private) добавляется новый модификатор и новая комбинация:
protected: доступно для потомков;protected internal: доступно потомкам и классам той же сборки.Когда переопределяется наследуемый компонент, как обсуждается далее, статус доступа изменить нельзя. Этим язык отличается от Java, где разрешается расширить права доступа, но не ограничить их. Кроме того, потомки не могут иметь доступность большую, чем их предки.
Наследуемый метод можно переопределить. Соглашения, однако, отличаются от других современных языков программирования. Как в С++, и в отличие от и Java, связывание в C# - удивительно для языка, первая версия которого появилась в 1999 году, - статическое. Другими словами, версия f, выполняемая при вызове a.f, будет следовать объявлению a, но не virtual:
class P {
public virtual void f (…) {…}
…
}
Потомок переопределяет виртуальный метод, в точности сохраняя сигнатуру и заменяя модификатор virtual на override:
class Q: P {
public override void f (…) {…}
…
}
Оригинальная версия переопределяемого метода остается доступной, как с Precursor в , для этого применяется нотация с точкой, а в качестве префикса используется имя родителя - base. Например, реализация f в Q может пользоваться результатами работы, проделанной родителем:
base.f (n);
Для реализации динамического связывания требуется точное соответствие схеме (оригинал метода специфицирован как virtual, новый - как override1). Если условие не выполняется, то компилятор по-другому интерпретирует текст. Рассмотрим следующий вариант:
class R {public void f (int i) {…}}
class S: R {public void f (int i) {…}}
R r1 ; S s1 = new S() ; int n;
r1 = s1;
Оригинал f не объявлен virtual в R, но все же потомок S дал новую реализацию. Это не является base). В этой ситуации применяется статическое связывание. Вызов s1.f(n) будет использовать новую версию, но вызов r1.f(n) будет использовать версию R независимо от того, с каким объектом связано будет r1 во время выполнения. Это довольно опасно, хотя компилятор отслеживает ситуацию и выдает предупреждающие сообщения. Для защиты от риска ошибки новый метод следует помечать как new:
class S: R {public new void f (int i) {…}
Динамическое связывание не встретится и в случае вызова p1.f (n) , где p1 типа P, и динамически типа Q, если в Q опустить при переопределении модификатор override. Это не будет восприниматься как ошибка, просто означает применение
При переопределении можно задать модификатор sealed, чтобы защитить метод от переопределения у потомков:
class Q1: P {
public sealed override void f (…) {…}
…
}
Этот модификатор допускается только для override-методов, поскольку остальные по определению не являются переопределяемыми. Модификатор sealed может быть у класса (sealed class T …), и тогда такой класс не может иметь потомков.
Объявление классов и методов с модификатором sealed характерно для .NET-библи-отек, вероятно, по той причине, что в условиях отсутствия контрактов это единственный способ не позволить потомкам исказить намерения родителя.
Выбор в языке C# по умолчанию C#-программистов: всегда объявлять методы virtual, удаляя этот модификатор в тех редких случаях, когда метод должен быть защищён от переопределения - sealed.
В C# в присутствии наследования действует специальное правило для конструкторов: любой конструктор должен вызвать конструктор родителя. Как результат, вызов конструктора приводит к цепочке вызовов, включающей вызовы всех родительских конструкторов и заканчивающейся вызовом конструктора по умолчанию прародителя - object. Этот эффект может достигаться явно или неявно.
base (как base (n); ). Так как конструкторы не имеют индивидуального имени, сигнатура аргументов, благодаря правилам перегрузки, однозначно определяет, какой конструктор будет вызван.Один из этих случаев должен быть применим. Если в конструкторе нет явного вызова конструктора родителя и родитель при этом не имеет конструктора по умолчанию, то возникнет ошибка периода компиляции.
Как отмечалось при обсуждении соответствующих механизмов Java, причина этих правил не полностью ясна. Намерением, вероятно, было убедиться, что экземпляр потомка удовлетворяет правилам согласованности, определенными предками. Цепь конструкторов появляется как попытка убедиться в такой согласованности. В отсутствие понятия инварианта класса она позволяет явно выразить ограничения.
Для U выражения exp типа T (как в тесте объектов ) можно использовать два механизма.
(U) exp, не учитывая объявленный тип exp. Если все хорошо и exp в самом деле представляет объект типа U в момент выполнения, то можно использовать это выражение для ссылки на значение приведенного типа. Обратная сторона в том, что если U, попытка вычисления выражения приведет к возникновению исключительной ситуации, обработку которой надо предусмотреть для безопасного программирования.Более приемлемым способом является использование специальной C#-конструкции - exp is U. Во втором случае exp по-прежнему статически принадлежит объявленному типу T, но теперь можно комбинировать два механизма с гарантией, что кастинг будет работать:
if (exp is U) {r ((U) exp)}; // Метод r объявлен как: r(U : x) {…}
В C# введены несколько механизмов ОО-парадигму.
При конструировании ПО, приходящего из разных источников, возможны конфликты имен используемых классов: два провайдера могут поставлять классы с одинаковыми именами. В C# такие конфликты могут разрешаться благодаря введению уровня структуризации более высокого, чем классы, - пространства имен.
По умолчанию имена всех типов (классов) принадлежат глобальному пространству имён. Разрешается определять собственное пространство имен, содержащее объявления классов:
namespace N1 {
… Объявления классов (и других типов по желанию) …
}
В этом случае клиент, которому необходим доступ к нескольким классам с одним именем, скажем, С, может устранить неопределенность, используя нотацию N1.C.
Важным предопределенным пространством имен является пространство System, содержащее базисные библиотеки классов.
Пространства имен могут быть вложенными. Это достигается как прямой вложенностью текста одного пространства в другое, так и с использованием нотации с точкой:
namespace N1.N2 {// N2 - подпространство N1: элементы доступны в нотации
// N1.N2.V.
… Объявления классов, включая V …
}
Клиент, часто использующий классы из некоторого пространства имен, может избежать повторения квалифицированных имен (как в N1.N2.C, N1.N2.D и т. д.) благодаря использованию директивы using:
using N1.N2; using SomeOtherNamespace; … Здесь можно использовать V как сокращенную запись N1.N2.V …
Как показано в примере, можно использовать любое число using-директив. Если соответствующие пространства имен содержат классы с конфликтующими именами, то приходится вернуться к нотации с точкой для разрешения конфликтов.
Предположим, вы хотите расширить концепцию, покрываемую существующим классом X. Обычный ОО-механизм состоит в объявлении нового класса, наследуемого от X. Но наследование может быть неудобным или неприменимым. Например, X может запрещать наследование (как отмечалось, обычная практика для .NET-библиотек). Вообще, определение нового класса означает определение нового типа. Даже если вы только добавляете методы, не добавляя полей, система типов не позволит применять новые операции к существующим объектам исходного типа X, например, к объектам, хранящимся в файле или в базе данных.
Чтобы справиться с этой проблемой, C# обеспечивает интересный механизм расширения методов: методы добавляются извне к существующему классу.
Очевидно, методы расширения представляют просто синтаксическое упрощение, так как проблему можно решить за счет статических методов: в любой класс можно добавить статический метод , который будет вызываться как с x1 типа X. Однако мы хотим, чтобы метод m вызывался в том же стиле, как если бы он был методом X:
x1.m (other_args);
При этом m объявляется не в классе X, а в другом классе. Синтаксический трюк состоит в том, чтобы маркировать первый аргумент m модификатором this:
public static class Y {
static void m (this X x, int arg1, int arg2) {…}
…
}
В результате в классе X появляется расширенный метод и [9] становится правильной конструкцией (с двумя целочисленными аргументами, заменяющими other_args).
По своей природе языки программирования ограничены семантическими механизмами, предусмотренными их создателями. Иногда возникает желание добавить новые свойства, не изменяющие существующую семантику, но полезные, например, для документирования или сериализации.
Вполне разумно для языка программирования обеспечить возможность таких расширений, известных как включение метаданных, - поддерживающей информации, добавляемой в документ, отделенной от основного содержания. В именно для такой цели используются предложения note, связанные с классами и методами. В .NET и C# поддержка метаданных обеспечивается в форме атрибутов (не путайте с ОО-понятием атрибута, используемым в этой книге, для которого в C# применяется термин "поле").
Некоторые атрибуты предопределены, но программисты могут определять собственные атрибуты, известные как
Примером предопределенного атрибута является атрибут сериализации , который можно присоединить к классу, указав тем самым, что экземпляры могут быть конвертированы в некоторое внешнее представление и храниться во внешней памяти:
[Serializable]
public class Z {… Обычное объявление класса …}
Здесь показано добавление атрибута к классу. То же соглашение действует при связывании атрибута с методом. Атрибут заключается в квадратные скобки и используется в качестве префикса класса или метода.
Для задания пользовательского атрибута следует определить класс - потомок класса System.Attribute (то есть класса Attribute из пространства имен System). Предположим, что мы хотим поставлять классы с базисной версией управляющей информации: именем автора, датой модификации, возможно, номером версии, все в строковом формате. Мы используем:
public class ChangeAttribute: System.Attribute {
private string author;
private string last;
public ChangeAttribute (string a, string l)
{author = a ; last = l;}
public string revision;
}
Заметьте, имя класса заканчивается словом Attribute, - это рекомендуемый стиль именования атрибутных классов.
Тогда можно поставлять класс или (здесь) метод с информацией о версии:
[ChangeAttribute ("Caroline", "24 December 2009")] public void r {…}
При задании атрибута мы должны передать аргументы выбранному конструктору. Также возможно задать значения public полей атрибутного класса, так называемых полей, допускающих запись. В данном примере таким является поле :
[ChangeAttribute ("Caroline", "24 December 2009", revision = "2.1" )]
public void r {…}
Атрибут ChangeAttribute, в том виде как он объявлен, применим к любым программным элементам: class, struct, method, field, delegate и некоторым другим. Можно ограничить применимость атрибута, присоединив собственное объявление у атрибута AttributeUsage:
[System.AttributeUsage(System.AttributeTargets.Class)]
public class ChangeAttribute: System.Attribute {… Остальное, как прежде …}
Вместо задания Class (в качестве области действия атрибута) можно использовать другие ключевые слова: All (по умолчанию), Assembly, Delegate, Event, Interface, Field, . Разрешается задавать несколько целей, разделяя их символом вертикальной черты |.
Для элементов, снабженных атрибутами, можно получать значения атрибутов через процесс, называемый отражением. Рассмотрим вызов объекта о:
o.GetType().GetCustomAttributes(true);
Результатом будет массив, содержащий атрибуты, определенные для класса объекта, с их значениями.
В C# отсутствуют несколько ОО-механизмов, интенсивно применяемых в этой книге, в первую очередь - контракты и множественное наследование.
Контракты введены в исследовательскую версию языка C# - SpecC#, разработанную в Microsoft Research. Эта версия языка доступна для свободного использования.
Полезно ознакомиться с несколькими поддерживающими C# конструкциями.
В C# комбинируются строгие требования к безопасности типов с предоставлением программистам возможности работы на низком уровне, характерном для языка С или ассемблера. Конструкция "" поддерживает четкое разделение между небезопасными элементами и нормальными, прошедшими проверку типов.
Объявление метода небезопасным () означает, что область данных расположена вне "кучи" - области, отводимой под объекты. Небезопасный метод может осуществлять в своей области непосредственные манипуляции с указателями и, следовательно, игнорировать нормальные правила работы с данными.
Типы, заданные перечислением, позволяют оперировать значениями из конечного множества предопределенных значений:
enum CardColors {Spades, Hearts, Diamonds, Clubs}
Значения, заданные перечислением, проецируются на целочисленный тип, по умолчанию int, но можно задать и другой тип, как в enum T:long {…}. Значения проецируются на отрезок, начинающийся с 0, но можно задать и другое начало:
enum CardColors1 {Spades = 1, Hearts, Diamonds, Clubs}
Значения можно обозначать, используя нотацию с точкой, как в CardColors.Spades. Допустимы взаимные преобразования между целыми и значениями
В версии языка C# 3.0 появились новые важные механизмы, привлекшие внимание программистов. Механизм, известный как Linq, позволяет языку программирования работать непосредственно с базами данных и Web. Для работы с данными используется типичный язык запросов SQL и XML для Web. Обычно такие возможности поддерживаются с помощью специальных библиотек, обеспечивающих интерфейс к реляционным базам данных или к протоколу HTTP. Оригинальность Linq в том, что он делает все гораздо выразительнее, оставаясь в рамках языка программирования. Например, запрос к базе данных можно выполнить так:
from e in Employees where e.salary > median select e.rank
Здесь идет ссылка на отношение Employees из базы данных. В результате создается спиок всех служащих, ранжированных по зарплате. Стиль тот же, что и у SQL-запросов, но запрос интегрирован в язык. Все используемые объекты - это нормальные объекты программы, такие как списки.
В C# идентификаторы следуют соглашениям, подобным . Они могут, однако, начинаться с подчеркивания, хотя эта возможность редко используется в обычных приложениях. Важная разница в том, что идентификаторы чувствительны к регистру: anIdentifier, AnIdentifier и anidentifier - это все различные идентификаторы. Лежащее в основе множество символов - это Unicode.
Комментарии в C# программах могут быть:
//;/* …*/.Однострочные комментарии, начинающиеся с /// (с добавленным слеш-символом), задают документированный комментарий, записываемый в виде XML-кода. Эти комментарии используются специальным инструментарием для разных целей - документирования, интеллектуальной подсказки.
Judith Bishop, Nigel Horspool, C# Concisely, Addison-Wesley, 2003. Введение в основные механизмы C# (для ранних версий языка).
Онлайн документация на сайте:http://msdn.microsoft.com/en-us/vcsharp/default.aspx
Лучшее место для получения детальной спецификации механизмов языка. Сайт также включает ссылки на несколько учебников.
На русском языке: Биллиг В.А. "Основы объектного программирования на C# 3.0", Интернет-Университет. Питер, Лаборатория знаний,
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.