Инструменты, алгоритмы и структуры данных

Введение в C# (по материалам Бенджамина Моранди)

Разбить на страницы
Показывать лекцию целиком

При появлении языка 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, приведенное в предыдущем приложении (как следствие, повторяются некоторые рассуждения, когда рассматриваются разделяемые концепции). Подобно другим приложениям, язык не рассматривается с чистого листа, - обсуждение предполагает знакомство с программистскими концепциями, введенными в этой книге. Описание языка сопровождается сравнением с соответствующими механизмами Eiffel.

Окружение языка и стиль

C# (произносится "C шарп") тесно связан с окружением Microsoft .NET, платформой для разработки и выполнения ПО, использующей виртуальную машину. В предыдущих обсуждениях отмечалась роль виртуальных машин и их преимущества для реализации языков высокого уровня.

.NET, CLI и взаимодействие с языком

В то время как виртуальная машина Java - JVM - была спроектирована специально для поддержки этого языка (хотя позднее она использовалась для реализации других языков программирования), главная цель проекта платформы .NET состояла с самого начала в поддержке нескольких языков. Это решение отражается как в имени виртуальной машины - Common Language Runtime (CLR) - "Общеязыковая Среда Выполнения", так и в поддержке взаимодействия API, Common Language Infrastructure (CLI) - "Общеязыковой Инфраструктуры", которая теперь является международным стандартом.

