【3日目】Rust入門者に贈るRust入門者がRustと闘う物語 -所有権を頑張って理解する-


最近、巷で話題のRust。システムプログラミング言語などの低レイヤー言語は触ったことがない筆者が、裸一貫で闘っていく、そんな物語です。

前回の記事

本日は、所有権をやっていきます。URL

所有権という名称を、Rustを調べていた時によく散見しましたが、どうやらとっても大事かつ難しい独特な概念のようです。焦らず、しっかりと腰を据えて学んで参ります。

今回は、概念の説明に3ページ使っているので、全て読破した後、また戻って手を動かすことにします。

なお、恥ずかしい話ですが、私は経済学を専攻しているためデータ構造とアルゴリズムなどの基礎的知識がほぼないです。なので用語とかはよくわからない状態で攻めていきます。

はじめに

※物語共通で使用するので不要な方は飛ばしてください。

開発環境や参考サイト、学習フローチャートなどの概要について話します。

本物語について

私は、今業務で使用している言語や趣味でしようしている言語を含め、使用している全ての言語を独学で勉強してきました(せざるを得なったためですが)。

そして、毎回のことのように何度も壁にぶつかり、やっとの思いで扱えるようになるため、プログラミング言語の取得は、修行のようなものだと考えています。

これは私自身の能力不足なのかもしれませんが、大小はあるにしても誰しもが共通して感じるものなのかなと思いまして、後にRustに入門する方々に参考になればと思い、物語として記録しておきます。

本物語の目的

・初心者としてありがちな凡ミスやくだらないエラーを赤裸々に記録し、後続する方々の参考にして頂くため
・自分用のメモ

本物語での記述について

ファイル名:わかりやすく日本語で書いていますが、自らの環境で真似するのはお勧めできません。

表現の置換:私の最も得意として楽に記述できる言語がJavaScript(特にES6の書き方が好きです)のため、多くの部分でJavaScriptで置換して表現します。多くの人がわかるようにPythonで置換しようと思ったのですが、Pythonの書き方は(個人的に)独特なので、これまた知名度の高いJavaScriptで置換しようと考えました。

エラーやつまづきポイント:敢えて全てのエラーの記録をお伝えしますし、遠回りの手法をお伝えすることがあります。これは私を反面教師にして同じ轍を進まなくて済むためと考えています。

※なお、全体を共通して「厳密性」より「なんとなく」を優先することが多々あるかと思います。その点ご了承ください。

対象読者

・今からRustに入門しようとしている方
・プログラミング自体はある程度できるけど、組み込み系やOS設計などの低レイヤーの部分は触れたことがないという方
・開発自体は好きだが、言語がある程度使えるまでの道のりがしんどい方

Why Rust

・業務でWebAssemblyが必要になりそうだった
・何か1つの言語を1から丁寧に学んでみたかった
・流行っていて今後本格的に低レイヤー言語としての地位を獲得しそうな雰囲気がした
・調べると、難しこと以外でデメリットが見当たらなかった
・名前が良い響き

開発環境とスキルセット

再現性は不明ですが、念のため記しておきます。

開発環境

PC: macOS Catalina 10.15.2
開発エディタ: VSCode
Rust: 1.41.0

スキルセット

基本的にWebフロントエンドとソフトウェア開発(金融系など)がメインですが、、

業務で使用
Javascript(デフォルト)(Node.js, Vue.js, React.js, Jquery, etc)
Python(主にAPIなどのバックエンド)(あまりフレームワークは使わない)
Swift(単純な業務アプリ等)
PHP(Wordpressカスタマイズとか頼まれたら)(比較的苦手)

趣味程度
Golang(速度が必要な処理でAPI作成)
Julia(計量経済学を学習した際に)
R(計量経済学を学習した際に)

主にWebのお仕事をもらいつつ、頼まれたら業務に必要なアプリ作ったりしています。
Swiftが一応システムプログラミング言語ですが、そこまで複雑じゃない業務管理アプリ程度しか作ったことがないので、システムプログラミングエンジニアとしては初心者です。
Golangは、Node.jsで速度処理に困った際に比較で学んだのがきっかけで業務での使用はないです。いつかお仕事できればいいな。

