Глава 8 готова

This commit is contained in:
Alexander Zhirov 2023-02-28 14:04:55 +03:00
parent b4d0baee1e
commit 0cad05de5a
2 changed files with 493 additions and 0 deletions

View File

@ -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