Квалификаторы типа выражают важные утверждения о типах языка. Эти утверждения исключительно полезны как для программиста, так и для компилятора, но их сложно выразить путем соглашений, обычного порождения подтипов (см. раздел 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-квалификаторы-типа)
Любой тип можно определить с квалификатором `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` очень информативен. Рассмотрим одну из простейших функций:
Функция `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` можно было бы объявить так:
Только потому, что название `либо_изменяемый_либо_нет` несколько длинновато, для обозначения этого типа было введено ключевое слово `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);`, что означает в точности то же самое, но несколько лучше смотрится. –*Прим. науч. ред.*