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

Очень кратко рассмотрим аспекты языка, связанные с наследованием. Как
уже говорилось, класс может иметь суперкласс, и в этом случае он наследует
все атрибуты суперкласса, в дополнение к тем, которые определяет сам.
Область видимости класса вложена в область видимости суперкласса, поэтому
любые атрибуты суперкласса могут быть переопределены в производном классе.
Подклассу доступны все публичные и все защищенные (но не приватные!)
декларации суперкласса. Механизмы let и conceal дают гибкие возможности
управления видимостью атрибутов суперкласса, позволяя скрывать их или давать
им альтернативные имена.

Любая функция, декларированная в некотором классе, может иметь
спецификатор virtual. Он означает, что данная функция является виртуальной
функцией
данного класса, т.е. может иметь альтернативную реализацию в любом
из его подклассов. Механизм виртуализации вызовов функций обеспечивает т.н.
динамическое связывание: в отличие от обычного связывания, основанного на
информации о типах времени компиляции, для виртуальной функции всегда
вызывается именно та версия, которая необходима, исходя из динамической
информации о реальном типе объекта данного класса, доступной при выполнении
программы. Переопределить виртуальную функцию очень просто. Для этого ее имя
должно быть включено в список переопределения instate, обычно завершающий
декларацию подкласса. Параметры и тип функции повторно задавать не нужно:
они жестко определяются virtual-декларацией суперкласса. Нередко в списке
instate дается и реализация новой версии виртуальной функции; в противном
случае реализация должна быть дана позднее.

Если виртуальная функция не переопределена в подклассе, наследуется ее
версия из суперкласса. Фактически, имя виртуальной функции -- это интерфейс,
за которым скрывается множество различных функций. Наконец, как и в C++,
подкласс может явно вызвать версию из какого-нибудь суперкласса с помощью
полностью квалифицированного имени.

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

Как обычно, мы продемонстрируем наследование, полиморфизм и абстракты
на более-менее реалистичном примере (работа с простейшими геометрическими
объектами).

!! Геометрическая фигура (абстрактный класс)
abstract Figure {
!! фигура обычно имеет...

!! -- некий периметр:
float () virtual perimeter;
!! -- некую площадь:
float () virtual area;
};

!! Точка
class Point : Figure {
} instate #perimeter { return 0f }, #area { return 0f };

!Отрезок (длины L)
class Line : Figure {
float L !! длина
} instate #perimeter { return L }, #area { return 0f };

!! Квадрат (со стороной S)
class Square : Figure {
float S !! сторона
} instate #perimeter { return 4 * S }, #area { return S * S };

!! Прямоугольник (со сторонами A, B)
class Rectangle : Figure {
float A, B
} instate #perimeter { return 2 * (A + B) }, #area { return A * B };

!! Круг (с радиусом R)
class Circle : Figure {
float R
} instate #perimeter { return 2 * PI * R }, #area { return PI * R * R };

При всей примитивности определенной нами иерархии объектов, с ней уже
можно делать что-то содержательное. К примеру следующий фрагмент
подсчитывает суммарную площадь фигур в массиве ссылок на фигуры fig_vec:

%Figure @ []@ fig_vec; !! ссылка на вектор ссылок на фигуры
float total_area = 0f; !! суммарная площадь
for u_int i = 0 while i <> fig_vec# do ++ i
{ total_area += fig_vec [i].area () }

Наконец мы отметим, что виртуальные функции -- это не единственный
полиморфный механизм в языке. При необходимости можно использовать
специальную операцию явного приведения указателя на суперкласс к указателю
на подкласс. Бинарная операция квалификации:

CLASS qual OBJ_PTR_EXPR

предпринимает попытку преобразовать OBJ_PTR_EXPR (указатель на некий
объект) к указателю на класс CLASS (который должен быть подклассом
OBJ_PTR_EXPR^). Операция возвращает выражение типа CLASS^: если объект, на
который указывает второй операнд, действительно является экземпляром класса
CLASS, возвращается указатель на него, в противном случае возвращается
значение nil. Вот почему возвращаемое значение всегда должно проверяться
прежде, чем с ним предпринимаются дальнейшие вычисления.

