dlang-book/book/08-квалификаторы-типа
Alexander Zhirov bbe57cd8b5 Переходы 5 2023-03-05 20:13:59 +03:00
..
images Перенос страниц 2023-03-05 15:30:34 +03:00
README.md Переходы 5 2023-03-05 20:13:59 +03:00

README.md

8. Квалификаторы типа

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

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).

image-8-6

Рис. 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. Обработка ошибок 🢂


  1. Такой подход был избран для квалификатора const в C++. ↩︎

  2. Это решение было предложено Саймоном Пейтоном-Джонсом. ↩︎

  3. Кроме того, у любого массива T[], const(T)[] и immutable(T)[] есть свойство dup, возвращающее копию массива типа T[], и свойство idup, возвращающее копию типа immutable(T)[]. Прим. науч. ред. ↩︎

  4. Приведенную ниже функцию можно было бы объявить как void print(in int[] data);, что означает в точности то же самое, но несколько лучше смотрится. Прим. науч. ред. ↩︎