C# — перекресток больших дорог, или Третьим будешь?
C# — перекресток больших дорог,
или Третьим будешь?
Когда Пифагор открыл известную свою теорему, он принес в жертву гекатомбу. С тех пор скоты дрожат, когда на свет божий появляется новая истина.
Л. Берне
Наверно, все уже слышали про появление нового языка программирования от Microsoft — C# (C-sharp, для некоторых "до-диез"). Практически во всех анонсах этот язык склонны сравнивать с Java. Многие, в том числе и я, подивились новым чудачествам Билли, да и забыли.
И вдруг, в очередной рассылке от HackZone (http://www.hackzone.ru/hack/) ее ведущий Дмитрий Леонов (человек, определенно, не такой ленивый, как я) вкратце описал особенности нового языка. Это было для меня откровением. В тексте так и просматривается какой-то старый и добрый знакомый. Не зря Microsoft отнекивается от сходства C# с Java, говоря, что он предназначен для решения совсем других задач. Любопытство пересилило, и я закопался в спецификации C#. Интересно, кто же оказался третьим в тесной компании C и C++? Анонс Microsoft говорит (в моем вольном переводе) следующее:
"По сравнению с такими языками, как Microsoft Visual Basic, разработка эквивалентных приложений на C и C++ зачастую требует намного больше времени. Именно поэтому разработчики искали язык, который обеспечивал бы лучший баланс между мощью и продуктивностью. Идеальным решением были бы быстрая разработка и возможность доступа ко всей функциональности низлежащей платформы.
Решение этой проблемы от Microsoft — язык C#. Он сочетает в себе продуктивность Visual Basic и мощь C++. C# будет являться частью Visual Studio 7.0. C# не имеет собственной библиотеки классов. Он будет, наравне с другими языками, обеспечивать доступ к Next Generation Windows Services (платформа.NET), включая общую библиотеку классов.
Современный дизайн C# устраняет многие ошибки программирования на C++. Каждый объект в C# является COM-объектом. Аналогично, программы на C# могут естественным образом использовать существующие COM-объекты. При необходимости C# позволяет делать вызовы различных API. Структура языка позволяет компонентам C# становиться Web-сервисами, доступными через Internet."
Здорово, правда? Слегка смахивает на смертный приговор для VC++ и VB. Однако самое интересное начинается при детальном изучении языка. Да, он в целом сохраняет синтаксис C++. Да, в нем есть ограничения присущие Java. Да, из него в принципе можно сделать среду быстрой разработки программ (RAD), и наверняка более серьезную, чем VB. Но откуда взяты основные нововведения (Microsoft не часто придумывает что-то действительно свое), какая идеология заложена в этот язык?
Вы не поверите. За всем этим стоит Object Pascal в последней редакции от Borland! Тише, тише... Не надо криков (и злобных, и восторженных). Перестанем брызгать слюной на газету (или на монитор) и будем разбираться по порядку. А по ходу вспоминать про Object Pascal.
Справочник рассказал... а мы вспомнили
Аналогия и опыт — две клюки, на которых мы тащимся по дороге умствований.
Фридрих Великий
1. Простейшая программа "Hello, world" на C# выглядит так:
using System;
class Hello {
static void Main() {
Console.WriteLine("Hello, world");
}
}
Из примера видно, что все функции реализуются только как члены класса. Точка входа — статическая функция main.
Теперь начинаются приветы от Object Pascal'а (далее OP). Вместо директивы #include используется using (uses в OP). Файлы-заголовки *.h упразднены, а директивой using фактически подключается пространство имен (namespace). Строго говоря, using есть и в последней редакции C++. Вместо "::","-> " и "." всегда используется ".".
2. Типы. C# поддерживает две основных разновидности типов: value types — обычные и reference types — ссылочные типы. Обычные включают простые типы: char, int, float и т.д., а также перечисления enum и структуры struct. Ссылочные типы включают классы (class), интерфейсы (interface), делегаты (delegate) и массивы (array). Немножко терпения, подробный рассказ об этих типах будет ниже.
Тип bool говорит сам за себя. Тип char представляет собой символ в формате Unicode (2 байта). Соответственно, string — это последовательность Unicode-символов.
Тип object является общим предком для всех остальных типов. C# обеспечивает "унифицированную систему типов". Таким образом, есть возможность вызывать методы объектов для любой переменной, даже примитивных типов, таких как int (от себя заметим, что и методы эти будут примитивны).
public class Hashtable {
public void Add(object Key, object Value) {...}
}
Пользователи класса Hashtable могут использовать Key и Value любых типов. Правда, остается открытым вопрос о том, как метод Add сможет определить, что за счастье к нему свалилось, если подразумевается работа на не слишком примитивном уровне. Приличного механизма RTTI (Run-Time Type Information), похоже, так и не появилось. Но это мы обсудим чуть позже. А пока угадайте, что следует из наличия общего предка?
Правильно, из этого следует отсутствие множественного наследования. Не спешите ругать дядю Билли, ему сейчас и так нелегко. Решение убрать множественное наследование не случайно. Это результат победы другой, гораздо более полезной концепции, а именно гомоморфной иерархии классов, представляющей модель дерева, где все классы имеют одного общего предка. Да и в трудах многих экспертов C++ я встречал очень осторожное отношение к множественному наследованию из-за порождаемых им проблем. Кроме того, существуют способы обойтись без него вообще.
Теперь вспомним про ОР. Все классы представляют собой гомоморфную иерархию, имеют мощнейшую систему RTTI и представляют собой ссылочный тип. Т.е. являются автоматически разименовываемыми указателями на экземпляры классов. C#, так же как и OP, допускает размещение экземпляров классов только в куче. А это, кстати, очень способствует автоматизации сборки мусора, т.к. стек живет своей жизнью. Справедливости ради надо заметить, что в иерархию классов ОР входят только собственно классы.
3. Классы. Как и ожидалось, C# сразу заявляет, что поддерживает наследование только от одного предка, но зато и от множества интерфейсов. Классы могут включать константы, поля, методы, свойства (!), индексаторы (разновидность свойств), события (!), операторы, конструкторы, деструкторы, а также вложенные объявления типов. Каждый член класса объявляется с указанием формы доступа:
• public — доступны для всего кода;
• protected — только из классов-потомков;
• internal — только из того же модуля;
• protected internal — из того же модуля только классам — потомкам;
• private — доступны только самому классу.
Веселое разнообразие, которое необходимо применить к каждому (!) члену класса. А теперь представьте, как будет выглядеть объявление класса с учетом того, что методы класса реализуются только непосредственно в этом самом объявлении. Ну ничего, злые языки и раньше утверждали, что C/C++ являются "write-only" языками. Но писали же люди, и нормально...
В ОР все это выглядит поприличнее. Классы также могут наследоваться от одного класса и множества интерфейсов. Про свойства и события мы поговорим чуть позже. А вот касательно реализации методов ОР придерживается прямо противоположного мнения — только вне объявления класса. И, конечно, модификатор доступа к каждому члену специально указывать не нужно. Примечательно, что C# использует вместо friend (C++) такой же принцип доступа к классу извне, как и ОР.
4. Интерфейсы. А это привет от COM-технологий. Сами интерфейсы, конечно, проживут и без COM, а вот COM без них, как без рук. Интерфейсы очень похожи на классы, но отличаются тем, что являются "чистыми" объявлениями, т.е. не содержат никакой реализации. Эта почетная задача возлагается на каждый класс, который в списке предков указал данный интерфейс. Пример интерфейса:
interface Iexample {
string this[int index] { get; set; }
event EventHandler E;
void F(int value);
string P { get; set; }
}
public delegate void EventHandler
(object sender, Event e);
Интерфейсы могут содержать методы, свойства, индексаторы и события. Таинственная строка со словом delegate необходима для объявления события E. Заметим также, что P — это свойство, this — тоже свойство, только, выражаясь по-военному, "более другое" — индексируемое. Естественно, что интерфейс может наследоваться от нескольких других интерфейсов:
interface IComboBox: ITextBox, IListBox {}
В целом, интерфейсы C# абсолютно аналогичны интерфейсам в ОР за исключением одной важной "фичи". Называется она "реализация через делегирование" и позволяет классу передать (полностью или частично) вышеозначенную почетную обязанность на реализацию своему свойству, имеющему тип другого класса или интерфейса... Когда же я научусь писать просто?:-) К счастью, в жизни (и в ОР) все выглядит гораздо проще, чем звучит:
property MyClass: TMyClass
read FMyClass
implements IMyInterface;
И все. Теперь класс может забыть о занудном перечислении и реализации всех членов интерфейса. Но главная прелесть в другом: можно динамически менять экземпляр класса (FMyClass), отвечающий за реализацию. "Подумаешь..." — скажет читатель. Ну и правильно, зато в C# есть
5. Контроль версий. В этом вопросе Microsoft не ограничилась изобретением велосипеда, а возвела целую теорию. Я даже не сразу понял, о чем речь.
Вот какой пример тяжелой программистской доли приводит дядя Билли. Существует погрязший в долгах и неизвестности автор класса Base. А еще есть халявщик — автор класса Derived, являющегося наследником Base. Класс Derived добавляет виртуальный метод F, а после этого "уходит на золото" и поступает к заказчикам. После этого автор класса Base, в поисках лучшей жизни, добавляет к Base свой собственный метод F. И тут автора класса Derived настигает расплата (не совсем понятно за что, наверно, за любовь к C++). Становится непонятно, что же происходит в Derived: переопределение (overriding) или перекрытие (hiding) метода F. Тут Билли напоминает нам, что взаимодействие между авторами невозможно в силу идеологических разногласий, а вероятность возникновения такой ситуации увеличивается пропорционально количеству нахлебников класса Base.
По сценарию появляется добрая фея в лице C# и взмахами волшебной дубины заставляет всех ясно определиться в своих помыслах. Автор класса Derived вынужден применить новое ключевое слово new при объявлении своего метода F, чем объявляет автору класса Base, что чихать он хотел на его метод F. Компилятор при этом заговорщицки перестает ворчать.
new public virtual void F() { ... }
Но однажды, с похмелья, автору класса Derived приходит в голову шальная мысль: "А почему бы не использовать функциональность метода F предка?" Фея ему подсказывает, что нужно использовать новое ключевое слово override. Мир, дружба, жвачка!
public override void F() {base.F(); ... }
Мораль сей басни такова: разработчики должны явно указывать переопределение виртуальных методов предков директивой override. Если они этого не делают, то компилятор выдает предупреждения. Чтобы его утихомирить, нужно указать директиву new. При этом виртуальная цепочка методов обрывается и появляется совершенно новый виртуальный метод.
Конечно, Билли кричит, что спас мир от неминуемой гибели, совершенно забывая об авторских правах. Директива override (и вся идеология контроля версий) существует в ОР уже лет пять, начиная с Delphi 1.0, а reintroduce (аналог new — успокоитель компилятора) года два-три.
6. Свойства. Свойства — это естественное расширение полей. Говоря простым языком, свойство похоже на переменную. Когда вы присваиваете свойству новое значение, вызывается один метод, а когда вы читаете свойство — другой метод. Автоматически! И вот тут Microsoft произносит судьбоносную (серьезно, я не шучу) речь:
"Успех таких инструментов быстрой разработки приложений (RAD), как VB, можно, с небольшим допущением, отнести в первую очередь к наличию свойств. Разработчики в VB могут думать о свойстве просто как о поле. Это позволяет им сфокусироваться на логике собственного приложения (!), а не на особенностях компонентов, которые им приходится использовать. Современные компонетно-ориентированные программы просто битком набиты свойствами. Ориентированные на использование методов языки (например, o.SetValue(o.GetValue() + 1)) имеют явные недостатки по сравнению с языками на основе свойств (тот же пример, o.Value++)."
Золотые слова! Именно свойства (в сочетании с мощной компонентой моделью) позволяют воплотить в жизнь хотя бы элементарное разделение труда программистов. Простой пример из Delphi VCL. Мне нужно изменить ширину окна на 20 пикселов:
Width:= Width + 20;
И все! Окно уже перерисовано. Я могу вообще не знать, какая работа производится внутри (а могу и знать, и активно вмешиваться). А вот и пример объявления свойства в C#. Здесь в виде свойства Caption реализуется заголовок кнопки:
public class Button: Control {
private string caption;
public string Caption {
get {
return caption;
}
set {
caption = value;
Repaint();
}
}
}
Ключевыми словами get и set обозначаются методы доступа к свойству, а value представляет собой новое значение, присваиваемое свойству. Обратите внимание на Repaint(), оно подразумевает автоматическую перерисовку окна после изменения заголовка. Но в C# есть еще одна разновидность свойств, называемая
7. Индексаторы. Особенности свойств, в принципе, позволяют им "закосить" под переменную любого типа. Но есть и исключение — массивы. Чтобы обратиться к элементу массива, необходимо указать его индекс(ы). А для этого приходится немножко изменить синтаксис объявления свойства. Так и получается индексатор:
public class ListBox: Control {
private string[] items;
public string this[int index] {
get {return items[index];}
set {
items[index] = value;
Repaint();
}
}
}
ListBox listBox =...;
listBox[0] = "hello"; //Перерисовка списка
В этом примере index — это целочисленный индекс. Но индексы могут быть любого (!) типа. Почему-то Microsoft забыла привести самые захватывающие случаи, когда в качестве индексов используются, например, переменные типа класса или строки.
Теперь подведем итог по свойствам. Несмотря на все старания, свойства в C# не дотягивают до свойств в ОР. То, что есть, реализуется менее гибко. К тому же ОР предлагает еще одну разновидность свойств — индексируемые свойства (не путать с индексаторами C#, в ОР они называются array properties). Суть заключается в том, что несколько свойств могут использовать общие методы доступа по чтению и записи, в которые передается дополнительный параметр, идентифицирующий обращающееся свойство. Из-за синтаксиса объявления классов в C# реализовать такие свойства можно только через... Ну, в общем, косвенно.
8. Делегаты. Название солидное, вот как Microsoft его обосновывает:
"Делегаты позволяют делать то же, что и указатели на функции в C++ и некоторых других языках. Однако, в отличие от них, делегаты являются объектно-ориентированными и безопасными."
За пышным фасадом скрывается конструкция, аналогичная procedure of object из ОР. И служит она, соответственно, в основном для работы с событиями (events). Делегат — это ссылочный тип, происходящий от общего предка System.Delegate. Делегат, не имеющий аргументов можно объявить так:
delegate void SimpleDelegate();
Кроме того, C# требует еще и создания экземпляра делегата (тип ведь ссылочный), так что без new не обойтись. Применить делегата можно вот так:
void MultiCall(SimpleDelegate d, int count) {
for (int i = 0; i < count; i++) d();
}
9. События. События позволяют классу предоставить "зацепки" (или "заглушки"), к которым клиенты класса могут присоединить исполнимый код в виде обработчиков событий. Таким образом, класс генерирует какое-то событие, а выполняется код клиента. Данный механизм использует ОО-концепцию — делегирование. Поэтому не удивительно, что события являются переменными или свойствами типа делегат, о которых мы только что поговорили.
public delegate void EventHandler(object sender, Event e);
public class Button: Control {
public event EventHandler Click;
public void Reset() { Click = null; }
}
В этом примере Click является событием и имеет тип делегата EventHandler. Из этого следует, что событию Click может быть назначен любой метод, принимающий в качестве параметров object и Event и возвращающий void. Делается это операторами += и -=.
void Button1_Click(object sender, Event e) {Console.WriteLine("Button1 was clicked!");}
Button1.Click += new EventHandler(Button1_Click);
Button1.Click -= new EventHandler(Button1_Click);
Класс Button можно переписать, объявив событие Click в стиле объявления свойств, что не имеет существенного значения:
public event EventHandler Click {get{...};set {...}}
Microsoft совершенно справедливо замечает, что события являются важнейшим аспектом дизайна библиотек классов вообще и системной библиотеки классов, в частности. Действительно, грамотно спроектированная библиотека в большинстве случаев позволяет расширять свою функциональность без создания новых классов, просто назначая необходимые обработчики событий. Таким образом библиотека делегирует клиенту право (именно право, а не обязанность) реализации нужной ему функциональности.
Словом, события являются большим шагом вперед. Но в сравнении с ОР события из C# явно не смотрятся. Нет, сами события абсолютно аналогичны. Все портит совершенно убойный синтаксис (операторы += и -=) и необходимость динамически создавать делегаты. К тому же, без соответствующей поддержки RTTI вся работа с событиями превращается в обычную рутину, и все нужно делать вручную.
10. Массивы. Размеры и измерения массива определяются во время его создания и остаются неизменными в течение всего времени существования экземпляра массива. Спецификация прямо говорит, что изменить их невозможно.
Обидно, однако... Вообще, динамический массив, быстрый, переменного размера, с автоматическим подсчетом ссылок и управлением временем жизни, полностью совместимый с традиционными массивами — мечта любого разработчика. И в ОР именно такие динамические массивы существуют на уровне языка!
11. Параметры params. Эта возможность введена для передачи в функции в качестве параметров массивов переменного размера. Для этого формальный параметр объявляется с использованием модификатора params. Этот параметр должен быть последним в списке формальных параметров. Пример:
void F(params int[] values) {
Console.WriteLine("values contains %0 items", values.Length);
}
Конечно, это тоже не выдумка парней из Редмонда. Параметры params — прямая аналогия параметров типа открытый массив (open arrays) из ОР. При этом ОР не накладывает никаких ограничений на положение таких параметров в списке.
Success = new Features(cool);
Хорошая мысль, откуда бы она ни была взята, гораздо лучше, чем собственная глупая, — не в обиду будет сказано тем, кто находит все в себе самих, не прибегая ни к кому.
Фр. Ламот-Левайе
12. Атрибуты. Вот это действительно что-то новенькое. Microsoft говорит, что C# позволяет разработчикам ввести новые типы описательной информации для различных программных элементов и получить эту информацию во время выполнения программы. Эти типы определяются с использованием атрибутов. Атрибуты — это классы, происходящие от абстрактного класса System.Attribute.
К примеру, можно определить атрибут HelpAttribute (или Help, для краткости), который затем можно поместить на классы или методы и обеспечить тем самым прямое отображение между программными элементами и документацией.
[AttributeUsage(AttributeTargets.All)]
public class HelpAttribute: System.Attribute {
public HelpAttribute(string url) {
this.url = url;
}
public string Topic = null;
private string url;
public string Url {
get { return url; }
}
}
Квадратные скобки здесь демонстрируют применение другого атрибута при объявлении атрибута Help. В этом случае атрибут AttributeUsage показывает, что наш атрибут Help может быть использован для любого программного элемента.
Уф. Вы что-нибудь поняли?.. Я тоже. Давайте смотреть дальше. Здесь мы применим вышеобъявленный атрибут, добавив конкретную информацию:
[Help("http://www.mycompany.com/.../Class1.htm")]
public class Class1 {
[Help("http://www.mycompany.com/.../Class1.htm", Topic ="F")]
public void F() {}
}
Атрибутивная информация может быть получена в run-time режиме:
using System;
class Test {
static void Main() {
Type type = typeof(Class1);
object[] arr = type.GetCustomAttributes(typeof(HelpAttribute));
if (arr.Length == 0)
Console.WriteLine("Class1 has no Help attribute.");
else {
HelpAttribute ha = (HelpAttribute) arr[0];
Console.WriteLine("Url = {0}, Topic = {1}", ha.Url, ha.Topic);
}
}
}
Класс Test проверяет, имеет ли класс Class1 атрибут Help, и выводит ассоциированные с ним значения Topic и URL.
Естественно, что возможность получить атрибут в run-time'е требует какой-то поддержки со стороны RTTI. Похоже, что именно в атрибутах, а не в классах, будет заложен этот мощный механизм, а поддерживаться все это будет со стороны NET-платформы. Пока все это выглядит несколько загадочным. Впрочем, есть несколько хороших примеров:
12.1. Поддержка COM. Атрибуты из этого раздела Microsoft предлагает использовать для создания.NET-приложений, которые взаимодействуют с COM-программами.
Атрибут COMImport. Применив его к классу, мы помечаем класс как реализованный внешне COM-класс. Такое объявление класса позволяет использовать имя C# для обращения к COM-классу.
Атрибут Guid. Используется для присвоения классу или интерфейсу уникального идентификатора GUID.
12.2. Внешние, extern методы. Объявление метода может включать модификатор extern, что говорит о внешней реализации метода. Как правило, этот модификатор используется совместно с атрибутом DllImport, позволяя реализовать методы в DLL. Такое решение дополнительно требует включения модификатора static. Пример:
class Path {
[DllImport("kernel32", setLastError=true)]
static extern bool CreateDirectory(string name, SecurityAttributes sa);
}
13. Завершенные классы. Завершенный (возможно, лучше перевести "тупиковый") класс объявляется с модификатором sealed. После выполнения такого ритуала наследование от завершенного класса возбраняется высшими силами. Это выражается в том, что компилятор будет бить вас по рукам каждый раз, когда он заметит завершенный класс в списке предков другого класса. Кроме того, завершенному классу строго запрещено исповедовать абстрактные идеи, т.е. об использовании модификатора abstract в нем можно забыть.
Остается только отметить, что классы в ОР подобными формами бесплодия не страдают. Мне представляется, что наибольшую пользу от sealed могут получить лишь классы, не вполне грамотно организованные внутри, когда потомок может нечаянно нарушить унаследованные механизмы работы. Кто знает другие выгоды, расскажите.
14. Поля readonly. Как вы догадались, речь идет о полях только для чтения. Объявляются такие поля с использованием модификатора readonly. Присвоение таким полям может осуществляться только в конструкторе класса либо как часть объявления.
Надо полагать, что readonly является развитием модификатора const из C++. В ОР такая возможность отсутствует, а разработчики обычно просто используют read-only свойства.
15. Автоматическое управление памятью. Под этим гордым названием подразумевается сборка мусора, т.е. уничтожение объекта, как только он становится больше не нужен. Выделяется память все так же, через оператор new. Операции с указателями запрещены, но не убраны. Ими можно пользоваться в специально помеченных участках кода. C# называет такие участки небезопасным (unsafe) кодом.
Интересно, что в ОР сборка мусора по определению отсутствует. Однако у новичков складывается устойчивое впечатление, что Delphi полностью управляет созданием и удалением всех объектов. Почему так происходит?
ОР автоматически управляет памятью и временем жизни таких объектов, как длинные строки, вариантные переменные, динамические массивы и интерфейсы. Кроме того, на уровне компонентов (важнейшая ветка библиотеки VCL) реализуется автоматическое управление дочерними компонентами, включая их удаление, а также уведомление о появлении или исчезновении других компонент в пределах того же родителя. А компоненты — кирпичи приложения.
При создании экземпляра компонента включается механизм RTTI, который восстанавливает из ресурсов (при наличии таковых) значения свойств и событий (т.е. назначенных им обработчиков событий) объявленных как published. На этом сюрпризы не заканчиваются. На уровне TObject (общий предок) реализованы виртуальные методы NewInstance и FreeInstance, которые заведуют выделением и освобождением памяти при создании нового экземпляра класса! Это значит, что любой класс может переопределить управление своей памятью до неузнаваемости.
Вы еще не забыли? В ОР сборка мусора отсутствует:-).
16. Поддержка исключений (exceptions). Этот момент является одним из важнейших для любого современного языка программирования. Исключения позволяют использовать реактивную модель написания кода. Нет, к Вашему любимому авиа-симулятору это не имеет никакого отношения.
Суть идеи в том, что код пишется для идеального случая функционирования программы, когда нет никаких ошибок. Ошибочная ситуация называется исключением и обрабатывается в специальном участке кода — обработчике исключения. Таким образом, разработчик избавляется от маразматической привычки повсюду расставлять операторы if.
Все исключения в C# происходят от общего предка — класса System.Exception. Для защиты некоторого участка кода структурной обработкой исключений используются блоки try...catch, try...finally или комбинированный вариант. Выражение catch позволяет указать тип обрабатываемых исключений. Блок, стоящий за finally, выполняется всегда, независимо от наличия или отсутствия исключений. Каждый блок try может содержать несколько catch и один finally.
Исключение генерируется в выражении с ключевым словом throw. Это является для разработчика способом создания исключительной ситуации. После создания исключения начинается поиск охватывающего блока try, содержащего соответствующий данному исключению catch. Если обработчик исключения не найден в данной функции, то поиск идет в вызвавшей функции. Если стек полностью раскручен и обработчик не найден, то процесс или поток завершается.
Отметим, что реализация исключений в C# значительно улучшилась по сравнению с C++ (ANSI) и доросла, наконец, до уровня исключений в OP. Самым главным моментом здесь, конечно, является общий предок для всех исключений. Большое значение имеет появление блока try...finally, а также возможность работы с экземпляром исключения в общем обработчике и повторно его генерировать. Кроме этого, ОР умеет автоматически устанавливать блок обработки исключений в каждом конструкторе и, при возникновении исключения, автоматически вызывается деструктор. Да много чего еще умеет...
17. Все остальное (или почти все). В C# сохранились перегрузка функций и операторов (перегрузки операторов в ОР нет). Остался также препроцессор (компилятор Delphi все делает за один проход, в течение нескольких секунд). Передача параметров в функцию по ссылке осуществляется с помощью модификатора ref. Кроме того, появился модификатор out для параметров, через которые возвращается значение. Каждый объект в C# является COM-объектом (в OP все классы также являются COM-объектами).
Следует отметить, что, конечно, многие нововведения в C# нельзя относить исключительно на счет ОР. Это давно известные языковые конструкции. Просто они впервые или наиболее удачным образом были реализованы именно в ОР. Но все же скопировано очень многое, в том числе и идеология языка.
В заключение официальной части хочу привести таблицу сравнения объектных моделей различных языков. В оригинальном виде она присутствует в книге Рэя Лишнера "Delphi in a Nutshell". Я же дополнил ее столбцом для C#.
1. В C++ можно эмулировать интерфейсы с помощью абстрактных классов. 2. См. "15. Автоматическое управление памятью". 3. Интерфейсы используют подсчет ссылок. 4. RTTI в C++ и C# ограничено сравнением и приведением типов. 5. Встроенный ассемблер не входит в стандарт C++, однако большинство компиляторов C++, включая Borland, поддерживают встроенный ассемблер как расширение языка.
// Комментарии.
Вернейший путь к истине — это познавать вещи, какими они есть на самом деле, а не заключать о них так, как нас учили.
Дж. Локк
Дабы не уподобляться поучающим, эти комментарии я предлагаю исключительно как мое личное мнение, не претендующее на абсолютную истинность. Возможно, кто-то сочтет его интересным и полезным для себя. Профессионалы, как правило, всегда имеют свое четкое мнение, поэтому убеждать их в чем-то — занятие неблагодарное. Им нужен только исходный материал, а мнение они составят. Так что эта категория читателей может с облегчением вздохнуть и выпить пива, для них я свое повествование уже закончил.
Ну вот, почти никого не осталось:-). Теперь можно и поболтать с Тобой, уважаемый читатель. Если у Тебя хватило терпения дойти до этого места, мои поздравления. Судя по всему, Тебе не безразличен выбор инструмента, которым Ты воплотишь все свои гениальные задумки в сухую последовательность машинных кодов. Разреши предложить Тебе мои скромные выводы в качестве отправной точки для размышлений.
Для чего же все-таки Microsoft придумала новый язык? Вот так неожиданно, вроде бы даже без особых причин. В этом случае Microsoft довольно честно объясняет все в своем анонсе. Действительно, C# — это не соперник Java. Это язык, предназначенный для того, чтобы привнести в среду разработчиков новые технологии программирования, которые отсутствуют у VC++ и VB. Конечно, Microsoft "забывает" сказать, что такие технологии уже давно существуют на рынке.
Но, как я уже однажды говорил, корни проблемы языков не в их особенностях, а в финансовых возможностях конкретных фирм. Кстати, интересный факт. Microsoft имеет неплохую долю в капитале Borland, а не так давно выплатила 250 M$ только за то, чтобы Delphi при работе с базами данных больше внимания уделяла ADO и MS Access.
Однако, кризис Visual Studio назрел, что и выразилось в появлении C#. Вы скажете: "Так уж и кризис?" Дело в том, что, по моему мнению, для написания на C/C++ качественных приложений нужно быть профессионалом, своего рода художником от программирования (это верно и для VB, т.к. его легкость не обладает мощью, и круг замыкается). В остальных случаях мы имеем то, что имеем, а именно кучу service pack'ов, багфиксов, эксплоитов к дырам в безопасности и постоянное ожидание сюрпризов от каждого приложения. Почему?
C и C++ действительно очень мощны. Но они же и низкоуровневы, более близки к ассемблеру, требуют высокой культуры программирования. Они идеально подходят для системного программирования, но попытки сделать из них что-то большее довольно часто выливаются в неоправданное растрачивание сил программистов, хотя дело, безусловно, делается. Первое время после перехода на C++ я постоянно ловил себя на том, что прокручивал в голове все мельчайшие детали функционирования программы. Конечно, этому очень способствовала многоплатформенность и системность задач. Однако я считаю, что у разработчика есть более важные проблемы, чем воплощать собой живой debugger.
C# в этом плане пока меняет не многое. Он также требует большого начального запаса знаний, которые зачастую не имеют никакого отношения к поставленной задаче. Ему еще необходима мощная, гибкая, по-настоящему визуальная среда, продуманная библиотека компонент (именно его, а не общая на всю Visual Studio). Хотя я пока не вижу способа создать на базе теперешнего C# серьезную систему RAD. По-моему, атрибуты не могут заменить полноценную поддержку RTTI. А без этого визуальное программирование будет ничуть не лучше, чем в VC++. Кстати, несмотря на то, что в описании C# то и дело встречается слово "компонент", продуманная концепция компонент отсутствует напрочь. Видимо, рассчитывают на ActiveX. Ну, посмотрим...
С другой стороны, никому не придет в голову писать драйверы на Delphi (это, в основном, вопрос startup-кода), хотя Delphi генерирует очень производительный код, а минимальное Delphi-приложение может быть в несколько раз меньше, чем аналогичное в том же MSVC. Просто эта среда ориентирована на построение сложных, комплексных приложений. Именно здесь начинают проявляться все ее архитектурные решения. Delphi обеспечивает совершенно уникальный уровень абстракции в сочетании с полным доступом к низкоуровневым средствам и одинаково хорошо встречает и матерого профессионала, и начинающего разработчика (давая ему возможность постепенно (!) стать профессионалом). Delphi насквозь пропитана духом ООП.
Сравнительный анализ (см. табл.) показывает, что на сегодняшний день Delphi имеет в своей основе язык, который уже в течение нескольких лет превосходит своих конкурентов в создании большинства приложений. Да, Borland вроде бы незаметно, но систематически доводила ОР до ума с появлением каждой новой версии Delphi, не ограничиваясь поддержкой новых технологий и улучшением визуальной среды. Единственное, что можно было бы добавить к ОР, как языку, это перегрузка операторов и возможность понижения видимости членов класса.
Важно еще и то, что C# пока существует только на бумаге. Ко времени появления соответствующего компилятора уже, скорее всего, увидят свет новые версии Delphi и C++ Builder в компании с Kylix (RAD для Linux) и кроссплатформенной библиотекой классов CLX. Это означает, что приложения, использующие стандартные компоненты, будут полностью переносимы на уровне исходников (RTL тоже кроссплатформенна). Кроме того, расширяется поддержка HTML и XML, усиливается RTTI для поддержки интерфейсов, что позволит любой класс опубликовать как Web-сервис, появляются Delphi Server Pages и Pascal-подобный скриптовый язык. Вместе с улучшениями самой среды это говорит о том, что новые инструменты от Borland, как минимум, перекрывают весь круг задач, для которых создавался C#.
Если вы думаете, что Borland — единственная контора, выпускающая компиляторы Pascal, то у меня есть несколько сюрпризов. Sybil (http://www.speedsoft-online.de/) — Delphi-подобная кроссплатформенная среда под Windows, Linux и OS/2. Free Pascal (http://www.freepascal.org/) — свободный кроссплатформенный компилятор под Windows, Dos, Linux, OS/2, Amiga. Кроме того, я слышал о разработке Pocket Pascal под Palm.
Конечно, эта статья не о Delphi и ОР. Поэтому я не буду слишком распинаться на эту тему, хотя говорить можно много и интересно. А что касается выводов... За время подготовки этого материала (около месяца, включая техническое рецензирование профессионалами в C++) я смог четко определиться, почему мне нравится работать именно в Delphi (это, наверное, тяжело не заметить:-). И окончательную точку в моих выводах поставил выход C#.
Надеюсь, что у Тебя, читатель, также появился повод для размышлений (особо интересными прошу поделиться). Желаю всем успехов в выборе достойного инструмента.
Владимир Волосенков, Brainbench Certified Master Delphi Programmer, Master Windows NT Workstation Administrator.
Тухлые овощи бросать на Uno@au.ru.
(c) Компьютерная газета
или Третьим будешь?
Когда Пифагор открыл известную свою теорему, он принес в жертву гекатомбу. С тех пор скоты дрожат, когда на свет божий появляется новая истина.
Л. Берне
Наверно, все уже слышали про появление нового языка программирования от Microsoft — C# (C-sharp, для некоторых "до-диез"). Практически во всех анонсах этот язык склонны сравнивать с Java. Многие, в том числе и я, подивились новым чудачествам Билли, да и забыли.
И вдруг, в очередной рассылке от HackZone (http://www.hackzone.ru/hack/) ее ведущий Дмитрий Леонов (человек, определенно, не такой ленивый, как я) вкратце описал особенности нового языка. Это было для меня откровением. В тексте так и просматривается какой-то старый и добрый знакомый. Не зря Microsoft отнекивается от сходства C# с Java, говоря, что он предназначен для решения совсем других задач. Любопытство пересилило, и я закопался в спецификации C#. Интересно, кто же оказался третьим в тесной компании C и C++? Анонс Microsoft говорит (в моем вольном переводе) следующее:
"По сравнению с такими языками, как Microsoft Visual Basic, разработка эквивалентных приложений на C и C++ зачастую требует намного больше времени. Именно поэтому разработчики искали язык, который обеспечивал бы лучший баланс между мощью и продуктивностью. Идеальным решением были бы быстрая разработка и возможность доступа ко всей функциональности низлежащей платформы.
Решение этой проблемы от Microsoft — язык C#. Он сочетает в себе продуктивность Visual Basic и мощь C++. C# будет являться частью Visual Studio 7.0. C# не имеет собственной библиотеки классов. Он будет, наравне с другими языками, обеспечивать доступ к Next Generation Windows Services (платформа.NET), включая общую библиотеку классов.
Современный дизайн C# устраняет многие ошибки программирования на C++. Каждый объект в C# является COM-объектом. Аналогично, программы на C# могут естественным образом использовать существующие COM-объекты. При необходимости C# позволяет делать вызовы различных API. Структура языка позволяет компонентам C# становиться Web-сервисами, доступными через Internet."
Здорово, правда? Слегка смахивает на смертный приговор для VC++ и VB. Однако самое интересное начинается при детальном изучении языка. Да, он в целом сохраняет синтаксис C++. Да, в нем есть ограничения присущие Java. Да, из него в принципе можно сделать среду быстрой разработки программ (RAD), и наверняка более серьезную, чем VB. Но откуда взяты основные нововведения (Microsoft не часто придумывает что-то действительно свое), какая идеология заложена в этот язык?
Вы не поверите. За всем этим стоит Object Pascal в последней редакции от Borland! Тише, тише... Не надо криков (и злобных, и восторженных). Перестанем брызгать слюной на газету (или на монитор) и будем разбираться по порядку. А по ходу вспоминать про Object Pascal.
Справочник рассказал... а мы вспомнили
Аналогия и опыт — две клюки, на которых мы тащимся по дороге умствований.
Фридрих Великий
1. Простейшая программа "Hello, world" на C# выглядит так:
using System;
class Hello {
static void Main() {
Console.WriteLine("Hello, world");
}
}
Из примера видно, что все функции реализуются только как члены класса. Точка входа — статическая функция main.
Теперь начинаются приветы от Object Pascal'а (далее OP). Вместо директивы #include используется using (uses в OP). Файлы-заголовки *.h упразднены, а директивой using фактически подключается пространство имен (namespace). Строго говоря, using есть и в последней редакции C++. Вместо "::","-> " и "." всегда используется ".".
2. Типы. C# поддерживает две основных разновидности типов: value types — обычные и reference types — ссылочные типы. Обычные включают простые типы: char, int, float и т.д., а также перечисления enum и структуры struct. Ссылочные типы включают классы (class), интерфейсы (interface), делегаты (delegate) и массивы (array). Немножко терпения, подробный рассказ об этих типах будет ниже.
Тип bool говорит сам за себя. Тип char представляет собой символ в формате Unicode (2 байта). Соответственно, string — это последовательность Unicode-символов.
Тип object является общим предком для всех остальных типов. C# обеспечивает "унифицированную систему типов". Таким образом, есть возможность вызывать методы объектов для любой переменной, даже примитивных типов, таких как int (от себя заметим, что и методы эти будут примитивны).
public class Hashtable {
public void Add(object Key, object Value) {...}
}
Пользователи класса Hashtable могут использовать Key и Value любых типов. Правда, остается открытым вопрос о том, как метод Add сможет определить, что за счастье к нему свалилось, если подразумевается работа на не слишком примитивном уровне. Приличного механизма RTTI (Run-Time Type Information), похоже, так и не появилось. Но это мы обсудим чуть позже. А пока угадайте, что следует из наличия общего предка?
Правильно, из этого следует отсутствие множественного наследования. Не спешите ругать дядю Билли, ему сейчас и так нелегко. Решение убрать множественное наследование не случайно. Это результат победы другой, гораздо более полезной концепции, а именно гомоморфной иерархии классов, представляющей модель дерева, где все классы имеют одного общего предка. Да и в трудах многих экспертов C++ я встречал очень осторожное отношение к множественному наследованию из-за порождаемых им проблем. Кроме того, существуют способы обойтись без него вообще.
Теперь вспомним про ОР. Все классы представляют собой гомоморфную иерархию, имеют мощнейшую систему RTTI и представляют собой ссылочный тип. Т.е. являются автоматически разименовываемыми указателями на экземпляры классов. C#, так же как и OP, допускает размещение экземпляров классов только в куче. А это, кстати, очень способствует автоматизации сборки мусора, т.к. стек живет своей жизнью. Справедливости ради надо заметить, что в иерархию классов ОР входят только собственно классы.
3. Классы. Как и ожидалось, C# сразу заявляет, что поддерживает наследование только от одного предка, но зато и от множества интерфейсов. Классы могут включать константы, поля, методы, свойства (!), индексаторы (разновидность свойств), события (!), операторы, конструкторы, деструкторы, а также вложенные объявления типов. Каждый член класса объявляется с указанием формы доступа:
• public — доступны для всего кода;
• protected — только из классов-потомков;
• internal — только из того же модуля;
• protected internal — из того же модуля только классам — потомкам;
• private — доступны только самому классу.
Веселое разнообразие, которое необходимо применить к каждому (!) члену класса. А теперь представьте, как будет выглядеть объявление класса с учетом того, что методы класса реализуются только непосредственно в этом самом объявлении. Ну ничего, злые языки и раньше утверждали, что C/C++ являются "write-only" языками. Но писали же люди, и нормально...
В ОР все это выглядит поприличнее. Классы также могут наследоваться от одного класса и множества интерфейсов. Про свойства и события мы поговорим чуть позже. А вот касательно реализации методов ОР придерживается прямо противоположного мнения — только вне объявления класса. И, конечно, модификатор доступа к каждому члену специально указывать не нужно. Примечательно, что C# использует вместо friend (C++) такой же принцип доступа к классу извне, как и ОР.
4. Интерфейсы. А это привет от COM-технологий. Сами интерфейсы, конечно, проживут и без COM, а вот COM без них, как без рук. Интерфейсы очень похожи на классы, но отличаются тем, что являются "чистыми" объявлениями, т.е. не содержат никакой реализации. Эта почетная задача возлагается на каждый класс, который в списке предков указал данный интерфейс. Пример интерфейса:
interface Iexample {
string this[int index] { get; set; }
event EventHandler E;
void F(int value);
string P { get; set; }
}
public delegate void EventHandler
(object sender, Event e);
Интерфейсы могут содержать методы, свойства, индексаторы и события. Таинственная строка со словом delegate необходима для объявления события E. Заметим также, что P — это свойство, this — тоже свойство, только, выражаясь по-военному, "более другое" — индексируемое. Естественно, что интерфейс может наследоваться от нескольких других интерфейсов:
interface IComboBox: ITextBox, IListBox {}
В целом, интерфейсы C# абсолютно аналогичны интерфейсам в ОР за исключением одной важной "фичи". Называется она "реализация через делегирование" и позволяет классу передать (полностью или частично) вышеозначенную почетную обязанность на реализацию своему свойству, имеющему тип другого класса или интерфейса... Когда же я научусь писать просто?:-) К счастью, в жизни (и в ОР) все выглядит гораздо проще, чем звучит:
property MyClass: TMyClass
read FMyClass
implements IMyInterface;
И все. Теперь класс может забыть о занудном перечислении и реализации всех членов интерфейса. Но главная прелесть в другом: можно динамически менять экземпляр класса (FMyClass), отвечающий за реализацию. "Подумаешь..." — скажет читатель. Ну и правильно, зато в C# есть
5. Контроль версий. В этом вопросе Microsoft не ограничилась изобретением велосипеда, а возвела целую теорию. Я даже не сразу понял, о чем речь.
Вот какой пример тяжелой программистской доли приводит дядя Билли. Существует погрязший в долгах и неизвестности автор класса Base. А еще есть халявщик — автор класса Derived, являющегося наследником Base. Класс Derived добавляет виртуальный метод F, а после этого "уходит на золото" и поступает к заказчикам. После этого автор класса Base, в поисках лучшей жизни, добавляет к Base свой собственный метод F. И тут автора класса Derived настигает расплата (не совсем понятно за что, наверно, за любовь к C++). Становится непонятно, что же происходит в Derived: переопределение (overriding) или перекрытие (hiding) метода F. Тут Билли напоминает нам, что взаимодействие между авторами невозможно в силу идеологических разногласий, а вероятность возникновения такой ситуации увеличивается пропорционально количеству нахлебников класса Base.
По сценарию появляется добрая фея в лице C# и взмахами волшебной дубины заставляет всех ясно определиться в своих помыслах. Автор класса Derived вынужден применить новое ключевое слово new при объявлении своего метода F, чем объявляет автору класса Base, что чихать он хотел на его метод F. Компилятор при этом заговорщицки перестает ворчать.
new public virtual void F() { ... }
Но однажды, с похмелья, автору класса Derived приходит в голову шальная мысль: "А почему бы не использовать функциональность метода F предка?" Фея ему подсказывает, что нужно использовать новое ключевое слово override. Мир, дружба, жвачка!
public override void F() {base.F(); ... }
Мораль сей басни такова: разработчики должны явно указывать переопределение виртуальных методов предков директивой override. Если они этого не делают, то компилятор выдает предупреждения. Чтобы его утихомирить, нужно указать директиву new. При этом виртуальная цепочка методов обрывается и появляется совершенно новый виртуальный метод.
Конечно, Билли кричит, что спас мир от неминуемой гибели, совершенно забывая об авторских правах. Директива override (и вся идеология контроля версий) существует в ОР уже лет пять, начиная с Delphi 1.0, а reintroduce (аналог new — успокоитель компилятора) года два-три.
6. Свойства. Свойства — это естественное расширение полей. Говоря простым языком, свойство похоже на переменную. Когда вы присваиваете свойству новое значение, вызывается один метод, а когда вы читаете свойство — другой метод. Автоматически! И вот тут Microsoft произносит судьбоносную (серьезно, я не шучу) речь:
"Успех таких инструментов быстрой разработки приложений (RAD), как VB, можно, с небольшим допущением, отнести в первую очередь к наличию свойств. Разработчики в VB могут думать о свойстве просто как о поле. Это позволяет им сфокусироваться на логике собственного приложения (!), а не на особенностях компонентов, которые им приходится использовать. Современные компонетно-ориентированные программы просто битком набиты свойствами. Ориентированные на использование методов языки (например, o.SetValue(o.GetValue() + 1)) имеют явные недостатки по сравнению с языками на основе свойств (тот же пример, o.Value++)."
Золотые слова! Именно свойства (в сочетании с мощной компонентой моделью) позволяют воплотить в жизнь хотя бы элементарное разделение труда программистов. Простой пример из Delphi VCL. Мне нужно изменить ширину окна на 20 пикселов:
Width:= Width + 20;
И все! Окно уже перерисовано. Я могу вообще не знать, какая работа производится внутри (а могу и знать, и активно вмешиваться). А вот и пример объявления свойства в C#. Здесь в виде свойства Caption реализуется заголовок кнопки:
public class Button: Control {
private string caption;
public string Caption {
get {
return caption;
}
set {
caption = value;
Repaint();
}
}
}
Ключевыми словами get и set обозначаются методы доступа к свойству, а value представляет собой новое значение, присваиваемое свойству. Обратите внимание на Repaint(), оно подразумевает автоматическую перерисовку окна после изменения заголовка. Но в C# есть еще одна разновидность свойств, называемая
7. Индексаторы. Особенности свойств, в принципе, позволяют им "закосить" под переменную любого типа. Но есть и исключение — массивы. Чтобы обратиться к элементу массива, необходимо указать его индекс(ы). А для этого приходится немножко изменить синтаксис объявления свойства. Так и получается индексатор:
public class ListBox: Control {
private string[] items;
public string this[int index] {
get {return items[index];}
set {
items[index] = value;
Repaint();
}
}
}
ListBox listBox =...;
listBox[0] = "hello"; //Перерисовка списка
В этом примере index — это целочисленный индекс. Но индексы могут быть любого (!) типа. Почему-то Microsoft забыла привести самые захватывающие случаи, когда в качестве индексов используются, например, переменные типа класса или строки.
Теперь подведем итог по свойствам. Несмотря на все старания, свойства в C# не дотягивают до свойств в ОР. То, что есть, реализуется менее гибко. К тому же ОР предлагает еще одну разновидность свойств — индексируемые свойства (не путать с индексаторами C#, в ОР они называются array properties). Суть заключается в том, что несколько свойств могут использовать общие методы доступа по чтению и записи, в которые передается дополнительный параметр, идентифицирующий обращающееся свойство. Из-за синтаксиса объявления классов в C# реализовать такие свойства можно только через... Ну, в общем, косвенно.
8. Делегаты. Название солидное, вот как Microsoft его обосновывает:
"Делегаты позволяют делать то же, что и указатели на функции в C++ и некоторых других языках. Однако, в отличие от них, делегаты являются объектно-ориентированными и безопасными."
За пышным фасадом скрывается конструкция, аналогичная procedure of object из ОР. И служит она, соответственно, в основном для работы с событиями (events). Делегат — это ссылочный тип, происходящий от общего предка System.Delegate. Делегат, не имеющий аргументов можно объявить так:
delegate void SimpleDelegate();
Кроме того, C# требует еще и создания экземпляра делегата (тип ведь ссылочный), так что без new не обойтись. Применить делегата можно вот так:
void MultiCall(SimpleDelegate d, int count) {
for (int i = 0; i < count; i++) d();
}
9. События. События позволяют классу предоставить "зацепки" (или "заглушки"), к которым клиенты класса могут присоединить исполнимый код в виде обработчиков событий. Таким образом, класс генерирует какое-то событие, а выполняется код клиента. Данный механизм использует ОО-концепцию — делегирование. Поэтому не удивительно, что события являются переменными или свойствами типа делегат, о которых мы только что поговорили.
public delegate void EventHandler(object sender, Event e);
public class Button: Control {
public event EventHandler Click;
public void Reset() { Click = null; }
}
В этом примере Click является событием и имеет тип делегата EventHandler. Из этого следует, что событию Click может быть назначен любой метод, принимающий в качестве параметров object и Event и возвращающий void. Делается это операторами += и -=.
void Button1_Click(object sender, Event e) {Console.WriteLine("Button1 was clicked!");}
Button1.Click += new EventHandler(Button1_Click);
Button1.Click -= new EventHandler(Button1_Click);
Класс Button можно переписать, объявив событие Click в стиле объявления свойств, что не имеет существенного значения:
public event EventHandler Click {get{...};set {...}}
Microsoft совершенно справедливо замечает, что события являются важнейшим аспектом дизайна библиотек классов вообще и системной библиотеки классов, в частности. Действительно, грамотно спроектированная библиотека в большинстве случаев позволяет расширять свою функциональность без создания новых классов, просто назначая необходимые обработчики событий. Таким образом библиотека делегирует клиенту право (именно право, а не обязанность) реализации нужной ему функциональности.
Словом, события являются большим шагом вперед. Но в сравнении с ОР события из C# явно не смотрятся. Нет, сами события абсолютно аналогичны. Все портит совершенно убойный синтаксис (операторы += и -=) и необходимость динамически создавать делегаты. К тому же, без соответствующей поддержки RTTI вся работа с событиями превращается в обычную рутину, и все нужно делать вручную.
10. Массивы. Размеры и измерения массива определяются во время его создания и остаются неизменными в течение всего времени существования экземпляра массива. Спецификация прямо говорит, что изменить их невозможно.
Обидно, однако... Вообще, динамический массив, быстрый, переменного размера, с автоматическим подсчетом ссылок и управлением временем жизни, полностью совместимый с традиционными массивами — мечта любого разработчика. И в ОР именно такие динамические массивы существуют на уровне языка!
11. Параметры params. Эта возможность введена для передачи в функции в качестве параметров массивов переменного размера. Для этого формальный параметр объявляется с использованием модификатора params. Этот параметр должен быть последним в списке формальных параметров. Пример:
void F(params int[] values) {
Console.WriteLine("values contains %0 items", values.Length);
}
Конечно, это тоже не выдумка парней из Редмонда. Параметры params — прямая аналогия параметров типа открытый массив (open arrays) из ОР. При этом ОР не накладывает никаких ограничений на положение таких параметров в списке.
Success = new Features(cool);
Хорошая мысль, откуда бы она ни была взята, гораздо лучше, чем собственная глупая, — не в обиду будет сказано тем, кто находит все в себе самих, не прибегая ни к кому.
Фр. Ламот-Левайе
12. Атрибуты. Вот это действительно что-то новенькое. Microsoft говорит, что C# позволяет разработчикам ввести новые типы описательной информации для различных программных элементов и получить эту информацию во время выполнения программы. Эти типы определяются с использованием атрибутов. Атрибуты — это классы, происходящие от абстрактного класса System.Attribute.
К примеру, можно определить атрибут HelpAttribute (или Help, для краткости), который затем можно поместить на классы или методы и обеспечить тем самым прямое отображение между программными элементами и документацией.
[AttributeUsage(AttributeTargets.All)]
public class HelpAttribute: System.Attribute {
public HelpAttribute(string url) {
this.url = url;
}
public string Topic = null;
private string url;
public string Url {
get { return url; }
}
}
Квадратные скобки здесь демонстрируют применение другого атрибута при объявлении атрибута Help. В этом случае атрибут AttributeUsage показывает, что наш атрибут Help может быть использован для любого программного элемента.
Уф. Вы что-нибудь поняли?.. Я тоже. Давайте смотреть дальше. Здесь мы применим вышеобъявленный атрибут, добавив конкретную информацию:
[Help("http://www.mycompany.com/.../Class1.htm")]
public class Class1 {
[Help("http://www.mycompany.com/.../Class1.htm", Topic ="F")]
public void F() {}
}
Атрибутивная информация может быть получена в run-time режиме:
using System;
class Test {
static void Main() {
Type type = typeof(Class1);
object[] arr = type.GetCustomAttributes(typeof(HelpAttribute));
if (arr.Length == 0)
Console.WriteLine("Class1 has no Help attribute.");
else {
HelpAttribute ha = (HelpAttribute) arr[0];
Console.WriteLine("Url = {0}, Topic = {1}", ha.Url, ha.Topic);
}
}
}
Класс Test проверяет, имеет ли класс Class1 атрибут Help, и выводит ассоциированные с ним значения Topic и URL.
Естественно, что возможность получить атрибут в run-time'е требует какой-то поддержки со стороны RTTI. Похоже, что именно в атрибутах, а не в классах, будет заложен этот мощный механизм, а поддерживаться все это будет со стороны NET-платформы. Пока все это выглядит несколько загадочным. Впрочем, есть несколько хороших примеров:
12.1. Поддержка COM. Атрибуты из этого раздела Microsoft предлагает использовать для создания.NET-приложений, которые взаимодействуют с COM-программами.
Атрибут COMImport. Применив его к классу, мы помечаем класс как реализованный внешне COM-класс. Такое объявление класса позволяет использовать имя C# для обращения к COM-классу.
Атрибут Guid. Используется для присвоения классу или интерфейсу уникального идентификатора GUID.
12.2. Внешние, extern методы. Объявление метода может включать модификатор extern, что говорит о внешней реализации метода. Как правило, этот модификатор используется совместно с атрибутом DllImport, позволяя реализовать методы в DLL. Такое решение дополнительно требует включения модификатора static. Пример:
class Path {
[DllImport("kernel32", setLastError=true)]
static extern bool CreateDirectory(string name, SecurityAttributes sa);
}
13. Завершенные классы. Завершенный (возможно, лучше перевести "тупиковый") класс объявляется с модификатором sealed. После выполнения такого ритуала наследование от завершенного класса возбраняется высшими силами. Это выражается в том, что компилятор будет бить вас по рукам каждый раз, когда он заметит завершенный класс в списке предков другого класса. Кроме того, завершенному классу строго запрещено исповедовать абстрактные идеи, т.е. об использовании модификатора abstract в нем можно забыть.
Остается только отметить, что классы в ОР подобными формами бесплодия не страдают. Мне представляется, что наибольшую пользу от sealed могут получить лишь классы, не вполне грамотно организованные внутри, когда потомок может нечаянно нарушить унаследованные механизмы работы. Кто знает другие выгоды, расскажите.
14. Поля readonly. Как вы догадались, речь идет о полях только для чтения. Объявляются такие поля с использованием модификатора readonly. Присвоение таким полям может осуществляться только в конструкторе класса либо как часть объявления.
Надо полагать, что readonly является развитием модификатора const из C++. В ОР такая возможность отсутствует, а разработчики обычно просто используют read-only свойства.
15. Автоматическое управление памятью. Под этим гордым названием подразумевается сборка мусора, т.е. уничтожение объекта, как только он становится больше не нужен. Выделяется память все так же, через оператор new. Операции с указателями запрещены, но не убраны. Ими можно пользоваться в специально помеченных участках кода. C# называет такие участки небезопасным (unsafe) кодом.
Интересно, что в ОР сборка мусора по определению отсутствует. Однако у новичков складывается устойчивое впечатление, что Delphi полностью управляет созданием и удалением всех объектов. Почему так происходит?
ОР автоматически управляет памятью и временем жизни таких объектов, как длинные строки, вариантные переменные, динамические массивы и интерфейсы. Кроме того, на уровне компонентов (важнейшая ветка библиотеки VCL) реализуется автоматическое управление дочерними компонентами, включая их удаление, а также уведомление о появлении или исчезновении других компонент в пределах того же родителя. А компоненты — кирпичи приложения.
При создании экземпляра компонента включается механизм RTTI, который восстанавливает из ресурсов (при наличии таковых) значения свойств и событий (т.е. назначенных им обработчиков событий) объявленных как published. На этом сюрпризы не заканчиваются. На уровне TObject (общий предок) реализованы виртуальные методы NewInstance и FreeInstance, которые заведуют выделением и освобождением памяти при создании нового экземпляра класса! Это значит, что любой класс может переопределить управление своей памятью до неузнаваемости.
Вы еще не забыли? В ОР сборка мусора отсутствует:-).
16. Поддержка исключений (exceptions). Этот момент является одним из важнейших для любого современного языка программирования. Исключения позволяют использовать реактивную модель написания кода. Нет, к Вашему любимому авиа-симулятору это не имеет никакого отношения.
Суть идеи в том, что код пишется для идеального случая функционирования программы, когда нет никаких ошибок. Ошибочная ситуация называется исключением и обрабатывается в специальном участке кода — обработчике исключения. Таким образом, разработчик избавляется от маразматической привычки повсюду расставлять операторы if.
Все исключения в C# происходят от общего предка — класса System.Exception. Для защиты некоторого участка кода структурной обработкой исключений используются блоки try...catch, try...finally или комбинированный вариант. Выражение catch позволяет указать тип обрабатываемых исключений. Блок, стоящий за finally, выполняется всегда, независимо от наличия или отсутствия исключений. Каждый блок try может содержать несколько catch и один finally.
Исключение генерируется в выражении с ключевым словом throw. Это является для разработчика способом создания исключительной ситуации. После создания исключения начинается поиск охватывающего блока try, содержащего соответствующий данному исключению catch. Если обработчик исключения не найден в данной функции, то поиск идет в вызвавшей функции. Если стек полностью раскручен и обработчик не найден, то процесс или поток завершается.
Отметим, что реализация исключений в C# значительно улучшилась по сравнению с C++ (ANSI) и доросла, наконец, до уровня исключений в OP. Самым главным моментом здесь, конечно, является общий предок для всех исключений. Большое значение имеет появление блока try...finally, а также возможность работы с экземпляром исключения в общем обработчике и повторно его генерировать. Кроме этого, ОР умеет автоматически устанавливать блок обработки исключений в каждом конструкторе и, при возникновении исключения, автоматически вызывается деструктор. Да много чего еще умеет...
17. Все остальное (или почти все). В C# сохранились перегрузка функций и операторов (перегрузки операторов в ОР нет). Остался также препроцессор (компилятор Delphi все делает за один проход, в течение нескольких секунд). Передача параметров в функцию по ссылке осуществляется с помощью модификатора ref. Кроме того, появился модификатор out для параметров, через которые возвращается значение. Каждый объект в C# является COM-объектом (в OP все классы также являются COM-объектами).
Следует отметить, что, конечно, многие нововведения в C# нельзя относить исключительно на счет ОР. Это давно известные языковые конструкции. Просто они впервые или наиболее удачным образом были реализованы именно в ОР. Но все же скопировано очень многое, в том числе и идеология языка.
В заключение официальной части хочу привести таблицу сравнения объектных моделей различных языков. В оригинальном виде она присутствует в книге Рэя Лишнера "Delphi in a Nutshell". Я же дополнил ее столбцом для C#.
1. В C++ можно эмулировать интерфейсы с помощью абстрактных классов. 2. См. "15. Автоматическое управление памятью". 3. Интерфейсы используют подсчет ссылок. 4. RTTI в C++ и C# ограничено сравнением и приведением типов. 5. Встроенный ассемблер не входит в стандарт C++, однако большинство компиляторов C++, включая Borland, поддерживают встроенный ассемблер как расширение языка.
// Комментарии.
Вернейший путь к истине — это познавать вещи, какими они есть на самом деле, а не заключать о них так, как нас учили.
Дж. Локк
Дабы не уподобляться поучающим, эти комментарии я предлагаю исключительно как мое личное мнение, не претендующее на абсолютную истинность. Возможно, кто-то сочтет его интересным и полезным для себя. Профессионалы, как правило, всегда имеют свое четкое мнение, поэтому убеждать их в чем-то — занятие неблагодарное. Им нужен только исходный материал, а мнение они составят. Так что эта категория читателей может с облегчением вздохнуть и выпить пива, для них я свое повествование уже закончил.
Ну вот, почти никого не осталось:-). Теперь можно и поболтать с Тобой, уважаемый читатель. Если у Тебя хватило терпения дойти до этого места, мои поздравления. Судя по всему, Тебе не безразличен выбор инструмента, которым Ты воплотишь все свои гениальные задумки в сухую последовательность машинных кодов. Разреши предложить Тебе мои скромные выводы в качестве отправной точки для размышлений.
Для чего же все-таки Microsoft придумала новый язык? Вот так неожиданно, вроде бы даже без особых причин. В этом случае Microsoft довольно честно объясняет все в своем анонсе. Действительно, C# — это не соперник Java. Это язык, предназначенный для того, чтобы привнести в среду разработчиков новые технологии программирования, которые отсутствуют у VC++ и VB. Конечно, Microsoft "забывает" сказать, что такие технологии уже давно существуют на рынке.
Но, как я уже однажды говорил, корни проблемы языков не в их особенностях, а в финансовых возможностях конкретных фирм. Кстати, интересный факт. Microsoft имеет неплохую долю в капитале Borland, а не так давно выплатила 250 M$ только за то, чтобы Delphi при работе с базами данных больше внимания уделяла ADO и MS Access.
Однако, кризис Visual Studio назрел, что и выразилось в появлении C#. Вы скажете: "Так уж и кризис?" Дело в том, что, по моему мнению, для написания на C/C++ качественных приложений нужно быть профессионалом, своего рода художником от программирования (это верно и для VB, т.к. его легкость не обладает мощью, и круг замыкается). В остальных случаях мы имеем то, что имеем, а именно кучу service pack'ов, багфиксов, эксплоитов к дырам в безопасности и постоянное ожидание сюрпризов от каждого приложения. Почему?
C и C++ действительно очень мощны. Но они же и низкоуровневы, более близки к ассемблеру, требуют высокой культуры программирования. Они идеально подходят для системного программирования, но попытки сделать из них что-то большее довольно часто выливаются в неоправданное растрачивание сил программистов, хотя дело, безусловно, делается. Первое время после перехода на C++ я постоянно ловил себя на том, что прокручивал в голове все мельчайшие детали функционирования программы. Конечно, этому очень способствовала многоплатформенность и системность задач. Однако я считаю, что у разработчика есть более важные проблемы, чем воплощать собой живой debugger.
C# в этом плане пока меняет не многое. Он также требует большого начального запаса знаний, которые зачастую не имеют никакого отношения к поставленной задаче. Ему еще необходима мощная, гибкая, по-настоящему визуальная среда, продуманная библиотека компонент (именно его, а не общая на всю Visual Studio). Хотя я пока не вижу способа создать на базе теперешнего C# серьезную систему RAD. По-моему, атрибуты не могут заменить полноценную поддержку RTTI. А без этого визуальное программирование будет ничуть не лучше, чем в VC++. Кстати, несмотря на то, что в описании C# то и дело встречается слово "компонент", продуманная концепция компонент отсутствует напрочь. Видимо, рассчитывают на ActiveX. Ну, посмотрим...
С другой стороны, никому не придет в голову писать драйверы на Delphi (это, в основном, вопрос startup-кода), хотя Delphi генерирует очень производительный код, а минимальное Delphi-приложение может быть в несколько раз меньше, чем аналогичное в том же MSVC. Просто эта среда ориентирована на построение сложных, комплексных приложений. Именно здесь начинают проявляться все ее архитектурные решения. Delphi обеспечивает совершенно уникальный уровень абстракции в сочетании с полным доступом к низкоуровневым средствам и одинаково хорошо встречает и матерого профессионала, и начинающего разработчика (давая ему возможность постепенно (!) стать профессионалом). Delphi насквозь пропитана духом ООП.
Сравнительный анализ (см. табл.) показывает, что на сегодняшний день Delphi имеет в своей основе язык, который уже в течение нескольких лет превосходит своих конкурентов в создании большинства приложений. Да, Borland вроде бы незаметно, но систематически доводила ОР до ума с появлением каждой новой версии Delphi, не ограничиваясь поддержкой новых технологий и улучшением визуальной среды. Единственное, что можно было бы добавить к ОР, как языку, это перегрузка операторов и возможность понижения видимости членов класса.
Важно еще и то, что C# пока существует только на бумаге. Ко времени появления соответствующего компилятора уже, скорее всего, увидят свет новые версии Delphi и C++ Builder в компании с Kylix (RAD для Linux) и кроссплатформенной библиотекой классов CLX. Это означает, что приложения, использующие стандартные компоненты, будут полностью переносимы на уровне исходников (RTL тоже кроссплатформенна). Кроме того, расширяется поддержка HTML и XML, усиливается RTTI для поддержки интерфейсов, что позволит любой класс опубликовать как Web-сервис, появляются Delphi Server Pages и Pascal-подобный скриптовый язык. Вместе с улучшениями самой среды это говорит о том, что новые инструменты от Borland, как минимум, перекрывают весь круг задач, для которых создавался C#.
Если вы думаете, что Borland — единственная контора, выпускающая компиляторы Pascal, то у меня есть несколько сюрпризов. Sybil (http://www.speedsoft-online.de/) — Delphi-подобная кроссплатформенная среда под Windows, Linux и OS/2. Free Pascal (http://www.freepascal.org/) — свободный кроссплатформенный компилятор под Windows, Dos, Linux, OS/2, Amiga. Кроме того, я слышал о разработке Pocket Pascal под Palm.
Конечно, эта статья не о Delphi и ОР. Поэтому я не буду слишком распинаться на эту тему, хотя говорить можно много и интересно. А что касается выводов... За время подготовки этого материала (около месяца, включая техническое рецензирование профессионалами в C++) я смог четко определиться, почему мне нравится работать именно в Delphi (это, наверное, тяжело не заметить:-). И окончательную точку в моих выводах поставил выход C#.
Надеюсь, что у Тебя, читатель, также появился повод для размышлений (особо интересными прошу поделиться). Желаю всем успехов в выборе достойного инструмента.
Владимир Волосенков, Brainbench Certified Master Delphi Programmer, Master Windows NT Workstation Administrator.
Тухлые овощи бросать на Uno@au.ru.
(c) Компьютерная газета
Компьютерная газета. Статья была опубликована в номере 31 за 2000 год в рубрике программирование :: разное