不変性

概念は、プログラムの変数によって表される。概念の相互作用は、通常、その概念を表す変数の値を変更する式によって実現される。例えば、次のコードは、購入を表すいくつかの変数を変更する

totalPrice = calculateAmount(itemPrices);
moneyInWallet -= totalPrice;
moneyAtMerchant += totalPrice;
D

変数の値を変更することを、その変数の変更または変更と呼ぶ。変数の変更を禁止することを、不変性と呼ぶ。

変更はほとんどのタスクに不可欠であるため、意図的に変更を禁止することは直感に反するかもしれないが、これは強力で便利な機能である。不変性の概念は、プログラミングコミュニティが一般的に得た経験に基づいている。不変性は、プログラムの正確性と保守性に役立つ。この考え方は非常に強力であるため、一部の関数型プログラミング言語では変更を完全に禁止している。

不変性の主な利点は以下の通りだ:

不変性はプログラミング全般で非常に一般的であり、Dプログラマーによって広く採用されているため、以下の奇妙な点が結果として受け入れられている:

不変の変数

変更できない変数を定義する方法は3つある。

enum定数

列挙型の章で、enumが名前付き定数値を定義することを説明した。

enum fileName = "list.txt";
D

その値がコンパイル時に決定できる限り、enum変数は、関数の戻り値を含む、より複雑な式でも初期化することができる。

int totalLines() {
    return 42;
}

int totalColumns() {
    return 7;
}

string name() {
    return "list";
}

void main() {
    enum fileName = name() ~ ".txt";
    enum totalSquares = totalLines() * totalColumns();
}
D
const_and_immutable.1

このような初期化を可能にするDの機能は、コンパイル時関数実行(CTFE)であり、これは後の章で説明する。

当然のことながら、enum定数の値は変更できない。

++totalSquares;    // ← コンパイルエラー
D

enumは不変の値を表現する非常に効果的な方法だが、コンパイル時の値にしか使用できない。

enum定数は、その定数がその値に置き換えられたかのようにプログラムがコンパイルされる、マニフェスト定数である。例として、次のenum定義と、それを使用する2つの式を考えてみよう。

enum i = 42;
writeln(i);
foo(i);
D

上記のコードは、iがすべてその値42に置き換えられた、以下のコードとまったく同じだ。

writeln(42);
foo(42);
D

この置換は、intのような単純な型では意味があり、結果のプログラムにも何の影響も与えないが、enum定数を使用すると、配列や連想配列で隠れたコストが発生する可能性がある。

enum a = [ 42, 100 ];
writeln(a);
foo(a);
D

aをその値に置き換えた後、コンパイラがコンパイルする同等のコードは次のようになる。

writeln([ 42, 100 ]); // 実行時に配列が作成される
foo([ 42, 100 ]);     // 実行時に別の配列が作成される
D

ここでの隠れたコストは、上記の2つの式に対して2つの別々の配列が作成されることだ。そのため、プログラム内で2回以上参照される配列や連想配列は、immutable変数として定義したほうが理にかなっている。

const変数

enumと同様に、このキーワードは、変数の値が決して変更されないことを指定する。enumとは異なり、const変数は、メモリアドレスを持つ実際の変数であり、const変数は通常、プログラムの実行中に初期化される。

コンパイラは、const変数の変更を許可しない:

const half = total / 2;
half = 10;    // ← コンパイルエラー
D

次のプログラムは、enumconstの両方を使用している。このプログラムは、ランダムに選んだ数字をユーザーに当てさせる。乱数はコンパイル時には決定できないため、enumとして定義することはできない。しかし、ランダムに選んだ値は決定後に変更してはならないため、この変数をconstとして指定するのが適切だ。

このプログラムでは、前の章で定義したreadInt()関数を利用している。

import std.stdio;
import std.random;

int readInt(string message) {
    int result;
    write(message, "? ");
    readf(" %s", &result);
    return result;
}

