ПЕРВЫЙ ВЗГЛЯД

В июне 2000 г. стало известно о новом языке программирования, родившемся в недрах компании Microsoft. Он стал частью вновь разработанной технологии Microsoft, названной .NET (Dot Net). В ее рамках предусмотрена единая среда выполнения программ (Common Language Runtime, CLR), написанных на разных языках программирования. Одним из таких языков, причем для этой среды основным, и является Си# (читается как “Си шарп”). Названием языка конечно же хотели подчеркнуть его родство с Си++, ведь # - это два пересекшихся плюса *1. Но больше всего новый язык похож на Java. И нет сомнений, что одной из причин его появления стало стремление Microsoft ответить на вызов компании Sun.

_____

*1 В первых дискуссиях о новом языке, возникших в русском Интернете, было предложение называть его по-русски как “Си-диез”. Очень симпатично. Ведь си - это еще и название ноты, а диез - означает ее повышение на полтона.

Хотя официально авторы Си# не называются, но на титульном листе одной из предварительных редакций справочника по языку обозначены Андерс Хейльсберг (Anders Hejlsberg) - создатель Турбо-Паскаля и Дельфи, перешедший в 1996 г. в Microsoft, и Скотт Вилтамут (Scott Wiltamuth).

Единая среда выполнения программ основана на использовании промежуточного языка IL (Intermediate Language - промежуточный язык) *1, исполняющего почти ту же роль, что и байт-код виртуальной машины языка Java. Используемые в рамках технологии .NET компиляторы для различных языков транслируют программы в IL-код. Так же как и байт-код Java, IL-код представляет собой команды гипотетической стековой вычислительной машины. Но разница есть и в устройстве и использовании IL.

_____

*1 Идея применения единого промежуточного языка для построения многоязычной системы программирования не нова. Еще в 60-х годах такие системы на основе общего машинно-ориентированного языка АЛМО были созданы в СССР для многих типов машин.

Во-первых, в отличие от JVM, IL не привязан к одному языку программирования. В составе предварительных версий Microsoft.NET имеются компиляторы для языков Си++, Си#, Visual Basic. Независимые разработчики могут добавлять и другие языки, создавая компиляторы с этих языков в IL-код.

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

Основные черты Си#

“Си# - простой, современный, объектно-ориентированный язык с безопасной системой типов, ведущий происхождение от Си и Си++. Он будет удобен и понятен для программистов, знающих эти два языка. Си# сочетает продуктивность Visual Basic и мощность Си++”. Такими словами начинается описание Си#. Мы же рассмотрим технические особенности языка.

- Единицей компиляции является файл (как в Си, Си++, Java). Файл может содержать одно или несколько описаний типов: классов (class), интерфейсов (interface), структур (struct), перечислений (enum), типов-делегатов (delegate) с указанием (или без указания) об их распределении по пространствам имен.

- Пространства имен (namespace) регулируют видимость объектов программы (как в Си++). Они могут быть вложенными. Разрешено употребление объектов программы без явного указания, какому пространству имен они принадлежат. Достаточно лишь общего упоминания об использовании этого пространства в директиве using (как в Турбо-Паскале). Предусмотрены псевдонимы для названий пространств имен в директиве using (как в языке Оберон).

- Элементарные типы данных: 8-разрядные (sbyte, byte), 16-разрядные (short, ushort), 32-разрядные (int, uint) и 64-разрядные (long, ulong) целые со знаком и без знака, вещественные одиночной (float) и двойной (double) точности, символы Unicode (char), логический тип (bool, не совместим с целыми), десятичный тип, обеспечивающий точность 28 значащих цифр (decimal).

- Структурированные типы: классы и интерфейсы (как в Java), одномерные и многомерные (в отличие от Java) массивы, строки (string), структуры (почти то же, что и классы, но размещаемые не в куче, т. е. динамической памяти и без наследования), перечисления, несовместимые с целыми (как в Паскале).

- Типы-делегаты, или просто “делегаты” (подобны процедурным типам в Модуле-2 и Обероне, указателям на функции в Си и Си++).

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

- Автоматическая сборка мусора (как в Обероне и Java).

- Обширный набор операций, имеющий 14 уровней приоритета. Переопределение операций (как в Алголе-68, Аде, Си++). С помощью операторов checked и unchecked можно управлять контролем переполнения при выполнении операций с целыми.