ちなみに、よく聞かれるので、JavaとRubyの使用経験についても記載しておきます。
・Java(Kotlin等も)は無茶振りで超単純なアプリを作ったくらいで、趣味にも入らないくらいです。
・Rubyは、変なセミナーに誘われることが多かったので怖くて触れてません。

学習スケジュール(予定)

そもそも私自身Rustを学ぶ目的というか必要がありまして、実は金融系ソフトウェアの開発にあたりWebAssemblyを使うかもしれない可能性があり、そのため最終的にWebAssembly系で何かできたらと思っています。

恥ずかしながら、こうしてスケジュールを組んで学習というのが初でして、いまいち書き方がわかりませんが、以下のようにしております。

基礎学習

n日目 やること
1日目 Rustと周辺ツールのインストールとセットアップ
2日目 3. 普遍的なプログラミング概念
3日目 4. 所有権を理解する
4日目 5. 構造体を使用して関連のあるデータを構造化する
5日目 6. Enumとパターンマッチング
6日目 7. モジュール
7日目 8. 一般的なコレクション
8日目 9. エラー処理
9日目 10. ジェネリック型、トレイト、ライフタイム
10日目 11. テスト
11日目 13. 関数型言語の機能: イテレータとクロージャ
12日目 14. CargoとCrates.ioについてより詳しく
13日目 15. スマートポインタ
14日目 16. 恐れるな!並行性
15日目 17. Rustのオブジェクト指向プログラミング機能
16日目 18. パターンは値の構造に合致する
17日目 19. 高度な機能

こちらを参考にして、スケジュールを組んでいます。

基本的にセクション毎を学んでいきます。量などにムラがあると思いますが、どうにかやり切る覚悟でいきましょう。

こちらはプロジェクト学習で最終的な復習をする前提で、一旦込み入ったことなどはスルーするかもしれないです。個人的に、80%くらいの精度で学習して、最後に実践的にボロボロになって、20%の総和を回収するのがなんだかんだ効率的だと思いますし、何より前に進んでいる実感からモチベが湧くので(ただ筋というかコアの部分は理解する必要があると思いますが)。

プロジェクト学習

n日目 やること
1つ目 2. 数当てゲームをプログラムする
2つ目 12. 入出力プロジェクト: コマンドラインプログラムを構築する
3つ目 20. 最後のプロジェクト: マルチスレッドのWebサーバを構築する

これらは1日でやり切れたらいいですが、文量なども多いので頑張れたらとしましょう。また、日数ではなくて、わからない点や疑問点をこのプロジェクトを通してゼロにしたいと考えているため、1つに1週間くらいかかることも想定されます。

付録

一旦スルーします。必要に応じて見ます。

応用学習

未定ですが、WebAssemblyで何かしたい。

参考サイト等(仮)

Rustチュートリアルがしっかりしてるサイト

何をやったか(3日目)

4. 所有権を理解する

今回は、ほぼほぼ文章になるので、上記URLのタブも開きつつ進んで頂けたらと思います。私は、その流れに合わせて独自の解釈をしていきたいと考えています(本当にわかりやすい説明を求めている方は他の人が書いているような記事を参照するのがいいかと)。

今回出てくる概念と用語一覧

・スタックとヒープ(不変性と可変性)
・所有権
・ムーブ
・クローンとコピー(Copy)
・参照と借用
・スライス
(以下は、記事末尾に軽く説明します。これらは、全体を通して必要になることも多いので、先に理解することを推奨します。)
・ポインタ
・メモリ
・スコープ
・ダングリングポインタ

個人的に理解しやすい方法

持論ですが、「理解」という行為は自らの言葉で言語化でき、抽象的概念であれば具体化した例を創り出せる次元のことだと思っているので、以下では、そこをゴールとしています。

