|
@@ -81,4 +81,2468 @@ struct Widget
|
|
|
|
|
|
[В начало ⮍](#7-1-структуры) [Наверх ⮍](#7-другие-пользовательские-типы)
|
|
|
|
|
|
+### 7.1.1. Семантика копирования
|
|
|
+
|
|
|
+Несколько заметных на глаз различий между структурами и классами
|
|
|
+есть следствие менее очевидных семантических различий. Повторим
|
|
|
+эксперимент, который мы уже проводили, обсуждая классы в разде-
|
|
|
+ле 6.2. На этот раз создадим структуру и объект с одинаковыми поля
|
|
|
+ми, а затем сравним поведение этих типов при копировании:
|
|
|
+
|
|
|
+```d
|
|
|
+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` ведет себя как значение, он и передается
|
|
|
+в функцию по значению.
|
|
|
+
|
|
|
+```d
|
|
|
+struct S
|
|
|
+{
|
|
|
+ int a, b, c;
|
|
|
+ double x, y, z;
|
|
|
+}
|
|
|
+
|
|
|
+void fun(S s)
|
|
|
+{
|
|
|
+ // fun получает копию
|
|
|
+ ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Передать объект-структуру по ссылке можно с помощью аргумента
|
|
|
+с ключевым словом `ref` (см. раздел 5.2.1):
|
|
|
+
|
|
|
+```d
|
|
|
+void fun(ref S s) // fun получает ссылку
|
|
|
+{
|
|
|
+ ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Раз уж мы заговорили о `ref`, отметим, что `this` передается по ссылке
|
|
|
+внутрь методов структуры `S` в виде скрытого параметра `ref S`.
|
|
|
+
|
|
|
+### 7.1.3. Жизненный цикл объекта-структуры
|
|
|
+
|
|
|
+В отличие от объектов-классов, объектам-структурам не свойственно
|
|
|
+бесконечное время жизни (lifetime). Время жизни для них четко огра
|
|
|
+ничено – так же как для временных (стековых) объектов функций.
|
|
|
+Чтобы создать объект-структуру, задайте имя нужного типа, как если
|
|
|
+бы вы вызывали функцию:
|
|
|
+
|
|
|
+```d
|
|
|
+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)` также разрешены и инициализируют по
|
|
|
+ля объекта в порядке их объявления. Продолжим предыдущий пример:
|
|
|
+
|
|
|
+```d
|
|
|
+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):
|
|
|
+
|
|
|
+```d
|
|
|
+struct Test
|
|
|
+{
|
|
|
+ double a = 0.4;
|
|
|
+ double b;
|
|
|
+ this(double b)
|
|
|
+ {
|
|
|
+ this.b = b;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+unittest
|
|
|
+{
|
|
|
+ auto t = Test(5);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Присутствие хотя бы одного пользовательского конструктора блокирует
|
|
|
+все упомянутые выше конструкторы, инициализирующие поля струк
|
|
|
+туры:
|
|
|
+
|
|
|
+```d
|
|
|
+auto t1 = Test(1.1, 1.2); // Ошибка! Нет конструктора, соответствующего вызову Test(double, double)
|
|
|
+```
|
|
|
+
|
|
|
+Есть важное исключение: компилятор всегда определяет конструктор
|
|
|
+без аргументов:
|
|
|
+
|
|
|
+```d
|
|
|
+auto t2 = Test(); // Все в порядке, создается объект с "начинкой" по умолчанию
|
|
|
+```
|
|
|
+
|
|
|
+Кроме того, пользовательский код не может определить собственный
|
|
|
+конструктор без аргументов:
|
|
|
+
|
|
|
+```d
|
|
|
+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`:
|
|
|
+
|
|
|
+```d
|
|
|
+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` по шагам:
|
|
|
+
|
|
|
+1. Скопировать значение `T.init` в память, где будет размещен объект,
|
|
|
+путем копирования «сырой» памяти (а-ля `memcpy`).
|
|
|
+2. Вызвать конструктор, если нужно.
|
|
|
+
|
|
|
+Если инициализация некоторых или всех полей структуры выглядит
|
|
|
+как `= void`, объем работ на первом шаге можно сократить, хотя и редко
|
|
|
+намного, зато такой маневр часто порождает трудноуловимые ошибки
|
|
|
+в вашем коде (тем не менее случай оправданного применения сокра
|
|
|
+щенной инициализации иллюстрирует пример с классом `Transmogrifier`
|
|
|
+в разделе 6.3.3).
|
|
|
+
|
|
|
+#### 7.1.3.4. Конструктор копирования this(this)
|
|
|
+
|
|
|
+Предположим, требуется определить объект, который содержит ло
|
|
|
+кальный (`private`) массив и предоставляет ограниченный API для ма
|
|
|
+нипуляции этим массивом:
|
|
|
+
|
|
|
+```d
|
|
|
+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` между копиями создается отдаленная
|
|
|
+зависимость. Судите сами:
|
|
|
+
|
|
|
+```d
|
|
|
+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].
|
|
|
+
|
|
|
+Обычно требуется, чтобы структура действительно вела себя как значе
|
|
|
+ние, то есть чтобы копия становилась полностью независимой от своего
|
|
|
+источника. Для этого определите конструктор копирования так:
|
|
|
+
|
|
|
+```d
|
|
|
+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; }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Конструктор копирования вступает в силу во время копирования объ
|
|
|
+екта. Чтобы инициализировать объект `приемник` с помощью объекта `источник` того же типа, компилятор должен выполнить следующие шаги:
|
|
|
+
|
|
|
+1. Скопировать участок «сырой» памяти объекта `источник` в участок
|
|
|
+«сырой» памяти объекта `приемник`.
|
|
|
+2. Транзитивно для каждого поля, содержащего другие поля (то есть
|
|
|
+поля, содержащего другое поле, содержащее третье поле, ...), для ко
|
|
|
+торого определен метод `this(this)`, вызвать эти конструкторы снизу
|
|
|
+вверх (начиная от наиболее глубоко вложенного поля).
|
|
|
+3. Вызвать метод `this(this)` с объектом приемник.
|
|
|
+
|
|
|
+Оригинальное название конструктора копирования «postblit construc
|
|
|
+tor» происходит от «blit» – популярной аббревиатуры понятия «block
|
|
|
+transfer», означавшего копирование «сырой» памяти. Язык применяет
|
|
|
+«сырое» копирование при инициализации и разрешает сразу после это
|
|
|
+го воспользоваться ловушкой. В предыдущем примере конструктор ко
|
|
|
+пирования превращает только что полученный псевдоним массива в на
|
|
|
+стоящую, полномасштабную копию, гарантируя, что с этого момента
|
|
|
+между объектом-оригиналом и объектом-копией не будет ничего обще
|
|
|
+го. Теперь, после добавления конструктора копирования, модуль легко
|
|
|
+проходит этот тест:
|
|
|
+
|
|
|
+```d
|
|
|
+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` по значению
|
|
|
+в функцию также создается копия:
|
|
|
+
|
|
|
+```d
|
|
|
+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` членом другой структуры, которая в свою очередь является чле
|
|
|
+ном третьей структуры:
|
|
|
+
|
|
|
+```d
|
|
|
+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`.
|
|
|
+Внесем ясность:
|
|
|
+
|
|
|
+```d
|
|
|
+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
|
|
|
+// Это не 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`
|
|
|
+(определенный выше) по значению:
|
|
|
+
|
|
|
+```d
|
|
|
+Widget hun(uint x)
|
|
|
+{
|
|
|
+ return Widget(x * 2);
|
|
|
+}
|
|
|
+
|
|
|
+unittest
|
|
|
+{
|
|
|
+ auto w = hun(1000);
|
|
|
+ ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Наивный подход: просто создать объект типа `Widget` внутри функции
|
|
|
+`hun`, а затем скопировать его в переменную `w`, применив побитовое копи
|
|
|
+рование с последующим вызовом `this(this)`. Но это было бы слишком
|
|
|
+расточительно: D полагается на перемещаемость объектов, так почему
|
|
|
+бы попросту не переместить в переменную `w` уже отживший свое времен
|
|
|
+ный объект, созданный функцией `hun`? Разницу никто не заметит, по
|
|
|
+скольку после того, как функция `hun` вернет результат, временный объ
|
|
|
+ект уже не нужен. Если в лесу упало дерево и никто этого не слышит, то
|
|
|
+легче переместить его, чем копировать. Похожий (но не идентичный)
|
|
|
+случай:
|
|
|
+
|
|
|
+```d
|
|
|
+Widget iun(uint x)
|
|
|
+{
|
|
|
+ auto result = Widget(x * 2);
|
|
|
+ ...
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+unittest
|
|
|
+{
|
|
|
+ auto w = iun(1000);
|
|
|
+ ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+В этом случае переменная `result` тоже уходит в небытие сразу же после
|
|
|
+того, как `iun` вернет управление, поэтому в вызове `this(this)` необходи
|
|
|
+мости нет. Наконец, еще более тонкий случай:
|
|
|
+
|
|
|
+```d
|
|
|
+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`
|
|
|
+стандартной библиотеки:
|
|
|
+
|
|
|
+```d
|
|
|
+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()`:
|
|
|
+
|
|
|
+```d
|
|
|
+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. Алгоритм уничтожения структуры
|
|
|
+
|
|
|
+По умолчанию объекты-структуры уничтожаются в порядке, строго
|
|
|
+обратном порядку их создания. То есть первым уничтожается объект-
|
|
|
+структура, определенный в заданной области видимости последним:
|
|
|
+
|
|
|
+```d
|
|
|
+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.
|
|
|
+
|
|
|
+```d
|
|
|
+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`). Поиск имен внутри методов
|
|
|
+структуры производится так же, как и внутри методов класса: парамет
|
|
|
+ры перекрывают одноименные внутренние элементы структуры, а име
|
|
|
+на внутренних элементов структуры перекрывают те же имена, объяв
|
|
|
+ленные на уровне модуля.
|
|
|
+
|
|
|
+```d
|
|
|
+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. Оператор присваивания
|
|
|
+
|
|
|
+По умолчанию, если задать:
|
|
|
+
|
|
|
+```d
|
|
|
+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`:
|
|
|
+
|
|
|
+```d
|
|
|
+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))`.
|
|
|
+
|
|
|
+Осталась одна проблема. Рассмотрим присваивание:
|
|
|
+
|
|
|
+```d
|
|
|
+Widget w;
|
|
|
+...
|
|
|
+w = Widget(50); // Ошибка! Невозможно привязать r-значение типа Widget к ссылке ref Widget!
|
|
|
+```
|
|
|
+
|
|
|
+Проблема в том, что метод `opAssign` в таком виде, в каком он определен
|
|
|
+сейчас, ожидает аргумент типа `ref Widget`, то есть l-значение типа `Widget`.
|
|
|
+Чтобы помимо l-значений можно было бы присваивать еще и r-значе
|
|
|
+ния, структура `Widget` должна определять *два* оператора присваивания:
|
|
|
+
|
|
|
+```d
|
|
|
+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`
|
|
|
+может проверять, достаточно ли в текущем массиве места для размеще
|
|
|
+ния нового содержимого, и если да, то достаточно и записи поверх ста
|
|
|
+рого массива на том же месте.
|
|
|
+
|
|
|
+```d
|
|
|
+// Внутри 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`.
|
|
|
+
|
|
|
+```d
|
|
|
+struct Point
|
|
|
+{
|
|
|
+ int x, y;
|
|
|
+}
|
|
|
+
|
|
|
+unittest
|
|
|
+{
|
|
|
+ Point a, b;
|
|
|
+ assert(a == b);
|
|
|
+ a.x = 1;
|
|
|
+ assert(a != b);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Чтобы определить собственный порядок сравнения, определите метод
|
|
|
+`opEquals`:
|
|
|
+
|
|
|
+```d
|
|
|
+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`:
|
|
|
+
|
|
|
+```d
|
|
|
+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` эквива
|
|
|
+лентно вычислению выражения
|
|
|
+
|
|
|
+```d
|
|
|
+a.leftBottom == b.leftBottom && a.rightTop == b.rightTop
|
|
|
+```
|
|
|
+
|
|
|
+что в свою очередь можно переписать так:
|
|
|
+
|
|
|
+```d
|
|
|
+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), во всем, кроме ви
|
|
|
+димости и ограничений доступа, наложенных на эти статические дан
|
|
|
+ные родительской структурой.
|
|
|
+
|
|
|
+```d
|
|
|
+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`
|
|
|
+применительно к структурам не имеет смысла, поскольку структуры
|
|
|
+не поддерживают наследование.
|
|
|
+
|
|
|
+За подробной информацией обратитесь к соответствующим разделам.
|
|
|
+А здесь мы лишь вкратце напомним смысл спецификаторов:
|
|
|
+
|
|
|
+```d
|
|
|
+struct S
|
|
|
+{
|
|
|
+ private int a; // Доступен в пределах текущего файла и в методах S
|
|
|
+ package int b; // Доступен в пределах каталога текущего файла
|
|
|
+ public int c; // Доступен в пределах текущего приложения
|
|
|
+ export int d; // Доступен вне текущего приложения (там, где оно используется)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Заметим, что хотя ключевое слово `export` разрешено везде, где синтак
|
|
|
+сис допускает применение спецификатора доступа, семантика этого
|
|
|
+ключевого слова зависит от реализации.
|
|
|
+
|
|
|
+### 7.1.8. Вложенность структур и классов
|
|
|
+
|
|
|
+Часто бывает удобно вложить в структуру другую структуру или класс.
|
|
|
+Например, контейнер дерева можно представить как оболочку-струк
|
|
|
+туру с простым интерфейсом поиска, а внутри нее для определения уз
|
|
|
+лов дерева использовать полиморфизм.
|
|
|
+
|
|
|
+```d
|
|
|
+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) { ... }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Аналогично структура может быть вложена в другую структуру...
|
|
|
+
|
|
|
+```d
|
|
|
+struct Widget
|
|
|
+{
|
|
|
+private:
|
|
|
+ struct Field
|
|
|
+ {
|
|
|
+ string name;
|
|
|
+ uint x, y;
|
|
|
+ }
|
|
|
+ Field[] fields;
|
|
|
+public:
|
|
|
+ ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+...и наконец, структура может быть вложена в класс.
|
|
|
+
|
|
|
+```d
|
|
|
+class Window
|
|
|
+{
|
|
|
+ struct Info
|
|
|
+ {
|
|
|
+ string name;
|
|
|
+ Window parent;
|
|
|
+ Window[] children;
|
|
|
+ }
|
|
|
+ Info getInfo();
|
|
|
+ ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+В отличие от классов, вложенных в другие классы, вложенные струк
|
|
|
+туры и классы, вложенные в другие структуры, не обладают никаким
|
|
|
+скрытым внутренним элементом `outer` – никакой специальный код не
|
|
|
+генерируется. Такие вложенные типы определяются в основном со
|
|
|
+структурной целью – чтобы получить нужное управление доступом.
|
|
|
+
|
|
|
+### 7.1.9. Структуры, вложенные в функции
|
|
|
+
|
|
|
+Вспомним, что говорилось в разделе 6.11.1: вложенные классы находят
|
|
|
+ся в привилегированном положении, ведь они обладают особыми, уни
|
|
|
+кальными свойствами. Вложенному классу доступны параметры и ло
|
|
|
+кальные переменные включающей функции. Если вы возвращаете вло
|
|
|
+женный класс в качестве результата функции, компилятор даже разме
|
|
|
+щает кадр функции в динамической памяти, чтобы параметры
|
|
|
+и локальные переменные функции выжили после того, как она вернет
|
|
|
+управление.
|
|
|
+
|
|
|
+Для единообразия и согласованности D оказывает структурам, вложен
|
|
|
+ным в функции, те же услуги, что и классам, вложенным в функции.
|
|
|
+Вложенная структура может обращаться к параметрам и локальным
|
|
|
+переменным включающей функции:
|
|
|
+
|
|
|
+```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`:
|
|
|
+
|
|
|
+```d
|
|
|
+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`:
|
|
|
+
|
|
|
+```d
|
|
|
+struct Final(T)
|
|
|
+{
|
|
|
+ // Запретить присваивание
|
|
|
+ @disable void opAssign(Final);
|
|
|
+ ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+С помощью атрибута `@disable` можно запретить и другие сгенерирован
|
|
|
+ные функции, например сравнение.
|
|
|
+
|
|
|
+До сих пор все шло хорошо. Чтобы реализовать `Final!T`, нужно с помо
|
|
|
+щью конструкции `alias this` сделать `Final(T)` подтипом `T`, но чтобы при
|
|
|
+этом полученный тип не являлся l-значением. Ошибочное решение вы
|
|
|
+глядит так:
|
|
|
+
|
|
|
+```d
|
|
|
+// Ошибочное решение
|
|
|
+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;`. Этот
|
|
|
+тест модуля делает что-то непредусмотренное:
|
|
|
+
|
|
|
+```d
|
|
|
+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` по значению:
|
|
|
+
|
|
|
+```d
|
|
|
+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`:
|
|
|
+
|
|
|
+```d
|
|
|
+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
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Чтобы избежать таких коллизий, воспользуемся соглашением об име
|
|
|
+новании. Для надежности будем просто добавлять к именам видимых
|
|
|
+свойств имя соответствующего типа:
|
|
|
+
|
|
|
+```d
|
|
|
+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*). Рас
|
|
|
+смотрим пример:
|
|
|
+
|
|
|
+```d
|
|
|
+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`, неявно определенного для каждого поля класса или
|
|
|
+структуры:
|
|
|
+
|
|
|
+```d
|
|
|
+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`
|
|
|
+позволяет получить смещение для любого внутреннего элемента струк
|
|
|
+туры в виде константы, известной во время компиляции.
|
|
|
+
|
|
|
+```d
|
|
|
+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` в действии:
|
|
|
+
|
|
|
+```d
|
|
|
+class A
|
|
|
+{
|
|
|
+ char a;
|
|
|
+ align(1) int b;
|
|
|
+ char c;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+При таком определении поля структуры `A` располагаются без пустот
|
|
|
+между ними. (В конце объекта при этом может оставаться зарезервиро
|
|
|
+ванное, но не занятое место.) Аргумент атрибута `align` означает *максимальное* выравнивание поля, но реальное выравнивание не может пре
|
|
|
+высить естественное выравнивание для типа этого поля. Получить ес
|
|
|
+тественное выравнивание типа `T` позволяет определенное компилятором
|
|
|
+свойство `T.alignof`. Если вы, например, укажете для `b` выравнивание
|
|
|
+align(200) вместо указанного в примере `align(1)`, то реально выравнива
|
|
|
+ние примет значение `4`, равное `int.alignof`.
|
|
|
+
|
|
|
+Атрибут `align` можно применять к целому классу или структуре:
|
|
|
+
|
|
|
+```d
|
|
|
+align(1) struct A
|
|
|
+{
|
|
|
+ char a;
|
|
|
+ int b;
|
|
|
+ char c;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Для структуры атрибут `align` устанавливает выравнивание по умолча
|
|
|
+нию заданным значением. Это умолчание можно переопределить инди
|
|
|
+видуа льными атрибутами `align` внутри структуры. Если для поля ти
|
|
|
+па `T` указать только ключевое слово `align` без числа, компилятор прочи
|
|
|
+тает это как `align(T.alignof)`, то есть такая запись переустанавливает
|
|
|
+выравнивание поля в его естественное значение.
|
|
|
+
|
|
|
+Атрибут `align` не предназначен для использования с указателями и ссыл
|
|
|
+ками. Сборщик мусора действует из расчета, что все ссылки и указатели
|
|
|
+выровнены по размеру типа `size_t`. Компилятор не настаивает на со
|
|
|
+блюдении этого ограничения, поскольку в общем случае у вас могут
|
|
|
+быть указатели и ссылки, не контролируемые сборщиком мусора. Та
|
|
|
+ким образом, следующее определение крайне опасно, поскольку компи
|
|
|
+лируется без предупреждений:
|
|
|
+
|
|
|
+```d
|
|
|
+struct Node
|
|
|
+{
|
|
|
+ short value;
|
|
|
+ align(2) Node* next; // Избегайте таких определений
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Если этот код выполнит присваивание `объект.next = new Node` (то есть за
|
|
|
+полнит `объект.next` ссылкой, контролируемой сборщиком мусора), хаос
|
|
|
+обеспечен: неверно выровненная ссылка пропадает из поля зрения сбор
|
|
|
+щика мусора, память может быть освобождена, и `объект.next` превраща
|
|
|
+ется в «висячий» указатель.
|
|
|
+
|
|
|
+## 7.2. Объединение
|
|
|
+
|
|
|
+Объединения в стиле C можно использовать и в D, но не забывайте, что
|
|
|
+делать это нужно редко и с особой осторожностью.
|
|
|
+
|
|
|
+Объединение (`union`) – это что-то вроде структуры, все внутренние поля
|
|
|
+которой начинаются по одному и тому же адресу. Таким образом, их об
|
|
|
+ласти памяти перекрываются, а это значит, что именно вы как пользо
|
|
|
+ватель объединения отвечаете за соответствие записываемой и считы
|
|
|
+ваемой информации: нужно всегда читать в точности тот тип, который
|
|
|
+был записан. В любой конкретный момент времени только один внут
|
|
|
+ренний элемент объединения обладает корректным значением.
|
|
|
+
|
|
|
+```d
|
|
|
+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`, укажите
|
|
|
+нужное инициализирующее выражение в фигурных скобках:
|
|
|
+
|
|
|
+```d
|
|
|
+unittest
|
|
|
+{
|
|
|
+ IntOrFloat iof = { 5 };
|
|
|
+ assert(iof._int == 5);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+В статическом объекте типа `union` может быть инициализировано и дру
|
|
|
+гое поле. Для этого используйте следующий синтаксис:
|
|
|
+
|
|
|
+```d
|
|
|
+unittest
|
|
|
+{
|
|
|
+ static IntOrFloat iof = { _float : 5 };
|
|
|
+ assert(iof._float == 5);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Следует отметить, что нередко объединение служит именно для того,
|
|
|
+чтобы считывать тип, отличный от исходно записанного, – в соответст
|
|
|
+вии с порядком управления представлением, принятым в некоторой
|
|
|
+системе. По этой причине компилятор не выявляет даже те случаи не
|
|
|
+корректного использования объединений, которые может обнаружить.
|
|
|
+Например, на 32-разрядной машине Intel следующий код компилиру
|
|
|
+ется и даже выполнение инструкции `assert` не порождает исключений:
|
|
|
+
|
|
|
+```
|
|
|
+unittest
|
|
|
+{
|
|
|
+ IntOrFloat iof;
|
|
|
+ iof._float = 1;
|
|
|
+ assert(iof._int == 0x3F80_0000);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Объединение может определять функции-члены и, в общем случае, лю
|
|
|
+бые из тех внутренних элементов, которые может определять структу
|
|
|
+ра, за исключением конструкторов и деструкторов.
|
|
|
+
|
|
|
+Чаще всего (точнее, наименее редко) объединения используются в каче
|
|
|
+стве анонимных членов структур. Например:
|
|
|
+
|
|
|
+```d
|
|
|
+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` – как сказать «да
|
|
|
+вайте перечислим несколько символьных значений», не ассоциируя их
|
|
|
+с новым типом:
|
|
|
+
|
|
|
+```d
|
|
|
+enum
|
|
|
+ mega = 1024 * 1024,
|
|
|
+ pi = 3.14,
|
|
|
+ euler = 2.72,
|
|
|
+ greet = "Hello";
|
|
|
+```
|
|
|
+
|
|
|
+С `enum` механизм автоматического определения типа работает так же,
|
|
|
+как и с `auto`, поэтому в нашем примере переменные `pi` и `euler` имеют тип
|
|
|
+`double`, a переменная `greet` – тип `string`. Чтобы определить одно или не
|
|
|
+сколько перечисляемых значений определенного типа, укажите их спра
|
|
|
+ва от ключевого слова `enum`:
|
|
|
+
|
|
|
+```d
|
|
|
+enum float verySmall = 0.0001, veryBig = 10000;
|
|
|
+enum dstring wideMsg = "Wide load";
|
|
|
+```
|
|
|
+
|
|
|
+Перечисляемые значения – это константы; они практически эквива
|
|
|
+лентны литералам, которые обозначают. В частности, поддерживают те
|
|
|
+же операции – например, невозможно получить адрес `pi`, как невоз
|
|
|
+можно получить адрес `3.14`:
|
|
|
+
|
|
|
+```d
|
|
|
+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`:
|
|
|
+
|
|
|
+```d
|
|
|
+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, созданный классом Con
|
|
|
+tainer, – единственный разумный способ обратиться из внешнего мира
|
|
|
+к аргументу, привязанному к параметру 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).is
|
|
|
+Numeric, чем освободит пользователя от хлопот с добавлением идентифи
|
|
|
+катора в конец имени шаблона.
|
|
|
+Шаблон, проделывающий фокус с «тезками», может определять внутри
|
|
|
+себя и другие идентификаторы, но они будут попросту недоступны за
|
|
|
+пределами этого шаблона. Дело в том, что компилятор заменяет иден
|
|
|
+тификаторы на раннем этапе процесса поиска имен. Единственный спо
|
|
|
+соб получить доступ к таким идентификаторам – обратиться к ним из
|
|
|
+тела самого шаблона. Например:
|
|
|
+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. Параметр шаблона this1
|
|
|
+Познакомившись с классами и структурами, можно параметризовать
|
|
|
+наш обобщенный метод типом неявного аргумента 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.get
|
|
|
+Name(), Derived1.getName() и Derived2.getName().
|
|
|
+Также параметр this удобен в случае, когда один метод нужно исполь
|
|
|
+зовать для разных квалификаторов неизменяемости объекта (см. гла
|
|
|
+ву 8).
|
|
|
+
|
|
|
+7.6. Инъекции кода с помощью
|
|
|
+конструкции mixin template
|
|
|
+При некоторых программных решениях приходится добавлять шаблон
|
|
|
+ный код (такой как определения данных и методов) в одну или несколь
|
|
|
+ко реализаций классов. К типичным примерам относятся поддержка
|
|
|
+сериализации, шаблон проектирования «Наблюдатель» [27] и передача
|
|
|
+событий в оконных системах.
|
|
|
+Для этих целей можно было бы воспользоваться механизмом наследова
|
|
|
+ния, но поскольку реализуется лишь одиночное наследование, опреде
|
|
|
+лить для заданного класса несколько источников шаблонного кода не
|
|
|
+возможно. Иногда необходим механизм, позволяющий просто вставить
|
|
|
+в класс некоторый готовый код, вместо того чтобы писать его вручную.
|
|
|
+Здесь-то и пригодится конструкция 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 – очень полезное средство, позволяющее привязать один иденти
|
|
|
+фикатор к другому. Нередко псевдоним – единственное средство полу
|
|
|
+чить извне доступ к идентификатору, вычисляемому в рамках вложен
|
|
|
+ной сущности, или к длинному и сложному идентификатору.
|
|
|
+Параметризированные контексты, использующие конструкцию templa
|
|
|
+te, весьма полезны для определения вычислений во время компиля
|
|
|
+ции, таких как интроспекция типов и определение особенностей типов.
|
|
|
+Одноименные шаблоны позволяют предоставлять абстракции в очень
|
|
|
+удобной, инкапсулированной форме.
|
|
|
+Кроме того, предлагаются параметризированные контексты, прини
|
|
|
+мающие форму шаблонов mixin, которые во многом ведут себя подобно
|
|
|
+макросам. В будущем шаблоны mixin может заменить развитое средст
|
|
|
+во AST-макросов.
|
|
|
+
|
|
|
[^1]: Не считая эквивалентных имен, создаваемых с помощью `alias`, о чем мы еще поговорим в этой главе (см. раздел 7.4).
|
|
|
+[^2]: Термин «клуктура» предложил Бартош Милевски.
|
|
|
+[^3]: Кроме того, ‹код1› может сохранить указатель на значение w, которое исполь
|
|
|
+зует ‹код2›.
|
|
|
+[^4]: На момент написания оригинала книги данная возможность отсутствова
|
|
|
+ла, но поскольку теперь она существует, мы добавили ее описание в пере
|
|
|
+вод. – Прим. науч. ред.
|