中途半端Rustからの再学習

現状
コンパイラに従いながらある程度コードを書くことができ、所有権や可変変数、借用チェックなどがわかる。
Result
型や Option
型が使え、 anyhow
を用いたエラーハンドリングはわかるが、最適なエラーハンドリングには悩むところがある。
また、struct
と impl
が使えるが、 trait
や ライフタイムなどはイマイチわかってない。
Box
dyn
もよくわかってない。
ようするに中途半端であやふや。
他の言語は、lua, js/ts が歴の長い使用言語で、 C/C++, C#, Python は時折触る程度。

トレイト(1)
interface
と似たものであることはわかっているが、具体的にどう違うのか、何ができるのか、という段階。
トレイトは
- データ型を分類するための仕組み
- 型に対して共通の振る舞いを定義できる
- ジェネリック型にトレイト境界として指定し、インスタンスを制約できる
トレイトは下記のようにして定義。
trait Entry {
fn get_name(&self) -> &'static str {
"Undefined"
}
fn get_parent(&self) -> &'static Self;
fn new(name: &str) -> Self;
}
get_name
のように、デフォルト実装もできる。
多分実装のときに困るが、コンパイルエラーは出ないので一旦このままで。

ライフタイム(1)
スコープを抜けたらオブジェクトが死ぬことはわかっているが、 'static
とか 'a
とか強要されるのなんですか? 状態。
複数の型の可能性があるときには、型を注釈しなければなりません。
同様に、参照のライフタイムがいくつか異なる方法で関係することがある場合には注釈しなければなりません。
関数や構造体の実装において時折 <'a>
のようなものが強要されるのはこれか。
-
ライフタイムの主な目的は、ダングリング参照を回避することです。
ダングリング参照によりプログラムは、 参照するつもりだったデータ以外のデータを参照してしまいます。
ダングリング参照という言葉は初めて聞いたが事象は知ってた。free()
したオブジェクト(宙ぶらりんになってしまったポインタ)が残ってるやつ。
無効なメモリ領域を指すポインタはダングリングポインタ(dangling pointer)と呼ばれる。
とりわけ、本来有効だったメモリ領域が解放処理などによって無効化されたにもかかわらず、そのメモリ領域を参照し続けているポインタのことを、ダングリングポインタと呼ぶ。
なお、ダングリングポインタは「ぶら下がりポインタ」と訳されることもある。
ダングル (dangle) は「ぶら下がる」という意味だそう。
下記で借用チェックの説明にて、'a
や 'b
を用いている。
暗黙的にライフタイムをコンパイラが推測しているときはわざわざ宣言する必要はなかったが、ライフタイムの注釈がつくならばこうなるのだと改めて理解。
下記の関数はエラーを吐く。
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
fn longest(x: &str, y: &str) -> &str
は、型のことだけを考えれば正しいが、ライフタイムがわからないから。
x
と y
のどっちのライフタイムが返ってくるんですか? って話。
下記の関数はコンパイルできる。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
一見すると、引数として与えられる x
も y
もライフタイムが同じであることが条件であるようだが、実はどちらかライフタイムの短い方に依存する。

ライフタイム(2)
構造体を持つ構造体を以下のように定義すると、コンパイルが通る。
struct Parent<'a> {
ref_chile: &'a Child
}
struct Child(usize);
Parent
は、フィールドにあるChild
の不変の参照よりもライフタイムが短い、ということをコンパイラに伝えている。
親より先に死ぬ子供があってたまるか、という状態(?)。
impl<'a> Parent<'a> {
pub fn get_child_age(&self) -> usize {
self.ref_child_a.0
}
pub fn compare_child_age(&'a self, another_child: &'a Child) -> &'a Child {
if self.ref_child_a.0 > another_child.0 {
self.ref_child_a
} else {
another_child
}
}
}
構造体にライフタイム注釈があるならば、メソッドでライフタイム注釈を使わなくとも、 impl
は必ずライフタイム注釈を付ける必要がある。
また、 compare_child_age
に関するライフタイム注釈を全て省略すると、下記のような推論が行われる。
fn compare_child_age(&'a self, another_child: &'b Child) -> &'a Child
これは、ライフタイム省略規則というものが係るからである。
- 参照型の引数はそれぞれ独自のライフタイムをもつ(=それぞれ異なるライフタイム注釈をつけることがでこきる)
- 引数の中で参照型が1つだけなら、その引数のライフタイムと戻り値(参照)のライフタイムと同一とみなす
- 第一引数が
&self
または&mut self
ならば、戻り値(参照)のライフタイムはself
と同一とみなす
1
より、ライフタイム注釈は各引数に対して個別に付けられ
3
より、戻り値のライフタイム注釈は 'a
と一緒になる
-
引数のライフタイムは 入力ライフタイム と呼ばれ、返り値のライフタイムは 出力ライフタイム と呼ばれる。
議論する必要のある1種の特殊なライフタイムが、
'static
であり、これは、この参照がプログラムの全期間生存できる事を意味します。
この文字列のテキストは、プログラムのバイナリに直接格納され、常に利用可能です。
故に、全文字列リテラルのライフタイムは、'static
なのです。
エラーメッセージで、
'static
を使用するようことがありますが、その参照が本当にプログラムの全期間生きるかどうか考えてください。
ほとんどの場合、問題は、ダングリング参照を生成しようとしているか、利用可能なライフタイムの不一致が原因です。
そのような場合、解決策はその問題を修正することであり、'static
ライフタイムを指定することではありません。
トレイトの際に下記のような実装をしたが、上記の文章を読むと、 'static
が適切ではないことがわかる。
trait Entry {
fn get_name(&self) -> &'static str {
"Undefined"
}
fn get_parent(&self) -> &'static Self;
fn new(name: &str) -> Self;
}
上記の学習に基づくと、下記のような実装でいいことになりそう。
trait Entry<'a> {
fn get_name(&self) -> &str {
"Undefined"
}
fn get_parent(&self) -> &Self;
fn new(name: &str) -> Self;
}
省略規則を適用するとこんな感じか
trait Entry<'a> {
fn get_name(&'a self) -> &'a str {
"Undefined"
}
fn get_parent(&'a self) -> &'a Self;
fn new(name: &'a str) -> Self;
}
Entry
トレイト内のライフタイムは全て <'a>
と同等か、それより長く生存すると想定するならば、これであっていることになる。

トレイト(2)
トレイトは継承を行うことができる。
pub trait Geometry {
...
}
pub trait Drawable: Geometry {
...
}
ある型から別の型に変換するとき、便利な
From
トレイトというのがあります。
from
メソッドを対応した型ごとに実装することで、その型からinto
メソッドで変換できます。
#[derive(Debug)]
struct Point { x: f64, y: f64 }
impl From<f64> for Point {
fn from(input: f64) -> Self {
Point { x: input, y: input }
}
}
fn main() {
let p1 = Point::from(1.0);
let p2: Point = (1.0).into();
println!("{:?} {:?}", p1, p2);
}
型を指定してあげなくても先の方を予測して勝手に変換してくれる仕組み、便利だなぁと思いつつ実装方法を知らなかった。

RAII(1)
リソースの確保をオブジェクトの初期化時に行い、リソースの開放をオブジェクトの破棄と同時に行う手法。
スコープから抜けたら所有権が破棄されるのは既知のとおりだが、C/C++のような言語だと、リソースの開放を自動的にやってくれる訳では無い。そうしたいなら std::unique_ptr
を使う必要がある。
Rust では明示的に破棄をする必要はないが、それを明示的にするにはどうするべきか、が記述されている。
ヒープメモリを確保するには、 Box<T>::new
を使用する。
let a = Box::new(10); // type inference
let a = Box::<i32>::new(20); // explicit type
let a = 30; // immutable object
let b = Box::new(a); // move object from stack memory to heap memory
let c = *b; // dereference
Box を使う例として二分木がある。
struct BinaryTree<T: std::cmp::PartialOrd + std::fmt::Display> {
val: T,
left: Option<Box<BinaryTree<T>>>,
right: Option<Box<BinaryTree<T>>>,
}
impl<T: std::cmp::PartialOrd + std::fmt::Display> BinaryTree<T> {
fn new(val: T) -> Self {
Self {
val,
left: None,
right: None,
}
}
fn insert(&mut self, val: T) {
if val < self.val {
println!("insert value {val} to left");
self.insert_left(val);
} else {
println!("insert value {val} to right");
self.insert_right(val);
}
}
fn insert_left(&mut self, val: T) {
match &mut self.left {
Some(left) => {
// left..insert(val);
left.insert(val);
}
None => {
self.left = Box::new(BinaryTree::new(val)).into();
}
}
}
fn insert_right(&mut self, val: T) {
match &mut self.right {
Some(right) => {
right.insert(val);
}
None => {
self.right = Box::new(BinaryTree::new(val)).into();
}
}
}
}
動作ほぼ未確認だがだいたいこんな感じ。

RAII(2)
同じオブジェクトを複数から束縛することを可能にするのが参照カウンタ
Rc<T>
。
Rc型はオブジェクトをヒープメモリに置くので、Box
の代わりに使うことができる。
use std::rc::Rc;
let a = Rc::new(10);
let b = Rc::clone(&a);
この入門記事では「ポインタ」という言葉を避けているが、この Rc<T>
は、 C++ でいう std::shared_ptr
であると理解した。
複数の変数に同じオブジェクトへの所有権をもたせ、全てが破棄された時点でメモリの開放を行う。
また、 Arc<T>
はスレッドセーフな参照カウンタである。
use std::rc::Rc;
fn main() {
let foo = Rc::new(50);
// reference count = 1
println!("reference count :{}",Rc::strong_count(&foo));
let bar = Rc::clone(&foo);
// reference count = 2
println!("reference count :{}",Rc::strong_count(&bar));
// &foo == &bar
println!("foo = {:p}", foo);
println!("bar = {:p}", bar);
}
ただ、Rc
で値を取ってきても可変ではないので、書き換えることはできない。

内部可変性(1)
Cell<T>
を用いると、mut
宣言しなくとも値を変更することができる。
use std::cell::Cell;
fn main() {
let a = Cell::new(10); // immutable object with interior mutability
dbg!(a.get()); // a.get() = 10
a.set(20);
dbg!(a.get()); // a.get() = 20
let b = a.replace(10);
dbg!(a.get()); // a.get() = 10
dbg!(b); // b = 20
let c = a.into_inner(); // turn Cell<T> into T
dbg!(c); // c = 10
dbg!(a); // borrow check - Error
}
get/set
はそのままなので置いといて、
replace
は内部の値を変更しつつ、以前の値をポップアウトする。
into_inner
はポップアウトする。
この Cell<T>
と、先の Rc<T>
を組み合わせて、可変な std::shared_ptr
を作り出す。
fn main() {
let a = Rc::new(Cell::new(10));
a.set(20);
dbg!(a.get()); // a.get() = 20
let b = Rc::clone(&a);
b.set(30);
dbg!(a.get()); // a.get() = 30
}

関数、クロージャ(1)
クロージャはそれぞれ独自の型を持っています。
クロージャはFn
トレイト、FnMut
トレイト、FnOnce
トレイトのどれかのインスタンスです。
それぞれ&self
、&mut self
、self
を内部的に引数として受け取っているかどうかの違いがあります。
また、Fn
トレイトはFnMut
トレイトを、FnMut
トレイトはFnOnce
トレイトを継承しています。
fn
は関数定義で使いますが、fn
は型でもあります。
そして、fn
のことを 関数ポインタ と言います。
fn
はFn
トレイトのインスタンスなので、FnMut
トレイト、FnOnce
トレイトのインスタンスでもあります。
急にようわからんこと&知らんことがいっぱい出てきた。ので詳細に書いている記事を漁った。
fn main() {
let closures = [3, 7, 1, 5, 8, 9, 2].iter().map(|&i| {
move |j| i + j
}).collect::<Vec<_>>();
println!("{}", closures[3](14));
}
というコードでは、「3を足す関数」「7を足す関数」「1を足す関数」「5を足す関数」…… のようにたくさんの「関数」を動的に生成していますが、こういうのはクロージャでないとできません。
move
? なんだこれ
と思ったので更に調べる。
// moveするクロージャ
let mut num = 0;
// 本当は所有権がムーブするが、数値はCopyな型なのでコピーされる
let mut ref_cls = move || {
num += 1;
num
};
assert_eq!(ref_cls(), 1);
// numはクロージャとは無関係になったので影響を受けない
assert_eq!(num, 0);
// moveするクロージャ
let s = "String".to_string();
let ref_cls = move || {
println!("{}", s);
};
// `s`は既にムーブしているので使うとエラー
// println!("{}", s);
本当に所有権が移るようになるっぽい。
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));
}
読むたび何か大事なことをすっぽかしているような気がするので、公式ドキュメントを読み返すことに。

関数、クロージャ(2)
実行に時間がかかる処理を持つ関数を呼び出すのを、不要なら呼び出さず、なおかつ2回以上呼び出す場合は前回の値を流用してね、というような処理を作るチュートリアル。
その中でキャッシュをもたせる構造体を作成したが、限界を悟ってクロージャを用いようねっていう流れになっていた。
その中で Fn
FnMut
FnOnce
の説明が入っていた。
直感的には、
FnOnce
は move || {}
に実装されるトレイトで、クロージャ内でキャプチャした変数に所有権を渡す。所有権は二回以上奪うことができないので、Once
という名前がついている。
FnMut
はクロージャ内で可変で値を借用する。
Fn
は不変で借用する。
という感じ。
move
は並列処理で多く出てくるそう。
で関数ポインタ、関数定義型って?
関数を参照するポインタが関数ポインタなんだろうというところはわかるが、「fn
が関数ポインタ」という文言がわからない。
関数は、型
fn
に型強制されます。
fn
型は、関数ポインタと呼ばれます。
うーん

関数、クロージャ(3)
fn type_of<T>(_: T) -> String {
let a = std::any::type_name::<T>();
return a.to_string();
}
fn double(value: i32) -> i32 {
value * 2
}
let f1: fn(i32) -> i32 = double;
let f2 = double;
println!("function pointer: {}", type_of(f1)); // function pointer: fn(i32) -> i32
println!("function pointer: {}", type_of(f2)); // function pointer: calc::main::double
型を示してポインタとして受け取るならばそれは関数ポインタとなり、そうでない場合は関数定義型になる。
イマイチ腑に落ちないのでとりあえずさようなら。
下記のコードはコンパイルが通らない。返すのが型ではないから。
fn returns_closure() -> Fn(i32) -> i32 {
|x| x + 1
}
通すだけならば以下の通り。
fn returns_closure() -> fn(i32) -> i32 {
|x| x + 1
}
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
fn returns_closure<'a>() -> &'a (dyn Fn(i32) -> i32) {
&|x| x + 1
}
fn returns_closure() -> Box<dyn Fn(i32) -> i32>{
Box::new(|x| x + 1)
}
dyn
と、ここについてくる impl
がわからない。
trait Trait {}
impl Trait for i32 {}
// old
// いままで
fn function1() -> Box<Trait> {}
// new
// これから
fn function2() -> Box<dyn Trait> {}
トレイトオブジェクトにトレイト名をそのまま使うのは悪手だった。
静的ディスパッチ - コンパイル時に呼び出すべきメソッドを決める、決まる。
動的ディスパッチ - 実行時にオブジェクトの型を調べ、対応するメソッドを呼び出す。
トレイトだけを指定すると、その引数にやってくる実態が何かはコンパイラにはわからない。
動的ディスパッチを用いて、実行時にその実態を知ってもらうようにしてあげる。
関数がトレイトを実装した型を返す場合、
impl Trait
という書き方ができる。
関数の引数に &impl
って書くやつと変わらんかったしたしかにそうだった。
いずれにしても全て本質的には返り値が違うので、注意しなければならない。
でも具体的にどう言う場面でどう注意したらいいかは思いつかない。

関数、クロージャ(4)
下記のコードはコンパイルが通らない。
fn put<T: std::fmt::Debug>(a: &T) {
println!("{:?}", a);
}
fn main() {
put("hoge");
}
動的サイズ型 は、実行時にサイズが決まる型で、
str
型などはそれにあたる。
そうでない、つまり、型のサイズが予め推論できるとき、Rust ではその型を自動的にSized
トレイトのインスタンスにする。
つまり、関数の引数やジェネリック型などはSized
トレイトのインスタンスでなければならない。
str
は動的サイズ型なので参照を付けていますが、これはエラーになってしまいます。
これは暗黙的にT: Sized
となっているためで、引数側に参照を付けてもT
がSized
トレイトのインスタンスでなければなりません。
しかし、引数側で参照を付けているので、コードとしては動的サイズ型を指定しても問題ありません。
そこで、T
が動的サイズ型を受け入れられるように?Sized
を指定するこができます。
ジェネリック型や関数の引数が Sized
トレイトのインスタンスであり、 str
は動的サイズ型なので、 &str
としたとしても str
は T
にはなり得ない(Sized
トレイトのインスタンスになり得ない)ので、エラーになってしまう。
で、?Sized
を指定する必要があるが、 ?
ってなんすか。
これ系の解説記事を探していると、 fat pointer
という言葉が時折出てくる。
自作スライスの説明で fat pointer という言葉が出てきた。
生ポインタと補助的な情報 (スライスなら長さ) を合わせた構造のことを指すらしい。
Rust 特有の用語ではなく、GC 本では dangling pointer の検出方法の一つとして紹介されている。
"fat pointer" とは、動的サイズ型への生ポインタや参照のこと。
動的なサイズのヒープバッファを管理する型(Vec<T>
など)も、コンパイラはVec<T>
インスタンスがスタックに占める正確なバイト数を知っているので、Sized
である。
スライス ([T], str) や、トレイト (dyn Trait) は動的サイズ型である。
Sizedを実装する型は、全て同じバイト数である。
C言語のsizeofに相当するstd::mem::size_of が使える。
(Sizedでない場合は値によって異なるため、std::mem::size_of_valを使う)
これらの値へのポインタは
16byte
になる。
最初の8byte
にはデータの先頭番地が入っている。
続く8byte
には、スライスの要素数や、バイト数などが入っている。
fat pointer については大まかにわかった。
特別な記法
?Sized
は、Sized
トレイトを実装しなくてはならないという制約を回避させてくれる。
?<Trait>
が特殊な文法なのでは?
と思ったが、コンパイラ怒られたので ?Sized
専用。

関数、クロージャ(5)
クロージャはトレイトであり、動的サイズ型であり、参照やBox型を使うことでオブジェクトとして扱えるようになります。
このようなオブジェクトをトレイトオブジェクトといいます。
トレイトオブジェクトを扱う側は実際のオブジェクトの型を知らなくても、そのメソッドを呼び出せるということ、
そしてオブジェクトの型によってメソッドの動作を変えられることになります。
これらの仕組みを 動的ディスパッチ といいます。
動的ディスパッチをクロージャやトレイトの始点から語っている。
そして、以下のものが静的ディスパッチであることを述べている。
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
Box<&dyn Trait>
で実装するものが動的ディスパッチで、 impl Trait
で実装するものが静的ディスパッチ。
多分動的ディスパッチのほうが遅くなるだろう、という直感はあるので、制約を緩めたいときに動的ディスパッチを用いるという具合か。
impl Trait
は抽象型ともいうらしい。

スレッド(1)
スレッドを作成、終了させるには、以下のようにする。
use std::thread;
let handle = thread::spawn(|| {
// thread code
});
handle // JoinHandle 型
.join() // Result 型
.unwrap();
クロージャの環境をスレッド間で共有することは通常の方法ではできません。
コピーを作成できるなら、環境にコピーされますが、そうでない(Vec<T>
など)なら所有権を移動しなければなりません。
その場合はmove || { /* thread code here */ }
を使います。
複数のスレッド間で状態を共有するには
Mutex
型で 排他制御 をする。
lock
メソッドでリソースをロックし、LockResult
型を返す。
LockResult
型はRAII
であるMutexGuard
型のオブジェクトを束縛しているので、自動でロックを解除する。
オブジェクトをスレッド間で共有するには、
Rc
型のマルチスレッド版であるArc
型を使う必要があります。
Rust のスレッドは OS スレッドで、Golang や Java などのような言語はグリーンスレッドと言い、違いとしては、仮想マシン上でマルチスレッドを実現するかというところらしい。
非同期処理は下記のような形で行う。
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel(); // 非同期チャネルを生成
thread::spawn(move || {
tx.send(42).unwrap(); // (非同期)送信
println!("Done immediately!"); // すぐにこの行に処理を移します
});
thread::sleep(Duration::from_secs(3)); // メインスレッドの処理を少し遅らせる
println!("got {}", rx.recv().unwrap()); // 受信
}
同期処理は下記のような形で行う。
バッファサイズを 1
にすると、送信はブロックされないが、同期処理を行うらしい。
use std::sync::mpsc::sync_channel;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = sync_channel::<i32>(0); // バッファサイズを0にする
thread::spawn(move || {
tx.send(53).unwrap(); // 同期送信。受け取ってもらうまで待ちます。
println!("Done after 3sec!"); // すぐには処理されません
});
thread::sleep(Duration::from_secs(3));
println!("got {}", rx.recv().unwrap());
}
- バッファが いっぱいになると
send
はブロック される
バッファのことがよく分からなかったので、ちょっと実験してみる。
let buffer_size = 0;
let (tx, rx) = sync_channel::<i32>(buffer_size);
thread::spawn(move || {
tx.send(1).unwrap();
println!("Done");
});
thread::sleep(Duration::from_secs(3));
println!("got {}", rx.recv().unwrap());
> got 1
> Done
let buffer_size = 1;
let (tx, rx) = sync_channel::<i32>(buffer_size);
thread::spawn(move || {
tx.send(1).unwrap();
println!("Done");
});
thread::sleep(Duration::from_secs(3));
println!("got {}", rx.recv().unwrap());
> Done
> got 1
let buffer_size = 1;
let (tx, rx) = sync_channel::<i32>(buffer_size);
thread::spawn(move || {
tx.send(1).unwrap();
tx.send(2).unwrap();
println!("Done");
});
thread::sleep(Duration::from_secs(3));
println!("got {}", rx.recv().unwrap());
println!("got {}", rx.recv().unwrap());
> got 1
> got 2
> Done
バッファサイズが 2
になると、tx.send(2).unwrap();
で処理が止まっている(であろう)様子がうかがえる。
ひとまずスレッドはこのあたりで。

マクロ(1)
マクロの作成方法が気になっていた。
そもそもマクロには種類がある。
macro_rules!
を使った 宣言的 (declarative) マクロ- 3種類の 手続き的 (procedural) マクロ:
- 構造体と
enum
にderive
属性を使ったときに追加されるコードを指定する#[derive]
- 任意の要素に使えるカスタムの属性を定義する、属性風のマクロ
- 関数のように見えるが、引数として指定されたトークンに対して作用する関数風のマクロ
また、
関数シグニチャは、関数の引数の数と型を宣言しなければなりません。
一方、マクロは可変長の引数を取れます。
println!("hello")
のように1引数で呼んだり、
println!("hello {}", name)
のように2引数で呼んだりできる。
気にしていなかったが、言われてみればたしかにそうだ。
下記は簡略化された vec!
マクロである。
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
#[macro_export]
注釈は、マクロを定義しているクレートがスコープに持ち込まれたなら、
無条件でこのマクロが利用可能になるべきということを示している。
将来、Rustには別種の宣言的マクロが登場する予定です。
そのアップデート以降、macro_rules!
は事実上非推奨となる予定です。
match
式のようなもので、正規表現のようなキャプチャをし、そのアームの先に書かれていることに基づいて式が展開されるという具合らしいが、非推奨になるならあまり深入りはしないことにする。
以降色々読んでみたが、あまり得たい情報は得られなかった気がするし、複雑なものは自分で定義するべきでもないような気もした。

なんやかんやで紙に書くお勉強に戻ってしまったので閉じます