- Методы с параметрами-значениями, параметрами-ссылками (ref) и выходными параметрами (out). Слова ref и out нужно записывать перед параметром не только в описании метода, но и при вызове. Наличие выходных параметров позволяет контролировать выполнение определяющих присваиваний. По правилам языка, любая переменная должна гарантированно получить значение до того, как будет предпринята попытка ее использования.

- Управляющие операторы: if, switch, while, do, for, break, continue (как в Си, Си++ и Java). Оператор foreach выполняет цикл для каждого элемента “коллекции”; существует несколько разновидностей оператора перехода goto.

- Обработка исключений (как в Java).

- Свойства - элементы классов (объектов), доступ к которым осуществляется так же, как и к полям (можно присвоить или получить значение), но реализуется неявно вызываемыми подпрограммами get и set (как в Объектном Паскале - входном языке системы Delphi).

- Индексаторы - элементы классов (объектов), позволяющие обращаться к объектам так же, как к массивам (указанием индекса в квадратных скобках). Реализуются неявно вызываемыми подпрограммами get и set *1. Например, доступ (для чтения) к символам строки может выполняться как к элементам массива благодаря тому, что для стандартного класса string реализован индексатор.

_____

*1 Стремление обобщенно оформить доступ к массиву, сделав его синтаксически неотличимым от обращения к функции, можно найти в языке Ада, где для записи индексов используются круглые скобки. В случае с индексаторами Си#, наоборот, обращение к функции или процедуре маскируется под обращение к массиву.

- События - элементы классов (поля или свойства) процедурного типа (делегаты), к которым вне класса, где они определены, применимы только операции += и -=, позволяющие добавить или удалить методы - обработчики событий для объектов данного класса.

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

- Препроцессор, предусматривающий, в отличие от Си и Си++, только средства условной компиляции.

Примеры программ на Си#

Рассмотрим вначале простейшую законченную программу, процесс ее компиляции и выполнения. Текст программы разместим в файле Hello.cs:

/* Простейшая программа на языке Си# */ class Hello {    

static void Main() {    

System.Console.WriteLine(“Hello,    

World!”);    

}

Для компиляции программы можно воспользоваться компилятором csc, входящим в Microsoft .NET Framework SDK - в состав комплекта разработчика для среды Microsoft .NET - и запускаемым из командной строки: csc Hello.cs

После компиляции будет получен исполнимый файл Hello.exe. Но запустить его на компьютере, работающем под управлением ОС Windows, можно только в том случае, если на нем установлена поддержка Microsoft .NET. Дело в том, что полученный после компиляции файл (несмотря на свое название) содержит не обычные машинные команды, а IL-код, который будет преобразован в код процессора при загрузке и запуске программы.

Но если .NET Framework SDK установлен, значит, соответствующая поддержка имеется. Запустив Hello.exe, получим:

Hello.exe

Hello, World!

Теперь обратимся к тексту программы. В ней определен единственный класс Hello, содержащий описание статического метода (метода класса) Main. Статический метод с названием Main (большие и малые буквы в Си# различаются) является точкой входа в программу, написанную на Си#. С выполнения этого метода начинается работа программы. В отличие от языка Java в Си# метод Main может быть без параметров или с параметрами; неважно также, возвращает ли он значение (являясь функцией) или нет. В нашем примере Main не имеет ни параметров, ни возвращаемого значения (void).

Единственный оператор в методе Main - вызов статического метода WriteLine. Это метод класса Console, предоставляющего доступ к стандартным выходному и входному потокам. Класс Console принадлежит (предопределенному) пространству имен System.

Для ссылки на класс Console использовано его полное название - System.Console (квалифицированный идентификатор), включающее обозначение пространства имен System. С помощью директивы using можно сокращать запись, применяя не квалифицированные названием пространства имен обозначения:

/* Простейшая программа на языке Си# */ using System; // разрешается    

// неквалифицированный доступ class Hello {    

static void Main() {    

Console.WriteLine(“Hello,    

World!”);    

}

Сортировка на Си#: найдите отличия

В качестве другого примера рассмотрим метод, выполняющий сортировку простыми вставками вещественного массива. Если этот текст сравнить с соответствующей программой на Java, то различия будут совершенно незаметны.

// Сортировка простыми вставками на // Си# public static void InsSort( float[] a ) {    

for (int i = 1; i<a.Length; i++) {    

float x = a[i];    

if ( x<a[i-1] ) {    

a[i] = a[i-1];    

int j = i-2;    

while (j>=0 && x<a[j])    

a[j+1] = a[j-];    

a[j+1] = x;    

}    

}

Различие есть одно. Слово “длина” пишется с большой буквы: Length (в Java - length). Length - это свойство (property) стандартного класса System.Array, который является родоначальником массивов в Си#.

Первые впечатления

К настоящему моменту существуют лишь предварительное описание языка Си# и предварительная версия средств разработки программ на этом языке. Поэтому делать какие-либо обобщающие выводы еще рано. Но некоторые суждения высказать можно.

В Си# сохранены и даже приведены в порядок некоторые традиционные конструкции: перечисления, структуры, многомерные массивы. Java в этом отношении демонстрирует более экстремистский подход: объекты и ничего, кроме объектов. Устранены несомненные несуразности Java вроде отсутствия передачи параметров по ссылке при отсутствии же указателей. Механизм передачи параметров в Си# хорошо продуман. Он предусматривает передачу как по значению, так и по ссылке.

Вместе с тем в Си# есть много конструкций, которые как минимум могут вызывать вопросы. Язык избыточен и, как следствие, сложен в использовании и реализации. Некоторые средства Си# могут провоцировать ошибки. Сомнения по поводу Си# становятся особенно сильны при его сравнении с языками, созданными Н. Виртом, в первую очередь с Обероном.

Пространства имен

В Си#, как и в других языках, происходящих от Си, так и не получила воплощения простая и ясная концепция модуля. Вместо этого использованы пространства имен - средство, появившееся на поздних стадиях стандартизации Си++. Пространства имен - это весьма общий механизм, поглощающий, в частности, и возможности, предоставляемые модулями. Но здесь налицо чрезмерное обобщение 4*, не обусловленное насущными потребностями; оно предоставляет программисту избыточные средства, а с ними и возможности для злоупотреблений. Вложенность пространств имен, их длинные составные обозначения служат препятствием к тому, чтобы потребовать обязательного явного (квалифицированного) использования имен, взятых из этих пространств 5*, как это сделано в Обероне для импортируемых модулем идентификаторов. Неявный же импорт, разрешаемый директивой using6*, - источник ошибок, связанных с коллизией имен. И вот тому пример.

_____

*1 Стремление к обобщению всего и вся можно заметить, например, в Алголе-68. Судьба его печальна.    

*2 Предпосылки для обязательной квалификации имен в Си# тем не менее есть: предусмотрены псевдонимы пространств имен, которые могут быть короче их полных обозначений. Но создатели языка, видимо, не рискнули потребовать обязательной квалификации, опасаясь перенапрячь программистов, привыкших к вольностям Си++.    

*3 Наследие Турбо-Паскаля версии 4.0. И по слову (в Турбо-Паскале - uses), и по создаваемым проблемам, и, видимо, по автору (А. Хейльсберг).

Рассмотрим программу, в которой определено пространство имен Hello, а внутри этого пространства имен - вложенные друг в друга классы A и B. Класс B содержит единственное статическое поле C, которое инициализировано значением “Привет!”.

// Эта программа хранится в файле Hello.cs namespace Hello {    

public class A {    

public class B {    

public static string C = “Привет!”;    

}    

}

Содержимое файла Hello.cs не является независимой программой, но может быть отдельной единицей компиляции, оттранслировав которую, можно получить динамически компонуемую библиотеку *1 (файл с расширением dll). Для этого при запуске компилятора csc нужно использовать параметр /target: csc /target:library Hello.cs

_____

*1 Компиляция и выполнение программ рассматриваемого примера производились с помощью компилятора Microsoft (R) Visual C# Compiler Version 7.00.9030 и единой языковой среды исполнения (CLR version 1.00.2204.21) под управлением ОС Windows 95.

В результате компиляции будет получена библиотека Hello.dll.

Теперь напишем основную программу, чтобы она могла воспользоваться ресурсами нашей библиотеки. А ресурс, собственно, один: строка, содержащая “Привет!”. Ее и напечатаем:

// Эта программа хранится в файле Print.cs class Print {    

static void Main() {    

System.Console.WriteLine(Hello.A.B.C);    

}

Поместим эту программу в файл Print.cs и откомпилируем ее. Чтобы при компиляции Print.cs была доступна библиотека Hello.dll, упомянем ее в команде, вызывающей компилятор, с помощью параметра /reference: csc /reference:Hello.dll Print.cs

В результате компиляции получается исполнимый файл Print.exe; его можно запустить и увидеть напечатанное слово “Привет!”:

Print.exe

Привет!

Теперь модифицируем программу Print.cs, воспользовавшись директивами using для указания пространств имен System и Hello, из которых импортируются нашей программой классы Console и A: using System; using Hello; class Print {

static void Main() {    

// Console - из пространства имен System;    

// A - из пространства имен Hello.    

Console.WriteLine(A.B.C);    

}

Компилируем заново Print.cs, запускаем, получаем тот же результат (а как же иначе): csc /reference:Hello.dll Print.cs

Print.exe

Привет!

Теперь, ничего не меняя в уже написанном коде Print.cs и Hello.cs, подключаем к трансляции Print.cs еще одну библиотеку (A.dll). В реальной задаче это могло понадобиться, когда программе Print стали нужны какие-то средства, имеющиеся в библиотеке A.dll. Компилируем и запускаем Print: csc /reference:Hello.dll,A.dll Print.cs

Print.exe

2.7182818284590451

Но что это? Вместо “Привета” (ведь мы ничего не меняли в программе, при компиляции по-прежнему упомянули библиотеку Hello.dll, которая оставалась на том же месте) выведено какое-то число *1!

_____

*1 Вы узнали приближенное значение числа e - основания натуральных логарифмов?

Дело в том, что, к нашему несчастью, во вновь подключенной библиотеке оказалось определено пространство имен A, в нем класс B, а в нем - доступное статическое поле C. В реальной ситуации в библиотеке A.dll могли быть определены также и другие классы и пространства имен. В нашем же примере A.dll была получена компиляцией файла A.cs:

// Эта программа хранится в файле A.cs namespace A {    

public class B {    

public static double C = 2.71828182845904523;    

}

Теперь в операторе Console.WriteLine (A.B.C); программы Print идентификатор A воспринимается как обозначающий пространство имен A, а не класс A пространства имен Hello! Язык Си# подвел нас. Причем дважды. Во-первых, когда была допущена коллизия имени класса A пространства имен Hello и названия пространства имен A. Эта коллизия была почему-то разрешена в пользу названия пространства имен, в то время как директива using Hello создала в пределах своего действия локальную область, в которой локальные имена должны были бы иметь преимущество. Во-вторых, возникшее несоответствие не было обнаружено даже при том, что два разных поля с именем С были разных типов. Если бы метод WriteLine *1 не был столь либерален к типу своих параметров, был бы шанс обнаружить ошибку.

_____

*1 На самом деле здесь имеется в виду не один метод WriteLine, а совокупность совместно используемых (перекрытых) методов с одинаковыми названиями, но разными типами параметров.

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

В чем причина таких прорех и как они могут быть устранены? Дело в том, что в отношении названий пространств имен в Си# не действует общее, признанное еще со времен Алгола-60 правило, согласно которому любой идентификатор в программе не может быть использован без предварительного описания. Для исключения рассмотренных коллизий необходимо, чтобы директивы using были обязательными к исполнению наряду с обязательной квалификацией идентификаторов через название пространства имен. Иными словами, следовало бы потребовать, чтобы программа Print записывалась только в таком виде:

// Безопасное использование пространств имен using System; // Описание названия пространства    

// имен using Hello; // Описание названия пространства    

// имен class Print {    

static void Main() {    

// Использование названий пространств    

// имен:    

System.Console.WriteLine(Hello.A.B.C);    

}

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

Пространство программы

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

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

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

Во-вторых, полностью квалифицированный идентификатор статического поля или метода мог бы в этом случае иметь уже не тройное, а всего лишь двойное обозначение. А это создает предпосылку для обязательного использования квалифицированных имен и директивы using. Требовать же обязательной квалификации при как минимум трех уровнях именования (пространство имен, класс, поле) создатели Си# по понятным причинам не стали.

Наконец, класс перестает противоестественно совмещать две различные роли - описания типа и пространства имен статических полей и методов. Такое совмещение, кстати, затрудняет понимание и изучение языков Java и Си#.

Рассмотрим перечисленные возможности на уже обсуждавшемся примере. Пространство имен А, содержащее статическое поле С, можно было бы определить так:

// Это программа на модифицированном Си# namespace A {    

public double C = 2.71828182845904523;    

Класс B для определения поля C больше не нужен. Устраним класс B и в программе из файла Hello:    

// Это программа на модифицированном Си# namespace Hello {    

namespace A {    

public string C = “Привет!”;    

}

При этом класс A превращен во вложенное пространство имен A, поскольку статические (такого слова в программе нет) поля теперь не должны располагаться внутри классов. Теперь при условии обязательного применения using и при обязательной квалификации импортируемых идентификаторов не может возникнуть никаких опасных коллизий вроде тех, что рассмотрены выше.

(Окончание следует)