Частично причина была в том, что Microsoft еще до .NET обеспечил реализацию нескольких языков, прежде всего, Visual Basic (VB), C++ и JScript для клиентских Web-приложений. VB популярен на массовом рынке и часто используется для разработки приложений по настройке офисных документов. Компания не могла, естественно, предложить соответствующим программистским сообществам бросить свои любимые языки и перейти на новый бренд. Она смогла обеспечить общую базу для взаимодействия и будущей эволюции. .NET и CLR/CLI были способны с самого начала обеспечить реализацию четырех поддерживаемых Microsoft языков (помимо трех упомянутых еще и C#), а также языки, разрабатываемые другими компаниями, включая Eiffel (с самого начала введения .NET в 1999) и Cobol, язык прошлых лет, но все еще важный для многих бизнес-приложений.

Языковая открытость среды означала не только доступность нескольких компиляторов, но и влекла высокую степень взаимодействия между программами, написанными на разных языках. В этом роль Общеязыковой инфраструктуры: обеспечить стандартное множество механизмов, которые могут быть использованы в любом языке. CLI представляет объектную модель, близкую ОО-языку, но без синтаксиса. Эта модель задает множество хорошо определенных механизмов - это ОО механизмы, изучаемые в этой книге: классы, их компоненты, наследование, универсальность, система типов, объекты, политика динамического создания объектов и сборка мусора - для которых CLI-проект предложил несколько специфических проектных решений.

При условии, что .NET-языки не слишком отклоняются от этих решений, они могут достичь степени взаимодействия, неслыханной в дни, предшествующие .NET. В частности, классы, написанные на разных языках, могут взаимодействовать друг с другом, используя как отношения наследования, так и клиентские. Например, Eiffel-класс может наследовать от класса C#, возможно и обратное наследование. Это просто предполагает, что компиляторы следуют единым CLI-правилам для поставщиков и клиентов сборок (целевых модулей, создаваемых .NET-компиляторами).

Эта схема взаимодействия доказала свою успешность (несмотря на существующую тенденцию производителей ПО игнорировать правила CLI-совместимости). Она позволяет каждому языку сохранять свою индивидуальность, пока ее можно отобразить в объектную модель CLI. Например, реализация Eiffel должна моделировать множественное наследование - не поддерживаемое напрямую CLI - через специальное использование CLI-механизмов (множественное наследование интерфейсов, концепцию, обсуждаемую позже в этом приложении).

Любимый сын

В сообществе языков, как и у людей, все языки равны, но некоторые равны более других. Язык C# - это любимый сын: его объектная модель наиболее тесно связана с CLI. VB .NET, который схож с предыдущей версией только синтаксически, является еще одним претендентом на звание "любимого". CLI-совместимая версия C++ - "управляемый C++" - существенно отличается от обычного C++. Ограничения необходимы, чтобы язык мог принимать участие в играх .NET-взаимодействия. Фактически, семантика C# определялась семантикой CLI, хотя последующие версии ее существенно расширили. Синтаксис языка соответствует традиции C, C++, Java, включая завершение операторов символом точки с запятой и применением фигурных скобок для окаймления блоков программы.

Общая структура программы

Базисными элементами C# программы являются классы и структуры, организованные в виде нескольких программных файлов.

Классы и структуры

C#-классы (ключевое слово class) и структуры (ключевое слово struct) задают описание множества возможных объектов периода выполнения. Объекты обладают свойствами, и к ним применимы методы. Общая форма объявления такова:

class name {
   … Объявление компонентов … 
}

При объявлении структуры вместо ключевого слова class используется слово struct. Компоненты могут быть разные. Это может быть:

  • поле, соответствующее атрибуту Eiffel;
  • константа, частный случай поля;
  • метод, реализованный в виде процедуры или функции;
  • метод-свойство, поле, сопровождаемое возможными методами - геттером и сеттером;
  • операция, функция с синтаксисом операции;
  • конструктор, процедура создания - метод, применяемый для создания объектов;
  • деструктор, редко применяемый метод для освобождения ресурсов, возможный только для классов;
  • событие, которое связано с делегатами и программированием, управляемым событиями;
  • индексатор;
  • вложенный тип.
  • Структура является упрощенной формой класса без возможности наследования. Остальная часть обсуждения фокусируется на классах, но большинство свойств, не связанных с наследованием, применимо и к структурам Главное отличие классов от структур состоит в том, что класс определяет ссылочный тип, а структура - развернутый. Значения ссылочного типа разделяют память - на один и тот же объект может указывать несколько ссылок, имена ссылок являются синонимами. Объект развернутого типа ни с кем свою память не разделяет. Все примитивные типы - арифметический и другие - реализованы как структуры..

    Классы и структуры группируются в сборки (понятие, соответствующее кластеру в 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 в Eiffel), в C# имеет имя this, которое, как и в Eiffel, можно опускать, когда из контекста ясно, что речь идет о текущем объекте.

    В C# разрешается объявлять статические члены, не требующие объекта и вызываемые как C.member, где C - имя класса. Определение статического компонента, например статического метода, может использовать только статические компоненты.

    Класс в целом может также быть объявленным как статический, если все его компоненты статические. Тогда невозможно создать экземпляры этого класса. Статический класс мoжет быть удобен, например, для группирования множества объектно-независимых общих свойств, таких как математические функции.

    Уже упоминалось, что метод Main должен быть статическим; причина в том, что на старте выполнения не существует объекта, который мог бы вызвать метод. В Eiffel проблема решается за счет того, что выполнение определяется как создание "корневого объекта", к которому применяется "корневая процедура По поводу статических компонентов класса в Java и C# смотри мой комментарий в приложении по Java. Статический конструктор C# создает статический объект, который и является целью вызова статических компонентов. В статический конструктор можно добавить свой код, например, для определения специфических констант класса. Выполнение программы C#, как и в Eiffel, можно рассматривать как создание статическим конструктором корневого статического объекта, который и вызывает корневую процедуру - Main.".

    Статус экспорта

    Для скрытия информации каждый тип и компонент имеет уровень доступности, определяя права клиентов на доступ. Цель та же, что и в Eiifel: механизм скрытия информации, включая селективный экспорт, но с грубой гранулярностью, поскольку в C# нельзя создать список ВИП-персон - классов, которым будет доступен некий компонент класса. Тремя возможными квалификаторами являются:

  • public: доступен в любом коде;
  • internal: доступен в коде той же сборки;
  • private: доступен коду самого класса или структуры. Это статус по умолчанию для компонентов класса Есть, конечно, и четвертый квалификатор - protected, - позволяющий получить доступ потом кам класса. Возможна и комбинация protected, internal. Позже об этом будет сказано..
  • Применимы некоторые ограничения: класс может быть только internal (по умолчанию) или public, если он не является внутренним классом (классом, объявленным внутри другого класса), который может быть также и private. Деструкторы не могут иметь модификаторов доступа. Операции, определенные программистом, должны быть static и public. Доступность компонентов не может превосходить доступность класса. Не трудно видеть смысл, стоящий за каждым из этих правил Важным ограничением для C# является то, что компоненты интерфейсов объявляются без указания квалификаторов доступа..

    Поля

    C#-поля соответствуют атрибутам. В этой книге (вне приложения) используется другая терминология: под полем (динамическим понятием) понимается составляющая объекта, соответствующая компоненту генерирующего класса - атрибуту (статическое понятие). В C# один термин применяется для обоих понятий.

    При объявлении поля задается его тип (перед именем поля, как в T f, вместо f: T в Eiffel). Объявление может включать инициализацию поля, используя для этого символ присваивания =, после которого может идти константное выражение, вычислимое в момент компиляции и не содержащее других полей, отличных от констант и статических полей. Вот пример объявления двух полей:

    class A {
        public string s1 = "ABC";
        public readonly string s2 = "DEF";
        … Other member declarations … 
    }
    

    Заметьте: точка с запятой завершает все объявления и операторы. Квалификатор readonly защищает поля от присваивания, за исключением инициализации в момент объявления, как здесь, или в конструкторе.

    В отличие от Eiffel, экспорт полей без статуса readonly дает клиентам право на чтение и запись. Для приложений, признающих преимущества скрытия информации, это означает, что поля должны иметь статус по умолчанию private и при необходимости снабжаться специальными методами доступа - геттером и сеттером. C# упрощает их написание, введя понятия метода - свойства, изучаемого ниже.

    Базисные типы

    C# обеспечивает несколько встроенных типов:

  • 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 в Eiffel). Ссылка null (void) записывается как null.

    Ссылки и значения

    Каждый C# тип является ссылочным или значимым типом. Отличия те же, что и для Eiffel. Переменная значимого типа непосредственно обозначает значение, которое может быть простым значением только что рассмотренного типа (встроенные типы, за исключением string и object, являются значимыми типами) или сложным объектом. Переменная ссылочного типа обозначает ссылку на объект.

    Перейти от значения к ссылке можно, используя операцию, называемую боксингом, или упаковкой:

    int i; object o;
    i = 1;
    o = i;//Boxing: Создает объект, обертывающий значение, присоединяет o к нему
    

    Как показывает этот пример, операция боксинга выполняется автоматически при присваивании ссылке переменной значимого типа. Обратная операция - распаковка - должна выполняться явно, с использованием кастинга - приведения к типу:

    i=(int)o;//Распаковка: Получение целого, хранимого в o, и присваивании его i.
    

    Константы

    Поле может быть объявлено константой, как const, указывающее, что для всех экземпляров класса оно сохраняет значение, заданное при инициализации. Значение может быть литеральной константой (манифестным целым, строкой и так далее) или константным выражением, включающим ранее определенные константы:

    public const string s3 = "ABC-";
    public const string s4 = s3 + "DEF";  // Значение: "ABC-DEF"
    

    Так как константы относятся к статическим объектам, для доступа к ним используется имя класса A.s4, если приведенное объявление константы появилось в классе A.

    Заметьте разницу между const- и readonly-полями. Значения первых должны быть заданы при объявлении, значения вторых - могут быть заданы в конструкторе (например, в статическом конструкторе).

    Методы

    Метод в C# может быть реализован процедурой (возвращающей тип void - результат отсутствует) или функцией, возвращающей результат, тип которого отличен от void. Вот примеры, иллюстрирующие некоторые важные возможности:

    class B {
       public void p(int arg1, ref int arg2) {… arg2 = 0;}   // Процедура
       public string f() {… return "ABC";}	                 // Функция
       public static string sf() {… return "DEF";}	         // Статическая функция
    }
    

    По умолчанию аргументы передаются "по значению" (как в Eiffel), в этом случае формальный аргумент представляет копию фактического аргумента (в зависимости от типа - ссылочного или значимого - копия может быть ссылкой или полным объектом). Аргументы можно передавать "по ссылке", снабдив их описателем ref. В этом случае присваивание аргументу, такому как arg2, в процедуре p, будет модифицировать и фактический аргумент.

    Фактический аргумент, соответствующий ref-формальному аргументу, должен также специфицироваться как ref при вызове:

    B v = new B();
    int x = 1;
    int y = 1;
    v.p(x, ref y);      //Не изменяет x, но значение y становится равным нулю
    

    Объявление локальных переменных может появляться в теле метода, предваряя их использование. Имена не должны совпадать с именами формальных аргументов и других локальных переменных.

    Имена локальных переменных и формальных аргументов могут совпадать с именами полей класса, имея приоритет. Конфликт не возникает, поскольку поле класса можно квалифицировать именем текущего объекта this, как в примере:

    int a;	                        // a - имя поля класса
    r (int a) {this.a = a;}    // и формального аргумента
    

    Метод может получить доступ к полю, используя нотацию this.a. Для конструкторов C# типичной практикой является именовать аргумент, служащий для инициализации поля, именем этого поля. Лучше избегать этого и выбирать разные имена для каждой цели.

    Перегрузка

    C# допускает перегрузку методов: несколько методов класса могут иметь одинаковые имена, если их сигнатуры различны (отличаются числом аргументов или их типами, включая и описатель ref, входящий в сигнатуру). Тип результата в сигнатуру не входит.

    Предыдущий методологический комментарий применим и здесь. Имена не являются дефицитным ресурсом. Перегрузка, однако, является распространенной практикой в C# и требуется для конструкторов, что будет обсуждаться ниже.

    Методы-свойства

    Политика экспорта, как отмечалось, не различает доступ на чтение и на запись. Это значит, что поле никогда не следует экспортировать, так как это позволило бы клиентам выполнять прямые присваивания x.a = v полю с именем a, нарушая все принципы скрытия информации. ОО-решение в этом случае обеспечивает сеттер-процедура и геттер-функция (в которой нет необходимости в Eiffel, так как при экспорте гарантируется статус "только для чтения"). В C# написание геттеров и сеттеров стандартизовано благодаря введению понятия метода-свойства. Вот образец использования метода-свойства для закрытого поля:

    class C {
       private string a;         // Закрытое поле
       public string ap {	     // Метод-свойство
         get {return a;}	     // Геттер
         set {	// Сеттер
           a = value;	         // Изменение значения поля
           … Возможно, другие операторы …
    }}}
    

    Этот механизм использует три ключевых слова get, set и value. Объявляются два специальных метода с именами get и set для доступа к атрибуту, в случае сеттера - через синтаксис присваивания:

    C x = new C();
    string b;
    b = x.ap;	        // Использование геттера
    x.ap = "ABC";       // Использование сеттера
    

    Эффект подобен тому, что достигается в Eiffel спецификацией присваивания. Механизм Eiffel не требует геттера, который на практике сводится к чтению поля. Сеттер, чаще всего, - нечто большее, чем присваивание, (например, при изменениях поля может требоваться запись в журнал регистрации), так что нормально записывать его явно как процедуру.

    Конструкторы

    Процедуры создания, используемые для создания и инициализации объектов, называются в C# конструкторами. Конструктор может быть:

  • конструктором экземпляра, динамически создающим и инициализирующим объект;
  • статическим конструктором, создающим и инициализирующим статический объект.
  • Следующий класс содержит пример каждого типа:

    class D {
       public D(string a) {	         // Конструктор 1: экземпляра
            … Инициализирует поле, обычно использует аргумент a … 
       }
       static D() {	                 // Конструктор 2: статический
            … Инициализирует статические поля …
    } 
    }
    

    Конструкторы не имеют собственных имен, используя имя класса, основываясь на перегрузке и разрешая конфликты через отличия в сигнатурах, если есть более одного конструктора.

    Иногда возникают проблемы, например, классу POINT, описывающему точку на плоскости, полезно иметь два конструктора make_cartesian и make_polar, задающих координаты точки в декартовой и полярной системе координат. Оба конструктора имеют одинаковую сигнатуру - два аргумента типа float. Для разрешения конфликта одному из конструкторов приходится добавлять фиктивный аргумент.

    Объявление конструктора не специфицирует возвращаемый тип (void тоже не задается). У статического конструктора не может быть никаких других модификаторов.

    Создание нового объекта основано на операции new (create в Eiffel) и вызове конструктора экземпляра. Вот пример:

    D x = new D("ABC");
    

    В C# нет различия между объявлениями (статикой) и операторами (динамикой), позволяя в объявлениях создавать и инициализировать объекты, выполняя операцию new с вызовом конструктора, создающего объект - экземпляр класса D в примере.

    Это, однако, далеко не полная история о конструкторах экземпляра и создании экземпляра. Детальная спецификация (дается ниже при обсуждении наследования) объясняет, что ваш вызов конструктора может в результате приводить к вызову других конструкторов, создавая цепочку вызовов, которая должна включать вызов конструктора каждого предка класса.

    Статический конструктор, существующий в единственном экземпляре, без аргументов, что отражено в примере, выполняется до того, как потребуется экземпляр класса или доступ к статическому элементу класса. Это позволяет инициализировать свойства, связанные с классом в целом, прежде чем появятся специфические экземпляры (как это делается в Eiffel через однократные once-функции). Представьте экземпляр системы, фиксирующей ошибки, где ошибки записываются в специальный журнал (файл). Первое появление ошибки приводит к созданию и открытию этого файла.

    Деструкторы

    В C# предполагается существование сборщика мусора: в то время как создание объектов выполняется явно при задании операции new, освобождение памяти для неиспользуемых более объектов возлагается на автоматический механизм - сборщик мусора (Garbage Collector - GC). Иногда можно попросить GC выполнить специфическую операцию, помимо освобождения памяти. Типичным примером является работа с файлами. Всякий раз, когда закончена работа с объектом, представляющим файл, требуется закрыть физический файл, связанный с объектом (в Eiffel можно для этих целей вызывать процедуру 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-компоненту Eiffel). Операции встроены в базисные типы, такие как int и float. Имена (знаки) операций и их приоритеты фиксированы (в отличие от Eiffel, нельзя определять собственные знаки операций). Главными доступными знаками операций являются:

    +    -   ~   !	 ++
    

    Здесь "!" - отрицание для булевских, "~" - отрицание для целых (взаимное обращение 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] задает элемент во второй строке и втором столбце. Такая политика требует внимательности при работе с индексами.

    В объявление типа массива границы не входят. Массивам память отводится динамически (как в Eiffel). Присваивание:

    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) " []" - "квадратные скобки" в Eiffel. Определение индексатора является обобщением метода-свойства:

    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# знакома по Eiffel; родовые параметры заключаются в угловые скобки <…>. Объявим:

    class F<G, H> where H: T, new() {
        … Объявление класса … 
    }
    

    Класс F имеет два родовых параметра. Для H задано ограничение (как в классе C [G, H -> T] в Eiffel), так что любой фактический родовой параметр должен наследовать от T. Включение в ограничение new() означает (как в Eiffel, если объявление имеет вид C [G, H -> T create make end] для процедуры создания make из T), что T должен обеспечить public-кон-структор без аргументов; это позволит методам F создавать экземпляры T.

    Родовое порождение также использует угловые скобки:

    F<V, W>  //W должен быть согласован с T и иметь конструктор без аргументов.
    

    Основные операторы

    Дадим обзор основных операторов языка.

    Присваивание, уже появляющееся в примерах, использует знак равенства:

    var = e;
    

    Заметьте: точка с запятой является не разделителем операторов, а завершителем, и должна завершать любой оператор.

    Вызов метода использует имя метода и список аргументов. В отличие от вызова в Eiffel, круглые скобки всегда сопровождают вызов метода, даже если список аргументов пуст, как в methodWithNoArgument ().

    Оператор return не имеет прямого аналога в Eiffel. Он обязателен для функций, задавая значение, возвращаемое функцией:

    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, если она задана, в противном случае ничего не делается (в Eiffel в отсутствие ветви else в операторе inspect в подобной ситуации в период выполнения генерируется ошибка). Оператор switch не задает конструкцию с одним входом и одним выходом, а представляет многоцелевой goto. Для правильной структурированности следует четко следовать показанной схеме. Принудительный оператор break, завершающий каждую ветвь, позволяет избежать типичной ошибки для C++ и C-версии switch, когда управление проваливается в другую ветвь.

    Доступно несколько форм оператора цикла. Наиболее общая идет от С и соотносится с конструкцией from в Eiffel:

    for (initialization; exit; modification) {
    … body …
    }
    

    Вначале выполняется инициализация цикла - initialization - и работа цикла заканчивается, если условие выхода из цикла - exit - получает в результате значение true. Обычно оно не выполняется после инициализации, и тогда выполняется тело цикла, а затем модификация параметров цикла - modification, после чего снова проверяется условие выхода. Цикл завершается, когда выполняется условие выхода. Модификация обеспечивает продвижение к следующему шагу, увеличивая индекс, продвигая курсор (в Eiffel она интегрирована с телом цикла).

    Можно использовать циклы в стиле 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# предлагается механизм делегатов (аналог агентов Eiffel) для описания методов как объектов. Ассоциированный механизм - события - дополняет делегаты для программирования, управляемого событиями (в Eiffel типы событий описываются как обычные объекты и, следовательно, нет необходимости в специальных конструкциях). Рассмотрим объявление делегата:

    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 - формальный аргумент, и вместо присваивания, такого как [6], передается делегат в качестве фактического аргумента:

    r (lettercount);
    

    Эквивалент Eiffel комбинации [4] или [5] задается одним оператором d:= agent lettercount, не требующим объявления типа, такого как [4]. Эта форма не имеет прямого эквивалента в C#, но можно использовать делегаты с "анонимными методами" (эквивалент встроенных агентов), как в примере:

    r (delegate (string s) {return lettercount (s);} ); 
    

    Здесь используется анонимный метод. Заметьте, что анонимный метод объявляет сигнатуру, но не объявляет тип результата. Хотя в C# нет прямого эквивалента Eiffel-понятия "открытый аргумент", но анонимные методы позволяют достичь того же результата.

    Эквивалентом встроенного агента Eiffel является лямбда-выражение 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# можно объявлять абстрактными как отдельные методы, так и класс в целом (это соответствует отложенным методам и классам Eiffel, но без контрактов):

    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# нет явного механизма переименования, аналогичного Eiffel, но конфликта можно избежать, используя в качестве префикса имя интерфейса и нотацию с точкой, например, IAnotherInterface.clashingname.

    Доступность и наследование

    В связи с наследованием в дополнение к трем ранее рассмотренным модификаторам доступа (public, internal, private) добавляется новый модификатор и новая комбинация:

  • protected: доступно для потомков;
  • protected internal: доступно потомкам и классам той же сборки.
  • Когда переопределяется наследуемый компонент, как обсуждается далее, статус доступа изменить нельзя. Этим язык отличается от Java, где разрешается расширить права доступа, но не ограничить их. Кроме того, потомки не могут иметь доступность большую, чем их предки.

    Переопределение и динамическое связывание

    Наследуемый метод можно переопределить. Соглашения, однако, отличаются от других современных языков программирования. Как в С++, и в отличие от Eiffel и Java, связывание в C# - удивительно для языка, первая версия которого появилась в 1999 году, - статическое. Другими словами, версия f, выполняемая при вызове a.f, будет следовать объявлению a, но не динамическому типу объекта, связанному с а в момент выполнения. Чтобы выполнялось динамическое связывание, метод должен быть объявлен как виртуальный - virtual:

    class P {
       public virtual void f (…) {…}
       … 
    }
    

    Потомок переопределяет виртуальный метод, в точности сохраняя сигнатуру и заменяя модификатор virtual на override:

    class Q: P {
       public override void f (…) {…}
       … 
    }
    

    Оригинальная версия переопределяемого метода остается доступной, как с Precursor в Eiffel, для этого применяется нотация с точкой, а в качестве префикса используется имя родителя - 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 (как в тесте объектов Eiffel) можно использовать два механизма.

  • Явный кастинг - написать (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# обеспечивает интересный механизм расширения методов: методы добавляются извне к существующему классу.

    Очевидно, методы расширения представляют просто синтаксическое упрощение, так как проблему можно решить за счет статических методов: в любой класс можно добавить статический метод sm, который будет вызываться как sm (x1, other_args) с 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).

    Атрибуты

    По своей природе языки программирования ограничены семантическими механизмами, предусмотренными их создателями. Иногда возникает желание добавить новые свойства, не изменяющие существующую семантику, но полезные, например, для документирования или сериализации.

    Вполне разумно для языка программирования обеспечить возможность таких расширений, известных как включение метаданных, - поддерживающей информации, добавляемой в документ, отделенной от основного содержания. В Eiffel именно для такой цели используются предложения note, связанные с классами и методами. В .NET и C# поддержка метаданных обеспечивается в форме атрибутов (не путайте с ОО-понятием атрибута, используемым в этой книге, для которого в C# применяется термин "поле").

    Некоторые атрибуты предопределены, но программисты могут определять собственные атрибуты, известные как атрибуты пользователя.

    Примером предопределенного атрибута является атрибут сериализации Serializable, который можно присоединить к классу, указав тем самым, что экземпляры могут быть конвертированы в некоторое внешнее представление и храниться во внешней памяти:

    [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 полей атрибутного класса, так называемых полей, допускающих запись. В данном примере таким является поле revision:

    [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, Method, Parameter, Struct. Разрешается задавать несколько целей, разделяя их символом вертикальной черты |.

    Для элементов, снабженных атрибутами, можно получать значения атрибутов через процесс, называемый отражением. Рассмотрим вызов объекта о:

    o.GetType().GetCustomAttributes(true);
    

    Результатом будет массив, содержащий атрибуты, определенные для класса объекта, с их значениями.

    Отсутствующие элементы

    В C# отсутствуют несколько ОО-механизмов, интенсивно применяемых в этой книге, в первую очередь - контракты и множественное наследование.

    Контракты введены в исследовательскую версию языка C# - SpecC#, разработанную в Microsoft Research. Эта версия языка доступна для свободного использования.

    Специфические свойства языка

    Полезно ознакомиться с несколькими поддерживающими C# конструкциями.

    Небезопасный код

    В C# комбинируются строгие требования к безопасности типов с предоставлением программистам возможности работы на низком уровне, характерном для языка С или ассемблера. Конструкция "unsafe" поддерживает четкое разделение между небезопасными элементами и нормальными, прошедшими проверку типов.

    Объявление метода небезопасным (unsafe) означает, что область данных расположена вне "кучи" - области, отводимой под объекты. Небезопасный метод может осуществлять в своей области непосредственные манипуляции с указателями и, следовательно, игнорировать нормальные правила работы с данными.

    Тип "перечисление"

    Типы, заданные перечислением, позволяют оперировать значениями из конечного множества предопределенных значений:

    enum CardColors {Spades, Hearts, Diamonds, Clubs}
    

    Значения, заданные перечислением, проецируются на целочисленный тип, по умолчанию int, но можно задать и другой тип, как в enum T:long {…}. Значения проецируются на отрезок, начинающийся с 0, но можно задать и другое начало:

    enum CardColors1 {Spades = 1, Hearts, Diamonds, Clubs}
    

    Значения можно обозначать, используя нотацию с точкой, как в CardColors.Spades. Допустимы взаимные преобразования между целыми и значениями перечисления Наиболее интересное применение перечислений - это шкалы. В этом случае каждый к-й элемент перечисления проецируется на значение 2к. Тогда переменная типа "перечисление" интерпретируется как набор битов, над которыми определены, как мы знаем, логические операции. Если необходимо работать с множеством объектов, характеризуемых набором бинарных свойств (свойство присутствует у объекта или нет; например, знает данный программист ОО-концепции или нет), то шкалы - прекрасный инструмент для таких задач..

    Linq

    В версии языка C# 3.0 появились новые важные механизмы, привлекшие внимание программистов. Механизм, известный как Linq, позволяет языку программирования работать непосредственно с базами данных и Web. Для работы с данными используется типичный язык запросов SQL и XML для Web. Обычно такие возможности поддерживаются с помощью специальных библиотек, обеспечивающих интерфейс к реляционным базам данных или к протоколу HTTP. Оригинальность Linq в том, что он делает все гораздо выразительнее, оставаясь в рамках языка программирования. Например, запрос к базе данных можно выполнить так:

    from e in Employees where e.salary > median select e.rank
    

    Здесь идет ссылка на отношение Employees из базы данных. В результате создается спиок всех служащих, ранжированных по зарплате. Стиль тот же, что и у SQL-запросов, но запрос интегрирован в язык. Все используемые объекты - это нормальные объекты программы, такие как списки.

    Лексические аспекты

    В C# идентификаторы следуют соглашениям, подобным Eiffel. Они могут, однако, начинаться с подчеркивания, хотя эта возможность редко используется в обычных приложениях. Важная разница в том, что идентификаторы чувствительны к регистру: 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", Интернет-Университет. Питер, Лаборатория знаний, 2010 Не могу удержаться от рекламирования собственного учебника, тем более, что он соответствует по духу этой книге..

    Страницы:

    При появлении языка 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, приведенное в предыдущем приложении (как следствие, повторяются некоторые рассуждения, когда рассматриваются разделяемые концепции). Подобно другим приложениям, язык не рассматривается с чистого листа, - обсуждение предполагает знакомство с программистскими концепциями, введенными в этой книге. Описание языка сопровождается сравнением с соответствующими механизмами Eiffel.

    Окружение языка и стиль

    C# (произносится "C шарп") тесно связан с окружением Microsoft .NET, платформой для разработки и выполнения ПО, использующей виртуальную машину. В предыдущих обсуждениях отмечалась роль виртуальных машин и их преимущества для реализации языков высокого уровня.

    .NET, CLI и взаимодействие с языком

    В то время как виртуальная машина Java - JVM - была спроектирована специально для поддержки этого языка (хотя позднее она использовалась для реализации других языков программирования), главная цель проекта платформы .NET состояла с самого начала в поддержке нескольких языков. Это решение отражается как в имени виртуальной машины - Common Language Runtime (CLR) - "Общеязыковая Среда Выполнения", так и в поддержке взаимодействия API, Common Language Infrastructure (CLI) - "Общеязыковой Инфраструктуры", которая теперь является международным стандартом.

    Частично причина была в том, что Microsoft еще до .NET обеспечил реализацию нескольких языков, прежде всего, Visual Basic (VB), C++ и JScript для клиентских Web-приложений. VB популярен на массовом рынке и часто используется для разработки приложений по настройке офисных документов. Компания не могла, естественно, предложить соответствующим программистским сообществам бросить свои любимые языки и перейти на новый бренд. Она смогла обеспечить общую базу для взаимодействия и будущей эволюции. .NET и CLR/CLI были способны с самого начала обеспечить реализацию четырех поддерживаемых Microsoft языков (помимо трех упомянутых еще и C#), а также языки, разрабатываемые другими компаниями, включая Eiffel (с самого начала введения .NET в 1999) и Cobol, язык прошлых лет, но все еще важный для многих бизнес-приложений.

    Языковая открытость среды означала не только доступность нескольких компиляторов, но и влекла высокую степень взаимодействия между программами, написанными на разных языках. В этом роль Общеязыковой инфраструктуры: обеспечить стандартное множество механизмов, которые могут быть использованы в любом языке. CLI представляет объектную модель, близкую ОО-языку, но без синтаксиса. Эта модель задает множество хорошо определенных механизмов - это ОО механизмы, изучаемые в этой книге: классы, их компоненты, наследование, универсальность, система типов, объекты, политика динамического создания объектов и сборка мусора - для которых CLI-проект предложил несколько специфических проектных решений.

    При условии, что .NET-языки не слишком отклоняются от этих решений, они могут достичь степени взаимодействия, неслыханной в дни, предшествующие .NET. В частности, классы, написанные на разных языках, могут взаимодействовать друг с другом, используя как отношения наследования, так и клиентские. Например, Eiffel-класс может наследовать от класса C#, возможно и обратное наследование. Это просто предполагает, что компиляторы следуют единым CLI-правилам для поставщиков и клиентов сборок (целевых модулей, создаваемых .NET-компиляторами).

    Эта схема взаимодействия доказала свою успешность (несмотря на существующую тенденцию производителей ПО игнорировать правила CLI-совместимости). Она позволяет каждому языку сохранять свою индивидуальность, пока ее можно отобразить в объектную модель CLI. Например, реализация Eiffel должна моделировать множественное наследование - не поддерживаемое напрямую CLI - через специальное использование CLI-механизмов (множественное наследование интерфейсов, концепцию, обсуждаемую позже в этом приложении).

    Любимый сын

    В сообществе языков, как и у людей, все языки равны, но некоторые равны более других. Язык C# - это любимый сын: его объектная модель наиболее тесно связана с CLI. VB .NET, который схож с предыдущей версией только синтаксически, является еще одним претендентом на звание "любимого". CLI-совместимая версия C++ - "управляемый C++" - существенно отличается от обычного C++. Ограничения необходимы, чтобы язык мог принимать участие в играх .NET-взаимодействия. Фактически, семантика C# определялась семантикой CLI, хотя последующие версии ее существенно расширили. Синтаксис языка соответствует традиции C, C++, Java, включая завершение операторов символом точки с запятой и применением фигурных скобок для окаймления блоков программы.

    Общая структура программы

    Базисными элементами C# программы являются классы и структуры, организованные в виде нескольких программных файлов.

    Классы и структуры

    C#-классы (ключевое слово class) и структуры (ключевое слово struct) задают описание множества возможных объектов периода выполнения. Объекты обладают свойствами, и к ним применимы методы. Общая форма объявления такова:

    class name {
       … Объявление компонентов … 
    }
    

    При объявлении структуры вместо ключевого слова class используется слово struct. Компоненты могут быть разные. Это может быть:

  • поле, соответствующее атрибуту Eiffel;
  • константа, частный случай поля;
  • метод, реализованный в виде процедуры или функции;
  • метод-свойство, поле, сопровождаемое возможными методами - геттером и сеттером;
  • операция, функция с синтаксисом операции;
  • конструктор, процедура создания - метод, применяемый для создания объектов;
  • деструктор, редко применяемый метод для освобождения ресурсов, возможный только для классов;
  • событие, которое связано с делегатами и программированием, управляемым событиями;
  • индексатор;
  • вложенный тип.
  • Структура является упрощенной формой класса без возможности наследования. Остальная часть обсуждения фокусируется на классах, но большинство свойств, не связанных с наследованием, применимо и к структурам Главное отличие классов от структур состоит в том, что класс определяет ссылочный тип, а структура - развернутый. Значения ссылочного типа разделяют память - на один и тот же объект может указывать несколько ссылок, имена ссылок являются синонимами. Объект развернутого типа ни с кем свою память не разделяет. Все примитивные типы - арифметический и другие - реализованы как структуры..

    Классы и структуры группируются в сборки (понятие, соответствующее кластеру в 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 в Eiffel), в C# имеет имя this, которое, как и в Eiffel, можно опускать, когда из контекста ясно, что речь идет о текущем объекте.

    В C# разрешается объявлять статические члены, не требующие объекта и вызываемые как C.member, где C - имя класса. Определение статического компонента, например статического метода, может использовать только статические компоненты.

    Класс в целом может также быть объявленным как статический, если все его компоненты статические. Тогда невозможно создать экземпляры этого класса. Статический класс мoжет быть удобен, например, для группирования множества объектно-независимых общих свойств, таких как математические функции.

    Уже упоминалось, что метод Main должен быть статическим; причина в том, что на старте выполнения не существует объекта, который мог бы вызвать метод. В Eiffel проблема решается за счет того, что выполнение определяется как создание "корневого объекта", к которому применяется "корневая процедура По поводу статических компонентов класса в Java и C# смотри мой комментарий в приложении по Java. Статический конструктор C# создает статический объект, который и является целью вызова статических компонентов. В статический конструктор можно добавить свой код, например, для определения специфических констант класса. Выполнение программы C#, как и в Eiffel, можно рассматривать как создание статическим конструктором корневого статического объекта, который и вызывает корневую процедуру - Main.".

    Статус экспорта

    Для скрытия информации каждый тип и компонент имеет уровень доступности, определяя права клиентов на доступ. Цель та же, что и в Eiifel: механизм скрытия информации, включая селективный экспорт, но с грубой гранулярностью, поскольку в C# нельзя создать список ВИП-персон - классов, которым будет доступен некий компонент класса. Тремя возможными квалификаторами являются:

  • public: доступен в любом коде;
  • internal: доступен в коде той же сборки;
  • private: доступен коду самого класса или структуры. Это статус по умолчанию для компонентов класса Есть, конечно, и четвертый квалификатор - protected, - позволяющий получить доступ потом кам класса. Возможна и комбинация protected, internal. Позже об этом будет сказано..
  • Применимы некоторые ограничения: класс может быть только internal (по умолчанию) или public, если он не является внутренним классом (классом, объявленным внутри другого класса), который может быть также и private. Деструкторы не могут иметь модификаторов доступа. Операции, определенные программистом, должны быть static и public. Доступность компонентов не может превосходить доступность класса. Не трудно видеть смысл, стоящий за каждым из этих правил Важным ограничением для C# является то, что компоненты интерфейсов объявляются без указания квалификаторов доступа..

    Поля

    C#-поля соответствуют атрибутам. В этой книге (вне приложения) используется другая терминология: под полем (динамическим понятием) понимается составляющая объекта, соответствующая компоненту генерирующего класса - атрибуту (статическое понятие). В C# один термин применяется для обоих понятий.

    При объявлении поля задается его тип (перед именем поля, как в T f, вместо f: T в Eiffel). Объявление может включать инициализацию поля, используя для этого символ присваивания =, после которого может идти константное выражение, вычислимое в момент компиляции и не содержащее других полей, отличных от констант и статических полей. Вот пример объявления двух полей:

    class A {
        public string s1 = "ABC";
        public readonly string s2 = "DEF";
        … Other member declarations … 
    }
    

    Заметьте: точка с запятой завершает все объявления и операторы. Квалификатор readonly защищает поля от присваивания, за исключением инициализации в момент объявления, как здесь, или в конструкторе.

    В отличие от Eiffel, экспорт полей без статуса readonly дает клиентам право на чтение и запись. Для приложений, признающих преимущества скрытия информации, это означает, что поля должны иметь статус по умолчанию private и при необходимости снабжаться специальными методами доступа - геттером и сеттером. C# упрощает их написание, введя понятия метода - свойства, изучаемого ниже.

    Базисные типы

    C# обеспечивает несколько встроенных типов:

  • 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 в Eiffel). Ссылка null (void) записывается как null.

    Ссылки и значения

    Каждый C# тип является ссылочным или значимым типом. Отличия те же, что и для Eiffel. Переменная значимого типа непосредственно обозначает значение, которое может быть простым значением только что рассмотренного типа (встроенные типы, за исключением string и object, являются значимыми типами) или сложным объектом. Переменная ссылочного типа обозначает ссылку на объект.

    Перейти от значения к ссылке можно, используя операцию, называемую боксингом, или упаковкой:

    int i; object o;
    i = 1;
    o = i;//Boxing: Создает объект, обертывающий значение, присоединяет o к нему
    

    Как показывает этот пример, операция боксинга выполняется автоматически при присваивании ссылке переменной значимого типа. Обратная операция - распаковка - должна выполняться явно, с использованием кастинга - приведения к типу:

    i=(int)o;//Распаковка: Получение целого, хранимого в o, и присваивании его i.
    

    Константы

    Поле может быть объявлено константой, как const, указывающее, что для всех экземпляров класса оно сохраняет значение, заданное при инициализации. Значение может быть литеральной константой (манифестным целым, строкой и так далее) или константным выражением, включающим ранее определенные константы:

    public const string s3 = "ABC-";
    public const string s4 = s3 + "DEF";  // Значение: "ABC-DEF"
    

    Так как константы относятся к статическим объектам, для доступа к ним используется имя класса A.s4, если приведенное объявление константы появилось в классе A.

    Заметьте разницу между const- и readonly-полями. Значения первых должны быть заданы при объявлении, значения вторых - могут быть заданы в конструкторе (например, в статическом конструкторе).

    Методы

    Метод в C# может быть реализован процедурой (возвращающей тип void - результат отсутствует) или функцией, возвращающей результат, тип которого отличен от void. Вот примеры, иллюстрирующие некоторые важные возможности:

    class B {
       public void p(int arg1, ref int arg2) {… arg2 = 0;}   // Процедура
       public string f() {… return "ABC";}	                 // Функция
       public static string sf() {… return "DEF";}	         // Статическая функция
    }
    

    По умолчанию аргументы передаются "по значению" (как в Eiffel), в этом случае формальный аргумент представляет копию фактического аргумента (в зависимости от типа - ссылочного или значимого - копия может быть ссылкой или полным объектом). Аргументы можно передавать "по ссылке", снабдив их описателем ref. В этом случае присваивание аргументу, такому как arg2, в процедуре p, будет модифицировать и фактический аргумент.

    Фактический аргумент, соответствующий ref-формальному аргументу, должен также специфицироваться как ref при вызове:

    B v = new B();
    int x = 1;
    int y = 1;
    v.p(x, ref y);      //Не изменяет x, но значение y становится равным нулю
    

    Объявление локальных переменных может появляться в теле метода, предваряя их использование. Имена не должны совпадать с именами формальных аргументов и других локальных переменных.

    Имена локальных переменных и формальных аргументов могут совпадать с именами полей класса, имея приоритет. Конфликт не возникает, поскольку поле класса можно квалифицировать именем текущего объекта this, как в примере:

    int a;	                        // a - имя поля класса
    r (int a) {this.a = a;}    // и формального аргумента
    

    Метод может получить доступ к полю, используя нотацию this.a. Для конструкторов C# типичной практикой является именовать аргумент, служащий для инициализации поля, именем этого поля. Лучше избегать этого и выбирать разные имена для каждой цели.

    Перегрузка

    C# допускает перегрузку методов: несколько методов класса могут иметь одинаковые имена, если их сигнатуры различны (отличаются числом аргументов или их типами, включая и описатель ref, входящий в сигнатуру). Тип результата в сигнатуру не входит.

    Предыдущий методологический комментарий применим и здесь. Имена не являются дефицитным ресурсом. Перегрузка, однако, является распространенной практикой в C# и требуется для конструкторов, что будет обсуждаться ниже.

    Методы-свойства

    Политика экспорта, как отмечалось, не различает доступ на чтение и на запись. Это значит, что поле никогда не следует экспортировать, так как это позволило бы клиентам выполнять прямые присваивания x.a = v полю с именем a, нарушая все принципы скрытия информации. ОО-решение в этом случае обеспечивает сеттер-процедура и геттер-функция (в которой нет необходимости в Eiffel, так как при экспорте гарантируется статус "только для чтения"). В C# написание геттеров и сеттеров стандартизовано благодаря введению понятия метода-свойства. Вот образец использования метода-свойства для закрытого поля:

    class C {
       private string a;         // Закрытое поле
       public string ap {	     // Метод-свойство
         get {return a;}	     // Геттер
         set {	// Сеттер
           a = value;	         // Изменение значения поля
           … Возможно, другие операторы …
    }}}
    

    Этот механизм использует три ключевых слова get, set и value. Объявляются два специальных метода с именами get и set для доступа к атрибуту, в случае сеттера - через синтаксис присваивания:

    C x = new C();
    string b;
    b = x.ap;	        // Использование геттера
    x.ap = "ABC";       // Использование сеттера
    

    Эффект подобен тому, что достигается в Eiffel спецификацией присваивания. Механизм Eiffel не требует геттера, который на практике сводится к чтению поля. Сеттер, чаще всего, - нечто большее, чем присваивание, (например, при изменениях поля может требоваться запись в журнал регистрации), так что нормально записывать его явно как процедуру.

    Конструкторы

    Процедуры создания, используемые для создания и инициализации объектов, называются в C# конструкторами. Конструктор может быть:

  • конструктором экземпляра, динамически создающим и инициализирующим объект;
  • статическим конструктором, создающим и инициализирующим статический объект.
  • Следующий класс содержит пример каждого типа:

    class D {
       public D(string a) {	         // Конструктор 1: экземпляра
            … Инициализирует поле, обычно использует аргумент a … 
       }
       static D() {	                 // Конструктор 2: статический
            … Инициализирует статические поля …
    } 
    }
    

    Конструкторы не имеют собственных имен, используя имя класса, основываясь на перегрузке и разрешая конфликты через отличия в сигнатурах, если есть более одного конструктора.

    Иногда возникают проблемы, например, классу POINT, описывающему точку на плоскости, полезно иметь два конструктора make_cartesian и make_polar, задающих координаты точки в декартовой и полярной системе координат. Оба конструктора имеют одинаковую сигнатуру - два аргумента типа float. Для разрешения конфликта одному из конструкторов приходится добавлять фиктивный аргумент.

    Объявление конструктора не специфицирует возвращаемый тип (void тоже не задается). У статического конструктора не может быть никаких других модификаторов.

    Создание нового объекта основано на операции new (create в Eiffel) и вызове конструктора экземпляра. Вот пример:

    D x = new D("ABC");
    

    В C# нет различия между объявлениями (статикой) и операторами (динамикой), позволяя в объявлениях создавать и инициализировать объекты, выполняя операцию new с вызовом конструктора, создающего объект - экземпляр класса D в примере.

    Это, однако, далеко не полная история о конструкторах экземпляра и создании экземпляра. Детальная спецификация (дается ниже при обсуждении наследования) объясняет, что ваш вызов конструктора может в результате приводить к вызову других конструкторов, создавая цепочку вызовов, которая должна включать вызов конструктора каждого предка класса.

    Статический конструктор, существующий в единственном экземпляре, без аргументов, что отражено в примере, выполняется до того, как потребуется экземпляр класса или доступ к статическому элементу класса. Это позволяет инициализировать свойства, связанные с классом в целом, прежде чем появятся специфические экземпляры (как это делается в Eiffel через однократные once-функции). Представьте экземпляр системы, фиксирующей ошибки, где ошибки записываются в специальный журнал (файл). Первое появление ошибки приводит к созданию и открытию этого файла.

    Деструкторы

    В C# предполагается существование сборщика мусора: в то время как создание объектов выполняется явно при задании операции new, освобождение памяти для неиспользуемых более объектов возлагается на автоматический механизм - сборщик мусора (Garbage Collector - GC). Иногда можно попросить GC выполнить специфическую операцию, помимо освобождения памяти. Типичным примером является работа с файлами. Всякий раз, когда закончена работа с объектом, представляющим файл, требуется закрыть физический файл, связанный с объектом (в Eiffel можно для этих целей вызывать процедуру 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-компоненту Eiffel). Операции встроены в базисные типы, такие как int и float. Имена (знаки) операций и их приоритеты фиксированы (в отличие от Eiffel, нельзя определять собственные знаки операций). Главными доступными знаками операций являются:

    +    -   ~   !	 ++
    

    Здесь "!" - отрицание для булевских, "~" - отрицание для целых (взаимное обращение 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] задает элемент во второй строке и втором столбце. Такая политика требует внимательности при работе с индексами.

    В объявление типа массива границы не входят. Массивам память отводится динамически (как в Eiffel). Присваивание:

    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) " []" - "квадратные скобки" в Eiffel. Определение индексатора является обобщением метода-свойства:

    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# знакома по Eiffel; родовые параметры заключаются в угловые скобки <…>. Объявим:

    class F<G, H> where H: T, new() {
        … Объявление класса … 
    }
    

    Класс F имеет два родовых параметра. Для H задано ограничение (как в классе C [G, H -> T] в Eiffel), так что любой фактический родовой параметр должен наследовать от T. Включение в ограничение new() означает (как в Eiffel, если объявление имеет вид C [G, H -> T create make end] для процедуры создания make из T), что T должен обеспечить public-кон-структор без аргументов; это позволит методам F создавать экземпляры T.

    Родовое порождение также использует угловые скобки:

    F<V, W>  //W должен быть согласован с T и иметь конструктор без аргументов.
    

    Основные операторы

    Дадим обзор основных операторов языка.

    Присваивание, уже появляющееся в примерах, использует знак равенства:

    var = e;
    

    Заметьте: точка с запятой является не разделителем операторов, а завершителем, и должна завершать любой оператор.

    Вызов метода использует имя метода и список аргументов. В отличие от вызова в Eiffel, круглые скобки всегда сопровождают вызов метода, даже если список аргументов пуст, как в methodWithNoArgument ().

    Оператор return не имеет прямого аналога в Eiffel. Он обязателен для функций, задавая значение, возвращаемое функцией:

    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, если она задана, в противном случае ничего не делается (в Eiffel в отсутствие ветви else в операторе inspect в подобной ситуации в период выполнения генерируется ошибка). Оператор switch не задает конструкцию с одним входом и одним выходом, а представляет многоцелевой goto. Для правильной структурированности следует четко следовать показанной схеме. Принудительный оператор break, завершающий каждую ветвь, позволяет избежать типичной ошибки для C++ и C-версии switch, когда управление проваливается в другую ветвь.

    Доступно несколько форм оператора цикла. Наиболее общая идет от С и соотносится с конструкцией from в Eiffel:

    for (initialization; exit; modification) {
    … body …
    }
    

    Вначале выполняется инициализация цикла - initialization - и работа цикла заканчивается, если условие выхода из цикла - exit - получает в результате значение true. Обычно оно не выполняется после инициализации, и тогда выполняется тело цикла, а затем модификация параметров цикла - modification, после чего снова проверяется условие выхода. Цикл завершается, когда выполняется условие выхода. Модификация обеспечивает продвижение к следующему шагу, увеличивая индекс, продвигая курсор (в Eiffel она интегрирована с телом цикла).

    Можно использовать циклы в стиле 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# предлагается механизм делегатов (аналог агентов Eiffel) для описания методов как объектов. Ассоциированный механизм - события - дополняет делегаты для программирования, управляемого событиями (в Eiffel типы событий описываются как обычные объекты и, следовательно, нет необходимости в специальных конструкциях). Рассмотрим объявление делегата:

    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 - формальный аргумент, и вместо присваивания, такого как [6], передается делегат в качестве фактического аргумента:

    r (lettercount);
    

    Эквивалент Eiffel комбинации [4] или [5] задается одним оператором d:= agent lettercount, не требующим объявления типа, такого как [4]. Эта форма не имеет прямого эквивалента в C#, но можно использовать делегаты с "анонимными методами" (эквивалент встроенных агентов), как в примере:

    r (delegate (string s) {return lettercount (s);} ); 
    

    Здесь используется анонимный метод. Заметьте, что анонимный метод объявляет сигнатуру, но не объявляет тип результата. Хотя в C# нет прямого эквивалента Eiffel-понятия "открытый аргумент", но анонимные методы позволяют достичь того же результата.

    Эквивалентом встроенного агента Eiffel является лямбда-выражение 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# можно объявлять абстрактными как отдельные методы, так и класс в целом (это соответствует отложенным методам и классам Eiffel, но без контрактов):

    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# нет явного механизма переименования, аналогичного Eiffel, но конфликта можно избежать, используя в качестве префикса имя интерфейса и нотацию с точкой, например, IAnotherInterface.clashingname.

    Доступность и наследование

    В связи с наследованием в дополнение к трем ранее рассмотренным модификаторам доступа (public, internal, private) добавляется новый модификатор и новая комбинация:

  • protected: доступно для потомков;
  • protected internal: доступно потомкам и классам той же сборки.
  • Когда переопределяется наследуемый компонент, как обсуждается далее, статус доступа изменить нельзя. Этим язык отличается от Java, где разрешается расширить права доступа, но не ограничить их. Кроме того, потомки не могут иметь доступность большую, чем их предки.

    Переопределение и динамическое связывание

    Наследуемый метод можно переопределить. Соглашения, однако, отличаются от других современных языков программирования. Как в С++, и в отличие от Eiffel и Java, связывание в C# - удивительно для языка, первая версия которого появилась в 1999 году, - статическое. Другими словами, версия f, выполняемая при вызове a.f, будет следовать объявлению a, но не динамическому типу объекта, связанному с а в момент выполнения. Чтобы выполнялось динамическое связывание, метод должен быть объявлен как виртуальный - virtual:

    class P {
       public virtual void f (…) {…}
       … 
    }
    

    Потомок переопределяет виртуальный метод, в точности сохраняя сигнатуру и заменяя модификатор virtual на override:

    class Q: P {
       public override void f (…) {…}
       … 
    }
    

    Оригинальная версия переопределяемого метода остается доступной, как с Precursor в Eiffel, для этого применяется нотация с точкой, а в качестве префикса используется имя родителя - 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 (как в тесте объектов Eiffel) можно использовать два механизма.

  • Явный кастинг - написать (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# обеспечивает интересный механизм расширения методов: методы добавляются извне к существующему классу.

    Очевидно, методы расширения представляют просто синтаксическое упрощение, так как проблему можно решить за счет статических методов: в любой класс можно добавить статический метод sm, который будет вызываться как sm (x1, other_args) с 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).

    Атрибуты

    По своей природе языки программирования ограничены семантическими механизмами, предусмотренными их создателями. Иногда возникает желание добавить новые свойства, не изменяющие существующую семантику, но полезные, например, для документирования или сериализации.

    Вполне разумно для языка программирования обеспечить возможность таких расширений, известных как включение метаданных, - поддерживающей информации, добавляемой в документ, отделенной от основного содержания. В Eiffel именно для такой цели используются предложения note, связанные с классами и методами. В .NET и C# поддержка метаданных обеспечивается в форме атрибутов (не путайте с ОО-понятием атрибута, используемым в этой книге, для которого в C# применяется термин "поле").

    Некоторые атрибуты предопределены, но программисты могут определять собственные атрибуты, известные как атрибуты пользователя.

    Примером предопределенного атрибута является атрибут сериализации Serializable, который можно присоединить к классу, указав тем самым, что экземпляры могут быть конвертированы в некоторое внешнее представление и храниться во внешней памяти:

    [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 полей атрибутного класса, так называемых полей, допускающих запись. В данном примере таким является поле revision:

    [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, Method, Parameter, Struct. Разрешается задавать несколько целей, разделяя их символом вертикальной черты |.

    Для элементов, снабженных атрибутами, можно получать значения атрибутов через процесс, называемый отражением. Рассмотрим вызов объекта о:

    o.GetType().GetCustomAttributes(true);
    

    Результатом будет массив, содержащий атрибуты, определенные для класса объекта, с их значениями.

    Отсутствующие элементы

    В C# отсутствуют несколько ОО-механизмов, интенсивно применяемых в этой книге, в первую очередь - контракты и множественное наследование.

    Контракты введены в исследовательскую версию языка C# - SpecC#, разработанную в Microsoft Research. Эта версия языка доступна для свободного использования.

    Специфические свойства языка

    Полезно ознакомиться с несколькими поддерживающими C# конструкциями.

    Небезопасный код

    В C# комбинируются строгие требования к безопасности типов с предоставлением программистам возможности работы на низком уровне, характерном для языка С или ассемблера. Конструкция "unsafe" поддерживает четкое разделение между небезопасными элементами и нормальными, прошедшими проверку типов.

    Объявление метода небезопасным (unsafe) означает, что область данных расположена вне "кучи" - области, отводимой под объекты. Небезопасный метод может осуществлять в своей области непосредственные манипуляции с указателями и, следовательно, игнорировать нормальные правила работы с данными.

    Тип "перечисление"

    Типы, заданные перечислением, позволяют оперировать значениями из конечного множества предопределенных значений:

    enum CardColors {Spades, Hearts, Diamonds, Clubs}
    

    Значения, заданные перечислением, проецируются на целочисленный тип, по умолчанию int, но можно задать и другой тип, как в enum T:long {…}. Значения проецируются на отрезок, начинающийся с 0, но можно задать и другое начало:

    enum CardColors1 {Spades = 1, Hearts, Diamonds, Clubs}
    

    Значения можно обозначать, используя нотацию с точкой, как в CardColors.Spades. Допустимы взаимные преобразования между целыми и значениями перечисления Наиболее интересное применение перечислений - это шкалы. В этом случае каждый к-й элемент перечисления проецируется на значение 2к. Тогда переменная типа "перечисление" интерпретируется как набор битов, над которыми определены, как мы знаем, логические операции. Если необходимо работать с множеством объектов, характеризуемых набором бинарных свойств (свойство присутствует у объекта или нет; например, знает данный программист ОО-концепции или нет), то шкалы - прекрасный инструмент для таких задач..

    Linq

    В версии языка C# 3.0 появились новые важные механизмы, привлекшие внимание программистов. Механизм, известный как Linq, позволяет языку программирования работать непосредственно с базами данных и Web. Для работы с данными используется типичный язык запросов SQL и XML для Web. Обычно такие возможности поддерживаются с помощью специальных библиотек, обеспечивающих интерфейс к реляционным базам данных или к протоколу HTTP. Оригинальность Linq в том, что он делает все гораздо выразительнее, оставаясь в рамках языка программирования. Например, запрос к базе данных можно выполнить так:

    from e in Employees where e.salary > median select e.rank
    

    Здесь идет ссылка на отношение Employees из базы данных. В результате создается спиок всех служащих, ранжированных по зарплате. Стиль тот же, что и у SQL-запросов, но запрос интегрирован в язык. Все используемые объекты - это нормальные объекты программы, такие как списки.

    Лексические аспекты

    В C# идентификаторы следуют соглашениям, подобным Eiffel. Они могут, однако, начинаться с подчеркивания, хотя эта возможность редко используется в обычных приложениях. Важная разница в том, что идентификаторы чувствительны к регистру: 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", Интернет-Университет. Питер, Лаборатория знаний, 2010 Не могу удержаться от рекламирования собственного учебника, тем более, что он соответствует по духу этой книге..

    Вернуться к учебному плану