値型と参照型

この章では、値型と参照型の概念について説明する。これらの概念は、構造体とクラスの違いを理解するために特に重要だ。

また、この章では、&演算子についても詳しく説明する。

この章の最後には、さまざまな型の変数について、次の2つの概念の結果を示す表が掲載されている。

値型

値型は簡単に説明できる。値型の変数は値を持つ。例えば、すべての整数型および浮動小数点型は値型だ。すぐにはわからないかもしれないが、固定長配列も値型だ。

例えば、型intの変数は整数値を持つ。

int speed = 123;
D

変数speedが占めるバイト数は、intのサイズと同じだ。メモリを左から右へ伸びるリボンとして視覚化すると、変数はその一部に存在していると想像できる。

       speed
   ───┬─────┬───
      │ 123 │
   ───┴─────┴───

値型変数がコピーされると、それらは独自の値を取得する。

int newSpeed = speed;
D

新しい変数には、独自の場所と値が割り当てられる。

       speed          newSpeed
   ───┬─────┬───   ───┬─────┬───
      │ 123 │         │ 123 │
   ───┴─────┴───   ───┴─────┴───

当然、これらの変数に対する変更は独立している:

speed = 200;
D

他の変数の値は変更されない。

       speed          newSpeed
   ───┬─────┬───   ───┬─────┬───
      │ 200 │         │ 123 │
   ───┴─────┴───   ───┴─────┴───
以下のassertチェックの使用

以下の例には、その条件が真であることを示すassertチェックが含まれている。つまり、これらは通常の意味でのチェックではなく、読者に"これは真である"ことを伝えるための私の表現方法だ。

例えば、以下のassert(speed == newSpeed)のチェックは、"speedはnewSpeedと同じである"という意味だ。

値の同一性

上記のメモリ表現が示すように、変数には2種類の等価性がある。

int speed = 123;
int newSpeed = speed;
assert(speed == newSpeed);
speed = 200;
assert(speed != newSpeed);
D
アドレス演算子、&

これまで、&演算子はreadf()と組み合わせて使用してきた。&演算子は、readf()に入力データを格納する場所を指定する。

注釈: 標準入力からの読み込みの章で見たように、readf()は明示的なポインタを使わずに使うこともできる。

変数のアドレスは他の目的にも使用できる。次のコードは、2つの変数のアドレスを単に表示するだけだ:

int speed = 123;
int newSpeed = speed;

writeln("speed   : ", speed,    " address: ", &speed);
writeln("newSpeed: ", newSpeed, " address: ", &newSpeed);
D

speednewSpeedは同じ値を持っているが、そのアドレスは異なる。

変数名アドレス
speed1237FFF4B39C738
newSpeed1237FFF4B39C73C

注釈:プログラムの実行ごとにアドレスの値が異なるのは正常だ。変数は、そのプログラムの実行中にたまたま利用可能なメモリの一部に存在しているからだ。

アドレスは通常、16進数形式で表示される。

