Не так давно я начал осваивать нынче сверхпопулярный, среди windows программистов, язык программирования C#. Осваивать я его начал, естественно, не просто так, а с корыстными целями, но об этом я рассказывать не буду. Нельзя сказать, что до этого я его в глаза не видел, писал я маленький тестик. Теперь же, после недельного знакомства с этим языком, я хочу поделиться своими впечатлениями.Сразу хочу предупредить, что всё, написанное в этой статье, является моим субъективным мнением.

Платформа .NET

Для работы программ, созданных на платформе .NET, то есть всех программ на языке C#, требуется виртуальная машина .NET. Платформа .NET должна была создать конкуренцию платформе Java, но разработчики Microsoft остановились на поддержке только операционных систем семейства Windows, похоронив тем самым кроссплатформенность. Однако компания Novell разрабатывает альтернативную кроссплатформенную реализацию .NET и C#. Этот проект получил название Mono.
Даже несмотря на то, что Microsoft предоставила Novell все права, проект Mono остаётся догоняющим. По этой причине проектирование Mono сильно отстаёт. Например, WPF в проекте Mono даже и не собираются реализовывать.Исходя из вышесказанного, нетрудно предположить, что мы можем либо писать только под Windows, либо принять ограничения Mono и жить в их рамках.
Но не всё так плохо, того, что реализовано проектом Mono достаточно для создания полноценных приложений. Открытым остаётся вопрос, а согласятся ли компании, создающие ПО на C#, на такие ограничения?
Проект .NET создавался сразу с несколькими целями, давайте разберёмся, что это за цели и каков результат.

Замена устаревшим технологиям COM и ActiveX

Позволю себе напомнить о том, что такое COM и ActiveX. Технология COM служит для межпроцессного, внутрепроцессного и сетевого взаимодействия объектно-ориентированных модулей. Взаимодействие строится на понятиях виртуальной таблицы (VTBL) и интерфейса класса. Приложение, которое хочет воспользоваться объектом другого модуля, просит этот модуль создать ему экземпляр какого-либо класса и, получив его, инициализирует специальным образом, заранее известный, интерфейс. Системой маршалинга COM вызовы виртуальных методов интерфейса преобразуются в корректные вызовы и все остаются довольны. Технология ActiveX — это надстройка над COM, которая служит для работы с графическим интерфейсом, обеспечивая такие понятия как компонент, узел компонента и обмен сообщениями.
Итак, как же технология .NET решает те же проблемы, что и COM. Во-первых .NET вводит понятие сборки (assembly), которая является её функциональной частью. Сборка представляет из себя бинарный модуль, хранящий полную информацию о том, что он содержит: пространства имён, зависимости, типы, методы итд. Любая сущность, объявленная в сборке как public автоматически становится доступной из других сборок. При том все события и делегаты остаются в рабочем состоянии, а виртуальная машина .NET обеспечивает, как и в случае с COM, все межпроцессные, внутрипроцессные и сетевые взаимодействия. Таким образом, отпадает необходимость в виртуальных таблицах, а значит и работа с графическими компонентами становится неотличимой от простого взаимодействия.
С первой задачей .NET справился, но на этом создатели не остановились. Раз уж нам известно всё о типах, находящихся внутри сборки, то пользователь этой сборки может расширять все типы, унаследовав их. Технология COM не позволяет наследоваться от классов из скомпилированного модуля.

Мультиязычная поддержка

В рамках платформы .NET реализована технология под названием CLR (Common Language Runtime). Это технология, позволяющая работать с платформой .NET из других языков программирования. Также, предоставляется возможность взаимодействия сборок, написанных на разных языках.
В рамках технологии CLR реализовано множество языков, как самой корпорацией Microsoft, так и другими компаниями. Среди этих языков, такие языки как C#, Visual Basic.NET, C++.NET, F#, IronPython, Delphi, Nemerle и многие другие. Так как я не работал со многими из тех языков, что значатся в списке, поддерживающих CLR, я скажу только о C++.NET. Достаточно справедливо будет отметить, что разработчики компании Microsoft внесли такие изменения в язык C++, что работать с ним стало просто не приятно. Попробовав поработать с C++.NET, я понял, что не выдержу издевательств над своим любимым языком и поспешно удалил тестовый проект. Больше я к нему не возвращался. Получается, что не CLR поддерживает язык, а язык изменили для CLR (то есть подвинули гору к Магомету). Из этого можно сделать вывод, что некоторые языки, как минимум C++ и Visual Basic подгонялись под платформу .NET только для того, чтобы создать изначальный пул языков CLR. Надеюсь, что с другими языками, которые создавались не в такой спешке, подобной истории не случилось.
Подводя итоги для этой цели, можно сказать, что платформа .NET не полностью справилась с поставленной задачей, так как я вынужден использовать не C++, а его модификацию только потому, что разработчикам .NET так захотелось. Но нельзя отрицать тот факт, что поддерживаемых языков, действительно, много, и их количество возрастает.

