ミックスイン
ミックスインは、生成されたコードをソースコードにミックスインするためのものだ。ミックスインされたコードは、テンプレートインスタンスまたはstring
として生成される。
コードは、文字列インポートとしてプログラムに挿入することもできる。
テンプレートミックスイン
テンプレート とその他のテンプレート"の章で、テンプレートは、コンパイラがそのパターンから実際のインスタンスを生成するためのパターンとしてコードを定義することを学んだ。テンプレートは、関数、構造体、共用体、クラス、インターフェース、およびその他の合法的なDコードを生成することができる。
テンプレートミックスインは、mixin
キーワードによって、テンプレートのインスタンス化をコードに挿入する。
以下の例でわかるように、mixin
キーワードはテンプレートミックスインの定義でも使われる。
特定のテンプレートパラメーターのセットに対するテンプレートのインスタンス化は、mixin
キーワードが現れるソースコードのその位置に挿入される。
例えば、エッジの配列と、そのエッジを操作する2つの関数を定義するテンプレートがあるとする。
このテンプレートでは、配列の要素の型と数は柔軟にできる。int
および2
に対するこのテンプレートのインスタンス化は、次の構文によってミックスインされる。
例えば、上記のmixin
は、2要素のint
配列と、テンプレートによって生成された2つの関数を、struct
定義の内部に挿入することができる。
その結果、Line
は、メンバー配列と2つのメンバー関数を定義することになる。
出力:
0 | 100 |
1 | 200 |
同じテンプレートの別のインスタンスは、例えば関数内で使用することができる。
このmixin
は、main()
内に配列と2つのローカル関数を挿入する。出力:
0 | Point(0, 0) |
1 | Point(0, 0) |
2 | Point(0, 0) |
3 | Point(3, 3) |
4 | Point(0, 0) |
テンプレートミックスインはローカルインポートを使用する必要がある
テンプレートのインスタンス化をそのままミックスインすると、テンプレート自体が使用しているモジュールに関する問題が発生する可能性がある: これらのモジュールは、mixin
のサイトでは利用できない可能性がある。
a
という名前のモジュールを考えてみよう。当然、このモジュールは、使用しているstd.string
モジュールをインポートする必要がある。
しかし、std.string
が実際のmixin
サイトにインポートされていない場合、コンパイラはその時点でformat()
の定義を見つけることができない。a
をインポートし、そのモジュールからA!int
をミックスインしようとする次のプログラムを考えてみよう。
エラー: 未定義の識別子形式
エラー: ミックスインdeneme.main.A!intのインスタンス化エラー
そのため、テンプレートミックスインが使用するモジュールは、ローカルスコープでインポートする必要がある。
テンプレート定義内にある限り、上記のimport
ディレクティブはa()
関数の外にも置くことができる。
ミックスインする型の識別
ミックスインは、それをミックスインしている実際の型を識別する必要がある場合がある。この情報は、テンプレートパラメータのthis
を通じて入手できる。これは、テンプレートの詳細の章で説明した。
プログラムの出力から、実際の型はMyStruct
としてテンプレート内で利用可能であることがわかる。
実際にミックスインされている型 | MyStruct |
---|
文字列ミックスイン
Dのもう1つの強力な機能は、コンパイル時にその文字列が認識されている限り、string
としてコードを挿入できることだ。文字列ミックスインの構文には、括弧の使用が必要だ。
例えば、hello worldプログラムは、mixin
を使用して次のように記述することもできる。
文字列がコードとして挿入され、プログラムは次の出力を生成する:
Hello, World!
さらに、プログラム全体を文字列ミックスインとして挿入することもできる。
これらの例では、文字列はコードとして記述することもできたため、ミックスインは必要ないことは明らかだ。
文字列ミックスインの威力は、コンパイル時にコードを生成できることにある。次の例では、CTFEを利用して、コンパイル時に文を生成している。
出力:
Hello, World!
Hi, World!
"writeln"
式は、printStatement()
内で実行されないことに注意。むしろ、printStatement()
は、main()
内で実行されるwriteln()
式を含むコードを生成する。生成されたコードは、以下と同等だ。
mixin
の複数の引数
コンパイル時にすべてが既知の場合、mixin
は複数の引数を受け取り、その文字列表現を自動的に連結する:
これは、例えばformat
式を使用する場合よりも便利である。
文字列ミックスインのデバッグ
生成されたコードはソースコード全体としてすぐには確認できないため、mixin
式でコンパイルエラーの原因を特定するのは難しい場合がある。文字列ミックスインのデバッグを支援するために、 dmd
コンパイラスイッチ-mixin
がある。これは、ミックスインされたすべてのコードを指定したファイルに書き出する。
次のようなプログラムを考えてみよう。このプログラムには、ミックスインされているコードに構文エラーがある。コンパイラエラーからは、struct
メンバーの定義の末尾にセミコロンが欠落していることが明確ではない:
-mixin
スイッチを指定してコンパイルすると、コンパイルエラーは指定したファイル ( 以下の例ではmixed_in_code
) 内の行を指す。
標準ライブラリによって混入された他のすべてのコードと共に、指定されたファイル内のmixed_in_code
の行に以下のコードが追加される:
[...]
// deneme.d(6)の拡張
struct S {
int m
} ← 154行
文字列ミックスインをデバッグするもう1つのオプションは、 pragma(msg)
である。これはコンパイル時に生成されたコードを出力する。ただし、デバッグのために一時的にmixin
キーワードをpragma(msg)
に置き換える必要があるため、実用性は低い。
ミックスインの名前空間
テンプレートミックスインでの名前の曖昧さを回避および解決することができる。
例えば、次のプログラムでは、main()
内に2つのi
変数が定義されている。1つはmain
で明示的に定義され、もう1つはミックスインされている。ミックスインされた名前が、周囲のスコープにある名前と同じ場合、周囲のスコープにある名前が使用される。
上記のコメントで示唆されているように、テンプレートミックスインは、その内容のための名前空間を定義し、テンプレートコードに現れる名前は、まずその名前空間で検索される。これは、print()
の動作で確認できる。
42
0 ← by print()によって表示される
複数のテンプレートミックスインで同じ名前が定義されている場合、コンパイラは名前衝突を解決できない。同じテンプレートインスタンスを2回ミックスインする短いプログラムでこれを確認しよう。
エラー: deneme.main.Templ!().iが ... deneme.main.Templ!().iと
競合している ...
これを防ぐために、テンプレートミックスインに名前空間識別子を割り当て、その識別子で含まれる名前を参照することができる。
文字列ミックスインには、このような名前空間機能はない。ただし、単純なラッパーテンプレートを介して文字列を渡すだけで、文字列をテンプレートミックスインとして使用することは簡単だ。
まず、文字列ミックスインで同様の名前衝突が発生する場合を見てみよう。
エラー: deneme.main.iの宣言はすでに定義されている
この問題を解決する1つの方法は、文字列ミックスインをテンプレートミックスインに効果的に変換する、次の単純なテンプレートを介してstring
を渡すことだ。
演算子オーバーロードにおける文字列ミックスイン
演算子オーバーロードの章で、mixin
式がいくつかの演算子の定義にどのように役立ったかを見てきた。
実際、ほとんどの演算子メンバー関数がテンプレートとして定義されているのは、演算子をstring
値として利用可能にし、コード生成に使用できるようにするためだ。この章とその演習問題の解答で、その例を見た。
デストラクタへの混在
ユーザー定義型に複数のデストラクタを混合することができる。これらのデストラクタは、それらを追加したmixin
文の逆の順序で呼び出される。この機能により、異なるリソースを1つの型に混合し、それぞれ独自のクリーンアップコードを導入することができる。
Barによってミックスインされたデストラクタ
Fooによってミックスインされたデストラクタ
実際のデストラクタ
この記事の執筆時点では、バグのため、コンストラクタなどの他の特殊関数には同じ動作は適用されない。さらに、文字列ミックスインによってミックスインされたデストラクタは、その型の既存のデストラクタと競合する。
テキストファイルのインポート
テキストファイルの内容をコンパイル時にコードに挿入することができる。内容はstring
リテラルとして扱われ、文字列が使用できる場所ならどこでも使用することができる。例えば、コードとして混合することができる。
例えば、ファイルシステムに、file_one
とfile_two
という2つのテキストファイルがあり、その内容が以下のとおりであるとする。
file_one
:こんにちは
file_two
:s ~= ", World!"; import std.stdio; writeln(s);
string
次のプログラム内の2つのimport
ディレクティブは、コンパイル時にそれらのファイルの内容が文字列リテラルに変換されたものに対応する。
テキストファイルのインポート(別名、文字列のインポート)には、テキストファイルの場所をコンパイラに指示する-J
コンパイラスイッチが必要だ。例えば、2つのファイルが現在のディレクトリ(Linux環境では.
で指定)にある場合、次のコマンドでプログラムをコンパイルできる。
出力:
こんにちは、世界!
ファイルの内容をstring
リテラルとして考慮すると、プログラムは次のものと同等になる:
さらに、混在する文字列も考慮すると、プログラムは次のものと同等になる:
例
(注釈:ラムダ構文がDに追加される前は、述語を文字列で指定するのがより一般的だった。この例のような文字列述語はPhobosでも引き続き使用されているが、ほとんどの場合、=>
ラムダ構文の方が適しているだろう。)
数値の配列を受け取り、特定の条件を満たす要素で構成される別の配列を返す、次の関数テンプレートを考えてみよう。
この関数テンプレートは、フィルタリング条件をテンプレートパラメータとして受け取り、その条件をそのままif
文に挿入する。
例えば、7未満の数を選択する条件の場合、if
の条件は次のコードのように記述する必要がある:
filter()
テンプレートのユーザーは、条件をstring
として指定できる:
重要なことは、テンプレートパラメータで使用される名前は、filter()
の実装で使用される変数の名前と一致しなければならないことだ。したがって、テンプレートにはその名前が何かを文書化し、ユーザーはその名前を使用しなければならない。
Phobosでは、a、b、nなどの単一の文字で構成される名前を使用している。