Closed16

読む「The Rust Programming Language」

ホリケンシュウホリケンシュウ

4章 所有権を理解する

結局Rust書いてて所有権まで理解してコード書けてないので、ここでもう一回整理しなおしたい。

4.1 所有権とは?

スタックとヒープ

メモリのスタック領域かヒープ領域か。
プログラム実行中に、動的にメモリ領域の割り当てや開放などが発生するのがヒープ領域。

スタック領域は、コンパイル時に確保サイズが判明する。
ヒープ領域は、コンパイル時に確保サイズが判明しない。

一般的に、スタック領域にあるデータへのアクセスは高速で、ヒープ領域にあるデータへのアクセスは低速(スタック領域に比べて)。

大事なのは

  • どのソースコードがヒープ領域のデータを必要とするか認識すること
  • ヒープ領域のデータの重複を最小限にすること
  • メモリ不足にならないように、ヒープ領域にある未使用のデータを取り除くこと

ただしこれらは、所有権によって解決される。

所有権規則

所有権のルール

  • Rustの各値は、所有者と呼ばれる変数と対応している。

  • いかなる時も所有者は一つである。

  • 所有者がスコープから外れたら、値は破棄される。

  • 文字列リテラル

    • スタック領域を利用する
    • why?: プログラムにハードコードされるためコンパイル時に決定されるから
  • String

    • ヒープ領域を利用する
    • why?: ユーザーからの入力など、コンパイル時に決まらないから
{
  // ヒープ領域に「Hello, World!」分の領域が確保される
  let s = String::from("Hello, World!");

  // s で作業する

  // スコープの最後で自動的に drop 関数が呼ばれる -> ヒープ領域から開放される
}

ムーブ

  • deep copy: ヒープ領域にある実態ごと複製する。Rustだと clone 関数。
  • shallow copy: スタック領域にある、ヒープ領域の実態の参照を複製する
  • move: 所有権を移動する
let s1 = String::from("hello");
// この時点で、s1の所有権はs2に奪われる -> ムーブと呼ばれる
let s2 = s1;

println!("{}, world!", s1);

たとえば、s2がs1の shallow copy だったとして、スコープを抜けた際に、s1とs2それぞれで drop 関数が呼ばれ、参照先のヒープ領域を開放しようとすると、二重開放エラーというバグになってしまう。
これを避けるために、Rust では shallow copy ではなく move という言語仕様になっている。

スタック領域に実態が保持される型

  • あらゆる整数型。u32など。
  • 論理値型であるbool。trueとfalseという値がある。
  • あらゆる浮動小数点型、f64など。
  • 文字型であるchar。
  • タプル。ただ、Copyの型だけを含む場合。例えば、(i32, i32)はCopyだが、 (i32, String)は違う。

これらは、Copyトレイトが付与されている。

4.2 参照と借用

参照

いわゆる & がついたやつ。
&String とかでいつも使っている。

String はヒープ領域を使ったものだけど、&String はそのヒープ領域への参照側を扱うことになる。

借用

関数の引数に参照を取ることを 借用 という。

可変な参照

関数の引数が &mut String などの場合、可変な参照を要求している。
ただし、いくつか注意点がある。

注意1: 可変な参照は複数作れない。

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

注意2: すでに不変な参照が行われている場合、可変な参照は作れない

let mut s = String::from("hello");

let r1 = &s; // 問題なし
let r2 = &s; // 問題なし
let r3 = &mut s; // 大問題!

宙に浮いた参照

String型などは、スコープを抜けるとヒープ領域から開放されてしまうが、&String型の参照は、スコープを抜けても対応するスタック領域は残り続ける。

4.3 スライス型

スライスとは、ヒープ領域の最初の要素への参照 + 長さ を表したもの。

文字列スライスとは &str であり、不変な参照である。
ヒープ領域を利用する String の中で、必要な部分を切り取る(スライス)するイメージ。
だから s[n..m] という書き方になる。

疑問や確認したいことなど

  • スタック領域って、開放されたりする?
  • 所有権は、ヒープ領域を利用する型のみが関係する概念?
    • つまり、スタック領域のみを利用する型(整数型・bool・浮動小数点型・charなど)は drop関数が存在しない?
  • String -> &String を生成した時のスタック領域も、コンパイル時に確保されている?
  • &str と &String の使い分けがわからない
ホリケンシュウホリケンシュウ

5章 構造体を使用して関係のあるデータを構造化する

5.1 構造体を定義し、インスタンス化する

structの一部のフィールドのみをmut(可変)にすることはできない
フィールド値を変更したい場合は、struct全体をmutにする必要がある。

フィールド省略化記法

フィールドと変数が同盟の場合、フィールド初期化省略記法が利用できる。
これはjavascriptなどと同様なので問題無し。

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

構造体更新記法

無意識に使っていた...たしかにそうだ。

let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    active: user1.active,
    sign_in_count: user1.sign_in_count,
};

これって、残りのフィールドのみが対象だったのか...上書きだと思ってた

let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    ..user1
};

タプル構造体

フィールドの名前が存在しない。順番で識別することになる。

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

ユニット様構造体

フィールドをもたない構造体。トレイトを実装するが、その型自体に保持させるデータがない場合に有効。
これは詳しくは10章の内容。

5.2 構造体を使ったプログラム例

関数の引数2つ(u32, u32) -> タプル(u32, u32) -> 構造体(width: u32, height: u32)
上記の流れでリファクタリングする道筋を示している。(わかりやすい)

