D для новичков, часть 1

Доброго времени суток, хабр!

Уже достаточно взрослый язык, а в сети очень мало материала на русском. Нужно восполнять пробел. В этой заметке хочу рассказать о достаточно скучной, но очень важной теме модификаторов, аттрибутов и тому подобных. Их обилие в D может отпугнуть людей, которые только начинают знакомиться с языком. Да и не все, кто пользуется языком имеет полное представление. Но не всё так страшно, не сложнее чем у других)

Объявление и инициализация переменных


Начнём с простого:

int z; // z == int.init == 0
int a = 5; // явно указывается тип
auto b = 5; // int
auto bl = 5_000_000_000; // long, так как в int не влезет
auto bu = 5U; // uint, u или U указывают, что тип unsigned
auto bl2 = 5L; // long, разрешена только заглавная L, l (строчная L) может быть спутанна 1 
auto bul = 5UL; // ulong, можно комбинировать U и L в любом порядке
const c = 5; // const(int)
immutable d = 5; // immutable(int)
shared e = 5; // shared(int)
shared const f = 5; // shared(const(int))
shared immutable g = 5; // immutable(int)
auto k; // ошибка: у каждой переменной должен быть конкретный тип, при такой записи он не может быть вычислен
import std.variant;
Variant k2; // для тех случаев, когда Вы не знаете, что будет лежать в переменной


Явно тип указывается только для z, a, k2, во всех остальных он выводится из литерала, т.к. по нему всегда можно легко вычислить тип переменной. Про основные типы данных можно почитать здесь. Помимо литерала тип переменной вычисляется автоматически если в неё записывается результат работы функции.
По умолчанию в D все переменные локальные для потока (TLS), чтобы использовать переменную в другом потоке она должна быть shared или immutable. Здесь стоит объяснить чем immutable отличается от const. Когда мы создаём переменную, то особой разницы нет, и ту и другую мы не можем менять после инициализации. Разница существенная появляется когда мы передаём их в функции и методы, по сему вернёмся к этому вопросу при рассмотрении аргументов функций.

Типы массивов

int[] a; // динамический массив
int[3] b; // статический массив
int[int] c; // ассоциативный массив (в квадратных скобках тип ключа)
int[][] d; // массив массивов
int[int[]] e; // ассоциативный массив с ключами из массивов


Последний вариант хоть и возможен, но не удобен, так как для задания значения в качестве ключа нужно использовать массив неизменяемых значений (immutable):

e[cast(immutable(int)[])[8,3]] = 42;


И вот мы плавно коснулись темы модификаторов типов массивов

immutable(int)[] a = [3,4]; // массив неизменяемых int'ов
a = [ 1, 2, 3, 4 ]; // мы можем менять саму по себе переменную a, так как это по сути только указатель (толстый, с длиной)
a.length = 8; // и так можем
a ~= a; // работаем только с переменной a
a[0] = 3; // ошибка: сами значения в массиве нельзя изменить, они immutable(int)

immutable(int[]) b = [8,3]; // неизменяемый массив неизменяемых int, к нему можно только обращаться
immutable int[] c = [1,2,3]; // immutable(int[]), как предыдущий пример


Не нашёл способа создать неизменяемый массив изменяемых данных.

Модификаторы можно комбинировать:

const(shared(int)[]) a = [1]; // константный массив константных разделяемых значений
shared(const(int)[]) b = [2]; // раделяемый массив разделяемых константных значений
const(shared int[]) c = [3]; // константный разделяемый массив
shared(const int[]) d = [4]; // раделяемый константный массив


Сначала может показаться, что между ними особой разницы нет

И даже можно это проверить по быстрому
void main()
{
    void fnc_a( const(shared(int)[]) a ) {}
    void fnc_b( shared(const(int)[]) a ) {}
    void fnc_c( const(shared int[]) a ) {}
    void fnc_d( shared(const int[]) a ) {}

    const(shared(int)[]) a = [1];
    shared(const(int)[]) b = [2];
    const(shared int[]) c = [3];
    shared(const int[]) d = [4];

    fnc_a( a );
    fnc_a( b );
    fnc_a( c );
    fnc_a( d );

    fnc_b( a );
    fnc_b( b );
    fnc_b( c );
    fnc_b( d );

    fnc_c( a );
    fnc_c( b );
    fnc_c( c );
    fnc_c( d );

    fnc_d( a );
    fnc_d( b );
    fnc_d( c );
    fnc_d( d );
}



Между последними двумя точно нет (это один тип). Но остальные различаются. Это будет показанно в разделе про аргументы функций (спойлер «передача массивов по ссылке»).

Стоит заметить, что string, wstring, dstring это просто alias’ы для immutable массивов соответствующих символов.

Функции и аргументы


Начнём с аргументов const и immutable:

