概要
以下のdev.to記事の日本語訳です。
https://dev.to/talzvon/rust-generic-types-in-method-definitions-4iah
内容自体はRust公式チュートリアル10.1章に包含されますが、入門者が読み過ごしやすいポイントをより丁寧に説明しています。訳者による大幅な抜粋・追記・変更あり。
対象
- Rust公式チュートリアル 10章でつまずいた人
- Rustで初めてジェネリクスの概念に触れた人
導入
もしあなたがRustの初心者で、Rustの公式チュートリアルを読んでいるのなら(もちろん読むべきです)、私のように10.1章: ジェネリックなデータ型の章で混乱した経験があるかもしれません (例: impl<T> Struct<T> {}
ってなんで<T>
を2回書くんだっけ?)。その混乱を解消するために、この記事を書きました。すでに公式チュートリアルをその部分まで読んで、ジェネリックなデータ型についておぼろげには理解をしていることが前提になります。この記事では型定義とメソッド定義におけるジェネリック型の使い方について、初心者が犯しがちな間違いに注意しながら、順番にみていくことにしましょう。
最もシンプルな例
まずは一番簡単な例から。チュートリアルで紹介されているとおり、struct
キーワードを使って、ユーザーが自前の型(Type)を定義することができます。それにメソッドを追加したい場合、次のようにしますよね。
// 型を宣言する
struct Container {
field: i32,
}
// 型にメソッドを実装する
impl Container {
fn foo(&self) {
// 何らかの操作
}
}
impl
キーワードを使って、型にメソッドを追加しています。
Rustにおいて、struct
キーワードを使って定義される「構造体(struct)」は「型(type)」の1種です(「構造体であれば型である」は正しく、「型であれば構造体である」は誤り)。たとえばスカラー型(i32
, bool
, etc...)や複合型(タプル・配列)・列挙型などは構造体ではない型の代表例です。すべての型について、impl
を用いたメソッドの実装が可能です。よって本稿では構造体を例示しつつ、「型」という表現を主に使って説明を進めます。
シンプルなジェネリック型の使用例
一方、構造体にジェネリックな型が含まれている場合、impl
を行う際にも型を指定しなければなりません。下の例で、[TYPE]
とは、impl
ブロックによってメソッドを実装される(具体的な)型を指しています。
struct Container<T> {
field: T,
}
impl Container<[TYPE]> {
}
例えば、次のような場合ですね。
struct Container<T> {
field: T,
}
impl Container<i32> {
fn foo(&self) {
println!("これはContainer::<i32>::foo()です!");
}
}
これの意味するところは、「T
がどんな型であったとしてもContainer
のインスタンスを作ることができるが、T
としてi32
型を使ったインスタンスだけがfoo
メソッドを持つことができる」ということになります。実際にインスタンスを作成してみると、このことが確認できます。
let inst1 = Container { field: 'c' };
let inst2 = Container { field: 10 };
let inst3 = Container { field: 12.2 };
inst1.foo(); // コンパイルエラー: - method doesn't exist
inst2.foo(); // 動作OK
inst3.foo(); // コンパイルエラー: - method doesn't exist
これら3つのインスタンスはすべてContainer<T>
型ですが、インスタンス生成時にジェネリック型としてi32
を渡したinst2
のみがfoo
メソッドを持ちます。
これはある種のパターンマッチと言っても良いでしょう。インスタンス生成時に渡される具体的な型(matcher)と、impl
内で定義されたメソッドの型情報(pattern)とを照らし合わせて、matcherとpatternが適合した時のみインスタンスにメソッドが付与される訳です。
独自型を使ったシンプルな例
先ほどはプリミティブ型であるi32
を使いましたが、struct
やenum
等によって自分で定義した型を使っても同じことです。以下のコードはi32
を独自型Color
に変更した以外は、先ほどのコードと全く同じものです。
struct Color {
red: u8,
green: u8,
blue: u8,
}
struct Container<T> {
filed: T,
}
impl Container<Color> {
fn foo(&self) {
println!("これはContainer::<Color>::foo()です!");
}
}
fn main() {
let inst1 = Container {
filed: Color { red: 10, green: 20, blue: 30 },
};
let inst2 = Container { field: 10 };
let inst3 = Container { field: 12.2 };
inst1.foo(); // 動作OK
inst2.foo(); // コンパイルエラー: - method doesn't exist
inst3.foo(); // コンパイルエラー: - method doesn't exist
}
「インスタンス生成時にジェネリック型としてColor
を渡したインスタンスのみがfoo
メソッドを持つ」。さっきと一緒ですね。
よくある入門者のミス
さて、ここまで特定の型にマッチした時のみメソッドを実装したい場合を見てきました。では、インスタンス生成時にT
としてどのような型を与えた場合でもメソッドを付与させたい時はどうすればよいのでしょうか。最初に思いつくのは、次のようなコードかもしれません:
struct Container<T> {
field: T,
}
impl Container<T> {
fn foo(&self) {
println!("これはContainer::<ANYTYPE>::foo()です??");
}
}
残念ながら、これは期待どおりには動きません。何がおかしいのでしょうか? Color
型を使った例と比較して、コンパイラがどう解釈するのかを忠実に再現してみてください: 「インスタンス生成時にジェネリック型としてT
を渡したインスタンスのみがfoo
メソッドを持つ」。
つまり、このコードは 「Tという名前の(具体的な)型」 に対してfoo
メソッドを付与しようとするのです。そんな名前の型はこのコードのどこにも定義されていないので、「Tなんて名前の型は見当たらないよ」というメッセージとともにコンパイルエラーという事になります。「私はContainer<T>
の部分でT
をジェネリック型引数として定義したよ!」と言うかもしれませんが、その定義のスコープはstruct
ブロックの中だけです。impl
ブロックからすれば知る由もありません。
要はここまでの情報では片手落ちだったのです。まだ型定義の際(struct
ブロック)のジェネリクスの使い方を学んだだけでした。我々は新たにメソッド定義の際(impl
ブロック)のジェネリクスの使い方を学ぶ必要があります。
全ての型にマッチするimplブロック
じゃあどうすればあなたはimpl
ブロックがすべての型に対してマッチすることをコンパイラに伝えられるでしょうか?
答え: impl
ブロックの中でもジェネリック型を定義してやれば良いのです。
struct Container<T> {
field: T,
}
impl<T> Container<T> {
fn foo(&self) {
println!("これはContainer::<ANYTYPE>::foo()です!");
}
}
ここでは、impl<T>
でまずジェネリック型T
を定義して、それからContainer<T>
とすることでインスタンスがどんな型をT
として持った場合でもマッチするように指定しました。トレイト境界(公式本10.2章で学びます)による制約なしで、あらゆる型です。結果として、インスタンス生成時にT
としてどのような型を与えた場合でもメソッドfoo
が付与されることになります。
これで、なぜimpl
行の中で<T>
を2回書く必要があるのか、お分かり頂けたかと思います。1回はジェネリック型引数を定義するため、もう1回はそれを使ってpatternを構築するためだったという訳です。
結局、何が混乱の元だったのでしょうか?それはおそらく、impl
ではジェネリック型引数の宣言(impl<T>
)とジェネリック型を用いたpatternの構築(Cotainer<T>
)が分離していたのに対し、struct
では型引数宣言とpattern構築を同時に行っている(struct Container<T>
)という違いからでしょう。
ではなぜstruct<T> Container<T>
のような書き方はしないのか?答えは「意味がないから」になるでしょう。struct Container<T>
の<T>
の部分にi32
のような具象型が来る可能性はありません。よって、この部分はその型で使用するジェネリック型引数を、使用する数だけ並べる以外にやりようがなく、すなわちそれはジェネリック型引数を宣言することと同義だからです。ですから、struct Container<T>
と書かれた時点で<T>
がジェネリック型であることは自明であり、わざわざstruct<T> Container<T>
のように別途T
がジェネリック型引数であることを宣言する必要は無い、冗長である、であれば省略してしまえ、という訳です。
(おまけ)ジェネリクスを書く時のルール
impl
の振る舞いに慣れるために、ジェネリック型を扱う時に覚えておくて良いいくつかのルールを紹介しましょう(訳注: 正確に言えばルールではなく、ルールから導かれるいくつかの重要な特徴)。
ルール1:
impl
定義内のmatcherの数は、型定義内のジェネリック型の引数の数と一致しなければならない
例えば下の例はもちろん上手く行きます。
struct Container<T, V> { }
impl Container<i32, i32> { }
struct
の定義には2つのジェネリック型引数T
, V
があるので、implブロックのmatcherの数は絶対に「2」でなければなりません。つまり下の例はダメです。
struct Container<T, V> { }
impl Container<i32> { }
次の例もダメです。
struct Container<T, V> { }
impl Container { }
ルール2
impl
定義内のジェネリック型引数の数は、型定義内のジェネリック型の引数の数と一致しなくても良い
これはうまくいきます(型定義では2つのジェネリック型引数、impl定義では0)。
struct Container<T, V> { }
impl Container<i32, i32> { }
下の例も同じくOKです(型定義では2つのジェネリック型引数、impl定義では1)。
struct Container<T, V> { }
impl<T> Container<T, i32> { }
下の例も当然OKです(型定義では2つのジェネリック型引数、impl定義では2)。
struct Container<T, V> { }
impl<T, V> Container<T, V> { }
ルール3
impl
定義内のジェネリック型引数の名前は、型定義内のジェネリック型の名前と一致しなくても良い
下の例は問題なく動作し、これまでの例と全く同じ振る舞いをします。
struct Container<T, V> { }
impl<A, B> Container<A, B> { }
型同士の関係性に対するマッチング
型定義内に複数のジェネリック型がある場合、それらに対して「具体的な型」をマッチングさせるだけでなく、それらジェネリック型の「関係性」に対してマッチングを行うことも可能です。
struct Container<T, V> { }
impl<X> Container<X, X> { }
上の例では、X
という一つのジェネリック型をimpl<X>
で定義し、そしてContainer<X, X>
とすることによって、ジェネリック型が両方とも同じ型であることを要求しています。どんな型でも良いから、とにかくT
とV
が同じ型である場合のみ、このimpl
ブロックが有効化される、ということです。くどいようですが、これはT
とV
が異なる型である場合にインスタンスを「作れない」という意味ではありません。インスタンスは作れます。ただ、そのインスタンスに対してこのimpl
ブロックは有効ではない、というだけです。