通常出力には Display トレイト、デバッグ出力に Debug トレイトが必要なのは知っている。
{:#?}は知らなかった。今度使ってみよう。println!("rect is {:#?}", rect);

5.3 メソッド記法

メソッド

The Book だと、メソッドはインスタンスメソッドのことを指すっぽい。
インスタンスメソッドって、必ず &self である必要があると勘違いしていた。
所有権奪ってもいいんだ、可変参照でもいいんだ。了解です。

関連関数

selfを引数に取らない関数。 ≒ クラス関数、の認識。
呼び出し方は .(ドット) ではなく ::(コロン2つ) になる

ホリケンシュウホリケンシュウ

6章 Enumとパターンマッチング

6.1 Enumを定義する

enum の variant には、ユニット様構造体、struct構造体、タプルのどれもがなりうる。

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

struct 同様、enum もメソッドを実装することができる

impl Message {
    fn call(&self) {
        // method body would be defined here
        // メソッド本体はここに定義される
    }
}

let m = Message::Write(String::from("hello"));
m.call();

Option enumとNull値に勝る利点

Rust の prelude にも含まれている標準機能 Option がまさに enum でできている。
一般的なプログラミング言語に存在する null という概念に相当するもの。

Rust では、データが存在しない可能性があるものに対して Option 型を当てはめる。Option を利用する全ての箇所で、null の場合を考慮したパターンマッチを強制する。これにより堅牢となっている。

enum Option<T> {
    Some(T),
    None,
}

6.2 match制御フロー演算子

match は慣れてきたので割愛。enum の variant の値を取り出すこともできる。

_というプレースホルダー

よくわからなくなる。パターンマッチの残り全てに適用させたい場合は _ を利用する。

let some_u8_value = 0u8;
match some_u8_value {
    1 => println!("one"),
    3 => println!("three"),
    5 => println!("five"),
    7 => println!("seven"),
    _ => (),
}

6.3 if letで簡潔な制御フロー

some_u8_value の値が 3 だったら、ってパターンマッチもいけるんですね...あんまり理解できてなかったかも。

let some_u8_value = Some(0u8);
match some_u8_value {
    Some(3) => println!("three"),
    _ => (),
}

if let はイマイチなれてないなあ...

let mut count = 0;
if let Coin::Quarter(state) = coin {
    println!("State quarter from {:?}!", state);
} else {
    count += 1;
}
ホリケンシュウホリケンシュウ

7章 肥大化していくプロジェクトをパッケージ、クレート、モジュールを利用して管理する

7.1 パッケージとクレート

Rust のパッケージには、1つ以上のクレートが必要。基本的に main.rs を含むバイナリクレートが1つ存在し、ライブラリクレートがいくつか存在するのが一般的と思う

  • src/main.rs ... バイナリクレート
  • src/lib.rs ... ライブラリクレート

7.2 モジュールを定義して、スコープとプライバシーを制御する

モジュールを適切に分割し、モジュールツリーを構成する。
モジュールごとにプライバシーの制御( public / private )などを制御できる。

7.3 モジュールツリーの要素を示すためのパス

  • 絶対パス
    • crate:: から始まる
  • 相対パス
    • self:: または super:: または現在のモジュールから始まる

どの書き方を選択するかは、将来のモジュールの変更具合によりそう。
僕だったら、基本は絶対パスで良いかな。

構造体とenumを公開する

構造体の場合、構造体自体に pub をつけても フィールドは 非公開 のまま。
逆にenumの場合、enum自体に pub をつけるとvariantも 公開 になる。

pub struct Breakfast {
   pub toast: String,
   seasonal_fruit: String,
}

pub enum Appetizer {
   Soup,
   Salad,
}

7.4 useキーワードでパスをスコープに持ち込む

use を使うことでいちいちパスを書かなくても利用することができる(シンボリックリンクに似ている)

関数をスコープに use で持ち込む場合は、関数の親モジュールを use するやり方が慣例的とされている。
逆に、構造体やenumの場合はフルパスを書くのが慣例的とのこと。

// 関数(add_to_waitlist)をuseしたい場合
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

// ○: Rust慣例的なやりかた
use self::front_of_house::hosting;
pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

// ×: Rust慣例的なやりかたではない
use crate::front_of_house::hosting::add_to_waitlist;
pub fn eat_at_restaurant() {
    add_to_waitlist();
    add_to_waitlist();
    add_to_waitlist();
}

7.5 モジュールを複数のファイルに分割する

モジュールと同じ名前のファイルを自動的に探しに行ってくれる。

// src/lib.rs
mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

// src/front_of_house.rb
pub mod hosting {
    pub fn add_to_waitlist() {}
}
ホリケンシュウホリケンシュウ

8章 一般的なコレクション

3つのコレクション

  • ベクタ型
  • 文字列
  • ハッシュマップ

8.1 ベクタで値のリストを保持する

ベクタ型: Vec<T>

// ベクタの生成
let mut v: Vec<i32> = Vec::new();

// ベクタの更新
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);

// ベクタの要素を読む
let v = vec![1, 2, 3, 4, 5];
// 添字記法
let third: &i32 = &v[2];
println!("The third element is {}", third);
// getメソッド
match v.get(2) {
    Some(third) => println!("The third element is {}", third),
    None => println!("There is no third element."),
}

// 存在しない要素を参照する場合
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];     // panicになる
let does_not_exist = v.get(100);  // Noneを返す

複数の型をベクタに保持したい場合...enumを利用する。

enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];

8.2 文字列でUTF-8でエンコードされたテキストを保持する

文字列とは?

通常、所有権を持つString、参照である文字列スライス &str の両方を指す

// 新規文字列を生成する

// パターン1
let mut s = String::new();
// パターン2
let s = "initial contents".to_string();
// パターン3
let s = String::from("initial contents");

// 文字列を更新する

// push_strは文字列スライスを引数にうけとる
let mut s = String::from("foo");
s.push_str("bar");  // sは"foobar"

// pushは1文字の文字列スライスを引数にうけとる
let mut s = String::from("lo");
s.push('l');

// +演算子、またはformat!マクロで連結

let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1はムーブされ、もう使用できないことに注意

// 文字列に添え字アクセスする

let s1 = String::from("hello");
let h = s1[0];  // エラーになる!!!!!!!

// 添え字アクセスをサポートしていない
let len = String::from("Hola").len();  // 4バイト
let len = String::from("Здравствуйте").len();  // 12バイト...ではなく24バイト

// 文字によってバイト数が異なるため、添字による統一的なアクセスができない。

8.3 キーとそれに紐づいた値をハッシュマップに格納する

ハッシュマップ: HashMap<K, V>

// ハッシュマップの生成(パターン1)
use std::collections::HashMap;  // プレリュードには含まれていないことに注意(以後省略)
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

// ハッシュマップの生成(パターン2)
use std::collections::HashMap;
let teams  = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();

ハッシュマップと所有権

i32のようなCopyトレイトを持つデータ型の場合・・・ハッシュマップに値がコピーされる
Stringのような所有権があるデータ型の場合・・・ハッシュマップに所有権がムーブされる

ハッシュマップの値にアクセスする

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

let team_name = String::from("Blue");
let score = scores.get(&team_name);  // Some(&10) となる

ハッシュマップを更新する

ハッシュマップは、値を上書きできる or キーにまだ値がない場合だけ追加できる、などの制御が可能。

// 値を上書きする
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{:?}", scores); // {"Blue": 25} と出力される

// キーに値がなかった時のみ値を挿入する
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
println!("{:?}", scores); // {"Yellow": 50, "Blue": 10} と出力される

// 古い値に基づいて値を更新する
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
    let count = map.entry(word).or_insert(0);
    *count += 1;
}
println!("{:?}", map); // {"world": 2, "hello": 1, "wonderful": 1} と出力される
ホリケンシュウホリケンシュウ