%Figure ^fig_ptr; !! указывает на фигуру
%Rectangle some_rect (10f, 20f); !! прямоугольник 10 * 20
%Circle some_circ (50f); !! окружность радиуса 50

fig_ptr = some_rect@; !! fig_ptr указывает на прямоугольник
Rectangle qual fig_ptr; !! вернет указатель на some_rect
Circle qual fig_ptr; !! вернет nil

fig_ptr = some_circ@; !! fig_ptr указывает на окружность
Rectangle qual fig_ptr; !! вернет nil
Circle qual fig_ptr; !! вернет указатель на some_circ

Квалификация с помощью qual очень похожа на динамическое приведение
типов dynamic_cast в последних версиях языка C++.

Определение операций

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

Новая семантика операции задается с помощью специального описателя
opdef:

opdef OP_DEF1 =' EXPR1 ( ,' OP_DEF2 =' EXPR2) ...

Как и все прочие описания, определения операций имеют локальный
характер. Каждый элемент OPDEF -- это конструкция, имитирующая синтаксис
соответствующей операции, но вместо операндов-выражений в ней задаются типы
данных. (Гарантированно могут использоваться любые примитивные типы и имена
классов
, но возможно, в будущем можно будет использовать любые производные
типы).

Соответствующее выражение EXPR будет подставляться вместо комбинации
OPDEF. При этом в EXPR допустимо использование специальных термов вида
(<1>), (<2>)..., соответствующих первому операнду, второму и
т.п. Пример:

opdef VECTOR + VECTOR = VECTOR.add (<1>, <2>)

Здесь определяется новая семантика операции +' для двух объектов
класса VECTOR. Вместо этой операции будет подставлен вызов функции add
(предположительно определенной в классе VECTOR) с обоими операндами в
качестве аргументов.

Фактически определение операции -- это разновидность макроопределения,
и в семантике макроподстановки имеется очень много общего с
let-определениями. Так, подстановка является семантической, а не
текстуальной. Но определенная операция -- это не вызов функции: и для самого
определения и для всех его операндов действует семантика подстановки, а не
вызова. Громоздкое определение вызовет генерацию большого количества лишнего
кода, а если в теле opdef-определения ссылка на параметр встречается
многократно, соответствующий ей операнд также будет подставлен несколько раз
(что, вообще-то, весьма нежелательно).

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

Приведем содержательный пример определения операций. Пусть у нас
имеется класс String, реализующий символьные строки, грубая модель которого
дана ниже:

class String {

!! (определения...)

!! длина текущей строки
u_int () #length;
!! конкатенация (сцепление) строк head & tail
%String (%String head, tail) #concat;
!! репликация (повторение n раз) строки str
%String (%String str; u_int n) #repl;
!! подстрока строки str (от from до to)
%String (%String str; u_int from, to) #substr;

!! ...
}

Теперь определим набор операций, позволяющих работать со строками
проще.

!! для компактности ...
let Str = String;
!! #' как длина строки:
opdef Str# = (<1>).len ();
!! +' как конкатенация:
opdef Str + Str = Str.concat ((<1>), (<2>));
!! *' как репликация:
opdef Str * u_int = Str.repl ((<1>), (<2>));
opdef u_int * Str = Str.repl ((<2>), (<1>));
!! отрезок как подстрока
opdef Str [u_int..u_int] = Str.substr (<1>, <2>, <3>);

Определенные так операции довольно удобно использовать:

Str("ABBA")#; !! 4
Str("Hello, ") + Str("world!"); !! Str("Hello, world!")
Str("A") * 5; !! Str("AAAAA")
3 * Str("Ha ") + Str("!"); !! Str("Ha Ha Ha !")
Str("Main program entry") [5..12]; !! Str("program")

Как уже говорилось, имеющиеся в языке операции ввода и вывода
предназначены исключительно для переопределения. Для большинства примитивных
типов (и для многих объектных) эти операции переопределены в стандартных
библиотеках ввода-вывода, что делает их использование очень простым. Их
разумное определение для пользовательских классов -- рекомендуемая практика.
Так, для упомянутого класса VECTOR мы можем определить операцию вывода
(OFile -- класс выходных потоков):

