Владимир Биллиг,
доцент факультета ПМиК Тверского государственного университета
Vladimir.Billig@tversu.ru


Статья продолжает серию публикаций о платформе Microsoft .NET, начатую в "BYTE/Россия" № 12'2001.

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

Какие цели стояли перед разработчиками

Андерс Хейлсберг, возглавлявший в Microsoft работу по созданию языка C# (C Sharp), следующим образом определил стоявшие перед ними цели:

  • создать первый компонентно-ориентированный язык программирования в семействе C/C++;
  • создать объектно-ориентированный язык, в котором любая сущность представляется объектом;
  • упростить C++, сохранив его мощь и основные конструкции.

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


*Значительная часть обсуждаемых в статье вопросов (библиотека классов, CLR, структура программы, типы данных) относится к .NET Framework; иными словами, это вопросы, общие для всех систем программирования (не только C#), работающих в этой среде. На эту тему см. также статью "Введение в .NET Framework" в этом номере журнала. - Прим. ред.

Платформа .NET - многоязыковая среда, открытая для свободного включения новых языков, создаваемых не только Microsoft, но и третьими фирмами. Все языки, включаемые в платформу .NET, должны опираться на единый каркас, роль которого играет .NET Framework. Это серьезное ограничение, одновременно являющееся и важнейшим достоинством.

.NET Framework и библиотека классов

В основе большинства приложений, создаваемых в среде VC++, лежал каркас приложений (Application Framework), ключевую роль в котором играла библиотека классов MFC. Всякий раз, когда создавался новый MFC-проект - EXE, ActiveX или DLL, из каркаса приложений автоматически выбирались классы, необходимые для построения проекта с заданными свойствами. Выбранные классы составляли каркас конкретного приложения. Аналогичную роль для офисных документов играет совокупность библиотек классов в среде Office.

Каркас .NET также содержит библиотеку классов (Class Library). Она служит тем же целям, что и любая библиотека классов, входящая в каркас. Библиотека содержит более сотни интерфейсов и классов, объединенных в группы по тематике. Каждая группа задается пространством имен (namespace), корневое пространство носит имя System. Классы библиотеки связаны отношением наследования, все классы являются наследниками класса System.Object. Для классов библиотеки, равно как и для классов в языке C#, не определено множественное наследование.

Чтобы дать некоторое представление о каркасе приложения и составе библиотеки, приведу пример. Если при построении нового проекта в C# выбрать в качестве типа проекта Windows Application, то каркас приложения, построенный по умолчанию, будет включать следующие пространства имен из библиотеки классов:

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;

Корневое пространство имен System содержит фундаментальные и базовые классы - общие для всех языков ссылочные типы и типы-значения, события, обработчики событий и средства обработки исключений. Пространство имен Drawing обеспечивает доступ к GDI. В это пространство входят классы Graphics, Bitmap, Brush, Point, Rectangle и т. п. Пространство имен Collections содержит интерфейсы и классы, определяющие различные коллекции объектов - списки, очереди, массивы, хэшируемые таблицы, словари.

Единый каркас многоязыковой среды программирования во многом способствует компонентной ориентированности не только отдельного языка, но и всей среды .NET. Поясню эту мысль. Как известно, системы типов в языках программирования отличаются. Если в библиотеке классов есть общие для всех языков типы, то при создании компонента, предназначенного для общего использования, всякая попытка использовать тип, не транслируемый в общий для всех языков тип, приведет к выдаче соответствующего уведомления. Возможность проверки типов - это лишь одно из достоинств CLS (Common Language Specification) - системы общих спецификаций, которым должен удовлетворять компонент. Так, класс, созданный в C# в соответствии с требованиями CLS, можно с успехом использовать в качестве базового класса в других языках. Так что единый каркас в многоязыковой среде - это революционное изобретение, уже по самой своей природе способствующее созданию действительно универсальных компонентов.

Среда выполнения Common Language Runtime

Библиотека классов - это статическая составляющая каркаса. В .NET Framework есть и динамическая составляющая - агент, задающий общую среду выполнения, CLR (Common Language Runtime). Роль этой среды весьма велика - в ее функции входит управление памятью, потоками, безопасностью, проверка кода на соответствие CLS, компиляция из промежуточного байт-кода в машинный код и многое другое. Важный элемент CLR - мощный механизм сборки мусора (garbage collector), управляющий работой с памятью типа "куча".

Язык C# в полной мере позволяет использовать все возможности CLR. Код, создаваемый на C#, по большей части безопасен. Среда CLR выполняет массу других проверок, следя за тем, чтобы не появлялись неопределенные указатели, не было выхода за границы массива и т. д. Но в тех ситуациях, когда это необходимо, например, в целях повышения эффективности, можно создавать и небезопасные (unsafe) блоки кода на C#, ограничивая действия CLR. Приведу фрагмент кода:

public struct Point
  {
    int x;
    int y;
    //Конструктор
    public Point(int x, int y)
    {
      this.x =x;
      this.y = y;
    }
      //Безопасный метод копирования массива
      public static Point[]
        CopyArraySafe(Point[] mas)
      {
        Point[] temp = new Point[mas.Length];
        for (int i=0; i<mas.Length; i++)
          {
            temp[i] = mas[i];
          }
        }
        return(temp);
      }
      //Небезопасный метод копирования массива точек
      unsafe public static Point[]
               CopyArrayUnsafe(Point[] mas)
      {
        Point[] temp = new Point[mas.Length];
        fixed(Point* src=mas, dest= temp)
        {
          Point* pSrc=src;
          Point* pDest=dest;
          for (int i=0; i>mas.Length; i++)
          {
            *pDest=*pSrc; pSrc++; pDest++;
          }
        }
        return(temp);
      }
    }