9章 エラー処理

9.1 panic!で回復不能なエラー

回復不能なエラーとして panic! マクロが存在。
実行すると、エラーメッセージを表示して、スタックを巻き戻し掃除して終了する。

RUST_BACKTRACE 環境変数をセットすると、スタックトレースを表示してくれる。

$ RUST_BACKTRACE=1 cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', /checkout/src/liballoc/vec.rs:1555:10
stack backtrace:
   0: std::sys::imp::backtrace::tracing::imp::unwind_backtrace
             at /checkout/src/libstd/sys/unix/backtrace/tracing/gcc_s.rs:49
   1: std::sys_common::backtrace::_print
             at /checkout/src/libstd/sys_common/backtrace.rs:71
   2: std::panicking::default_hook::{{closure}}
             at /checkout/src/libstd/sys_common/backtrace.rs:60
             at /checkout/src/libstd/panicking.rs:381
   3: std::panicking::default_hook
             at /checkout/src/libstd/panicking.rs:397
   4: std::panicking::rust_panic_with_hook
             at /checkout/src/libstd/panicking.rs:611
   5: std::panicking::begin_panic
             at /checkout/src/libstd/panicking.rs:572
   6: std::panicking::begin_panic_fmt
             at /checkout/src/libstd/panicking.rs:522
   7: rust_begin_unwind
             at /checkout/src/libstd/panicking.rs:498
   8: core::panicking::panic_fmt
             at /checkout/src/libcore/panicking.rs:71
   9: core::panicking::panic_bounds_check
             at /checkout/src/libcore/panicking.rs:58
  10: <alloc::vec::Vec<T> as core::ops::index::Index<usize>>::index
             at /checkout/src/liballoc/vec.rs:1555
  11: panic::main
             at src/main.rs:4
  12: __rust_maybe_catch_panic
             at /checkout/src/libpanic_unwind/lib.rs:99
  13: std::rt::lang_start
             at /checkout/src/libstd/panicking.rs:459
             at /checkout/src/libstd/panic.rs:361
             at /checkout/src/libstd/rt.rs:61
  14: main
  15: __libc_start_main
  16: <unknown>

デバッグシンボルって、行番号のこと??(はじめてきく単語だ...)

9.2.Resultで回復可能なエラー

enum Result<T, E> {
    Ok(T),
    Err(E),
}

?演算子は、Resultを返す関数でしか使用できない

自分、これは案外ハマるポイントかも。今まであんまり意識できてなかった。

9.3.panic!すべきかするまいか

  • panic!マクロ
    • プログラムが処理できない状態にあり、無効だったり不正な値で処理を継続するのではなく、プロセスに処理を中止するよう指示することを通知する
  • Result enum
    • コードが回復可能な方法で処理が失敗するかもしれないことを示唆する
    • 呼び出し側のコードに成功や失敗する可能性を処理する必要があることも教える
ホリケンシュウホリケンシュウ

10章 ジェネリック型、トレイト、ライフタイム

ジェネリック型を使うことで、抽象的な型に対して処理するコードを可能にしてくれる。
TypeScript などでも触っているから、概念自体に驚きとかは流石にない。
Rustでも、ジェネリック型を使った関数とか定義できるようになっていきたい。

10.1 ジェネリックなデータ型

型引数の名前にはどんな識別子も使用できるが、慣習として T を使用する。
なぜならば、Rustの引数名は短く、型の命名規則がキャメルケースだから。
"type" の省略形なので、 T が多くのRustプログラマの既定の選択なのだ。

まあでも、TypeScriptでも普通は T だよね。

書き方は、関数名の直後に <T> で、ジェネリック型を利用することを宣言する。

fn largest<T>(list: &[T]) -> T {

関数定義だけでなく、構造体定義でもジェネリック型は利用できる

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}

複数のジェネリック型を利用することも可能

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 };
}

ただし、多くのジェネリックな型が必要な時は、 コードの小分けが必要なサインかもしれないので注意。

メソッド定義の場合

impl の直後に <T> を記述する必要がある。これはまだ慣れてないな自分...
このコードが、Point構造体の xフィールドに対する getter であることはすぐに分かる。

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());
}

ジェネリクスを使用したコードのパフォーマンス

Rustでは、ジェネリクスを、具体的な型があるコードよりもジェネリックな型を使用したコードを実行するのが遅くならないように実装しています。

そうなんだ。

コンパイラはこれを、ジェネリクスを使用しているコードの単相化をコンパイル時に行うことで達成しています。

なるほど。つまりコンパイル時に、実際の呼び出し部分を見て、その呼び出しに当てはまる型によるコードを生成してるのか。
なら、コンパイルの時間が増える?
それと、引数がコマンドライン引数とかだったら、大丈夫なのか? => 引数を何型で受け取るか決めてるはずだから、こっちは平気なのかな。

10.2 トレイト:共通の振る舞いを定義する

注釈: 違いはあるものの、トレイトは他の言語でよくインターフェイスと呼ばれる機能に類似しています。

僕はこの認識から入りました。

デフォルト実装 なんてあったんだ、知らなかった。

pub trait Summary {
    fn summarize(&self) -> String {
        // "(もっと読む)"
        String::from("(Read more...)")
    }
}

impl Summary for Tweet {}

トレイトを引数で使う

