ふと思い立って Rust という言語である自分用のツールを書いた。大変気に入ったのでメモを残す。
基本情報
- ウェブサイト: https://www.rust-lang.org/
- 色んなインストール方法があるが、周辺ツールを使い始めると結局本家お薦めの
curl https://sh.rustup.rs -sSf | sh
が一番良かった。
- 色んなインストール方法があるが、周辺ツールを使い始めると結局本家お薦めの
- チュートリアル: https://doc.rust-lang.org/book/guessing-game.html
- Rust の基本が書いてあるが、一番面白い参照について触れてない。
- 参照について https://doc.rust-lang.org/book/ownership.html
- Ownership, References and Borrowing, Lifetimes の三章で Rust の Rust らしい部分が解説されている。
引っかかった所
Rust はとても素直な文法だけど、記号を使っていて検索に引っかかりづらく分かりにくい文法があった。
- 変数名のビックリマーク ! はマクロを表す。https://doc.rust-lang.org/book/macros.html
- 最初の Hello World から println! が出て来るが、これはマクロ。マクロがあるとエラーメッセージがわかりづらく注意が必要。
- 関数適用後のはてなマーク ? は try! マクロ https://m4rw3r.github.io/rust-questionmark-operator
- API ドキュメントのサンプルによく ? が出て来る。これは関数適用の結果がエラーの時に return するマクロ。
- エラー処理を例外を使わず簡潔に表現出来る。
- カッコ閉じ忘れ等の文法間違いで std::ops::Carrier` is not satisfied という謎のエラーが出るので注意が必要。
- 関数からの戻り値
- 最後の文に ; があると値が返らない。; が無いとその値が返る。
- ポインタ演算子が無い。
- val が Struct の時、その参照を &val と書くが、val.member と &val.member は同じように Struct の要素を指す。
色々な Struct の返し方
Rust の面白いメモリ管理の仕組みは Ownershipに詳しく書いてあるけど、実際にコードを書かないと何のためにあるのかわかりづらいです。そこで、ある Struct を色んな方法で関数から返して違いを見ます。仕様は以下のような感じです。
- ある関数は文字列の組 (String, String) を受け取る。
- ある関数は二つの文字列から出来た Struct を返す。
#[derive(Debug)]
struct RealStruct {
head: String,
tail: String,
}
1. 参照を借りて実体を返す。clone が発生する。
まず最初に思いついた rust コンパイラに怒られない方法は、文字列の組の参照を受け取って各要素をコピーし、Struct を作って返すやり方です。
fn real_from_ref(pair: &(String, String)) -> RealStruct {
RealStruct {
head: pair.0.clone(), // pair.0 で引数 pair の最初の要素を指す
tail: pair.1.clone(),
}
}
入力は参照なので軽いですが、Struct を作るにあたって文字列を clone() しています。rust はこの clone() が無いとコンパイルを通しません。何故かと言うと、rust ではヒープ内のデータ(この場合 String)の持ち主(実体を割り当てられた変数)のスコープの外でデータを参照出来ないという制限があるからです。
Rust の画期的な所は、スタック上のデータと同じようにヒープ上のデータもスコープから抜けると自動的に解放する所です。このお陰で GC が無くても安全にメモリを使う事が出来ます。ヒープ内のデータには必ず一人持ち主がいます。その持ち主がスコープから抜ける時にヒープ内のデータも解放されます。上の real_from_ref
でもしも clone() しないと、同じ文字列を参照する別の変数が出来てしまうので Rust はコンパイルエラーを出します。実際に real_from_ref
を呼び出すコードは以下のようになります。
let dst: RealStruct;
{
let src = ("Hello".to_string(), "world".to_string()); // to_string() は文字列リテラルから String オブジェクトを作る。
dst = real_from_ref(&src);
println!("inside dst: {:?}", dst); // ちゃんと作れた。
println!("src: {:?}", src); // 元データも読める。
}
println!("outside dst: {:?}", dst); // スコープを抜けても使える。
clone() の分だけ手間ですが、お陰で src のスコープを抜けた後でも dst は使えます。
2. 実体を受け取り実体を返す。受け取ったデータをそのまま要素として使う。
次に実体を受け取り実体をそのまま返してみます。受け取ったデータをそのまま clone() せず要素として使います。
fn real_from_real(pair: (String, String)) -> RealStruct {
RealStruct {
head: pair.0,
tail: pair.1,
}
}
普通の言語っぽい書き方ですが、この場合呼び出し側に注意が必要です。
let dst: RealStruct;
{
let src = ("Hello".to_string(), "world".to_string());
dst = real_from_real(src);
println!("inside dst: {:?}", dst); // ちゃんと作れた。
// println!("src: {:?}", src); // 元データは移動してしまったので読めない!!!
}
println!("outside dst: {:?}", dst); // スコープを抜けても使える。
Rust では、ヒープ内のデータの持ち主は一人と決まっています。dst = real_from_real(src);
で src にあったデータの所有権を関数に渡してしまったので、その後 src を使うことは出来ません。このように実体から clone() 無しで実体を作ると元の変数を使えなくなります。これによって安全にゼロコピーな処理を書く事が出来ます。
3. 参照を受け取り参照入りの構造を返す。
今度はちょっと趣向を変えて、Struct に参照を入れてみます。
#[derive(Debug)]
struct RefStruct<'a> {
head: &'a str,
tail: &'a str,
}
'a は lifetime parameter という物ですが、まだ私は説明出来ません。。。兎に角参照を使った構造を返してみます。これだと clone() もせず、所有権も移動しないので一番よさげに見えます。
fn ref_from_ref(pair: &(String, String)) -> RefStruct {
RefStruct {
head: &pair.0,
tail: &pair.1,
}
}
残念ながらこれも呼び出し側に注意が必要です。
{
let src = ("Hello".to_string(), "world".to_string());
let dst: RefStruct; // dst は src の後で宣言する必要がある。スコープを出る時宣言の逆順で破棄するので、src より長生きしてはならない。
dst = ref_from_ref(&src);
println!("inside dst: {:?}", dst); // ちゃんと作れた。
println!("src: {:?}", src); // 元データも読める。
}
このコードでは所有権の移動もコピーも無いので、dst に構造が渡された後でもデータの所有権は src にあります。という事は、src のスコープを外れると dst を使う事は出来ません。Rust コンパイラはデータの所有者のスコープ外でデータを参照しようとするとちゃんとエラーを検出してくれます。
まとめ
利点 | 欠点 | |
---|---|---|
参照を借りて clone() して実体を返す | 引数のスコープから外れても使える | clone() が必要 |
実体を受け取り要素に入れて返す | 引数のスコープから外れても使える | 元データが使えなくなる |
参照を受け取り要素の参照にして返す | 適用後も引数のデータを使える | 元データがスコープから外れると作ったデータも使えない |