Здесь метод CopyArrayUnsafe, написанный в духе C++ и возвращающий массив точек, помечен как небезопасный. Он более эффективен, чем его безопасный аналог, поскольку использует адресную арифметику вместо работы с индексами массивов. Но он работает непосредственно с указателями, что чревато ошибками. В безопасных блоках применение указателей запрещено. Внутри небезопасного блока в C# разрешено применение оператора fixed, позволяющего определять указатели на управляемые переменные, фиксируя их и предохраняя от вмешательства сборщика мусора. Последнему запрещается на момент выполнения небезопасного блока выполнять операции над областью памяти, занятой зафиксированными переменными. Следует понимать, что небезопасный код не будет запускаться в среде, в которой не установлена высокая степень доверия к компоненту, содержащему небезопасный код.

Резюмируя сказанное, отметим следующие моменты:

  • среда .NET Framework, задающая единый каркас многоязыковой среды разработки приложений, во многом ориентирована на компонентное программирование. Она оказывает несомненное влияние на все языки, погруженные в эту среду;
  • разработчики .NET Framework просто не могли не создать новый язык программирования, который бы в полной мере учитывал все возможности .NET Framework, не неся на себе груза прошлого. Таким языком и стал язык C#;
  • главным достоинством языка C# можно назвать его согласованность с возможностями .NET Framework и вытекающую отсюда компонентную ориентированность.

Остановимся подробнее на некоторых свойствах языка C#.

Структура программы

Программа на C# состоит из одного или нескольких файлов. Каждый файл может содержать одно или несколько пространств имен. Каждое пространство имен может содержать вложенные пространства имен и типы, такие как классы, структуры, интерфейсы, перечисления и делегаты - функциональные типы. При создании нового проекта C# в Visual Studio выбирается один из 10 возможных типов проектов, в том числе Windows Application, Class Library, Web Control Library, ASP.NET Application и ASP.NET Web Service. На основании сделанного выбора автоматически создается каркас проекта.

Типы данных C#