// トレイト境界構文
pub fn notify<T: Summary>(item1: &T, item2: &T) {

// 引数に直接 &impl Xxxx と記述するシンタックスシュガーも存在
pub fn notify(item1: &impl Summary, item2: &impl Summary) {

// 複数のトレイトを持つことを保証したい場合
pub fn notify<T: Summary + Display>(item: &T) {
// シンタックスシュガー
pub fn notify(item: &(impl Summary + Display)) {

// where句を使った記法も存在
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
// 上の書き方がこうなる
fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{

トレイトを戻り値で使う

// 戻り値の型として impl Summary と指定することが可能
fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    }
}

戻り値で impl Summary としていても、戻り値の型が Tweet か NewsArticle かが if で分岐するような関数は書けないのか・・・なんでだろう?
それってつまり、戻り値の型は impl Summary って書く必要すらなさそうに思うけども・・・??

トレイト境界を使用して、メソッド実装を条件分けする

どのトレイトが実装されているか?の状況に応じて、どのメソッドを実装するかどうかを条件分けできるみたいだ!(知らなかった!)
下記の例だと TDisplay + PartialOrd を満たす場合に cmp_display メソッドを実装してくれている。

use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

10.3 ライフタイムで参照を検証する

ライフタイムの概念は、他のプログラミング言語の道具とはどこか異なり、間違いなくRustで一番際立った機能になっています

間違いない。書き方含め、なかなかとっつきにくかった。
でもヒープ&スタックの話を理解すると、ああこういう概念も必要になるよな、ってすんなり分かる。
やっぱり Rust は、この The Book を上から順番に読むのが一番良いね多分。

ライフタイム注釈記法

まず記法に慣れよう

&i32        // a reference
            // (ただの)参照
&'a i32     // a reference with an explicit lifetime
            // 明示的なライフタイム付きの参照
&'a mut i32 // a mutable reference with an explicit lifetime
            // 明示的なライフタイム付きの可変参照

関数にライフタイム注釈を入れる場合はこうなる。
型パラメータのように <> で括る形になる。

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

上記 'a は、引数 xy のうち、ライフタイムが短い方に揃うらしい。(これは覚えておく必要がありそうだ)

コンパイラはどのようなルールに則ってライフタイムを確認しているか?

コンパイラは3つの規則を活用し、明示的な注釈がない時に、参照がどんなライフタイムになるかを計算します
最初の規則は入力ライフタイムに適用され、2番目と3番目の規則は出力ライフタイムに適用されます。

3つの規則があるのか。

最初の規則は、参照である各引数は、独自のライフタイム引数を得るというものです。換言すれば、 1引数の関数は、1つのライフタイム引数を得るということです: fn foo<'a>(x: &'a i32); 2つ引数のある関数は、2つの個別のライフタイム引数を得ます: fn foo<'a, 'b>(x: &'a i32, y: &'b i32); 以下同様。

引数の数だけ、ライフタイムもありうるわな。

2番目の規則は、1つだけ入力ライフタイム引数があるなら、そのライフタイムが全ての出力ライフタイム引数に代入されるというものです: fn foo<'a>(x: &'a i32) -> &'a i32。

入力ライフタイムが1つなら、出力ライフタイムも1つに特定される、ことを確認しているのか。

3番目の規則は、複数の入力ライフタイム引数があるけれども、メソッドなのでそのうちの一つが&selfや&mut selfだったら、 selfのライフタイムが全出力ライフタイム引数に代入されるというものです。 この3番目の規則により、必要なシンボルの数が減るので、メソッドが遥かに読み書きしやすくなります。

メソッドの場合、 &self なら出力ライフタイムも &self と同じライフタイムになる、ことを確認しているのか。

ここまでくると、下のコードも割とすんなり読めるようになるぞ。

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    //       "アナウンス! {}"
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
ホリケンシュウホリケンシュウ

12章 コマンドライン引数を受け付ける

12.1.コマンドライン引数を受け付ける

env::args() で入力を受け取れるのか。そしてそれはイテレータを返すのか。

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    println!("{:?}", args);

    // 添字でアクセスできる
    let args_1 = args[1];
    let args_0 = args[0];
}

12.2.ファイルを読み込む

下記のようにして、入力でうけとったファイル名を元に中身を出力することができる。
ただし、このファイル(main.rs)では責務が多すぎるので、次項で整えていく。

use std::env;
use std::fs::File;
use std::io::prelude::*;

fn main() {
    // --snip--
    println!("In file {}", filename);

    // ファイルが見つかりませんでした
    let mut f = File::open(filename).expect("file not found");

    let mut contents = String::new();
    f.read_to_string(&mut contents)
        // ファイルの読み込み中に問題がありました
        .expect("something went wrong reading the file");

    // テキストは\n{}です
    println!("With text:\n{}", contents);
}

12.3.リファクタリングしてモジュール性とエラー処理を向上させる

まずは、コマンドライン引数を解釈する処理を main から分離することを意識する。
Config という構造体を作り、そのフィールドにアクセスする形にリファクタできた。

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    // --snip--
}

// --snip--

impl Config {
    fn new(args: &[String]) -> Config {
        let query = args[1].clone();
        let filename = args[2].clone();

        Config { query, filename }
    }
}

あとは Result を使ってエラーを返す可能性を呼び出し側に伝えて、実際の実行部分も別モジュールに切り出している。(この辺りはもう割愛!)

12.4.テスト駆動開発でライブラリの機能を開発する

読んでいて特に問題なし。割愛。

pub fn run(config: Config) -> Result<(), Box<Error>> {
    let mut f = File::open(config.filename)?;

    let mut contents = String::new();
    f.read_to_string(&mut contents)?;

    for line in search(&config.query, &contents) {
        println!("{}", line);
    }

    Ok(())
}

12.5.環境変数を取り扱う

env::var("環境変数名") を使うことで、Result で括られた環境変数の値を取り出せる模様。

この項では、大文字小文字を気にするかどうか?を環境変数 CASE_INSENSITIVE の bool値 を見て判断して、ロジックを分岐させていた。

12.6. 標準出力ではなく標準エラーにエラーメッセージを書き込む

eprintln! を利用すれば、標準エラー出力にメッセージを書き込めるのか。
これは明確に使い分けができそうだな。

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    if let Err(e) = minigrep::run(config) {
        eprintln!("Application error: {}", e);

        process::exit(1);
    }
}
ホリケンシュウホリケンシュウ

13章 関数型言語の機能: イテレータとクロージャ

13.1 クロージャ:環境をキャプチャできる匿名関数

入力値の数字を、2秒後に返す関数 simulated_expensive_calculation

use std::thread;
use std::time::Duration;

fn simulated_expensive_calculation(intensity: u32) -> u32 {
    // ゆっくり計算します
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    intensity
}
  • simulated_user_specified_value: 運動強度のユーザー入力値
  • simulated_random_number: 乱数
fn main() {
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(
        simulated_user_specified_value,
        simulated_random_number
    );
}

fn generate_workout(intensity: u32, random_number: u32) {
    if intensity < 25 {

        println!(
            // 今日は{}回腕立て伏せをしてください!
            "Today, do {} pushups!",
            simulated_expensive_calculation(intensity)
        );

        println!(
            // 次に、{}回腹筋をしてください!
            "Next, do {} situps!",
            simulated_expensive_calculation(intensity)
        );
    } else {
        if random_number == 3 {
            // 今日は休憩してください!水分補給を忘れずに!
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                // 今日は、{}分間走ってください!
                "Today, run for {} minutes!",
                simulated_expensive_calculation(intensity)
            );
        }
    }
}

関数でリファクタリング

GOOD: 1回だけ呼び出される形になった!
BAD: 呼び出さないパターンでも1回呼び出されてしまう...

=> これを解決するのが クロージャ

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_result =
        simulated_expensive_calculation(intensity);

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_result
        );
        println!(
            "Next, do {} situps!",
            expensive_result
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_result
            );
        }
    }
}