opdef OFile <: VECTOR =
(<1>) <: ( <: (<2>).x <: ,' <: (<2>).y
<: ,' <: (<2>).z <: )'

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

opdef OFile <: VECTOR =
(<2>).((<1>) <: ( <: x <: ,' <: y <: ,' <:
z <: )')

Но, вообще-то говоря, если определенная так операция вывода будет
использоваться интенсивно, это приведет к заметному переизбытку
сгенерированного кода. Лучшим решением будет определить функцию для вывода
объектов VECTOR, а потом, уже через нее, операцию.

Импорт и экспорт.
Прагматы.

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

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

Любые внешние объекты модуля (например, глобальные переменные, функции,
типы данных и классы) могут быть экспортированы во внешнюю среду. Это
делается за счет помещения их в блок декларации экспорта, имеющей вид:

export { DECLARATION_LIST }

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

import MODULE { STMT_LIST }

В отличие от декларации экспорта, декларация импорта может быть
локальной: она может встретиться в любом блоке или, к примеру, в декларации
класса. Здесь MODULE -- это текстовая строка, задающая имя модуля. В более
общем случае, это имя импортируемого ресурса, который может быть глобальным
(общесистемным) или даже сетевым (синтаксис MODULE зависит от реализации и
здесь не рассмотрен). STMT_LIST -- произвольный список инструкций, в котором
будет доступно все, экспортированное ресурсом MODULE. В частности, он может
содержать другие декларации import, что позволяет импортировать описания из
нескольких модулей.

Точная семантика механизма импорта/экспорта -- слишком сложная тема,
чтобы рассматривать ее здесь в деталях. Если кратко, то передаче через этот
механизм могут подвергаться декларации переменных и функций, классов, все
определенные пользователем типы, макроопределения и операции. Заметим, что
каждый модуль фактически состоит из внешней (декларативной) и внутренней
(реализационной) частей. Для правильной компиляции всех импортеров этого
модуля требуется лишь знание первой из них; реализационная часть модуля (в
виде сгенерированного кода) остается приватной.

Наконец, существует специальное служебное средство для управления
процессом компиляции -- прагматы:

pragma PRAGMA_STR

Литеральная строка PRAGMA_STR содержит директивы компилятору, набор
которых также может сильно зависеть от реализации и пока определен очень
приблизительно. Предполагается, что прагматы будут задавать опции
компилятора, такие, как режимы кодогенерации, обработки предупреждений и
ошибок, вывода листинга и т.п.

Перспективы развития и нереализованные возможности языка

Ксерион -- язык пока еще очень молодой и весьма далекий от
совершенства. В процессе разработки языка у его создателей возникали самые
разные идеи относительно возможностей его дальнейшего развития -- как в
краткосрочной, так и в "стратегической" перспективе. На некоторых из этих
идей стоит остановиться подробнее.

Так, практически неизбежным представляется включение в язык
let-макроопределений с параметрами. Функционально они будут похожи на
параметризованные #define C-препроцессора -- но, в отличие от последних, они
будут, подобно opdef'ам, иметь строго типизованные параметры и аналогичную
семантику подстановки. Не исключено, что параметризованные макроопределения
будут даже допускать перегрузку и выбор одного из вариантов на основе типов
аргументов.

В более отдаленной перспективе, возможно, появится и столь мощный
макро-механизм, как шаблоны (template) для деклараций классов и функций,
подобные аналогичным средствам в C++. Однако, пока трудно уверенно сказать,
какой вид примет этот механизм в окончательной форме.

Сейчас в языке отсутствуют какие-либо формы инструкции выбора,
аналогичной switch/case в C и C++, но их отсутствие очень чувствуется.
Скорее всего, когда аналогичный механизм будет включен в язык, он будет
существенно более мощным. В частности, он будет допускать нелинейную логику
перебора и более сложные критерии проверки "случаев".

Безусловно, было бы очень полезным также введение в язык механизма
перечислимых типов (enum), подобного имеющимся и в Паскале, и в C.

На повестке дня стоят и более сложные вопросы. Должно ли в Ксерионе
быть реализовано множественное наследование, как в C++? Этот вопрос является
одним из самых спорных. Возможны разные варианты: полный запрет
множественного наследования (что вряд ли приемлимо), множественное
наследование только от специальных абстрактных классов-интерфейсов (такой
подход принят в Java), наследование только от неродственных
классов-родителей, и, наконец, наследование без каких-либо ограничений.

Есть достаточно много неясных вопросов, связанных с аспектами защиты
содержимого классов
. В настоящей редакции языка принят намного более
либеральный подход к этому вопросу, чем в C++ и Java. Язык допускает
разнообразные механизмы инициализации экземпляра класса (экземпляром,
списком компонент, конструктором и, наконец, всегда доступна автоматическая
неявная инициализация). Как правило, объекты всегда инициализируются неким
"разумным" образом, однако может возникнуть потребность и в классах --
"черных ящиках", инициализация которых происходит исключительно через
посредство конструкторов. С самой семантикой конструкторов также есть
некоторые неясности.

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

Впрочем, что бы не планировали разработчики -- окончательный выбор, как
мы надеемся, будет принадлежать самим пользователям языка.

Заключение

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

!!
!! Исходный файл: "complex.xrn"
!! Реализация класса `complex`:
!! комплексные числа (иммутабельные)
!!

!! внешние функции (в реальной программе импортируемые):
double (double x, y) #atan2; !! двухаргументный арктангенс
double (double x, y) #hypot; !! гипотенуза
double (double x) #sqrt; !! квадратный корень

class complex {
!! компоненты класса
double Re, Im; !! (real, imag)

!! [Унарные операции над %complex]
%complex (%complex op1) %opUnary;

%opUnary #conj; !! Сопряжение
%opUnary #neg; !! Отрицание
%opUnary #sqrt; !! Квадратный корень

!! [Бинарные операции над %complex]
%complex (%complex op1, op2) %opBinary;

%opBinary #add; !! Сложение
%opBinary #sub; !! Вычитание
%opBinary #mul; !! Умножение
%opBinary #div; !! Деление

!! Проверка на нуль
bool () is_zero { return Re -- 0f && Im -- 0f };

!! [Сравнения для %complex]
bool (%complex op1, op2) %opCompare;

!! (на равенство):
%opCompare eq { return op1.Re -- op2.Re && op1.Im -- op2.Im };
!! (на неравенство):
%opCompare ne { return op1.Re <> op2.Re || op1.Im <> op2.Im
};

!! Модуль
double (%complex op) mod { return hypot (op.Re, op.Im) };
!! Аргумент
double (%complex op) arg { return atan2 (op.Re, op.Im) };
};

!! Реализация предекларированных функций

!! Сопряженное для op1
#complex.conj { return #(op1.Re, - op1.Im) };

!! Отрицание op1
#complex.neg { return #(- op1.Re, - op1.Im) };

!! Сложение op1 и op2
#complex.add { return #(op1.Re + op2.Re, op1.Im + op2.Im) };

!! Вычитание op1 и op2
#complex.sub { return #(op1.Re - op2.Re, op1.Im - op2.Im) };

!! Произведение op1 и op2
#complex.mul {
return #(op1.Re * op2.Re - op1.Im * op2.Im,
op1.Im * op2.Re + op1.Re * op2.Im)
};

!! Частное op1 и op2
#complex.div {
!! (делитель должен быть ненулевой)
assert ~op2.is_zero ();

double denom = op2.Re * op2.Re + op2.Im * op2.Im;
return # ((op1.Re * op2.Re + op1.Im * op2.Im) / denom,
- (op1.Re * op2.Im + op2.Re * op1.Im) / denom)
};

let g_sqrt = sqrt; !! (глобальная функция `sqrt`)

!! Квадратный корень из op1 (одно из значений)
#complex.sqrt {
double norm = complex.mod (op1);
return #(g_sqrt ((norm + op1.Re) / 2f), g_sqrt ((norm - op1.Re) / 2f))
};

!!
!! Операции для работы с complex
!!

!! унарный '-' как отрицание
opdef -complex = complex.neg ((<1>));
!! унарный '~' как сопряжение
opdef ~complex = complex.conj ((<1>));

!! бинарный '+' как сложение
opdef complex + complex = complex.add ((<1>), (<2>));
!! бинарный '-' как вычитание
opdef complex - complex = complex.sub ((<1>), (<2>));
!! бинарный '*' как умножение
opdef complex * complex = complex.mul ((<1>), (<2>));
!! бинарный '/' как деление
opdef complex / complex = complex.div ((<1>), (<2>));