void main() {
    enum min = 1;
    enum max = 10;

    const number = uniform(min, max + 1);

    writefln("I am thinking of a number between %s and %s.",
             min, max);

    auto isCorrect = false;
    while (!isCorrect) {
        const guess = readInt("What is your guess");
        isCorrect = (guess == number);
    }

    writeln("Correct!");
}
D
const_and_immutable.2

観察事項:

型を完全に記述する必要はないが、constは通常、実際の型を括弧で囲んで指定する。例えば、const(int)など。次のプログラムの出力は、3つの変数の型のフルネームが実際には同じであることを示している。

import std.stdio;

void main() {
    const      inferredType = 0;
    const int  explicitType = 1;
    const(int) fullType     = 2;

    writeln(typeof(inferredType).stringof);
    writeln(typeof(explicitType).stringof);
    writeln(typeof(fullType).stringof);
}
D
const_and_immutable.3

型の実際の名前には、constが含まれている。

const(int)
const(int)
const(int)

括弧の使用には意味があり、型のどの部分が不変であるかを指定する。これは、スライス全体とその要素の不変性について説明する以下で説明する。

immutable変数

変数を定義する際、immutableキーワードはconstと同じ効果を持つ。immutable変数は変更できない:

immutable half = total / 2;
half = 10;    // ← コンパイルエラー
D

プログラムの他の部分でimmutableの変数が必要でない限り、不変の変数はconstまたはimmutableとして定義できる。関数で、パラメータがimmutableでなければならないと特に指定されている場合、そのパラメータに対応する変数はimmutableとして定義しなければならない。これについては、後で説明する。

パラメーター

次の2章で見るように、関数はそのパラメータを変更することができる。例えば、その関数の引数として渡されたスライスの要素を変更することができる。

スライスとその他の配列機能の章で学んだように、スライスは要素を所有せず、要素へのアクセスを提供する。同じ要素へのアクセスを提供するスライスは、同時に複数存在する場合がある。

このセクションの例ではスライスにのみ焦点を当てている、このトピックは連想配列やクラスにも適用できる。

関数の引数として渡されるスライスは、その関数が呼び出されたときのスライスとは別物だ。引数は、スライス変数のコピーだ(要素はコピーされない)。

import std.stdio;

void main() {
    int[] slice = [ 10, 20, 30, 40 ];  // 1
    halve(slice);
    writeln(slice);
}

void halve(int[] numbers) {            // 2
    foreach (ref number; numbers) {
        number /= 2;
    }
}
D
const_and_immutable.4

プログラムの実行がhalve()関数に入ったとき、同じ4つの要素へのアクセスを提供するスライスは2つある。

  1. main()で定義され、halve()のパラメーターとして渡されるsliceという名前のスライス
  2. halve()が引数として受け取るnumbersという名前のスライス。これは、slice

両方のスライスは同じ要素を参照しており、foreachループでキーワードrefを使用しているため、要素の値は半分になる。

[5, 10, 15, 20]

関数が引数として渡されたスライスの要素を変更できることは、確かに便利である。この例で見たように、その目的のためだけに存在する関数もある。

コンパイラは、const型の変数をそのような関数の引数として渡すことを許可しない。

const(int[]) slice = [ 10, 20, 30, 40 ];
halve(slice);    // ← コンパイルエラー
D

コンパイルエラーは、const(int[])型の変数をint[]型の引数として使用できないことを示している。

エラー: 関数deneme.halve (int[] numbers)は、引数の型
(const(int[]))を使用して呼び出すことはできない
constパラメーター

const変数は、引数を変更するhalve()のような関数に渡されないようにすることは重要であり、当然のことである。しかし、以下のprint()関数のように、変数を変更する意図のない関数にも渡せないとしたら、それは制限となる。

import std.stdio;

void main() {
    const(int[]) slice = [ 10, 20, 30, 40 ];
    print(slice);    // ← コンパイルエラー
}

