見出し画像

スーパー雑な Rust メモ : 所有権(ちょっとずつ覚えれば良い!)

Rust 勉強の、ごく個人的にメモした内容を note に載せておこうと思います。
個人的なメモや感想レベルなので、専門的で正確(?)な解説は本家や書籍をあたってください。

所有権が飲み込めるまで

Rust の難関(初心者離脱ポイント)のひとつとされる所有権(Ownership)。

僕の場合、最初に『プログラミングRust』の第1版(当時ほかに選択肢がなかった)を読んで、所有権について知り、このときは知っただけで終わりました。ほとんど理解できなかったです。

2018年当時の僕は、Python、JavaScript など、静的な型の宣言が不要で、ガベージコレクタ(GC)ありの言語「しか」使っていなかったんです。
C言語などを基礎として学んではいましたが、実際に現場レベルの使用をしたことはなかったです。

だからこそ、自分のスキルの幅を広げたくて Rust に挑戦したんですが、所有権を理解するためのイメージ力というか素養というか、そういうのが足りなかったなぁと思います。

ちなみにその後、仕事で使う必要があって Go 言語を使うようになりました。
ここで実際のプロジェクトで静的型付け言語を使うことによって、型があることの利便性などを身をもって体験しました。

Go を経験したら、型に対しての苦手意識がなくなったので TypeScript なんかも勉強して使ってみました。

僕の場合、実際のプロジェクトで使ってみないと覚えないようです。

以降現在も Go 言語を主にメインの開発言語として使っています。
そうやって Go を使っていく中でレベルアップのための書籍を読んでいると、「ここは GC の関係でこう書いている」みたいなコードを見かけるようになりました。

GC というのはある意味で(めんどくさがりの人には特に)ブラックボックス性が高くて、

  • 仕組みを理解してしまうか

  • 挙動のポイントを押さえてお付き合いしていくか

の二択(もしくは両方)だと個人的には思います。

GC の挙動に関してあれこれ考えたり、情報を集めたり、それを踏まえてコードを工夫しだしたり、そういうステージになってくると、

「GC がない言語ってどういうアプローチしてるんだろう?」

という疑問が自然と湧いてくるようになります。

僕の場合ですが、このステージが訪れるタイミングまで Rust の学習をサボっていて、そうしているうちに世の中で Rust が注目されるようになり、書籍も増え…と、学習者には美味しい状況になった、という感じです。

そんな流れで自分の側の準備がある程度できた状態で、クジラ飛行机さんの『手を動かして考えればよくわかる 高効率言語 Rust 書きかた・作りかた』という書籍に出会い、所有権の考え方、実際のコードの中でなにがどうなるのか、などを理解することが出来ました。

以下は僕なりに所有権を理解するコツってこのへんかな?というのを少し書きます。

消費ポイントを知る

消費ポイントとは、値の所有権がもとの持ち主から別の持ち主へ移るタイミングのことを指しています。

所有権の移動(Move)になる基本中の基本パターンはこれです、別の変数に代入するとき。
※ ↓ このコードはコンパイルエラーになります。

fn main() {
    let a = "aaa".to_string();
    let b = a;
    println!("a={}, b={}", a, b);
    //                     ^ value borrowed here after move
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=33a92114a64572a45913c947104f6f46

String 型はヒープ(Heap)に記憶領域を確保するので他の変数へ単純に代入をしてしまうと、所有権が移動します。

関数の引数に値を渡した際も消費されてしまいます。
※ ↓ このコードはコンパイルエラーになります。

fn main() {
    let a = "aaa".to_string();
    let b = consume_string(a);
    // Error               - value moved here
    println!("{} -> {}", a, b);
}

fn consume_string(s: String) -> String{
    format!("{} -> b", s)
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=f70fa249044069a414d89fa5c97c9246

それに加えて、for 文などループ処理を使用した際にも Move が発生します。
※ ↓ このコードはコンパイルエラーになります。

fn main() {
    let arr = ["a", "b", "c",].map(|x| x.to_string());
    for x in arr { // Error : `arr` moved due to this implicit call to `.into_iter()`
        println!("{}", x);
    }
    println!("{:?}", arr);
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=8d8ef172edddf55d7e97bfd8606db8a7

ここに挙げた3つの「消費ポイント」は、全て値をコピー(クローン)するか、または、参照を使うことでエラーを回避できます。

fn main() {
    let a = "aaa".to_string();
    let b = a.clone(); // a の移動を回避
    println!("a={}, b={}", a, b);
    
    let a = "aaa".to_string();
    // String 型を受け取る=引数を消費する関数に対して
    // クローンを渡して移動を回避
    let b = consume_string(a.clone());
    println!("{} -> {}", a, b);
    // もしくは &str 型を受け取ることで引数を消費しない関数を使用
    let b = ref_string(&a);
    println!("{} -> {}", a, b);

    let arr = ["a", "b", "c",].map(|x| x.to_string());
    for x in arr.iter() { // 参照を供給するイテレータを使用
        println!("{}", x);
    }
    println!("{:?}", arr);
}

fn consume_string(s: String) -> String {
    format!("{} -> b", s)
}

fn ref_string(s: &str) -> String {
    format!("{} -> b", s)
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=5a6fd0faa650ec17d42a363dbb608d9a

GC がない、というか、言語設計上 GC を不採用にしている Rust のような言語では、このように値(データ)の消費がどのタイミングで行われるのかをプログラマが把握していて、かつ、なんらかの対処をしながらコードを書き進めていく必要があります。

これは GC ありの言語でコーディングするのに慣れていると、まずは正直面倒くさいです。
また、以前の自分のように GC ありしか使用経験がないと、値の顛末(いつまで生きてて、どこで参照されて、どこで書き換えられて…など)を毎度考えるという発想や習慣が欠如しています。

そういうわけで、この所有権システムを理解して、実際に自分が書くコードの中で適切に変数を使っていく、という Rust の基本のところで挫折する初心者が多いのだと思います。

いっぺんには覚えられないので出会った順に覚えて行けば良い

ここでは所有権がからむコーディングのほんのかじりだけ書きました。

でも、上に挙げた3箇所(別変数への代入、関数呼び出し、ループでのイテレーション)で所有権が移動(Move)するのが Rust のデフォなんだと知っておけば、シチュエーションごとに「この場合はどうするんだろう?」とまずは問いを立てることができると思うんですね。

少なくとも理由不明のまま闇雲に値を参照にしたりクローンしたりすることは避けられるんじゃないかと。

Rust の言語仕様を詳しく解説している書籍ほどこの傾向にあるという印象ですが、所有権の話が始まるとずんずん深いところまで話が進んで、最初はついていけたけど後半はもう全然わからん!みたいになりがちなんじゃないか、と。

だからここに挙げた超基本のところをまずはおさえて、実際にコードをなんでもいいから書いてみる。
それで、あとはコードでやりたいことの幅が広がってきたらどうせ知識的課題も出てくるわけだから、課題に出会った順に解決して覚えていく。これで良いのではないかと思います。

SN

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