読む「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
って書く必要すらなさそうに思うけども・・・??
トレイト境界を使用して、メソッド実装を条件分けする
どのトレイトが実装されているか?の状況に応じて、どのメソッドを実装するかどうかを条件分けできるみたいだ!(知らなかった!)
下記の例だと T
が Display + 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
は、引数 x
と y
のうち、ライフタイムが短い方に揃うらしい。(これは覚えておく必要がありそうだ)
コンパイラはどのようなルールに則ってライフタイムを確認しているか?
コンパイラは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
}
}
11章 自動テストを書く
一旦スキップする
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メソッドで得られる値が可変参照
- iter
イテレータを消費するメソッド
- 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メソッドで得られる値が可変参照
- iter
イテレータを消費するメソッド
- 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トレイトを実装している
- スコープを抜けた際に、スタック領域のポインタと、ポインタが参照しているヒープ領域の両方が解放される
- Derefトレイトを実装している
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個のスレッド = プログラム
-
少し理屈がわからなかったけど、
- グリーンスレッドモデルは、スレッドを管理するために大きなランタイムが必要
- Rustは、Cコードを呼び出したい、ランタイムの肥大化を拒否したい
- Rustはグリーンスレッドモデルではなく
1:1
モデルの実装を提供 - ただし
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--
冒頭あったように mpsc
は mutiple 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スクラップには全てを書き切ることはできなかったけど、完了にする。