不変性
概念は、プログラムの変数によって表される。概念の相互作用は、通常、その概念を表す変数の値を変更する式によって実現される。例えば、次のコードは、購入を表すいくつかの変数を変更する。
変数の値を変更することを、その変数の変更または変更と呼ぶ。変数の変更を禁止することを、不変性と呼ぶ。
変更はほとんどのタスクに不可欠であるため、意図的に変更を禁止することは直感に反するかもしれないが、これは強力で便利な機能である。不変性の概念は、プログラミングコミュニティが一般的に得た経験に基づいている。不変性は、プログラムの正確性と保守性に役立つ。この考え方は非常に強力であるため、一部の関数型プログラミング言語では変更を完全に禁止している。
不変性の主な利点は以下の通りだ:
- 一部の概念は、その定義上、不変である。例えば、1週間は常に7日で、数学の定数πは決して変化しない。また、プログラムがサポートする自然言語のリストは固定されている場合がある (例えば、英語とトルコ語のみ)。
- 不変の概念を表していない場合でも、一部の変数は、初期化後に変更されることを意図していない。そのため、変更することはプログラマーのミスとなる場合がある。例えば、上記のコードの
totalPriceは、初期値が割り当てられた後は変更すべきではないだろう。 - そのパラメータの一部のみを変更可能と定義することで、関数は、どのパラメータをそのまま入力として使用し、どのパラメータを副作用として変更するかを指定することができる。
不変性はプログラミング全般で非常に一般的であり、Dプログラマーによって広く採用されているため、以下の奇妙な点が結果として受け入れられている:
- これまで書いてきたプログラムからもわかるように、不変性は絶対的に必要ではない。
- 不変性の概念は、Dでは
const("constant"の略)およびimmutableキーワードで表現される。この2つの単語は英語では同じ意味だが、プログラムでの役割は異なり、互換性がない場合もある。inoutsharedのように、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の定数として定義されている。numberconstとして指定されているのは、実行時に初期化後に変更するのは適切ではないためだ。同様に、各ユーザーの推測値についても、読み込んだ後は変更すべきではない。- これらの変数の型は明示的に指定されていない。
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パラメーターを優先して使用。constimmutable変数は、実行時に知ることができる不変の概念を表す(あるいは、多少曖昧だが、参照できるメモリ位置がある必要がある)。constパラメータは、関数が変更しないものだ。変更可能、const、immutable変数は、constパラメータの引数として渡すことができる。immutableimmutableパラメータは、関数が特にそのように要求する変数だ。 引数として渡すことができるのは、immutable変数だけだ。const(int[])スライス自体とその要素の両方を変更できないことを指定する。const(int)[]要素のみ変更できないことを指定する。