値型と参照型
この章では、値型と参照型の概念について説明する。これらの概念は、構造体とクラスの違いを理解するために特に重要だ。
また、この章では、&
演算子についても詳しく説明する。
この章の最後には、さまざまな型の変数について、次の2つの概念の結果を示す表が掲載されている。
- 値の比較
- アドレス比較
値型
値型は簡単に説明できる。値型の変数は値を持つ。例えば、すべての整数型および浮動小数点型は値型だ。すぐにはわからないかもしれないが、固定長配列も値型だ。
例えば、型int
の変数は整数値を持つ。
変数speed
が占めるバイト数は、int
のサイズと同じだ。メモリを左から右へ伸びるリボンとして視覚化すると、変数はその一部に存在していると想像できる。
speed ───┬─────┬─── │ 123 │ ───┴─────┴───
値型変数がコピーされると、それらは独自の値を取得する。
新しい変数には、独自の場所と値が割り当てられる。
speed newSpeed ───┬─────┬─── ───┬─────┬─── │ 123 │ │ 123 │ ───┴─────┴─── ───┴─────┴───
当然、これらの変数に対する変更は独立している:
他の変数の値は変更されない。
speed newSpeed ───┬─────┬─── ───┬─────┬─── │ 200 │ │ 123 │ ───┴─────┴─── ───┴─────┴───
以下のassert
チェックの使用
以下の例には、その条件が真であることを示すassert
チェックが含まれている。つまり、これらは通常の意味でのチェックではなく、読者に"これは真である"ことを伝えるための私の表現方法だ。
例えば、以下のassert(speed == newSpeed)
のチェックは、"speedはnewSpeedと同じである"という意味だ。
値の同一性
上記のメモリ表現が示すように、変数には2種類の等価性がある。
- 値の等価性:この本の中で多くの例で登場する
==
演算子は、変数の値を比較する。この意味で2つの変数が等しいと言う場合、その値は等しいということになる。 - 値の同一性:別々の値を持つという意味では、
speed
とnewSpeed
は別個の変数だ。たとえその値が等しくても、これらは別々の変数だ。
アドレス演算子、&
これまで、&
演算子はreadf()
と組み合わせて使用してきた。&
演算子は、readf()
に入力データを格納する場所を指定する。
注釈: 標準入力からの読み込みの章で見たように、readf()
は明示的なポインタを使わずに使うこともできる。
変数のアドレスは他の目的にも使用できる。次のコードは、2つの変数のアドレスを単に表示するだけだ:
speed
とnewSpeed
は同じ値を持っているが、そのアドレスは異なる。
変数名 | 値 | アドレス |
---|---|---|
speed | 123 | 7FFF4B39C738 |
newSpeed | 123 | 7FFF4B39C73C |
注釈:プログラムの実行ごとにアドレスの値が異なるのは正常だ。変数は、そのプログラムの実行中にたまたま利用可能なメモリの一部に存在しているからだ。
アドレスは通常、16進数形式で表示される。
さらに、2つのアドレスが4離れていることは、2つの整数がメモリ内で隣り合っていることを示している。(16進数のCの値は12なので、8と12の差は4であることに注意。
参照変数
参照型について説明する前に、まず参照変数を定義しよう。
用語:これまで、この本ではいくつかの文脈で"アクセスを提供する"という表現を使用してきた。例えば、スライスや連想配列は要素を所有していないが、Dランタイムが所有する要素へのアクセスを提供する。同じ意味の別の表現として、"スライスは0個以上の要素の参照である"という"の参照である"という表現があり、これは"このスライスは2つの要素を参照している"というように、より短い"参照"という表現で用いられることもある。最後に、参照を介して値にアクセスする行為は、参照解除と呼ばれる。
参照変数は、他の変数のエイリアスのように振る舞う変数だ。変数のように見え、変数のように使われるが、それ自体は値を持っていない。参照変数に変更を加えると、実際の変数の値が変更される。
これまで、参照変数は2つの文脈で使用してきた。
ref
foreach
ループ:ref
キーワードは、ループ変数をその反復に対応する実際の要素にする。ref
キーワードを使用しない場合、ループ変数は実際の要素のコピーになる。これは、
&
演算子でも示すことができる。2つの変数のアドレスが同じ場合、2つの変数は同じ値(この場合は同じ要素)を参照することになる。これらは別々の変数だが、
element
とslice[i]
のアドレスが同じであることから、これらは同じ値の同一性を持っていることがわかる。つまり、
element
とslice[i]
は同じ値の参照だ。どちらかを変更すると、実際の値にも影響する。次のメモリレイアウトは、i
が3のときの反復のスナップショットを示している。slice[0] slice[1] slice[2] slice[3] slice[4] ⇢ ⇢ ⇢ (element) ──┬────────┬────────┬────────┬────────┬─────────┬── │ 0 │ 1 │ 2 │ 3 │ 4 │ ──┴────────┴────────┴────────┴────────┴─────────┴──
ref
およびout
関数パラメータ:ref
またはout
として指定された関数パラメータは、関数が呼び出された実際の変数のエイリアスだ。次の例は、関数の
ref
パラメータとout
パラメータに同じ変数を渡して、このケースを示している。ここでも、&
演算子は、両方のパラメータの値の同一性を示している。別々のパラメーターとして定義されているにもかかわらず、
refParameter
とoutParameter
はoriginalVariable
の別名だ:originalVariableのアドレス 7FFF24172958 refParameterのアドレス 7FFF24172958 outParameterのアドレス 7FFF24172958
参照型
参照型の変数は、個別のIDを持っているが、個別の値は持っていない。これらは、既存の変数へのアクセスを提供する。
この概念はスライスで既に説明した。スライスは要素を所有せず、既存の要素へのアクセスを提供する:
参照変数とは対照的に、参照型は単なるエイリアスではない。この違いを理解するために、既存のスライスのコピーとして別のスライスを定義しよう。
2つのスライスはそれぞれ独自のアドレスを持っている。つまり、別個の識別子を持っている:
次のリストは、参照変数と参照型の違いをまとめたものだ。
- 参照変数はアイデンティティを持たず、既存の変数のエイリアスだ。
- 参照型の変数には識別があるが、値は所有しない。むしろ、既存の値へのアクセスを提供する。
slice
とslice2
がメモリ内に存在している様子は、次の図で説明できる。
slice slice2 ───┬───┬───┬───┬───┬───┬─── ───┬───┬─── ───┬───┬─── │ 0 │ 1 │ 2 │ 3 │ 4 │ │ o │ │ o │ ───┴───┴───┴───┴───┴───┴─── ───┴─│─┴─── ───┴─│─┴─── ▲ │ │ │ │ │ └────────────────────┴────────────┘
2つのスライスが参照する3つの要素が強調表示されている。
C++とDの違いの1つは、Dではクラスが参照型であることだ。クラスについては後の章で詳しく説明するが、この事実を示す簡単な例を以下に示す。
クラスオブジェクトは、new
キーワードで構築される:
variable
は、new
で構築された匿名MyClass
オブジェクトへの参照だ:
(anonymous MyClass object) variable ───┬───────────────────┬─── ───┬───┬─── │ ... │ │ o │ ───┴───────────────────┴─── ───┴─│─┴─── ▲ │ │ │ └────────────────────┘
スライスと同様に、variable
がコピーされると、コピーは同じオブジェクトへの別の参照になる。コピーには独自のアドレスがある:
同じオブジェクトを参照する点では等価だが、別々の変数だ:
(anonymous MyClass object) variable variable2 ───┬───────────────────┬─── ───┬───┬─── ───┬───┬─── │ ... │ │ o │ │ o │ ───┴───────────────────┴─── ───┴─│─┴─── ───┴─│─┴─── ▲ │ │ │ │ │ └────────────────────┴────────────┘
これは、オブジェクトのメンバを修正することで示すこともできる:
別の参照型として、連想配列がある。スライスやクラスと同様に、連想配列がコピーされたり別の変数に代入されたりすると、どちらも同じ要素のセットにアクセスできる。
次の章で説明するように、元の連想配列がnull
だった場合、要素の共有は発生しない。
代入操作の違い
値型と参照変数では、代入操作によって実際の値が変更される。
一方、参照型では、代入操作によってアクセスされる値が変わる。例えば、以下のslice3
変数の代入では、どの要素の値も変更されない。むしろ、slice3
が参照する要素が変更される。
今回は、MyClass
型の2つのオブジェクトを使用して、同じ効果を実証しよう。
上記のaCopy
変数は、最初にvariable1
と同じオブジェクトを参照し、次にvariable2
と同じオブジェクトを参照する。その結果、aCopy
を通じて変更される.member
は、最初にvariable1
の値となり、次にvariable2
の値となる。
参照型の変数は、いかなるオブジェクトも参照してはならない。
参照変数には、常にそのエイリアスである実際の変数がある。変数がないと、その変数は存在し始めない。一方、参照型の変数は、オブジェクトを参照せずに存在し始めることができる。
例えば、MyClass
変数は、new
によって実際のオブジェクトが作成されていなくても定義することができる。
このような変数には、null
という特別な値が割り当てられる。null
およびis
キーワードについては、後の章で説明する。
固定長配列は値型、スライスは参照型
Dの配列とスライスは、値型と参照型に関して違いがある。
すでに上で見たように、スライスは参照型だ。一方、固定長配列は値型だ。それらは要素を所有し、個々の値として振る舞う。
array1
は、定義時に長さが指定されているため、固定長の配列だ。auto
は、コンパイラがarray2
の型を推測するため、これも固定長の配列だ。array2
の要素の値は、array1
の要素の値からコピーされる。各配列には、それぞれ独自の要素がある。一方の要素を変更しても、もう一方の要素には影響しない。
実験
次のプログラムは、==
演算子を異なる型に適用した実験だ。この演算子は、特定の型の両方の変数と、それらの変数のアドレスに適用される。このプログラムは、次の出力を生成する。
変数の種類 | a == b | &a == &b |
---|---|---|
値が等しい変数 (値型) | true | false |
値が異なる変数 (値型) | false | false |
foreachで'ref'変数を使用 | true | true |
foreachで'ref'変数を使用しない | true | false |
'out'パラメーターを持つ関数 | true | true |
'ref'パラメーターを持つ関数 | true | true |
'in'パラメーターを持つ関数 | true | false |
同じ要素にアクセス可能なスライス | true | false |
異なる要素へのアクセスを提供するスライス | false | false |
MyClassの変数を同じオブジェクトに(reference type) | true | false |
MyClassの変数を異なるオブジェクトに(reference type) | false | false |
上記の表は、次のプログラムによって生成された。
注釈
- このプログラムは、異なる型の関数パラメータを比較する際にモジュール変数を使用している。モジュール変数は、すべての関数の外側のモジュールレベルで定義される。モジュール内のすべてのコードからグローバルにアクセスできる。
-
std.array
モジュール内のreplicate()
関数は、配列(上記の"="
上記の文字列)を受け取り、指定された回数繰り返し出力する。
要約
- 値型の変数には、独自の値とアドレスがある。
- 参照変数には、独自の値もアドレスも持たない。これらは、既存の変数の別名だ。
- 参照型の変数には独自のアドレスがあるが、それらが参照する値はそれらには属さない。
- 参照型では、代入によって値が変更されるのではなく、アクセスされる値が変更される。
- 参照型の変数は、
null
になる。