import std.stdio;
class A { int val; }
void func1( const A a ) { writeln( a.val ); }
void func2( immutable A a ) { writeln( a.val ); }
void main()
{
    auto a = new A; // обычная переменная, не const и не immutable
    func1( a ); // всё в порядке
    func2( a ); // здесь компилятор скажет, что нельзя вызывать функцию func2 с аргументом типа A, нужен именно immutable
}


И так мы видим, что immutable в этом случае не тоже самое что const. Когда мы объявляем const аргумент, то мы даём гарантию, что внутри функции этот аргумент меняться не будет. В случае immutable мы даём гарантию, что аргумент никогда после инициализации меняться не будет. Последнее утверждение позволяет использовать immutable переменные как shared в других потоках, так как они всё равно неизменяемые (никогда и ни при каких условиях).
Тут есть скользкий момент: если мы заменим class на struct (и соответственно инициализируем переменную не как new A, а как A.init), то код заработает. Это объясняется тем, что структуры, численные типы, статические массивы передаются по значению, а классы, динамические и ассоциативные массивы передаются по ссылке. А при передаче по значению создаётся копия, которая неявно может быть приведена к нужному типу.

Типы что передаются по значению можно передавать и по ссылке:

import std.stdio;
struct A { int val; }
void func0( ref A a ) { writeln( a.val ); }
void func1( ref const A a ) { writeln( a.val ); }
void func2( ref immutable A a ) { writeln( a.val ); }
void main()
{
    auto a = A.init;
    func0( a );
    func1( a ); // всё в порядке, мы просто обещаем не менять переменную внутри функции
    func2( a ); // тут ситуация как с классом
    immutable A b;
    func2( b );
    func1( b ); // всё в порядке, immutable является подтипом const
    func0( b ); // тут компилятор сообщит, что так нельзя
}


Передача массивов по ссылке
void main()
{
    void fnc_a( ref const(shared(int)[]) a ) {}
    void fnc_b( ref shared(const(int)[]) a ) {}
    void fnc_c( ref const(shared int[]) a ) {}

    const(shared(int)[]) a = [1];
    shared(const(int)[]) b = [2];
    const(shared int[]) c = [3];

    fnc_a( a );
    //fnc_a( b );
    //fnc_a( c );

    //fnc_b( a );
    fnc_b( b );
    //fnc_b( c );

    //fnc_c( a );
    fnc_c( b );
    fnc_c( c );
}


Массив является толстым указателем (в D массивы хранят размер массива и укзатель), а этот указатель при передаче в функцию копируется и при копировании может приводиться к нужному типу, как с обычными числами. А вот ссылки уже не могут неявно приводиться. Исключением в примере является вызов функции, принимающей ref const (shared int[]) с аргументом shared (const (int)[]), но тут всё логично: тип элементов внутри shared (const (int)), а сам массив shared, а принимается shared const. По сути исключением является то, что простой аргумент может быть передан в функцию, ожидающую константную ссылку. Но вот с immutable это уже не прокатит. Зато в связке с shared возможны другие комбинации:
void main()
{
    void fnc_a( ref immutable(shared(int)[]) a ) {}
    void fnc_b( ref shared(immutable(int)[]) a ) {}
    void fnc_c( ref immutable(shared int[]) a ) {}

    immutable(shared(int)[]) a = [1];
    shared(immutable(int)[]) b = [2];
    immutable(shared int[]) c = [3];

    fnc_a( a );
    //fnc_a( b );
    fnc_a( c );

    //fnc_b( a );
    fnc_b( b );
    //fnc_b( c );

    fnc_c( a );
    //fnc_c( b );
    fnc_c( c );
}


Так как в этом случае тип переменной a и с совпадёт: immutable (int[]). Модификатор immutable «съедает» все комбинации что внутри.


Если Вы хотите написать функцию, которая работает с разными ссылками то подойдёт const, но если вы хотите в зависимости от аргумента возвращать соответствующий тип, не используя при этом метапрограммирование, Вам подойдёт inout:

import std.stdio;
inout(int)[] func( inout(int)[] a ) { return a[2..4]; }
void main()
{
    auto a = [ 1,2,3,4,5 ];
    auto af = func(a);
    static assert( is( typeof(af) == int[] ) );

    const(int)[] b = [ 1,2,3,4,5 ];
    auto bf = func(b);
    static assert( is( typeof(bf) == const(int)[] ) );

    immutable(int)[] c = [ 1,2,3,4,5 ];
    auto cf = func(c);
    static assert( is( typeof(cf) == immutable(int)[] ) );
}


Для случаев, когда аргумент, передаваемый по ссылке, работает на выход (для записи в него результата, начальное значение нас не волнует) есть специальное ключевое слово out:

struct A { int val; }
void func( out A a ) { } // ничего не делаем
void main()
{
    auto a = A(5);
    assert( a.val == 5 );
    func( a );
    assert( a.val == 0 ); // значение поменялось
}


