Денис Гаев. Ксерион: язык и технология программирования
Версия от: 03.05.2002
Написать автору: mailto:dgaev@mail.ru
Аннотация
Настоящий документ представляет собой краткий неформальный обзор
основных возможностей языка программирования Ксерион (по состоянию на весну
2002 г.). Он не претендует на формальную строгость и полноту изложения и
оставляет за пределами рассмотрения многие тонкости и "темные места" языка.
Не рассмотрены стандартные языковые библиотеки, совершенно не затронуты
реализационные аспекты Ксерион-системы и многое другое. Тем не менее, он
представляется авторам вполне удовлетворительным в качестве начального
ознакомительного курса.
Предполагается, что читатель имеет представление о базовых принципах
объектно-ориентированного программирования, и в минимальной степени знаком с
каким-либо из современных ОО-языков (предпочтительно, C++ или Java).
Содержание
o Введение
o Лексический состав языка
o Примитивные типы и операции над ними
o Массивы, или векторные типы
o Указатели и ссылки
o Функционалы и функции
o Другие разновидности описаний
o Инструкции и поток управления
o Объекты и классы
o Определение операций
o Импорт и экспорт. Прагматы
o Перспективные возможности языка
o Заключение
Ксерион: язык и технология программирования
Если ксерион бросить в расплавленную медь, получится серебро. Если в
серебро, то -- золото.
Если в никель, то -- палладий. Если в палладий, то -- платина...
А. Лазарчук, М. Успенский
"Посмотри в глаза чудовищ"
Введение
Ксерион -- это современный, полнофункциональный
объектно-ориентированный язык программирования. При разработке языка
основными источниками идей послужили: C и C++, Паскаль (включая его
объектно-ориентированные диалекты, такие как Delphi) и Java. Помимо
перечисленных, в определенной степени на язык также оказали влияние
Algol-68, Simula, BCPL, CLU, Eiffel и некоторые другие менее известные
языки.
Ксерион является гибридным (или процедурно-объектным) языком
программирования, напоминая в этом отношении C++ и (в меньшей степени) Java.
Он не является "чистым" объектно-ориентированным языком, подобным SmallTalk
или Actor: в языке не существует понятий "метакласса", "методов классов" и
механизмов для динамического создания классов во время выполнения программы.
Большая часть атрибутов для объектов классов жестко задается во время
компиляции и не может быть изменена во время выполнения программы.
Ксерион -- строго типизованный язык. Это означает, что большая часть
проверок типов осуществляется во время компиляции, и лишь отдельные
специфические атрибуты объектной типизации могут проверяться при выполнении.
Система типизации языка основана на четком разделении, проводимым между
примитивными (простыми), производными и объектными типами данных.
Ксерион не является языком "сверхвысокого уровня", т.е. не содержит в
явном виде таких высокоуровневых структур данных, как списки, кортежи,
множества, ассоциативные массивы и т.п.. Однако, все перечисленные механизмы
могут быть эффективно реализованы средствами самого языка (и их реализация,
безусловно, будут предоставляться стандартными библиотеками).
Ксерион обладает рядом важных особенностей, специфичных для этого языка
или реализованных в нем лучше, чем во многих других языках программирования.
Для языка в целом характерны:
-- мощность и гибкость. В языке присутствуют практически все
возможности, характерные для традиционных процедурных языков, таких как C++
или Паскаль, без произвольных ограничений на их использование, характерных,
например, для Java. При этом многие из этих возможностей становятся намного
мощнее и потенциально ценнее. Например, в Ксерионе допустимо динамическое
определение размеров массивов, произвольные инициализаторы для массивов,
аргументов функций и компонент классов и многое другое.
-- иерархическому подходу к разработке и реализации программы во многом
содействует принятый в языке принцип локальности. Любую Ксерион-программу
можно рассматривать как иерархию вложенных друг в друга областей действия, а
любое описание (декларация) имеет локальный характер, т.е. действует только
в пределах самой внутренней из содержащих ее областей действия. Самой
внешней всегда является глобальная область действия, но ее использование
лучше максимально ограничить. Правильный подход к использованию принципа
локальности, предполагающий описание переменных, функций, типов данных,
классов и т.п. только там, где они нужны, является важным фактором улучшения
как надежности, так и эффективности программы.
-- вопросам эффективности при разработке языка уделялось особое
внимание. Так, контроль над такими принципиальными для эффективности
моментами, как распределение памяти, находится в руках разработчика.
Некоторые средства языка, такие, как специфические атрибуты указательных
типов static и strict, дают программисту явную возможность улучшать
эффективность программного кода за счет его общности. Не менее важно наличие
агрегатных операций присваивания и сравнения для векторных и объектных
типов.
-- надежность. Язык является надежным в том смысле, что на нем
невозможно написать "сбойный" код. Например, обеспечивается полная проверка
диапазона при обращениях к массивам, проверка валидности используемых
указателей на объекты данных и ссылок на функции, предусмотрен надежный
механизм преобразования между родственными объектными типами и т.п. Все
ошибки подобного рода, выявленные при выполнении программы, возбуждают
исключительные ситуации, которые могут быть перехвачены и корректно
обработаны.
-- последовательность и ясность. Предположительно, многие конструкции
языка имеют более последовательный и компактный синтаксис, чем их аналоги в
Паскале, Java и C++. Можно сказать, что для синтаксиса языка характерна
большая "ортогональность", чем для многих других языков. Если некая
конструкция языка синтаксически правильна, она почти всегда имеет какую-то
разумную семантику и может быть полезна в определенной ситуации. Кроме того,
язык минимизирует или полностью исключает необходимость в дублирующих
описаниях: каждый объект языка должен быть описан только один раз. Многие
часто употребляемые языковые конструкции могут быть записаны более
компактно. Активное использование макроопределений let также может
значительно "уплотнить" программу (возможно, в ущерб ее понятности).
-- переносимость реализации -- один из самых важных аспектов языка.
Результатом работы компилятора является внутренний код Ксерион-системы
(здесь не описанный). Этот код может быть выполнен в режиме интерпретации с
достаточно высокой эффективностью на любой 32-битовой платформе, но
ориентирован в основном на трансляцию в машинный код целевого процессора.
Лексический состав языка
На самом базовом уровне любая Ксерион-программа может рассматриваться
просто как поток лексем. Последние подразделяются на: ограничители,
разделители и знаки операций, ключевые слова, идентификаторы, литералы и
комментарии. В промежутках между лексемами могут присутствовать пробельные
символы (пробелы, концы строк и большинство управляющих символов),
игнорируемые при компиляции.
В том месте, где допустим пробельный символ, всегда может встретиться и
комментарий. Комментарии определяются двумя способами:
З как (непустая) последовательность символов, ограниченная двумя
символами !';
З как последовательность от двух символов !' до конца текущей строки.
В случае, когда применяются комментарии первого типа, рекомендуется
использовать внутри них пару дополнительных скобок
("()","[]","{}","<>" или что-нибудь в этом роде), чтобы начало и конец
комментария легко различались. Вот примеры:
!! допустимый комментарий
! и это тоже ... !
!{ но такая форма предпочтительней }!
Идентификаторы -- это символические имена, которые имеют все объекты (в
широком смысле) языка: переменные, константы, типы данных, функции, классы,
макроопределения, метки и пр. К идентификаторам предъявляются практически те
же требования, что и в C: это последовательности латинских букв, десятичных
цифр и знаков подчеркивания, начинающиеся не с цифры. Как минимум первые 127
символов идентификатора являются значащими; заглавные и строчные буквы
различаются. Помимо этого, идентификатор должен отличаться от ключевого
слова. Следующие идентификаторы являются ключевыми словами:
abstract, alloc, assert
bool, break
case, char, class, conceal, const, constructor, continue
destructor, do, double
else, enum, export
false, float, for
goto
if, import, inline, instate, int, interface
keyword
label, let, limited, long, loop
mediator
native, nil
opdef
package, pragma, private, protected
qual, quad
realloc, return
shared, short, static, strict, switch
tiny, this, true, type
u_int, u_long, u_short, u_tiny, unless, until
virtual, void
w_char, with, while
Помимо этого, законным идентификатором является последовательность
символов, заключенная в обратные кавычки. Внутри кавычек допустимы
произвольные символы, в т.ч. национальные, управляющие и пробельные и т.п.
Сами кавычки -- ограничители, а не часть идентификатора (alpha и `alpha` --
это один и тот же идентификатор).
!! примеры идентификаторов:
x;
ABACUS:
file_12;
ActiveApplet;
`Предельный допуск`
Для представления значений большинства примитивных типов в языке есть
литералы. Целые литералы по умолчанию представляют собой последовательности
десятичных цифр. Литералы могут быть не только десятичными: префикс $o'
указывает на восьмеричный литерал, $h' (или $x') -- на шестнадцатеричный,
$b' -- на двоичный. Все целые литералы могут также иметь суффикс, явно
задающий их тип (см. следующий раздел): 't' (для u_tiny), 's' (для u_short),
'i', (для u_int, по умолчанию), 'l' (для u_long). Литералы с плавающей
точкой определены как в C/C++, но также могут иметь явный суффикс типа: 'f'
(для float, по умолчанию), 'd' (для double), 'q' (для quad). Символьные
литералы (тип char) ограничены простыми кавычками. Строковые литералы (тип
char [], массив из символов) ограничены двойными кавычками. В отличие от
C/C++, они могут содержать физические управляющие символы (такие, как
перевод строки) и не завершаются автоматически нулевым байтом (последнее не
требуется, т.к. библиотечные средства языка определяют длину массивов по
другому). Вот примеры литералов (в скобках даны их типы):
37t; !! 37 (u_tiny)
37s; !! 37 (u_short)
$b100101; !! 37 (u_int)
$o45; !! то же, что и выше
$x25; !! то же, что и выше
3.14159; !! 3.14159 (float)
3.14159d; !! 3.14159 (double)
true; !! истина (bool)
false; !! ложь (bool)
'@'; !! символ @' (char)
"Это строка"; !! строка (char [])
"Это -- еще один пример строки,
которая займет несколько строк
при выводе" !! еще одна строка (char [])
Примитивные типы и операции над ними
Примитивные типы данных играют фундаментальную роль в системе типов
языка, поскольку они являются теми простейшими "кирпичиками", из которых
строится все остальное. Их можно разделить на числовые, символьные,
логический и пустой. В свою очередь, числовые типы представлены восемью
целочисленными и тремя "плавающими" типами. Целочисленные типы данных -- это
четыре вида значений, имеющих знак (tiny, short, int, long) и их беззнаковые
аналоги (u_tiny, u_short, u_int, u_long). "Знаковые" значения представляют
целые числа в дополнительном коде и различаются разрядностью: тип tiny
обеспечивает только 8 двоичных разрядов, short -- 16, int -- 32 и long --
64. Соответствующие им типы без знака имеют ту же разрядность, но
представляют только неотрицательные числа. Три типа представляют значения с
плавающей точкой (в соответствии со стандартом IEEE-754): float -- плавающее
со стандартной точностью, double -- с двойной точностью и quad
(зарезервирован на будущее, в настоящее время с точки зрения реализации
неотличим от double). Два простых типа предназначены для работы с символами:
тип char представляет 8-битовые символы набора ASCII/ISO, а тип w_char --
16-битовые набора Unicode. Логический (булевский) тип bool представляет лишь
два логических значения: истину (true) и ложь (false). В завершение упомянем
тип void (пустой), вообще не имеющий значений и предназначенный, в основном,
для описания функций-процедур, не возвращающих какого-либо результата.
Любая переменная в языке должна быть описана (декларирована) перед
использованием. Для простейших типов синтаксис деклараций прост (и, в
основном, C-подобен):
!! I, J, K -- беззнаковые целые переменные
u_int I = 1, J = 2, K = 3;
!! X, Y, Z -- плавающие переменные стандартной точности
float X, Y, Z = 0.001;
!! DONE -- логическая переменная
bool DONE = false
Из этих примеров также видно, что описание переменной может
сопровождаться ее инициализацией (и это рекомендуемая практика). Если
переменная примитивного типа не инициализирована явно, она будет содержать
неопределенное значение (мусор).
Наряду с обычными переменными, в языке присутствуют константы --
переменные, значение которых после описания не может быть изменено. Описание
константы предваряется ключевым словом const. Понятно, что константа
непременно должна быть инициализирована:
!! space -- символьная константа
char const space = ;
!! factor -- плавающая константа
float const factor = 2 * PI * PI;
!! median -- целая константа
int const median = (low + high) // 2
Наряду с константностью, важным атрибутом переменной является режим
размещения, указывающий каким именно образом переменная будет создана, и
сколько времени она просуществует. По умолчанию, режим размещения определяет
контекст описания: в зависимости от того места, где встретилось описание,
переменная может быть объявлена глобальной, локальной (в функции) или
компонентной (в классе). Однако, любая переменная или константа может быть
явно описана как статическая (static), т.е. имеющая время жизни, совпадающее
со временем выполнения программы:
u_int i, j, static counter = 0
Заметьте, что ключевое слово static -- атрибут описываемой переменной
(в данном случае, counter), а не описания в целом, как принято в C.
Для примитивных типов данных определено множество операций. Подробно
рассматривать систему операций мы не будем, так как она во многом
позаимствована из C. Отметим лишь наиболее существенные различия. Так, в
отличие от C, в Ксерионе различаются операции плавающего ('/') и
целочисленного ('//') деления (а взятие остатка от деления выполняется
операцией '-/'). Наряду с привычными для C-программиста операциями битовых
сдвигов вправо и влево ('<<' и '>>'), существуют также бинарные
операции битового вращения ('<.<' и '>.>'), и унарная операция
транспозиции ('>.<'). (Последнюю операцию можно описать как
"перестановку половинок": для значения типа u_tiny она меняет местами
старшие и младшие 4 бита, для u_short - старшие и младшие 8 битов и т.д.)
Разумеется, предусмотрены привычные для C-программиста операции инкремента
('++') и декремента ('-- ') в префиксной и постфиксной форме.
Все операции сравнения возвращают значение типа bool. Для всех типов
определены операции сравнения на равенство ('--') и неравенство
('<>'), а для многих типов данных определены также сравнения на
упорядоченность ('<', '<=', '>', '>='). В частности, все
примитивные типы являются упорядоченными. (Для числовых типов это
самоочевидно, символьные типы упорядочены в соответствии с внутренней
кодировкой, а для логических значений принято false < true). Кроме того,
для всех примитивных типов определены бинарные операции "максимум" ('?>')
и "минимум" ('?<'), возвращающие, соответственно, больший и меньший из
своих операндов.
Обычные бинарные арифметико-логические операции "и" ('&'), "или"
('|') и "исключающее или" ('~') применимы как к логическим, так и к целым
значениям (в последнем случае они выполняются побитно). Это же справедливо и
для унарной операции "не" ('~'). Только для типа bool определены
условно-логические операции "и" и "или" ('&&' и '||'), которые, как
и в C, по возможности избегают вычисления второго операнда. Есть и
C-подобная тернарная операция выбора: X ? Y : Z понимается как "если X
(выражение типа bool), то Y, иначе Z". Наконец, операция присваивания ('=')
как и в C возвращает присвоенное значение в качестве результата (она
определена не только для примитивных типов, но об этом позже). Есть также
операции присваивания, совмещенного с большинством бинарных операций, такие
как '+=', '-=', '*=', '/=' и т.п.
В отличие от C и Java, в языке отсутствует бинарная операция ','
(последовательность). Но вместо нее имеется более мощное средство: выражение
может дополняться встроенным блоком кода, выполняющимся до или после его
вычисления:
!! "встроенный блок", префиксная форма
({ STMT_LIST } EXPR);
!! "встроенный блок", постфиксная форма
(EXPR { STMT_LIST })
В обеих формах это выражение возвращает значение выражения EXPR.
Однако, при этом еще и выполняются инструкции из STMT_LIST -- до вычисления
EXPR (в префиксной форме) или после него (в постфиксной). К сожалению, блок
инструкций не может напрямую вернуть значение, которое можно было бы
использовать в выражении.
Система операций языка всем перечисленным не ограничивается, но
операции, определенные для производных и объектных типов мы рассмотрим чуть
позже. Наконец, в языке имеются бинарные операции ввода ('<:') и вывода
(':>'), которые необычны тем, что вообще не имеют никакой
предопределенной семантики, и предназначены исключительно для
переопределения (например, для операций ввода-вывода в системных
библиотеках).
Система приоритетов несколько отличается от принятой в C. Скажем,
операции сдвигов и вращения считаются мультипликативными, т.е. имеют тот же
приоритетный уровень, что и умножение и деление. Приоритет логических и
условно-логических операций одинаков (и более низок, чем у сравнений). Все
бинарные операции, кроме операций присваивания, имеют левую ассоциативность.
Значения примитивных типов могут неявно преобразовываться друг в друга,
но правила этих преобразований приняты более жесткие, нежели в C. Допустимы
лишь те преобразования, которые не приводят к потере информации. Так,
младшие целочисленные типы могут обобщаться до старших (tiny → short
→ int → long), так же, как и все плавающие (float → double
→ quad) и все символьные (char → w_char), а целочисленные
значения неявно обобщаются до плавающих. Другие неявные преобразования
запрещены: в частности, нельзя неявно использовать символьные и логические
значения в качестве целых (и наоборот). Большинство операций также не
позволяют смешивать целые операнды со знаком и без знака: они должны быть
приведены к единой знаковости во избежание возможной неоднозначности.
Когда неявные преобразования не работают, можно прибегнуть к операции
явного приведения типов, имеющей такой вид:
:TYPE EXPR !! преобразовать выражение EXPR к типу TYPE
Семантика подобного преобразования также не таит в себе особых
сюрпризов: плавающие значения преобразуются в целые путем отбрасывания
дробной части, символьные в числовые -- в соответствии со своей кодировкой,
а логические значения false и true считаются эквивалентными 0 и -1. Очень
важно заметить, что эта операция преобразования определена только для
примитивных типов, и к производным, в отличие от C, она неприменима.
Массивы (векторные типы)
Массивы -- однородные наборы значений единого типа, обеспечивающие
произвольный доступ к любому из этих значений (элементов) по целочисленному
индексу -- это одно из принципиально важных средств языка. В отличие от Java
и многих других языков, массивы не являются объектами в смысле ООП. Они
могут иметь те же свойства и атрибуты (режим размещения, константность и
пр.), что и переменные примитивных типов.
Вот примеры описаний массивов:
!! intvec -- массив из LENGTH целых
int [LENGTH] intvec;
!! text -- матрица символов, (HEIGHT строк) * (WIDTH столбцов)
char [WIDTH][HEIGHT] text
Обратите внимание на то, что синтаксис описания массивов -- префиксный:
конструкция вида [SIZE] называется префиксом описания (декларатором)
массива. Она означает, что тип декларируемых далее объектов меняется с TYPE
на TYPE [SIZE] (массив из SIZE элементов типа TYPE). Вкладывая векторные
деклараторы друг в друга, можно описывать двух- и более мерные массивы.
Строго говоря, понятие "многомерный массив" в языке отсутствует -- их с
успехом заменяют массивы, состоящие из массивов, и так далее. Именно это мы
будем подразумевать, говоря об n-мерных массивах (при этом число n мы будем
называть размерностью, или рангом массива). Однако, никакими специальными
свойствами многомерные массивы не обладают (т.е. семантика всех операций над
ними выводится из семантики операций над одномерными массивами).
Заметим, что префиксный синтаксис в описаниях массивов -- это не
исключение. Все производные типы языка вводятся с помощью аналогичных
префиксных конструкций, благодаря чему даже самые сложные и запутанные
описания читаются достаточно легко и единообразно -- справа налево (от
переменной или другого описываемого объекта к "корню" описания). Как и в C,
префикс(ы) описаний имеют более высокий приоритет, чем запятая, разделяющая
декларации в списке:
int [10] aa, bb !! aa -- массив из 10 целых, bb -- целое
Однако, часто необходимо описать несколько массивов одинаковой
размерности. Тогда префикс массива (как и любой общий префикс производного
типа) можно "вынести за скобки" (фигурные). Этот прием, называемый
факторизацией, очень упрощает сложные описания:
int [10] { aa, bb } !! aa и bb -- массивы из 10 целых
Факторизацию можно применять и рекурсивно:
int i, [10] { v, [20] { vv, [30] vvv } };
!! более громоздкая форма предыдущего описания:
int i, [10] v, [10][20] vv, [10][20][30] vvv
В качестве размера массива требуется некое выражение типа u_int. На
него не накладывается других ограничений -- в частности, не требуется, чтобы
оно было вычисляемым во время компиляции. В общем случае размер массива
определяется только при выполнении программы. Однако, он фиксирован в том
смысле, что вычисляется один раз, после чего уже не может измениться (в
языке нет настоящих гибких массивов, размеры которых можно менять "на
лету"). В жесткой системе типов языка размеры массивов рассматриваются как
особый случай: все, что связано с ними, обычно проверяется только при
выполнении программы. Массив может даже оказаться пустым, т.к. нулевой
размер не считается ошибкой.
Для массивов определен ряд операций. Так, поскольку размер массива
всегда известен компилятору и исполняющей системе языка, его нетрудно узнать
с помощью унарной постфиксной операции '#'. Для переменных, описанных выше:
intvec#; !! возвращает значение LENGTH (u_int)
text#; !! возвращает значение HEIGHT (u_int)
vvv# !! возвращает 30 (u_int)
Часто работа с массивом осуществляется поэлементно. Бинарная операция
индексирования позволяет в любой момент получить доступ к любому элементу
массива. Так же, как и в C, отсчет индексов ведется с нуля:
!! первый элемент массива intvec (int)
intvec [0];
!! последний элемент массива intvec (int)
intvec [LENGTH - 1];
!! "верхняя" строка матрицы text (char [WIDTH])
text [0];
!! "нижняя" строка матрицы text (char [WIDTH])
text [HEIGHT - 1];
!! "левый верхний" символ матрицы text (char)
text [0][0];
!! "правый нижний" символ матрицы text (char)
text [HEIGHT - 1][WIDTH - 1]
Операция индексирования всегда проверяет корректность индекса, не
позволяя обратиться к несуществующему элементу. Если при вычислении A [I] не
соблюдается условие I < A#, нормальное выполнение программы прервется и
будет возбуждена исключительная ситуация (ArraySizeException). Заметим
также, что хотя при описании массива мы использовали префиксный синтаксис,
для доступа к элементу используется привычная постфиксная нотация.
(Известный по языку C принцип "декларация имитирует использование" в
Ксерионе верен "с точностью до наоборот": описатели для массивов, указателей
и функционалов используют префиксный синтаксис, но соответствующие операции
над этими типами (индексирование, разыменование, вызов функции) -- только
постфиксный).
Возможен не только поэлементный доступ к массивам: в языке определен
ряд агрегатных операций, позволяющих работать с массивами, как с единым
целым. Но прежде заметим, что там, где можно работать с массивом,
допускается работа и с любым его непрерывным фрагментом (отрезком).
Тернарная операция взятия отрезка -- A [FROM..TO] -- возвращает отрезок
массива A от (включительно) элемента с индексом FROM до (не включая)
элемента с индексом TO (т.е. справедливо тождество: A [FROM..TO]# -- TO --
FROM). Разумеется, корректность индексов проверяется (если нарушено условие
FROM <= TO && TO <= A#, возбуждается знакомое нам исключение
ArraySizeException). Впрочем, отрезок нулевой длины допустим, также как и
массив.
V [0 .. N] !! отрезок: начальные N элементов массива V
V [V#-N .. V#] !! отрезок: конечные N элементов массива V
В отличие от индексирования, операция получения отрезка никогда не
понижает ранг массива: результат всегда имеет ту же размерность, что и
операнд. Отрезок длиной 1 -- это массив длины 1, а не один элемент.
Вследствие этого, при работе с многомерным массивом можно получить отрезок
только по самому внешнему измерению, т.к. все внутренние для этой операции
недоступны. Наконец, отметим, что операции взятия индекса и отрезка
сохраняют такие особенности своего операнда, как константность и L-контекст
(т.е. если массив константен, то любой его элемент также является константой
и т.п.).
Завершая разговор об индексировании массивов, следует упомянуть особую
операцию "пустой индекс". Она полезна в основном для получения внутренних
размеров многомерных массивов:
text [0]# !! возвращает WIDTH
text []# !! то же самое
Вторая запись немного короче, а главное -- явно подчеркивает, что
операция индексирования здесь носит фиктивный характер, т.к. нам нужен не
определенный элемент массива text, а лишь доступ к общему типу его
элементов. Результат, выдаваемый операцией [] -- т.н. неопределенное
выражение, имеющее тип, но не значение. Подробнее о семантике неопределенных
выражений, и случаях, когда они могут потребоваться, мы поговорим позже.
Для массивов, как и для примитивных типов, доступно присваивание:
float [25] { VA, VB };
VA = VB !! скопировать все элементы из массива VA в массив VB
Для присваивания массивов требуется, чтобы типы их элементов точно
совпадали (т.к. неявные приведения, доступные для примитивных типов, не
обобщаются на массивы из них). Помимо этого, должны совпадать и размеры
присваиваемых массивов (по всем измерениям, если они многомерные). Заметьте,
что в приведенном случае их совпадение очевидно, и потому проверка периода
выполнения будет опущена. Однако, вот пример более общей ситуации:
char [SIZE1] str1, [SIZE2] str2;
str1 = str2
Здесь перед присваиванием произойдет проверка условия SIZE1 -- SIZE2,
и, если оно окажется ложным, будет возбуждено все то же исключение
ArraySizeException.
Не менее важно то, что массиву можно присвоить скаляр. В этом случае
его значение (вычисленное один раз) будет "размножено" и присвоено всем
элементам массива. Этот прием называется векторизацией и обобщается на
многомерные массивы: массиву может быть присвоен массив меньшего ранга --
при этом он "размножается" по одному или большему числу измерений. Как и при
обычном присваивании, требуется идентичность базовых типов массивов, а все
"внутренние" размеры обязательно будут проверены на равенство:
!{ Присваивает str1 всем HEIGHT строкам матрицы text
(предварительно убедившись, что text []# -- str1#,
т.е. WIDTH -- SIZE1) }!
text = str1
Порядок присваивания элементов в массиве считается неопределенным.
Часто это действительно не принципиально, однако при присваивании
перекрывающихся отрезков одного и того же массива он оказывается
существенным. Поэтому существуют две специальные формы операции
присваивания: инкрементная ('=#') и декрементная ('=#@') (они определены
только для массивов):
A [10..19] =# A [15..24]; !! инкрементное присваивание
A [10..19] =#@ A [15..24] !! декрементное присваивание
Здесь операндами являются два перекрывающихся отрезка массива A. В
первом случае присваивание будет осуществляться от первого элемента к
последнему, т.е. будет неразрушающим и все элементы "уцелеют" при
копировании. Во втором случае, копирование произойдет в обратном порядке,
при этом отрезок частично перезапишет сам себя. Это не обязательно ошибка.
Например, если необходимо "размножить" небольшой отрезок на всю длину
массива, присваивание с одновременной "автоматической" перезаписью является
вполне допустимым (и весьма эффективным) техническим приемом.
Как и переменные примитивных типов, массивы могут быть (а константные
-- и должны быть) инициализированы. Конечно, все, что может быть присвоено
массиву, является и законным инициализатором для него. Однако, помимо этого,
допускается еще одна форма инициализации массива -- списковая.
int [5] List1 = { 1, 2, 3, 4, 5 };
int [5] List2 = { 1, List1[2]*3, List1[0]*List1[4] + 2, 4, List1# }
Как легко видеть из второго примера, инициализаторы -- любые выражения,
соответствующие типу элементов массива. Они вычисляются только при
выполнении инициализации. Столь же гибкий подход допустим и при
инициализации многомерных массивов. Вот вполне законный, хотя и несколько
надуманный пример:
int [3][5] Matrix = {
!! строка #0: зададим списком
{ 100, 200, 300 },
!! строка #1: возьмем из List1
List1 [0..3],
!! строка #2: зададим списком
{ List1[0]*List2[2], List1[1]*List2[1], List1[2]*List2[0] },
!! строка #3: возьмем из List2
List2 [2..5],
!! строка #4: векторизуем 100 на 3 элемента
100
}
Списковые инициализаторы массивов -- пример т.н. инициализирующих
выражений, определенных и для некоторых других типов. Их можно использовать
только в контексте инициализации для переменной данного типа, т.е.
использовать список элементов, скажем, как присваиваемое значение, нельзя:
List1 = { 10, 20, 30, 40, 50 } !! ошибка!
В языке имеется не только агрегатное присваивание, но и агрегатное
сравнение. Для того, чтобы два массива были сравнимыми, требуется, как и при
присваивании, точное совпадение их базовых типов. Однако, различие в
размерах при сравнении не считается фатальной ошибкой. Проще всего описать
семантику сравнений на равенство/неравенство: два массива считаются равными,
если равны их размеры и соответствующие элементы попарно; в противном случае
они не равны:
str1 -- str2; !! истинно, если str1# -- str2#
!! И str1 [I] -- str2 [I] для любого I
str1 <> str2 !! в противном случае
Если же базовый тип массивов упорядочен (например, является примитивным
типом), допустимо также сравнение массивов на упорядоченность. При этом
семантика сравнения определена аналогично лексикографическому ("словарному")
сравнению символьных строк. Вот строгое определение операций "больше" и
"меньше" для массивов:
str1 < str2; !! истинно, если существует такое N, что
!! 1) str1 [0..N] -- str2 [0..N]
!! 2) str1#
-- N && str2# > N
!! ИЛИ ЖЕ
!! str1 [N]
< str2 [N]
str1 > str2 !! истинно, если существует такое N, что
!! 1) str1 [0..N] -- str2 [0..N]
!! 2) str1# > N &&
str2# -- N
!! ИЛИ ЖЕ
!! str1 [N]
> str2 [N]
Другими словами: массив str1 меньше [больше] массива str2, если первый
отличающийся элемент массива str1 меньше [больше] соответствующего элемента
массива str2, или же если все элементы str1 равны элементам str2, а длина
str1 меньше длины str2 [... все элементы str2 равны элементам str1, а длина
str2 меньше длины str1].
Правила сравнения массивов рекурсивно обобщаются на массивы более
высоких размерностей. Если один из операндов сравнения имеет меньший ранг,
чем другой, он неявно подвергается векторизации по всем "недостающим"
внешним измерениям. Продемонстрируем все это на примерах:
str1 -- !! истинно, если все символы str1 -- пробелы
str1 <> !! истинно, если хотя бы один символ str1 отличен от
пробела
str1 -- text !! истинно, если str1# -- text []#
!! И все строки text совпадают с str1
str1 <> text !! истинно, если str1# <> text []#
!! ИЛИ хотя бы одна строка text отлична от str1
Возможность сравнения массивов, безусловно, ценна, но не менее важно
знать, в каком именно месте они различаются. Для этого предусмотрены
операции сканирующего сравнения (сканирования). Для каждой из операций
простого сравнения ('--', '<>', '<', '>' ...) имеется
соответствующая операция инкрементного ('--#', '<>#', '<#', '>#'
...) и декрементного ('--#@', '<>#@', '<#@', '>#@' ...)
сканирования. Во многом они подобны соответствующим им операциям сравнения,
в частности, они предъявляют абсолютно те же требования к типам операндов и
выполняются практически таким же образом. Главное отличие -- возвращаемое
ими значение имеет не тип bool, а тип u_int -- и означает оно,
соответственно не истинность/ложность операции сравнения в целом, а число
элементов массива (начальных для инкрементных операций, конечных -- для
декрементных), для которых соответствующее условие удовлетворяется. Так, для
сканирования на равенство:
!! в инкрементной форме:
VAL -- A --# B; !! означает, что:
!! A [0..VAL] -- B [0..VAL]
!! И
!! A [VAL] <> B [VAL]
!! (если они существуют).
!! в декрементной форме:
VAL -- A --#@ B; !! означает, что:
!! A [A#-VAL..A#] -- B [B#-VAL..B#]
!! И
!! A [A#-VAL-1] <> B [B#-VAL-1]
!! (если они существуют).
Как и при сравнении, операнды сканирования могут подвергаться
векторизации. Таким образом, сканирование можно использовать и в качестве
операции поиска элемента в массиве:
!! найти первый пробел в массиве str1:
if (first_count = str1 <># ) -- str1#
{ !( пробелы не найдены ... )! }
else { !( str1 [first_count] -- первый пробел )! }
!! найти последний пробел в массиве str1:
if (last_count = str1 <>#@ ) -- str1#
{ !( пробелы не найдены ... )! }
else { !( str1 [str# - last_count - 1] -- последний пробел )! }
Резюмируя заметим, что система векторных операций языка может поначалу
показаться довольно сложной. Тем не менее, возможность относительно
компактной записи довольно сложных операций над массивами слишком ценна,
чтобы ею пренебрегать. Кроме того, все агрегатные операции реализованы
максимально эффективно, и их использование может дать весьма существенный
выигрыш, особенно в библиотеках и других системно-значимых компонентах.
Указательные и ссылочные типы
Реализация нетривиальных структур данных, таких, как линейные и
кольцевые списки, деревья, графы и сети была бы практически нереальна без
указателей. В том или ином виде такой механизм предусмотрен в любом языке.
Даже в Java, где декларирован отказ от указателей, эта концепция неявно
присутствует, т.к. все массивы и объекты доступны только через ссылки. В
Ксерионе подход является более традиционным: как и в C и Паскале, доступны
указатели на переменные любых типов. Правда, в отличие от C, в использование
указателей внесен ряд ограничений, продиктованных соображениями
безопасности.
Все указательные типы данных вводятся с помощью префиксного описателя
'^'. Например:
int ^ip; !! ip - указатель на целое
int ^^ipp !! ipp - указатель на указатель на целое
Эти два описания легко объединить с помощью факторизации:
int ^{ ip, ^ ipp } !! то же, что и выше
Префикс '^' может предваряться ключевыми словами const, limited и
strict, смысл которых мы рассмотрим чуть позже. Для всех указательных типов
определен единственный литерал -- nil, означающий отсутствие ссылочного
значения.
С указателями прямо связаны две операции: именование и разыменование.
Так, L-выражение любого типа легко превратить в указатель на этот тип с
помощью операции именования (постфикс '@'):
int a; double b;
a@; !! указатель на переменную a (int ^)
b@ !! указатель на переменную b (float ^)
Обратная операция -- разыменование (постфикс '^') -- позволяет перейти
от указателя к переменной (константе), на которую он указывает (результат
этой операции -- L-выражение). Понятно, что попытка разыменования значения
nil вызовет ошибку периода выполнения (NilDerefException).
ip^; !! разыменовать ip (int)
ipp^; !! разыменовать ipp (int ^)
ipp^^ !! разыменовать ipp дважды (int)
Традиционно указатели считаются довольно опасным языковым механизмом.
По этой причине в Ксерионе имеется ряд ограничений на их использование.
Прежде всего, в отличие от примитивных типов, для указательных типов
действует принудительная инициализация: если указательная переменная