Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Rustハンズオン第3回 基礎文法編
Search
Yuki Toyoda
May 15, 2021
3
900
Rustハンズオン第3回 基礎文法編
2021/03/17 に開催した社内向け Rust ハンズオンの資料です。
Yuki Toyoda
May 15, 2021
Tweet
Share
More Decks by Yuki Toyoda
See All by Yuki Toyoda
SeaQL Projectsについて
helloyuk13
1
460
年末ですし、今年のRustの進捗の話をしましょう
helloyuk13
2
2.8k
SwiftでAWS Lambda
helloyuk13
0
200
Rustハンズオン@エウレカ社
helloyuk13
20
10k
Rust ハンズオン第6回 ベアメタル Rust 編
helloyuk13
0
310
Rust で Web アプリケーションはどこまで開発できるのか
helloyuk13
25
70k
Rustハンズオン第4回 Webバックエンド編
helloyuk13
2
670
Rustハンズオン第5回 WebAssembly編
helloyuk13
6
2.5k
第1回 Rust Hands-On
helloyuk13
8
2.4k
Featured
See All Featured
The Pragmatic Product Professional
lauravandoore
31
6.2k
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
29
2.2k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
363
22k
Code Reviewing Like a Champion
maltzj
518
39k
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
25
640
Principles of Awesome APIs and How to Build Them.
keavy
125
17k
Web Components: a chance to create the future
zenorocha
309
42k
Creatively Recalculating Your Daily Design Routine
revolveconf
216
12k
Thoughts on Productivity
jonyablonski
67
4.2k
Learning to Love Humans: Emotional Interface Design
aarron
271
40k
Fantastic passwords and where to find them - at NoRuKo
philnash
50
2.8k
Adopting Sorbet at Scale
ufuk
73
8.9k
Transcript
Rust ハンズオン第 3 回 中級編 1
⽬次 . cat を実装する( Option や Result を使ってみる) . 所有権、借⽤、ライフタイムのエクササイズ
2
今⽇のゴール Option 型と Result 型の使い⽅を知る。 所有権、借⽤、ライフタイム関連のコンパイルエラーの直し⽅を知る。 3
gist ⻑めのソースコードはコピー&ペーストできるように、gist に貼りました。 エクササイズのときなどにご利⽤ください。 https://gist.github.com/yuk1ty/5a9c686d9ec9031d0c4bd95a00bdf5b6 4
cat を実装する 5
プロジェクトの作成 新しくプロジェクトを作りましょう。 「Hello, World」できることを確認します。 $ cargo new grep-rs $ cd
grep-rs $ cargo run 6
cat プログラムの⼿順 . まず指定したパスのファイルを読み込みます。 . ファイルの中⾝を⽂字列で取得します。 . 成功した場合は、内容をすべて標準出⼒します。 7
指定したパスのファイルを読み込む std::fs::read_to_string という関数を使うと、引数で指定したパスから⽂字列とし て内容を読み込みます。 Ok や Err という⾒慣れない⽂字が出てきましたね。 fn main()
{ let path = "./src/main.rs"; match std::fs::read_to_string(path) { Ok(content) => print!("{}", content), Err(why) => println!("{}", why), } } 8
Result : エラーハンドリングを⾏う Rust では Result 型を⽤いてエラーハンドリングをします。 正常系だった場合は Ok で囲んで返します。
エラーが送出したい場所に対して、 Err で囲んで返します。 fn division(dividened: i32, divisor: i32) -> Result<i32, CalcError> { if divisor == 0 { return Err(CalcError::DividedByZero); } if dividened < 0 { return Err(CalcError::DetectedNegative(dividened)); } if divisor < 0 { return Err(CalcError::DetectedNegative(divisor)); } Ok(dividened / divisor) } 9
Result : エラーハンドリングを⾏う エラー型⾃体も enum で記述することが多いです。 実運⽤では anyhow と thiserror
というクレートを組み合わせて作ります。 enum CalcError { // ゼロ除算を⾏った場合に返すエラー DividedByZero, // 負の数が⼊っていた場合に返すエラー。中にその数値を⼊れる。 DetectedNegative(i32), } 10
Result : エラーハンドリングを⾏う Result は enum なので、match 式で分岐を記述できます。 fn main()
{ let answer = division(4, 2); match answer { Ok(value) => println!("answer is {}", value), Err(err) => match err { // eprintln! マクロはエラー出⼒をできる。 CalcError::DividedByZero => eprintln!(" ゼロ除算です"), CalcError::DetectedNegative(num) => { eprintln!("{} は負の数です。負の数は⼊れられません。", num) } }, } } 11
ファイルを読み込んだ際のエラーハンドリング というわけで、 Ok や Err でエラーハンドリングをしていることがわかります。 たとえばファイルが⾒つからなかった場合には、 Err 側に⼊ってきます。 fn
main() { let path = "./src/main.rs"; match std::fs::read_to_string(path) { Ok(content) => print!("{}", content), Err(why) => println!("{}", why), } } 12
急いでいるときに使える unwrap コードを書いていると、急いでいる場⾯があると思います。 プロダクションではできるかぎりエラーハンドリングをするべきですが、急ぎのときは unwrap という関数を使⽤できます。 あるいは、絶対にエラーにならないはずの場所であえて unwrap を使っておくこともあ ります。
エラーだった場合は、その時点でパニック(プログラムが強制的に異常終了)します。 fn main() { let path = "./src/main.rs"; let content = std::fs::read_to_string(path).unwrap(); print!("{}", content); } 13
ファイルパスは実⾏時の引数から渡せるようにする さきほどの例では、ファイルパスはハードコーディングでした。実⾏時に引数で渡せる ようにすると、柔軟になるでしょう。 std::env::args という関数を使うと、実⾏時引数を取得できます。 nth という関数を実⾏すると、何番⽬の引数を取るかを取得できます。 fn main() {
let mut args = std::env::args(); match args.nth(1) { Some(path) => match std::fs::read_to_string(path) { Ok(content) => print!("{}", content), Err(why) => println!("{}", why), }, None => println!("1 つ⽬の実⾏時引数にファイルパスを⼊れる必要があります。") } } 14
⼊れ⼦はちょっと読みにくいので、関数を出す ネストが発⽣しました。好みの問題ではありますが、ネストは⼀般に読みにくさを増し ます。 ファイルの読み込み処理を関数に切り出しましょう。 ところで、 Some や None という⾒慣れない⽂字が出てきています。 fn
run(path: String) { match std::fs::read_to_string(path) { Ok(content) => print!("{}", content), Err(why) => println!("{}", why), } } fn main() { let mut args = std::env::args(); match args.nth(1) { Some(path) => run(path), None => println!("1 つ⽬の実⾏時引数にファイルパスを⼊れる必要があります。") } } 15
「ない」を⽰す Option null あるいは「ないこと」を⽰すには Option 型を使います。 Option も enum なので、match
式で分岐を記述できます。 fn find(source: Vec<i32>, target: i32) -> Option<i32> { for s in source.into_iter() { if s == target { return Some(s) } } None } fn main() { let vec = vec![1, 2, 3, 4]; match find(vec, 3) { Some(value) => println!("value: {}", value), None => println!("not found!") } } 16
急いでいるときに使える unwrap Result と同様に unwrap 関数が⽤意されています。 None に対して unwrap が⾏われるとパニックします。
fn find(source: Vec<i32>, target: i32) -> Option<i32> { for s in source.into_iter() { if s == target { return Some(s) } } None } fn main() { let vec = vec![1, 2, 3, 4]; let value = find(vec, 3).unwrap(); println!("value: {}", value); } 17
実⾏してみよう cargo run [ 読み込んでみたいパス] で実⾏できます。 fn run(path: String) {
match std::fs::read_to_string(path) { Ok(content) => print!("{}", content), Err(why) => println!("{}", why), } } fn main() { let mut args = std::env::args(); match args.nth(1) { Some(path) => run(path), None => println!("1 つ⽬の実⾏時引数にファイルパスを⼊れる必要があります。") } } 18
なぜ Result ? Either のほうが好きなんだけど? 諦めましょう。Rust は昔 Result 型も Either
型をもっていましたが、エラーハンド リングという⽤途で使われるはずの Either はユーザーにはほぼ使われず、 Result 型のみが残ったという経緯があります。 まとめた: https://zenn.dev/helloyuki/scraps/e5af11fecac719 ちなみに Scala と Rust を⾏き来すると、エラーを⼊れる側を間違えてよく怒られます。 19
所有権、借⽤、ライフタイム 20
所有権 Rust では、値には所有者がかならず⼀⼈います。 関数を呼び出したり、別の変数に値を格納したりすると、値の所有者が移ります。 これをムーブするといいます。 21
値と変数 let s = "this is a value".to_string(); ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
変数 値 22
値の所有者がだんだん移っていく例 ⼀度変数に束縛した値を、別の変数に再代⼊するとまずは起こります。 // 下記は main 関数内に書いているイメージ。 // 変数 `s` に値を紐付けた。
let s: String = "this is a value".to_string(); // 以下の⾏で、`s` の値は `t` に所有権が移る。 let t = s; // `s` はもう使⽤できないので、コンパイルエラー。 println!("{}", s); 23
24
25
26
値の所有者がだんだん移っていく例 関数に値を⼊れても、同様に所有権の移動が起こります。 fn print_something(s: String) { println!("{}", s); } //
s はここで解放される。 // 下記は main 関数に書いてあるイメージ。 // 変数 `s` に値を紐付けた。 let s = "this is a value".to_string(); // 以下の⾏で、`s` の値の所有権は `print_something` 関数に移る。 print_something(s); // `s` はもう使⽤できないので、コンパイルエラー。 println!("{}", s); 27
コピーセマンティクス 先ほどまで紹介したものを「ムーブセマンティクス」といいます。 ⼀⽅で、数値型など軽めのものまでムーブしていては、正直だるいです。 i32 や f64 などのプリミティブ型は Copy トレイトが実装されていて、⾃動でコピ ーを⾏ってくれます。
これをコピーセマンティクスと呼びます。 ⾃分で Copy トレイトを実装すれば、独⾃のデータ型に対してコピーセマンティクスを 適⽤できます。 28
コピーセマンティクスの例 下記はコピーセマンティクスなので、裏で⾃動でコピーが⾛ります。 fn main() { let a: i32 = 1;
let b = a; // この時点で、a は b にコピーされる。 println!("{}", a); // ムーブセマンティクスならコンパイルエラーだが、通る。 } 29
所有権を毎度移していると⼤変なので、貸し出ししよう ⼀度使ってしまうと消えてばかりでは、正直不便ですよね。 借⽤という機能があって、それを利⽤すると所有権を貸し出すことができます。 借⽤は、実質的には参照になっています。 30
先ほどの例を完全に動くようにしてみる あまり旨味を感じられないが、変数に所有権を移していた例のコンパイルを通るように します。 // 下記は main 関数内に書いているイメージ。 // 変数 `s`
に値を紐付けた。 let s = "this is a value".to_string(); // 以下の⾏では、`t` は `s` を借⽤する。 let t = &s; // `s` の所有権はまだなくなっていないので、標準出⼒できる。 println!("{}", s); 31
先ほどの例を完全に動くようにしてみる こちらはよくやる、関数に所有権を移してしまっていた例。 仮引数は s の参照を受け取るようにし、実引数は s の借⽤を渡すようにします。 // `s` は参照を受け取る。
fn print_something(s: &str) { println!("{}", s); } // 下記は main 関数に書いてあるイメージ。 // 変数 `s` に値を紐付けた。 let s = "this is a value".to_string(); // 以下の⾏で、`s` の値を借⽤して渡す。 print_something(&s); // `s` は解放されていないので、標準出⼒できる。 println!("{}", s); 32
エクササイズ 1 下記のコードのコンパイルエラーを通せるようにしてみましょう。 struct User { tag: i32 } impl
User { fn new(num: i32) -> User { User { tag: num } } fn print_tag(self) -> i32 { self.tag } } fn main() { let mut user = User::new(1); assert_eq!(user.print_tag(), 1); user.tag = 2; assert_eq!(user.print_tag(), 2); } 33
解説: エクササイズ 1 main 関数内で束縛している user の所有権が問題になっている。 1 回⽬の assert_eq!
にて、 print_tag メソッドが呼ばれるが、これに所有権が移 る。 次の⾏に⾏くまでに所有権が解放されてしまう。 print_tag メソッドは借⽤を利⽤するように修正する。 34
解答: エクササイズ 2 struct User { tag: i32 } impl
User { fn new(num: i32) -> User { User { tag: num } } fn print_tag(&self) -> i32 { self.tag } } fn main() { let mut user = User::new(1); assert_eq!(user.print_tag(), 1); user.tag = 2; assert_eq!(user.print_tag(), 2); } 35
エクササイズ 2 ちょっと難問。時間がなかったら⾶ばすかも。 下記のコードのコンパイルエラーを通せるようにしてみましょう。 fn main() { let mut list
= vec![]; add_elem(list, 1); add_elem(list, 2); add_elem(list, 3); assert_eq!(list, vec![1, 2, 3]); } fn add_elem(mut target: Vec<i32>, elem: i32) { target.push(elem); } 36
解説: エクササイズ 2 add_elem が問題。呼び出すと list の所有権が add_elem 関数に移る。 2
回⽬以降は呼び出せない。 なので、 add_elem が受け取るリストは &mut にする必要がある。 可変参照を関数の実引数として渡すには、 &mut を先頭につける必要がある。 37
解答: エクササイズ 2 fn main() { let mut list =
vec![]; add_elem(&mut list, 1); add_elem(&mut list, 2); add_elem(&mut list, 3); assert_eq!(list, vec![1, 2, 3]); } fn add_elem(target: &mut Vec<i32>, elem: i32) { target.push(elem); } 38
ライフタイム ⼀度借⽤したものをプログラムが終わるまでいかしておくと、2 重解放などの脆弱性の 温床になってしまいます。 参照と、参照をもつもの(参照をもつ構造体や、参照をもつ enum など)は、ライフタ イムが適⽤されます。 その参照が⽣存できるスコープのようなものです。 初⼼者のうちは、コンパイラが怒るまでは⾃分でがんばらないようにしましょう。
39
ライフタイムはブロック(スコープ)単位で識別される fn main() { let r; // r ----------------------- {
// | // x のスコープはこのブロック内まで。 // | let x = 1; // | x -------------- r = &x; // | | } // x が解放される // | + -------------- // | // * は参照外し。 // | // &x で &i32 型だったが、それを i32 型にしている。 // この時点で x は破棄されている // | // が、x を使おうとしている // | println!("{}", *r); // | <-- ここで使⽤ } // + ------------------------ 40
ライフタイムはブロック(スコープ)単位で識別される fn main() { let r; // r --------------------- {
// | let x = 1; // | x ------------- r = &x; // | | // ブロック内で print するようにしたので、// | | // x が残った状態で使⽤できている。 // | | println!("{}", *r); // | <- 使⽤ + ------------- } // | } // + --------------------- 41
ライフタイム識別⼦ 'a , 'b といったように書かれます。("tick a", "tick b" と読みます) //
関数の場合 fn lifetime_string<'a>() -> &'a str { "lifetime string" } // 構造体の場合 struct LifetimeString<'a> { value: &'a str } 42
書き⽅: 参照を引数として渡したとき 普段は省略されていますが、実は関数を何も省略せずに定義すると下記のようになりま す。 関数に渡した s という仮引数のライフタイムと、返り値の &str のライフタイムが同じ になるということです。
// 下記は fn g(p: i32) { ... } とも書ける fn g<'a>(p: &'a i32) { ... } let x = 10; g(x); 43
書き⽅: 構造体の参照 構造体に参照をもたせることもできる。 その際にはライフタイム識別⼦が必要になる。 struct S<'a> { r: &'a i32
} 44
書き⽅: 構造体の構造体 さらに新しい構造体 T を定義し、 S をもたせたいとします。 その際には T のライフタイム識別⼦と紐付けておく必要があります。
struct T<'a> { s: S<'a> } 45
最初のうちは… 関数では、 実体または参照を仮引数に⼊れ、実体を返すようにしてもよいと思っています。 参照を関数が返してしまうと、ライフタイムの管理が⼀気に⼤変になります。 構造体では、 参照を無理して持たせるのではなく、実体をまずはもたせてみましょう。 慣れてきたら、参照にする必要な箇所を徐々に参照にしていくようにしましょう。e.g. 巨⼤なオブジェクトを持っていて、コピーコストが⼤きい物など。 46
エクササイズ 1 下記コードのコンパイルを通してみましょう。 fn longest<'a>(x: &'a str, y: &'a str)
-> &'a str { if x.len() > y.len() { x } else { y } } fn main() { let string1 = String::from("long string is long"); let result; { let string2 = String::from("xyz"); result = longest(string1.as_str(), string2.as_str()); } println!("The longest string is {}", result); } 47
解説: エクササイズ 1 string1 は {} で囲まれたブロック外まで有効。 string2 は {}
で囲まれたブロック内でのみ有効。 println! した時点では、 string2 のライフタイムが切れてしまっている。 48
解説: エクササイズ 1 fn main() { let string1 = String::from("long
string is long"); let result; { let string2 = String::from("xyz"); result = longest(string1.as_str(), string2.as_str()); } // `string2` のライフタイムがここで切れてしまう。 // ⽚⽅のライフタイムが切れている `result` を使⽤しようとするので、コンパイルエラー // `string2` がダングリングポインタになってしまっている。 println!("The longest string is {}", result); } 49
解説: エクササイズ 1 fn main() { let string1 = String::from("long
string is long"); // string1 -------------- let result; // | { // | let string2 = String::from("xyz"); // | string 2 ---- result = longest(string1.as_str(), string2.as_str()); // | | } // | + ----------- // | println!("The longest string is {}", result); // + -------------------- } 50
解答: エクササイズ 1 fn longest<'a>(x: &'a str, y: &'a str)
-> &'a str { if x.len() > y.len() { x } else { y } } fn main() { let string1 = String::from("long string is long"); let result; { let string2 = String::from("xyz"); result = longest(string1.as_str(), string2.as_str()); println!("The longest string is {}", result); } } 51
解説: エクササイズ 1 fn main() { let string1 = String::from("long
string is long"); // string1 -------------- let result; // | { // | let string2 = String::from("xyz"); // | string 2 ---- result = longest(string1.as_str(), string2.as_str()); // | | println!("The longest string is {}", result); // | | } // | +------------ } // + -------------------- 52
エクササイズ 2 下記コードに適切にライフタイム識別⼦を付与し、コンパイルを通してみましょう。 struct User { id: UserId, user_name: UserName
} impl User { fn new(user_name: &str) -> Self { User { id: UserId(1), user_name: UserName(user_name), } } } struct UserId(i32); struct UserName(&str); fn main() { let user = User::new("namae"); assert_eq!(user.user_name.0, "namae"); } 53
解説: エクササイズ 2 コンパイルエラーに従いながら、順番にライフタイムパラメータを付与していく。 impl まで込みで必要になる。 参照をもつもののみつければよい。 UserId には不要。 54
解答: エクササイズ 2 struct User<'a> { id: UserId, user_name: UserName<'a>
} impl<'a> User<'a> { fn new(user_name: &'a str) -> Self { User { id: UserId(1), user_name: UserName(user_name), } } } struct UserId(i32); struct UserName<'a>(&'a str); fn main() { let user = User::new("namae"); assert_eq!(user.user_name.0, "namae"); } 55
所有権にもっと慣れるために https://aloso.github.io/2021/03/09/creating-an-iterator 56
ライフタイムについての深い話 https://doc.rust-jp.rs/book-ja/ch10-03-lifetime-syntax.html https://github.com/pretzelhammer/rust-blog/blob/master/posts/common-rust- lifetime-misconceptions.md 57
もっとエクササイズしたい Rustlings: https://github.com/rust-lang/rustlings 58