クロージャでリファクタリングして、コードを保存する

  • クロージャの仮引数を縦棒で括った変数で指定
  • この記法はSmalltalkやRubyのクロージャ定義と類似しているところからきた
  • カンマで複数の引数を定義できる |param1, param2|
  • 波括弧はセミコロンが必要
  • 波括弧は、中の式が1つなら省略可能

GOOD: 実行時にだけ呼び出されるようになった!
BAD: 2回呼び出しがあると2倍の時間がかかる...

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_closure = |num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    };

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_closure(intensity)
        );
        println!(
            "Next, do {} situps!",
            expensive_closure(intensity)
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_closure(intensity)
            );
        }
    }
}

クロージャの型推論と注釈

input と output に型注釈を明示することが可能。関数っぽくなる。

let expensive_closure = |num: u32| -> u32 {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    num
};

型推論が働くため、1度目に String でクロージャを利用し、2度目に i64 でクロージャを利用しようとすると、コンパイルエラーになる。

let example_closure = |x| x;

let s = example_closure(String::from("hello"));
let n = example_closure(5);

ジェネリック引数とFnトレイトを使用してクロージャを保存する

  • ジェネリック引数T(Fn(u32) -> u32)はクロージャを指定している
  • このクロージャは、u32をinputにとり、u32をoutputする
  • value が None なら calculationクロージャを実行、その値をvalueに保存してSome状態にする
  • value が Some なら、そのまま value を返す
  • これでクロージャが1回のみ実行される形になった!
impl<T> Cacher<T>
    where T: Fn(u32) -> u32
{
    fn new(calculation: T) -> Cacher<T> {
        Cacher {
            calculation,
            value: None,
        }
    }

    fn value(&mut self, arg: u32) -> u32 {
        match self.value {
            Some(v) => v,
            None => {
                let v = (self.calculation)(arg);
                self.value = Some(v);
                v
            },
        }
    }
}

Cacherを使う形に修正
GOOD: 実行時に1回だけ呼び出されるようになった!
BAD: なし

fn generate_workout(intensity: u32, random_number: u32) {
    let mut expensive_result = Cacher::new(|num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    });

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_result.value(intensity)
        );
        println!(
            "Next, do {} situps!",
            expensive_result.value(intensity)
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_result.value(intensity)
            );
        }
    }
}

Cacher実装の限界

このCacher構造体は、2回呼び出すと意図通りにいかない欠点?がある。

#[test]
fn call_with_different_values() {
    let mut c = Cacher::new(|a| a);

    let v1 = c.value(1);
    let v2 = c.value(2);

    assert_eq!(v2, 2);
}

1度 1 を返すように呼び出すと、その後は必ず 1 が返る

thread 'call_with_different_values' panicked at 'assertion failed: `(left == right)`
  left: `1`,
 right: `2`', src/main.rs

これを解決するためのアイデアとしては、 単純な u32 を返すかわりに HashMap を返すようにして、与えられた u32 を HashMap の key に設定して、さまざまな u32 を返す形に変更するなどが考えられる。

クロージャで環境をキャプチャする

  • クロージャには関数にはない追加の能力がある
  • 環境をキャプチャし、 自分が定義されたスコープの変数にアクセスできる
  • equal_to_x クロージャは、そのスコープにあるxをキャプチャできてしまう
fn main() {
    let x = 4;

    let equal_to_x = |z| z == x;

    let y = 4;

    assert!(equal_to_x(y));
}
  • 反対に、関数ではこのようなことはできない
  • このようなコードを書いてもコンパイルエラーになる
fn main() {
    let x = 4;

    fn equal_to_x(z: i32) -> bool { z == x }

    let y = 4;

    assert!(equal_to_x(y));
}
  • ただし、この環境をキャプチャするためにメモリを使用するためオーバーヘッドは存在する

クロージャは3つの方法で環境をキャプチャできる

  • FnOnce: キャプチャした変数の「所有権ごと奪っ」て利用する
  • FnMut: キャプチャした変数を「可変」で利用する
  • Fn: キャプチャした変数を「借用」で利用する

以下、xは所有権をクロージャに奪われているので、後続のprintlnで使えないため、コンパイルエラーになる。

fn main() {
    let x = vec![1, 2, 3];

    let equal_to_x = move |z| z == x;

    // ここでは、xを使用できません: {:?}
    println!("can't use x here: {:?}", x);

    let y = vec![1, 2, 3];

    assert!(equal_to_x(y));
}

13.2 一連の要素をイテレータで処理する

  • Rustは標準でイテレータを提供している
  • もしイテレータの提供がない言語の場合、添え字アクセスで各要素にアクセスして値を得てベクタの総要素数に到達するまでループするやり方になるでしょう
  • イテレータにより、ベクタなどの添え字アクセスできる構造体だけでなく、さまざまなシーケンスに対して同じロジックを適用する柔軟性が得られる
let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

for val in v1_iter {
    // {}でした
    println!("Got: {}", val);
}

Iterator トレイトとnextメソッド

標準ライブラリに定義されている Iterator トレイト

  • Iteratorの実装には、nextメソッドの定義が要求される
pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // デフォルト実装のあるメソッドは省略
    // methods with default implementations elided
}

ベクタから生成されたイテレータのnextメソッドを呼び出した例

#[test]
fn iterator_demonstration() {
    let v1 = vec![1, 2, 3];

    let mut v1_iter = v1.iter();

    assert_eq!(v1_iter.next(), Some(&1));
    assert_eq!(v1_iter.next(), Some(&2));
    assert_eq!(v1_iter.next(), Some(&3));
    assert_eq!(v1_iter.next(), None);
}
  • v1_iter を可変にする必要がある
    • 今シーケンスのどこにいるか?という状態を返る必要があるから。
  • 違い
    • iter
      • nextメソッドで得られる値が不変参照
    • into_iter
      • nextメソッドで得られる値が所有権ごと付いてくる
    • iter_mut
      • nextメソッドで得られる値が可変参照

イテレータを消費するメソッド

  • Rustは標準でイテレータを提供している
  • もしイテレータの提供がない言語の場合、添え字アクセスで各要素にアクセスして値を得てベクタの総要素数に到達するまでループするやり方になるでしょう
  • イテレータにより、ベクタなどの添え字アクセスできる構造体だけでなく、さまざまなシーケンスに対して同じロジックを適用する柔軟性が得られる
let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

for val in v1_iter {
    // {}でした
    println!("Got: {}", val);
}

Iterator トレイトとnextメソッド

標準ライブラリに定義されている Iterator トレイト

  • Iteratorの実装には、nextメソッドの定義が要求される
pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // デフォルト実装のあるメソッドは省略
    // methods with default implementations elided
}

ベクタから生成されたイテレータのnextメソッドを呼び出した例

