ジェネリックなデータ型
関数シグニチャや構造体などの要素の定義を生成するのにジェネリクスを使用することができ、 それはさらに他の多くの具体的なデータ型と使用することもできます。まずは、 ジェネリクスで関数、構造体、enum、メソッドを定義する方法を見ましょう。それから、 ジェネリクスがコードのパフォーマンスに与える影響を議論します。
関数定義では
ジェネリクスを使用する関数を定義する時、通常、引数や戻り値のデータ型を指定する関数のシグニチャにジェネリクスを配置します。 そうすることでコードがより柔軟になり、コードの重複を阻止しつつ、関数の呼び出し元により多くの機能を提供します。
largest
関数を続けます。リスト10-4はどちらもスライスから最大値を探す2つの関数を示しています。
ファイル名: src/main.rs
fn largest_i32(list: &[i32]) -> i32 { let mut largest = list[0]; for &item in list.iter() { if item > largest { largest = item; } } largest } fn largest_char(list: &[char]) -> char { let mut largest = list[0]; for &item in list.iter() { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest_i32(&number_list); println!("The largest number is {}", result); # assert_eq!(result, 100); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest_char(&char_list); println!("The largest char is {}", result); # assert_eq!(result, 'y'); }
largest_i32
関数は、リスト10-3で抽出したスライスから最大のi32
を探す関数です。
largest_char
関数は、スライスから最大のchar
を探します。関数本体には同じコードがあるので、
単独の関数にジェネリックな型引数を導入してこの重複を排除しましょう。
これから定義する新しい関数の型を引数にするには、ちょうど関数の値引数のように型引数に名前をつける必要があります。
型引数の名前にはどんな識別子も使用できますが、T
を使用します。というのも、慣習では、
Rustの引数名は短く(しばしばたった1文字になります)、Rustの型の命名規則がキャメルケースだからです。
"type"の省略形なので、T
が多くのRustプログラマの既定の選択なのです。
関数の本体で引数を使用するとき、コンパイラがその名前の意味を把握できるようにシグニチャでその引数名を宣言しなければなりません。
同様に、型引数名を関数シグニチャで使用する際には、使用する前に型引数名を宣言しなければなりません。
ジェネリックなlargest
関数を定義するために、型名宣言を山カッコ(<>
)内、関数名と引数リストの間に配置してください。
こんな感じに:
fn largest<T>(list: &[T]) -> T {
この定義は以下のように解読します: 関数largest
は、なんらかの型T
に関してジェネリックであると。
この関数にはlist
という引数が1つあり、これは型T
の値のスライスです。
largest
関数は同じT
型の値を返します。
リスト10-5は、シグニチャにジェネリックなデータ型を使用してlargest
関数定義を組み合わせたものを示しています。
このリストはさらに、この関数をi32
値かchar
値のどちらかで呼べる方法も表示しています。
このコードはまだコンパイルできないことに注意してください。ですが、この章の後ほど修正します。
ファイル名: src/main.rs
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}
直ちにこのコードをコンパイルしたら、以下のようなエラーが出ます:
error[E0369]: binary operation `>` cannot be applied to type `T`
(エラー: 2項演算`>`は、型`T`に適用できません)
--> src/main.rs:5:12
|
5 | if item > largest {
| ^^^^^^^^^^^^^^
|
= note: an implementation of `std::cmp::PartialOrd` might be missing for `T`
(注釈: `std::cmp::PartialOrd`の実装が`T`に対して存在しない可能性があります)
注釈がstd::cmp::PartialOrd
に触れています。これは、トレイトです。トレイトについては、次の節で語ります。
とりあえず、このエラーは、largest
の本体は、T
がなりうる全ての可能性のある型に対して動作しないと述べています。
本体で型T
の値を比較したいので、値が順序付け可能な型のみしか使用できないのです。比較を可能にするために、
標準ライブラリには型に実装できるstd::cmp::PartialOrd
トレイトがあります(このトレイトについて詳しくは付録Cを参照されたし)。
ジェネリックな型が特定のトレイトを持つと指定する方法は「トレイト境界」節で習うでしょうが、
先にジェネリックな型引数を使用する他の方法を探究しましょう。
構造体定義では
構造体を定義して<>
記法で1つ以上のフィールドにジェネリックな型引数を使用することもできます。
リスト10-6は、Point<T>
構造体を定義してあらゆる型のx
とy
座標を保持する方法を示しています。
ファイル名: src/main.rs
struct Point<T> { x: T, y: T, } fn main() { let integer = Point { x: 5, y: 10 }; let float = Point { x: 1.0, y: 4.0 }; }
構造体定義でジェネリクスを使用する記法は、関数定義のものと似ています。まず、山カッコ内に型引数の名前を構造体名の直後に宣言します。 そうすると、本来具体的なデータ型を記述する構造体定義の箇所に、ジェネリックな型を使用できます。
ジェネリックな型を1つだけ使用してPoint<T>
を定義したので、この定義は、Point<T>
構造体がなんらかの型T
に関して、
ジェネリックであると述べていて、その型がなんであれ、x
とy
のフィールドは両方その同じ型になっていることに注意してください。
リスト10-7のように、異なる型の値のあるPoint<T>
のインスタンスを生成すれば、コードはコンパイルできません。
ファイル名: src/main.rs
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
この例で、x
に整数値5を代入すると、このPoint<T>
のインスタンスに対するジェネリックな型T
は整数になるとコンパイラに知らせます。
それからy
に4.0を指定する時に、このフィールドはx
と同じ型と定義したはずなので、このように型不一致エラーが出ます:
error[E0308]: mismatched types
--> src/main.rs:7:38
|
7 | let wont_work = Point { x: 5, y: 4.0 };
| ^^^ expected integral variable, found
floating-point variable
|
= note: expected type `{integer}`
found type `{float}`
x
とy
が両方ジェネリックだけれども、異なる型になり得るPoint
構造体を定義するには、
複数のジェネリックな型引数を使用できます。例えば、リスト10-8では、Point
の定義を変更して、
型T
とU
に関してジェネリックにし、x
が型T
で、y
が型U
になります。
ファイル名: src/main.rs
struct Point<T, U> { x: T, y: U, } fn main() { let both_integer = Point { x: 5, y: 10 }; let both_float = Point { x: 1.0, y: 4.0 }; let integer_and_float = Point { x: 5, y: 4.0 }; }
これで、示されたPoint
インスタンスは全部使用可能です!所望の数だけ定義でジェネリックな型引数を使用できますが、
数個以上使用すると、コードが読みづらくなります。コードで多くのジェネリックな型が必要な時は、
コードの小分けが必要なサインかもしれません。
enum定義では
構造体のように、列挙子にジェネリックなデータ型を保持するenumを定義することができます。
標準ライブラリが提供しているOption<T>
enumをもう一度見ましょう。このenumは第6章で使用しました:
# #![allow(unused_variables)] #fn main() { enum Option<T> { Some(T), None, } #}
この定義はもう、あなたにとってより道理が通っているはずです。ご覧の通り、Option<T>
は、
型T
に関してジェネリックで2つの列挙子のあるenumです: その列挙子は、型T
の値を保持するSome
と、
値を何も保持しないNone
です。Option<T>
enumを使用することで、オプショナルな値があるという抽象的な概念を表現でき、
Option<T>
はジェネリックなので、オプショナルな値の型に関わらず、この抽象を使用できます。
enumも複数のジェネリックな型を使用できます。第9章で使用したResult
enumの定義が一例です:
# #![allow(unused_variables)] #fn main() { enum Result<T, E> { Ok(T), Err(E), } #}
Result
enumは2つの型T
、E
に関してジェネリックで、2つの列挙子があります: 型T
の値を保持するOk
と、
型E
の値を保持するErr
です。この定義により、Result
enumを、成功する(なんらかの型T
の値を返す)か、
失敗する(なんらかの型E
のエラーを返す)可能性のある処理がある、あらゆる箇所に使用するのが便利になります。
事実、ファイルを開くのに成功した時にT
に型std::fs::File
が入り、ファイルを開く際に問題があった時にE
に型std::io::Error
が入ったものが、
リスト9-3でファイルを開くのに使用したものです。
自分のコード内で、保持している値の型のみが異なる構造体やenum定義の場面を認識したら、 代わりにジェネリックな型を使用することで重複を避けることができます。
メソッド定義では
(第5章のように、)定義にジェネリックな型を使うメソッドを構造体やenumに実装することもできます。リスト10-9は、
リスト10-6で定義したPoint<T>
構造体にx
というメソッドを実装したものを示しています。
ファイル名: src/main.rs
struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn x(&self) -> &T { &self.x } } fn main() { let p = Point { x: 5, y: 10 }; println!("p.x = {}", p.x()); }
ここで、フィールドx
のデータへの参照を返すx
というメソッドをPoint<T>
に定義しました。
impl
の直後にT
を宣言しなければならないことに注意してください。こうすることで、型Point<T>
にメソッドを実装していることを指定するために、T
を使用することができます。
impl
の後にT
をジェネリックな型として宣言することで、コンパイラは、Point
の山カッコ内の型が、
具体的な型ではなくジェネリックな型であることを認識できるのです。
例えば、ジェネリックな型を持つPoint<T>
インスタンスではなく、Point<f32>
だけにメソッドを実装することもできるでしょう。
リスト10-10では、具体的な型f32
を使用しています。つまり、impl
の後に型を宣言しません。
# #![allow(unused_variables)] #fn main() { # struct Point<T> { # x: T, # y: T, # } # impl Point<f32> { fn distance_from_origin(&self) -> f32 { (self.x.powi(2) + self.y.powi(2)).sqrt() } } #}
このコードは、Point<f32>
にはdistance_from_origin
というメソッドが存在するが、
T
がf32
ではないPoint<T>
の他のインスタンスにはこのメソッドが定義されないことを意味します。
このメソッドは、この点が座標(0.0, 0.0)の点からどれだけ離れているかを測定し、
浮動小数点数にのみ利用可能な数学的処理を使用します。
構造体定義のジェネリックな型引数は、必ずしもその構造体のメソッドシグニチャで使用するものと同じにはなりません。
例を挙げれば、リスト10-11は、リスト10-8のPoint<T, U>
にメソッドmixup
を定義しています。
このメソッドは、他のPoint
を引数として取り、この引数はmixup
を呼び出しているself
のPoint
とは異なる型の可能性があります。
このメソッドは、(型T
の)self
のPoint
のx
値と渡した(型W
の)Point
のy
値から新しいPoint
インスタンスを生成します。
ファイル名: src/main.rs
struct Point<T, U> { x: T, y: U, } impl<T, U> Point<T, U> { fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> { Point { x: self.x, y: other.y, } } } fn main() { let p1 = Point { x: 5, y: 10.4 }; let p2 = Point { x: "Hello", y: 'c'}; let p3 = p1.mixup(p2); println!("p3.x = {}, p3.y = {}", p3.x, p3.y); }
main
で、x
(値は5
)にi32
、y
(値は10.4
)にf64
を持つPoint
を定義しました。p2
変数は、
x
(値は"Hello"
)に文字列スライス、y
(値はc
)にchar
を持つPoint
構造体です。
引数p2
でp1
にmixup
を呼び出すと、p3
が得られ、x
はi32
になります。x
はp1
由来だからです。
p3
変数のy
は、char
になります。y
はp2
由来だからです。println!
マクロの呼び出しは、
p3.x = 5, p3.y = c
と出力するでしょう。
この例の目的は、一部のジェネリックな引数はimpl
で宣言され、他の一部はメソッド定義で宣言される場面をデモすることです。
ここで、ジェネリックな引数T
とU
はimpl
の後に宣言されています。構造体定義にはまるからです。
ジェネリックな引数V
とW
はfn mixup
の後に宣言されています。何故なら、このメソッドにしか関係ないからです。
ジェネリクスを使用したコードのパフォーマンス
ジェネリックな型引数を使用すると、実行時にコストが発生するのかな、と思うかもしれません。 嬉しいことにRustでは、ジェネリクスを、具体的な型があるコードよりもジェネリックな型を使用したコードを実行するのが遅くならないように実装しています。
コンパイラはこれを、ジェネリクスを使用しているコードの単相化をコンパイル時に行うことで達成しています。 単相化(monomorphization)は、コンパイル時に使用されている具体的な型を入れることで、 ジェネリックなコードを特定のコードに変換する過程のことです。
この過程において、コンパイラは、リスト10-5でジェネリックな関数を生成するために使用した手順と真逆のことをしています: コンパイラは、ジェネリックなコードが呼び出されている箇所全部を見て、 ジェネリックなコードが呼び出されている具体的な型のコードを生成するのです。
標準ライブラリのOption<T>
enumを使用する例でこれが動作する方法を見ましょう:
# #![allow(unused_variables)] #fn main() { let integer = Some(5); let float = Some(5.0); #}
コンパイラがこのコードをコンパイルすると、単相化を行います。その過程で、コンパイラはOption<T>
のインスタンスに使用された値を読み取り、
2種類のOption<T>
を識別します: 一方はi32
で、もう片方はf64
です。そのように、
コンパイラは、Option<T>
のジェネリックな定義をOption_i32
とOption_f64
に展開し、
それにより、ジェネリックな定義を特定の定義と置き換えます。
単相化されたバージョンのコードは、以下のようになります。ジェネリックなOption<T>
が、
コンパイラが生成した特定の定義に置き換えられています:
ファイル名: src/main.rs
enum Option_i32 { Some(i32), None, } enum Option_f64 { Some(f64), None, } fn main() { let integer = Option_i32::Some(5); let float = Option_f64::Some(5.0); }
Rustでは、ジェネリックなコードを各インスタンスで型を指定したコードにコンパイルするので、 ジェネリクスを使用することに対して実行時コストを払うことはありません。コードを実行すると、 それぞれの定義を手作業で複製した時のように振る舞います。単相化の過程により、 Rustのジェネリクスは実行時に究極的に効率的になるのです。