Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rustハンズオン第3回 基礎文法編

68dad178ea4fa6aa86862b3a66a15306?s=47 Yuki Toyoda
May 15, 2021
230

Rustハンズオン第3回 基礎文法編

2021/03/17 に開催した社内向け Rust ハンズオンの資料です。

68dad178ea4fa6aa86862b3a66a15306?s=128

Yuki Toyoda

May 15, 2021
Tweet

Transcript

  1. Rust ハンズオン第 3 回 中級編 1

  2. ⽬次 . cat を実装する( Option や Result を使ってみる) . 所有権、借⽤、ライフタイムのエクササイズ

    2
  3. 今⽇のゴール Option 型と Result 型の使い⽅を知る。 所有権、借⽤、ライフタイム関連のコンパイルエラーの直し⽅を知る。 3

  4. gist ⻑めのソースコードはコピー&ペーストできるように、gist に貼りました。 エクササイズのときなどにご利⽤ください。 https://gist.github.com/yuk1ty/5a9c686d9ec9031d0c4bd95a00bdf5b6 4

  5. cat を実装する 5

  6. プロジェクトの作成 新しくプロジェクトを作りましょう。 「Hello, World」できることを確認します。 $ cargo new grep-rs $ cd

    grep-rs $ cargo run 6
  7. cat プログラムの⼿順 . まず指定したパスのファイルを読み込みます。 . ファイルの中⾝を⽂字列で取得します。 . 成功した場合は、内容をすべて標準出⼒します。 7

  8. 指定したパスのファイルを読み込む 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
  9. 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
  10. Result : エラーハンドリングを⾏う エラー型⾃体も enum で記述することが多いです。 実運⽤では anyhow と thiserror

    というクレートを組み合わせて作ります。 enum CalcError { // ゼロ除算を⾏った場合に返すエラー DividedByZero, // 負の数が⼊っていた場合に返すエラー。中にその数値を⼊れる。 DetectedNegative(i32), } 10
  11. 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
  12. ファイルを読み込んだ際のエラーハンドリング というわけで、 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
  13. 急いでいるときに使える unwrap コードを書いていると、急いでいる場⾯があると思います。 プロダクションではできるかぎりエラーハンドリングをするべきですが、急ぎのときは unwrap という関数を使⽤できます。 あるいは、絶対にエラーにならないはずの場所であえて unwrap を使っておくこともあ ります。

    エラーだった場合は、その時点でパニック(プログラムが強制的に異常終了)します。 fn main() { let path = "./src/main.rs"; let content = std::fs::read_to_string(path).unwrap(); print!("{}", content); } 13
  14. ファイルパスは実⾏時の引数から渡せるようにする さきほどの例では、ファイルパスはハードコーディングでした。実⾏時に引数で渡せる ようにすると、柔軟になるでしょう。 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
  15. ⼊れ⼦はちょっと読みにくいので、関数を出す ネストが発⽣しました。好みの問題ではありますが、ネストは⼀般に読みにくさを増し ます。 ファイルの読み込み処理を関数に切り出しましょう。 ところで、 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
  16. 「ない」を⽰す 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
  17. 急いでいるときに使える 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
  18. 実⾏してみよう 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
  19. なぜ Result ? Either のほうが好きなんだけど? 諦めましょう。Rust は昔 Result 型も Either

    型をもっていましたが、エラーハンド リングという⽤途で使われるはずの Either はユーザーにはほぼ使われず、 Result 型のみが残ったという経緯があります。 まとめた: https://zenn.dev/helloyuki/scraps/e5af11fecac719 ちなみに Scala と Rust を⾏き来すると、エラーを⼊れる側を間違えてよく怒られます。 19
  20. 所有権、借⽤、ライフタイム 20

  21. 所有権 Rust では、値には所有者がかならず⼀⼈います。 関数を呼び出したり、別の変数に値を格納したりすると、値の所有者が移ります。 これをムーブするといいます。 21

  22. 値と変数 let s = "this is a value".to_string(); ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    変数 値 22
  23. 値の所有者がだんだん移っていく例 ⼀度変数に束縛した値を、別の変数に再代⼊するとまずは起こります。 // 下記は main 関数内に書いているイメージ。 // 変数 `s` に値を紐付けた。

    let s: String = "this is a value".to_string(); // 以下の⾏で、`s` の値は `t` に所有権が移る。 let t = s; // `s` はもう使⽤できないので、コンパイルエラー。 println!("{}", s); 23
  24. 24

  25. 25

  26. 26

  27. 値の所有者がだんだん移っていく例 関数に値を⼊れても、同様に所有権の移動が起こります。 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
  28. コピーセマンティクス 先ほどまで紹介したものを「ムーブセマンティクス」といいます。 ⼀⽅で、数値型など軽めのものまでムーブしていては、正直だるいです。 i32 や f64 などのプリミティブ型は Copy トレイトが実装されていて、⾃動でコピ ーを⾏ってくれます。

    これをコピーセマンティクスと呼びます。 ⾃分で Copy トレイトを実装すれば、独⾃のデータ型に対してコピーセマンティクスを 適⽤できます。 28
  29. コピーセマンティクスの例 下記はコピーセマンティクスなので、裏で⾃動でコピーが⾛ります。 fn main() { let a: i32 = 1;

    let b = a; // この時点で、a は b にコピーされる。 println!("{}", a); // ムーブセマンティクスならコンパイルエラーだが、通る。 } 29
  30. 所有権を毎度移していると⼤変なので、貸し出ししよう ⼀度使ってしまうと消えてばかりでは、正直不便ですよね。 借⽤という機能があって、それを利⽤すると所有権を貸し出すことができます。 借⽤は、実質的には参照になっています。 30

  31. 先ほどの例を完全に動くようにしてみる あまり旨味を感じられないが、変数に所有権を移していた例のコンパイルを通るように します。 // 下記は main 関数内に書いているイメージ。 // 変数 `s`

    に値を紐付けた。 let s = "this is a value".to_string(); // 以下の⾏では、`t` は `s` を借⽤する。 let t = &s; // `s` の所有権はまだなくなっていないので、標準出⼒できる。 println!("{}", s); 31
  32. 先ほどの例を完全に動くようにしてみる こちらはよくやる、関数に所有権を移してしまっていた例。 仮引数は 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
  33. エクササイズ 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
  34. 解説: エクササイズ 1 main 関数内で束縛している user の所有権が問題になっている。 1 回⽬の assert_eq!

    にて、 print_tag メソッドが呼ばれるが、これに所有権が移 る。 次の⾏に⾏くまでに所有権が解放されてしまう。 print_tag メソッドは借⽤を利⽤するように修正する。 34
  35. 解答: エクササイズ 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
  36. エクササイズ 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
  37. 解説: エクササイズ 2 add_elem が問題。呼び出すと list の所有権が add_elem 関数に移る。 2

    回⽬以降は呼び出せない。 なので、 add_elem が受け取るリストは &mut にする必要がある。 可変参照を関数の実引数として渡すには、 &mut を先頭につける必要がある。 37
  38. 解答: エクササイズ 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
  39. ライフタイム ⼀度借⽤したものをプログラムが終わるまでいかしておくと、2 重解放などの脆弱性の 温床になってしまいます。 参照と、参照をもつもの(参照をもつ構造体や、参照をもつ enum など)は、ライフタ イムが適⽤されます。 その参照が⽣存できるスコープのようなものです。 初⼼者のうちは、コンパイラが怒るまでは⾃分でがんばらないようにしましょう。

    39
  40. ライフタイムはブロック(スコープ)単位で識別される fn main() { let r; // r ----------------------- {

    // | // x のスコープはこのブロック内まで。 // | let x = 1; // | x -------------- r = &x; // | | } // x が解放される // | + -------------- // | // * は参照外し。 // | // &x で &i32 型だったが、それを i32 型にしている。 // この時点で x は破棄されている // | // が、x を使おうとしている // | println!("{}", *r); // | <-- ここで使⽤ } // + ------------------------ 40
  41. ライフタイムはブロック(スコープ)単位で識別される fn main() { let r; // r --------------------- {

    // | let x = 1; // | x ------------- r = &x; // | | // ブロック内で print するようにしたので、// | | // x が残った状態で使⽤できている。 // | | println!("{}", *r); // | <- 使⽤ + ------------- } // | } // + --------------------- 41
  42. ライフタイム識別⼦ 'a , 'b といったように書かれます。("tick a", "tick b" と読みます) //

    関数の場合 fn lifetime_string<'a>() -> &'a str { "lifetime string" } // 構造体の場合 struct LifetimeString<'a> { value: &'a str } 42
  43. 書き⽅: 参照を引数として渡したとき 普段は省略されていますが、実は関数を何も省略せずに定義すると下記のようになりま す。 関数に渡した s という仮引数のライフタイムと、返り値の &str のライフタイムが同じ になるということです。

    // 下記は fn g(p: i32) { ... } とも書ける fn g<'a>(p: &'a i32) { ... } let x = 10; g(x); 43
  44. 書き⽅: 構造体の参照 構造体に参照をもたせることもできる。 その際にはライフタイム識別⼦が必要になる。 struct S<'a> { r: &'a i32

    } 44
  45. 書き⽅: 構造体の構造体 さらに新しい構造体 T を定義し、 S をもたせたいとします。 その際には T のライフタイム識別⼦と紐付けておく必要があります。

    struct T<'a> { s: S<'a> } 45
  46. 最初のうちは… 関数では、 実体または参照を仮引数に⼊れ、実体を返すようにしてもよいと思っています。 参照を関数が返してしまうと、ライフタイムの管理が⼀気に⼤変になります。 構造体では、 参照を無理して持たせるのではなく、実体をまずはもたせてみましょう。 慣れてきたら、参照にする必要な箇所を徐々に参照にしていくようにしましょう。e.g. 巨⼤なオブジェクトを持っていて、コピーコストが⼤きい物など。 46

  47. エクササイズ 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
  48. 解説: エクササイズ 1 string1 は {} で囲まれたブロック外まで有効。 string2 は {}

    で囲まれたブロック内でのみ有効。 println! した時点では、 string2 のライフタイムが切れてしまっている。 48
  49. 解説: エクササイズ 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
  50. 解説: エクササイズ 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
  51. 解答: エクササイズ 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
  52. 解説: エクササイズ 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
  53. エクササイズ 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
  54. 解説: エクササイズ 2 コンパイルエラーに従いながら、順番にライフタイムパラメータを付与していく。 impl まで込みで必要になる。 参照をもつもののみつければよい。 UserId には不要。 54

  55. 解答: エクササイズ 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
  56. 所有権にもっと慣れるために https://aloso.github.io/2021/03/09/creating-an-iterator 56

  57. ライフタイムについての深い話 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

  58. もっとエクササイズしたい Rustlings: https://github.com/rust-lang/rustlings 58