Типы данных в языке C# делятся на две категории - ссылочные и значимые. Объектам ссылочного типа память отводится в куче (heap, динамически распределяемое пространство), значимым - в стеке. К значимым типам относятся все встроенные скалярные типы - bool, byte, char, int, uint, float, double и другие подобные типы, а также перечисления и структурные типы, определенные пользователем. Обратите внимание, что переменная типа int в языке C# - это объект, и она может участвовать во всех операциях, где требуется объект. Класс int, как и другие классы, является наследником базового класса Object. Для него определен конструктор по умолчанию и ряд методов, например, метод ToString, преобразующий значение в строку. Приведу пример:

public static int Main(String[] args)
    {
      int n = new int();
      float x;
      n=7;
      x=9.5f;
      String S = "";
      S +=n.ToString();
      S +="-" + x.ToString();
      Console.WriteLine("Hello, " +S + "!");
      return(0);
    }

Обратите внимание, что объект n (я специально называю n объектом, а не переменной) здесь инициализируется при помощи привычной для объектов конструкции new. При этом в стеке выделяется память и вызывается конструктор по умолчанию, который и присвоит нуль в качестве значения n. В языке C# конструкция new означает создание нового объекта. Где будет выделена память для объекта - в стеке или в куче, зависит от типа объекта. В нашем примере объектам n и x память будет отведена в стеке, а объекту S - в куче, поскольку строки относятся к ссылочным типам. Массивам, как и строкам, память также отводится в куче. Для значимых типов программист обязан явно задать начальное значение, прежде чем объект будет использован в вычислениях. В данном примере еще на этапе компиляции появилось бы сообщение об ошибке, если бы до использования объектов x и S в правой части оператора присваивания этим объектам не было бы присвоено значение.

Не следует думать, что в языке C# работа с простыми переменными неэффективна, поскольку они заменены "тяжеловесными" объектами. Это не так. Просто определены две эффективные операции - boxing и unboxing, которые при необходимости преобразуют значение в объект и обратно.

"Классические" объекты - экземпляры стандартных и собственных классов относятся, естественно, к ссылочным типам. Память им отводится в куче при вызове new и соответствующего конструктора класса. Деструкторов, освобождающих память, в классе может и не быть. Освобождение памяти в куче возложено на сборщик мусора, который сам решает, когда можно безопасно освободить память, выделенную для того или иного объекта. Поэтому объект уже может закончить свое существование, а память будет еще занята и освободится лишь на очередном шаге работы сборщика мусора. Деструкторы для классов следует задавать лишь в тех случаях, когда объекты используют другие ресурсы, например, файлы. Деструкторы в этом случае играют роль завершающих блоков finally, известных по обработке исключительных ситуаций. Они освобождают занятые ресурсы, отличные от памяти.

Атрибуты

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

Приведу два примера, поясняющих суть встроенных атрибутов. Атрибут Conditional заменяет препроцессорный оператор ifdef языка C++. Он аннотирует отладочные функции, вызываемые при условии определения отладочной переменной в препроцессорной директиве #define. Вот пример:

#define mydebug
using System;
using System.Diagnostics;
namespace ConsoleApplication1
{
  public class HelloWorld
  {
    public static int Main()
    {
      // code
      Test1();
      // code
      return(0);
    }
    [Conditional("mydebug")]
    public static void Test1()
    {
      //Код, выполняемый при отладке
    }
  }
}

В данном примере функция Test1 аннотирована как отладочная. Поскольку переменная mydebug определена, то эта функция будет выполняться при ее вызове в Main. На отладочные функции накладывается естественное ограничение - они должны быть void-функциями. Чтобы применить данный атрибут, требуется подключить класс ConditionalAttribute из пространства имен System.Diagnostics.

Второй пример демонстрирует применение атрибута DllImport для вызова внешних функций DLL. Аннотируемым элементом является описание импортируемой функции. В примере вызывается функция WinApi MessageBox из библиотеки User32.dll. Приведу три необходимых фрагмента кода:

//Задание пространства имен, необходимого для поддержки 
//импорта функций 
using System.Runtime.InteropServices;

//Аннотированное объявление импортируемой из DLL функции
[DllImport("user32.dll")]
public static extern int MessageBox(int h,
  string mes, string cap, int type);

//Вызов импортируемой функции
int ret=MessageBox(0, "Hello, World!", "Hello", 0); 

Как видим, встроенные атрибуты могут играть самые разные роли в языке C#. Однако полностью мощь этого аппарата проявляется в возможности создавать собственные атрибуты с последующей их обработкой. Хорошая иллюстрация этого - аннотирование всех изменений разрабатываемого проекта. Пользовательский атрибут может аннотировать класс и задаваться всякий раз, когда в класс вносятся очередные изменения. Определим собственный атрибут Review, имеющий три параметра (имя автора, дата и описание сути изменения). Тогда аннотации могут выглядеть следующим образом:

[Review("Vladimir", "13-01-2002", "Добавлен метод Clear")]
[Review("Nina", "23-01-2002", "Добавлен метод Home")]
 class MyClass
{
  …
}

Код, демонстрирующий работу с атрибутом Review, приводить не буду, ограничусь описанием того, что нужно делать при введении собственных атрибутов. Прежде всего требуется определить для атрибута свой класс. Этот класс является наследником базового класса System.Attribute. Класс должен содержать по крайней мере один конструктор и свойства, хранящие значения параметров атрибута. Аннотации можно рассматривать как вызов конструкторов атрибутного класса, а хранимые в CLR метаданные, связанные с атрибутом, - как экземпляры этого класса. Если возникает необходимость программно получить информацию о значениях атрибутов, например, найти все изменения, сделанные разработчиком "Vladimir", используются специальные средства. Я не буду подробно о них рассказывать, упомяну только метод GetCustomAttributes, который возвращает массив всех хранимых экземпляров класса. Процесс получения значений атрибутов в C# и называется отражением.

XML-документация

Еще одним новшеством языка C# стала возможность задавать комментарии в виде XML-тегов, что позволяет автоматически строить документацию по разрабатываемому проекту. Предусмотрена программная обработка некоторых XML-тегов компилятором языка; кроме того, для других тегов действуют специальные соглашения. Рассмотрим пример построения класса, хранящего записи о служащих. Класс и все члены класса сопровождаются XML-тегами комментариев:

using System;
using System.Xml;
namespace ClassEmployee
{
  /// <summary>
  /// Class Employee содержит данные о служащих.
  /// Идентификационный номер ID - <see cref="int"/> целое 
  /// Фамилия name - <see cref="string"/> строка 
  /// </summary>
  public class Employee
  {
    /// <summary>
    ///  Идентификационный номер служащего
    /// </summary>
    int ID;
    /// <summary>
    /// Фамилия служащего
    /// </summary>
    string name;
    /// <summary>
    /// Конструктор класса Employee(int ID, string name).
    /// <param name ="ID">ID номер является целым</param>
    /// <param name ="name">name является строкой</param>
    /// </summary>
    public Employee(int ID, string name)
    {
      this.ID = ID;
      this.name = name;
    }
    /// <summary>
    /// Конструктор без параметров класса Employee.
    /// </summary>
    /// <remarks>
    /// <seealso cref="Employee(int,string)"/>
    /// </remarks>
    public Employee()
    {
      ID = -1;
      name = null;
    }
  }
}

XML-теги комментариев начинаются с тройного символа "/" (слэш). Какие роли играют разные типы тегов? Тег

