不変性
概念は、プログラムの変数によって表される。概念の相互作用は、通常、その概念を表す変数の値を変更する式によって実現される。例えば、次のコードは、購入を表すいくつかの変数を変更する。
変数の値を変更することを、その変数の変更または変更と呼ぶ。変数の変更を禁止することを、不変性と呼ぶ。
変更はほとんどのタスクに不可欠であるため、意図的に変更を禁止することは直感に反するかもしれないが、これは強力で便利な機能である。不変性の概念は、プログラミングコミュニティが一般的に得た経験に基づいている。不変性は、プログラムの正確性と保守性に役立つ。この考え方は非常に強力であるため、一部の関数型プログラミング言語では変更を完全に禁止している。
不変性の主な利点は以下の通りだ:
- 一部の概念は、その定義上、不変である。例えば、1週間は常に7日で、数学の定数πは決して変化しない。また、プログラムがサポートする自然言語のリストは固定されている場合がある (例えば、英語とトルコ語のみ)。
- 不変の概念を表していない場合でも、一部の変数は、初期化後に変更されることを意図していない。そのため、変更することはプログラマーのミスとなる場合がある。例えば、上記のコードの
totalPrice
は、初期値が割り当てられた後は変更すべきではないだろう。 - そのパラメータの一部のみを変更可能と定義することで、関数は、どのパラメータをそのまま入力として使用し、どのパラメータを副作用として変更するかを指定することができる。
不変性はプログラミング全般で非常に一般的であり、Dプログラマーによって広く採用されているため、以下の奇妙な点が結果として受け入れられている:
- これまで書いてきたプログラムからもわかるように、不変性は絶対的に必要ではない。
- 不変性の概念は、Dでは
const
("constant"の略)およびimmutable
キーワードで表現される。この2つの単語は英語では同じ意味だが、プログラムでの役割は異なり、互換性がない場合もある。inout
shared
のように、const
とimmutable
は型修飾子だ。) - "不変の変数"と"定数変数"という用語は、"変数"という単語を文字通り、変化するものとして解釈した場合、意味を成さない。より広い意味では、"変数"という単語は、変更可能か不変かに関わらず、プログラムのあらゆる概念を意味すると解釈されることが多い。
- 自己実現的予言の例として、関数は不変性(constが正しいことを強制される)を遵守せざるを得ず、その結果、より有用になる。(このconstが正しいことを強制的に遵守することは、ウイルス性疾患に似ていると表現されることもある。)
不変の変数
変更できない変数を定義する方法は3つある。
enum
定数
列挙型の章で、enum
が名前付き定数値を定義することを説明した。
その値がコンパイル時に決定できる限り、enum
変数は、関数の戻り値を含む、より複雑な式でも初期化することができる。
このような初期化を可能にするDの機能は、コンパイル時関数実行(CTFE)であり、これは後の章で説明する。
当然のことながら、enum
定数の値は変更できない。
enum
は不変の値を表現する非常に効果的な方法だが、コンパイル時の値にしか使用できない。
enum
定数は、その定数がその値に置き換えられたかのようにプログラムがコンパイルされる、マニフェスト定数である。例として、次のenum
定義と、それを使用する2つの式を考えてみよう。
上記のコードは、i
がすべてその値42
に置き換えられた、以下のコードとまったく同じだ。
この置換は、int
のような単純な型では意味があり、結果のプログラムにも何の影響も与えないが、enum
定数を使用すると、配列や連想配列で隠れたコストが発生する可能性がある。
a
をその値に置き換えた後、コンパイラがコンパイルする同等のコードは次のようになる。
ここでの隠れたコストは、上記の2つの式に対して2つの別々の配列が作成されることだ。そのため、プログラム内で2回以上参照される配列や連想配列は、immutable
変数として定義したほうが理にかなっている。
const
変数
enum
と同様に、このキーワードは、変数の値が決して変更されないことを指定する。enum
とは異なり、const
変数は、メモリアドレスを持つ実際の変数であり、const
変数は通常、プログラムの実行中に初期化される。
コンパイラは、const
変数の変更を許可しない:
次のプログラムは、enum
とconst
の両方を使用している。このプログラムは、ランダムに選んだ数字をユーザーに当てさせる。乱数はコンパイル時には決定できないため、enum
として定義することはできない。しかし、ランダムに選んだ値は決定後に変更してはならないため、この変数をconst
として指定するのが適切だ。
このプログラムでは、前の章で定義したreadInt()
関数を利用している。
観察事項:
min
とmax
は、このプログラムの動作に不可欠な部分であり、その値はコンパイル時に既知である。そのため、これらはenum
の定数として定義されている。number
const
として指定されているのは、実行時に初期化後に変更するのは適切ではないためだ。同様に、各ユーザーの推測値についても、読み込んだ後は変更すべきではない。- これらの変数の型は明示的に指定されていない。
auto
、enum
などの使用と同様、const
変数の型は右側の式から推測できる。
型を完全に記述する必要はないが、const
は通常、実際の型を括弧で囲んで指定する。例えば、const(int)
など。次のプログラムの出力は、3つの変数の型のフルネームが実際には同じであることを示している。
型の実際の名前には、const
が含まれている。
const(int)
const(int)
const(int)
括弧の使用には意味があり、型のどの部分が不変であるかを指定する。これは、スライス全体とその要素の不変性について説明する以下で説明する。
immutable
変数
変数を定義する際、immutable
キーワードはconst
と同じ効果を持つ。immutable
変数は変更できない:
プログラムの他の部分でimmutable
の変数が必要でない限り、不変の変数はconst
またはimmutable
として定義できる。関数で、パラメータがimmutable
でなければならないと特に指定されている場合、そのパラメータに対応する変数はimmutable
として定義しなければならない。これについては、後で説明する。
パラメーター
次の2章で見るように、関数はそのパラメータを変更することができる。例えば、その関数の引数として渡されたスライスの要素を変更することができる。
スライスとその他の配列機能の章で学んだように、スライスは要素を所有せず、要素へのアクセスを提供する。同じ要素へのアクセスを提供するスライスは、同時に複数存在する場合がある。
このセクションの例ではスライスにのみ焦点を当てているが、このトピックは連想配列やクラスにも適用できる。
関数の引数として渡されるスライスは、その関数が呼び出されたときのスライスとは別物だ。引数は、スライス変数のコピーだ(要素はコピーされない)。
プログラムの実行がhalve()
関数に入ったとき、同じ4つの要素へのアクセスを提供するスライスは2つある。
main()
で定義され、halve()
のパラメーターとして渡されるslice
という名前のスライスhalve()
が引数として受け取るnumbers
という名前のスライス。これは、slice
両方のスライスは同じ要素を参照しており、foreach
ループでキーワードref
を使用しているため、要素の値は半分になる。
[5, 10, 15, 20]
関数が引数として渡されたスライスの要素を変更できることは、確かに便利である。この例で見たように、その目的のためだけに存在する関数もある。
コンパイラは、const
型の変数をそのような関数の引数として渡すことを許可しない。
コンパイルエラーは、const(int[])
型の変数をint[]
型の引数として使用できないことを示している。
エラー: 関数deneme.halve (int[] numbers)は、引数の型
(const(int[]))を使用して呼び出すことはできない
const
パラメーター
const
変数は、引数を変更するhalve()
のような関数に渡されないようにすることは重要であり、当然のことである。しかし、以下のprint()
関数のように、変数を変更する意図のない関数にも渡せないとしたら、それは制限となる。
上記では、const
であるという理由だけでスライスの出力が禁止されるのは意味がない。この状況を適切に処理するには、const
パラメータを使用する。これは、関数をconst-correctにするという。これは、関数に不変性を強制する、前述の自己実現的予言だ。
const
キーワードは、その変数の特定の参照(スライスなど) を通じて、その変数が変更されないことを指定する。パラメータをconst
と指定すると、関数内でスライスの要素が変更されないことが保証される。print()
がこの保証を提供すると、プログラムはコンパイル可能になる。
この不変更の保証により、変更可能、const
、およびimmutable
変数を引数として渡すことができるため、柔軟性が向上する。
逆に、関数内で変更されないパラメータをconst
と定義しなかった場合、その関数の適用性が低下する。このような関数は、const-correctではない。
const
パラメータのもう1つの利点は、プログラマに有用な情報を提供できることだ。関数に渡された変数は変更されないことを知っていれば、コードが理解しやすくなる。
const
パラメータは、変更可能な、const
、およびimmutable
の変数を受け入れることができるという事実には、興味深い結果がある。これについては、以下の"パラメータはconst
またはimmutable
にするべきか"のセクションで説明する。
in
パラメーター
次の章で見るように、in
はconst
を意味し、‑preview=in
コマンドラインスイッチと組み合わせて使用するとより有用だ。このため、const
パラメータよりもin
パラメータを使用することをおすすめする。
immutable
パラメータ
const
パラメータは、変更可能な const
およびimmutable
変数を引数として受け入れるため、歓迎すべきものと考えられる。
一方、immutable
パラメーターは選択的であるため、強い要件を課す:引数はimmutable
でなければならない。const
パラメーターは"私は変更しない"と伝えるのに対し、immutable
パラメーターは"あなたも変更しないでください"と追加する。
関数のimmutable
パラメータとして渡せるのは、immutable
変数だけだ。
そのため、immutable
指定子は、この要件が実際に必要な場合にのみ使用すべきだ。実際、特定の文字列型を通じて、immutable
指定子を間接的に使用してきた。これについては、以下で説明する。
const
またはimmutable
として指定されたパラメータは、引数として渡された実際の変数を変更しないことを約束している。これは、不変性について言及できる実際の変数が存在するのは参照型のみであるため、参照型にのみ関係する。
参照型と 値型については、次の章で説明する。これまでに説明した型の中で、スライスと連想配列だけが参照型で、その他は値型だ。
パラメーターはconst
かimmutable
のどちらにするべきか?
注釈: in
はconst
を意味するため、このセクションはin
についても説明している。
上記のセクションでは、より柔軟性が高いconst
パラメーターがimmutable
パラメーターよりも好ましいように思われるかもしれない。しかし、これは必ずしも真実ではない。
const
は、元の変数が変更可能であったか、const
であったか、immutable
であったかの情報を消去する。この情報は、コンパイラからも隠される。
この事実の結果、const
パラメータは、immutable
パラメータを取る関数の引数として渡すことはできない。例えば、以下の中間関数foo()
は、関数を通して渡される実際の変数がmain
でimmutable
として定義されているにもかかわらず、そのconst
パラメータをbar()
に渡すことはできない。
bar()
パラメーターがimmutable
である必要がある。しかし、foo()
のconst
パラメーターが参照する元の変数がimmutable
であるかどうかは(一般的には)不明だ。
注釈:上記のコードを見ると、main()
の元の変数はimmutable
であることは明らかだ。しかし、コンパイラは、関数が呼び出されるすべての場所を考慮せずに、関数を個別にコンパイルする。コンパイラにとって、foo()
のslice
パラメータは、変更可能な変数またはimmutable
変数を参照している可能性がある。
解決策として、bar()
を呼び出す際にパラメーターの不変コピーを渡す方法がある。
この方法ではコードはコンパイルされるが、スライスとその内容をコピーするコストが発生し、元の変数がimmutable
だった場合、無駄な処理になる。
この分析から、パラメーターを常にconst
として宣言することが、すべての状況で最良のアプローチではないことが明白である。結局、foo()
のパラメーターがimmutable
として定義されていれば、bar()
を呼び出す前にコピーする必要はないでした:
コードはコンパイルされるが、パラメーターをimmutable
として定義すると、同様のコストが発生する:この場合、foo()
を呼び出す際に、元の変数がimmutable
でなかった場合、immutable
のコピーが必要になる:
テンプレートが役立つ(テンプレートについては後の章で説明する)。この本では、この時点で次の関数を完全に理解することは期待していないが、この問題の解決策として紹介する。次の関数テンプレートfoo()
は、変更可能な変数mutable、const
、およびimmutable
で呼び出すことができる。パラメータは、元の変数が変更可能な場合にのみコピーされる。元の変数がimmutable
の場合は、コピーは行われない。
初期化
変数の初期値が単純な式に依存している場合、変更を禁止することは制限とみなすことができる。例えば、以下のfruits
配列の内容はaddCitrus
の値に依存しているが、変数がconst
であるため、コードはコンパイルに失敗する。
auto
と定義して変数を変更可能にすれば、コードはコンパイル可能になるが、初期化コードを関数に移動することで、const
と定義することも可能である。
ローカル配列result
は変更可能だが、fruits
は依然としてconst
であることに注意(おそらくこれはプログラマの意図した通りだ)。コードを名前付き関数に移動することが不可能または面倒な場合は、代わりにラムダを使用することができる。
ラムダ式はハイライトされた波括弧で定義され、末尾の括弧で実行される。再び、fruits
変数は意図した通りconst
になる。
Dでは、shared static this()
(およびstatic this()
)と呼ばれる特別な初期化ブロックで、const
およびimmutable
変数に代入することができる。これらのブロックは、モジュールスコープ(どの関数外)で定義された変数を初期化するためのものだ。shared static this()
ブロックでは、const
およびimmutable
変数を変更することができる。
shared static this()
ブロックは、プログラムがmain()
関数の本体を実行する前に実行される。
スライスと要素の不変性
上記で、const
スライスの型はconst(int[])
と表示されていることを確認した。const
の後の括弧が示すように、const
はスライス全体を指している。このようなスライスは、いかなる方法でも変更することはできない。要素を追加したり削除したり、要素の値を変更したり、スライスが別の要素セットへのアクセスを提供し始めたりすることはできない。
不変性をそこまで極端にするのは、すべてのケースにふさわしいとは限らない。ほとんどの場合、重要なのは要素自体の不変性だ。スライスは要素にアクセスするための単なるツールなので、要素が変更されない限り、スライス自体に変更を加えても問題はないはずだ。これは、これまで見てきた、関数がスライス自体のコピーを受け取るケースでは特に当てはまる。
要素のみ不変であることを指定するには、要素の型だけを括弧で囲んだconst
キーワードを使いる。コードをそれに応じて変更すると、スライス自体は不変ではなく、要素のみが不変になる。
2つの構文は非常によく似ているが、意味は異なる。要約すると:
この区別は、これまで書いたプログラムの一部で既に適用されている。覚えておくと良いが、3つの文字列エイリアスは不変性に関連している:
string
は、immutable(char)[]
wstring
はimmutable(wchar)[]
の別名だdstring
はimmutable(dchar)[]
の別名だ
同様に、文字列リテラルも不変である:
- リテラルの型は
"hello"c
はstring
- リテラル
"hello"w
はwstring
- リテラル
"hello"d
はdstring
これらの定義に従って、D文字列は通常、immutable
文字の配列である。
const
とimmutable
は推移的である
上記のスライスa
とb
のコードコメントで述べたように、これらのスライスとその要素はどちらもimmutable
だ。
これは、構造 体およびクラスにも当てはまる。これらについては、後の章で説明する。例えば、const
struct
変数のすべてのメンバーはconst
であり、immutable
struct
変数のすべてのメンバーはimmutable
だ。(クラスについても同様だ。)
.dup
そして.idup
文字列が関数のパラメータとして渡される場合、不変性に不一致が生じる可能性がある。.dup
および.idup
プロパティは、希望する不変性を持つ配列のコピーを作成する。
.dup
は、配列の変更可能なコピーを作成する("dup"は"do duplicate"の略)。.idup
配列の不変コピーを作成する
例えば、パラメータの不変性を要求する関数は、変更可能な文字列の不変コピーで呼び出す必要がある場合がある。
使用方法
- 原則として、変更可能な変数よりも不変の変数を使用。
- 値がコンパイル時に計算できる場合は、定数値を
enum
として定義。例えば、1分あたりの秒数の定数値は、enum
とすることができる。右辺から型を推測できる場合は、型を明示的に指定する必要はない。
enum
配列とenum
連想配列の隠れたコストを考慮。プログラム内で1回以上使用される場合は、immutable
変数として定義。- 値が決して変化しないが、コンパイル時にはわからない変数は、
const
として指定。ここでも、型は推測できる。 - 関数がパラメータを変更しない場合は、そのパラメータを
in
として指定。これにより、プログラマーの意図が明確になり、変更可能変数とimmutable
変数の両方を引数として渡すことができる。 - この本でのほとんどの例とは異なり、パラメータのどの部分が不変であるべきかを常に指定。
const
パラメータは、immutable
を受け取る関数には渡せないことを考慮。上記の"パラメータはconst
にするべきか、immutable
にするべきか"のセクションを参照。- 関数がパラメータを変更する場合は、そのパラメータを変更可能のままにしておいてほしい(
in
、const
、immutable
では、いずれにしても変更は許可されない)。出力:
olleh
要約
enum
変数は、コンパイル時に既知の不変の概念を表す。enum
配列と連想配列は、そのenum
定数が参照されるたびに新しい配列を作成するコストがかかる。配列と連想配列には、代わりにimmutable
を使用することを推奨する。const
パラメーターよりもin
パラメーターを優先して使用。const
immutable
変数は、実行時に知ることができる不変の概念を表す(あるいは、多少曖昧だが、参照できるメモリ位置がある必要がある)。const
パラメータは、関数が変更しないものだ。変更可能、const
、immutable
変数は、const
パラメータの引数として渡すことができる。immutable
immutable
パラメータは、関数が特にそのように要求する変数だ。 引数として渡すことができるのは、immutable
変数だけだ。const(int[])
スライス自体とその要素の両方を変更できないことを指定する。const(int)[]
要素のみ変更できないことを指定する。