void print(int[] slice) {
    writefln("%s elements: ", slice.length);

    foreach (i, element; slice) {
        writefln("%s: %s", i, element);
    }
}
D
const_and_immutable.5

上記では、constであるという理由だけでスライスの出力が禁止されるのは意味がない。この状況を適切に処理するには、constパラメータを使用する。これは、関数をconst-correctにするという。これは、関数に不変性を強制する、前述の自己実現的予言だ。

constキーワードは、その数の特定の参照(スライスなど) を通じて、その変数が変更されないことを指定する。パラメータをconstと指定すると、関数内でスライスの要素が変更されないことが保証される。print()がこの保証を提供すると、プログラムはコンパイル可能になる。

print(slice);    // 現在コンパイルできる
// ...
void print(const int[] slice) {
// ...
}
D

この不変更の保証により、変更可能const、およびimmutable変数を引数として渡すことができるため、柔軟性が向上する。

int[] mutableSlice = [ 7, 8 ];
print(mutableSlice);    // コンパイルできる

const int[] slice = [ 10, 20, 30, 40 ];
print(slice);           // コンパイルできる

immutable int[] immSlice = [ 1, 2 ];
print(immSlice);        // コンパイルできる
D

逆に、関数内で変更されないパラメータをconstと定義しなかった場合、その関数の適用性が低下する。このような関数は、const-correctではない。

constパラメータのもう1つの利点は、プログラマに有用な情報を提供できることだ。関数に渡された変数は変更されないことを知っていれば、コードが理解しやすくなる。

constパラメータは、変更可能なconst、およびimmutableの変数を受け入れることができるという事実には、興味深い結果がある。これについては、以下の"パラメータはconstまたはimmutableにするべきか"のセクションで説明する。

inパラメーター

次の章で見るように、inconstを意味し、‑preview=inコマンドラインスイッチと組み合わせて使用するとより有用だ。このため、constパラメータよりもinパラメータを使用することをおすすめする。

immutableパラメータ

constパラメータは、変更可能な constおよびimmutable変数を引数として受け入れるため、歓迎すべきものと考えられる。

一方、immutableパラメーターは選択的であるため、強い要件を課す:引数はimmutableでなければならない。constパラメーターは"私は変更しない"と伝えるのに対し、immutableパラメーターは"あなたも変更しないでください"と追加する。

関数のimmutableパラメータとして渡せるのは、immutable変数だけだ。

void func(immutable int[] slice) {
    // ...
}

void main() {
    immutable int[] immSlice = [ 1, 2 ];
              int[]    slice = [ 8, 9 ];

    func(immSlice);      // コンパイルする
    func(slice);         // ← コンパイルエラー
}
D
const_and_immutable.6

そのため、immutable指定子は、この要件が実際に必要な場合にのみ使用すべきだ。実際、特定の文字列型を通じて、immutable指定子を間接的に使用してきた。これについては、以下で説明する。

constまたはimmutableとして指定されたパラメータは、引数として渡された実際の変数を変更しないことを約束している。これは、不変性について言及できる実際の変数が存在するのは参照型のみであるため、参照型にのみ関係する。

参照型と 値型については、次の章で説明する。これまでに説明した型の中で、スライスと連想配列だけが参照型で、その他は値型だ。

パラメーターはconstimmutableのどちらにするべきか?

注釈: inconstを意味するため、このセクションはinについても説明している。

上記のセクションでは、より柔軟性が高いconstパラメーターがimmutableパラメーターよりも好ましいように思われるかもしれない。しかし、これは必ずしも真実ではない。

constは、元の変数が変更可能であったかconstであったか、immutableであったかの情報を消去する。この情報は、コンパイラからも隠される。

この事実の結果、constパラメータは、immutableパラメータを取る関数の引数として渡すことはできない。例えば、以下の中間関数foo()は、関数を通して渡される実際の変数がmainimmutableとして定義されているにもかかわらず、そのconstパラメータをbar()に渡すことはできない。