#[test]
fn iterator_demonstration() {
    let v1 = vec![1, 2, 3];

    let mut v1_iter = v1.iter();

    assert_eq!(v1_iter.next(), Some(&1));
    assert_eq!(v1_iter.next(), Some(&2));
    assert_eq!(v1_iter.next(), Some(&3));
    assert_eq!(v1_iter.next(), None);
}
  • v1_iter を可変にする必要がある
    • 今シーケンスのどこにいるか?という状態を返る必要があるから。
  • 違い
    • iter
      • nextメソッドで得られる値が不変参照
    • into_iter
      • nextメソッドで得られる値が所有権ごと付いてくる
    • iter_mut
      • nextメソッドで得られる値が可変参照

イテレータを消費するメソッド

  • Iterator トレイトを実装して提供されるデフォルト実装には、内部でnextを呼んでいるものもある。
  • nextを呼び出すメソッドは 消費アダプタ と呼ばれる
    • 呼び出しが iter を消費することになるから
    • 例) sum メソッド(下記)
#[test]
fn iterator_sum() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    let total: i32 = v1_iter.sum();

    assert_eq!(total, 6);
}

他のイテレータを生成するメソッド

  • Iterator トレイトに定義された他のメソッドは イテレータアダプタ と呼ばれる
  • イテレータは怠惰なので、消費アダプタが呼ばれないと結果は得られない
    • into_iter(xxx)
    • .map(yyy) // イテレータアダプタ
    • .collect::<zzz>() // 消費アダプタ

もし消費アダプタが使われていないと警告される

let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1);


warning: unused `std::iter::Map` which must be used: iterator adaptors are lazy
and do nothing unless consumed
(警告: 使用されねばならない`std::iter::Map`が未使用です: イテレータアダプタは怠惰で、
消費されるまで何もしません)
 --> src/main.rs:4:5
  |
4 |     v1.iter().map(|x| x + 1);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: #[warn(unused_must_use)] on by default

環境をキャプチャするクロージャを使用する

filterイテレータアダプタを使った例

struct Shoe {
    size: u32,
    style: String,
}

fn shoes_in_my_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter()
        .filter(|s| s.size == shoe_size)
        .collect()
}

#[test]
fn filters_by_size() {
    let shoes = vec![
        Shoe { size: 10, style: String::from("sneaker") },
        Shoe { size: 13, style: String::from("sandal") },
        Shoe { size: 10, style: String::from("boot") },
    ];

    let in_my_size = shoes_in_my_size(shoes, 10);

    assert_eq!(
        in_my_size,
        vec![
            Shoe { size: 10, style: String::from("sneaker") },
            Shoe { size: 10, style: String::from("boot") },
        ]
    );
}

Iterator トレイトで独自のイテレータを作成する

(割愛)

他のIteratorトレイトメソッドを使用する

(割愛)

13.3 入出力プロジェクトを改善する

13.4 パフォーマンス比較:ループVSイテレータ

ホリケンシュウホリケンシュウ

15章 スマートポインタ

  • ポインタ
    • Rustにおいて最もありふれた種類のポインタは「参照」
  • スマートポインタ
    • ポインタのように振る舞うだけでなく、追加のメタデータと能力があるデータ構造
    • この章では「参照カウント」という能力について深掘り
    • この「参照カウント」のおかげで、データに複数の所有者をもたせられる
    • ポインタとは対象てきに、指しているデータを「所有」する
    • 特徴としてDerefとDropトレイトを実装している
    • ヒープに値を確保するBox<T>
    • 複数の所有権を可能にする参照カウント型のRc<T>
    • RefCell<T>を通してアクセスされ、コンパイル時ではなく実行時に借用規則を強制する型のRef<T>とRefMut<T>

内部可変性パターン、循環参照、いったいなんぞや・・・

15.1 ヒープのデータを指すBOX<T>を使用する

  • ボックス Box<T> を使う場面
    • コンパイル時にはサイズを知ることができない型があり、正確なサイズを要求する文脈でその型の値を使用する時
    • 多くのデータがあり、その所有権を移したいが、その際にデータがコピーされないようにしたい時
    • 値を所有する必要があり、特定の型であることではなく、特定のトレイトを実装する型であることのみ気にかけている時 -> 通称トレイトオブジェクト
// Boxを使うことで、i32のデータをヒープ領域に格納することができる
// この例のような実装は、通常はほぼない
fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
}
  • Box<T>は以下を備えている
    • Derefトレイトを実装している
      • Box<T>の値を参照のように扱うことができる。
    • Dropトレイトを実装している
      • スコープを抜けた際に、スタック領域のポインタと、ポインタが参照しているヒープ領域の両方が解放される

15.2 Derefトレイトでスマートポインタを普通の参照のように扱う

  • Derefトレイトを実装すると、参照外し演算子 * の振る舞いをカスタマイズできる
fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y); // 成功
    assert_eq!(5, y);   // 失敗_異なる型の比較はできない
}

下記のような書き換えも可能

fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Derefトレイトの他にも、DerefMutトレイトというものがある
以下の3つの場合に型やトレイト実装を見つけた時にコンパイラは、参照外し型強制を行う

  • T: Deref<Target=U>の時、&Tから&U
  • T: DerefMut<Target=U>の時、&mut Tから&mut U
  • T: Deref<Target=U>の時、&mut Tから&U

[疑問]

  • derefって destructuring reference の略とか?
  • deref って Box<T> -> T のイメージだけど、実際は Box<T> -> *(&T) の動き?

15.3 Dropトレイトで片付け時にコードを走らせる

Box<T> は Drop トレイトを実装することでカスタマイズし、参照先のヒープ領域の解放まで行っている。

  • Drop トレイトは Rust のプレリュードに含まれている
  • いつdropが呼ばれているか、println! だけを実行する drop トレイトの実装をして確かめてみる
struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        // CustomSmartPointerをデータ`{}`とともにドロップするよ
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer { data: String::from("my stuff") };      // 俺のもの
    let d = CustomSmartPointer { data: String::from("other stuff") };   // 別のもの
    println!("CustomSmartPointers created.");                           // CustomSmartPointerが生成された
}

// 出力(変数定義と逆順でdropされる)
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
  • drop メソッドは任意のタイミングで実行することはできない
  • 必ず、スコープを抜ける際に呼ばれるようにコンパイラに制御されている
  • 代わりに std::mem::drop 関数を呼ぶことならできる
fn main() {
    let c = CustomSmartPointer { data: String::from("some data") };
    println!("CustomSmartPointer created.");
    drop(c);
    // CustomSmartPointerはmainが終わる前にドロップされた
    println!("CustomSmartPointer dropped before the end of main.");
}

