Русский перевод моей статьи «Const-correctness in D». Оригинал опубликован на Medium.

Ключевое слово const знакомо каждому программисту — оно присутствует во многих современных C-подобных языках и используется в самых разных контекстах. В общем смысле оно означает, что переменная неизменяема и может быть инициализирована только один раз. Чаще всего такой упрощенный смысл const встречается в динамических языках, однако в мире статической типизации, в том числе и в D, существует несколько видов неизменяемости, и понимание этих различий крайне важно для написания надежного ПО.

Для чего это нужно?

Когда вы запускаете программу, процессор выполняет вычисления. Если эти вычисления хоть немного сложнее простой математической формулы, у программы появляется состояние — промежуточные данные в памяти, сохраняемые для дальнейшего использования. Основная сложность любой программы связана с управлением этим состоянием и безопасной работой с памятью, ведь «сырая» память — это просто набор адресуемых байт без какой-либо внутренней структуры. Чтобы уменьшить сложность и избежать некорректного поведения, мы определяем модель данных и накладываем на нее правила — например, систему типов в языке программирования. Она описывает типы данных и их свойства. При наличии статической типизации компилятор производит семантический анализ и не пропускает программу, нарушающую правила системы типов.

Const-корректность — это дополнительное ограничение для улучшения семантического анализа. Простая программа в языке со статической типизацией может обойтись и без const, но в сложной системе, где состояние разделяется между несколькими асинхронными пользователями или даже потоками, важно минимизировать побочные эффекты — то есть, исключить возможность случайного изменения общих данных, когда логика программы этого не позволяет. const — один из способов это сделать.

const в языке D

В C-подобных языках const — это дополнительное правило системы типов, означающее, что переменная данного типа не может быть изменена (то есть, const относится не к самой переменной, а ее типу). В D тоже есть модификатор const, но очень важно понимать, чем он отличается от const в C++. В C++ const нетранзитивен: неизменяемый объект может иметь изменяемые члены. В D const транзитивен. Как только он применяется к типу, он применяется и ко всем его членам:

class Foo
{
    int x = 10;
}

const(Foo) foo = new Foo();
foo.x = 5; // ошибка!

Кроме того, в D есть синтаксическое различие между const-указателями и изменяемыми указателями на const-объект:

  • const int* — неизменяемый указатель на const int
  • const(int*) — неизменяемый указатель на const int
  • const(int)* — изменяемый указатель на const int

const vs immutable

В D также есть модификатор типов immutable, что часто вызывает путаницу. immutable означает, что объект такого типа не может быть изменен. const означает, что объект не может быть изменен через идентификатор такого типа, однако он может быть изменен через другой, изменяемый идентификатор. Поэтому const можно воспринимать как интерфейс, обеспечивающий доступ к объекту только для чтения, тогда как сам объект может быть изменяемым или неизменяемым в зависимости от логики программы.

Когда использовать const, а когда immutable? Рассмотрим пример:

class Foo
{
    int x = 10;
}

int bar(Foo foo1, Foo foo2)
{
    return foo1.x + foo2.x;
}

void main()
{
    immutable(Foo) foo1 = new Foo();
    Foo foo2 = new Foo();
    bar(foo1, foo2); // ошибка!
}

Программа не скомпилируется, потому что foo1immutable. Решение — использовать const:

int bar(const(Foo) foo1, const(Foo) foo2)
{
    return foo1.x + foo2.x;
}

Теперь функция bar принимает любые параметры — и изменяемые, и не изменяемые.

Интересный вопрос: можно ли снять const через cast? Формально — да, но безопасно — нет. Поскольку D — системный язык, он не запрещает вам нарушать правила — компилятор допускает приведение immutable или const к изменяемым типам. Это сделано исключительно для совместимости с C-библиотеками. Это не означает, что такие данные можно безопасно изменять — поведение становится неопределенным. Как обычно, такие небезопасные приемы нужно использовать крайне осторожно. Никогда не делайте так:

const int x = 10;
int* px = cast(int*)&x;
*px = 5; // неопределенное поведение!

const vs inout

Еще один модификатор типов в D — inout. Он нечасто используется, и его смысл может быть непонятным для начинающих. Представьте, что ваша функция должна вернуть значение с такой же степенью изменяемости, как и у параметра:

auto halfArray(const(int)[] a)
{
    return a[$ / 2 .. $];
}

immutable(int)[] arr1 = [4, 5, 7, 8];
immutable(int)[] arr2 = halfArray(arr1); // ошибка!

Функция вернет const(int)[], а не immutable(int)[], как ожидалось. Решение — заменить const на inout:

auto halfArray(inout(int)[] a)
{
    return a[$ / 2 .. $];
}

Такая функция будет работать с const, immutable и обычными изменяемыми типами:

int[] arr = [4, 5, 7, 8];
int[] result = halfArray(arr);

Const-методы

Когда const применяется к методу, это означает, что неявный this для доступа к полям объекта тоже станет const, и, как следует из транзитивности, свойства объекта менять будет нельзя:

class Foo
{
    int prop() const
    {
        return _prop;
    }

    protected int _prop = 0;
}

Попытка изменить поле внутри такого метода приведет к ошибке:

class Foo
{
    int prop() const
    {
        _prop = 8; // ошибка!
        return _prop;
    }

    protected int _prop = 0;
}

Модификатор const может применяться только к методам, но не свободным функциям. Однако функции все равно могут возвращать const-значения. Важно понимать синтаксическое различие:

  • const int foo() или int foo() constconst-метод, возвращающий int
  • const(int) foo() — метод или функция, возвращающая const(int)

Может существовать const-метод, возвращающий const-тип:

import std.string;

class Foo
{
    const(char*) prop() const
    {
        return _prop.toStringz();
    }

    protected string _prop = "что-то";
}

ref const

В некоторых случаях можно объявить переменные с модификатором ref const, чтобы запретить изменять объект по ссылке:

void foo(ref const(int) v)
{
    v = 0; // ошибка
}

Это полезно, например, в foreach, чтобы элементы агрегатного объекта не могли быть случайно изменены:

int[] arr = [1, 2, 3, 4, 5];

foreach (ref const v; arr)
{
    foo(v);
    v = 0; // тоже ошибка
}

Оставить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *