初心者がRustを勉強して学んだこと

2023年12月20日掲載

キービジュアル

この記事は、ソフトバンクアドベントカレンダー2023 の20日目の記事です。

Rustは、8年連続でStack Overflowの調査で「最も愛されるプログラム言語」を獲得し、じんわりと人気が増え続けているプログラム言語です。

今回は、Rust言語を勉強するきっかけ、実際に勉強した感想などを記事としてまとめてみました。Rust言語に興味がある方に参考にしていただければ幸いです。

目次

なぜRustを学び始めたのか

  • C++に対する苦痛な経験があります。

大学時代、C++を使ってCUDAなどのGPU関連のライブラリを使った研究をしたことがあり、ほとんどの時間はCMakeとの戦いに費やしました。

また、何でもかんでもconstとprivateを書くのは普通に面倒でした。

  • 所有権と借用という、Rustの特有の概念に興味があります。

  • Tauriというアプリ開発フレームワークに興味があるためです。

こちらのアプリ開発フレームワークは、Web技術でUIを開発し(HTML、CSS、JavaScript)、Rustでバックエンドを開発するという面白い仕組みで、試してみたいと考えました。

学習資料について

ここはRustを勉強する際に最初に驚いたところです。

Rust開発チームの公式サイトでいろいろ学習資料が公開されていますが、これらの資料の品質は非常に高いです。

また、無料で公開されているのでお勧めです。

今回の勉強に自分が使った資料は主にこちらの2種類です:

The Rust Programming Language

まずは、基礎教材という立ち位置のThe Rust Programming Languageという、公式の勉強本ですが、この本は異常とも言えるほどクオリティーが高いです。この質の本が無料でネット上で公開されていることに最初は非常に驚きを覚えました。

その質の高さが故か、Rust界隈では「the book」(聖書)と呼ばれているらしいです。

非公式ですが、この本の日本語訳もあります。

又、文字を読むのが苦手な場合、こちらのYoutube動画をお勧めします。「the book」の内容を丸ごと動画の形で紹介する動画です。

Rustlings

Rustlingsとは、Rustに関わる演習問題のツールとなります。実際にRustコードを読み書きして基礎知識を学べる素晴らしいオープンソースプロジェクトです。

又、Rustlingsの練習問題の順番は、ほとんど「the book」に従っているため、「the book」を読みながら、勉強した内容を「Rustlings」で復習するというスタイルをお勧めします。

Rustの仕様の面白いところ

まだ勉強の途中ですが、個人的に面白い、嬉しいと感じた仕様を軽く紹介したいと思います。

所有権と借用

Rustといえば、一番特徴的な仕様といえば、やはり所有権と借用となります。Rustがメモリ安全な言語と呼ばれたのは、この仕様の存在が大きいです。

例えば、下のコードを考えます:

fn main() {
    let a = "Hello".to_string();
    println!("{}", a);
    let b = a;
    println!("{}", b);
    // println!("{}", a); エラー:borrow of moved value: `a`
}

他の言語だと考えにくいですが、2回目aを出力しようとしたところ、コンパイルエラーが発生します。

原因は、この一行のコードです:

let b = a;

一見すると、ただのデータのコピー、もしくは参照の受け渡しですが、Rustの世界では、これは「aが所有するデータの所有権をbに渡す」ことを意味します。2回目のaの出力は、既に何のデータも所持していないため、当然エラーになります。

こちらのコードを1つだけ文字を追加することで、エラーが消えます:

let b = &a;

修正後のコードは「aの所有権を一時的にbに貸す」ことを意味します。いわゆる参照渡しです。所有権は、1人だけ持ちますが、データに変更を加えない限り、一時的な借用に特に回数の制限がありません。

又、所有権を持つ対象は、現在のスコープから抜くと、自動的にメモリから解放されます。コンパイル時点で、既に解放された対象(違うスコープ)を借用しようとすると、コンパイルエラーになります。例えば、関数内で生成されたデータの参照を返すと、問答無用にエラーになります。(関数内のデータは関数から抜くと自動的に解放されるため、参照先が消えます)

データに変更を加えたい場合は、借用権に更なる制限がかかりますが、今回はその説明を割愛します。

データの所有権を厳しく制限をかけた結果、c/c++で多発していた参照先の消え、多重メモリ解放、データ書き込みの競合など、コンパイル時点で早期発見することが可能となりました。