// 出力(スコープを抜ける前にdropされていることがわかる)
CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.

15.4 RC<T>は、参照カウント方式のスマートポインタ

  • Rc<T> とは reference counting の略で参照カウントと言う
  • 例えば、リビングにあるテレビを例にして、家族のうち1人でもいたらテレビが使われているけど、だれもいなかったらテレビを消すようなイメージ
  • コンパイル時には、どの部分が最後にデータを使用し終わるかを決定できない時に利用する
  • Rc<T> はあくまでシングルスレッドでのみ有効
  • マルチスレッドではおそらく Arc<T> なのかな?

Rc<T> のデータをクローンする場合は、通常 Rc::clone を利用する
なぜなら Rc::clone は、参照カウントをインクリメントするだけで効率が良いから。

let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = a.clone();  // たしかに書けるが
let b = Rc::clone(&a);  // 通常はこのように書く

参照カウントは Rc::strong_count 関数で確認できる
weak_count 関数もあり、こちらは循環参照という概念に関係している(後述)

15.5 RefCell<T>と内部可変性パターン

  • 忘れてた -> 不変参照の後に、可変参照はできない(4.2. 参照と借用より)
  • RefCell<T> の値に対して、 borrow_mut メソッドで可変参照を取得でき、 borrow メソッドで参照を取得できる。

15.6 循環参照は、メモリをリークすることもある

  • Rc<T>RefCell<T> を使用すれば、互いに参照しあう 循環参照 状態を生成することができる
  • すると、参照カウントが絶対に0にならないため、メモリリークを引き起こし、かつ絶対にドロップされない。
  • 弱い参照 を利用することで、循環参照を回避できる
ホリケンシュウホリケンシュウ

16 恐れるな!並行性

これらの文脈で行うプログラミングは困難で、エラーが起きやすいものでした: Rustはこれを変えると願っています。

並行性のあるプログラム書いていくぞー!
並行性 と言うと、今回は 並列 または 並列 と脳内で置き換えるみたい。
並列 はマルチスレッドなプログラムのイメージがある。じゃあ 並行 は・・・?

この章で講義する話題

  • スレッドを生成して、複数のコードを同時に走らせる方法
  • チャンネルがスレッド間でメッセージを送るメッセージ受け渡し並行性
  • 複数のスレッドが何らかのデータにアクセスする状態共有並行性
  • 標準ライブラリが提供する型だけでなく、ユーザが定義した型に対してもRustの並行性の安全保証を拡張するSyncとSendトレイト

16.1 スレッドを使用してコードを同時に走らせる

  • OSが提供するAPIを呼び出してスレッドを生成するモデルを 1:1 と呼ぶらしい。
    • 1個のスレッド = OS, 1個のスレッド = プログラム
  • プログラミング言語が提供するスレッドを グリーンスレッド と呼ぶらしい。
    • M:N モデルとも呼ばれるらしい
    • M個のスレッド = OS, N個のスレッド = プログラム

少し理屈がわからなかったけど、

  1. グリーンスレッドモデルは、スレッドを管理するために大きなランタイムが必要
  2. Rustは、Cコードを呼び出したい、ランタイムの肥大化を拒否したい
  3. Rustはグリーンスレッドモデルではなく 1:1 モデルの実装を提供
  4. ただし M:N モデルを実装したクレートも存在している

spawnで新規スレッドを生成する

thread::spawn 関数で新規スレッドを生成。新規スレッドで走らせたいコードとしてクロージャを渡す。

use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        for i in 1..10 {
            // やあ!立ち上げたスレッドから数字{}だよ!
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        // メインスレッドから数字{}だよ!
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }
}

joinハンドルで全スレッドの終了を待つ

メインスレッドが終了する前に、立ち上げたスレッドが確実に完了するようになる。

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }

    handle.join().unwrap();
}

ハンドルに対してjoinを呼び出すと、ハンドルが表すスレッドが終了するまで現在実行中のスレッドをブロックします。 スレッドをブロックするとは、そのスレッドが動いたり、終了したりすることを防ぐことです。

なるほど。現在実行中のスレッドが main スレッドだらかってことか。
なんでもどこでも handle() 呼べば終了しない、ってわけじゃないってことだな。

どこでjoinを呼ぶかといったほんの些細なことが、スレッドが同時に走るかどうかに影響することもあります。

やはりそのようだ。

16.2 メッセージ受け渡しを使ってスレッド間でデータを転送する

use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel();
}
  • mpsc::channel関数で新しいチャンネルを生成している
  • mpscはmultiple producer, single consumerを表している
  • tx: 転送機(おそらく transfer ナントカ)
  • rx: 受信機(おそらく receiver ナントカ)

転送機に hi という文字列を送らせてみる

use std::thread;
use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });

    let received = rx.recv().unwrap();
    // 値は{}です
    println!("Got: {}", received);
}
  • recv メソッド: メインスレッドの実行をブロックする
    • Resultを返す
    • チャンネルの送信側がクローズしたらErrを返す
    • 受信するまで待機していい場合に利用
  • try_recv メソッド: メインスレッドの実行をブロックしない
    • Resultを返す
    • メッセージがなければErrを返す
    • 受診するまでの間に別の処理を行わせるなどする場合に利用

複数の値を送信し、受信側が待機するのを確かめる

use std::thread;
use std::sync::mpsc;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        // スレッドからやあ(hi from the thread)
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thread"),
        ];

        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    for received in rx {
        println!("Got: {}", received);
    }
}
  • rx ってイテレータなの?なんだか急だな。うまく消化できない。
  • ここまで recv メソッドで値を出力してたのに rx の display でメッセージが出力だなんて急だなあ

転送機をクローンして複数の生成器を作成する

// --snip--

let (tx, rx) = mpsc::channel();

let tx1 = mpsc::Sender::clone(&tx);
thread::spawn(move || {
    let vals = vec![
        String::from("hi"),
        String::from("from"),
        String::from("the"),
        String::from("thread"),
    ];

    for val in vals {
        tx1.send(val).unwrap();
        thread::sleep(Duration::from_secs(1));
    }
});

thread::spawn(move || {
    // 君のためにもっとメッセージを(more messages for you)
    let vals = vec![
        String::from("more"),
        String::from("messages"),
        String::from("for"),
        String::from("you"),
    ];

    for val in vals {
        tx.send(val).unwrap();
        thread::sleep(Duration::from_secs(1));
    }
});

for received in rx {
    println!("Got: {}", received);
}

// --snip--

冒頭あったように mpscmutiple producer, single consumer である。
複数の送信機 + 単一の受信機。しっかりメッセージが受け取られていることがわかる。