また、具体的に説明していても雲を掴むように思えたので、喩え話や別分野の概念を流用したりします。なお、誤った理解の可能性もあります。

スタックとヒープ(不変性と可変性)

スタックとヒープについては名前だけしか知らなかったので、理解するのに時間がかかりました。旧ドキュメントを読み込みました。

定義

スタック

スタックは、得た順番に値を並べ、逆の順で値を取り除いていきます。参照元

とあります。得た順番とありますが、得るのはデータのことです。

ヒープ

ヒープは、もっとごちゃごちゃしています: ヒープにデータを置く時、あるサイズのスペースを求めます。 OSはヒープ上に十分な大きさの空の領域を見つけ、使用中にし、ポインタを返してきます。ポインタとは、その場所へのアドレスです。(参照元は同上)

とあります。

比較

以下は高レベルでの比較です。

スタックは非常に速く、Rustにおいてはデフォルトでメモリーを割り当てる場所です。 しかし、その割当ては関数呼出しに対してローカルであり、サイズは制限されます。 一方、ヒープは遅く、あなたのプログラムで明示的に割り当てるものです。 しかし、サイズは事実上無制限であり、グローバルにアクセスすることができます。参照元

つまり、スコープ制限がありサイズの制限もありが高速なスタックと、グローバルでサイズ無制限だが遅いヒープという簡単なまとめができそう。

解釈

上記の定義比較で解釈しつつ、プラスで以下と考えている。

メモリ割り当ての仕方が、スタックは順々に入れられるが、ヒープは飛び飛びになることもある。

加えて、感覚的な比較として以下と捉えている。

例:料理屋さんなどで、フルコースがスタック、食べ放題がヒープ(料理人やホールの人間などの人的割り当て(allocate)をフルコースなら簡単に決めれるが、食べ放題だとプラス複数名いて欲しくなる。)。

学習で使用した方法

私は、こちらをゴールに据えて、
上記シチュエーションを理解する目的でその他の文献を調べてなんとか自分で表が書けるように持っていきました。

所有権

定義

以下のルールを持つ概念

ルール

・Rustの各値は、所有者と呼ばれる変数と対応している。
・いかなる時も所有者は一つである。
・所有者がスコープから外れたら、値は破棄される。

解釈

このルールを聞いて、思い浮かんだのは、全単射写像です。ここで詳しく説明するのは脱線するので省略しますが、写像と考えて進むとスムーズに理解できました。簡単に言うと、rustのルール1のまんまですが、1対1の対応をしていることです。ニコイチです。

コード例

src/main.rs
fn main() {
    //これでヒープ領域に保存される変数を宣言できる
    let mut s = String::from("hello");

    s.push_str(", world!"); // push_str()関数は、リテラルをStringに付け加える

    println!("{}", s);
}

学習で使用した方法

今どいつが所有権(ないし参照(後述))持っているかを追いながらコードを見ればわかりやすいと思います。所有権は1人しか持てないので、そこが追いやすいです。

ムーブ

定義

ヒープデータのコピーは、シャローコピーではなくムーブする。

ムーブとは、下記コード例にあるが、ヒープデータs1をs2がコピーしようとした時、同一ポインタを所有することになるが、s1はもはや存在しないかのように扱われてしまう。

解釈

シャローコピーされるけど、元のやつはアクセスできなくなってしまうっていうので結構理解しやすいのかなと思います。

イメージ.sh
//普通(JavaScriptなど)
ln -s target.txt anotherName.txt
//Rust
mv target.txt anotherName.txt

各ファイルをポインタと思ってください。

コード例

src/main.rs
fn main() { //コピーについて
    let x = 5; // スタック
    let y = x; // 純粋にコピーされる。

    let s1 = String::from("hello"); // ヒープ
    let s2 = s1; // s1はs2に取って代わられる。これ以降s1にアクセスしようとするとエラー
}

クローンとコピー(Copy)

定義

クローン

ディープコピーすること。当然ですが、シャローコピーよりコストが高いです。

