Глава 8 готова
This commit is contained in:
parent
b4d0baee1e
commit
0cad05de5a
|
@ -1,5 +1,16 @@
|
|||
# 8. Квалификаторы типа
|
||||
|
||||
- [8.1. Квалификатор immutable](#8-1-квалификатор-immutable)
|
||||
- [8.1.1. Транзитивность](#8-1-1-транзитивность)
|
||||
- [8.2. Составление типов с помощью immutable](#8-2-составление-типов-с-помощью-immutable)
|
||||
- [8.3. Неизменяемые параметры и методы](#8-3-неизменяемые-параметры-и-методы)
|
||||
- [8.4. Неизменяемые конструкторы](#8-4-неизменяемые-конструкторы)
|
||||
- [8.5. Преобразования с участием immutable](#8-5-преобразования-с-участием-immutable)
|
||||
- [8.6. Квалификатор const](#8-6-квалификатор-const)
|
||||
- [8.7. Взаимодействие между const и immutable](#8-7-взаимодействие-между-const-и-immutable)
|
||||
- [8.8. Распространение квалификатора с параметра на результат](#8-8-распространение-квалификатора-с-параметра-на-результат)
|
||||
- [8.9. Итоги](#8-9-итоги)
|
||||
|
||||
Квалификаторы типа выражают важные утверждения о типах языка. Эти утверждения исключительно полезны как для программиста, так и для компилятора, но их сложно выразить путем соглашений, обычного порождения подтипов (см. раздел 6.4.2) или параметризации типами (см. раздел 6.14).
|
||||
|
||||
Показательный пример квалификатора типа – квалификатор типа `const` (введенный в языке C и доработанный в C++). Примененный к типу `T`, этот квалификатор выражает следующее утверждение: значения типа `T` можно инициализировать и читать, но не перезаписывать. Соблюдение этого ограничения гарантируется компилятором. Квалификатор `const` довольно полезен внутри модуля, поскольку гарантирует инициаторам вызовов регламентированное поведение функций. Например, сигнатура
|
||||
|
@ -68,3 +79,485 @@ immutable pi = 3.14, val = 42;
|
|||
Для `pi` компилятор выводит тип `immutable(double)`, а для `val` – `immutable(int)`.
|
||||
|
||||
[В начало ⮍](#8-1-квалификатор-immutable) [Наверх ⮍](#8-квалификаторы-типа)
|
||||
|
||||
### 8.1.1. Транзитивность
|
||||
|
||||
Любой тип можно определить с квалификатором `immutable`. Например:
|
||||
|
||||
```d
|
||||
struct Point { int x, y; }
|
||||
auto origin = immutable(Point)(0, 0);
|
||||
```
|
||||
|
||||
Поскольку для всех типов `T` справедливо, что `immutable(T)` – такой же тип, как любой другой, запись `immutable(Point)(0, 0)` является литералом структуры – так же как и `Point(0, 0)`.
|
||||
|
||||
Неизменяемость естественным образом распространяется на все внутренние элементы объекта. Ведь пользователь ожидает, что если запрещено присваивание объекту `origin` в целом, то запрещено и присваивание полям `origin.x` и `origin.y`. Иначе было бы очень легко нарушить ограничение, налагаемое квалификатором immutable на объект в целом.
|
||||
|
||||
```d
|
||||
unittest
|
||||
{
|
||||
auto anotherOrigin = immutable(Point)(1, 1);
|
||||
origin = anotherOrigin; // Ошибка!
|
||||
origin.x = 1; // Ошибка!
|
||||
origin.y = 1; // Ошибка!
|
||||
}
|
||||
```
|
||||
|
||||
На самом деле, `immutable` *распространяется* абсолютно на каждое поле `Point`, тип каждого поля объекта квалифицируется тем же квалификатором, что и сам объект. Например, такой тест будет пройден:
|
||||
|
||||
```d
|
||||
static assert(is(typeof(origin.x) == immutable(int))); // Тест пройден
|
||||
```
|
||||
|
||||
Но мир не настолько прост. Рассмотрим структуру, в которой есть некоторая косвенность, например поле массива:
|
||||
|
||||
```d
|
||||
struct DataSample
|
||||
{
|
||||
int id;
|
||||
double[] payload;
|
||||
}
|
||||
```
|
||||
|
||||
Очевидно, что поля объекта типа `immutable(DataSample)` не могут быть изменены. Но как насчет изменения элемента массива `payload`?
|
||||
|
||||
```d
|
||||
unittest
|
||||
{
|
||||
auto ds = immutable(DataSample)(5, [ 1.0, 2.0 ]);
|
||||
ds.payload[1] = 4.5; // ?
|
||||
}
|
||||
```
|
||||
|
||||
В данном случае может быть принято одно из двух возможных решений, у каждого из которых есть свои плюсы и минусы. Один из вариантов – сделать действие квалификатора поверхностным, то есть руководствоваться тем, что квалификатор immutable, примененный к структуре DataSample, применяется и ко всем ее непосредственным полям, но никак не влияет на данные, косвенно доступные через эти поля[^1]. Альтернативное решение – сделать неизменяемость транзитивной, что означало бы следующее: делая объект неизменяемым, вы также делаете неизменяемыми все данные, к которым можно обратиться через этот объект. Язык D пошел по второму пути.
|
||||
|
||||
Транзитивная неизменяемость гораздо строже нетранзитивной. Определив неизменяемое значение, вы накладываете ограничение неизменяемости на целую сеть данных, связанную с этим значением (через ссылки, массивы и указатели). Таким образом, определять транзитивно неизменяемые значения гораздо сложнее, чем поверхностно неизменяемые. Но приложенные усилия окупаются сторицей. D выбрал транзитивную изменяемость по двум основным причинам:
|
||||
|
||||
- *Функциональное программирование*. Словосочетание «функциональный стиль» каждый интерпретирует по-своему, но большинство согласятся, что отсутствие побочных эффектов – важный принцип. Обеспечить соблюдение этого ограничения лишь соглашением – значит отказаться от масштабируемости. Транзитивная неизменяемость позволяет программисту применять функциональный стиль для хорошо определенного фрагмента программы, а компилятору – проверять этот функциональный код на предмет непреднамеренных изменений данных.
|
||||
- *Параллельное программирование*. Параллелизм – это огромная и очень сложная тема, занимаясь которой, ощущаешь нехватку твердых гарантий и неоспоримых истин. Неизменяемое разделение – один из таких островков уверенности: разделение неизменяемых данных между потоками всегда корректно, безопасно и эффективно. Чтобы позволить компилятору проверять, не изменяемы ли на самом деле разделяемые данные, неизменяемость должна быть транзитивной; в противном случае поток, обладающий доступом к неизменяемому фрагменту данных, может легко перейти к изменяемому разделению, попросту обратившись к косвенным полям этого фрагмента данных.
|
||||
|
||||
Получив значение типа `immutable(T)`, можно быть абсолютно уверенным, что все, до чего можно добраться через это значение, также квалифицировано с помощью `immutable`, то есть неизменяемо. Более того, *никто* никогда не сможет изменить эти данные – данные, помеченные квалификатором `immutable`, все равно что впаяны. Это очень надежная гарантия, позволяющая, к примеру, беззаботно разделять такие неизменяемые данные между потоками.
|
||||
|
||||
[В начало ⮍](#8-1-1-транзитивность) [Наверх ⮍](#8-квалификаторы-типа)
|
||||
|
||||
## 8.2. Составление типов с помощью immutable
|
||||
|
||||
Учитывая, что для точного выбора типа, который нужно квалифицировать, с квалификаторами используют скобки и что тип с квалификатором – это полноправный новый тип, можно сделать вывод, что, комбинируя ключевое слово `immutable` с другими конструкторами типов, можно создавать весьма сложные структуры данных. Сравним, например, следующие два типа:
|
||||
|
||||
```d
|
||||
alias immutable(int[]) T1;
|
||||
alias immutable(int)[] T2;
|
||||
```
|
||||
|
||||
В первом определении круглые скобки поглотили полностью весь тип массива; во втором случае затронут лишь тип элементов массива `int`, но не сам массив. Если при употреблении квалификатора `immutable` круглые скобки отсутствуют, квалификатор применяется ко всему типу, так что эквивалентное определение `T1` выглядит так:
|
||||
|
||||
```d
|
||||
alias immutable int[] T1;
|
||||
```
|
||||
|
||||
Тип `T1` незамысловат: он представляет собой неизменяемый массив значений типа `int`. Само написание этого типа говорит то же самое. В соответствии со свойством транзитивности нельзя изменить ни массив в целом (например, присвоив переменной, содержащей массив, новый массив), ни какой-либо его элемент в отдельности:
|
||||
|
||||
```d
|
||||
T1 a = [ 1, 3, 5 ];
|
||||
T1 b = [ 2, 4 ];
|
||||
a = b; // Ошибка!
|
||||
a[0] = b[1]; // Ошибка!
|
||||
```
|
||||
|
||||
Второе определение кажется более тонким, но на самом деле понять его довольно просто, если вспомнить, что `immutable(int)` – самостоятельный тип. Тогда `immutable(int)[]` – это просто массив элементов этого самостоятельного типа, вот и все. Вывод о свойствах этого массива напрашивается сам. Можно присвоить значение массиву в целом, но нельзя изменить (в том числе через присваивание) отдельные его элементы:
|
||||
|
||||
```d
|
||||
T2 a = [ 1, 3, 5 ];
|
||||
T2 b = [ 2, 4 ];
|
||||
a = b; // Все в порядке
|
||||
a[0] = b[1]; // Ошибка!
|
||||
a ~= b; // Все в порядке (но как тонко!)
|
||||
```
|
||||
|
||||
Может показаться странным, но добавление элементов в конец массива законно. Почему? Да просто потому, что эта операция не изменяет те элементы, которые в массиве уже есть. (Она может повлечь копирование данных, если потребуется переносить массив в другую область памяти, но в этом нет ничего страшного.)
|
||||
|
||||
Как уже говорилось (см. раздел 4.5), `string` – это в действительности лишь псевдоним для типа `immutable(char)[]`. На самом деле, многие из полезных свойств типа `string`, задействованных в предыдущих главах, – заслуга квалификатора `immutable`.
|
||||
|
||||
Сочетания ключевого слова `immutable` с параметрами-типами интерпретируются по той же логике. Предположим, есть обобщенный тип `Container!T`. Тогда в сочетании `immutable(Container!T)` квалификатор будет относиться ко всему контейнеру, а в сочетании `Container!(immutable(T))` – лишь к отдельным его элементам.
|
||||
|
||||
[В начало ⮍](#8-2-составление-типов-с-помощью-immutable) [Наверх ⮍](#8-квалификаторы-типа)
|
||||
|
||||
## 8.3. Неизменяемые параметры и методы
|
||||
|
||||
В сигнатуре функции квалификатор `immutable` очень информативен. Рассмотрим одну из простейших функций:
|
||||
|
||||
```d
|
||||
string process(string input);
|
||||
```
|
||||
|
||||
На самом деле это лишь краткая запись сигнатуры
|
||||
|
||||
```d
|
||||
immutable(char)[] process(immutable(char)[] input);
|
||||
```
|
||||
|
||||
Функция `process` гарантирует, что не будет изменять отдельные знаки параметра `input`, так что инициатор вызова функции `process` может быть уверен, что после вызова строка окажется такой же, как и перед ним:
|
||||
|
||||
```d
|
||||
string s1 = "чепуха";
|
||||
string s2 = process(s1);
|
||||
assert(s1 == "чепуха"); // Выполняется всегда
|
||||
```
|
||||
|
||||
Более того, пользователь функции `process` может рассчитывать на то, что ее результат не сможет быть изменен: нет никаких скрытых псевдонимов, никакая другая функция не сможет позже изменить `s2`. В этом случае `immutable` также означает неизменяемость.
|
||||
|
||||
Структуры и классы могут определять неизменяемые методы. В подобных случаях квалификатор применяется к `this`:
|
||||
|
||||
```d
|
||||
class A
|
||||
{
|
||||
int[] fun(); // Обычный метод
|
||||
int[] gun() immutable; // Можно вызвать, только если объект неизменяемый
|
||||
immutable int[] hun(); // То же, что выше
|
||||
}
|
||||
```
|
||||
|
||||
Третья форма записи выглядит подозрительно: может показаться, что квалификатор `immutable` относится к `int[]`, но на самом деле он относится к `this`. Если нужно определить неизменяемый метод, возвращающий `immutable int[]`, получается что-то вроде заикания:
|
||||
|
||||
```d
|
||||
immutable immutable(int[]) iun();
|
||||
```
|
||||
|
||||
Поэтому в таких случаях ключевое слово `immutable` лучше писать в конце:
|
||||
|
||||
```d
|
||||
immutable(int[]) iun() immutable;
|
||||
```
|
||||
|
||||
Разрешение оставить несколько сбивающий с толку `immutable` в начале определения продиктовано в основном стремлением унифицировать формат указания для всех свойств методов (таких как `final` или `static`). Например, определить сразу несколько неизменяемых методов, можно так:
|
||||
|
||||
```d
|
||||
class A
|
||||
{
|
||||
immutable
|
||||
{
|
||||
int foo();
|
||||
int[] bar();
|
||||
void baz();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Кроме того, квалификатор `immutable` можно использовать в виде метки:
|
||||
|
||||
```d
|
||||
class A
|
||||
{
|
||||
immutable:
|
||||
int foo();
|
||||
int[] bar();
|
||||
void baz();
|
||||
}
|
||||
```
|
||||
|
||||
Разумеется, неизменяемые методы могут быть вызваны только применительно к неизменяемым объектам:
|
||||
|
||||
```d
|
||||
class C
|
||||
{
|
||||
void fun() {}
|
||||
void gun() immutable {}
|
||||
}
|
||||
|
||||
unittest
|
||||
{
|
||||
auto c1 = new C;
|
||||
auto c2 = new immutable(C);
|
||||
c1.fun(); // Все в порядке
|
||||
c2.gun(); // Все в порядке
|
||||
// Никакие другие вызовы не сработают
|
||||
}
|
||||
```
|
||||
|
||||
[В начало ⮍](#8-3-неизменяемые-параметры-и-методы) [Наверх ⮍](#8-квалификаторы-типа)
|
||||
|
||||
## 8.4. Неизменяемые конструкторы
|
||||
|
||||
Работать с неизменяемым объектом совсем несложно, а вот его построение – весьма деликатный процесс. Причина в том, что в процессе построения нужно удовлетворить два противоречивых требования: 1) присвоить полям значения, 2) сделать их неизменяемыми. Поэтому D особенно внимателен к неизменяемым конструкторам.
|
||||
|
||||
Проверка типов в неизменяемом конструкторе выполняется простым и осторожным способом. Компилятор разрешает присваивание полей только внутри конструктора, а чтение полей (включая передачу this в качестве аргумента при вызове метода) запрещено. Как только выполнение неизменяемого конструктора завершается, объект «замораживается» – после этого нельзя потребовать ни одного изменения. Вызов нестатического метода считается за чтение, поскольку такой метод обладает доступом к объекту this и способен прочесть любое его поле. (Компилятор не проверяет, читает ли метод поля на самом деле, – для перестраховки он предполагает, что метод все же читает некоторое поле.)
|
||||
|
||||
Это правило строже, чем необходимо; ведь в действительности запрещается лишь присваивание значения полю после того, как это поле было прочитано. Однако это более строгое правило практически не мешает выразительности, при этом оно простое и понятное. Например:
|
||||
|
||||
```d
|
||||
class A
|
||||
{
|
||||
int a;
|
||||
int[] b;
|
||||
this() immutable
|
||||
{
|
||||
a = 5;
|
||||
b = [ 1, 2, 3 ];
|
||||
// Вызов fun() не был бы разрешен
|
||||
}
|
||||
void fun() immutable
|
||||
{
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Вызывать из неизменяемого конструктора конструктор родителя `super` в порядке вещей, если этот вызов адресован также неизменяемому конструктору. Такие вызовы не угрожают нарушить неизменяемость.
|
||||
|
||||
Инициализировать неизменяемые объекты обычно помогает рекурсия. К примеру, рассмотрим реализующий абстракцию односвязного списка класс, инициализируемый с помощью массива:
|
||||
|
||||
```d
|
||||
class List
|
||||
{
|
||||
private int payload;
|
||||
private List next;
|
||||
this(int[] data) immutable
|
||||
{
|
||||
enforce(data.length);
|
||||
payload = data[0];
|
||||
if (data.length == 1) return;
|
||||
next = new immutable(List)(data[1 .. $]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Чтобы корректно инициализировать хвост списка, конструктор рекурсивно вызывает сам себя с более коротким массивом. Попытки инициализировать список в цикле не пройдут компиляцию, поскольку проход по создаваемому списку означает чтение полей, а это запрещено. Рекурсия изящно решает эту проблему[^2].
|
||||
|
||||
[В начало ⮍](#8-4-неизменяемые-конструкторы) [Наверх ⮍](#8-квалификаторы-типа)
|
||||
|
||||
## 8.5. Преобразования с участием immutable
|
||||
|
||||
Рассмотрим пример кода:
|
||||
|
||||
```d
|
||||
unittest
|
||||
{
|
||||
int a = 42;
|
||||
immutable(int) b = a;
|
||||
int c = b;
|
||||
}
|
||||
```
|
||||
|
||||
Более строгая система типизации не приняла бы этот код. Он включает два преобразования: сначала из `int` в `immutable(int)`, а затем обратно из `immutable(int)` в `int`. Собственно, по общим правилам эти преобразования незаконны. Например, если в этом коде заменить `int` на `int[]`, ни одно из следующих преобразований не будет корректным:
|
||||
|
||||
```d
|
||||
int[] a = [ 42 ];
|
||||
immutable(int[]) b = a; // Нет!
|
||||
int[] c = b; // Нет!
|
||||
```
|
||||
|
||||
Если бы такие преобразования были разрешены, неизменяемость бы не соблюдалась, поскольку тогда неизменяемые массивы разделяли бы свое содержимое с изменяемыми.
|
||||
|
||||
Тем не менее компилятор распознает и разрешает некоторые автоматические преобразования между неизменяемыми и изменяемыми данными. А именно разрешено двунаправленное преобразование между `T` и `immutable(T)`, если у `T` «нет изменяемой косвенности». Интуитивно понятно, что «нет изменяемой косвенности» означает запрет перезаписывать косвенно доступные через `T` данные. Определение этого понятия рекурсивно:
|
||||
|
||||
- у встроенных типов значений, таких как `int`, «нет изменяемой косвенности»;
|
||||
- у массивов фиксированной длины из элементов типов, у которых «нет изменяемой косвенности», в свою очередь, тоже «нет изменяемой косвенности»;
|
||||
- у массивов и указателей, ссылающихся на типы, у которых «нет изменяемой косвенности», тоже «нет изменяемой косвенности»;
|
||||
- у структур, ни в одном поле которых «нет изменяемой косвенности», тоже «нет изменяемой косвенности».
|
||||
|
||||
Например, у типа `S1` нет изменяемой косвенности, а у типа `S2` – есть:
|
||||
|
||||
```d
|
||||
struct S1
|
||||
{
|
||||
int a;
|
||||
double[3] b;
|
||||
string c;
|
||||
}
|
||||
|
||||
struct S2
|
||||
{
|
||||
int x;
|
||||
float[] y;
|
||||
}
|
||||
```
|
||||
|
||||
Из-за поля `S2.y` у структуры `S2` есть изменяемая косвенность, так что преобразования вида `immutable(S2)` ↔ `S2` запрещены. Если бы они были разрешены, изменяемые и неизменяемые объекты стали бы некорректно разделять данные, хранимые в y, что нарушило бы гарантии, предоставленные квалификатором `immutable`.
|
||||
|
||||
Вернемся к примеру, приведенному в начале этого раздела. Тип `int` не обладает изменяемой косвенностью, так что компилятор волен разрешить преобразования из `int` в `immutable(int)` и обратно.
|
||||
|
||||
Чтобы определить такие преобразования для структуры, вам потребуется немного поработать вручную, направляя процесс в нужное русло. Вы предоставляете соответствующие конструкторы, а компилятор обеспечивает корректность вашего кода. Проще всего одолеть преобразование, заручившись поддержкой универсальной служебной функции преобразования `std.conv.to`[^3], которая понимает все тонкости преобразований типов с квалификаторами и всегда принимает соответственные меры.
|
||||
|
||||
```d
|
||||
import std.conv;
|
||||
|
||||
struct S
|
||||
{
|
||||
private int[] a;
|
||||
// Преобразование из неизменяемого в изменяемое
|
||||
this(immutable(S) source)
|
||||
{
|
||||
// Поместить дубликат массива в массив не-immutable
|
||||
a = to!(int[])(source.a);
|
||||
}
|
||||
// Преобразование из изменяемого в неизменяемое
|
||||
this(S source) immutable
|
||||
{
|
||||
// Поместить дубликат массива в массив immutable
|
||||
a = to!(immutable(int[]))(source.a);
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
unittest
|
||||
{
|
||||
S a;
|
||||
auto b = immutable(S)(a);
|
||||
auto c = S(b);
|
||||
}
|
||||
```
|
||||
|
||||
Преобразование не является неявным, но оно допустимо и безопасно.
|
||||
|
||||
[В начало ⮍](#8-5-преобразования-с-участием-immutable) [Наверх ⮍](#8-квалификаторы-типа)
|
||||
|
||||
## 8.6. Квалификатор const
|
||||
|
||||
Немного поэкспериментировав с квалификатором типа `immutable`, мы видим, что этот квалификатор слишком строг, чтобы быть полезным в большинстве случаев. Да, если вы обязались не изменять определенные данные на протяжении работы целой программы, `immutable` вам подходит. Но часто неизменяемость – свойство, полезное в рамках модуля: хотелось бы оставить за собой право изменять некоторые данные, а остальным запретить это делать. Такие данные нельзя назвать неизменяемыми (то есть пометить квалификатором `immutable`), поскольку `immutable` означает: «Видите письмена, высеченные на камне? Это ваши данные». А вам нужно средство, чтобы выразить такое ограничение: «Вы не можете изменить эти данные, но кто-кто другой – может». Или, как сказал Алан Перлис: «Кому константа, а кому и переменная». Посмотрим, как система типов вполне серьезно реализует изречение Перлиса.
|
||||
|
||||
Простой вариант использования таких данных – функция, например `print`, которая печатает какие-то данные. Функция `print` не изменяет переданные в нее данные, так что она допускает применение квалификатора `immutable`:
|
||||
|
||||
```d
|
||||
void print(immutable(int[]) data) { ... }
|
||||
|
||||
unittest
|
||||
{
|
||||
immutable(int[]) myData = [ 10, 20 ];
|
||||
print(myData); // Все в порядке
|
||||
}
|
||||
```
|
||||
|
||||
Отлично. Далее, пусть у нас есть значение типа `int[]`, которое мы только что вычислили и хотим напечатать. С таким аргументом наша функция не сработает, поскольку значение типа `int[]` не приводится к типу `immutable(int)[]` – а если бы приводилось, то возникла бы неподобающая общность изменяемых и якобы неизменяемых данных. Получается, что функция `print` не может напечатать данные типа `int[]`. Такое ограничение довольно неоправданно, поскольку `print` вообще не затрагивает свои аргументы, так что эта функция должна работать с неизменяемыми данными так же, как с изменяемыми.
|
||||
|
||||
Что нам нужно, так это нечто вроде общего «либо изменяемого, либо нет» типа. В этом случае функцию `print` можно было бы объявить так:
|
||||
|
||||
```d
|
||||
void print(либо_изменяемый_либо_нет(int[]) data) { ... }
|
||||
```
|
||||
|
||||
Только потому, что название `либо_изменяемый_либо_нет` несколько длинновато, для обозначения этого типа было введено ключевое слово `const`. Смысл абсолютно тот же: текущий код не может изменить значение типа `const(T)`, но есть вероятность, что это может сделать другой код. Такая двусмысленность отражает тот факт, что в роли `const(T)` может выступать как `T`, так и `immutable(T)`. Это качество квалификатора `const` делает его совершенным для организации взаимодействия между функциональным и обычным процедурным кодом. Продолжим начатый выше пример[^4]:
|
||||
|
||||
```d
|
||||
void print(const(int[]) data) { ... }
|
||||
|
||||
unittest
|
||||
{
|
||||
immutable(int[]) myData = [ 10, 20 ];
|
||||
print(myData); // Все в порядке
|
||||
int[] myMutableData = [ 32, 42 ];
|
||||
print(myMutableData); // Все в порядке
|
||||
}
|
||||
```
|
||||
|
||||
Этот пример подразумевает, что как изменяемые, так и неизменяемые данные неявно преобразуются к `const`, что, в свою очередь, подразумевает нечто вроде взаимоотношения с подтипами. На самом деле, все так и есть: `const(T)` – это супертип и для типа `T`, и для типа `immutable(T)` (рис. 8.1).
|
||||
|
||||
![image-8-6](images/image-8-6.png)
|
||||
|
||||
***Рис. 8.1.*** *Для всех типов `T`: `const(T)` – супертип и для `T`, и для `immutable(T)`. Следовательно, код, работающий со значениями типа `const(T)`, принимает значения как изменяемого, так и неизменяемого типа `T`*
|
||||
|
||||
Для квалификатора `const` верны те же правила транзитивности и преобразований, что и для квалификатора `immutable`. На конструкторы объектов `const`, в отличие от конструкторов `immutable`, ограничения не накладываются: внутри конструктора `const` объект считается изменяемым.
|
||||
|
||||
Метод, объявленный с квалификатором `const`, может вызываться для объектов с любым квалификатором, так как он гарантирует, что ничего менять не будет, но не требует этого от объекта. Например:
|
||||
|
||||
```d
|
||||
class C
|
||||
{
|
||||
void gun() const {}
|
||||
}
|
||||
|
||||
unittest
|
||||
{
|
||||
auto c1 = new C;
|
||||
auto c2 = new immutable(C);
|
||||
auto c3 = new const(C);
|
||||
c1.gun(); // Все в порядке
|
||||
c2.gun(); // Все в порядке
|
||||
c3.gun(); // Все в порядке
|
||||
}
|
||||
```
|
||||
|
||||
[В начало ⮍](#8-6-квалификатор-const) [Наверх ⮍](#8-квалификаторы-типа)
|
||||
|
||||
## 8.7. Взаимодействие между const и immutable
|
||||
|
||||
Нередко квалификатор пытается подействовать на тип, который уже находится под влиянием другого квалификатора. Например:
|
||||
|
||||
```d
|
||||
struct A
|
||||
{
|
||||
const(int[]) c;
|
||||
immutable(int[]) i;
|
||||
}
|
||||
|
||||
unittest
|
||||
{
|
||||
const(A) ca;
|
||||
immutable(A) ia;
|
||||
}
|
||||
```
|
||||
|
||||
Какие типы имеют поля `ca.i` и `ia.c`? Если бы квалификаторы применялись вслепую, получились бы типы `const(immutable(int[]))` и `immutable(const(int[]))` соответственно; очевидно, что-то тут лишнее, не говоря уже о типах `ca.c` и `ia.i`, когда один и тот же квалификатор применяется дважды!
|
||||
|
||||
При наложении одного квалификатора на другой D руководствуется простыми правилами композиции. Если квалификаторы идентичны, они сокращаются до одного. В противном случае как `const(immutable(T))`, так и `immutable(const(T))` сокращаются до `immutable(T)`, поскольку это более строгий тип. Эти правила применяются при распространении на типы элементов массива; например, элементы массива `const(immutable(T)[])` имеют тип `immutable(T)`, а не `const(immutable(T))`. При этом тип самого массива несократим.
|
||||
|
||||
[В начало ⮍](#8-7-взаимодействие-между-const-и-immutable) [Наверх ⮍](#8-квалификаторы-типа)
|
||||
|
||||
## 8.8. Распространение квалификатора с параметра на результат
|
||||
|
||||
C и C++ определяют поверхностный квалификатор `const` с неприятной особенностью: функция, возвращающая параметр, должна либо повторять свое определение дважды – для константных и неконстантных данных, либо вести опасную игру. Показательный пример такой функции – функция `strchr` из стандартной библиотеки C, которая в ней определена так:
|
||||
|
||||
```d
|
||||
char* strchr(const char* input, int c);
|
||||
```
|
||||
|
||||
Эта функция очищает типы от квалификаторов: несмотря на то что `input` – константное значение, которое, если рассуждать наивно, не должно измениться, возвращение в выводе указателя, порожденного от `input`, снимает с данных это обещание. `strchr` способствует появлению кода, изменяющего неизменяемые данные без приведения типов. C++ избавился от этой проблемы, введя два определения `strchr`:
|
||||
|
||||
```d
|
||||
char* strchr(char* input, int c);
|
||||
const char* strchr(const char* input, int c);
|
||||
```
|
||||
|
||||
Эти функции делают одно и то же, но их нельзя соединить в одну, поскольку в C++ нет средств, позволяющих сказать: «Если у аргумента есть квалификатор, пожалуйста, распространите его и на тип возвращаемого значения».
|
||||
|
||||
Для решения этой проблемы D предлагает «подстановочный» идентификатор квалификатора: `inout`. С участием `inout` объявление `strchr` выглядело бы так:
|
||||
|
||||
```d
|
||||
inout(char)* strchr(inout(char)* input, int c);
|
||||
```
|
||||
|
||||
(Конечно же, в коде на D было бы предпочтительнее использовать массивы, а не указатели.) Компилятор понимает, что ключевое слово `inout` может быть заменено квалификатором `immutable`, `const` или ничем (последняя альтернатива имеет место в случае изменяемого входного значения). Он проверяет тело `strchr`, чтобы удостовериться в том, что код этой функции безопасно работает со всеми возможными типами входного значения.
|
||||
|
||||
Квалификатор может быть перенесен с метода на его результат, например:
|
||||
|
||||
```d
|
||||
class X { ... }
|
||||
|
||||
class Y
|
||||
{
|
||||
private Y _another;
|
||||
inout(Y) another() inout
|
||||
{
|
||||
enforce(_another !is null);
|
||||
return _another;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Метод `another` принимает объекты с любым квалификатором. Этот метод можно переопределить, что очень примечательно, поскольку `inout` можно воспринимать как обобщенный параметр, а обобщенные методы обычно переопределять нельзя. Компилятор способен сделать метод с `inout` переопределяемым, поскольку может проверить, работает ли код в теле этого метода со всеми квалификаторами.
|
||||
|
||||
[В начало ⮍](#8-8-распространение-квалификатора-с-параметра-на-результат) [Наверх ⮍](#8-квалификаторы-типа)
|
||||
|
||||
## 8.9. Итоги
|
||||
|
||||
Квалификаторы типа выражают важные свойства типов, которые другие механизмы абстрагирования не позволяют выразить. Основное внимание в главе было уделено квалификатору типа `immutable`, предоставляющему очень надежные гарантии: неизменяемое значение за все время его жизни никогда не сможет быть изменено, транзитивно. Это очень полезное свойство. Оно позволяет обеспечить чисто функциональную семантику и помогает организовать безопасное разделение данных между потоками.
|
||||
|
||||
Сила квалификатора `immutable` также является его слабостью: он не допускает использование нескольких шаблонов обработки данных, когда за запись информации отвечают одни, а за ее чтение – другие. Эту проблему решает квалификатор `const`, выражающий контекстную неизменяемость: собственник значения `const` не может изменять данные, но другие части программы могут обладать этим правом.
|
||||
|
||||
Наконец, чтобы избежать повторения идентичного кода для функций, принимающих параметры с квалификатором и без квалификатора, был введен подстановочный квалификатор `inout`. Вместо ключевого слова `inout` подставляется `immutable`, `const` или пустое место при отсутствии квалификатора.
|
||||
|
||||
[В начало ⮍](#8-9-итоги) [Наверх ⮍](#8-квалификаторы-типа)
|
||||
|
||||
[^1]: Такой подход был избран для квалификатора `const` в C++.
|
||||
[^2]: Это решение было предложено Саймоном Пейтоном-Джонсом.
|
||||
[^3]: Кроме того, у любого массива `T[]`, `const(T)[]` и `immutable(T)[]` есть свойство `dup`, возвращающее копию массива типа `T[]`, и свойство `idup`, возвращающее копию типа `immutable(T)[]`. – *Прим. науч. ред.*
|
||||
[^4]: Приведенную ниже функцию можно было бы объявить как `void print(in int[] data);`, что означает в точности то же самое, но несколько лучше смотрится. – *Прим. науч. ред.*
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
Loading…
Reference in New Issue