Got: hi
Got: more
Got: from
Got: messages
Got: for
Got: the
Got: thread
Got: you

16.3 状態共有並行性

  • メモリを共有することで並行性を扱ってみる。
  • メモリ共有並行性と言うっぽい
  • 複数のスレッドが、同時に同じメモリ位置にアクセスができるということ
  • つまり、複数の所有権が存在することに似ている
  • ミューテックスという機能を見ていく

ミューテックスを使用して一度に1つのスレッドからデータにアクセスすることを許可する

ミューテックスは、どんな時も1つのスレッドにしかなんらかのデータへのアクセスを許可しないというように、 "mutual exclusion"(相互排他)の省略形です。

mutable eXXXX とかだと思ってたら全然違ったw

  • ミューテックスの2つの規則
    • データを使用する前にロックの獲得を試みなければならない。
    • ミューテックスが死守しているデータの使用が終わったら、他のスレッドがロックを獲得できるように、 データをアンロックしなければならない。

16.4 SyncとSendトレイトで拡張可能な並行性

割愛

ホリケンシュウホリケンシュウ

17 Rustのオブジェクト指向プログラミング機能

17.1 オブジェクト指向言語の特徴

  • オブジェクト指向プログラミング、通称 OOP

  • OOP の特徴は、オブジェクトやカプセル化、 継承など

  • オブジェクト

    • 構造体やEnumを定義すればデータがあるし、impl すればメソッドを提供できる
    • オブジェクトとは呼ばれないものの、Rustでも同じ機能を提供できる
  • カプセル化

    • 構造体のフィールドのpubの有無
    • メソッドのpubの有無
    • これらによって、Rustでもカプセル化と同じ機能を提供できる
  • 継承

    • Rustに継承という概念は存在しない
    • そもそも継承を使うシーンとは?
      • コードの再利用(ある型に特定の振る舞いを定義して、実装側に強制させる)
      • 多相性
    • Rustでは?
      • コードの再利用
        • トレイトを利用することで可能。デフォルト実装もある。
      • 多相性
        • ???

17.2 トレイトオブジェクトで異なる型の値を許容する

トレイトオブジェクトは、ダイナミックディスパッチを行う

単相化の結果吐かれるコードは、 スタティックディスパッチを行い、これは、コンパイル時にコンパイラがどのメソッドを呼び出しているかわかる時のことです
これは、ダイナミックディスパッチとは対照的で、この時、コンパイラは、コンパイル時にどのメソッドを呼び出しているのかわかりません。ダイナミックディスパッチの場合、コンパイラは、どのメソッドを呼び出すか実行時に弾き出すコードを生成します。

なるほど。どういう場合に単相化されて、どう言う場合に単相化されないのかな?

トレイトオブジェクトを使用すると、コンパイラはダイナミックディスパッチを使用しなければなりません

なるほど。つまりトレイトを使用すると単相化されないわけだ。
実行時にどのメソッドを呼ぶか検索するから、実行時のコストが少しあがるわけだ。
とはいえ、実行時のコストと引き換えに、柔軟性のあるコードが手にはいるわけだ。

トレイトオブジェクトには、オブジェクト安全性が必要

なんだろう、なんか上手く飲み込めないな。
トレイトオブジェクトってなんだっけ・・・

トレイトオブジェクトのうち、オブジェクト安全なものか否か、という違いがある模様。

トレイト安全なものの条件

  • 戻り値の型がSelfでない
  • ジェネリックな型引数がない

つまり -> Self だったり T なものを使うメソッドがないものを言うのかな。

17.3 オブジェクト指向デザインパターンを実装する

ホリケンシュウホリケンシュウ

18 パターンとマッチング

18.1 パターンが使用されることのある箇所全部

matchアーム

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

条件分岐if let式

if let式を使うことの欠点は、コンパイラが網羅性を確認してくれないことです。一方でmatch式ではしてくれます。

記述量が多少多くなったとしても、僕は match を使っていこうかな。
else を使わないのであれば使ってもいい、という話をしていた方もいました。

while let条件分岐ループ

let mut stack = Vec::new();

stack.push(1);
stack.push(2);
stack.push(3);

while let Some(top) = stack.pop() {
    println!("{}", top);
}

forループ

let v = vec!['a', 'b', 'c'];

for (index, value) in v.iter().enumerate() {
    println!("{} is at index {}", value, index);
}

let文

let (x, y, z) = (1, 2, 3);

関数の引数

fn print_coordinates(&(x, y): &(i32, i32)) {
    // 現在の位置: ({}, {})
    println!("Current location: ({}, {})", x, y);
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}

18.2 論駁(ろんばく)可能性:パターンが合致しないかどうか

パターンには2つの形態がある

  • 論駁可能なもの
  • 論駁不可能なもの
let Some(x) = some_option_value;

Noneの場合があるため、コンパイル時にエラーが発生

error[E0005]: refutable pattern in local binding: `None` not covered
(エラー: ローカル束縛に論駁可能なパターン: `None`がカバーされていません)
 -->
  |
3 | let Some(x) = some_option_value;
  |     ^^^^^^^ pattern `None` not covered

パターンを使用している構文を変更することで対応できる

if let Some(x) = some_option_value {
    println!("{}", x);
}

18.3 パターン記法

あまり目新しい情報はなかったのでひと安心。
たまに見かけるけど、自分で書くとなったら意識できていなかったであろう ref については押さえておく。

refとref mutでパターンに参照を生成する

ref を使用することで、参照を取り出すことができる。そのためパターンマッチ部分が所有権を奪わずに済む。

let robot_name = Some(String::from("Bors"));

match robot_name {
    Some(ref name) => println!("Found a name: {}", name),
    None => (),
}

println!("robot_name is: {:?}", robot_name);
ホリケンシュウホリケンシュウ

19 高度な機能

19.1 Unsafe Rust

unsave superposers

  • 生ポインタを参照外しすること
  • unsafeな関数やメソッドを呼ぶこと
  • 可変で静的な変数にアクセスしたり変更すること
  • unsafeなトレイトを実装すること

参照やスマートポインタと異なり、生ポイントは

  • 同じ場所への不変と可変なポインタや複数の可変なポインタが存在することで借用規則を無視できる
  • 有効なメモリを指しているとは保証されない
  • nullの可能性がある
  • 自動的な片付けは実装されていない

19.2 高度なトレイト

19.3 高度な型

19.4 高度な関数とクロージャ

19.5 マクロ

ホリケンシュウホリケンシュウ

社内の輪読会で最後まで読み終えることができた。
zennスクラップには全てを書き切ることはできなかったけど、完了にする。

このスクラップは2ヶ月前にクローズされました