Что, с точки зрения программиста на С++, представляет собой технология .NET Framework? Каковы отличия между языками С++ и C#/VB.NET? В чем концептуальная разница между двумя платформами разработки? Изложение лингвистических и концептуальных отличий языков .NET от С++ адресовано программистам, решившим перейти на новую платформу.
Вряд ли эта статья вызовет положительные эмоции как у тех, кто программирует на C++, так и у поклонников платформы .NET. Для программиста на языке С++ среда .NET Framework слишком сложна, а используемые в ней языки слишком просты. Среда слишком сложна, потому что использует шаблоны программирования или идиомы, с которыми программист, привыкший к C++, незнаком. Ему необходимо приложить очень большие усилия, чтобы понять и научиться использовать новые идиомы. Кроме этого нужно отправить в мусорную корзину массу технических деталей, без которых немыслимо программирование на С++. Именно потому, что при использовании языков .NET Framework не нужно помнить бесчисленное множество деталей, они и просты. Большой объем готового кода в обширной библиотеке, ориентация на компоненты, поддержка со стороны окружения (дизайнеры Visual Studio, управляемый код) и т.п. делают .NET Framework еще привлекательнее. Однако очень сложно отказаться от знаний, навыков и опыта, накопленного за годы упорного труда, и начать все заново.
Программисты, работающие с .NET Framework, не найдут в статье много интересного и возможно даже расценят ее как «наезд». Однако, автор все же решил потратить свои силы и время на то, чтобы сформулировать лингвистические и концептуальные различия двух платформ.
Языковые отличия
.NET Framework поддерживает большое количество языков; часть из них совершенно новые (например, C#), другие представляют собой существенную переработку старых языков (например, VB.NET). C++ также не остался в стороне. С помощью управляемого С++ (MC++) абсолютно любою программу на этом языке можно скомпилировать в промежуточный язык — IL (Intermediate Language), который может быть использован для выполнения в среде .NET.
МС++ ничем не отличается от обычного С++: он поддерживает шаблоны, множественное наследование и другие возможности стандартного С++. Кроме этого, в нем имеется большое количество расширений, которые позволяют использовать возможности .NET Framework, такие как атрибуты, управляемые объекты (managed objects), библиотека FCL и др. [1]. МС++, бесспорно, мощный язык, однако создание на нем полноценных приложений для .NET Framework затруднительно: он порождает неверифицируемый и сравнительно медленный код (тот же C# по результатам некоторых тестов генерирует лучший код; см. http://www.rsdn.ru/article/dotnet/templates.xml).
Перейдем к языковым различиям между С++ и C#. Не оставим в стороне и VB.NET.
Локальные статические переменные
Локальных статических переменных в C# и VB.NET нет, а жаль — часто возникает ситуация, когда со статической переменной работает только одна функция класса. Идеальным было бы ее объявить статической внутри функции, чтобы не засорять внешнее пространство и тем самым упростить код. Не вижу технических причин, по которым локальные статические переменные нельзя было эмулировать с помощью переменных-членов, как это делается в МС++.
Локальные классы
CLR поддерживает вложенные классы (они, кстати, отличаются от классов в С++), но не поддерживает локальные классы в функциях. В С++ локальные классы играют очень важную роль: с их помощью можно создавать аналоги подфункций, блоков try..finally и многое другое. Надеюсь, с выходом следующего стандарта их можно будет использовать в качестве параметров шаблонов.
Что касается вложенных классов в CLR, то они всегда являются друзьями внешних классов. Не спорю, в определенных обстоятельствах это удобно, но экономия всего одной строки кода ради ограничения возможностей по сокрытию тела класса, как мне кажется, того не стоит. Сильно подозреваю, такое решение связано со следующим пунктом.
Друзья
Если бы были разрешены друзья в том виде, в котором они присутствуют в С++, алгоритм проверки наличия доступа у вызывающего кода существенно усложнился бы. Необходимо было бы хранить информацию в метаданных сборки о том, кто чей друг. А это вызвало бы перекрестные зависимости между сборками, причем поведение кода в одной сборке зависело бы от манифеста другой сборки. Все это привело бы к тому, что проверка доступа проводились бы всегда динамически. Не трудно догадаться, что это привело бы к существенному снижению быстродействия.
Как же решать проблемы, ради которых и появилось понятие друга в С++? Здесь многое зависит от конкретной задачи, но во многих случаях единственным приемлемым решением оказывается следующее: методы и поля класса, которые должны быть доступны для другого класса-друга, необходимо выделить в базовый класс, перенести этот класс в сборку класса-друга и назначить уровень доступа Internal (Friend для VB.NET) для членов класса (не стоит путать этот новый уровень доступа в .NET Framework с друзьями С++; последние никогда не являлись модификаторами доступа, они лишь открывали некоторые члены класса или сам класс для друга). Затем, в основной сборке нужно создать класс, унаследованный от созданного в другой сборке, и реализовать оставшуюся функциональность.
Модификаторы const
Увы, константность в .NET Framework сильно хромает. Нет ни константных параметров, ни константных методов. Невозможно гарантировать неизменность больших объектов при передаче их в функцию по ссылке. Создавать всякий раз копии нереально. В одном проекте это можно обойти с помощью административных мер, но не при создании библиотеки.
Еще одна проблема состоит в том, что в CLR константами могут быть только простые типы и объекты типа string и object. Пользовательские типы нельзя создать неизменяемыми. Пример на VB.NET:
Const gi As Integer = 9
Const ss As String = «»
Const s As some_impl = New some_impl() ?ошибка
Константы в .NET Framework идентичны статическим константам в С++. Простым константам членам-класса в С++ соответствуют переменные-члены ReadOnly в .NET Framework.
Перегрузка внешних функций
Не сказал бы, что это важная черта С++, но иногда ее не хватает. Рассмотрим такой код:
class some_class {
public:
static void foo(char){}
class some_nested {
public:
void foo(int){}
void call_foo() {
foo(1); // вызов some_class::some_nested::foo(int)
foo(?a?); // вызов some_class::foo(char)
}
};
};
Опираясь на информацию о типах аргументов, компилятор разрешает вызов той или иной функции. В VB.NET и C# вызов внешней функции запрещен: компилятор просто не видит внешней функции и выдает ошибку о несоответствии типов аргументов. Вот пример на VB.NET:
Class some_class
? внешняя функция с параметром типа char
Public Shared Sub foo(ByVal c As Char)
End Sub
Class some_nested
? внутренняя функция с параметром типа integer
Public Sub foo(ByVal i As Integer)
End Sub
Public Sub call_foo()
foo(«a»c) ?ошибка (вызов функции с параметром типа char)
End Sub
End Class
End Class
Естественно, если область видимости функции указана явно (например, some_class.foo()), код успешно компилируется.
Еще один интересный момент, который касается вложенных классов, связан с тем, что в .NET Framework разрешено наследование вложенного класса от внешнего. Например:
Class some_class
Class some_nested : inherits some_class
End Class
End Class
Не знаю, насколько это правильно и выгодно, но отличие от С++ налицо.
Уровень доступа при наследовании
В С++ производный класс может наследоваться от базового с указанием формы наследования. Это может быть открытая и закрытая форма. Открытое наследование означает, что производный класс является базовым, закрытое — что производный класс использует базовый. Открытое наследование используется в большинстве иерархий классов, а закрытое — когда хотят скрыть факт принадлежности производного класса к базовому. При открытом наследовании экземпляры производного класса можно использовать везде, где требуется базовый класс. Кроме этого, разрешены неявные понижающие преобразования из производного в базовый класс. Все это запрещено для закрытого наследования.
В .NET Framework закрытого наследования нет — есть только открытое. Конечно, его можно эмулировать с помощью делегирования, но это будет именно эмуляция, что приведет к менее прозрачному коду при реализации объектной модели и повысит сложность.
Уровень доступа виртуальных функций
В .NET Framework такие понятия, как виртуальная функция, абстрактный класс и интерфейс, отличны от их аналогов в С++. Интерфейс определяет контракт, состоящий из набора функций, свойств и событий, которые производный класс обязан реализовать, даже если он является абстрактным. Это касается только высокоуровневых языков, наподобие C# и VB.NET; в MC++, к примеру, такого ограничения нет.
Реализация интерфейсной функции хоть и является виртуальной функцией, однако в C# и VB.NET компилятор запретит переопределение в производных классах этих виртуальных членов. Для того, чтобы это стало возможным, необходимо явно указать, что данная реализация интерфейсной функции, свойства или события является виртуальной.
Абстрактный класс — это не интерфейс, а такой же класс, как и все остальные, который просто не реализовал некоторые виртуальные функции (или свойства). Абстрактный класс в .NET Framework является наиболее близким аналогом интерфейса в С++ (вообще в С++ интерфейс и абстрактный класс — синонимы). Проблема в том, что невозможно изменить уровень доступа виртуальной функции в производном классе. Справедливости ради нужно отметить, что в С++ сокрытие реализации виртуальной функции в производном классе в основном используется при реализации интерфейса. В .NET Framework реализация интерфейса может быть закрытой (пример закрытой реализации интерфейса и виртуальной функции на языке VB.NET можно найти в Сети по адресу www.rsdn.ru/team/alex/virtfunc.txt). Я не совсем понимаю причины такого решения. Остается надеяться, что эта возможность, относительно редко используемая в С++, нам совершено не понадобится.
Массивы
Cинтаксис объявления массива в C# несколько изменился по сравнению с С++ (теперь квадратные скобки нужно ставить возле типа), изменен и механизм инициализации. Список инициализации теперь должен в точности соответствовать длине массива. Если она будет больше, чем количество элементов в списке инициализации, компилятор выдаст ошибку. В С++ ошибок не будет, а недостающие элементы будут инициализированы нулями. Указание размера массива меньше списка инициализации приводит к ошибке в обоих языках. Приведем пример.
C#:
int[] arrInt = new int[2] {1}; // ошибка
С++:
int arrInt[2] = {1}; // эквивалентно {1,0}
Встроенные массивы в .NET Framework более удобны, чем встроенные массивы в С++, однако они сильно уступают по производительности стандартным массивам С++. Стандартные контейнеры STL также намного превосходят контейнеры библиотеки FCL из пространства имен System.Collections.
Виртуальные вызовы в конструкторе
Эта особенность, как представляется, может принести наибольшую головную боль программистам. Дело в том, что в .NET Framework разрешены виртуальные вызовы функций в конструкторах класса. Как результат, некоторый код может исполняться в еще полностью не инициализированных классах. В С++ гарантируется, что прежде чем будет вызвана какая-либо функция класса, все его члены и базовые классы будут сконструированы. В С++ можно вызвать из конструктора виртуальную функцию, однако выбор функции будет происходить на этапе компиляции, а вызов в любом случае не будет полиморфным. .NET Framework такой гарантии не дает, поэтому код виртуальных функций должен быть написан особенно осторожно, так как есть вероятность его исполнения в частично несконструированном экземпляре класса. Вот пример неправильно реализованной виртуальной функции, которая вызывается в конструкторе:
Class some_class
Public Sub New()
foo(«dima») ? вызов виртуальной функции
End Sub
Public Overridable Sub foo(ByVal i As String)
End Sub
End Class
Class some_derived
Inherits some_class
Private arrStr() As String
Public Sub New()
arrStr = New String(9) {}
End Sub
Public Overrides Sub foo(ByVal i As String)
? если функция вызвана из конструктора some_class,
? будет исключение NullReferenceException
arrStr(0) = i
End Sub
End Class
При создании экземпляра класса some_derived последовательность вызовов будет следующей: вызывается конструктор класса some_class, конструктор класса some_class полиморфно вызывает функцию some_derived::foo, вызывается конструктор класса some_derived. Инициализация класса some_derived происходит позже, чем вызов одной из его функций, поскольку конструктор — это уже не первая функция класса, вызываемая при создании экземпляра. Думаю, это грубейшая ошибка разработчиков: конструктор на то и предназначен, чтобы быть первой вызываемой функцией для создаваемого объекта, где производиться вся необходимая инициализация.
Первый предварительный вывод
.NET Framework имеют много тонких отличий от С++. При внешнем сходстве с С# проблемы для С++ программистов только усугубляются. С другой стороны, Microsoft упорно продолжает продвигать платформу, позиционируя ее как платформу будущего.
Концептуальные отличия
C++ — это язык плюс стандартная библиотека STL. Со своей стороны, .NET Framework — это платформа, а не просто язык или технология. К тому же, это комплексная платформа, рассчитанная на создание программ в разных средах от карманных компьютеров до Internet-приложений. .NET Framework — это CLR (Common Language Runtime) и FCL (Framework Class Library). Сравнивать же можно только сопоставимые категории. Поэтому с целью сравнения условно соотнесем CLR с языком С++, а FCL — с STL, и именно в этой плоскости будем проводить сравнение (см. таблицу 1).
Шаблоны
Таблица 1. Сравнение CLR и C++ |
Шаблоны — очень мощное средство языка С++, позволяющее реализовать модель, так называемого, обобщенного программирования. С помощью шаблонов можно создавать универсальные алгоритмы, работающие с различными типами. Стандартная библиотека шаблонов STL полностью базируется на них, что придает ей чрезвычайную гибкость (впрочем, как и особенную сложность). Шаблоны — очень сильный «козырь» С++ перед .NET Framework, поэтому следующая версия CLR в .NET Framework 2.0 будет поддерживать механизм generic. Не думаю, что это будут полноценные шаблоны С++. Основным отличием данного механизма от шаблонов С++ будет поддержка в среде времени исполнения и возможность инстанциировать их в других сборках. Строго говоря, возможность компиляции шаблонов и инстанциирование их в других единицах трансляции прописана в стандарте С++, однако на практике, очень небольшое количество компиляторов могут выполнить такую работу.
Generic являются полноценными типами, к которым можно обращаться с помощью рефлексии (например, методы GetGenericMethodDefinition и GetGenericParameters). Это несомненное достоинство накладывает определенное ограничение на generic: в момент компиляции generic компилятор должен быть уверен, что будущий параметр-тип поддерживает все операции, которые используются в шаблоне, например, данный код не будет откомпилирован, так как в общем случае объект типа Object не поддерживает сравнение (C#):
static public T Min(T t1, T t2) {
if (t1 < t2) return t1;
else return t2;
}
Проблема решается с помощью ограничений (constraint), которые могут налагать ограничения на реализуемый типом интерфейс, базовый класс или конструктор. Правильный код выглядит так:
static public T Min(T t1, T t2)
where T : IСomparable {
if (t1.CompareTo(t2) < 0) return t1;
else return t2;
}
Эквивалент на VB.NET:
Function Min(Of T As IComparable)
(ByVal t1 As T, ByVal t2 As T) As T
If t1.CompareTo(t2) < 0 Then Return t1 Else Return t2
End Function
Проблему производительности generic решают, но не так, как хотелось бы (например, можно было бы избавиться от боксинга и виртуальных вызовов). Основной плюс от их использования будет выражаться в виде более гибких структур данных и алгоритмов.
Множественное наследование
С множественным наследованием реализации дела похуже. Единственный способ объединить две реализации заключается в делегировании: не ахти что, но выход из положения.
Пример для C#:
interface interface1 // Первый интерфейс
{ void foo1(); }
interface interface2 // Второй интерфейс
{ void foo2(); }
class interface1impl : interface1 // Реализация первого интерфейса
{ public void foo1(){} }
class interface2impl : interface2 // Реализация второго интерфейса
{ public void foo2(){}}
class cls : interface1, interface2 // Использование реализаций
{
private interface1impl i1 = new interface1impl();
private interface2impl i2 = new interface2impl();
public void foo1(){i1.ddd_func();}
public void foo2(){i2.ddd_func2();}
}
Решение очевидное и стандартное, и его можно автоматизировать с помощью атрибутов. Да и его реализация достаточно тривиальна: идея состоит в том, чтобы создать производный класс и в нем, на основе информации атрибутов, реализовать интерфейсы. Генерация класса будет происходить в процессе выполнения. Вот так может выглядеть наш специализированный атрибут (класс, наследуемый от System.Attribute), с помощью которого можно помечать, какой член класса отвечает за реализацию интерфейса:
Inherited:=True, AllowMultiple:=False)> _
Class ImplementsAttribute : Inherits Attribute
Private _t As Type
Public Sub New(ByVal t As Type)
_t = t
End Sub
Public ReadOnly Property InterfaceType() As Type
Get
Return _t
End Get
End Property
End Class
А вот как он используется:
Interface SomeIntf
Sub Hello()
End Interface
Class SomeIntfImpl : Implements SomeIntf
Public Sub Hello() Implements SomeIntf.Hello
Console.WriteLine(«Hi from VB.NET!»)
End Function
End Class
Class SomeClass
? Переменная-член _Intf будет использоваться
? для реализации интерфейса SomeIntf
_
Protected _Intf As New SomeIntfImpl()
End Class
Shared Sub Main()
Dim c As SomeClass =
CType(CreateImplementation(GetType(SomeClass)), SomeClass)
CType(c, SomeIntf).Hello()
End Sub
Алгоритм функции CreateImplementation таков: с помощью Reflection создается новый класс, производный от указанного в параметре типа; с помощью CodeDom класс компилируется в сборку (assembly); создается экземпляр сгенерированного класса, и клиенту отдается ссылка (полный код функции находится по адресу www.rsdn.ru/team/alex/createimplementation.txt).
Это очень сильно отличается от того, что допускалось в С++. Любой тип в .NET Framework представлен в качестве объекта и доступен во время исполнения программы. Более того, на лету вы можете создавать свои собственные типы (хотя это и не быстро). Реализовать с помощью рефлексии агрегирование (по аналогии с агрегированием СОМ) также не представляется сложным (правда, она будет несколько менее эффективна, так как будет использовать делегирование).
Возможно, в C# и в VB.NET когда-нибудь появится множественное наследование реализации, но пока приходится обходиться тем, что есть.
Сборка мусора
В общем случае освобождение ресурсов и освобождение памяти, занимаемой объектом — две различные операции. Единственное ограничение состоит в том, что освобождение ресурсов должно предшествовать разрушению объекта (освобождению памяти). В С++, как правило, ресурсы, занимаемые объектом, освобождаются в деструкторе, который вызывается непосредственно перед освобождением памяти. Это хорошо, потому что во многих случаях компилятор автоматически определяет, когда объект перестает жить (для стековых объектов) и, так как освобождение ресурсов должно предшествовать освобождению памяти, вызывает деструктор. Для не стековых объектов программист сам определяет момент, когда нужно удалить объект. К счастью для С++, существует много классов-оберток, которые автоматизируют процесс удаления объекта после того, как он становится не нужен, например, boost::shared_ptr, который работает на основе подсчета ссылок. Ключевой момент для С++ заключается в том, что вызов деструктора и освобождение памяти — неотделимые операции. Замечу, что это не совсем так. У программиста всегда есть возможность вручную вызвать деструктор. Однако здесь нужно быть очень осторожным. Если вызвать деструктор, а после этого попытаться удалить объект с помощью функции delete, это приведет к повторному вызову деструктора, что является неопределенным поведением. Неопределенное поведение в С++ означает, что дальнейшая работа программы не гарантируется и может привести к каким угодно последствиям. Память нужно освободить каким-либо другим способом, не вызывая функцию delete. Это можно сделать с помощью оператора delete, который не вызывает деструктор, а просто освобождает память. Однако это опять приведет к неопределенному поведению, так как память для оператора delete не была выделена с помощью соответствующего оператора new.
Таким образом, мы приходим к следующей последовательности, при которой явный вызов деструктора не приводит к неопределенному поведению. Выделяем память с помощью оператора new, вызываем конструктор на выделенной памяти с помощью placement new, работаем, вызываем деструктор, и, наконец, освобождаем память с помощью оператора delete.
В .NET Framework каждый ссылочный объект может реализовать метод Finalize, который синтаксически представлен в виде деструктора в С#. Он вызывается сборщиком мусора непосредственно перед освобождением памяти для объекта. Все бы было замечательно, если бы не две проблемы: порядок вызовов методов Finalize не определен и метод Finalize вызывается в неопределенное время, а может быть и не вызван вовсе. Однако, для освобождения ценных ресурсов, вроде заголовков файлов или соединений с базой данных, эти условия совершенно не приемлемы. Ресурсы должны освобождаться тогда, когда в них отпадает надобность, а не когда сборщик мусора решит собрать память. По этой причине, большое количество классов в .NET Framework имеют метод Close (его программист обязан вызывать собственноручно), где и происходит освобождение ресурсов. Память под выделенный объект по-прежнему освобождалась сборщиком мусора. Пример пользовательского кода можно посмотреть в [1]. Все бы было просто замечательно, если бы лень не была двигателем прогресса. Подобный код на С++ выглядит гораздо аккуратнее и надежнее.
Для того, чтобы код был также удобен и безопасен, был создан новый интерфейс — IDisposable, предназначенный для унификации вызовов метода Close по освобождению ресурсов. Вместо метода Close нужно вызывать единственный метод интерфейса IDisposable — Dispose, а для того чтобы код был более похож на С++, в C# ввели ключевое слово using, которое автоматически вызывает метод Dispose при выходе из области видимости. Вот пример использования [1]:
void Test() {
using (TextWriter tw = new StreamWriter(«test.txt»,true)) {
tw.Write(«123»);
}
}
Освобождение ресурсов и освобождение памяти окончательно разделены. Это очень ново для С++ программиста, но в этом есть смысл — память перестает быть ресурсом, а освобождение всего остального достигается почти вручную. Однако проблемы все же остались. Действительно, using присутствует только в C#, а других языках производится по-прежнему вручную писать try/finally. Кроме того, правильно написать методы Finalize и Dispose довольно сложно [1]. Наконец, некоторые классы не освобождают ресурсы в методе Finalize, предоставляя единственную для этого возможность через интерфейс IDisposable. Однако многие забывают использовать using или вручную вызывать Dispose. Это приводит к потере или порче ресурсов.
Решением этих проблем мог бы служить механизм подсчета ссылок (как в СОМ или в boost::shared_ptr) для автоматического вызова метода Dispose. Мне непонятно, почему проектировщики прошли мимо этой очевидной и очень мощной возможности, которая бы действительно явилась ноу-хау в современных языках программирования. Сейчас же, детерминированное завершение в С++ все еще может соперничать с механизмами уборки мусора в .NET Framework. Кстати, для MС++ достаточно легко реализовать эту идею. В http://www.rsdn.ru/team/alex/auto_ptr.txt приведен простой пример реализации интеллектуального указателя (smart pointer) для управляемых объектов. Он очень похож на стандартный auto_ptr (не только именем), но вместо вызова деструктора и освобождения памяти вызывает метод Dispose. Его можно использовать в качестве замены ключевого слова using из C#. Можно доработать этот класс и добавить интеллектуальный указатель с подсчетом ссылок.
Второй предварительный вывод
Таким образом, мы приходим к выводу, что язык С++ устарел. Необычайно широкий охват современных технологий в FCL существенно раздвигает границы применения .NET Framework. Если рассматривать пространства имен System.Web, System.Reflection, System.Runtime.Serialization, то сравнение с С++ становится просто бессмысленным. Вот если бы кроме STL в стандарт входило бы еще некоторое множество библиотек, которые бы удовлетворяли нужды программистов, то в сравнении был бы смысл. И уже некоторые подвижки в этом плане есть. Например, библиотека Boost является очень хорошим и своевременным расширением стандартной библиотеки, куда входят многие нужные классы. К сожалению, библиотека еще не стала стандартом, она достаточно сложна и требует хороших компиляторов. Кроме этого, некоторые вещи (как-то метаинформация) требуют поддержки со стороны языка. У программиста на C++ есть альтернатива — писать самому всю недостающую функциональность библиотек и самого языка, либо присматриваться к .NET Framework (есть, правда, и третья возможность — искать в Сети полные ошибок классы, тратить уйму времени на их разбор, тестирование, исправление и частичное переделывание под конкретные нужды).
С++ никуда и никем не вытесняется. Просто программная инженерия развивается, и новые технологии предлагают решения ранее нерешенных проблем.
Перспективы
Платформа .NET Framework молода и, по сравнению с остальными средствами, на ней написано еще ничтожно мало реальных систем. Рассмотрим актуальные проблемы проектирования и возможную роль .NET Framework в их решении.
Масштабируемость. Очевидно, масштабируемость может быть достигнута только в компонентно-ориентированных, многоуровневых системах, где связь между компонентами очень слаба, а внутри — напротив, очень сильна. Такие компоненты и целые уровни можно выносить на отдельные серверы, увеличивая таким образом производительность простым наращиванием аппаратных средств. Безусловно, этот атрибут качества в основном зависит от проектировщика или архитектора, однако .NET Framework предоставляются все необходимые средства для этого. В старые времена, когда Windows только начинала завоевывать мир, господствовала клиент-серверная архитектура и ее масштабируемость была очень невелика — сервер представлялся монолитным модулем, реализующим бизнес-функции, работу с базами данных, безопасностью и другими вещами. Клиент-серверная архитектура скоро уступила место более сложной, n-звенной архитектуре. В ней пропагандировалось вынесение логически взаимосвязанных частей в отдельные модули с очень сильной внутренней связью и очень слабой внешней. Слабая связанность достигалась за счет использования интерфейсов, которые определяли контракт между сервером и клиентом. Бизнес-объекты начали выносить в отдельные слои, а в качестве основного средства для реализации такой архитектуры использовали СОМ. Это замечательная технология, которая позволяет создавать слабо связанные компонентно-ориентированные системы, но у нее есть один недостаток: она предназначена для корпоративного пользования. Конечно, есть средства, которые позволяют использовать DCOM по протоколу HTTP, но количество проблем и сложность системы от этого возрастают многократно. Через некоторое время стало ясно, что для дальнейшего масштабирования программных систем, с вынесением части логики на отдельные серверы в Internet, механизм СОМ не подходит. Главный гуру по СОМ, Дон Бокс, понял это и начал работу над созданием текстового протокола, основанного на XML, который бы заменил Object RPC в качестве транспорта для удаленного вызова процедур. Его работа (конечно, он работал не один) получила название SOAP. На основе этого формата была построена концепция Web-служб. Платформа .NET Framework очень неплохо поддерживает SOAP 1.1, WSDL 1.1 и WSE. Кроме этого, .NET Framework предлагает собственное средство распределенному взаимодействию систем, названное .NET Remoting. И это неплохо, потому что есть возможность сравнения различных механизмов взаимодействия [3]. В многозвенной архитектуре появился новый компонент — сервисный агент. (За подробностями отсылаю вас к пространному документу, озаглавленному Application Architecture for .NET: Designing Applications and Services.)
Конфигурируемость. .NET Framework снова вводит в моду настройки в текстовом файле. Теперь это специальный файл в формате XML. В нем можно задавать как системные параметры приложения, так и пользовательские. Также можно создать специальные пользовательские разделы конфигурационного файла и обрабатывать их с помощью собственных средств. Часть информации из реестра перекочевала непосредственно в сам исполнимый файл. Теперь все, что нужно для запуска приложения, находится в самом исполнимом файле и в файле конфигурации. Конфигурирование, мониторинг, информация о местонахождении сервисов и политика исключений вынесены в специальный блок операционного управления (operational management), которое, наряду с безопасностью и взаимодействием, представляет собой новый модуль в многозвенной архитектуре под названием политики (policy).
Работоспособность. Ошибки есть в любой программе и в любом конечном продукте. Вопрос в том, сколько их и готово ли само приложение к ним. Количество грубых ошибок, связанных с низкоуровневой работой с памятью в приложениях .NET Framework на порядок меньше. Это достигается благодаря верифицируемому коду и отсутствию необходимости работать с «сырой» памятью. Кроме этого, улучшены средства по отладке кода и получению информации об исключении.
Для решения многочисленных задач проектирования в новой среде в MSDN существует специальный раздел по шаблонам проектирования (design pattern). В помощь разработчикам появляются так называемые блоки приложения (application block), которые являются надстройкой над стандартными средствами и позволяют упростить их использование. Иными словами, создается вся та инфраструктура, которая необходимая для построения программных систем.
Окончательный вывод
В .NET Framework не все гладко и стройно. Есть большое количество недоработок и досадных ошибок. Скажем, контейнерные классы значительно уступают по производительности и удобству контейнерам библиотеки STL. Но уже сейчас ясно, что платформа .NET Framework принесла в программирование новые концепции. Проектировщики получили совершенно новый инструмент. Трудно говорить о том, что инструмент этот лучше, чем прежний, — он просто другой, хотя некоторые положительные черты видны невооруженным взглядом: метаинформация описывает все и вся, мощная поддержка в рамках среды времени исполнения, концепция сборок (новые исполняемые модули, содержащие код и метаинформацию), очень богатая библиотека.
Литература
- Игорь Ткачев, Управляемый С++. www.rsdn.ru/article/dotnet/mcpp.xml.
- Дин Леффингуэлл, Дон Уидрик, Принципы работы с требованиями к программному обеспечению. М.: Вильямс, 2002.
- Мари Кантор, Управление программными проектами. М.: Вильямс, 2002.
Алексей Ширшов (alex@megatec.ru) — менеджер проектов компании E-System (Москва), MCSD.
Улучшения в C#
Возможность передавать в функцию переменное количество параметров в виде массива. Это стало возможным благодаря тому, что любая сущность в .NET Framework является производной от одного базового класса Object. Параметры могут быть как одного типа, так и разных. В последнем случае, все они приводятся к базовому типу, например:
public void foo(params object[] args){
foreach(object item in args) {
Console.WriteLine(item.ToString());
}
}
Это намного удобнее и проще, чем работать с макросами va_start, va_end и va_list из языка Си.
Отсутствие неявных понижающих преобразований для простых типов. Надо признать, что решение поддерживать неявные преобразования Си для простых типов в С++ было не самым лучшим, например:
public void foo() {
char c = ?c?; int i = c;
// В этой строчке будет выдано сообщение
// «Cannot implicitly convert type ?int? to ?char?»
char cc = i;
}
Это не может не радовать, хотя современные компиляторы С++ (тот же VC++ 7.0 и выше) имеют средства дополнительного контроля подобных преобразований с потерей данных. Например, в компиляторе VC++ 7.0 и выше появилась опция /RTCc, которая на этапе выполнения позволяет выявить подобную проблему.
Необходимость инициализации простых типов перед использованием. Если вы попытаетесь прочесть значение неинициализированной переменной, компилятор выдаст ошибку. Это также, на мой взгляд, очень полезное отличие от стандарта С++. Программист может попросту забыть инициализировать переменную, и в дальнейшем использовать заведомо ложное значение. В последних версия компилятора С++ от Microsoft появилась возможность контролировать инициализацию, правда на этапе выполнения, с помощью опции /RTCu. Вот пример на С++, который успешно компилируется, но в процессе выполнения выдает исключение:
class cls {
public:
int j;
};
int main() {
cls a;
// «Run-Time Check Failure #3 -
// The variable ?a? is being used without being defined.»
int h = a.j;
return 0;
}