見出し画像

Rustaceanへの道

コロナになってブログをかけていませんでした。すみません。38度後半の熱が4日間続いて死ぬかと思いました。

最近、rust を使って IOTA 関連の個人開発をしています。初めはrustの勉強を全くせずにググったりcopilotの補完機能やChatGPTを使って「rustを使って書く」ことをしていました。初めてプログラミングを勉強したときは文法などから勉強しましたがそれ以降は基本的にこのやり方で新しい言語を学んできました。いわば書きたいと思ったら作りながら覚えるという感じです。
しかし、rustだとそのやり方だとつまづいたので「調べながら作る」のは一旦やめてrustの概念や独特な仕様について勉強しているところです。
rust を勉強していると他の言語で言うところのGC(Garbage Collection)やメモリの解放についてもう一度意識するきっかけになり、書いていてとても面白い言語だなと思いました。

そのためこの記事では、備忘録とともにrustのどんなところがおもしろいと感じるのかを書いていきます。

所有権(Ownership)

rustでは所有権(ownership)という概念があります。これは、メモリの使い方をどうやって管理するかを扱うことのできる機能のことです。
C#では GC が、一定期間使用されていない変数があれば、その変数が使用しているメモリを解放してくれますし、C ではプログラマが明示的にメモリの割り当て、解放をする必要があります。GC、明示的なメモリ管理どちらともにメリット、デメリットがありますが。GC ではプログラマがメモリ管理に気を配る必要がないため、メモリリークや不正なメモリアクセスによりプログラムが落ちることがほとんどありません。そのかわり、メモリ管理をGCに一任するため、メモリのFragmentationなどがおき効率よくメモリを使用することができないというデメリットがあります。一方で、Cなどの言語ではメモリの解放を失敗するとメモリリークがおきますし、ダングリングが起きたりもします。

これらの問題を解決するのが所有権です。
rustでは、変数hogeが以下のように宣言されるとhogeが文字列(Hello)の所有権を持つことになります。ここで大事な点は所有権はスコープを抜けると、drop という関数が自動でよばれ所有権がなくなるというところです。

let hoge = String::from("Hello");

このとき、メモリに持っている情報は、Hello が格納されている Heap 領域へのpointerと、length、capacity です。lengthとcapacity はそれぞれhogeが使用しているメモリ量を byte で表したもの、capacityはメモリから割り当てられたそうメモリ量を byte で表したものです。つまりcapacityはhogeが長くなる分のメモリ量も含まれます。

https://doc.rust-lang.org/stable/book/ch04-02-references-and-borrowing.htmlより引用

このときhogeをfugaという変数に代入したとします。

let fuga = hoge;

このとき、今までの言語の感覚だと Heap領域が新たに作られ、fuga はその新しい Heap 領域への pointer を持つような感覚がします。
つまり、hoge の標準出力は"Hello"となり、fuga の標準出力も"Hello"となるのが今までの私の感覚でした。

println!(hoge); // "Hello"
pringln!(fuga); // "Hello"

しかし、let fuga = hoge としたとき、hogeは"Hello"の所有権を失うのでhogeという変数を使用することができないのです。これをrustでは move と言います。

let fuga = hoge;
println!(hoge); // Error

先ほどの図で言うとhogeは Heap 領域への pointer を失い、fugaが新たに pointer、length、capacity を持ちます。

なぜ、このようなことが起きるのかというと、rust において先ほど話したメモリ管理が厳密であるからです。もし、let fuga = hoge としたとき shallow copy が起きたらどうでしょうか。同じ Heap 領域に対してfugaもhogeもpointerを持つことになります。しかし、所有権の概念により先ほど話したようにスコープを抜けたときfugaもhogeも所有権を失います。
つまり同じ Heap領域に対してdropが2回呼ばれることになってしまいます。これではここでメモリの扱いに不備が生まれるのでエラーになってしまいます。
これを防ぐために所有権のmoveが行われるのです。C#だとメモリ領域とHeap領域の不要な確保をしてしまい、Cだとfugaにもhogeにもメモリ解放を行う必要があるところ、所有権とmoveの概念によりGCを使わなくてもメモリ管理が安全に行われるという点は本当に素晴らしいです。


参照と借用

move は関数に変数を渡した時にも発生してしまいます。これは少し不便を生んでしまいます。例えば以下のコードではhogeは関数 give_owner の後に使うことはできません。

fn give_owner(s: String) {
    println!(s);
}

let hoge = String::from("Hello");
give_owener(hoge);

println!(hoge); // Error!!!

なぜなら give_ownerにこの時点でhogeの所有権を渡しているからです。これはコンパイラでコンパイル時に生成されメモリ領域に置かれるマシン語と、ランタイム時に評価されHeap領域に置かれる値の違いについて想像すると理解は容易です。関数を実行するということはメモリ上のある領域にジャンプする必要があります。その際に引数があるということはジャンプ先で使用できるように所有権を渡す必要があることは直感的に理解できます。

しかし、引数で使用したらその変数はそれ以降使用できないのは少し不便です。それを解決するのが参照(references)と借用(borrowing)です。
&を変数の前につけると所有権を渡すことなく値を参照することができます。

let hoge = String::from("Hello");
let len = get_len(&hoge)

println!("The length of '{hoge}' is '{len}'.");

fn get_len(s: &String) -> usize {
    s.len()
}

上記のコードでは hoge、String の前に&がついています。&hoge は referencing, &String は borrowing です。get_len 関数はhogeの値を「借りている」のです。
これだと所有権を get_len に渡さないのでエラーなく動かすことができます。

感想

上記の2点だけでもrustではメモリ、Heapでどのように値が置かれていくのかを意識しながらコードを書きつつ、メモリ管理そのものには気を使いすぎることなく開発ができるということがわかります。他の言語ではコードを書きながらメモリに置かれている変数、Heapに置かれている変数を意識しながら書くことはほとんどないですがrustでは書きながら無意識に配慮することができるのです。これはとてもおもしろいです。加えて、borrowingしているときはその変数を書き換えることができないなどの制約もあり、安全なコードを書くことができるんだろうなという予感がプンプンします。mutという修飾子をつけることで参照先でも値を変更することができるという技もあるのですが、それも一つの関数に対して一つしか渡すことができません。それも所有権という性質を考えれば、2 つ以上の参照先のデータを書き換えるのを制限することは理にかなってるとも思えます。

まだまだrust勉強中の身ですがrustをチョットワカルまでは行かないまでも完全に理解するくらいにはなりたいです。


引用


この記事が気に入ったらサポートをしてみませんか?