Язык C#

Давайте же перейдём к виновнику сей статьи — к языку C#. Язык C# — это первый из языков CLR, который создавался с нуля и специально для платформы .NET, поэтому он должен отражать все намерения разработчиков. Язык разрабатывался под сильным влиянием языка Java и должен был стать его конкурентом. В этой статье я не буду писать учебник по этому языку, о нём написано много книг и на msdn есть руководство. Здесь я расскажу о тех конструкциях языка, которые мне понравились и о тех, которые мне не понравились.

Наследование

Как и в Java, в C# запрещено множественное наследование классов, но разрешено множественное наследование интерфейсов. Хорошо это или плохо? Споры по этому поводу не умолкают с тех пор, как придумали наследование. Моё мнение — плохо. Ведь у каждого программиста есть своя собственная голова на плечах и он ею может подумать и сам решить все проблемы, которые несёт неверное использование множественного наследования, а современные компиляторы способны подсказать ему, что в коде возникла путаница. А вот пользы от множественного наследования гораздо больше, чем вреда. Если вспомнить истоки ООП, то основной причиной создания этого подхода было определение типов, соответствующих реальным объектам и предметам. А коли это так, то «диван-кровать» и «компьютер моноблок» являются примерами таких реальных объектов, которые в ООП было бы правильно описывать множественным наследованием (от «дивана» и «кровати» в первом случае и от «системный блок» и «монитор» — во втором).

Упаковка объектов

В C# все типы неявно наследуются от типа System.Object или просто object. Тип object используется для, так называемой, упаковки объектов. Упаковка в синтаксическом виде — это ни что иное, как приведение объекта к типу object

string str = "Hello";
object obj = (object)str;

Надо отметить, что не смотря на все советы Скотта Мэйерса, приведение типов в C# осталось в стиле языка Си.
Итак, что же особенного в данной ситуации. Упаковка объектов в платформе .NET используется повсеместно, что может привести к путанице. Конечно, при неверном приведении среда исполнения выдаст исключение, но мне представляется, что runtime — это уже поздно. Ловить исключения при каждом присвоении смешно, согласитесь.
В качестве примера использования упаковки можно привести операцию клонирования. В интерфейсе IClonable объявлен метод Clone, который возвращает тип object. Служит этот метод только одной цели — создание копии объекта. Давайте посмотрим, на то, как выглядит определение и использование клонирования.

class Sheep : ICloneable
{
private string theName;
private int theAge;

public Sheep(string aName, int anAge)
{
theName = aName;
theAge = anAge;
}

public object Clone()
{
return new Sheep(theName, theAge);
}
}

class Program
{
static void Main(string[] args)
{
Sheep dolly = new Sheep("Dolly", 1);
Sheep dolly2 = (Sheep)dolly.Clone();
}
}

Представляется мне это всё, мягко говоря, не удобным и плохо читабельным.

Копирование объектов

Раз уж я заговорил о клонировании, то добью эту тему до конца. В C# все типы данных поделены на два вида: структурные и ссылочные. Структурные — это те типы, которые мы, C++ программисты, привыкли называть POD (Primitive Old Data) типами (int, float, bool, …), перчисления и структуры — которые в C# обрели новую жизнь. Все структурные типы являются наследниками типа System.ValueType и память для их объектов всегда выделяется в стеке. Все остальные типы, то бишь классы и интерфейсы, называются ссылочными и наследуются, как уже было сказано, от System.Object или любого производного, кроме System.ValueType. Память для объектов ссылочных типов выделяется в управляемой куче.
До сих пор всё выглядит сносно, но теперь давайте представим всё на деле. Допустим, у нас есть некая структура, содержащая какие-то данные.

struct Data
{
public int theData;

public Data(int aData)
{
theData = aData;
}
}

class Program
{
static void PrintIncrementedData(Data data)
{
Console.Write("Incremental data: {0}n", ++data.theData);
}

static void Main(string[] args)
{
Data data = new Data(50);
PrintIncrementedData(data);
PrintIncrementedData(data);
}
}

Данная программа не выглядит реально, но тем не менее, ведёт себя так, как мне хочется, а именно, выводит такой результат

Incremental data: 51
Incremental data: 51

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

Incremental data: 51
Incremental data: 52

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

Константность

Здесь я хотел бы поговорить о таких понятиях, которые с C++ называются константными методами и константными возвращаемыми значениями. Если быть точнее, то возвращаемое значение просто имеет константный тип. Но формулировки не столь важны, в C# нет ни того, ни другого. Методы в C# всегда возвращают ссылку на объет ссылочного типа или копию объекта структурного типа. Чем это чревато. Давайте посмотрим на пример из книги «C# и платформа .NET» Эндрю Троелсена, описанный в третьей главе, в разделе, посвящённом инкапсуляции. Так как этот пример там размазан по нескольким параграфам, я соберу его и немного дополню, смысл от этого не изменится.

