Rust
- 2020/04/29 一通り読んで要点をリストアップ
- 2020/05/11 学習に際して作成したコードを含むリポジトリを作成
この資料では「The Rust Programming Language 2nd ed.」の日本語版を読んで学習した内容をまとめる。
学習に際して作成したコードは こちらのリポジトリで公開している。
Qiita の記事によると
- Mozilla が応援している言語
- 2006年から開発が始まった新しめの言語
- 2016年、2017年、2018年の Stack Overflow Developer Survey で「最も愛されているプログラミング言語」で一位を獲得している
- C/C++ と同等の処理速度
- C/C++ の代替えを目指している
- 静的に型が付く、コンパイラ言語
- 静的に変数の寿命もわかり、自動でメモリを解放(GC より速い)
- 関数内部限定での極めて賢い型推論
- C/C++ と比べて極めて安全
- オブジェクト指向ではないし関数型言語でもない新たな概念を持つ
- デフォルトでスレッドセーフが保証された変数達
- C++ のムーブセマンティクスを言語レベルでサポート
- C++ と違い、制約付きのテンプレートがある
- DSL 作成可能で健全なマクロ
- タプル、代数的データ型とマッチ式もあります
- 標準でテスト機能が付いている
- 標準でパッケージマネージャーがある
インストール
- Windows: 公式サイトからインストーラをダウンロードして実行。
- Mac Linux:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
を実行。 - すこしだけ設定が入るが、すべてデフォルトでよい。
VSCode での開発に向けて
-
Rust(rls)
をインストールするとコーディングがやりやすくなる。
一般
- コードセキュア。
- メモリセーフ。
- Rust ではパッケージのことを
クレート
と呼ぶ。 - 式指向言語。
- 例えば
let y = 6
は文であり、値を返さない。
- 例えば
- スコープを定義する
{}
も式となる。 - 式として返す場合は、その後ろに
;
を付けない。 - コメントは
//
(2つ以上のスラッシュ)か/* */
を使う。- ほかにもあるかも。
他の言語との連携
- PyO3: Python と Rust を連携する Rust モジュール。
参考資料
-
https://doc.rust-jp.rs/book/second-edition/
-
The Rust Programming Language 2nd ed.
の日本語版。
-
-
https://doc.rust-jp.rs/book-ja-pdf/book.pdf
- 上記サイトの内容の PDF 版。
-
https://areweguiyet.com/
- Rust 用の GUI の例。
Cargo について
- 新しいプロジェクトの生成は
cargo new prj_name
。-
--bin
は実行的なアプリケーション(バイナリクレート)の作成時につけるらしい。- なくてもフォルダ構成は変化なし。
-
--lib
はライブラリクレートの作成で使用する。
-
- ビルドは生成されたプロジェクトのフォルダ内で行う。
-
Cargo.toml
(toml: Tom's Obvious, Minimal Language) が含まれているフォルダ。
-
- ビルドの方法として次がある。
-
cargo build
: 完全なコンパイル- リリース版を生成する時は
--release
オプションをつける。
- リリース版を生成する時は
-
cargo run
: コンパイルされていればそれを実行。されていなければコンパイルして実行。 -
cargo check
: コード確認とコンパイル可能性のみ検証。
-
- ビルド結果は
~/prj_nama/target/debug/
に保存される。 - プロジェクトのビルド後に
Cargo.lock
が生成される。- このファイルに記述されているバージョンが、以後のビルドで参照される。
-
cargo run
の後に引数を与えるとstd::env::args()
で出力できる引数となる。- 第一引数は実行するプログラムの現在位置からの相対パスである。
Cargo.toml
- 外部クレートの呼び出しは
[dependencies]
のヘッダの下に記述する。
2 & 3 章:記法
変数と定数について
- 宣言されたスコープ内で有効。
- 入れ子のスコープの場合、外側のスコープの変数・定数は内側でも使える。
変数
- 変数の宣言は
let
。 -
let a = 1;
はイミュータブルな(変更不可能な)変数を設定する。 -
let mut a = 1;
はミュータブルな(変更可能な)変数を設定する。 - 変数は何度でも同じ名前で再宣言して使用できる(
Shadowing
)。- 実行的には新しい変数を生成していることになる。
- 不変変数を可変変数として再定義することが可能。
- 不変変数の値を更新(上書き)することが可能。
- Shadowing によって、同一名で別の型の値で更新できる。
定数
- 定数の宣言は
const
。 -
const A: u32 = 10;
で定数を設定する。- 定数の命名はすべて大文字とアンダースコアで行う(Rust の命名規則)。
- 必ず型注釈を付ける。
クレート
- クレートの呼び出しには
extern crate XXX
を使う。
ループ
-
loop
は無限ループを定義する。 -
while 式
は式が true である限り繰り返す -
for x in ...
は for ループ。 - for ループで配列の要素を参照するときは
a.iter()
のようにする。 - for ループでの参照範囲として
Range
型が用いられることも多々ある。 - 文字列等でイテレータを作ることができる。
-
.iter()
を用いる。
-
- Python の enumerate のように、インデックスと要素のイテレータを作ることもできる。
-
.enumerate() -> (index, &item)
となる。
-
- ループの途中で、条件式の中で
return x;
と書くと、ループを抜けた上でx
を返すことができる。 - for ループで参照するオブジェクトの値を変えるときは参照外しを行う。
-
for i in &mut v { *i += 50; }
のように mut キーワードをつける必要がある。 - 参照外し演算子
"*"
を参照変数の前につける。
-
データ型
- Rust は静的型付き言語である。
- 型が明確でない書き方だとエラーが出る。
- 整数には符号付き(
i32
など)と符号なし(正数、u32
など)がある。-
isize
とusize
というのもある。これは OS のビットに依る。
-
- 浮動小数点型には
f32
(float) とf64
(double) がある。 - bool 型、char 型、タプル型、配列型などがある。
- リテラル型は長さを変えられない。
- コンパイル時に実行ファイル内にハードコードされるため。
- 一方で String 型は可変。
- メモリをヒープ領域に各日しているため。
-
.push_str(str)
で String の後ろに str を追加できる。
- 配列はスタック。
- 配列はベクタ型に比べてそれほど柔軟ではない。
- Range 型は
1..4
のように与える。- 最初の数字以上、最後の数字未満の値を出す。
-
.rev()
で数字の並びを反対にできる。
- 複数の型の可能性があるときに、値の後ろに型を書くことで型を指定することができる。
- たとえば
8u8
とすると u8 型の 8 を与える。
- たとえば
関数
-
fn func() {}
で関数を宣言する。- C/C++ のようにヘッダーが必要ない。
-
fn func(x: i32) {}
のように関数の引数には型を明示的につける。 - 配列を参照する時は
fn test(x: &[i32])
のようにする。 -
fn func() -> i32 {}
のように関数の戻り値を与える場合は型指定する。 - 関数の戻り値は、関数を定義するスコープ中の最後の式の値となる。
- タプルの形で、複数の値を返すことも可能。
フロー制御
-
if-else
で if 文が定義できる。- 条件式に括弧は必要ない。
- 条件式は bool 型を返す必要がある。
- if 文の各分岐のことをアームと呼ぶ。
- 各アームが返す値の型は同一でなくてはならない。
クロージャ
- 記法は
let a = |b| {...};
である。- 変数に保存できる。
- 型指定する場合は
let a = |b: u32| {...};
のように記述する。 - 複数の引数をとる場合は
|(a, b)| {...}
のように記述する。
イテレータ
- ベクタ型の場合
.iter()
でイテレータを生成可能。
4 章:所有権
- Rust 独自の概念。
スタックとヒープ
- スタック:Last in, first out、つまり最後に入ったものが最初に取り出される。
- スタック上のデータはすべて既知の固定サイズでなくてはならない。
- ヒープでは可変データを取り扱うことができる(固定をポインタ押し付けている)。
- データのアクセス速度はスタックの方が速い
- サイズが分かっているから?
- サイズが決まっている型はスタック型、サイズが可変の型はヒープ型として扱われる。
- スタック型:たとえば i32, f64 など
- ヒープ型:String など
所有権の扱い
所有権の規則
- Rust の各値は、所有者と呼ばれる変数と対応している。
- 値の所有者は唯一である。
- 所有者がスコープから外れたら、その値は破棄される。
メモリと確保
- String 型では次のようにメモリを確保・使用している。
- 実行時に OS に要求される。
- String 型を使用し終えたら OS に返還する。
- GC を行わない一方で、スコープから外れたら自動的にスコープ内でのみ存在する変数に割り当てられたメモリを解放している。
- 型のサイズが決まっている変数の場合、let 文で代入すると元の変数のコピーが後の変数に入る。
- スタックに保持されており、値のコピーも高速なのでこのような処理が行われる。
- String 型は3つの部品でできている。
- メモリへのポインタ
- 長さ
- 許容量
- String 型のようにヒープを利用しているものは、それを単純にほかの変数に代入すると、ポインタと同じように上記の3つの情報しか渡さない。
- つまり中の文字についてはポインタに割り当てられたヒープ内から動かない。
- これをという。
- String 型を完全にコピーする事もできる。
所有権と関数
- String 型のようにヒープを割り当てられるものは、関数の引数として渡した場合に注意する必要がある。
- 関数の引数へムーブされるため、スコープから外れてしまう。これによって元の変数が使用できなくなる(関数内のスコープで解放されるため)。
参照と借用
- 参照は所有権をムーブせず、値などの情報を借りる(借用)ことを許す。
-
&
を型の頭につける。
-
- 関数中で値を変更したい場合は、次の場所に mut キーワードをつける。
- 変数の宣言のところ(たとえば
let mut s = String::new()
) - 関数の呼び出し部(たとえば
change(&mut s)
のように) - 関数の定義の引数(例えば
fn change(s: &mut String)
)
- 変数の宣言のところ(たとえば
- 特定のスコープ内で、ある変数に対する可変な参照は一つしか持てない。このことは次の状況で起こるデータ競合を防ぐ。
- 2つ以上のポインタが同時に同じデータにアクセスする。
- 少なくとも一つのポインタがデータに書き込みを行っている。
- データへのアクセスを同期する機構が使用されていない。
- 不変な参照は複数あっても良いが、可変の参照と同時に存在することはできない。
- 所有権の利点として、ダングリングポインタの生成が未然に防がれる。
- ある関数で生成された参照を戻り値として返すと、その分のメモリをもつポインタは存在するが、メモリは解放されてしまっている。このようなポインタをダングリングポインタという。
- つまり、特定のスコープ内で生成された変数の参照をスコープ外に返すことはできない。
スライス型
- スライスは所有権がないが、データの一部を参照できるデータ型である。
- 変数の後ろに
[0..5]
のように書いて参照する。 - 先頭からスライスする場合は
[..5]
のように書いてよい。 - 末尾までスライスする場合は
[5..]
のように書いてよい。 - 両方を省略して全体をスライスすることもできる:
[..]
。 - 文字列スライスの型は
&str
で表現される。 - 配列のスライスもある。
- 例えば
let a = [1,2,3,4,5];
のスライスは&a[..]
のように取れる。この時の型は&[i32]
。
- 例えば
5 章:構造体
- Rustには class が存在しないが、構造体によって class と同様の実装が可能。
-
struct
によって定義する。 - Python の
dict
のようにフィールドを定義する。- いわゆる(?)
key:value
のスタイル。 -
struct User {name: String, address: String}
のように。
- いわゆる(?)
- 構造体のインスタンスを可変にする場合は、インスタンスそのものを可変にしなければならない。
- つまりフィールドの一部のみを可変にすることはできない。
- 関数で構造体を生成して返す時、フィールドと引数の名前を同じにすると、省略記法が使える。
- 構造体中の一部の値を利用して新しいインスタンスを生成することが可能。
-
..
を用いて残りのフィールドを参照できる。
-
- 名前月フィールドのないタプル構造体が生成できる。
-
srtuct Color(i32,i32,i32);
のように。
-
- 一切フィールドのない構造体も定義可能。
-
struct Test ();
のように。
-
- 構造体のフィールドにリテラル型やスライス型を持たせるにはライフタイムを定義する必要がある。
- これは構造体が生きている間は、構造体が全ての内部のフィールドの値を所有している必要があることによる。
- 例えば列挙型の中に匿名の構造体を持たせることが可能。
メソッド記法
- 構造体に対して
impl
キーワードによってメソッドを定義することができる。- これによっていわゆる class のような振る舞いをするように構造体を拡張できる。
- 例えば
impl Rect {fn area ...}
のように。
- 自身を参照するときは、メソッドの引数に
&self
を入れる。-
fn area (&self) {...}
のように。 - Python の
def area(self)
に似てる。
-
- メソッド呼び出しでは自動参照と参照外しが行われる。
-
p1
をインスタンスとして、p1.area()
と(&p1).area()
は同じ動作をする。
-
- impl ブロック内で self を引数にとらない関数を定義することもできる。
- このような関数は関連関数と呼ばれている。
- 一つの構造体に対して impl ブロックを複数定義することができる。
- 例えば複数の関数を別個の impl ブロック内に定義するといった使い方が可能。
6 章:Enum とパターンマッチング
- 列挙型は
enum
キーワードで定義する。 - 列挙型の中の値を列挙子と呼ぶ。
- 各列挙子に情報を付与できる。
- 例えば
enum IpAddr {V4(String), ...}
のようにすると String 型の情報を付与できる。 - 構造体でも列挙型でもよい。
- 例えば
- それぞれの列挙子に付与する情報の型は揃っていなくてもよい。
- enum にもメソッドを定義することができる。
- すべての列挙子でメソッドを実行できる。
-
Option<T>
という列挙型は、値が存在するか不在化という概念を与える。- null を実装しない代わりに使える列挙型。
match によるフロー制御
-
match
演算子を用いて C/C++ のswitch
のようなフロー制御が可能。 - すべてのパターンを網羅する必要がある。
-
_
によって、列挙されたパターン以外のすべてを扱うことができる。
if let による簡潔なフロー制御
-
if let
記法を用いることで、一つのパターンにマッチする値を取り出すことができる。-
if let Some(3) = some_value {...}
のように記述する。
-
- この記法では包括性チェックがないので適切に使用する必要がある。
7 章:モジュール
-
mod
キーワードを用いて次のことが可能。- モジュールを定義する。
- 同じディレクトリ中のモジュールをインポートする。
- モジュール内のコードは、この宣言の直後の
{}
または別のファイルに存在する。 - 標準では、関数、型、定数、モジュールは非公開である。
-
pub
を用いることでこれらの要素を公開することが可能。 - 他のファイルでモジュールまたはモジュール内の定義を参照する時は
use
を用いる。 - 希望の関数が2モジュール以上ネストされている場合、関数ではなくその親モジュールをスコープに導入するのが慣習的。
mod とファイルシステム
- モジュールを作成するときはライブラリクレートを作成する。
- モジュール内に別のモジュールを定義することができる。
- モジュール内のサブモジュールを定義したいときがあるが、たとえば次のようにフォルダ構成を行うことでそれを達成できる。
/src: メインモジュールの親ディレクトリ client.rs: lib 用のサブモジュール lib.rs: モジュールのメインファイル /network: lib用のサブモジュール mod.rs: network モジュールのメインファイル server.rs: network 用のサブモジュール
- おそらく
lib.rs
とmod.rs
という命名が必須。
- おそらく
pub キーワードによる公開・非公開の制御
- デフォルトでは private 扱い。
-
pub
キーワードをつけることで public にできる。 - モジュールと関数のそれぞれで公開・非公開を設定できる。
- 要素の公開性については次の規則がある。
- 要素が public => どの親モジュールからでもアクセス可能。
- 要素が private => 直接の親モジュール(つまり要素が入っているモジュール)とその親のサブモジュールのみアクセス可能。
異なるモジュールの名前を参照するには
-
use
キーワードを用いることで、あるモジュールにネストされたモジュールを現在のスコープに持ってくることができる。- 例えば
use a::series::of;
とすると、of
を現在のスコープに持ってくる。 - 関数も参照可能。
- enum 型やその列挙子も参照可能。
- 例えば
-
"*"
を使うことで、ある名前空間の要素をすべて一度にスコープに導入できる。- glob と呼ばれる。
- Python での
from my_module import *
における"*"
の役割に類似する。
- スコープの中では、パスは常に現在のモジュールに対して相対的に判断される。
- use 文ではクレートのルートに相対的になる。
-
::
を頭につけて、ルートから持ってくることができる。 -
super
を頭につけて、一つ上からもできる。
8 章:一般的なコレクション
- コレクションと呼ばれるデータ構造たちが含まれる。
ベクタ型
- メモリ上に値を隣り合わせに並べる単独のデータ構造。
- 同じ型の値のみ保持できる。
- テンプレートは
Vec<T>
。- 例えば
let v: Vec<i32> = Vec::new();
のように生成する。
- 例えば
-
vec!
はベクタ型を生成するマクロ。-
let v = vec![1, 2, 3];
のように使う。
-
-
.push(T)
で値を追加できる。- 既に格納されている値と同じ型をもつ値のみ追加可能。
- ベクタの要素は参照によって読むことができる。
- 不変変数によって参照されている場合、被参照ベクタの値を変えることはできない。
-
.get(index)
メソッドを用いると安全にアクセスできる。- もし index が不適切であった場合に Option の None を返す。
- for ループでベクタの要素に順番にアクセス可能。
-
for i in &v {}
のように参照にする。
-
- 列挙型を用いることでベクタの要素に複数の型を持たせることができる。
- ある列挙型 A の列挙子はすべて同一の列挙型 A をもつため。
文字列
- Rust での文字列は通常 String と文字列スライス &str を指す。
- どちらも UTF-8 でエンコードされている。
- 標準ライブラリには OsString、OsStr、CString、CStr などが含まれる。
-
.push_str()
は文字列スライスまたは String を追加できる。- 追加される String から所有権は奪われない。
-
.push()
は一文字(char 型)を追加できる。 -
文字列には &str しか追加できない。
- "+" 演算子の仕様による:
fn add(self, s2: &str) -> String
。
- "+" 演算子の仕様による:
- "+" 演算子では
self
から所有権を奪っているため、第一引数の所有権が代入先に移る。-
let s3 = s1 + &s2;
では s1 から s2 への参照が行われ、s1 の所有権が s3 に移る。
-
- "+" 演算子は第二引数より先が &String であった場合 &str に型強制(参照外し型強制)する。
-
format!
マクロは所有権を移さずに文字列を生成する。 - 文字列はインデックスによるアクセスをサポートしていない。
- String は
Vec<u8>
のラッパーらしい。- そのため
.len()
を用いると UTF8 での長さを返す。
- そのため
- スライスによるアクセスは可能だが、文字の切れ目以外の部分でスライスしようとするとエラーが出る。
-
.char()
を用いることで文字列の各要素にアクセスすることができる。
ハッシュマップ
-
HashMap<K, V>
は Rust での標準のハッシュパップ型である。 -
use std::collections::HashMap;
でインポートすることで使用可能。 - キーの型はすべて同じである必要がある。
- 値の型はすべて同じである必要がある。
-
.insert(k, v)
で key-value pair を追加する。 - タプルのベクタに対して
.collect()
メソッドを使用することでもハッシュマップの生成が可能。- 例えば
let source: HashMap<_, _> = keys.iter().zip(values.iter()).collect();
のように生成する。 - HashMap の型の部分にアンダースコアを入れることで、ベクタのデータ型からハッシュマップが含む型を推論させることができる。
- 例えば
-
.insert()
によって所有権がムーブする。 -
.insert()
の引数に参照を与えることができる。- ハッシュマップが有効である間は、被参照変数も有効でなくてはならない。
-
.get(key)
でkey
に対応する値を取り出すことが可能。- ヒープ型変数の場合は
key
に参照を入れるべき。
-
Option<>
(Some<>
orNone
) を返す。
- ヒープ型変数の場合は
- for ループでは参照の形で中身を
(key, value)
のタプルに展開して走査できる。 - 同じキーに insert すると値が上書きされる。
-
.entry(key)
はkey
に値があるかどうか確認できる。 -
.or_insert(value)
は.entry(key)
と組み合わせてkey
に値がない場合にvalue
を設定する。 - HashMap は標準で暗号学的に安全なハッシュ関数を使用している。
- hasher(BuildHasher トレイトを実装する型)を指定することで、別のハッシュ関数に切り替えられる。
9 章:エラー処理
- Rust では、エラーは回復可能なエラーと回復不能なエラーに大別される。
- Rust には例外が存在しない代わりに次の機能が用意されている。
- 回復可能なエラーに戻り値として
Result<T, E>
が用意されている。 - 回復不能なエラーに
panic!
が用意されており、実行を中止する。
- 回復可能なエラーに戻り値として
panic! による処理
- パニックが発生するとプログラムの巻き戻しが起こる。
- スタックを遡り、各関数のデータを片づけていく。
- 上述の片づけをせずに異常終了させる場合は Cargo.toml に次を追加する。
-
[profile.X]
(X = release, debug?) 欄にpanic = 'abort'
。
-
-
RUST_BACKTRACE
環境変数を設定することで、パニックが発生した時のトレースバックの表示を変更できる。- どこで設定できる?
Result<T, E>
-
enum Result<T, E> { Ok(T), Err(E), }
の列挙型である。 - ビルドしたときにエラーが出ると
T, E
の中身が表示される。 - match 文に Result を返すような別の match 文を追加して、順にエラー対処することが可能。
- 例えば
path
で指定されるファイルを読もうとして失敗したらpath
をもつファイルを作成するというような処理。
- 例えば
- Err の種類(
std::io::ErrorKind
)によって処理を変えることが可能。- Python の try-except に類似する実装が可能。
-
std::io::ErrorKind
は標準のエラー列挙子を与える列挙型。 -
Result<T, E>
型のヘルパーメソッドである.unwrap()
関数によって次の処理が可能。- OK => ハンドルを返す。
- Err => panic! を呼ぶ。
- 同じくヘルパーメソッドである
.expect(str)
関数によって、panic! のメッセージを指定して unwrap と同じ処理が可能。 -
unwrap_or_else(|err|{...})
によって独自のエラー処理を定義可能。 - エラーの委譲:関数内でのエラーを呼び出し元に返すこと。
- 関数の戻り値として
Result<T, E>
型を返す実装がその例。
- 関数の戻り値として
- エラーの委譲のショートカットとして
?
演算子が用意されている。関数がエラーになったときにその値を呼び出し元に返してくれる。- たとえば
f.read_to_string(&mut s)?;
のように使う。 - 既定のエラー処理をする場合に便利。
- 連続して使える:
File::open(path)?.read_to_string(&mut s)?;
のように。 -
Result<T, E>
型を返す関数でしか使用できない。
- たとえば
-
?
演算子は match 文とは異なり、標準ライブラリのFrom
トレイトで定義されるエラー値を返す。- 現在の関数の戻り値で定義されているエラー型に変換されてエラーが返される。
panic! と Result<T, E>
型による対応の使い分け
- サンプルコードでは panic! を使用しない方が良い。
- アプリケーションにエラーを処理してほしい方法へのプレースホルダーであると解釈される可能性があるため。
- プロトタイプの段階では unwrap や expect が良さそう。
- 具体的な処理の実装の前に、エラーが発生すべきところに容易に挿入できるという点で。
- プログラマ側で情報を持っている場合(たとえばハードコーディング)は unwrap などを使うのが良さそう。
- エラーの検証のために型を自作するという手もあり。
10 章:ジェネリック型、トレイト、ライフタイム
- Rust にもジェネリクスという抽象的な扱い(テンプレートとか)がある。
-
Option<T>
,Vec<T>
,HashMap<K, V>
,Result<T, E>
など。
-
関数の抽出
- コードから関数を抽出する手順として次が挙げられている。
- 重複したコードを認識するv。
- 重複コードを関数本体に抽出し、コードの入力と戻り値を関数シグニチャで指定する。
- 重複したコードを代わりに関数を呼び出すように更新する。
ジェネリックなデータ型
- 関数でジェネリック変数
T
を与えて実装するにはひと工夫必要。 - 構造体の場合は
Point<T, U, ...>
のように型T, U, ...
を与えることができる。 - 列挙型の場合はすでに見たように
Option<T>
やResult<T, E>
のように定義可能。 - メソッドの定義の場合
impl<T>
のように impl キーワードの後ろにジェネリックな型を宣言する必要がある。- あえて型を固定宣言することによって、型固定の構造体にだけメソッドを定義することもできる。
- メソッドの中でさらにジェネリックなメソッドを定義することで、別のジェネリック型をもつオブジェクトを生成することも可能。
- 単相化:コンパイル時に使用されている具体的な型を入れることで、ジェネリックなコードを特定のコードに変換する。
- これによりコードのパフォーマンスの向上が図られている。
トレイト
-
トレイト は共通の振る舞いを抽象的に与える機能である。
- ほかの言語でのインターフェースに似ている。
- trait キーワードを用いてトレイトを定義できる。
- たとえば
pub trait Trait1 { fn func(); }
のように。
- たとえば
- 構造体などにトレイト内のメソッドを実装する場合は
impl Trait for Struct { fn method() }
のように記述する。 - トレイトが public の場合、実装されているメソッドも public になる。
- トレイト内のメソッドのデフォルト実装を用意することができる。
- デフォルト実装内でほかのデフォルト実装がないメソッドを呼び出すことが可能。
- その場合、型にトレイトを実装する際に呼び出されるメソッドの実装が必要となる。
- トレイト境界により、ジェネリックな型を制限し、型が特定のトレイトや振る舞いを実装するものに制限することができる。
-
fn func<T: Copy>(v: T) {}
とすると Copy トレイトを実装しているジェネリックな型のみ使用可能となる。
-
- トレイト境界を使用してメソッド実装を条件分けすることができる。
-
impl<T: Display + PartialOrd> Pair<T> {}
とすると Display と PartialOrd の両方を実装する T をもつ Pair 型に対してメソッドが実装される。
-
- トレイト実装もトレイト境界によって条件分けできる。
- ブランケット実装とよばれる。
- 例えば ToString トレイトは Display トレイトを実装している型に対して定義されている。
- トレイト境界は where キーワードで与えることも可能。
-
fn test<T>() where T: Display {}
のように。
-
ライフタイム
- ライフタイムの主たる目的はダングリング参照を回避することである。
- どの参照を返すのかわからないメソッドでは、返す参照にかかわるライフタイムの注釈を与える必要があるらしい。
- ライフタイム注釈は次のように記述する。
-
&'a i32
: 明示的なライフタイム付きの参照 -
&'a mut i32
: 明示的なライフタイム付きの可変参照
-
- ライフタイムは小さい方に合わせられる。
- 構造体に参照を保持させる場合、構造体の定義と全参照にライフタイム注釈をつける必要がある。
- 構造体の参照メンバが保持する参照よりも構造体自身が長生きしないことを保証している。
- ライフタイム省略規則により、コンパイラが考慮する一連の特定のライフタイム注釈のケースについては、プログラマが明示的に記述する必要がない。
- 参照である各引数は、独自のライフタイム引数を得る。
- 引数ごとに別々のライフタイムが与えられる。
- 一つだけ入力ライフタイム引数がある場合、そのライフタイムがすべての出力ライフタイム引数に代入される。
-
&self
や&mut self
が引数にある場合は self のライフタイムがすべての出力ライフタイム引数に代入される。
- 参照である各引数は、独自のライフタイム引数を得る。
- 上記の省略規則に則ってライフタイムが与えられた結果、参照戻り値のライフタイムが与えらえていない場合、エラーが生じる。
- この場合は明示的にライフタイムを示す必要がある。
- 学習したての頃は、コンパイルしてライフタイムに関するエラーが出たらその都度対応するのが良さそう。
- ライフタイムのあるメソッドの実装を行う場合、ジェネリックな型引数と同じ記法を用いる。
-
impl<'a> ImportantExcept<'a> {}
のように書く。
-
- 静的ライフタイム :プログラム全体の期間を示す。
- 文字列移リテラルはすべて 'static ライフタイムを持っている。
- プログラムのバイナリに直接格納されているため。
- つまり
let s: &'static str = "I have a static lifetime.";
という風に扱われている。
11 章:自動テストを書く
テストの記述法
- 典型的なテスト関数の本体は以下の動作を行う。
- 必要なデータや状態を設定する。
- テスト対象のコードを実行する。
- 結果が想定通りかどうかアサーションする。
- Rust では test 属性や should_panic 属性などで対応できる。
- 期待される正しい値を返すことの確認のほかに、想定通りのエラー状態を扱っていることを確認することも重要。
テスト関数の解剖
- Rust におけるテストのうち最も単純なものは test 属性で注釈された関数。
-
cargo test
コマンドは現在ディレクトリ内のすべてのテストを実行する。 -
assert!(func)
マクロは bool を返す関数func
に対してテストする。 -
assert_eq!(a, b)
マクロはa, b
の間の等価性をテストする。 -
assert_ne!(a, b)
マクロはa, b
の間の非等価性をテストする。 - 上記マクロの第二引数としてカスタムの失敗メッセージを与えることができる。
- 第三引数以下には、第二引数の文字列に代入する変数を与える。
-
#[should_panic]
属性をつけると、テストする関数内でpanic!
マクロが呼び出されたときにテストを通過させる。- この属性に
expected
引数として文字列を与えると、panic 時に想定されるメッセージに指定した文字列が入っているかどうかを確かめてくれる。
- この属性に
テストの実行され方を制御する
-
cargo test
には二種類のオプションがある:-
cargo test
そのものにかかるオプション:--<option>
で指定。 - 生成されたテストバイナリに対してかかるオプション:
-- --<option>
で指定。
-
-
cargo test
のオプションは--help
で参照可能。 - 生成されたテストバイナリへのオプションは
-- --help
で参照可能。 -
-- --test-threads
はテストに使用するスレッドの数を制限できる。- Rust のテストではマルチスレッドで並列実行している。
-
cargo test
の第二引数に関数の名前を入力することで、テストしたい関数だけをテストできる。- 関数の名前の一部を入力すると、その一部を含む名前をもつ関数すべてをテストする。
-
#[ignore]
属性をつけるとそのテスト関数をテスト対象から外すことができる。
テストの体系化
- Rust のコミュニティでは単体テストと結合テストの2つのカテゴリでテストをとらえている。
- 単体テスト:個々のライブラリをテストする。
- 結合テスト:ライブラリ外でのテストで、ライブラリの様々な部分が正常に動作しているかをテストする。
単体テストについて
- 単体テストの準備は次のように行う。
- テスト対象となるコードとともに src ディレクトリの書くファイルに設置する。
- 各ファイルに tests という名前のモジュールを作る。
- tests モジュールにテスト関数を含ませる。
- tests モジュールを
cfg(test)
で注釈する。
-
#[cfg(test)]
注釈はcargo test
を実行したときだけテストコードをコンパイルし、実行することを指示する。- cfg = configuration
- したがってライブラリのビルドに含まれない。
- Rust では非公開関数をテストすることができる。
結合テストについて
- Cargo では
tests
ディレクトリを特別に扱い、cargo test
を実行した場合にのみこのディレクトリのファイルをコンパイルする。 - 結合テストの準備は次のように行う。
-
tests
ディレクトリを作成する。 - tests ディレクトリ中に .rs ファイルを作成し、そのファイル内でテストしたいライブラリ(クレート)を呼び出す。
-
extern crate mylib;
のように。
-
- 対象ライブラリ中の関数を呼ぶテスト関数を記述し、
#[test]
注釈をつける。
-
-
--test <filename>
オプションをつけることで<filename>
で指定されたファイルだけを結合テストする。 - tests ディレクトリ内に相異なる複数の結合テストファイルがあり、それらが共通の関数を呼び出す場合、共通関数を tests ディレクトリ内のモジュールとして定義するとよい。
- たとえば
tests/common/mod.rs
を作成してその中に共通利用の関数を定義する。
- たとえば
12 章:入出力プロジェクトの開発
- この章ではこれまで学習した内容を応用してコマンドラインプログラムを構築することを目指す。
-
grep
を作成する。- 引数としてファイル名と文字列を受け取る。
- ファイルを読み込んで、ファイル内で文字列引数を含む行を探す。
- 見つかった行を出力する。
- Rust コミュニティのメンバーの一人である Andrew Gallant が
ripgrep
という高速かつ全機能を持った grep 関数を作成した。
コマンドライン引数を受け付ける
-
std::env::args()
関数は Rust の標準ライブラリ関数で、コマンドライン引数の値を読み取る。 -
std::env::args()
関数は、引数に不正なユニコードが含まれる場合に panic! を呼び出す。 - 不正なユニコードを含む引数を受け付ける必要がある場合、
std::env::args_os()
を使用すればよい。
ファイルを読み込む
-
std::fs::File
がファイルを読み込むためのトレイト(?) -
std::io::prelude::*
を導入することで、ファイル入出力に有用なトレイトを呼び出すことができる。 -
f.read_to_string(&mut contents)
によってファイルインスタンスf
中の内容を String としてcontents
に保持できる。
リファクタリングによるモジュール性とエラー処理の向上
- main 関数の仕事を減らす、つまり関数として仕事を取り出す。
- 設定用変数をまとめて一つの構造に押し込める。
- たとえば設定用の構造体を定義して用いる。
- expect エラーを改善する。
- エラーの処理コードを一か所に集める。
- 各関数を Result 型で返すようにして main でエラー処理できるようにする。
- Rust コミュニティによって、main が肥大化した場合にバイナリプログラムの仕事・責任を分別するガイドラインが次のように定義されている。
- プログラムを main.rs と lib.rs に分けて、ロジックを lib.rs に移動する。
- public/private について注意深く扱う必要がある。
- コマンドライン引数の解析ロジックが小規模な限り main.rs に置いてもよい。
- コマンドライン引数の解析ロジックが複雑化し始めたら main.rs から lib.rs へと抽出する。
- プログラムを main.rs と lib.rs に分けて、ロジックを lib.rs に移動する。
- 上述のガイドラインによって main 関数に残る仕事は次に限定される。
- 引数の値でコマンドライン引数の解析ロジックを呼び出す。
- ほかのあらゆる設定を行う。
- lib.rs の
run
関数を呼び出す。 -
run
がエラーを返したときに処理する。
テスト駆動開発
- テスト駆動開発(Test-Driven Development, TDD)は以下の手順に従う。
- 失敗するテストを書き、実行して想定通りの理由で失敗することを確かめる。
- 十分な量のコードを書くか偏光して、新しいテストを通過するようにする。
- 追加または変更したばかりのコードをリファクタリングし、テストが通り続けることを確認する。
- 手順1から順に繰り返す。
- TDD によりコードデザインも駆動できる。
環境変数を取り扱う
-
std::env::var(str)
関数を用いることでstr
に対応する環境変数の値を取り出すことができる。- 返ってくるのは Ok(value) または Err である。
標準出力ではなく標準エラーにエラーメッセージを書き込む
- 多くの端末では二種類の標準エラー出力がある:
-
stdout
:普通の情報用の標準出力 -
stderr
:エラーメッセージ用の標準エラー出力
-
-
eprintln!
マクロで標準エラーストリームに出力することができる。
13 章:イテレータとクロージャ
- クロージャ:変数に保存できる関数に似た文法要素
- イテレータ:一連の要素を処理する方法
クロージャ:環境をキャプチャできる匿名関数
- Rust において、クロージャは次の機能を持つ匿名関数。
- Python の lambda 式に類似している。
- 変数に保存する。
- 引数としてほかの関数に渡すことができる。
- 呼び出されたスコープの値をキャプチャできる。
- クロージャで定義された関数の呼び出しは1箇所のみになる。
- クロージャは 変数に保存することができる。
- クロージャでは通常短い文脈で使用されるため、型推論が行われる。
- 型注釈をつけるのであれば
|num: u32| -> u32 {...}
のように引数につけることもできる。 - 最初の呼び出しの時点で型推論が行われるため、二回目以降の呼び出しでは一回目と同じ型でしか使用できないことが保証される。
- 型注釈をつけるのであれば
- クロージャやクロージャの戻り値を保持する構造体が作成できる。
-
メモ化(memoization)
または遅延評価(lazy evaluation)
として知られるパターン。
-
- クロージャを保持する構造体を作成するためにはクロージャの型を指定する必要がある。
- 構造体の性質による。
- クロージャは以下のいずれかのトレイトを実装している。
-
Fn
:環境から値を不変で借用する。 -
FnMut
:環境から値を可変で借用する。 -
FnOnce
:環境から値の所有権をムーブして使用する。
-
- 上記のどのトレイトを使用するかは、指定がなければ推論される。
- スコープないで FnOnce トレイトを使用する場合は move キーワードを使用する。
-
let a = move |z| z == x;
のように書く。
-
イテレータ:一連の要素の処理
- ベクタ型の場合
.iter()
でイテレータを生成可能。 - for ループでイテレータの内部の要素を走査できる。
- Iterator トレイトには type キーワードで定義される
Item
とそれをSelf::Item
で参照して返すnext
メソッドが実装されている。- これらを関連型という。
- 次のようにイテレータを生成するメソッドが複数ある。
-
.iter()
:不燃参照へのイテレータを生成する -
.into_iter()
:対象の所有権を奪う -
.iter_mut()
:可変参照へのイテレータを生成する
-
- 消費アダプタ:呼び出しによりイテレータの要素を消費するメソッド。
-
.next()
や.sum()
などがある。
-
- 消費アダプタ以外のメソッドはイテレータアダプタと呼ばれる。
- イテレータをほかの種類のイテレータに変換する。
-
.map(|x| ...)
は括弧内のクロージャを各要素に実行した結果のイテレータを返す。 -
.collect()
はイテレータを消費してベクタを生成する。 -
filter(|x| ...)
はクロージャを true にする要素からなるイテレータを返す。 - 環境をキャプチャするクロージャを用いてイテレータを生成することが可能。
- 自作でイテレータを実装するときは Iterator トレイトを実装する。
-
impl Iterator for Counter {...}
のように定義する。 -
type Item
とfn next(&mut self) -> Option<Self::Item> {}
を必ず実装する。
-
-
.zip(other)
はほかのイテレータ other の要素との組を持つイテレータを返す。- 自身と other のどちらかが None の要素を持つ場合はその部分をスキップする。
12 章で構築したプロジェクトの改善
- イテレータを使用して、12 章の
Config
構造体とsearch
関数を改善できる。-
Config
については引数のstd::env::args()
がイテレータなのでこれを利用する。 -
search
についてはイテレータにフィルタを適用することができるのでこれを利用する。
-
ループとイテレータのパフォーマンス比較
- わずかにイテレータの方が速い様子。
- イテレータはゼロ代償抽象化の一つらしい。
14 章:Cargo と Crates.io について
リリースプロファイルでビルドをカスタマイズする
- Cargo の主なプロファイル
- dev プロファイル:デフォルトで使用される。
- release プロファイル:
cargo build --release
で使用可能。
- Cargo.toml において
opt-level
は最適化レベルを設定する。-
[profile.dev]
または[profile.release]
セクションで設定可能。 -
opt-level = 0
だと最適化が行われない。
-
- ほかの設定は Cargo ドキュメンテーション を参照するべし。
Crates.io にクレートを公開する
- Crates.io はパッケージ登録所である。
- Crates.io にパッケージを公開することができる。
ドキュメンテーション
- よく使われるのが次の二種類?
- 3連スラッシュ(
///
)を使用して記述:このコメントに続く要素(関数など)の説明 -
//!
を使用して記述:このコメントを含む要素(クレートなど)の説明
- 3連スラッシュ(
- Markdown 記法が使用できる。
- 最初の行に関数などの説明を記述する。
- 次のセクションがよく使用される。
-
Examples
:使用法 -
Panics
:panic!
を呼び出す可能性のある筋書き -
Errors
:Result を返す場合に起こりうるエラーの種類とそれらを引き起こす条件 -
Safety
:関数を呼び出すのに unsafe な場合にその理由を説明し、関数が呼び出し元に保持していると期待する不定条件を記述する
-
-
cargo doc
を実行すると HTML ドキュメントが生成される。-
rustdoc
を実行して HTML ドキュメントを生成し、target/doc
ディレクトリに配置している。 -
--open
オプションをつけると既定のブラウザで HTML を開くことができる。
-
公開 API のエクスポート
- 深い階層にある要素をエクスポートする際に、 pub use キーワードによって再エクスポートして、非公開構造とは異なる公開構造を構築できる。
- 再エクスポートされた要素はドキュメンテーション上で
Re-export
の欄に掲載される。
- 再エクスポートされた要素はドキュメンテーション上で
クレートの公開までの作業
- Crates.io のアカウントを作成する。
- Github のアカウントで可能。
- API トークンを取得する。
-
cargo login <token>
を実行する。 - Cargo.toml ファイルを編集し、公開用にする。
-
[package]
セクションに必要な情報を記述する。たとえば以下のフィールド。- name
- version
- authors
- description
- license
-
-
cargo publish
で公開する。
既存のクレートの更新
- Cargo.toml の version の値を変更して再公開すればよい。
- セマンティックバージョンルールに従うべき。
特定のバージョンの削除
- バージョンを取り下げることで、新規プロジェクトがそのバージョンに依存することを防ぐ。
-
cargo yank --ver xxx
コマンドで可能。-
--undo
をつけると再度公開できる。
-
- あくまで取り下げ作業であり、Crates.io からコードが削除されるわけではない。
Cargo のワークスペース
- ワークスペースは同じ Cargo.lock と出力ディレクトリを共有する一連のパッケージを指す。
- パッケージを含める親ディレクトリを用意し、そこに
[workspace]
セクションとmembers = [...]
メタデータをもつ Cargo.toml を作成する。 - その中にパッケージを作成していく。
- ワークスペースをビルドするとワークスペース内にのみ target ディレクトリが生成され、それを共有する。
- そこに各パッケージをコンパイルして生成されたファイルが格納される。
- ワークスペース内であるバイナリクレートを別のライブラリクレートに依存させる際は、前者の toml に後者へのパス依存を追加する。
- ワークスペースから中のバイナリクレートを実行する時は
-p
オプションとパッケージ名を入れて cargo run を実行する。 - ワークスペース内の各パッケージで同じ外部クレートに依存している場合、コンパイル時にバージョンの差異を調べて特定の(新しい方の?)バージョンの外部クレートの情報を Cargo.lock に追加する。
- test コードはワークスペースをテストビルドすることで実行できる。
- 特定のクレートのテストを実行する場合は
-p
オプションをつける。
- 特定のクレートのテストを実行する場合は
cargo install で Crates.io からバイナリをインストールする
- cargo install でインストールされるバイナリはすべてインストールのルートディレクトリの
bin
ディレクトリに保持される。- rustup を使用しており、かつデフォルトの状態では
$HOME/.cargo/bin
がルートディレクトリ。 - ルートディレクトリが
$PATH
に含まれていないとインストールしたバイナリが使用できない。
- rustup を使用しており、かつデフォルトの状態では
独自のコマンドで Cargo 拡張する
-
cargo-something
というバイナリが$PATH
に含まれているときcargo something
によってそのバイナリを実行できる。-
cargo --list
で列挙される。
-
15 章 スマートポインタ
- ポインタ:メモリのアドレスを含む変数。何らかのデータを参照する。
- Rust では参照(
&
)がポインタの一つ。- データを借用する機能をもつ。
-
スマートポインタ:ポインタにメタデータと機能が追加されたもの。
- C++ にも存在する。
- String, Vec<T> もスマートポインタ。
- Box<T>: ヒープに値を確保する。
- Rc<T>: 参照カウント型。
- Ref<T>, RefMut<T>: 実行時に借用規則を強制する。
- スマートポインタは構造体を使用して実装される。
- スマートポインタは次のトレイトを実装している。
- Deref: スマートポインタのインスタンスが参照のように振る舞うことを可能にする。
- Drop: スマートポインタのインスタンスがスコープを外れたときに実行されるコードをカスタマイズできる。
-
参照カウント式スマートポインタ:データに複数の所有者を持たせることができる。
- すべての所有者がいなくなったらデータがメモリから解放される。
- Box, Rc, RefCell の違いが次のように与えられている。
- Rc<T>: 同じデータの複数の所有者を持たせる。
- Box<T>: 不変または可変借用をコンパイル時に精査する。
- RefCell<T>: この型が不変でもその内部の値を可変化できる。
Box<T>
- Box<T> は次の場面で活躍する。
- コンパイル時にサイズを知ることができない型があり、正確なサイズを要求するコンテキストでその型の値を使用するとき。
- 多くのデータがあり、所有権を移したいがそうするときにデータがコピーされないことを確認するとき。
- 値を所有する必要があり、特定の型ではなく特定のトレイトを実装する型であることのみ気にかけているとき。(トレイトオブジェクトに関連する。)
- Box は既知のサイズを持つ。
- 再帰的な型の定義に Box を挟むことで、再帰的な型を存在させることができる。
- 例えば
コンスリスト
を定義できる。 - 関節参照(値を直接格納する代わりに、データ構造を変更して値へのポインタを格納する)の考えを使用している。
- 例えば
Deref トレイト
- Deref トレイトを実装すると、参照外し演算子("*")の振る舞いをカスタマイズできる。
- Deref トレイトの実装時、次の関連型を定義する必要がある。
type Target = T;
fn deref(&self) -> &T {&self.0}
- 関数によってはコンパイラが参照外し型強制と認識して処理する。
- DerefMut トレイトを使用すると、可変参照の "*" 演算子をオーバーライドすることが可能。
- 次の場合に型やトレイト実装を見つけたときに、コンパイラが参照外し型強制を行う。
-
T: Deref<Target=U>
のとき、&T から &U。 -
T: DerefMut<Target=U>
のとき、&mut T から &U。これは前者と同じ。 -
T: Deref<Target=U>
のとき、&mut T から &U。これは可変参照を不変参照に型強制している。
-
Drop トレイト
- Drop トレイトは値がスコープを抜けそうになった時に起こることをカスタマイズできる。
- Box は Drop をカスタマイズして Box が指しているヒープの領域を解放している。
- 変数は生成された順番と逆の順序でドロップされる。
-
std::mem::drop
を使うと値を強制的にドロップさせることが可能。
Rc<T>:参照カウント方式のスマートポインタ
- Rc<T> は値が使用中かどうかを決定する値への参照の数を追跡する。
-
Rc::clone(&x)
では参照を与えて、Rc の中で参照をカウントしている。- clone をするごとにカウントが増える。
-
Rc::strong_count(&a)
によってa
の参照数を出力する。
RefCell<T> と内部可変性パターン
- 内部可変性:対象のデータへの不変参照がある時でさえもデータを可変化できる Rust でのデザインパターン。
- データ構造内で unsafe コードを使用する必要がある。
- RefCell<T> 型は内部可変性パターンに従っている。
- RefCell<T> 型は保持す津データに対して単独の所有権を表す。
- RefCell では実行時に借用規則の不変条件が強制される。
- 従って借用規則が敗れているとプログラムがパニックして終了する。
- 参照と Box では借用規則の不変条件がコンパイルに強制されている。
- ほかのポインタと同じく
new(...)
によってインスタンスを生成する。 -
.borrow()
で RefCell 内の値を不変で参照する。 -
.borrow_mut()
で RefCell 内の値を可変で参照する。
循環参照によるメモリリークの可能性
- Rust でも循環参照を生成する可能性があるので注意が必要。
- RefCell を使用した例が挙げられている。
-
Weak<T>
を用いることで循環参照を回避できる可能性がある。- いわゆる弱参照。
-
Rc::downgrade(&a)
によってa
への弱参照を返す。 -
(Weak).upgrade() -> Option<Rc<T>>
は Weak ポインタを Rc にアップグレードして返す。- Weak ポインタが参照する値が drop するか、もしくは空の場合 None を返す。
16 章:並行性
- この章では次の内容を扱う。
- スレッドを生成して複数のコードを並列で走らせる。
- メッセージ受け渡し並行性:スレッド間でメッセージを送る。
- 状態共有並行性:複数のスレッドから特定のデータにアクセスする。
- Sync/Send trait:Rust の並行性の安全保障を拡張する。
スレッドを使用してコードを並行して走らせる
- スレッド生成モデルとして次がある。
- 1:1 モデル:一つの OS スレッドに対して一つの言語スレッドを生成する。
- 言語が OS の API を呼び出して生成するモデルがこれに該当する。
- M:N モデル:言語がスレッドを提供(グリーンスレッドと呼ばれる)して異なる数の OS スレッド上で実行する。
- 1:1 モデル:一つの OS スレッドに対して一つの言語スレッドを生成する。
-
std::thread::spawn(|| {})
関数(クロージャを使用)を用いて新規スレッドが生成できる。-
spawn
は生成する(=produce)の意味を持つ。
-
-
JoinHandle.join()
を用いることで、指定されたスレッドが完了するまでそのスレッドを生成しているスレッドの終了をブロックする。-
handle = thread::spawn(); handle.join().unwrap();
のように使用する。
-
- 別スレッド内で呼び出し元のスレッドの値を参照することができないようになっている。
- 参照した値がいつ元のスレッドのスコープから外れるかわからないため。
- したがって move を使って強制的に所有権を移すことが求められる。
スレッド間でデータを転送する
-
std::sync::mpsc::channel()
を用いてスレッド間でデータ転送が可能。- mpsc = multiple producer, single consumer
- 転送機(tx)と受信機(rx)が生成される。
- 簡単な使い方は次のようになる。
- tx を別スレッドに移す。
-
tx.send(val)
で tx から rx にデータを転送する。- エラー対処のため
.unwrap()
をつけた方がよい。 -
val
の所有権はこの時点で移動する。
- エラー対処のため
-
rx.recv()
でデータを取り出す。- 受け取った値を
Result<T, E>
の形で返す。 - エラー対処のため
.unwrap()
をつけた方がよい。 - データが tx から転送されるまで呼び出し元スレッドの実行をブロックする。
- 受け取った値を
-
rx.try_recv()
は呼び出し元スレッドをブロックせず即座にResult<T,E>
を返す。- 値がまだ届いてない場合は Err を返す。
- tx から複数の値を順に送信することが可能。
- rx に順に送信される。
- rx をイテレータとして使用することができる。
-
mpsc::Sender::clone(&tx)
によって同じ受信機を持つクローンを生成できる。
状態共有並行性
- メモリ共有並行性は複数の所有権に似ており、複数のスレッドから同じメモリ位置にアクセスすることを可能にする。
- Mutex (mutual exclusion) を使う場合は次の規則を考慮するべきである。
- データを使用する前にロックの獲得を試みる。
- Mutex が排他使用しているデータの使用が終了したときそのデータをアンロックする。
-
std::sync::Mutex
型を使用して Mutex が実装可能。-
.lock()
によって値を排他使用する。
-
-
std::sync::Arc<T>
を使用することで Mutex を複数のスレッド間で共有することが可能である。-
std::sync::atomic
の標準ライブラリに属する Atomic な型はスレッド間で共有しても安全である。
-
Sync/Send とライトで拡張可能な並行性
-
std::marker::Send
トレイトを実装すると、その型の所有権をスレッド間で転送できる。 -
std::marker::Sync
トレイトを実装すると、その型を複数のスレッドから参照しても安全となる。
17 章:Rust のオブジェクト指向プログラミング機能
- この章では Rust でのオブジェクト指向のデザインパターンを考える。
オブジェクト指向言語の特徴
- オブジェクト、カプセル化、継承といった特徴を持つ。
- オブジェクトはデータとそのデータを処理するプロシージャを梱包している。
- カプセル化は、オブジェクトを使用するコードからオブジェクトの実装詳細にアクセスできないことを指す。
- オブジェクトと相互作用する唯一の手段はオブジェクトによって公開されている API である。
- 継承はあるオブジェクトの定義を受け継いだ他のオブジェクトを定義できる機構である。
- 多相性(ポリモーフィズム)ともいう。
- Rust ではトレイトを使用して多相性を可能にする。
トレイトオブジェクトで異なる型の値を許容する
- トレイトオブジェクトはでダイナミックディスパッチを行う。
- コンパイラが実行時にどのメソッドを呼び出すかをはじき出すコードを生成する。
- cf. スタティックディスパッチ:単相化の結果、コンパイル時にコンパイラがどのメソッドを呼び出しているか「理解」している。
- トレイトオブジェクトはオブジェクト安全である必要がある。
- 戻り値の型が Self でない。(
Clone
トレイトは Self を返すのでオブジェクト安全でない。) - ジェネリックな型引数がない。
- 戻り値の型が Self でない。(
- トレイトを実装することで、そのトレイトを「継承」した構造体であると考えることができる。
- 1.39 時点のバージョンではダイナミックディスパッチを行うような場合に
dyn
を明示する必要がある様子。
オブジェクト指向デザインパターンを実装する
- 例えばトレイトのメソッドの引数に
self: Box<Self>
と型指定をした場合、Box に対して呼ばれた場合にのみそのメソッドが呼ばれる。- また、この記法により
Box<Self>
の所有権が奪われる。
- また、この記法により
- この節で実装したようなステートパターンの欠点として次の点がある。
- 状態が状態間の遷移を実装しているので、状態の一部が密に結合した状態になってしまう。
- ロジックの一部を重複させてしまう。
結論として
- オブジェクト指向なパターンを実装できるが、かならずしもそれが Rust の強みを活かす最善の方法にはならない。
18 章:パターンとマッチング
- Rust での パターン は以下の要素を組み合わせて構成される。
- リテラル
- 分配された配列、enum、構造体、タプル
- 変数
- ワイルドカード
- プレースホルダ
- パターンは何かしらの値と比較され、その値と合致したら値の部分を使用する。
パターンが使用される状況
-
match アーム:全てのパターンに対して網羅的でなくてはならない。
match value { pattern1 => expression1, pattern2 => expression2, pattern3 => expression3, _ => the other expression }
-
if let 式:else if 等と組み合わせた表現が可能。
if let Some(color) = favorit_color { ... } else if is_tuesday { ... } else if let Ok(age) = age { ... } else { ... }
-
while let ループ:パターンが合致し続ける限りループする。
while let Some(top) = stack.pop() { println!("{}", top); }
-
for ループ
for (index, value) in v.iter().enumerate( { println!("{} is at index {}", value, index); }
-
let 文
let pattern = expression; let (x, y, z) = (1, 2, 3);
-
関数の引数
fn foo(x: i32){...}
論駁可能性
- パターンには論駁可能なものと不可能なものがある。
- 論駁不可能:渡される可能性のあるあらゆる値に合致するパターン。
- 例:
let x = 5;
- 例:
- 論駁可能:何らかの可能性のある値に対して合致しないことがあるパターン。
- 例:
if let Some(x) = a_value {...}
- 例:
- 論駁不可能:渡される可能性のあるあらゆる値に合致するパターン。
- 論駁不可能なパターンを使用する箇所で論駁可能なパターンを使用することはできない。
- 例:
let Some(x) = some_option_value;
は論駁不可能な let 文に対して論駁可能な Some を使用しているためエラー。 - 逆も然り。
- 例:
パターン記法
-
リテラルにマッチする場合:
let x = 1; match x { 1 => println!("one"), 2 => println!("two"), _ => println!("anything"), }
-
名前付き変数にマッチする場合:
let x = Some(5); let y = 10; match x { Some(50) => println!("Got 50"), Some(y) => println!("Matched, y = {:?}", y), _ => println!("Default case, x = {:?}", x), }
-
複数のパターン:
let x = 1; match x { 1 | 2 => println!("one or two"), 3 => println!("three"), _ => println!("anything"), }
-
値の範囲に合致させる:Python でいうところの
if x in [...]
みたいなもの。let x = 5; match x { 1 ... 5 => println!("one through five"), _ => println!("something else"), } let x = 'c'; match x { 'a' ... 'j' => println!("early ASCII letter"), 'k' ... 'z' => println!("late ASCII letter"), _ => println!("something else"), }
-
構造体を分配する:
struct Point {x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; let Point { x: a, y: b } = p; // p の値を分配して a, b に入れる let p = Point { x: 0, y: 7 }; let Point { x, y } = p; // メンバーと同じ名前の変数に格納する場合は省略記法を使う }
-
構造体を分配する+マッチ:
let p = Point { x: 0, y: 7 }; match p { // y = 0 を持つ場合 Point { x, y: 0 } => println!("On the x axis at {}", x), // x = 0 を持つ場合 Point { x: 0, y } => println!("On the y axis at {}", y), // どちらでもない場合("_" に相当) Point { x, y } => println!("On neither axis: ({}, {})", x, y), }
-
enum を分配する:
enum Message { Quit, // enum Move { x: i32, y: i32 }, // struct Write(String), // 関数 ChangeColor(i32, i32, i32), // 匿名構造体 } fn main() { let msg = Message::ChangeColor(0, 160, 255); match msg { Message::Quit => { println!("The Quit variant has no data to de-structure.") }, Message::Move { x, y } => { println!( "Move in the x direction {} and in the y direction {}", x, y ) }, Message::Write(text) => println!("Text message: {}", text), Message::ChangeColor(r, g, b) => { "Change the color to red {}, green {}, and blue {}", r, g, b } } }
-
参照を分配する:
struct Point { x: i32, y: i32, } let points = vec![ Point { x: 0, y: 0 }, Point { x: 1, y: 5 }, Point { x: 10, y: -3 }, ]; let sum_of_squares: i32 = points .iter() // iter 化 .map(|&Point { x, y }| x * X + y * y) // 各要素に関数を適用。map の引数は参照である .sum(); // 適用後の値を積算
-
構造体とタプルを分配する:
struct Point { x: i32, y: i32, } let ((feet, inches) Point {x, y}) = ((3, 10), Point { x: 3, y: -10 });
-
_
で値全体を無視する:// 最初の引数を無視する fn foo(_: i32, y: i32) { println!("This code only uses the y parameter: {}", y); } fn main() { foo(3, 4); // 最初の 3 は関数 foo 内では無視される }
-
ネストされた
_
で値の一部を無視する:// 例1 let mut setting_value = Some(5); let new_setting_value = Some(10); match (setting_value, new_setting_value) { (Some(_), Some(_)) => { println!("Can't overwrite an existing customized value"); }, _ => { setting_value = new_setting_value; } } println!("setting is {:?}", setting_value); // 例2 let numbers = (2, 4, 8, 16, 32); match numbers { // 2番目と4番目の値を無視する (first, _, third, _, fifth) => { println!("Some numbers: {}, {}, {}", first, third, fifth) }, }
-
名前を
_
で初めて未使用の変数を無視する:fn main() { let _x = 5; // 未使用変数警告が出ることを回避できる let y = 10; }
-
_
は値を束縛しないが_x
のように変数名に_
をつけたものは値を束縛する。// 例1:値を束縛する let s = Some(String::from("Hello!")); if let Some(_s) = s { // _s に s の所有権が移る println!("found a string"); } println!("{:?}", s); // エラーが出る // 例2:値を束縛しない let s = Some(String::from("Hello!")); if let Some(_) = s { println!("found a string"); } println!("{:?}", s); // エラーが出ない
-
..
で値の残りの部分を無視する:タプルやタプル構造体パターン一つにつき一回のみ使用できる。struct Point { x: i32, y: i32, z: i32 } let origin = Point { x: 0, y: 0, z: 0}; match origin { Point {x, .. } => println!("x is {}", x), } let numbers = (2, 4, 8, 16, 32); match numbers { // 2番目と4番目の値を無視する (first, .., last) => { println!("Some numbers: {}, {}", first, fifth); }, }
-
ref と ref mut でパターンに参照を生成する:
let mut robot_name = Some(String::from("Bors")); // ref による参照生成 match robot_name { Some(ref name) => println!("Found a name: {}", name), None => (), } println!("robot_name is {:?}", robot_name); // Some("Bors") // ref mut による可変参照生成 match robot_name { Some(ref mut name) => *name = String::from("Another name"), None => (), } println!("robot_name is {:?}", robot_name); // Some("Another name")
-
マッチガード:match 後に if 文を追加する。
// 例1 let num = Some(4); match num { Some(x) if x < 5 => println!("less than five: {}", x), Some(x) => println!("{}", x), None => (), } // 例2 let x = 4; let y = false; match x { 4 | 5 | 6 if y => println!("yes"), _ => println!("no"), }
-
@
演算子による束縛:enum などで値を保持する変数に対して、match 文においてその値を保持する変数を生成すると同時にパターンに一致するかどうか調べるときに便利。enum Message { Hello { id: i32 }, } let msg = Message::Hello { id: 5 }; match msg { Message::Hello { id: id_variable @ 3...7 } => { println!("Found an id in range: {}", id_variable) }, Message::Hello { id: 10...12 } => { println!("Found an id in another range") }, Message::Hello { id } => { println!("Found some other id: {}", id) }, }
19 章:高度な機能
- この章では次の内容を扱う。
- Unsafe Rust
- unsafe superpowers
- 高度なライフタイム
- lifetime subtyping
- ジェネリックな型への参照に対するライフタイム境界
- 高度なトレイト
- 関連型
- デフォルト型引数
- フルパス記法
- スーパートレイト
- トレイトに関連するニュータイプパターン
- 高度な型
- 型エイリアス
- never 型
- 動的サイズ型
- 高度な関数とクロージャ
- 関数ポインタ
- クロージャの返却
- Unsafe Rust
Unsafe Rust
- Unsafe Rust はメモリ安全保証を強制しないコーディングを可能にする。
Unsafe superpowers
-
unsafe superpowers は unsafe Rust を特徴づける次の4つの行動を指す。
- 生ポインタを参照外しすること
- unsafe な関数やメソッドを呼ぶこと
- 可変で静的な変数にアクセスしたり変更したりすること
- unsafe な trait を実装すること
- unsafe であるというのは、そのブロック内のコードがメモリに「合法的に」アクセスすることを保証するということである。
- メモリ安全性に関するエラーが unsafe ブロック内にあると考えることもできる。
- unsafe なコードを安全なアブストラクトの中に閉じ込めてできるだけ分離することが重要。
生ポインタの参照外し
- 生ポインタには次の型がある。
*const T
*mut T
- 生ポインタの特徴として次が挙げられる。
- 同じ場所への不変と可変なポインタや複数の可変なポインタが存在することで借用規則を無視できる。
- 有効なメモリを指しているという保証がない。
- null の可能性がある。
- 自動的な片付け(GC)は実装されていない。
- 参照から生ポインタを生成するときは次のようにする:
- 参照から不変の生ポインタを生成:
let r1 = &num as *const i32;
- 参照から可変の生ポインタを生成:
let r2 = &mut num as *mut i32;
- 参照から不変の生ポインタを生成:
- 参照から生ポインタを生成することは、生ポインタの有効性を保証する。
- スコープ内で参照自体が有効であることが保証されているため。
- アドレスを与えて生ポインタを生成することは可能だが、そのポインタが有効かどうかは保証されない。
- 生ポインタは safe なスコープ内で生成できるが、safe なスコープ内で参照外しすることができない。
- unsafe スコープ内でのみ参照外しが可能。
unsafe な関数やメソッド
- unsafe キーワードを頭につけることで関数やメソッドを unsafe にすることができる。
- unsafe なコードを safe な関数でラップすることはよく行われる実装方法である。
- extern キーワードを用いることで外部関数インターフェイス(FFI)を実装できる。
- C 言語の標準ライブラリは
extern "C"
の宣言だけで使用できる。 - extern ブロック内で宣言された関数は Rust コード内で常に unsafe になる。
- C 言語の標準ライブラリは
- Rust のライブラリ中で関数に次の処理を施すことで、その関数を外部で使用できるようにすることができる。
-
extern "X"
(X は C など)をつける。 -
#[no_mangle]
注釈をつける:言語によって異なるマングルが行われているため。
-
可変で静的な変数にアクセスしたり変更したりする
- Rust においてグローバル変数は static 変数(静的変数)と呼ばれる。
-
static HELLO_WORLD: &str = "Hello. world!";
のように宣言する。
-
- Rust での慣習上、静的変数の名前は
SCREAMING_SNAKE_CASE
とする。 - 静的変数には型注釈をつける必要がある。
- 静的変数を可変にすることができる。
- 可変な静的変数を変更・参照する場合は unsafe スコープ内で行う。
unsafe な trait を実装する
- 少なくとも一つのメソッドにコンパイラが確かめられない何らかの不定条件があるとき、その trait は unsafe である。
- unsafe な trait も unsafe キーワードで定義できる。
- unsafe な trait を実装する場合も unsafe キーワードが必要である。
高度なライフタイム
ライフタイム・サブタイピング
- ライフタイム・サブタイピング は、あるライフタイムが他のライフタイムよりも長くなるべきであるように指定することをさす。
- 参照先のデータのライフタイム('s とする)を参照するデータのそれ('c とする)よりも長くしたい場合、
's: 'c
と表記することで解決できる。
ジェネリックな型への参照に対するライフタイム境界
- ライフタイム境界:ジェネリックな型へのライフタイムによる制限。
- ライフタイム・サブタイピングを用いてジェネリックな型(T)のライフタイム境界を与えることができる。
- 例えば
struct Ref<'a, T: 'a>(&'a T);
のように。
- 例えば
-
'static
を用いてライフタイム境界を与えることも可能。- 例えば
struct Ref<T: 'static>(&'static T);
のように。 -
'static
はプログラム全体と同じだけのライフタイムを持つように制限する。
- 例えば
Trait オブジェクトのライブタイムの推論
- トレイトオブジェクトのデフォルトのライフタイムに関して次の規則がある。
- 何も指定がないときのライフタイムは
'static
。 -
&'a Trait
や&'a mut Trait
の指定のときライフタイムは'a
、 - 単独の
T: 'a
節でのトレイトオブジェクトのライフタイムは'a
。 - 複数の
T: 'a
節でのトレイトオブジェクトのライフタイムはなく、明示が必要。
- 何も指定がないときのライフタイムは
高度なトレイト
関連型でトレイト定義においてプレースホルダーの型を指定する
-
関連型:trait と型のプレースホルダ―を結合するもの。
- trait のメソッド定義がシグニチャでプレースホルダーの値を使用できるようにしている。
- ジェネリック型を使用して同じような動作をさせようとすると、どの型の trait を使用しているかわからなくなるため型注釈をつける必要が生じる。
デフォルトのジェネリック型引数と演算子オーバーロード
-
<PlaceholderType=ConcreteType>
という記法を用いることで、ジェネリックな型に規定の型を指定することができる。 - デフォルト型引数を指定する状況として次が挙げられる。
- 既存のコードを破壊せずに型を拡張する。
- ほとんどのユーザーが必要としない特定の場合でカスタマイズを可能にする。
- 演算子オーバーロードを実装する際にデフォルトのジェネリック型を与えることが有用となる。
- 例えば
trait Add<RHS=Self>
では Self が規定の型となっている。
- 例えば
明確化のためのフルパス記法
- 複数の trait で同じ名称のメソッドを実装しているとき、特定の trait でのメソッドを呼び出す場合にフルパスで記述する必要がある。
- Trait の関連関数(
self
を引数にとらないメソッド)を呼ぶ場合は次のような記法をとる必要がある。<Struct as Trait>::method()
スーパートレイト
- あるトレイトに別のトレイトの機能を実装させることができる。
- A トレイトが B トレイトの機能を実装するとき A を スーパートレイト といい B をサブトレイトという。
- スーパートレイトを実装する構造体はサブトレイトも実装していなくてはならない。
- トレイト境界による。
ニュータイプパターン
-
ニュータイプパターン を用いることでオープンルールを解除できる。
- オープンルール:トレイトまたは型がクレートにローカルである限り型にトレイトを実装できるという規則。
- たとえば
Vec<T>
にDisplay
を実装するといった操作が可能になる。
- 具体的には ラッパー構造体を作成して実装する。
- ここでのラッパー構造体とは、実装したいトレイトの対象を型としてもつタプル構造体をさす。
高度な型
- ここではニュータイプパターンの有用性を確認した後に型エイリアスを議論する。
ニュータイプパターンの別の用途:型安全性と抽象化を求めて
- ニュータイプを使用すると前節の機能の他に次のようなことが可能となる。
- 非公開の内部の型の API と異なる公開 API を提供する。
- 内部の実装を隠匿する(カプセル化のような機能)。
型エイリアス
- type キーワードを用いることで型のエイリアスを定義できる。
-
type Kilometer = i32;
のように。 - 関連型で見たキーワード。
- C/C++ での
typedef
のようなもの。
-
- 型エイリアスは元の型と同等に扱われる。
- 型エイリアスの主な用途は繰り返しを減らすことである。
- 長い型を何度も書きたくない時とかに有用。
- 型エイリアスは
Result<T, E>
ともよく組み合わせて使用される。- 例えば
Write
トレイトの中ではtype Result<T> = Result<T, std::io::Error>;
として定義されているらしい。
- 例えば
never 型
-
never
型は値を絶対に返さない関数に使用する。-
!
で表現される。
-
- continue や panic! 、loop が never 型を返す例である。
- never 型は他のどんな型にも型強制されうる。
- 例えば match アーム内で Ok => int, Err => continue とすると、continue が返す ! は int 型に強制される。
動的サイズ付け型と Sized トレイト
-
動的サイズ付け型(DST) を用いることで、実行時にのみサイズを知ることができる値を使用するコードが書けるようになる。
- 例として
str
がある(&は付けない)。
- 例として
- 全てのトレイトは、トレイト名を使用して参照できる動的サイズ付け型である。
-
Sized
トレイトは型のサイズがコンパイルにわかるかどうかを決定する。- コンパイル時にサイズが判明する全てのものに自動的に実装される。
-
?Sized
というトレイト境界は、対象とする型が Sized かもしれないし違うかもしれないというものである。- 特にジェネリック関数で使用されている。
高度な関数とクロージャ
関数ポインタ
-
fn
キーワードは関数ポインタを表す。 - fn ポインタはクロージャトレイト(Fn, FnMut, FnOnce)全てを実装している。
- 従ってクロージャとしても使える。
クロージャの返却
- クロージャはトレイトで表現されており、直接返却する(戻り値にする)ことができない。
- トレイトの場合、代わりにトレイトを実装する具体的な型を用いる。
- クロージャを格納するために必要なスペースが不明なため。
- 例えばクロージャを Box でラップすることでクロージャを返すことができる。
20 章:最後のプロジェクト:マルチスレッドの Web server を構築する
- プラン:
- TCP と HTTP を扱う。
- Socket で TCP 接続を listen する。
- 少量の HTTP リクエストを構文解析する。
- 適切な HTTP レスポンスを生成する。
- スレッドプールでサーバーのスループットを強化する。
- より完全なクレートが https://crates.io/ で利用可能である。
Single thread の Web server を構築する
- HTTP と TCP を利用する。
- TCP:情報があるサーバーから別のサーバーへ到達する方法に関するプロトコル。
- HTTP は TCP の上に成り立っている。
TCP 接続を listen する
-
std::net::TcpListener
を用いて実装できる。 -
TcpListener::bind(&str addr)
は指定されたアドレスへ TCP 接続するインスタンスを生成する。 -
tcp_listener.incoming()
はtcp_listener
が接続しているアドレスからの一連のストリームを与えるイテレータ(TcpStream
型)を返す。
リクエストを読み取る
-
tcp_stream.read(&mut buffer)
はtcp_stream
インスタンスから読み取った値をbuffer
に書き込む。-
tcp_stream
は mut である必要がある。 -
buffer
はバッファの役割をもち、let mut buffer = [0; 512];
のように配列([u8]
)で与える。
-
- Read の操作を行うので
std::io::prelude::*
を呼び出しておく必要がある。 - Read に失敗する可能性があるので
.unwrap()
などの例外処理を付けることが推奨される。
レスポンスを記述する
-
tcp_stream.write(bytes)
はバイト列bytes
をtcp_stream
ストリームに書き込む。 - Write に失敗する可能性があるので
.unwrap()
などの例外処理を付けることが推奨される。 -
tcp_stream.flush()
によって TCP stream の処理が終了するまで待機させる。 - HTTP v1.1 の仕様では、一般的な成功のレスポンスは次の文字列で示される。
HTTP/1.1 200 OK\r\n\r\n
HTML を返す
- レスポンスとして HTML ファイルの中身を返す場合、その中身を前節のレスポンスヘッダーの後ろにつければよい。
マルチスレッド化
- 前節で実装したサーバーはシングルスレッドであり、順に処理している。
- これをマルチスレッド化する。
スレッドプールでスループットを向上させる
- スレッドプール:タスクを処理する準備ができているスレッドの一連。
- プログラムが新しいタスクを受け取った時に、プール中のスレッドを割り当ててそのスレッド上で処理を行う。
- プール内のスレッドの数はできるだけ少数に制限する方がよい。
- DoS 攻撃から保護する目的もある。
- Compiler driven development (CDD):欲しい関数を呼び出すコードを書き、実行時にコンパイルが出すエラーを見てコードが動くように変更していく開発姿勢。
- スレッドプールは
thread::spawn
と同等の操作を行う関数を実装する必要がある。- 従ってその関数の引数(クロージャまたは関数)は
FnOnce() + Send + 'static
のトレイト境界を持つ必要がある。
- 従ってその関数の引数(クロージャまたは関数)は
-
thread::spawn
はJoinHandle<T>
を返すので、スレッドプールもこの型を保持できるようにする。- 例えば
Vec<JoinHandle<()>
のように。
- 例えば
- 標準ライブラリは生成されたスレッドを、データを受信するまで待機させるような機能を有しない。
- その機能を実現するためのデータ構造体を実装する必要がある。
- よく Worker と呼ばれるものである。
- Worker と ThreadPool の関係は次のように定義されうる。
- Worker が単独の
JoinHandle<()>
をメンバーに持つ。 - ThreadPool が Worker をメンバーに持つ。
- Worker が単独の
- 実装の一例を次に示す。
-
id
とJoinHandle<()>
を保持する Worker 構造体を定義する。 - ThreadPool に Worker のリスト(ベクター)を持たせる。
-
Worker::new(id)
を、 id 番号を引数に持ち空のクロージャで生成されるスレッドを保持する Worker を生成するように実装する。 -
ThreadPool::new
では for ループで id を生成し、それらで新しい Worker を生成してベクターメンバーに格納するように実装する。
-
- Worker 構造体を、 ThreadPol が保持するキューから実行するコードを取得しスレッド上で実行するように実装する。
- ThreadPool がチャンネルを生成し、受信側を Worker に渡す。
- 16 章で厚かったように、Arc 構造体と Mutex 構造体を用いてチャンネルを複製することでスレッド間通信を実現する。
- 送信したいクロージャを保持する Job 構造体を定義し、チャンネルを通して ThreadPool から Worker に渡すようにする。
- Job は
thread::spawn
の引数であるから、対応する type (型エイリアス)にして問題ない。
- Job は
- ThreadPool がチャンネルを生成し、受信側を Worker に渡す。
- Rust 1.41.0 では Box から中身をムーブすることが許されていないので、ここでは
self: Box<Self>
の形で self をムーブすることを陽に示して対処する。-
FnBox
トレイトを実装。
-
シャットダウンと片付けの実装
ThreadPool に Drop トレイトを実装する
- メインスレッドが閉じる前にサブスレッドが閉じるように Drop トレイトを実装することが望ましい。
スレッドに仕事のリッスンを止めさせる
- ThreadPool が drop できるように、Worker 内で loop している部分を抜け出す仕組みが必要。
- Job を持つ Enum 型を実装し、match アームで break できるようにする。
関数リファレンス
関数 | 説明 |
---|---|
print!(str,obj1, ...) |
printf と同じ動きをするマクロ |
println!(str,obj1, ...) |
print! に改行を加えたマクロ。objはstd::fmt::Display を満たしている必要がある。{:?} と#derive(Debug) を組み合せることでデバッグ出力を確認できる。{:#?} を用いると別の方法で整形された出力になる |
write!(fmt, &str, obj1, ...) |
fmt::Formatter である fmt とobj と &str を用いて生成される文字列を返す |
eprintln!(str,obj) |
標準エラーストリームに出力するマクロ |
vec![..] |
Vecオブジェクトを生成するマクロ |
panic!(str) |
エラーが生じたときにパニックを発生させるマクロ。str が与えられている場合はそれを表示する |
assert!(func) |
func がtrueを返すことをテストする |
assert_eq!(a,b) |
aとbが同じであることを確認する。異なる場合はエラーを返す |
assert_ne!(a,b) |
aとbが異なることを確認する。異なる場合はエラーを返す |
&str.to_string() |
文字列リテラルをStringに変換する |
&str.split_whitespace() |
文字列リテラルを空白で分離してiteratorとして分離された要素を返す |
&str.lines() |
行ごとの要素を含むイテレータを返す |
&str.contains(str) |
str が含まれていればtrueを返す |
&str.repeat(n) |
str を n 回繰り返した文字列を返す |
&str.as_bytes() |
str のバイト列を返す |
format!(str,obj) |
objを参照してstrで指定する文字列を生成する |
String::new() |
Stringオブジェクトに__束縛__されている変数を生成する |
String::from(str) |
strの値をもつStringオブジェクトを生成する |
.trim() |
Stringオブジェクトの前後の空白と改行を除去 |
.parse() |
型を判断して数値に変換 |
.clone() |
完全にコピーして(おそらく)別のポインタを割り当てる |
.as_bytes() |
バイト列に変換する |
.push_str(s) |
文字列または文字列スライスsの値を追加する |
.push(c) |
一文字を追加する |
.len() |
UTF8での文字の長さを返す |
.chars() |
文字列の各要素が入った配列を返す |
.bytes() |
文字列の各バイトが入った配列を返す |
Vec::new() |
空のベクタ型オブジェクトを生成する |
.push(T) |
値を追加する |
.get(index) |
indexで指定される位置の値を返す。indexが不正である時はOptionのNoneを返す |
.len() |
ベクタの長さ(要素の数)を返す |
.iter() |
不変参照のイテレータを返す |
.iter_mut() |
可変参照のイテレータを返す |
.into_iter() |
所有権がムーブされたイテレータを返す |
HashMap::new() |
ハッシュマップオブジェクトを生成する |
.insert(k,v) |
Key-valuepairを追加する |
get(key) |
key に対応する値を取得する |
.entry(key) |
key に値があるか確認する |
.or_insert(value) |
.entry(key) と組み合わせてkey に値がない場合にvalue を挿入する |
Mutex::new(T) |
Mutex を生成する。型は値 T によって決まる |
.lock() |
値のロックを獲得する |
Option.as_ref() |
Option の内部の値に対する参照を返す |
std::io::stdin().read_line(&obj) |
標準入力を受け付けてobj 参照に値を入れる。io::Result 型の値を返す。 |
.unwrap() |
結果がErr の場合にパニックを発生させる |
.expect(str) |
結果がErr の場合にstr を表示してパニックを発生させる(つけなかったら警告が出るらしい)。.expect() の代わりにmatch{} を使うと例外処理ができる |
`.unwrap_or_else( | err |
f64::powi(x,p) |
$x^p$を計算して返す |
f64::sqrt(x) |
$\sqrt{x}$を計算して返す |
File::open(path) |
path で指定されたファイルを読み込み専用で開く。OKならファイルハンドルが、エラーならErrインスタンスが返される |
iterator.next() |
iterator の現在位置の要素(の参照)を返して次の要素の位置に(内部的に)移動する |
iterator.sum() |
iterator の要素の合計を返す |
`iterator.map( | x |
iterator.collect() |
iterator を消費してベクタを生成する |
std::env::var(str) |
環境変数str に対応する値を含むOkを返す。値が定義されていない場合はErrを返す |