Rustの勉強 10日目
The Rust Programming Language 日本語版 第10章
しばらく日が開いてしまったが、また続きを行う。ジェネリクスとトレイトとライフタイムについて。
10.1 ジェネリックなデータ型
- 多くのジェネリクスを持つ言語のように、型引数にまず
T
を使う。(これは慣例) - ジェネリクスを持つ関数の場合は次のように関数名の直後に型引数を与えて宣言する
fn largest<T>(list: &[T]) -> T { ... }
- 関数内の
T
に対する振る舞い次第でT
が特定のトレイトを持っている必要が有ることを指定する必要がでてくる- 例えば比較であれば
std::cmp::PartialOrd
トレイトを持つ必要がある - トレイトに関しては次節で説明
- 例えば比較であれば
- 構造体の定義では構造体名のあとに型引数を与える
- フィールドごとに異なる型を持たせたかったら、型引数を複数用意してあげる
struct Point<T, U> {
x: T,
y: U,
}
- Enumの定義でも同様
enum Result<T, E> {
Ok(T),
Err(E),
}
- メソッドの定義の場合は
impl
キーワードの直後に型引数を与えることに注意- これでimpl宣言内での
Point<T>
のT
が具体的な型ではなくジェネリックな型であることを伝える
- これでimpl宣言内での
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
- Rustでは多相な型をコンパイル時に単相化することで実行スピードを犠牲にしないようにしている
10.2 トレイト: 共通の振る舞いを定義する
- トレイトは他の言語でインターフェースと呼ばれる機能に似ている
-
trait
キーワードで宣言- メソッドシグネチャを羅列する
- トレイトを実装する際には
impl
キーワードにfor
で指定する
trait Summary {
fn summerize(&self) -> String;
}
impl Summary for NewsArticle {
fn summerize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
- トレイトを公開したい場合には他と同様に
pub
キーワードで行う - 外部のトレイトを外部の型に対して実装することは不可能(これができてしまうと例えば標準ライブラリの型の振る舞いを勝手に変えることができてしまう)
- デフォルトのメソッド定義をしておくと、トレイトの実装を簡便かつ共通にできる
- カスタム実装がある場合にはオーバーライドされる
trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
impl Summary for NewsArticle {} // summarizeはデフォルト実装が採用される
- トレイトを関数の引数として受け取れる
fn notify(item: &impl Summary)
- これはトレイト境界の糖衣構文
- トレイトをジェネリック型の制約として指定できる(トレイト境界)
fn notify<T: Summary>(item: &T)
- 複数の引数がある場合にはトレイト境界をきちんと与えるほうがわかりやすい気がする
- たとえば次の例だと(1)と(2)は同じだけど、
item1
とitem2
が同じ型であることを強制させるためには(3)のようにしないといけない
- たとえば次の例だと(1)と(2)は同じだけど、
fn notify(item1: &impl Summary, item2: &impl Summary) {...} // (1)
fn notify<T: Summary, U: Summary>(item1: &T, item2: &U) {...} // (2)
fn notify<T: Summary>(item1: &T, item2: &T) {...} // (3)
- 複数のトレイトを実装していることを求める場合には
+
で指定するfn notify(item: &(impl Summary + Display))
fn notify<T: Summary + Display>(item: &T)
- しかし実装すべきトレイとが多いときは
where
でトレイト境界を後置したほうが読みやすくなると思う
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
...
}
- ブランケット実装とかいう特定のトレイトを満たす型に対して実装するトレイトが強い
- 次のコードは
Display
トレイトを満たすすべての型T
に対してToString
トレイトを実装している
- 次のコードは
impl<T: Display> ToString for T { ... }
10.3 ライフタイムで参照を検証する
- ライフタイムとは「参照が有効になるスコープ」
- Rustコンパイラには借用チェッカーなる機構があり、すべての借用が有効かを自動で決定する
- ライフタイム注釈の型引数は
'a
のようにアポストロフィーを与える必要がある - 関数内でどのデータにどういった処理を行っているかをよく考えてライフタイム注釈を与える必要がある
- 次の関数は
y
を受け取っても返すわけではないのでライフタイムに影響しない
- 次の関数は
fn foo<'a>(x: &'a str, y: &str) -> &'a str {
x
}
- 構造体定義にライフタイム注釈を入れると、構造体のライフタイムが特定の参照を持つフィールドと同じになることが表現できる。
- 構造体に参照もたせるの、難しすぎない???
- 当然
impl
を書く際にも同じライフタイム注釈が必要になる
// ImportantExcerptのライフタイムはpartフィールドと同じになる
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}
-
ライフタイム省略規則: コンパイラが勝手に適用してライフタイム注釈の省略を可能にしてくれるもの
- 最初の規則は、参照である各引数は、独自のライフタイム引数を得るというものです。(
fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
) - 2番目の規則は、1つだけ入力ライフタイム引数があるなら、そのライフタイムが全ての出力ライフタイム引数に代入されるというものです。(
fn foo<'a>(x: &'a i32) -> &'a i32
) - 3番目の規則は、複数の入力ライフタイム引数があるけれども、メソッドなのでそのうちの一つが
&self
や&mut self
だったら、self
のライフタイムが全出力ライフタイム引数に代入されるというものです。
- 最初の規則は、参照である各引数は、独自のライフタイム引数を得るというものです。(
-
'static
というプログラムの全期間において生存する静的ライフタイムというものが存在する
Rustlings
options1.rs
与えられた時間が [0..24]
のどれかだったら Some
を返して、それ以外だったら None
を返す。条件にある通りの数字を返すだけ。テストも Some
を扱うように変更。
fn maybe_icecream(time_of_day: u16) -> Option<u16> {
// We use the 24-hour system here, so 10PM is a value of 22
// The Option output should gracefully handle cases where time_of_day > 24.
if time_of_day < 22 {
Some(5)
} else if 22 <= time_of_day && time_of_day < 25 {
Some(0)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_icecream() {
assert_eq!(maybe_icecream(9), Some(5));
assert_eq!(maybe_icecream(10), Some(5));
assert_eq!(maybe_icecream(23), Some(0));
assert_eq!(maybe_icecream(22), Some(0));
assert_eq!(maybe_icecream(25), None);
}
#[test]
fn raw_value() {
let icecreams = maybe_icecream(12);
assert_eq!(icecreams, Some(5));
}
}
options2.rs
if let
の書き方を忘れてしまってたのでもう一度ドキュメント見直した。match
のアーム1個だけと思って書けばまあまあ思い出しやすいなと思った。
pop
で戻り値が Option<Option<T>>
になってることを見逃していたので、テストの assert
を変更した。
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple_option() {
let target = "rustlings";
let optional_target = Some(target);
// TODO: Make this an if let statement whose value is "Some" type
if let Some(word) = optional_target {
assert_eq!(word, target);
}
}
#[test]
fn layered_option() {
let mut range = 10;
let mut optional_integers: Vec<Option<i8>> = Vec::new();
for i in 0..(range + 1) {
optional_integers.push(Some(i));
}
// TODO: make this a while let statement - remember that vector.pop also adds another layer of Option<T>
// You can stack `Option<T>`'s into while let and if let
while let Some(integer) = optional_integers.pop() {
assert_eq!(integer, Some(range));
range -= 1;
}
}
}
options3.rs
match
の最初のアームで p
がムーブされちゃってるので、参照しか使わないようにするために Some(ref p)
に書き換えた。
struct Point {
x: i32,
y: i32,
}
fn main() {
let y: Option<Point> = Some(Point { x: 100, y: 200 });
match y {
Some(ref p) => println!("Co-ordinates are {},{} ", &p.x, &p.y),
_ => println!("no match"),
}
y; // Fix without deleting this line.
}
traits1.rs
トレイトで定義されている関数が実装されていなかったので、impl
で実装した。 Self
という大文字始まりのものがタイポではなく、トレイトを実装する構造体を指すことを知った。
trait AppendBar {
fn append_bar(self) -> Self;
}
impl AppendBar for String {
fn append_bar(self) -> String {
String::from(self + "Bar")
}
}
fn main() {
let s = String::from("Foo");
let s = s.append_bar();
println!("s: {}", s);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_foo_bar() {
assert_eq!(String::from("Foo").append_bar(), String::from("FooBar"));
}
#[test]
fn is_bar_bar() {
assert_eq!(
String::from("").append_bar().append_bar(),
String::from("BarBar")
);
}
}
traits2.rs
テストを見て、どうやら "Bar"
という文字列をベクタに追加してるようだったので、push
するコードを書いた
trait AppendBar {
fn append_bar(self) -> Self;
}
impl AppendBar for Vec<String> {
fn append_bar(mut self) -> Vec<String> {
self.push(String::from("Bar"));
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_vec_pop_eq_bar() {
let mut foo = vec![String::from("Foo")].append_bar();
assert_eq!(foo.pop().unwrap(), String::from("Bar"));
assert_eq!(foo.pop().unwrap(), String::from("Foo"));
}
}
traits3.rs
一瞬 version_number
を返すのかと思って実装に困っていたが、よくテストを見たら単純に "Some information"
という String
を返せばいいだけだったのでデフォルト実装を書くだけで終わった。
pub trait Licensed {
fn licensing_info(&self) -> String {
String::from("Some information")
}
}
struct SomeSoftware {
version_number: i32,
}
struct OtherSoftware {
version_number: String,
}
impl Licensed for SomeSoftware {} // Don't edit this line
impl Licensed for OtherSoftware {} // Don't edit this line
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_licensing_info_the_same() {
let licensing_info = String::from("Some information");
let some_software = SomeSoftware { version_number: 1 };
let other_software = OtherSoftware {
version_number: "v2.0.0".to_string(),
};
assert_eq!(some_software.licensing_info(), licensing_info);
assert_eq!(other_software.licensing_info(), licensing_info);
}
}
traits4.rs
最初はトレイト境界を指定せずに直接的に書いていた。つまり次のような感じ。
fn compare_license_types(software: impl Licensed, software_two: impl Licensed) -> bool {
software.licensing_info() == software_two.licensing_info()
}
しかしトレイト境界を書いたほうが、仮引数の型がスッキリすると思って書き換えてみたところ、特に変わらなかった。
pub trait Licensed {
fn licensing_info(&self) -> String {
"some information".to_string()
}
}
struct SomeSoftware {}
struct OtherSoftware {}
impl Licensed for SomeSoftware {}
impl Licensed for OtherSoftware {}
fn compare_license_types<T: Licensed, U: Licensed>(software: T, software_two: U) -> bool {
software.licensing_info() == software_two.licensing_info()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compare_license_information() {
let some_software = SomeSoftware {};
let other_software = OtherSoftware {};
assert!(compare_license_types(some_software, other_software));
}
#[test]
fn compare_license_information_backwards() {
let some_software = SomeSoftware {};
let other_software = OtherSoftware {};
assert!(compare_license_types(other_software, some_software));
}
}
traits5.rs
SomeTrait
と OtherTrait
を両方実装している型を渡す必要があったので、トレイト境界の +
を使った表現で実装した。
pub trait SomeTrait {
fn some_function(&self) -> bool {
true
}
}
pub trait OtherTrait {
fn other_function(&self) -> bool {
true
}
}
struct SomeStruct {}
struct OtherStruct {}
impl SomeTrait for SomeStruct {}
impl OtherTrait for SomeStruct {}
impl SomeTrait for OtherStruct {}
impl OtherTrait for OtherStruct {}
// YOU MAY ONLY CHANGE THE NEXT LINE
fn some_func<T: SomeTrait + OtherTrait>(item: T) -> bool {
item.some_function() && item.other_function()
}
fn main() {
some_func(SomeStruct {});
some_func(OtherStruct {});
}
とりあえず途中だけど、ライフタイムの問題はまた次の日に解く。
Discussion