Страница:
параметра (который может быть заполнен позже):
z_func (3.14, , false, false, y: 8.9)
!! (x ← 3.14, y ← 8.9, p ← false, q ← false)
При неосторожном сочетании всех этих приемов вполне может оказаться
так, что при вызове функции параметр оставлен без значения, или же
инициализирован два (или более) раза. Второе является безусловной ошибкой, а
вот первое может считаться допустимым. Дело в том, что к параметрам функции,
как и к любым переменным, может быть применена инициализация по умолчанию.
Любой явно заданный аргумент "вытесняет" неявное значение параметра.
Аналогичная возможность имеется и в C++, но там инициализация по умолчанию
может относиться лишь к последним аргументам в списке, а инициализаторами
обязаны быть литеральные значения. В Ксерионе оба этих ограничения
отсутствуют. Более того, один неочевидный (но весьма полезный) аспект
описаний состоит в том, что инициализатор для параметра может содержать
другие параметры, описания которых предшествуют ему. Применение этого метода
лучше показать на примере:
!! Заметьте, что здесь три описания нельзя объединить
void (int a = 5; int b = a; int c = a + b) x_func;
x_func (11, 12, 13); !! все аргументы задано явно
!! (a ← 11, b ← 12, c ← 13)
x_func (10, 20); !! a и b заданы, c по умолчанию
!! (a ← 10, b ← 20, c ← 30)
x_func (10); !! a задано, b и c по умолчанию
!! (a ← 10, b ← 10, c ← 20)
x_func (); !! все по умолчанию
!! (a ← 5, b ← 5, c ← 10)
Даже в качестве размеров параметров-массивов могут использоваться
выражения, содержащие ранее декларированные параметры. Это тоже может
оказаться полезным:
!! матричное произведение: C = A (*) B
void (u_int L, M, N; double [L][M] @A, [M][N] @B, [L][N] @C) MatrixProduct {
! ... ! }
Семантика передачи аргументов -- это всегда семантика инициализации,
т.е. допустимы не только простые выражения, но и любые инициализаторы,
подходящие по типу. То же относится к значению, возвращаемому инструкцией
return. Заметим, что параметры-массивы (в отличие от C, C++ и Java) также
передаются (и возвращаются) по значению, что может быть весьма дорогим
удовольствием. Как правило, массивы лучше передавать через указатель или
ссылку, а передачу по значению использовать лишь в тех случаях, когда это
действительно оправдано. Помимо своих параметров, функции доступна вся
внешняя среда -- т.е. все переменные и константы (независимо от режима их
размещения) и прочие виды описаний, доступные в точке, где дано описание
функции.
В языке не существует перегруженных (overloaded) функций, подобных
имеющимся в C++. Имя каждой функции в своей области действия должно быть
уникально (как и для любого другого субъекта описания).
В заключение отметим, что функциональный тип допускает отдельную форму
инициализатора, прямо задающего тело безымянной функции. (Некоторые языки
программирования называют подобное "лямбда-нотацией"). Неявный инициализатор
имеет вид #' <тело функции>. Имена и типы параметров и возвращаемого
значения явно не задаются, а определяются автоматически, исходя из контекста
инициализации. Например:
int (float a, b, c) t_func = #{ return :int (a * b * c) };
t_func (2, 3, 4) !! 24 (int)
Дополнительные разновидности описаний
Чтобы завершить разговор об описаниях, мы рассмотрим некоторые
специальные декларативные конструкции. Все они имеют скорее вспомогательное,
чем принципиальное значение, но все-таки они полезны при создании реальных
программ.
Прежде всего, в Ксерионе имеется свой аналог описания typedef в C,
позволяющий вводить новые типы. Однако, это не самостоятельная конструкция,
а лишь еще один вид терма описания (type <имя типа>), который, как
всегда, может совмещаться с другими термами. Например:
!! flt -- синоним float,
!! pflt -- указатель на float
!! ppflt -- указатель на указатель на float
float type flt, ^ type pflt, ^^ type ppflt
Ключевое слово type слишком громоздко, поэтому его можно сократить до
символа %' (что обычно на практике и делается). Для того, чтобы
использовать новоопределенный тип в качестве корня описания, он тоже должен
предваряться словом type (или символом %'):
%flt x, y, z; !! т.е. float x, y, z
%pflt p1, p2; !! т.е. float ^ {p1, p2}
%ppft pp1, pp2, pp3 !! т.е. float ^^ {pp1, pp2, pp3}
С точки зрения семантики подобная запись -- не более, чем средство
сократить длинные описания. В отличие от объектных типов, никакими
принципиально новыми свойствами тип, введенный через описание type, обладать
не будет.
Приведенные выше описания -- это частный случай более общего подхода,
позволяющего использовать в качестве корня описания не только определенный
программистом тип, но и произвольное выражение, имеющее смысл. Вот несколько
тривиальных примеров:
%(2 * 2) xx, yy, zz; !! т.е. u_int xx, yy, zz
%(10 < 20) pp, qq; !! т.е. bool pp, qq
%("text" []) cc !! т.е. char cc
Выражение в корне описания (если это не просто идентификатор, оно
должно быть заключено в скобки) вычисляется, но его значение игнорируется, и
в качестве базы описания используется только его тип. Наконец, отметим, что
имена определенных пользователем (но не встроенных!) типов -- это также
законные (но неопределенные) выражения. Все это открывает возможности для
многих полезных трюков. Так, использование имен производных типов в
выражениях (и выражений -- в корнях описаний) дает простой механизм
типизационной декомпозиции, т.е. перехода от производных типов к их базовым.
Вот пример того, как это можно использовать на практике:
!! если v_type -- векторный тип:
%(v_type []) %v_type_elem; !! v_type_elem -- это тип элементов v_type
!! если p_type -- указательный тип:
%(p_type ^) %p_type_ref; !! p_type_ref -- это тип,
!!
получаемый разыменованием p_type
!! если f_type -- функциональный тип:
%(f_type ()) %f_type_result !! f_type_result -- это тип значения,
!!
возвращаемого f_type при вызове
Существует еще одна важная форма описаний -- это макроопределения
(let-определения). В основном, они применимы для тех же целей, что и
определения #define в C/C++, т.е. как макроподстановки повторяющихся
фрагментов исходного кода программы. Но не менее важны и различия. Если
средства C-препроцессора -- это надстройка над языком, то let-определения --
это часть языка Ксерион, а объектом let-подстановки может быть не всякая
строка символов -- это должно быть законное выражение языка. Общий синтаксис
макроопределения имеет такой вид:
let NAME1 =' EXPR1 ( ,' NAME2 =' EXPR2) ...
Это определение делает все идентификаторы NAME# синонимами для
соответствующих выражений EXPR#. Как и прочие виды определений,
макроопределения локальны для содержащего их блока или области действия.
Важно также то, что выражение EXPR должно быть корректно не только
синтаксически, но и семантически: в частности, все идентификаторы,
упомянутые в EXPR, должны иметь смысл. В целом механизм макроопределений
обеспечивает не только текстуальную, но и семантическую подстановку: все
имена будут иметь в точке обращения к макро тот же смысл, который они имели
в точке его определения. Например:
int value; !! целая переменная
let v1 = value; !! v1 -- синоним value
{ float value; !! переопределение value в подблоке
value; !! (float value)
v1 !! (а это -- int value)
}
Наконец, если EXPR является L-выражением, то NAME -- также L-выражение.
Механизм макроопределений является довольно мощным средством, используемым
для самых разных целей: от определения символических литералов (в отличие от
констант-переменных, для них не требуется дополнительная память) до простого
сокращения слишком длинных идентификаторов переменных, функций, типов и
классов:
%err_no (%string FileName) #SystemOpenFile;
let SysOpen = SystemOpenFile !! сокращение
В завершение рассмотрим описание conceal -- механизм "скрытия" имен.
Если идентификатор, определенный в некой внешней области действия (например,
глобальный) необходимо сделать недоступным в некой внутренней (и всех
областях, вложенных в нее), этого легко добиться с помощью специального
описателя conceal:
conceal NAME ( ,' NAME1) ...
Описатель conceal делает все перечисленные в нем имена локально
недоступными (от описателя до конца внутренней области действия, содержащей
его). В сущности, описание conceal NAME работает примерно как let
NAME=<nothing>. В основном, механизм conceal предназначен для работы с
объектами и иерархиями классов (например, скрытия каких-нибудь атрибутов
базового класса в производных классах), что, конечно, не означает, что его
нельзя использовать для других целей.
Инструкции и поток управления
Собственно программа состоит в основном из операторов или инструкций
языка (последний термин кажется нам предпочтительным, поэтому им мы и будем
пользоваться). Простейшие виды инструкций мы уже рассмотрели. Так, все виды
описаний являются законными инструкциями, допустимыми в любом месте
программы. Любое выражение -- это также инструкция (возвращаемое значение,
если оно есть, игнорируется). В языке предусмотрен такой механизм
группировки инструкций, как блок, т.е. последовательность инструкций,
разделенных точками с запятой ( ;') и заключенная в фигурные скобки ("{}").
Блок рассматривается как единая инструкция и является областью локализации
для всех содержащихся в нем описаний. Заметьте, что в этом отношении язык
следует традициям Паскаля: тоска с запятой -- это синтаксический разделитель
инструкций (но ни одна инструкция не завершается этим символом). Во многих
случаях избыточная точка с запятой не считается ошибкой, т.к. в языке
определена пустая инструкция, не содержащая ни одного символа (и, очевидно,
не выполняющая никаких действий). Любая инструкция может быть помечена
меткой вида LABEL :', что позволяет инструкциям break, continue и goto на
нее ссылаться. Рассмотрим другие виды инструкций.
Инструкция утверждения (assert) имеет вид:
assert CND
Семантика ее проста: вычисляется CND (выражение типа bool). Если оно
истинно, ничего не происходит, в противном случае возбуждается
исключительная ситуация AssertException. Эта инструкция нужна в основном для
"отлова" логических ошибок в процессе отладки программы.
Конечно же, имеется условная инструкция (if/unless), имеющая следующий
вид:
(if P_CND | unless N_CND) BLOCK
[else E_STMT]
Если (для if-формы) выражение P_CND истинно или (для unless-формы)
выражение N_CND ложно, выполняется блок BLOCK. В противном случае, если
присутствует необязательная часть else, будет выполнена инструкция E_STMT.
Заметим, что тело условной инструкции -- это всегда блок, ограниченный
фигурными скобками (что снимает проблему неоднозначности "висящего else").
Однако, круглые скобки вокруг условия (как в C) не требуются (хотя, конечно,
ничему и не помешают). В части else допустима произвольная инструкция
(например, другой if/unless). Очевидно, что формы if и unless полностью
взаимозаменяемы, и какую из них использовать -- вопрос конкретного случая.
В отличие от большинства языков, в Ксерионе имеется только одна (зато
довольно мощная) инструкция цикла. Вот ее самый общий синтаксис:
[for I_EXPR]
(while P_CND_PRE | until N_CND_PRE | loop)
[do R_EXPR]
BLOCK
[while P_CND_POST | until N_CND_POST]
Хотя она выглядит довольно громоздкой, большая часть ее компонент
необязательна. Необязательная часть for задает инициализатор цикла --
выражение I_EXPR, которое всегда вычисляется один раз перед самым началом
работы цикла. Далее всегда следует заголовок цикла, задающей его
предусловие, проверяемое перед каждой итерацией цикла. Если (в форме while)
P_CND_PRE ложно или (в форме until) N_CND_PRE истинно, цикл завершит свою
работу. Если же заголовок цикла сводится к loop, предусловие отсутствует.
Телом цикла является блок BLOCK, обычно выполняющий основную работу.
Необязательная часть do задает поститерацию цикла: выражение R_STMT будет
вычисляться на каждой итерации после тела цикла. Наконец, цикл может иметь и
постусловие: если (в форме while) P_CND_POST ложно или (в форме until)
N_CND_POST истинно, цикл также завершится. Какую из двух форм использовать
для пред- и постусловия -- это, опять-таки, вопрос предпочтения. Предусловие
и постусловие могут присутствовать одновременно -- в этом случае, цикл
прерывается, когда перестает соблюдаться хотя бы одно из них. Наконец
заметим, что вместо выражения I_EXPR может быть дано любое описание, и при
этом цикл становится областью локализации для него (т.е. как бы неявно
заключается в блок). Элементы for и do логически избыточны -- они нужны
только для того, чтобы можно было ради наглядности собрать в заголовке всю
логику управления циклом. Так, если нужен цикл с переменной i, меняющей
значение от (включая) 0 до (исключая) N; это обычно записывается так:
for u_int i = 0 while i < N do ++ i { !( тело цикла )! }
Нередко необходимо прервать выполнение цикла где-нибудь посередине. Для
этого удобно использовать инструкцию прерывания break:
break [LABEL]
Она прерывает выполнение содержащего ее цикла, помеченного меткой LABEL
(равно как и всех вложенных в него циклов, если они есть). Если элемент
LABEL опущен, прерывается самый внутренний из циклов, содержащих инструкцию
break. Инструкция продолжения continue:
continue [LABEL]
вызовет прерывание текущей итерации цикла LABEL (или, если метка
опущена, самого вложенного цикла) и переход к его следующей итерации
(включая выполнение поститерации и проверку постусловия, если они есть).
В завершение упомянем об инструкции перехода goto:
goto [LABEL]
передающей управление инструкции, помеченной меткой LABEL. О вредности
подобных инструкций классики структурного программирования написали столько,
что нет смысла их повторять. Инструкция goto в языке есть, а использовать ли
ее в программе -- дело вашей совести и личных предпочтений.
Для завершения работы функции применяется уже знакомая нам инструкция
return:
return [EXPR]
Она допустима только в определении функции и обеспечивает выход из нее
с возвратом значения EXPR (подходящего типа). Выражение EXPR опускается,
если тип функции -- void.
Наконец, в языке имеется инструкция with, тесно связанная с объектами и
потому рассмотренная в следующем разделе.
Объекты и классы
Ксерион -- это объектно-ориентированный язык. В нем присутствует
концепция объекта -- ключевого механизма абстракции данных, обеспечивающего
для них инкапсуляцию, наследование и полиморфизм.
Каждый объект языка относится к одному из классов, определяющих
специфичные для него свойства и атрибуты. Самый общий синтаксис описания
класса таков:
class CLASS_NAME [ :' SUPERCLASS_NAME]
{
CLASS_DECLS
}
[instate INSTATE_LIST]
[destructor DESTRUCTOR_BODY]
Рассмотрим все элементы описания по порядку. Прежде всего, каждый класс
обязан иметь уникальное в своей области действия имя (CLASS_NAME). Класс
может быть либо корневым, либо же производным от уже определенного
суперкласса (класса SUPERCLASS_NAME). Далее следует заключенное в фигурные
скобки тело описания класса, представляющее собой список CLASS_DECLS. Его
элементами могут быть практически все виды описаний языка (включая и
некоторые другие, рассмотренные ниже). В большинстве случаев в описании
класса присутствуют переменные, константы и функции.
Любая переменная, описание которой содержится в декларации класса, по
умолчанию считается его компонентой. Это значит, что для каждого объекта
класса существует собственная копия этой переменной. Если же переменная
имеет явно специфицированный режим размещения static или shared, она
является переменной класса, т.е., в отличие от его компонент, существует в
единственном экземпляре, вне зависимости от того, сколько объектов данного
класса было создано. Разница между режимами static и shared состоит в том,
что static-переменные существуют глобально (время их существования совпадает
со временем выполнения программы), а для shared область действия, равно как
и время существования, определяются декларацией класса.
В декларации класса могут присутствовать вложенные блоки личных
(private) и защищенных (protected) описаний. Как и в C++, имена всех
объектов, декларированных в private-блоке, доступны только внутри декларации
класса, а в protected-блоке -- также и внутри деклараций всех его
подклассов. Все прочие декларации являются публичными, т.е. доступными извне
без каких-либо ограничений.
Синтаксически описание класса играет роль корня описания. Заметим, что
после того, как класс декларирован, для ссылок на него (как и на все прочие
производные типы) используется ключевое слово type или %' (а не class).
В семантике объектов уникальным (и весьма важным) является понятие
текущего экземпляра объекта. Для каждого класса определен один и только один
текущий экземпляр. Его можно рассматривать как неявную переменную класса с
типом CLASS_NAME^ и режимом размещения shared, инициализируемую, как и все
указатели, значением nil. В процессе выполнения программы текущий экземпляр
класса может временно меняться. Обратиться к текущему экземпляру некоторого
класса (скажем, CLASS_NAME), можно очень просто: по имени этого класса. В
контексте описания любого класса вместо его имени можно использовать
ключевой слово this:
CLASS_NAME; !! текущий экземпляр класса CLASS_NAME
this !! текущий экземпляр текущего класса
Рассмотрим теперь бинарную операцию доступа к классу .' (точка).
Первым операндом этой операции всегда является объект некоторого класса, а
второй операнд (произвольное выражение) -- это результат операции (от него
выражение также заимствует L-контекстность и константность). Как и в C++ и
Паскале, она может использоваться, например, для доступа к отдельным
компонентам объекта, но в Ксерионе ее семантика значительно шире. Формально
она имеет два независимых аспекта: декларативный и процедурный.
Декларативный аспект операции состоит в том, что ее второй операнд
вычисляется в контексте пространства имен данного класса (т.е. в нем
доступны имена компонент, переменных, функций и иные атрибуты класса).
Процедурный аспект -- в том, что она (на время вычисления своего второго
операнда) делает свой первый операнд-объект текущим экземпляром для своего
класса. Оба перечисленных аспекта сочетаются естественным образом, как видно
из примеров:
!! тривиальный вектор из трех компонент
class VECTOR { float x, y, z };
%VECTOR vec1, vec2; !! пара объектов класса VECTOR
vec1.x; !! x-компонента vec1
vec2.(x + y + z); !! сумма компонент vec2
vec1.(x*x + y*y + z*z) !! норма вектора vec1
Если же первый операнд -- это ссылка на текущий объект (иными словами,
имя класса), то декларативная семантика остается неизменной, но процедурная
вырождается в пустую операцию (т.к. текущий объект уже является таковым).
Таким образом, операция доступа к классу становится практически точным
аналогом операции ::' (квалификации) из C++:
VECTOR.x !! x-компонента текущего экземпляра VECTOR
this.x !! то же самое в контексте класса VECTOR
В системе инструкций языка имеется свой аналог операции доступа к
классу -- инструкция присоединения with:
with OBJ_EXPR BLOCK
Ее семантика практически та же: выполнить блок инструкций BLOCK в
контексте класса, определенного OBJ_EXPR (декларативная), и с OBJ_EXPR в
качестве текущего экземпляра этого класса (процедурная). К примеру:
with vec1 { x = y = z = 0f }; !! обнулить компоненты vec1
with VECTOR { x = y = z = 0f } !! то же с текущим экземпляром VECTOR
В языке не существует специального понятия метода класса -- в основном
потому, что они и не требуются. Методы классов в C++ и Java характеризуются
тем, что вместе с другими аргументами они неявно получают указатель на
текущий объект класса, с которым должны работать. Однако, в Ксерионе понятие
текущего объекта является глобальным и равно применимым ко всем функциям.
Функции, декларированные внутри класса, отличаются от других только тем, что
имеют непосредственный доступ ко всем атрибутам класса (включая его личную и
защищенную часть). Если же последнее не требуется, функции, работающие с
объектами определенного класса, могут быть декларированы и за его пределами.
Приведем пример для описанного нами класса VECTOR:
!! Умножение вектора на скаляр `a`
void (float a) scale_VECTOR
{ with VECTOR { x *= a; y *= a; z *= a } }
Описанный нами "псевдо-метод" scale_VECTOR использовать на практике так
же просто, как и функции, декларированные вместе с самим классом:
vec2.Scale_VECTOR (1.5) !! Умножить vec2 на 1.5
with vec2 { Scale_VECTOR (1.5) } !! то же, что и выше
Scale_VECTOR (2f) !! Умножить текущий экземпляр VECTOR на 2
Помимо этого, для каждого класса автоматически определяются операции
присваивания, инициализации и сравнения (на равенство и неравенство).
Присваивание объектов состоит в последовательном присваивании всех их
компонент. Аналогичным образом определяется экземплярная инициализация:
объект всегда может быть инициализирован присваиванием ему другого объекта
того же класса. Операция сравнения также определена как покомпонентная: если
все соответствующие компоненты равны, два объекта считаются равными; в
противном случае они различны. Эти операции над объектами всегда доступны; в
отличие от C++ их невозможно переопределить или же "разопределить".
Конечно же, помимо экземплярной инициализации предусмотрены и другие
законные способы инициализировать объект класса. Для классов всегда
определена списковая инициализация, а может быть доступен и вызов
конструктора. Рассмотрим эти возможности по порядку.
Самый тривиальный способ инициализации создаваемого объекта -- это
инициализация его списком компонент. В принципе, этот способ аналогичен
списковой инициализации классов и структур в C и C++, но он допускает больше
возможностей.
Общий синтаксис спискового инициализатора объекта имеет примерно такой
вид:
#' (' <COMP_LIST> )'
где COMP_LIST -- это список инициализаторов для компонент объекта. Его
синтаксис мы подробно рассматривать не будем, поскольку он полностью
идентичен списку аргументов функций. Единственное различие: список здесь
применяется не к параметрам функционала, а к компонентам объекта. В списке
допустимы и позиционные инициализаторы, и именные. Практически ничем не
отличается и семантика. Компоненты объекта, как и параметры функции, могут
иметь инициализацию по умолчанию (в том числе, и с использованием ранее
описанных компонент), и явная инициализация переопределяет неявную. Наконец,
заметим, что при инициализации декларируемой переменной может использоваться
сокращенная форма: вместо VAR = #( LIST ) можно написать просто VAR ( LIST
). Приведем примеры для класса VECTOR:
%VECTOR null = #(0f, 0f, 0f); !! нулевой вектор
%VECTOR null (0f, 0f, 0f) !! (то же, короче)
%VECTOR null (x: 0f, y: 0f, z: 0f) !! (то же, очень развернуто)
!! координатные векторы-орты
%VECTOR PX (1f, 0f, 0f), PY (0f, 1f, 0f), PZ (0f, 0f, 1f)
%VECTOR NX (-1f, 0f, 0f), NY (0f, -1f, 0f), NZ (0f, 0f, -1f)
Для наиболее тривиальных классов, подобных классу VECTOR, списковая
инициализация является самым простым и удобным способом создания объекта.
Однако, часто нужны и классы-"черные ящики", имеющие нетривиальную
внутреннюю структуру, целостность которой должна всегда быть обеспечена.
Списковая инициализация для них неудобна и ненадежна, и лучше использовать
специальные функции -- конструкторы.
Все конструкторы декларируются внутри соответствующего класса.
Синтаксис описания такой же, как и у функций, только в качестве
возвращаемого типа используется фиктивный тип constructor (на самом деле,
конструкторы не возвращают значения вообще). В отличие от C++ и Java, все
конструкторы в Ксерионе -- именованные: класс может иметь произвольное
количество конструкторов, но их имена должны различаться (и ни одно из них
не совпадает с именем класса). Так, к описанию класса VECTOR мы могли бы
добавить конструктор:
!! инициализация вектора полярными координатами
!! (len -- модуль, phi -- долгота, theta -- широта)
сonstructor (float len, phi, theta) polar
{ x = len * sin(phi) * cos(theta), y = len * cos(phi) * cos(theta), z = len
* sin(theta) }
Тот же конструктор может быть более компактно записан так:
сonstructor (float len, phi, theta) polar :
(len * sin(phi) * cos(theta), len * cos(phi) * cos(theta), len * sin(theta)
) {}
Конструкция в круглых скобках после двоеточия -- это тот же списковый
инициализатор для объекта, элементы которого могут обращаться к параметрам
конструктора. В данном случае можно выбрать, какую именно форму
использовать, но если какие-то компоненты класса требуют нетривиальной
инициализации (например, сами являются объектами), использовать
список-инициализатор в конструкторе -- это единственный корректный способ
задать им начальное значение. Независимо от того, как конструктор polar
определен, использовать его можно так:
%VECTOR anyvec = :polar (200f, PI/4f, PI/6f)
Обратите внимание на двоеточие перед вызовом конструктора: оно явно
указывает на то, что при инициализации будет использован конструктор для
этого класса.
Как и в C++, в Ксерионе существуют временные объекты. Временный объект
создается либо указанием списка компонент, либо обращением к конструктору
(обычно квалифицированному с помощью операции .'). Например:
VECTOR (0.5, 0.3, -0.7) !! временный вектор
VECTOR.polar (10.0, 2f*PI, PI/2f) !! другой вариант
Существование временных объектов обычно длится не дольше, чем
выполняется инструкция, в которой они были созданы.
Не только инициализация, но и деинициализация объекта может потребовать
нетривиальных действий, поэтому для класса может быть задан деструктор. Это
-- просто блок кода, определяющий действия, неявно выполняемые при
z_func (3.14, , false, false, y: 8.9)
!! (x ← 3.14, y ← 8.9, p ← false, q ← false)
При неосторожном сочетании всех этих приемов вполне может оказаться
так, что при вызове функции параметр оставлен без значения, или же
инициализирован два (или более) раза. Второе является безусловной ошибкой, а
вот первое может считаться допустимым. Дело в том, что к параметрам функции,
как и к любым переменным, может быть применена инициализация по умолчанию.
Любой явно заданный аргумент "вытесняет" неявное значение параметра.
Аналогичная возможность имеется и в C++, но там инициализация по умолчанию
может относиться лишь к последним аргументам в списке, а инициализаторами
обязаны быть литеральные значения. В Ксерионе оба этих ограничения
отсутствуют. Более того, один неочевидный (но весьма полезный) аспект
описаний состоит в том, что инициализатор для параметра может содержать
другие параметры, описания которых предшествуют ему. Применение этого метода
лучше показать на примере:
!! Заметьте, что здесь три описания нельзя объединить
void (int a = 5; int b = a; int c = a + b) x_func;
x_func (11, 12, 13); !! все аргументы задано явно
!! (a ← 11, b ← 12, c ← 13)
x_func (10, 20); !! a и b заданы, c по умолчанию
!! (a ← 10, b ← 20, c ← 30)
x_func (10); !! a задано, b и c по умолчанию
!! (a ← 10, b ← 10, c ← 20)
x_func (); !! все по умолчанию
!! (a ← 5, b ← 5, c ← 10)
Даже в качестве размеров параметров-массивов могут использоваться
выражения, содержащие ранее декларированные параметры. Это тоже может
оказаться полезным:
!! матричное произведение: C = A (*) B
void (u_int L, M, N; double [L][M] @A, [M][N] @B, [L][N] @C) MatrixProduct {
! ... ! }
Семантика передачи аргументов -- это всегда семантика инициализации,
т.е. допустимы не только простые выражения, но и любые инициализаторы,
подходящие по типу. То же относится к значению, возвращаемому инструкцией
return. Заметим, что параметры-массивы (в отличие от C, C++ и Java) также
передаются (и возвращаются) по значению, что может быть весьма дорогим
удовольствием. Как правило, массивы лучше передавать через указатель или
ссылку, а передачу по значению использовать лишь в тех случаях, когда это
действительно оправдано. Помимо своих параметров, функции доступна вся
внешняя среда -- т.е. все переменные и константы (независимо от режима их
размещения) и прочие виды описаний, доступные в точке, где дано описание
функции.
В языке не существует перегруженных (overloaded) функций, подобных
имеющимся в C++. Имя каждой функции в своей области действия должно быть
уникально (как и для любого другого субъекта описания).
В заключение отметим, что функциональный тип допускает отдельную форму
инициализатора, прямо задающего тело безымянной функции. (Некоторые языки
программирования называют подобное "лямбда-нотацией"). Неявный инициализатор
имеет вид #' <тело функции>. Имена и типы параметров и возвращаемого
значения явно не задаются, а определяются автоматически, исходя из контекста
инициализации. Например:
int (float a, b, c) t_func = #{ return :int (a * b * c) };
t_func (2, 3, 4) !! 24 (int)
Дополнительные разновидности описаний
Чтобы завершить разговор об описаниях, мы рассмотрим некоторые
специальные декларативные конструкции. Все они имеют скорее вспомогательное,
чем принципиальное значение, но все-таки они полезны при создании реальных
программ.
Прежде всего, в Ксерионе имеется свой аналог описания typedef в C,
позволяющий вводить новые типы. Однако, это не самостоятельная конструкция,
а лишь еще один вид терма описания (type <имя типа>), который, как
всегда, может совмещаться с другими термами. Например:
!! flt -- синоним float,
!! pflt -- указатель на float
!! ppflt -- указатель на указатель на float
float type flt, ^ type pflt, ^^ type ppflt
Ключевое слово type слишком громоздко, поэтому его можно сократить до
символа %' (что обычно на практике и делается). Для того, чтобы
использовать новоопределенный тип в качестве корня описания, он тоже должен
предваряться словом type (или символом %'):
%flt x, y, z; !! т.е. float x, y, z
%pflt p1, p2; !! т.е. float ^ {p1, p2}
%ppft pp1, pp2, pp3 !! т.е. float ^^ {pp1, pp2, pp3}
С точки зрения семантики подобная запись -- не более, чем средство
сократить длинные описания. В отличие от объектных типов, никакими
принципиально новыми свойствами тип, введенный через описание type, обладать
не будет.
Приведенные выше описания -- это частный случай более общего подхода,
позволяющего использовать в качестве корня описания не только определенный
программистом тип, но и произвольное выражение, имеющее смысл. Вот несколько
тривиальных примеров:
%(2 * 2) xx, yy, zz; !! т.е. u_int xx, yy, zz
%(10 < 20) pp, qq; !! т.е. bool pp, qq
%("text" []) cc !! т.е. char cc
Выражение в корне описания (если это не просто идентификатор, оно
должно быть заключено в скобки) вычисляется, но его значение игнорируется, и
в качестве базы описания используется только его тип. Наконец, отметим, что
имена определенных пользователем (но не встроенных!) типов -- это также
законные (но неопределенные) выражения. Все это открывает возможности для
многих полезных трюков. Так, использование имен производных типов в
выражениях (и выражений -- в корнях описаний) дает простой механизм
типизационной декомпозиции, т.е. перехода от производных типов к их базовым.
Вот пример того, как это можно использовать на практике:
!! если v_type -- векторный тип:
%(v_type []) %v_type_elem; !! v_type_elem -- это тип элементов v_type
!! если p_type -- указательный тип:
%(p_type ^) %p_type_ref; !! p_type_ref -- это тип,
!!
получаемый разыменованием p_type
!! если f_type -- функциональный тип:
%(f_type ()) %f_type_result !! f_type_result -- это тип значения,
!!
возвращаемого f_type при вызове
Существует еще одна важная форма описаний -- это макроопределения
(let-определения). В основном, они применимы для тех же целей, что и
определения #define в C/C++, т.е. как макроподстановки повторяющихся
фрагментов исходного кода программы. Но не менее важны и различия. Если
средства C-препроцессора -- это надстройка над языком, то let-определения --
это часть языка Ксерион, а объектом let-подстановки может быть не всякая
строка символов -- это должно быть законное выражение языка. Общий синтаксис
макроопределения имеет такой вид:
let NAME1 =' EXPR1 ( ,' NAME2 =' EXPR2) ...
Это определение делает все идентификаторы NAME# синонимами для
соответствующих выражений EXPR#. Как и прочие виды определений,
макроопределения локальны для содержащего их блока или области действия.
Важно также то, что выражение EXPR должно быть корректно не только
синтаксически, но и семантически: в частности, все идентификаторы,
упомянутые в EXPR, должны иметь смысл. В целом механизм макроопределений
обеспечивает не только текстуальную, но и семантическую подстановку: все
имена будут иметь в точке обращения к макро тот же смысл, который они имели
в точке его определения. Например:
int value; !! целая переменная
let v1 = value; !! v1 -- синоним value
{ float value; !! переопределение value в подблоке
value; !! (float value)
v1 !! (а это -- int value)
}
Наконец, если EXPR является L-выражением, то NAME -- также L-выражение.
Механизм макроопределений является довольно мощным средством, используемым
для самых разных целей: от определения символических литералов (в отличие от
констант-переменных, для них не требуется дополнительная память) до простого
сокращения слишком длинных идентификаторов переменных, функций, типов и
классов:
%err_no (%string FileName) #SystemOpenFile;
let SysOpen = SystemOpenFile !! сокращение
В завершение рассмотрим описание conceal -- механизм "скрытия" имен.
Если идентификатор, определенный в некой внешней области действия (например,
глобальный) необходимо сделать недоступным в некой внутренней (и всех
областях, вложенных в нее), этого легко добиться с помощью специального
описателя conceal:
conceal NAME ( ,' NAME1) ...
Описатель conceal делает все перечисленные в нем имена локально
недоступными (от описателя до конца внутренней области действия, содержащей
его). В сущности, описание conceal NAME работает примерно как let
NAME=<nothing>. В основном, механизм conceal предназначен для работы с
объектами и иерархиями классов (например, скрытия каких-нибудь атрибутов
базового класса в производных классах), что, конечно, не означает, что его
нельзя использовать для других целей.
Инструкции и поток управления
Собственно программа состоит в основном из операторов или инструкций
языка (последний термин кажется нам предпочтительным, поэтому им мы и будем
пользоваться). Простейшие виды инструкций мы уже рассмотрели. Так, все виды
описаний являются законными инструкциями, допустимыми в любом месте
программы. Любое выражение -- это также инструкция (возвращаемое значение,
если оно есть, игнорируется). В языке предусмотрен такой механизм
группировки инструкций, как блок, т.е. последовательность инструкций,
разделенных точками с запятой ( ;') и заключенная в фигурные скобки ("{}").
Блок рассматривается как единая инструкция и является областью локализации
для всех содержащихся в нем описаний. Заметьте, что в этом отношении язык
следует традициям Паскаля: тоска с запятой -- это синтаксический разделитель
инструкций (но ни одна инструкция не завершается этим символом). Во многих
случаях избыточная точка с запятой не считается ошибкой, т.к. в языке
определена пустая инструкция, не содержащая ни одного символа (и, очевидно,
не выполняющая никаких действий). Любая инструкция может быть помечена
меткой вида LABEL :', что позволяет инструкциям break, continue и goto на
нее ссылаться. Рассмотрим другие виды инструкций.
Инструкция утверждения (assert) имеет вид:
assert CND
Семантика ее проста: вычисляется CND (выражение типа bool). Если оно
истинно, ничего не происходит, в противном случае возбуждается
исключительная ситуация AssertException. Эта инструкция нужна в основном для
"отлова" логических ошибок в процессе отладки программы.
Конечно же, имеется условная инструкция (if/unless), имеющая следующий
вид:
(if P_CND | unless N_CND) BLOCK
[else E_STMT]
Если (для if-формы) выражение P_CND истинно или (для unless-формы)
выражение N_CND ложно, выполняется блок BLOCK. В противном случае, если
присутствует необязательная часть else, будет выполнена инструкция E_STMT.
Заметим, что тело условной инструкции -- это всегда блок, ограниченный
фигурными скобками (что снимает проблему неоднозначности "висящего else").
Однако, круглые скобки вокруг условия (как в C) не требуются (хотя, конечно,
ничему и не помешают). В части else допустима произвольная инструкция
(например, другой if/unless). Очевидно, что формы if и unless полностью
взаимозаменяемы, и какую из них использовать -- вопрос конкретного случая.
В отличие от большинства языков, в Ксерионе имеется только одна (зато
довольно мощная) инструкция цикла. Вот ее самый общий синтаксис:
[for I_EXPR]
(while P_CND_PRE | until N_CND_PRE | loop)
[do R_EXPR]
BLOCK
[while P_CND_POST | until N_CND_POST]
Хотя она выглядит довольно громоздкой, большая часть ее компонент
необязательна. Необязательная часть for задает инициализатор цикла --
выражение I_EXPR, которое всегда вычисляется один раз перед самым началом
работы цикла. Далее всегда следует заголовок цикла, задающей его
предусловие, проверяемое перед каждой итерацией цикла. Если (в форме while)
P_CND_PRE ложно или (в форме until) N_CND_PRE истинно, цикл завершит свою
работу. Если же заголовок цикла сводится к loop, предусловие отсутствует.
Телом цикла является блок BLOCK, обычно выполняющий основную работу.
Необязательная часть do задает поститерацию цикла: выражение R_STMT будет
вычисляться на каждой итерации после тела цикла. Наконец, цикл может иметь и
постусловие: если (в форме while) P_CND_POST ложно или (в форме until)
N_CND_POST истинно, цикл также завершится. Какую из двух форм использовать
для пред- и постусловия -- это, опять-таки, вопрос предпочтения. Предусловие
и постусловие могут присутствовать одновременно -- в этом случае, цикл
прерывается, когда перестает соблюдаться хотя бы одно из них. Наконец
заметим, что вместо выражения I_EXPR может быть дано любое описание, и при
этом цикл становится областью локализации для него (т.е. как бы неявно
заключается в блок). Элементы for и do логически избыточны -- они нужны
только для того, чтобы можно было ради наглядности собрать в заголовке всю
логику управления циклом. Так, если нужен цикл с переменной i, меняющей
значение от (включая) 0 до (исключая) N; это обычно записывается так:
for u_int i = 0 while i < N do ++ i { !( тело цикла )! }
Нередко необходимо прервать выполнение цикла где-нибудь посередине. Для
этого удобно использовать инструкцию прерывания break:
break [LABEL]
Она прерывает выполнение содержащего ее цикла, помеченного меткой LABEL
(равно как и всех вложенных в него циклов, если они есть). Если элемент
LABEL опущен, прерывается самый внутренний из циклов, содержащих инструкцию
break. Инструкция продолжения continue:
continue [LABEL]
вызовет прерывание текущей итерации цикла LABEL (или, если метка
опущена, самого вложенного цикла) и переход к его следующей итерации
(включая выполнение поститерации и проверку постусловия, если они есть).
В завершение упомянем об инструкции перехода goto:
goto [LABEL]
передающей управление инструкции, помеченной меткой LABEL. О вредности
подобных инструкций классики структурного программирования написали столько,
что нет смысла их повторять. Инструкция goto в языке есть, а использовать ли
ее в программе -- дело вашей совести и личных предпочтений.
Для завершения работы функции применяется уже знакомая нам инструкция
return:
return [EXPR]
Она допустима только в определении функции и обеспечивает выход из нее
с возвратом значения EXPR (подходящего типа). Выражение EXPR опускается,
если тип функции -- void.
Наконец, в языке имеется инструкция with, тесно связанная с объектами и
потому рассмотренная в следующем разделе.
Объекты и классы
Ксерион -- это объектно-ориентированный язык. В нем присутствует
концепция объекта -- ключевого механизма абстракции данных, обеспечивающего
для них инкапсуляцию, наследование и полиморфизм.
Каждый объект языка относится к одному из классов, определяющих
специфичные для него свойства и атрибуты. Самый общий синтаксис описания
класса таков:
class CLASS_NAME [ :' SUPERCLASS_NAME]
{
CLASS_DECLS
}
[instate INSTATE_LIST]
[destructor DESTRUCTOR_BODY]
Рассмотрим все элементы описания по порядку. Прежде всего, каждый класс
обязан иметь уникальное в своей области действия имя (CLASS_NAME). Класс
может быть либо корневым, либо же производным от уже определенного
суперкласса (класса SUPERCLASS_NAME). Далее следует заключенное в фигурные
скобки тело описания класса, представляющее собой список CLASS_DECLS. Его
элементами могут быть практически все виды описаний языка (включая и
некоторые другие, рассмотренные ниже). В большинстве случаев в описании
класса присутствуют переменные, константы и функции.
Любая переменная, описание которой содержится в декларации класса, по
умолчанию считается его компонентой. Это значит, что для каждого объекта
класса существует собственная копия этой переменной. Если же переменная
имеет явно специфицированный режим размещения static или shared, она
является переменной класса, т.е., в отличие от его компонент, существует в
единственном экземпляре, вне зависимости от того, сколько объектов данного
класса было создано. Разница между режимами static и shared состоит в том,
что static-переменные существуют глобально (время их существования совпадает
со временем выполнения программы), а для shared область действия, равно как
и время существования, определяются декларацией класса.
В декларации класса могут присутствовать вложенные блоки личных
(private) и защищенных (protected) описаний. Как и в C++, имена всех
объектов, декларированных в private-блоке, доступны только внутри декларации
класса, а в protected-блоке -- также и внутри деклараций всех его
подклассов. Все прочие декларации являются публичными, т.е. доступными извне
без каких-либо ограничений.
Синтаксически описание класса играет роль корня описания. Заметим, что
после того, как класс декларирован, для ссылок на него (как и на все прочие
производные типы) используется ключевое слово type или %' (а не class).
В семантике объектов уникальным (и весьма важным) является понятие
текущего экземпляра объекта. Для каждого класса определен один и только один
текущий экземпляр. Его можно рассматривать как неявную переменную класса с
типом CLASS_NAME^ и режимом размещения shared, инициализируемую, как и все
указатели, значением nil. В процессе выполнения программы текущий экземпляр
класса может временно меняться. Обратиться к текущему экземпляру некоторого
класса (скажем, CLASS_NAME), можно очень просто: по имени этого класса. В
контексте описания любого класса вместо его имени можно использовать
ключевой слово this:
CLASS_NAME; !! текущий экземпляр класса CLASS_NAME
this !! текущий экземпляр текущего класса
Рассмотрим теперь бинарную операцию доступа к классу .' (точка).
Первым операндом этой операции всегда является объект некоторого класса, а
второй операнд (произвольное выражение) -- это результат операции (от него
выражение также заимствует L-контекстность и константность). Как и в C++ и
Паскале, она может использоваться, например, для доступа к отдельным
компонентам объекта, но в Ксерионе ее семантика значительно шире. Формально
она имеет два независимых аспекта: декларативный и процедурный.
Декларативный аспект операции состоит в том, что ее второй операнд
вычисляется в контексте пространства имен данного класса (т.е. в нем
доступны имена компонент, переменных, функций и иные атрибуты класса).
Процедурный аспект -- в том, что она (на время вычисления своего второго
операнда) делает свой первый операнд-объект текущим экземпляром для своего
класса. Оба перечисленных аспекта сочетаются естественным образом, как видно
из примеров:
!! тривиальный вектор из трех компонент
class VECTOR { float x, y, z };
%VECTOR vec1, vec2; !! пара объектов класса VECTOR
vec1.x; !! x-компонента vec1
vec2.(x + y + z); !! сумма компонент vec2
vec1.(x*x + y*y + z*z) !! норма вектора vec1
Если же первый операнд -- это ссылка на текущий объект (иными словами,
имя класса), то декларативная семантика остается неизменной, но процедурная
вырождается в пустую операцию (т.к. текущий объект уже является таковым).
Таким образом, операция доступа к классу становится практически точным
аналогом операции ::' (квалификации) из C++:
VECTOR.x !! x-компонента текущего экземпляра VECTOR
this.x !! то же самое в контексте класса VECTOR
В системе инструкций языка имеется свой аналог операции доступа к
классу -- инструкция присоединения with:
with OBJ_EXPR BLOCK
Ее семантика практически та же: выполнить блок инструкций BLOCK в
контексте класса, определенного OBJ_EXPR (декларативная), и с OBJ_EXPR в
качестве текущего экземпляра этого класса (процедурная). К примеру:
with vec1 { x = y = z = 0f }; !! обнулить компоненты vec1
with VECTOR { x = y = z = 0f } !! то же с текущим экземпляром VECTOR
В языке не существует специального понятия метода класса -- в основном
потому, что они и не требуются. Методы классов в C++ и Java характеризуются
тем, что вместе с другими аргументами они неявно получают указатель на
текущий объект класса, с которым должны работать. Однако, в Ксерионе понятие
текущего объекта является глобальным и равно применимым ко всем функциям.
Функции, декларированные внутри класса, отличаются от других только тем, что
имеют непосредственный доступ ко всем атрибутам класса (включая его личную и
защищенную часть). Если же последнее не требуется, функции, работающие с
объектами определенного класса, могут быть декларированы и за его пределами.
Приведем пример для описанного нами класса VECTOR:
!! Умножение вектора на скаляр `a`
void (float a) scale_VECTOR
{ with VECTOR { x *= a; y *= a; z *= a } }
Описанный нами "псевдо-метод" scale_VECTOR использовать на практике так
же просто, как и функции, декларированные вместе с самим классом:
vec2.Scale_VECTOR (1.5) !! Умножить vec2 на 1.5
with vec2 { Scale_VECTOR (1.5) } !! то же, что и выше
Scale_VECTOR (2f) !! Умножить текущий экземпляр VECTOR на 2
Помимо этого, для каждого класса автоматически определяются операции
присваивания, инициализации и сравнения (на равенство и неравенство).
Присваивание объектов состоит в последовательном присваивании всех их
компонент. Аналогичным образом определяется экземплярная инициализация:
объект всегда может быть инициализирован присваиванием ему другого объекта
того же класса. Операция сравнения также определена как покомпонентная: если
все соответствующие компоненты равны, два объекта считаются равными; в
противном случае они различны. Эти операции над объектами всегда доступны; в
отличие от C++ их невозможно переопределить или же "разопределить".
Конечно же, помимо экземплярной инициализации предусмотрены и другие
законные способы инициализировать объект класса. Для классов всегда
определена списковая инициализация, а может быть доступен и вызов
конструктора. Рассмотрим эти возможности по порядку.
Самый тривиальный способ инициализации создаваемого объекта -- это
инициализация его списком компонент. В принципе, этот способ аналогичен
списковой инициализации классов и структур в C и C++, но он допускает больше
возможностей.
Общий синтаксис спискового инициализатора объекта имеет примерно такой
вид:
#' (' <COMP_LIST> )'
где COMP_LIST -- это список инициализаторов для компонент объекта. Его
синтаксис мы подробно рассматривать не будем, поскольку он полностью
идентичен списку аргументов функций. Единственное различие: список здесь
применяется не к параметрам функционала, а к компонентам объекта. В списке
допустимы и позиционные инициализаторы, и именные. Практически ничем не
отличается и семантика. Компоненты объекта, как и параметры функции, могут
иметь инициализацию по умолчанию (в том числе, и с использованием ранее
описанных компонент), и явная инициализация переопределяет неявную. Наконец,
заметим, что при инициализации декларируемой переменной может использоваться
сокращенная форма: вместо VAR = #( LIST ) можно написать просто VAR ( LIST
). Приведем примеры для класса VECTOR:
%VECTOR null = #(0f, 0f, 0f); !! нулевой вектор
%VECTOR null (0f, 0f, 0f) !! (то же, короче)
%VECTOR null (x: 0f, y: 0f, z: 0f) !! (то же, очень развернуто)
!! координатные векторы-орты
%VECTOR PX (1f, 0f, 0f), PY (0f, 1f, 0f), PZ (0f, 0f, 1f)
%VECTOR NX (-1f, 0f, 0f), NY (0f, -1f, 0f), NZ (0f, 0f, -1f)
Для наиболее тривиальных классов, подобных классу VECTOR, списковая
инициализация является самым простым и удобным способом создания объекта.
Однако, часто нужны и классы-"черные ящики", имеющие нетривиальную
внутреннюю структуру, целостность которой должна всегда быть обеспечена.
Списковая инициализация для них неудобна и ненадежна, и лучше использовать
специальные функции -- конструкторы.
Все конструкторы декларируются внутри соответствующего класса.
Синтаксис описания такой же, как и у функций, только в качестве
возвращаемого типа используется фиктивный тип constructor (на самом деле,
конструкторы не возвращают значения вообще). В отличие от C++ и Java, все
конструкторы в Ксерионе -- именованные: класс может иметь произвольное
количество конструкторов, но их имена должны различаться (и ни одно из них
не совпадает с именем класса). Так, к описанию класса VECTOR мы могли бы
добавить конструктор:
!! инициализация вектора полярными координатами
!! (len -- модуль, phi -- долгота, theta -- широта)
сonstructor (float len, phi, theta) polar
{ x = len * sin(phi) * cos(theta), y = len * cos(phi) * cos(theta), z = len
* sin(theta) }
Тот же конструктор может быть более компактно записан так:
сonstructor (float len, phi, theta) polar :
(len * sin(phi) * cos(theta), len * cos(phi) * cos(theta), len * sin(theta)
) {}
Конструкция в круглых скобках после двоеточия -- это тот же списковый
инициализатор для объекта, элементы которого могут обращаться к параметрам
конструктора. В данном случае можно выбрать, какую именно форму
использовать, но если какие-то компоненты класса требуют нетривиальной
инициализации (например, сами являются объектами), использовать
список-инициализатор в конструкторе -- это единственный корректный способ
задать им начальное значение. Независимо от того, как конструктор polar
определен, использовать его можно так:
%VECTOR anyvec = :polar (200f, PI/4f, PI/6f)
Обратите внимание на двоеточие перед вызовом конструктора: оно явно
указывает на то, что при инициализации будет использован конструктор для
этого класса.
Как и в C++, в Ксерионе существуют временные объекты. Временный объект
создается либо указанием списка компонент, либо обращением к конструктору
(обычно квалифицированному с помощью операции .'). Например:
VECTOR (0.5, 0.3, -0.7) !! временный вектор
VECTOR.polar (10.0, 2f*PI, PI/2f) !! другой вариант
Существование временных объектов обычно длится не дольше, чем
выполняется инструкция, в которой они были созданы.
Не только инициализация, но и деинициализация объекта может потребовать
нетривиальных действий, поэтому для класса может быть задан деструктор. Это
-- просто блок кода, определяющий действия, неявно выполняемые при