class FullName
{
public string firstName;
public string secondName;

public FullName(string aFirstName, string aSecondName)
{
firstName = aFirstName;
secondName = aSecondName;
}
}

class Emploee
{
private FullName theFullName;

public Emploee(FullName aFullName)
{
theFullName = aFullName;
}

public FullName Name
{
get { return theFullName; }
}
}


class Program
{
static void Main(string[] args)
{
Emploee emploee = new Emploee(new FullName("William", "Gates"));
FullName name = emploee.Name;
name.firstName = "Linus";
name.secondName = "Torvalds";
Console.Write("1: {0} {1}n", emploee.Name.firstName,
emploee.Name.secondName);
Console.Write("2: {0} {1}n", name.firstName, name.secondName);
}
}

Не смотря на то, что автор утверждает, что свойство Name доступно только для чтения, я не применяя никаких усилий изменил значение приватной переменной, в чём легко убедиться, скомпилировав и запустив программу.

1: Linus Torvalds
2: Linus Torvalds

Стоит ли упоминать, что подобное поведение может привести к, довольно плачевным, последствиям. Для того, чтобы избежать такой ситуации не остаётся ничего другого, кроме как скопировать объект FullName перед возвратом из свойства.

public FullName Name
{
get { return new FullName(theFullName.firstName,
theFullName.secondName); }
}

Естественно, такой подход гораздо более затратный, нежели возврат константной ссылки в C++.

Делегаты

Увидев в первый раз концепцию делегатов, я пришёл в восторг. Это, действительно, мощная и простая технология позволяет создавать такие конструкции, для которых в C++ применялись колбеки, функторы и обсерверы. Если вкратце, то делегат — это особый вид функции, который объявляется в одном классе, а реализован может быть в другом. Кроме того, один делегат может быть реализован много раз и, при вызове, такого делегата, будут вызваны все его реализации. Давайте посмотрим на примере.

class People
{
public delegate void SpeakDelegate();
public SpeakDelegate Speak;
}

class Man
{
private string theName;

public Man(string aName)
{
theName = aName;
}

public void Speak()
{
Console.Write("Hello, my name is {0}n", theName);
}
}

class Program
{
static void Main(string[] args)
{
Man jhon = new Man("Jhon");
Man alice = new Man("Alice");
People people = new People();
people.Speak +=
new People.SpeakDelegate(jhon.Speak) +
new People.SpeakDelegate(alice.Speak);
people.Speak();
}
}

При вызове people.Speak будут вызваны методы Speak двух объектов — jhon и alice, а результат будет таким

Hello, my name is Jhon
Hello, my name is Alice

Этот же механизм (с небольшими дополнениями) используется при отправке сообщений. Но об этом я говорить не буду.
В общем-то, всё это выглядит очень красиво, но стоит помнить одну деталь — вызов делегата, у которого нет подписчиков приведёт к генерации исключения и крешу программы, если его не обработать. Следовательно, при вызове делегата мы должны каждый раз проверять, инициализирован ли он, благо не инициализированные делегаты всегда равны null. На первый взгляд, может показаться, что разработчики могли бы гарантировать безопасный вызов делегатов. Но нет, делегаты могут возвращать значение, а какое значение может вернуть то, что не было вызвано? Раз уж я об этом заговорил, то возвращаемым значением делегата — это значение, которое вернёт последняя вызванная делегатом функция.

Оператор foreach

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

foreach(ТипЭлементаКоллекции ссылка in коллекция)
{
}

«ссылка» принимает значение каждого из элементов и является именно ссылкой, а не копией. Чтобы Ваш класс мог работать как коллекция в операторе foreach, он должен реализовать интерфейс IEnumerable, в котором всего один метод — GetEnumerator. Вот если Вы используете не встроенные коллекции, то Вам придётся реализовать и интерфейс IEnumerator.

Коллекции

Список всевозможных коллекций в .NET достаточно большой, но что более всего меня вводит в недоумение — это то, что большинство из них не являются обобщёнными. Например, класс ArrayList хранит объекты типа object. Как я уже говорил в разделе об упаковке объектов, это может вызвать путаницу. По какой причине часть контейнеров сделали обобщёнными, а часть — хранящими объекты типа object для меня остаётся загадкой.

Разные мелочи

Из того, что я видел только в C#, стоит отметить статические конструкторы. Это особый вид конструкторов, в котором можно инициализировать статические члены класса. Кроме того, статические члены можно инициализировать прямо при объявлении, даже вызовом какой-либо функции.
Не удалось мне найти достойной замены оператору typedef из C++. Можно использовать ключевое слово using, но распространятся такой «typedef» будет только внутри одного модуля.

Итоги

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