なぜジェネリクスか
複数の要素型で動くべき型に、すぐに行き当たります。2 つの値の「ペア」は、その値が整数か文字列かユーザー定義の shape かは気にしません。ジェネリクスを使えば、型を 一度 書けば、呼び出し側が必要とするどんな要素型でもインスタンス化できます。
代替——IntPair、StringPair、BytePair などを書く——はすぐに飽きるし、合成できません。Zero のジェネリクスはこの仕事のための標準的な道具です。
ジェネリック関数
関数名とパラメータリストの間の山括弧に型パラメータを宣言します。
fun makePair<T, U>(left: T, right: U) -> Pair<T, U> {
return Pair { left: left, right: right }
}
T と U はプレースホルダーの型——呼び出し側が何にするかを決めます。
呼び出しは通常、それらを綴り出す必要はありません——コンパイラーが引数の型から推論します。
let pair = makePair(40, 2_u8)
ここでは T は i32 と推論され(サフィックスのない整数リテラルのデフォルト)、U は u8 です(_u8 サフィックスから)。結果のバインディング pair の型は Pair<i32, u8> です。
推論が間違った型を選ぶ場合——たとえばリテラルが曖昧な場合——呼び出し箇所でパラメータを固定できます。
let pair = makePair<u8, u8>(1, 2)
(山括弧の呼び出し構文の正確な形は Zero バージョンで異なる場合があります——正確な綴りは現行ドキュメントを確認してください。推論優先の動作が安定した部分です。)
ジェネリックな shape
shape も同じ方法で型パラメータを取ります。
shape Pair<T, U> {
left: T,
right: U,
}
各フィールドの型はパラメータを言及できます。インスタンスはそれらを固定します。
let intBytes: Pair<i32, u8> = Pair { left: 40, right: 2_u8 }
let words: Pair<String, String> = Pair { left: "hi", right: "there" }
ジェネリックな shape とジェネリック関数を組み合わせた動作例——Run をクリックして推論の動きを見てください。
ジェネリックな shape 宣言ひとつ、ジェネリック関数ひとつ、強く型付けされた呼び出しひとつ。IntBytePair のような決まり文句はありません。
型エイリアス
同じパラメータ化された型が何度も登場するなら、type で名前を付けます。
type BytePair = Pair<u8, u8>
これで BytePair は、型を書ける場所ならどこでも Pair<u8, u8> と互換になります。
エイリアスは単なる命名機能で、別個の型を作りません。BytePair を取る関数は喜んで Pair<u8, u8> 型の値を受け入れます(その逆も)。
標準ライブラリのジェネリクス
同じ機構が標準ライブラリの多くを動かしています。実際の Zero コードで見かけるいくつか。
Maybe<T>——Tか何もないかを保持するオプション値。Span<T>——Tの値に対する借用スライス。Span<u8>はバイトバッファに対する定番のビュー。ref<T>とmutref<T>——コピーせずにデータを共有する必要があるケースのための明示的な参照型。
これらをすべて一度に学ぶ必要はありません。ジェネリクスのポイントは、同じ shape が手元のどんな要素型でも動く、ということです。
ジェネリクスが見合うとき(そして見合わないとき)
同じ関数や shape を異なる要素型で 2 回書きそうになったら、ジェネリクスに手を伸ばしましょう。次のときは具体型を使います。
- 関数のロジックが特定の型でしか意味をなさない(たとえば
Stringのパーサー)。 - デバッグしやすくするためにエラーメッセージに型を表示させたい。
- パフォーマンス特性が特定のメモリサイズに依存する。
ジェネリクスのコストは実在します——バイナリが大きくなり(各インスタンス化が新しいコードを生成)、コンパイル時間がわずかに遅くなります。ほとんどのアプリケーションコードでは無視できるコストですが、バイナリサイズが重要な、組み込み風のきついコードを書くときには知っておく価値があります。
制約についてのメモ
「T は == をサポートしなければならない」「T は Iterator を実装しなければならない」のように、型パラメータに制約を付けられるジェネリックシステムもあります。pre-1.0 における Zero の制約のストーリーはまだ進化中です——公式リポジトリのサンプルは、洗練された境界なしの素のジェネリクスを使っています。言語が落ち着くにつれて、言語の他の部分と一貫した小さく規則的な形で制約構文が登場することが期待されます。今のところは、実際に渡す T で動くジェネリクスを書き、操作がサポートされていないとコンパイラーが教えてくれるのを待ちましょう。
次回: enum
ジェネリクスは型をパラメータ化させました。次のビルディングブロックは、スペクトルのもう一方の端にあります——enum、バリアントが追加データを運ばないケースのための Zero のプレーンな列挙型です。
よくある質問
Zero のジェネリクスはどう動きますか?
関数または shape 名の後ろ、山括弧で型パラメータを宣言します。fun makePair<T, U>(left: T, right: U) -> Pair<T, U> や shape Pair<T, U> { left: T, right: U }。呼び出し側はパラメータを明示的に固定する(Pair<i32, u8>)か、呼び出しの引数からコンパイラーに推論させます。
Zero の shape はジェネリックにできますか?
はい。関数で使うのと同じ山括弧構文で shape も型パラメータを取れます: shape Pair<T, U> { left: T, right: U }。各フィールドは型でパラメータを使えます。インスタンスはパラメータ化された型を書いて作ります——たとえば Pair<i32, u8> のように。
ジェネリック関数を呼ぶとき型パラメータを指定する必要がありますか?
通常は不要です。コンパイラーは引数の型から推論します。makePair(40, 2_u8) を呼ぶだけで十分です——T は i32 に、U は u8 になります。推論が間違った型を選ぶときや、呼び出し箇所でそれを文書化したいときは、パラメータを明示的に固定できます。
Zero の型エイリアスとは?
型エイリアスは、より長い型式の短縮名です。type BytePair = Pair<u8, u8> と書けば、Pair<u8, u8> を書けるところならどこでも BytePair と書けます。エイリアスは純粋な命名機能です——新しい型を導入するわけではなく、既存の型を指すより短い方法を提供するだけです。
ジェネリクスは Zero の標準ライブラリのどこで使われていますか?
あちこちで——任意の要素型を保持または操作する型ならどこでも。オプション値の Maybe<T>、バイトスライスの Span<u8>、要素型でパラメータ化されたコンテナ型などです。同じジェネリック機構がユーザー定義型と標準ライブラリ型の両方を扱います。