さらに、2つのアドレスが4離れていることは、2つの整数がメモリ内で隣り合っていることを示している。(16進数のCの値は12なので、8と12の差は4であることに注意。

参照変数

参照型について説明する前に、まず参照変数を定義しよう。

用語:これまで、この本ではいくつかの文脈で"アクセスを提供する"という表現を使用してきた。例えば、スライスや連想配列は要素を所有していないが、Dランタイムが所有する要素へのアクセスを提供する。同じ意味の別の表現として、"スライスは0個以上の要素の参照である"という"の参照である"という表現があり、これは"このスライスは2つの要素を参照している"というように、より短い"参照"という表現で用いられることもある。最後に、参照を介して値にアクセスする行為は、参照解除と呼ばれる

参照変数は、他の変数のエイリアスのように振る舞う変数だ。変数のように見え、変数のように使われるが、それ自体は値を持っていない。参照変数に変更を加えると、実際の変数の値が変更される。

これまで、参照変数は2つの文脈で使用してきた。

参照型

参照型の変数は、個別のIDを持っているが、個別の値は持っていない。これらは、既存の変数へのアクセスを提供する。

この概念はスライスで既に説明した。スライスは要素を所有せず、既存の要素へのアクセスを提供する:

void main() {
    // ここでは'array'という名前だが、この変数も
    // スライスだ。これは、すべての
    // 初期要素へのアクセスを提供する:
    int[] array = [ 0, 1, 2, 3, 4 ];

    // 最初の要素と最後の要素以外の要素へのアクセスを提供するスライス:
    //
    int[] slice = array[1 .. $ - 1];

    // この時点で、slice[0]とarray[1]は
    // 同じ値へのアクセスを提供する:
    assert(&slice[0] == &array[1]);

    // slice[0]を変更すると、array[1]も変更される:
    slice[0] = 42;
    assert(array[1] == 42);
}
D
value_vs_reference.2

参照変数とは対照的に、参照型は単なるエイリアスではない。この違いを理解するために、既存のスライスのコピーとして別のスライスを定義しよう。

int[] slice2 = slice;
D

2つのスライスはそれぞれ独自のアドレスを持っている。つまり、別個の識別子を持っている:

assert(&slice != &slice2);
D

次のリストは、参照変数と参照型の違いをまとめたものだ。

sliceslice2がメモリ内に存在している様子は、次の図で説明できる。

                                 slice        slice2
 ───┬───┬───┬───┬───┬───┬───  ───┬───┬───  ───┬───┬───
    │ 0 │ 1  2  3 │ 4 │        │ o │        │ o │
 ───┴───┴───┴───┴───┴───┴───  ───┴─│─┴───  ───┴─│─┴───
              ▲                    │            │
              │                    │            │
              └────────────────────┴────────────┘

2つのスライスが参照する3つの要素が強調表示されている。

C++とDの違いの1つは、Dではクラスが参照型であることだ。クラスについては後の章で詳しく説明するが、この事実を示す簡単な例を以下に示す。

class MyClass {
    int member;
}
D

クラスオブジェクトは、newキーワードで構築される:

auto variable = new MyClass;
D

variableは、newで構築された匿名MyClassオブジェクトへの参照だ:

  (anonymous MyClass object)    variable
 ───┬───────────────────┬───  ───┬───┬───
    │        ...        │        │ o │
 ───┴───────────────────┴───  ───┴─│─┴───
              ▲                    │
              │                    │
              └────────────────────┘

スライスと同様に、variableがコピーされると、コピーは同じオブジェクトへの別の参照になる。コピーには独自のアドレスがある:

auto variable = new MyClass;
auto variable2 = variable;
assert(variable == variable2);
assert(&variable != &variable2);
D

同じオブジェクトを参照する点では等価だが、別々の変数だ:

  (anonymous MyClass object)    variable    variable2
 ───┬───────────────────┬───  ───┬───┬───  ───┬───┬───
    │        ...        │        │ o │        │ o │
 ───┴───────────────────┴───  ───┴─│─┴───  ───┴─│─┴───
              ▲                    │            │
              │                    │            │
              └────────────────────┴────────────┘

これは、オブジェクトのメンバを修正することで示すこともできる:

auto variable = new MyClass;
variable.member = 1;

auto variable2 = variable;   // それらは同じオブジェクトを共有している
variable2.member = 2;

assert(variable.member == 2); // 変数がアクセスする
                              // オブジェクトが
                              // 変更された。
D

別の参照型として、連想配列がある。スライスやクラスと同様に、連想配列がコピーされたり別の変数に代入されたりすると、どちらも同じ要素のセットにアクセスできる。

string[int] byName =
[
    1   : "one",
    10  : "ten",
    100 : "hundred",
];

// 2つの連想配列は、同じ要素の
// セットを共有する
string[int] byName2 = byName;

// 2番目で追加されたマッピングは ...
byName2[4] = "four";

// ... 1番目を通じて表示される。
assert(byName[4] == "four");
D

次の章で説明するように、元の連想配列がnullだった場合、要素の共有は発生しない。

代入操作の違い

値型と参照変数では、代入操作によって実際の値が変更される。

void main() {
    int number = 8;

    halve(number);      // 実際の値が変更される
    assert(number == 4);
}

void halve(ref int dividend) {
    dividend /= 2;
}
D
value_vs_reference.3

一方、参照型では、代入操作によってアクセスされる値が変わる。例えば、以下のslice3変数の代入では、どの要素の値も変更されない。むしろ、slice3が参照する要素が変更される。

int[] slice1 = [ 10, 11, 12, 13, 14 ];
int[] slice2 = [ 20, 21, 22 ];

int[] slice3 = slice1[1 .. 3]; // slice1の要素1および
                               // 要素2へのアクセス

slice3[0] = 777;
assert(slice1 == [ 10, 777, 12, 13, 14 ]);

// この代入は、
// slice3がアクセスを許可している要素を変更しない。slice3が
// 他の要素へのアクセスを許可するようにする。
slice3 = slice2[$ - 1 .. $]; // 最後の要素へのアクセス

slice3[0] = 888;
assert(slice2 == [ 20, 21, 888 ]);
D

今回は、MyClass型の2つのオブジェクトを使用して、同じ効果を実証しよう。

auto variable1 = new MyClass;
variable1.member = 1;

auto variable2 = new MyClass;
variable2.member = 2;

auto aCopy = variable1;
aCopy.member = 3;

aCopy = variable2;
aCopy.member = 4;

assert(variable1.member == 3);
assert(variable2.member == 4);
D

上記のaCopy変数は、最初にvariable1と同じオブジェクトを参照し、次にvariable2と同じオブジェクトを参照する。その結果、aCopyを通じて変更される.memberは、最初にvariable1の値となり、次にvariable2の値となる。

参照型の変数は、いかなるオブジェクトも参照してはならない。

参照変数には、常にそのエイリアスである実際の変数がある。変数がないと、その変数は存在し始めない。一方、参照型の変数は、オブジェクトを参照せずに存在し始めることができる。

例えば、MyClass変数は、newによって実際のオブジェクトが作成されていなくても定義することができる。

MyClass variable;
D

このような変数には、nullという特別な値が割り当てられる。nullおよびisキーワードについては、後の章で説明する。

固定長配列は値型、スライスは参照型

Dの配列とスライスは、値型と参照型に関して違いがある。

すでに上で見たように、スライスは参照型だ。一方、固定長配列は値型だ。それらは要素を所有し、個々の値として振る舞う。

int[3] array1 = [ 10, 20, 30 ];

auto array2 = array1; // array2の要素はarray1の要素
                      // とは異なる
array2[0] = 11;

// 最初の配列は影響を受けない:
assert(array1[0] == 10);
D

array1は、定義時に長さが指定されているため、固定長の配列だ。autoは、コンパイラがarray2の型を推測するため、これも固定長の配列だ。array2の要素の値は、array1の要素の値からコピーされる。各配列には、それぞれ独自の要素がある。一方の要素を変更しても、もう一方の要素には影響しない。

実験

次のプログラムは、==演算子を異なる型に適用した実験だ。この演算子は、特定の型の両方の変数と、それらの変数のアドレスに適用される。このプログラムは、次の出力を生成する。

変数の種類a == b&a == &b
値が等しい変数 (値型)truefalse
値が異なる変数 (値型)falsefalse
foreachで'ref'変数を使用truetrue
foreachで'ref'変数を使用しないtruefalse
'out'パラメーターを持つ関数truetrue
'ref'パラメーターを持つ関数truetrue
'in'パラメーターを持つ関数truefalse
同じ要素にアクセス可能なスライスtruefalse
異なる要素へのアクセスを提供するスライスfalsefalse
MyClassの変数を同じオブジェクトに(reference type)truefalse
MyClassの変数を異なるオブジェクトに(reference type)falsefalse

上記の表は、次のプログラムによって生成された。

import std.stdio;
import std.array;

int moduleVariable = 9;

class MyClass {
    int member;
}

void printHeader() {
    immutable dchar[] header =
        "                     Type of variable" ~
        "                      a == b  &a == &b";

    writeln();
    writeln(header);
    writeln(replicate("=", header.length));
}

void printInfo(const dchar[] label,
               bool valueEquality,
               bool addressEquality) {
    writefln("%55s%9s%9s",
             label, valueEquality, addressEquality);
}

void main() {
    printHeader();

    int number1 = 12;
    int number2 = 12;
    printInfo("variables with equal values (value type)",
              number1 == number2,
              &number1 == &number2);

    int number3 = 3;
    printInfo("variables with different values (value type)",
              number1 == number3,
              &number1 == &number3);

    int[] slice = [ 4 ];
    foreach (i, ref element; slice) {
        printInfo("foreach with 'ref' variable",
                  element == slice[i],
                  &element == &slice[i]);
    }

    foreach (i, element; slice) {
        printInfo("foreach without 'ref' variable",
                  element == slice[i],
                  &element == &slice[i]);
    }

    outParameter(moduleVariable);
    refParameter(moduleVariable);
    inParameter(moduleVariable);

    int[] longSlice = [ 5, 6, 7 ];
    int[] slice1 = longSlice;
    int[] slice2 = slice1;
    printInfo("slices providing access to same elements",
              slice1 == slice2,
              &slice1 == &slice2);

    int[] slice3 = slice1[0 .. $ - 1];
    printInfo("slices providing access to different elements",
              slice1 == slice3,
              &slice1 == &slice3);

    auto variable1 = new MyClass;
    auto variable2 = variable1;
    printInfo(
        "MyClass variables to same object (reference type)",
        variable1 == variable2,
        &variable1 == &variable2);

    auto variable3 = new MyClass;
    printInfo(
        "MyClass variables to different objects (reference type)",
        variable1 == variable3,
        &variable1 == &variable3);
}

void outParameter(out int parameter) {
    printInfo("function with 'out' parameter",
              parameter == moduleVariable,
              &parameter == &moduleVariable);
}

void refParameter(ref int parameter) {
    printInfo("function with 'ref' parameter",
              parameter == moduleVariable,
              &parameter == &moduleVariable);
}

void inParameter(in int parameter) {
    printInfo("function with 'in' parameter",
              parameter == moduleVariable,
              &parameter == &moduleVariable);
}
D
value_vs_reference.4

注釈

要約