7ed9f244ea | ||
---|---|---|
.. | ||
images | ||
README.md |
README.md
7. Другие пользовательские типы
- 7.1. Структуры
- 7.1.1. Семантика копирования
- 7.1.2. Передача объекта-структуры в функцию
- 7.1.3. Жизненный цикл объекта-структуры
- 7.1.3.1. Конструкторы
- 7.1.3.2. Делегирование конструкторов
- 7.1.3.3. Алгоритм построения
- 7.1.3.4. Конструктор копирования this(this)
- 7.1.3.5. Аргументы в пользу this(this)
- 7.1.3.6. Уничтожение объекта и освобождение памяти
- 7.1.3.7. Алгоритм уничтожения структуры
- 7.1.4. Статические конструкторы и деструкторы
- 7.1.5. Методы
- 7.1.5.1. Оператор присваивания
- 7.1.5.2. Сравнение структур на равенство
- 7.1.6. Статические внутренние элементы
- 7.1.7. Спецификаторы доступа
- 7.1.8. Вложенность структур и классов
- 7.1.9. Структуры, вложенные в функции
- 7.1.10. Порождение подтипов в случае структур. Атрибут @disable
- 7.1.11. Взаимное расположение полей. Выравнивание
- 7.1.11.1. Атрибут align
- 7.2. Объединение
- 7.3. Перечисляемые значения
Применяя классы, основные типы и функции, можно написать много хороших программ. С параметризированными классами и функциями дело идет еще лучше. Но нередко мы с сожалением отмечаем, что по нескольким причинам классы не представляют собой инструмент с максимальной абстракцией типа.
Во-первых, классы подчиняются ссылочной семантике и из-за этого могут воплощать многие проектные решения не полностью или с ощутимыми накладными расходами. На практике трудно моделировать с помощью класса такую простую сущность, как точка с двумя или тремя координатами, если таких точек больше нескольких миллионов: разработчик оказывается перед непростым выбором – хорошая абстракция или приемлемое быстродействие. Кроме того, для линейной алгебры ссылочная семантика – большая морока. Попробуйте убедить математика или программиста-теоретика, что присваивание a = b
должно делать из матрицы a лишь псевдоним матрицы b
, а не отдельную копию! Даже такой простой тип, как массив, довольно накладно моделировать в виде класса в сравнении с мощной и лаконичной абстракцией массива, имеющейся в языке D (см. главу 4). Можно, конечно, сделать массивы «волшебными», но опыт то и дело показывает, что предоставлять множество «волшебных» типов, не воспроизводимых в пользовательском коде, – дурной тон и признак плохо спроектированного языка. Затраты на массив – всего два слова, а выделение памяти под экземпляр класса и использование дополнительного косвенного обращения означают большие накладные расходы по памяти и времени для всех примитивов массива. Даже такой простой тип, как int
, нельзя выразить в виде класса дешево и элегантно (причем речь не об удобстве оператора). У такого класса, как BigInt
, та же проблема: a = b
делает нечто совершенно иное,
чем соответствующая операция присваивания для типа int
.
Во-вторых, классы живут вечно, а значит, с их помощью трудно моделировать ресурсы с выраженным конечным временем жизни (такие как дескрипторы файлов, дескрипторы графического контекста, мьютексы, сокеты и т. д.). Работая с такими ресурсами как с классами, нужно постоянно быть начеку, чтобы не забыть своевременно освободить инкапсулированные ресурсы с помощью метода, вроде close
или dispose
. В таких случаях обычно помогает инструкция scope
(см. раздел 3.13), но лучше, когда подобная контекстная семантика инкапсулирована в типе – раз и навсегда.
В-третьих, классы – это механизм для довольно «тяжелых» и высокоуровневых абстракций, то есть они не позволяют легко выражать «легковесные» абстракции вроде перечисляемых типов или псевдонимов для заданного типа.
D не был бы настоящим языком для системного программирования, если бы предоставлял для выражения абстракций только классы. Кроме классов в запасе у D есть структуры (типы-значения, сравнимые с классами по мощности, но с семантикой значения и без полиморфизма), типы enum
(легковесные перечисляемые типы и простые константы), объединения (низкоуровневое хранилище с перекрыванием для разных типов) и вспомогательные механизмы определения типов, такие как alias
. Все эти средства последовательно рассматриваются в этой главе.
7.1. Структуры
Структуры позволяют определять простые, инкапсулированные типы-значения. Удобная аналогия – тип 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
(иначе предполагалось бы наличие структур-потомков).
Определим простую структуру:
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.1. Семантика копирования
Несколько заметных на глаз различий между структурами и классами есть следствие менее очевидных семантических различий. Повторим эксперимент, который мы уже проводили, обсуждая классы в разде- ле 6.2. На этот раз создадим структуру и объект с одинаковыми поля ми, а затем сравним поведение этих типов при копировании:
class C
{
int x = 42;
double y = 3.14;
}
struct S
{
int x = 42;
double y = 3.14;
}
unittest
{
C c1 = new C;
S s1; // Никакого оператора new для S: память выделяется в стеке
auto c2 = c1;
auto s2 = s1;
c2.x = 100;
s2.x = 100;
assert(c1.x == 100); // c1 и c2 ссылаются на один и тот же объект...
assert(s1.x == 42); // ...а s2 – это настоящая копия s1
}
При работе со структурами нет никаких ссылок, которые можно привя
зывать и перепривязывать с помощью операций инициализации и при
сваивания. Каждое имя экземпляра структуры связано с отдельным
значением. Как уже говорилось, объект-структура ведет себя как значение, а объект-класс – как ссылка. На рис. 7.1 показано положение дел
сразу после определения c2
и s2
.
Рис. 7.1. Инструкции auto c2 = c1;
для объекта-класса c1
и auto s2 = s1;
для объекта-структуры s1
действуют совершенно по-разному, поскольку класс по своей природе – ссылка, а структура – значение
В отличие от имен c1
и с2
, допускающих привязку к любому объекту,
имена s1
и s2
прочно привязаны к реальным объектам. Нет способа за
ставить два имени ссылаться на один и тот же объект-структуру (кроме
ключевого слова alias
, задающего простую эквивалентность имен; см.
раздел 7.4), и не бывает имени структуры без закрепленного за ним зна
чения, так что сравнение s1 is null
бессмысленно и порождает ошибку
во время компиляции.
7.1.2. Передача объекта-структуры в функцию
Поскольку объект типа struct
ведет себя как значение, он и передается
в функцию по значению.
struct S
{
int a, b, c;
double x, y, z;
}
void fun(S s)
{
// fun получает копию
...
}
Передать объект-структуру по ссылке можно с помощью аргумента
с ключевым словом ref
(см. раздел 5.2.1):
void fun(ref S s) // fun получает ссылку
{
...
}
Раз уж мы заговорили о ref
, отметим, что this
передается по ссылке
внутрь методов структуры S
в виде скрытого параметра ref S
.
7.1.3. Жизненный цикл объекта-структуры
В отличие от объектов-классов, объектам-структурам не свойственно бесконечное время жизни (lifetime). Время жизни для них четко огра ничено – так же как для временных (стековых) объектов функций. Чтобы создать объект-структуру, задайте имя нужного типа, как если бы вы вызывали функцию:
import std.math;
struct Test
{
double a = 0.4;
double b;
}
unittest
{
// Чтобы создать объект, используйте имя структуры так, как используете функцию
auto t = Test();
assert(t.a == 0.4 && IsNaN(t.b));
}
Вызов Test()
создает объект-структуру, все поля которого инициализи
рованы по умолчанию. В нашем случае это означает, что поле t.a
при
нимает значение 0.4
, а t.b
остается инициализированным значением
double.init
.
Вызовы Test(1)
и Test(1.5, 2.5)
также разрешены и инициализируют по
ля объекта в порядке их объявления. Продолжим предыдущий пример:
unittest
{
auto t1 = Test(1);
assert(t1.a == 1 && IsNaN(t1.b));
auto t2 = Test(1.5, 2.5);
assert(t2.a == 1.5 && t2.b == 2.5);
}
Поначалу может раздражать разница в синтаксисе выражения, создаю
щего объект-структуру Test(‹аргументы›)
, и выражения, создающего объ
ект-класс new Test(‹аргументы›)
. D мог бы отказаться от использования
ключевого слова new при создании объектов-классов, но это new
напоми
нает программисту, что выполняется операция выделения памяти (то
есть необычное действие).
7.1.3.1. Конструкторы
Конструктор структуры определяется так же, как конструктор класса (см. раздел 6.3.1):
struct Test
{
double a = 0.4;
double b;
this(double b)
{
this.b = b;
}
}
unittest
{
auto t = Test(5);
}
Присутствие хотя бы одного пользовательского конструктора блокирует все упомянутые выше конструкторы, инициализирующие поля струк туры:
auto t1 = Test(1.1, 1.2); // Ошибка! Нет конструктора, соответствующего вызову Test(double, double)
Есть важное исключение: компилятор всегда определяет конструктор без аргументов:
auto t2 = Test(); // Все в порядке, создается объект с "начинкой" по умолчанию
Кроме того, пользовательский код не может определить собственный конструктор без аргументов:
struct Test
{
double a = 0.4;
double b;
this() { b = 0; } // Ошибка! Структура не может определить конструктор по умолчанию!
}
Зачем нужно такое ограничение? Все из-за T.init
– значения по умолча
нию, определяемого каждым типом. Оно должно быть статически из
вестно, что противоречит существованию конструктора по умолчанию,
выполняющего произвольный код. (Для классов T.init
– это пустая
ссылка null
, а не объект, построенный по умолчанию.) Правило для всех
структур: конструктор по умолчанию инициализирует все поля объек
та-структуры значениями по умолчанию.
7.1.3.2. Делегирование конструкторов
Скопируем пример из раздела 6.3.2 с заменой ключевого слова class
на
struct
:
struct Widget
{
this(uint height)
{
this(1, height); // Положиться на другой конструктор
}
this(uint width, uint height)
{
this.width = width;
this.height = height;
}
uint width, height;
...
}
Код запускается, не требуя внесения каких-либо других изменений. Так же как и классы, структуры позволяют одному конструктору деле гировать построение объекта другому конструктору с теми же ограни чениями.
7.1.3.3. Алгоритм построения
Классу приходится заботиться о выделении динамической памяти
и инициализации своего базового подобъекта (см. раздел 6.3.3). Со
структурами все гораздо проще, поскольку выделение памяти – явный
шаг алгоритма построения. Алгоритм построения объекта-структуры
типа T
по шагам:
- Скопировать значение
T.init
в память, где будет размещен объект, путем копирования «сырой» памяти (а-ляmemcpy
). - Вызвать конструктор, если нужно.
Если инициализация некоторых или всех полей структуры выглядит
как = void
, объем работ на первом шаге можно сократить, хотя и редко
намного, зато такой маневр часто порождает трудноуловимые ошибки
в вашем коде (тем не менее случай оправданного применения сокра
щенной инициализации иллюстрирует пример с классом Transmogrifier
в разделе 6.3.3).
7.1.3.4. Конструктор копирования this(this)
Предположим, требуется определить объект, который содержит ло
кальный (private
) массив и предоставляет ограниченный API для ма
нипуляции этим массивом:
struct Widget
{
private int[] array;
this(uint length)
{
array = new int[length];
}
int get(size_t offset)
{
return array[offset];
}
void set(size_t offset, int value)
{
array[offset] = value;
}
}
У класса Widget
, определенного таким образом, есть проблема: при ко
пировании объектов типа Widget
между копиями создается отдаленная
зависимость. Судите сами:
unittest
{
auto w1 = Widget(10);
auto w2 = w1;
w1.set(5, 100);
w2.set(5, 42); // Также изменяет элемент w1.array[5]!
assert(w1.get(5) == 100); // Не проходит!?!
}
В чем проблема? Копирование содержимого w1
в w2
«поверхностно», то
есть оно выполняется поле за полем, без транзитивного копирования,
на какую бы память косвенно ни ссылалось каждое из полей. При ко
пировании массива память под новый массив не выделяется; копиру
ются лишь границы массива (см. раздел 4.1.4). После копирования w1
и w2
действительно обладают различными полями с массивами, но ссы
лаются эти поля на одну и ту же область памяти. Такой объект, являю
щийся значением, но содержащий неявные разделяемые ссылки, мож
но в шутку назвать «клуктурой», то есть гибридом структуры (семанти
ка значения) и класса (семантика ссылки)2.
Обычно требуется, чтобы структура действительно вела себя как значе ние, то есть чтобы копия становилась полностью независимой от своего источника. Для этого определите конструктор копирования так:
struct Widget
{
private int[] array;
this(uint length)
{
array = new int[length];
}
// Конструктор копирования
this(this)
{
array = array.dup;
}
// Как раньше
int get(size_t offset) { return array[offset]; }
void set(size_t offset, int value) { array[offset] = value; }
}
Конструктор копирования вступает в силу во время копирования объ
екта. Чтобы инициализировать объект приемник
с помощью объекта источник
того же типа, компилятор должен выполнить следующие шаги:
- Скопировать участок «сырой» памяти объекта
источник
в участок «сырой» памяти объектаприемник
. - Транзитивно для каждого поля, содержащего другие поля (то есть
поля, содержащего другое поле, содержащее третье поле, ...), для ко
торого определен метод
this(this)
, вызвать эти конструкторы снизу вверх (начиная от наиболее глубоко вложенного поля). - Вызвать метод
this(this)
с объектом приемник.
Оригинальное название конструктора копирования «postblit construc tor» происходит от «blit» – популярной аббревиатуры понятия «block transfer», означавшего копирование «сырой» памяти. Язык применяет «сырое» копирование при инициализации и разрешает сразу после это го воспользоваться ловушкой. В предыдущем примере конструктор ко пирования превращает только что полученный псевдоним массива в на стоящую, полномасштабную копию, гарантируя, что с этого момента между объектом-оригиналом и объектом-копией не будет ничего обще го. Теперь, после добавления конструктора копирования, модуль легко проходит этот тест:
unittest
{
auto w1 = Widget(10);
auto w2 = w1; // this(this) здесь вызывается с w2
w1.set(5, 100);
w2.set(5, 42);
assert(w1.get(5) == 100); // Пройдено
}
Вызов конструктора копирования вставляется в каждом случае копи
рования какого-либо объекта при явном или неявном создании новой
переменной. Например, при передаче объекта типа Widget
по значению
в функцию также создается копия:
void fun(Widget w) // Передать по значению
{
w.set(2, 42);
}
void gun(ref Widget w) // Передать по ссылке
{
w.set(2, 42);
}
unittest
{
auto w1 = Widget(10);
w1.set(2, 100);
fun(w1); // Здесь создается копия
assert(w1.get(2) == 100); // Тест пройден
gun(w1); // А здесь копирования нет
assert(w1.get(2) == 42); // Тест пройден
}
Второй шаг (часть с «транзитивным полем») процесса конструирования
при копировании заслуживает особого внимания. Основанием для та
кого поведения является инкапсуляция: конструктор копирования
объекта-структуры должен быть вызван даже тогда, когда эта структу
ра встроена в другую. Предположим, например, что мы решили сделать
Widget
членом другой структуры, которая в свою очередь является чле
ном третьей структуры:
struct Widget2
{
Widget w1;
int x;
}
struct Widget3
{
Widget2 w2;
string name;
this(this)
{
name = name ~ " (copy)";
}
}
Теперь, если потребуется копировать объекты, содержащие другие объ
екты типа Widget
, будет очень некстати, если компилятор забудет, как
нужно копировать подобъекты типа Widget
. Вот почему при копирова
нии объектов типа Widget2
инициируется вызов конструктора this(this)
для подобъекта w1
, невзирая на то, что Widget2
вообще об этом ничего не
знает. Кроме того, при копировании объектов типа Widget3
конструктор
this(this)
по-прежнему вызывается применительно к полю w1
поля w2
.
Внесем ясность:
unittest
{
Widget2 a;
a.w1 = Widget(10); // Выделить память под данные
auto b = a; // this(this) вызывается для b.w1
assert(a.w1.array !is b.w1.array); // Тест пройден
Widget3 c;
c.w2.w1 = Widget(20);
auto d = c; // this(this) вызывается для d.w2.w1
assert(c.w2.w1.array !is d.w2.w1.array); // Тест пройден
}
Вкратце, если вы определите для некоторой структуры конструктор ко
пирования this(this)
, компилятор позаботится о том, чтобы конструк
тор копирования вызывался в каждом случае копирования этого объ
екта-структуры независимо от того, является ли он самостоятельным
объектом или частью более крупного объекта-структуры.
7.1.3.5. Аргументы в пользу this(this)
Зачем был введен конструктор копирования? Ведь ничего подобного в других языках пока нет. Почему бы просто не передавать исходный объект в будущую копию (как это делает C++)?
// Это не D
struct S
{
this(S another) { ... }
// Или
this(ref S another) { ... }
}
Опыт с C++ показал, что основная причина неэффективности программ на C++ – злоупотребление копированием объектов. Чтобы сократить по тери эффективности по этой причине, C++ устанавливает ряд случаев, в которых компилятор может пропускать вызов конструктора копиро вания (copy elision). Правила для этих случаев очень быстро усложни лись, но все равно не охватывали все моменты, когда можно обойтись без конструирования, то есть проблема осталась не решенной. Развива ющийся стандарт C++ затрагивает эти вопросы, определяя новый тип «ссылка на r-значение», позволяющий пользователю управлять пропус ками вызова конструктора копирования, но плата за это – еще большее усложнение языка.
Благодаря конструктору копирования подход D становится простым и во многом автоматизируемым. Начнем с того, что объекты в D долж ны быть перемещаемыми, то есть не должны зависеть от своего располо жения: копирование «сырой» памяти позволяет переместить объект в другую область памяти, не нарушая его целостность. Тем не менее это ограничение означает, что объект не может содержать так называемые внутренние указатели – адреса подобъектов, являющихся его частя ми. Без этой техники можно обойтись, так что D попросту ее исключает. Создавать объекты с внутренними указателями в D запрещается, и ком пилятор, как и подсистема времени исполнения, вправе предполагать, что это правило соблюдается. Перемещаемые объекты открывают для компилятора и подсистемы времени исполнения (например, для сбор щика мусора) большие возможности, позволяющие программам стать более быстрыми и компактными.
Благодаря перемещаемости объектов копирование объектов становится
логическим продолжением перемещения объектов: конструктор копи
рования this(this)
делает копирование объектов эквивалентом переме
щения с возможной последующей пользовательской обработкой. Таким
образом, пользовательский код не может изменить поля исходного объ
екта (что очень хорошо, поскольку копирование не должно затрагивать
объект-источник), но зато может корректировать поля, которые не долж
ны неявно разделять состояние с объектом-источником. Чтобы избежать
лишнего копирования, компилятор вправе по собственному усмотре
нию не вставлять вызов this(this)
, если может доказать, что источник
копии не будет использован после завершения процесса копирования.
Рассмотрим, например, функцию, возвращающую объект типа Widget
(определенный выше) по значению:
Widget hun(uint x)
{
return Widget(x * 2);
}
unittest
{
auto w = hun(1000);
...
}
Наивный подход: просто создать объект типа Widget
внутри функции
hun
, а затем скопировать его в переменную w
, применив побитовое копи
рование с последующим вызовом this(this)
. Но это было бы слишком
расточительно: D полагается на перемещаемость объектов, так почему
бы попросту не переместить в переменную w
уже отживший свое времен
ный объект, созданный функцией hun
? Разницу никто не заметит, по
скольку после того, как функция hun
вернет результат, временный объ
ект уже не нужен. Если в лесу упало дерево и никто этого не слышит, то
легче переместить его, чем копировать. Похожий (но не идентичный)
случай:
Widget iun(uint x)
{
auto result = Widget(x * 2);
...
return result;
}
unittest
{
auto w = iun(1000);
...
}
В этом случае переменная result
тоже уходит в небытие сразу же после
того, как iun
вернет управление, поэтому в вызове this(this)
необходи
мости нет. Наконец, еще более тонкий случай:
void jun(Widget w)
{
...
}
unittest
{
auto w = Widget(1000);
... // ‹код1›
jun(w);
... // ‹код2›
}
В этом случае сложнее выяснить, можно ли избавиться от вызова
this(this)
. Вполне вероятно, что ‹код2›
продолжает использовать w
, и то
гда перемещение этого значения из unittest
в jun
было бы некоррект
ным3.
Ввиду всех перечисленных соображений в D приняты следующие пра вила пропуска вызова конструктора копирования:
- Все анонимные r-значения перемещаются, а не копируются. Вызов
конструктора копирования
this(this)
всегда пропускается, если ори гиналом является анонимное r-значение (то есть временный объект, как в функцииhun
выше). - В случае именованных временных объектов, которые создаются
внутри функции и располагаются в стеке, а затем возвращаются этой
функцией в качестве результата, вызов конструктора копирования
this(this)
пропускается. - Нет никаких гарантий, что компилятор воспользуется другими воз можностями пропустить вызов конструктора копирования.
Но иногда требуется предписать компилятору выполнить перемеще
ние. Фактически это выполняет функция move
из модуля std.algorithm
стандартной библиотеки:
import std.algorithm;
void kun(Widget w)
{
...
}
unittest
{
auto w = Widget(1000);
... // ‹код1›
// Вставлен вызов move
kun(move(w));
assert(w == Widget.init); // Пройдено
... // ‹код2›
}
Вызов функции move
гарантирует, что w
будет перемещена, а ее содержи
мое будет заменено пустым, сконструированным по умолчанию объек
том типа Widget
. Кстати, это один из тех случаев, где пригодится неизме
няемый и не порождающий исключения конструктор по умолчанию
Widget.init
(см. раздел 7.1.3.1). Без него сложно было бы найти способ ос
тавить источник перемещения в строго определенном пустом состоянии.
7.1.3.6. Уничтожение объекта и освобождение памяти
Структура может определять деструктор с именем ~this()
:
import std.stdio;
struct S
{
int x = 42;
~this()
{
writeln("Структура S с содержимым ", x, " исчезает. Пока!");
}
}
void main()
{
writeln("Создание объекта типа S.");
{
S object;
writeln("Внутри области видимости объекта ");
}
writeln("Вне области видимости объекта");
}
Эта программа гарантированно выведет на экран:
Создание объекта типа S.
Внутри области видимости объекта
Структура S с содержимым 42 исчезает. Пока!
Вне области видимости объекта.
Каждая структура обладает временем жизни в пределах области видимости (scoped lifetime), то есть ее жизнь действительно заканчивается с окончанием области видимости объекта. Подробнее:
- время жизни нестатического объекта, определенного внутри функ ции, заканчивается в конце текущей области видимости (то есть контекста) до уничтожения всех объектов-структур, определенных перед ним;
- время жизни объекта, определенного в качестве члена другой струк туры, заканчивается непосредственно после окончания времени жиз ни включающего объекта;
- время жизни объекта, определенного в контексте модуля, бесконеч но; если вам нужно вызвать деструктор этого объекта, сделайте это в деструкторе модуля (см. раздел 11.3);
- время жизни объекта, определенного в качестве члена класса, за канчивается в тот момент, когда сборщик мусора забирает память включающего объекта.
Язык гарантирует автоматический вызов деструктора ~this
по оконча
нии времени жизни объекта-структуры, что очень удобно, если вы хо
тите автоматически выполнять такие операции, как закрытие файлов
и освобождение всех важных ресурсов.
Оригинал копии, использующей конструктор копирования, подчиня
ется обычным правилам для времени жизни, но деструктор оригинала
копии, полученной перемещением «сырой» памяти без вызова this(this)
,
не вызывается.
Освобождение памяти объекта-структуры по идее выполняется сразу же после деструкции.
7.1.3.7. Алгоритм уничтожения структуры
По умолчанию объекты-структуры уничтожаются в порядке, строго обратном порядку их создания. То есть первым уничтожается объект- структура, определенный в заданной области видимости последним:
import std.conv, std.stdio;
struct S
{
private string name;
this(string name)
{
writeln(name, " создан.");
this.name = name;
}
~this()
{
writeln(name, " уничтожен.");
}
}
void main()
{
auto obj1 = S("первый объект");
foreach (i; 0 .. 3)
{
auto obj = S(text("объект ", i));
}
auto obj2 = S("последний объект");
}
Эта программа выведет на экран:
первый объект создан.
объект 0 создан.
объект 0 уничтожен.
объект 1 создан.
объект 1 уничтожен.
объект 2 создан.
объект 2 уничтожен.
последний объект создан.
последний объект уничтожен.
первый объект уничтожен.
Как и ожидалось, объект, созданный первым, был уничтожен послед ним. На каждой итерации цикл входит в контекст и выходит из контек ста управляемой инструкции.
Можно явно инициировать вызов деструктора объекта-структуры с по
мощью инструкции clear(объект);
. С функцией clear
мы уже познакоми
лись в разделе 6.3.5. Тогда она оказалась полезной для уничтожения
состояния объекта-класса. Для объектов-структур функция clear
дела
ет то же самое: вызывает деструктор, а затем копирует биты значения
.init
в область памяти объекта. В результате получается правильно
сконструированный объект, правда, без какого-либо интересного содер
жания.
7.1.4. Статические конструкторы и деструкторы
Структура может определять любое число статических конструкторов и деструкторов. Это средство полностью идентично одноименному сред ству для классов, с которым мы уже встречались в разделе 6.3.6.
import std.stdio;
struct A
{
static ~this()
{
writeln("Первый статический деструктор");
}
...
static this()
{
writeln("Первый статический конструктор ");
}
...
static this()
{
writeln("Второй статический конструктор");
}
...
static ~this()
{
writeln("Второй статический деструктор");
}
}
void main()
{
writeln("Внимание, говорит main");
}
Парность статических конструкторов и деструкторов не требуется. Под
система поддержки времени исполнения не делает ничего интересно
го – просто выполняет все статические конструкторы перед вычислени
ем функции main
в порядке их определения. По завершении выполне
ния main
подсистема поддержки времени исполнения так же скучно вы
зывает все статические деструкторы в порядке, обратном порядку их
определения. Предыдущая программа выведет на экран:
Первый статический конструктор
Второй статический конструктор
Внимание, говорит main
Второй статический деструктор
Первый статический деструктор
Порядок выполнения очевиден для статических конструкторов и де структоров, расположенных внутри одного модуля, но в случае не скольких модулей не всегда все так же ясно. Порядок выполнения ста тических конструкторов и деструкторов из разных модулей определен в разделе 6.3.6.
7.1.5. Методы
Структуры могут определять функции-члены, также называемые мето дами. Поскольку в случае структур о наследовании и переопределении речи нет, методы структур лишь немногим больше, чем функции.
Нестатические методы структуры S
принимают скрытый параметр this
по ссылке (эквивалент параметра ref S
). Поиск имен внутри методов
структуры производится так же, как и внутри методов класса: парамет
ры перекрывают одноименные внутренние элементы структуры, а име
на внутренних элементов структуры перекрывают те же имена, объяв
ленные на уровне модуля.
void fun(int x)
{
assert(x != 0);
}
// Проиллюстрируем правила поиска имен
struct S
{
int x = 1;
static int y = 324;
void fun(int x)
{
assert(x == 0); // Обратиться к параметру x
assert(this.x == 1); // Обратиться к внутреннему элементу x
}
void gun()
{
fun(0); // Вызвать метод fun
.fun(1); // Вызвать функцию fun, определенную на уровне модуля
}
// Тесты модуля могут быть внутренними элементами структуры
unittest
{
S obj;
obj.gun();
assert(y == 324); // Тесты модуля, являющиеся "внутренними элементами", видят статические данные
}
}
Кроме того, в этом примере есть тест модуля, определенный внутри
структуры. Такие тесты модуля, являющиеся «внутренними элемента
ми», не наделены никаким особым статусом, но их очень удобно встав
лять после каждого определения метода. Коду тела внутреннего теста
модуля доступна та же область видимости, что и обычным статическим
методам: например, тесту модуля в предыдущем примере не требуется
снабжать статическое поле y префиксом S
, как это не потребовалось бы
любому методу структуры.
Некоторые особые методы заслуживают более тщательного рассмотре
ния. К ним относятся оператор присваивания opAssign
, используемый
оператором =
, оператор равенства opEquals
, используемый операторами
==
и !=
, а также упорядочивающий оператор opCmp
, используемый опера
торами <
, <=
, >=
и >
. На самом деле, эта тема относится к главе 12, так как
затрагивает вопрос перегрузки операторов, но эти операторы особен
ные: компилятор может сгенерировать их автоматически, со всем их
особым поведением.
7.1.5.1. Оператор присваивания
По умолчанию, если задать:
struct Widget { ... } // Определен так же, как в разделе 7.1.3.4
Widget w1, w2;
...
w1 = w2;
то присваивание делается через копирование всех внутренних элемен
тов по очереди. В случае типа 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
:
struct Widget
{
private int[] array;
... // this(uint), this(this), и т. д.
ref Widget opAssign(ref Widget rhs)
{
array = rhs.array.dup;
return this;
}
}
Оператор присваивания возвращает ссылку на this
, тем самым позво
ляя создавать цепочки присваиваний а-ля w1 = w2 = w3
, которые компиля
тор заменяет на w1.opAssign(w2.opAssign(w3))
.
Осталась одна проблема. Рассмотрим присваивание:
Widget w;
...
w = Widget(50); // Ошибка! Невозможно привязать r-значение типа Widget к ссылке ref Widget!
Проблема в том, что метод opAssign
в таком виде, в каком он определен
сейчас, ожидает аргумент типа ref Widget
, то есть l-значение типа Widget
.
Чтобы помимо l-значений можно было бы присваивать еще и r-значе
ния, структура Widget
должна определять два оператора присваивания:
import std.algorithm;
struct Widget
{
private int[] array;
... // this(uint), this(this), и т. д.
ref Widget opAssign(ref Widget rhs)
{
array = rhs.array.dup;
return this;
}
ref Widget opAssign(Widget rhs)
{
swap(array, rhs.array);
return this;
}
}
В версии метода, принимающей r-значения, уже отсутствует обраще
ние к свойству .dup
. Почему? Ну, r-значение (а с ним и его массив) – это
практически собственность второго метода opAssign
: оно было скопиро
вано перед входом в функцию и будет уничтожено сразу же после того,
как функция вернет управление. Это означает, что больше нет нужды
дублировать rhs.array
, потому что его потерю никто не ощутит. Доста
точно лишь поменять местами rhs.array
и this.array
. Функция opAssign
возвращает результат, и rhs
и старый массив объекта this
уходят в ни
куда, а this
остается с массивом, ранее принадлежавшим rhs
, – совер
шенное сохранение состояния.
Теперь можно совсем убрать первую перегруженную версию оператора
opAssign
: та версия, что принимает rhs
по значению, заботится обо всем
сама (l-значения автоматически конвертируются в r-значения). Но оста
вив версию с l-значением, мы сохраняем точку, через которую можно оп
тимизировать работу оператора присваивания. Вместо того чтобы дуб
лировать структуру-оригинал с помощью свойства .dup
, метод opAssign
может проверять, достаточно ли в текущем массиве места для размеще
ния нового содержимого, и если да, то достаточно и записи поверх ста
рого массива на том же месте.
// Внутри Widget ...
ref Widget opAssign(ref Widget rhs)
{
if (array.length < rhs.array.length)
{
array = rhs.array.dup;
}
else
{
// Отрегулировать длину
array.length = rhs.array.length;
// Скопировать содержимое массива array (см. раздел 4.1.7)
array[] = rhs.array[];
}
return this;
}
7.1.5.2. Сравнение структур на равенство
Средство для сравнения объектов-структур предоставляется «в комп
лекте» – это операторы ==
и !=
. Сравнение представляет собой поочеред
ное сравнение внутренних элементов объектов и возвращает false
, если
хотя бы два соответствующих друг другу элемента сравниваемых объ
ектов не равны, иначе результатом сравнения является true
.
struct Point
{
int x, y;
}
unittest
{
Point a, b;
assert(a == b);
a.x = 1;
assert(a != b);
}
Чтобы определить собственный порядок сравнения, определите метод
opEquals
:
import std.math, std.stdio;
struct Point
{
float x = 0, y = 0;
// Добавлено
bool opEquals(ref const Point rhs) const
{
// Выполнить приблизительное сравнение
return approxEqual(x, rhs.x) && approxEqual(y, rhs.y);
}
}
unittest
{
Point a, b;
assert(a == b);
a.x = 1e-8;
assert(a == b);
a.y = 1e-1;
assert(a != b);
}
По сравнению с методом opEquals
для классов (см. раздел 6.8.3) метод
opEquals
для структур гораздо проще: ему не нужно беспокоиться о кор
ректности своих действий из-за наследования. Компилятор попросту
заменяет сравнение объектов-структур на вызов метода opEquals
. Ко
нечно, применительно к структурам остается требование определять
осмысленный метод opEquals
: рефлексивный, симметричный и транзи
тивный. Заметим, что хотя метод Point.opEquals
выглядит довольно ос
мысленно, он не проходит тест на транзитивность. Лучшим вариантом
оператора сравнения на равенство было бы сравнение двух объектов ти
па Point
, значения координат которых предварительно усечены до сво
их старших разрядов. Такую проверку было бы гораздо проще сделать
транзитивной.
Если структура содержит внутренние элементы, определяющие мето
ды opEquals
, а сама такой метод не определяет, при сравнении все равно
будут вызваны существующие методы opEquals
внутренних элементов.
Продолжим работать с примером, содержащим структуру Point
:
struct Rectangle
{
Point leftBottom, rightTop;
}
unittest
{
Rectangle a, b;
assert(a == b);
a.leftBottom.x = 1e-8;
assert(a == b);
a.rightTop.y = 5;
assert(a != b);
}
Для любых двух объектов a
и b
типа Rectangle
вычисление a == b
эквива
лентно вычислению выражения
a.leftBottom == b.leftBottom && a.rightTop == b.rightTop
что в свою очередь можно переписать так:
a.leftBottom.opEquals(b.leftBottom) && a.rightTop.opEquals(b.rightTop)
Этот пример также показывает, что сравнение выполняется в порядке
объявления полей (т. е. поле leftBottom
проверяется до проверки rightTop
), и если встретились два неравных поля, сравнение завершается до
того, как будут проверены все поля, благодаря сокращенному вычисле
нию логических связок, построенных с помощью оператора &&
(short
circuit evaluation).
7.1.6. Статические внутренние элементы
Структура может определять статические данные и статические внут
ренние функции. Помимо ограниченной видимости и подчинения пра
вилам доступа (см. раздел 7.1.7) режим работы статических внутренних
функций ничем не отличается от режима работы обычных функций.
Нет скрытого параметра this
, не вовлечены никакие другие особые ме
ханизмы.
Точно так же статические данные схожи с глобальными данными, определенными на уровне модуля (см. раздел 5.2.4), во всем, кроме ви димости и ограничений доступа, наложенных на эти статические дан ные родительской структурой.
import std.stdio;
struct Point
{
private int x, y;
private static string formatSpec = "(%s %s)\n";
static void setFormatSpec(string newSpec)
{
... // Проверить корректность спецификации формата
formatSpec = newSpec;
}
void print()
{
writef(formatSpec, x, y);
}
}
void main()
{
auto pt1 = Point(1, 2);
pt1.print();
// Вызвать статическую внутреннюю функцию, указывая ее принадлежность префиксом Point или pt1
Point.setFormatSpec("[%s, %s]\n");
auto pt2 = Point(5, 3);
// Новая спецификация действует на все объекты типа Point
pt1.print();
pt2.print();
}
Эта программа выведет на экран:
(1 2)
[1, 2]
[5, 3]
7.1.7. Спецификаторы доступа
Структуры подчиняются спецификаторам доступа private
(см. раз-
дел 6.7.1), package
(см. раздел 6.7.2), public
(см. раздел 6.7.4) и export
(см.
раздел 6.7.5) тем же образом, что и классы. Спецификатор protected
применительно к структурам не имеет смысла, поскольку структуры
не поддерживают наследование.
За подробной информацией обратитесь к соответствующим разделам. А здесь мы лишь вкратце напомним смысл спецификаторов:
struct S
{
private int a; // Доступен в пределах текущего файла и в методах S
package int b; // Доступен в пределах каталога текущего файла
public int c; // Доступен в пределах текущего приложения
export int d; // Доступен вне текущего приложения (там, где оно используется)
}
Заметим, что хотя ключевое слово export
разрешено везде, где синтак
сис допускает применение спецификатора доступа, семантика этого
ключевого слова зависит от реализации.
7.1.8. Вложенность структур и классов
Часто бывает удобно вложить в структуру другую структуру или класс. Например, контейнер дерева можно представить как оболочку-струк туру с простым интерфейсом поиска, а внутри нее для определения уз лов дерева использовать полиморфизм.
struct Tree
{
private:
class Node
{
int value;
abstract Node left();
abstract Node right();
}
class NonLeaf : Node
{
Node _left, _right;
override Node left() { return _left; }
override Node right() { return _right; }
}
class Leaf : Node
{
override Node left() { return null; }
override Node right() { return null; }
}
// Данные
Node root;
public:
void add(int value) { ... }
bool search(int value) { ... }
}
Аналогично структура может быть вложена в другую структуру...
struct Widget
{
private:
struct Field
{
string name;
uint x, y;
}
Field[] fields;
public:
...
}
...и наконец, структура может быть вложена в класс.
class Window
{
struct Info
{
string name;
Window parent;
Window[] children;
}
Info getInfo();
...
}
В отличие от классов, вложенных в другие классы, вложенные струк
туры и классы, вложенные в другие структуры, не обладают никаким
скрытым внутренним элементом outer
– никакой специальный код не
генерируется. Такие вложенные типы определяются в основном со
структурной целью – чтобы получить нужное управление доступом.
7.1.9. Структуры, вложенные в функции
Вспомним, что говорилось в разделе 6.11.1: вложенные классы находят ся в привилегированном положении, ведь они обладают особыми, уни кальными свойствами. Вложенному классу доступны параметры и ло кальные переменные включающей функции. Если вы возвращаете вло женный класс в качестве результата функции, компилятор даже разме щает кадр функции в динамической памяти, чтобы параметры и локальные переменные функции выжили после того, как она вернет управление.
Для единообразия и согласованности D оказывает структурам, вложен ным в функции, те же услуги, что и классам, вложенным в функции. Вложенная структура может обращаться к параметрам и локальным переменным включающей функции:
void fun(int a)
{
int b;
struct Local
{
int c;
int sum()
{
// Обратиться к параметру, переменной и собственному внутреннему элементу структуры Local
return a + b + c;
}
}
Local obj;
int x = obj.sum();
// (void*).sizeof – размер указателя на окружение
// int.sizeof – размер единственного поля структуры
assert(Local.sizeof == (void*).sizeof + int.sizeof);
}
unittest
{
fun(5);
}
Во вложенные структуры встраивается волшебный «указатель на кадр»,
с помощью которого они получают доступ к внешним значениям, та
ким как a
и b
в этом примере. Из-за этого дополнительного состояния
размер объекта Local
не 4 байта, как можно было ожидать, а 8 (на 32-раз
рядной машине) – еще 4 байта занимает указатель на кадр. Если хотите
определить вложенную структуру без этого багажа, просто добавьте
в определение структуры Local
ключевое слово static
перед ключевым
словом struct
– тем самым вы превратите Local
в обычную структуру, то
есть закроете для нее доступ к a
и b
.
Вложенные структуры практически бесполезны, разве что, по сравне нию со вложенными классами, позволяют избежать беспричинного ограничения. Функции не могут возвращать объекты вложенных структур, так как вызывающему их коду недоступна информация о ти пах таких объектов. Используя замысловатые вложенные структуры, код неявно побуждает создавать все больше сложных функций, а в идеале именно этого надо избегать в первую очередь.
7.1.10. Порождение подтипов в случае структур. Атрибут @disable
К структурам неприменимы наследование и полиморфизм, но этот тип
данных по-прежнему поддерживает конструкцию alias this
, впервые
представленную в разделе 6.13. С помощью alias this
можно сделать
структуру подтипом любого другого типа. Определим, к примеру, про
стой тип Final
, поведением очень напоминающий ссылку на класс – во
всем, кроме того что переменную типа Final
невозможно перепривязать!
Пример использования переменной Final
:
import std.stdio;
class Widget
{
void print()
{
writeln("Привет, я объект класса Widget. Вот, пожалуй, и все обо мне.");
}
}
unittest
{
auto a = Final!Widget(new Widget);
a.print(); // Все в порядке, просто печатаем a
auto b = a; // Все в порядке, a и b привязаны к одному и тому же объекту типа Widget
a = b; // Ошибка! opAssign(Final!Widget) деактивизирован!
a = new Widget; // Ошибка! Невозможно присвоить значение r-значению, возвращенному функцией get()!
}
Предназначение типа Final
– быть особым видом ссылки на класс, раз
и навсегда привязанной к одному объекту. Такие «преданные» ссылки
полезны для реализации множества проектных идей.
Первый шаг – избавиться от присваивания. Проблема в том, что опера
тор присваивания генерируется автоматически, если не объявлен поль
зователем, поэтому структура Final
должна вежливо указать компиля
тору не делать этого. Для этого предназначен атрибут @disable
:
struct Final(T)
{
// Запретить присваивание
@disable void opAssign(Final);
...
}
С помощью атрибута @disable
можно запретить и другие сгенерирован
ные функции, например сравнение.
До сих пор все шло хорошо. Чтобы реализовать Final!T
, нужно с помо
щью конструкции alias this
сделать Final(T)
подтипом T
, но чтобы при
этом полученный тип не являлся l-значением. Ошибочное решение вы
глядит так:
// Ошибочное решение
struct Final(T)
{
private T payload;
this(T bindTo)
{
payload = bindTo;
}
// Запретить присваивание
@disable void opAssign(Final);
// Сделать Final(T) подклассом T
alias payload this;
}
Структура Final
хранит ссылку на себя в поле payload
, которое инициа
лизируется в конструкторе. Кроме того, объявив, но не определяя ме
тод opAssign
, структура эффективно «замораживает» присваивание. Та
ким образом, клиентский код, пытающийся присвоить значение объек
ту типа Final!T
, или не сможет обратиться к payload
(из-за private
), или
получит ошибку во время компоновки.
Ошибка Final
– в использовании инструкции alias payload this;
. Этот
тест модуля делает что-то непредусмотренное:
class A
{
int value = 42;
this(int x) { value = x; }
}
unittest
{
auto v = Final!A(new A(42));
void sneaky(ref A ra)
{
ra = new A(4242);
}
sneaky(v); // Хм-м-м...
assert(v.value == 4242); // Проходит?!?
}
alias payload this
действует довольно просто: каждый раз, когда значе
ние объект
типа Final!T
используется в недопустимом для этого типа кон
тексте, компилятор вместо объект
пишет объект.payload
(то есть делает
объект.payload
псевдонимом для объекта
в соответствии с именем и син
таксисом конструкции alias
). Но выражение объект.payload
представля
ет собой непосредственное обращение к полю объект
, следовательно, яв
ляется l-значением. Это l-значение привязано к переданному по ссылке
параметру функции sneaky
и, таким образом, позволяет sneaky
напря
мую изменять значение поля объекта v
.
Чтобы это исправить, нужно сделать объект псевдонимом r-значения.
Так мы получим полную функциональность, но ссылка, сохраненная
в payload
, станет неприкосновенной. Очень просто осуществить привяз
ку к r-значению с помощью свойства (объявленного с атрибутом @property
), возвращающего payload
по значению:
struct Final(T)
{
private T payload;
this(T bindTo)
{
payload = bindTo;
}
// Запретить присваивание, оставив метод opAssign неопределенным
private void opAssign(Final);
// Сделать Final(T) подклассом T, не разрешив при этом перепривязывать payload
@property T get() { return payload; }
alias get this;
}
Ключевой момент в новом определении структуры – то, что метод 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
:
class Widget
{
private int x;
@property int get() { return x; }
}
unittest
{
auto w = Final!Widget(new Widget);
auto x = w.get; // Получает Widget из Final, а не int из Widget
}
Чтобы избежать таких коллизий, воспользуемся соглашением об име новании. Для надежности будем просто добавлять к именам видимых свойств имя соответствующего типа:
struct Final(T)
{
private T Final_payload;
this(T bindTo)
{
Final_payload = bindTo;
}
// Запретить присваивание
@disable void opAssign(Final);
// Сделать Final(T) подтипом T, не разрешив при этом перепривязывать payload
@property T Final_get() { return Final_payload; }
alias Final_get this;
}
Соблюдение такого соглашения сводит к минимуму риск непредвиден ных коллизий. (Конечно, иногда можно намеренно перехватывать не которые методы, оставив вызовы к ним за перехватчиком.)
7.1.11. Взаимное расположение полей. Выравнивание
Как располагаются поля в объекте-структуре? D очень консервативен в отношении структур: он располагает элементы их содержимого в том же порядке, в каком они указаны в определении структуры, но сохра няет за собой право вставлять между полями отступы (padding). Рас смотрим пример:
struct A
{
char a;
int b;
char c;
}
Если бы компилятор располагал поля в точном соответствии с размера
ми, указанными в структуре 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
, неявно определенного для каждого поля класса или
структуры:
import std.stdio;
struct A
{
char a;
int b;
char c;
}
void main()
{
A x;
writefln("%s %s %s", x.a.offsetof, x.b.offsetof, x.c.offsetof);
}
Эталонная реализация компилятора выведет 0 4 8
, открывая схему рас
положения полей, которую мы уже видели на рис. 7.2. Не совсем удоб
но, что для доступа к некоторой статической информации о типе A
при
ходится создавать объект этого типа, но синтаксис A.a.offsetof
не ком
пилируется. Здесь поможет такой трюк: выражение A.init.a.offsetof
позволяет получить смещение для любого внутреннего элемента струк
туры в виде константы, известной во время компиляции.
import std.stdio;
struct A
{
char a;
int b;
char c;
}
void main()
{
// Получить доступ к смещениям полей, не создавая объект
writefln("%s %s %s", A.init.a.offsetof,
A.init.b.offsetof, A.init.c.offsetof);
}
D гарантирует, что все байты отступов последовательно заполняются нулями.
7.1.11.1. Атрибут align
Чтобы перекрыть выбор компилятора, определив собственное выравни
вание, что повлияет на вставляемые отступы, объявляйте поля с атрибу
том align
. Такое переопределение может понадобиться для взаимодейст
вия с определенной аппаратурой или для работы по бинарному протоко
лу, задающему особое выравнивание. Пример атрибута align
в действии:
class A
{
char a;
align(1) int b;
char c;
}
При таком определении поля структуры A
располагаются без пустот
между ними. (В конце объекта при этом может оставаться зарезервиро
ванное, но не занятое место.) Аргумент атрибута align
означает максимальное выравнивание поля, но реальное выравнивание не может пре
высить естественное выравнивание для типа этого поля. Получить ес
тественное выравнивание типа T
позволяет определенное компилятором
свойство T.alignof
. Если вы, например, укажете для b
выравнивание
align(200) вместо указанного в примере align(1)
, то реально выравнива
ние примет значение 4
, равное int.alignof
.
Атрибут align
можно применять к целому классу или структуре:
align(1) struct A
{
char a;
int b;
char c;
}
Для структуры атрибут align
устанавливает выравнивание по умолча
нию заданным значением. Это умолчание можно переопределить инди
видуа льными атрибутами align
внутри структуры. Если для поля ти
па T
указать только ключевое слово align
без числа, компилятор прочи
тает это как align(T.alignof)
, то есть такая запись переустанавливает
выравнивание поля в его естественное значение.
Атрибут align
не предназначен для использования с указателями и ссыл
ками. Сборщик мусора действует из расчета, что все ссылки и указатели
выровнены по размеру типа size_t
. Компилятор не настаивает на со
блюдении этого ограничения, поскольку в общем случае у вас могут
быть указатели и ссылки, не контролируемые сборщиком мусора. Та
ким образом, следующее определение крайне опасно, поскольку компи
лируется без предупреждений:
struct Node
{
short value;
align(2) Node* next; // Избегайте таких определений
}
Если этот код выполнит присваивание объект.next = new Node
(то есть за
полнит объект.next
ссылкой, контролируемой сборщиком мусора), хаос
обеспечен: неверно выровненная ссылка пропадает из поля зрения сбор
щика мусора, память может быть освобождена, и объект.next
превраща
ется в «висячий» указатель.
7.2. Объединение
Объединения в стиле C можно использовать и в D, но не забывайте, что делать это нужно редко и с особой осторожностью.
Объединение (union
) – это что-то вроде структуры, все внутренние поля
которой начинаются по одному и тому же адресу. Таким образом, их об
ласти памяти перекрываются, а это значит, что именно вы как пользо
ватель объединения отвечаете за соответствие записываемой и считы
ваемой информации: нужно всегда читать в точности тот тип, который
был записан. В любой конкретный момент времени только один внут
ренний элемент объединения обладает корректным значением.
union IntOrFloat
{
int _int;
float _float;
}
unittest
{
IntOrFloat iof;
iof._int = 5;
// Читать только iof._int, но не iof._float
assert(iof._int == 5);
iof._float = 5.5;
// Читать только iof._float, но не iof._int
assert(iof._float == 5.5);
}
Поскольку типы int
и float
имеют строго один и тот же размер (4 байта),
внутри объединения IntOrFloat
их области памяти в точности совпадают.
Но детали их расположения не регламентированы, например, пред
ставления _int
и _float
могут отличаться порядком хранения байтов:
старший байт _int
может иметь наименьший адрес, а старший байт
_float
(тот, что содержит знак и большую часть показателя степени) –
наибольший адрес.
Объединения не помечаются, то есть сам объект типа union
не содержит
«метки», которая служила бы средством, позволяющим определять,
какой из внутренних элементов является «хорошим». Ответственность
за корректное использование объединения целиком ложится на плечи
пользователя, что делает объединения довольно неприятным средством
при построении более крупных абстракций.
В определенном, но неинициализированном объекте типа union
уже
есть одно инициализированное поле: первое поле автоматически ини
циализируется соответствующим значением .init
, поэтому оно доступ
но для чтения сразу по завершении построения по умолчанию. Чтобы
инициализировать первое поле значением, отличным от .init
, укажите
нужное инициализирующее выражение в фигурных скобках:
unittest
{
IntOrFloat iof = { 5 };
assert(iof._int == 5);
}
В статическом объекте типа union
может быть инициализировано и дру
гое поле. Для этого используйте следующий синтаксис:
unittest
{
static IntOrFloat iof = { _float : 5 };
assert(iof._float == 5);
}
Следует отметить, что нередко объединение служит именно для того,
чтобы считывать тип, отличный от исходно записанного, – в соответст
вии с порядком управления представлением, принятым в некоторой
системе. По этой причине компилятор не выявляет даже те случаи не
корректного использования объединений, которые может обнаружить.
Например, на 32-разрядной машине Intel следующий код компилиру
ется и даже выполнение инструкции assert
не порождает исключений:
unittest
{
IntOrFloat iof;
iof._float = 1;
assert(iof._int == 0x3F80_0000);
}
Объединение может определять функции-члены и, в общем случае, лю бые из тех внутренних элементов, которые может определять структу ра, за исключением конструкторов и деструкторов.
Чаще всего (точнее, наименее редко) объединения используются в каче стве анонимных членов структур. Например:
import std.contracts;
struct TaggedUnion
{
enum Tag { _tvoid, _tint, _tdouble, _tstring, _tarray }
private Tag _tag;
private union
{
int _int;
double _double;
string _string;
TaggedUnion[] _array;
}
public:
void opAssign(int v)
{
_int = v;
_tag = Tag._tint;
}
int getInt()
{
enforce(_tag == Tag._tint);
return _int;
}
...
}
unittest
{
TaggedUnion a;
a = 4;
assert(a.getInt() == 4);
}
(Подробно тип enum
описан в разделе 7.3.)
Этот пример демонстрирует чисто классический способ использования
union
в качестве вспомогательного средства для определения так назы
ваемого размеченного объединения (discriminated union, tagged union),
также известного как алгебраический тип. Размеченное объединение
инкапсулирует небезопасный объект типа union
в «безопасной коробке»,
которая отслеживает последний присвоенный тип. Сразу после ини
циализации поле Tag
содержит значение Tag._tvoid
, по сути означающее,
что объект не инициализирован. При присваивании объединению неко
торого значения срабатывает оператор opAssign
, устанавливающий тип
объекта в соответствии с типом присваиваемого значения. Чтобы полу
чить законченную реализацию, потребуется определить методы opAssign(double)
, opAssign(string)
и opAssign(TaggedUnion[])
с соответствующи
ми функциями getXxx()
.
Внутренний элемент типа union
анонимен, то есть одновременно являет
ся и определением типа, и определением внутреннего элемента. Память
под анонимное объединение выделяется как под обычный внутренний
элемент структуры, и внутренние элементы этого объединения напря
мую видимы внутри структуры (как показывают методы TaggedUnion
).
В общем случае можно определять как анонимные структуры, так и ано
нимные объединения, и вкладывать их как угодно.
В конце концов вы должны понять, что объединение не такое уж зло,
каким может показаться. Как правило, использовать объединение вме
сто того, чтобы играть типами с помощью выражения cast
, – хороший
тон в общении между программистом и компилятором. Объединение
указателя и целого числа указывает сборщику мусора, что ему следует
быть осторожнее и не собирать этот указатель. Если вы сохраните ука
затель в целом числе и будете время от времени преобразовывать его на
зад к типу указателя (с помощью cast
), результаты окажутся непред
сказуемыми, ведь сборщик мусора может забрать память, ассоцииро
ванную с этим тайным указателем.
7.3. Перечисляемые значения
Типы, принимающие всего несколько определенных значений, оказа
лись очень полезными – настолько полезными, что язык Java после не
скольких лет героических попыток эмулировать перечисляемые типы
с помощью идиомы в конце концов добавил их к основным типам [8].
Определить хорошие перечисляемые типы непросто – в C (и особенно
в C++) типу enum
присущи свои странности. D попытался учесть пред
шествующий опыт, определив простое и полезное средство для работы
с перечисляемыми типами.
Начнем с азов. Простейший способ применить enum
– как сказать «да
вайте перечислим несколько символьных значений», не ассоциируя их
с новым типом:
enum
mega = 1024 * 1024,
pi = 3.14,
euler = 2.72,
greet = "Hello";
С enum
механизм автоматического определения типа работает так же,
как и с auto
, поэтому в нашем примере переменные pi
и euler
имеют тип
double
, a переменная greet
– тип string
. Чтобы определить одно или не
сколько перечисляемых значений определенного типа, укажите их спра
ва от ключевого слова enum
:
enum float verySmall = 0.0001, veryBig = 10000;
enum dstring wideMsg = "Wide load";
Перечисляемые значения – это константы; они практически эквива
лентны литералам, которые обозначают. В частности, поддерживают те
же операции – например, невозможно получить адрес pi
, как невоз
можно получить адрес 3.14
:
auto x = pi; // Все в порядке, x обладает типом double
auto y = pi * euler; // Все в порядке, y обладает типом double
euler = 2.73; // Ошибка! Невозможно изменить перечисляемое значение!
void fun(ref double x) {
...
}
fun(pi); // Ошибка! Невозможно получить адрес 3.14!
Как показано выше, типы перечисляемых значений не ограничиваются
типом int
– типы double
и string
также допустимы. Какие вообще типы
можно использовать с enum
? Ответ прост: c enum
можно использовать лю
бой основной тип и любую структуру. Есть лишь два требования к ини
циализирующему значению при определении перечисляемых значений:
- инициализирующее значение должно быть вычислимым во время компиляции;
- тип инициализирующего значения должен позволять копирование,
то есть в его определении не должно быть
@disable this(this)
(см. раз- дел 7.1.3.4).
Первое требование гарантирует независимость перечисляемого значе ния от параметров времени исполнения. Второе требование обеспечива ет возможность копировать значение; копия создается при каждом об ращении к перечисляемому значению.
Невозможно определить перечисляемое значение типа class
, поскольку
объекты классов должны всегда создаваться с помощью оператора new
(за исключением не представляющего интерес значения null
), а выра
жение с new
во время компиляции вычислить невозможно. Не будет не
ожиданностью, если в будущем это ограничение снимут или ослабят.
Создадим перечисление значений типа struct
:
struct Color
{
ubyte r, g, b;
}
enum
red = Color(255, 0, 0),
green = Color(0, 255, 0),
blue = Color(0, 0, 255);
Когда бы вы ни использовали, например, идентификатор green
, код бу
дет вести себя так, будто вместо этого идентификатора вы написали
Color(0, 255, 0)
.
7.3.1. Перечисляемые типы
Можно дать имя группе перечисляемых значений, создав таким обра зом новый тип на ее основе:
enum OddWord { acini, alembicated, prolegomena, aprosexia }
Члены именованной группы перечисляемых значений не могут иметь разные типы; все перечисляемые значения должны иметь один и тот же тип, поскольку пользователи могут впоследствии определять и исполь зовать значения этого типа. Например:
OddWord w;
assert(w == OddWord.acini); // Инициализирующим значением по умолчанию является первое значение в множестве - acini.
w = OddWord.aprosexia; // Всегда уточняйте имя значения (кстати, это не то, что вы могли подумать) с помощью имени типа.
int x = w; // OddWord конвертируем в int, но не наоборот.
assert(x == 3); // Значения нумеруются по порядку: 0, 1, 2, ...
Тип, автоматически определяемый для поименованного перечисления, –
int
. Присвоить другой тип можно так:
enum OddWord : byte { acini, alembicated, prolegomena, aprosexia }
С новым определением (byte
называют базовым типом OddWord
) значе
ния идентификаторов перечисления не меняются, изменяется лишь
способ их хранения. Вы можете с таким же успехом назначить членам
перечисления тип double
или real
, но связанные с идентификаторами
значения останутся прежними: 0
, 1
и т. д. Но если сделать базовым ти
пом OddWord
нечисловой тип, например string
, то придется указать ини
циализирующее значение для каждого из значений, поскольку компи
лятору неизвестна никакая естественная последовательность, которой
он мог бы придерживаться.
Возвратимся к числовым перечислениям. Присвоив какому-либо члену перечисления особое значение, вы таким образом сбросите счетчик, ис пользуемый компилятором для присваивания значений идентифика торам. Например:
enum E { a, b = 2, c, d = -1, e, f }
assert(E.c == 3);
assert(E.e == 0);
Если два идентификатора перечисления получают одно и то же значе
ние (как в случае с E.a
и E.e
), конфликта нет. Фактически равные значе
ния можно создавать, даже не подозревая об этом – из-за непреодолимо
го желания типов с плавающей запятой удивить небдительных пользо
вателей:
enum F : float { a = 1E30, b, c, d }
assert(F.a == F.d); // Тест пройден
Корень этой проблемы в том, что наибольшее значение типа int
, кото
рое может быть представлено значением типа float
, равно 16_777_216
,
и выход за эту границу сопровождается все возрастающими диапазона
ми целых значений, представляемых одним и тем же числом типа float
.
7.3.2. Свойства перечисляемых типов
Для всякого перечисляемого типа E
определены три свойства: E.init
(это
свойство принимает первое из значений, определенных в E
), E.min
(наи
меньшее из определенных в E
значений) и E.max
(наибольшее из опреде
ленных в E
значений). Два последних значения определены, только ес
ли базовым типом E
является тип, поддерживающий сравнение во вре
мя компиляции с помощью оператора <
.
Вы вправе определить внутри enum
собственные значения min
, max
и init
,
но поступать так не рекомендуется: обобщенный код частенько рассчи
тывает на то, что эти значения обладают особой семантикой.
Один из часто задаваемых вопросов: «Можно ли добраться до имени пе
речисляемого значения?» Вне всяких сомнений, сделать это возможно
и на самом деле легко, но не с помощью встроенного механизма, а на ос
нове рефлексии времени компиляции. Рефлексия работает так: с неко
торым перечисляемым типом Enum
связывается известная во время ком
пиляции константа __traits(allMembers, Enum)
, которая содержит все чле
ны Enum
в виде кортежа значений типа string
. Поскольку строками мож
но манипулировать во время компиляции, как и во время исполнения,
такой подход дает значительную гибкость. Например, немного забежав
вперед, напишем функцию toString
, которая возвращает строку, соот
ветствующую заданному перечисляемому значению. Функция парамет
ризирована перечисляемым типом.
string toString(E)(E value) if (is(E == enum))
{
foreach (s; __traits(allMembers, E))
{
if (value == mixin("E." ~ s)) return s;
}
return null;
}
enum OddWord { acini, alembicated, prolegomena, aprosexia }
void main()
{
auto w = OddWord.alembicated;
assert(toString(w) == "alembicated");
}
Незнакомое пока выражение 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
.
7.4. alias
В ряде случаев мы уже имели дело с size_t
– целым типом без знака,
достаточно вместительным, чтобы представить размер любого объекта.
Тип size_t
не определен языком, он просто принимает форму uint
или
ulong
в зависимости от адресного пространства конечной системы (32
или 64 бита соответственно).
Если бы вы открыли файл object.di, один из копируемых на компьютер пользователя (а значит, и на ваш) при инсталляции компилятора D, то нашли бы объявление примерно следующего вида:
alias typeof(int.sizeof) size_t;
Свойство .sizeof
точно измеряет размер типа в байтах; в данном случае
это тип int
. Вместо int
в примере мог быть любой другой тип; в данном
случае имеет значение не указанный тип, а тип размера, возвращаемый
оператором typeof
. Компилятор измеряет размеры объектов, используя
uint
на 32-разрядных архитектурах и ulong
на 64-разрядных. Следова
тельно, конструкция alias
позволяет назначить size_t
синонимом uint
или ulong
.
Обобщенный синтаксис объявления с ключевым словом alias
ничуть не
сложнее приведенного выше:
alias ‹существующийИдентификатор› ‹новыйИдентификатор›;
В качестве идентификатора ‹существующийИдентификатор›
можно подста
вить все, у чего есть имя. Это может быть тип, переменная, модуль – ес
ли что-то обладает идентификатором, то для этого объекта можно соз
дать псевдоним. Например:
import std.stdio;
void fun(int) {}
void fun(string) {}
int var;
enum E { e }
struct S { int x; }
S s;
unittest
{
alias object.Object Root; // Предок всех классов
alias std phobos; // Имя пакета
alias std.stdio io; // Имя модуля
alias var sameAsVar; // Переменная
alias E MyEnum; // Перечисляемый тип
alias E.e myEnumValue; // Значение этого типа
alias fun gun; // Перегруженная функция
alias S.x field; // Поле структуры
alias s.x sfield; // Поле объекта
}
Правила применения псевдонима просты: используйте псевдоним вез
де, где допустимо использовать исходный идентификатор. Именно это
делает компилятор, но с точностью до наоборот: он с пониманием заме
няет идентификатор-псевдоним оригинальным идентификатором. Да
же сообщения об ошибках и отлаживаемая программа могут «видеть
сквозь» псевдонимы и показывать исходные идентификаторы, что мо
жет оказаться неожиданным. Например, в некоторых сообщениях об
ошибках или в отладочных символах можно увидеть immutable(char)[]
вместо string
. Но что именно будет показано, зависит от реализации
компилятора.
С помощью конструкции alias
можно создавать псевдонимы псевдони
мов для идентификаторов, уже имеющих псевдонимы. Например:
alias int Int;
alias Int MyInt;
Здесь нет ничего особенного, просто следование обычным правилам:
к моменту определения псевдонима MyInt
псевдоним Int
уже будет заме
нен исходным идентификатором int
, для которого Int
является псевдо
нимом.
Конструкцию alias
часто применяют, когда требуется дать сложной це
почке идентификаторов более короткое имя или в связке с перегружен
ными функциями из разных модулей (см. раздел 5.5.2).
Также конструкцию alias
часто используют с параметризированными
структурами и классами. Например:
// Определить класс-контейнер
class Container(T)
{
alias T ElementType;
...
}
unittest
{
Container!int container;
Container!int.ElementType element;
...
}
Здесь общедоступный псевдоним ElementType
, созданный классом Container
, – единственный разумный способ обратиться из внешнего мира
к аргументу, привязанному к параметру T
класса Container
. Идентифи
катор T
видим лишь внутри определения класса Container
, но не снару
жи: выражение Container!int.T
не компилируется.
Наконец, конструкция alias
весьма полезна в сочетании с конструкци
ей static if
. Например:
// Из файла object.di
// Определить тип разности между двумя указателями
static if (size_t.sizeof == 4)
{
alias int ptrdiff_t;
}
else
{
alias long ptrdiff_t;
}
// Использовать ptrdiff_t ...
С помощью объявления псевдоним ptrdiff_t
привязывается к разным ти
пам в зависимости от того, по какой ветке статического условия пойдет
поток управления. Без этой возможности привязки код, которому потре
бовался такой тип, пришлось бы разместить в одной из веток static if
.
7.5. Параметризированные контексты (конструкция template)
Мы уже рассмотрели средства, облегчающие параметризацию во время компиляции (эти средства сродни шаблонам из C++ и родовым типам из языков Java и C#), – это функции (см. раздел 5.3), параметризирован ные классы (см. раздел 6.14) и параметризированные структуры, кото рые подчиняются тем же правилам, что и параметризированные клас сы. Тем не менее иногда во время компиляции требуется каким-либо образом манипулировать типами, не определяя функцию, структуру или класс. Один из механизмов, подходящих под это описание (широко используемый в C++), – выбор того или иного типа в зависимости от статически известного логического условия. При этом не определяется никакой новый тип и не вызывается никакая функция – лишь создает ся псевдоним для одного из существующих типов.
Для случаев, когда требуется организовать параметризацию во время компиляции без определения нового типа или функции, D предоставля ет параметризированные контексты. Такой параметризированный кон текст вводится следующим образом:
template Select(bool cond, T1, T2)
{
...
}
Этот код – на самом деле лишь каркас для только что упомянутого меха
низма выбора во время компиляции. Скоро мы доберемся и до реализа
ции, а пока сосредоточимся на порядке объявления. Объявление с клю
чевым словом template
вводит именованный контекст (в данном случае
это Select
) с параметрами, вычисляемыми во время компиляции (в дан
ном случае это логическое значение и два типа). Объявить контекст
можно на уровне модуля, внутри определения класса, внутри определе
ния структуры, внутри любого другого объявления контекста, но не
внутри определения функции.
В теле параметризированного контекста разрешается использовать все
те же объявления, что и обычно, кроме того, могут быть использованы
параметры контекста. Доступ к любому объявлению контекста можно
получить извне, расположив перед его именем имя контекста и ., на
пример: Select!(true, int, double).foo
. Давайте прямо сейчас закончим
определение контекста Select
, чтобы можно было поиграть с ним:
template Select(bool cond, T1, T2)
{
static if (cond)
{
alias T1 Type;
}
else
{
alias T2 Type;
}
}
unittest
{
alias Select!(false, int, string).Type MyType;
static assert(is(MyType == string));
}
Заметим, что тот же результат мы могли бы получить на основе струк
туры или класса, поскольку эти типы могут определять в качестве сво
их внутренних элементов псевдонимы, доступные с помощью обычного
синтаксиса с оператором .
(точка):
struct /* или class */ Select2(bool cond, T1, T2)
{
static if (cond)
{
alias T1 Type;
}
else
{
alias T2 Type;
}
}
unittest
{
alias Select2!(false, int, string).Type MyType;
static assert(is(MyType == string));
}
Согласитесь, такое решение выглядит не очень привлекательно. К при
меру, для Select2
в документации пришлось бы написать: «Не создавай
те объекты типа Select2
! Он определен только ради псевдонима внутри
него!» Доступный специализированный механизм определения пара
метризированных контекстов позволяет избежать двусмысленности на
мерений, не вызывает недоумения и исключает возможность некоррект
ного использования.
В контексте, определенном с ключевым словом template
, можно объяв
лять не только псевдонимы – там могут присутствовать самые разные
объявления. Определим еще один полезный шаблон. На этот раз это бу
дет шаблон, возвращающий логическое значение, которое сообщает, яв
ляется ли заданный тип строкой (в любой кодировке):
template isSomeString(T)
{
enum bool value = is(T : const(char[])) || is(T : const(wchar[])) || is(T : const(dchar[]));
}
unittest
{
// Не строки
static assert(!isSomeString!(int).value);
static assert(!isSomeString!(byte[]).value);
// Строки
static assert(isSomeString!(char[]).value);
static assert(isSomeString!(dchar[]).value);
static assert(isSomeString!(string).value);
static assert(isSomeString!(wstring).value);
static assert(isSomeString!(dstring).value);
static assert(isSomeString!(char[4]).value);
}
Параметризированные контексты могут быть рекурсивными; к приме ру, вот одно из возможных решений задачи с факториалом:
template factorial(uint n)
{
static if (n <= 1)
enum ulong value = 1;
else
enum ulong value = factorial!(n - 1).value * n;
}
Несмотря на то что factorial
является совершенным функциональным
определением, в данном случае это не лучший подход. При необходимо
сти вычислять значения во время компиляции, пожалуй, стоило бы
воспользоваться механизмом вычислений во время компиляции (см.
раздел 5.12). В отличие от приведенного выше шаблона factorial
, функ
ция factorial
более гибка, поскольку может вычисляться как во время
компиляции, так и во время исполнения. Конструкция template
больше
всего подходит для манипуляции типами, имеющей место в Select
и isSomeString
.
7.5.1. Одноименные шаблоны
Конструкция template
может определять любое количество идентифи
каторов, но, как видно из предыдущих примеров, нередко в ней опреде
лен ровно один идентификатор. Обычно шаблон определяется лишь
с целью решить единственную задачу и в качестве результата сделать
доступным единственный идентификатор (такой как Type
в случае Select
или value
в случае isSomeString
).
Необходимость помнить о том, что в конце вызова надо указать этот
идентификатор, и всегда его указывать может раздражать. Многие про
сто забывают добавить в конец .Type
, а потом удивляются, почему вызов
Select!(cond, A, B)
порождает таинственное сообщение об ошибке.
D помогает здесь, определяя правило, известное как фокус с одноимен
ным шаблоном: если внутри конструкции template
определен иденти
фикатор, совпадающий с именем самого шаблона, то при любом после
дующем использовании имени этого шаблона в его конец будет автома
тически дописываться одноименный идентификатор. Например:
template isNumeric(T)
{
enum bool isNumeric = is(T : long) || is(T : real);
}
unittest
{
static assert(isNumeric!(int));
static assert(!isNumeric!(char[]));
}
Если теперь некоторый код использует выражение isNumeric!(T)
, компи
лятор в каждом случае автоматически заменит его на isNumeric!(T).isNumeric
, чем освободит пользователя от хлопот с добавлением идентифи
катора в конец имени шаблона.
Шаблон, проделывающий фокус с «тезками», может определять внутри себя и другие идентификаторы, но они будут попросту недоступны за пределами этого шаблона. Дело в том, что компилятор заменяет иден тификаторы на раннем этапе процесса поиска имен. Единственный спо соб получить доступ к таким идентификаторам – обратиться к ним из тела самого шаблона. Например:
template isNumeric(T)
{
enum bool test1 = is(T : long);
enum bool test2 = is(T : real);
enum bool isNumeric = test1 || test2;
}
unittest
{
static assert(isNumeric!(int).test1); // Ошибка! Тип bool не определяет свойство test1!
}
Это сообщение об ошибке вызвано соблюдением правила об одноимен
ности: перед тем как делать что-либо еще, компилятор расширяет вызов
isNumeric!(int)
до isNumeric!(int).isNumeric
. Затем пользовательский код
делает попытку заполучить значение isNumeric!(int).isNumeric.test1
, что
равносильно попытке получить внутренний элемент test1
из логическо
го значения, отсюда и сообщение об ошибке. Короче говоря, используй
те одноименные шаблоны тогда и только тогда, когда хотите открыть
доступ лишь к одному идентификатору. Этот случай скорее частый, чем
редкий, поэтому одноименные шаблоны очень популярны и удобны.
7.5.2. Параметр шаблона this4
Познакомившись с классами и структурами, можно параметризовать
наш обобщенный метод типом неявного аргумента this
. Например:
class Parent
{
static string getName(this T)()
{
return T.stringof;
}
}
class Derived1: Parent{}
class Derived2: Parent{}
unittest
{
assert(Parent.getName() == "Parent");
assert(Derived1.getName() == "Derived1");
assert(Derived2.getName() == "Derived2");
}
Параметр шаблона this T
предписывает компилятору в теле getName
считать T
псевдонимом typeof(this)
.
В обычный статический метод класса не передаются никакие скрытые
параметры, поэтому невозможно определить, для какого конкретно
класса вызван этот метод. В приведенном примере компилятор создает
три экземпляра шаблонного метода Parent.getName(this T)()
: Parent.getName()
, Derived1.getName()
и Derived2.getName()
.
Также параметр this
удобен в случае, когда один метод нужно исполь
зовать для разных квалификаторов неизменяемости объекта (см. гла
ву 8).
7.6. Инъекции кода с помощью конструкции mixin template
При некоторых программных решениях приходится добавлять шаблон ный код (такой как определения данных и методов) в одну или несколь ко реализаций классов. К типичным примерам относятся поддержка сериализации, шаблон проектирования «Наблюдатель» и передача событий в оконных системах.
Для этих целей можно было бы воспользоваться механизмом наследова ния, но поскольку реализуется лишь одиночное наследование, опреде лить для заданного класса несколько источников шаблонного кода не возможно. Иногда необходим механизм, позволяющий просто вставить в класс некоторый готовый код, вместо того чтобы писать его вручную.
Здесь-то и пригодится конструкция mixin template
(шаблон mixin
). Стоит
отметить, что сейчас это средство в основном экспериментальное. Воз
можно, в будущих версиях языка шаблоны mixin
заменит более общий
инструмент AST-макросов.
Шаблон mixin
определяется почти так же, как параметризированный
контекст (шаблон), о котором недавно шла речь. Пример шаблона mixin
,
определяющего переменную и функции для ее чтения и записи:
mixin template InjectX()
{
private int x;
int getX() { return x; }
void setX(int y)
{
... // Проверки
x = y;
}
}
Определив шаблон mixin
, можно вставить его в нескольких местах:
// Сделать инъекцию в контексте модуля
mixin InjectX;
class A
{
// Сделать инъекцию в класс
mixin InjectX;
...
}
void fun()
{
// Сделать инъекцию в функцию
mixin InjectX;
setX(10);
assert(getX() == 10);
}
Теперь этот код определяет переменную и две обслуживающие ее функ
ции на уровне модуля, внутри класса A
и внутри функции fun
– как буд
то тело InjectX
было вставлено вручную. В частности, потомки класса A
могут переопределять методы getX
и setX
, как если бы сам класс опреде
лял их. Копирование и вставка без неприятного дублирования кода –
вот что такое mixin template
.
Конечно же, следующий логический шаг – подумать о том, что InjectX
не принимает никаких параметров, но производит впечатление, что мог
бы, – и действительно может:
mixin template InjectX(T)
{
private T x;
T getX() { return x; }
void setX(T y)
{
... // Проверки
x = y;
}
}
Теперь при обращении к InjectX
нужно передавать аргумент так:
mixin InjectX!int;
mixin InjectX!double;
Но на самом деле такие вставки приводят к двусмысленности: что если
вы сделаете две рассмотренные подстановки, а затем пожелаете восполь
зоваться функцией getX
? Есть две функции с этим именем, так что про
блема с двусмысленностью очевидна. Чтобы решить этот вопрос, D по
зволяет вводить имена для конкретных подстановок в шаблоны mixin
:
mixin InjectX!int MyInt;
mixin InjectX!double MyDouble;
Задав такие определения, вы можете недвусмысленно обратиться к внут
ренним элементам любого из шаблонов mixin
, просто указав нужный
контекст:
MyInt.setX(5);
assert(MyInt.getX() == 5);
MyDouble.setX(5.5);
assert(MyDouble.getX() == 5.5);
Таким образом, шаблоны mixin
– это почти как копирование и вставка;
вы можете многократно копировать и вставлять код, а потом указы
вать, к какой именно вставке хотите обратиться.
7.6.1. Поиск идентификаторов внутри mixin
Самая большая разница между шаблоном mixin
и обычным шаблоном
(в том виде, как он определен в разделе 7.5), способная вызвать больше
всего вопросов, – это поиск имен.
Шаблоны исключительно модульны: код внутри шаблона ищет иденти фикаторы в месте определения шаблона. Это положительное качество: оно гарантирует, что, проанализировав определение шаблона, вы уже ясно представляете его содержимое и осознаете, как он работает.
Шаблон mixin
, напротив, ищет идентификаторы в месте подстановки,
а это означает, что понять поведение шаблона mixin
можно только с уче
том контекста, в котором вы собираетесь этот шаблон использовать.
Чтобы проиллюстрировать разницу, рассмотрим следующий пример, в котором идентификаторы объявляются как в месте определения, так и в месте подстановки:
import std.stdio;
string lookMeUp = "Найдено на уровне модуля";
template TestT()
{
string get() { return lookMeUp; }
}
mixin template TestM()
{
string get() { return lookMeUp; }
}
void main()
{
string lookMeUp = "Найдено на уровне функции";
alias TestT!() asTemplate;
mixin TestM!() asMixin;
writeln(asTemplate.get());
writeln(asMixin.get());
}
Эта программа выведет на экран:
Найдено на уровне модуля
Найдено на уровне функции
Склонность шаблонов mixin
привязываться к локальным идентифика
торам придает им выразительности, но следовать их логике становится
сложно. Такое поведение делает шаблоны mixin
применимыми лишь
в ограниченном количестве случаев; прежде чем доставать из ящика
с инструментами эти особенные ножницы, необходимо семь раз отме
рить.
7.7. Итоги
Классы позволяют эффективно представить далеко не любую абстрак цию. Например, они не подходят для мелкокалиберных объектов, кон текстно-зависимых ресурсов и типов значений. Этот пробел восполня ют структуры. В частности, благодаря конструкторам и деструкторам легко определять типы контекстно-зависимых ресурсов.
Объединения – низкоуровневое средство, позволяющее хранить разные типы данных в одной области памяти с перекрыванием.
Перечисления – это обычные отдельные значения, определенные поль зователем. Перечислению может быть назначен новый тип, что позво ляет более точно проверять типы значений, определенных в рамках этого типа.
alias
– очень полезное средство, позволяющее привязать один иденти
фикатор к другому. Нередко псевдоним – единственное средство полу
чить извне доступ к идентификатору, вычисляемому в рамках вложен
ной сущности, или к длинному и сложному идентификатору.
Параметризированные контексты, использующие конструкцию template
, весьма полезны для определения вычислений во время компиля
ции, таких как интроспекция типов и определение особенностей типов.
Одноименные шаблоны позволяют предоставлять абстракции в очень
удобной, инкапсулированной форме.
Кроме того, предлагаются параметризированные контексты, прини
мающие форму шаблонов mixin
, которые во многом ведут себя подобно
макросам. В будущем шаблоны mixin
может заменить развитое средст
во AST-макросов.
-
Не считая эквивалентных имен, создаваемых с помощью
alias
, о чем мы еще поговорим в этой главе (см. раздел 7.4). ↩︎ -
Термин «клуктура» предложил Бартош Милевски. ↩︎
-
Кроме того, ‹код1› может сохранить указатель на значение w, которое исполь зует ‹код2›. ↩︎
-
На момент написания оригинала книги данная возможность отсутствова ла, но поскольку теперь она существует, мы добавили ее описание в пере вод. – Прим. науч. ред. ↩︎