Применяя классы, основные типы и функции, можно написать много хороших программ. С параметризированными классами и функциями дело идет еще лучше. Но нередко мы с сожалением отмечаем, что по нескольким причинам классы не представляют собой инструмент с максимальной абстракцией типа.
Во-первых, классы подчиняются ссылочной семантике и из-за этого могут воплощать многие проектные решения не полностью или с ощутимыми накладными расходами. На практике трудно моделировать с помощью класса такую простую сущность, как точка с двумя или тремя координатами, если таких точек больше нескольких миллионов: разработчик оказывается перед непростым выбором – хорошая абстракция или приемлемое быстродействие. Кроме того, для линейной алгебры ссылочная семантика – большая морока. Попробуйте убедить математика или программиста-теоретика, что присваивание `a = b` должно делать из матрицы a лишь псевдоним матрицы `b`, а не отдельную копию! Даже такой простой тип, как массив, довольно накладно моделировать в виде класса в сравнении с мощной и лаконичной абстракцией массива, имеющейся в языке D (см. главу 4). Можно, конечно, сделать массивы «волшебными», но опыт то и дело показывает, что предоставлять множество «волшебных» типов, не воспроизводимых в пользовательском коде, – дурной тон и признак плохо спроектированного языка. Затраты на массив – всего два слова, а выделение памяти под экземпляр класса и использование дополнительного косвенного обращения означают большие накладные расходы по памяти и времени для всех примитивов массива. Даже такой простой тип, как `int`, нельзя выразить в виде класса дешево и элегантно (причем речь не об удобстве оператора). У такого класса, как `BigInt`, та же проблема: `a = b` делает нечто совершенно иное,
чем соответствующая операция присваивания для типа `int`.
Во-вторых, классы живут вечно, а значит, с их помощью трудно моделировать ресурсы с выраженным *конечным* временем жизни (такие как дескрипторы файлов, дескрипторы графического контекста, мьютексы, сокеты и т. д.). Работая с такими ресурсами как с классами, нужно постоянно быть начеку, чтобы не забыть своевременно освободить инкапсулированные ресурсы с помощью метода, вроде `close` или `dispose`. В таких случаях обычно помогает инструкция `scope` (см. раздел 3.13), но лучше, когда подобная контекстная семантика инкапсулирована в типе – раз и навсегда.
В-третьих, классы – это механизм для довольно «тяжелых» и высокоуровневых абстракций, то есть они не позволяют легко выражать «легковесные» абстракции вроде перечисляемых типов или псевдонимов для заданного типа.
D не был бы настоящим языком для системного программирования, если бы предоставлял для выражения абстракций только классы. Кроме классов в запасе у D есть структуры (типы-значения, сравнимые с классами по мощности, но с семантикой значения и без полиморфизма), типы `enum` (легковесные перечисляемые типы и простые константы), объединения (низкоуровневое хранилище с перекрыванием для разных типов) и вспомогательные механизмы определения типов, такие как `alias`. Все эти средства последовательно рассматриваются в этой главе.
Структуры позволяют определять простые, инкапсулированные типы-значения. Удобная аналогия – тип `int`: значение типа `int`– это 4 байта, допускающие определенные операции. В`int` нет никакого скрытого состояния и никаких косвенных обращений, и две переменные типа `int` всегда ссылаются на разные значения[^1]. Соглашение о структурах исключает динамический полиморфизм, переопределение методов, наследование и бесконечное время жизни. Структура – это преувеличенный тип `int`.
Как вы помните, класс ведет себя как ссылка (см. раздел 6.2), то есть вы всегда манипулируете объектом посредством ссылки на него, причем копирование ссылок лишь увеличивает количество ссылок на тот же объект без дублирования самого объекта. А структура – это тип-значение, то есть, по сути, ведет себя «как `int`»: имя жестко привязано к представленному им значению, а при копировании значения структуры на самом деле копируется целый объект, а не только ссылка.
Определяют структуру так же, как класс, за исключением следующих моментов:
- вместо ключевого слова `class` используется ключевое слово `struct`;
- свойственное классам наследование и реализация интерфейсов запрещены, то есть в определении структуры нельзя указать `:BaseType` или `:Interface`, и очевидно, что внутри структуры не определена ссылка `super`;
- методы структуры нельзя переопределять – все методы являются финальными (вы можете указать в определении метода структуры ключевое слово `final`, но это было бы совершенно излишне);
- нельзя применять к структуре инструкцию `synchronized` (см. главу 13);
- структуре запрещается определять конструктор по умолчанию `this()` (см. раздел 7.1.3.1);
- структуре разрешается определять конструктор копирования (postblit constructor) `this(this)` (см. раздел 7.1.3.4);
- запрещен спецификатор доступа `protected` (иначе предполагалось бы наличие структур-потомков).
Определим простую структуру:
```d
struct Widget
{
// Константа
enum fudgeFactor = 0.2;
// Разделяемое неизменяемое значение
static immutable defaultName = "Виджет";
// Некоторое состояние, память под которое выделяется для каждого экземпляра класса Widget
string name = defaultName;
uint width, height;
// Статический метод
static double howFudgy()
{
return fudgeFactor;
}
// Метод
void changeName(string another)
{
name = another;
}
}
```
[В начало ⮍](#7-1-структуры) [Наверх ⮍](#7-другие-пользовательские-типы)
Несколько заметных на глаз различий между структурами и классами есть следствие менее очевидных семантических различий. Повторим эксперимент, который мы уже проводили, обсуждая классы в разделе 6.2. На этот раз создадим структуру и объект с одинаковыми полями, а затем сравним поведение этих типов при копировании:
При работе со структурами нет никаких ссылок, которые можно привязывать и перепривязывать с помощью операций инициализации и присваивания. Каждое имя экземпляра структуры связано с отдельным значением. Как уже говорилось, объект-структура ведет себя *как значение*, а объект-класс –*как ссылка*. На рис. 7.1 показано положение дел сразу после определения `c2` и `s2`.
***Рис. 7.1.*** *Инструкции `auto c2 = c1;` для объекта-класса `c1` и `auto s2 = s1;` для объекта-структуры `s1` действуют совершенно по-разному, поскольку класс по своей природе – ссылка, а структура – значение*
В отличие от имен `c1` и `с2`, допускающих привязку к любому объекту, имена `s1` и `s2` прочно привязаны к реальным объектам. Нет способа заставить два имени ссылаться на один и тот же объект-структуру (кроме ключевого слова `alias`, задающего простую эквивалентность имен; см. раздел 7.4), и не бывает имени структуры без закрепленного за ним значения, так что сравнение `s1 is null` бессмысленно и порождает ошибку во время компиляции.
В отличие от объектов-классов, объектам-структурам не свойственно бесконечное время жизни (lifetime). Время жизни для них четко ограничено – так же как для временных (стековых) объектов функций. Чтобы создать объект-структуру, задайте имя нужного типа, как если бы вы вызывали функцию:
Вызов `Test()` создает объект-структуру, все поля которого инициализированы по умолчанию. В нашем случае это означает, что поле `t.a` принимает значение `0.4`, а`t.b` остается инициализированным значением `double.init`.
Поначалу может раздражать разница в синтаксисе выражения, создающего объект-структуру `Test(‹аргументы›)`, и выражения, создающего объект-класс `new Test(‹аргументы›)`. D мог бы отказаться от использования ключевого слова new при создании объектов-классов, но это `new` напоминает программисту, что выполняется операция выделения памяти (то есть необычное действие).
Зачем нужно такое ограничение? Все из-за `T.init`– значения по умолчанию, определяемого каждым типом. Оно должно быть статически известно, что противоречит существованию конструктора по умолчанию, выполняющего произвольный код. (Для классов `T.init`– это пустая ссылка `null`, а не объект, построенный по умолчанию.) Правило для всех структур: конструктор по умолчанию инициализирует все поля объекта-структуры значениями по умолчанию.
Код запускается, не требуя внесения каких-либо других изменений. Так же как и классы, структуры позволяют одному конструктору делегировать построение объекта другому конструктору с теми же ограничениями.
Классу приходится заботиться о выделении динамической памяти и инициализации своего базового подобъекта (см. раздел 6.3.3). Со структурами все гораздо проще, поскольку выделение памяти – явный шаг алгоритма построения. Алгоритм построения объекта-структуры типа `T` по шагам:
Если инициализация некоторых или всех полей структуры выглядит как `= void`, объем работ на первом шаге можно сократить, хотя и редко намного, зато такой маневр часто порождает трудноуловимые ошибки в вашем коде (тем не менее случай оправданного применения сокращенной инициализации иллюстрирует пример с классом `Transmogrifier` в разделе 6.3.3).
Предположим, требуется определить объект, который содержит локальный (`private`) массив и предоставляет ограниченный API для манипуляции этим массивом:
У класса `Widget`, определенного таким образом, есть проблема: при копировании объектов типа `Widget` между копиями создается отдаленная зависимость. Судите сами:
В чем проблема? Копирование содержимого `w1` в `w2` «поверхностно», то есть оно выполняется поле за полем, без транзитивного копирования, на какую бы память косвенно ни ссылалось каждое из полей. При копировании массива память под новый массив не выделяется; копируются лишь границы массива (см. раздел 4.1.4). После копирования `w1` и `w2` действительно обладают различными полями с массивами, но ссылаются эти поля на одну и ту же область памяти. Такой объект, являющийся значением, но содержащий неявные разделяемые ссылки, можно в шутку назвать «клуктурой», то есть гибридом структуры (семантика значения) и класса (семантика ссылки)[^2].
Обычно требуется, чтобы структура действительно вела себя как значение, то есть чтобы копия становилась полностью независимой от своего источника. Для этого определите конструктор копирования так:
Конструктор копирования вступает в силу во время копирования объекта. Чтобы инициализировать объект `приемник`с помощью объекта `источник` того же типа, компилятор должен выполнить следующие шаги:
1. Скопировать участок «сырой» памяти объекта `источник` в участок «сырой» памяти объекта `приемник`.
2. Транзитивно для каждого поля, содержащего другие поля (то есть поля, содержащего другое поле, содержащее третье поле, ...), для которого определен метод `this(this)`, вызвать эти конструкторы снизу вверх (начиная от наиболее глубоко вложенного поля).
Оригинальное название конструктора копирования «postblit constructor» происходит от «blit» – популярной аббревиатуры понятия «block transfer», означавшего копирование «сырой» памяти. Язык применяет «сырое» копирование при инициализации и разрешает сразу после этого воспользоваться ловушкой. В предыдущем примере конструктор копирования превращает только что полученный псевдоним массива в настоящую, полномасштабную копию, гарантируя, что с этого момента между объектом-оригиналом и объектом-копией не будет ничего общего. Теперь, после добавления конструктора копирования, модуль легко проходит этот тест:
Вызов конструктора копирования вставляется в каждом случае копирования какого-либо объекта при явном или неявном создании новой переменной. Например, при передаче объекта типа `Widget` по значению в функцию также создается копия:
Второй шаг (часть с «транзитивным полем») процесса конструирования при копировании заслуживает особого внимания. Основанием для такого поведения является *инкапсуляция*: конструктор копирования объекта-структуры должен быть вызван даже тогда, когда эта структура встроена в другую. Предположим, например, что мы решили сделать `Widget` членом другой структуры, которая в свою очередь является членом третьей структуры:
Теперь, если потребуется копировать объекты, содержащие другие объекты типа `Widget`, будет очень некстати, если компилятор забудет, как нужно копировать подобъекты типа `Widget`. Вот почему при копировании объектов типа `Widget2` инициируется вызов конструктора `this(this)` для подобъекта `w1`, невзирая на то, что `Widget2` вообще об этом ничего не знает. Кроме того, при копировании объектов типа `Widget3` конструктор `this(this)` по-прежнему вызывается применительно к полю `w1` поля `w2`. Внесем ясность:
Вкратце, если вы определите для некоторой структуры конструктор копирования `this(this)`, компилятор позаботится о том, чтобы конструктор копирования вызывался в каждом случае копирования этого объекта-структуры независимо от того, является ли он самостоятельным объектом или частью более крупного объекта-структуры.
Зачем был введен конструктор копирования? Ведь ничего подобного в других языках пока нет. Почему бы просто не передавать исходный объект в будущую копию (как это делает C++)?
Опыт с C++ показал, что основная причина неэффективности программ на C++ – злоупотребление копированием объектов. Чтобы сократить потери эффективности по этой причине, C++ устанавливает ряд случаев, в которых компилятор может пропускать вызов конструктора копирования (copy elision). Правила для этих случаев очень быстро усложнились, но все равно не охватывали все моменты, когда можно обойтись без конструирования, то есть проблема осталась не решенной. Развивающийся стандарт C++ затрагивает эти вопросы, определяя новый тип «ссылка на r-значение», позволяющий пользователю управлять пропусками вызова конструктора копирования, но плата за это – еще большее усложнение языка.
Благодаря конструктору копирования подход D становится простым и во многом автоматизируемым. Начнем с того, что объекты в D должны быть *перемещаемыми*, то есть не должны зависеть от своего расположения: копирование «сырой» памяти позволяет переместить объект в другую область памяти, не нарушая его целостность. Тем не менее это ограничение означает, что объект не может содержать так называемые *внутренние указатели*– адреса подобъектов, являющихся его частями. Без этой техники можно обойтись, так что D попросту ее исключает. Создавать объекты с внутренними указателями в D запрещается, и компилятор, как и подсистема времени исполнения, вправе предполагать, что это правило соблюдается. Перемещаемые объекты открывают для компилятора и подсистемы времени исполнения (например, для сборщика мусора) большие возможности, позволяющие программам стать более быстрыми и компактными.
Благодаря перемещаемости объектов копирование объектов становится логическим продолжением перемещения объектов: конструктор копирования `this(this)` делает копирование объектов эквивалентом перемещения с возможной последующей пользовательской обработкой. Таким образом, пользовательский код не может изменить поля исходного объекта (что очень хорошо, поскольку копирование не должно затрагивать объект-источник), но зато может корректировать поля, которые не должны неявно разделять состояние с объектом-источником. Чтобы избежать лишнего копирования, компилятор вправе по собственному усмотрению не вставлять вызов `this(this)`, если может доказать, что источник копии не будет использован после завершения процесса копирования. Рассмотрим, например, функцию, возвращающую объект типа `Widget` (определенный выше) по значению:
Наивный подход: просто создать объект типа `Widget` внутри функции `hun`, а затем скопировать его в переменную `w`, применив побитовое копирование с последующим вызовом `this(this)`. Но это было бы слишком расточительно: D полагается на перемещаемость объектов, так почему бы попросту не переместить в переменную `w` уже отживший свое временный объект, созданный функцией `hun`? Разницу никто не заметит, поскольку после того, как функция `hun` вернет результат, временный объект уже не нужен. Если в лесу упало дерево и никто этого не слышит, то легче переместить его, чем копировать. Похожий (но не идентичный) случай:
В этом случае переменная `result` тоже уходит в небытие сразу же после того, как `iun` вернет управление, поэтому в вызове `this(this)` необходимости нет. Наконец, еще более тонкий случай:
В этом случае сложнее выяснить, можно ли избавиться от вызова `this(this)`. Вполне вероятно, что `‹код2›` продолжает использовать `w`, и тогда перемещение этого значения из `unittest` в `jun` было бы некорректным[^3].
-Все анонимные r-значения перемещаются, а не копируются. Вызов конструктора копирования `this(this)` всегда пропускается, если оригиналом является анонимное r-значение (то есть временный объект, как в функции `hun` выше).
-В случае именованных временных объектов, которые создаются внутри функции и располагаются в стеке, а затем возвращаются этой функцией в качестве результата, вызов конструктора копирования `this(this)` пропускается.
- Нет никаких гарантий, что компилятор воспользуется другими возможностями пропустить вызов конструктора копирования.
Но иногда требуется предписать компилятору выполнить перемещение. Фактически это выполняет функция `move` из модуля `std.algorithm` стандартной библиотеки:
Вызов функции `move` гарантирует, что `w` будет перемещена, аее содержимое будет заменено пустым, сконструированным по умолчанию объектом типа `Widget`. Кстати, это один из тех случаев, где пригодится неизменяемый и не порождающий исключения конструктор по умолчанию `Widget.init` (см. раздел 7.1.3.1). Без него сложно было бы найти способ оставить источник перемещения в строго определенном пустом состоянии.
Каждая структура обладает *временем жизни в пределах области видимости* (*scoped lifetime*), то есть ее жизнь действительно заканчивается с окончанием области видимости объекта. Подробнее:
- время жизни нестатического объекта, определенного внутри функции, заканчивается в конце текущей области видимости (то есть контекста) до уничтожения всех объектов-структур, определенных перед ним;
- время жизни объекта, определенного в качестве члена другой структуры, заканчивается непосредственно после окончания времени жизни включающего объекта;
- время жизни объекта, определенного в контексте модуля, бесконечно; если вам нужно вызвать деструктор этого объекта, сделайте это в деструкторе модуля (см. раздел 11.3);
- время жизни объекта, определенного в качестве члена класса, заканчивается в тот момент, когда сборщик мусора забирает память включающего объекта.
Язык гарантирует автоматический вызов деструктора `~this` по окончании времени жизни объекта-структуры, что очень удобно, если вы хотите автоматически выполнять такие операции, как закрытие файлов и освобождение всех важных ресурсов.
Оригинал копии, использующей конструктор копирования, подчиняется обычным правилам для времени жизни, но деструктор оригинала копии, полученной перемещением «сырой» памяти без вызова `this(this)`, не вызывается.
Освобождение памяти объекта-структуры по идее выполняется сразу же после деструкции.
По умолчанию объекты-структуры уничтожаются в порядке, строго обратном порядку их создания. То есть первым уничтожается объект-структура, определенный в заданной области видимости последним:
Как и ожидалось, объект, созданный первым, был уничтожен последним. На каждой итерации цикл входит в контекст и выходит из контекста управляемой инструкции.
Можно явно инициировать вызов деструктора объекта-структуры с помощью инструкции `clear(объект);`. С функцией `clear` мы уже познакомились в разделе 6.3.5. Тогда она оказалась полезной для уничтожения состояния объекта-класса. Для объектов-структур функция `clear` делает то же самое: вызывает деструктор, а затем копирует биты значения `.init` в область памяти объекта. В результате получается правильно сконструированный объект, правда, без какого-либо интересного содержания.
Структура может определять любое число статических конструкторов и деструкторов. Это средство полностью идентично одноименному средству для классов, с которым мы уже встречались в разделе 6.3.6.
Парность статических конструкторов и деструкторов не требуется. Подсистема поддержки времени исполнения не делает ничего интересного – просто выполняет все статические конструкторы перед вычислением функции `main` в порядке их определения. По завершении выполнения `main` подсистема поддержки времени исполнения так же скучно вызывает все статические деструкторы в порядке, обратном порядку их определения. Предыдущая программа выведет на экран:
Порядок выполнения очевиден для статических конструкторов и деструкторов, расположенных внутри одного модуля, но в случае нескольких модулей не всегда все так же ясно. Порядок выполнения статических конструкторов и деструкторов из разных модулей определен в разделе 6.3.6.
Структуры могут определять функции-члены, также называемые методами. Поскольку в случае структур о наследовании и переопределении речи нет, методы структур лишь немногим больше, чем функции.
Нестатические методы структуры `S` принимают скрытый параметр `this` по ссылке (эквивалент параметра `ref S`). Поиск имен внутри методов структуры производится так же, как и внутри методов класса: параметры перекрывают одноименные внутренние элементы структуры, а имена внутренних элементов структуры перекрывают те же имена, объявленные на уровне модуля.
Кроме того, в этом примере есть тест модуля, определенный внутри структуры. Такие тесты модуля, являющиеся «внутренними элементами», не наделены никаким особым статусом, но их очень удобно вставлять после каждого определения метода. Коду тела внутреннего теста модуля доступна та же область видимости, что и обычным статическим методам: например, тесту модуля в предыдущем примере не требуется снабжать статическое поле y префиксом `S`, как это не потребовалось бы любому методу структуры.
Некоторые особые методы заслуживают более тщательного рассмотрения. К ним относятся оператор присваивания `opAssign`, используемый оператором `=`, оператор равенства `opEquals`, используемый операторами `==` и `!=`, а также упорядочивающий оператор `opCmp`, используемый операторами `<`, `<=`, `>=` и `>`. На самом деле, эта тема относится к главе 12, так как затрагивает вопрос перегрузки операторов, но эти операторы особенные: компилятор может сгенерировать их автоматически, со всем их особым поведением.
то присваивание делается через копирование всех внутренних элементов по очереди. В случае типа `Widget` такой подход может вызвать проблемы, о которых говорилось в разделе 7.1.3.4. Если помните, структура `Widget` обладает внутренним локальным массивом типа `int[]`, и планировалось, что он будет индивидуальным для каждого объекта типа `Widget`. В ходе последовательного присваивания полей объекта `w2` объекту `w1` поле `w2.array` будет присвоено полю `w1.array`, но это будет только простое присваивание границ массива – в действительности, содержимое массива скопировано не будет. Этот момент необходимо подкорректировать, поскольку на самом деле мы хотим создать *дубликат* массива оригинальной структуры и присвоить его целевой структуре.
Пользовательский код может перехватить присваивание, определив метод `opAssign`. По сути, если `lhs` определяет `opAssign`с совместимой сигнатурой, присваивание `lhs = rhs` транслируется в `lhs.opAssign(rhs)`, иначе если `lhs` и `rhs` имеют один и тот же тип, выполняется обычное присваивание поле за полем. Давайте определим метод `Widget.opAssign`:
Оператор присваивания возвращает ссылку на `this`, тем самым позволяя создавать цепочки присваиваний а-ля `w1 = w2 = w3`, которые компилятор заменяет на `w1.opAssign(w2.opAssign(w3))`.
Проблема в том, что метод `opAssign` в таком виде, в каком он определен сейчас, ожидает аргумент типа `ref Widget`, то есть l-значение типа `Widget`. Чтобы помимо l-значений можно было бы присваивать еще и r-значения, структура `Widget` должна определять *два* оператора присваивания:
В версии метода, принимающей r-значения, уже отсутствует обращение к свойству `.dup`. Почему? Ну, r-значение (ас ним и его массив) – это практически собственность второго метода `opAssign`: оно было скопировано перед входом в функцию и будет уничтожено сразу же после того, как функция вернет управление. Это означает, что больше нет нужды дублировать `rhs.array`, потому что его потерю никто не ощутит. Достаточно лишь поменять местами `rhs.array` и `this.array`. Функция `opAssign` возвращает результат, и `rhs` и старый массив объекта `this` уходят в никуда, а`this` остается с массивом, ранее принадлежавшим `rhs`, – совершенное сохранение состояния.
Теперь можно совсем убрать первую перегруженную версию оператора `opAssign`: та версия, что принимает `rhs` по значению, заботится обо всем сама (l-значения автоматически конвертируются в r-значения). Но оставив версию с l-значением, мы сохраняем точку, через которую можно оптимизировать работу оператора присваивания. Вместо того чтобы дублировать структуру-оригинал с помощью свойства `.dup`, метод `opAssign` может проверять, достаточно ли в текущем массиве места для размещения нового содержимого, и если да, то достаточно и записи поверх старого массива на том же месте.
Средство для сравнения объектов-структур предоставляется «в комплекте» – это операторы `==` и `!=`. Сравнение представляет собой поочередное сравнение внутренних элементов объектов и возвращает `false`, если хотя бы два соответствующих друг другу элемента сравниваемых объектов не равны, иначе результатом сравнения является `true`.
По сравнению с методом `opEquals` для классов (см. раздел 6.8.3) метод `opEquals` для структур гораздо проще: ему не нужно беспокоиться о корректности своих действий из-за наследования. Компилятор попросту заменяет сравнение объектов-структур на вызов метода `opEquals`. Конечно, применительно к структурам остается требование определять осмысленный метод `opEquals`: рефлексивный, симметричный и транзитивный. Заметим, что хотя метод `Point.opEquals` выглядит довольно осмысленно, он не проходит тест на транзитивность. Лучшим вариантом оператора сравнения на равенство было бы сравнение двух объектов типа `Point`, значения координат которых предварительно усечены до своих старших разрядов. Такую проверку было бы гораздо проще сделать транзитивной.
Если структура содержит внутренние элементы, определяющие методы `opEquals`, а сама такой метод не определяет, при сравнении все равно будут вызваны существующие методы `opEquals` внутренних элементов. Продолжим работать с примером, содержащим структуру `Point`:
Этот пример также показывает, что сравнение выполняется в порядке объявления полей (т. е. поле `leftBottom` проверяется до проверки `rightTop`), и если встретились два неравных поля, сравнение завершается до того, как будут проверены все поля, благодаря сокращенному вычислению логических связок, построенных с помощью оператора `&&` (short circuit evaluation).
Структура может определять статические данные и статические внутренние функции. Помимо ограниченной видимости и подчинения правилам доступа (см. раздел 7.1.7) режим работы статических внутренних функций ничем не отличается от режима работы обычных функций. Нет скрытого параметра `this`, не вовлечены никакие другие особые механизмы.
Точно так же статические данные схожи с глобальными данными, определенными на уровне модуля (см. раздел 5.2.4), во всем, кроме видимости и ограничений доступа, наложенных на эти статические данные родительской структурой.
Структуры подчиняются спецификаторам доступа `private` (см. раздел 6.7.1), `package` (см. раздел 6.7.2), `public` (см. раздел 6.7.4) и `export` (см. раздел 6.7.5) тем же образом, что и классы. Спецификатор `protected` применительно к структурам не имеет смысла, поскольку структуры не поддерживают наследование.
Заметим, что хотя ключевое слово `export` разрешено везде, где синтаксис допускает применение спецификатора доступа, семантика этого ключевого слова зависит от реализации.
Часто бывает удобно вложить в структуру другую структуру или класс. Например, контейнер дерева можно представить как оболочку-структуру с простым интерфейсом поиска, а внутри нее для определения узлов дерева использовать полиморфизм.
В отличие от классов, вложенных в другие классы, вложенные структуры и классы, вложенные в другие структуры, не обладают никаким скрытым внутренним элементом `outer`– никакой специальный код не генерируется. Такие вложенные типы определяются в основном со структурной целью – чтобы получить нужное управление доступом.
Вспомним, что говорилось в разделе 6.11.1: вложенные классы находятся в привилегированном положении, ведь они обладают особыми, уникальными свойствами. Вложенному классу доступны параметры и локальные переменные включающей функции. Если вы возвращаете вложенный класс в качестве результата функции, компилятор даже размещает кадр функции в динамической памяти, чтобы параметры и локальные переменные функции выжили после того, как она вернет управление.
Для единообразия и согласованности D оказывает структурам, вложенным в функции, те же услуги, что и классам, вложенным в функции. Вложенная структура может обращаться к параметрам и локальным переменным включающей функции:
Во вложенные структуры встраивается волшебный «указатель на кадр», с помощью которого они получают доступ к внешним значениям, таким как `a` и `b` в этом примере. Из-за этого дополнительного состояния размер объекта `Local` не 4 байта, как можно было ожидать, а 8 (на 32-раз рядной машине) – еще 4 байта занимает указатель на кадр. Если хотите определить вложенную структуру без этого багажа, просто добавьте в определение структуры `Local` ключевое слово `static` перед ключевым словом `struct`– тем самым вы превратите `Local` в обычную структуру, то есть закроете для нее доступ к `a` и `b`.
Вложенные структуры практически бесполезны, разве что, по сравнению со вложенными классами, позволяют избежать беспричинного ограничения. Функции не могут возвращать объекты вложенных структур, так как вызывающему их коду недоступна информация о типах таких объектов. Используя замысловатые вложенные структуры, код неявно побуждает создавать все больше сложных функций, а в идеале именно этого надо избегать в первую очередь.
К структурам неприменимы наследование и полиморфизм, но этот тип данных по-прежнему поддерживает конструкцию `alias this`, впервые представленную в разделе 6.13. С помощью `alias this` можно сделать структуру подтипом любого другого типа. Определим, к примеру, простой тип `Final`, поведением очень напоминающий ссылку на класс – во всем, кроме того что переменную типа `Final` невозможно перепривязать! Пример использования переменной `Final`:
Предназначение типа `Final`– быть особым видом ссылки на класс, раз и навсегда привязанной к одному объекту. Такие «преданные» ссылки полезны для реализации множества проектных идей.
Первый шаг – избавиться от присваивания. Проблема в том, что оператор присваивания генерируется автоматически, если не объявлен пользователем, поэтому структура `Final` должна вежливо указать компилятору не делать этого. Для этого предназначен атрибут `@disable`:
До сих пор все шло хорошо. Чтобы реализовать `Final!T`, нужно с помощью конструкции `alias this` сделать `Final(T)` подтипом `T`, но чтобы при этом полученный тип не являлся l-значением. Ошибочное решение выглядит так:
Структура `Final` хранит ссылку на себя в поле `payload`, которое инициализируется в конструкторе. Кроме того, объявив, но не определяя метод `opAssign`, структура эффективно «замораживает» присваивание. Таким образом, клиентский код, пытающийся присвоить значение объекту типа `Final!T`, или не сможет обратиться к `payload` (из-за `private`), или получит ошибку во время компоновки.
`alias payload this` действует довольно просто: каждый раз, когда значение `объект` типа `Final!T` используется в недопустимом для этого типа контексте, компилятор вместо `объект` пишет `объект.payload` (то есть делает `объект.payload`*псевдонимом* для `объекта` в соответствии с именем и синтаксисом конструкции `alias`). Но выражение `объект.payload` представляет собой непосредственное обращение к полю `объект`, следовательно, является l-значением. Это l-значение привязано к переданному по ссылке параметру функции `sneaky` и, таким образом, позволяет `sneaky` напрямую изменять значение поля объекта `v`.
Чтобы это исправить, нужно сделать объект псевдонимом r-значения. Так мы получим полную функциональность, но ссылка, сохраненная в `payload`, станет неприкосновенной. Очень просто осуществить привязку к r-значению с помощью свойства (объявленного с атрибутом `@property`), возвращающего `payload` по значению:
Ключевой момент в новом определении структуры – то, что метод `get` возвращает значение типа `T`, а не `ref T`. Конечно, объект, на который ссылается `payload`, изменить можно (если хотите избежать этого, ознакомьтесь с квалификаторами `const` и `immutable`; см. главу 8). Но структура `Final` свои обязательства теперь выполняет. Во-первых, для любого типа класса `T` справедливо, что `Final!T` ведет себя как `T`. Во-вторых, однажды привязав переменную типа `Final!T` к некоторому объекту с помощью конструктора, вы не сможете ее перепривязать ни к какому другому объекту. В частности, тест модуля, из-за которого пришлось отказаться от предыдущего определения `Final`, больше не компилируется, поскольку вызов `sneaky(v)` теперь некорректен: r-значение типа `A` (неявно полученное из `v`с помощью `v.get`) не может быть привязано к `ref A`, как требуется функции `sneaky` для ее черных дел.
В нашей бочке меда осталась только одна ложка дегтя (на самом деле, всего лишь чайная ложечка), от которой надо избавиться. Всякий раз, когда тип, подобный `Final`, использует конструкцию `alias get this`, необходимо уделять особое внимание собственным идентификаторам `Final`, перекрывающим одноименные идентификаторы, определенные в типе, псевдонимом которого становится `Final`. Предположим, мы используем тип `Final!Widget`, а класс `Widget` и сам определяет свойство `get`:
Чтобы избежать таких коллизий, воспользуемся соглашением об именовании. Для надежности будем просто добавлять к именам видимых свойств имя соответствующего типа:
Соблюдение такого соглашения сводит к минимуму риск непредвиденных коллизий. (Конечно, иногда можно намеренно перехватывать некоторые методы, оставив вызовы к ним за перехватчиком.)
Как располагаются поля в объекте-структуре? D очень консервативен в отношении структур: он располагает элементы их содержимого в том же порядке, в каком они указаны в определении структуры, но сохраняет за собой право вставлять между полями *отступы* (*padding*). Рассмотрим пример:
Если бы компилятор располагал поля в точном соответствии с размерами, указанными в структуре `A`, то адресом поля `b` оказался бы адрес объекта `A` плюс 1 (поскольку поле `a` типа `char` занимает ровно 1 байт). Но такое расположение проблематично, ведь современные компьютерные системы извлекают данные только блоками по 4 или 8 байт, то есть могут извлекать только данные, расположенные по адресам, кратным 4 и 8 соответственно. Предположим, объект типа `A` расположен по «хорошему» адресу, например кратному 8. Тогда адрес поля `b` точно окажется не в лучшем районе города. Чтобы извлечь `b`, процессору придется повозиться, ведь нужно будет «склеивать» значение `b`, собирая его из кусочков размером в байт. Усугубляет ситуацию то, что в зависимости от компилятора и низкоуровневой архитектуры аппаратного обеспечения эта операция сборки может быть выполнена лишь в ответ на прерывание ядра «обращение к невыровненным данным», обработка которого требует своих (и немалых) накладных расходов. А это вам не семечки щелкать: такая дополнительная гимнастика легко снижает скорость доступа на несколько порядков.
Вот почему современные компиляторы располагают данные в памяти с*отступами*. Компилятор вставляет в объект дополнительные байты, чтобы обеспечить расположение всех полей с удобными смещениями. Таким образом, выделение под объекты областей памяти с адресами, кратными слову, гарантирует быстрый доступ ко всем внутренним элементам этих объектов. На рис. 7.2 показано расположение полей типа `A` по схеме с отступами.
***Рис. 7.2.*** *Расположение полей типа `A` по схеме с отступами. Заштрихованные области – это отступы, вставленные для правильного выравнивания. Компилятор вставляет в объект две лакуны, тем самым добавляя 6 байт простаивающего места или 50% общего размера объекта*
Полученное расположение полей характеризуется обилием отступов (заштрихованных областей). В случае классов компилятор волен упорядочивать поля по собственному усмотрению, но при работе со структурой есть смысл позаботиться о расположении данных, если объем используемой памяти имеет значение. Лучше всего расположить поле типа `int` первым, а после него – два поля типа `char`. При таком порядке полей структура займет 64 бита, включая 2 байта отступа.
Каждое из полей объекта обладает известным во время компиляции смещением относительно начального адреса объекта. Это смещение всегда одинаково для всех объектов заданного типа в рамках одной программы (оно может меняться от компиляции к компиляции, но не от запуска к запуску). Смещение доступно пользовательскому коду как значение свойства `.offsetof`, неявно определенного для каждого поля класса или структуры:
Эталонная реализация компилятора выведет `0 4 8`, открывая схему расположения полей, которую мы уже видели на рис. 7.2. Не совсем удобно, что для доступа к некоторой статической информации о типе `A` приходится создавать объект этого типа, но синтаксис `A.a.offsetof` не компилируется. Здесь поможет такой трюк: выражение `A.init.a.offsetof` позволяет получить смещение для любого внутреннего элемента структуры в виде константы, известной во время компиляции.
Чтобы перекрыть выбор компилятора, определив собственное выравнивание, что повлияет на вставляемые отступы, объявляйте поля с атрибутом `align`. Такое переопределение может понадобиться для взаимодействия с определенной аппаратурой или для работы по бинарному протоколу, задающему особое выравнивание. Пример атрибута `align` в действии:
При таком определении поля структуры `A` располагаются без пустот между ними. (В конце объекта при этом может оставаться зарезервированное, но не занятое место.) Аргумент атрибута `align` означает *максимальное* выравнивание поля, но реальное выравнивание не может превысить естественное выравнивание для типа этого поля. Получить естественное выравнивание типа `T` позволяет определенное компилятором свойство `T.alignof`. Если вы, например, укажете для `b` выравнивание `align(200)` вместо указанного в примере `align(1)`, то реально выравнивание примет значение `4`, равное `int.alignof`.
Для структуры атрибут `align` устанавливает выравнивание по умолчанию заданным значением. Это умолчание можно переопределить индивидуа льными атрибутами `align` внутри структуры. Если для поля типа `T` указать только ключевое слово `align` без числа, компилятор прочитает это как `align(T.alignof)`, то есть такая запись переустанавливает выравнивание поля в его естественное значение.
Атрибут `align` не предназначен для использования с указателями и ссылками. Сборщик мусора действует из расчета, что все ссылки и указатели выровнены по размеру типа `size_t`. Компилятор не настаивает на соблюдении этого ограничения, поскольку в общем случае у вас могут быть указатели и ссылки, не контролируемые сборщиком мусора. Таким образом, следующее определение крайне опасно, поскольку компилируется без предупреждений:
Если этот код выполнит присваивание `объект.next = new Node` (то есть заполнит `объект.next` ссылкой, контролируемой сборщиком мусора), хаос обеспечен: неверно выровненная ссылка пропадает из поля зрения сборщика мусора, память может быть освобождена, и `объект.next` превращается в «висячий» указатель.
Объединение (`union`) – это что-то вроде структуры, все внутренние поля которой начинаются по одному и тому же адресу. Таким образом, их области памяти перекрываются, а это значит, что именно вы как пользователь объединения отвечаете за соответствие записываемой и считываемой информации: нужно всегда читать в точности тот тип, который был записан. В любой конкретный момент времени только один внутренний элемент объединения обладает корректным значением.
Поскольку типы `int` и `float` имеют строго один и тот же размер (4 байта), внутри объединения `IntOrFloat` их области памяти в точности совпадают. Но детали их расположения не регламентированы, например, представления `_int` и `_float` могут отличаться порядком хранения байтов: старший байт `_int` может иметь наименьший адрес, а старший байт `_float` (тот, что содержит знак и большую часть показателя степени) – наибольший адрес.
Объединения не помечаются, то есть сам объект типа `union` не содержит «метки», которая служила бы средством, позволяющим определять, какой из внутренних элементов является «хорошим». Ответственность за корректное использование объединения целиком ложится на плечи пользователя, что делает объединения довольно неприятным средством при построении более крупных абстракций.
В определенном, но неинициализированном объекте типа `union` уже есть одно инициализированное поле: первое поле автоматически инициализируется соответствующим значением `.init`, поэтому оно доступно для чтения сразу по завершении построения по умолчанию. Чтобы инициализировать первое поле значением, отличным от `.init`, укажите нужное инициализирующее выражение в фигурных скобках:
Следует отметить, что нередко объединение служит именно для того, чтобы считывать тип, отличный от исходно записанного, – в соответствии с порядком управления представлением, принятым в некоторой системе. По этой причине компилятор не выявляет даже те случаи некорректного использования объединений, которые может обнаружить. Например, на 32-разрядной машине Intel следующий код компилируется и даже выполнение инструкции `assert` не порождает исключений:
Объединение может определять функции-члены и, в общем случае, любые из тех внутренних элементов, которые может определять структура, за исключением конструкторов и деструкторов.
Этот пример демонстрирует чисто классический способ использования `union` в качестве вспомогательного средства для определения так называемого *размеченного объединения* (*discriminated union*, *tagged union*), также известного как алгебраический тип. Размеченное объединение инкапсулирует небезопасный объект типа `union` в «безопасной коробке», которая отслеживает последний присвоенный тип. Сразу после инициализации поле `Tag` содержит значение `Tag._tvoid`, по сути означающее, что объект не инициализирован. При присваивании объединению некоторого значения срабатывает оператор `opAssign`, устанавливающий тип объекта в соответствии с типом присваиваемого значения. Чтобы получить законченную реализацию, потребуется определить методы `opAssign(double)`, `opAssign(string)` и `opAssign(TaggedUnion[])`с соответствующими функциями `getXxx()`.
Внутренний элемент типа `union` анонимен, то есть одновременно является и определением типа, и определением внутреннего элемента. Память под анонимное объединение выделяется как под обычный внутренний элемент структуры, и внутренние элементы этого объединения напрямую видимы внутри структуры (как показывают методы `TaggedUnion`). В общем случае можно определять как анонимные структуры, так и анонимные объединения, и вкладывать их как угодно.
В конце концов вы должны понять, что объединение не такое уж зло, каким может показаться. Как правило, использовать объединение вместо того, чтобы играть типами с помощью выражения `cast`, – хороший тон в общении между программистом и компилятором. Объединение указателя и целого числа указывает сборщику мусора, что ему следует быть осторожнее и не собирать этот указатель. Если вы сохраните указатель в целом числе и будете время от времени преобразовывать его назад к типу указателя (с помощью `cast`), результаты окажутся непредсказуемыми, ведь сборщик мусора может забрать память, ассоциированную с этим тайным указателем.
Типы, принимающие всего несколько определенных значений, оказались очень полезными – настолько полезными, что язык Java после нескольких лет героических попыток эмулировать перечисляемые типы с помощью идиомы в конце концов добавил их к основным типам. Определить хорошие перечисляемые типы непросто – в C (и особенно в C++) типу `enum` присущи свои странности. D попытался учесть предшествующий опыт, определив простое и полезное средство для работы с перечисляемыми типами.
С`enum` механизм автоматического определения типа работает так же, как и с`auto`, поэтому в нашем примере переменные `pi` и `euler` имеют тип `double`, a переменная `greet`– тип `string`. Чтобы определить одно или несколько перечисляемых значений определенного типа, укажите их справа от ключевого слова `enum`:
Перечисляемые значения – это константы; они практически эквивалентны литералам, которые обозначают. В частности, поддерживают те же операции – например, невозможно получить адрес `pi`, как невозможно получить адрес `3.14`:
Как показано выше, типы перечисляемых значений не ограничиваются типом `int`– типы `double` и `string` также допустимы. Какие вообще типы можно использовать с`enum`? Ответ прост: c `enum` можно использовать любой основной тип и любую структуру. Есть лишь два требования к инициализирующему значению при определении перечисляемых значений:
Первое требование гарантирует независимость перечисляемого значения от параметров времени исполнения. Второе требование обеспечивает возможность копировать значение; копия создается при каждом обращении к перечисляемому значению.
Невозможно определить перечисляемое значение типа `class`, поскольку объекты классов должны всегда создаваться с помощью оператора `new` (за исключением не представляющего интерес значения `null`), а выражение с`new` во время компиляции вычислить невозможно. Не будет неожиданностью, если в будущем это ограничение снимут или ослабят.
Когда бы вы ни использовали, например, идентификатор `green`, код будет вести себя так, будто вместо этого идентификатора вы написали `Color(0, 255, 0)`.
Члены именованной группы перечисляемых значений не могут иметь разные типы; все перечисляемые значения должны иметь один и тот же тип, поскольку пользователи могут впоследствии определять и использовать значения этого типа. Например:
С новым определением (`byte` называют *базовым типом*`OddWord`) значения идентификаторов перечисления не меняются, изменяется лишь способ их хранения. Вы можете с таким же успехом назначить членам перечисления тип `double` или `real`, но связанные с идентификаторами значения останутся прежними: `0`, `1` и т. д. Но если сделать базовым типом `OddWord` нечисловой тип, например `string`, то придется указать инициализирующее значение для каждого из значений, поскольку компилятору неизвестна никакая естественная последовательность, которой он мог бы придерживаться.
Возвратимся к числовым перечислениям. Присвоив какому-либо члену перечисления особое значение, вы таким образом сбросите счетчик, используемый компилятором для присваивания значений идентификаторам. Например:
Если два идентификатора перечисления получают одно и то же значение (как в случае с`E.a` и `E.e`), конфликта нет. Фактически равные значения можно создавать, даже не подозревая об этом – из-за непреодолимого желания типов с плавающей запятой удивить небдительных пользователей:
Корень этой проблемы в том, что наибольшее значение типа `int`, которое может быть представлено значением типа `float`, равно `16_777_216`, и выход за эту границу сопровождается все возрастающими диапазонами целых значений, представляемых одним и тем же числом типа `float`.
Для всякого перечисляемого типа `E` определены три свойства: `E.init` (это свойство принимает первое из значений, определенных в `E`), `E.min` (наименьшее из определенных в `E` значений) и `E.max` (наибольшее из определенных в `E` значений). Два последних значения определены, только если базовым типом `E` является тип, поддерживающий сравнение во время компиляции с помощью оператора `<`.
Вы вправе определить внутри `enum` собственные значения `min`, `max` и `init`, но поступать так не рекомендуется: обобщенный код частенько рассчитывает на то, что эти значения обладают особой семантикой.
Один из часто задаваемых вопросов: «Можно ли добраться до имени перечисляемого значения?» Вне всяких сомнений, сделать это возможно и на самом деле легко, но не с помощью встроенного механизма, а на основе рефлексии времени компиляции. Рефлексия работает так: с некоторым перечисляемым типом `Enum` связывается известная во время компиляции константа `__traits(allMembers, Enum)`, которая содержит все члены `Enum` в виде кортежа значений типа `string`. Поскольку строками можно манипулировать во время компиляции, как и во время исполнения, такой подход дает значительную гибкость. Например, немного забежав вперед, напишем функцию `toString`, которая возвращает строку, соответствующую заданному перечисляемому значению. Функция параметризирована перечисляемым типом.
Незнакомое пока выражение `mixin("E." ~ s)`– это *выражение*`mixin`. Выражение `mixin` принимает строку, известную во время компиляции, и просто вычисляет ее как обычное выражение в рамках текущего контекста. В нашем примере это выражение включает имя перечисления `E`, оператор `.` для выбора внутренних элементов и переменную `s` для перебора идентификаторов перечисляемых значений. В данном случае s последовательно принимает значения `"acini"`, `"alembicated"`, ..., `"aprosexia"`. Таким образом, конкатенированная строка примет вид `"E.acini"` и т. д., а выражение `mixin` вычислит ее, сопоставив указанным идентификаторам реальные значения. Обнаружив, что переданное значение равно очередному значению, вычисленному выражением `mixin`, функция `toString` возвращает результат. Получив некорректный аргумент value, функция `toString` могла бы порождать исключение, но чтобы упростить себе жизнь, мы решили просто возвращать константу `null`.
Рассмотренная функция `toString` уже реализована в модуле `std.conv` стандартной библиотеки, имеющем дело с общими преобразованиями. Имя этой функции немного отличается от того, что использовали мы: вам придется писать `to!string(w)` вместо `toString(w)`, что говорит о гибкости этой функции (также можно сделать вызов `to!dstring(w)` или `to!byte(w)` и т. д.). Этот же модуль определяет и обратную функцию, которая конвертирует строку в значение перечисляемого типа; например вызов `to!OddWord("acini")` возвращает `OddWord.acini`.
В ряде случаев мы уже имели дело с`size_t`– целым типом без знака, достаточно вместительным, чтобы представить размер любого объекта. Тип `size_t` не определен языком, он просто принимает форму `uint` или `ulong` в зависимости от адресного пространства конечной системы (32 или 64 бита соответственно).
Если бы вы открыли файл object.di, один из копируемых на компьютер пользователя (а значит, и на ваш) при инсталляции компилятора D, то нашли бы объявление примерно следующего вида:
Свойство `.sizeof` точно измеряет размер типа в байтах; в данном случае это тип `int`. Вместо `int` в примере мог быть любой другой тип; в данном случае имеет значение не указанный тип, а тип размера, возвращаемый оператором `typeof`. Компилятор измеряет размеры объектов, используя `uint` на 32-разрядных архитектурах и `ulong` на 64-разрядных. Следовательно, конструкция `alias` позволяет назначить `size_t` синонимом `uint` или `ulong`.
В качестве идентификатора `‹существующийИдентификатор›` можно подставить все, у чего есть имя. Это может быть тип, переменная, модуль – если что-то обладает идентификатором, то для этого объекта можно создать псевдоним. Например:
Правила применения псевдонима просты: используйте псевдоним везде, где допустимо использовать исходный идентификатор. Именно это делает компилятор, но с точностью до наоборот: он с пониманием заменяет идентификатор-псевдоним оригинальным идентификатором. Даже сообщения об ошибках и отлаживаемая программа могут «видеть сквозь» псевдонимы и показывать исходные идентификаторы, что может оказаться неожиданным. Например, в некоторых сообщениях об ошибках или в отладочных символах можно увидеть `immutable(char)[]` вместо `string`. Но что именно будет показано, зависит от реализации компилятора.
Здесь нет ничего особенного, просто следование обычным правилам: к моменту определения псевдонима `MyInt` псевдоним `Int` уже будет заменен исходным идентификатором `int`, для которого `Int` является псевдонимом.
Конструкцию `alias` часто применяют, когда требуется дать сложной цепочке идентификаторов более короткое имя или в связке с перегруженными функциями из разных модулей (см. раздел 5.5.2).
Здесь общедоступный псевдоним `ElementType`, созданный классом `Container`, – единственный разумный способ обратиться из внешнего мира к аргументу, привязанному к параметру `T` класса `Container`. Идентификатор `T` видим лишь внутри определения класса `Container`, но не снаружи: выражение `Container!int.T` не компилируется.
С помощью объявления псевдоним `ptrdiff_t` привязывается к разным типам в зависимости от того, по какой ветке статического условия пойдет поток управления. Без этой возможности привязки код, которому потребовался такой тип, пришлось бы разместить в одной из веток `static if`.
Мы уже рассмотрели средства, облегчающие параметризацию во время компиляции (эти средства сродни шаблонам из C++ и родовым типам из языков Java и C#), – это функции (см. раздел 5.3), параметризированные классы (см. раздел 6.14) и параметризированные структуры, которые подчиняются тем же правилам, что и параметризированные классы. Тем не менее иногда во время компиляции требуется каким-либо образом манипулировать типами, не определяя функцию, структуру или класс. Один из механизмов, подходящих под это описание (широко используемый в C++), – выбор того или иного типа в зависимости от статически известного логического условия. При этом не определяется никакой новый тип и не вызывается никакая функция – лишь создается псевдоним для одного из существующих типов.
Для случаев, когда требуется организовать параметризацию во время компиляции без определения нового типа или функции, D предоставляет параметризированные контексты. Такой параметризированный контекст вводится следующим образом:
Этот код – на самом деле лишь каркас для только что упомянутого механизма выбора во время компиляции. Скоро мы доберемся и до реализации, а пока сосредоточимся на порядке объявления. Объявление с ключевым словом `template` вводит именованный контекст (в данном случае это `Select`) с параметрами, вычисляемыми во время компиляции (в данном случае это логическое значение и два типа). Объявить контекст можно на уровне модуля, внутри определения класса, внутри определения структуры, внутри любого другого объявления контекста, но не внутри определения функции.
В теле параметризированного контекста разрешается использовать все те же объявления, что и обычно, кроме того, могут быть использованы параметры контекста. Доступ к любому объявлению контекста можно получить извне, расположив перед его именем имя контекста и `.`, например: `Select!(true, int, double).foo`. Давайте прямо сейчас закончим определение контекста `Select`, чтобы можно было поиграть с ним:
Заметим, что тот же результат мы могли бы получить на основе структуры или класса, поскольку эти типы могут определять в качестве своих внутренних элементов псевдонимы, доступные с помощью обычного синтаксиса с оператором `.` (точка):
Согласитесь, такое решение выглядит не очень привлекательно. К примеру, для `Select2` в документации пришлось бы написать: «Не создавайте объекты типа `Select2`! Он определен только ради псевдонима внутри него!» Доступный специализированный механизм определения параметризированных контекстов позволяет избежать двусмысленности намерений, не вызывает недоумения и исключает возможность некорректного использования.
В контексте, определенном с ключевым словом `template`, можно объявлять не только псевдонимы – там могут присутствовать самые разные объявления. Определим еще один полезный шаблон. На этот раз это будет шаблон, возвращающий логическое значение, которое сообщает, является ли заданный тип строкой (в любой кодировке):
Несмотря на то что `factorial` является совершенным функциональным определением, в данном случае это не лучший подход. При необходимости вычислять значения во время компиляции, пожалуй, стоило бы воспользоваться механизмом вычислений во время компиляции (см. раздел 5.12). В отличие от приведенного выше шаблона `factorial`, функция `factorial` более гибка, поскольку может вычисляться как во время компиляции, так и во время исполнения. Конструкция `template` больше всего подходит для манипуляции типами, имеющей место в `Select` и `isSomeString`.
Конструкция `template` может определять любое количество идентификаторов, но, как видно из предыдущих примеров, нередко в ней определен ровно один идентификатор. Обычно шаблон определяется лишь с целью решить единственную задачу и в качестве результата сделать доступным единственный идентификатор (такой как `Type` в случае `Select` или `value` в случае `isSomeString`).
Необходимость помнить о том, что в конце вызова надо указать этот идентификатор, и всегда его указывать может раздражать. Многие просто забывают добавить в конец `.Type`, а потом удивляются, почему вызов `Select!(cond, A, B)` порождает таинственное сообщение об ошибке.
D помогает здесь, определяя правило, известное как фокус с одноименным шаблоном: если внутри конструкции `template` определен идентификатор, совпадающий с именем самого шаблона, то при любом последующем использовании имени этого шаблона в его конец будет автоматически дописываться одноименный идентификатор. Например:
Если теперь некоторый код использует выражение `isNumeric!(T)`, компилятор в каждом случае автоматически заменит его на `isNumeric!(T).isNumeric`, чем освободит пользователя от хлопот с добавлением идентификатора в конец имени шаблона.
Шаблон, проделывающий фокус с «тезками», может определять внутри себя и другие идентификаторы, но они будут попросту недоступны за пределами этого шаблона. Дело в том, что компилятор заменяет идентификаторы на раннем этапе процесса поиска имен. Единственный способ получить доступ к таким идентификаторам – обратиться к ним из тела самого шаблона. Например:
Это сообщение об ошибке вызвано соблюдением правила об одноименности: перед тем как делать что-либо еще, компилятор расширяет вызов `isNumeric!(int)` до `isNumeric!(int).isNumeric`. Затем пользовательский код делает попытку заполучить значение `isNumeric!(int).isNumeric.test1`, что равносильно попытке получить внутренний элемент `test1` из логического значения, отсюда и сообщение об ошибке. Короче говоря, используйте одноименные шаблоны тогда и только тогда, когда хотите открыть доступ лишь к одному идентификатору. Этот случай скорее частый, чем редкий, поэтому одноименные шаблоны очень популярны и удобны.
В обычный статический метод класса не передаются никакие скрытые параметры, поэтому невозможно определить, для какого конкретно класса вызван этот метод. В приведенном примере компилятор создает три экземпляра шаблонного метода `Parent.getName(this T)()`: `Parent.getName()`, `Derived1.getName()` и `Derived2.getName()`.
При некоторых программных решениях приходится добавлять шаблонный код (такой как определения данных и методов) в одну или несколько реализаций классов. К типичным примерам относятся поддержка сериализации, шаблон проектирования «Наблюдатель» и передача событий в оконных системах.
Для этих целей можно было бы воспользоваться механизмом наследования, но поскольку реализуется лишь одиночное наследование, определить для заданного класса несколько источников шаблонного кода невозможно. Иногда необходим механизм, позволяющий просто вставить в класс некоторый готовый код, вместо того чтобы писать его вручную.
Здесь-то и пригодится конструкция `mixin template` (шаблон `mixin`). Стоит отметить, что сейчас это средство в основном экспериментальное. Возможно, в будущих версиях языка шаблоны `mixin` заменит более общий инструмент AST-макросов.
Шаблон `mixin` определяется почти так же, как параметризированный контекст (шаблон), о котором недавно шла речь. Пример шаблона `mixin`, определяющего переменную и функции для ее чтения и записи:
Теперь этот код определяет переменную и две обслуживающие ее функции на уровне модуля, внутри класса `A` и внутри функции `fun`– как будто тело `InjectX` было вставлено вручную. В частности, потомки класса `A` могут переопределять методы `getX` и `setX`, как если бы сам класс определял их. Копирование и вставка без неприятного дублирования кода – вот что такое `mixin template`.
Конечно же, следующий логический шаг – подумать о том, что `InjectX` не принимает никаких параметров, но производит впечатление, что мог бы, – и действительно может:
Но на самом деле такие вставки приводят к двусмысленности: что если вы сделаете две рассмотренные подстановки, а затем пожелаете воспользоваться функцией `getX`? Есть две функции с этим именем, так что проблема с двусмысленностью очевидна. Чтобы решить этот вопрос, D позволяет вводить *имена* для конкретных подстановок в шаблоны `mixin`:
Таким образом, шаблоны `mixin`– это *почти* как копирование и вставка; вы можете многократно копировать и вставлять код, а потом указывать, к какой именно вставке хотите обратиться.
Самая большая разница между шаблоном `mixin` и обычным шаблоном (в том виде, как он определен в разделе 7.5), способная вызвать больше всего вопросов, – это поиск имен.
Шаблоны исключительно модульны: код внутри шаблона ищет идентификаторы в месте *определения* шаблона. Это положительное качество: оно гарантирует, что, проанализировав определение шаблона, вы уже ясно представляете его содержимое и осознаете, как он работает.
Шаблон `mixin`, напротив, ищет идентификаторы в месте *подстановки*, а это означает, что понять поведение шаблона `mixin` можно только с учетом контекста, в котором вы собираетесь этот шаблон использовать.
Склонность шаблонов `mixin` привязываться к локальным идентификаторам придает им выразительности, но следовать их логике становится сложно. Такое поведение делает шаблоны `mixin` применимыми лишь в ограниченном количестве случаев; прежде чем доставать из ящика с инструментами эти особенные ножницы, необходимо семь раз отмерить.
Классы позволяют эффективно представить далеко не любую абстракцию. Например, они не подходят для мелкокалиберных объектов, контекстно-зависимых ресурсов и типов значений. Этот пробел восполняют структуры. В частности, благодаря конструкторам и деструкторам легко определять типы контекстно-зависимых ресурсов.
Перечисления – это обычные отдельные значения, определенные пользователем. Перечислению может быть назначен новый тип, что позволяет более точно проверять типы значений, определенных в рамках этого типа.
`alias`– очень полезное средство, позволяющее привязать один идентификатор к другому. Нередко псевдоним – единственное средство получить извне доступ к идентификатору, вычисляемому в рамках вложенной сущности, или к длинному и сложному идентификатору.
Параметризированные контексты, использующие конструкцию `template`, весьма полезны для определения вычислений во время компиляции, таких как интроспекция типов и определение особенностей типов. Одноименные шаблоны позволяют предоставлять абстракции в очень удобной, инкапсулированной форме.
Кроме того, предлагаются параметризированные контексты, принимающие форму шаблонов `mixin`, которые во многом ведут себя подобно макросам. В будущем шаблоны `mixin` может заменить развитое средство AST-макросов.
[^3]: Кроме того, `‹код1›` может сохранить указатель на значение `w`, которое использует `‹код2›`.
[^4]: На момент написания оригинала книги данная возможность отсутствовала, но поскольку теперь она существует, мы добавили ее описание в перевод. –*Прим. науч. ред.*