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