# 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` довольно полезен внутри модуля, поскольку гарантирует инициаторам вызовов регламентированное поведение функций. Например, сигнатура

```d
// Функция из стандартной библиотеки C
int printf(const char * format, ...);
```

обещает пользователям, что функция `printf` не будет пытаться изменить знаки, переданные в параметре `format`. Подобная гарантия также полезна при масштабной разработке, поскольку сокращает количество зависимостей, созданных немодульными изменениями. Определить такие ограничения и гарантировать подчинение им можно и посредством соглашения, но подобные соглашения неудобны, и соблюдать их трудно. D определяет три типа квалификаторов:

- `const` означает неизменяемость в рамках заданного контекста. Значение типа, заданного с ключевым словом `const`, нельзя изменить напрямую. Однако другие сущности в программе могут обладать правом перезаписывать эти данные: так у инициатора вызова функции `printf` может быть право записи в переменную `format`, а у самой функции – нет.
- `immutable` означает абсолютную, контекстно-независимую неизменяемость. Значение типа, заданного с ключевым словом `immutable`, после инициализации нельзя изменить ни при каких обстоятельствах нигде в программе. Это гораздо более строгое ограничение, чем у квалификатора `const`.
- `shared` означает разделение значения между потоками.

Все они дополняют друг друга. Квалификаторы `const` и `immutable` важны для масштабной разработки. Кроме того, без квалификатора `immutable` невозможно было бы программировать в функциональном стиле, а квалификатор `const` способствует интеграции кода в функциональном стиле с кодом в объектно-ориентированном и процедурном стиле. Квалификаторы `immutable` и `shared` позволяют реализовать многопоточность. Подробное описание квалификатора `shared` и разговор о многопоточности мы отложим до главы 13. А здесь сосредоточимся на квалификаторах `const` и `immutable`.

[В начало ⮍](#8-квалификаторы-типа)

## 8.1. Квалификатор immutable

Значение типа с квалификатором `immutable` высечено на камне: сразу же после инициализации такого значения можно считать, что оно навечно прожжено в хранящей его памяти. Оно никогда не изменится за все время исполнения программы.

Форма записи типа с квалификатором такова: `‹квалификатор›(T)`, где `‹квалификатор›` – одно из ключевых слов `immutable`, `const` и `shared`. Например, определим неизменяемое целое число:

```d
immutable(int) forever = 42;
```

Попытки каким-либо способом изменить значение переменной `forever` приведут к ошибке во время компиляции. Более того, `immutable(int)` – это полноправный тип, как любой другой тип (он отличается от типа `int`). Например, можно присвоить ему псевдоним:

```d
alias immutable(int) StableInt;
StableInt forever = 42;
```

Определяя копию переменной `forever` с ключевым словом `auto`, вы распространите тип `immutable(int)` и на копию, так что и сама копия будет неизменяемым целым числом. Ничего особенного здесь нет, но именно этим отличаются квалификаторы типов и простые классы памяти, такие как `static` (см. раздел 5.2.5) или `ref` (см. раздел 5.2.1).

```d
unittest
{
    immutable(int) forever = 42;
    auto andEver = forever;
    ++andEver; // Ошибка! Нельзя изменять неизменяемое значение!
}
```

Значение типа с квалификатором `immutable` необязательно инициализировать константой, известной во время компиляции:

```d
void fun(int x)
{
    immutable(int) xEntry = x;
    ...
}
```

Примененный таким образом квалификатор `immutable` оказывает услугу тем, кто будет разбираться в работе функции `fun`. С первого взгляда понятно, что переменная `xEntry` будет хранить переданное на входе в функцию значение `x` от начала и до конца тела этой функции.

В определениях с квалификатором `immutable` необязательно указывать тип – он будет определен так же, как если бы вместо `immutable` стояло ключевое слово `auto`:

```d
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);`, что означает в точности то же самое, но несколько лучше смотрится. – *Прим. науч. ред.*
