.Net глазами дельфийца - первые впечатления.
Часть 1. C#
Оглавление
Введение
Чего нет в C#
Чего нет в Delphi
Заключение
Введение
При знакомстве с новым языком
программирования любого программиста в первую очередь интересует
семантическая основа языка, т.е. насколько его выразительные возможности
позволяют реализовать привычные логические конструкции.
Учитывая то, что C#, как и Delphi, выступает
одновременно в двух качествах, т.е. с одной стороны, является
семантически строго определенным языком программирования и, с другой
стороны, использует поставляемые в составе .Net библиотеки классов и
компонентов, на первом этапе имеет смысл сконцентрироваться на самом
языке программирования, т.к. изучение и сравнительный анализ библиотек
классов - гораздо более объемная работа.
Чего нет в C#
Отсутствие в C# некоторых вещей обусловлено
тем, что C# является <чисто> объектным языком программирования, а Delphi
- гибридным. Тем не менее, в C# или имеются, или могут быть легко
реализованы самостоятельно практически все семантически эквивалентные
конструкции.
Итак, C# не предоставляет следующие
возможности (их рассмотрение не вошло в настоящий документ в силу или
второстепенного значения, или наличия семантически эквивалентных
реализаций в библиотеке CLR):
- значения параметров по умолчанию
- множества (set) - реализуется в виде специальных классов в
библиотеке CLR
- диапазоны (subrange) - реализуется в виде специальных классов в
библиотеке CLR
- синонимы простых типов
- ресурсные строки (resourcestring) - рассматривается как частный
случай констант
Более существенные конструкции, которых нет в
C#:
Процедуры, функции
Если считать, что процедуры - это просто
функции, которые не возвращают никакого значения, то семантическая
нагрузка процедур и функций в Delphi одинакова. Это - выполнение
некоторого фрагмента кода, который, возможно, зависит от входных
параметров:
procedure A(aParam: integer);
begin
// ...
end;
function B(aParam: integer): integer;
begin
// ...
Result := 0;
end;
A(1);
X := B(1);
В C# семантическим эквивалентом процедур и
функций выступают статические методы классов.
// класс-обертка
class Func {
// статический метод без возвращаемого значения - эквивалент процедуры
static public void A(int aParam);
// статический метод - эквивалент функции
static public int B(int aParam);
}
// вызов процедуры
Func.A(1);
// вызов функции
int X := Func.B(1);
Глобальные константы
Семантическая нагрузка в Delphi - определение
значений примитивных типов данных, доступных из любого места кода и
неизменяемых в процессе выполнения программы.
const A = 100;
const B = 'строка';
D := A;
ShowMessage(B);
Семантический эквивалент в C# - статические
константы.
// класс-обертка
class Const {
// описание констант
public const int A = 100;
public const string B = "строка";
}
// использование констант
int a = Const.A;
MessageBox.Show(Const.B);
Кроме статических констант C# предоставляет
механизм статических полей <только для чтения>, который позволяет
программисту использовать в качестве констант не только примитивные
значения, но и объекты. Пример кода:
// класс-обертка
class Const {
// число-константа
public static readonly int A = 1;
// объект-константа
public static readonly MyObject Obj = new MyObject();
}
Глобальные переменные
Семантическая нагрузка в Delphi -
формирование объектов программы (как примитивных типов, так и сложных),
доступных из любого места кода и, возможно, изменяемых в процессе
выполнения программы.
var A: integer;
B := A;
A := 1;
Семантический эквивалент в C# - статические
поля классов.
// класс-обертка
class Globals {
// определение статических переменных
// инициализация по умолчанию = 0
public static int A;
// одновременные описание и инициализация
public static int B = 1;
}
// использование статических переменных
int a = Globals.A;
Globals.A = 1;
int b = Globals.B;
Globals.B = 1;
Предварительное объявление типов
Предварительное объявление типов на самом
деле не предусмотрено общей теорией объектно-ориентированного
программирования и является частным решением Delphi, направленным на
ослабление правила, которое было введено еще в классическом Pascal, -
<все типы данных, используемые для построения сложных типов, должны быть
или примитивного типа, или описаны до их использования>.
Пример кода на Delphi:
type
TMyObject1 = class;
TMyObject2 = class;
TMyObject1 = class
function GetChild(Index: int): TMyObject2;
end;
TMyObject2 = class
property Owner: TMyObject1 read fOwner;
end;
В C# предописание типов не требуется, т.к. в
пределах области видимости классов (обрамляющий класс, пространство
имен) порядок объявления несущественен. Такое решение упрощает написание
кода:
namespace MyObjects {
public class TMyObject1 {
public TMyObject2 GetChild(int Index) { ... }
}
public class TMyObject2 {
public TMyObject1 Owner {
get { return owner; }
}
}
}
Типизированные константы
Типизированные константы в Delphi позволяют
хранить не только значения примитивных типов, но и массивы, записи, а
также указатели, включая указатели на процедуры и функции:
const A: integer = 1;
const B: array [1..3] of integer = (1, 2, 3);
В C# константы всегда типизированы - как при
использовании модификатора const, так и readonly.
В Delphi при использовании директивы
компилятора {$J+} (установлено по умолчанию) типизованные константы
ведут себя как обычные переменные, которые инициализируются одновременно
с описанием, т.е. их значение может быть изменено в ходе выполнения
программы:
const A: integer = 1;
X := A;
A := 2;
В C# действуют более строгие правила - если
константа, то поменять ее значение невозможно. Если же используется поле
<только для чтения>, то его содержимое может быть изменено в контексте
объекта:
// класс-обертка
public class Const {
// число-константа
public readonly int A = 1;
// метод изменяет значение поля A
public void ChangeA(int NewValue) { A = NewValue; }
}
При использовании типизованных констант в
качестве инициализируемых переменных в области видимости подпрограммы
(метода, процедуры, функции) в Delphi наблюдается недокументированный
побочный эффект - данные, записанные в локальные типизованные константы,
сохраняются между вызовами подпрограмм:
procedure A;
const B: integer = 0;
begin
Inc(B);
ShowMessage(IntToStr(B));
end;
procedure Do;
begin
A;
A;
A;
end;
Результаты вывода: 1, 2, 3
В C# подобный побочный эффект отсутствует.
Вообще, стандартизация C# в качестве международного стандарта (ECMA,
2001 год) гарантирует отсутствие побочных эффектов. Поэтому если в
программе C# необходимо сохранять некоторое состояние между вызовами
подпрограмм (методов), используются стандартные средства, в частности,
статические или обыкновенные поля классов.
Const-параметры
В Delphi семантический смысл const-параметров
заключается в указании компилятору на возможность оптимизации передачи в
функцию (процедуру, метод) неизменяемой ссылки на некоторый объект
программы. Так, например, конструкция типа:
procedure A(const S: string);
означает, что в качестве параметра процедуры
будет передаваться ссылка на строку (при этом копирования содержимого
строки в стек вызова процедуры не происходит). Кроме того, содержимое
строки S внутри процедуры изменить нельзя.
В C# не предусмотрено прямого эквивалента
const-параметров. Тем не менее, в случае необходимости может быть
построена семантически эквивалентная конструкция (аналогия
вышеприведенному примеру):
class ReadOnlyString {
ReadOnlyString(string S) { this.S = S; }
public readonly string S;
static void Test(ReadOnlyString s) { Console.Write(s.S); }
static void Main() {
string s = "проверка const-параметров";
ReadOnlyString.Test(new ReadOnlyString(s));
}
}
Приведенный код иллюстрирует использование
классов-<оберток> (т.н. wrappers) и полей <только для чтения>.
Указатели
В Delphi указатели чаще всего используются
для <тонкого> управления такими конструкциями, как записи. В частности,
при передаче записи в качестве параметра в подпрограмму (процедуру,
функцию или метод) происходит побайтное копирование в стек вызова, что
может приводить к серьезным накладным расходам. Альтернативное решение -
передать в качестве параметра указатель на запись и уже через него
внутри подпрограммы получить доступ к элементам записи.
В C# приведенный пример использования
указателей на записи более изящно и безопасно реализуется с
использованием такой конструкции, как struct. Структуры, как и записи
Delphi, могут использоваться для хранения данных и, что более важно с
точки зрения семантики, являются объектами, передаваемыми не по ссылке,
а по значению.
На самом деле, в C# имеется всего лишь одна
возможность использовать указатели - т.н. <небезопасный> код (unsafe
code). Особенности его использования определены в стандарте C#
достаточно подробно (спецификация C#, приложение A). Однако, необходимо
отметить, что в практическом программировании редко приходится
использовать указатели для иных целей, кроме оптимизации
производительности. Поэтому, учитывая классическое правило <80/20> (<80%
пива выпиваются двадцатью процентами населения>, или <80% используемых
ресурсов программы приходится на 20% кода>, или <80% объема работы
позволяет улучшить производительность только на 20%>), можно
акцентироваться на оптимизации кода (в терминах C# - использование
небезопасного кода) только при необходимости и только тогда, когда
выявлены те самые 20% кода, которые используют 80% ресурсов.
Чего нет в Delphi
Теперь можно рассмотреть те преимущества,
которые имеет C# по сравнению с Delphi (порядок перечисления произволен
и ни в коей мере не отражает объективные приоритеты или субъективные
предпочтения):
Описание переменных в коде программы
Возможность описания переменных в коде
программы пришла в C# из C++. Пример кода:
class VarTest {
static void Main() {
int a;
int b, c = 1, d;
for (int i = 0; i < 10; i++) {
int x = i * 2;
}
}
}
В этом примере продемонстрированы сразу
несколько возможностей, отсутствующих в Delphi:
- описание переменных <по месту>
- одновременное описание и инициализация переменных
- локальная область видимости и время жизни переменных
Возможность передачи в метод переменного количества параметров
Пример кода, иллюстрирующий возможность
передачи в метод переменного количества параметров:
class Test {
static void F(params int[] args) {
// обработка параметров
}
static void Main() {
F();
F(1);
F(1, 2);
F(1, 2, 3);
F(new int[] {1, 2, 3, 4});
}
}
В Delphi отсутствие переменного количества
параметров можно частично скомпенсировать либо использованием значений
параметров по умолчанию, либо передачей в качестве параметра открытого
массива.
Однако в первом случае все равно количество
параметров ограничено и должно быть определено при написании
соответствующей подпрограммы.
Во втором же случае, хотя и наблюдается
сходство с C#, есть существенные ограничения:
- нельзя передать пустой массив (т.е. массив с нулевым количеством
элементов)
- передаваемый массив должен быть создан и заполнен или до вызова
подпрограммы, или в момент вызова с помощью конструктора открытого
массива
- неоднородные типы данных поддерживаются только при передаче
вариантного открытого массива (впрочем, код разбора такого массива
внутри подпрограммы обычно бывает довольно
- громоздким).
Автоматическое удаление объектов
При программировании в терминах объектов
Delphi приходится постоянно учитывать, когда создаются объекты
определенного класса и, что еще важнее, кто и когда их удаляет.
Ситуация несколько упрощается при построении
визуальных приложений и использовании компонентов - установив компонент
на форму (или модуль данных), тем самым мы определяем механизм создания
и удаления объектов соответствующего класса, когда форма-владелец
создает объект в процессе своей инициализации и удаляет его в момент
своего удаления.
При построении невизуальных приложений
(например, реализация бизнес-правил на промежуточном слое
многоуровневого приложения) можно использовать механизм автоматического
удаления COM-объектов или <интерфейсных> объектов (объектов, являющихся
наследниками от TInterfacedObject).
Впрочем, необходимо признать, что
автоматическое удаление объектов в Delphi реализовано не лучшим образом,
т.к.:
- приходится использовать две или даже три разные модели
программирования для визуальных и невизуальных приложений
(управление компонентами посредством формы-владельца, интерфейсные
объекты, COM-объекты);
- при использовании COM-объектов отладка не является прозрачной с
точки зрения объектной модели Delphi, т.к. в целях совместимости со
стандартами OLE приходится соблюдать ряд ограничений, в т.ч. в
вопросах генерации ошибок, совместимых с COM.
Более того, <стандартные> объекты Delphi,
которые часто используются при построении прикладных объектов, например,
списки, стеки, битовые массивы и пр., не поддерживают механизм
автоматического удаления, поэтому на практике приходится или
использовать смешанную модель, в которой лишь некоторые объекты
удаляются автоматически, или в целях стандартизации кода вообще
отказываться от автоматического удаления.
Рассмотрим простую семантическую конструкцию
- загрузку списка объектов.
В Delphi типичная реализация выглядят
примерно так:
procedure LoadList(aList: TObjectList);
begin
aList.Clear;
// заполнение списка
end;
. . .
try
MyObjectList = TObjectList.Create;
LoadList(MyObjectList);
// далее - использование объектов из списка
finally
MyObjectList.Free;
end;
На самом деле из-за ограничений Delphi
(TObjectList не может удаляться автоматически) семантика приведенного
кода разбивается на две отдельные фазы:
- создать пустой список объектов
- заполнить список
В C# аналогичное действие (загрузка списка
объектов) реализуется проще и семантически точнее:
class ListLoadTest {
Collection LoadList() {
Collection c = new Collection();
// непосредственная загрузка объектов и добавление их в коллекцию
return c;
}
static void Main() {
Collection c = LoadList();
foreach (MyObject in c) {
// что-то сделать с очередным объектом из списка
}
}
}
Строго говоря, в приведенном коде C# даже два
преимущества:
- во-первых, не нужно думать об удалении списка объектов и его
элементов (если бы в Delphi для хранения объектов вместо TObjectList
использовался бы TList или просто массив, объекты пришлось бы
удалять вручную)
- во-вторых, список объектов создается и заполняется в одном
месте.
Таким образом, довольно простая вещь -
автоматическое удаление объектов, - позволяет писать более точный
(семантически) и понятный код.
Поля классов <только для чтения>
В Delphi для того, чтобы реализовать
концепцию <поле только для чтения>, можно использовать свойства
(properties), при этом приходится писать нечто подобное:
type
TMyObject = class
private
fData: integer
public
// эквивалент поля для чтения
property Data: integer read fData;
end;
Поля <только для чтения> в C# введены на
уровне языка:
class A {
public readonly int Data;
}
Разница между (статическими) константами и
полями <только для чтения> заключается в том, что если константы могут
быть вычислены на стадии компиляции, что справедливо, например, для
простых типов, то значения полей <только для чтения> определяются только
на стадии выполнения программы.
Это приводит к интересным последствиям. В
стандарте C# рассматривается ситуация, когда имеется библиотека и
использующая ее программа, компилируемые раздельно. Если в библиотеке
использовать константу, то при изменении ее значения (и перекомпиляции
библиотеки) нужно перекомпилировать и программу. Если же использовать
поле <только для чтения>, то программу перекомпилировать не обязательно,
т.к. значение поля определяется на стадии исполнения.
Индексаторы
В Delphi можно реализовать свойство класса
типа массив и, установив для него атрибут default, получить некоторое
подобие индексатора:
type
TMyObject = class
public
property Items[Index: integer]: string read GetItem; default;
end;
Тогда в коде можно использовать две
эквивалентные конструкции:
S := MyObject.Items[I];
S := MyObject[I];
Вторая строка как раз и демонстрирует
основную идею индексаторов C# - возможность обращаться к объекту как к
массиву. Однако в Delphi есть существенное ограничение - можно
использовать только одно свойство (типа массива) по умолчанию.
В C# можно реализовать произвольное
количество индексаторов для класса:
class A {
int this[int Index] { . . . }
string this[char Col, int Row] { . . . }
static void Main() {
A a = new A();
for (int i = 0; i < a.Count; i++)
Console.Writeln(a[i].ToString());
for (char c = 'a'; c < 'z'; c++)
for (int r = 1; r < 100; r++)
Console.Writeln(a[c, r]);
}
}
Делегаты
В Delphi обработчики событий играют роль
делегатов - <делегируют> реальную работу внешнему объекту. Однако из-за
того, что Delphi является гибридным языком программирования, встречаются
ситуации, когда семантически эквивалентные задачи реализуются разными
способами. Так, например, в класса TList при сортировке используется
указатель на функцию сравнения элементов:
type TListSortCompare = function (Item1, Item2: Pointer): Integer;
procedure Sort(Compare: TListSortCompare);
Т.е. на самом деле задача сравнения элементов
списка также <делегируется> функции, внешней по отношению к объекту
TList.
Такая семантическая неоднозначность отнюдь не
упрощает программирование.
В C# реализован более общий и однозначный
подход - делегаты:
delegate void SimpleDelegate();
class Test {
static void F() {
System.Console.WriteLine("Test.F");
}
static void Main() {
SimpleDelegate d = new SimpleDelegate(F);
d();
}
}
Причем в качестве реализации делегата может
выступать как статический, так и обычный метод.
Возможность реализации модели обработки событий <один-много>
Хотя по умолчанию в C# компоненты реализуют
схему подключения обработчиков событий <один-к-одному>, при
необходимости может быть реализована модель <один-много>. Основа такой
возможности заключается в том, что в качестве обработчиков событий
используются делегаты, а их подключение к компоненту реализуется с
помощью перегружаемого метода:
class Control: Component {
// Unique keys for events
static readonly object mouseDownEventKey = new object();
static readonly object mouseUpEventKey = new object();
// Return event handler associated with key
protected delegate GetEventHandler(object key) {...}
// Add event handler associated with key
protected void AddEventHandler(object key, Delegate handler) {...}
// Remove event handler associated with key
protected void RemoveEventHandler(object key, Delegate handler) {...}
// MouseDown event
public event MouseEventHandler MouseDown {
add { AddEventHandler(mouseDownEventKey, value); }
remove { RemoveEventHandler(mouseDownEventKey, value); }
}
// MouseUp event
public event MouseEventHandler MouseUp {
add { AddEventHandler(mouseUpEventKey, value); }
remove { RemoveEventHandler(mouseUpEventKey, value); }
}
}
Приведенный код взят из спецификации C# и,
хотя выглядит относительно объемным, прозрачно иллюстрирует возможность
использования произвольного внутреннего механизма для хранения ссылок на
обработчики и подключения произвольного количества внешних обработчиков.
Цикл foreach
Цикл foreach перенят в C# из Visual Basic.
Получилась довольно удобная вещь:
class Test {
static void Main() {
double[] values = {1.2, 2.3, 3.4, 4.5};
foreach (double elementValue in values)
Console.Write("{0} ", elementValue);
}
}
Семантически аналогичный код в Delphi
выглядит более громоздким из-за необходимости использовать итератор
(переменная I), а также (в общем случае) вычислять границы массива:
procedure A;
const Values: array [1..4] of double = (1.2, 2.3, 3.4, 4.5);
var I: integer;
begin
for I := Low(Values) to High(Values) do
ShowMessage(FloatToStr(Values[I]));
end;
Статические конструкторы
Некоторый семантический аналог статическим
конструкторам в Delphi - секция initialize.
К сожалению, в Delphi порядок вызова секций
initialize соответствует порядку подключения модулей. Такая практика
может приводить к неожиданным ошибкам - первоначально рассчитывая на
конкретный порядок подключения модулей, можно случайно в процессе
разработки изменить этот порядок и в результате, например, получить
обращение к несуществующему или некорректно инициализированному
глобальному объекту программы.
C# предоставляет более строгое объектное
решение, которое, в частности, позволяет управлять правами доступа:
class A {
static protected A GlobalA;
static A() { GlobalA = new A; }
}
В C# порядок работы статического конструктора
определен только на уровне класса, при наличии же нескольких классов со
статическими конструкторами порядок их активизации не фиксирован. Такой
подход заставляет более тщательно проектировать программу с самого
начала и исключает появление в последующем ошибок, аналогичных описанной
выше.
Операторы классов
Операторы классов в C# почти эквивалентны
операторам классов в C++:
public class Digit {
byte value;
public Digit(byte value) {
if (value < 0 || value > 9) throw new ArgumentException();
this.value = value;
}
public static Digit operator+(Digit a, Digit b) {
return new Digit(a.value + b.value);
}
static public Main() {
Digit a = new Digit(5);
Digit b = new Digit(3);
Digit plus = a + b;
}
}
По сравнению с C++ в C# строго и однозначно
определен порядок реализации пользовательских правил преобразования
объектов (преобразования рассматриваются как частный случай операторов).
Примечание: Delphi не имеет
механизмов, эквивалентных операторам классов.
Структуры
Структуры в C# аналогичны записям в Delphi в
том смысле, что являются данными, передаваемыми по значению, а не по
ссылке.
На самом деле семантика структур в C# ближе к
классам, за исключением двух основных ограничений:
- структуры не могут быть абстрактными или содержать абстрактные
методы;
- наследование от структур не поддерживается, в связи с чем методы
структур не могут быть виртуальными.
Пример структуры:
struct Point {
public int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
Использование структур может как повысить
производительность программы (например, при размещении большого
количества мелких объектов лучше использовать структуры), так и ухудшить
ее (если используется структура, содержащая большие объемы данных, то
при передаче ее в качестве параметра будет выполняться лишнее
копирование).
Существует эмпирическое правило: если объем
данных меньше 16 байт, то для их хранения лучше использовать структуру,
если больше - класс.
Атрибуты
Интереснейшая возможность C#, отсутствующая
как в Delphi, так и в других наиболее популярных языках программирования
(VB, C++, Java), - атрибуты:
[Help("http://www.microsoft.com/.../Class1.htm")]
public class Class1 {
[Help("http://www.microsoft.com/.../Class1.htm", Topic = "F")]
public void F() {}
}
Атрибуты похожи на свойства классов Delphi,
за исключением того, что их значения устанавливаются на стадии
компиляции и в процессе выполнения программы могут быть только считаны.
Однако сфера применения атрибутов в поставляемой библиотеке классов CLR
весьма широка - от хранения вспомогательной информации декларативного
характера до обеспечения совместимости объектов .Net с COM (атрибуты
совместимости с COM описаны в приложении B спецификации C#).
Привычки, сформированные под влиянием Delphi,
не позволяют даже сразу придумать, для чего можно использовать атрибуты,
однако поле деятельности здесь просматривается широкое - от отслеживания
версий алгоритмов до контроля за совместимостью программ.
Возможность использовать русский язык для имен объектов программы
В соответствии со спецификацией C# для
именования объектов программы (классы, методы, переменные и пр.)
используются символы Unicode. Отсюда следует (и реально проверено), что
в качестве имен можно использовать русские названия, например:
class ЭлементУчета {
public int readonly ИнвентарныйНомер;
public string readonly Наименование;
public double РассчитатьАмортизацию(int ЛетВЭксплуатации) { ... }
}
Конечно, использование русского языка в коде
программы - вопрос спорный. Тем не менее, такая возможность есть.
Использовать же ее или нет - вопрос стандартов написания кода в пределах
рабочей группы (отдела, предприятия, корпорации).
Заключение
Безусловно, изложенные выше моменты не могут
претендовать на абсолютную полноту и глубокую детализацию. Тем не менее,
даже на их основе напрашивается естественный вывод - C# является весьма
интересным языком программирования, по сравнению с Delphi во многих
аспектах даже более мощным и в то же время строгим.
Можно ли на нем писать реальные промышленные задачи?
Если ориентироваться на монолитные приложения
в стиле АРМ, то скорее всего нужно более детально проанализировать
возможности исполнения программ .Net на конкретной аппаратной платформе
(навряд ли Framework.Net сможет работать на 486 с 8М ОЗУ). Если же
сделать акцент на многоуровневых приложениях, то C# для реализации
промежуточного слоя по сравнению с Delphi является более перспективным
кандидатом.
Общий ответ на поставленный вопрос - писать
реальные задачи на C# вполне можно, и, скорее всего, даже с меньшими
затратами, чем на Delphi.
Трудно ли дельфийцу освоить C#?
Можно утверждать, что нет, т.к. в основе и
Delphi, и C# лежит парадигма объектно-ориентированного программирования.
Программист, который может в Delphi писать код в терминах объектов, а не
только на уровне обработчиков стандартных компонентов, сможет писать и
на C#.
Впрочем, C# - не единственный язык, который
можно использовать в .Net. Даже в штатной поставке Visual Studio .Net
вместе с C# идет C++ и VB.Net, а вообще список языков программирования,
реализованных для платформы .Net, уже на сегодня перевалил за десяток.
Может быть, и Borland когда-нибудь выпустит Delphi.Net - не зря ведь
<отцом> C# является как раз автор Delphi.
Ждать ли дельфийцам выхода Delphi.Net?
На этот вопрос может ответить только фирма
Borland. На самом деле, если глубже поразбираться в вопросе
декларируемой совместимости программ, написанных на Delphi 6 и Kylix, то
оказывается, что о полной совместимости речь не идет - даже у
одноименных классов VCL и CLX встречаются разные интерфейсы. Поэтому
можно предположить, что Delphi.Net будет совместима с <обычной> Delphi
тоже не на все 100%. В чем же тогда будет преимущество Delphi.Net перед
тем же C#? Для <закостенелого> дельфийца - только в знакомом синтаксисе.
Ну а пока Delphi.Net выйдет в свет, можно и C# освоить (тем более, что
его корни лежат в C++, а C++ всегда считался более мощным языком, чем
Pascal), или можно на VB.Net писать. Но делать это можно уже СЕГОДНЯ.