<summary>
позволяет задать описание элементов класса. Аналогичную роль играет тег
<remarks>
, обычно используемый для более длинных описаний. Для конструкций
<see cref="имя"/>
и
<seealso cref="имя"/>
компилятор проверяет, действительно ли существует имя, на которое ссылается тег. Если проверка успешна, к имени добавляется известная компилятору информация, например, описание типа из System, в который отображается встроенный тип языка. Добавляется префикс, например, T или M, указывающий на вид элемента - тип, метод. Для тега
<param>
компилятор проверяет, существует ли параметр у описываемого метода и все ли параметры метода описаны в документации. Если что-то не так, будет сформировано предупреждающее сообщение.

В среде Visual Studio можно задать соответствующую опцию, которая позволяет на основе проекта, снабженного комментариями, построить XML-файл документации. Я это и сделал для данного примера. Приведу лишь одну строку из полученного файла, демонстрирующую, как выглядит тег

<seealso>
из нашего примера после его обработки компилятором:
<seealso cref="M:ClassEmployee.Employee.#ctor
  (System.Int32,System.String)" /> 

Если в браузер добавлена поддержка XSL - расширенного языка стилей, то файл документации просто форматируется и достаточно элегантно отображается в браузере. Среда Visual Studio позволяет построить по комментариям соответствующий Web-отчет, который также с успехом может использоваться в документации.

C# и C++

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

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

Наследование и шаблоны. В языке C# не реализованы такие важные для C++ концепции, как множественное наследование и шаблоны. Множественное наследование интерфейсов в C# возможно.

Каркас приложений. Для программистов каркас, на котором строятся приложения, играет зачастую куда большую роль, чем та или иная языковая конструкция. Здесь различия большие: .NET Framework отличается от среды, привычной для программистов VC++.

Типы данных. В языке C# появилась принципиально новая классификация типов, различающая значимые и ссылочные типы. Как следствие, применяются разные способы работы с объектами этих типов. В языке устранена разница между переменными и объектами. Все объекты имеют единого предка - класс Object. Упрощена система встроенных типов.

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

Массивы. В языке C# появилась возможность объявлять классические многомерные массивы. Работа с массивами в C# более безопасна, поскольку контролируется выход за границы массива.

Классы. Следует отметить различия в подходах к закрытию свойств класса. В C# введены процедуры-свойства get и set, аналогично тому, как это сделано в языке VB/VBA.

Синтаксические и семантические детали. В C# оператору switch возвращена естественная семантика, не требующая задания break. Булевы переменные в языке C# имеют два значения, нельзя вместо них использовать целочисленные переменные, как это принято в C++. Конструкция "int i=1; if(i)…", законная для C++, в C# работать не будет. В C# используется точка всюду, где в C++ используются три разных символа - ".", "::", "->".

C# и Java

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

Если же говорить о различиях, то прежде всего следует сказать, что создание платформы .NET, .NET Framework и C# имеет побудительные мотивы, не сводящиеся к простой конкуренции с языком и платформой Java.

Создание Web-ориентированной платформы .NET - это веление времени, магистральное развитие программирования. Microsoft просто обязана была создать ее вне зависимости от существования платформы Java.

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

Создание .NET Framework автоматически определило целесообразность создания языка программирования, в полной мере согласованного с новыми идеями, заложенными в этой среде.

Отмечу два важных отличия в деталях реализации C# и Java. В среде выполнения Java промежуточный байт-код интерпретируется, в то время как в CLR он компилируется, что повышает эффективность исполнения (по данным некоторых исследований, время исполнения уменьшается на порядок). Выбор языковых конструкций языка прародителя C++ был различным. Например, авторы Java отказались от перечислений, в то время как в C# этот тип был не только сохранен, но и развит. Сохранена также возможность перегрузки операторов. Я уже не говорю о том, что в язык C# добавлены весьма полезные нововведения, в том числе некоторые из описанных в этой статье.

Что касается дополнительной информации, в Интернете есть немало ресурсов, посвященных C#. Можно порекомендовать для начала обратиться к специальному сайту http://www.c-sharpcorner.com.