ゼロから学ぶRustのメモ
関数、ジェネリック型、ジェネリック関数
関数とは、値を受け取り値を返すようなもの
ジェネリック型とは、定数や型を受け取り型を返すもの
ジェネリック関数とは、定数や型を受け取り関数を返すもの
リンクリストを表すジェネリック型
enum List<T> { // (1)
Node { data: T, next: Box<List<T>> }, // (2)
Nil,
}
fn main() {
let n1 = List::<u32>::Nil;
let n2 = List::<u32>::Node {
data: 40, next: Box::<List<u32>>::new(n1) }; // (3)
let n3 = List::Node { data: 40, next: Box::new(n2) }; // (3)
}
(1) T
がジェネリック型の引数であり、ここに型や定数を渡すことができる
(2)Box
もまたジェネリック型であり、List<T>
という型をBox
の引数に私、具体的なBox
型を得ている。
(3) n2
とn3
の宣言の仕方はほぼ同じ。n2
は型Tを省略せずに書いたもの。n3
は型を省略して型推論に任せたもの。
ジェネリック関数の例
fn make_pair<T1, T2> (a: T1, b: T2) -> (T1, T2) {
(a, b)
}
fn main() {
make_pair::<u8, bool>(40, false);
make_pair(40, false);
}
make_pair
はT1
とT2
に型を受け取り型を返す。make_pair
ではu8
とbool
を受け取り(u8, bool)
を返す関数を得ている。
定数を受け取るジェネリック型の例
constant generics という。
#[derive(Debug)]
struct Buffer<const S: usize> {
buf: [u8; S]
}
fn main() {
let buf = Buffer::<128> { buf: [0; 128] };
println!("{:?}", buf);
}
クロージャー
map
や fold
を使える状況では使う
for
よりもコードを書いた意図が伝えることができる。
マルチスレッド
spawn と join
スレッドの生成はspawn
で行い、スレッドの終了をjoin
で行う
fn worker() -> u32 {
println!("worker");
100
}
fn main() {
let handler = std::thread::spawn(worker);
match handler.join() {
Ok(n) => println!("{n}"),
Err(e) => println!("{:?}", e),
}
}
チャンネル
channel(チャネル)は複数のスレッド間でデータをやり取りするための抽象的な通信路。例えるならデータをやり取りするための回線のようなもの。Rustではsync_channel
かchannel
を用いると、チャネルを生成できる
チャネル
チャネルは複数のスレッド間でデータをやり取りするための抽象的な通信路。
sync_channel
メソッドで、チャネルの両端にある、受信側と送信側を作る。新しいスレッドを作成して、そこでrx.recv()
を使いデータを受信している。データを送信するのはメインスレッドで、tx.send()
を使っている。最後にhandler.join()
でスレッドの終了を待ち合わせている。
fn main() {
let (tx, rx) = std::sync::mpsc::sync_channel(4);
// スレッドを生成しrxから受信
let handler = std::thread::spawn(move || match rx.recv() {
Ok((x, y)) => println!("({}, {})", x, y),
Err(e) => eprintln!("{e}"),
});
// チャネルに送信
if let Err(e) = tx.send((10, 20)) {
eprintln!()
}
if let Err(e) = handler.join() {
eprintln!("{:?}", e);
}
}
Rustではsync_channel
かchannel
を使うとチャネルが生成できる。これらの違いは、チャネル内部に保持可能なバッファの違い。sync_channel
はバッファのサイズが有限で、channel
はバッファのサイズに制限がない。
バッファは基本的にキューで実装されており、もしバッファを使い果たしたときにsend
で送信するとプログラムが一時停止する。つまり、send
を呼び出した側のスレッドのプログラムは、バッファに空きができるまで停止する。一方、channel
は、バッファに空きができない場合はバッファのサイズを動的に大きくする。
これらの違いより、メモリを多く消費してしまうchannel
よりsync_channel
を使った方がいい。
channel
もsync_channel
も複数スレッドから送信が可能だが受信は1スレッドからしかできない。
無効な参照の図
このコードでは変数a
を出力するとき、すでに破棄されたb
の値を参照しているのでエラーになる。その時の図がこちら。
fn main() {
let a;
{
let b = 10;
a = &b;
}
println!("{a}");
}
正しく動くコードはこちら
fn main() {
let a;
{
let b = 10;
a = &b;
println!("{a}");
}
}
Rust 2018以前ではこのコードが動かなかったが、Rust 2018 以降はこのように書いてもエラーは出ない。これを理解するには字句ライフタイムと非字句ライフタイムという考え方を理解する。字句ライフタイムは、ある変数のライフタイムはその変数が定義されてからその変数が定義されたブロックが終了するまでの期間であると決定されている。一方、非字句ライフタイムでは、変数の利用のされ方まで、意味的な解釈をしてライフタイムが決定される。下のコードの字句ライフタイムは、a
のライフタイムはaが定義されてからmain関数のブロックの閉じカッコまで。一方、下のコードの非字句ライフタイムはaが定義されてから最後に使われるprintln!("a");
の行までとなる。
非字句ライフタイムは自然にコードを記述できるようになったが、人間がライフタイムを判断するのが難しくなった。
制御フローグラフという技術が使われている。
ライフタイム・サブタイピング
サブタイピングとは型の継承を表現するためのプログラミング言語機能。例えば、「数字」という型があった場合、「数」からハセ市して「整数」や「浮動小数展」という型を定義できる。この時、「数」のことは「整数」や「浮動小数点」型のスーパータイプと呼ばれ、「整数」や「浮動小数点」のことは「数」型のスーパータイプと呼ばれる。スーパータイプをもとにして、新たなサブタイプを定義することを継承と呼び、サブタイプを元のスーパータイプとして型付けることをサブタイピングと呼ぶ。
以下のコードは、ライフタイム・サブタイピングを示すコードである。
fn add<'a> (x: &'a mut i32, y: &'a i32) {
*x += *y;
}
fn main() {
let mut x = 10;
{
let y = 20;
add(&mut x, &y);
}
println!("{x}");
}
このコードでは、add関数が``aというライフタイムの参照
xと
yを引数に受け取る。
addの受け取る参照のライフタイムは同じである。一方、変数
xのライフタイムは0から5行目であり、変数
y`のライフタイムは2から4行となっている。このように呼び出し元ではxとyのライフタイムは異なっているが、Rustコンパイラは0から5行目と、2から4行目というライフタイムを比較し、より短い方のライフタイムに統一する。
可変参照としての借用(p72)
この節の用語
用語 | 説明 |
---|---|
オリジナル変数 |
参照する元となる変数 |
&借用 |
オリジナル変数a に対して、&a と借用した参照 |
&mut 借用 |
オリジナル変数a に対して、&mut a と借用した参照 |
rcnt |
&借用 の参照カウント。初期値は0 。 |
wcnt |
&mut 借用 の参照カウント。初期値は0 。 |
変数の借用時を状態遷移図で表しているのが面白かった。借用しているときは変数を使うことができないし、&mut借用しているときは貸し出しすらもできないのがよくわかる。
他にも&借用を変数に代入したときはコピーで、&mut借用を変数に代入したときはムーブ*になるのが注意。というかここが曖昧な理解だったので状態遷移図と表で示してくれるのは助かる。
コード | aの状態 | bの状態 | cの状態 | rcnt |
wcnt |
---|---|---|---|---|---|
let mut a = 10; //オリジナル変数 |
状態 RW | 状態 RW | 0 | 0 | |
let b = &a; // &借用 |
状態 R | 状態R借用 | 状態 R | 1 | 0 |
let c = &mut a; // &mut 借用 |
遷移不可 | 状態R借用 | 遷移不可 | 1 | 0 |
println!("{a} {b} {c}"); |
オリジナル変数a
が初期状態で「状態RW」にあり、参照カウントは0となる。その後、aの&mut借用であるbを定義している。するとaは「状態None」、bは初期状態の「状態RW」となり、wcnt
は1となる。次に、変数c
を定義しているが、これはb
からムーブして得ている。
コード | aの状態 |
b の状態 |
c の状態 |
rcnt |
wcnt |
---|---|---|---|---|---|
let mut a = 10; // オリジナル変数
|
状態 RW | 0 | 0 | ||
let b = &mut a; // &mut 借用
|
状態 None | 状態RW借用 | 0 | 1 | |
let c = b; // ムーブ
|
状態 None | 終了 | 状態RW借用 | 0 | 1 |
*c = 30; |
状態 None | 終了 | 状態RW借用 | 0 | 1 |
*b = 20; // コンパイルエラー
|
状態 None | 終了 | 状態RW借用 | 0 | 1 |
a = 40; // コンパイルエラー
|
状態 None | 終了 | 状態RW借用 | 0 | 1 |
println!("{a} {b} {c}"); |
次はオリジナル変数a
が初期状態で「状態RW」にある。最初の状態ではaは読み書きできるが、次の行でbに参照を渡しているから、「状態R」になり読み込みしかできなくなる。
以降、b,c,dに代入をしている。bを代入したcも&aがコピーされ、bもそのまま&aを持ち続ける。dでは2回目の&aの代入をしている。aは破棄されておらず今も生きている。しかし状態Rだから、&aでしか渡せず&mut参照は渡せない。
コード | aの状態 |
b の状態 |
c の状態 |
d の状態 |
rcnt |
---|---|---|---|---|---|
let mut a = 10; // オリジナル変数
|
状態 RW | 0 | |||
let b = &a; // &借用
|
状態 R | 状態R借用 | 1 | ||
let c = b; // &借用コピー
|
状態 R | 状態R借用 | 状態R借用 | 2 | |
let d = &a; // &借用
|
状態 R | 状態R借用 | 状態R借用 | 状態R借用 | 3 |
println!("{a} {b} {c}"); |