https://doc.rust-lang.org/book/second-edition/
Apache License Version 2.0
Generic Types, Traits, and Lifetimes
同じロジックでも,扱う変数の型が違えばこれまで学んだ関数の定義方法では,型ごとに似たような関数をいくつも書かなければならない.このようなロジックの重複を解消するのがgenericで,変数の型に関係なく関数やstructを定義できる.
genericはすでにChap 6でOption<T>
を, Chap. 8でVec<T>
やHashMap<K, V>
を,Chap. 9でResult<T, E>
を使った.これを一般化した用法をこの章では学ぶ.
まず,単純に同じロジックだが引数や返り値の型が違う二つの関数を見て,それをgenericによって一本化する.次に単純に一本化できない場合の解決法を学び,最後にlifetimeという,reference同士の関係性を記述するgenericの一種を導入し,リ方法を学ぶ.
Generic Data Types
Using Generic Data Types in Function Definitions
src/main.rs listing 10-4a
fn largest(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
という関数を考える.これはi32のリストのreferenceを取ってその最大値(i32)を返す関数で,['y', 'm', 'a', 'q']
と言うようなリストを引数に取ることはできないので,型だけを(list: &[char]) -> char
としたような関数を更に定義しなければならない.これは明らかに無駄なので,generic
によって一本化することを考える.
引数や返り値をgeneric型にするには,そのgeneric型の変数の型名と変数自体の名を関数宣言の際に定義し,関数の内部でその変数名を使ってロジックを書く.変数の型名にはほとんど必ずT
を使う.具体的な記法は
fn largest<T>(list: &[T]) -> T {
のようになる.改めてlargestを書き直したのがlisting 10-5aである.しかしこれはコンパイルできない.
src/main.rs listing 10-5a
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
shell
error[E0369]: binary operation `>` cannot be applied to type `T`
|
5 | if item > largest {
| ^^^^
|
note: an implementation of `std::cmp::PartialOrd` might be missing for `T`
とエラーが出る.これは任意の型T
に不等号の演算が定義されていないことが原因で,T
の取りうる値をプログラマが制限しなければならない.そのため,標準ライブラリのもつtrait std::cmp::PartialOrd
を使うようにする(後述).
Using Generic Data Types in Struct Definitions
structの定義にもgenericを使える.
src/main.rs listing 10-6a
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point {x: 5, y: 10};
let float = Point {x: 1.0, y: .40};
}
listing 10-6では,generic型の型名をT
しか決めておらず,x, yは1つのinstanceでは必ず同じT
の型しか持てない.x, yがそれぞれ異なる型の値を持てるようにするには,generic型の名前を予め二つ用意する.
src/main.rs listing 10-8
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};
}
は正常にコンパイルできる.generic型の型名はいくつあってもいいが,コードを読んで把握しづらくなるほど多いようならロジック自体を考え直すべき.
Using Generic Data Types in Enum Definitions
structと同様に,enumもgeneric型をそのvariantsに持てる.Option<T>
がこれを行っているのはすでに見た.Rustは変数を取らないときにはgeneric型をvariantに入れないことを許しているから
enum Option<T> {
Some(T),
None,
}
というふうにOption<T>
を定義できる.また,あるvariantに入る型と別のvariantに入る型が異なっている場合にも
enum Result<T, E> {
Ok(T),
Err(E),
}
と記述できる.
Using Generic Data Types in Method Definitions
Chap.5 でやったように,struct やenumにmethodを定義できるが,これにもgenericが使える.例えば
src/main.rs listing 10-9
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());
}
また例えば
src/main.rs listing10-10
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: self.y,
}
}
}
fn main () {
let p1 = Point {x: 5, y: 10.4} ;
let p2 = Point {x: "Hello", y: 'C'};
let p3 = p1.pixup(p2);
}
は正常に実行できる.
Performance of Code using Generics
Rustはコンパイル時にgenericを具体的な型のコードたちに変換するので,genericが実行時のオーバーヘッドになることはない.Rustのこの働きをmonomorphizationという.
たとえば
let integer = Some(5);
let float = Some(5.0);
をコンパイルするとき
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);
}
のように内部で変換する.
Traits: Defining Shared Behavior
traitは型の振る舞いを抽象化する.つまり複数の型に同時にメソッドを定義できる.また関数の引数などにgeneric型を使っているときtraitによってその引数が取れる型の範囲を制限して,Using Generic Data Types in Function Definitionsでみたエラーに対処することができる.
Defining a Trait
型のふるまいは型に実装されているメソッドたちによって決まる.異なった型たちが同じ名前のメソッドを持っているとき,その型たちは振る舞いを共有していると考えることが出来る.traitによって複数の型に同じ名前のメソッドを同時に定義できる.例えばNewsArticle
型とTweet
型を考える.両者ともに,そのインスタンスの要約を返すメソッドsummary
を,summarizable
traitによって持たせる.traitはmoduleのように定義するが,body blockにはsignatureだけ書く.
src/lib.rs listing 10-11
pub trait Summarizable {
fn summary(&self) -> String; // method signatureのみ書く
fn author(&self) -> String; // 複数のメソッドも書ける.
fn content(&self) -> String; // 1行に一つのmethod signatureを書き,セミコロンを打つ.
}
Implementing a Trait on a Type
Summarizable
traitを定義したところで,型にtraitを実装する.通常のメソッド定義は
impl NewsArticle { fn summary...}
と書くが,traitを実装するときは
impl Summarizable for Newsarticle { fn summary signature { }}
と書く.fn summary signature { }
の中に実際のロジックをコーディングする.
具体的な例:
lib.rs listing 10-12
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summarizable for NewsArticle {
fn summary(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summarizable for Tweet {
fn summary(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
こうしてSummarizable
がNewsArticle
とTweet
に実装できた.それぞれの型のinstanceにドット記法でSummarizable
の中のメソッドを呼べる.
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summary());
は1 new tweet: horse_ebooks: of course, as you probably already know, people.
を出力する.
これまではすべてをlib.rsに書いてきた.これらをaggregatorというcrateにして,他の場所にあるWeatherForecast
structにSummarizable
traitを実装したい場合,Summarizable
をまずインポートする.
lib.rs listing 10-13 例
extern crate aggregator;
use aggregatro::Summarizable;
struct WeatherForecast {
high_temp: f64,
low_temp: f64,
chance_of_precipitation: f64
}
impl Summarizable for WeatherForecast {
fn summary(&self) -> String {
format!("The high will be {}, and the low will be {}. The chance of precipitation is{}%", self. high_temp, self.low_temp, self.chance_of_precipitation)
}
}
traitとtypeがともにexternalであるとき,そのtypeにtraitを新たに実装することはできない.例えば,Vec
はexternal traitでDisplay
はexternal traitだから,Vec
にDisplay
を実装することはできない.こうしたルールをOrphan ruleという.
Default Implementations
traitを定義するとき,予めロジックを決めておいて,改めて型にtraitのメソッドを実装しない限りそのデフォルトのロジックをその型のメソッドとすることが出来る.Default implementationという.そのためには,listing 10-11ではセミコロンで止めていおいたメソッドのsignatureを,実際のロジックまで書くようにし,
lib.rs listing 10-14
pub trait Summarizable {
fn summary(&self) -> String {
String::from("(Read more...)");
}
}
さらに型への実装で{ }
を空白にする. impl Summarizable for NewsArticle {}
仮にここでtraitの定義とは別のロジックを書いたら,新しいロジックが優先される.
default implementationはそのtraitの他のメソッドを,デフォルトが定義されていなくても,呼ぶことが出来る.例えば
pub trait Summarizable {
fn author_summary(&self) -> String;
fn summary(&self) -> String {
format!("(Read more from {}...)", self.author_summary())
}
}
このSummarizable
を使うときは,author_summary
を型に実装する.
impl Summarizable for Tweet {
fn author_summary(&self) -> String {
format!("@{}", self.username)
}
}
Trait Bounds
traitをgeneric type parameterと使うことも出来る.generic typeは野放図に使うとUsing Generic Data Types in Function Definitionsのようなエラーを生じることがあるので,そのgeneric typeを取れる型が特定のtraitを実装されている型であると制限して,その制限下のどの型でも動くとコンパイラが判断すれば,コンパイルしてくれる.こうしてgeneric typeの型を制限することを,”generic typeにtrait boundsを指定する”という
例えばlisting 10-12でsummarizable
をNewsArticle
とTweet
に実装したので,NewsArticle
とTweet
を引数に取るnotify
という関数をgenericを使って定義する.generic type parameter T
をSummarizable
traitが実装されている型に制限するには,定義時に<T: Summarizable>
とすれば良い.例えば
pub fn notify<T: Summarizable>(item: T) {
println!("Breaking news! {}", item.summary())
}
また,Summarizable
とDisplay
を同時に実装している型にgeneric typeを制限したいときには<T: Summarizable + Display
とする.
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {
というふうに複数の引数にそれぞれのtrait boundsを設定することが可能だが,読みづらいのでwhere
キーワードを使って
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
というふうに定義することも可能である.
Fixing the largest
Function with Trait Bounds
Using Generic Data Types in Function Definitionsで見たエラーを実際に修正しよう.
error[E0369]: binary operation `>` cannot be applied to type `T`
|
5 | if item > largest {
| ^^^^
|
note: an implementation of `std::cmp::PartialOrd` might be missing for `T`
不等号演算子が定義されている型がT
に来るかもしれないというのでエラーメッセージが出たので,不等号を定義する標準ライブラリのtrait, std::cmp::PartialOrd
をtrait boundとしてみる.
fn largest<T: PartialOrd>(list: &[T]) -> T {
しかし,これでもエラーが出る.
error[E0508]: cannot move out of type `[T]`, a non-copy array
--> src/main.rs:4:23
|
4 | let mut largest = list[0];
| ----------- ^^^^^^^ cannot move out of here
| |
| hint: to prevent move, use `ref largest` or `ref mut largest`
error[E0507]: cannot move out of borrowed content
--> src/main.rs:6:9
|
6 | for &item in list.iter() {
| ^----
| ||
| |hint: to prevent move, use `ref item` or `ref mut item`
| cannot move out of borrowed content
cannot move out of type [T], a non-copy array.
に着目する.T
がCopy
traitを実装していないため,largest = list[0]
が実行できなかったことを示している.よってtrait boundにCopy
を加えることで,コンパイルが可能になる.
src/main.rs listing 10-15
use std::cmp::PartialOrd;
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
let result = largest(&numbers);
println!("The largest number is {}", result);
let chars = vec!['y', 'm', 'a', 'q'];
let result = largest(&chars);
println!("The largest char is {}", result);
}
Copy
をtrait boundに加えたくない場合,かわりにClone
をtrait boundsに加えても良いが,Clone
はheap構造を使うので,性能が落ちる可能性がある.
0 件のコメント:
コメントを投稿