Во время вызова func переменной a присваивается значение A.init (инициализирующее значение для типа данных).

Вы можете захотеть передать аргумент по ссылке, с гарантией, что он не будет изменён. Сначала может показаться, что для этого существует ключевое слово in, но это не так, in является сокращением для const scope, поэтому следует многословно указывать что вы хотите:

void func( ref const int v ) {}


Это полезно при передаче больших структур, в целях избежания накладных расходов на копирование. Но подобная запись, не будет работать с rvalue значениями, тоесть в данном случае нельзя будет вызвать так func (5), так как литерал не имеет адреса (это касается и структур, создаваемых в момент выхова функции). К сожалению это можно обойти только одним способом — используя шаблоны:

void func(T)( auto ref const T v ) if( is(T==int) ){}


Конструкция auto ref позволит инстанцировать функцию как для принятия ссылки, а если это не возможно, то для принятия копии аргумента. Конструкция ограничения сигнатуры if (is (T==int)) позволяет инстанцировать функцию только при выполнении условия внутри (в нашем случае это условие идентичности типа T с int), всегда является compile-time. По сути для ссылок и для копирования инстанцируются 2 разные функции.

Не хорошо
Конструкция auto ref для возвращаемого типа работает и с обычными функциями, как это будет показанно дальше. Разработчиками проблема обсуждается, и даже есть решение. Вот тут не так всё просто и однозначно, как может показаться.


В D, как и во многих языках есть ленивое вычисление агрументов (вычисление аргумента только в момент, когда он используется) функции:

import std.stdio;
void foo( bool x, lazy string str )
{
    writeln( "foo call" );
    if( x ) writeln( str );
}
string bar() { writeln( "build string" ); return "hello habr"; }
void main()
{
    writeln( "x = false" );
    foo( false, bar() );
    writeln( "x = true" );
    foo( true, bar() );
}


выведет

x = false
foo call
x = true
foo call
build string
hello habr


Полный список классов хранения (storage class) аргументов:

  • нет — аргумент как изменяемая копия
  • in — тоже что и const scope
  • out — передача по ссылке с инициализацией значением по умолчанию
  • ref — просто передача по ссылке
  • scope — ссылки внутри такого параметна не могут быть «выпущенны наружу» (escaped), например присвоенны глобальной переменной*
  • lazy — аргумент вычисляется только в момент, когда используется в теле функции
  • const — аргумент неявно приводится к const типу
  • immutable — аргумент неявно приводится к immutable типу
  • shared — аргумент неявно приводится к shared типу
  • inout — агрумент неявно приводится к inout типу


по поводу scope
На самом деле я не понял что он делает. В документации написанно:

scope — references in the parameter cannot be escaped (e.g. assigned to a global variable)


Что опровергается работоспособностью вот такого кода:
int* glob1;
int* glob2;
struct A { int val; int* ptr; }
void func( scope A a )
{
    glob1 = &(a.val);
    glob2 = a.ptr;
}
void main()
{
    auto val = 10;
    auto a = A(5,&val);
    func( a );
    assert( &val != &(a.val) ); // a копируется
    assert( &val == glob2 );
}


Может я что-то не правильно понял? Может они выпилили реализацию этого поведения потому что хотят сделать ключевое слово scope deprecated. Может быть это просто баг.

Естестенно ключевое слово auto может быть примененно для вычисления возвращаемого значения:

auto func( int a ) { return a * 2; }


При нескольких return вычисляется объемлющий тип:

auto func( int a, double b ) // возвращаемый тип double
{
    if( a > b ) return a;
    else if( b > a ) return b;
    else return 0UL;
}


Для случая, когда Вы хотите вернуть при возможности ссылку можно использовать auto ref возвращаемый тип.

class A
{
    auto ref int foo( ref int x ) { return 3; }
}
class B : A
{
    override auto ref int foo( ref int x ) { return x; }
}
class C : A
{
    int k;
    override auto ref int foo( ref int x ) { return k; }
}
void main()
{
    auto a = new A;
    auto b = new B;
    auto c = new C;

    int val = 10;
    //a.foo( val ) = 12; // ошибка: функция возвращает не rvalue значение
    b.foo( val ) = 14; // всё в порядке: возвращаемое значение это ссылка на val
    assert( val == 14 );
    c.foo( val ) = 16; // всё в порядке: возвращаемое значение это ссылка на поле объекта c
    assert( val == 14 );
    assert( c.k == 16 );
}


Пример с ООП приведён, потому, что я не совсем понимаю зачем использовать auto ref вне этого контекста, если у Вас есть хороший, простой пример, илюстрирующий необходимость auto ref для обычных функций, то буду рад добавить его.

Во второй части поговорим о @​safe, pure, nothrow и некоторых других аспектах.
Здесь мог забыть что-то важное (неявное для новичков в языке), так что коментаторам велком, добавлю.

© Habrahabr.ru