50 KiB
8. Квалификаторы типа
[🢀 7. Другие пользовательские типы] [8. Квалификаторы типа] [9. Обработка ошибок 🢂]
- 8.1. Квалификатор immutable
- 8.2. Составление типов с помощью immutable
- 8.3. Неизменяемые параметры и методы
- 8.4. Неизменяемые конструкторы
- 8.5. Преобразования с участием immutable
- 8.6. Квалификатор const
- 8.7. Взаимодействие между const и immutable
- 8.8. Распространение квалификатора с параметра на результат
- 8.9. Итоги
Квалификаторы типа выражают важные утверждения о типах языка. Эти утверждения исключительно полезны как для программиста, так и для компилятора, но их сложно выразить путем соглашений, обычного порождения подтипов (см. раздел 6.4.2) или параметризации типами (см. раздел 6.14).
Показательный пример квалификатора типа – квалификатор типа const
(введенный в языке C и доработанный в C++). Примененный к типу T
, этот квалификатор выражает следующее утверждение: значения типа T
можно инициализировать и читать, но не перезаписывать. Соблюдение этого ограничения гарантируется компилятором. Квалификатор const
довольно полезен внутри модуля, поскольку гарантирует инициаторам вызовов регламентированное поведение функций. Например, сигнатура
// Функция из стандартной библиотеки 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.1. Квалификатор immutable
Значение типа с квалификатором immutable
высечено на камне: сразу же после инициализации такого значения можно считать, что оно навечно прожжено в хранящей его памяти. Оно никогда не изменится за все время исполнения программы.
Форма записи типа с квалификатором такова: ‹квалификатор›(T)
, где ‹квалификатор›
– одно из ключевых слов immutable
, const
и shared
. Например, определим неизменяемое целое число:
immutable(int) forever = 42;
Попытки каким-либо способом изменить значение переменной forever
приведут к ошибке во время компиляции. Более того, immutable(int)
– это полноправный тип, как любой другой тип (он отличается от типа int
). Например, можно присвоить ему псевдоним:
alias immutable(int) StableInt;
StableInt forever = 42;
Определяя копию переменной forever
с ключевым словом auto
, вы распространите тип immutable(int)
и на копию, так что и сама копия будет неизменяемым целым числом. Ничего особенного здесь нет, но именно этим отличаются квалификаторы типов и простые классы памяти, такие как static
(см. раздел 5.2.5) или ref
(см. раздел 5.2.1).
unittest
{
immutable(int) forever = 42;
auto andEver = forever;
++andEver; // Ошибка! Нельзя изменять неизменяемое значение!
}
Значение типа с квалификатором immutable
необязательно инициализировать константой, известной во время компиляции:
void fun(int x)
{
immutable(int) xEntry = x;
...
}
Примененный таким образом квалификатор immutable
оказывает услугу тем, кто будет разбираться в работе функции fun
. С первого взгляда понятно, что переменная xEntry
будет хранить переданное на входе в функцию значение x
от начала и до конца тела этой функции.
В определениях с квалификатором immutable
необязательно указывать тип – он будет определен так же, как если бы вместо immutable
стояло ключевое слово auto
:
immutable pi = 3.14, val = 42;
Для pi
компилятор выводит тип immutable(double)
, а для val
– immutable(int)
.
8.1.1. Транзитивность
Любой тип можно определить с квалификатором immutable
. Например:
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 на объект в целом.
unittest
{
auto anotherOrigin = immutable(Point)(1, 1);
origin = anotherOrigin; // Ошибка!
origin.x = 1; // Ошибка!
origin.y = 1; // Ошибка!
}
На самом деле, immutable
распространяется абсолютно на каждое поле Point
, тип каждого поля объекта квалифицируется тем же квалификатором, что и сам объект. Например, такой тест будет пройден:
static assert(is(typeof(origin.x) == immutable(int))); // Тест пройден
Но мир не настолько прост. Рассмотрим структуру, в которой есть некоторая косвенность, например поле массива:
struct DataSample
{
int id;
double[] payload;
}
Очевидно, что поля объекта типа immutable(DataSample)
не могут быть изменены. Но как насчет изменения элемента массива payload
?
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.2. Составление типов с помощью immutable
Учитывая, что для точного выбора типа, который нужно квалифицировать, с квалификаторами используют скобки и что тип с квалификатором – это полноправный новый тип, можно сделать вывод, что, комбинируя ключевое слово immutable
с другими конструкторами типов, можно создавать весьма сложные структуры данных. Сравним, например, следующие два типа:
alias immutable(int[]) T1;
alias immutable(int)[] T2;
В первом определении круглые скобки поглотили полностью весь тип массива; во втором случае затронут лишь тип элементов массива int
, но не сам массив. Если при употреблении квалификатора immutable
круглые скобки отсутствуют, квалификатор применяется ко всему типу, так что эквивалентное определение T1
выглядит так:
alias immutable int[] T1;
Тип T1
незамысловат: он представляет собой неизменяемый массив значений типа int
. Само написание этого типа говорит то же самое. В соответствии со свойством транзитивности нельзя изменить ни массив в целом (например, присвоив переменной, содержащей массив, новый массив), ни какой-либо его элемент в отдельности:
T1 a = [ 1, 3, 5 ];
T1 b = [ 2, 4 ];
a = b; // Ошибка!
a[0] = b[1]; // Ошибка!
Второе определение кажется более тонким, но на самом деле понять его довольно просто, если вспомнить, что immutable(int)
– самостоятельный тип. Тогда immutable(int)[]
– это просто массив элементов этого самостоятельного типа, вот и все. Вывод о свойствах этого массива напрашивается сам. Можно присвоить значение массиву в целом, но нельзя изменить (в том числе через присваивание) отдельные его элементы:
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.3. Неизменяемые параметры и методы
В сигнатуре функции квалификатор immutable
очень информативен. Рассмотрим одну из простейших функций:
string process(string input);
На самом деле это лишь краткая запись сигнатуры
immutable(char)[] process(immutable(char)[] input);
Функция process
гарантирует, что не будет изменять отдельные знаки параметра input
, так что инициатор вызова функции process
может быть уверен, что после вызова строка окажется такой же, как и перед ним:
string s1 = "чепуха";
string s2 = process(s1);
assert(s1 == "чепуха"); // Выполняется всегда
Более того, пользователь функции process
может рассчитывать на то, что ее результат не сможет быть изменен: нет никаких скрытых псевдонимов, никакая другая функция не сможет позже изменить s2
. В этом случае immutable
также означает неизменяемость.
Структуры и классы могут определять неизменяемые методы. В подобных случаях квалификатор применяется к this
:
class A
{
int[] fun(); // Обычный метод
int[] gun() immutable; // Можно вызвать, только если объект неизменяемый
immutable int[] hun(); // То же, что выше
}
Третья форма записи выглядит подозрительно: может показаться, что квалификатор immutable
относится к int[]
, но на самом деле он относится к this
. Если нужно определить неизменяемый метод, возвращающий immutable int[]
, получается что-то вроде заикания:
immutable immutable(int[]) iun();
Поэтому в таких случаях ключевое слово immutable
лучше писать в конце:
immutable(int[]) iun() immutable;
Разрешение оставить несколько сбивающий с толку immutable
в начале определения продиктовано в основном стремлением унифицировать формат указания для всех свойств методов (таких как final
или static
). Например, определить сразу несколько неизменяемых методов, можно так:
class A
{
immutable
{
int foo();
int[] bar();
void baz();
}
}
Кроме того, квалификатор immutable
можно использовать в виде метки:
class A
{
immutable:
int foo();
int[] bar();
void baz();
}
Разумеется, неизменяемые методы могут быть вызваны только применительно к неизменяемым объектам:
class C
{
void fun() {}
void gun() immutable {}
}
unittest
{
auto c1 = new C;
auto c2 = new immutable(C);
c1.fun(); // Все в порядке
c2.gun(); // Все в порядке
// Никакие другие вызовы не сработают
}
8.4. Неизменяемые конструкторы
Работать с неизменяемым объектом совсем несложно, а вот его построение – весьма деликатный процесс. Причина в том, что в процессе построения нужно удовлетворить два противоречивых требования: 1) присвоить полям значения, 2) сделать их неизменяемыми. Поэтому D особенно внимателен к неизменяемым конструкторам.
Проверка типов в неизменяемом конструкторе выполняется простым и осторожным способом. Компилятор разрешает присваивание полей только внутри конструктора, а чтение полей (включая передачу this в качестве аргумента при вызове метода) запрещено. Как только выполнение неизменяемого конструктора завершается, объект «замораживается» – после этого нельзя потребовать ни одного изменения. Вызов нестатического метода считается за чтение, поскольку такой метод обладает доступом к объекту this и способен прочесть любое его поле. (Компилятор не проверяет, читает ли метод поля на самом деле, – для перестраховки он предполагает, что метод все же читает некоторое поле.)
Это правило строже, чем необходимо; ведь в действительности запрещается лишь присваивание значения полю после того, как это поле было прочитано. Однако это более строгое правило практически не мешает выразительности, при этом оно простое и понятное. Например:
class A
{
int a;
int[] b;
this() immutable
{
a = 5;
b = [ 1, 2, 3 ];
// Вызов fun() не был бы разрешен
}
void fun() immutable
{
...
}
}
Вызывать из неизменяемого конструктора конструктор родителя super
в порядке вещей, если этот вызов адресован также неизменяемому конструктору. Такие вызовы не угрожают нарушить неизменяемость.
Инициализировать неизменяемые объекты обычно помогает рекурсия. К примеру, рассмотрим реализующий абстракцию односвязного списка класс, инициализируемый с помощью массива:
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.5. Преобразования с участием immutable
Рассмотрим пример кода:
unittest
{
int a = 42;
immutable(int) b = a;
int c = b;
}
Более строгая система типизации не приняла бы этот код. Он включает два преобразования: сначала из int
в immutable(int)
, а затем обратно из immutable(int)
в int
. Собственно, по общим правилам эти преобразования незаконны. Например, если в этом коде заменить int
на int[]
, ни одно из следующих преобразований не будет корректным:
int[] a = [ 42 ];
immutable(int[]) b = a; // Нет!
int[] c = b; // Нет!
Если бы такие преобразования были разрешены, неизменяемость бы не соблюдалась, поскольку тогда неизменяемые массивы разделяли бы свое содержимое с изменяемыми.
Тем не менее компилятор распознает и разрешает некоторые автоматические преобразования между неизменяемыми и изменяемыми данными. А именно разрешено двунаправленное преобразование между T
и immutable(T)
, если у T
«нет изменяемой косвенности». Интуитивно понятно, что «нет изменяемой косвенности» означает запрет перезаписывать косвенно доступные через T
данные. Определение этого понятия рекурсивно:
- у встроенных типов значений, таких как
int
, «нет изменяемой косвенности»; - у массивов фиксированной длины из элементов типов, у которых «нет изменяемой косвенности», в свою очередь, тоже «нет изменяемой косвенности»;
- у массивов и указателей, ссылающихся на типы, у которых «нет изменяемой косвенности», тоже «нет изменяемой косвенности»;
- у структур, ни в одном поле которых «нет изменяемой косвенности», тоже «нет изменяемой косвенности».
Например, у типа S1
нет изменяемой косвенности, а у типа S2
– есть:
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, которая понимает все тонкости преобразований типов с квалификаторами и всегда принимает соответственные меры.
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.6. Квалификатор const
Немного поэкспериментировав с квалификатором типа immutable
, мы видим, что этот квалификатор слишком строг, чтобы быть полезным в большинстве случаев. Да, если вы обязались не изменять определенные данные на протяжении работы целой программы, immutable
вам подходит. Но часто неизменяемость – свойство, полезное в рамках модуля: хотелось бы оставить за собой право изменять некоторые данные, а остальным запретить это делать. Такие данные нельзя назвать неизменяемыми (то есть пометить квалификатором immutable
), поскольку immutable
означает: «Видите письмена, высеченные на камне? Это ваши данные». А вам нужно средство, чтобы выразить такое ограничение: «Вы не можете изменить эти данные, но кто-кто другой – может». Или, как сказал Алан Перлис: «Кому константа, а кому и переменная». Посмотрим, как система типов вполне серьезно реализует изречение Перлиса.
Простой вариант использования таких данных – функция, например print
, которая печатает какие-то данные. Функция print
не изменяет переданные в нее данные, так что она допускает применение квалификатора immutable
:
void print(immutable(int[]) data) { ... }
unittest
{
immutable(int[]) myData = [ 10, 20 ];
print(myData); // Все в порядке
}
Отлично. Далее, пусть у нас есть значение типа int[]
, которое мы только что вычислили и хотим напечатать. С таким аргументом наша функция не сработает, поскольку значение типа int[]
не приводится к типу immutable(int)[]
– а если бы приводилось, то возникла бы неподобающая общность изменяемых и якобы неизменяемых данных. Получается, что функция print
не может напечатать данные типа int[]
. Такое ограничение довольно неоправданно, поскольку print
вообще не затрагивает свои аргументы, так что эта функция должна работать с неизменяемыми данными так же, как с изменяемыми.
Что нам нужно, так это нечто вроде общего «либо изменяемого, либо нет» типа. В этом случае функцию print
можно было бы объявить так:
void print(либо_изменяемый_либо_нет(int[]) data) { ... }
Только потому, что название либо_изменяемый_либо_нет
несколько длинновато, для обозначения этого типа было введено ключевое слово const
. Смысл абсолютно тот же: текущий код не может изменить значение типа const(T)
, но есть вероятность, что это может сделать другой код. Такая двусмысленность отражает тот факт, что в роли const(T)
может выступать как T
, так и immutable(T)
. Это качество квалификатора const
делает его совершенным для организации взаимодействия между функциональным и обычным процедурным кодом. Продолжим начатый выше пример4:
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).
Рис. 8.1. Для всех типов T
: const(T)
– супертип и для T
, и для immutable(T)
. Следовательно, код, работающий со значениями типа const(T)
, принимает значения как изменяемого, так и неизменяемого типа T
Для квалификатора const
верны те же правила транзитивности и преобразований, что и для квалификатора immutable
. На конструкторы объектов const
, в отличие от конструкторов immutable
, ограничения не накладываются: внутри конструктора const
объект считается изменяемым.
Метод, объявленный с квалификатором const
, может вызываться для объектов с любым квалификатором, так как он гарантирует, что ничего менять не будет, но не требует этого от объекта. Например:
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.7. Взаимодействие между const и immutable
Нередко квалификатор пытается подействовать на тип, который уже находится под влиянием другого квалификатора. Например:
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.8. Распространение квалификатора с параметра на результат
C и C++ определяют поверхностный квалификатор const
с неприятной особенностью: функция, возвращающая параметр, должна либо повторять свое определение дважды – для константных и неконстантных данных, либо вести опасную игру. Показательный пример такой функции – функция strchr
из стандартной библиотеки C, которая в ней определена так:
char* strchr(const char* input, int c);
Эта функция очищает типы от квалификаторов: несмотря на то что input
– константное значение, которое, если рассуждать наивно, не должно измениться, возвращение в выводе указателя, порожденного от input
, снимает с данных это обещание. strchr
способствует появлению кода, изменяющего неизменяемые данные без приведения типов. C++ избавился от этой проблемы, введя два определения strchr
:
char* strchr(char* input, int c);
const char* strchr(const char* input, int c);
Эти функции делают одно и то же, но их нельзя соединить в одну, поскольку в C++ нет средств, позволяющих сказать: «Если у аргумента есть квалификатор, пожалуйста, распространите его и на тип возвращаемого значения».
Для решения этой проблемы D предлагает «подстановочный» идентификатор квалификатора: inout
. С участием inout
объявление strchr
выглядело бы так:
inout(char)* strchr(inout(char)* input, int c);
(Конечно же, в коде на D было бы предпочтительнее использовать массивы, а не указатели.) Компилятор понимает, что ключевое слово inout
может быть заменено квалификатором immutable
, const
или ничем (последняя альтернатива имеет место в случае изменяемого входного значения). Он проверяет тело strchr
, чтобы удостовериться в том, что код этой функции безопасно работает со всеми возможными типами входного значения.
Квалификатор может быть перенесен с метода на его результат, например:
class X { ... }
class Y
{
private Y _another;
inout(Y) another() inout
{
enforce(_another !is null);
return _another;
}
}
Метод another
принимает объекты с любым квалификатором. Этот метод можно переопределить, что очень примечательно, поскольку inout
можно воспринимать как обобщенный параметр, а обобщенные методы обычно переопределять нельзя. Компилятор способен сделать метод с inout
переопределяемым, поскольку может проверить, работает ли код в теле этого метода со всеми квалификаторами.
8.9. Итоги
Квалификаторы типа выражают важные свойства типов, которые другие механизмы абстрагирования не позволяют выразить. Основное внимание в главе было уделено квалификатору типа immutable
, предоставляющему очень надежные гарантии: неизменяемое значение за все время его жизни никогда не сможет быть изменено, транзитивно. Это очень полезное свойство. Оно позволяет обеспечить чисто функциональную семантику и помогает организовать безопасное разделение данных между потоками.
Сила квалификатора immutable
также является его слабостью: он не допускает использование нескольких шаблонов обработки данных, когда за запись информации отвечают одни, а за ее чтение – другие. Эту проблему решает квалификатор const
, выражающий контекстную неизменяемость: собственник значения const
не может изменять данные, но другие части программы могут обладать этим правом.
Наконец, чтобы избежать повторения идентичного кода для функций, принимающих параметры с квалификатором и без квалификатора, был введен подстановочный квалификатор inout
. Вместо ключевого слова inout
подставляется immutable
, const
или пустое место при отсутствии квалификатора.
[🢀 7. Другие пользовательские типы] [8. Квалификаторы типа] [9. Обработка ошибок 🢂]
-
Такой подход был избран для квалификатора
const
в C++. ↩︎ -
Это решение было предложено Саймоном Пейтоном-Джонсом. ↩︎
-
Кроме того, у любого массива
T[]
,const(T)[]
иimmutable(T)[]
есть свойствоdup
, возвращающее копию массива типаT[]
, и свойствоidup
, возвращающее копию типаimmutable(T)[]
. – Прим. науч. ред. ↩︎ -
Приведенную ниже функцию можно было бы объявить как
void print(in int[] data);
, что означает в точности то же самое, но несколько лучше смотрится. – Прим. науч. ред. ↩︎