もう1つの利点としては、多くのプログラミング言語を持つGarbage Collection(GC)機能が不要となります。(スコープを抜くと自動的に解放されるため、定期的な確認と解放が必要がありません)。GC機能は、予測不能なプログラムの性能低下を招く恐れがあるため、性能に厳しいサービスはRustとの相性がいいです。例えば、CloudFlare社は、Nginxの代替として、Rustで新しいWebサーバーを開発しました

enum

Rustのenum機能は非常に強力です。特にエラーハンドリングと密接に関わっています。

多くのプログラミング言語において、関数の戻り値を処理する際に、細心の注意を払わないと、バグの温床になります。私の体感では、バグ3割ぐらいは、戻り値がnullなどのエッジケースの処理を忘れることから発生したものです。

Rustにはnullが存在しません。代わりに、Option、あるいはResultというenumを利用することになります。

enumを返す利点としては、強制的に全ての戻り値のパターンに対する処理をしないと、コンパイルエラーになります。

例えば、下のファイルを読み込み、その中身をprintするコード:

use std::fs;

fn main() {
    let contents = fs::read_to_string("test.txt");
   //  println!("With text:\n{contents}"); エラー
}

このコードでは、コンパイル時点でエラーとなります。何故かと言うと、test.txtというファイルが本当に存在するかどうかが分からないため、contentsには読み込み成功時のファイルの中身と、読み込み失敗時のエラーという、2種類の値が存在する可能性があります。すべての可能性においての挙動を定義しない限り、コンパイルエラーが発生します。

以下のように、読み込み成功時と読み込み失敗時の挙動を別々で定義するとコンパイルエラーが消えます:

use std::fs;

fn main() {
    let contents = match fs::read_to_string("test.txt") {
        Ok(fc) => fc,
        Err(_) => "ファイル読み込み失敗。".to_string(),
    };
    println!("With text:\n{contents}");
}

この仕様により、nullの処理忘れによるバグはほとんど発生しなくなり、コード全体の安定性を向上させることができます。

コンパイラメッセージ

Rustは、安全性を強調するため、独特なルールが多く、コーディングする際に注意する点が数多く存在します。

幸い、Rustのコンパイラはとても優秀で、非常に読みやすいエラーメッセージを返してくれます。

例えば、所有権を紹介するパーツのコード例のエラーメッセージが以下となります:

error[E0382]: borrow of moved value: `a`
 --> src/main.rs:6:20
  |
2 |     let a = "Hello".to_string();
  |         - move occurs because `a` has type `String`, which does not implement the `Copy` trait
3 |     println!("{}", a);
4 |     let b = a;
  |             - value moved here
5 |     println!("{}", b);
6 |     println!("{}", a); 
  |                    ^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
  |
4 |     let b = a.clone();
  |              ++++++++

For more information about this error, try `rustc --explain E0382`.
error: could not compile `playground` (bin "playground") due to previous error

このエラーメッセージを見れば、エラーになる原因は一発で分かると思います。エラーが発生しているところの情報だけでなく、上下の文脈を含めて解析し、エラーが発生する経緯を丁寧に教えてくれます。又、このエラーを解消するための改善策も一緒に出されています。(本文では別の方法でエラーを解消しましたが、この方法も正しいです。)

Rustを書く過程は、ある意味でこの賢いコンパイラと対話する過程であり、非常に楽しいです。

Cargo

前にも話しましたが、C++を使って一番苦労したのは、CMakeを使ったBuildシステムとの戦いです。何故このようになったかというと、C++のBuild周りの生態系は非常に混乱した状態です。

  • Compiler:gcc、clang、msvc、nvcc(CUDA用)…

  • Build tool: make、msbuild、ninja…

  • Project generator: CMake、qmake、Premake…

一見すると「選択肢が多くいいことじゃない?」と思うかもしれませんが、組み合わせによる相性問題が頻出するため、非常に苦労します。プログラミングは本来、コードを書くことが大事なので、周辺のツールの設定などに労力をかけたくないです。

Rustの場合、標準搭載のCargoでBuild周りがすべて事足りるので、非常に楽です。「これだけでいいの?」「C++でかかった労力は何なんだ」というのが、Rustを触った最初の感想でした。

Rustを学ぶ際苦戦した点

Rustのいいところを沢山紹介しましたが、やはり勉強の過程において苦戦したところもありました。多くは他の言語では見られない、安全性の理由でRust独自の仕様です。

文字列

