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
950
Rustハンズオン第3回 基礎文法編
2021/03/17 に開催した社内向け Rust ハンズオンの資料です。
Yuki Toyoda
May 15, 2021
Tweet
Share
More Decks by Yuki Toyoda
See All by Yuki Toyoda
RustでWeb開発コソコソ噂話
helloyuk13
12
14k
SeaQL Projectsについて
helloyuk13
1
520
年末ですし、今年のRustの進捗の話をしましょう
helloyuk13
2
2.9k
SwiftでAWS Lambda
helloyuk13
0
220
Rustハンズオン@エウレカ社
helloyuk13
21
11k
Rust ハンズオン第6回 ベアメタル Rust 編
helloyuk13
0
340
Rust で Web アプリケーションはどこまで開発できるのか
helloyuk13
25
72k
Rustハンズオン第4回 Webバックエンド編
helloyuk13
2
700
Rustハンズオン第5回 WebAssembly編
helloyuk13
6
2.5k
Featured
See All Featured
How to train your dragon (web standard)
notwaldorf
88
5.7k
YesSQL, Process and Tooling at Scale
rocio
169
14k
Being A Developer After 40
akosma
87
590k
Mobile First: as difficult as doing things right
swwweet
222
9k
Docker and Python
trallard
42
3.1k
Building a Modern Day E-commerce SEO Strategy
aleyda
38
7k
Adopting Sorbet at Scale
ufuk
73
9.1k
The Pragmatic Product Professional
lauravandoore
32
6.3k
Intergalactic Javascript Robots from Outer Space
tanoku
270
27k
Designing Experiences People Love
moore
138
23k
Reflections from 52 weeks, 52 projects
jeffersonlam
347
20k
Writing Fast Ruby
sferik
628
61k
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