コピー(Copy)

スタックデータはデフォルトで値が丸っ切り複製され、ムーブされることはない。そのため、ディープ・シャローコピーの概念もない。

解釈

これらはわかりやすいのかなと思います。強いて言うなら、Copyトレイトの種類は網羅したいところですね。付録に書いてあるとのことで見ておきます。

コード例

src/main.rs
fn main() {
    //クローン
    let s1 = String::from("hello");
    let s2 = s1.clone();//丸ごとデータの実際の値がコピーされる

    println!("s1 = {}, s2 = {}", s1, s2);

    //(既出)コピー
    let x = 5;
    let y = x;//丸ごとデータの実際の値がコピーされる

    println!("x = {}, y = {}", x, y);
}

所有権と関数

この部分は以上のまとめとしてもわかりやすいですし、実践的な内容のため取り扱うことにしました。

コード例

src/main.rs
fn main() {
    let s = String::from("hello");  // sがスコープに入る

    takes_ownership(s);             // sの値が関数にムーブされ...
                                    // ... ここではもう有効ではない
    //println!("{}!", s);             // エラーになります。

    let x = 5;                      // xがスコープに入る

    makes_copy(x);                  // xも関数にムーブされるが、
                                    // i32はCopyなので、この後にxを使っても大丈夫
    //println!("{}!", x);             // 問題なく表示されます。

} // ここでxがスコープを抜け、sも。だけど、sの値はムーブされてるので、何も特別なことはない。
  //

fn takes_ownership(some_string: String) { // some_stringがスコープに入る。
    println!("{}", some_string);
} // ここでsome_stringがスコープを抜け、`drop`が呼ばれる。後ろ盾してたメモリが解放される。
  // 

fn makes_copy(some_integer: i32) { // some_integerがスコープに入る
    println!("{}", some_integer);
} // ここでsome_integerがスコープを抜ける。何も特別なことはない。

少し写経に手を加えました。

ただ、ムーブ元データの再利用に少し難があるとのことで次に参照について紹介されます。

参照と借用

定義

参照

引数として所有権がうつるわけでなく、あくまで参照としてデータの受け渡しができる。そのため、参照元のデータ(ムーブだったらムーブ元のデータ)は有効のデータとして使用できる。

&をつけることで使用可能。基本は不変。

借用

参照すること。返却が必須。

解釈

ムーブは譲渡、参照は賃借でイメージ。持ち家か賃貸か。

内部でディープコピーが行われていると勝手に解釈(事実はわかりません、実際コストがムーブと比べてどのくらいかかるのか気になりますね)。

不変参照と可変参照

不変参照という言葉があるかは知りません。不変的な参照のことです。

不変参照

通常の参照ですが、関数に渡された後に変更しようとしてもエラーがおきます。(コード例2に載せています)

可変参照

参照を変更可能にしたもの(コード例3)。ただ以下の注意点に留意する必要がある。

注意点

可変参照は同一スコープ内で一つまでしか使用不可(コード例4に記載。データ競合というものを避けるためらしい)
不変参照をしたものを可変参照することはできない(コード例5に記載。ただ僕の環境ではエラーにならなかったです。)
・参照は基本変更不可(既出)
・ダングリングポインタに注意(文末で解説)

コード例

コード例1
src/main.rs
fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    // '{}'の長さは、{}です
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}
コード例2
src/main.rs
fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}
コード例3
src/main.rs
fn main() {
    let mut s = String::from("hello");//ここでもmutキーワードが必要

    change(&mut s);//mutキーワードで可変参照となる
}

fn change(some_string: &mut String) {//引数定義にもmutが必要
    some_string.push_str(", world");
}
コード例4
src/main.rs
fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    //let r2 = &mut s; // これはエラー

    let mut s = String::from("hello");

    // これはセーフ
    {
        let r1 = &mut s;
    } // r1はここでスコープを抜けるので、問題なく新しい参照を作ることができる

    let r2 = &mut s;
}

コード例5

