Closed8

ゼロから学ぶ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) n2n3の宣言の仕方はほぼ同じ。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_pairT1T2に型を受け取り型を返す。make_pairではu8boolを受け取り(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);
}
ひげひげ

mapfoldを使える状況では使う

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_channelchannelを用いると、チャネルを生成できる

ひげひげ

チャネル

チャネルは複数のスレッド間でデータをやり取りするための抽象的な通信路。
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_channelchannelを使うとチャネルが生成できる。これらの違いは、チャネル内部に保持可能なバッファの違い。sync_channelはバッファのサイズが有限で、channelはバッファのサイズに制限がない。
バッファは基本的にキューで実装されており、もしバッファを使い果たしたときにsendで送信するとプログラムが一時停止する。つまり、sendを呼び出した側のスレッドのプログラムは、バッファに空きができるまで停止する。一方、channelは、バッファに空きができない場合はバッファのサイズを動的に大きくする。
これらの違いより、メモリを多く消費してしまうchannelよりsync_channelを使った方がいい。
channelsync_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というライフタイムの参照xyを引数に受け取る。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}");
このスクラップは2024/12/20にクローズされました