void main() {
    /* 元の変数は不変 */
    immutable int[] slice = [ 10, 20, 30, 40 ];
    foo(slice);
}

/* より便利にするために、パラメータをconstとして
 * 受け取る関数。 */
void foo(const int[] slice) {
    bar(slice);    // ← コンパイルエラー
}

/* 不変のスライスを必要とする関数。 */
void bar(immutable int[] slice) {
    // ...
}
D
const_and_immutable.7

bar()パラメーターがimmutableである必要がある。しかし、foo()constパラメーターが参照する元の変数がimmutableであるかどうかは(一般的には)不明だ。

注釈:上記のコードを見ると、main()の元の変数はimmutableであることは明らかだ。しかし、コンパイラは、関数が呼び出されるすべての場所を考慮せずに、関数を個別にコンパイルする。コンパイラにとって、foo()sliceパラメータは、変更可能な変数またはimmutable変数を参照している可能性がある。

解決策として、bar()を呼び出す際にパラメーターの不変コピーを渡す方法がある。

void foo(const int[] slice) {
    bar(slice.idup);
}
D

この方法ではコードはコンパイルされるが、スライスとその内容をコピーするコストが発生し、元の変数がimmutableだった場合、無駄な処理になる。

この分析から、パラメーターを常にconstとして宣言することが、すべての状況で最良のアプローチではないことが明白である。結局、foo()のパラメーターがimmutableとして定義されていれば、bar()を呼び出す前にコピーする必要はないでした:

void foo(immutable int[] slice) {  // 今回は不変
    bar(slice);    // コピーはもう必要ない
}
D

コードはコンパイルされるが、パラメーターをimmutableとして定義すると、同様のコストが発生する:この場合、foo()を呼び出す際に、元の変数がimmutableでなかった場合、immutableのコピーが必要になる:

foo(mutableSlice.idup);
D

テンプレートが役立つ(テンプレートについては後の章で説明する)。この本では、この時点で次の関数を完全に理解することは期待していないが、この問題の解決策として紹介する。次の関数テンプレートfoo()は、変更可能な変数mutableconst、およびimmutableで呼び出すことができる。パラメータは、元の変数が変更可能な場合にのみコピーされる。元の変数がimmutableの場合は、コピーは行われない。

import std.conv;
// ...

/* これはテンプレートであるため、foo()は可変変数と不変変数の
 * 両方で呼び出すことができる。 */
void foo(T)(T[] slice) {
    /* 'to()'は、元の変数が既に不変の場合、
     * コピーを作成しない。 */
    bar(to!(immutable T[])(slice));
}
D
初期化

変数の初期値が単純な式に依存している場合、変更を禁止することは制限とみなすことができる。例えば、以下のfruits配列の内容はaddCitrusの値に依存しているが、変数がconstであるため、コードはコンパイルに失敗する。

const fruits = [ "apple", "pear" ];

if (addCitrus) {
    fruits ~= [ "orange" ];    // ← コンパイルエラー
}
D

autoと定義して変数を変更可能にすれば、コードはコンパイル可能になるが、初期化コードを関数に移動することで、constと定義することも可能である。

bool addCitrus;

string[] makeFruits() {
    auto result = [ "apple", "pear" ];

    if (addCitrus) {
        result ~= [ "orange" ];
    }

    return result;
}

void main() {
    const fruits = makeFruits();
}
D
const_and_immutable.8

ローカル配列resultは変更可能だが、fruitsは依然としてconstであることに注意(おそらくこれはプログラマの意図した通りだ)。コードを名前付き関数に移動することが不可能または面倒な場合は、代わりにラムダを使用することができる。

const fruits = {
  // 'makeFruits()'とまったく同じコードだ。
  auto result = [ "apple", "pear" ];

  if (addCitrus) {
      result ~= [ "orange" ];
  }

  return result;
}();
D