Rustの文字列の種類が非常に多いです。しかも1つ1つ全部のタイプが異なり、混用することができません。

よく使われるのは2種類:Stringと&str。しかもこの2種類の挙動もかなり違います。

1つの例としては、他の言語ではよく見られる、文字列同士を+サインで繋がるやり方がありますが、RustではString + String、&str + &str、&str + Stringどちらもエラーになります。必ずString + &strじゃないと文字列を繋がらないという煩わしい仕様です。

こちらのサンプルコードでは、aとbはStringタイプで、&aと&bは&strタイプになります。a + &b以外全部エラーとなります:

fn main() {
    let a = "Hello ".to_string();
    let b = "World".to_string();
    // println!("{}", a + b); エラー
    // println!("{}", &a + &b); エラー
    // println!("{}", &a + b); エラー
    println!("{}", a + &b);
}

ライフタイム

例えば、以下の関数を作成しました:

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

一見、2つの文字列の長さを比較し、長い方の文字列を返すだけ、何の変哲もない関数ですが、実は、このコードはコンパイルエラーになります。

Rustに借用権の概念を前述しましたが、多くの場合、この借用権がいつまで有効になるかはコンパイル時点で分かりますが、上記のコードのような、コンパイル時では分からないケースも存在します。具体的に言うと、上記のコードでは&x(xの借用)を返すか、&y(yの借用)を返すかは、実際にコードを実行しないと分からないため、もし借用元のxとyのスコープが異なる場合、返りの借用はいつまでに有効になるかは判断できません。

このコードを修正するために、ライフタイムを注釈する必要があります:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

修正後のコードは、明示的に、「xとyのどちらライフタイムが短い方に、帰り値のライフタイムにします」を宣言して、このエラーを解消できます。

ここで説明したライフタイムはあくまで基礎なので、実際にコーディングする際により複雑な状況に遭遇すると思います。今のところ、ライフタイムはRustを勉強する際に一番難しい壁だと感じています。

Rustの学習曲線

上記のRustとJavaScriptの学習曲線の比較は、こちらのYoutube動画から引用したものです。

自分はRustを勉強している途中で、最初のハードルの高さには確かに感じましたが、マスターにするための労力はまだ未知の状態です。

一方、JavaScriptの難易度は、確かにあるポイントを超えると劇的に上がるという実感がありました。

有名なJavaScriptの謎仕様として、

0 == []; // true
0 == "0"; // true
"0" == []; // false

2 + "2"; // "22"
2 - "2"; // 0

が上げられます。手軽さの対価として、このような曖昧な仕様も沢山存在しているのが悩みところです。

Rust信者になろう

Rustに熱狂的なファンが多く、コミュニティは非常に熱量が高いことも有名な話です。

Rust信者末期の症状の1つとして、何でもRustで書き直す衝動が抑えられないらしいです。1例として、GithubでRust言語による書き換えが行われた既存ソフトウェアの代替一覧が公開されたりしています。

また、こちらの動画も是非見てください。

最後に

今回は、初心者の自分がRustを勉強する際の心得などを記事にしてみました。最後は、Rustを習得したら、どのような使い道があるかについて、調べた情報を簡単にまとめてみます。

  • CLIツール:RustはCLIツールの作成に非常に向いています。一番の要因は、Cargoによってツールの配布は非常に簡単にできるからです。apt、yum、brewなど別々でパッケージを用意しなくても、cargo installでほとんどのOSで使えます。

  • WebAssembly:この領域では、Rustは群を抜いて一番よく使われる言語になっています。

  • 組込み:あんまり詳しくないですが、ほとんどの組込みシステムに向けて開発できるらしいです。

  • Web系:高い性能を求められる場面では、Rust制のバックエンドの出番となります。又、近年ではフロントエンド側のツールもRustで書き直すのが流行りです。(deno、SWC、Turbopackなど)

  • ブロックチェーン:詳しくないですが、現在Rustエンジニアを募集しているところは、ブロックチェーン企業が多いらしいです。

  • OS:最近大きな話題となったのは、Linux、Windows、Androidなどのカーネルの一部はRustによる実験的な開発が始まったらしいです。又、Googleが最近、RISC-Vチップ向けのRust制OS、「KataOS」を発表しました。OS分野の活躍は今後十分に考えられます。

他にも機械学習、ゲーム開発など色んな分野でRustが活躍しているらしいので、もしRustに興味がありましたら、是非試してみてください。

おすすめの記事

条件に該当するページがございません