Опубликован: 06.10.2011 | Уровень: для всех | Доступ: платный
Дополнительный материал 2:

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

При появлении языка C# в 1999 Microsoft представлял язык следующим образом.

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

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

Элегантно спроектированный OO-язык C# является великолепным выбором при построении архитектуры компонентов - начиная от высокоуровневых бизнес-объектов до приложений системного уровня. Используя простые конструкции языка C#, эти компоненты могут быть конвертированы в XML Web-сервисы, вызваны затем в Интернете из любого языка, выполняемого на любой операционной системе.

 Андерс Хейлсберг (разработчик C#), 2007

Рис. 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;
  • константа, частный случай поля;
  • метод, реализованный в виде процедуры или функции;
  • метод-свойство, поле, сопровождаемое возможными методами - геттером и сеттером;
  • операция, функция с синтаксисом операции;
  • конструктор, процедура создания - метод, применяемый для создания объектов;
  • деструктор, редко применяемый метод для освобождения ресурсов, возможный только для классов;
  • событие, которое связано с делегатами и программированием, управляемым событиями;
  • индексатор;
  • вложенный тип.

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

Классы и структуры группируются в сборки (понятие, соответствующее кластеру в Eiffel2 Для программиста классы, структуры и другие частные случаи - интерфейсы, делегаты, перечисления - группируются в проекты, преобразуемые в сборки в результате компиляции проекта.).

Выполнение программы

Каждая выполняемая программа должна иметь по меньшей мере один метод, называемый Main и помеченный как статический (static - понятие, поясняемое в следующем разделе). Выполнение программы начинается с выполнения этого метода. Можно написать классический пример "Hello world" с одним классом и Main-методом:

public class Program {
    static void Main(string[] arguments) {
      System.Console.WriteLine("Hello world!");
    }
 }

Main может не иметь аргументов или, если выполнение нуждается в аргументах, предоставляемых пользователем, иметь один аргумент, представленный массивом строк (string[]). Метод может не возвращать результат, или возвращать целочисленное значение, которое обычно рассматривается как статус, сигнализирующий об уровне ошибок, если метод завершается с ошибками.

Базисная ОО-модель

Многие концепции C# совпадают с теми, что мы видели при изучении этой книги. Но есть некоторые вариации.

Статические компоненты и классы

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

Обычно для использования компонента класса необходим целевой объект. Стандартная ОО-нотация доступа к компоненту имеет вид target.member (возможно, с передачей аргументов компоненту), где target обозначает целевой объект. Текущий объект (Current в Eiffel), в C# имеет имя this, которое, как и в Eiffel, можно опускать, когда из контекста ясно, что речь идет о текущем объекте.

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

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

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

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

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

  • public: доступен в любом коде;
  • internal: доступен в коде той же сборки;
  • private: доступен коду самого класса или структуры. Это статус по умолчанию для компонентов класса4 Есть, конечно, и четвертый квалификатор - protected, - позволяющий получить доступ потом кам класса. Возможна и комбинация protected, internal. Позже об этом будет сказано..

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

Поля

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

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

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

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

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

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

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

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

Ольга Попова
Ольга Попова
Россия
Михаил Окнов
Михаил Окнов
Россия