ラムダ式はハイライトされた波括弧で定義され、末尾の括弧で実行される。再び、fruits変数は意図した通りconstになる。

Dでは、shared static this()(およびstatic this())と呼ばれる特別な初期化ブロックで、constおよびimmutable変数に代入することができる。これらのブロックは、モジュールスコープ(どの関数外)で定義された変数を初期化するためのものだ。shared static this()ブロックでは、constおよびimmutable変数を変更することができる。

immutable int[] i;

shared static this() {
    // このブロックでは、'const'および'immutable'モジュール
    // 変数を変更することができる:
    i ~= 43;

    // 変数は、プログラムの残りの部分では
    // 引き続き'const'および'immutable'である。
}
D

shared static this()ブロックは、プログラムがmain()関数の本体を実行する前に実行される。

スライスと要素の不変性

上記で、constスライスの型はconst(int[])と表示されていることを確認した。constの後の括弧が示すように、constはスライス全体を指している。このようなスライスは、いかなる方法でも変更することはできない。要素を追加したり削除したり、要素の値を変更したり、スライスが別の要素セットへのアクセスを提供し始めたりすることはできない。

const int[] slice = [ 1, 2 ];
slice ~= 3;               // ← コンパイルエラー
slice[0] = 3;             // ← コンパイルエラー
slice.length = 1;         // ← コンパイルエラー

const int[] otherSlice = [ 10, 11 ];
slice = otherSlice;       // ← コンパイルエラー
D

不変性をそこまで極端にするのは、すべてのケースにふさわしいとは限らない。ほとんどの場合、重要なのは要素自体の不変性だ。スライスは要素にアクセスするための単なるツールなので、要素が変更されない限り、スライス自体に変更を加えても問題はないはずだ。これは、これまで見てきた、関数がスライス自体のコピーを受け取るケースでは特に当てはまる。

要素のみ不変であることを指定するには、要素の型だけを括弧で囲んだconstキーワードを使いる。コードをそれに応じて変更すると、スライス自体は不変ではなく、要素のみが不変になる。

const(int)[] slice = [ 1, 2 ];
slice ~= 3;               // 要素を追加できる
slice[0] = 3;             // ← コンパイルエラー
slice.length = 1;         // 要素を削除できる

const int[] otherSlice = [ 10, 11 ];
slice = otherSlice;       /* 他の要素へのアクセスを
                           * 提供できる */
D

2つの構文は非常によく似ているが、意味は異なる。要約すると:

const int[]  a = [1]; /* 要素もスライスも
                       * 変更できない */

const(int[]) b = [1]; /* 上記と同じ意味 */

const(int)[] c = [1]; /* 要素は変更できないが、スライスは
                       * 変更できる */
D

この区別は、これまで書いたプログラムの一部で既に適用されている。覚えておくと良いが、3つの文字列エイリアスは不変性に関連している:

同様に、文字列リテラルも不変である:

これらの定義に従って、D文字列は通常、immutable文字の配列である。

constimmutableは推移的である

上記のスライスabのコードコメントで述べたように、これらのスライスとその要素はどちらもimmutableだ。

これは、構造 体およびクラスにも当てはまる。これらについては、後の章で説明する。例えば、const struct変数のすべてのメンバーはconstであり、immutable struct変数のすべてのメンバーはimmutableだ。(クラスについても同様だ。)

.dupそして.idup

文字列が関数のパラメータとして渡される場合、不変性に不一致が生じる可能性がある。.dupおよび.idupプロパティは、希望する不変性を持つ配列のコピーを作成する。

例えば、パラメータの不変性を要求する関数は、変更可能な文字列の不変コピーで呼び出す必要がある場合がある。

void foo(string s) {
    // ...
}

void main() {
    char[] salutation;
    foo(salutation);                // ← コンパイルエラー
    foo(salutation.idup);           // ← これはコンパイルされる
}
D
const_and_immutable.9
使用方法
要約