
ジェネリック型【Rustを分かりたい】 #21
いつかやろうと思っていたジェネリック型についてなんとなく理解できたので、記事にします。テーマは"抽象化"です。
ちなみにIt mediaの記事がすっごい分かりやすかったので、よければ👇。*ログインしないと全文見られませんがそれだけの価値はあると思います。
注意
この記事(今シリーズ)は初心者がRustをかじりながら、備忘録のような形で投稿していく予定です。そのため、今シリーズ全体を通して信憑性は非常に低いです。また専門の方などから見れば、無茶苦茶なこと、おかしなことをしているかもしれませんがご容赦ください。
ジェネリック型とは
ジェネリック型とは抽象化したデータ型です。データ型を抽象化することで、関数などの再利用性を上げることが出来ます。例えば vec![1,2,3,4] 👈のような整数配列の最大値を求める関数において、 vec![1.0,2.0,3.0,4.0]; のような小数配列も一緒に扱えたら便利です。しかし普通に関数を定義すると整数しか扱えません。largest関数の引数は&[i32]でi32配列しか受け付けません。
もし、小数の最大値も求めたい場合は新たに関数を増やすことで対応は出来ます。しかしlargest_float関数は引数と戻り値が違うだけで中身の処理は同じです。これでは重複分が勿体無いですし、似たような関数を書かなければいけなくなります。
fn largest(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn largest_float(list: &[f64]) -> f64 {
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);
}
👆は以下より一部改変。
ここで、引数などに指定する型の制限を緩くします。型の制限を緩くするにはTと置きます。こうすることでTは何らかの型のデータが入るとされます。
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);
}
こうすることで、largest関数はいかなる型のデータも受け付ける用になりました。しかしこれでは関数内での操作に対応してない型が入ってしまう可能性があります。largest関数内では値の大小を比較するために><演算子を使用して、std::cmp::PartialOrdトレイト(トレイトは別記事で。今は"型に実装されている機能"とでも(+とか-のかの算術演算))を使用しています。その操作があらゆる型Tに対して正常に動作するわけではないためです。比較できない型が関数に渡された際にエラーになってしまいます。
Tだけでは型の制限が緩すぎて問題になることが分かりました。例えばsum関数(配列の和を返す)に文字を入れたら当然文字を加算することはできないので、エラーになります。(jsだと結合になるんだっけか…。ただこれはRustです。)これを解決するために受け入れる型に制限をかけます。
ちなみに構造体、enumにも
先ほどは関数にTを指定しましたが構造体などにも指定できます。Point2はTとUを別に用意しています。これはTのみの場合はTは全て同じ型になるためです。もしフィールド内で別の型を使用する場合はTとは別にUも指定します。
struct Point<T> {
x: T,
y: T,
}
struct Point2<T,U> {
x: T,
y: U,
}
enum Res<T, E> {
Ok(T),
Err(E),
}
fn foo() {
let point = Point { x: 1, y: 2 };
// let bad_point = Point {x: 1, y: 2.0};
let bad_point = Point2 {x: 1, y: 2.0};
}
メソッドにも
構造体などにメソッドを実装する場合にもTは使用できます。以下は四角形を表す構造体と高さの参照を返すコードです。この場合計算などの型に依存した操作はしてないのでエラーにはなりません。ちなみにimplの後にも<T>が必要です。コンパイラのためだそうです。詳細はdocへ。
struct Rectangle<T> {
height: T,
width: T,
}
impl<T> Rectangle<T> {
fn height(&self) -> &T {
&self.height
}
}
しかしここで面積を求めるareaメソッドを追加します。しかし👇のコードは実行出来ません。先ほどのlargest関数のようにあらゆる型に掛け算をさせようとしたためです。
struct Rectangle<T> {
height: T,
width: T,
}
impl<T> Rectangle<T> {
fn height(&self) -> &T {
&self.height
}
fn area(&self) -> T {
self.height * self.width
}
}
型に境界を設ける
ここまでずっと正常に動かないコードしかありませんでした。これの原因は型の制限が緩すぎるためです。それなら型を制限すればいいのです。<T: 必要なトレイト>とすることで、型が特定の機能(今回ならops::Mul<Output = T>*1とCopy)を絞って条件をつけることが出来ます。条件は+で追加出来ます。
*1: <Output = T>に関しては掛け算では必要らしい。よく知りません。
struct Rectangle<T> {
height: T,
width: T,
}
impl<T: std::ops::Mul<Output = T> + Copy> Rectangle<T> {
fn height(&self) -> &T {
&self.height
}
fn area(&self) -> T {
self.height * self.width
}
}
トレイトとは…?(別記事で。こっちの記事に統合予定)
一応雑な説明を。トレイトとは各構造体などに共通した機能を事前に宣言しておく感じです。オブジェクト指向のクラスとポリフォーリズムに近い?(OOPちゃんとやったことないのでなんとなくそんな感じがするだけですが)。事前に宣言しておいたほうが機能を各型に実装している感じです。
*詳細は次の記事でやります!すいません。
Rustで事前に定義されているops::AddAssign(加算)トレイトは計算可能な型に対して加算を返すメソッドが実装されているような感じです。(ということは自分のオリジナルな型にもAddAssignを実装すれば文字列結合とかも出来る…?)
ちなみに先ほどのlargest関数にも「比較とCopyを実装した型しか受け付けない」と制限することで実行できるようになります。Copyは所有権とか面倒なんで付けてます。この条件ならスカラ型(i32とか)だけですかね。
fn largest<T: std::cmp::PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
まとめ。というか感想
普通に書いたはずですが、若干翻訳感が出てる気がする…
トレイトに関する詳細は来週、というか次回の予定です。すいません。
あと、よくトレイトがJavaなどのインターフェースに近いという説明をされるのですが、私はまともにプログラミングの勉強をしたのがRustからなので、別言語の仕様を知らずイメージが湧かないなぁとなっていました。