以下コードでエラーにならなかった。仕様が変わったのかな?

src/main.rs
fn main() {
    let mut s = String::from("hello");
    let r1 = &s; // 問題なし
    let r2 = &s; // 問題なし
    let r3 = &mut s; // 大問題!じゃなかった
    let r4 = r3.push_str(", world"); // 通る
}

スライス

ここの範囲は、部分的な参照ということを念頭におけば、普通に他の言語のArrayやList型と同じなので、割と端折ります。

定義

コレクション全体というより、 その内の一連の要素を参照する機能

解釈

参照の一部バージョン

文字列スライス

コード例

基本的な使い方と、同じ意味の表現集です。

src/main.rs
fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5];
    let world = &s[6..11];

    //同じ意味の表現1
    {
        let s = String::from("hello");

        let slice = &s[0..2];
        let slice = &s[..2];
    }

    //同じ意味の表現2
    {
        let s = String::from("hello");

        let len = s.len();

        let slice = &s[3..len];
        let slice = &s[3..];
    }

    //同じ意味の表現3
    {
        let s = String::from("hello");

        let len = s.len();

        let slice = &s[0..len];
        let slice = &s[..];
    }
}

用語集

・ポインタ
・メモリ
・スコープ
・ダングリングポインタ

を説明します。

ポインタ

変数のアドレスを格納している変数のことで、アドレスという固定値をプログラム上で扱いやすくしています。

メモリ

プログラムで使用するデータを(一時的に)保存する場所。ポインタがあるだけその分メモリ確保が必要になる。

スタックデータは、固定長で済むが、ヒープデータは可変長のためメモリを余分に食う。

スコープ

rustでは、{}で囲われていたら、スコープ。短文で説明するのが難しいので、調べると具体例が出ると思います。

ダングリングポインタ

ポインタが、まだ使うのに解放されてしまった場合に発生する問題。

src/main.rs
fn dangle() -> &String { // dangleはStringへの参照を返す

    let s = String::from("hello"); // sは新しいString

    &s // String sへの参照を返す
} // ここで、sはスコープを抜け、ドロップされる。そのメモリは消される。
  // 危険だ

ざっくり要約

・所有権は、難しい
・本文中にあるように、スタックとヒープは大事だと思います
・所有権を引数に渡す方法は、ムーブと参照(とスライス)がある

よくわからなかったこと、問題点など

・可変参照でのコード例がエラーにならず、通ってしまう

コード例5

以下コードでエラーにならなかった。仕様が変わったのかな?

src/main.rs
fn main() {
    let mut s = String::from("hello");
    let r1 = &s; // 問題なし
    let r2 = &s; // 問題なし
    let r3 = &mut s; // 大問題!じゃなかった
    let r4 = r3.push_str(", world"); // 通る
}

・参照渡しの方法と概念はわかったが、仕組みがわからない(内部的にディープコピーなのかなど)
・このレベルで所有権を学習したと思っていいのかとても不安。もっと実践を積みたいし、高度な部分を知りたい。

気をつけた方が良いこと

・全てです笑(本当に僕なんかフロントエンドエンジニアからすると、結構細かい仕様だと思うので、注意しまくりました)

まとめ

今回はページ数に対してのボリューム凄かったです。最後時間なかったので記述が適当になっているので、問題があったら指摘して頂けると幸いです。
システムエンジニアリングが全く範囲外だったので苦労しました。

実は、僕コロナウイルス系のメディアを運営しているのですが、コロナウイルス系のニュースが多すぎて管理に非常に時間がとられてしまいました。マッキーも捕まるし、ニュースが忙しかったですね。
それ抜きでも、純粋に8時間くらいこのセクションに使ってしまいました。難しいです。
特にスタックとヒープの概念が初めてすぎて、最初にガッツリつまずきました。ずっと、配列の話してるもんだと思ってました笑

明日は日中に出せるよう頑張ります。

それにしても、エラーが出なかったのはなんでなんだろう。

次の記事

遅れましたが書きました。こちら